#![deny(clippy::all)]
#![warn(clippy::pedantic)]
use parlov_core::{OracleClass, OracleResult, OracleVerdict, ProbeSet, ResponseSummary, ResponseSurface};
use crate::{Analyzer, SampleDecision};
use super::classifier::classify;
use super::diff::diff_headers;
pub struct ExistenceAnalyzer;
impl Analyzer for ExistenceAnalyzer {
fn evaluate(&self, data: &ProbeSet) -> SampleDecision {
let b0 = data.baseline[0].status;
let p0 = data.probe[0].status;
if b0 == p0 {
return SampleDecision::Complete(annotate(classify(b0, p0), &data.baseline[0], &data.probe[0]));
}
let samples = data.baseline.len();
if samples < 3 {
return SampleDecision::NeedMore;
}
let stable = is_consistent(&data.baseline) && is_consistent(&data.probe);
if stable {
SampleDecision::Complete(annotate(classify(b0, p0), &data.baseline[0], &data.probe[0]))
} else {
SampleDecision::Complete(unstable_result(&data.baseline, &data.probe))
}
}
fn oracle_class(&self) -> OracleClass {
OracleClass::Existence
}
}
fn is_consistent(surfaces: &[ResponseSurface]) -> bool {
surfaces.iter().all(|s| s.status == surfaces[0].status)
}
fn unstable_result(baseline: &[ResponseSurface], probe: &[ResponseSurface]) -> OracleResult {
let baseline_stable = is_consistent(baseline);
let probe_stable = is_consistent(probe);
let which = match (baseline_stable, probe_stable) {
(false, false) => "baseline and probe sides",
(false, true) => "baseline side",
(true, false) => "probe side",
(true, true) => unreachable!("unstable_result called when both sides are stable"),
};
let result = OracleResult {
class: OracleClass::Existence,
verdict: OracleVerdict::NotPresent,
severity: None,
evidence: vec![format!("status codes were inconsistent across samples on {which}")],
label: None,
leaks: None,
rfc_basis: None,
baseline_summary: None,
probe_summary: None,
header_diffs: vec![],
};
annotate(result, &baseline[0], &probe[0])
}
fn annotate(
mut result: OracleResult,
baseline: &ResponseSurface,
probe: &ResponseSurface,
) -> OracleResult {
result.baseline_summary = Some(ResponseSummary { status: baseline.status.as_u16() });
result.probe_summary = Some(ResponseSummary { status: probe.status.as_u16() });
result.header_diffs = diff_headers(&baseline.headers, &probe.headers);
result
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
use http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
use parlov_core::{OracleVerdict, ProbeSet, ResponseSurface, ResponseSummary, Severity};
fn surface(status: u16) -> ResponseSurface {
ResponseSurface {
status: StatusCode::from_u16(status).expect("valid status code"),
headers: HeaderMap::new(),
body: Bytes::new(),
timing_ns: 0,
}
}
fn surface_with_header(status: u16, name: &str, value: &str) -> ResponseSurface {
let mut headers = HeaderMap::new();
let header_name = HeaderName::from_bytes(name.as_bytes()).expect("valid header name");
let header_value = HeaderValue::from_str(value).expect("valid header value");
headers.insert(header_name, header_value);
ResponseSurface {
status: StatusCode::from_u16(status).expect("valid status code"),
headers,
body: Bytes::new(),
timing_ns: 0,
}
}
fn probe_set_1(baseline_status: u16, probe_status: u16) -> ProbeSet {
ProbeSet {
baseline: vec![surface(baseline_status)],
probe: vec![surface(probe_status)],
}
}
fn probe_set_n(baseline_statuses: &[u16], probe_statuses: &[u16]) -> ProbeSet {
ProbeSet {
baseline: baseline_statuses.iter().map(|&s| surface(s)).collect(),
probe: probe_statuses.iter().map(|&s| surface(s)).collect(),
}
}
#[test]
fn evaluate_1_sample_diff_returns_need_more() {
let ps = probe_set_1(403, 404);
assert!(matches!(ExistenceAnalyzer.evaluate(&ps), SampleDecision::NeedMore));
}
#[test]
fn evaluate_2_samples_diff_returns_need_more() {
let ps = probe_set_n(&[403, 403], &[404, 404]);
assert!(matches!(ExistenceAnalyzer.evaluate(&ps), SampleDecision::NeedMore));
}
#[test]
fn evaluate_3_samples_stable_diff_confirmed() {
let ps = probe_set_n(&[403, 403, 403], &[404, 404, 404]);
let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
panic!("expected Complete");
};
assert_eq!(result.verdict, OracleVerdict::Confirmed);
assert_eq!(result.severity, Some(Severity::High));
}
#[test]
fn evaluate_3_samples_unstable_probe_not_present() {
let ps = probe_set_n(&[403, 403, 403], &[404, 200, 404]);
let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
panic!("expected Complete");
};
assert_eq!(result.verdict, OracleVerdict::NotPresent);
assert_eq!(result.severity, None);
}
#[test]
fn evaluate_3_samples_unstable_baseline_not_present() {
let ps = probe_set_n(&[403, 200, 403], &[404, 404, 404]);
let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
panic!("expected Complete");
};
assert_eq!(result.verdict, OracleVerdict::NotPresent);
assert_eq!(result.severity, None);
}
#[test]
fn evaluate_complete_not_present_on_same_status() {
let ps = probe_set_1(404, 404);
let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
panic!("expected Complete");
};
assert_eq!(result.verdict, OracleVerdict::NotPresent);
assert_eq!(result.severity, None);
}
#[test]
fn evaluate_complete_confirmed_high_on_403_vs_404() {
let ps = probe_set_n(&[403, 403, 403], &[404, 404, 404]);
let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
panic!("expected Complete");
};
assert_eq!(result.verdict, OracleVerdict::Confirmed);
assert_eq!(result.severity, Some(Severity::High));
}
#[test]
fn analyze_provided_method_delegates_to_evaluate() {
let ps = probe_set_n(&[200, 200, 200], &[404, 404, 404]);
let result = ExistenceAnalyzer.analyze(&ps);
assert_eq!(result.verdict, OracleVerdict::Confirmed);
assert_eq!(result.severity, Some(Severity::High));
}
#[test]
fn need_more_variant_is_constructible() {
let _decision: SampleDecision = SampleDecision::NeedMore;
}
#[test]
fn evaluate_populates_baseline_and_probe_summary() {
let ps = ProbeSet {
baseline: vec![surface(403), surface(403), surface(403)],
probe: vec![surface(404), surface(404), surface(404)],
};
let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
panic!("expected Complete");
};
assert_eq!(result.baseline_summary, Some(ResponseSummary { status: 403 }));
assert_eq!(result.probe_summary, Some(ResponseSummary { status: 404 }));
}
#[test]
fn evaluate_populates_header_diffs_when_headers_differ() {
let ps = ProbeSet {
baseline: vec![
surface_with_header(403, "x-error-code", "FORBIDDEN"),
surface_with_header(403, "x-error-code", "FORBIDDEN"),
surface_with_header(403, "x-error-code", "FORBIDDEN"),
],
probe: vec![
surface_with_header(404, "x-error-code", "NOT_FOUND"),
surface_with_header(404, "x-error-code", "NOT_FOUND"),
surface_with_header(404, "x-error-code", "NOT_FOUND"),
],
};
let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
panic!("expected Complete");
};
assert_eq!(result.header_diffs.len(), 1);
assert_eq!(result.header_diffs[0].name, "x-error-code");
assert_eq!(result.header_diffs[0].baseline, Some("FORBIDDEN".to_string()));
assert_eq!(result.header_diffs[0].probe, Some("NOT_FOUND".to_string()));
}
#[test]
fn evaluate_empty_header_diffs_when_headers_identical() {
let ps = ProbeSet {
baseline: vec![
surface_with_header(403, "x-request-id", "abc"),
surface_with_header(403, "x-request-id", "abc"),
surface_with_header(403, "x-request-id", "abc"),
],
probe: vec![
surface_with_header(404, "x-request-id", "abc"),
surface_with_header(404, "x-request-id", "abc"),
surface_with_header(404, "x-request-id", "abc"),
],
};
let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
panic!("expected Complete");
};
assert!(result.header_diffs.is_empty());
}
#[test]
fn evaluate_header_absent_on_probe_side() {
let ps = ProbeSet {
baseline: vec![
surface_with_header(403, "x-resource-id", "123"),
surface_with_header(403, "x-resource-id", "123"),
surface_with_header(403, "x-resource-id", "123"),
],
probe: vec![surface(404), surface(404), surface(404)],
};
let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
panic!("expected Complete");
};
let diff = result
.header_diffs
.iter()
.find(|d| d.name == "x-resource-id")
.expect("expected x-resource-id diff");
assert_eq!(diff.baseline, Some("123".to_string()));
assert_eq!(diff.probe, None);
}
#[test]
fn unstable_result_populates_summaries() {
let ps = ProbeSet {
baseline: vec![surface(403), surface(403), surface(403)],
probe: vec![surface(404), surface(200), surface(404)],
};
let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ps) else {
panic!("expected Complete");
};
assert_eq!(result.verdict, OracleVerdict::NotPresent);
assert_eq!(result.baseline_summary, Some(ResponseSummary { status: 403 }));
assert_eq!(result.probe_summary, Some(ResponseSummary { status: 404 }));
}
}