#![cfg(all(feature = "json", feature = "ron"))]
use crate::seeded_bytes;
use crate::seeded_sweep;
use lgwks_std::json;
use lgwks_std::ron::{self, FromSliceError};
use seeded_bytes::{below, fold_bytes, next_byte, next_text};
use seeded_sweep::{
SWEEP_SEEDS, assert_distinct_seeds_diverge, assert_same_seed_replays, fold, fold_usize,
initial_trace,
};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
struct Shape {
name: String,
tag: String,
count: i64,
nested: Nested,
items: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
struct Nested {
state: String,
depth: i64,
}
#[derive(Debug, serde::Deserialize)]
struct Borrowed<'a> {
value: &'a str,
}
fn draw_shape(state: &mut u64) -> Shape {
let name_len = below(state, 12);
let name = next_text(state, name_len);
let tag_len = below(state, 8);
let tag = next_text(state, tag_len);
let count = i64::from(next_byte(state)).saturating_sub(128);
let state_len = below(state, 6);
let nested = Nested {
state: next_text(state, state_len),
depth: i64::from(next_byte(state)),
};
let item_count = below(state, 4);
let items = (0..item_count)
.map(|_| {
let len = below(state, 6);
next_text(state, len)
})
.collect();
Shape {
name,
tag,
count,
nested,
items,
}
}
fn borrowed_from(buffer: &[u8], borrowed: &str) -> bool {
let start = buffer.as_ptr();
let end = start.wrapping_add(buffer.len());
let address = borrowed.as_ptr();
address >= start && address < end
}
fn codec_trace(seed: u64) -> u64 {
let mut state = seed;
let mut trace = initial_trace();
for _ in 0..24 {
let value = draw_shape(&mut state);
let json_text = json::to_string(&value).map_err(|error| error.to_string());
let json_back = json_text
.as_ref()
.map_err(Clone::clone)
.and_then(|text| json::from_str::<Shape>(text).map_err(|error| error.to_string()));
assert_eq!(
json_back,
Ok(value.clone()),
"seed {seed}: a value must round-trip through JSON text"
);
let ron_text = ron::to_string(&value).map_err(|error| error.to_string());
let ron_back = ron_text
.as_ref()
.map_err(Clone::clone)
.and_then(|text| ron::from_str::<Shape>(text).map_err(|error| error.to_string()));
assert_eq!(
ron_back,
Ok(value.clone()),
"seed {seed}: a value must round-trip through RON text"
);
if let Some((json_text, ron_text)) = json_text.as_ref().ok().zip(ron_text.as_ref().ok()) {
fold_bytes(&mut trace, json_text.as_bytes());
fold_bytes(&mut trace, ron_text.as_bytes());
}
fold_usize(&mut trace, value.items.len());
}
trace
}
#[test]
fn a_seeded_value_round_trips_through_json_text() -> Result<(), json::Error> {
for seed in SWEEP_SEEDS {
let mut state = seed;
for _ in 0..48 {
let value = draw_shape(&mut state);
let text = json::to_string(&value)?;
let restored: Shape = json::from_str(&text)?;
assert_eq!(
restored, value,
"seed {seed}: JSON must round-trip the value"
);
assert!(
!text.contains('\n'),
"seed {seed}: compact JSON has no newlines"
);
}
}
Ok(())
}
#[test]
fn a_seeded_value_round_trips_through_ron_text() -> Result<(), ron::Error> {
for seed in SWEEP_SEEDS {
let mut state = seed;
for _ in 0..48 {
let value = draw_shape(&mut state);
let text = ron::to_string(&value)?;
let restored: Shape = ron::from_str(&text)?;
assert_eq!(
restored, value,
"seed {seed}: RON must round-trip the value"
);
}
}
Ok(())
}
fn assert_field_borrows(seed: u64, buffer: &[u8], decoded: &Borrowed<'_>, value: &str, what: &str) {
assert_eq!(
decoded.value, value,
"seed {seed}: the {what} field keeps its value"
);
assert!(
borrowed_from(buffer, decoded.value),
"seed {seed}: the {what} field must point into the supplied input"
);
}
fn borrowed_document(value: &str, ron_style: bool) -> String {
if ron_style {
format!("(value: \"{value}\")")
} else {
format!(r#"{{"value":"{value}"}}"#)
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Door {
Text,
Slice,
}
fn check_text_door(
seed: u64,
ron_style: bool,
document: &str,
value: &str,
what: &str,
) -> Result<(), Box<dyn std::error::Error>> {
if ron_style {
let decoded: Borrowed<'_> = ron::from_str(document)?;
assert_field_borrows(seed, document.as_bytes(), &decoded, value, what);
} else {
let decoded: Borrowed<'_> = json::from_str(document)?;
assert_field_borrows(seed, document.as_bytes(), &decoded, value, what);
}
Ok(())
}
fn check_slice_door(
seed: u64,
ron_style: bool,
bytes: Vec<u8>,
value: &str,
what: &str,
) -> Result<(), Box<dyn std::error::Error>> {
if ron_style {
let decoded: Borrowed<'_> = ron::from_slice(&bytes)?;
assert_field_borrows(seed, &bytes, &decoded, value, what);
} else {
let decoded: Borrowed<'_> = json::from_slice(&bytes)?;
assert_field_borrows(seed, &bytes, &decoded, value, what);
}
Ok(())
}
fn sweep_borrowed_field(
seed: u64,
ron_style: bool,
door: Door,
what: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let mut state = seed;
for _ in 0..48 {
let value_len = below(&mut state, 16);
let value = next_text(&mut state, value_len);
let document = borrowed_document(&value, ron_style);
match door {
Door::Text => check_text_door(seed, ron_style, &document, &value, what)?,
Door::Slice => check_slice_door(seed, ron_style, document.into_bytes(), &value, what)?,
}
}
Ok(())
}
#[test]
fn an_unescaped_json_field_borrows_from_the_supplied_text() -> Result<(), Box<dyn std::error::Error>>
{
for seed in SWEEP_SEEDS {
sweep_borrowed_field(seed, false, Door::Text, "JSON text")?;
}
Ok(())
}
#[test]
fn an_unescaped_json_field_borrows_from_the_supplied_slice()
-> Result<(), Box<dyn std::error::Error>> {
for seed in SWEEP_SEEDS {
sweep_borrowed_field(seed, false, Door::Slice, "JSON slice")?;
}
Ok(())
}
#[test]
fn an_unescaped_ron_field_borrows_from_the_supplied_input() -> Result<(), Box<dyn std::error::Error>>
{
for seed in SWEEP_SEEDS {
sweep_borrowed_field(seed, true, Door::Text, "RON text")?;
sweep_borrowed_field(seed, true, Door::Slice, "RON slice")?;
}
Ok(())
}
#[test]
fn an_escaped_field_is_refused_as_a_borrow_in_both_codecs() {
for seed in SWEEP_SEEDS {
let mut state = seed;
for _ in 0..24 {
let escapes = ["\\n", "\\\\", "\\\"", "\\t"];
let escape = escapes[below(&mut state, escapes.len())];
let json_text = format!(r#"{{"value":"line{escape}feed"}}"#);
let json_result: Result<Borrowed<'_>, _> = json::from_str(&json_text);
assert!(
json_result.is_err(),
"seed {seed}: an escaped JSON field cannot be borrowed"
);
let ron_text = format!("(value: \"line{escape}feed\")");
let ron_result: Result<Borrowed<'_>, _> = ron::from_str(&ron_text);
assert!(
ron_result.is_err(),
"seed {seed}: an escaped RON field cannot be borrowed"
);
}
}
}
#[test]
fn a_malformed_document_is_refused_by_both_the_borrowing_and_owned_paths() {
for seed in SWEEP_SEEDS {
let mut state = seed;
for _ in 0..32 {
let truncated = below(&mut state, 24);
let text = format!(r#"{{"value":"{}"#, "x".repeat(truncated));
let borrowed_result: Result<Borrowed<'_>, _> = json::from_str(&text);
let owned_result: Result<Shape, _> = json::from_str(&text);
assert!(
borrowed_result.is_err(),
"seed {seed}: a truncated JSON document is not a borrow"
);
assert!(
owned_result.is_err(),
"seed {seed}: the same document is refused as an owned decode too"
);
}
}
}
#[test]
fn a_non_utf8_slice_is_refused_as_a_transport_fault() -> Result<(), Box<dyn std::error::Error>> {
for seed in SWEEP_SEEDS {
let mut state = seed;
for _ in 0..32 {
let prefix = below(&mut state, 4);
let mut bytes = vec![b'('; prefix];
bytes.extend_from_slice(&[0xff]);
let arm = match ron::from_slice::<Shape>(&bytes) {
Err(FromSliceError::Utf8(_)) => 1,
Err(ref error) => {
assert!(
std::error::Error::source(error).is_some(),
"seed {seed}: the refusal keeps its source"
);
2
}
Ok(_) => 3,
};
assert_eq!(
arm, 1,
"seed {seed}: a non-UTF-8 byte is a transport fault, not a content fault"
);
}
}
Ok(())
}
#[test]
fn a_non_utf8_json_slice_is_refused_with_a_document_location()
-> Result<(), Box<dyn std::error::Error>> {
for seed in SWEEP_SEEDS {
let mut state = seed;
for _ in 0..32 {
let mut bytes = vec![b'{'; below(&mut state, 4)];
bytes.extend_from_slice(&[0xff]);
let result: Result<Shape, _> = json::from_slice(&bytes);
match result {
Err(ref error) => {
assert!(
error.line() >= 1 && error.column() >= 1,
"seed {seed}: the JSON refusal carries a line and column, \
but the facade said {error}"
);
}
Ok(_) => return Err(format!("seed {seed}: a non-UTF-8 byte was accepted").into()),
}
}
}
Ok(())
}
#[test]
fn trailing_content_after_a_document_is_refused_in_both_codecs() {
for seed in SWEEP_SEEDS {
let mut state = seed;
for _ in 0..24 {
let tail_len = below(&mut state, 8);
let tail = next_text(&mut state, tail_len);
let json_text = format!(r#"{{"value":"x"}}{tail}"#);
assert!(
json::from_str::<Borrowed<'_>>(&json_text).is_err(),
"seed {seed}: JSON refuses trailing content"
);
let ron_text = format!("(value: \"x\") {tail}");
assert!(
ron::from_str::<Borrowed<'_>>(&ron_text).is_err(),
"seed {seed}: RON refuses trailing content"
);
}
}
}
#[test]
fn a_syntax_error_keeps_its_source_location() {
for seed in SWEEP_SEEDS {
let mut state = seed;
for _ in 0..24 {
let pad_len = below(&mut state, 4);
let pad = next_text(&mut state, pad_len);
let broken = format!("{pad}(value: )");
let ron_error = ron::from_str::<Borrowed<'_>>(&broken);
assert!(
ron_error
.as_ref()
.err()
.is_some_and(|error| error.span.start.col > 0),
"seed {seed}: the RON error carries its column"
);
let json_broken = format!("{pad}{{");
let json_error = json::from_str::<Borrowed<'_>>(&json_broken);
assert!(
json_error
.as_ref()
.err()
.is_some_and(|error| error.line() >= 1 && error.column() > 0),
"seed {seed}: the JSON error carries its line and column"
);
}
}
}
#[test]
fn each_codec_renders_its_own_documented_shape() -> Result<(), Box<dyn std::error::Error>> {
let value = Shape {
name: String::from("name"),
tag: String::from("tag"),
count: 7,
nested: Nested {
state: String::from("ready"),
depth: 2,
},
items: vec![String::from("one"), String::from("two")],
};
let json_text = json::to_string(&value)?;
assert!(!json_text.contains('\n'), "compact JSON has no newline");
assert!(
!json_text.contains(" "),
"compact JSON has no double-space indent"
);
let ron_pretty = ron::to_string_pretty(&value)?;
assert!(
ron_pretty.contains('\n'),
"pretty RON is indented across lines"
);
Ok(())
}
#[test]
fn a_value_tree_round_trips_the_drawn_value() -> Result<(), json::Error> {
for seed in SWEEP_SEEDS {
let mut state = seed;
for _ in 0..24 {
let value = draw_shape(&mut state);
let tree = json::to_value(&value)?;
let back: Shape = json::from_value(tree.clone())?;
assert_eq!(
back, value,
"seed {seed}: a value tree round-trips the drawn value"
);
let rendered = json::to_string(&tree)?;
let decoded: Shape = json::from_str(&rendered)?;
assert_eq!(
decoded, value,
"seed {seed}: the rendered tree decodes back to the drawn value"
);
}
}
Ok(())
}
#[test]
fn the_empty_containers_are_values_in_both_codecs() -> Result<(), Box<dyn std::error::Error>> {
let value = Shape {
name: String::new(),
tag: String::new(),
count: 0,
nested: Nested {
state: String::new(),
depth: 0,
},
items: Vec::new(),
};
let json_text = json::to_string(&value)?;
assert!(
json_text.contains(r#""items":[]"#),
"an empty array is rendered"
);
assert_eq!(
json::from_str::<Shape>(&json_text)?,
value,
"the empty-container value round-trips through JSON"
);
let ron_text = ron::to_string(&value)?;
assert_eq!(
ron::from_str::<Shape>(&ron_text)?,
value,
"the empty-container value round-trips through RON"
);
Ok(())
}
#[test]
fn multi_byte_text_round_trips_through_both_codecs() -> Result<(), Box<dyn std::error::Error>> {
let samples = ["héllo", "こんにちは", "☃☃☃", "ünïcødé", "aé☃"];
for sample in samples {
let value = Shape {
name: String::from(sample),
tag: String::from(sample),
count: 0,
nested: Nested {
state: String::from(sample),
depth: 0,
},
items: vec![String::from(sample)],
};
let json_back: Shape = json::from_str(&json::to_string(&value)?)?;
assert_eq!(json_back, value, "{sample:?} must round-trip through JSON");
let ron_back: Shape = ron::from_str(&ron::to_string(&value)?)?;
assert_eq!(ron_back, value, "{sample:?} must round-trip through RON");
}
Ok(())
}
#[test]
fn a_shape_with_empty_collections_still_round_trips() -> Result<(), Box<dyn std::error::Error>> {
for seed in SWEEP_SEEDS {
let mut state = seed;
for _ in 0..16 {
let mut value = draw_shape(&mut state);
value.items.clear();
let json_back: Shape = json::from_str(&json::to_string(&value)?)?;
assert_eq!(
json_back, value,
"seed {seed}: the emptied shape round-trips"
);
}
}
Ok(())
}
#[test]
fn the_same_seed_replays_to_the_same_codec_trace() {
for seed in SWEEP_SEEDS {
assert_same_seed_replays(codec_trace, seed);
}
}
#[test]
fn distinct_codec_seeds_diverge_in_their_trace() {
assert_distinct_seeds_diverge(codec_trace, SWEEP_SEEDS[0], SWEEP_SEEDS[1]);
}
#[test]
fn two_seeds_draw_two_different_values() {
let mut first = SWEEP_SEEDS[0];
let mut second = SWEEP_SEEDS[1];
let left = draw_shape(&mut first);
let right = draw_shape(&mut second);
assert_ne!(left, right, "the two sweep seeds drew the same value");
let mut trace = initial_trace();
fold(&mut trace, u64::try_from(left.items.len()).unwrap_or(0));
fold_bytes(&mut trace, left.name.as_bytes());
fold_bytes(&mut trace, right.name.as_bytes());
assert_ne!(
trace,
initial_trace(),
"the two values must fold into distinct traces"
);
}