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