Skip to content

Commit 8fd9f82

Browse files
committed
Auto merge of #10758 - epage:docs, r=weihanglo
docs(contrib): Add documentation for ui tests ### What does this PR try to resolve? This only adds information about snapshot testing using `snapbox` and keeps the functional testing documentation focused on the existing facilities. We can updated this as our use of `snapbox` matures. ### How should we test and review this PR? I did not generate and verify the HTML In writing this, I did notice that we define `cargo_test_support::compare::assert` but only use it for filesystem asserts and not binary asserts. We should probably add our own function that wraps `snapbox::cmd::Command::cargo()` and passes in `cargo_test_support::compare::assert`. I've left that out of this PR to keep things focused.
2 parents 2fcd302 + 619630c commit 8fd9f82

File tree

1 file changed

+163
-61
lines changed

1 file changed

+163
-61
lines changed

src/doc/contrib/src/tests/writing.md

Lines changed: 163 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -6,65 +6,69 @@ tests is also encouraged!
66
## Testsuite
77

88
Cargo has a wide variety of integration tests that execute the `cargo` binary
9-
and verify its behavior, located in the [`testsuite`] directory. The
10-
[`support`] crate contains many helpers to make this process easy.
9+
and verify its behavior, located in the [`testsuite`] directory. The
10+
[`support`] crate and [`snapbox`] contain many helpers to make this process easy.
11+
12+
There are two styles of tests that can roughly be categorized as
13+
- functional tests
14+
- The fixture is programmatically defined
15+
- The assertions are regular string comparisons
16+
- Easier to share in an issue as a code block is completely self-contained
17+
- More resilient to insignificant changes though ui tests are easy to update when a change does occur
18+
- ui tests
19+
- The fixture is file-based
20+
- The assertions use file-backed snapshots that can be updated with an env variable
21+
- Easier to review the expected behavior of the command as more details are included
22+
- Easier to get up and running from an existing project
23+
- Easier to reason about as everything is just files in the repo
1124

1225
These tests typically work by creating a temporary "project" with a
1326
`Cargo.toml` file, executing the `cargo` binary process, and checking the
1427
stdout and stderr output against the expected output.
1528

16-
### `cargo_test` attribute
17-
18-
Cargo's tests use the `#[cargo_test]` attribute instead of `#[test]`. This
19-
attribute injects some code which does some setup before starting the test,
20-
creating the little "sandbox" described below.
21-
22-
### Basic test structure
23-
24-
The general form of a test involves creating a "project", running `cargo`, and
25-
checking the result. Projects are created with the [`ProjectBuilder`] where
26-
you specify some files to create. The general form looks like this:
27-
28-
```rust,ignore
29-
let p = project()
30-
.file("src/main.rs", r#"fn main() { println!("hi!"); }"#)
31-
.build();
32-
```
33-
34-
The project creates a mini sandbox under the "cargo integration test"
35-
directory with each test getting a separate directory such as
36-
`/path/to/cargo/target/cit/t123/`. Each project appears as a separate
37-
directory. There is also an empty `home` directory created that will be used
38-
as a home directory instead of your normal home directory.
39-
40-
If you do not specify a `Cargo.toml` manifest using `file()`, one is
41-
automatically created with a project name of `foo` using `basic_manifest()`.
42-
43-
To run Cargo, call the `cargo` method and make assertions on the execution:
29+
### Functional Tests
4430

31+
Generally, a functional test will be placed in `tests/testsuite/<command>.rs` and will look roughly like:
4532
```rust,ignore
46-
p.cargo("run --bin foo")
47-
.with_stderr(
48-
"\
49-
[COMPILING] foo [..]
50-
[FINISHED] [..]
51-
[RUNNING] `target/debug/foo`
52-
",
53-
)
54-
.with_stdout("hi!")
55-
.run();
33+
#[cargo_test]
34+
fn <description>() {
35+
let p = project()
36+
.file("src/main.rs", r#"fn main() { println!("hi!"); }"#)
37+
.build();
38+
39+
p.cargo("run --bin foo")
40+
.with_stderr(
41+
"\
42+
[COMPILING] foo [..]
43+
[FINISHED] [..]
44+
[RUNNING] `target/debug/foo`
45+
",
46+
)
47+
.with_stdout("hi!")
48+
.run();
49+
}
50+
}
5651
```
5752

