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 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 current_config.preprocess.clear();
192 current_config.postprocess.clear();
193 let all_path = current_path.join("__all__.toml");
194 if all_path.exists() {
195 match current_config.update(&all_path, !args.nodebug) {
196 Ok(_config) => current_config = _config,
197 Err(e) => return Err(vec![e]),
198 }
199 }
200 let read_dir = match current_path.read_dir() {
201 Ok(read_dir) => read_dir,
202 Err(e) => return Err(vec![BuildError::ReadDir(current_path, e)]),
203 };
204 let (sub_dir_futures, files): (Vec<_>, Vec<_>) =
205 read_dir.into_iter().partition_map(|entry| {
206 let path = entry.unwrap().path();
207 if path.is_dir() {
208 if path.file_name().unwrap() == GOLDEN_DIR {
209 Either::Left(None)
210 } else {
211 let current_config = current_config.clone();
212 Either::Left(Some(tokio::spawn(walk(current_config, path, args))))
213 }
214 } else {
215 Either::Right(path)
216 }
217 });
218 let mut errs = Vec::new();
219 let mut file_configs = files
220 .into_iter()
221 .filter_map(|file| {
222 if current_config.match_extension(&file) {
223 match args.filtered(&file) {
224 Ok(filtered) => {
225 if filtered {
226 Some((file, FullConfig::new_filtered()))
227 } else {
228 let config_file = file.with_extension("toml");
229 let current_config = current_config.clone();
230 if config_file.is_file() {
231 match current_config.update(&config_file, !args.nodebug) {
232 Ok(config) => Some((file, config)),
233 Err(e) => {
234 errs.push(e);
235 None
236 }
237 }
238 } else {
239 Some((file, current_config))
240 }
241 .and_then(|(file, config)| match config.eval(&file, args) {
242 Ok(config) => Some((file, config)),
243 Err(e) => {
244 errs.push(e);
245 None
246 }
247 })
248 }
249 }
250 Err(e) => {
251 errs.push(e);
252 None
253 }
254 }
255 } else {
256 None
257 }
258 })
259 .collect::<Vec<_>>();
260 for f in sub_dir_futures.into_iter().flatten() {
261 match f.await.expect("join handle") {
262 Ok(res) => file_configs.extend(res),
263 Err(e) => errs.extend(e),
264 }
265 }
266 if errs.is_empty() { Ok(file_configs) } else { Err(errs) }
267}