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 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 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}