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