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