Skip to content

Commit 584b2f0

Browse files
committed
[#222] test: 파싱 성공·실패 계약을 단언하는 property 추가
CodeRabbit 리뷰 반영. 기존 property는 전부 '패닉하지 않는다'와 '결정적이다'만 확인하고 결과를 버렸습니다. 지적대로 유효 SQL이 실제로 파싱되는지는 아무도 검증하지 않고 있었습니다. 이게 왜 문제인지 실제로 확인했습니다. parse()가 무조건 빈 결과를 돌려주도록 바꿔봤더니: parser_never_panics_on_arbitrary_text ok parser_never_panics_on_sql_like_input ok parser_never_panics_on_mutated_statement ok parsing_is_deterministic ok '아무것도 파싱하지 않는 파서'가 기존 property를 전부 만족합니다. 두 가지를 추가했습니다. - valid_statement() 생성기 + valid_statements_parse_into_exactly_one_statement 구성상 유효한 SQL(SELECT/INSERT/UPDATE/DELETE/DROP 7형태, 세미콜론 유무 포함)이 파싱되고 정확히 한 문장을 만드는지 확인합니다. - a_bare_statement_keyword_is_an_error 실패 쪽 계약입니다. 키워드만 있고 뒤가 없는 입력은 조용히 빈 결과가 아니라 에러여야 합니다. bite-test: 위의 '빈 결과만 돌려주는 파서'를 넣으면 신규 2건만 FAILED로 뒤집히고 기존 패닉 property는 전부 통과합니다. cargo test 687 passed / 0 failed (직전 683 + 4). clippy --all-targets 73건으로 master와 동일.
1 parent 40bae65 commit 584b2f0

1 file changed

Lines changed: 63 additions & 0 deletions

File tree

src/engine/parser/test/property.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,33 @@ fn mutated_statement() -> impl Strategy<Value = String> {
8484
})
8585
}
8686

87+
/// Statements that are valid by construction. Needed because every other
88+
/// generator here explores *malformed* input: they prove the parser does not
89+
/// panic, but nothing so far asserts that well-formed SQL actually parses. A
90+
/// parser that rejected everything would satisfy all the panic and determinism
91+
/// properties below.
92+
fn valid_statement() -> impl Strategy<Value = String> {
93+
let table = prop::sample::select(vec!["foo", "bar", "baz_1"]);
94+
let column = prop::sample::select(vec!["a", "b", "id"]);
95+
let literal = prop::sample::select(vec!["1", "42", "'x'"]);
96+
let terminator = prop::sample::select(vec!["", ";"]);
97+
98+
(table, column, literal, terminator).prop_flat_map(
99+
|(table, column, literal, terminator)| {
100+
let shapes = vec![
101+
format!("SELECT {} FROM {}", column, table),
102+
format!("SELECT * FROM {}", table),
103+
format!("SELECT {} FROM {} WHERE {} = {}", column, table, column, literal),
104+
format!("INSERT INTO {} ({}) VALUES ({})", table, column, literal),
105+
format!("UPDATE {} SET {} = {}", table, column, literal),
106+
format!("DELETE FROM {} WHERE {} = {}", table, column, literal),
107+
format!("DROP TABLE {}", table),
108+
];
109+
prop::sample::select(shapes).prop_map(move |shape| format!("{}{}", shape, terminator))
110+
},
111+
)
112+
}
113+
87114
proptest! {
88115
#![proptest_config(ProptestConfig::with_cases(1024))]
89116

@@ -150,6 +177,42 @@ proptest! {
150177

151178
/// The tokenizer must terminate and must not invent trailing tokens for
152179
/// input that is only whitespace.
180+
/// The success half of the contract: well-formed SQL must parse, and must
181+
/// produce exactly one statement. Without this, "the parser never panics"
182+
/// would also hold for a parser that rejected every input.
183+
#[test]
184+
fn valid_statements_parse_into_exactly_one_statement(sql in valid_statement()) {
185+
let parsed = try_parse(&sql);
186+
prop_assert!(
187+
parsed.is_ok(),
188+
"valid SQL should parse: {:?} -> {:?}",
189+
sql,
190+
parsed.as_ref().err()
191+
);
192+
prop_assert_eq!(
193+
parsed.unwrap().len(),
194+
1,
195+
"valid SQL should yield one statement: {:?}",
196+
sql
197+
);
198+
}
199+
200+
/// The failure half: input that cannot be a statement must come back as an
201+
/// error, not as a silently empty parse. A statement keyword with nothing
202+
/// after it is unambiguously incomplete.
203+
#[test]
204+
fn a_bare_statement_keyword_is_an_error(
205+
keyword in prop::sample::select(vec!["SELECT", "INSERT", "UPDATE", "DELETE", "DROP"])
206+
) {
207+
let parsed = try_parse(keyword);
208+
prop_assert!(
209+
parsed.is_err(),
210+
"{:?} alone is incomplete and must be rejected, got {:?}",
211+
keyword,
212+
parsed.ok()
213+
);
214+
}
215+
153216
#[test]
154217
fn whitespace_only_input_yields_no_statements(
155218
spaces in proptest::string::string_regex(r"[ \t\r\n]{0,40}").expect("valid regex")

0 commit comments

Comments
 (0)