Skip to content

Commit d5bf6ff

Browse files
author
Ivan Dubrov
committed
🐥 baby steps towards stable Rust
Started working on #4 (support for stable Rust). First issue we need to solve is to get access to the harness (since we don't really want to implement it ourselves). There is https://crates.io/crates/libtest crate, which is recent version of Rust internal test harness, extracted as a crate. However, it only compiles on nightly, so it won't help us here. There is also https://crates.io/crates/rustc-test, but it is 2 years old. I haven't checked its features, but might not support some of the desired functionality (like, JSON output in tests? colored output?). So, the third option (which I'm using here) is to use `test` crate from the Rust itself and also set `RUSTC_BOOTSTRAP=1` for our crate so we can access it on stable channel. Not great, but works for now. Second issue is to get access to the tests. On nightly, we use `#[test_case]` to hijack Rust tests registration so we can get access to them in nightly. Cannot do that on stable. What would help here is something along the lines of https://internals.rust-lang.org/t/idea-global-static-variables-extendable-at-compile-time/9879 or https://internals.rust-lang.org/t/pre-rfc-add-language-support-for-global-constructor-functions. Don't have that, so we use https://crates.io/crates/ctor crate to build our own registry of tests, similar to https://crates.io/crates/inventory. The caveat here is potentially hitting dtolnay/inventory#7 issue which would manifest itself as test being silently ignored. Not great, but let's see how bad it will be. Third piece of the puzzle is to intercept execution of tests. This is done by asking users to use `harness = false` in their `Cargo.toml`, in which case we take full control of test execution. Finally, the last challenge is that with `harness = false`, we don't have a good way to intercept "standard" tests (`#[test]`): https://users.rust-lang.org/t/capturing-test-when-harness-false-in-cargo-toml/28115 So, the plan here is to provide `#[datatest::test]` attribute that will behave similar to built-in `#[test]` attribute, but will use our own registry for tests. No need to support `#[bench]` as it is not supported on stable channel anyway. The caveat in this case is that if you use built-in `#[test]`, your test will be silently ignored. Not great, not sure what to do about it. Proper solution, of course, would be driving RFC for custom test frameworks: rust-lang/rust#50297 😅
1 parent 7656031 commit d5bf6ff

File tree

10 files changed

+420
-266
lines changed

10 files changed

+420
-266
lines changed

Cargo.toml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "datatest"
3-
version = "0.3.5"
3+
version = "0.4.0"
44
authors = ["Ivan Dubrov <[email protected]>"]
55
edition = "2018"
66
repository = "https://github.com/commure/datatest"
@@ -10,13 +10,21 @@ description = """
1010
Data-driven tests in Rust
1111
"""
1212

13+
[[test]]
14+
name = "datatest_stable"
15+
harness = false
16+
17+
[build-dependencies]
18+
version_check = "0.9.1"
19+
1320
[dependencies]
14-
datatest-derive = { path = "datatest-derive", version = "=0.3.5" }
21+
datatest-derive = { path = "datatest-derive", version = "=0.4.0" }
1522
regex = "1.0.0"
1623
walkdir = "2.1.4"
1724
serde = "1.0.84"
1825
serde_yaml = "0.8.7"
1926
yaml-rust = "0.4.2"
27+
ctor = "0.1.10"
2028

2129
[dev-dependencies]
2230
serde = { version = "1.0.84", features = ["derive"] }

build.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use version_check::Channel;
2+
3+
fn main() {
4+
let is_nightly = Channel::read().map_or(false, |ch| ch.is_nightly());
5+
if is_nightly {
6+
println!("cargo:rustc-cfg=feature=\"nightly\"");
7+
} else {
8+
println!("cargo:rustc-cfg=feature=\"stable\"");
9+
}
10+
println!("cargo:rustc-env=RUSTC_BOOTSTRAP=1");
11+
}

