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