Skip to content

Commit 250c46e

Browse files
committed
multi-thread test runner
Signed-off-by: Alex Chi <[email protected]>
1 parent ab9f041 commit 250c46e

16 files changed

+189
-91
lines changed

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ keywords = ["sql", "database", "planner", "cli"]
1313
[dependencies]
1414
anyhow = "1"
1515
async-trait = "0.1"
16+
console = "0.15"
17+
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
1618
glob = "0.3"
1719
libtest-mimic = "0.4"
1820
serde = { version = "1.0", features = ["derive"] }
1921
serde_yaml = "0.8"
2022
similar = "2"
2123
tokio = { version = "1", features = ["rt", "fs"] }
22-
console = "0.15"
2324

2425
[workspace]
2526
members = ["naivedb"]

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Here's an example of test description file:
1919
SELECT * FROM t1, t2 WHERE t1.v1 = t2.v2;
2020
desc: Test whether join works correctly.
2121
before: ["*sql1", "*sql2", "CREATE TABLE t3(v3 int);"]
22-
test:
22+
tasks:
2323
- logical
2424
- physical
2525
```
@@ -34,7 +34,7 @@ Basically, it is like:
3434
- "*test_case_1" # use *id to reference to another test case
3535
- "*test_case_2"
3636
- CREATE TABLE t2(v2 int); # or directly write a SQL here
37-
test: # run logical and physical test for this case
37+
tasks: # run logical and physical test for this case
3838
- logical
3939
- physical
4040
```

naivedb/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ edition = "2021"
55
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
66

77
[dependencies]
8+
anyhow = { version = "1", features = ["backtrace"] }
89
async-trait = "0.1"
910
sqlplannertest = { path = ".." }
10-
anyhow = "1"
1111
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "fs"] }
1212

1313
[[test]]

naivedb/src/lib.rs

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use anyhow::Result;
1+
use anyhow::{anyhow, Result};
22
use async_trait::async_trait;
33

