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 label: None,
68 leaks: None,
69 rfc_basis: None,
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use bytes::Bytes;
77 use http::{HeaderMap, StatusCode};
78 use parlov_core::{OracleVerdict, ProbeSet, ResponseSurface, Severity};
79
80 fn surface(status: u16) -> ResponseSurface {
81 ResponseSurface {
82 status: StatusCode::from_u16(status).expect("valid status code"),
83 headers: HeaderMap::new(),
84 body: Bytes::new(),
85 timing_ns: 0,
86 }
87 }
88
89 fn probe_set_1(baseline_status: u16, probe_status: u16) -> ProbeSet {
90 ProbeSet {
91 baseline: vec![surface(baseline_status)],
92 probe: vec![surface(probe_status)],
93 }
94 }
95
96 fn probe_set_n(baseline_statuses: &[u16], probe_statuses: &[u16]) -> ProbeSet {
97 ProbeSet {
98 baseline: baseline_statuses.iter().map(|&s| surface(s)).collect(),
99 probe: probe_statuses.iter().map(|&s| surface(s)).collect(),
100 }
101 }
102
103 #[test]
106 fn evaluate_1_sample_diff_returns_need_more() {
107 let ps = probe_set_1(403, 404);
108 assert!(matches!(ExistenceAnalyzer.evaluate(&ps), SampleDecision::NeedMore));
109 }
110
111 #[test]
112 fn evaluate_2_samples_diff_returns_need_more() {
113 let ps = probe_set_n(&[403, 403], &[404, 404]);
114 assert!(matches!(ExistenceAnalyzer.evaluate(&ps), SampleDecision::NeedMore));
115 }
116
117 #[test]
118 fn evaluate_3_samples_stable_diff_confirmed() {
119 let ps = probe_set_n(&[403, 403, 403], &[404, 404, 404]);
120 let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
121 panic!("expected Complete");
122 };
123 assert_eq!(result.verdict, OracleVerdict::Confirmed);
124 assert_eq!(result.severity, Some(Severity::High));
125 }
126
127 #[test]
128 fn evaluate_3_samples_unstable_probe_not_present() {
129 let ps = probe_set_n(&[403, 403, 403], &[404, 200, 404]);
131 let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
132 panic!("expected Complete");
133 };
134 assert_eq!(result.verdict, OracleVerdict::NotPresent);
135 assert_eq!(result.severity, None);
136 }
137
138 #[test]
139 fn evaluate_3_samples_unstable_baseline_not_present() {
140 let ps = probe_set_n(&[403, 200, 403], &[404, 404, 404]);
142 let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
143 panic!("expected Complete");
144 };
145 assert_eq!(result.verdict, OracleVerdict::NotPresent);
146 assert_eq!(result.severity, None);
147 }
148
149 #[test]
152 fn evaluate_complete_not_present_on_same_status() {
153 let ps = probe_set_1(404, 404);
155 let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
156 panic!("expected Complete");
157 };
158 assert_eq!(result.verdict, OracleVerdict::NotPresent);
159 assert_eq!(result.severity, None);
160 }
161
162 #[test]
163 fn evaluate_complete_confirmed_high_on_403_vs_404() {
164 let ps = probe_set_n(&[403, 403, 403], &[404, 404, 404]);
165 let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
166 panic!("expected Complete");
167 };
168 assert_eq!(result.verdict, OracleVerdict::Confirmed);
169 assert_eq!(result.severity, Some(Severity::High));
170 }
171
172 #[test]
173 fn analyze_provided_method_delegates_to_evaluate() {
174 let ps = probe_set_n(&[200, 200, 200], &[404, 404, 404]);
176 let result = ExistenceAnalyzer.analyze(&ps);
177 assert_eq!(result.verdict, OracleVerdict::Confirmed);
178 assert_eq!(result.severity, Some(Severity::High));
179 }
180
181 #[test]
183 fn need_more_variant_is_constructible() {
184 let _decision: SampleDecision = SampleDecision::NeedMore;
185 }
186}