Skip to content

Commit c25cfbf

Browse files
authored
Merge pull request #2318 from Manishearth/post-build-contexts
eRFC: Custom test frameworks
2 parents 4557562 + a7e3169 commit c25cfbf

File tree

1 file changed

+389
-0
lines changed

1 file changed

+389
-0
lines changed

text/2318-custom-test-frameworks.md

Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
- Feature Name: `custom_test_frameworks`
2+
- Start Date: 2018-01-25
3+
- RFC PR: [rust-lang/rfcs#2318](https://github.com/rust-lang/rfcs/pull/2318)
4+
- Rust Issue: [rust-lang/rust#50297](https://github.com/rust-lang/rust/issues/50297)
5+
6+
# Summary
7+
[summary]: #summary
8+
9+
This is an *experimental RFC* for adding the ability to integrate custom test/bench/etc frameworks ("test frameworks") in Rust.
10+
11+
# Motivation
12+
[motivation]: #motivation
13+
14+
Currently, Rust lets you write unit tests with a `#[test]` attribute. We also have an unstable `#[bench]` attribute which lets one write benchmarks.
15+
16+
In general it's not easy to use your own testing strategy. Implementing something that can work
17+
within a `#[test]` attribute is fine (`quickcheck` does this with a macro), but changing the overall
18+
strategy is hard. For example, `quickcheck` would work even better if it could be done as:
19+
20+
```rust
21+
#[quickcheck]
22+
fn test(input1: u8, input2: &str) {
23+
// ...
24+
}
25+
```
26+
27+
If you're trying to do something other than testing, you're out of luck -- only tests, benches, and examples
28+
get the integration from `cargo` for building auxiliary binaries the correct way. [cargo-fuzz] has to
29+
work around this by creating a special fuzzing crate that's hooked up the right way, and operating inside
30+
of that. Ideally, one would be able to just write fuzz targets under `fuzz/`.
31+
32+
[Compiletest] (rustc's test framework) would be another kind of thing that would be nice to
33+
implement this way. Currently it compiles the test cases by manually running `rustc`, but it has the
34+
same problem as cargo-fuzz where getting these flags right is hard. This too could be implemented as
35+
a custom test framework.
36+
37+
A profiling framework may want to use this mode to instrument the binary in a certain way. We
38+
can already do this via proc macros, but having it hook through `cargo test` would be neat.
39+
40+
Overall, it would be good to have a generic framework for post-build steps that can support use
41+
cases like `#[test]` (both the built-in one and quickcheck), `#[bench]` (both built in and custom
42+
ones like [criterion]), `examples`, and things like fuzzing. While we may not necessarily rewrite
43+
the built in test/bench/example infra in terms of the new framework, it should be possible to do so.
44+
45+
The main two features proposed are:
46+
47+
- An API for crates that generate custom binaries, including
48+
introspection into the target crate.
49+
- A mechanism for `cargo` integration so that custom test frameworks
50+
are at the same level of integration as `test` or `bench` as
51+
far as build processes are concerned.
52+
53+
[cargo-fuzz]: https://github.com/rust-fuzz/cargo-fuzz
54+
[criterion]: https://github.com/japaric/criterion.rs
55+
[Compiletest]: http://github.com/laumann/compiletest-rs
56+
57+
# Detailed proposal
58+
[detailed-proposal]: #detailed-proposal
59+
60+
(As an eRFC I'm merging the "guide-level/reference-level" split for now; when we have more concrete
61+
ideas we can figure out how to frame it and then the split will make more sense)
62+
63+
The basic idea is that crates can define test frameworks, which specify
64+
how to transform collected test functions and construct a `main()` function,
65+
and then crates using these can declare them in their Cargo.toml, which will let
66+
crate developers invoke various test-like steps using the framework.
67+
68+
69+
## Procedural macro for a new test framework
70+
71+
A test framework is like a procedural macro that is evaluated after all other macros in the target
72+
crate have been evaluated. The exact mechanism is left up to the experimentation phase, however we
73+
have some proposals at the end of this RFC.
74+
75+
76+
A crate may only define a single framework.
77+
78+
## Cargo integration
79+
80+
Alternative frameworks need to integrate with cargo.
81+
In particular, when crate `a` uses a crate `b` which provides an
82+
framework, `a` needs to be able to specify when `b`'s framework
83+
should be used. Furthermore, cargo needs to understand that when
84+
`b`'s framework is used, `b`'s dependencies must also be linked.
85+
86+
Crates which define a test framework must have a `[testing.framework]`
87+
key in their `Cargo.toml`. They cannot be used as regular dependencies.
88+
This section works like this:
89+
90+
```rust
91+
[testing.framework]
92+
kind = "test" # or bench
93+
```
94+
95+
`lib` specifies if the `--lib` mode exists for this framework by default,
96+
and `folders` specifies which folders the framework applies to. Both can be overridden
97+
by consumers.
98+
99+
`single-target` indicates that only a single target can be run with this
100+
framework at once (some tools, like cargo-fuzz, run forever, and so it
101+
does not make sense to specify multiple targets).
102+
103+
Crates that wish to *use* a custom test framework, do so by including a framework
104+
under a new `[[testing.frameworks]]` section in their
105+
`Cargo.toml`:
106+
107+
```toml
108+
[[testing.frameworks]]
109+
provider = { quickcheck = "1.0" }
110+
```
111+
112+
This pulls in the framework from the "quickcheck" crate. By default, the following
113+
framework is defined:
114+
115+
```toml
116+
[[testing.frameworks]]
117+
provider = { test = "1.0" }
118+
```
119+
120+
(We may define a default framework for bench in the future)
121+
122+
Declaring a test framework will replace the existing default one. You cannot declare
123+
more than one test or bench framework.
124+
125+
To invoke a particular framework, a user invokes `cargo test` or `cargo bench`. Any additional
126+
arguments are passed to the testing binary. By convention, the first position argument should allow
127+
filtering which targets (tests/benchmarks/etc.) are run.
128+
129+
## To be designed
130+
131+
This contains things which we should attempt to solve in the course of this experiment, for which this eRFC
132+
does not currently provide a concrete proposal.
133+
134+
## Procedural macro design
135+
136+
137+
We have a bunch of concrete proposals here, but haven't yet chosen one.
138+
139+
### main() function generation with test collector
140+
141+
One possible design is to have a proc macro that simply generates `main()`
142+
143+
It is passed the `TokenStream` for every element in the
144+
target crate that has a set of attributes the test framework has
145+
registered interest in. For example, to declare a test framework
146+
called `mytest`:
147+
148+
```rust
149+
extern crate proc_macro;
150+
use proc_macro::{TestFrameworkContext, TokenStream};
151+
152+
// attributes() is optional
153+
#[test_framework]
154+
pub fn test(context: &TestFrameworkContext) -> TokenStream {
155+
// ...
156+
}
157+
```
158+
159+
where
160+
161+
```rust
162+
struct TestFrameworkContext<'a> {
163+
items: &'a [AnnotatedItem],
164+
// ... (may be added in the future)
165+
}
166+
167+
struct AnnotatedItem
168+
tokens: TokenStream,
169+
span: Span,
170+
attributes: TokenStream,
171+
path: SomeTypeThatRepresentsPathToItem
172+
}
173+
```
174+
175+
`items` here contains an `AnnotatedItem` for every item in the
176+
target crate that has one of the attributes declared in `attributes`
177+
along with attributes sharing the name of the framework (`test`, here --
178+
the function must be named either `test` or `bench`).
179+
180+
The annotated function _must_ be named "test" for a test framework and
181+
"bench" for a bench framework. We currently do not support
182+
any other kind of framework, but we may in the future.
183+
184+
So an example transformation would be to take something like this:
185+
186+
```rust
187+
#[test]
188+
fn foo(x: u8) {
189+
// ...
190+
}
191+
192+
mod bar {
193+
#[test]
194+
fn bar(x: String, y: u8) {
195+
// ...
196+
}
197+
}
198+
```
199+
200+
and output a `main()` that does something like:
201+
202+
```rust
203+
fn main() {
204+
// handles showing failures, etc
205+
let mut runner = quickcheck::Runner();
206+
207+
runner.iter("foo", |random_source| foo(random_source.next().into()));
208+
runner.iter("bar::bar", |random_source| bar::bar(random_source.next().into(),
209+
random_source.next().into()));
210+
runner.finish();
211+
}
212+
```
213+
214+
The compiler will make marked items `pub(crate)` (i.e. by making
215+
all their parent modules public). `#[test]` and `#[bench]` items will only exist
216+
with `--cfg test` (or bench), which is automatically set when running tests.
217+
218+
219+
### Whole-crate procedural macro
220+
221+
An alternative proposal was to expose an extremely general whole-crate proc macro:
222+
223+
```rust
224+
#[test_framework(attributes(foo, bar))]
225+
pub fn mytest(crate: TokenStream) -> TokenStream {
226+
// ...
227+
}
228+
```
229+
230+
and then we can maintain a helper crate, out of tree, that uses `syn` to provide a nicer
231+
API, perhaps something like:
232+
233+
```rust
234+
fn clean_entry_point(tree: syn::ItemMod) -> syn::ItemMod;
235+
236+
trait TestCollector {
237+
fn fold_function(&mut self, path: syn::Path, func: syn::ItemFn) -> syn::ItemFn;
238+
}
239+
240+
fn collect_tests<T: TestCollector>(collector: &mut T, tree: syn::ItemMod) -> ItemMod;
241+
```
242+
243+
This lets us continue to develop things outside of tree without perma-stabilizing an API;
244+
and it also lets us provide a friendlier API via the helper crate.
245+
246+
It also lets crates like `cargo-fuzz` introduce things like a `#![no_main]` attribute or do
247+
other antics.
248+
249+
Finally, it handles the "profiling framework" case as mentioned in the motivation. On the other hand,
250+
these tools usually operate at a different layer of abstraction so it might not be necessary.
251+
252+
A major drawback of this proposal is that it is very general, and perhaps too powerful. We're currently using the
253+
more focused API in the eRFC, and may switch to this during experimentation if a pressing need crops up.
254+
255+
### Alternative procedural macro with minimal compiler changes
256+
257+
The above proposal can be made even more general, minimizing the impact on the compiler.
258+
259+
This assumes that `#![foo]` ("inner attribute") macros work on modules and on crates.
260+
261+
The idea is that the compiler defines no new proc macro surface, and instead simply exposes
262+
a `--attribute` flag. This flag, like `-Zextra-plugins`, lets you attach a proc macro attribute
263+
to the whole crate before compiling. (This flag actually generalizes a bunch of flags that the
264+
compiler already has)
265+
266+
Test crates are now simply proc macro attributes:
267+
268+
```rust
269+
#[test_framework(attributes(test, foo, bar))]
270+
pub fn harness(crate: TokenStream) -> TokenStream {
271+
// ...
272+
}
273+
```
274+
275+
The cargo functionality will basically compile the file with the right dependencies
276+
and `--attribute=your_crate::harness`.
277+
278+
279+
### Standardizing the output
280+
281+
We should probably provide a crate with useful output formatters and stuff so that if test harnesses desire, they can
282+
use the same output formatting as a regular test. This also provides a centralized location to standardize things
283+
like json output and whatnot.
284+
285+
@killercup is working on a proposal for this which I will try to work in.
286+
287+
# Drawbacks
288+
[drawbacks]: #drawbacks
289+
290+
- This adds more sections to `Cargo.toml`.
291+
- This complicates the execution path for cargo, in that it now needs
292+
to know about testing frameworks.
293+
- Flags and command-line parameters for test and bench will now vary
294+
between testing frameworks, which may confuse users as they move
295+
between crates.
296+
297+
# Rationale and alternatives
298+
[alternatives]: #alternatives
299+
300+
We could stabilize `#[bench]` and extend libtest with setup/teardown and
301+
other requested features. This would complicate the in-tree libtest,
302+
introduce a barrier for community contributions, and discourage other
303+
forms of testing or benchmarking.
304+
305+
# Unresolved questions
306+
[unresolved]: #unresolved-questions
307+
308+
These are mostly intended to be resolved during the experimental
309+
feature. Many of these have strawman proposals -- unlike the rest of this RFC,
310+
these proposals have not been discussed as thoroughly. If folks feel like
311+
there's consensus on some of these we can move them into the main RFC.
312+
313+
## Integration with doctests
314+
315+
Documentation tests are somewhat special, in that they cannot easily be
316+
expressed as `TokenStream` manipulations. In the first instance, the
317+
right thing to do is probably to have an implicitly defined framework
318+
called `doctest` which is included in the testing set
319+
`test` by default (as proposed above).
320+
321+
Another argument for punting on doctests is that they are intended to
322+
demonstrate code that the user of a library would write. They're there
323+
to document *how* something should be used, and it then makes somewhat
324+
less sense to have different "ways" of running them.
325+
326+
## Standardizing the output
327+
328+
We should probably provide a crate with useful output formatters and
329+
stuff so that if test harnesses desire, they can use the same output
330+
formatting as a regular test. This also provides a centralized location
331+
to standardize things like json output and whatnot.
332+
333+
## Namespacing
334+
335+
Currently, two frameworks can both declare interest in the same
336+
attributes. How do we deal with collisions (e.g., most test crates will
337+
want the attribute `#[test]`). Do we namespace the attributes by the
338+
framework name (e.g., `#[mytest::test]`)? Do we require them to be behind
339+
`#[cfg(mytest)]`?
340+
341+
## Runtime dependencies and flags
342+
343+
The code generated by the framework may itself have dependencies.
344+
Currently there's no way for the framework to specify this. One
345+
proposal is for the crate to specify _runtime_ dependencies of the
346+
framework via:
347+
348+
```toml
349+
[testing.framework.dependencies]
350+
libfuzzer-sys = ...
351+
```
352+
353+
If a crate is currently running this framework, its
354+
dev-dependencies will be semver-merged with the frameworks's
355+
`framework.dependencies`. However, this may not be strictly necessary.
356+
Custom derives have a similar problem and they solve it by just asking
357+
users to import the correct crate.
358+
359+
## Naming
360+
361+
The general syntax and toml stuff should be approximately settled on before this eRFC merges, but
362+
iterated on later. Naming the feature is hard, some candidates are:
363+
364+
- testing framework
365+
- post-build context
366+
- build context
367+
- execution context
368+
369+
None of these are particularly great, ideas would be nice.
370+
371+
## Bencher
372+
373+
Should we be shipping a bencher by default at all (i.e., in libtest)? Could we instead default
374+
`cargo bench` to a `rust-lang-nursery` `bench` crate?
375+
376+
If this RFC lands and [RFC 2287] is rejected, we should probably try to stabilize
377+
`test::black_box` in some form (maybe `mem::black_box` and `mem::clobber` as detailed
378+
in [this amendment]).
379+
380+
## Cargo integration
381+
382+
A previous iteration of this RFC allowed for test frameworks to declare new attributes
383+
and folders, so you would have `cargo test --kind quickcheck` look for tests in the
384+
`quickcheck/` folder that were annotated with `#[quickcheck]`.
385+
386+
This is no longer the case, but we may wish to add this again.
387+
388+
[RFC 2287]: https://github.com/rust-lang/rfcs/pull/2287
389+
[this amendment]: https://github.com/Manishearth/rfcs/pull/1

0 commit comments

Comments
 (0)