Skip to content

Commit 282a600

Browse files
authored
feat: support checking error message for statement & bump to 0.8.0 (#102)
1 parent b664483 commit 282a600

File tree

9 files changed

+165
-42
lines changed

9 files changed

+165
-42
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
## [0.8.0] - 2022-11-22
10+
11+
- Support checking error message using `statement error <regex>` and `query error <regex>` syntax.
12+
- Breaking change: `Record::Statement`, `Record::Query` and `TestErrorKind` are changed accordingly.
13+
914
## [0.7.1] - 2022-11-15
1015

1116
- Fix: `--external-engine-command-template` should not be required

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
members = ["sqllogictest", "sqllogictest-bin", "examples/*", "tests"]
33

44
[workspace.package]
5-
version = "0.7.1"
5+
version = "0.8.0"
66
edition = "2021"
77
homepage = "https://github.com/risinglightdb/sqllogictest-rs"
88
keywords = ["sql", "database", "parser", "cli"]

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Add the following lines to your `Cargo.toml` file:
1616

1717
```toml
1818
[dependencies]
19-
sqllogictest = "0.7"
19+
sqllogictest = "0.8"
2020
```
2121

2222
Implement `DB` trait for your database structure:

examples/basic/basic.slt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,26 @@ Bob
1313
Eve
1414

1515
statement ok
16-
drop table t
16+
drop table t
17+
18+
# The error message is: Hey you got FakeDBError!
19+
20+
statement error
21+
give me an error
22+
23+
statement error FakeDB
24+
give me an error
25+
26+
statement error (?i)fakedb
27+
give me an error
28+
29+
statement error Hey you
30+
give me an error
31+
32+
# statement is expected to fail with error: "Hey we", but got error: "Hey you got FakeDBError!"
33+
# statement error Hey we
34+
# give me an error
35+
36+
# query error is actually equivalent to statement error
37+
query error Hey you
38+
give me an error

examples/basic/examples/basic.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ pub struct FakeDBError;
77

88
impl std::fmt::Display for FakeDBError {
99
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
10-
write!(f, "{:?}", self)
10+
write!(f, "{:?}", "Hey you got FakeDBError!")
1111
}
1212
}
1313

@@ -29,7 +29,7 @@ impl sqllogictest::DB for FakeDB {
2929
if sql.starts_with("drop") {
3030
return Ok("".into());
3131
}
32-
unimplemented!("unsupported SQL: {}", sql);
32+
Err(FakeDBError)
3333
}
3434
}
3535

