|
| 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