use spate_core::checkpoint::AckRef;
use spate_core::deser::{Deserializer, Owned};
use spate_core::framing::RecordFramer;
use spate_core::record::{PartitionId, RawPayload};
use spate_json::{JsonDeserializerBuilder, JsonFraming, JsonSettings, NdjsonFramer, OnError};
#[path = "../benches/support/decode_rig.rs"]
mod decode_rig;
#[path = "../benches/support/frame_rig.rs"]
mod frame_rig;
#[path = "../benches/support/lines.rs"]
mod lines;
#[path = "../benches/support/orders.rs"]
mod orders;
#[path = "../benches/support/shapes.rs"]
mod shapes;
use decode_rig::Sink;
use lines::Eol;
use orders::{BAD_EVERY, Corruption, LineItem, RECORDS};
fn digest(bytes: &[u8]) -> u64 {
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
for &byte in bytes {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x1000_0000_01b3);
}
hash
}
fn pin(bytes: &[u8]) -> (usize, u64) {
(bytes.len(), digest(bytes))
}
fn raw(bytes: &[u8]) -> RawPayload<'_> {
RawPayload {
bytes,
key: None,
partition: PartitionId(0),
offset: 1,
timestamp_ms: 0,
}
}
fn drive(settings: JsonSettings, payload: &[u8]) -> (bool, u64) {
let mut deser = JsonDeserializerBuilder::from_settings(settings).build_serde::<LineItem>();
let (ack, _rx) = AckRef::test_pair();
let mut sink = Sink(0);
let failed = deser.deserialize(&raw(payload), &ack, &mut sink).is_err();
(failed, sink.0)
}
fn drive_value(settings: JsonSettings, payload: &[u8]) -> (bool, u64) {
let mut deser = JsonDeserializerBuilder::from_settings(settings).build_value();
let (ack, _rx) = AckRef::test_pair();
let mut sink = Sink(0);
let failed = deser.deserialize(&raw(payload), &ack, &mut sink).is_err();
(failed, sink.0)
}
fn settings(framing: JsonFraming, on_error: OnError, reject_duplicate_keys: bool) -> JsonSettings {
JsonSettings {
framing,
on_error,
reject_duplicate_keys,
}
}
fn frame(stream: &[u8], chunk: usize) -> (usize, usize) {
let mut framer = NdjsonFramer::new(frame_rig::MAX_RECORD_BYTES);
let (mut records, mut bytes) = (0usize, 0usize);
for piece in stream.chunks(chunk) {
framer.push(piece).expect("inside the record cap");
while let Some(record) = framer.pop() {
records += 1;
bytes += record.len();
}
}
framer.finish().expect("the stream frames cleanly");
while let Some(record) = framer.pop() {
records += 1;
bytes += record.len();
}
(records, bytes)
}
#[test]
fn the_corpora_are_reproducible() {
assert_eq!(orders::lines_ndjson(RECORDS), orders::lines_ndjson(RECORDS));
assert_eq!(
orders::lines_ndjson_bad_every(RECORDS, BAD_EVERY, Corruption::TypeMismatch),
orders::lines_ndjson_bad_every(RECORDS, BAD_EVERY, Corruption::TypeMismatch)
);
assert_eq!(shapes::wide_flat(), shapes::wide_flat());
assert_eq!(shapes::deep_nested(), shapes::deep_nested());
assert_eq!(shapes::numeric_array(), shapes::numeric_array());
assert_eq!(shapes::large_string(), shapes::large_string());
assert_eq!(
lines::stream(lines::RECORDS, lines::LINE_BYTES, Eol::Lf, 0),
lines::stream(lines::RECORDS, lines::LINE_BYTES, Eol::Lf, 0)
);
}
#[test]
fn the_line_item_shape_is_the_measured_workload() {
let doc = serde_json::to_value(orders::sample_line(7)).expect("encode an order line");
let obj = doc.as_object().expect("an order line is a JSON object");
let shape: Vec<(&str, &str)> = obj
.iter()
.map(|(k, v)| {
let kind = match v {
serde_json::Value::String(_) => "string",
serde_json::Value::Bool(_) => "bool",
serde_json::Value::Array(_) => "array",
serde_json::Value::Number(n) if n.is_f64() => "float",
serde_json::Value::Number(_) => "int",
serde_json::Value::Null => "null",
serde_json::Value::Object(_) => "object",
};
(k.as_str(), kind)
})
.collect();
assert_eq!(
shape,
[
("ok", "bool"),
("qty", "int"),
("ratio", "float"),
("sku", "string"),
("tags", "array"),
("ts_ms", "int"),
("unit", "string"),
]
);
}
#[test]
fn the_corpora_are_pinned_across_revisions() {
assert_eq!(
pin(&orders::order_document()),
(265, 0xed39_44fc_72ba_fb50),
"single_typed / single_value"
);
assert_eq!(
pin(&orders::lines_ndjson(RECORDS)),
(242_897, 0xa3d6_4abc_50c3_2286),
"ndjson_clean / ndjson_fail_clean"
);
assert_eq!(
pin(&orders::lines_array(RECORDS)),
(242_898, 0x0c28_c24d_f9f9_05a4),
"array_clean"
);
assert_eq!(
pin(&orders::lines_ndjson_bad_every(
RECORDS,
BAD_EVERY,
Corruption::Syntax
)),
(242_697, 0x2c32_faca_f2c0_1388),
"ndjson_syntax_10pct"
);
assert_eq!(
pin(&orders::lines_ndjson_bad_every(
RECORDS,
BAD_EVERY,
Corruption::TypeMismatch
)),
(241_186, 0x699a_8e82_2d17_1b30),
"ndjson_type_10pct"
);
assert_eq!(
pin(&orders::lines_ndjson_bad_every(
RECORDS,
1,
Corruption::Syntax
)),
(240_897, 0x4be5_a663_e642_cb62),
"ndjson_syntax_all"
);
assert_eq!(
pin(&orders::lines_ndjson_bad_last(
RECORDS,
Corruption::TypeMismatch
)),
(242_889, 0x8576_1234_48d1_cc3c),
"ndjson_fail_bad_last"
);
assert_eq!(
pin(&orders::lines_array_bad_last(RECORDS)),
(242_890, 0xe02a_92e8_bb84_e836),
"array_bad_last"
);
assert_eq!(
pin(&shapes::wide_flat()),
(74_287, 0x075b_13f6_2104_f0db),
"wide_flat / dup_guard_wide"
);
assert_eq!(
pin(&shapes::wide_flat_duplicate_key()),
(74_287, 0x6f95_ab21_4ce9_0f31),
"dup_guard_hit"
);
assert_eq!(
pin(&shapes::deep_nested()),
(143_958, 0x0941_11aa_484c_8fcb),
"deep_nested / dup_guard_deep"
);
assert_eq!(
pin(&shapes::numeric_array()),
(388_106, 0xe2a9_d100_9c4f_0be4),
"numeric_array"
);
assert_eq!(
pin(&shapes::large_string()),
(532_518, 0x27fc_30d7_f65d_8cd5),
"large_string"
);
assert_eq!(
pin(&lines::stream(
lines::RECORDS,
lines::LINE_BYTES,
Eol::Lf,
0
)),
(1_608_000, 0x0d34_2f26_6fec_1036),
"lf_fetch_chunks / lf_line_chunks / lf_split_chunks"
);
assert_eq!(
pin(&lines::stream(
lines::RECORDS,
lines::LINE_BYTES,
Eol::Crlf,
0
)),
(1_616_000, 0xb870_694c_3a68_f4be),
"crlf_fetch_chunks"
);
assert_eq!(
pin(&lines::stream(
lines::RECORDS,
lines::LINE_BYTES,
Eol::Lf,
1
)),
(1_616_000, 0x86ad_0a8e_5f37_61d2),
"lf_blank_interleaved"
);
assert_eq!(
pin(&lines::stream(
lines::WIDE_RECORDS,
lines::WIDE_LINE_BYTES,
Eol::Lf,
0
)),
(1_601_000, 0x1470_cca1_b2c4_3f60),
"lf_wide_lines"
);
}
#[test]
fn the_error_cases_emit_what_they_claim() {
let skip_ndjson = settings(JsonFraming::Ndjson, OnError::Skip, false);
assert_eq!(
drive(skip_ndjson.clone(), &orders::lines_ndjson(RECORDS)),
(false, RECORDS),
"ndjson_clean"
);
for how in [Corruption::Syntax, Corruption::TypeMismatch] {
assert_eq!(
drive(
skip_ndjson.clone(),
&orders::lines_ndjson_bad_every(RECORDS, BAD_EVERY, how)
),
(false, orders::good_lines(RECORDS, BAD_EVERY)),
"ndjson_*_10pct with {how:?}"
);
}
assert_eq!(
drive(
skip_ndjson,
&orders::lines_ndjson_bad_every(RECORDS, 1, Corruption::Syntax)
),
(false, 0),
"ndjson_syntax_all"
);
let fail_ndjson = settings(JsonFraming::Ndjson, OnError::Fail, false);
assert_eq!(
drive(fail_ndjson.clone(), &orders::lines_ndjson(RECORDS)),
(false, RECORDS),
"ndjson_fail_clean"
);
assert_eq!(
drive(
fail_ndjson,
&orders::lines_ndjson_bad_last(RECORDS, Corruption::TypeMismatch)
),
(true, 0),
"ndjson_fail_bad_last: the payload fails and emits no prefix"
);
let skip_array = settings(JsonFraming::Array, OnError::Skip, false);
assert_eq!(
drive(skip_array.clone(), &orders::lines_array(RECORDS)),
(false, RECORDS),
"array_clean"
);
assert_eq!(
drive(skip_array, &orders::lines_array_bad_last(RECORDS)),
(false, 0),
"array_bad_last: one bad element drops the whole payload"
);
}
#[test]
fn the_two_corruptions_fail_for_different_reasons() {
let syntax = orders::bad_line(0, Corruption::Syntax);
let mismatch = orders::bad_line(0, Corruption::TypeMismatch);
assert!(
serde_json::from_slice::<serde_json::Value>(&syntax).is_err(),
"the truncated record is still well-formed JSON, so it is not a syntax \
corruption at all"
);
assert!(
serde_json::from_slice::<serde_json::Value>(&mismatch).is_ok(),
"the mismatched record no longer parses, so it is a second syntax case \
rather than the type-mismatch one"
);
let one = |bytes: &[u8]| {
let mut deser = JsonDeserializerBuilder::from_settings(settings(
JsonFraming::Single,
OnError::Fail,
false,
))
.build_serde::<LineItem>();
let (ack, _rx) = AckRef::test_pair();
let mut sink = Sink(0);
deser
.deserialize(&raw(bytes), &ack, &mut sink)
.expect_err("a poison record decodes cleanly")
.to_string()
};
assert_ne!(
one(&syntax),
one(&mismatch),
"the two corruptions produce the same failure, so the error-kind axis \
is one case measured twice"
);
}
#[test]
fn the_duplicate_key_corpus_is_the_only_one_the_guard_rejects() {
let guarded = settings(JsonFraming::Single, OnError::Skip, true);
let unguarded = settings(JsonFraming::Single, OnError::Skip, false);
assert_eq!(
drive_value(guarded.clone(), &shapes::wide_flat()),
(false, 1),
"dup_guard_wide: clean input still decodes with the guard on"
);
assert_eq!(
drive_value(guarded.clone(), &shapes::deep_nested()),
(false, 1),
"dup_guard_deep: clean input still decodes with the guard on"
);
assert_eq!(
drive_value(guarded, &shapes::wide_flat_duplicate_key()),
(false, 0),
"dup_guard_hit: the guard drops the duplicated document"
);
assert_eq!(
drive_value(unguarded, &shapes::wide_flat_duplicate_key()),
(false, 1),
"the duplicated document decodes fine with the guard off, so the \
guard case is measuring the guard and not a malformed payload"
);
}
#[test]
fn the_duplicate_is_the_last_key_in_the_object() {
let clean: serde_json::Value = serde_json::from_slice(&shapes::wide_flat()).unwrap();
let duplicated: serde_json::Value =
serde_json::from_slice(&shapes::wide_flat_duplicate_key()).unwrap();
let clean = clean.as_object().expect("an object");
let duplicated = duplicated.as_object().expect("an object");
assert_eq!(clean.len(), shapes::WIDE_FIELDS);
assert_eq!(
duplicated.len(),
shapes::WIDE_FIELDS - 1,
"the duplicated document does not repeat exactly one key"
);
let last = format!("f{:06}", shapes::WIDE_FIELDS - 1);
assert!(
clean.contains_key(&last),
"the clean document has no last field to have been repeated"
);
assert!(
!duplicated.contains_key(&last),
"the repeated key is not the last one, so the guard finds it early"
);
}
#[test]
fn the_shape_corpora_have_the_shapes_their_cases_name() {
let deep: serde_json::Value = serde_json::from_slice(&shapes::deep_nested()).unwrap();
let docs = deep.as_array().expect("an array of documents");
assert_eq!(docs.len(), shapes::DEEP_DOCS);
for doc in docs {
let mut level = doc;
let mut depth = 1;
while let Some(next) = level.as_object().expect("an object").get("n") {
level = next;
depth += 1;
}
assert_eq!(
depth,
shapes::DEEP_DEPTH,
"a document is not as deep as claimed"
);
assert_eq!(
level.as_object().expect("an object").len(),
shapes::DEEP_WIDTH,
"the innermost level carries a recursive field it should not"
);
}
let numbers: serde_json::Value = serde_json::from_slice(&shapes::numeric_array()).unwrap();
let numbers = numbers.as_array().expect("an array");
assert_eq!(numbers.len(), shapes::NUMBERS);
assert!(
numbers.iter().all(serde_json::Value::is_number),
"the numeric array carries something that is not a number"
);
let large: serde_json::Value = serde_json::from_slice(&shapes::large_string()).unwrap();
let text = large["text"].as_str().expect("a text field");
assert_eq!(
text.len(),
shapes::TEXT_BYTES,
"the decoded text is not the declared length, so the escapes are not \
being counted the way the corpus builder counts them"
);
let others: usize = large
.as_object()
.expect("an object")
.iter()
.filter(|(key, _)| key.as_str() != "text")
.map(|(key, value)| key.len() + value.to_string().len())
.sum();
assert!(
text.len() > 1000 * others,
"the text field ({} bytes) no longer dwarfs its neighbors ({others} \
bytes), which is the whole premise of the case",
text.len()
);
}
#[test]
fn every_stream_frames_its_declared_records_under_every_chunking() {
for (records, width) in [
(lines::RECORDS, lines::LINE_BYTES),
(lines::WIDE_RECORDS, lines::WIDE_LINE_BYTES),
] {
for eol in [Eol::Lf, Eol::Crlf] {
for blank_every in [0, 1] {
let stream = lines::stream(records, width, eol, blank_every);
let want = (records, lines::expect_bytes(records, width));
for chunk in [
lines::FETCH_CHUNK_BYTES,
width + 1,
lines::SPLIT_CHUNK_BYTES,
stream.len(),
] {
assert_eq!(
frame(&stream, chunk),
want,
"{records}x{width} eol={eol:?} blank_every={blank_every} chunk={chunk}"
);
}
}
}
}
}
#[test]
fn every_framed_line_is_a_json_document_of_the_declared_width() {
for (records, width) in [
(lines::RECORDS, lines::LINE_BYTES),
(lines::WIDE_RECORDS, lines::WIDE_LINE_BYTES),
] {
let stream = lines::stream(records, width, Eol::Lf, 0);
let mut framer = NdjsonFramer::new(frame_rig::MAX_RECORD_BYTES);
framer.push(&stream).unwrap();
framer.finish().unwrap();
let mut seen = 0;
while let Some(record) = framer.pop() {
assert_eq!(record.len(), width, "a line is not the declared width");
serde_json::from_slice::<serde_json::Value>(&record)
.expect("a framed line is not a JSON document");
seen += 1;
}
assert_eq!(seen, records);
}
}
#[test]
fn the_wide_stream_is_the_same_quantity_of_bytes() {
assert_eq!(
lines::expect_bytes(lines::RECORDS, lines::LINE_BYTES),
lines::expect_bytes(lines::WIDE_RECORDS, lines::WIDE_LINE_BYTES),
);
}
#[test]
fn a_second_drive_emits_what_the_first_did() {
let mut ndjson = decode_rig::batch_rig::<LineItem>(
JsonFraming::Ndjson,
OnError::Skip,
orders::lines_ndjson(RECORDS),
RECORDS,
("fixtures-ndjson", "json"),
);
let mut array = decode_rig::batch_rig::<LineItem>(
JsonFraming::Array,
OnError::Skip,
orders::lines_array(RECORDS),
RECORDS,
("fixtures-array", "json"),
);
let mut poisoned = decode_rig::batch_rig::<LineItem>(
JsonFraming::Ndjson,
OnError::Skip,
orders::lines_ndjson_bad_every(RECORDS, BAD_EVERY, Corruption::Syntax),
orders::good_lines(RECORDS, BAD_EVERY),
("fixtures-poisoned", "json"),
);
let mut storm = decode_rig::batch_rig::<LineItem>(
JsonFraming::Ndjson,
OnError::Skip,
orders::lines_ndjson_bad_every(RECORDS, 1, Corruption::Syntax),
0,
("fixtures-storm", "json"),
);
for drive in 1..=3 {
assert_eq!(
decode_rig::decode_run::<Owned<LineItem>, _>(&mut ndjson),
RECORDS,
"ndjson drive {drive}"
);
assert_eq!(
decode_rig::decode_run::<Owned<LineItem>, _>(&mut array),
RECORDS,
"array drive {drive}"
);
assert_eq!(
decode_rig::decode_run::<Owned<LineItem>, _>(&mut poisoned),
orders::good_lines(RECORDS, BAD_EVERY),
"poisoned drive {drive}"
);
assert_eq!(
decode_rig::decode_run::<Owned<LineItem>, _>(&mut storm),
0,
"storm drive {drive}"
);
}
let mut failing = decode_rig::batch_rig::<LineItem>(
JsonFraming::Ndjson,
OnError::Fail,
orders::lines_ndjson_bad_last(RECORDS, Corruption::TypeMismatch),
0,
("fixtures-failing", "json"),
);
for drive in 1..=3 {
assert_eq!(
decode_rig::decode_run_err::<Owned<LineItem>, _>(&mut failing),
0,
"failing drive {drive}"
);
}
for (label, payload) in [
("fixtures-wide", shapes::wide_flat as fn() -> Vec<u8>),
("fixtures-deep", shapes::deep_nested),
("fixtures-numeric", shapes::numeric_array),
("fixtures-large", shapes::large_string),
] {
let mut rig = decode_rig::shape_rig(payload(), false, 1, (label, "json"));
for drive in 1..=3 {
assert_eq!(
decode_rig::decode_run::<Owned<serde_json::Value>, _>(&mut rig),
1,
"{label} drive {drive}"
);
}
}
let mut guarded =
decode_rig::shape_rig(shapes::wide_flat(), true, 1, ("fixtures-guard", "json"));
let mut rejected = decode_rig::shape_rig(
shapes::wide_flat_duplicate_key(),
true,
0,
("fixtures-reject", "json"),
);
for drive in 1..=3 {
assert_eq!(
decode_rig::decode_run::<Owned<serde_json::Value>, _>(&mut guarded),
1,
"guarded drive {drive}"
);
assert_eq!(
decode_rig::decode_run::<Owned<serde_json::Value>, _>(&mut rejected),
0,
"rejected drive {drive}"
);
}
let stream = lines::stream(lines::RECORDS, lines::LINE_BYTES, Eol::Lf, 0);
let rig = frame_rig::Rig {
chunks: lines::chunks(&stream, lines::FETCH_CHUNK_BYTES),
expect_records: lines::RECORDS,
expect_bytes: lines::expect_bytes(lines::RECORDS, lines::LINE_BYTES),
};
let first = frame_rig::frame_stream(&rig);
assert_eq!(
first,
(rig.expect_records, rig.expect_bytes),
"frame drive 1"
);
for drive in 2..=3 {
assert_eq!(frame_rig::frame_stream(&rig), first, "frame drive {drive}");
}
}
#[test]
fn the_decode_rig_would_accumulate_without_its_reset() {
let mut rig = decode_rig::batch_rig::<LineItem>(
JsonFraming::Ndjson,
OnError::Skip,
orders::lines_ndjson(RECORDS),
RECORDS,
("fixtures-noreset", "json"),
);
for _ in 0..2 {
let payload = decode_rig::raw_payload(&rig.payload);
rig.deser
.deserialize(&payload, &rig.ack, &mut rig.sink)
.expect("the corpus is clean");
}
assert_eq!(
rig.sink.0,
RECORDS * 2,
"two drives without a reset should have accumulated"
);
}