sqllogictest-bin/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ rand = "0.8"
3030
rust_decimal = { version = "1.7.0", features = ["tokio-pg"] }
3131
serde = { version = "1", features = ["derive"] }
3232
serde_json = "1"
33-
sqllogictest = { path = "../sqllogictest", version = "0.7" }
33+
sqllogictest = { path = "../sqllogictest", version = "0.8" }
3434
tempfile = "3"
3535
thiserror = "1"
3636
tokio = { version = "1", features = [

sqllogictest/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ tempfile = "3"
1919
thiserror = "1"
2020
futures = "0.3"
2121
libtest-mimic = "0.6"
22+
regex = "1.7.0"

sqllogictest/src/parser.rs

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use std::path::Path;
55
use std::sync::Arc;
66
use std::time::Duration;
77

8+
use regex::Regex;
9+
810
use crate::ParseErrorKind::InvalidIncludeFile;
911

1012
/// The location in source file.
@@ -62,7 +64,7 @@ impl Location {
6264
}
6365

6466
/// A single directive in a sqllogictest file.
65-
#[derive(Debug, PartialEq, Eq, Clone)]
67+
#[derive(Debug, Clone)]
6668
#[non_exhaustive]
6769
pub enum Record {
6870
/// An include copies all records from another files.
@@ -72,8 +74,9 @@ pub enum Record {
7274
Statement {
7375
loc: Location,
7476
conditions: Vec<Condition>,
75-
/// The SQL command is expected to fail instead of to succeed.
76-
error: bool,
77+
/// The SQL command is expected to fail with an error messages that matches the given regex.
78+
/// If the regex is an empty string, any error message is accepted.
79+
expected_error: Option<Regex>,
7780
/// The SQL command.
7881
sql: String,
7982
/// Expected rows affected.
@@ -87,6 +90,9 @@ pub enum Record {
8790
type_string: String,
8891
sort_mode: Option<SortMode>,
8992
label: Option<String>,
93+
/// The SQL command is expected to fail with an error messages that matches the given regex.
94+
/// If the regex is an empty string, any error message is accepted.
95+
expected_error: Option<Regex>,
9096
/// The SQL command.
9197
sql: String,
9298
/// The expected results.
@@ -203,6 +209,8 @@ pub enum ParseErrorKind {
203209
InvalidType(String),
204210
#[error("invalid number: {0:?}")]
205211
InvalidNumber(String),
212+
#[error("invalid error message: {0:?}")]
213+
InvalidErrorMessage(String),
206214
#[error("invalid duration: {0:?}")]
207215
InvalidDuration(String),
208216
#[error("invalid control: {0:?}")]
@@ -272,14 +280,19 @@ fn parse_inner(loc: &Location, script: &str) -> Result<Vec<Record>, ParseError>
272280
}
273281
["statement", res @ ..] => {
274282
let mut expected_count = None;
275-
let error = match res {
276-
["ok"] => false,
277-
["error"] => true,
283+
let mut expected_error = None;
284+
match res {
285+
["ok"] => {}
286+
["error", err_str @ ..] => {
287+
let err_str = err_str.join(" ");
288+
expected_error = Some(Regex::new(&err_str).map_err(|_| {
289+
ParseErrorKind::InvalidErrorMessage(err_str).at(loc.clone())
290+
})?);
291+
}
278292
["count", count_str] => {
279293
expected_count = Some(count_str.parse::<u64>().map_err(|_| {
280294
ParseErrorKind::InvalidNumber((*count_str).into()).at(loc.clone())
281295
})?);
282-
false
283296
}
284297
_ => return Err(ParseErrorKind::InvalidLine(line.into()).at(loc)),
285298
};
@@ -297,17 +310,35 @@ fn parse_inner(loc: &Location, script: &str) -> Result<Vec<Record>, ParseError>
297310
records.push(Record::Statement {
298311
loc,
299312
conditions: std::mem::take(&mut conditions),
300-
error,
313+
expected_error,
301314
sql,
302315
expected_count,
303316
});
304317
}
305-
["query", type_string, res @ ..] => {
306-
let sort_mode = match res.first().map(|&s| SortMode::try_from_str(s)).transpose() {
307-
Ok(sm) => sm,
308-
Err(k) => return Err(k.at(loc)),
309-
};
310-
let label = res.get(1).map(|s| s.to_string());
318+
["query", res @ ..] => {
319+
let mut type_string = "";
320+
let mut sort_mode = None;
321+
let mut label = None;
322+
let mut expected_error = None;
323+
match res {
324+
["error", err_str @ ..] => {
325+
let err_str = err_str.join(" ");
326+
expected_error = Some(Regex::new(&err_str).map_err(|_| {
327+
ParseErrorKind::InvalidErrorMessage(err_str).at(loc.clone())
328+
})?);
329+
}
330+
[type_str, res @ ..] => {
331+
type_string = type_str;
332+
sort_mode =
333+
match res.first().map(|&s| SortMode::try_from_str(s)).transpose() {
334+
Ok(sm) => sm,
335+
Err(k) => return Err(k.at(loc)),
336+
};
337+
label = res.get(1).map(|s| s.to_string());
338+
}
339+
_ => return Err(ParseErrorKind::InvalidLine(line.into()).at(loc)),
340+
}
341+
311342
// The SQL for the query is found on second an subsequent lines of the record
312343
// up to first line of the form "----" or until the end of the record.
313344
let mut sql = match lines.next() {
@@ -345,6 +376,7 @@ fn parse_inner(loc: &Location, script: &str) -> Result<Vec<Record>, ParseError>
345376
label,
346377
sql,
347378
expected_results,
379+
expected_error,
348380
});
349381
}
350382
["control", res @ ..] => match res {

sqllogictest/src/runner.rs

Lines changed: 85 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -111,29 +111,47 @@ impl TestError {
111111
}
112112
}
113113

114+
#[derive(Debug, Clone)]
115+
pub enum RecordKind {
116+
Statement,
117+
Query,
118+
}
119+
120+
impl std::fmt::Display for RecordKind {
121+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122+
match self {
123+
RecordKind::Statement => write!(f, "statement"),
124+
RecordKind::Query => write!(f, "query"),
125+
}
126+
}
127+
}
128+
114129
/// The error kind for running sqllogictest.
115130
#[derive(thiserror::Error, Debug, Clone)]
116131
pub enum TestErrorKind {
117132
#[error("parse error: {0}")]
118133
ParseError(ParseErrorKind),
119-
#[error("statement is expected to fail, but actually succeed:\n[SQL] {sql}")]
120-
StatementOk { sql: String },
121-
#[error("statement failed: {err}\n[SQL] {sql}")]
122-
StatementFail {
134+
#[error("{kind} is expected to fail, but actually succeed:\n[SQL] {sql}")]
135+
Ok { sql: String, kind: RecordKind },
136+
#[error("{kind} failed: {err}\n[SQL] {sql}")]
137+
Fail {
123138
sql: String,
124139
err: Arc<dyn std::error::Error + Send + Sync>,
140+
kind: RecordKind,
141+
},
142+
#[error("{kind} is expected to fail with error:\n\t\x1b[91m{expected_err}\x1b[0m\nbut got error:\n\t\x1b[91m{err}\x1b[0m\n[SQL] {sql}")]
143+
ErrorMismatch {
144+
sql: String,
145+
err: Arc<dyn std::error::Error + Send + Sync>,
146+
expected_err: String,
147+
kind: RecordKind,
125148
},
126149
#[error("statement is expected to affect {expected} rows, but actually {actual}\n[SQL] {sql}")]
127150
StatementResultMismatch {
128151
sql: String,
129152
expected: u64,
130153
actual: String,
131154
},
132-
#[error("query failed: {err}\n[SQL] {sql}")]
133-
QueryFail {
134-
sql: String,
135-
err: Arc<dyn std::error::Error + Send + Sync>,
136-
},
137155
#[error("query result mismatch:\n[SQL] {sql}\n[Diff]\n{}", difference::Changeset::new(.expected, .actual, "\n"))]
138156
QueryResultMismatch {
139157
sql: String,
@@ -214,17 +232,24 @@ impl<D: AsyncDB> Runner<D> {
214232
match record {
215233
Record::Statement { conditions, .. } if self.should_skip(&conditions) => {}
216234
Record::Statement {
217-
error,
235+
conditions: _,
236+
237+
expected_error,
218238
sql,
219239
loc,
220240
expected_count,
221-
..
222241
} => {
223242
let sql = self.replace_keywords(sql);
224243
let ret = self.db.run(&sql).await;
225-
match ret {
226-
Ok(_) if error => return Err(TestErrorKind::StatementOk { sql }.at(loc)),
227-
Ok(count_str) => {
244+
match (ret, expected_error) {
245+
(Ok(_), Some(_)) => {
246+
return Err(TestErrorKind::Ok {
247+
sql,
248+
kind: RecordKind::Statement,
249+
}
250+
.at(loc))
251+
}
252+
(Ok(count_str), None) => {
228253
if let Some(expected_count) = expected_count {
229254
if expected_count.to_string() != count_str {
230255
return Err(TestErrorKind::StatementResultMismatch {
@@ -236,38 +261,76 @@ impl<D: AsyncDB> Runner<D> {
236261
}
237262
}
238263
}
239-
Err(e) if !error => {
240-
return Err(TestErrorKind::StatementFail {
264+
(Err(e), Some(expected_error)) => {
265+
if !expected_error.is_match(&e.to_string()) {
266+
return Err(TestErrorKind::ErrorMismatch {
267+
sql,
268+
err: Arc::new(e),
269+
expected_err: expected_error.to_string(),
270+
kind: RecordKind::Statement,
271+
}
272+
.at(loc));
273+
}
274+
}
275+
(Err(e), None) => {
276+
return Err(TestErrorKind::Fail {
241277
sql,
242278
err: Arc::new(e),
279+
kind: RecordKind::Statement,
243280
}
244281
.at(loc));
245282
}
246-
_ => {}
247283
}
248284
if let Some(hook) = &mut self.hook {
249285
hook.on_stmt_complete(&sql).await;
250286
}
251287
}
252288
Record::Query { conditions, .. } if self.should_skip(&conditions) => {}
253289
Record::Query {
290+
conditions: _,
291+
254292
loc,
255293
sql,
294+
expected_error,
256295
expected_results,
257296
sort_mode,
258-
..
297+
298+
// not handle yet,
299+
type_string: _,
300+
label: _,
259301
} => {
260302
let sql = self.replace_keywords(sql);
261-
let output = match self.db.run(&sql).await {
262-
Ok(output) => output,
263-
Err(e) => {
264-
return Err(TestErrorKind::QueryFail {
303+
let output = match (self.db.run(&sql).await, expected_error) {
304+
(Ok(_), Some(_)) => {
305+
return Err(TestErrorKind::Ok {
306+
sql,
307+
kind: RecordKind::Query,
308+
}
309+
.at(loc))
310+
}
311+
(Ok(output), None) => output,
312+
(Err(e), Some(expected_error)) => {
313+
if !expected_error.is_match(&e.to_string()) {
314+
return Err(TestErrorKind::ErrorMismatch {
315+
sql,
316+
err: Arc::new(e),
317+
expected_err: expected_error.to_string(),
318+
kind: RecordKind::Query,
319+
}
320+
.at(loc));
321+
}
322+
return Ok(());
323+
}
324+
(Err(e), None) => {
325+
return Err(TestErrorKind::Fail {
265326
sql,
266327
err: Arc::new(e),
328+
kind: RecordKind::Query,
267329
}
268330
.at(loc));
269331
}
270332
};
333+
271334
let mut output = split_lines_and_normalize(&output);
272335
let mut expected_results = split_lines_and_normalize(&expected_results);
273336
match sort_mode.as_ref().or(self.sort_mode.as_ref()) {

0 commit comments

Comments
 (0)