1use std::collections::HashMap;
16use std::fs::File;
17use std::io::{BufRead, BufReader};
18use std::path::Path;
19use std::process::Stdio;
20use std::sync::Arc;
21
22use anyhow::{Context, anyhow, bail};
23use tokio::io::AsyncWriteExt;
24use tokio::process::Command;
25use tracing::{debug, error, info};
26
27use crate::schedule::TestResult::{Different, Same};
28use crate::{DatabaseMode, FileManager, Opts, Psql, init_env};
29
30#[derive(PartialEq)]
32enum TestResult {
33 Same,
35 Different,
37}
38
39struct TestCase {
40 test_name: String,
41 opts: Opts,
42 psql: Arc<Psql>,
43 file_manager: Arc<FileManager>,
44}
45
46pub(crate) struct Schedule {
47 opts: Opts,
48 file_manager: Arc<FileManager>,
49 psql: Arc<Psql>,
50 schedules: Vec<Vec<String>>,
54}
55
56const PREFIX_IGNORE: &str = "--@ ";
60
61impl Schedule {
62 pub(crate) fn new(opts: Opts) -> anyhow::Result<Self> {
63 Ok(Self {
64 opts: opts.clone(),
65 file_manager: Arc::new(FileManager::new(opts.clone())),
66 psql: Arc::new(Psql::new(opts.clone())),
67 schedules: Schedule::parse_from(opts.schedule_file_path())?,
68 })
69 }
70
71 fn do_init(self) -> anyhow::Result<Self> {
72 init_env();
73
74 self.file_manager.init()?;
75 self.psql.init()?;
76
77 Ok(self)
78 }
79
80 fn parse_from<P: AsRef<Path>>(path: P) -> anyhow::Result<Vec<Vec<String>>> {
81 let file = File::options()
82 .read(true)
83 .open(path.as_ref())
84 .with_context(|| format!("Failed to open schedule file: {:?}", path.as_ref()))?;
85
86 let reader = BufReader::new(file);
87 let mut schedules = Vec::new();
88
89 for line in reader.lines() {
90 let line = line?;
91 if line.starts_with("test: ") {
92 schedules.push(line[5..].split_whitespace().map(str::to_owned).collect());
93 debug!("Add one parallel schedule: {:?}", schedules.last().unwrap());
94 }
95 }
96
97 Ok(schedules)
98 }
99
100 pub(crate) async fn run(self) -> anyhow::Result<()> {
107 let s = self.do_init()?;
108 s.do_run().await
109 }
110
111 async fn do_run(self) -> anyhow::Result<()> {
112 let mut different_tests = Vec::new();
113 for parallel_schedule in &self.schedules {
114 info!("Running parallel schedule: {:?}", parallel_schedule);
115 let ret = self
116 .run_one_schedule(parallel_schedule.iter().map(String::as_str))
117 .await?;
118
119 let mut diff_test = ret
120 .iter()
121 .filter(|(_test_name, test_result)| **test_result == Different)
122 .map(|t| t.0.clone())
123 .collect::<Vec<String>>();
124
125 if !diff_test.is_empty() {
126 error!(
127 "Parallel schedule failed, these tests are different: {:?}",
128 diff_test
129 );
130 different_tests.append(&mut diff_test);
131 } else {
132 info!("Parallel schedule succeeded!");
133 }
134 }
135
136 if !different_tests.is_empty() {
137 info!(
138 "RisingWave regress tests failed, these tests are different from expected output: {:?}",
139 different_tests
140 );
141 bail!(
142 "RisingWave regress tests failed, these tests are different from expected output: {:?}",
143 different_tests
144 )
145 } else {
146 info!("RisingWave regress tests passed.");
147 Ok(())
148 }
149 }
150
151 async fn run_one_schedule(
152 &self,
153 tests: impl Iterator<Item = &str>,
154 ) -> anyhow::Result<HashMap<String, TestResult>> {
155 let mut join_handles = HashMap::new();
156
157 for test_name in tests {
158 let test_case = self.create_test_case(test_name);
159 let join_handle = tokio::spawn(async move { test_case.run().await });
160 join_handles.insert(test_name, join_handle);
161 }
162
163 let mut result = HashMap::new();
164
165 for (test_name, join_handle) in join_handles {
166 let ret = join_handle
167 .await
168 .with_context(|| format!("Running test case {} panicked!", test_name))??;
169
170 result.insert(test_name.to_owned(), ret);
171 }
172
173 Ok(result)
174 }
175
176 fn create_test_case(&self, test_name: &str) -> TestCase {
177 TestCase {
178 test_name: test_name.to_owned(),
179 opts: self.opts.clone(),
180 psql: self.psql.clone(),
181 file_manager: self.file_manager.clone(),
182 }
183 }
184}
185
186impl TestCase {
187 async fn run(self) -> anyhow::Result<TestResult> {
188 let host = &self.opts.host();
189 let port = &self.opts.port().to_string();
190 let database_name = self.opts.database_name();
191 let pg_user_name = self.opts.pg_user_name();
192 let args: Vec<&str> = vec![
193 "-X",
194 "-a",
195 "-q",
196 "-h",
197 host,
198 "-p",
199 port,
200 "-d",
201 database_name,
202 "-U",
203 pg_user_name,
204 "-v",
205 "HIDE_TABLEAM=on",
206 "-v",
207 "HIDE_TOAST_COMPRESSION=on",
208 ];
209 println!(
210 "Ready to run command:\npsql {}\n for test case:{}",
211 args.join(" "),
212 self.test_name
213 );
214
215 let extra_lines_added_to_input = match self.opts.database_mode() {
216 DatabaseMode::Risingwave => {
217 vec![
218 "SET RW_IMPLICIT_FLUSH TO true;\n",
219 "SET QUERY_MODE TO LOCAL;\n",
220 ]
221 }
222 DatabaseMode::Postgres => vec![],
223 };
224
225 let actual_output_path = self.file_manager.output_of(&self.test_name)?;
226 let actual_output_file = File::options()
227 .create_new(true)
228 .write(true)
229 .open(&actual_output_path)
230 .with_context(|| {
231 format!(
232 "Failed to create {:?} for writing output.",
233 actual_output_path
234 )
235 })?;
236
237 let mut command = Command::new("psql");
238 command.env(
239 "PGAPPNAME",
240 format!("risingwave_regress/{}", self.test_name),
241 );
242 command.args(args);
243 info!(
244 "Starting to execute test case: {}, command: {:?}",
245 self.test_name, command
246 );
247 let mut child = command
248 .stdin(Stdio::piped())
249 .stdout(actual_output_file.try_clone().with_context(|| {
250 format!("Failed to clone output file: {:?}", actual_output_path)
251 })?)
252 .stderr(actual_output_file)
253 .spawn()
254 .with_context(|| format!("Failed to spawn child for test case: {}", self.test_name))?;
255
256 let child_stdin = child
257 .stdin
258 .as_mut()
259 .ok_or_else(|| anyhow!("Cannot get the stdin handle of the child process."))?;
260 for extra_line in &extra_lines_added_to_input {
261 child_stdin.write_all(extra_line.as_bytes()).await?;
262 }
263
264 let read_all_lines_from = std::fs::read_to_string;
265
266 let input_path = self.file_manager.source_of(&self.test_name)?;
267 let input_file_content = read_all_lines_from(input_path)?;
268 info!("input_file_content:{}", input_file_content);
269 child_stdin.write_all(input_file_content.as_bytes()).await?;
270
271 let status = child.wait().await.with_context(|| {
272 format!("Failed to wait for finishing test case: {}", self.test_name)
273 })?;
274
275 if !status.success() {
276 let error_output = read_all_lines_from(actual_output_path)?;
277 let error_msg = format!(
278 "Execution of test case {} failed, reason:\n{}",
279 self.test_name, error_output
280 );
281 error!("{}", error_msg);
282 bail!(error_msg);
283 }
284
285 let expected_output_path = self.file_manager.expected_output_of(&self.test_name)?;
286
287 let input_lines = input_file_content
288 .lines()
289 .filter(|s| !s.is_empty() && *s != PREFIX_IGNORE);
290 let mut expected_lines = std::io::BufReader::new(File::open(expected_output_path)?)
291 .lines()
292 .map(|s| s.unwrap())
293 .filter(|s| !s.is_empty());
294 let mut actual_lines = std::io::BufReader::new(File::open(actual_output_path)?)
295 .lines()
296 .skip(extra_lines_added_to_input.len())
297 .map(|s| s.unwrap())
298 .filter(|s| !s.is_empty() && s != PREFIX_IGNORE);
299
300 let mut is_diff = false;
321 let mut pending_input = vec![];
322 for input_line in input_lines {
323 let original_input_line = input_line.strip_prefix(PREFIX_IGNORE).unwrap_or(input_line);
324
325 let mut expected_output = vec![];
327 while let Some(line) = expected_lines.next()
328 && line != original_input_line
329 {
330 expected_output.push(line);
331 }
332
333 let mut actual_output = vec![];
334 while let Some(line) = actual_lines.next()
335 && line != input_line
336 {
337 actual_output.push(line);
338 }
339
340 if expected_output.is_empty() && actual_output.is_empty() {
343 pending_input.push(input_line);
344 continue;
345 }
346
347 let query_input = std::mem::replace(&mut pending_input, vec![input_line]);
348
349 is_diff = !compare_output(&query_input, &expected_output, &actual_output) || is_diff;
350 }
351 let expected_output: Vec<_> = expected_lines.collect();
353 let actual_output: Vec<_> = actual_lines.collect();
354 is_diff = !compare_output(&pending_input, &expected_output, &actual_output) || is_diff;
355
356 Ok(if is_diff { Different } else { Same })
357 }
358}
359
360fn compare_output(query: &[&str], expected: &[String], actual: &[String]) -> bool {
361 let compare_lines = |expected: &[String], actual: &[String]| {
362 let eq = expected == actual;
363 if !eq {
364 error!("query input:\n{}", query.join("\n"));
365
366 let (expected_output, actual_output) = (expected.join("\n"), actual.join("\n"));
367 let diffs = format_diff(&expected_output, &actual_output);
368 error!("Diff:\n{}", diffs);
369 }
370 eq
371 };
372
373 if let Some(l) = query.last()
374 && l.starts_with(PREFIX_IGNORE)
375 {
376 return true;
377 }
378 if !expected.is_empty()
379 && !actual.is_empty()
380 && expected[0].starts_with("ERROR: ")
381 && actual[0].starts_with("ERROR: ")
382 {
383 return true;
384 }
385
386 let is_select = query
387 .iter()
388 .any(|line| line.to_lowercase().starts_with("select"));
389 if !is_select {
390 return compare_lines(expected, actual);
392 }
393
394 let is_order_by = query
398 .iter()
399 .any(|line| line.to_lowercase().contains("order by"));
400
401 if expected.len() < 3
414 || !expected[1].starts_with("---")
415 || !matches!(expected.last(), Some(l) if l.ends_with(" rows)") || l == "(1 row)")
416 || actual.len() < 3
417 || !actual[1].starts_with("---")
418 || !matches!(actual.last(), Some(l) if l.ends_with(" rows)") || l == "(1 row)")
419 {
420 return compare_lines(expected, actual);
422 }
423
424 if is_order_by {
425 compare_lines(expected, actual)
426 } else {
427 let mut expected_sorted = expected.to_vec();
428 expected_sorted[2..expected.len() - 1].sort();
429 let mut actual_sorted = actual.to_vec();
430 actual_sorted[2..actual.len() - 1].sort();
431
432 compare_lines(&expected_sorted, &actual_sorted)
433 }
434}
435
436fn format_diff(expected_output: &String, actual_output: &String) -> String {
437 use std::fmt::Write;
438
439 use similar::{ChangeTag, TextDiff};
440 let diff = TextDiff::from_lines(expected_output, actual_output);
441
442 let mut diff_str = "".to_owned();
443 for change in diff.iter_all_changes() {
444 let sign = match change.tag() {
445 ChangeTag::Delete => "-",
446 ChangeTag::Insert => "+",
447 ChangeTag::Equal => " ",
448 };
449 write!(diff_str, "{}{}", sign, change).unwrap();
450 }
451 diff_str
452}