datatest-derive/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "datatest-derive"
3-
version = "0.3.5"
3+
version = "0.4.0"
44
authors = ["Ivan Dubrov <[email protected]>"]
55
edition = "2018"
66
repository = "https://github.com/commure/datatest"

datatest-derive/src/lib.rs

Lines changed: 90 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,14 @@
22
#![deny(unused_must_use)]
33
extern crate proc_macro;
44

5-
#[macro_use]
6-
extern crate syn;
7-
#[macro_use]
8-
extern crate quote;
9-
extern crate proc_macro2;
10-
115
use proc_macro2::{Span, TokenStream};
6+
use quote::quote;
127
use std::collections::HashMap;
138
use syn::parse::{Parse, ParseStream, Result as ParseResult};
149
use syn::punctuated::Punctuated;
1510
use syn::spanned::Spanned;
1611
use syn::token::Comma;
17-
use syn::{ArgCaptured, FnArg, Ident, ItemFn, Pat};
12+
use syn::{braced, parse_macro_input, ArgCaptured, FnArg, Ident, ItemFn, Pat};
1813

1914
type Error = syn::parse::Error;
2015

@@ -90,6 +85,29 @@ impl Parse for FilesTestArgs {
9085
}
9186
}
9287

88+
enum Channel {
89+
Stable,
90+
Nightly,
91+
}
92+
93+
/// Wrapper that turns on behavior that works on stable Rust.
94+
#[proc_macro_attribute]
95+
pub fn files_stable(
96+
args: proc_macro::TokenStream,
97+
func: proc_macro::TokenStream,
98+
) -> proc_macro::TokenStream {
99+
files_internal(args, func, Channel::Stable)
100+
}
101+
102+
/// Wrapper that turns on behavior that works only on nightly Rust.
103+
#[proc_macro_attribute]
104+
pub fn files_nightly(
105+
args: proc_macro::TokenStream,
106+
func: proc_macro::TokenStream,
107+
) -> proc_macro::TokenStream {
108+
files_internal(args, func, Channel::Nightly)
109+
}
110+
93111
/// Proc macro handling `#[files(...)]` syntax. This attribute defines rules for deriving
94112
/// test function arguments from file paths. There are two types of rules:
95113
/// 1. Pattern rule, `<arg_name> in "<regexp>"`
@@ -131,11 +149,10 @@ impl Parse for FilesTestArgs {
131149
/// I could have made this proc macro to handle these cases explicitly and generate a different
132150
/// code, but I decided to not add a complexity of type analysis to the proc macro and use traits
133151
/// instead. See `datatest::TakeArg` and `datatest::DeriveArg` to see how this mechanism works.
134-
#[proc_macro_attribute]
135-
#[allow(clippy::needless_pass_by_value)]
136-
pub fn files(
152+
fn files_internal(
137153
args: proc_macro::TokenStream,
138154
func: proc_macro::TokenStream,
155+
channel: Channel,
139156
) -> proc_macro::TokenStream {
140157
let mut func_item = parse_macro_input!(func as ItemFn);
141158
let args: FilesTestArgs = parse_macro_input!(args as FilesTestArgs);
@@ -195,7 +212,7 @@ pub fn files(
195212

196213
params.push(arg.value.value());
197214
invoke_args.push(quote! {
198-
::datatest::TakeArg::take(&mut <#ty as ::datatest::DeriveArg>::derive(&paths_arg[#idx]))
215+
::datatest::__internal::TakeArg::take(&mut <#ty as ::datatest::__internal::DeriveArg>::derive(&paths_arg[#idx]))
199216
})
200217
} else {
201218
return Error::new(pat_ident.span(), "mapping is not defined for the argument")
@@ -231,31 +248,34 @@ pub fn files(
231248
let orig_func_name = &func_item.ident;
232249

233250
let (kind, bencher_param) = if info.bench {
234-
(quote!(BenchFn), quote!(bencher: &mut ::datatest::Bencher,))
251+
(
252+
quote!(BenchFn),
253+
quote!(bencher: &mut ::datatest::__internal::Bencher,),
254+
)
235255
} else {
236256
(quote!(TestFn), quote!())
237257
};
238258

239-
// Adding `#[allow(unused_attributes)]` to `#orig_func` to allow `#[ignore]` attribute
259+
let registration = test_registration(channel, &desc_ident);
240260
let output = quote! {
241-
#[test_case]
261+
#registration
242262
#[automatically_derived]
243263
#[allow(non_upper_case_globals)]
244-
static #desc_ident: ::datatest::FilesTestDesc = ::datatest::FilesTestDesc {
264+
static #desc_ident: ::datatest::__internal::FilesTestDesc = ::datatest::__internal::FilesTestDesc {
245265
name: concat!(module_path!(), "::", #func_name_str),
246266
ignore: #ignore,
247267
root: #root,
248268
params: &[#(#params),*],
249269
pattern: #pattern_idx,
250270
ignorefn: #ignore_func_ref,
251-
testfn: ::datatest::FilesTestFn::#kind(#trampoline_func_ident),
271+
testfn: ::datatest::__internal::FilesTestFn::#kind(#trampoline_func_ident),
252272
};
253273

254274
#[automatically_derived]
255275
#[allow(non_snake_case)]
256276
fn #trampoline_func_ident(#bencher_param paths_arg: &[::std::path::PathBuf]) {
257277
let result = #orig_func_name(#(#invoke_args),*);
258-
datatest::assert_test_result(result);
278+
::datatest::__internal::assert_test_result(result);
259279
}
260280

261281
#func_item
@@ -323,11 +343,28 @@ impl Parse for DataTestArgs {
323343
}
324344
}
325345

346+
/// Wrapper that turns on behavior that works on stable Rust.
326347
#[proc_macro_attribute]
327-
#[allow(clippy::needless_pass_by_value)]
328-
pub fn data(
348+
pub fn data_stable(
329349
args: proc_macro::TokenStream,
330350
func: proc_macro::TokenStream,
351+
) -> proc_macro::TokenStream {
352+
data_internal(args, func, Channel::Stable)
353+
}
354+
355+
/// Wrapper that turns on behavior that works only on nightly Rust.
356+
#[proc_macro_attribute]
357+
pub fn data_nightly(
358+
args: proc_macro::TokenStream,
359+
func: proc_macro::TokenStream,
360+
) -> proc_macro::TokenStream {
361+
data_internal(args, func, Channel::Nightly)
362+
}
363+
364+
fn data_internal(
365+
args: proc_macro::TokenStream,
366+
func: proc_macro::TokenStream,
367+
channel: Channel,
331368
) -> proc_macro::TokenStream {
332369
let mut func_item = parse_macro_input!(func as ItemFn);
333370
let cases: DataTestArgs = parse_macro_input!(args as DataTestArgs);
@@ -376,23 +413,24 @@ pub fn data(
376413

377414
let (case_ctor, bencher_param, bencher_arg) = if info.bench {
378415
(
379-
quote!(::datatest::DataTestFn::BenchFn(Box::new(::datatest::DataBenchFn(#trampoline_func_ident, case)))),
380-
quote!(bencher: &mut ::datatest::Bencher,),
416+
quote!(::datatest::__internal::DataTestFn::BenchFn(Box::new(::datatest::__internal::DataBenchFn(#trampoline_func_ident, case)))),
417+
quote!(bencher: &mut ::datatest::__internal::Bencher,),
381418
quote!(bencher,),
382419
)
383420
} else {
384421
(
385-
quote!(::datatest::DataTestFn::TestFn(Box::new(move || #trampoline_func_ident(case)))),
422+
quote!(::datatest::__internal::DataTestFn::TestFn(Box::new(move || #trampoline_func_ident(case)))),
386423
quote!(),
387424
quote!(),
388425
)
389426
};
390427

428+
let registration = test_registration(channel, &desc_ident);
391429
let output = quote! {
392-
#[test_case]
430+
#registration
393431
#[automatically_derived]
394432
#[allow(non_upper_case_globals)]
395-
static #desc_ident: ::datatest::DataTestDesc = ::datatest::DataTestDesc {
433+
static #desc_ident: ::datatest::__internal::DataTestDesc = ::datatest::__internal::DataTestDesc {
396434
name: concat!(module_path!(), "::", #func_name_str),
397435
ignore: #ignore,
398436
describefn: #describe_func_ident,
@@ -402,12 +440,12 @@ pub fn data(
402440
#[allow(non_snake_case)]
403441
fn #trampoline_func_ident(#bencher_param arg: #ty) {
404442
let result = #orig_func_ident(#bencher_arg #ref_token arg);
405-
datatest::assert_test_result(result);
443+
::datatest::__internal::assert_test_result(result);
406444
}
407445

408446
#[automatically_derived]
409447
#[allow(non_snake_case)]
410-
fn #describe_func_ident() -> Vec<::datatest::DataTestCaseDesc<::datatest::DataTestFn>> {
448+
fn #describe_func_ident() -> Vec<::datatest::DataTestCaseDesc<::datatest::__internal::DataTestFn>> {
411449
let result = #cases
412450
.into_iter()
413451
.map(|input| {
@@ -427,3 +465,29 @@ pub fn data(
427465
};
428466
output.into()
429467
}
468+
469+
fn test_registration(channel: Channel, desc_ident: &syn::Ident) -> TokenStream {
470+
match channel {
471+
// On nightly, we rely on `custom_test_frameworks` feature
472+
Channel::Nightly => quote!(#[test_case]),
473+
// On stable, we use `ctor` crate to build a registry of all our tests
474+
Channel::Stable => {
475+
let registration_fn =
476+
syn::Ident::new(&format!("{}__REGISTRATION", desc_ident), desc_ident.span());
477+
let tokens = quote! {
478+
#[allow(non_snake_case)]
479+
#[datatest::__internal::ctor]
480+
fn #registration_fn() {
481+
use ::datatest::__internal::RegistrationNode;
482+
static mut REGISTRATION: RegistrationNode = RegistrationNode {
483+
descriptor: &#desc_ident,
484+
next: None,
485+
};
486+
// This runs only once during initialization, so should be safe
487+
::datatest::__internal::register(unsafe { &mut REGISTRATION });
488+
}
489+
};
490+
tokens
491+
}
492+
}
493+
}

src/lib.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,17 +115,28 @@ mod data;
115115
mod files;
116116
mod runner;
117117

118+
/// Internal re-exports for the procedural macro to use.
118119
#[doc(hidden)]
119-
pub use crate::data::{DataBenchFn, DataTestDesc, DataTestFn};
120-
#[doc(hidden)]
121-
pub use crate::files::{DeriveArg, FilesTestDesc, FilesTestFn, TakeArg};
122-
#[doc(hidden)]
123-
pub use crate::runner::{assert_test_result, runner};
120+
pub mod __internal {
121+
pub use crate::data::{DataBenchFn, DataTestDesc, DataTestFn};
122+
pub use crate::files::{DeriveArg, FilesTestDesc, FilesTestFn, TakeArg};
123+
pub use crate::runner::assert_test_result;
124+
pub use crate::test::Bencher;
125+
pub use ctor::ctor;
126+
127+
// To maintain registry on stable channel
128+
pub use crate::runner::{register, RegistrationNode};
129+
}
130+
131+
pub use crate::runner::runner;
132+
124133
#[doc(hidden)]
125-
pub use crate::test::Bencher;
134+
#[cfg(feature = "stable")]
135+
pub use datatest_derive::{data_stable as data, files_stable as files};
126136

127137
#[doc(hidden)]
128-
pub use datatest_derive::{data, files};
138+
#[cfg(feature = "nightly")]
139+
pub use datatest_derive::{data_nightly as data, files_nightly as files};
129140

130141
/// Experimental functionality.
131142
#[doc(hidden)]

0 commit comments

Comments
 (0)