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) filter: &'static [&'static str],
28}
29
30#[derive(Debug, Parser)]
31#[command(version)]
32// , separated by space
33struct 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}