cargo_regression/
regression.rs

1use core::fmt;
2use std::{
3  io,
4  path::PathBuf,
5  process::{ExitCode, Termination},
6  sync::{
7    Arc,
8    atomic::{AtomicUsize, Ordering},
9  },
10  time::{Duration, Instant},
11};
12
13use colored::Colorize;
14use itertools::{Either, Itertools};
15use tokio::{
16  fs::remove_dir_all,
17  sync::{Mutex, Semaphore},
18};
19
20use crate::{
21  Args,
22  assert::{AssertError, DisplayErrs},
23  config::FullConfig,
24};
25
26pub(crate) const GOLDEN_DIR: &str = "__golden__";
27
28#[derive(Debug, thiserror::Error)]
29pub enum BuildError {
30  #[error("file \"{0}\": {1}")]
31  Toml(PathBuf, toml::de::Error),
32  #[error("task \"{0}\": its permit = {1}, exceed total permits = {2}")]
33  PermitEcxceed(PathBuf, u32, u32),
34  #[error("task \"{0}\": need to specify '{1}'")]
35  MissConfig(PathBuf, &'static str),
36  #[error("file \"{0}\": {1}")]
37  UnableToRead(PathBuf, io::Error),
38  #[error("read dir \"{0}\": {1}")]
39  ReadDir(PathBuf, io::Error),
40  #[error("clean dir \"{0}\": {1}")]
41  CleanDir(PathBuf, io::Error),
42  #[error("input extensions can not contains 'toml'")]
43  InputExtToml,
44}
45
46#[derive(Debug)]
47pub(crate) enum FailedState {
48  ReportSaved(PathBuf),
49  NoReport(PathBuf, Vec<AssertError>),
50}
51pub(crate) enum State {
52  Ok(Option<Duration>),
53  Failed(Option<(FailedState, Duration)>),
54  Ignored,
55  FilteredOut,
56}
57
58impl fmt::Display for FailedState {
59  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60    match self {
61      Self::ReportSaved(report) => {
62        write!(f, "\n     report: {}", report.display())
63      }
64      Self::NoReport(input, errs) => {
65        write!(f, "\n----------- {} -----------\n{}", input.display(), DisplayErrs(errs))
66      }
67    }
68  }
69}
70impl fmt::Display for State {
71  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72    match self {
73      Self::Ok(None) => write!(f, "{}", "ok".green()),
74      Self::Ok(Some(time)) => write!(f, "{:.2}s {}", time.as_secs_f32(), "ok".green()),
75      Self::Failed(Some((_, time))) => {
76        write!(f, "{:.2}s {}", time.as_secs_f32(), "FAILED".red())
77      }
78      Self::Failed(None) => write!(f, "{}", "FAILED".red()),
79      Self::Ignored => write!(f, "{}", "ignored".yellow()),
80      Self::FilteredOut => write!(f, "{}", "filtered out".bright_black()),
81    }
82  }
83}
84
85pub(crate) struct TestResult {
86  count_ok: usize,
87  count_ignored: usize,
88  count_filtered: usize,
89  faileds: Vec<FailedState>,
90}
91
92pub struct TestExitCode(Result<TestResult, Vec<BuildError>>, Instant);
93
94impl Termination for TestExitCode {
95  fn report(self) -> ExitCode {
96    let time = self.1.elapsed().as_secs_f32();
97    match self.0 {
98      Ok(TestResult { count_ok, count_ignored, count_filtered, faileds }) => {
99        println!();
100        let failed_num = faileds.len();
101        if failed_num == 0 {
102          println!(
103            "test result: {}. {count_ok} passed; {failed_num} failed; {count_ignored} ignored; {count_filtered} filtered out; finished in {time:.2}s",
104            State::Ok(None)
105          );
106          ExitCode::SUCCESS
107        } else {
108          eprint!("failures:");
109          for failed in &faileds {
110            eprint!("{failed}");
111          }
112          eprintln!(
113            "\n\ntest result: {}. {count_ok} passed; {failed_num} failed; {count_ignored} ignored; {count_filtered} filtered out; finished in {time:.2}s",
114            State::Failed(None)
115          );
116          ExitCode::FAILURE
117        }
118      }
119      Err(build_errs) => {
120        eprintln!("Fail to build test:");
121        for err in &build_errs {
122          eprintln!("{err}");
123        }
124        ExitCode::FAILURE
125      }
126    }
127  }
128}
129
130impl Args {
131  pub async fn test(self) -> TestExitCode {
132    let now = Instant::now();
133    TestExitCode(
134      match self.rebuild() {
135        Ok(args) => _test(args).await,
136        Err(e) => Err(vec![e]),
137      },
138      now,
139    )
140  }
141}
142async fn _test(args: &'static Args) -> Result<TestResult, Vec<BuildError>> {
143  let f1 = async {
144    if args.workdir.exists() {
145      remove_dir_all(&args.workdir)
146        .await
147        .map_err(|e| BuildError::CleanDir(args.workdir.to_path_buf(), e))
148    } else {
149      Ok(())
150    }
151  };
152  let f2 = walk(FullConfig::new(args), args.rootdir.to_path_buf(), args);
153  // walkthrough all config
154  let (clean_dir, file_configs) = tokio::join!(f1, f2);
155  if let Err(e) = clean_dir {
156    return Err(vec![e]);
157  }
158  let file_configs = file_configs?;
159  let count_ok = Arc::new(AtomicUsize::new(0));
160  let count_ignored = Arc::new(AtomicUsize::new(0));
161  let count_filtered = Arc::new(AtomicUsize::new(0));
162  let faileds = Arc::new(Mutex::new(Vec::with_capacity(file_configs.len())));
163  let scheduler = Arc::new(Semaphore::new(args.permits as usize));
164  let handles: Vec<_> = file_configs
165    .into_iter()
166    .map(|(path, config)| {
167      let scheduler = scheduler.clone();
168      let count_ok = count_ok.clone();
169      let count_ignored = count_ignored.clone();
170      let count_filtered = count_filtered.clone();
171      let faileds = faileds.clone();
172      tokio::spawn(async move {
173        let _permit = scheduler
174          .acquire_many(*config.permit)
175          .await
176          .expect("Semaphore closed");
177        let state = config.test(&path, args).await;
178        println!("test {} ... {}", path.display(), state);
179        match state {
180          State::Ok(Some(_)) => _ = count_ok.fetch_add(1, Ordering::Release),
181          State::Failed(Some((failed, _))) => faileds.lock().await.push(failed),
182          State::Ok(None) | State::Failed(None) => unreachable!(),
183          State::Ignored => _ = count_ignored.fetch_add(1, Ordering::Release),
184          State::FilteredOut => _ = count_filtered.fetch_add(1, Ordering::Release),
185        }
186      })
187    })
188    .collect();
189  for handle in handles {
190    handle.await.unwrap();
191  }
192  scheduler.close();
193  Ok(TestResult {
194    count_ok: count_ok.load(Ordering::Relaxed),
195    count_ignored: count_ignored.load(Ordering::Relaxed),
196    count_filtered: count_filtered.load(Ordering::Relaxed),
197    faileds: Arc::try_unwrap(faileds).unwrap().into_inner(),
198  })
199}
200
201#[async_recursion::async_recursion]
202async fn walk(
203  mut current_config: FullConfig,
204  current_path: PathBuf,
205  args: &'static Args,
206) -> Result<Vec<(PathBuf, FullConfig)>, Vec<BuildError>> {
207  let all_path = current_path.join("__all__.toml");
208  if all_path.exists() {
209    match current_config.update(&all_path, !args.nodebug) {
210      Ok(_config) => current_config = _config,
211      Err(e) => return Err(vec![e]),
212    }
213  }
214  let read_dir = match current_path.read_dir() {
215    Ok(read_dir) => read_dir,
216    Err(e) => return Err(vec![BuildError::ReadDir(current_path, e)]),
217  };
218  let (sub_dir_futures, files): (Vec<_>, Vec<_>) =
219    read_dir.into_iter().partition_map(|entry| {
220      let path = entry.unwrap().path();
221      if path.is_dir() {
222        if path.file_name().unwrap() == GOLDEN_DIR {
223          Either::Left(None)
224        } else {
225          let current_config = current_config.clone();
226          Either::Left(Some(tokio::spawn(walk(current_config, path, args))))
227        }
228      } else {
229        Either::Right(path)
230      }
231    });
232  let mut errs = Vec::new();
233  let mut file_configs = files
234    .into_iter()
235    .filter_map(|file| {
236      if current_config.match_extension(&file) {
237        match args.filtered(&file) {
238          Ok(filtered) => {
239            if filtered {
240              Some((file, FullConfig::new_filtered()))
241            } else {
242              let config_file = file.with_extension("toml");
243              let current_config = current_config.clone();
244              if config_file.is_file() {
245                match current_config.update(&config_file, !args.nodebug) {
246                  Ok(config) => Some((file, config)),
247                  Err(e) => {
248                    errs.push(e);
249                    None
250                  }
251                }
252              } else {
253                Some((file, current_config))
254              }
255              .and_then(|(file, config)| match config.eval(&file, args) {
256                Ok(config) => Some((file, config)),
257                Err(e) => {
258                  errs.push(e);
259                  None
260                }
261              })
262            }
263          }
264          Err(e) => {
265            errs.push(e);
266            None
267          }
268        }
269      } else {
270        None
271      }
272    })
273    .collect::<Vec<_>>();
274  for f in sub_dir_futures.into_iter().flatten() {
275    match f.await.expect("join handle") {
276      Ok(res) => file_configs.extend(res),
277      Err(e) => errs.extend(e),
278    }
279  }
280  if errs.is_empty() { Ok(file_configs) } else { Err(errs) }
281}