allwright/
client_config.rs1use std::fs;
2use std::path::{Path, PathBuf};
3
4use super::command::merge_retry_config;
5use super::runtime::set_server_addr;
6use super::launch::launch_browser;
7use super::types::{
8 AllwrightConfig, BrowserKind, Error, LaunchOptions, ResolveConfigOptions, ResolvedConfig,
9 Result,
10};
11
12const CONFIG_FILENAMES: [&str; 6] = [
13 "allwright.config.yaml",
14 "allwright.config.yml",
15 "allwright.config.json",
16 ".allwright/config.yaml",
17 ".allwright/config.yml",
18 ".allwright/config.json",
19];
20
21pub fn find_config_file(start_dir: impl AsRef<Path>) -> Option<PathBuf> {
22 let mut current_dir = start_dir.as_ref().to_path_buf();
23
24 loop {
25 for filename in CONFIG_FILENAMES {
26 let candidate = current_dir.join(filename);
27 if candidate.is_file() {
28 return Some(candidate);
29 }
30 }
31
32 if !current_dir.pop() {
33 return None;
34 }
35 }
36}
37
38pub fn load_config_file(config_file: impl AsRef<Path>) -> Result<AllwrightConfig> {
39 let resolved = config_file.as_ref().to_path_buf();
40 let raw = fs::read_to_string(&resolved).map_err(|error| {
41 Error::new(format!(
42 "failed to read allwright config {}: {error}",
43 resolved.display()
44 ))
45 })?;
46
47 let extension = resolved
48 .extension()
49 .and_then(|value| value.to_str())
50 .unwrap_or_default()
51 .to_ascii_lowercase();
52
53 let config = match extension.as_str() {
54 "json" => serde_json::from_str::<AllwrightConfig>(&raw).map_err(|error| {
55 Error::new(format!(
56 "failed to parse allwright config {} as JSON: {error}",
57 resolved.display()
58 ))
59 })?,
60 "yaml" | "yml" => serde_yaml::from_str::<AllwrightConfig>(&raw).map_err(|error| {
61 Error::new(format!(
62 "failed to parse allwright config {} as YAML: {error}",
63 resolved.display()
64 ))
65 })?,
66 _ => {
67 return Err(Error::new(format!(
68 "unsupported allwright config file extension .{} for {}",
69 if extension.is_empty() {
70 "<none>"
71 } else {
72 &extension
73 },
74 resolved.display()
75 )));
76 }
77 };
78
79 validate_config_shape(&config, &resolved)?;
80 Ok(config)
81}
82
83pub fn resolve_config(options: ResolveConfigOptions) -> Result<ResolvedConfig> {
84 let cwd = options.cwd.unwrap_or(std::env::current_dir().map_err(|error| {
85 Error::new(format!(
86 "failed to determine current working directory: {error}"
87 ))
88 })?);
89 let config_file_path = match options.config_file {
90 Some(path) => Some(path),
91 None => find_config_file(cwd),
92 };
93 let file_config = match &config_file_path {
94 Some(path) => load_config_file(path)?,
95 None => AllwrightConfig::default(),
96 };
97 let suite_name = options.suite.and_then(|suite| {
98 let trimmed = suite.trim().to_owned();
99 if trimmed.is_empty() {
100 None
101 } else {
102 Some(trimmed)
103 }
104 });
105 let suite_config = match &suite_name {
106 Some(name) => {
107 let suite = file_config
108 .suites
109 .as_ref()
110 .and_then(|suites| suites.get(name))
111 .cloned();
112 if suite.is_none() {
113 return Err(Error::new(format!(
114 "allwright config suite \"{}\" was not found in {}",
115 name,
116 config_file_path
117 .as_ref()
118 .map(|path| path.display().to_string())
119 .unwrap_or_else(|| "the resolved config file".to_string())
120 )));
121 }
122 suite
123 }
124 None => None,
125 };
126
127 let server_addr = suite_config
128 .as_ref()
129 .and_then(|suite| suite.server.as_ref())
130 .and_then(|server| server.addr.clone())
131 .or_else(|| {
132 file_config
133 .server
134 .as_ref()
135 .and_then(|server| server.addr.clone())
136 });
137 let browser_name = suite_config
138 .as_ref()
139 .and_then(|suite| suite.browser.as_ref())
140 .and_then(|browser| browser.name)
141 .or_else(|| {
142 file_config
143 .browser
144 .as_ref()
145 .and_then(|browser| browser.name)
146 })
147 .unwrap_or(BrowserKind::Chromium);
148 let browser_binary = suite_config
149 .as_ref()
150 .and_then(|suite| suite.browser.as_ref())
151 .and_then(|browser| browser.binary.clone())
152 .or_else(|| {
153 file_config
154 .browser
155 .as_ref()
156 .and_then(|browser| browser.binary.clone())
157 });
158 let mut launch_options = merge_launch_options(
159 file_config
160 .browser
161 .as_ref()
162 .and_then(|browser| browser.launch_options.clone()),
163 suite_config
164 .as_ref()
165 .and_then(|suite| suite.browser.as_ref())
166 .and_then(|browser| browser.launch_options.clone()),
167 );
168 if let Some(binary) = &browser_binary {
169 launch_options.browser_binary = Some(binary.clone());
170 }
171 let expect = merge_retry_config(
172 file_config.expect.clone(),
173 suite_config.and_then(|suite| suite.expect),
174 );
175
176 Ok(ResolvedConfig {
177 config_file_path,
178 suite_name,
179 server_addr,
180 browser_name,
181 browser_binary,
182 launch_options,
183 expect,
184 })
185}
186
187pub async fn launch_configured_browser(config: &ResolvedConfig) -> Result<super::types::Browser> {
188 if let Some(server_addr) = &config.server_addr {
189 set_server_addr(server_addr.clone())?;
190 }
191 launch_browser(config.browser_name, config.launch_options.clone()).await
192}
193
194fn merge_launch_options(
195 base: Option<LaunchOptions>,
196 override_options: Option<LaunchOptions>,
197) -> LaunchOptions {
198 let mut merged = base.unwrap_or_default();
199 if let Some(override_options) = override_options {
200 if override_options.browser_binary.is_some() {
201 merged.browser_binary = override_options.browser_binary;
202 }
203 if override_options.timeout_ms.is_some() {
204 merged.timeout_ms = override_options.timeout_ms;
205 }
206 }
207 merged
208}
209
210fn validate_config_shape(config: &AllwrightConfig, source: &Path) -> Result<()> {
211 if let Some(schema_version) = config.schema_version {
212 if schema_version != 1 {
213 return Err(Error::new(format!(
214 "allwright config {} has unsupported schemaVersion {}; expected 1",
215 source.display(),
216 schema_version
217 )));
218 }
219 }
220 Ok(())
221}