fallow_output/audit_test_reach.rs
1//! Test reach of the changed exports in the review brief.
2
3use serde::Serialize;
4
5/// Maximum number of rows in [`TestReachFacts::exports`]. The rest is counted
6/// in [`TestReachFacts::exports_omitted`].
7pub const TEST_REACH_EXPORT_CAP: usize = 25;
8
9/// Maximum number of files in [`ExportTestReachFact::tests`] and
10/// [`ExportTestReachFact::mocked_targets`].
11pub const TEST_REACH_FILE_CAP: usize = 3;
12
13/// Whether the project has tests to measure against.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
15#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
16#[serde(rename_all = "kebab-case")]
17pub enum TestReachStatus {
18 /// The project has test files, and each changed export has a reach class.
19 Measured,
20 /// The project has no test files. Every changed export counts as
21 /// `none-file`.
22 NoTests,
23}
24
25/// Why mock detection is or is not active.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
27#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
28#[serde(rename_all = "kebab-case")]
29pub enum TestReachMasking {
30 /// At least one test replaces a module with a closed factory mock, and
31 /// `mocked-only` can be reported.
32 Active,
33 /// No test declares a closed factory mock.
34 NoMocks,
35 /// Too many distinct mock sets: mock detection is off, so a mocked
36 /// module counts as reached.
37 Capped,
38}
39
40/// How tests reach one changed export.
41///
42/// Treat this as an open set: a later version can add a class. A consumer
43/// must read an unknown value as "needs attention".
44#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
45#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
46#[serde(rename_all = "kebab-case")]
47pub enum TestReachClass {
48 /// Only tests that replace a module on the import path with a mock
49 /// reference the export. The real code does not run in those tests.
50 MockedOnly,
51 /// Tests load the declaring file, but no test-reachable module references
52 /// the export.
53 None,
54 /// No test loads the declaring file.
55 NoneFile,
56 /// Tests reach the export only through other modules.
57 Transitive,
58 /// A test file references the export.
59 Direct,
60 /// A framework reads the export as configuration with a literal value, so
61 /// it needs no test.
62 Framework,
63}
64
65/// Whether the export existed before the change.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
67#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
68#[serde(rename_all = "kebab-case")]
69pub enum ExportChange {
70 /// The file exported this name at the base of the diff, and the diff
71 /// changes its declaration. An edited, rewritten or moved declaration is
72 /// `modified`.
73 Modified,
74 /// The file did not export this name at the base: a new export, a
75 /// renamed export, or an export of a new file.
76 New,
77}
78
79/// Number of changed exports per reach class. The keys are exactly the
80/// [`TestReachClass`] values.
81#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
82#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
83pub struct TestReachCounts {
84 /// Exports that a test file references.
85 pub direct: usize,
86 /// Exports that tests reach only through other modules.
87 pub transitive: usize,
88 /// Exports that only mocking tests reference.
89 #[serde(rename = "mocked-only")]
90 pub mocked_only: usize,
91 /// Exports in a test-reached file without a test-reachable reference.
92 pub none: usize,
93 /// Exports in a file that no test loads.
94 #[serde(rename = "none-file")]
95 pub none_file: usize,
96 /// Framework configuration exports.
97 pub framework: usize,
98}
99
100impl TestReachCounts {
101 /// Count one export of `class`.
102 pub const fn add(&mut self, class: TestReachClass) {
103 match class {
104 TestReachClass::Direct => self.direct += 1,
105 TestReachClass::Transitive => self.transitive += 1,
106 TestReachClass::MockedOnly => self.mocked_only += 1,
107 TestReachClass::None => self.none += 1,
108 TestReachClass::NoneFile => self.none_file += 1,
109 TestReachClass::Framework => self.framework += 1,
110 }
111 }
112}
113
114/// One changed export that needs attention.
115#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
116#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
117pub struct ExportTestReachFact {
118 /// Root-relative path of the file that declares the export.
119 pub file: String,
120 /// The export name, `default` for a default export.
121 pub export_name: String,
122 /// 1-based line of the export.
123 pub line: u32,
124 /// The reach class.
125 pub reach: TestReachClass,
126 /// Whether the diff adds or changes the declaration.
127 pub change: ExportChange,
128 /// Test files that name the export, sorted, at most
129 /// [`TEST_REACH_FILE_CAP`]. On a `mocked-only` row these are the mocking
130 /// tests. Absent when no test file names the export.
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub tests: Option<Vec<String>>,
133 /// Number of test files that name the export, before the cap. Present with
134 /// `tests`.
135 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub tests_total: Option<usize>,
137 /// True when one of the test files that name the export changed in this
138 /// diff. Present with `tests`.
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub test_changed: Option<bool>,
141 /// The mocked modules on the import paths, sorted, at most
142 /// [`TEST_REACH_FILE_CAP`]. A mocked module can be a barrel in front of
143 /// the declaring file. Present only on `mocked-only` rows.
144 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub mocked_targets: Option<Vec<String>>,
146}
147
148/// How tests reach the value exports that the diff changes.
149///
150/// A changed export is a value export whose declaring statement overlaps an
151/// added line. The section is advisory: it never changes the verdict or the
152/// exit code.
153#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
154#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
155pub struct TestReachFacts {
156 /// `measured`, or `no-tests` when the project has no test files.
157 pub status: TestReachStatus,
158 /// Whether mock detection is active.
159 pub masking: TestReachMasking,
160 /// Number of changed value exports.
161 pub total: usize,
162 /// Number of changed exports per reach class.
163 pub counts: TestReachCounts,
164 /// The exports that need attention (`mocked-only`, `none`, `none-file`,
165 /// `transitive`), at most [`TEST_REACH_EXPORT_CAP`]. The order is reach
166 /// class, then `modified` before `new`, then file, line and name. Empty
167 /// for `no-tests`.
168 pub exports: Vec<ExportTestReachFact>,
169 /// Number of attention rows left out by the cap.
170 pub exports_omitted: usize,
171 /// True when the project uses an end-to-end test runner (Playwright or
172 /// Cypress). End-to-end tests drive the running app and do not import
173 /// source files, so the reach classes do not count them. Absent when no
174 /// such runner is present.
175 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
176 pub e2e_runner: bool,
177}