Skip to main content

allwright/
client_config.rs

1use 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}