tidy/
unit_tests.rs

1//! Tidy check to ensure `#[test]` and `#[bench]` are not used directly inside
2//! of the standard library.
3//!
4//! `core` and `alloc` cannot be tested directly due to duplicating lang items.
5//! All tests and benchmarks must be written externally in
6//! `{coretests,alloctests}/{tests,benches}`.
7//!
8//! Outside of the standard library, tests and benchmarks should be outlined
9//! into separate files named `tests.rs` or `benches.rs`, or directories named
10//! `tests` or `benches` unconfigured during normal build.
11
12use std::path::Path;
13
14use crate::diagnostics::{CheckId, DiagCtx};
15use crate::walk::{filter_dirs, walk};
16
17pub fn check(root_path: &Path, stdlib: bool, diag_ctx: DiagCtx) {
18    let mut check = diag_ctx.start_check(CheckId::new("unit_tests").path(root_path));
19
20    let skip = move |path: &Path, is_dir| {
21        let file_name = path.file_name().unwrap_or_default();
22
23        // Skip excluded directories and non-rust files
24        if is_dir {
25            if filter_dirs(path) || path.ends_with("src/doc") {
26                return true;
27            }
28        } else {
29            let extension = path.extension().unwrap_or_default();
30            if extension != "rs" {
31                return true;
32            }
33        }
34
35        // Tests in a separate package are always allowed
36        if is_dir && file_name != "tests" && file_name.as_encoded_bytes().ends_with(b"tests") {
37            return true;
38        }
39
40        if !stdlib {
41            // Outside of the standard library tests may also be in separate files in the same crate
42            if is_dir {
43                if file_name == "tests" || file_name == "benches" {
44                    return true;
45                }
46            } else {
47                if file_name == "tests.rs" || file_name == "benches.rs" {
48                    return true;
49                }
50            }
51        }
52
53        if is_dir {
54            // FIXME remove those exceptions once no longer necessary
55            file_name == "std_detect" || file_name == "std" || file_name == "test"
56        } else {
57            // Tests which use non-public internals and, as such, need to
58            // have the types in the same crate as the tests themselves. See
59            // the comment in alloctests/lib.rs.
60            path.ends_with("library/alloc/src/collections/btree/borrow/tests.rs")
61                || path.ends_with("library/alloc/src/collections/btree/map/tests.rs")
62                || path.ends_with("library/alloc/src/collections/btree/node/tests.rs")
63                || path.ends_with("library/alloc/src/collections/btree/set/tests.rs")
64                || path.ends_with("library/alloc/src/collections/linked_list/tests.rs")
65                || path.ends_with("library/alloc/src/collections/vec_deque/tests.rs")
66                || path.ends_with("library/alloc/src/raw_vec/tests.rs")
67                || path.ends_with("library/alloc/src/wtf8/tests.rs")
68        }
69    };
70
71    walk(root_path, skip, &mut |entry, contents| {
72        let path = entry.path();
73        let package = path
74            .strip_prefix(root_path)
75            .unwrap()
76            .components()
77            .next()
78            .unwrap()
79            .as_os_str()
80            .to_str()
81            .unwrap();
82        for (i, line) in contents.lines().enumerate() {
83            let line = line.trim();
84            let is_test = || line.contains("#[test]") && !line.contains("`#[test]");
85            let is_bench = || line.contains("#[bench]") && !line.contains("`#[bench]");
86            if !line.starts_with("//") && (is_test() || is_bench()) {
87                let explanation = if stdlib {
88                    format!(
89                        "`{package}` unit tests and benchmarks must be placed into `{package}tests`"
90                    )
91                } else {
92                    "unit tests and benchmarks must be placed into \
93                         separate files or directories named \
94                         `tests.rs`, `benches.rs`, `tests` or `benches`"
95                        .to_owned()
96                };
97                let name = if is_test() { "test" } else { "bench" };
98                check.error(format!(
99                    "`{}:{}` contains `#[{name}]`; {explanation}",
100                    path.display(),
101                    i + 1,
102                ));
103                return;
104            }
105        }
106    });
107}