I think I like the general gist of the proposals, but I have some qualms. I don’t want to add a whole new category of ‘thing’ here, in particular any kind of special case proc macro seems like a really bad idea - not least because proc macros are very unstable.
I think that it also puts the test framework at kind of the wrong level of integration - it feels like it is extra-linguistic, when it could be more part of the language.
I haven’t had time to work through any ideas seriously, so I apologise for the half-formed suggestions, hopefully we can integrate into the existing proposals.
I imagine that a test framework should be a crate, and that a client crate which wants to use it would have it as a dev-dependency in Cargo and a #[cfg(test)] extern crate
in the Rust code. That test crate can contain macros, including attribute macros which the client can use. Thus any program transformation is done entirely like usual macro expansion.
We extend the test
attribute to allow is to be used more flexibly, e.g., #[test::foo]
, #[test(...)]
, etc.). The expectation is that the test framework macros output items marked with test
attributes (I realise we want to apply to things which are not strictly tests, but they are free to use their own attributes and expand to test
, test
doesn’t have to be user-facing).
The test runner itself is indicated by a trait, the trait is a lang item in core or std or wherever. Any type which implements that trait is treated as a test runner by the compiler. There might be multiple test runners for any crate. When the compiler is building in test mode then any test runners in any dependent crates are given (to a run_tests
method) a data structure representing all items marked with any test
attribute in the crate being tested. Strawman for that: Vec where
struct TestFixture<T, U> {
tokens: TokenStream,
span: Span,
attributes: TokenStream,
fn_ptr: Option<Fn(T) -> U>,
}
run_tests
also takes some context object which it can use for emitting test events. I think that run_tests
is the only method a test runner trait would need.
If TestFixture is a function and has the signature T -> U
then the function pointer is in the fn_ptr
field. That gives the test runner a way to run test functions. T
and U
are associated types in the TestRunner trait.
I believe that this approach doesn’t need any Cargo integration, since running the compiler in the same way as cargo test
today would run any test frameworks which are depended upon. However, we probably want to add some convenience arguments for running only one test runner, etc.
Appendix
Test runner trait
trait TestRunner {
type TestArg;
type TestResult;
fn run_tests(tests: &[TestFixture], context: &mut TestContext);
}