cargo_regression/
regression.rs

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