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