diff options
| author | Valentin Popov <valentin@popov.link> | 2026-06-22 16:29:33 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-06-22 16:29:33 +0300 |
| commit | 162de8ccabdd3ccf55e1da28532cad6e8345093d (patch) | |
| tree | d7f33966ddb1dd4fe9e82caea62bc99aeb269535 | |
| parent | 0b23cf48e7aba160b2786d8359e8cfb4ca13da07 (diff) | |
| download | fparkan-162de8ccabdd3ccf55e1da28532cad6e8345093d.tar.xz fparkan-162de8ccabdd3ccf55e1da28532cad6e8345093d.zip | |
fix: require manifests for licensed gates
| -rw-r--r-- | README.md | 21 | ||||
| -rw-r--r-- | apps/fparkan-game/src/main.rs | 27 | ||||
| -rw-r--r-- | crates/fparkan-assets/src/lib.rs | 18 | ||||
| -rw-r--r-- | crates/fparkan-corpus/src/lib.rs | 30 | ||||
| -rw-r--r-- | crates/fparkan-fx/src/lib.rs | 29 | ||||
| -rw-r--r-- | crates/fparkan-material/src/lib.rs | 25 | ||||
| -rw-r--r-- | crates/fparkan-mission-format/src/lib.rs | 25 | ||||
| -rw-r--r-- | crates/fparkan-msh/src/lib.rs | 29 | ||||
| -rw-r--r-- | crates/fparkan-nres/src/lib.rs | 12 | ||||
| -rw-r--r-- | crates/fparkan-prototype/src/lib.rs | 31 | ||||
| -rw-r--r-- | crates/fparkan-resource/src/lib.rs | 14 | ||||
| -rw-r--r-- | crates/fparkan-rsli/src/lib.rs | 12 | ||||
| -rw-r--r-- | crates/fparkan-runtime/src/lib.rs | 46 | ||||
| -rw-r--r-- | crates/fparkan-terrain-format/src/lib.rs | 33 | ||||
| -rw-r--r-- | crates/fparkan-terrain/src/lib.rs | 29 | ||||
| -rw-r--r-- | crates/fparkan-texm/src/lib.rs | 25 | ||||
| -rw-r--r-- | xtask/src/main.rs | 254 |
17 files changed, 493 insertions, 167 deletions
@@ -40,10 +40,27 @@ cargo xtask ci - разместите игровые каталоги в [`testdata/`](testdata); - игровые ресурсы в репозиторий не включаются, так как защищены авторским правом. -Локальный licensed gate: +Локальный licensed gate использует некоммитимый manifest: ```bash -cargo xtask acceptance report --suite licensed --stage 5 --root testdata +cat > /private/tmp/fparkan-corpora.toml <<'EOF' +schema = 1 + +[[corpus]] +id = "part1-local" +kind = "part1" +root = "/absolute/path/to/IS" +expected_profile = "parkan-is-part1" + +[[corpus]] +id = "part2-local" +kind = "part2" +root = "/absolute/path/to/IS2" +expected_profile = "parkan-is-part2" +EOF + +FPARKAN_CORPORA_MANIFEST=/private/tmp/fparkan-corpora.toml \ + cargo xtask acceptance report --suite licensed --stage 5 ``` ## Contributing & Support diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs index 2486cfc..05a1e0a 100644 --- a/apps/fparkan-game/src/main.rs +++ b/apps/fparkan-game/src/main.rs @@ -268,16 +268,16 @@ mod tests { RenderCase { root: "IS", mission: "MISSIONS/CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma", - expected: "{\"mission\":\"MISSIONS/CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma\",\"objects\":33,\"frames\":1,\"tick\":1,\"draws\":33,\"captures\":1,\"last_capture_bytes\":810,\"hash\":\"8584c4307bc911fc82bf909018662f392f3982bf909018666298bde408fe4242\"}", + expected: "{\"mission\":\"MISSIONS/CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma\",\"objects\":33,\"frames\":1,\"tick\":1,\"draws\":33,\"captures\":1,\"last_capture_bytes\":810,\"hash\":\"ca17cc76e55c45e83c1c9c1c088e84bf1a698be91a7730943210fe27596af841\"}", }, RenderCase { root: "IS2", mission: "MISSIONS/Campaign/CAMPAIGN.00/Mission.02/data.tma", - expected: "{\"mission\":\"MISSIONS/Campaign/CAMPAIGN.00/Mission.02/data.tma\",\"objects\":10,\"frames\":1,\"tick\":1,\"draws\":10,\"captures\":1,\"last_capture_bytes\":235,\"hash\":\"c52267cb14f699cb73b958e46c99c23ec23e73b958e46c99b3650afbcce56291\"}", + expected: "{\"mission\":\"MISSIONS/Campaign/CAMPAIGN.00/Mission.02/data.tma\",\"objects\":10,\"frames\":1,\"tick\":1,\"draws\":10,\"captures\":1,\"last_capture_bytes\":235,\"hash\":\"5d720b3ab690076a398a79a404850bbeaee2e33811b5bb570ec8a96d4a7a2fc4\"}", }, ] { assert_eq!( - run(&render_args(&workspace_root().join("testdata").join(case.root), case.mission)), + run(&render_args(&licensed_root(case.root), case.mission)), Ok(case.expected.to_string()) ); } @@ -313,11 +313,20 @@ mod tests { ] } - fn workspace_root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .and_then(Path::parent) - .expect("workspace root") - .to_path_buf() + fn licensed_root(name: &str) -> PathBuf { + let variable = match name { + "IS" => "FPARKAN_CORPUS_PART1_ROOT", + "IS2" => "FPARKAN_CORPUS_PART2_ROOT", + _ => panic!("unknown licensed corpus part: {name}"), + }; + let root = std::env::var_os(variable) + .map(PathBuf::from) + .unwrap_or_else(|| panic!("{variable} is required for licensed corpus tests")); + assert!( + root.is_dir(), + "licensed corpus root is missing: {}", + root.display() + ); + root } } diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs index 9015c2c..2da6624 100644 --- a/crates/fparkan-assets/src/lib.rs +++ b/crates/fparkan-assets/src/lib.rs @@ -543,10 +543,20 @@ mod tests { } fn fixture_root(part: &str) -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("testdata") - .join(part) + let variable = match part { + "IS" => "FPARKAN_CORPUS_PART1_ROOT", + "IS2" => "FPARKAN_CORPUS_PART2_ROOT", + _ => panic!("unknown licensed corpus part: {part}"), + }; + let root = std::env::var_os(variable) + .map(PathBuf::from) + .unwrap_or_else(|| panic!("{variable} is required for licensed corpus tests")); + assert!( + root.is_dir(), + "licensed corpus root is missing: {}", + root.display() + ); + root } fn repository_with_archives( diff --git a/crates/fparkan-corpus/src/lib.rs b/crates/fparkan-corpus/src/lib.rs index 7ada2fe..460bbbf 100644 --- a/crates/fparkan-corpus/src/lib.rs +++ b/crates/fparkan-corpus/src/lib.rs @@ -511,13 +511,7 @@ mod tests { #[test] #[ignore = "requires licensed corpus"] fn report_for_testdata_roots() { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("testdata") - .join("IS"); - if !root.is_dir() { - return; - } + let root = licensed_root("IS"); let manifest = discover(&root, DiscoverOptions::default()).expect("manifest"); let report = report(&root, &manifest).expect("report"); assert!(report.files > 0); @@ -892,10 +886,24 @@ mod tests { } fn testdata_root(part: &str) -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("testdata") - .join(part) + licensed_root(part) + } + + fn licensed_root(part: &str) -> PathBuf { + let variable = match part { + "IS" => "FPARKAN_CORPUS_PART1_ROOT", + "IS2" => "FPARKAN_CORPUS_PART2_ROOT", + _ => panic!("unknown licensed corpus part: {part}"), + }; + let root = std::env::var_os(variable) + .map(PathBuf::from) + .unwrap_or_else(|| panic!("{variable} is required for licensed corpus tests")); + assert!( + root.is_dir(), + "licensed corpus root is missing: {}", + root.display() + ); + root } fn assert_discovered_paths_stay_under_root(part: &str) { diff --git a/crates/fparkan-fx/src/lib.rs b/crates/fparkan-fx/src/lib.rs index b1b5071..3fa4aae 100644 --- a/crates/fparkan-fx/src/lib.rs +++ b/crates/fparkan-fx/src/lib.rs @@ -846,9 +846,7 @@ mod tests { #[ignore = "requires licensed corpus"] fn licensed_corpus_fxid_exact_eof_and_distribution() { for (corpus, expected_count) in [("IS", 923_usize), ("IS2", 1065_usize)] { - let Some(root) = corpus_root(corpus) else { - continue; - }; + let root = corpus_root(corpus); let mut count = 0usize; let mut opcodes = BTreeMap::<FxOpcode, usize>::new(); let mut time_modes = BTreeMap::<u32, usize>::new(); @@ -898,9 +896,7 @@ mod tests { ("IS", 923_usize, 467_usize, 10_553_431_922_547_057_702_u64), ("IS2", 1065_usize, 532_usize, 9_217_284_592_334_143_531_u64), ] { - let Some(root) = corpus_root(corpus) else { - continue; - }; + let root = corpus_root(corpus); let mut count = 0usize; let mut emitting = 0usize; let mut hash = FNV_OFFSET; @@ -992,12 +988,21 @@ mod tests { dst[..len].copy_from_slice(&src[..len]); } - fn corpus_root(name: &str) -> Option<PathBuf> { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("testdata") - .join(name); - root.is_dir().then_some(root) + fn corpus_root(name: &str) -> PathBuf { + let variable = match name { + "IS" => "FPARKAN_CORPUS_PART1_ROOT", + "IS2" => "FPARKAN_CORPUS_PART2_ROOT", + _ => panic!("unknown licensed corpus part: {name}"), + }; + let root = std::env::var_os(variable) + .map(PathBuf::from) + .unwrap_or_else(|| panic!("{variable} is required for licensed corpus tests")); + assert!( + root.is_dir(), + "licensed corpus root is missing: {}", + root.display() + ); + root } fn files_under(root: &Path) -> Vec<PathBuf> { diff --git a/crates/fparkan-material/src/lib.rs b/crates/fparkan-material/src/lib.rs index 2a05f87..32d48a1 100644 --- a/crates/fparkan-material/src/lib.rs +++ b/crates/fparkan-material/src/lib.rs @@ -1129,9 +1129,7 @@ mod tests { ("IS", 905_usize, 439_usize, 95_usize), ("IS2", 1127_usize, 515_usize, 95_usize), ] { - let Some(root) = corpus_root(corpus) else { - continue; - }; + let root = corpus_root(corpus); let mut mat0_count = 0usize; let mut archive_wear_count = 0usize; let mut standalone_wear_count = 0usize; @@ -1185,12 +1183,21 @@ mod tests { } } - fn corpus_root(name: &str) -> Option<PathBuf> { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("testdata") - .join(name); - root.is_dir().then_some(root) + fn corpus_root(name: &str) -> PathBuf { + let variable = match name { + "IS" => "FPARKAN_CORPUS_PART1_ROOT", + "IS2" => "FPARKAN_CORPUS_PART2_ROOT", + _ => panic!("unknown licensed corpus part: {name}"), + }; + let root = std::env::var_os(variable) + .map(PathBuf::from) + .unwrap_or_else(|| panic!("{variable} is required for licensed corpus tests")); + assert!( + root.is_dir(), + "licensed corpus root is missing: {}", + root.display() + ); + root } fn files_under(root: &Path) -> Vec<PathBuf> { diff --git a/crates/fparkan-mission-format/src/lib.rs b/crates/fparkan-mission-format/src/lib.rs index 0c85c39..e796d61 100644 --- a/crates/fparkan-mission-format/src/lib.rs +++ b/crates/fparkan-mission-format/src/lib.rs @@ -992,9 +992,7 @@ mod tests { ("IS", 29_usize, 34_usize, 101_usize, 864_usize, 28_usize), ("IS2", 31_usize, 61_usize, 91_usize, 885_usize, 41_usize), ] { - let Some(root) = corpus_root(corpus) else { - continue; - }; + let root = corpus_root(corpus); let mut files = 0usize; let mut paths = 0usize; let mut clans = 0usize; @@ -1143,12 +1141,21 @@ mod tests { } } - fn corpus_root(name: &str) -> Option<PathBuf> { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("testdata") - .join(name); - root.is_dir().then_some(root) + fn corpus_root(name: &str) -> PathBuf { + let variable = match name { + "IS" => "FPARKAN_CORPUS_PART1_ROOT", + "IS2" => "FPARKAN_CORPUS_PART2_ROOT", + _ => panic!("unknown licensed corpus part: {name}"), + }; + let root = std::env::var_os(variable) + .map(PathBuf::from) + .unwrap_or_else(|| panic!("{variable} is required for licensed corpus tests")); + assert!( + root.is_dir(), + "licensed corpus root is missing: {}", + root.display() + ); + root } fn files_under(root: &Path) -> Vec<PathBuf> { diff --git a/crates/fparkan-msh/src/lib.rs b/crates/fparkan-msh/src/lib.rs index 3ec3def..5a54a59 100644 --- a/crates/fparkan-msh/src/lib.rs +++ b/crates/fparkan-msh/src/lib.rs @@ -1239,9 +1239,7 @@ mod tests { #[ignore = "requires licensed corpus"] fn licensed_corpus_msh_assets_validate() { for (corpus, expected) in [("IS", 435_usize), ("IS2", 511_usize)] { - let Some(root) = corpus_root(corpus) else { - continue; - }; + let root = corpus_root(corpus); let mut count = 0usize; for path in files_under(&root) { let Ok(bytes) = std::fs::read(&path) else { @@ -1304,9 +1302,7 @@ mod tests { 13_040_438_305_408_523_893_u64, ), ] { - let Some(root) = corpus_root(corpus) else { - continue; - }; + let root = corpus_root(corpus); let mut models = 0usize; let mut animated_models = 0usize; let mut node_samples = 0usize; @@ -1725,12 +1721,21 @@ mod tests { name.len() >= 4 && name[name.len() - 4..].eq_ignore_ascii_case(b".msh") } - fn corpus_root(name: &str) -> Option<PathBuf> { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("testdata") - .join(name); - root.is_dir().then_some(root) + fn corpus_root(name: &str) -> PathBuf { + let variable = match name { + "IS" => "FPARKAN_CORPUS_PART1_ROOT", + "IS2" => "FPARKAN_CORPUS_PART2_ROOT", + _ => panic!("unknown licensed corpus part: {name}"), + }; + let root = std::env::var_os(variable) + .map(PathBuf::from) + .unwrap_or_else(|| panic!("{variable} is required for licensed corpus tests")); + assert!( + root.is_dir(), + "licensed corpus root is missing: {}", + root.display() + ); + root } fn files_under(root: &Path) -> Vec<PathBuf> { diff --git a/crates/fparkan-nres/src/lib.rs b/crates/fparkan-nres/src/lib.rs index 3f8d27d..5607c7a 100644 --- a/crates/fparkan-nres/src/lib.rs +++ b/crates/fparkan-nres/src/lib.rs @@ -1948,10 +1948,14 @@ mod tests { expected_files: usize, expected_entries: usize, ) -> Result<CorpusGateResult, String> { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("testdata") - .join(name); + let variable = match name { + "IS" => "FPARKAN_CORPUS_PART1_ROOT", + "IS2" => "FPARKAN_CORPUS_PART2_ROOT", + _ => return Err(format!("unknown licensed corpus part: {name}")), + }; + let root = std::env::var_os(variable) + .map(PathBuf::from) + .ok_or_else(|| format!("{variable} is required for licensed corpus tests"))?; if !root.is_dir() { return Err(format!( "licensed corpus root is missing: {}", diff --git a/crates/fparkan-prototype/src/lib.rs b/crates/fparkan-prototype/src/lib.rs index 35089b0..32e736b 100644 --- a/crates/fparkan-prototype/src/lib.rs +++ b/crates/fparkan-prototype/src/lib.rs @@ -1768,8 +1768,9 @@ mod tests { } #[test] + #[ignore = "requires licensed corpus"] fn resolves_known_part1_registry_cases() { - let root = corpus_root("IS").expect("part 1 root"); + let root = corpus_root("IS"); let vfs = Arc::new(DirectoryVfs::new(&root)); let repo = CachedResourceRepository::new(vfs.clone()); let cases = [ @@ -1799,9 +1800,10 @@ mod tests { } #[test] + #[ignore = "requires licensed corpus"] fn resolves_some_registry_entries_in_both_corpora() { for corpus in ["IS", "IS2"] { - let root = corpus_root(corpus).expect("corpus root"); + let root = corpus_root(corpus); let objects = std::fs::read(root.join("objects.rlb")).expect("objects.rlb"); let document = fparkan_nres::decode( Arc::from(objects.into_boxed_slice()), @@ -1830,7 +1832,7 @@ mod tests { fn licensed_corpora_unit_dat_parse_counts() { let cases = [("IS", 425, 5_219), ("IS2", 676, 8_145)]; for (corpus, expected_files, expected_records) in cases { - let root = corpus_root(corpus).expect("corpus root"); + let root = corpus_root(corpus); let mut dat_paths = Vec::new(); collect_unit_dat_files(&root, &mut dat_paths); dat_paths.sort(); @@ -1863,7 +1865,7 @@ mod tests { #[ignore = "requires licensed corpus"] fn licensed_corpora_registry_payloads_are_record_aligned() { for corpus in ["IS", "IS2"] { - let root = corpus_root(corpus).expect("corpus root"); + let root = corpus_root(corpus); let objects = std::fs::read(root.join("objects.rlb")).expect("objects.rlb"); let document = fparkan_nres::decode( Arc::from(objects.into_boxed_slice()), @@ -1909,12 +1911,21 @@ mod tests { } } - fn corpus_root(name: &str) -> Option<std::path::PathBuf> { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("testdata") - .join(name); - root.is_dir().then_some(root) + fn corpus_root(name: &str) -> std::path::PathBuf { + let variable = match name { + "IS" => "FPARKAN_CORPUS_PART1_ROOT", + "IS2" => "FPARKAN_CORPUS_PART2_ROOT", + _ => panic!("unknown licensed corpus part: {name}"), + }; + let root = std::env::var_os(variable) + .map(std::path::PathBuf::from) + .unwrap_or_else(|| panic!("{variable} is required for licensed corpus tests")); + assert!( + root.is_dir(), + "licensed corpus root is missing: {}", + root.display() + ); + root } fn generated_acyclic_graph( diff --git a/crates/fparkan-resource/src/lib.rs b/crates/fparkan-resource/src/lib.rs index b84f6f9..70916a5 100644 --- a/crates/fparkan-resource/src/lib.rs +++ b/crates/fparkan-resource/src/lib.rs @@ -666,7 +666,7 @@ fn c_name_bytes(raw: &[u8; 12]) -> &[u8] { mod tests { use super::*; use fparkan_vfs::{DirectoryVfs, MemoryVfs}; - use std::path::Path; + use std::path::PathBuf; #[test] fn cached_repository_reads_synthetic_nres() { @@ -937,10 +937,14 @@ mod tests { } fn licensed_repository_gate(corpus: &str) -> Result<(), String> { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("testdata") - .join(corpus); + let variable = match corpus { + "IS" => "FPARKAN_CORPUS_PART1_ROOT", + "IS2" => "FPARKAN_CORPUS_PART2_ROOT", + _ => return Err(format!("unknown licensed corpus part: {corpus}")), + }; + let root = std::env::var_os(variable) + .map(PathBuf::from) + .ok_or_else(|| format!("{variable} is required for licensed corpus tests"))?; if !root.is_dir() { return Err(format!( "licensed corpus root is missing: {}", diff --git a/crates/fparkan-rsli/src/lib.rs b/crates/fparkan-rsli/src/lib.rs index b147a9b..e9237ff 100644 --- a/crates/fparkan-rsli/src/lib.rs +++ b/crates/fparkan-rsli/src/lib.rs @@ -2066,10 +2066,14 @@ mod tests { } fn corpus_files(name: &str) -> Result<Vec<PathBuf>, String> { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("testdata") - .join(name); + let variable = match name { + "IS" => "FPARKAN_CORPUS_PART1_ROOT", + "IS2" => "FPARKAN_CORPUS_PART2_ROOT", + _ => return Err(format!("unknown licensed corpus part: {name}")), + }; + let root = std::env::var_os(variable) + .map(PathBuf::from) + .ok_or_else(|| format!("{variable} is required for licensed corpus tests"))?; if !root.is_dir() { return Err(format!( "licensed corpus root is missing: {}", diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs index 4bc9e25..7cfb541 100644 --- a/crates/fparkan-runtime/src/lib.rs +++ b/crates/fparkan-runtime/src/lib.rs @@ -697,7 +697,7 @@ mod tests { #[test] #[ignore = "requires licensed corpus"] fn load_trace_records_preparation_before_registration_and_raw_transforms() { - let root = workspace_root().join("testdata").join("IS"); + let root = licensed_root("IS"); let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(&root)); let mut engine = create( EngineConfig { @@ -739,7 +739,7 @@ mod tests { #[test] #[ignore = "requires licensed corpus"] fn missing_map_and_missing_reachable_resource_fail_before_registration() { - let root = workspace_root().join("testdata").join("IS"); + let root = licensed_root("IS"); for (denied, mission) in [ ( DenyRule::Suffix("Land.map"), @@ -783,7 +783,7 @@ mod tests { #[test] #[ignore = "requires licensed corpus"] fn registration_phase_failure_uses_normal_teardown_and_keeps_engine_world() { - let root = workspace_root().join("testdata").join("IS"); + let root = licensed_root("IS"); let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root)); let mut engine = create( EngineConfig { @@ -827,9 +827,9 @@ mod tests { mission: "MISSIONS/CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma", object_count: 33, expected_hash: [ - 0x19, 0xdc, 0xd3, 0x9b, 0x35, 0xad, 0x90, 0x6c, 0x92, 0x2d, 0x83, 0x7b, 0x7a, - 0xb3, 0xa6, 0x15, 0xa6, 0x15, 0x92, 0x2d, 0x83, 0x7b, 0x7a, 0xb3, 0xe9, 0xcd, - 0x9a, 0x56, 0x48, 0xb6, 0x0c, 0xee, + 0xc7, 0xb0, 0x6e, 0x0a, 0x31, 0x1f, 0x5d, 0x8c, 0xde, 0x64, 0xa5, 0x33, 0x1f, + 0x2c, 0xd0, 0x2c, 0x21, 0x44, 0x2f, 0x34, 0x5d, 0x16, 0xe8, 0x94, 0xaf, 0xa2, + 0x2b, 0xa9, 0xd4, 0x24, 0xd2, 0xf9, ], }, HeadlessCase { @@ -837,9 +837,9 @@ mod tests { mission: "MISSIONS/Campaign/CAMPAIGN.00/Mission.02/data.tma", object_count: 10, expected_hash: [ - 0x59, 0x6e, 0x88, 0xcc, 0xd0, 0x3a, 0xd9, 0x68, 0x1b, 0x2d, 0xcb, 0x0d, 0x91, - 0x19, 0x5a, 0x27, 0x5a, 0x27, 0x1b, 0x2d, 0xcb, 0x0d, 0x91, 0x19, 0x44, 0x66, - 0x68, 0x9d, 0x6c, 0xb4, 0x2c, 0x37, + 0x3c, 0xe5, 0xa6, 0x39, 0x47, 0x86, 0x76, 0xe1, 0xb2, 0x1a, 0x8e, 0x96, 0x3d, + 0x60, 0x6e, 0xc6, 0x8c, 0xe2, 0x28, 0x4f, 0x57, 0xd9, 0xe1, 0xe4, 0xb5, 0x95, + 0xdf, 0x88, 0xd3, 0x2f, 0x4a, 0x4d, ], }, ] { @@ -855,8 +855,7 @@ mod tests { #[test] #[ignore = "requires licensed corpus"] fn licensed_corpora_load_all_mission_foundations() { - let root = workspace_root(); - let part1 = load_all(&root.join("testdata").join("IS")); + let part1 = load_all(&licensed_root("IS")); assert_eq!(part1.missions, 29); assert_eq!(part1.paths, 34); assert_eq!(part1.clans, 101); @@ -876,7 +875,7 @@ mod tests { assert_eq!(part1.texture_requests, part1.texture_resolved); assert_eq!(part1.lightmap_requests, part1.lightmap_resolved); - let part2 = load_all(&root.join("testdata").join("IS2")); + let part2 = load_all(&licensed_root("IS2")); assert_eq!(part2.missions, 31); assert_eq!(part2.paths, 61); assert_eq!(part2.clans, 91); @@ -928,7 +927,7 @@ mod tests { } fn run_headless_case(case: HeadlessCase) -> WorldSnapshot { - let root = workspace_root().join("testdata").join(case.root); + let root = licensed_root(case.root); let vfs: Arc<dyn Vfs> = Arc::new(DirectoryVfs::new(root)); let mut engine = create( EngineConfig { @@ -1048,12 +1047,21 @@ mod tests { } } - fn workspace_root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .and_then(Path::parent) - .expect("workspace root") - .to_path_buf() + fn licensed_root(name: &str) -> PathBuf { + let variable = match name { + "IS" => "FPARKAN_CORPUS_PART1_ROOT", + "IS2" => "FPARKAN_CORPUS_PART2_ROOT", + _ => panic!("unknown licensed corpus part: {name}"), + }; + let root = std::env::var_os(variable) + .map(PathBuf::from) + .unwrap_or_else(|| panic!("{variable} is required for licensed corpus tests")); + assert!( + root.is_dir(), + "licensed corpus root is missing: {}", + root.display() + ); + root } #[derive(Clone, Copy)] diff --git a/crates/fparkan-terrain-format/src/lib.rs b/crates/fparkan-terrain-format/src/lib.rs index 8fd17ef..a8bc30d 100644 --- a/crates/fparkan-terrain-format/src/lib.rs +++ b/crates/fparkan-terrain-format/src/lib.rs @@ -1494,9 +1494,7 @@ Generator 1 ("IS", 33_usize, 299_450_usize, 275_882_usize), ("IS2", 32_usize, 188_024_usize, 184_454_usize), ] { - let Some(root) = corpus_root(corpus) else { - continue; - }; + let root = corpus_root(corpus); let mut files = 0usize; let mut vertices = 0usize; let mut faces = 0usize; @@ -1540,9 +1538,7 @@ Generator 1 #[ignore = "requires licensed corpus"] fn licensed_corpus_build_dat_validate() { for (corpus, expected_ai_prefix) in [("IS", false), ("IS2", true)] { - let Some(root) = corpus_root(corpus) else { - continue; - }; + let root = corpus_root(corpus); let path = root.join("BuildDat.lst"); let bytes = std::fs::read(&path).expect("read BuildDat.lst"); let categories = @@ -1591,9 +1587,7 @@ Generator 1 ("IS", 33_usize, 34_662_usize, 197_698_usize, 20_usize), ("IS2", 32_usize, 18_984_usize, 114_968_usize, 14_usize), ] { - let Some(root) = corpus_root(corpus) else { - continue; - }; + let root = corpus_root(corpus); let mut files = 0usize; let mut areals = 0usize; let mut vertices = 0usize; @@ -1883,12 +1877,21 @@ Generator 1 out.extend_from_slice(&value.to_le_bytes()); } - fn corpus_root(name: &str) -> Option<PathBuf> { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("testdata") - .join(name); - root.is_dir().then_some(root) + fn corpus_root(name: &str) -> PathBuf { + let variable = match name { + "IS" => "FPARKAN_CORPUS_PART1_ROOT", + "IS2" => "FPARKAN_CORPUS_PART2_ROOT", + _ => panic!("unknown licensed corpus part: {name}"), + }; + let root = std::env::var_os(variable) + .map(PathBuf::from) + .unwrap_or_else(|| panic!("{variable} is required for licensed corpus tests")); + assert!( + root.is_dir(), + "licensed corpus root is missing: {}", + root.display() + ); + root } fn files_under(root: &Path) -> Vec<PathBuf> { diff --git a/crates/fparkan-terrain/src/lib.rs b/crates/fparkan-terrain/src/lib.rs index 92f36dd..ff91219 100644 --- a/crates/fparkan-terrain/src/lib.rs +++ b/crates/fparkan-terrain/src/lib.rs @@ -800,9 +800,7 @@ mod tests { ("IS", 33_usize, 34_662_usize), ("IS2", 32_usize, 18_984_usize), ] { - let Some(root) = corpus_root(corpus) else { - continue; - }; + let root = corpus_root(corpus); let mut files = 0usize; let mut areals = 0usize; let mut located_centers = 0usize; @@ -856,9 +854,7 @@ mod tests { ("IS", 33_usize, 275_882_usize), ("IS2", 32_usize, 184_454_usize), ] { - let Some(root) = corpus_root(corpus) else { - continue; - }; + let root = corpus_root(corpus); let mut files = 0usize; let mut faces = 0usize; for path in files_under(&root) { @@ -1051,12 +1047,21 @@ mod tests { vertices.first().copied() } - fn corpus_root(name: &str) -> Option<PathBuf> { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("testdata") - .join(name); - root.is_dir().then_some(root) + fn corpus_root(name: &str) -> PathBuf { + let variable = match name { + "IS" => "FPARKAN_CORPUS_PART1_ROOT", + "IS2" => "FPARKAN_CORPUS_PART2_ROOT", + _ => panic!("unknown licensed corpus part: {name}"), + }; + let root = std::env::var_os(variable) + .map(PathBuf::from) + .unwrap_or_else(|| panic!("{variable} is required for licensed corpus tests")); + assert!( + root.is_dir(), + "licensed corpus root is missing: {}", + root.display() + ); + root } fn files_under(root: &Path) -> Vec<PathBuf> { diff --git a/crates/fparkan-texm/src/lib.rs b/crates/fparkan-texm/src/lib.rs index fef5369..8747737 100644 --- a/crates/fparkan-texm/src/lib.rs +++ b/crates/fparkan-texm/src/lib.rs @@ -1074,9 +1074,7 @@ mod tests { #[ignore = "requires licensed corpus"] fn licensed_corpus_texm_assets_validate_and_decode_mip0() { for (corpus, expected) in [("IS", 518_usize), ("IS2", 631_usize)] { - let Some(root) = corpus_root(corpus) else { - continue; - }; + let root = corpus_root(corpus); let mut count = 0usize; for path in files_under(&root) { let Ok(bytes) = std::fs::read(&path) else { @@ -1158,12 +1156,21 @@ mod tests { } } - fn corpus_root(name: &str) -> Option<PathBuf> { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("testdata") - .join(name); - root.is_dir().then_some(root) + fn corpus_root(name: &str) -> PathBuf { + let variable = match name { + "IS" => "FPARKAN_CORPUS_PART1_ROOT", + "IS2" => "FPARKAN_CORPUS_PART2_ROOT", + _ => panic!("unknown licensed corpus part: {name}"), + }; + let root = std::env::var_os(variable) + .map(PathBuf::from) + .unwrap_or_else(|| panic!("{variable} is required for licensed corpus tests")); + assert!( + root.is_dir(), + "licensed corpus root is missing: {}", + root.display() + ); + root } fn files_under(root: &Path) -> Vec<PathBuf> { diff --git a/xtask/src/main.rs b/xtask/src/main.rs index fd33aa6..2bf6d07 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -10,6 +10,10 @@ use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; +const CORPORA_MANIFEST_ENV: &str = "FPARKAN_CORPORA_MANIFEST"; +const PART1_ROOT_ENV: &str = "FPARKAN_CORPUS_PART1_ROOT"; +const PART2_ROOT_ENV: &str = "FPARKAN_CORPUS_PART2_ROOT"; + fn main() { let args = std::env::args().skip(1).collect::<Vec<_>>(); let code = match run(&args) { @@ -46,12 +50,12 @@ fn run(args: &[String]) -> Result<(), String> { } [cmd, suite, rest @ ..] if cmd == "test" && suite == "synthetic" => { let options = parse_test_options(rest, PathBuf::from("testdata"))?; - run_stage_tests(options.stage, TestSuite::Synthetic) + run_stage_tests(options.stage, TestSuite::Synthetic, None) } [cmd, suite, rest @ ..] if cmd == "test" && suite == "licensed" => { let options = parse_test_options(rest, PathBuf::from("testdata"))?; - validate_licensed_root(&options.root)?; - run_stage_tests(options.stage, TestSuite::Licensed) + let roots = load_licensed_roots(options.manifest.as_deref())?; + run_stage_tests(options.stage, TestSuite::Licensed, Some(&roots)) } [cmd, subcmd, rest @ ..] if cmd == "corpus" && subcmd == "baseline" => { let root = parse_root(rest)?; @@ -62,7 +66,7 @@ fn run(args: &[String]) -> Result<(), String> { Ok(()) } _ => Err( - "usage: cargo xtask ci | policy | acceptance report --suite synthetic|licensed [--stage 0..5|all] [--root testdata] [--out <path>] | acceptance audit [--roadmap <path>] [--coverage <path>] [--out <path>] [--strict] | package --target <triple> --app viewer|game|headless|cli | test synthetic|licensed [--stage 0..5|all] [--root testdata] | corpus baseline --root <path>" + "usage: cargo xtask ci | policy | acceptance report --suite synthetic|licensed [--stage 0..5|all] [--manifest corpora.toml] [--out <path>] | acceptance audit [--roadmap <path>] [--coverage <path>] [--out <path>] [--strict] | package --target <triple> --app viewer|game|headless|cli | test synthetic|licensed [--stage 0..5|all] [--manifest corpora.toml] | corpus baseline --root <path>" .to_string(), ), } @@ -94,6 +98,23 @@ fn cargo_owned(args: &[String]) -> Result<(), String> { } } +fn cargo_with_env(args: &[&str], envs: &[(&str, &Path)]) -> Result<(), String> { + let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); + let mut command = Command::new(cargo); + command.args(args); + for (key, value) in envs { + command.env(key, value); + } + let status = command + .status() + .map_err(|err| format!("failed to run cargo: {err}"))?; + if status.success() { + Ok(()) + } else { + Err(format!("cargo exited with {status}")) + } +} + fn clippy_rustup(args: &[&str]) -> Result<(), String> { let rustup = std::env::var_os("RUSTUP").unwrap_or_else(|| "rustup".into()); let status = Command::new(rustup) @@ -153,19 +174,124 @@ fn collect_rust_files(dir: &Path, out: &mut Vec<PathBuf>) -> Result<(), String> Ok(()) } -fn validate_licensed_root(root: &Path) -> Result<(), String> { - for part in ["IS", "IS2"] { - let part_root = root.join(part); - if !part_root.is_dir() { - return Err(format!( - "licensed corpus part is missing: {}", - part_root.display() - )); +#[derive(Clone, Debug, Eq, PartialEq)] +struct LicensedCorpusRoots { + part1: PathBuf, + part2: PathBuf, +} + +impl LicensedCorpusRoots { + fn envs(&self) -> [(&str, &Path); 2] { + [ + (PART1_ROOT_ENV, self.part1.as_path()), + (PART2_ROOT_ENV, self.part2.as_path()), + ] + } +} + +fn load_licensed_roots(manifest: Option<&Path>) -> Result<LicensedCorpusRoots, String> { + let manifest = manifest + .map(Path::to_path_buf) + .or_else(|| std::env::var_os(CORPORA_MANIFEST_ENV).map(PathBuf::from)) + .ok_or_else(|| { + format!( + "licensed tests require --manifest or {CORPORA_MANIFEST_ENV}=<absolute corpora.toml>" + ) + })?; + parse_licensed_manifest(&manifest) +} + +fn parse_licensed_manifest(path: &Path) -> Result<LicensedCorpusRoots, String> { + let text = fs::read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))?; + let mut part1 = None; + let mut part2 = None; + let mut current_kind: Option<String> = None; + let mut current_root: Option<PathBuf> = None; + + for raw_line in text.lines() { + let line = raw_line.split('#').next().unwrap_or_default().trim(); + if line.is_empty() { + continue; + } + if line == "[[corpus]]" { + flush_manifest_entry(&mut part1, &mut part2, &mut current_kind, &mut current_root)?; + continue; + } + let Some((key, value)) = line.split_once('=') else { + continue; + }; + let key = key.trim(); + match key { + "kind" => current_kind = Some(parse_manifest_string(value.trim())?), + "root" => current_root = Some(PathBuf::from(parse_manifest_string(value.trim())?)), + _ => {} } } + flush_manifest_entry(&mut part1, &mut part2, &mut current_kind, &mut current_root)?; + + let roots = LicensedCorpusRoots { + part1: part1.ok_or_else(|| "licensed manifest is missing kind = \"part1\"".to_string())?, + part2: part2.ok_or_else(|| "licensed manifest is missing kind = \"part2\"".to_string())?, + }; + validate_licensed_part("part1", &roots.part1)?; + validate_licensed_part("part2", &roots.part2)?; + Ok(roots) +} + +fn flush_manifest_entry( + part1: &mut Option<PathBuf>, + part2: &mut Option<PathBuf>, + current_kind: &mut Option<String>, + current_root: &mut Option<PathBuf>, +) -> Result<(), String> { + let Some(kind) = current_kind.take() else { + *current_root = None; + return Ok(()); + }; + let root = current_root + .take() + .ok_or_else(|| format!("licensed manifest entry {kind} is missing root"))?; + match kind.as_str() { + "part1" => assign_manifest_root(part1, root, "part1"), + "part2" => assign_manifest_root(part2, root, "part2"), + _ => Ok(()), + } +} + +fn assign_manifest_root( + target: &mut Option<PathBuf>, + root: PathBuf, + kind: &str, +) -> Result<(), String> { + if target.replace(root).is_some() { + return Err(format!("licensed manifest contains duplicate {kind} root")); + } Ok(()) } +fn parse_manifest_string(value: &str) -> Result<String, String> { + let trimmed = value.trim(); + if let Some(quoted) = trimmed + .strip_prefix('"') + .and_then(|value| value.strip_suffix('"')) + { + Ok(quoted.to_string()) + } else { + Err(format!("manifest value must be a quoted string: {trimmed}")) + } +} + +fn validate_licensed_part(kind: &str, root: &Path) -> Result<(), String> { + if root.is_dir() { + Ok(()) + } else { + Err(format!( + "licensed corpus {kind} root is missing: {}", + root.display() + )) + } +} + fn parse_root(args: &[String]) -> Result<PathBuf, String> { let mut iter = args.iter(); while let Some(arg) = iter.next() { @@ -717,6 +843,7 @@ impl fmt::Display for Stage { struct TestOptions { stage: Stage, root: PathBuf, + manifest: Option<PathBuf>, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -747,6 +874,7 @@ struct AcceptanceOptions { suite: TestSuite, stage: Stage, root: PathBuf, + manifest: Option<PathBuf>, out: PathBuf, } @@ -762,6 +890,7 @@ fn parse_test_options(args: &[String], default_root: PathBuf) -> Result<TestOpti let mut options = TestOptions { stage: Stage::All, root: default_root, + manifest: None, }; let mut iter = args.iter(); while let Some(arg) = iter.next() { @@ -778,6 +907,12 @@ fn parse_test_options(args: &[String], default_root: PathBuf) -> Result<TestOpti .ok_or_else(|| "--root requires a path".to_string())?; options.root = PathBuf::from(value); } + "--manifest" => { + let value = iter + .next() + .ok_or_else(|| "--manifest requires a path".to_string())?; + options.manifest = Some(PathBuf::from(value)); + } _ => return Err(format!("unknown test option: {arg}")), } } @@ -788,6 +923,7 @@ fn parse_acceptance_options(args: &[String]) -> Result<AcceptanceOptions, String let mut suite = None; let mut stage = Stage::All; let mut root = PathBuf::from("testdata"); + let mut manifest = None; let mut out = None; let mut iter = args.iter(); while let Some(arg) = iter.next() { @@ -810,6 +946,12 @@ fn parse_acceptance_options(args: &[String]) -> Result<AcceptanceOptions, String .ok_or_else(|| "--root requires a path".to_string())?; root = PathBuf::from(value); } + "--manifest" => { + let value = iter + .next() + .ok_or_else(|| "--manifest requires a path".to_string())?; + manifest = Some(PathBuf::from(value)); + } "--out" => { let value = iter .next() @@ -832,6 +974,7 @@ fn parse_acceptance_options(args: &[String]) -> Result<AcceptanceOptions, String suite, stage, root, + manifest, out, }) } @@ -1156,10 +1299,12 @@ fn json_escape(value: &str) -> String { } fn run_acceptance_report(options: &AcceptanceOptions) -> Result<(), String> { - if options.suite == TestSuite::Licensed { - validate_licensed_root(&options.root)?; - } - run_stage_tests(options.stage, options.suite)?; + let roots = if options.suite == TestSuite::Licensed { + Some(load_licensed_roots(options.manifest.as_deref())?) + } else { + None + }; + run_stage_tests(options.stage, options.suite, roots.as_ref())?; if let Some(parent) = options.out.parent() { fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?; @@ -1208,22 +1353,35 @@ fn stage_report_packages(stage: Stage) -> Vec<&'static str> { } } -fn run_stage_tests(stage: Stage, suite: TestSuite) -> Result<(), String> { +fn run_stage_tests( + stage: Stage, + suite: TestSuite, + roots: Option<&LicensedCorpusRoots>, +) -> Result<(), String> { let mut suffix = Vec::new(); if suite == TestSuite::Licensed { suffix.extend(["--", "--ignored"]); } + let envs = roots.map(LicensedCorpusRoots::envs); match stage { Stage::All => { let mut args = vec!["test", "--workspace", "--locked", "--offline"]; args.extend(suffix); - cargo(&args) + if let Some(envs) = envs { + cargo_with_env(&args, &envs) + } else { + cargo(&args) + } } Stage::Number(number) => { for package in stage_packages(number)? { let mut args = vec!["test", "-p", package, "--locked", "--offline"]; args.extend(suffix.iter().copied()); - cargo(&args)?; + if let Some(envs) = envs { + cargo_with_env(&args, &envs)?; + } else { + cargo(&args)?; + } } Ok(()) } @@ -1276,6 +1434,13 @@ mod tests { values.iter().map(|value| (*value).to_string()).collect() } + fn temp_dir(name: &str) -> PathBuf { + let suffix = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_or(0, |duration| duration.as_nanos()); + std::env::temp_dir().join(format!("fparkan-xtask-{name}-{suffix}")) + } + #[test] fn parses_stage_and_root_options() { let args = strings(&["--stage", "3", "--root", "fixtures"]); @@ -1286,6 +1451,7 @@ mod tests { Ok(TestOptions { stage: Stage::Number(3), root: PathBuf::from("fixtures"), + manifest: None, }) ); } @@ -1297,8 +1463,8 @@ mod tests { "licensed", "--stage", "5", - "--root", - "testdata", + "--manifest", + "corpora.toml", "--out", "target/report.json", ])); @@ -1309,6 +1475,7 @@ mod tests { suite: TestSuite::Licensed, stage: Stage::Number(5), root: PathBuf::from("testdata"), + manifest: Some(PathBuf::from("corpora.toml")), out: PathBuf::from("target/report.json"), }) ); @@ -1320,6 +1487,7 @@ mod tests { suite: TestSuite::Licensed, stage: Stage::Number(0), root: PathBuf::from("/private/game"), + manifest: Some(PathBuf::from("/private/corpora.toml")), out: PathBuf::from("target/report.json"), }; let report = render_acceptance_report(&options); @@ -1421,11 +1589,55 @@ mod tests { Ok(TestOptions { stage: Stage::All, root: PathBuf::from("testdata"), + manifest: None, }) ); } #[test] + fn parses_licensed_corpora_manifest() -> Result<(), String> { + let root = temp_dir("manifest"); + let part1 = root.join("IS"); + let part2 = root.join("IS2"); + fs::create_dir_all(&part1).map_err(|err| err.to_string())?; + fs::create_dir_all(&part2).map_err(|err| err.to_string())?; + let manifest = root.join("corpora.toml"); + fs::write( + &manifest, + format!( + "schema = 1\n\n[[corpus]]\nid = \"part1-local\"\nkind = \"part1\"\nroot = \"{}\"\nexpected_profile = \"parkan-is-part1\"\n\n[[corpus]]\nid = \"part2-local\"\nkind = \"part2\"\nroot = \"{}\"\nexpected_profile = \"parkan-is-part2\"\n", + part1.display(), + part2.display() + ), + ) + .map_err(|err| err.to_string())?; + + assert_eq!( + parse_licensed_manifest(&manifest)?, + LicensedCorpusRoots { part1, part2 } + ); + fs::remove_dir_all(root).map_err(|err| err.to_string())?; + Ok(()) + } + + #[test] + fn licensed_roots_require_manifest_configuration() { + let previous = std::env::var_os(CORPORA_MANIFEST_ENV); + std::env::remove_var(CORPORA_MANIFEST_ENV); + + assert_eq!( + load_licensed_roots(None), + Err(format!( + "licensed tests require --manifest or {CORPORA_MANIFEST_ENV}=<absolute corpora.toml>" + )) + ); + + if let Some(value) = previous { + std::env::set_var(CORPORA_MANIFEST_ENV, value); + } + } + + #[test] fn rejects_unknown_stage() { assert_eq!(Stage::parse("6"), Err("stage out of range: 6".to_string())); assert_eq!( |