58-
This uses the [`Execs`] struct to build up a command to execute, along with
59-
the expected output.
53+
`#[cargo_test]`:
54+
- This is used in place of `#[test]`
55+
- This attribute injects code which does some setup before starting the
56+
test, creating a filesystem "sandbox" under the "cargo integration test"
57+
directory for each test such as
58+
`/path/to/cargo/target/cit/t123/`
59+
- The sandbox will contain a `home` directory that will be used instead of your normal home directory
6060

61-
See [`support::compare`] for an explanation of the string pattern matching.
62-
Patterns are used to make it easier to match against the expected output.
61+
[`ProjectBuilder`] via `project()`:
62+
- Each project is in a separate directory in the sandbox
63+
- If you do not specify a `Cargo.toml` manifest using `file()`, one is
64+
automatically created with a project name of `foo` using `basic_manifest()`.
6365

64-
Browse the `pub` functions and modules in the [`support`] crate for a variety
65-
of other helpful utilities.
66+
[`Execs`] via `p.cargo(...)`:
67+
- This executes the command and evaluates different assertions
68+
- See [`support::compare`] for an explanation of the string pattern matching.
69+
Patterns are used to make it easier to match against the expected output.
6670

67-
### Testing Nightly Features
71+
#### Testing Nightly Features
6872

