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 println!();
92 let failed_num = faileds.len();
93 if failed_num == 0 {
94 println!("test result: {}. {count_ok} passed; {failed_num} failed; {count_ignored} ignored; {count_filtered} filtered out; finished in {time:.2}s", State::Ok(None));
95 ExitCode::SUCCESS
96 } else {
97 eprint!("failures:");
98 for failed in &faileds {
99 eprint!("{failed}");
100 }
101 eprintln!("\n\ntest result: {}. {count_ok} passed; {failed_num} failed; {count_ignored} ignored; {count_filtered} filtered out; finished in {time:.2}s", State::Ok(None));
102 ExitCode::FAILURE
103 }
104 }
105 Err(build_errs) => {
106 eprintln!("Fail to build test:");
107 for err in &build_errs {
108 eprintln!("{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 = walk(FullConfig::new(args), args.rootdir.to_path_buf(), args);
139 let (clean_dir, file_configs) = tokio::join!(f1, f2);
141 if let Err(e) = clean_dir {
142 return Err(vec![e]);
143 }
144 let scheduler = Arc::new(Semaphore::new(args.permits as usize));
145 let handles: Vec<_> = file_configs?
146 .into_iter()
147 .map(|(path, config)| {
148 let scheduler = scheduler.clone();
149 tokio::spawn(async move {
150 let _permit = scheduler
151 .acquire_many(*config.permit)
152 .await
153 .expect("Semaphore closed");
154 let state = config.test(&path, args).await;
155 println!("test {} ... {}", path.display(), state);
156 state
157 })
158 })
159 .collect();
160
161 let mut count_ok = 0;
162 let mut count_ignored = 0;
163 let mut count_filtered = 0;
164 let mut faileds = Vec::with_capacity(handles.len());
165 for handle in handles {
167 match handle.await.unwrap() {
168 State::Ok(Some(_)) => count_ok += 1,
169 State::Failed(Some((failed, _))) => faileds.push(failed),
170 State::Ok(None) | State::Failed(None) => unreachable!(),
171 State::Ignored => count_ignored += 1,
172 State::FilteredOut => count_filtered += 1,
173 }
174 }
175 scheduler.close();
176 Ok(TestResult { count_ok, count_ignored, count_filtered, faileds })
177}
178
179#[async_recursion::async_recursion]
180async fn walk(
181 mut current_config: FullConfig,
182 current_path: PathBuf,
183 args: &'static Args,
184) -> Result<Vec<(PathBuf, FullConfig)>, Vec<BuildError>> {
185 let all_path = current_path.join("__all__.toml");
186 if all_path.exists() {
187 match current_config.update(&all_path, args.debug) {
188 Ok(_config) => current_config = _config,
189 Err(e) => return Err(vec![e]),
190 }
191 }
192 let read_dir = match current_path.read_dir() {
193 Ok(read_dir) => read_dir,
194 Err(e) => return Err(vec![BuildError::ReadDir(current_path, e)]),
195 };
196 let (sub_dir_futures, files): (Vec<_>, Vec<_>) =
197 read_dir.into_iter().partition_map(|entry| {
198 let path = entry.unwrap().path();
199 if path.is_dir() {
200 if path.file_name().unwrap() == GOLDEN_DIR {
201 Either::Left(None)
202 } else {
203 let current_config = current_config.clone();
204 Either::Left(Some(tokio::spawn(walk(current_config, path, args))))
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 current_config.match_extension(&file) {
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}