parlov_analysis/existence/
analyzer.rs1#![deny(clippy::all)]
4#![warn(clippy::pedantic)]
5
6use parlov_core::{OracleClass, OracleResult, OracleVerdict, ProbeSet, ResponseSurface};
7
8use crate::{Analyzer, SampleDecision};
9
10use super::classifier::classify;
11
12pub struct ExistenceAnalyzer;
18
19impl Analyzer for ExistenceAnalyzer {
20 fn evaluate(&self, data: &ProbeSet) -> SampleDecision {
21 let b0 = data.baseline[0].status;
22 let p0 = data.probe[0].status;
23
24 if b0 == p0 {
25 return SampleDecision::Complete(classify(b0, p0));
26 }
27
28 let samples = data.baseline.len();
29 if samples < 3 {
30 return SampleDecision::NeedMore;
31 }
32
33 let stable = is_consistent(&data.baseline) && is_consistent(&data.probe);
34 if stable {
35 SampleDecision::Complete(classify(b0, p0))
36 } else {
37 SampleDecision::Complete(unstable_result(&data.baseline, &data.probe))
38 }
39 }
40
41 fn oracle_class(&self) -> OracleClass {
42 OracleClass::Existence
43 }
44}
45
46fn is_consistent(surfaces: &[ResponseSurface]) -> bool {
48 surfaces.iter().all(|s| s.status == surfaces[0].status)
49}
50
51fn unstable_result(baseline: &[ResponseSurface], probe: &[ResponseSurface]) -> OracleResult {
52 let baseline_stable = is_consistent(baseline);
53 let probe_stable = is_consistent(probe);
54
55 let which = match (baseline_stable, probe_stable) {
56 (false, false) => "baseline and probe sides",
57 (false, true) => "baseline side",
58 (true, false) => "probe side",
59 (true, true) => unreachable!("unstable_result called when both sides are stable"),
60 };
61
62 OracleResult {
63 class: OracleClass::Existence,
64 verdict: OracleVerdict::NotPresent,
65 severity: None,
66 evidence: vec![format!("status codes were inconsistent across samples on {which}")],
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use bytes::Bytes;
74 use http::{HeaderMap, StatusCode};
75 use parlov_core::{OracleVerdict, ProbeSet, ResponseSurface, Severity};
76
77 fn surface(status: u16) -> ResponseSurface {
78 ResponseSurface {
79 status: StatusCode::from_u16(status).expect("valid status code"),
80 headers: HeaderMap::new(),
81 body: Bytes::new(),
82 timing_ns: 0,
83 }
84 }
85
86 fn probe_set_1(baseline_status: u16, probe_status: u16) -> ProbeSet {
87 ProbeSet {
88 baseline: vec![surface(baseline_status)],
89 probe: vec![surface(probe_status)],
90 }
91 }
92
93 fn probe_set_n(baseline_statuses: &[u16], probe_statuses: &[u16]) -> ProbeSet {
94 ProbeSet {
95 baseline: baseline_statuses.iter().map(|&s| surface(s)).collect(),
96 probe: probe_statuses.iter().map(|&s| surface(s)).collect(),
97 }
98 }
99
100 #[test]
103 fn evaluate_1_sample_diff_returns_need_more() {
104 let ps = probe_set_1(403, 404);
105 assert!(matches!(ExistenceAnalyzer.evaluate(&ps), SampleDecision::NeedMore));
106 }
107
108 #[test]
109 fn evaluate_2_samples_diff_returns_need_more() {
110 let ps = probe_set_n(&[403, 403], &[404, 404]);
111 assert!(matches!(ExistenceAnalyzer.evaluate(&ps), SampleDecision::NeedMore));
112 }
113
114 #[test]
115 fn evaluate_3_samples_stable_diff_confirmed() {
116 let ps = probe_set_n(&[403, 403, 403], &[404, 404, 404]);
117 let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
118 panic!("expected Complete");
119 };
120 assert_eq!(result.verdict, OracleVerdict::Confirmed);
121 assert_eq!(result.severity, Some(Severity::High));
122 }
123
124 #[test]
125 fn evaluate_3_samples_unstable_probe_not_present() {
126 let ps = probe_set_n(&[403, 403, 403], &[404, 200, 404]);
128 let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
129 panic!("expected Complete");
130 };
131 assert_eq!(result.verdict, OracleVerdict::NotPresent);
132 assert_eq!(result.severity, None);
133 }
134
135 #[test]
136 fn evaluate_3_samples_unstable_baseline_not_present() {
137 let ps = probe_set_n(&[403, 200, 403], &[404, 404, 404]);
139 let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
140 panic!("expected Complete");
141 };
142 assert_eq!(result.verdict, OracleVerdict::NotPresent);
143 assert_eq!(result.severity, None);
144 }
145
146 #[test]
149 fn evaluate_complete_not_present_on_same_status() {
150 let ps = probe_set_1(404, 404);
152 let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
153 panic!("expected Complete");
154 };
155 assert_eq!(result.verdict, OracleVerdict::NotPresent);
156 assert_eq!(result.severity, None);
157 }
158
159 #[test]
160 fn evaluate_complete_confirmed_high_on_403_vs_404() {
161 let ps = probe_set_n(&[403, 403, 403], &[404, 404, 404]);
162 let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
163 panic!("expected Complete");
164 };
165 assert_eq!(result.verdict, OracleVerdict::Confirmed);
166 assert_eq!(result.severity, Some(Severity::High));
167 }
168
169 #[test]
170 fn analyze_provided_method_delegates_to_evaluate() {
171 let ps = probe_set_n(&[200, 200, 200], &[404, 404, 404]);
173 let result = ExistenceAnalyzer.analyze(&ps);
174 assert_eq!(result.verdict, OracleVerdict::Confirmed);
175 assert_eq!(result.severity, Some(Severity::High));
176 }
177
178 #[test]
180 fn need_more_variant_is_constructible() {
181 let _decision: SampleDecision = SampleDecision::NeedMore;
182 }
183}