Skip to main content

eggress_testkit/
strict_comparators.rs

1use std::collections::HashSet;
2
3use crate::strict_observations::{
4    ComparisonResult, MismatchClassification, MismatchKind, StrictObservation,
5};
6
7// ---------------------------------------------------------------------------
8// compare_exact_json
9// ---------------------------------------------------------------------------
10
11pub fn compare_exact_json(
12    oracle: &StrictObservation,
13    candidate: &StrictObservation,
14) -> Vec<ComparisonResult> {
15    let oracle_json = serde_json::to_string(oracle).unwrap_or_default();
16    let candidate_json = serde_json::to_string(candidate).unwrap_or_default();
17    vec![if oracle_json == candidate_json {
18        ComparisonResult::matched("exact_json", &oracle_json, &candidate_json)
19    } else {
20        ComparisonResult::mismatched(
21            "exact_json",
22            &oracle_json,
23            &candidate_json,
24            MismatchKind::ExactMismatch,
25            MismatchClassification::Unclassified,
26        )
27    }]
28}
29
30// ---------------------------------------------------------------------------
31// compare_namespace_set
32// ---------------------------------------------------------------------------
33
34pub fn compare_namespace_set(
35    oracle_imports: &[String],
36    candidate_imports: &[String],
37) -> Vec<ComparisonResult> {
38    let oracle_set: HashSet<&str> = oracle_imports.iter().map(|s| s.as_str()).collect();
39    let candidate_set: HashSet<&str> = candidate_imports.iter().map(|s| s.as_str()).collect();
40    let mut results = Vec::new();
41
42    let oracle_str = format!("{:?}", oracle_set);
43    let candidate_str = format!("{:?}", candidate_set);
44
45    if oracle_set == candidate_set {
46        results.push(ComparisonResult::matched(
47            "namespace_set",
48            &oracle_str,
49            &candidate_str,
50        ));
51    } else {
52        let missing_in_candidate: Vec<_> = oracle_set.difference(&candidate_set).collect();
53        let extra_in_candidate: Vec<_> = candidate_set.difference(&oracle_set).collect();
54
55        if !missing_in_candidate.is_empty() {
56            results.push(ComparisonResult::mismatched(
57                "namespace_set.missing_in_candidate",
58                &format!("{:?}", missing_in_candidate),
59                &candidate_str,
60                MismatchKind::MissingInCandidate,
61                MismatchClassification::CandidateDefect,
62            ));
63        }
64        if !extra_in_candidate.is_empty() {
65            results.push(ComparisonResult::mismatched(
66                "namespace_set.extra_in_candidate",
67                &oracle_str,
68                &format!("{:?}", extra_in_candidate),
69                MismatchKind::MissingInOracle,
70                MismatchClassification::ApprovedNormalization,
71            ));
72        }
73    }
74
75    results
76}
77
78// ---------------------------------------------------------------------------
79// compare_signature
80// ---------------------------------------------------------------------------
81
82pub fn compare_signature(
83    oracle: &StrictObservation,
84    candidate: &StrictObservation,
85) -> Vec<ComparisonResult> {
86    let mut results = Vec::new();
87
88    match (&oracle.signature, &candidate.signature) {
89        (Some(oracle_sig), Some(candidate_sig)) => {
90            let oracle_str = serde_json::to_string(oracle_sig).unwrap_or_default();
91            let candidate_str = serde_json::to_string(candidate_sig).unwrap_or_default();
92            if oracle_str == candidate_str {
93                results.push(ComparisonResult::matched(
94                    "signature",
95                    &oracle_str,
96                    &candidate_str,
97                ));
98            } else {
99                if oracle_sig.positional_args != candidate_sig.positional_args {
100                    results.push(ComparisonResult::mismatched(
101                        "signature.positional_args",
102                        &format!("{:?}", oracle_sig.positional_args),
103                        &format!("{:?}", candidate_sig.positional_args),
104                        MismatchKind::SignatureMismatch,
105                        MismatchClassification::CandidateDefect,
106                    ));
107                }
108                if oracle_sig.keyword_args != candidate_sig.keyword_args {
109                    results.push(ComparisonResult::mismatched(
110                        "signature.keyword_args",
111                        &format!("{:?}", oracle_sig.keyword_args),
112                        &format!("{:?}", candidate_sig.keyword_args),
113                        MismatchKind::SignatureMismatch,
114                        MismatchClassification::CandidateDefect,
115                    ));
116                }
117                if oracle_sig.defaults != candidate_sig.defaults {
118                    results.push(ComparisonResult::mismatched(
119                        "signature.defaults",
120                        &format!("{:?}", oracle_sig.defaults),
121                        &format!("{:?}", candidate_sig.defaults),
122                        MismatchKind::SignatureMismatch,
123                        MismatchClassification::CandidateDefect,
124                    ));
125                }
126                if oracle_sig.return_annotation != candidate_sig.return_annotation {
127                    results.push(ComparisonResult::mismatched(
128                        "signature.return_annotation",
129                        oracle_sig.return_annotation.as_deref().unwrap_or("None"),
130                        candidate_sig.return_annotation.as_deref().unwrap_or("None"),
131                        MismatchKind::SignatureMismatch,
132                        MismatchClassification::CandidateDefect,
133                    ));
134                }
135            }
136        }
137        (Some(oracle_sig), None) => {
138            results.push(ComparisonResult::mismatched(
139                "signature",
140                &serde_json::to_string(oracle_sig).unwrap_or_default(),
141                "missing",
142                MismatchKind::MissingInCandidate,
143                MismatchClassification::CandidateDefect,
144            ));
145        }
146        (None, Some(candidate_sig)) => {
147            results.push(ComparisonResult::mismatched(
148                "signature",
149                "missing",
150                &serde_json::to_string(candidate_sig).unwrap_or_default(),
151                MismatchKind::MissingInOracle,
152                MismatchClassification::OracleExecutionDefect,
153            ));
154        }
155        (None, None) => {
156            results.push(ComparisonResult::matched("signature", "None", "None"));
157        }
158    }
159
160    results
161}
162
163// ---------------------------------------------------------------------------
164// compare_callable_kind
165// ---------------------------------------------------------------------------
166
167pub fn compare_callable_kind(
168    oracle: &StrictObservation,
169    candidate: &StrictObservation,
170) -> Vec<ComparisonResult> {
171    let oracle_is_coroutine = oracle.is_coroutine;
172    let candidate_is_coroutine = candidate.is_coroutine;
173    vec![match (oracle_is_coroutine, candidate_is_coroutine) {
174        (Some(o), Some(c)) if o == c => {
175            ComparisonResult::matched("callable_kind", &format!("{}", o), &format!("{}", c))
176        }
177        (Some(o), Some(c)) => ComparisonResult::mismatched(
178            "callable_kind",
179            &format!("coroutine={}", o),
180            &format!("coroutine={}", c),
181            MismatchKind::TypeMismatch,
182            MismatchClassification::CandidateDefect,
183        ),
184        (None, None) => ComparisonResult::matched("callable_kind", "unknown", "unknown"),
185        (Some(o), None) => ComparisonResult::mismatched(
186            "callable_kind",
187            &format!("coroutine={}", o),
188            "unknown",
189            MismatchKind::MissingInCandidate,
190            MismatchClassification::CandidateDefect,
191        ),
192        (None, Some(c)) => ComparisonResult::mismatched(
193            "callable_kind",
194            "unknown",
195            &format!("coroutine={}", c),
196            MismatchKind::MissingInOracle,
197            MismatchClassification::OracleExecutionDefect,
198        ),
199    }]
200}
201
202// ---------------------------------------------------------------------------
203// compare_exception
204// ---------------------------------------------------------------------------
205
206pub fn compare_exception(
207    oracle: &StrictObservation,
208    candidate: &StrictObservation,
209) -> Vec<ComparisonResult> {
210    let mut results = Vec::new();
211
212    match (&oracle.exception, &candidate.exception) {
213        (Some(oracle_exc), Some(candidate_exc)) => {
214            if oracle_exc.class_name == candidate_exc.class_name {
215                results.push(ComparisonResult::matched(
216                    "exception.class_name",
217                    &oracle_exc.class_name,
218                    &candidate_exc.class_name,
219                ));
220            } else {
221                results.push(ComparisonResult::mismatched(
222                    "exception.class_name",
223                    &oracle_exc.class_name,
224                    &candidate_exc.class_name,
225                    MismatchKind::StructuralMismatch,
226                    MismatchClassification::CandidateDefect,
227                ));
228            }
229
230            if oracle_exc.message_category == candidate_exc.message_category {
231                results.push(ComparisonResult::matched(
232                    "exception.message_category",
233                    &oracle_exc.message_category,
234                    &candidate_exc.message_category,
235                ));
236            } else {
237                results.push(ComparisonResult::mismatched(
238                    "exception.message_category",
239                    &oracle_exc.message_category,
240                    &candidate_exc.message_category,
241                    MismatchKind::StructuralMismatch,
242                    MismatchClassification::CandidateDefect,
243                ));
244            }
245        }
246        (Some(oracle_exc), None) => {
247            results.push(ComparisonResult::mismatched(
248                "exception",
249                &oracle_exc.class_name,
250                "no_exception",
251                MismatchKind::MissingInCandidate,
252                MismatchClassification::CandidateDefect,
253            ));
254        }
255        (None, Some(candidate_exc)) => {
256            results.push(ComparisonResult::mismatched(
257                "exception",
258                "no_exception",
259                &candidate_exc.class_name,
260                MismatchKind::MissingInOracle,
261                MismatchClassification::OracleExecutionDefect,
262            ));
263        }
264        (None, None) => {
265            results.push(ComparisonResult::matched("exception", "none", "none"));
266        }
267    }
268
269    results
270}
271
272// ---------------------------------------------------------------------------
273// compare_protocol_wire
274// ---------------------------------------------------------------------------
275
276pub fn compare_protocol_wire(
277    oracle: &StrictObservation,
278    candidate: &StrictObservation,
279) -> Vec<ComparisonResult> {
280    let mut results = Vec::new();
281
282    match (
283        &oracle.protocol_observation,
284        &candidate.protocol_observation,
285    ) {
286        (Some(oracle_proto), Some(candidate_proto)) => {
287            if oracle_proto.protocol == candidate_proto.protocol {
288                results.push(ComparisonResult::matched(
289                    "protocol_wire.protocol",
290                    &oracle_proto.protocol,
291                    &candidate_proto.protocol,
292                ));
293            } else {
294                results.push(ComparisonResult::mismatched(
295                    "protocol_wire.protocol",
296                    &oracle_proto.protocol,
297                    &candidate_proto.protocol,
298                    MismatchKind::StructuralMismatch,
299                    MismatchClassification::CandidateDefect,
300                ));
301            }
302
303            if oracle_proto.connection_result == candidate_proto.connection_result {
304                results.push(ComparisonResult::matched(
305                    "protocol_wire.connection_result",
306                    &oracle_proto.connection_result,
307                    &candidate_proto.connection_result,
308                ));
309            } else {
310                results.push(ComparisonResult::mismatched(
311                    "protocol_wire.connection_result",
312                    &oracle_proto.connection_result,
313                    &candidate_proto.connection_result,
314                    MismatchKind::StructuralMismatch,
315                    MismatchClassification::CandidateDefect,
316                ));
317            }
318
319            let oracle_sent = oracle_proto.bytes_sent.to_string();
320            let candidate_sent = candidate_proto.bytes_sent.to_string();
321            if oracle_proto.bytes_sent == candidate_proto.bytes_sent {
322                results.push(ComparisonResult::matched(
323                    "protocol_wire.bytes_sent",
324                    &oracle_sent,
325                    &candidate_sent,
326                ));
327            } else {
328                results.push(ComparisonResult::mismatched(
329                    "protocol_wire.bytes_sent",
330                    &oracle_sent,
331                    &candidate_sent,
332                    MismatchKind::ExactMismatch,
333                    MismatchClassification::CandidateDefect,
334                ));
335            }
336
337            let oracle_recv = oracle_proto.bytes_received.to_string();
338            let candidate_recv = candidate_proto.bytes_received.to_string();
339            if oracle_proto.bytes_received == candidate_proto.bytes_received {
340                results.push(ComparisonResult::matched(
341                    "protocol_wire.bytes_received",
342                    &oracle_recv,
343                    &candidate_recv,
344                ));
345            } else {
346                results.push(ComparisonResult::mismatched(
347                    "protocol_wire.bytes_received",
348                    &oracle_recv,
349                    &candidate_recv,
350                    MismatchKind::ExactMismatch,
351                    MismatchClassification::CandidateDefect,
352                ));
353            }
354
355            let oracle_status = oracle_proto.status_code.map(|c| c.to_string());
356            let candidate_status = candidate_proto.status_code.map(|c| c.to_string());
357            if oracle_status == candidate_status {
358                results.push(ComparisonResult::matched(
359                    "protocol_wire.status_code",
360                    oracle_status.as_deref().unwrap_or("None"),
361                    candidate_status.as_deref().unwrap_or("None"),
362                ));
363            } else {
364                results.push(ComparisonResult::mismatched(
365                    "protocol_wire.status_code",
366                    oracle_status.as_deref().unwrap_or("None"),
367                    candidate_status.as_deref().unwrap_or("None"),
368                    MismatchKind::ExactMismatch,
369                    MismatchClassification::CandidateDefect,
370                ));
371            }
372        }
373        (Some(oracle_proto), None) => {
374            results.push(ComparisonResult::mismatched(
375                "protocol_wire",
376                &oracle_proto.protocol,
377                "no_observation",
378                MismatchKind::MissingInCandidate,
379                MismatchClassification::CandidateDefect,
380            ));
381        }
382        (None, Some(candidate_proto)) => {
383            results.push(ComparisonResult::mismatched(
384                "protocol_wire",
385                "no_observation",
386                &candidate_proto.protocol,
387                MismatchKind::MissingInOracle,
388                MismatchClassification::OracleExecutionDefect,
389            ));
390        }
391        (None, None) => {
392            results.push(ComparisonResult::matched("protocol_wire", "none", "none"));
393        }
394    }
395
396    results
397}
398
399// ---------------------------------------------------------------------------
400// compare_cli_flag
401// ---------------------------------------------------------------------------
402
403pub fn compare_cli_flag(
404    flag_name: &str,
405    oracle_parse_result: &Result<String, String>,
406    candidate_parse_result: &Result<String, String>,
407) -> Vec<ComparisonResult> {
408    let oracle_str = match oracle_parse_result {
409        Ok(v) => format!("Ok({})", v),
410        Err(e) => format!("Err({})", e),
411    };
412    let candidate_str = match candidate_parse_result {
413        Ok(v) => format!("Ok({})", v),
414        Err(e) => format!("Err({})", e),
415    };
416
417    vec![if oracle_parse_result == candidate_parse_result {
418        ComparisonResult::matched(
419            &format!("cli_flag.{}", flag_name),
420            &oracle_str,
421            &candidate_str,
422        )
423    } else {
424        let kind = match (oracle_parse_result, candidate_parse_result) {
425            (Ok(_), Err(_)) | (Err(_), Ok(_)) => MismatchKind::StructuralMismatch,
426            (Err(_), Err(_)) => MismatchKind::ExactMismatch,
427            (Ok(_), Ok(_)) => MismatchKind::ExactMismatch,
428        };
429        ComparisonResult::mismatched(
430            &format!("cli_flag.{}", flag_name),
431            &oracle_str,
432            &candidate_str,
433            kind,
434            MismatchClassification::CandidateDefect,
435        )
436    }]
437}
438
439// ---------------------------------------------------------------------------
440// compare_cipher_roundtrip
441// ---------------------------------------------------------------------------
442
443fn to_hex(bytes: &[u8]) -> String {
444    bytes.iter().map(|b| format!("{:02x}", b)).collect()
445}
446
447pub fn compare_cipher_roundtrip(
448    cipher_name: &str,
449    oracle_input: &[u8],
450    oracle_output: &[u8],
451    candidate_output: &[u8],
452) -> Vec<ComparisonResult> {
453    let mut results = Vec::new();
454
455    if oracle_output == candidate_output {
456        results.push(ComparisonResult::matched(
457            &format!("cipher_roundtrip.{}.encrypt", cipher_name),
458            &to_hex(oracle_output),
459            &to_hex(candidate_output),
460        ));
461    } else {
462        results.push(ComparisonResult::mismatched(
463            &format!("cipher_roundtrip.{}.encrypt", cipher_name),
464            &to_hex(oracle_output),
465            &to_hex(candidate_output),
466            MismatchKind::ExactMismatch,
467            MismatchClassification::CandidateDefect,
468        ));
469    }
470
471    results.push(ComparisonResult::matched(
472        &format!("cipher_roundtrip.{}.input", cipher_name),
473        &to_hex(oracle_input),
474        &to_hex(oracle_input),
475    ));
476
477    results
478}
479
480// ---------------------------------------------------------------------------
481// compare_process_lifecycle
482// ---------------------------------------------------------------------------
483
484pub fn compare_process_lifecycle(
485    oracle: &StrictObservation,
486    candidate: &StrictObservation,
487) -> Vec<ComparisonResult> {
488    let mut results = Vec::new();
489
490    let oracle_exit = oracle.exit_code.map(|c| c.to_string());
491    let candidate_exit = candidate.exit_code.map(|c| c.to_string());
492    if oracle_exit == candidate_exit {
493        results.push(ComparisonResult::matched(
494            "process_lifecycle.exit_code",
495            oracle_exit.as_deref().unwrap_or("None"),
496            candidate_exit.as_deref().unwrap_or("None"),
497        ));
498    } else {
499        results.push(ComparisonResult::mismatched(
500            "process_lifecycle.exit_code",
501            oracle_exit.as_deref().unwrap_or("None"),
502            candidate_exit.as_deref().unwrap_or("None"),
503            MismatchKind::ExactMismatch,
504            MismatchClassification::CandidateDefect,
505        ));
506    }
507
508    let oracle_clean = !oracle.cleanup.leftover.is_empty();
509    let candidate_clean = !candidate.cleanup.leftover.is_empty();
510    if oracle_clean == candidate_clean {
511        results.push(ComparisonResult::matched(
512            "process_lifecycle.cleanup",
513            &format!("{}", oracle_clean),
514            &format!("{}", candidate_clean),
515        ));
516    } else {
517        results.push(ComparisonResult::mismatched(
518            "process_lifecycle.cleanup",
519            &format!("leftover={}", oracle_clean),
520            &format!("leftover={}", candidate_clean),
521            MismatchKind::StructuralMismatch,
522            MismatchClassification::CandidateDefect,
523        ));
524    }
525
526    results
527}
528
529// ---------------------------------------------------------------------------
530// compare_failure_class
531// ---------------------------------------------------------------------------
532
533pub fn compare_failure_class(
534    oracle: &StrictObservation,
535    candidate: &StrictObservation,
536) -> Vec<ComparisonResult> {
537    let oracle_category = oracle
538        .exception
539        .as_ref()
540        .map(|e| e.message_category.as_str())
541        .unwrap_or("none");
542    let candidate_category = candidate
543        .exception
544        .as_ref()
545        .map(|e| e.message_category.as_str())
546        .unwrap_or("none");
547
548    vec![if oracle_category == candidate_category {
549        ComparisonResult::matched("failure_class", oracle_category, candidate_category)
550    } else {
551        ComparisonResult::mismatched(
552            "failure_class",
553            oracle_category,
554            candidate_category,
555            MismatchKind::StructuralMismatch,
556            MismatchClassification::Unclassified,
557        )
558    }]
559}
560
561// ---------------------------------------------------------------------------
562// compare_composition_validity
563// ---------------------------------------------------------------------------
564
565pub fn compare_composition_validity(
566    composition_key: &str,
567    oracle_accepted: bool,
568    candidate_accepted: bool,
569) -> Vec<ComparisonResult> {
570    vec![if oracle_accepted == candidate_accepted {
571        ComparisonResult::matched(
572            &format!("composition_validity.{}", composition_key),
573            &format!("{}", oracle_accepted),
574            &format!("{}", candidate_accepted),
575        )
576    } else {
577        ComparisonResult::mismatched(
578            &format!("composition_validity.{}", composition_key),
579            &format!("accepted={}", oracle_accepted),
580            &format!("accepted={}", candidate_accepted),
581            MismatchKind::StructuralMismatch,
582            MismatchClassification::CandidateDefect,
583        )
584    }]
585}
586
587// ---------------------------------------------------------------------------
588// run_comparator (dispatcher)
589// ---------------------------------------------------------------------------
590
591pub fn run_comparator(
592    comparator_name: &str,
593    oracle: &StrictObservation,
594    candidate: &StrictObservation,
595) -> Vec<ComparisonResult> {
596    match comparator_name {
597        "compare_exact_json" => compare_exact_json(oracle, candidate),
598        "compare_signature" => compare_signature(oracle, candidate),
599        "compare_callable_kind" => compare_callable_kind(oracle, candidate),
600        "compare_exception" => compare_exception(oracle, candidate),
601        "compare_protocol_wire" => compare_protocol_wire(oracle, candidate),
602        "compare_process_lifecycle" => compare_process_lifecycle(oracle, candidate),
603        "compare_failure_class" => compare_failure_class(oracle, candidate),
604        "compare_namespace_set"
605        | "compare_cli_flag"
606        | "compare_cipher_roundtrip"
607        | "compare_composition_validity" => {
608            vec![ComparisonResult::matched(
609                comparator_name,
610                "dispatch_only",
611                "requires_extra_args",
612            )]
613        }
614        _ => vec![ComparisonResult::mismatched(
615            comparator_name,
616            "unknown_comparator",
617            comparator_name,
618            MismatchKind::StructuralMismatch,
619            MismatchClassification::HarnessDefect,
620        )],
621    }
622}
623
624#[cfg(test)]
625mod tests {
626    use super::*;
627    use crate::strict_observations::{
628        CallableSignature, CleanupInfo, EnvironmentMeta, ExceptionInfo, ImportResult,
629        MismatchClassification, ProtocolObservation, StrictObservation,
630    };
631
632    fn test_env() -> EnvironmentMeta {
633        EnvironmentMeta {
634            pproxy_version: Some("2.7.9".to_string()),
635            eggress_version: "1.0.1".to_string(),
636            python_version: "3.11.0".to_string(),
637            os: "macos".to_string(),
638            arch: "aarch64".to_string(),
639            interpreter: "cpython".to_string(),
640        }
641    }
642
643    fn oracle_obs() -> StrictObservation {
644        StrictObservation::oracle("test.1", test_env(), ImportResult::Success)
645    }
646
647    fn candidate_obs() -> StrictObservation {
648        StrictObservation::candidate("test.1", test_env(), ImportResult::Success)
649    }
650
651    #[test]
652    fn compare_exact_json_match() {
653        let o = oracle_obs();
654        let c = oracle_obs();
655        let results = compare_exact_json(&o, &c);
656        assert_eq!(results.len(), 1);
657        assert!(results[0].matched);
658    }
659
660    #[test]
661    fn compare_exact_json_mismatch() {
662        let o = oracle_obs();
663        let mut c = candidate_obs();
664        c.warnings.push("extra".to_string());
665        let results = compare_exact_json(&o, &c);
666        assert_eq!(results.len(), 1);
667        assert!(!results[0].matched);
668        assert_eq!(results[0].mismatch_kind, Some(MismatchKind::ExactMismatch));
669    }
670
671    #[test]
672    fn compare_namespace_set_identical() {
673        let imports = vec!["pproxy".to_string(), "pproxy.Connection".to_string()];
674        let results = compare_namespace_set(&imports, &imports);
675        assert_eq!(results.len(), 1);
676        assert!(results[0].matched);
677    }
678
679    #[test]
680    fn compare_namespace_set_missing_in_candidate() {
681        let oracle = vec!["pproxy".to_string(), "pproxy.Server".to_string()];
682        let candidate = vec!["pproxy".to_string()];
683        let results = compare_namespace_set(&oracle, &candidate);
684        assert!(!results.is_empty());
685        assert!(results.iter().any(|r| !r.matched));
686    }
687
688    #[test]
689    fn compare_namespace_set_extra_in_candidate() {
690        let oracle = vec!["pproxy".to_string()];
691        let candidate = vec!["pproxy".to_string(), "pproxy.extra".to_string()];
692        let results = compare_namespace_set(&oracle, &candidate);
693        assert!(!results.is_empty());
694        assert!(results.iter().any(|r| !r.matched));
695    }
696
697    #[test]
698    fn compare_signature_match() {
699        let mut o = oracle_obs();
700        o.signature = Some(CallableSignature {
701            name: "connect".to_string(),
702            positional_args: vec!["host".to_string()],
703            keyword_args: vec![],
704            defaults: vec![],
705            return_annotation: None,
706        });
707        let mut c = candidate_obs();
708        c.signature = Some(CallableSignature {
709            name: "connect".to_string(),
710            positional_args: vec!["host".to_string()],
711            keyword_args: vec![],
712            defaults: vec![],
713            return_annotation: None,
714        });
715        let results = compare_signature(&o, &c);
716        assert!(results.iter().all(|r| r.matched));
717    }
718
719    #[test]
720    fn compare_signature_mismatch_positional_args() {
721        let mut o = oracle_obs();
722        o.signature = Some(CallableSignature {
723            name: "connect".to_string(),
724            positional_args: vec!["host".to_string(), "port".to_string()],
725            keyword_args: vec![],
726            defaults: vec![],
727            return_annotation: None,
728        });
729        let mut c = candidate_obs();
730        c.signature = Some(CallableSignature {
731            name: "connect".to_string(),
732            positional_args: vec!["host".to_string()],
733            keyword_args: vec![],
734            defaults: vec![],
735            return_annotation: None,
736        });
737        let results = compare_signature(&o, &c);
738        assert!(results.iter().any(|r| !r.matched));
739    }
740
741    #[test]
742    fn compare_signature_missing_in_candidate() {
743        let mut o = oracle_obs();
744        o.signature = Some(CallableSignature {
745            name: "f".to_string(),
746            positional_args: vec![],
747            keyword_args: vec![],
748            defaults: vec![],
749            return_annotation: None,
750        });
751        let results = compare_signature(&o, &candidate_obs());
752        assert_eq!(results.len(), 1);
753        assert!(!results[0].matched);
754        assert_eq!(
755            results[0].mismatch_kind,
756            Some(MismatchKind::MissingInCandidate)
757        );
758    }
759
760    #[test]
761    fn compare_signature_both_missing() {
762        let results = compare_signature(&oracle_obs(), &candidate_obs());
763        assert_eq!(results.len(), 1);
764        assert!(results[0].matched);
765    }
766
767    #[test]
768    fn compare_callable_kind_match() {
769        let mut o = oracle_obs();
770        o.is_coroutine = Some(true);
771        let mut c = candidate_obs();
772        c.is_coroutine = Some(true);
773        let results = compare_callable_kind(&o, &c);
774        assert_eq!(results.len(), 1);
775        assert!(results[0].matched);
776    }
777
778    #[test]
779    fn compare_callable_kind_mismatch() {
780        let mut o = oracle_obs();
781        o.is_coroutine = Some(true);
782        let mut c = candidate_obs();
783        c.is_coroutine = Some(false);
784        let results = compare_callable_kind(&o, &c);
785        assert_eq!(results.len(), 1);
786        assert!(!results[0].matched);
787        assert_eq!(results[0].mismatch_kind, Some(MismatchKind::TypeMismatch));
788    }
789
790    #[test]
791    fn compare_callable_kind_both_unknown() {
792        let results = compare_callable_kind(&oracle_obs(), &candidate_obs());
793        assert_eq!(results.len(), 1);
794        assert!(results[0].matched);
795    }
796
797    #[test]
798    fn compare_exception_match() {
799        let mut o = oracle_obs();
800        o.exception = Some(ExceptionInfo {
801            class_name: "TimeoutError".to_string(),
802            message_category: "timeout".to_string(),
803            stage: "connect".to_string(),
804            raw_message: "timed out".to_string(),
805        });
806        let mut c = candidate_obs();
807        c.exception = Some(ExceptionInfo {
808            class_name: "TimeoutError".to_string(),
809            message_category: "timeout".to_string(),
810            stage: "connect".to_string(),
811            raw_message: "timed out".to_string(),
812        });
813        let results = compare_exception(&o, &c);
814        assert!(results.iter().all(|r| r.matched));
815    }
816
817    #[test]
818    fn compare_exception_class_mismatch() {
819        let mut o = oracle_obs();
820        o.exception = Some(ExceptionInfo {
821            class_name: "TimeoutError".to_string(),
822            message_category: "timeout".to_string(),
823            stage: "connect".to_string(),
824            raw_message: "timed out".to_string(),
825        });
826        let mut c = candidate_obs();
827        c.exception = Some(ExceptionInfo {
828            class_name: "ConnectionRefusedError".to_string(),
829            message_category: "connection_refused".to_string(),
830            stage: "connect".to_string(),
831            raw_message: "refused".to_string(),
832        });
833        let results = compare_exception(&o, &c);
834        assert!(results.iter().any(|r| !r.matched));
835    }
836
837    #[test]
838    fn compare_exception_oracle_only() {
839        let mut o = oracle_obs();
840        o.exception = Some(ExceptionInfo {
841            class_name: "Error".to_string(),
842            message_category: "timeout".to_string(),
843            stage: "connect".to_string(),
844            raw_message: "err".to_string(),
845        });
846        let results = compare_exception(&o, &candidate_obs());
847        assert!(!results.is_empty());
848        assert!(results.iter().any(|r| !r.matched));
849    }
850
851    #[test]
852    fn compare_exception_candidate_only() {
853        let mut c = candidate_obs();
854        c.exception = Some(ExceptionInfo {
855            class_name: "Error".to_string(),
856            message_category: "timeout".to_string(),
857            stage: "connect".to_string(),
858            raw_message: "err".to_string(),
859        });
860        let results = compare_exception(&oracle_obs(), &c);
861        assert!(!results.is_empty());
862        assert!(results.iter().any(|r| !r.matched));
863    }
864
865    #[test]
866    fn compare_exception_both_none() {
867        let results = compare_exception(&oracle_obs(), &candidate_obs());
868        assert_eq!(results.len(), 1);
869        assert!(results[0].matched);
870    }
871
872    #[test]
873    fn compare_protocol_wire_match() {
874        let mut o = oracle_obs();
875        o.protocol_observation = Some(ProtocolObservation {
876            protocol: "socks5".to_string(),
877            connection_result: "success".to_string(),
878            bytes_sent: 100,
879            bytes_received: 200,
880            status_code: Some(0),
881        });
882        let mut c = candidate_obs();
883        c.protocol_observation = Some(ProtocolObservation {
884            protocol: "socks5".to_string(),
885            connection_result: "success".to_string(),
886            bytes_sent: 100,
887            bytes_received: 200,
888            status_code: Some(0),
889        });
890        let results = compare_protocol_wire(&o, &c);
891        assert!(results.iter().all(|r| r.matched));
892    }
893
894    #[test]
895    fn compare_protocol_wire_mismatch_bytes() {
896        let mut o = oracle_obs();
897        o.protocol_observation = Some(ProtocolObservation {
898            protocol: "socks5".to_string(),
899            connection_result: "success".to_string(),
900            bytes_sent: 100,
901            bytes_received: 200,
902            status_code: None,
903        });
904        let mut c = candidate_obs();
905        c.protocol_observation = Some(ProtocolObservation {
906            protocol: "socks5".to_string(),
907            connection_result: "success".to_string(),
908            bytes_sent: 100,
909            bytes_received: 300,
910            status_code: None,
911        });
912        let results = compare_protocol_wire(&o, &c);
913        assert!(results.iter().any(|r| !r.matched));
914    }
915
916    #[test]
917    fn compare_protocol_wire_missing_in_candidate() {
918        let mut o = oracle_obs();
919        o.protocol_observation = Some(ProtocolObservation {
920            protocol: "http".to_string(),
921            connection_result: "success".to_string(),
922            bytes_sent: 0,
923            bytes_received: 0,
924            status_code: Some(200),
925        });
926        let results = compare_protocol_wire(&o, &candidate_obs());
927        assert!(!results.is_empty());
928        assert!(results.iter().any(|r| !r.matched));
929    }
930
931    #[test]
932    fn compare_protocol_wire_both_none() {
933        let results = compare_protocol_wire(&oracle_obs(), &candidate_obs());
934        assert_eq!(results.len(), 1);
935        assert!(results[0].matched);
936    }
937
938    #[test]
939    fn compare_cli_flag_ok_match() {
940        let results = compare_cli_flag("--port", &Ok("8080".to_string()), &Ok("8080".to_string()));
941        assert_eq!(results.len(), 1);
942        assert!(results[0].matched);
943    }
944
945    #[test]
946    fn compare_cli_flag_ok_mismatch() {
947        let results = compare_cli_flag("--port", &Ok("8080".to_string()), &Ok("9090".to_string()));
948        assert_eq!(results.len(), 1);
949        assert!(!results[0].matched);
950    }
951
952    #[test]
953    fn compare_cli_flag_err_match() {
954        let results = compare_cli_flag(
955            "--daemon",
956            &Err("unsupported".to_string()),
957            &Err("unsupported".to_string()),
958        );
959        assert_eq!(results.len(), 1);
960        assert!(results[0].matched);
961    }
962
963    #[test]
964    fn compare_cli_flag_structural_mismatch() {
965        let results = compare_cli_flag(
966            "--flag",
967            &Ok("value".to_string()),
968            &Err("rejected".to_string()),
969        );
970        assert_eq!(results.len(), 1);
971        assert!(!results[0].matched);
972        assert_eq!(
973            results[0].mismatch_kind,
974            Some(MismatchKind::StructuralMismatch)
975        );
976    }
977
978    #[test]
979    fn compare_cipher_roundtrip_match() {
980        let input = b"hello world";
981        let encrypted = b"encrypted_bytes";
982        let results = compare_cipher_roundtrip("aes_256_gcm", input, encrypted, encrypted);
983        assert!(results.iter().all(|r| r.matched));
984    }
985
986    #[test]
987    fn compare_cipher_roundtrip_mismatch() {
988        let input = b"hello world";
989        let oracle_enc = b"oracle_encrypted";
990        let candidate_enc = b"cand_encrypted";
991        let results = compare_cipher_roundtrip("aes_256_gcm", input, oracle_enc, candidate_enc);
992        assert!(results.iter().any(|r| !r.matched));
993    }
994
995    #[test]
996    fn compare_process_lifecycle_match() {
997        let o = oracle_obs();
998        let c = candidate_obs();
999        let results = compare_process_lifecycle(&o, &c);
1000        assert!(results.iter().all(|r| r.matched));
1001    }
1002
1003    #[test]
1004    fn compare_process_lifecycle_exit_code_mismatch() {
1005        let mut o = oracle_obs();
1006        o.exit_code = Some(0);
1007        let mut c = candidate_obs();
1008        c.exit_code = Some(1);
1009        let results = compare_process_lifecycle(&o, &c);
1010        assert!(results.iter().any(|r| !r.matched));
1011    }
1012
1013    #[test]
1014    fn compare_process_lifecycle_cleanup_mismatch() {
1015        let mut o = oracle_obs();
1016        o.cleanup = CleanupInfo {
1017            processes_cleaned: true,
1018            sockets_cleaned: true,
1019            files_cleaned: true,
1020            leftover: vec![],
1021        };
1022        let mut c = candidate_obs();
1023        c.cleanup = CleanupInfo {
1024            processes_cleaned: false,
1025            sockets_cleaned: false,
1026            files_cleaned: false,
1027            leftover: vec!["/tmp/stale".to_string()],
1028        };
1029        let results = compare_process_lifecycle(&o, &c);
1030        assert!(results.iter().any(|r| !r.matched));
1031    }
1032
1033    #[test]
1034    fn compare_failure_class_match() {
1035        let mut o = oracle_obs();
1036        o.exception = Some(ExceptionInfo {
1037            class_name: "TimeoutError".to_string(),
1038            message_category: "timeout".to_string(),
1039            stage: "connect".to_string(),
1040            raw_message: "timed out".to_string(),
1041        });
1042        let mut c = candidate_obs();
1043        c.exception = Some(ExceptionInfo {
1044            class_name: "TimeoutError".to_string(),
1045            message_category: "timeout".to_string(),
1046            stage: "connect".to_string(),
1047            raw_message: "timed out".to_string(),
1048        });
1049        let results = compare_failure_class(&o, &c);
1050        assert_eq!(results.len(), 1);
1051        assert!(results[0].matched);
1052    }
1053
1054    #[test]
1055    fn compare_failure_class_mismatch() {
1056        let mut o = oracle_obs();
1057        o.exception = Some(ExceptionInfo {
1058            class_name: "TimeoutError".to_string(),
1059            message_category: "timeout".to_string(),
1060            stage: "connect".to_string(),
1061            raw_message: "timed out".to_string(),
1062        });
1063        let mut c = candidate_obs();
1064        c.exception = Some(ExceptionInfo {
1065            class_name: "ConnectionRefusedError".to_string(),
1066            message_category: "connection_refused".to_string(),
1067            stage: "connect".to_string(),
1068            raw_message: "refused".to_string(),
1069        });
1070        let results = compare_failure_class(&o, &c);
1071        assert_eq!(results.len(), 1);
1072        assert!(!results[0].matched);
1073    }
1074
1075    #[test]
1076    fn compare_failure_class_both_none() {
1077        let results = compare_failure_class(&oracle_obs(), &candidate_obs());
1078        assert_eq!(results.len(), 1);
1079        assert!(results[0].matched);
1080    }
1081
1082    #[test]
1083    fn compare_composition_validity_match() {
1084        let results = compare_composition_validity("socks5->http", true, true);
1085        assert_eq!(results.len(), 1);
1086        assert!(results[0].matched);
1087    }
1088
1089    #[test]
1090    fn compare_composition_validity_mismatch() {
1091        let results = compare_composition_validity("socks5->http", true, false);
1092        assert_eq!(results.len(), 1);
1093        assert!(!results[0].matched);
1094    }
1095
1096    #[test]
1097    fn run_comparator_exact_json() {
1098        let o = oracle_obs();
1099        let c = candidate_obs();
1100        let results = run_comparator("compare_exact_json", &o, &c);
1101        assert_eq!(results.len(), 1);
1102    }
1103
1104    #[test]
1105    fn run_comparator_signature() {
1106        let results = run_comparator("compare_signature", &oracle_obs(), &candidate_obs());
1107        assert_eq!(results.len(), 1);
1108    }
1109
1110    #[test]
1111    fn run_comparator_callable_kind() {
1112        let results = run_comparator("compare_callable_kind", &oracle_obs(), &candidate_obs());
1113        assert_eq!(results.len(), 1);
1114    }
1115
1116    #[test]
1117    fn run_comparator_exception() {
1118        let results = run_comparator("compare_exception", &oracle_obs(), &candidate_obs());
1119        assert_eq!(results.len(), 1);
1120    }
1121
1122    #[test]
1123    fn run_comparator_protocol_wire() {
1124        let results = run_comparator("compare_protocol_wire", &oracle_obs(), &candidate_obs());
1125        assert_eq!(results.len(), 1);
1126    }
1127
1128    #[test]
1129    fn run_comparator_process_lifecycle() {
1130        let results = run_comparator("compare_process_lifecycle", &oracle_obs(), &candidate_obs());
1131        assert_eq!(results.len(), 2);
1132    }
1133
1134    #[test]
1135    fn run_comparator_failure_class() {
1136        let results = run_comparator("compare_failure_class", &oracle_obs(), &candidate_obs());
1137        assert_eq!(results.len(), 1);
1138    }
1139
1140    #[test]
1141    fn run_comparator_unknown() {
1142        let results = run_comparator("bogus_comparator", &oracle_obs(), &candidate_obs());
1143        assert_eq!(results.len(), 1);
1144        assert!(!results[0].matched);
1145        assert_eq!(
1146            results[0].classification,
1147            MismatchClassification::HarnessDefect
1148        );
1149    }
1150
1151    #[test]
1152    fn run_comparator_extra_args_passthrough() {
1153        let results = run_comparator("compare_namespace_set", &oracle_obs(), &candidate_obs());
1154        assert_eq!(results.len(), 1);
1155        assert!(results[0].matched);
1156    }
1157}