1use clap::Parser;
2use derive_setters::Setters;
3use std::{
4 ffi::{OsStr, OsString},
5 path::{Path, PathBuf},
6};
7
8use crate::regression::BuildError;
9
10#[derive(Debug, Clone, Copy)]
11#[derive(Setters)]
12pub struct Args {
13 pub(crate) debug: bool,
14 pub(crate) regolden: bool,
15 pub(crate) print_errs: bool,
16 pub(crate) permits: u32,
18 pub(crate) exe_path: &'static str,
19 pub(crate) args: &'static [&'static str],
20 pub(crate) work_dir: &'static str,
21 pub(crate) root_dir: &'static str,
22 #[setters(skip)]
23 pub(crate) root_dir_abs: &'static str,
24 pub(crate) extensions: &'static [&'static str],
26 pub(crate) filter: &'static [&'static str],
28}
29
30#[derive(Debug, Parser)]
31#[command(version)]
32struct ArgsBuilder {
34 #[clap(long, help = "Debug mode flag, recommended")]
35 debug: bool,
36 #[clap(long, help = "Regolden mode flag")]
37 regolden: bool,
38 #[clap(long, help = "Print errors [default: false, save errs to report]")]
39 print_errs: bool,
40 #[clap(long, help = "Default executable path")]
41 exe_path: Option<String>,
42 #[clap(long, help = "Default arguements", num_args = 1..)]
43 args: Vec<String>,
44 #[clap(long, help="Default input extensions(s)", num_args = 1..)]
45 extensions: Vec<String>,
46 #[clap(long, help="Input filter. E.g., --filter ./cases/*", default_value = None, num_args = 1..)]
47 filter: Vec<String>,
48 #[clap(long, help = "Total permits to limit max parallelism", default_value_t = 1)]
49 permits: u32,
50 #[clap(long, default_value_t = String::from("./tmp"))]
51 work_dir: String,
52 #[clap(value_parser)]
53 root_dir: String,
54}
55
56impl Default for Args {
57 fn default() -> Self {
58 Self::new()
59 }
60}
61
62impl Args {
63 pub const fn new() -> Self {
64 Self {
65 debug: false,
66 print_errs: false,
67 regolden: false,
68 permits: 1,
69 exe_path: "",
70 args: &[],
71 work_dir: "",
72 root_dir: "",
73 root_dir_abs: "",
74 extensions: &[],
75 filter: &[],
76 }
77 }
78 pub(crate) fn rebuild(mut self) -> Result<Self, BuildError> {
79 self.root_dir_abs = leak_string(
80 std::fs::canonicalize(self.root_dir)
81 .map_err(|e| BuildError::ReadDir(PathBuf::from(&self.root_dir), e))?
82 .display()
83 .to_string(),
84 );
85 self.filter = leak_string_vec_res(self.filter.iter().map(|path| {
86 let path = PathBuf::from(path);
87 match std::fs::canonicalize(&path) {
88 Ok(p) => Ok(p.display().to_string()),
89 Err(e) => Err(BuildError::ReadDir(PathBuf::from(&path), e)),
90 }
91 }))?;
92 if self.extensions.iter().any(|&s| s == "toml") {
93 return Err(BuildError::InputExtToml);
94 }
95 Ok(self)
96 }
97 pub fn parse_from<I, T>(itr: I) -> Self
98 where
99 I: IntoIterator<Item = T>,
100 T: Into<OsString> + Clone,
101 {
102 let builder = ArgsBuilder::parse_from(itr);
103 let mut args = Args::new();
104 if let Some(exe_path) = builder.exe_path {
105 args = args.exe_path(leak_string(exe_path));
106 }
107 args
108 .permits(builder.permits)
109 .regolden(builder.regolden)
110 .print_errs(builder.print_errs)
111 .debug(builder.debug)
112 .args(leak_string_vec(builder.args))
113 .extensions(leak_string_vec(builder.extensions))
114 .filter(leak_string_vec(builder.filter))
115 .work_dir(leak_string(builder.work_dir))
116 .root_dir(leak_string(builder.root_dir))
117 }
118 pub(super) fn filtered(&self, file: &Path) -> Result<bool, BuildError> {
119 if self.filter.is_empty() {
120 Ok(false)
121 } else {
122 let file_abs = std::fs::canonicalize(file)
123 .map_err(|e| BuildError::ReadDir(PathBuf::from(&file), e))?
124 .display()
125 .to_string();
126 Ok(!self.filter.iter().any(|pattern| *pattern == file_abs))
127 }
128 }
129}
130
131fn leak_string(s: String) -> &'static str {
132 Box::leak(s.into_boxed_str())
133}
134fn leak_string_vec(iter: impl IntoIterator<Item = String>) -> &'static [&'static str] {
135 Box::leak(
136 iter
137 .into_iter()
138 .map(leak_string)
139 .collect::<Vec<_>>()
140 .into_boxed_slice(),
141 )
142}
143fn leak_string_vec_res<E>(
144 iter: impl IntoIterator<Item = Result<String, E>>,
145) -> Result<&'static [&'static str], E> {
146 Ok(Box::leak(
147 iter
148 .into_iter()
149 .map(|res| res.map(leak_string))
150 .collect::<Result<Vec<_>, _>>()?
151 .into_boxed_slice(),
152 ))
153}
154
155pub(crate) fn match_extension<I: Iterator<Item = impl AsRef<str>>>(
156 file: &Path,
157 extensions: I,
158) -> bool {
159 file
160 .extension()
161 .and_then(OsStr::to_str)
162 .and_then(|s| {
163 if extensions.into_iter().any(|ext_s| ext_s.as_ref() == s) {
164 Some(())
165 } else {
166 None
167 }
168 })
169 .is_some()
170}