6973
If you are testing a Cargo feature that only works on "nightly" Cargo, then
7074
you need to call `masquerade_as_nightly_cargo` on the process builder like
@@ -85,17 +89,7 @@ if !is_nightly() {
8589
}
8690
```
8791

88-
### Platform-specific Notes
89-
90-
When checking output, use `/` for paths even on Windows: the actual output
91-
of `\` on Windows will be replaced with `/`.
92-
93-
Be careful when executing binaries on Windows. You should not rename, delete,
94-
or overwrite a binary immediately after running it. Under some conditions
95-
Windows will fail with errors like "directory not empty" or "failed to remove"
96-
or "access is denied".
97-
98-
### Specifying Dependencies
92+
#### Specifying Dependencies
9993

10094
You should not write any tests that use the network such as contacting
10195
crates.io. Typically, simple path dependencies are the easiest way to add a
@@ -123,6 +117,110 @@ If you need to test with registry dependencies, see
123117
If you need to test git dependencies, see [`support::git`] to create a git
124118
dependency.
125119

120+
### UI Tests
121+
122+
UI Tests are a bit more spread out and generally look like:
123+
124+
`tests/testsuite/<command>/mod.rs`:
125+
```rust,ignore
126+
mod <case>;
127+
```
128+
129+
`tests/testsuite/<command>/<case>/mod.rs`:
130+
```rust,ignore
131+
use cargo_test_support::prelude::*;
132+
use cargo_test_support::compare::assert;
133+
use cargo_test_support::Project;
134+
use cargo_test_support::curr_dir;
135+
136+
#[cargo_test]
137+
fn <name>() {
138+
let project = Project::from_template(curr_dir!().join("in"));
139+
let project_root = project.root();
140+
let cwd = &project_root;
141+
142+
snapbox::cmd::Command::cargo()
143+
.arg("run")
144+
.arg_line("--bin foo")
145+
.current_dir(cwd)
146+
.assert()
147+
.success()
148+
.stdout_matches_path(curr_dir!().join("stdout.log"))
149+
.stderr_matches_path(curr_dir!().join("stderr.log"));
150+
151+
assert().subset_matches(curr_dir!().join("out"), &project_root);
152+
}
153+
```
154+
155+
Then populate
156+
- `tests/testsuite/<command>/<case>/in` with the project's directory structure
157+
- `tests/testsuite/<command>/<case>/out` with the files you want verified
158+
- `tests/testsuite/<command>/<case>/stdout.log` with nothing
159+
- `tests/testsuite/<command>/<case>/stderr.log` with nothing
160+
161+
`#[cargo_test]`:
162+
- This is used in place of `#[test]`
163+
- This attribute injects code which does some setup before starting the
164+
test, creating a filesystem "sandbox" under the "cargo integration test"
165+
directory for each test such as
166+
`/path/to/cargo/target/cit/t123/`
167+
- The sandbox will contain a `home` directory that will be used instead of your normal home directory
168+
169+
`Project`:
170+
- The project is copied from a directory in the repo
171+
- Each project is in a separate directory in the sandbox
172+
173+
[`Command`] via `Command::cargo()`:
174+
- Set up and run a command.
175+
176+
[`OutputAssert`] via `Command::assert()`:
177+
- Perform assertions on the result of the [`Command`]
178+
179+
[`Assert`] via `assert()`:
180+
- Verify the command modified the file system as expected
181+
182+
#### Updating Snapshots
183+
184+
The project, stdout, and stderr snapshots can be updated by running with the
185+
`SNAPSHOTS=overwrite` environment variable, like:
186+
```console
187+
$ SNAPSHOTS=overwrite cargo test
188+
```
189+
190+
Be sure to check the snapshots to make sure they make sense.
191+
192+
#### Testing Nightly Features
193+
194+
If you are testing a Cargo feature that only works on "nightly" Cargo, then
195+
you need to call `masquerade_as_nightly_cargo` on the process builder like
196+
this:
197+
198+
```rust,ignore
199+
snapbox::cmd::Command::cargo()
200+
.masquerade_as_nightly_cargo()
201+
```
202+
203+
If you are testing a feature that only works on *nightly rustc* (such as
204+
benchmarks), then you should exit the test if it is not running with nightly
205+
rust, like this:
206+
207+
```rust,ignore
208+
if !is_nightly() {
209+
// Add a comment here explaining why this is necessary.
210+
return;
211+
}
212+
```
213+
214+
### Platform-specific Notes
215+
216+
When checking output, use `/` for paths even on Windows: the actual output
217+
of `\` on Windows will be replaced with `/`.
218+
219+
Be careful when executing binaries on Windows. You should not rename, delete,
220+
or overwrite a binary immediately after running it. Under some conditions
221+
Windows will fail with errors like "directory not empty" or "failed to remove"
222+
or "access is denied".
223+
126224
## Debugging tests
127225

128226
In some cases, you may need to dig into a test that is not working as you
@@ -152,10 +250,14 @@ environment. The general process is:
152250
3. Run with arguments: `r check`
153251

154252
[`testsuite`]: https://github.com/rust-lang/cargo/tree/master/tests/testsuite/
155-
[`ProjectBuilder`]: https://github.com/rust-lang/cargo/blob/e4b65bdc80f2a293447f2f6a808fa7c84bf9a357/crates/cargo-test-support/src/lib.rs#L225-L231
156-
[`Execs`]: https://github.com/rust-lang/cargo/blob/e4b65bdc80f2a293447f2f6a808fa7c84bf9a357/crates/cargo-test-support/src/lib.rs#L558-L579
253+
[`ProjectBuilder`]: https://github.com/rust-lang/cargo/blob/d847468768446168b596f721844193afaaf9d3f2/crates/cargo-test-support/src/lib.rs#L196-L202
254+
[`Execs`]: https://github.com/rust-lang/cargo/blob/d847468768446168b596f721844193afaaf9d3f2/crates/cargo-test-support/src/lib.rs#L531-L550
157255
[`support`]: https://github.com/rust-lang/cargo/blob/master/crates/cargo-test-support/src/lib.rs
158256
[`support::compare`]: https://github.com/rust-lang/cargo/blob/master/crates/cargo-test-support/src/compare.rs
159-
[`support::registry::Package`]: https://github.com/rust-lang/cargo/blob/e4b65bdc80f2a293447f2f6a808fa7c84bf9a357/crates/cargo-test-support/src/registry.rs#L73-L149
257+
[`support::registry::Package`]: https://github.com/rust-lang/cargo/blob/d847468768446168b596f721844193afaaf9d3f2/crates/cargo-test-support/src/registry.rs#L311-L389
160258
[`support::git`]: https://github.com/rust-lang/cargo/blob/master/crates/cargo-test-support/src/git.rs
161259
[Running Cargo]: ../process/working-on-cargo.md#running-cargo
260+
[`snapbox`]: https://docs.rs/snapbox/latest/snapbox/
261+
[`Command`]: https://docs.rs/snapbox/latest/snapbox/cmd/struct.Command.html
262+
[`OutputAssert`]: https://docs.rs/snapbox/latest/snapbox/cmd/struct.OutputAssert.html
263+
[`Assert`]: https://docs.rs/snapbox/latest/snapbox/struct.Assert.html

0 commit comments

Comments
 (0)