risingwave_regress_test/
file.rs

1// Copyright 2025 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::ffi::OsStr;
16use std::fs::{File, create_dir_all, read_dir};
17use std::io::{BufRead, BufReader, BufWriter, Write};
18use std::path::{Path, PathBuf};
19
20use anyhow::{Context, bail};
21use tracing::info;
22
23use crate::Opts;
24
25pub(crate) struct FileManager {
26    opts: Opts,
27}
28
29impl FileManager {
30    pub(crate) fn new(opts: Opts) -> Self {
31        Self { opts }
32    }
33
34    /// Initialize file related stuff.
35    ///
36    /// - Create necessary directories.
37    /// - Convert source files.
38    pub(crate) fn init(&self) -> anyhow::Result<()> {
39        ensure_dir(self.opts.absolutized_output_dir()?)?;
40        ensure_dir(self.opts.absolutized_output_dir()?.join("results"))?;
41        ensure_dir(self.opts.absolutized_output_dir()?.join("sql"))?;
42        ensure_dir(self.test_table_space_dir()?)?;
43        ensure_dir(self.result_dir()?)?;
44
45        self.convert_source_files()?;
46        Ok(())
47    }
48
49    /// Try to find the input file of `test_name`.
50    pub(crate) fn source_of(&self, test_name: &str) -> anyhow::Result<PathBuf> {
51        let mut path = self
52            .opts
53            .absolutized_input_dir()?
54            .join("sql")
55            .join(format!("{}.sql", test_name));
56
57        if path.exists() {
58            return Ok(path);
59        }
60
61        path = self
62            .opts
63            .absolutized_output_dir()?
64            .join("sql")
65            .join(format!("{}.sql", test_name));
66
67        if path.exists() {
68            return Ok(path);
69        }
70
71        bail!("Can't find source of test case: {}", test_name)
72    }
73
74    /// Try to find the output file of `test_name`.
75    pub(crate) fn output_of(&self, test_name: &str) -> anyhow::Result<PathBuf> {
76        Ok(self
77            .opts
78            .absolutized_output_dir()?
79            .join("results")
80            .join(format!("{}.out", test_name)))
81    }
82
83    /// Try to find the diff file of `test_name`.
84    pub(crate) fn diff_of(&self, test_name: &str) -> anyhow::Result<PathBuf> {
85        Ok(self
86            .opts
87            .absolutized_output_dir()?
88            .join("results")
89            .join(format!("{}.diff", test_name)))
90    }
91
92    /// Try to find the expected output file of `test_name`.
93    pub(crate) fn expected_output_of(&self, test_name: &str) -> anyhow::Result<PathBuf> {
94        let mut path = self
95            .opts
96            .absolutized_input_dir()?
97            .join("expected")
98            .join(format!("{}.out", test_name));
99
100        if path.exists() {
101            return Ok(path);
102        }
103
104        path = self
105            .opts
106            .absolutized_output_dir()?
107            .join("expected")
108            .join(format!("{}.sql", test_name));
109
110        if path.exists() {
111            return Ok(path);
112        }
113
114        bail!("Can't find expected output of test case: {}", test_name)
115    }
116
117    /// Convert source files in input dir, use [`Self::replace_placeholder`].
118    pub(crate) fn convert_source_files(&self) -> anyhow::Result<()> {
119        self.convert_source_files_internal("input", "sql", "sql")?;
120        self.convert_source_files_internal("output", "expected", "out")?;
121        Ok(())
122    }
123
124    /// Converts files ends with ".source" suffix in `input_subdir` and output them to
125    /// `dest_subdir` with filename ends with `suffix`
126    ///
127    /// The `input_subdir` is relative to [`crate::Opts::input_dir`], and `output_subdir` is
128    /// relative to [`crate::Opts::output_dir`].
129    fn convert_source_files_internal(
130        &self,
131        input_subdir: &str,
132        dest_subdir: &str,
133        suffix: &str,
134    ) -> anyhow::Result<()> {
135        let output_subdir_path = self.opts.absolutized_output_dir()?.join(dest_subdir);
136        ensure_dir(&output_subdir_path)?;
137
138        let input_subdir_path: PathBuf = self.opts.absolutized_input_dir()?.join(input_subdir);
139
140        let dir_entries = read_dir(&input_subdir_path)
141            .with_context(|| format!("Failed to read dir {:?}", input_subdir_path))?;
142
143        for entry in dir_entries {
144            let path = entry?.path();
145            let extension = path.extension().and_then(OsStr::to_str);
146
147            if path.is_file() && extension == Some("source") {
148                let filename = path.file_prefix().unwrap();
149                let output_filename = format!("{}.{}", filename.to_str().unwrap(), suffix);
150                let output_path = output_subdir_path.join(output_filename);
151                info!("Converting {:?} to {:?}", path, output_path);
152                self.replace_placeholder(path, output_path)?;
153            } else {
154                info!("Skip converting {:?}", path);
155            }
156        }
157
158        Ok(())
159    }
160
161    /// Replace predefined placeholders in `input` with correct values and output them to
162    /// `output`.
163    ///
164    /// ## Placeholders
165    /// * `@abs_srcdir@`: Absolute path of input directory.
166    /// * `@abs_builddir@`: Absolute path of output directory.
167    /// * `@testtablespace@`: Absolute path of tablespace for test.
168    fn replace_placeholder<P: AsRef<Path>>(&self, input: P, output: P) -> anyhow::Result<()> {
169        let abs_input_dir = self.opts.absolutized_input_dir()?;
170        let abs_output_dir = self.opts.absolutized_output_dir()?;
171        let test_tablespace = self.test_table_space_dir()?;
172
173        let reader = BufReader::new(
174            File::options()
175                .read(true)
176                .open(&input)
177                .with_context(|| format!("Failed to open input file: {:?}", input.as_ref()))?,
178        );
179
180        let mut writer = BufWriter::new(
181            File::options()
182                .write(true)
183                .create_new(true)
184                .open(&output)
185                .with_context(|| format!("Failed to create output file: {:?}", output.as_ref()))?,
186        );
187
188        for line in reader.lines() {
189            let mut new_line = line?;
190            new_line = new_line.replace("@abs_srcdir@", abs_input_dir.to_str().unwrap());
191            new_line = new_line.replace("@abs_builddir@", abs_output_dir.to_str().unwrap());
192            new_line = new_line.replace("@testtablespace@", test_tablespace.to_str().unwrap());
193            writer.write_all(new_line.as_bytes())?;
194            writer.write_all("\n".as_bytes())?;
195        }
196
197        Ok(writer.flush()?)
198    }
199
200    fn test_table_space_dir(&self) -> anyhow::Result<PathBuf> {
201        self.opts
202            .absolutized_output_dir()
203            .map(|p| p.join("testtablespace"))
204    }
205
206    fn result_dir(&self) -> anyhow::Result<PathBuf> {
207        self.opts.absolutized_output_dir().map(|p| p.join("result"))
208    }
209}
210
211/// Check `dir` not exists or is empty.
212///
213/// # Return
214///
215/// * If `dir` doesn't exist, create it and all its parents.
216/// * If `dir` exits, return error if not empty.
217fn ensure_dir<P: AsRef<Path>>(dir: P) -> anyhow::Result<()> {
218    let dir = dir.as_ref();
219    if !dir.exists() {
220        create_dir_all(dir).with_context(|| format!("Failed to create dir {:?}", dir))?;
221        return Ok(());
222    }
223
224    if !dir.is_dir() {
225        bail!("{:?} already exists and is not a directory!", dir);
226    }
227
228    let dir_entry = read_dir(dir).with_context(|| format!("Failed to read dir {:?}", dir))?;
229    if dir_entry.count() != 0 {
230        bail!("{:?} is not empty!", dir);
231    }
232
233    Ok(())
234}