mod support {
pub mod parser_hardening;
}
use proptest::prelude::*;
use reddb_server::storage::query::parser::{self, ParseError, ParserLimits};
use support::parser_hardening::{
self as harness, assert_no_panic_on, corpus::probabilistic_adversarial_inputs,
probabilistic_grammar, HardenedParser,
};
pub struct ProbabilisticParser;
impl HardenedParser for ProbabilisticParser {
type Error = ParseError;
fn parse(input: &str) -> Result<(), Self::Error> {
parser::parse(input).map(|_| ())
}
fn parse_with_limits(input: &str, limits: ParserLimits) -> Result<(), Self::Error> {
let mut p = parser::Parser::with_limits(input, limits)?;
p.parse().map(|_| ())
}
}
#[test]
fn probabilistic_parser_does_not_panic_on_adversarial_corpus() {
let handle = std::thread::Builder::new()
.stack_size(8 * 1024 * 1024)
.spawn(|| {
for (name, input) in probabilistic_adversarial_inputs() {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
assert_no_panic_on::<ProbabilisticParser>(&input);
}));
if result.is_err() {
panic!("probabilistic adversarial corpus entry {} panicked", name);
}
}
})
.expect("spawn corpus thread");
handle.join().expect("corpus thread panic");
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 256,
max_shrink_iters: 64,
..ProptestConfig::default()
})]
#[test]
fn proptest_create_drop_roundtrips(s in probabilistic_grammar::create_drop_stmt()) {
harness::roundtrip_property::<ProbabilisticParser>(&s);
prop_assert!(
ProbabilisticParser::parse(&s).is_ok(),
"create/drop did not parse: {}", s
);
}
#[test]
fn proptest_hll_op_roundtrips(s in probabilistic_grammar::hll_op_stmt()) {
harness::roundtrip_property::<ProbabilisticParser>(&s);
prop_assert!(
ProbabilisticParser::parse(&s).is_ok(),
"hll op did not parse: {}", s
);
}
#[test]
fn proptest_sketch_op_roundtrips(s in probabilistic_grammar::sketch_op_stmt()) {
harness::roundtrip_property::<ProbabilisticParser>(&s);
prop_assert!(
ProbabilisticParser::parse(&s).is_ok(),
"sketch op did not parse: {}", s
);
}
#[test]
fn proptest_filter_op_roundtrips(s in probabilistic_grammar::filter_op_stmt()) {
harness::roundtrip_property::<ProbabilisticParser>(&s);
prop_assert!(
ProbabilisticParser::parse(&s).is_ok(),
"filter op did not parse: {}", s
);
}
#[test]
fn proptest_modifier_roundtrips(s in probabilistic_grammar::modifier_stmt()) {
harness::roundtrip_property::<ProbabilisticParser>(&s);
prop_assert!(
ProbabilisticParser::parse(&s).is_ok(),
"modifier did not parse: {}", s
);
}
#[test]
fn proptest_probabilistic_arbitrary_suffix_no_panic(
prefix in prop_oneof![
Just("CREATE HLL ".to_string()),
Just("CREATE SKETCH ".to_string()),
Just("CREATE FILTER ".to_string()),
Just("HLL ADD ".to_string()),
Just("HLL COUNT ".to_string()),
Just("SKETCH ADD ".to_string()),
Just("FILTER ADD ".to_string()),
Just("FILTER CHECK ".to_string()),
Just("FILTER DELETE ".to_string()),
Just("DROP FILTER ".to_string()),
],
suffix in ".{0,512}",
) {
let s = format!("{}{}", prefix, suffix);
harness::roundtrip_property::<ProbabilisticParser>(&s);
}
#[test]
fn proptest_probabilistic_input_size_limit_enforced(len in 200usize..2000) {
let limits = ParserLimits {
max_input_bytes: 64,
..ParserLimits::default()
};
let suffix = "x".repeat(len);
let input = format!("CREATE FILTER {} CAPACITY 100", suffix);
let r = ProbabilisticParser::parse_with_limits(&input, limits);
prop_assert!(r.is_err(), "oversized probabilistic input must error");
}
}
use reddb_server::storage::query::ast::{ProbabilisticCommand, QueryExpr};
fn parse_query(input: &str) -> QueryExpr {
parser::parse(input)
.unwrap_or_else(|e| panic!("expected ok for {input:?}, got error: {e}"))
.query
}
#[test]
fn happy_create_hll_bare_parses() {
let q = parse_query("CREATE HLL visitors");
match q {
QueryExpr::ProbabilisticCommand(ProbabilisticCommand::CreateHll {
name,
precision,
if_not_exists,
}) => {
assert_eq!(name, "visitors");
assert_eq!(precision, 14);
assert!(!if_not_exists);
}
other => panic!("expected CreateHll, got {other:?}"),
}
}
#[test]
fn happy_create_hll_if_not_exists_parses() {
let q = parse_query("CREATE HLL IF NOT EXISTS visitors");
match q {
QueryExpr::ProbabilisticCommand(ProbabilisticCommand::CreateHll {
if_not_exists, ..
}) => assert!(if_not_exists),
other => panic!("expected CreateHll, got {other:?}"),
}
}
#[test]
fn happy_create_hll_precision_parses() {
let q = parse_query("CREATE HLL visitors PRECISION 14");
match q {
QueryExpr::ProbabilisticCommand(ProbabilisticCommand::CreateHll {
name,
precision,
if_not_exists,
}) => {
assert_eq!(name, "visitors");
assert_eq!(precision, 14);
assert!(!if_not_exists);
}
other => panic!("expected CreateHll, got {other:?}"),
}
}
#[test]
fn happy_hll_add_collects_string_elements() {
let q = parse_query("HLL ADD visitors 'alice' 'bob' 'carol'");
match q {
QueryExpr::ProbabilisticCommand(ProbabilisticCommand::HllAdd { name, elements }) => {
assert_eq!(name, "visitors");
assert_eq!(elements, vec!["alice", "bob", "carol"]);
}
other => panic!("expected HllAdd, got {other:?}"),
}
}
#[test]
fn happy_hll_count_multi_name_parses() {
let q = parse_query("HLL COUNT visitors_a visitors_b visitors_c");
match q {
QueryExpr::ProbabilisticCommand(ProbabilisticCommand::HllCount { names }) => {
assert_eq!(names, vec!["visitors_a", "visitors_b", "visitors_c"]);
}
other => panic!("expected HllCount, got {other:?}"),
}
}
#[test]
fn happy_create_sketch_with_width_parses() {
let q = parse_query("CREATE SKETCH events WIDTH 5000");
match q {
QueryExpr::ProbabilisticCommand(ProbabilisticCommand::CreateSketch {
name,
width,
depth,
if_not_exists,
}) => {
assert_eq!(name, "events");
assert_eq!(width, 5000);
assert_eq!(depth, 5, "default depth pinned at 5");
assert!(!if_not_exists);
}
other => panic!("expected CreateSketch, got {other:?}"),
}
}
#[test]
fn happy_sketch_add_with_count_parses() {
let q = parse_query("SKETCH ADD events 'click' 7");
match q {
QueryExpr::ProbabilisticCommand(ProbabilisticCommand::SketchAdd {
name,
element,
count,
}) => {
assert_eq!(name, "events");
assert_eq!(element, "click");
assert_eq!(count, 7);
}
other => panic!("expected SketchAdd, got {other:?}"),
}
}
#[test]
fn happy_sketch_add_default_count_is_one() {
let q = parse_query("SKETCH ADD events 'click'");
match q {
QueryExpr::ProbabilisticCommand(ProbabilisticCommand::SketchAdd { count, .. }) => {
assert_eq!(count, 1, "default count pinned at 1");
}
other => panic!("expected SketchAdd, got {other:?}"),
}
}
#[test]
fn happy_create_filter_with_capacity_parses() {
let q = parse_query("CREATE FILTER seen CAPACITY 200000");
match q {
QueryExpr::ProbabilisticCommand(ProbabilisticCommand::CreateFilter {
name,
capacity,
if_not_exists,
}) => {
assert_eq!(name, "seen");
assert_eq!(capacity, 200_000);
assert!(!if_not_exists);
}
other => panic!("expected CreateFilter, got {other:?}"),
}
}
#[test]
fn happy_create_filter_default_capacity_is_100k() {
let q = parse_query("CREATE FILTER seen");
match q {
QueryExpr::ProbabilisticCommand(ProbabilisticCommand::CreateFilter {
capacity, ..
}) => {
assert_eq!(capacity, 100_000, "default capacity pinned at 100k");
}
other => panic!("expected CreateFilter, got {other:?}"),
}
}
#[test]
fn happy_filter_add_check_delete_roundtrip() {
match parse_query("FILTER ADD seen 'user-42'") {
QueryExpr::ProbabilisticCommand(ProbabilisticCommand::FilterAdd { name, element }) => {
assert_eq!(name, "seen");
assert_eq!(element, "user-42");
}
other => panic!("expected FilterAdd, got {other:?}"),
}
match parse_query("FILTER CHECK seen 'user-42'") {
QueryExpr::ProbabilisticCommand(ProbabilisticCommand::FilterCheck { name, element }) => {
assert_eq!(name, "seen");
assert_eq!(element, "user-42");
}
other => panic!("expected FilterCheck, got {other:?}"),
}
match parse_query("FILTER DELETE seen 'user-42'") {
QueryExpr::ProbabilisticCommand(ProbabilisticCommand::FilterDelete { name, element }) => {
assert_eq!(name, "seen");
assert_eq!(element, "user-42");
}
other => panic!("expected FilterDelete, got {other:?}"),
}
}
#[test]
fn happy_filter_count_and_drop_filter_if_exists() {
match parse_query("FILTER COUNT seen") {
QueryExpr::ProbabilisticCommand(ProbabilisticCommand::FilterCount { name }) => {
assert_eq!(name, "seen");
}
other => panic!("expected FilterCount, got {other:?}"),
}
match parse_query("DROP FILTER IF EXISTS seen") {
QueryExpr::ProbabilisticCommand(ProbabilisticCommand::DropFilter { name, if_exists }) => {
assert_eq!(name, "seen");
assert!(if_exists);
}
other => panic!("expected DropFilter, got {other:?}"),
}
}
#[test]
fn sketch_depth_clause_parses() {
let parsed = parser::parse("CREATE SKETCH events DEPTH 5")
.expect("DEPTH should be accepted as a sketch modifier");
match parsed.query {
reddb_server::storage::query::ast::QueryExpr::ProbabilisticCommand(
ProbabilisticCommand::CreateSketch { width, depth, .. },
) => {
assert_eq!(width, 1000, "default WIDTH stays at 1000");
assert_eq!(depth, 5, "user-supplied DEPTH lands at 5");
}
other => panic!("expected CreateSketch, got {other:?}"),
}
}
#[test]
fn filter_capacity_zero_rejected() {
let r = parser::parse("CREATE FILTER seen CAPACITY 0");
assert!(r.is_err(), "CAPACITY=0 should be rejected as degenerate");
}
#[test]
fn sketch_width_zero_rejected() {
let r = parser::parse("CREATE SKETCH events WIDTH 0");
assert!(r.is_err(), "WIDTH=0 should be rejected as degenerate");
}
#[test]
fn filter_capacity_negative_surfaces_positive_integer_error() {
let r = parser::parse("CREATE FILTER seen CAPACITY -1");
match r {
Err(e) => {
assert!(
format!("{e}")
.to_lowercase()
.contains("must be a positive integer"),
"expected 'must be a positive integer' message, got: {e}"
);
}
Ok(_) => panic!("CAPACITY -1 should not parse successfully"),
}
}