use nox_roblox::mods; use nox_roblox::tweaks; use nox_roblox::tweaks::Choices; fn tmp(name: &str) -> std::path::PathBuf { let p = std::env::temp_dir().join(format!("nox-mods-{name}")); let _ = std::fs::remove_dir_all(&p); std::fs::create_dir_all(&p).unwrap(); p } #[test] fn scan_walks_nested_folders() { let dir = tmp("scan"); std::fs::create_dir_all(dir.join("content/textures/Cursors")).unwrap(); std::fs::create_dir_all(dir.join("content/sounds")).unwrap(); std::fs::write(dir.join("content/textures/Cursors/a.png"), b"12345").unwrap(); std::fs::write(dir.join("content/sounds/b.ogg"), b"xy").unwrap(); std::fs::write(dir.join("top.txt"), b"").unwrap(); let files = mods::scan(&dir); assert_eq!(files.len(), 3); let names: Vec = files.iter().map(|f| f.display()).collect(); assert!(names.contains(&"content/textures/Cursors/a.png".to_string())); assert!(names.contains(&"content/sounds/b.ogg".to_string())); assert!(names.contains(&"top.txt".to_string())); assert_eq!(mods::total_bytes(&files), 7); // Sorted, so the list does not reshuffle between scans. let again: Vec = mods::scan(&dir).iter().map(|f| f.display()).collect(); assert_eq!(names, again); } #[test] fn scan_of_a_missing_folder_is_empty_not_an_error() { let files = mods::scan(std::path::Path::new("/definitely/not/here")); assert!(files.is_empty()); } #[test] fn apply_copies_over_the_client_preserving_layout() { let src = tmp("apply-src"); let dst = tmp("apply-dst"); std::fs::create_dir_all(src.join("content/textures")).unwrap(); std::fs::write(src.join("content/textures/cursor.png"), b"NEW").unwrap(); std::fs::write(src.join("loose.dll"), b"DLL").unwrap(); // One target already exists and must be overwritten, the other is new. std::fs::create_dir_all(dst.join("content/textures")).unwrap(); std::fs::write(dst.join("content/textures/cursor.png"), b"ORIGINAL-LONGER").unwrap(); let applied = mods::apply(&src, &dst); assert_eq!(applied.copied, 2); assert!(applied.failed.is_empty(), "{:?}", applied.failed); assert_eq!(std::fs::read(dst.join("content/textures/cursor.png")).unwrap(), b"NEW"); assert_eq!(std::fs::read(dst.join("loose.dll")).unwrap(), b"DLL"); } #[test] fn scaffold_creates_the_suggested_layout() { let dir = tmp("scaffold"); mods::scaffold(&dir).unwrap(); for s in mods::SUGGESTED { assert!(dir.join(s).is_dir(), "{s} not created"); } // Empty folders are not files, so nothing is reported as a mod yet. assert!(mods::scan(&dir).is_empty()); } #[test] fn tweaks_expand_into_flags() { let flags = tweaks::merge(&["uncap_fps".into()], "Automatic", Choices::default(), &[]); assert_eq!(flags, vec![("DFIntTaskSchedulerTargetFps".to_string(), "9999".to_string())]); let none = tweaks::merge(&[], "Automatic", Choices::default(), &[]); assert!(none.is_empty(), "nothing enabled should write nothing"); let unknown = tweaks::merge(&["not-a-tweak".into()], "Automatic", Choices::default(), &[]); assert!(unknown.is_empty(), "unknown ids are ignored, not fatal"); } #[test] fn render_modes_and_quality_contribute() { let vk = tweaks::merge(&[], "Vulkan", Choices::default(), &[]); assert!(vk.iter().any(|(k, v)| k == "FFlagDebugGraphicsPreferVulkan" && v == "true")); let q = tweaks::merge(&[], "Automatic", Choices { quality: 7, ..Default::default() }, &[]); assert!(q.iter().any(|(k, v)| k == "DFIntDebugFRMQualityLevelOverride" && v == "7")); let auto = tweaks::merge(&[], "Automatic", Choices::default(), &[]); assert!(auto.is_empty()); // An unrecognised renderer name falls back to automatic rather than breaking. assert!(tweaks::merge(&[], "Glide", Choices::default(), &[]).is_empty()); } #[test] fn custom_flags_win_over_tweaks() { let flags = tweaks::merge( &["uncap_fps".into()], "Automatic", Choices::default(), &[("DFIntTaskSchedulerTargetFps".into(), "144".into())], ); assert_eq!(flags.len(), 1, "the key must not be duplicated"); assert_eq!(flags[0].1, "144", "the custom value must win"); } #[test] fn every_tweak_is_well_formed() { let mut seen = std::collections::HashSet::new(); for t in tweaks::TWEAKS { assert!(seen.insert(t.id), "duplicate tweak id {}", t.id); assert!(!t.label.is_empty() && !t.detail.is_empty(), "{} lacks text", t.id); assert!(!t.flags.is_empty(), "{} sets no flags", t.id); for (k, _) in t.flags { assert!( k.starts_with("FFlag") || k.starts_with("DFFlag") || k.starts_with("FInt") || k.starts_with("DFInt") || k.starts_with("FString") || k.starts_with("DFString"), "{k} does not look like an engine flag" ); } assert!(tweaks::find(t.id).is_some()); } assert!(seen.len() >= 8); } #[test] fn msaa_and_texture_choices_expand_correctly() { // Only the sample counts the engine understands produce a flag. for bad in [0u8, 3, 5, 8, 255] { assert!(tweaks::msaa_flags(bad).is_empty(), "{bad} should not set a flag"); } for good in [1u8, 2, 4] { let f = tweaks::msaa_flags(good); assert_eq!(f.len(), 1); assert_eq!(f[0].0, "FIntDebugForceMSAASamples"); assert_eq!(f[0].1, good.to_string()); } // The texture override needs switching on as well as given a level. assert!(tweaks::texture_flags(-1).is_empty()); assert!(tweaks::texture_flags(4).is_empty()); for level in 0..=3i8 { let f = tweaks::texture_flags(level); assert_eq!(f.len(), 2, "level {level} needs the enable flag too"); assert!(f.iter().any(|(k, v)| k == "DFFlagTextureQualityOverrideEnabled" && v == "true")); assert!(f.iter().any(|(k, v)| k == "DFIntTextureQualityOverride" && *v == level.to_string())); } } #[test] fn choices_reach_the_merged_output() { let flags = tweaks::merge( &[], "Automatic", Choices { quality: 5, msaa: 4, texture: 2 }, &[], ); assert!(flags.iter().any(|(k, v)| k == "DFIntDebugFRMQualityLevelOverride" && v == "5")); assert!(flags.iter().any(|(k, v)| k == "FIntDebugForceMSAASamples" && v == "4")); assert!(flags.iter().any(|(k, v)| k == "DFIntTextureQualityOverride" && v == "2")); assert_eq!(flags.len(), 4, "quality + msaa + two texture flags"); } #[test] fn every_tweak_belongs_to_a_declared_category() { for t in tweaks::TWEAKS { assert!( tweaks::CATEGORIES.contains(&t.category), "{} has category {:?}, which is not in CATEGORIES", t.id, t.category ); } // Every category should actually be used, or it is dead UI. for c in tweaks::CATEGORIES { assert!(tweaks::TWEAKS.iter().any(|t| t.category == c), "{c} has no tweaks"); } } #[test] fn default_choices_change_nothing() { // Zero is a valid texture level, so the "off" sentinel has to be negative. // A derived Default would pin every fresh install to the lowest detail. let d = Choices::default(); assert!(d.texture < 0, "default texture must mean 'leave it alone'"); assert_eq!(d.quality, 0); assert_eq!(d.msaa, 0); assert!( tweaks::merge(&[], "Automatic", d, &[]).is_empty(), "a default configuration must write no flags at all" ); }