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