1use super::freshness::{
11 evaluate_pair, FreshnessOutcome, FreshnessTrigger, FreshnessVerdict, TransformationPair,
12};
13use super::identity::{CompilationIdentity, OccurrenceSpan, SymbolIdentity};
14use super::manifest::{Case, ExpectedPresentFact, Provenance};
15use super::report::{
16 denominator_shrunk, evaluate_case, ratio_or_undefined, ObservedCase, ObservedFact,
17};
18use super::uncertainty::{ContextHealth, Uncertainty};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum FaultControl {
24 EmptyOutput,
26 PromotedFailedBinding,
28 PromotedAmbiguity,
30 DroppedProject,
32 DroppedGeneratedDocument,
34 WrongRequestedTarget,
37 StaleCacheReuse,
39 DenominatorShrinkage,
41 SelfAgreeingWrongProducer,
44}
45
46impl FaultControl {
47 pub const ALL: [FaultControl; 9] = [
49 FaultControl::EmptyOutput,
50 FaultControl::PromotedFailedBinding,
51 FaultControl::PromotedAmbiguity,
52 FaultControl::DroppedProject,
53 FaultControl::DroppedGeneratedDocument,
54 FaultControl::WrongRequestedTarget,
55 FaultControl::StaleCacheReuse,
56 FaultControl::DenominatorShrinkage,
57 FaultControl::SelfAgreeingWrongProducer,
58 ];
59
60 pub fn label(self) -> &'static str {
62 match self {
63 FaultControl::EmptyOutput => "empty-output",
64 FaultControl::PromotedFailedBinding => "promoted-failed-binding",
65 FaultControl::PromotedAmbiguity => "promoted-ambiguity",
66 FaultControl::DroppedProject => "dropped-project",
67 FaultControl::DroppedGeneratedDocument => "dropped-generated-document",
68 FaultControl::WrongRequestedTarget => "wrong-requested-target",
69 FaultControl::StaleCacheReuse => "stale-cache-reuse",
70 FaultControl::DenominatorShrinkage => "denominator-shrinkage",
71 FaultControl::SelfAgreeingWrongProducer => "self-agreeing-wrong-producer",
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct ControlResult {
79 pub control: FaultControl,
81 pub caught: bool,
83}
84
85fn identity(member: &str) -> SymbolIdentity {
86 SymbolIdentity {
87 assembly: "Fixture".to_string(),
88 declaring_type: "Fixture.Cases".to_string(),
89 member: member.to_string(),
90 generic_arity: 0,
91 overload_signature: format!("{member}()"),
92 compilation: CompilationIdentity {
93 project: "Cases".to_string(),
94 tfm: "net8.0".to_string(),
95 configuration: "Release".to_string(),
96 },
97 }
98}
99
100fn occurrence(line: u32) -> OccurrenceSpan {
101 OccurrenceSpan {
102 file: "src/Cases.cs".to_string(),
103 start_line: line,
104 start_col: 1,
105 end_line: line,
106 end_col: 20,
107 }
108}
109
110fn base_case() -> Case {
111 Case {
112 id: "fault-control-base".to_string(),
113 scenario_family: "shared-language-semantics".to_string(),
114 language: "csharp".to_string(),
115 profiles: vec!["csharp-net8.0-sdk".to_string()],
116 prerequisites: vec![],
117 fact_contract: "semantic-truth-v1".to_string(),
118 source: vec!["src/Cases.cs".to_string()],
119 context: "complete".to_string(),
120 present: vec![ExpectedPresentFact {
121 identity: identity("Load"),
122 occurrence: occurrence(10),
123 state: "confirmed".to_string(),
124 }],
125 absent: vec![],
126 unresolved: vec![],
127 diagnostics: vec![],
128 provenance: Provenance::Reviewed,
129 }
130}
131
132fn clean_observation() -> ObservedCase {
133 ObservedCase {
134 context: ContextHealth::Complete,
135 facts: vec![ObservedFact {
136 identity: identity("Load"),
137 occurrence: occurrence(10),
138 state: Uncertainty::Confirmed,
139 }],
140 diagnostics: vec![],
141 }
142}
143
144fn caught_empty_output(case: &Case) -> bool {
145 let mutated = ObservedCase {
146 facts: vec![],
147 ..clean_observation()
148 };
149 !evaluate_case(case, &mutated).is_pass()
150}
151
152fn caught_promoted_unresolved(expected_state: &str, reason: &str) -> bool {
153 let case = Case {
154 present: vec![],
155 unresolved: vec![super::manifest::ExpectedUnresolvedFact {
156 identity: identity("Load"),
157 occurrence: occurrence(10),
158 state: expected_state.to_string(),
159 reason: reason.to_string(),
160 }],
161 ..base_case()
162 };
163 let promoted = clean_observation();
164 !evaluate_case(&case, &promoted).is_pass()
165}
166
167fn caught_dropped_project(case: &Case) -> bool {
168 let mutated = ObservedCase {
169 context: ContextHealth::Partial {
170 reason: "one requested project was dropped".to_string(),
171 },
172 ..clean_observation()
173 };
174 !evaluate_case(case, &mutated).is_pass()
175}
176
177fn caught_dropped_generated_document(case: &Case) -> bool {
178 let mutated = ObservedCase {
179 context: ContextHealth::Partial {
180 reason: "a generated document was omitted".to_string(),
181 },
182 facts: vec![],
183 ..clean_observation()
184 };
185 !evaluate_case(case, &mutated).is_pass()
186}
187
188fn caught_wrong_requested_target(case: &Case) -> bool {
189 let mutated = ObservedCase {
190 facts: vec![ObservedFact {
191 identity: identity("SomeOtherMember"),
192 occurrence: occurrence(99),
193 state: Uncertainty::Confirmed,
194 }],
195 ..clean_observation()
196 };
197 !evaluate_case(case, &mutated).is_pass()
198}
199
200fn caught_stale_cache_reuse() -> bool {
206 let pair = TransformationPair {
207 id: "fault-control-stale-cache-reuse",
208 trigger: FreshnessTrigger::DependencyCompilation,
209 mapping: "identity",
210 expected: FreshnessVerdict::Stale,
211 };
212 let reused_after_trigger = clean_observation();
213 let still_cached = clean_observation();
214 let outcome = evaluate_pair(&reused_after_trigger, &still_cached, &pair);
215 matches!(outcome, FreshnessOutcome::MissedStaleness)
216}
217
218fn caught_denominator_shrinkage() -> bool {
221 let declared_case_count = 2;
222 let full_denominator = 2;
223 let dropped_denominator = 1;
224 let full_ratio = ratio_or_undefined(1, full_denominator);
225 let dropped_ratio = ratio_or_undefined(1, dropped_denominator);
226 dropped_ratio > full_ratio && denominator_shrunk(dropped_denominator, declared_case_count)
227}
228
229fn caught_self_agreeing_wrong_producer(case: &Case) -> bool {
230 let self_agreeing_but_wrong = ObservedCase {
233 context: ContextHealth::Complete,
234 facts: vec![ObservedFact {
235 identity: identity("LoadFromCache"),
236 occurrence: occurrence(10),
237 state: Uncertainty::Confirmed,
238 }],
239 diagnostics: vec![],
240 };
241 !evaluate_case(case, &self_agreeing_but_wrong).is_pass()
242}
243
244pub fn run_control(control: FaultControl) -> ControlResult {
247 let case = base_case();
248 let caught = match control {
249 FaultControl::EmptyOutput => caught_empty_output(&case),
250 FaultControl::PromotedFailedBinding => {
251 caught_promoted_unresolved("unresolved", "binding could not be verified")
252 }
253 FaultControl::PromotedAmbiguity => {
254 caught_promoted_unresolved("ambiguous", "an off-target overload also matches")
255 }
256 FaultControl::DroppedProject => caught_dropped_project(&case),
257 FaultControl::DroppedGeneratedDocument => caught_dropped_generated_document(&case),
258 FaultControl::WrongRequestedTarget => caught_wrong_requested_target(&case),
259 FaultControl::StaleCacheReuse => caught_stale_cache_reuse(),
260 FaultControl::DenominatorShrinkage => caught_denominator_shrinkage(),
261 FaultControl::SelfAgreeingWrongProducer => caught_self_agreeing_wrong_producer(&case),
262 };
263 ControlResult { control, caught }
264}
265
266pub fn run_battery() -> Vec<ControlResult> {
268 FaultControl::ALL.iter().map(|c| run_control(*c)).collect()
269}
270
271pub fn battery_is_exhaustive_and_all_caught() -> Result<(), Vec<FaultControl>> {
278 let results = run_battery();
279 let missed: Vec<FaultControl> = results
280 .iter()
281 .filter(|r| !r.caught)
282 .map(|r| r.control)
283 .collect();
284 if missed.is_empty() && results.len() == FaultControl::ALL.len() {
285 Ok(())
286 } else {
287 Err(missed)
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn every_registered_control_is_caught() {
297 assert_eq!(battery_is_exhaustive_and_all_caught(), Ok(()));
298 }
299
300 #[test]
301 fn each_row_reports_which_control_it_is() {
302 for result in run_battery() {
303 assert!(result.caught, "{} was not caught", result.control.label());
304 }
305 }
306
307 #[test]
308 fn the_registered_list_has_nine_rows() {
309 assert_eq!(FaultControl::ALL.len(), 9);
310 }
311}