1use foch_test::compile::Bundle;
10
11pub const MAX_PATH: usize = 260;
13
14pub fn fits_path_budget(runtime_dir_len: usize, longest_relative: usize) -> bool {
19 runtime_dir_len + longest_relative + 2 <= MAX_PATH
21}
22
23pub fn command_file(bundle: &Bundle, disable_ai: bool) -> String {
26 let base = bundle
27 .files
28 .get("commands.txt")
29 .map(String::as_str)
30 .unwrap_or_default();
31 if disable_ai {
32 format!("ai\n{base}")
33 } else {
34 base.to_string()
35 }
36}
37
38pub fn isolated_settings(template: &str) -> String {
42 let mut out = String::with_capacity(template.len());
43 for line in template.split_inclusive('\n') {
44 let trimmed = line.trim_start();
45 let indent = &line[..line.len() - trimmed.len()];
46 let rewritten = if let Some((key, _)) = trimmed.split_once('=') {
47 match key.trim() {
48 "fullScreen" => Some(format!("{indent}fullScreen=no\n")),
49 "borderless" => Some(format!("{indent}borderless=no\n")),
50 "master_volume" | "music_volume" | "sound_fx_volume" | "ambient_volume"
51 | "dev_master_volume" => Some(format!("{indent}{}=0.000000\n", key.trim())),
52 _ => None,
53 }
54 } else {
55 None
56 };
57 out.push_str(&rewritten.unwrap_or_else(|| line.to_string()));
58 }
59 if !out.ends_with('\n') {
60 out.push('\n');
61 }
62 out
63}
64
65pub fn dlc_load(mod_files: &[&str]) -> String {
67 let mods: Vec<String> = mod_files
68 .iter()
69 .map(|name| format!("\"mod/{name}\""))
70 .collect();
71 format!(
72 "{{\"enabled_mods\":[{}],\"disabled_dlcs\":[]}}",
73 mods.join(",")
74 )
75}
76
77pub fn all_cases_ended(log: &str, bundle: &Bundle) -> bool {
80 let marker = &bundle.marker_prefix;
81 bundle.cases.iter().all(|&case| {
82 let needle = format!("{} END", bundle.case_prefix(case));
83 log.lines()
84 .any(|line| line.contains(marker.as_str()) && line.trim_end().ends_with(&needle))
85 })
86}
87
88#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92pub enum LayerAction {
93 Copy,
94 Link,
95 Skip,
96}
97
98pub fn classify_entry(name: &str, is_dir: bool) -> LayerAction {
101 let lower = name.to_ascii_lowercase();
102 if matches!(
103 lower.as_str(),
104 "plugins" | "foch-runtime" | "foch-player-runtime" | "foch-host"
105 ) || foch::plugin::store::is_proxy_name(&lower)
106 {
107 return LayerAction::Skip;
108 }
109 if is_dir {
110 return LayerAction::Link;
111 }
112 let skip = matches!(
113 lower.as_str(),
114 "universal-checksum-patcher.exe" | "console_history.txt" | "userdir.txt"
115 );
116 if skip {
117 LayerAction::Skip
118 } else {
119 LayerAction::Copy
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use foch_test::compile::{Bundle, Launch, Requirements};
127 use foch_test::model::{AiMode, CaseIndex, GameDate, RunId, Tag};
128 use foch_test::plan::SessionId;
129 use std::collections::BTreeMap;
130
131 fn bundle() -> Bundle {
132 let date = GameDate::parse("1444.11.11").unwrap();
133 Bundle {
134 protocol: 2,
135 run_id: RunId::parse("run-1").unwrap(),
136 mod_name: "demo".into(),
137 session: SessionId(0),
138 cases: vec![CaseIndex(0), CaseIndex(2)],
139 marker_prefix: "FOCH_TEST_V2 run-1 abc123".into(),
140 namespace: "foch_test_demo_abc123".into(),
141 test_mod_name: "demo tests [abc123]".into(),
142 files: BTreeMap::from([(
143 "commands.txt".to_string(),
144 "speed 0\nrun start.txt\nspeed 5\n".to_string(),
145 )]),
146 launch: Launch {
147 args: vec!["-start_tag=SWE".into()],
148 ai: AiMode::Off,
149 player: Tag::parse("SWE").unwrap(),
150 start_date: date,
151 end_date: date,
152 },
153 checks: Vec::new(),
154 windows: Vec::new(),
155 requires: Requirements {
156 start_date: date,
157 ai_off: true,
158 shared_session: false,
159 },
160 }
161 }
162
163 #[test]
164 fn path_budget_matches_windows_max_path() {
165 assert!(fits_path_budget(131, 127));
167 assert!(!fits_path_budget(132, 127));
168 }
169
170 #[test]
171 fn command_file_prepends_ai_only_when_disabling() {
172 let bundle = bundle();
173 assert_eq!(
174 command_file(&bundle, false),
175 "speed 0\nrun start.txt\nspeed 5\n"
176 );
177 assert_eq!(
178 command_file(&bundle, true),
179 "ai\nspeed 0\nrun start.txt\nspeed 5\n"
180 );
181 }
182
183 #[test]
184 fn settings_are_windowed_and_muted_preserving_other_keys() {
185 let template = "graphics={\n\tfullScreen=yes\n\tborderless=yes\n\tsize={ x=1920 }\n}\nmaster_volume=100.000000\nmusic_volume=50.000000\ndifficulty=\"LUCK_NONE\"\n";
186 let out = isolated_settings(template);
187 assert!(out.contains("\tfullScreen=no\n"));
188 assert!(out.contains("\tborderless=no\n"));
189 assert!(out.contains("master_volume=0.000000\n"));
190 assert!(out.contains("music_volume=0.000000\n"));
191 assert!(
192 out.contains("\tsize={ x=1920 }\n"),
193 "unrelated keys preserved"
194 );
195 assert!(out.contains("difficulty=\"LUCK_NONE\"\n"));
196 }
197
198 #[test]
199 fn all_cases_ended_requires_every_case() {
200 let bundle = bundle();
201 let line = |case: usize, record: &str| {
202 format!(
203 "[x]: EVENT [1444.11.11]:{} c{case} {record}\n",
204 bundle.marker_prefix
205 )
206 };
207 let mut log = String::new();
208 log.push_str(&line(0, "END"));
209 assert!(!all_cases_ended(&log, &bundle), "case 2 has not ended");
210 log.push_str(&line(2, "PASS scope"));
211 assert!(!all_cases_ended(&log, &bundle));
212 log.push_str(&line(2, "END"));
213 assert!(all_cases_ended(&log, &bundle));
214 }
215
216 #[test]
217 fn layer_classification_copies_files_links_dirs_skips_noise() {
218 assert_eq!(classify_entry("eu4.exe", false), LayerAction::Copy);
219 assert_eq!(classify_entry("d3d9.dll", false), LayerAction::Skip);
220 assert_eq!(classify_entry("VERSION.DLL", false), LayerAction::Skip);
221 assert_eq!(classify_entry("Plugins", true), LayerAction::Skip);
222 assert_eq!(classify_entry("steam_api64.dll", false), LayerAction::Copy);
223 assert_eq!(classify_entry("common", true), LayerAction::Link);
224 assert_eq!(
225 classify_entry("console_history.txt", false),
226 LayerAction::Skip
227 );
228 assert_eq!(classify_entry("userdir.txt", false), LayerAction::Skip);
229 }
230
231 #[test]
232 fn dlc_load_enables_source_and_test_mods() {
233 assert_eq!(
234 dlc_load(&["source_mod.mod", "test_mod.mod"]),
235 "{\"enabled_mods\":[\"mod/source_mod.mod\",\"mod/test_mod.mod\"],\"disabled_dlcs\":[]}"
236 );
237 }
238}