1use std::collections::HashSet;
7use std::fmt;
8use std::fs;
9use std::path::{Path, PathBuf};
10use std::str::FromStr;
11
12use serde::Deserialize;
13use thiserror::Error;
14
15pub const PINNED_PPROXY_VERSION: &str = "2.7.9";
17
18pub const ALLOWED_CATEGORIES: &[&str] = &[
24 "protocol",
25 "udp",
26 "routing",
27 "security",
28 "cli",
29 "uri",
30 "transport",
31 "platform",
32 "system_proxy",
33 "python",
34 "python-api",
35 "packaging",
36 "performance",
37 "inbound_tcp",
38 "upstream_tcp",
39];
40
41#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
47pub struct ManifestMeta {
48 pub pproxy_version: String,
49 pub manifest_version: String,
50}
51
52#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
54pub struct FullManifestEntry {
55 pub id: String,
56 pub category: String,
57 pub pproxy_version: String,
58 pub egress_status: String,
59 pub evidence_level: String,
60 #[serde(default)]
61 pub tests: Vec<String>,
62 #[serde(default)]
63 pub divergence: String,
64 #[serde(default)]
65 pub external_dependency: Option<String>,
66}
67
68#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
70pub struct FullManifest {
71 pub meta: ManifestMeta,
72 pub features: Vec<FullManifestEntry>,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
81pub enum EgressStatus {
82 Compatible,
83 Supported,
84 Partial,
85 IntentionalNonParity,
86 Experimental,
87 Unsupported,
88}
89
90impl FromStr for EgressStatus {
91 type Err = ValidationError;
92
93 fn from_str(s: &str) -> Result<Self, Self::Err> {
94 match s {
95 "compatible" => Ok(Self::Compatible),
96 "supported" => Ok(Self::Supported),
97 "partial" => Ok(Self::Partial),
98 "intentional_non_parity" => Ok(Self::IntentionalNonParity),
99 "experimental" => Ok(Self::Experimental),
100 "unsupported" => Ok(Self::Unsupported),
101 other => Err(ValidationError::InvalidEgressStatus {
102 value: other.to_string(),
103 }),
104 }
105 }
106}
107
108impl fmt::Display for EgressStatus {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 match self {
111 Self::Compatible => write!(f, "compatible"),
112 Self::Supported => write!(f, "supported"),
113 Self::Partial => write!(f, "partial"),
114 Self::IntentionalNonParity => write!(f, "intentional_non_parity"),
115 Self::Experimental => write!(f, "experimental"),
116 Self::Unsupported => write!(f, "unsupported"),
117 }
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
123pub enum EvidenceLevel {
124 Unimplemented,
125 ImplementedSynthetic,
126 ImplementedDifferential,
127 ImplementedInterop,
128 Compatible,
129 IntentionalNonParity,
130}
131
132impl FromStr for EvidenceLevel {
133 type Err = ValidationError;
134
135 fn from_str(s: &str) -> Result<Self, Self::Err> {
136 match s {
137 "unimplemented" => Ok(Self::Unimplemented),
138 "implemented_synthetic" => Ok(Self::ImplementedSynthetic),
139 "implemented_differential" => Ok(Self::ImplementedDifferential),
140 "implemented_interop" => Ok(Self::ImplementedInterop),
141 "compatible" => Ok(Self::Compatible),
142 "intentional_non_parity" => Ok(Self::IntentionalNonParity),
143 other => Err(ValidationError::InvalidEvidenceLevel {
144 value: other.to_string(),
145 }),
146 }
147 }
148}
149
150impl fmt::Display for EvidenceLevel {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 match self {
153 Self::Unimplemented => write!(f, "unimplemented"),
154 Self::ImplementedSynthetic => write!(f, "implemented_synthetic"),
155 Self::ImplementedDifferential => write!(f, "implemented_differential"),
156 Self::ImplementedInterop => write!(f, "implemented_interop"),
157 Self::Compatible => write!(f, "compatible"),
158 Self::IntentionalNonParity => write!(f, "intentional_non_parity"),
159 }
160 }
161}
162
163#[derive(Debug, Clone, Error, PartialEq, Eq)]
169pub enum ValidationError {
170 #[error("TOML parse error: {message}")]
171 TomlParse { message: String },
172
173 #[error("file I/O error: {message}")]
174 Io { message: String },
175
176 #[error("invalid egress_status value: \"{value}\"")]
177 InvalidEgressStatus { value: String },
178
179 #[error("invalid evidence_level value: \"{value}\"")]
180 InvalidEvidenceLevel { value: String },
181
182 #[error(
183 "feature \"{id}\" has egress_status=\"compatible\" but evidence_level=\"{evidence}\" (must be \"compatible\")"
184 )]
185 CompatibleStatusRequiresCompatibleEvidence { id: String, evidence: String },
186
187 #[error(
188 "feature \"{id}\" has evidence_level=\"compatible\" but no test names (at least one required)"
189 )]
190 CompatibleEvidenceRequiresTests { id: String },
191
192 #[error(
193 "feature \"{id}\" has evidence_level=\"implemented_synthetic\" with egress_status=\"compatible\" (not allowed)"
194 )]
195 SyntheticCannotPairWithCompatible { id: String },
196
197 #[error("feature \"{id}\" has egress_status=\"intentional_non_parity\" but empty divergence")]
198 IntentionalNonParityRequiresDivergence { id: String },
199
200 #[error("duplicate feature id: \"{id}\"")]
201 DuplicateFeatureId { id: String },
202
203 #[error(
204 "meta.pproxy_version=\"{actual}\" does not match expected pinned version \"{expected}\""
205 )]
206 PproxyVersionMismatch { actual: String, expected: String },
207
208 #[error(
209 "feature \"{id}\" has evidence_level=\"compatible\" with differential test names but no external_dependency (expected \"pproxy==2.7.9\")"
210 )]
211 CompatibleDifferentialMissingExternalDependency { id: String },
212
213 #[error(
214 "feature \"{id}\" has evidence_level=\"implemented_interop\" but no external_dependency and no divergence explaining the interop suite"
215 )]
216 InteropMissingExternalDependencyOrDivergence { id: String },
217
218 #[error(
219 "feature \"{id}\" has no external_dependency but evidence_level=\"{evidence}\" (expected external_dependency for non-synthetic, non-intentional-non-parity evidence)"
220 )]
221 MissingExternalDependency { id: String, evidence: String },
222
223 #[error("feature \"{id}\" has unknown category \"{category}\" (must be one of: {allowed:?})")]
224 InvalidCategory {
225 id: String,
226 category: String,
227 allowed: Vec<String>,
228 },
229
230 #[error(
231 "feature \"{id}\" has egress_status=\"intentional_non_parity\" but evidence_level=\"{evidence}\" (must be \"intentional_non_parity\" or \"implemented_synthetic\")"
232 )]
233 IntentionalNonParityEvidenceMismatch { id: String, evidence: String },
234
235 #[error(
236 "feature \"{id}\" has egress_status=\"unsupported\" with empty divergence (expected rationale explaining why not implemented)"
237 )]
238 UnsupportedRequiresDivergence { id: String },
239
240 #[error(
241 "feature \"{id}\" has egress_status=\"experimental\" with empty divergence (expected rationale describing the experiment)"
242 )]
243 ExperimentalRequiresDivergence { id: String },
244
245 #[error(
246 "feature \"{id}\" has category=\"platform\" but divergence does not mention a platform constraint (e.g. \"Linux only\", \"Unix only\")"
247 )]
248 PlatformMissingConstraint { id: String },
249
250 #[error(
251 "feature \"{id}\" tests reference a file path (\"{test}\") rather than a test function or group alias"
252 )]
253 TestReferenceIsFilePath { id: String, test: String },
254
255 #[error(
256 "feature \"{id}\" tests reference a CI workflow (\"{test}\") rather than a test function or group alias"
257 )]
258 TestReferenceIsCIWorkflow { id: String, test: String },
259}
260
261#[derive(Debug, Clone, Error, PartialEq, Eq)]
266#[error("{errors:#?}")]
267pub struct ValidationErrors {
268 pub errors: Vec<ValidationError>,
269 pub warnings: Vec<ValidationError>,
270}
271
272impl ValidationErrors {
273 pub fn new() -> Self {
275 Self {
276 errors: Vec::new(),
277 warnings: Vec::new(),
278 }
279 }
280
281 pub fn push(&mut self, err: ValidationError) {
283 self.errors.push(err);
284 }
285
286 pub fn warn(&mut self, warning: ValidationError) {
288 self.warnings.push(warning);
289 }
290
291 pub fn is_empty(&self) -> bool {
293 self.errors.is_empty()
294 }
295
296 pub fn len(&self) -> usize {
298 self.errors.len()
299 }
300}
301
302impl Default for ValidationErrors {
303 fn default() -> Self {
304 Self::new()
305 }
306}
307
308pub fn validate_manifest(manifest: &FullManifest) -> Result<(), ValidationErrors> {
318 let mut errs = ValidationErrors::new();
319
320 if manifest.meta.pproxy_version != PINNED_PPROXY_VERSION {
322 errs.push(ValidationError::PproxyVersionMismatch {
323 actual: manifest.meta.pproxy_version.clone(),
324 expected: PINNED_PPROXY_VERSION.to_string(),
325 });
326 }
327
328 let mut seen_ids = HashSet::new();
330 for feature in &manifest.features {
331 if !seen_ids.insert(feature.id.clone()) {
332 errs.push(ValidationError::DuplicateFeatureId {
333 id: feature.id.clone(),
334 });
335 }
336 }
337
338 for feature in &manifest.features {
340 let status = EgressStatus::from_str(&feature.egress_status);
342 let evidence = EvidenceLevel::from_str(&feature.evidence_level);
343
344 if let Err(ref e) = status {
345 errs.push(e.clone());
346 }
347 if let Err(ref e) = evidence {
348 errs.push(e.clone());
349 }
350
351 let status = match status {
353 Ok(s) => s,
354 Err(_) => continue,
355 };
356 let evidence = match evidence {
357 Ok(e) => e,
358 Err(_) => continue,
359 };
360
361 if status == EgressStatus::Compatible && evidence != EvidenceLevel::Compatible {
363 errs.push(
364 ValidationError::CompatibleStatusRequiresCompatibleEvidence {
365 id: feature.id.clone(),
366 evidence: feature.evidence_level.clone(),
367 },
368 );
369 }
370
371 if evidence == EvidenceLevel::Compatible
373 && feature.tests.iter().all(|t| t.trim().is_empty())
374 {
375 errs.push(ValidationError::CompatibleEvidenceRequiresTests {
376 id: feature.id.clone(),
377 });
378 }
379
380 if evidence == EvidenceLevel::ImplementedSynthetic && status == EgressStatus::Compatible {
382 errs.push(ValidationError::SyntheticCannotPairWithCompatible {
383 id: feature.id.clone(),
384 });
385 }
386
387 if status == EgressStatus::IntentionalNonParity && feature.divergence.trim().is_empty() {
389 errs.push(ValidationError::IntentionalNonParityRequiresDivergence {
390 id: feature.id.clone(),
391 });
392 }
393
394 if evidence == EvidenceLevel::Compatible {
396 let has_differential_test =
397 feature.tests.iter().any(|t| t.starts_with("differential_"));
398 if has_differential_test && feature.external_dependency.is_none() {
399 errs.push(
400 ValidationError::CompatibleDifferentialMissingExternalDependency {
401 id: feature.id.clone(),
402 },
403 );
404 }
405 }
406
407 if evidence == EvidenceLevel::ImplementedInterop
409 && feature.external_dependency.is_none()
410 && feature.divergence.trim().is_empty()
411 {
412 errs.push(
413 ValidationError::InteropMissingExternalDependencyOrDivergence {
414 id: feature.id.clone(),
415 },
416 );
417 }
418
419 if matches!(
422 evidence,
423 EvidenceLevel::Compatible | EvidenceLevel::ImplementedDifferential
424 ) && feature.external_dependency.is_none()
425 && feature.tests.iter().any(|t| t.starts_with("differential_"))
426 {
427 errs.push(ValidationError::MissingExternalDependency {
428 id: feature.id.clone(),
429 evidence: feature.evidence_level.clone(),
430 });
431 }
432
433 if !ALLOWED_CATEGORIES.contains(&feature.category.as_str()) {
435 errs.push(ValidationError::InvalidCategory {
436 id: feature.id.clone(),
437 category: feature.category.clone(),
438 allowed: ALLOWED_CATEGORIES.iter().map(|s| s.to_string()).collect(),
439 });
440 }
441
442 if status == EgressStatus::IntentionalNonParity
445 && !matches!(
446 evidence,
447 EvidenceLevel::IntentionalNonParity | EvidenceLevel::ImplementedSynthetic
448 )
449 {
450 errs.push(ValidationError::IntentionalNonParityEvidenceMismatch {
451 id: feature.id.clone(),
452 evidence: feature.evidence_level.clone(),
453 });
454 }
455
456 if status == EgressStatus::Unsupported && feature.divergence.trim().is_empty() {
458 errs.push(ValidationError::UnsupportedRequiresDivergence {
459 id: feature.id.clone(),
460 });
461 }
462
463 if status == EgressStatus::Experimental && feature.divergence.trim().is_empty() {
465 errs.push(ValidationError::ExperimentalRequiresDivergence {
466 id: feature.id.clone(),
467 });
468 }
469
470 if feature.category == "platform" {
472 let d = feature.divergence.to_lowercase();
473 let has_platform_keyword = [
474 "linux", "macos", "windows", "freebsd", "unix", "solaris", "bsd", "android", "ios",
475 ]
476 .iter()
477 .any(|kw| d.contains(kw));
478 if !has_platform_keyword {
479 errs.push(ValidationError::PlatformMissingConstraint {
480 id: feature.id.clone(),
481 });
482 }
483 }
484
485 for test in &feature.tests {
495 if test.starts_with(".github/workflows/") || test.starts_with(".github\\workflows\\") {
496 errs.push(ValidationError::TestReferenceIsCIWorkflow {
497 id: feature.id.clone(),
498 test: test.clone(),
499 });
500 continue;
501 }
502 if !test.contains("::") {
503 continue;
504 }
505 let (_, needle) = match test.split_once("::") {
507 Some(parts) if !parts.1.is_empty() => parts,
508 _ => continue,
509 };
510 let first_char = needle.chars().next();
511 let looks_like_test_id = match first_char {
512 Some(c) if c.is_ascii_alphabetic() || c == '_' => needle
513 .chars()
514 .all(|c| c.is_ascii_alphanumeric() || c == '_'),
515 _ => false,
516 };
517 if !looks_like_test_id {
518 errs.push(ValidationError::TestReferenceIsFilePath {
519 id: feature.id.clone(),
520 test: test.clone(),
521 });
522 }
523 }
524 }
525
526 if errs.is_empty() {
527 Ok(())
528 } else {
529 Err(errs)
530 }
531}
532
533pub fn validate_manifest_file(path: &Path) -> Result<FullManifest, ValidationErrors> {
535 let content = fs::read_to_string(path).map_err(|e| {
536 let mut errs = ValidationErrors::new();
537 errs.push(ValidationError::Io {
538 message: format!("failed to read {}: {}", path.display(), e),
539 });
540 errs
541 })?;
542
543 let manifest: FullManifest = toml::from_str(&content).map_err(|e| {
544 let mut errs = ValidationErrors::new();
545 errs.push(ValidationError::TomlParse {
546 message: e.to_string(),
547 });
548 errs
549 })?;
550
551 validate_manifest(&manifest)?;
552 Ok(manifest)
553}
554
555pub fn find_manifest_path() -> Option<PathBuf> {
564 if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
566 let candidate =
567 PathBuf::from(&manifest_dir).join("../../tests/compat/pproxy_manifest.toml");
568 if candidate.exists() {
569 return Some(candidate);
570 }
571 }
572
573 let cwd = std::env::current_dir().ok()?;
575 let mut dir = cwd.as_path();
576 loop {
577 let candidate = dir.join("tests/compat/pproxy_manifest.toml");
578 if candidate.exists() {
579 return Some(candidate);
580 }
581 dir = dir.parent()?;
582 }
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588
589 fn make_manifest(meta: ManifestMeta, features: Vec<FullManifestEntry>) -> FullManifest {
590 FullManifest { meta, features }
591 }
592
593 fn default_meta() -> ManifestMeta {
594 ManifestMeta {
595 pproxy_version: PINNED_PPROXY_VERSION.to_string(),
596 manifest_version: "1".to_string(),
597 }
598 }
599
600 fn compatible_feature(id: &str) -> FullManifestEntry {
601 FullManifestEntry {
602 id: id.to_string(),
603 category: "protocol".to_string(),
604 pproxy_version: PINNED_PPROXY_VERSION.to_string(),
605 egress_status: "compatible".to_string(),
606 evidence_level: "compatible".to_string(),
607 tests: vec!["test_a".to_string()],
608 divergence: "some divergence".to_string(),
609 external_dependency: None,
610 }
611 }
612
613 #[test]
614 fn valid_manifest_passes() {
615 let manifest = make_manifest(
616 default_meta(),
617 vec![
618 compatible_feature("feat_a"),
619 FullManifestEntry {
620 id: "feat_b".to_string(),
621 category: "udp".to_string(),
622 pproxy_version: PINNED_PPROXY_VERSION.to_string(),
623 egress_status: "supported".to_string(),
624 evidence_level: "implemented_synthetic".to_string(),
625 tests: vec!["unit_tests".to_string()],
626 divergence: "different entry points".to_string(),
627 external_dependency: None,
628 },
629 ],
630 );
631 let result = validate_manifest(&manifest);
632 assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
633 }
634
635 #[test]
636 fn compatible_status_with_synthetic_evidence_fails() {
637 let manifest = make_manifest(
638 default_meta(),
639 vec![FullManifestEntry {
640 id: "bad_feat".to_string(),
641 category: "protocol".to_string(),
642 pproxy_version: PINNED_PPROXY_VERSION.to_string(),
643 egress_status: "compatible".to_string(),
644 evidence_level: "implemented_synthetic".to_string(),
645 tests: vec!["some_test".to_string()],
646 divergence: "n/a".to_string(),
647 external_dependency: None,
648 }],
649 );
650 let errs = validate_manifest(&manifest).unwrap_err();
651 assert!(
652 errs.errors
653 .iter()
654 .any(|e| matches!(e, ValidationError::CompatibleStatusRequiresCompatibleEvidence { id, .. } if id == "bad_feat")),
655 "expected CompatibleStatusRequiresCompatibleEvidence"
656 );
657 assert!(
658 errs.errors
659 .iter()
660 .any(|e| matches!(e, ValidationError::SyntheticCannotPairWithCompatible { id, .. } if id == "bad_feat")),
661 "expected SyntheticCannotPairWithCompatible"
662 );
663 }
664
665 #[test]
666 fn duplicate_ids_fail() {
667 let manifest = make_manifest(
668 default_meta(),
669 vec![compatible_feature("dup"), compatible_feature("dup")],
670 );
671 let errs = validate_manifest(&manifest).unwrap_err();
672 assert!(
673 errs.errors.iter().any(
674 |e| matches!(e, ValidationError::DuplicateFeatureId { id, .. } if id == "dup")
675 ),
676 "expected DuplicateFeatureId"
677 );
678 }
679
680 #[test]
681 fn compatible_evidence_with_empty_tests_fails() {
682 let manifest = make_manifest(
683 default_meta(),
684 vec![FullManifestEntry {
685 id: "no_tests".to_string(),
686 category: "protocol".to_string(),
687 pproxy_version: PINNED_PPROXY_VERSION.to_string(),
688 egress_status: "compatible".to_string(),
689 evidence_level: "compatible".to_string(),
690 tests: vec![],
691 divergence: "n/a".to_string(),
692 external_dependency: None,
693 }],
694 );
695 let errs = validate_manifest(&manifest).unwrap_err();
696 assert!(
697 errs.errors
698 .iter()
699 .any(|e| matches!(e, ValidationError::CompatibleEvidenceRequiresTests { id, .. } if id == "no_tests")),
700 "expected CompatibleEvidenceRequiresTests"
701 );
702 }
703
704 #[test]
705 fn compatible_evidence_with_whitespace_only_tests_fails() {
706 let manifest = make_manifest(
707 default_meta(),
708 vec![FullManifestEntry {
709 id: "blank_tests".to_string(),
710 category: "protocol".to_string(),
711 pproxy_version: PINNED_PPROXY_VERSION.to_string(),
712 egress_status: "compatible".to_string(),
713 evidence_level: "compatible".to_string(),
714 tests: vec![" ".to_string(), "".to_string()],
715 divergence: "n/a".to_string(),
716 external_dependency: None,
717 }],
718 );
719 let errs = validate_manifest(&manifest).unwrap_err();
720 assert!(
721 errs.errors
722 .iter()
723 .any(|e| matches!(e, ValidationError::CompatibleEvidenceRequiresTests { id, .. } if id == "blank_tests")),
724 "expected CompatibleEvidenceRequiresTests for whitespace-only tests"
725 );
726 }
727
728 #[test]
729 fn intentional_non_parity_without_divergence_fails() {
730 let manifest = make_manifest(
731 default_meta(),
732 vec![FullManifestEntry {
733 id: "no_div".to_string(),
734 category: "cli".to_string(),
735 pproxy_version: PINNED_PPROXY_VERSION.to_string(),
736 egress_status: "intentional_non_parity".to_string(),
737 evidence_level: "intentional_non_parity".to_string(),
738 tests: vec![],
739 divergence: String::new(),
740 external_dependency: None,
741 }],
742 );
743 let errs = validate_manifest(&manifest).unwrap_err();
744 assert!(
745 errs.errors
746 .iter()
747 .any(|e| matches!(e, ValidationError::IntentionalNonParityRequiresDivergence { id, .. } if id == "no_div")),
748 "expected IntentionalNonParityRequiresDivergence"
749 );
750 }
751
752 #[test]
753 fn intentional_non_parity_with_whitespace_divergence_fails() {
754 let manifest = make_manifest(
755 default_meta(),
756 vec![FullManifestEntry {
757 id: "ws_div".to_string(),
758 category: "cli".to_string(),
759 pproxy_version: PINNED_PPROXY_VERSION.to_string(),
760 egress_status: "intentional_non_parity".to_string(),
761 evidence_level: "intentional_non_parity".to_string(),
762 tests: vec![],
763 divergence: " ".to_string(),
764 external_dependency: None,
765 }],
766 );
767 let errs = validate_manifest(&manifest).unwrap_err();
768 assert!(
769 errs.errors
770 .iter()
771 .any(|e| matches!(e, ValidationError::IntentionalNonParityRequiresDivergence { id, .. } if id == "ws_div")),
772 "expected IntentionalNonParityRequiresDivergence for whitespace divergence"
773 );
774 }
775
776 #[test]
777 fn invalid_egress_status_fails() {
778 let manifest = make_manifest(
779 default_meta(),
780 vec![FullManifestEntry {
781 id: "bad_status".to_string(),
782 category: "protocol".to_string(),
783 pproxy_version: PINNED_PPROXY_VERSION.to_string(),
784 egress_status: "bogus".to_string(),
785 evidence_level: "compatible".to_string(),
786 tests: vec!["test".to_string()],
787 divergence: "n/a".to_string(),
788 external_dependency: None,
789 }],
790 );
791 let errs = validate_manifest(&manifest).unwrap_err();
792 assert!(
793 errs.errors
794 .iter()
795 .any(|e| matches!(e, ValidationError::InvalidEgressStatus { value, .. } if value == "bogus")),
796 "expected InvalidEgressStatus"
797 );
798 }
799
800 #[test]
801 fn invalid_evidence_level_fails() {
802 let manifest = make_manifest(
803 default_meta(),
804 vec![FullManifestEntry {
805 id: "bad_evidence".to_string(),
806 category: "protocol".to_string(),
807 pproxy_version: PINNED_PPROXY_VERSION.to_string(),
808 egress_status: "supported".to_string(),
809 evidence_level: "not_real".to_string(),
810 tests: vec!["test".to_string()],
811 divergence: "n/a".to_string(),
812 external_dependency: None,
813 }],
814 );
815 let errs = validate_manifest(&manifest).unwrap_err();
816 assert!(
817 errs.errors
818 .iter()
819 .any(|e| matches!(e, ValidationError::InvalidEvidenceLevel { value, .. } if value == "not_real")),
820 "expected InvalidEvidenceLevel"
821 );
822 }
823
824 #[test]
825 fn pproxy_version_mismatch_fails() {
826 let mut meta = default_meta();
827 meta.pproxy_version = "1.2.3".to_string();
828 let manifest = make_manifest(meta, vec![compatible_feature("f")]);
829 let errs = validate_manifest(&manifest).unwrap_err();
830 assert!(
831 errs.errors
832 .iter()
833 .any(|e| matches!(e, ValidationError::PproxyVersionMismatch { .. })),
834 "expected PproxyVersionMismatch"
835 );
836 }
837
838 #[test]
839 fn compatible_evidence_without_tests_all_variants_fail() {
840 let manifest = make_manifest(
842 default_meta(),
843 vec![FullManifestEntry {
844 id: "mixed_blanks".to_string(),
845 category: "protocol".to_string(),
846 pproxy_version: PINNED_PPROXY_VERSION.to_string(),
847 egress_status: "compatible".to_string(),
848 evidence_level: "compatible".to_string(),
849 tests: vec!["".to_string(), " ".to_string(), "\t".to_string()],
850 divergence: "n/a".to_string(),
851 external_dependency: None,
852 }],
853 );
854 let errs = validate_manifest(&manifest).unwrap_err();
855 assert!(!errs.is_empty());
856 assert!(errs
857 .errors
858 .iter()
859 .any(|e| matches!(e, ValidationError::CompatibleEvidenceRequiresTests { .. })));
860 }
861
862 #[test]
863 fn multiple_errors_collected() {
864 let manifest = make_manifest(
865 ManifestMeta {
866 pproxy_version: "0.0.1".to_string(),
867 manifest_version: "1".to_string(),
868 },
869 vec![
870 FullManifestEntry {
871 id: "dup".to_string(),
872 category: "protocol".to_string(),
873 pproxy_version: PINNED_PPROXY_VERSION.to_string(),
874 egress_status: "bogus".to_string(),
875 evidence_level: "also_bogus".to_string(),
876 tests: vec![],
877 divergence: String::new(),
878 external_dependency: None,
879 },
880 FullManifestEntry {
881 id: "dup".to_string(),
882 category: "protocol".to_string(),
883 pproxy_version: PINNED_PPROXY_VERSION.to_string(),
884 egress_status: "intentional_non_parity".to_string(),
885 evidence_level: "intentional_non_parity".to_string(),
886 tests: vec![],
887 divergence: String::new(),
888 external_dependency: None,
889 },
890 ],
891 );
892 let errs = validate_manifest(&manifest).unwrap_err();
893 assert!(
894 errs.len() >= 4,
895 "expected at least 4 errors, got {}",
896 errs.len()
897 );
898 assert!(errs
899 .errors
900 .iter()
901 .any(|e| matches!(e, ValidationError::PproxyVersionMismatch { .. })));
902 assert!(errs
903 .errors
904 .iter()
905 .any(|e| matches!(e, ValidationError::InvalidEgressStatus { .. })));
906 assert!(errs
907 .errors
908 .iter()
909 .any(|e| matches!(e, ValidationError::InvalidEvidenceLevel { .. })));
910 assert!(errs
911 .errors
912 .iter()
913 .any(|e| matches!(e, ValidationError::DuplicateFeatureId { .. })));
914 assert!(errs.errors.iter().any(|e| matches!(
915 e,
916 ValidationError::IntentionalNonParityRequiresDivergence { .. }
917 )));
918 }
919
920 #[test]
921 fn intentional_non_parity_with_divergence_passes() {
922 let manifest = make_manifest(
923 default_meta(),
924 vec![FullManifestEntry {
925 id: "ok_innp".to_string(),
926 category: "cli".to_string(),
927 pproxy_version: PINNED_PPROXY_VERSION.to_string(),
928 egress_status: "intentional_non_parity".to_string(),
929 evidence_level: "intentional_non_parity".to_string(),
930 tests: vec![],
931 divergence: "Deliberate design choice".to_string(),
932 external_dependency: None,
933 }],
934 );
935 let result = validate_manifest(&manifest);
936 assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
937 }
938
939 #[test]
940 fn supported_with_synthetic_passes() {
941 let manifest = make_manifest(
942 default_meta(),
943 vec![FullManifestEntry {
944 id: "ok_sup".to_string(),
945 category: "protocol".to_string(),
946 pproxy_version: PINNED_PPROXY_VERSION.to_string(),
947 egress_status: "supported".to_string(),
948 evidence_level: "implemented_synthetic".to_string(),
949 tests: vec!["unit_tests".to_string()],
950 divergence: "n/a".to_string(),
951 external_dependency: None,
952 }],
953 );
954 let result = validate_manifest(&manifest);
955 assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
956 }
957
958 #[test]
959 fn toml_parse_error() {
960 let bad_toml = "this is not [valid toml {{{{";
961 let manifest: Result<FullManifest, _> = toml::from_str(bad_toml);
962 assert!(manifest.is_err());
963 }
964
965 #[test]
966 fn from_str_roundtrip_egress_status() {
967 for variant in &[
968 "compatible",
969 "supported",
970 "partial",
971 "intentional_non_parity",
972 "experimental",
973 "unsupported",
974 ] {
975 let status = EgressStatus::from_str(variant).unwrap();
976 assert_eq!(status.to_string(), *variant);
977 }
978 }
979
980 #[test]
981 fn from_str_roundtrip_evidence_level() {
982 for variant in &[
983 "unimplemented",
984 "implemented_synthetic",
985 "implemented_differential",
986 "implemented_interop",
987 "compatible",
988 "intentional_non_parity",
989 ] {
990 let level = EvidenceLevel::from_str(variant).unwrap();
991 assert_eq!(level.to_string(), *variant);
992 }
993 }
994
995 #[test]
996 fn from_str_invalid_egress_status() {
997 let result = EgressStatus::from_str("nope");
998 assert!(result.is_err());
999 match result.unwrap_err() {
1000 ValidationError::InvalidEgressStatus { value } => assert_eq!(value, "nope"),
1001 other => panic!("unexpected error: {:?}", other),
1002 }
1003 }
1004
1005 #[test]
1006 fn from_str_invalid_evidence_level() {
1007 let result = EvidenceLevel::from_str("nope");
1008 assert!(result.is_err());
1009 match result.unwrap_err() {
1010 ValidationError::InvalidEvidenceLevel { value } => assert_eq!(value, "nope"),
1011 other => panic!("unexpected error: {:?}", other),
1012 }
1013 }
1014
1015 #[test]
1016 fn validation_errors_collection() {
1017 let mut errs = ValidationErrors::new();
1018 assert!(errs.is_empty());
1019 assert_eq!(errs.len(), 0);
1020
1021 errs.push(ValidationError::DuplicateFeatureId {
1022 id: "a".to_string(),
1023 });
1024 errs.push(ValidationError::DuplicateFeatureId {
1025 id: "b".to_string(),
1026 });
1027 assert!(!errs.is_empty());
1028 assert_eq!(errs.len(), 2);
1029 }
1030
1031 #[test]
1032 fn validate_manifest_file_missing_path() {
1033 let path = Path::new("/nonexistent/path/manifest.toml");
1034 let result = validate_manifest_file(path);
1035 assert!(result.is_err());
1036 let errs = result.unwrap_err();
1037 assert!(errs
1038 .errors
1039 .iter()
1040 .any(|e| matches!(e, ValidationError::Io { .. })));
1041 }
1042
1043 #[test]
1044 fn compatible_differential_without_external_dependency_fails() {
1045 let manifest = make_manifest(
1046 default_meta(),
1047 vec![FullManifestEntry {
1048 id: "no_dep".to_string(),
1049 category: "protocol".to_string(),
1050 pproxy_version: PINNED_PPROXY_VERSION.to_string(),
1051 egress_status: "compatible".to_string(),
1052 evidence_level: "compatible".to_string(),
1053 tests: vec!["differential_something".to_string()],
1054 divergence: "some divergence".to_string(),
1055 external_dependency: None,
1056 }],
1057 );
1058 let errs = validate_manifest(&manifest).unwrap_err();
1059 assert!(
1060 errs.errors.iter().any(|e| matches!(
1061 e,
1062 ValidationError::CompatibleDifferentialMissingExternalDependency { id, .. }
1063 if id == "no_dep"
1064 )),
1065 "expected CompatibleDifferentialMissingExternalDependency"
1066 );
1067 }
1068
1069 #[test]
1070 fn compatible_differential_with_external_dependency_passes() {
1071 let manifest = make_manifest(
1072 default_meta(),
1073 vec![FullManifestEntry {
1074 id: "has_dep".to_string(),
1075 category: "protocol".to_string(),
1076 pproxy_version: PINNED_PPROXY_VERSION.to_string(),
1077 egress_status: "compatible".to_string(),
1078 evidence_level: "compatible".to_string(),
1079 tests: vec!["differential_something".to_string()],
1080 divergence: "some divergence".to_string(),
1081 external_dependency: Some("pproxy==2.7.9".to_string()),
1082 }],
1083 );
1084 let result = validate_manifest(&manifest);
1085 assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
1086 }
1087
1088 #[test]
1089 fn validate_real_manifest() {
1090 let path = match find_manifest_path() {
1091 Some(p) => p,
1092 None => {
1093 eprintln!("manifest file not found, skipping");
1094 return;
1095 }
1096 };
1097 eprintln!("Validating manifest at: {}", path.display());
1098 match validate_manifest_file(&path) {
1099 Ok(manifest) => {
1100 eprintln!(
1101 "Manifest OK: {} features, meta.pproxy_version={}",
1102 manifest.features.len(),
1103 manifest.meta.pproxy_version
1104 );
1105 }
1106 Err(errs) => {
1107 eprintln!("Manifest validation FAILED with {} errors:", errs.len());
1108 for (i, err) in errs.errors.iter().enumerate() {
1109 eprintln!(" ERROR {}: {}", i + 1, err);
1110 }
1111 for (i, warn) in errs.warnings.iter().enumerate() {
1112 eprintln!(" WARNING {}: {}", i + 1, warn);
1113 }
1114 panic!(
1115 "manifest validation failed with {} errors (see above)",
1116 errs.len()
1117 );
1118 }
1119 }
1120 }
1121
1122 #[test]
1123 fn manifest_test_names_exist() {
1124 const GROUP_ALIASES: &[&str] = &[
1125 "integration_tests",
1126 "unit_tests",
1127 "cli_tests",
1128 "scheduler_runtime_tests",
1129 "udp_tests",
1130 "udp_upstream_tests",
1131 "tls_tests",
1132 "shadowsocks_tcp_tests",
1133 "shadowsocks_udp_tests",
1134 "pproxy_compat_tests",
1135 "pproxy_cli_tests",
1136 "pproxy_redaction_tests",
1137 "wheel_tests",
1138 "reload_tests",
1139 "interoperability_shadowsocks_tcp",
1140 "implicit_in_echo_tests",
1141 "test_pproxy_compat.py",
1142 "test_pproxy_redaction.py",
1143 "test_pproxy_concurrency.py",
1144 "test_server_lifecycle.py",
1145 "test_pproxy_oracle.py",
1146 "deny_audit_gate",
1149 "python_wheel_ci_workflow",
1150 ];
1151
1152 let manifest_path = match find_manifest_path() {
1153 Some(p) => p,
1154 None => {
1155 eprintln!("manifest not found, skipping");
1156 return;
1157 }
1158 };
1159 let manifest = validate_manifest_file(&manifest_path).expect("manifest should be valid");
1160
1161 let workspace_root = manifest_path
1162 .parent()
1163 .and_then(|p| p.parent())
1164 .and_then(|p| p.parent())
1165 .expect("should have workspace root");
1166
1167 let mut source_files = Vec::new();
1168 let crates_dir = workspace_root.join("crates");
1169 if crates_dir.exists() {
1170 for entry in walk_dir_recursive(&crates_dir) {
1171 if entry.extension().is_some_and(|e| e == "rs" || e == "py") {
1172 source_files.push(entry);
1173 }
1174 }
1175 }
1176 let python_dir = workspace_root.join("python");
1177 if python_dir.exists() {
1178 for entry in walk_dir_recursive(&python_dir) {
1179 if entry.extension().is_some_and(|e| e == "py") {
1180 source_files.push(entry);
1181 }
1182 }
1183 }
1184 let workflows_dir = workspace_root.join(".github").join("workflows");
1185 if workflows_dir.exists() {
1186 for entry in walk_dir_recursive(&workflows_dir) {
1187 if entry.extension().is_some_and(|e| e == "yml" || e == "yaml") {
1188 source_files.push(entry);
1189 }
1190 }
1191 }
1192
1193 let mut missing = Vec::new();
1194 for feature in &manifest.features {
1195 for test_name in &feature.tests {
1196 if GROUP_ALIASES.contains(&test_name.as_str()) {
1197 continue;
1198 }
1199 let (file_filter, needle) = match test_name.split_once("::") {
1205 Some((file_part, fn_part)) if !fn_part.is_empty() => {
1206 (Some(file_part.to_string()), fn_part.to_string())
1207 }
1208 _ => (None, test_name.clone()),
1209 };
1210 let file_filter_stem = file_filter.as_deref().map(|p| {
1211 std::path::Path::new(p)
1212 .file_stem()
1213 .map(|s| s.to_string_lossy().to_string())
1214 .unwrap_or_else(|| p.to_string())
1215 });
1216 let found = source_files.iter().any(|path| {
1217 if let Some(ref stem) = file_filter_stem {
1218 let path_stem = path.file_stem().map(|s| s.to_string_lossy().to_string());
1220 if path_stem.as_deref() != Some(stem.as_str()) {
1221 return false;
1222 }
1223 return std::fs::read_to_string(path)
1225 .map(|content| content.contains(needle.as_str()))
1226 .unwrap_or(false);
1227 }
1228 let path_str = path.to_string_lossy().replace('\\', "/");
1230 let path_stem = path
1231 .file_stem()
1232 .map(|s| s.to_string_lossy().to_string())
1233 .unwrap_or_default();
1234 if path_stem == needle || path_str.contains(needle.as_str()) {
1235 return true;
1236 }
1237 std::fs::read_to_string(path)
1238 .map(|content| content.contains(needle.as_str()))
1239 .unwrap_or(false)
1240 });
1241 if !found {
1242 missing.push((feature.id.clone(), test_name.clone()));
1243 }
1244 }
1245 }
1246
1247 if !missing.is_empty() {
1248 eprintln!("Manifest references test names not found in codebase:");
1249 for (feat, test) in &missing {
1250 eprintln!(" feature \"{}\" references \"{}\"", feat, test);
1251 }
1252 panic!(
1253 "{} manifest test name(s) not found in codebase",
1254 missing.len()
1255 );
1256 }
1257 }
1258
1259 fn walk_dir_recursive(dir: &std::path::Path) -> Vec<PathBuf> {
1260 let mut results = Vec::new();
1261 if let Ok(entries) = std::fs::read_dir(dir) {
1262 for entry in entries.flatten() {
1263 let path = entry.path();
1264 if path.is_dir() {
1265 results.extend(walk_dir_recursive(&path));
1266 } else {
1267 results.push(path);
1268 }
1269 }
1270 }
1271 results
1272 }
1273
1274 #[test]
1275 fn compatibility_evidence_doc_matches_manifest() {
1276 let manifest_path = match find_manifest_path() {
1277 Some(p) => p,
1278 None => {
1279 eprintln!("manifest not found, skipping");
1280 return;
1281 }
1282 };
1283 let manifest = validate_manifest_file(&manifest_path).expect("manifest should be valid");
1284
1285 let workspace_root = manifest_path
1286 .parent()
1287 .and_then(|p| p.parent())
1288 .and_then(|p| p.parent())
1289 .expect("should have workspace root");
1290
1291 let evidence_doc_path = workspace_root.join("docs/COMPATIBILITY_EVIDENCE.md");
1292 if !evidence_doc_path.exists() {
1293 eprintln!("COMPATIBILITY_EVIDENCE.md not found, skipping");
1294 return;
1295 }
1296 let evidence_doc =
1297 fs::read_to_string(&evidence_doc_path).expect("should be able to read evidence doc");
1298
1299 let compatible_features: Vec<&FullManifestEntry> = manifest
1302 .features
1303 .iter()
1304 .filter(|f| f.egress_status == "compatible")
1305 .collect();
1306
1307 let mut missing = Vec::new();
1308 for feature in &compatible_features {
1309 if !evidence_doc.contains(&format!("`{}`", feature.id)) {
1312 missing.push(feature.id.clone());
1313 }
1314 }
1315
1316 if !missing.is_empty() {
1317 eprintln!(
1318 "The following compatible features are not listed in docs/COMPATIBILITY_EVIDENCE.md:"
1319 );
1320 for id in &missing {
1321 eprintln!(" - {}", id);
1322 }
1323 panic!(
1324 "{} compatible feature(s) missing from COMPATIBILITY_EVIDENCE.md",
1325 missing.len()
1326 );
1327 }
1328 }
1329
1330 #[test]
1331 fn readme_pproxy_compatible_claims_match_manifest() {
1332 let manifest_path = match find_manifest_path() {
1333 Some(p) => p,
1334 None => {
1335 eprintln!("manifest not found, skipping");
1336 return;
1337 }
1338 };
1339 let manifest = validate_manifest_file(&manifest_path).expect("manifest should be valid");
1340
1341 let workspace_root = manifest_path
1342 .parent()
1343 .and_then(|p| p.parent())
1344 .and_then(|p| p.parent())
1345 .expect("should have workspace root");
1346
1347 let readme_path = workspace_root.join("README.md");
1348 if !readme_path.exists() {
1349 eprintln!("README.md not found, skipping");
1350 return;
1351 }
1352 let readme = fs::read_to_string(&readme_path).expect("should be able to read README.md");
1353
1354 let manifest_compatible: HashSet<String> = manifest
1356 .features
1357 .iter()
1358 .filter(|f| f.egress_status == "compatible")
1359 .map(|f| f.id.clone())
1360 .collect();
1361
1362 let mut overclaims = Vec::new();
1364 for line in readme.lines() {
1365 if line.contains("pproxy-compatible") || line.contains("pproxy compatible") {
1366 let mut remaining = line;
1368 while let Some(start) = remaining.find('`') {
1369 let rest = &remaining[start + 1..];
1370 if let Some(end) = rest.find('`') {
1371 let candidate = &rest[..end];
1372 if candidate.contains('_')
1373 && !candidate.starts_with("EGRESS_REQUIRE")
1374 && !candidate.starts_with("cargo")
1375 && candidate.len() > 3
1376 && candidate.len() < 80
1377 && !manifest_compatible.contains(candidate)
1378 && manifest.features.iter().any(|f| f.id == *candidate)
1379 {
1380 overclaims.push(candidate.to_string());
1381 }
1382 remaining = &rest[end + 1..];
1383 } else {
1384 break;
1385 }
1386 }
1387 }
1388 }
1389
1390 if !overclaims.is_empty() {
1391 eprintln!("README claims pproxy-compatible for features not marked compatible in the manifest:");
1392 for id in &overclaims {
1393 let entry = manifest.features.iter().find(|f| f.id == *id).unwrap();
1394 eprintln!(
1395 " - `{}`: manifest egress_status=\"{}\"",
1396 id, entry.egress_status
1397 );
1398 }
1399 panic!(
1400 "{} feature(s) overclaimed as pproxy-compatible in README",
1401 overclaims.len()
1402 );
1403 }
1404 }
1405
1406 #[test]
1407 fn parity_matrix_compatible_claims_match_manifest() {
1408 let manifest_path = match find_manifest_path() {
1409 Some(p) => p,
1410 None => {
1411 eprintln!("manifest not found, skipping");
1412 return;
1413 }
1414 };
1415 let manifest = validate_manifest_file(&manifest_path).expect("manifest should be valid");
1416
1417 let workspace_root = manifest_path
1418 .parent()
1419 .and_then(|p| p.parent())
1420 .and_then(|p| p.parent())
1421 .expect("should have workspace root");
1422
1423 let matrix_path = workspace_root.join("docs/PARITY_MATRIX.md");
1424 if !matrix_path.exists() {
1425 eprintln!("PARITY_MATRIX.md not found, skipping");
1426 return;
1427 }
1428 let matrix =
1429 fs::read_to_string(&matrix_path).expect("should be able to read PARITY_MATRIX.md");
1430
1431 let manifest_compatible: HashSet<String> = manifest
1433 .features
1434 .iter()
1435 .filter(|f| f.egress_status == "compatible")
1436 .map(|f| f.id.clone())
1437 .collect();
1438
1439 let mut overclaims = Vec::new();
1442 for line in matrix.lines() {
1443 if line.contains("| Compatible |") || line.contains("|Compatible|") {
1444 let mut remaining = line;
1445 while let Some(start) = remaining.find('`') {
1446 let rest = &remaining[start + 1..];
1447 if let Some(end) = rest.find('`') {
1448 let candidate = &rest[..end];
1449 if candidate.contains('_')
1450 && !candidate.starts_with("EGRESS_REQUIRE")
1451 && candidate.len() > 3
1452 && candidate.len() < 80
1453 && !manifest_compatible.contains(candidate)
1454 && manifest.features.iter().any(|f| f.id == *candidate)
1455 {
1456 overclaims.push(candidate.to_string());
1457 }
1458 remaining = &rest[end + 1..];
1459 } else {
1460 break;
1461 }
1462 }
1463 }
1464 }
1465
1466 if !overclaims.is_empty() {
1467 eprintln!("PARITY_MATRIX.md claims Compatible for features not marked compatible in the manifest:");
1468 for id in &overclaims {
1469 let entry = manifest.features.iter().find(|f| f.id == *id).unwrap();
1470 eprintln!(
1471 " - `{}`: manifest egress_status=\"{}\"",
1472 id, entry.egress_status
1473 );
1474 }
1475 panic!(
1476 "{} feature(s) overclaimed as Compatible in PARITY_MATRIX.md",
1477 overclaims.len()
1478 );
1479 }
1480 }
1481}