use anda_kip::{parse_json, parse_kip, parse_kml, parse_kql, parse_meta, quote_str, unquote_str};
use proptest::prelude::*;
const KQL_SEEDS: &[&str] = &[
r#"
FIND(?drug.name)
WHERE {
?drug {type: "Drug"}
}
"#,
r#"
FIND(?drug_class, COUNT(DISTINCT ?drug))
WHERE {
?drug CONCEPT {type: "Drug"}
(?drug, "is_class_of"|"subclass_of"{1,3}, ?drug_class)
OPTIONAL { ?edge STRUCTURAL (?drug, "has_step", ?step) }
FILTER(?drug.attributes.risk_level < 3 && !IS_NULL(?drug_class))
}
ORDER BY COUNT(?drug) DESC
LIMIT 10
"#,
r#"
FIND(?b, ?slot)
WHERE {
?b BELIEF (:alice, "timezone", ?tz)
?slot BELIEF SLOT (:alice, "timezone")
?a ASSERTION {proposition: (id: "P-1"), stance: "support"}
}
AS OF SEQ 4200
FOR TIME :world_time
WITH EPISTEMIC { explain: "summary" }
"#,
];
const KML_SEEDS: &[&str] = &[
r#"
ASSERT ?a (:alice, "prefers", :dark_mode) {
by: :alice,
mode: "stated",
confidence: 0.9,
evidence: [:msg, :screenshot]
} SUPERSEDING :old
"#,
r#"
MUTATE {
CREATE CONCEPT ?alice { TYPE "Person" CLIENT KEY "person:alice" NAME "Alice" }
CREATE EVIDENCE ?msg { SET FIELDS { evidence_class: "user_statement" } }
ENSURE PROPOSITION ?p (?alice, "prefers", :dark_mode)
CREATE ASSERTION ?claim {
SET FIELDS { proposition: ?p, asserted_by: ?alice, stance: "support" }
SET STRUCTURAL { ("evidence", ?msg) {role: "support"} }
}
UPSERT CONCEPT ?drug {
MATCH {key: "drug:aspirin"}
SET ATTRIBUTES { risk_level: 2 }
UNSET ATTRIBUTES { deprecated_note }
SET FACET "MnemonicState" { salience: 0.4 }
} EXPECT VERSION 3
UPDATE ?c
SET FACET "MnemonicState" { memory_strength: MUL(?c.facets["MnemonicState"].memory_strength, 0.99) }
WHERE { ?c CONCEPT {type: "Experience"} }
LIMIT 100
EXPECT VERSION :v OF FACET "MnemonicState"
TRANSITION :act TO "completed" SET FIELDS { ended_at: :now }
TRANSITION :old TO "superseded" BY :claim
TRANSITION :stale TO "archived" WHERE { ?stale CONCEPT {type: "Event"} } LIMIT 5 EXPECT VERSION 2 OF ATTRIBUTES
PURGE :leak REFERENCE POLICY "detach" CONFIRM "PURGE"
}
"#,
];
const META_SEEDS: &[&str] = &[
"DESCRIBE PRIMER MODE \"compact\"",
"DESCRIBE TYPE \"Person\"",
"LIST SCHEMA PACKAGES STATUS \"active\" LIMIT 20 CURSOR :page",
"SEARCH COGNITION \"dark mode\" WITH TYPE \"Preference\" MODE \"hybrid\" THRESHOLD 0.7 LIMIT 5",
"HISTORY ELEMENT \"C-1\" FROM SEQ 1 TO SEQ 99 LIMIT 10",
"EXPORT CAPSULE :out WHERE { ?c CONCEPT {type: \"Experience\"} } WITH { redact: true } AS OF SEQ 7",
];
const SPLICES: &[&str] = &[
"{",
"}",
"(",
")",
"[",
"]",
"\"",
"\\",
"'",
"?",
"_",
",",
"@",
"$",
":",
";",
"|",
"\u{0}",
"\u{7f}",
"🦀",
"NULL",
"FIND",
"MUTATE",
"ASSERT",
"BELIEF",
"SLOT",
"SUPERSEDING",
"PURGE",
"CONFIRM",
"WHERE",
"META",
"ATTRIBUTES",
"STRUCTURAL",
"EXPECT",
"AS OF",
"0x",
"1e999",
"-0",
"{1,}",
"//",
"/*",
"*/",
"\n",
"\r\n",
"\t",
"?var",
"\"unterminated",
];
fn all_parsers_terminate(input: &str) {
let _ = parse_kip(input);
let _ = parse_kql(input);
let _ = parse_kml(input);
let _ = parse_meta(input);
let _ = parse_json(input);
}
#[derive(Debug, Clone)]
enum Mutation {
Delete { at: usize, len: usize },
Insert { at: usize, splice: usize },
Truncate { at: usize },
Duplicate { at: usize, len: usize },
}
fn mutation_strategy() -> impl Strategy<Value = Mutation> {
prop_oneof![
(0usize..10_000, 1usize..64).prop_map(|(at, len)| Mutation::Delete { at, len }),
(0usize..10_000, 0usize..SPLICES.len())
.prop_map(|(at, splice)| Mutation::Insert { at, splice }),
(0usize..10_000).prop_map(|at| Mutation::Truncate { at }),
(0usize..10_000, 1usize..64).prop_map(|(at, len)| Mutation::Duplicate { at, len }),
]
}
fn apply_mutations(seed: &str, mutations: &[Mutation]) -> String {
let mut chars: Vec<char> = seed.chars().collect();
for mutation in mutations {
if chars.is_empty() {
break;
}
match mutation {
Mutation::Delete { at, len } => {
let at = at % chars.len();
let end = (at + len).min(chars.len());
chars.drain(at..end);
}
Mutation::Insert { at, splice } => {
let at = at % (chars.len() + 1);
chars.splice(at..at, SPLICES[*splice].chars());
}
Mutation::Truncate { at } => {
chars.truncate(at % (chars.len() + 1));
}
Mutation::Duplicate { at, len } => {
let at = at % chars.len();
let end = (at + len).min(chars.len());
let dup: Vec<char> = chars[at..end].to_vec();
chars.splice(end..end, dup);
}
}
}
chars.into_iter().collect()
}
#[test]
fn seed_documents_parse_ok() {
for seed in KQL_SEEDS {
parse_kql(seed).unwrap_or_else(|err| panic!("KQL seed failed to parse: {err:?}"));
}
for seed in KML_SEEDS {
parse_kml(seed).unwrap_or_else(|err| panic!("KML seed failed to parse: {err:?}"));
}
for seed in META_SEEDS {
parse_meta(seed).unwrap_or_else(|err| panic!("META seed failed to parse: {err:?}"));
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(128))]
#[test]
fn arbitrary_input_never_panics(input in "\\PC{0,300}") {
all_parsers_terminate(&input);
}
#[test]
fn arbitrary_bytes_never_panic(input in prop::collection::vec(any::<char>(), 0..200)) {
let input: String = input.into_iter().collect();
all_parsers_terminate(&input);
}
#[test]
fn mutated_statements_never_panic(
seed in 0usize..(KQL_SEEDS.len() + KML_SEEDS.len() + META_SEEDS.len()),
mutations in prop::collection::vec(mutation_strategy(), 1..10),
) {
let seeds: Vec<&str> = KQL_SEEDS
.iter()
.chain(KML_SEEDS)
.chain(META_SEEDS)
.copied()
.collect();
let input = apply_mutations(seeds[seed], &mutations);
all_parsers_terminate(&input);
}
#[test]
fn mutated_meta_never_panics(
seed in 0usize..META_SEEDS.len(),
mutations in prop::collection::vec(mutation_strategy(), 1..6),
) {
let input = apply_mutations(META_SEEDS[seed], &mutations);
all_parsers_terminate(&input);
}
#[test]
fn quote_unquote_roundtrip(input in "\\PC{0,200}") {
let quoted = quote_str(&input);
prop_assert_eq!(unquote_str("ed), Some(input));
}
}