Skip to content

Commit aa371eb

Browse files
committed
Auto merge of #10596 - blyxyas:book-write_tests, r=flip1995
Clippy Book Chapter Updates Reborn: Writing tests This PR adds a new chapter to the book: "Writing tests". The changes have been mainly done from reviews from #9426 and some minor re-writes. ## Notes - We still need to check that the `git status`es are correct, as `cargo dev new_lint` changed a lot since 2022. - Requires #10598: Link to "Emitting Lints" where I flagged with `FIXME:`. - To talk about the whole project, please use the tracking issue for the project #10597 (It also contains a timeline, discussions and more information) changelog: Add a new "Writing tests" chapter to the book r? `@flip1995`
2 parents a8b5245 + 8ee6ca0 commit aa371eb

File tree

2 files changed

+224
-0
lines changed

2 files changed

+224
-0
lines changed

book/src/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- [Basics](development/basics.md)
1515
- [Adding Lints](development/adding_lints.md)
1616
- [Defining Lints](development/defining_lints.md)
17+
- [Writing tests](development/writing_tests.md)
1718
- [Lint Passes](development/lint_passes.md)
1819
- [Type Checking](development/type_checking.md)
1920
- [Method Checking](development/method_checking.md)

book/src/development/writing_tests.md

+223
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# Testing
2+
3+
Developing lints for Clippy is a Test-Driven Development (TDD) process because
4+
our first task before implementing any logic for a new lint is to write some test cases.
5+
6+
## Develop Lints with Tests
7+
8+
When we develop Clippy, we enter a complex and chaotic realm full of
9+
programmatic issues, stylistic errors, illogical code and non-adherence to convention.
10+
Tests are the first layer of order we can leverage to define when and where
11+
we want a new lint to trigger or not.
12+
13+
Moreover, writing tests first help Clippy developers to find a balance for
14+
the first iteration of and further enhancements for a lint.
15+
With test cases on our side, we will not have to worry about over-engineering
16+
a lint on its first version nor missing out some obvious edge cases of the lint.
17+
This approach empowers us to iteratively enhance each lint.
18+
19+
## Clippy UI Tests
20+
21+
We use **UI tests** for testing in Clippy. These UI tests check that the output
22+
of Clippy is exactly as we expect it to be. Each test is just a plain Rust file
23+
that contains the code we want to check.
24+
25+
The output of Clippy is compared against a `.stderr` file. Note that you don't
26+
have to create this file yourself. We'll get to generating the `.stderr` files
27+
with the command [`cargo bless`](#cargo-bless) (seen later on).
28+
29+
### Write Test Cases
30+
31+
Let us now think about some tests for our imaginary `foo_functions` lint. We
32+
start by opening the test file `tests/ui/foo_functions.rs` that was created by
33+
`cargo dev new_lint`.
34+
35+
Update the file with some examples to get started:
36+
37+
```rust
38+
#![warn(clippy::foo_functions)] // < Add this, so the lint is guaranteed to be enabled in this file
39+
40+
// Impl methods
41+
struct A;
42+
impl A {
43+
pub fn fo(&self) {}
44+
pub fn foo(&self) {} //~ ERROR: function called "foo"
45+
pub fn food(&self) {}
46+
}
47+
48+
// Default trait methods
49+
trait B {
50+
fn fo(&self) {}
51+
fn foo(&self) {} //~ ERROR: function called "foo"
52+
fn food(&self) {}
53+
}
54+
55+
// Plain functions
56+
fn fo() {}
57+
fn foo() {} //~ ERROR: function called "foo"
58+
fn food() {}
59+
60+
fn main() {
61+
// We also don't want to lint method calls
62+
foo();
63+
let a = A;
64+
a.foo();
65+
}
66+
```
67+
68+
Without actual lint logic to emit the lint when we see a `foo` function name,
69+
this test will just pass, because no lint will be emitted. However, we can now
70+
run the test with the following command:
71+
72+
```sh
73+
$ TESTNAME=foo_functions cargo uitest
74+
```
75+
76+
Clippy will compile and it will conclude with an `ok` for the tests:
77+
78+
```
79+
...Clippy warnings and test outputs...
80+
test compile_test ... ok
81+
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.48s
82+
```
83+
84+
This is normal. After all, we wrote a bunch of Rust code but we haven't really
85+
implemented any logic for Clippy to detect `foo` functions and emit a lint.
86+
87+
As we gradually implement our lint logic, we will keep running this UI test command.
88+
Clippy will begin outputting information that allows us to check if the output is
89+
turning into what we want it to be.
90+
91+
### Example output
92+
93+
As our `foo_functions` lint is tested, the output would look something like this:
94+
95+
```
96+
failures:
97+
98+
---- compile_test stdout ----
99+
normalized stderr:
100+
error: function called "foo"
101+
--> $DIR/foo_functions.rs:6:12
102+
|
103+
LL | pub fn foo(&self) {}
104+
| ^^^
105+
|
106+
= note: `-D clippy::foo-functions` implied by `-D warnings`
107+
108+
error: function called "foo"
109+
--> $DIR/foo_functions.rs:13:8
110+
|
111+
LL | fn foo(&self) {}
112+
| ^^^
113+
114+
error: function called "foo"
115+
--> $DIR/foo_functions.rs:19:4
116+
|
117+
LL | fn foo() {}
118+
| ^^^
119+
120+
error: aborting due to 3 previous errors
121+
```
122+
123+
Note the *failures* label at the top of the fragment, we'll get rid of it
124+
(saving this output) in the next section.
125+
126+
> _Note:_ You can run multiple test files by specifying a comma separated list:
127+
> `TESTNAME=foo_functions,bar_methods,baz_structs`.
128+
129+
### `cargo bless`
130+
131+
Once we are satisfied with the output, we need to run this command to
132+
generate or update the `.stderr` file for our lint:
133+
134+
```sh
135+
$ TESTNAME=foo_functions cargo uibless
136+
```
137+
138+
This writes the emitted lint suggestions and fixes to the `.stderr` file, with
139+
the reason for the lint, suggested fixes, and line numbers, etc.
140+
141+
Running `TESTNAME=foo_functions cargo uitest` should pass then. When we commit
142+
our lint, we need to commit the generated `.stderr` files, too.
143+
144+
In general, you should only commit files changed by `cargo bless` for the
145+
specific lint you are creating/editing.
146+
147+
> _Note:_ If the generated `.stderr`, and `.fixed` files are empty,
148+
> they should be removed.
149+
150+
## `toml` Tests
151+
152+
Some lints can be configured through a `clippy.toml` file. Those configuration
153+
values are tested in `tests/ui-toml`.
154+
155+
To add a new test there, create a new directory and add the files:
156+
157+
- `clippy.toml`: Put here the configuration value you want to test.
158+
- `lint_name.rs`: A test file where you put the testing code, that should see a
159+
different lint behavior according to the configuration set in the
160+
`clippy.toml` file.
161+
162+
The potential `.stderr` and `.fixed` files can again be generated with `cargo
163+
bless`.
164+
165+
## Cargo Lints
166+
167+
The process of testing is different for Cargo lints in that now we are
168+
interested in the `Cargo.toml` manifest file. In this case, we also need a
169+
minimal crate associated with that manifest. Those tests are generated in
170+
`tests/ui-cargo`.
171+
172+
Imagine we have a new example lint that is named `foo_categories`, we can run:
173+
174+
```sh
175+
$ cargo dev new_lint --name=foo_categories --pass=late --category=cargo
176+
```
177+
178+
After running `cargo dev new_lint` we will find by default two new crates,
179+
each with its manifest file:
180+
181+
* `tests/ui-cargo/foo_categories/fail/Cargo.toml`: this file should cause the
182+
new lint to raise an error.
183+
* `tests/ui-cargo/foo_categories/pass/Cargo.toml`: this file should not trigger
184+
the lint.
185+
186+
If you need more cases, you can copy one of those crates (under
187+
`foo_categories`) and rename it.
188+
189+
The process of generating the `.stderr` file is the same as for other lints
190+
and prepending the `TESTNAME` variable to `cargo uitest` works for Cargo lints too.
191+
192+
## Rustfix Tests
193+
194+
If the lint you are working on is making use of structured suggestions,
195+
[`rustfix`] will apply the suggestions from the lint to the test file code and
196+
compare that to the contents of a `.fixed` file.
197+
198+
Structured suggestions tell a user how to fix or re-write certain code that has
199+
been linted with [`span_lint_and_sugg`].
200+
201+
Should `span_lint_and_sugg` be used to generate a suggestion, but not all
202+
suggestions lead to valid code, you can use the `//@no-rustfix` comment on top
203+
of the test file, to not run `rustfix` on that file.
204+
205+
We'll talk about suggestions more in depth in a later chapter.
206+
<!-- FIXME: (blyxyas) Link to "Emitting lints" when that gets merged -->
207+
208+
Use `cargo bless` to automatically generate the `.fixed` file after running
209+
the tests.
210+
211+
[`rustfix`]: https://github.com/rust-lang/rustfix
212+
[`span_lint_and_sugg`]: https://doc.rust-lang.org/beta/nightly-rustc/clippy_utils/diagnostics/fn.span_lint_and_sugg.html
213+
214+
## Testing Manually
215+
216+
Manually testing against an example file can be useful if you have added some
217+
`println!`s and the test suite output becomes unreadable.
218+
219+
To try Clippy with your local modifications, run from the working copy root.
220+
221+
```sh
222+
$ cargo dev lint input.rs
223+
```

0 commit comments

Comments
 (0)