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        let failed_num = faileds.len();
92        let (code, state) = if failed_num == 0 {
93          (ExitCode::SUCCESS, State::Ok(None))
94        } else {
95          print!("\nfailures:");
96          for failed in &faileds {
97            print!("{failed}");
98          }
99          println!();
100          (ExitCode::FAILURE, State::Failed(None))
101        };
102        println!("\ntest result: {state}. {count_ok} passed; {failed_num} failed; {count_ignored} ignored; {count_filtered} filtered out; finished in {time:.2}s");
103        code
104      }
105      Err(build_errs) => {
106        println!("Fail to build test:");
107        for err in &build_errs {
108          println!("{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 =
139    async move { walk(FullConfig::new(args), args.rootdir.to_path_buf(), args).await };
140  // walkthrough all config
141  let (clean_dir, file_configs) = tokio::join!(f1, f2);
142  if let Err(e) = clean_dir {
143    return Err(vec![e]);
144  }
145  let scheduler = Arc::new(Semaphore::new(args.permits as usize));
146  let handles: Vec<_> = file_configs?
147    .into_iter()
148    .map(|(path, config)| {
149      let scheduler = scheduler.clone();
150      tokio::spawn(async move {
151        let _permit = scheduler
152          .acquire_many(*config.permit)
153          .await
154          .expect("Semaphore closed");
155        let state = config.test(&path, args).await;
156        println!("test {} ... {}", path.display(), state);
157        state
158      })
159    })
160    .collect();
161
162  let mut count_ok = 0;
163  let mut count_ignored = 0;
164  let mut count_filtered = 0;
165  let mut faileds = Vec::with_capacity(handles.len());
166  // TODO: iter
167  for handle in handles {
168    match handle.await.unwrap() {
169      State::Ok(Some(_)) => count_ok += 1,
170      State::Failed(Some((failed, _))) => faileds.push(failed),
171      State::Ok(None) | State::Failed(None) => unreachable!(),
172      State::Ignored => count_ignored += 1,
173      State::FilteredOut => count_filtered += 1,
174    }
175  }
176  scheduler.close();
177  Ok(TestResult { count_ok, count_ignored, count_filtered, faileds })
178}
179
180#[async_recursion::async_recursion]
181async fn walk(
182  mut current_config: FullConfig,
183  current_path: PathBuf,
184  args: &'static Args,
185) -> Result<Vec<(PathBuf, FullConfig)>, Vec<BuildError>> {
186  let all_path = current_path.join("__all__.toml");
187  if all_path.exists() {
188    match current_config.update(&all_path, args.debug) {
189      Ok(_config) => current_config = _config,
190      Err(e) => return Err(vec![e]),
191    }
192  }
193  let read_dir = match current_path.read_dir() {
194    Ok(read_dir) => read_dir,
195    Err(e) => return Err(vec![BuildError::ReadDir(current_path, e)]),
196  };
197  let (sub_dir_futures, files): (Vec<_>, Vec<_>) =
198    read_dir.into_iter().partition_map(|entry| {
199      let path = entry.unwrap().path();
200      if path.is_dir() {
201        if path.file_name().unwrap() == GOLDEN_DIR {
202          Either::Left(None)
203        } else {
204          let current_config = current_config.clone();
205          Either::Left(Some(tokio::spawn(async move {
206            walk(current_config, path, args).await
207          })))
208        }
209      } else {
210        Either::Right(path)
211      }
212    });
213  let mut errs = Vec::new();
214  let mut file_configs = files
215    .into_iter()
216    .filter_map(|file| {
217      if current_config.match_extension(&file) {
218        match args.filtered(&file) {
219          Ok(filtered) => {
220            if filtered {
221              Some((file, FullConfig::new_filtered()))
222            } else {
223              let config_file = file.with_extension("toml");
224              let current_config = current_config.clone();
225              if config_file.is_file() {
226                match current_config.update(&config_file, args.debug) {
227                  Ok(config) => Some((file, config)),
228                  Err(e) => {
229                    errs.push(e);
230                    None
231                  }
232                }
233              } else {
234                Some((file, current_config))
235              }
236              .and_then(|(file, config)| match config.eval(&file, args) {
237                Ok(config) => Some((file, config)),
238                Err(e) => {
239                  errs.push(e);
240                  None
241                }
242              })
243            }
244          }
245          Err(e) => {
246            errs.push(e);
247            None
248          }
249        }
250      } else {
251        None
252      }
253    })
254    .collect::<Vec<_>>();
255  for f in sub_dir_futures.into_iter().flatten() {
256    match f.await.expect("join handle") {
257      Ok(res) => file_configs.extend(res),
258      Err(e) => errs.extend(e),
259    }
260  }
261  if errs.is_empty() {
262    Ok(file_configs)
263  } else {
264    Err(errs)
265  }
266}