Skip to main content

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}