Skip to content

Commit c13c5a0

Browse files
djrenrenmark-i-m
authored andcommitted
Add testing chapter
1 parent 0298ced commit c13c5a0

File tree

2 files changed

+146
-0
lines changed

2 files changed

+146
-0
lines changed

src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- [Incremental compilation](./incremental-compilation.md)
1818
- [Debugging and Testing](./incrcomp-debugging.md)
1919
- [The parser](./the-parser.md)
20+
- [Testing](./testing.md)
2021
- [Macro expansion](./macro-expansion.md)
2122
- [Name resolution](./name-resolution.md)
2223
- [The HIR (High-level IR)](./hir.md)

src/testing.md

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
### The `#[test]` attribute
2+
Today, rust programmers rely on a built in attribute called `#[test]`.
3+
All you have to do is mark a function as a test and include some asserts like so:
4+
5+
```rust,ignore
6+
#[test]
7+
fn my_test() {
8+
assert!(2+2 == 4);
9+
}
10+
```
11+
12+
When this program is compiled using `rustc --test` or `cargo test`, it will
13+
produce an executable that can run this, and any other test function. This
14+
method of testing allows tests to live alongside code in an organic way. You
15+
can even put tests inside private modules:
16+
17+
```rust,ignore
18+
mod my_priv_mod {
19+
fn my_priv_func() -> bool {}
20+
21+
#[test]
22+
fn test_priv_func() {
23+
assert!(my_priv_func());
24+
}
25+
}
26+
```
27+
Private items can thus be easily tested without worrying about how to expose
28+
the them to any sort of external testing apparatus. This is key to the
29+
ergonomics of testing in Rust. Semantically, however, it's rather odd.
30+
How does any sort of `main` function invoke these tests if they're not visible?
31+
What exactly is `rustc --test` doing?
32+
33+
`#[test]` is implemented as a syntactic transformation inside the compiler's
34+
[`libsyntax` crate][libsyntax]. Essentially, it's a fancy macro, that
35+
rewrites the crate in 3 steps:
36+
37+
#### Step 1: Re-Exporting
38+
39+
As mentioned earlier, tests can exist inside private modules, so we need a way of
40+
exposing them to the main function, without breaking any existing code. To that end,
41+
`libsyntax` will create local modules called `__test_reexports` that recursively reexport tests.
42+
This expansion translates the above example into:
43+
44+
```rust,ignore
45+
mod my_priv_mod {
46+
fn my_priv_func() -> bool {}
47+
48+
pub fn test_priv_func() {
49+
assert!(my_priv_func());
50+
}
51+
52+
pub mod __test_reexports {
53+
pub use super::test_priv_func;
54+
}
55+
}
56+
```
57+
58+
Now, our test can be accessed as
59+
`my_priv_mod::__test_reexports::test_priv_func`. For deeper module
60+
structures, `__test_reexports` will reexport modules that contain tests, so a
61+
test at `a::b::my_test` becomes
62+
`a::__test_reexports::b::__test_reexports::my_test`. While this process seems
63+
pretty safe, what happens if there is an existing `__test_reexports` module?
64+
The answer: nothing.
65+
66+
To explain, we need to understand [how the AST represents
67+
identifiers][Ident]. The name of every function, variable, module, etc. is
68+
not stored as a string, but rather as an opaque [Symbol][Symbol] which is
69+
essentially an ID number for each identifier. The compiler keeps a separate
70+
hashtable that allows us to recover the human-readable name of a Symbol when
71+
necessary (such as when printing a syntax error). When the compiler generates
72+
the `__test_reexports` module, it generates a new Symbol for the identifier,
73+
so while the compiler-generated `__test_reexports` may share a name with your
74+
hand-written one, it will not share a Symbol. This technique prevents name
75+
collision during code generation and is the foundation of Rust's macro
76+
hygiene.
77+
78+
#### Step 2: Harness Generation
79+
Now that our tests are accessible from the root of our crate, we need to do something with them.
80+
`libsyntax` generates a module like so:
81+
82+
```rust,ignore
83+
pub mod __test {
84+
extern crate test;
85+
const TESTS: &'static [self::test::TestDescAndFn] = &[/*...*/];
86+
87+
#[main]
88+
pub fn main() {
89+
self::test::test_static_main(TESTS);
90+
}
91+
}
92+
```
93+
94+
While this transformation is simple, it gives us a lot of insight into how tests are actually run.
95+
The tests are aggregated into an array and passed to a test runner called `test_static_main`.
96+
We'll come back to exactly what `TestDescAndFn` is, but for now, the key takeaway is that there is a crate
97+
called [`test`][test] that is part of Rust core, that implements all of the runtime for testing. `test`'s interface is unstable,
98+
so the only stable way to interact with it is through the `#[test]` macro.
99+
100+
#### Step 3: Test Object Generation
101+
If you've written tests in Rust before, you may be familiar with some of the optional attributes available on test functions.
102+
For example, a test can be annotated with `#[should_panic]` if we expect the test to cause a panic. It looks something like this:
103+
104+
```rust,ignore
105+
#[test]
106+
#[should_panic]
107+
fn foo() {
108+
panic!("intentional");
109+
}
110+
```
111+
112+
This means our tests are more than just simple functions, they have configuration information as well. `test` encodes this configuration
113+
data into a struct called [`TestDesc`][TestDesc]. For each test function in a crate, `libsyntax` will parse its attributes and generate a `TestDesc` instance.
114+
It then combines the `TestDesc` and test function into the predictably named `TestDescAndFn` struct, that `test_static_main` operates on.
115+
For a given test, the generated `TestDescAndFn` instance looks like so:
116+
117+
```rust,ignore
118+
self::test::TestDescAndFn{
119+
desc: self::test::TestDesc{
120+
name: self::test::StaticTestName("foo"),
121+
ignore: false,
122+
should_panic: self::test::ShouldPanic::Yes,
123+
allow_fail: false,
124+
},
125+
testfn: self::test::StaticTestFn(||
126+
self::test::assert_test_result(::crate::__test_reexports::foo())),
127+
}
128+
```
129+
130+
Once we've constructed an array of these test objects, they're passed to the
131+
test runner via the harness generated in step 2.
132+
133+
### Inspecting the generated code
134+
On nightly rust, there's an unstable flag called `unpretty` that you can use to print out the module source after macro expansion:
135+
136+
```bash
137+
$ rustc my_mod.rs -Z unpretty=hir
138+
```
139+
140+
[test]: https://doc.rust-lang.org/test/index.html
141+
[TestDesc]: https://doc.rust-lang.org/test/struct.TestDesc.html
142+
[Symbol]: https://doc.rust-lang.org/nightly/nightly-rustc/syntax/ast/struct.Ident.html
143+
[Ident]: https://doc.rust-lang.org/nightly/nightly-rustc/syntax/ast/struct.Ident.html
144+
[eRFC]: https://github.com/rust-lang/rfcs/blob/master/text/2318-custom-test-frameworks.md
145+
[libsyntax]: https://github.com/rust-lang/rust/tree/master/src/libsyntax

0 commit comments

Comments
 (0)