1use std::fs::{read_dir, OpenOptions};
4use std::io::{Cursor, Read, Seek, Write};
5use std::path::{Path, PathBuf};
6use std::str::FromStr;
7use std::time::Instant;
8
9use prettydiff::basic::{DiffOp, SliceChangeset};
10use prettydiff::diff_lines;
11use regex::Regex;
12use walkdir::WalkDir;
13
14use crate::case::TestCase;
15use crate::error::{Result, SqlnessError};
16use crate::{config::Config, environment::EnvController};
17
18pub struct Runner<E: EnvController> {
34 config: Config,
35 env_controller: E,
36}
37
38impl<E: EnvController> Runner<E> {
39 pub fn new(config: Config, env_controller: E) -> Self {
40 Self {
41 config,
42 env_controller,
43 }
44 }
45
46 pub async fn run(&self) -> Result<()> {
47 let environments = self.collect_env()?;
48 let mut errors = Vec::new();
49 let filter = Regex::new(&self.config.env_filter)?;
50 for env in environments {
51 if !filter.is_match(&env) {
52 println!("Environment({env}) is skipped!");
53 continue;
54 }
55 let env_config = self.read_env_config(&env);
56 let config_path = env_config.as_path();
57 let config_path = if config_path.exists() {
58 Some(config_path)
59 } else {
60 None
61 };
62 let db = self.env_controller.start(&env, config_path).await;
63 let run_result = self.run_env(&env, &db).await;
64 self.env_controller.stop(&env, db).await;
65
66 if let Err(e) = run_result {
67 println!("Environment {env} run failed, error:{e:?}.");
68
69 if self.config.fail_fast {
70 return Err(e);
71 }
72
73 errors.push(e);
74 }
75 }
76
77 if let Some(e) = errors.pop() {
79 return Err(e);
80 }
81
82 Ok(())
83 }
84
85 fn read_env_config(&self, env: &str) -> PathBuf {
86 let mut path_buf = std::path::PathBuf::new();
87 path_buf.push(&self.config.case_dir);
88 path_buf.push(env);
89 path_buf.push(&self.config.env_config_file);
90
91 path_buf
92 }
93
94 fn collect_env(&self) -> Result<Vec<String>> {
95 let mut result = vec![];
96
97 for dir in read_dir(&self.config.case_dir)? {
98 let dir = dir?;
99 if dir.file_type()?.is_dir() {
100 let file_name = dir.file_name().to_str().unwrap().to_string();
101 result.push(file_name);
102 }
103 }
104
105 Ok(result)
106 }
107
108 async fn run_env(&self, env: &str, db: &E::DB) -> Result<()> {
109 let case_paths = self.collect_case_paths(env).await?;
110 let mut failed_cases = vec![];
111 let mut errors = vec![];
112 let start = Instant::now();
113 for path in case_paths {
114 let is_success = self.run_single_case(db, &path).await;
115 let case_name = path.as_os_str().to_str().unwrap().to_owned();
116 match is_success {
117 Ok(false) => failed_cases.push(case_name),
118 Ok(true) => {}
119 Err(e) => {
120 if self.config.fail_fast {
121 println!("Case {case_name} failed with error {e:?}");
122 println!("Stopping environment {env} due to previous error.");
123 break;
124 } else {
125 errors.push((case_name, e))
126 }
127 }
128 }
129 }
130
131 println!(
132 "Environment {} run finished, cost:{}ms",
133 env,
134 start.elapsed().as_millis()
135 );
136
137 if !failed_cases.is_empty() {
138 println!("Failed cases:");
139 println!("{failed_cases:#?}");
140 }
141
142 if !errors.is_empty() {
143 println!("Error cases:");
144 println!("{errors:#?}");
145 }
146
147 let error_count = failed_cases.len() + errors.len();
148 if error_count == 0 {
149 Ok(())
150 } else {
151 Err(SqlnessError::RunFailed { count: error_count })
152 }
153 }
154
155 async fn run_single_case(&self, db: &E::DB, path: &Path) -> Result<bool> {
157 let case_path = path.with_extension(&self.config.test_case_extension);
158 let mut case = TestCase::from_file(&case_path, &self.config)?;
159 let result_path = path.with_extension(&self.config.result_extension);
160 let mut result_file = OpenOptions::new()
161 .create(true)
162 .write(true)
163 .read(true)
164 .truncate(false)
165 .open(&result_path)?;
166
167 let mut old_result = String::new();
169 result_file.read_to_string(&mut old_result)?;
170
171 let mut new_result = Cursor::new(Vec::new());
173 let timer = Instant::now();
174 case.execute(db, &mut new_result).await?;
175 let elapsed = timer.elapsed();
176
177 result_file.set_len(0)?;
179 result_file.rewind()?;
180 result_file.write_all(new_result.get_ref())?;
181
182 let new_result = String::from_utf8(new_result.into_inner()).expect("not utf8 string");
184 if let Some(diff) = self.compare(&old_result, &new_result) {
185 println!("Result unexpected, path:{case_path:?}");
186 println!("{diff}");
187 return Ok(false);
188 }
189
190 println!(
191 "Test case {:?} finished, cost: {}ms",
192 path.as_os_str(),
193 elapsed.as_millis()
194 );
195
196 Ok(true)
197 }
198
199 async fn collect_case_paths(&self, env: &str) -> Result<Vec<PathBuf>> {
200 let mut root = PathBuf::from_str(&self.config.case_dir).unwrap();
201 root.push(env);
202
203 let filter = Regex::new(&self.config.test_filter)?;
204 let test_case_extension = self.config.test_case_extension.as_str();
205 let mut cases: Vec<_> = WalkDir::new(&root)
206 .follow_links(self.config.follow_links)
207 .into_iter()
208 .filter_map(|entry| {
209 entry
210 .map_or(None, |entry| Some(entry.path().to_path_buf()))
211 .filter(|path| {
212 path.extension()
213 .map(|ext| ext == test_case_extension)
214 .unwrap_or(false)
215 })
216 })
217 .map(|path| path.with_extension(""))
218 .filter(|path| {
219 let filename = path
220 .file_name()
221 .unwrap_or_default()
222 .to_str()
223 .unwrap_or_default();
224 let filename_with_env = format!("{env}:{filename}");
225 filter.is_match(&filename_with_env)
226 })
227 .collect();
228
229 cases.sort_by(|a, b| {
231 let a_lower = a.to_string_lossy().to_lowercase();
232 let b_lower = b.to_string_lossy().to_lowercase();
233 a_lower.cmp(&b_lower)
234 });
235
236 Ok(cases)
237 }
238
239 fn compare(&self, expected: &str, actual: &str) -> Option<String> {
241 let diff = diff_lines(expected, actual);
242 let diff = diff.diff();
243 let is_different = diff.iter().any(|d| !matches!(d, DiffOp::Equal(_)));
244 if is_different {
245 return Some(format!("{}", SliceChangeset { diff }));
246 }
247
248 None
249 }
250}