#[path = "../build_support/catalog_resources.rs"] mod catalog_resources; use std::{ fs, path::{Path, PathBuf}, sync::atomic::{AtomicU64, Ordering}, time::{Duration, SystemTime}, }; use catalog_resources::{CatalogResources, reset_catalog_resources}; static NEXT_DIRECTORY: AtomicU64 = AtomicU64::new(0); struct TestDirectory(PathBuf); impl TestDirectory { fn new() -> Self { let path = std::env::temp_dir().join(format!( "lanspread-catalog-resources-{}-{}", std::process::id(), NEXT_DIRECTORY.fetch_add(1, Ordering::Relaxed), )); fs::create_dir(&path).expect("test directory should be unique"); Self(path) } } impl Drop for TestDirectory { fn drop(&mut self) { fs::remove_dir_all(&self.0).expect("test directory should be removed"); } } #[test] fn resets_catalog_copies_for_native_and_cross_target_profiles() { let root = TestDirectory::new(); for profile in ["target/release", "custom/x86_64-pc-windows-msvc/production"] { let profile = root.0.join(profile); let out_dir = profile.join("build/launcher-test/out"); fs::create_dir_all(&out_dir).expect("fixture filesystem operation should succeed"); fs::create_dir(profile.join("manifests")) .expect("fixture filesystem operation should succeed"); fs::write(profile.join("manifests/fixture.json"), b"old fixture") .expect("fixture filesystem operation should succeed"); fs::write( profile.join("manifests/removed-game.json"), b"old production", ) .expect("fixture filesystem operation should succeed"); fs::write(profile.join("game.db"), b"old database") .expect("fixture filesystem operation should succeed"); fs::write(profile.join("launcher"), b"binary") .expect("fixture filesystem operation should succeed"); fs::write(out_dir.join("embedded-index"), b"new authority") .expect("fixture filesystem operation should succeed"); let resources = reset_catalog_resources(&out_dir).expect("fixture filesystem operation should succeed"); assert_eq!( resources, [profile.join("game.db"), profile.join("manifests")] ); assert!(resources.iter().all(|path| !path.exists())); assert_eq!( fs::read(profile.join("launcher")) .expect("fixture filesystem operation should succeed"), b"binary" ); assert_eq!( fs::read(out_dir.join("embedded-index")) .expect("fixture filesystem operation should succeed"), b"new authority" ); // Missing copies are a normal input after manual cleanup or a clean build. assert_eq!( reset_catalog_resources(&out_dir).expect("fixture filesystem operation should succeed"), resources ); } } #[test] fn rejects_non_cargo_layout_before_removing_anything() { let root = TestDirectory::new(); fs::write(root.0.join("game.db"), b"keep") .expect("fixture filesystem operation should succeed"); assert!(reset_catalog_resources(&root.0.join("unexpected/launcher/out")).is_err()); assert!(reset_catalog_resources(Path::new("out")).is_err()); assert!(reset_catalog_resources(Path::new("target/release/build/launcher/out")).is_err()); assert_eq!( fs::read(root.0.join("game.db")).expect("fixture filesystem operation should succeed"), b"keep" ); } #[cfg(unix)] #[test] fn rejects_linked_manifest_destination_without_traversing_it() { let root = TestDirectory::new(); let profile = root.0.join("target/debug"); let out_dir = profile.join("build/launcher-test/out"); fs::create_dir_all(&out_dir).expect("fixture filesystem operation should succeed"); let source = root.0.join("source-manifests"); fs::create_dir(&source).expect("fixture filesystem operation should succeed"); fs::write(source.join("keep.json"), b"authority") .expect("fixture filesystem operation should succeed"); std::os::unix::fs::symlink(&source, profile.join("manifests")) .expect("fixture filesystem operation should succeed"); assert!(reset_catalog_resources(&out_dir).is_err()); assert_eq!( fs::read(source.join("keep.json")).expect("fixture filesystem operation should succeed"), b"authority" ); } fn timestamp(offset: u64) -> SystemTime { SystemTime::UNIX_EPOCH + Duration::from_secs(1_600_000_000 + offset) } fn set_modified(path: &Path, modified: SystemTime) { fs::OpenOptions::new() .write(true) .open(path) .expect("fixture filesystem operation should succeed") .set_modified(modified) .expect("fixture filesystem operation should succeed"); } fn write_catalog(root: &Path, database: &str, manifests: &[(&str, &str)]) { fs::create_dir_all(root.join("manifests")) .expect("fixture filesystem operation should succeed"); fs::write(root.join("game.db"), database).expect("fixture filesystem operation should succeed"); for (name, body) in manifests { fs::write(root.join("manifests").join(name), body) .expect("fixture filesystem operation should succeed"); } for path in catalog_files(root) { // Sources can be older than every build using them. Their timestamps // must never replace the timestamps of changed destination bytes. set_modified(&path, timestamp(0)); } } fn catalog_files(root: &Path) -> Vec { let mut paths = vec![root.join("game.db")]; paths.extend( fs::read_dir(root.join("manifests")) .expect("fixture filesystem operation should succeed") .map(|entry| { entry .expect("manifest directory entry should be readable") .path() }), ); paths.sort(); paths } fn simulate_tauri_copy(source: &Path, destination: &Path, modified: SystemTime) { fs::create_dir_all(destination.join("manifests")) .expect("fixture filesystem operation should succeed"); for path in catalog_files(source) { let destination = destination.join( path.strip_prefix(source) .expect("catalog path should be below its source root"), ); fs::copy(path, &destination).expect("fixture filesystem operation should succeed"); set_modified(&destination, modified); } } fn assert_catalog_matches(source: &Path, destination: &Path, watched: &[PathBuf]) { let expected = catalog_files(source) .iter() .map(|path| { destination.join( path.strip_prefix(source) .expect("catalog path should be below its source root"), ) }) .collect::>(); assert_eq!(watched, expected); assert_eq!(catalog_files(destination), expected); for path in watched { let source = source.join( path.strip_prefix(destination) .expect("copied path should be below its destination root"), ); assert_eq!( fs::read(path).expect("fixture filesystem operation should succeed"), fs::read(source).expect("fixture filesystem operation should succeed") ); } } #[test] fn unchanged_copies_keep_previous_output_timestamps() { let root = TestDirectory::new(); let source = root.0.join("source"); let profile = root.0.join("target/release"); let out_dir = profile.join("build/launcher-test/out"); fs::create_dir_all(&out_dir).expect("fixture filesystem operation should succeed"); write_catalog(&source, "database", &[("game.json", "manifest")]); simulate_tauri_copy(&source, &profile, timestamp(10)); let resources = CatalogResources::prepare(&out_dir, &source).expect("catalog resources should prepare"); simulate_tauri_copy(&source, &profile, timestamp(20)); let watched = resources.finish().expect("copied resources should finish"); assert_catalog_matches(&source, &profile, &watched); for path in watched { assert_eq!( fs::metadata(path) .expect("fixture filesystem operation should succeed") .modified() .expect("fixture filesystem operation should succeed"), timestamp(10) ); } } #[test] fn changed_copies_keep_new_timestamps_then_stabilize_on_the_next_build() { let root = TestDirectory::new(); let source = root.0.join("source"); let profile = root.0.join("target/release"); let out_dir = profile.join("build/launcher-test/out"); fs::create_dir_all(&out_dir).expect("fixture filesystem operation should succeed"); write_catalog( &source, "old database", &[("game.json", "old manifest"), ("shared.json", "unchanged")], ); simulate_tauri_copy(&source, &profile, timestamp(10)); // Equal lengths ensure that changed bytes, rather than just file sizes, // distinguish a different catalog occupying the shared profile directory. write_catalog( &source, "new database", &[("game.json", "new manifest"), ("shared.json", "unchanged")], ); for copied_at in [timestamp(20), timestamp(30)] { let resources = CatalogResources::prepare(&out_dir, &source).expect("catalog resources should prepare"); simulate_tauri_copy(&source, &profile, copied_at); let watched = resources.finish().expect("copied resources should finish"); assert_catalog_matches(&source, &profile, &watched); for path in watched { let expected = if path.ends_with("shared.json") { timestamp(10) } else { timestamp(20) }; assert_eq!( fs::metadata(path) .expect("fixture filesystem operation should succeed") .modified() .expect("fixture filesystem operation should succeed"), expected ); } } } #[test] fn missing_database_manifest_or_manifest_directory_is_repaired_and_watched() { let root = TestDirectory::new(); let source = root.0.join("source"); let profile = root.0.join("target/release"); let out_dir = profile.join("build/launcher-test/out"); fs::create_dir_all(&out_dir).expect("fixture filesystem operation should succeed"); write_catalog(&source, "database", &[("game.json", "manifest")]); for missing in ["game.db", "manifests/game.json", "manifests"] { simulate_tauri_copy(&source, &profile, timestamp(10)); let missing_path = profile.join(missing); if missing_path.is_dir() { fs::remove_dir_all(&missing_path).expect("fixture filesystem operation should succeed"); } else { fs::remove_file(&missing_path).expect("fixture filesystem operation should succeed"); } let resources = CatalogResources::prepare(&out_dir, &source).expect("catalog resources should prepare"); simulate_tauri_copy(&source, &profile, timestamp(20)); let watched = resources.finish().expect("copied resources should finish"); assert_catalog_matches(&source, &profile, &watched); for path in watched { let expected = if path.starts_with(&missing_path) { timestamp(20) } else { timestamp(10) }; assert_eq!( fs::metadata(path) .expect("fixture filesystem operation should succeed") .modified() .expect("fixture filesystem operation should succeed"), expected ); } } } #[test] fn mode_switch_and_catalog_shrink_remove_previous_manifests() { let root = TestDirectory::new(); let fixture = root.0.join("fixture"); let production = root.0.join("production"); let profile = root.0.join("target/release"); let out_dir = profile.join("build/launcher-test/out"); fs::create_dir_all(&out_dir).expect("fixture filesystem operation should succeed"); write_catalog( &fixture, "fixture database", &[ ("fixture.json", "fixture only"), ("shared.json", "fixture game"), ], ); write_catalog( &production, "production database", &[ ("production.json", "production only"), ("shared.json", "real game"), ], ); simulate_tauri_copy(&fixture, &profile, timestamp(10)); let resources = CatalogResources::prepare(&out_dir, &production) .expect("production resources should prepare"); simulate_tauri_copy(&production, &profile, timestamp(20)); assert_catalog_matches( &production, &profile, &resources.finish().expect("copied resources should finish"), ); assert!(!profile.join("manifests/fixture.json").exists()); fs::remove_file(production.join("manifests/production.json")) .expect("fixture filesystem operation should succeed"); write_catalog( &production, "smaller database", &[("shared.json", "real game")], ); let resources = CatalogResources::prepare(&out_dir, &production) .expect("production resources should prepare"); simulate_tauri_copy(&production, &profile, timestamp(30)); assert_catalog_matches( &production, &profile, &resources.finish().expect("copied resources should finish"), ); assert!(!profile.join("manifests/production.json").exists()); let resources = CatalogResources::prepare(&out_dir, &fixture).expect("fixture resources should prepare"); simulate_tauri_copy(&fixture, &profile, timestamp(40)); assert_catalog_matches( &fixture, &profile, &resources.finish().expect("copied resources should finish"), ); }