use super::*;
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: u64) -> u64 {
self.next() % n
}
}
fn random_json(rng: &mut Rng, depth: u32) -> String {
let pick = if depth == 0 { 4 + rng.below(5) } else { rng.below(9) };
match pick {
0 | 1 => {
let n = rng.below(4);
let fields: Vec<String> = (0..n)
.map(|_| {
let key = ["a", "b", "c", "d"][rng.below(4) as usize];
format!("\"{key}\": {}", random_json(rng, depth - 1))
})
.collect();
format!("{{{}}}", fields.join(", "))
}
2 | 3 => {
let n = [0, 1, 2, 12][rng.below(4) as usize];
let items: Vec<String> = (0..n).map(|_| random_json(rng, depth - 1)).collect();
format!("[{}]", items.join(", "))
}
4 => "null".into(),
5 => "true".into(),
6 => rng.below(100).to_string(),
7 => "1.5".into(),
_ => "\"s\"".into(),
}
}
fn node_of(documents: &[String]) -> SchemaNode {
let mut node = SchemaNode::new();
for document in documents {
let mut bytes = document.clone().into_bytes();
let tape = simd_json::to_tape(&mut bytes).unwrap();
node.add_object(DataType::Object(tape.as_value()));
}
node
}
fn random_documents(rng: &mut Rng) -> Vec<String> {
(0..rng.below(4)).map(|_| random_json(rng, 3)).collect()
}
fn build(documents: &[String], from_schema: bool) -> SchemaNode {
let node = node_of(documents);
if !from_schema {
return node;
}
let mut by_schema = SchemaNode::new();
by_schema.add_schema(DataType::Schema(&node.to_schema()));
by_schema
}
#[test]
fn test_absorb_matches_adding_the_schema() {
let mut rng = Rng(0x9e37_79b9_7f4a_7c15);
for _ in 0..5000 {
let (target, other) = (random_documents(&mut rng), random_documents(&mut rng));
let target_from_schema = rng.below(3) == 0;
let mut by_schema = build(&target, target_from_schema);
by_schema.add_schema(DataType::Schema(&build(&other, true).to_schema()));
let mut absorbed = build(&target, target_from_schema);
absorbed.absorb(build(&other, true));
assert_eq!(absorbed, by_schema, "{target:?} absorbing {other:?}");
}
}
#[test]
fn test_absorb_matches_adding_the_documents() {
let mut rng = Rng(0x5851_f42d_4c95_7f2d);
for _ in 0..5000 {
let (target, other) = (random_documents(&mut rng), random_documents(&mut rng));
let mut absorbed = node_of(&target);
absorbed.absorb(node_of(&other));
let together = node_of(&[target.clone(), other.clone()].concat());
assert_eq!(
absorbed.to_schema(),
together.to_schema(),
"{target:?} absorbing {other:?}"
);
}
}
#[test]
fn test_absorb_keeps_a_key_missing_from_an_empty_object_not_required() {
for (first, second) in [("{\"a\": 1}", "{}"), ("{}", "{\"a\": 1}")] {
let mut absorbed = node_of(&[first.to_string()]);
absorbed.absorb(node_of(&[second.to_string()]));
assert_eq!(absorbed.to_schema().get("required"), None, "{first} then {second}");
}
}
#[test]
fn test_absorb_all_matches_absorbing_in_turn() {
let mut rng = Rng(0x0123_4567_89ab_cdef);
for _ in 0..300 {
let n = PAR_MERGE_MIN + rng.below(8) as usize;
let rows: Vec<Vec<String>> = (0..n)
.map(|_| {
let fields: Vec<String> = (0..rng.below(4))
.map(|_| {
let key = ["a", "b", "c"][rng.below(3) as usize];
format!("\"{key}\": {}", random_json(&mut rng, 2))
})
.collect();
vec![format!("{{{}}}", fields.join(", "))]
})
.collect();
let from_schema = rng.below(3) == 0;
let target: Vec<String> = random_documents(&mut rng);
let mut in_turn = build(&target, from_schema);
for row in &rows {
in_turn.absorb(node_of(row));
}
let mut all = build(&target, from_schema);
all.absorb_all(rows.iter().map(|row| node_of(row)).collect());
assert_eq!(all, in_turn, "{target:?} absorbing {rows:?}");
let mut from_empty = SchemaNode::new();
from_empty.absorb_all(rows.iter().map(|row| node_of(row)).collect());
let mut in_turn = SchemaNode::new();
rows.iter().for_each(|row| in_turn.absorb(node_of(row)));
assert_eq!(from_empty, in_turn, "absorbing {rows:?}");
}
}
#[test]
fn test_hash_schema_matches_schema_equality() {
let hash = |node: &SchemaNode| {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
node.hash_schema(&mut hasher);
std::hash::Hasher::finish(&hasher)
};
let mut rng = Rng(0xfeed_beef_dead_cafe);
let mut equal = 0;
for _ in 0..20000 {
let (a, b) = (random_documents(&mut rng), random_documents(&mut rng));
let (a, b) = (build(&a, rng.below(3) == 0), build(&b, rng.below(3) == 0));
let text = |node: &SchemaNode| serde_json::to_string(&node.to_schema()).unwrap();
let same_schema = text(&a) == text(&b);
assert_eq!(hash(&a) == hash(&b), same_schema, "{} vs {}", a.to_schema(), b.to_schema());
equal += same_schema as usize;
}
assert!(equal > 100, "too few equal pairs ({equal}) to test anything");
}
#[derive(Default)]
struct Recorder(Vec<u8>);
impl Hasher for Recorder {
fn write(&mut self, bytes: &[u8]) {
self.0.extend_from_slice(bytes);
}
fn finish(&self) -> u64 {
0
}
}
#[test]
fn test_scalar_hash_is_its_serialised_schema() {
let schemas = [
json!({"type": "null"}),
json!({"type": "boolean"}),
json!({"type": "integer"}),
json!({"type": "number"}),
json!({"type": "string"}),
json!({"type": "string", "format": "date"}),
json!({"type": "integer", "minimum": 0}),
];
for schema in schemas {
let mut node = SchemaNode::new();
node.add_schema(DataType::Schema(&schema));
assert_eq!(node.active_strategies.len(), 1, "{schema}");
let strategy = &node.active_strategies[0];
let mut written = Recorder::default();
strategy.hash_schema(&mut written);
let mut expected = vec![b'S'];
serde_json::to_writer(&mut expected, &strategy.to_schema()).unwrap();
assert_eq!(
String::from_utf8_lossy(&written.0),
String::from_utf8_lossy(&expected),
"{schema}"
);
}
}
#[test]
fn test_hash_schema_tells_keyworded_scalars_apart() {
let hash = |schema: Value| {
let mut node = SchemaNode::new();
node.add_schema(DataType::Schema(&schema));
let mut hasher = std::collections::hash_map::DefaultHasher::new();
node.hash_schema(&mut hasher);
hasher.finish()
};
let in_object = |a: Value| json!({"type": "object", "properties": {"a": a}});
let bare = json!({"type": "string"});
let date = json!({"type": "string", "format": "date"});
let time = json!({"type": "string", "format": "time"});
for wrap in [|schema| schema, in_object] {
assert_ne!(hash(wrap(bare.clone())), hash(wrap(date.clone())));
assert_ne!(hash(wrap(date.clone())), hash(wrap(time.clone())));
assert_eq!(hash(wrap(date.clone())), hash(wrap(date.clone())));
}
assert_ne!(
hash(json!({"type": "integer"})),
hash(json!({"type": "number"}))
);
}
#[test]
fn test_hash_schema_values_are_unchanged() {
let keyworded = json!({"type": "object", "properties": {
"a": {"type": "string", "format": "date"},
"b": {"type": "integer", "minimum": 0},
}});
let mut rng = Rng(0x0dd_ba11_c0ff_ee00);
let mut hasher = xxhash_rust::xxh64::Xxh64::new(0);
for i in 0..2000 {
let mut node = build(&random_documents(&mut rng), rng.below(3) == 0);
if i % 5 == 0 {
node.add_schema(DataType::Schema(&keyworded));
}
node.hash_schema(&mut hasher);
}
assert_eq!(hasher.finish(), PINNED_NODES_HASH);
let mut builder = crate::genson_rs::SchemaBuilder::new(None);
let mut document = br#"{"a": 1, "b": [true, null, 1.5, "s"], "c": {"d": {}}}"#.to_vec();
builder.add_object(simd_json::to_tape(&mut document).unwrap().as_value());
assert_eq!(builder.schema_hash(), PINNED_BUILDER_HASH);
}
const PINNED_NODES_HASH: u64 = 4095574756938490984;
const PINNED_BUILDER_HASH: u64 = 11897637414506759766;
#[test]
fn test_add_schemas_matches_adding_in_turn() {
let mut rng = Rng(0x2545_f491_4f6c_dd1d);
for _ in 0..3000 {
let schemas: Vec<Value> = (0..1 + rng.below(5))
.map(|_| build(&random_documents(&mut rng), false).to_schema())
.collect();
let schemas: Vec<&Value> = schemas.iter().collect();
let mut in_turn = SchemaNode::new();
for schema in &schemas {
in_turn.add_schema(DataType::Schema(schema));
}
let mut batched = SchemaNode::new();
batched.add_schemas(&schemas);
assert_eq!(batched.to_schema(), in_turn.to_schema(), "adding {schemas:?}");
}
}
#[test]
fn test_add_schemas_into_a_seeded_node_matches_adding_in_turn() {
let mut rng = Rng(0x6c07_8965_4f35_a2b1);
for _ in 0..3000 {
let seed = build(&random_documents(&mut rng), false).to_schema();
let schemas: Vec<Value> = (0..1 + rng.below(5))
.map(|_| build(&random_documents(&mut rng), false).to_schema())
.collect();
let schemas: Vec<&Value> = schemas.iter().collect();
let mut in_turn = SchemaNode::new();
in_turn.add_schema(DataType::Schema(&seed));
for schema in &schemas {
in_turn.add_schema(DataType::Schema(schema));
}
let mut batched = SchemaNode::new();
batched.add_schema(DataType::Schema(&seed));
batched.add_schemas(&schemas);
assert_eq!(
batched.to_schema(),
in_turn.to_schema(),
"seed {seed} adding {schemas:?}"
);
}
}
#[test]
fn test_add_schemas_merges_a_property_the_node_already_holds() {
for keys in [&["a", "b"][..], &["a", "b", "c", "d", "e"]] {
let object = |value_type: &str| {
let properties: serde_json::Map<String, Value> = keys
.iter()
.map(|key| (key.to_string(), json!({"type": value_type})))
.collect();
json!({"type": "object", "properties": properties, "required": keys})
};
let mut node = SchemaNode::new();
node.add_schema(DataType::Schema(&object("integer")));
node.add_schemas(&[&object("string")]);
let schema = node.to_schema();
for key in keys {
assert_eq!(
schema["properties"][key]["type"],
json!(["integer", "string"]),
"{} keys: {schema}",
keys.len()
);
}
}
}
#[test]
fn test_absorb_scalar_matches_adding_its_schema() {
let scalars = ["null", "true", "1", "1.5", "\"s\""];
let schemas = [
json!({"type": "string", "format": "date"}),
json!({"type": "integer", "minimum": 0}),
json!({"type": "number", "minimum": 0}),
];
let target = |i: usize| match scalars.get(i) {
Some(s) => node_of(&[s.to_string()]),
None => {
let mut node = SchemaNode::new();
node.add_schema(DataType::Schema(&schemas[i - scalars.len()]));
node
}
};
for i in 0..scalars.len() + schemas.len() {
for other in scalars {
let other = || node_of(&[other.to_string()]);
let mut by_schema = target(i);
by_schema.add_schema(DataType::Schema(&other().to_schema()));
let mut absorbed = target(i);
absorbed.absorb(other());
assert_eq!(
absorbed,
by_schema,
"{} absorbing {}",
target(i).to_schema(),
other().to_schema()
);
}
}
}