sqlness/
runner.rs

1// Copyright 2022 CeresDB Project Authors. Licensed under Apache-2.0.
2
3use 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
18/// The entrypoint of this crate.
19///
20/// To run your integration test cases, simply [`new`] a `Runner` and [`run`] it.
21///
22/// [`new`]: crate::Runner#method.new
23/// [`run`]: crate::Runner#method.run
24///
25/// ```rust, ignore, no_run
26/// async fn run_integration_test() {
27///     let runner = Runner::new(root_path, env).await;
28///     runner.run().await;
29/// }
30/// ```
31///
32/// For more detailed explaination, refer to crate level documentment.
33pub 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        // only return first error
78        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    /// Return true when this case pass, otherwise false.
156    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        // Read old result out for compare later
168        let mut old_result = String::new();
169        result_file.read_to_string(&mut old_result)?;
170
171        // Execute testcase
172        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        // Truncate and write new result back
178        result_file.set_len(0)?;
179        result_file.rewind()?;
180        result_file.write_all(new_result.get_ref())?;
181
182        // Compare old and new result
183        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        // sort the cases in an os-independent order.
230        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    /// Compare result, return None if them are the same, else return diff changes
240    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}