Skip to main content

risingwave_regress_test/
schedule.rs

1// Copyright 2022 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// Result of each test case.
31#[derive(PartialEq)]
32enum TestResult {
33    /// Execution of the test case succeeded, and results are same.
34    Same,
35    /// Execution of the test case succeeded, but outputs are different from expected result.
36    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 of test names.
51    ///
52    /// Each item is called a parallel schedule, which runs parallel.
53    schedules: Vec<Vec<String>>,
54}
55
56// Test queries commented out with `--@ ` are ignored for comparison.
57// Unlike normal comment `--`, this does not require modification to expected output file.
58// We can simplify toggle the case on/off by just updating the input sql file.
59const 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    /// Run all test schedules.
101    ///
102    /// # Returns
103    ///
104    /// `Ok` If no error happens and all outputs are expected,
105    /// `Err` If any error happens, or some outputs are unexpected. Details are logged in log file.
106    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        // We split the output lines (either expected or actual) based on matching lines from input.
301        // For example:
302        //     input      |    output
303        // --+------------+--+-----------------------------------
304        //  1| select v1  | 1| select v1
305        //  2| from t;    | 2| from t;
306        //   |            | 3|  v1
307        //   |            | 4| ----
308        //   |            | 5|   1
309        //   |            | 6|   2
310        //   |            | 7| (2 rows)
311        //   |            | 8|
312        //  3| select v1; | 9| select v1;
313        //   |            |10| ERROR:  column "v1" does not exist
314        //   |            |11| LINE 1: select v1;
315        //   |            |12|                ^
316        //
317        // We would split the output lines into 2 chunks:
318        // * query 1..=2 and output  3..= 7
319        // * query 9..=9 and output 10..=12
320        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            // Find the matching output line, and collect lines before the next matching line.
326            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 no unmatched lines skipped, this input line belongs to the same query as previous
341            // lines.
342            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        // There may be more lines after the final matching lines.
352        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        // no special handling when we do not recognize it
391        return compare_lines(expected, actual);
392    }
393
394    // This could fail in the corner case.
395    // For example, a subquery has a `order by` clause, which should not be allowed and
396    // is meaningless as only the outermost `order by` clause is effective.
397    let is_order_by = query
398        .iter()
399        .any(|line| line.to_lowercase().contains("order by"));
400
401    // The output of `select` is assumed to have the following format:
402    //
403    // line of output column names
404    // line of separator starting with "---" (comment assumed to start with only two "--".)
405    // x lines of output rows
406    // line of summary "(x rows)"
407    //
408    // Note that zero column output (`select;`) has no title line and only 2 hyphens. This would not
409    // be recognized and thus compared as-is without special treatment, which is expected.
410    // --
411    // (x rows)
412
413    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        // no special handling when we do not recognize it
421        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}