cargo_regression/
args.rs

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  /// to schedule tasks, default is 1
17  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  // TODO: use static hashset
25  pub(crate) extensions: &'static [&'static str],
26  // TODO: use static hashset
27  pub(crate) include: &'static [&'static str],
28  pub(crate) exclude: &'static [&'static str],
29}
30
31#[derive(Debug, Parser)]
32#[command(version)]
33// , separated by space
34struct 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}