44
#[derive(Default)]
@@ -12,7 +12,25 @@ impl NaiveDb {
1212

1313
#[async_trait]
1414
impl sqlplannertest::PlannerTestRunner for NaiveDb {
15-
async fn run(&mut self, _test_case: &sqlplannertest::ParsedTestCase) -> Result<String> {
16-
Ok("hello, world!".to_string())
15+
async fn run(&mut self, test_case: &sqlplannertest::ParsedTestCase) -> Result<String> {
16+
use std::fmt::Write;
17+
let mut result = String::new();
18+
let r = &mut result;
19+
for task in &test_case.tasks {
20+
writeln!(r, "=== {}", task)?;
21+
writeln!(r, "I'm a naive db, so I don't now how to process")?;
22+
for before in &test_case.before_sql {
23+
writeln!(r, "{}", before)?;
24+
}
25+
writeln!(r, "{}", test_case.sql)?;
26+
writeln!(r)?;
27+
}
28+
if test_case.sql.contains("ERROR") {
29+
return Err(anyhow!("Ooops, error!"));
30+
}
31+
if test_case.sql.contains("PANIC") {
32+
panic!("I'm panic!");
33+
}
34+
Ok(result)
1735
}
1836
}

naivedb/tests/1.sql

-15
This file was deleted.

naivedb/tests/2.sql

-15
This file was deleted.

naivedb/tests/2.yml

-13
This file was deleted.

naivedb/tests/_panic.yml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
- sql: |
2+
PANIC
3+
desc: Test whether panic works
4+
tasks:
5+
- logical
6+
- physical

naivedb/tests/error.planner.sql

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- Test whether error works
2+
ERROR
3+
4+
/*
5+
Error
6+
Ooops, error!
7+
*/
8+

naivedb/tests/error.yml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
- sql: |
2+
ERROR
3+
desc: Test whether error works
4+
tasks:
5+
- logical
6+
- physical

naivedb/tests/ok.planner.sql

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
-- sql1
2+
CREATE TABLE t1(v1 int);
3+
4+
/*
5+
6+
*/
7+
8+
-- sql2
9+
CREATE TABLE t2(v2 int);
10+
11+
/*
12+
13+
*/
14+
15+
-- Test whether join works correctly.
16+
SELECT * FROM t1, t2 WHERE t1.v1 = t2.v2;
17+
18+
/*
19+
=== logical
20+
I'm a naive db, so I don't now how to process
21+
CREATE TABLE t1(v1 int);
22+
23+
CREATE TABLE t2(v2 int);
24+
25+
CREATE TABLE t3(v3 int);
26+
SELECT * FROM t1, t2 WHERE t1.v1 = t2.v2;
27+
28+
29+
=== physical
30+
I'm a naive db, so I don't now how to process
31+
CREATE TABLE t1(v1 int);
32+
33+
CREATE TABLE t2(v2 int);
34+
35+
CREATE TABLE t3(v3 int);
36+
SELECT * FROM t1, t2 WHERE t1.v1 = t2.v2;
37+
*/
38+

naivedb/tests/1.yml renamed to naivedb/tests/ok.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
SELECT * FROM t1, t2 WHERE t1.v1 = t2.v2;
99
desc: Test whether join works correctly.
1010
before: ["*sql1", "*sql2", "CREATE TABLE t3(v3 int);"]
11-
test:
11+
tasks:
1212
- logical
1313
- physical

src/apply.rs

+85-27
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,113 @@
11
use std::future::Future;
22
use std::path::Path;
33

4-
use anyhow::{anyhow, Context, Result};
4+
use anyhow::{anyhow, Context, Error, Result};
5+
use console::style;
6+
use futures_util::{stream, StreamExt, TryFutureExt};
57

6-
use crate::{discover_tests, parse_test_cases, ParsedTestCase, PlannerTestRunner, TestCase};
8+
use crate::{
9+
discover_tests, parse_test_cases, ParsedTestCase, PlannerTestRunner, TestCase, RESULT_SUFFIX,
10+
};
711

812
pub async fn planner_test_apply<F, Ft, R>(path: impl AsRef<Path>, runner_fn: F) -> Result<()>
913
where
1014
F: Fn() -> Ft + Send + Sync + 'static,
1115
Ft: Future<Output = Result<R>> + Send,
12-
R: PlannerTestRunner,
16+
R: PlannerTestRunner + 'static,
1317
{
14-
let tests = discover_tests(path)?;
18+
let tests = discover_tests(path)?
19+
.into_iter()
20+
.map(|path| {
21+
let path = path?;
22+
let filename = path
23+
.file_name()
24+
.context("unable to extract filename")?
25+
.to_os_string();
26+
let testname = filename
27+
.to_str()
28+
.context("unable to convert to string")?
29+
.to_string();
30+
Ok::<_, Error>((path, testname))
31+
})
32+
.collect::<Result<Vec<_>, _>>()?;
33+
let test_stream = stream::iter(tests).map(|(path, testname)| {
34+
let runner_fn = &runner_fn;
35+
let testname_x = testname.clone();
36+
async {
37+
let mut runner = runner_fn().await?;
38+
tokio::spawn(async move {
39+
let testcases = tokio::fs::read(&path).await?;
40+
let testcases: Vec<TestCase> = serde_yaml::from_slice(&testcases)?;
41+
let testcases = parse_test_cases(testcases)?;
42+
let mut generated_result = String::new();
43+
for testcase in testcases {
44+
let runner_result = runner.run(&testcase).await;
45+
generate_result(&testcase, &runner_result, &mut generated_result)?;
46+
}
47+
let path = {
48+
let mut path = path;
49+
path.set_extension(RESULT_SUFFIX);
50+
path
51+
};
52+
tokio::fs::write(&path, generated_result).await?;
53+
54+
Ok::<_, Error>(())
55+
})
56+
.await??;
57+
Ok::<_, Error>(testname)
58+
}
59+
.map_err(|e| (e, testname_x))
60+
});
61+
62+
let mut test_stream =
63+
test_stream.buffer_unordered(std::thread::available_parallelism()?.into());
64+
1565
let mut test_discovered = false;
16-
for entry in tests {
17-
test_discovered = true;
18-
let path = entry.context("failed to read glob entry")?;
19-
let filename = path.file_name().context("unable to extract filename")?;
20-
let testname = filename.to_str().context("unable to convert to string")?;
21-
let mut runner = runner_fn().await?;
22-
let testcases = tokio::fs::read(&path).await?;
23-
let testcases: Vec<TestCase> = serde_yaml::from_slice(&testcases)?;
24-
let testcases = parse_test_cases(testcases)?;
25-
let mut generated_result = String::new();
26-
println!("{}", testname);
27-
for testcase in testcases {
28-
let runner_result = runner.run(&testcase).await?;
29-
generate_result(&testcase, &runner_result, &mut generated_result)?;
66+
let mut failed_cases = vec![];
67+
while let Some(item) = test_stream.next().await {
68+
match item {
69+
Ok(name) => println!("{} {}", style("[DONE]").green().bold(), name),
70+
Err((e, name)) => {
71+
println!("{} {}: {:#}", style("[FAIL]").red().bold(), name, e);
72+
failed_cases.push(name);
73+
}
3074
}
31-
let path = {
32-
let mut path = path;
33-
path.set_extension("sql");
34-
path
35-
};
36-
tokio::fs::write(&path, generated_result).await?;
75+
test_discovered = true;
3776
}
77+
3878
if !test_discovered {
3979
return Err(anyhow!("no test discovered"));
4080
}
81+
82+
if !failed_cases.is_empty() {
83+
println!("Failed cases: {:#?}", failed_cases);
84+
return Err(anyhow!("Cannot apply planner test"));
85+
}
86+
4187
Ok(())
4288
}
4389

4490
/// Generate a text-based result based on test case and runner result
4591
pub fn generate_result(
4692
testcase: &ParsedTestCase,
47-
runner_result: &str,
93+
runner_result: &Result<String>,
4894
mut r: impl std::fmt::Write,
4995
) -> Result<()> {
50-
writeln!(r, "-----")?;
96+
match (&testcase.id, &testcase.desc) {
97+
(Some(id), Some(desc)) => writeln!(r, "-- {}: {}", id, desc)?,
98+
(Some(id), None) => writeln!(r, "-- {}", id)?,
99+
(None, Some(desc)) => writeln!(r, "-- {}", desc)?,
100+
(None, None) => writeln!(r, "-- (no id or description)")?,
101+
}
51102
writeln!(r, "{}", testcase.sql)?;
52-
writeln!(r, "{}", runner_result)?;
103+
match runner_result {
104+
Ok(runner_result) => {
105+
writeln!(r, "/*\n{}\n*/", runner_result.trim_end())?;
106+
}
107+
Err(err) => {
108+
writeln!(r, "/*\nError\n{}\n*/", err)?;
109+
}
110+
}
53111
writeln!(r)?;
54112
Ok(())
55113
}

src/lib.rs

+9-8
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub struct TestCase {
1919
pub desc: Option<String>,
2020
pub sql: String,
2121
pub before: Option<Vec<String>>,
22-
pub test: Option<Vec<String>>,
22+
pub tasks: Option<Vec<String>>,
2323
}
2424

2525
/// A parsed test case.
@@ -29,24 +29,25 @@ pub struct ParsedTestCase {
2929
pub desc: Option<String>,
3030
pub sql: String,
3131
pub before_sql: Vec<String>,
32-
pub test: Vec<String>,
32+
pub tasks: Vec<String>,
3333
}
3434

3535
/// A planner test runner.
3636
#[async_trait]
37-
pub trait PlannerTestRunner {
37+
pub trait PlannerTestRunner: Send {
3838
/// Run a test case and return the result
3939
async fn run(&mut self, test_case: &ParsedTestCase) -> Result<String>;
4040
}
4141

42-
pub fn parse_test_cases(test: Vec<TestCase>) -> Result<Vec<ParsedTestCase>> {
43-
resolve_testcase_id(test)
42+
pub fn parse_test_cases(tests: Vec<TestCase>) -> Result<Vec<ParsedTestCase>> {
43+
resolve_testcase_id(tests)
4444
}
4545

46-
const SUFFIX: &str = ".yml";
46+
const TEST_SUFFIX: &str = ".yml";
47+
const RESULT_SUFFIX: &str = "planner.sql";
4748

4849
pub fn discover_tests(path: impl AsRef<Path>) -> Result<Paths> {
49-
let pattern = format!("**/[!_]*{}", SUFFIX);
50+
let pattern = format!("**/[!_]*{}", TEST_SUFFIX);
5051
let path = path.as_ref().join(&pattern);
5152
let path = path.to_str().context("non utf-8 path")?;
5253
let paths = glob::glob(path).context("failed to discover test")?;
@@ -70,7 +71,7 @@ mod tests {
7071
SELECT * FROM t1, t2 WHERE t1.v1 = t2.v2;
7172
desc: "Test whether join works correctly."
7273
before: ["*sql1", "*sql2", "CREATE TABLE t3(v3 int);"]
73-
test:
74+
tasks:
7475
- logical
7576
- physical
7677
"#

src/resolve_id.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub fn resolve_testcase_id(testcases: Vec<TestCase>) -> Result<Vec<ParsedTestCas
5454
id: testcase.id,
5555
desc: testcase.desc,
5656
sql: testcase.sql,
57-
test: testcase.test.unwrap_or_default(),
57+
tasks: testcase.tasks.unwrap_or_default(),
5858
})
5959
})
6060
.collect::<Result<Vec<_>>>()

0 commit comments

Comments
 (0)