ferridriver_test/
config.rs1pub use ferridriver_config::test::{
10 BrowserConfig, CliOverrides, ContextConfig, GeolocationConfig, GracefulShutdown, HttpCredentialsConfig,
11 ProjectConfig, ProxyConfig, ReportSlowTestsConfig, ReporterConfig, ShardArg, TestConfig, TraceMode,
12 UpdateSnapshotsMode, VideoConfig, VideoMode, ViewportConfig, WebServerConfig,
13};
14
15use std::path::Path;
16use std::path::PathBuf;
17
18pub fn parse_common_cli_args() -> CliOverrides {
27 let args: Vec<String> = std::env::args().collect();
28 let mut overrides = CliOverrides::default();
29 let mut i = 1;
30 while i < args.len() {
31 match args[i].as_str() {
32 "--headless" => overrides.headless = true,
33 "--workers" | "-j" => {
34 i += 1;
35 overrides.workers = args.get(i).and_then(|v| v.parse().ok());
36 },
37 "--retries" => {
38 i += 1;
39 overrides.retries = args.get(i).and_then(|v| v.parse().ok());
40 },
41 "--timeout" => {
42 i += 1;
43 overrides.timeout = args.get(i).and_then(|v| v.parse().ok());
44 },
45 "--backend" => {
46 i += 1;
47 overrides.backend = args.get(i).cloned();
48 },
49 "--grep" | "-g" => {
50 i += 1;
51 overrides.grep = args.get(i).cloned();
52 },
53 "--tag" => {
54 i += 1;
55 overrides.tag = args.get(i).cloned();
56 },
57 "--list" => overrides.list_only = true,
58 "--update-snapshots" | "-u" => {
59 let mode = match args.get(i + 1).map(String::as_str) {
60 Some("all") => {
61 i += 1;
62 UpdateSnapshotsMode::All
63 },
64 Some("changed") => {
65 i += 1;
66 UpdateSnapshotsMode::Changed
67 },
68 Some("missing") => {
69 i += 1;
70 UpdateSnapshotsMode::Missing
71 },
72 Some("none") => {
73 i += 1;
74 UpdateSnapshotsMode::None
75 },
76 _ => UpdateSnapshotsMode::All,
77 };
78 overrides.update_snapshots = Some(mode);
79 },
80 "--forbid-only" => overrides.forbid_only = true,
81 "--last-failed" => overrides.last_failed = true,
82 "--max-failures" => {
83 i += 1;
84 overrides.max_failures = args.get(i).and_then(|v| v.parse().ok());
85 },
86 "--repeat-each" => {
87 i += 1;
88 overrides.repeat_each = args.get(i).and_then(|v| v.parse().ok());
89 },
90 "--global-timeout" => {
91 i += 1;
92 overrides.global_timeout = args.get(i).and_then(|v| v.parse().ok());
93 },
94 "-x" => overrides.fail_fast = true,
95 "--pass-with-no-tests" => overrides.pass_with_no_tests = true,
96 "--ignore-snapshots" => overrides.ignore_snapshots = true,
97 "--tsconfig" => {
98 i += 1;
99 overrides.tsconfig = args.get(i).cloned();
100 },
101 "--fully-parallel" => overrides.fully_parallel = Some(true),
102 "--project" => {
103 i += 1;
104 if let Some(name) = args.get(i) {
105 overrides.project_filter.push(name.clone());
106 }
107 },
108 "--no-deps" => overrides.no_deps = true,
109 "--teardown" => {
110 i += 1;
111 overrides.teardown = args.get(i).cloned();
112 },
113 "--only-changed" => {
114 let next = args.get(i + 1).cloned();
115 match next {
116 Some(value) if !value.starts_with('-') => {
117 i += 1;
118 overrides.only_changed = Some(value);
119 },
120 _ => overrides.only_changed = Some(String::new()),
121 }
122 },
123 "--fail-on-flaky-tests" => overrides.fail_on_flaky_tests = true,
124 "--profile" => {
125 i += 1;
126 overrides.profile = args.get(i).cloned();
127 },
128 "--tags" | "-t" => {
129 i += 1;
130 overrides.bdd_tags = args.get(i).cloned();
131 },
132 "--dry-run" => overrides.bdd_dry_run = true,
133 "--strict" => overrides.bdd_strict = true,
134 "--fail-fast" => overrides.bdd_fail_fast = true,
135 "--step-timeout" => {
136 i += 1;
137 overrides.bdd_step_timeout = args.get(i).and_then(|v| v.parse().ok());
138 },
139 "--order" => {
140 i += 1;
141 overrides.bdd_order = args.get(i).cloned();
142 },
143 "--language" => {
144 i += 1;
145 overrides.bdd_language = args.get(i).cloned();
146 },
147 _ => {},
148 }
149 i += 1;
150 }
151 overrides
152}
153
154pub fn resolve_config(overrides: &CliOverrides) -> ferridriver::error::Result<TestConfig> {
167 use ferridriver::FerriError;
168 let cfg = if let Some(path) = &overrides.config_path {
169 ferridriver_config::FerridriverConfig::load_from(Path::new(path)).map_err(|e| FerriError::backend(e.to_string()))?
170 } else {
171 ferridriver_config::FerridriverConfig::load(None).map_err(|e| FerriError::backend(e.to_string()))?
172 };
173 resolve_config_from(cfg.test, overrides)
174}
175
176pub fn resolve_config_from(mut config: TestConfig, overrides: &CliOverrides) -> ferridriver::error::Result<TestConfig> {
186 use ferridriver::FerriError;
187 if let Some(profile_name) = &overrides.profile {
189 if let Some(profile_value) = config.profiles.get(profile_name) {
190 let mut base = serde_json::to_value(&config)?;
191 json_merge(&mut base, profile_value);
192 config = serde_json::from_value(base)?;
193 } else {
194 return Err(FerriError::invalid_argument(
195 "profile",
196 format!("profile '{profile_name}' not found in config"),
197 ));
198 }
199 }
200
201 if let Ok(w) = std::env::var("FERRIDRIVER_WORKERS") {
203 if let Ok(v) = w.parse() {
204 config.workers = v;
205 }
206 }
207 if let Ok(r) = std::env::var("FERRIDRIVER_RETRIES") {
208 if let Ok(v) = r.parse() {
209 config.retries = v;
210 }
211 }
212 if let Ok(t) = std::env::var("FERRIDRIVER_TIMEOUT") {
213 if let Ok(v) = t.parse() {
214 config.timeout = v;
215 }
216 }
217 if let Ok(b) = std::env::var("FERRIDRIVER_BACKEND") {
218 config.browser.backend = b;
219 }
220
221 if let Some(w) = overrides.workers {
223 config.workers = w;
224 }
225 if let Some(t) = overrides.timeout {
226 config.timeout = t;
227 }
228 if let Some(r) = overrides.retries {
229 config.retries = r;
230 }
231 if !overrides.reporter.is_empty() {
232 config.reporter = overrides
233 .reporter
234 .iter()
235 .map(|name| ReporterConfig {
236 name: name.clone(),
237 options: std::collections::BTreeMap::new(),
238 })
239 .collect();
240 }
241 if overrides.headless {
242 config.browser.headless = true;
243 }
244 if let Some(ref b) = overrides.browser {
245 config.browser.browser.clone_from(b);
246 }
247 if let Some(ref b) = overrides.backend {
248 config.browser.backend.clone_from(b);
249 }
250 if let Some(ref ch) = overrides.channel {
251 config.browser.channel = Some(ch.clone());
252 }
253 if let Some(ref p) = overrides.executable_path {
254 config.browser.executable_path = Some(p.clone());
255 }
256 if !overrides.browser_args.is_empty() {
257 config.browser.args.clone_from(&overrides.browser_args);
258 }
259 if let Some(ref url) = overrides.base_url {
260 config.base_url = Some(url.clone());
261 }
262 if let Some(w) = overrides.viewport_width {
263 if let Some(ref mut vp) = config.browser.viewport {
264 vp.width = w;
265 }
266 }
267 if let Some(h) = overrides.viewport_height {
268 if let Some(ref mut vp) = config.browser.viewport {
269 vp.height = h;
270 }
271 }
272 if let Some(m) = overrides.is_mobile {
273 config.browser.use_options.is_mobile = m;
274 }
275 if let Some(t) = overrides.has_touch {
276 config.browser.use_options.has_touch = t;
277 }
278 if let Some(ref cs) = overrides.color_scheme {
279 config.browser.use_options.color_scheme = Some(cs.clone());
280 }
281 if let Some(ref l) = overrides.locale {
282 config.browser.use_options.locale = Some(l.clone());
283 }
284 if let Some(o) = overrides.offline {
285 config.browser.use_options.offline = o;
286 }
287 if let Some(b) = overrides.bypass_csp {
288 config.browser.use_options.bypass_csp = b;
289 }
290 if let Some(dir) = &overrides.output_dir {
291 config.output_dir = PathBuf::from(dir);
292 }
293 if let Some(ref patterns) = overrides.test_match {
294 config.test_match.clone_from(patterns);
295 }
296 if overrides.forbid_only {
297 config.forbid_only = true;
298 }
299 if let Some(video) = &overrides.video {
300 config.video.mode = VideoMode::parse_label(video);
301 }
302 if let Some(trace) = &overrides.trace {
303 config.trace = TraceMode::parse_label(trace);
304 }
305 if let Some(ref ss) = overrides.storage_state {
306 config.storage_state = Some(ss.clone());
307 }
308 if let Some(mode) = overrides.update_snapshots {
309 config.update_snapshots = mode;
310 }
311 if let Some(n) = overrides.max_failures {
312 config.max_failures = n;
313 }
314 if let Some(n) = overrides.repeat_each {
315 config.repeat_each = n;
316 }
317 if overrides.fail_fast {
318 config.fail_fast = true;
319 }
320 if let Some(t) = overrides.global_timeout {
321 config.global_timeout = t;
322 }
323 if overrides.ignore_snapshots {
324 config.ignore_snapshots = true;
325 }
326 if overrides.pass_with_no_tests {
327 config.pass_with_no_tests = true;
328 }
329 if let Some(ref ts) = overrides.tsconfig {
330 config.tsconfig = Some(ts.clone());
331 }
332 if let Some(ref n) = overrides.name {
333 config.name = Some(n.clone());
334 }
335 if let Some(p) = overrides.fully_parallel {
336 config.fully_parallel = p;
337 }
338 if overrides.fail_on_flaky_tests {
339 config.fail_on_flaky_tests = true;
340 }
341 if let Ok(t) = std::env::var("FERRIDRIVER_GLOBAL_TIMEOUT") {
342 if let Ok(v) = t.parse() {
343 config.global_timeout = v;
344 }
345 }
346 if let Ok(v) = std::env::var("FERRIDRIVER_VIDEO") {
347 config.video.mode = VideoMode::parse_label(&v);
348 }
349 if let Ok(t) = std::env::var("FERRIDRIVER_TRACE") {
350 config.trace = TraceMode::parse_label(&t);
351 }
352
353 if config.workers == 0 {
355 let cpus = std::thread::available_parallelism()
356 .map(|n| n.get() as u32)
357 .unwrap_or(4);
358 config.workers = (cpus / 2).max(1);
359 }
360
361 config.browser.normalize();
363
364 Ok(config)
365}
366
367fn json_merge(base: &mut serde_json::Value, overlay: &serde_json::Value) {
368 match (base, overlay) {
369 (serde_json::Value::Object(base_map), serde_json::Value::Object(overlay_map)) => {
370 for (key, value) in overlay_map {
371 if let Some(existing) = base_map.get_mut(key) {
372 json_merge(existing, value);
373 } else {
374 base_map.insert(key.clone(), value.clone());
375 }
376 }
377 },
378 (base, _) => {
379 *base = overlay.clone();
380 },
381 }
382}