1use std::collections::HashSet;
2
3use crate::strict_observations::{
4 ComparisonResult, MismatchClassification, MismatchKind, StrictObservation,
5};
6
7pub 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
30pub 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
78pub 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
163pub 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
202pub 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
272pub 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
399pub 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
439fn 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
480pub 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
529pub 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
561pub 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
587pub 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}