Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,208 @@
|
||||
use dever_runtime::wire::{self, document, json::Json, serde_json};
|
||||
|
||||
#[test]
|
||||
fn proven_fragments_keep_exact_counts_when_nested() {
|
||||
for count in [0, 1, document::MAX_VALUES - 2, document::MAX_VALUES - 1] {
|
||||
let values = vec![7i64; count];
|
||||
let proven = Json::encode_proven(&values, count + 1, 1).unwrap();
|
||||
let parsed = Json::parse(proven.as_str()).unwrap();
|
||||
assert_eq!(Json::array(&[proven]), Json::array(&[parsed]));
|
||||
}
|
||||
let value = Json::encode_proven(&(), 1, 0).unwrap();
|
||||
let mut wrapped = value;
|
||||
for _ in 0..document::MAX_DEPTH {
|
||||
wrapped = Json::array(&[wrapped]).unwrap();
|
||||
}
|
||||
assert!(Json::array(&[wrapped]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_output_counts_values_and_response_envelope() {
|
||||
let maximum = vec![0i64; document::MAX_VALUES - 1];
|
||||
let encoded = Json::encode(&maximum).unwrap();
|
||||
assert_eq!(encoded.as_str(), serde_json::to_string(&maximum).unwrap());
|
||||
assert!(Json::encode(&vec![0i64; document::MAX_VALUES]).is_err());
|
||||
assert!(wire::response(&vec![0i64; document::MAX_VALUES - 4]).is_ok());
|
||||
assert!(wire::response(&vec![0i64; document::MAX_VALUES - 3]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_fragments_keep_the_envelope_and_its_budgets() {
|
||||
for source in ["null", "[1e9999,-0]", r#"{"text":"\u96ea"}"#] {
|
||||
let value = Json::parse(source).unwrap();
|
||||
assert_eq!(
|
||||
wire::response_with(|output| output.json(&value)).unwrap(),
|
||||
format!("{{\"code\":0,\"message\":\"ok\",\"data\":{source}}}")
|
||||
);
|
||||
}
|
||||
for count in [document::MAX_VALUES - 4, document::MAX_VALUES - 3] {
|
||||
let values = vec![0; count];
|
||||
let value = Json::encode(&values).unwrap();
|
||||
let output = wire::response_with(|output| output.json(&value));
|
||||
assert_eq!(output.is_ok(), count == document::MAX_VALUES - 4);
|
||||
assert_eq!(output.is_ok(), wire::response(&values).is_ok());
|
||||
}
|
||||
let prefix = wire::response_with(|output| output.value(""))
|
||||
.unwrap()
|
||||
.len();
|
||||
let value = Json::string(&"x".repeat(document::MAX_BYTES - prefix)).unwrap();
|
||||
assert_eq!(
|
||||
wire::response_with(|output| output.json(&value))
|
||||
.unwrap()
|
||||
.len(),
|
||||
document::MAX_BYTES
|
||||
);
|
||||
let value = Json::string(&"x".repeat(document::MAX_BYTES - prefix + 1)).unwrap();
|
||||
assert!(wire::response_with(|output| output.json(&value)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_subtrees_consume_the_outer_depth_and_value_budgets() {
|
||||
let nested = "[".repeat(document::MAX_DEPTH) + &"]".repeat(document::MAX_DEPTH);
|
||||
let typed: serde_json::Value = serde_json::from_str(&nested).unwrap();
|
||||
assert_eq!(Json::encode(&typed).unwrap().as_str(), nested);
|
||||
assert!(Json::encode(&vec![typed]).is_err());
|
||||
for raw in [
|
||||
Json::parse(&nested).unwrap(),
|
||||
serde_json::from_str(&nested).unwrap(),
|
||||
] {
|
||||
assert_eq!(
|
||||
Json::encode_with(|output| output.json(&raw))
|
||||
.unwrap()
|
||||
.as_str(),
|
||||
nested
|
||||
);
|
||||
assert!(Json::array(&[raw]).is_err());
|
||||
}
|
||||
let maximum = Json::encode(&vec![0i64; document::MAX_VALUES - 1]).unwrap();
|
||||
assert!(Json::encode_with(|output| output.json(&maximum)).is_ok());
|
||||
assert!(Json::array(&[maximum]).is_err());
|
||||
let allowed = Json::encode(&vec![0i64; document::MAX_VALUES - 2]).unwrap();
|
||||
assert!(Json::array(&[allowed]).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_output_keeps_spelling_and_rejects_invalid_object_keys() {
|
||||
let source = r#" {"雪": [1e9999, -0, "\u96ea"]} "#;
|
||||
let raw = Json::parse(source).unwrap();
|
||||
assert_eq!(
|
||||
Json::array(&[raw]).unwrap().as_str(),
|
||||
format!("[{}]", source.trim())
|
||||
);
|
||||
let duplicate =
|
||||
serde_json::value::RawValue::from_string(r#"{"a":1,"\u0061":2}"#.into()).unwrap();
|
||||
assert!(Json::encode(&duplicate).is_err());
|
||||
let surrogate = serde_json::value::RawValue::from_string(r#""\ud800""#.into()).unwrap();
|
||||
assert!(Json::encode(&surrogate).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_arrays_match_serde_for_empty_and_mixed_inputs() {
|
||||
let sources = [" null ", " [1e9999,-0] ", r#" {"雪":"\u96ea"} "#, " [] "];
|
||||
for count in [0, 1, 4, 16] {
|
||||
let sources: Vec<_> = sources.into_iter().cycle().take(count).collect();
|
||||
let reference: Vec<Box<serde_json::value::RawValue>> = sources
|
||||
.iter()
|
||||
.map(|source| serde_json::from_str(source).unwrap())
|
||||
.collect();
|
||||
for values in [
|
||||
sources
|
||||
.iter()
|
||||
.map(|source| Json::parse(source).unwrap())
|
||||
.collect::<Vec<_>>(),
|
||||
reference
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(Json::from_validated_raw)
|
||||
.collect(),
|
||||
] {
|
||||
let encoded = Json::array(&values).unwrap();
|
||||
assert_eq!(encoded.as_str(), serde_json::to_string(&reference).unwrap());
|
||||
assert_eq!(
|
||||
Json::array(std::slice::from_ref(&encoded))
|
||||
.unwrap()
|
||||
.as_str(),
|
||||
format!("[{}]", encoded.as_str())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validated_raw_transfers_its_original_allocation_into_text() {
|
||||
let source = r#"{"huge":1e9999,"text":"\u96ea"}"#;
|
||||
let raw: Box<serde_json::value::RawValue> = serde_json::from_str(source).unwrap();
|
||||
let allocation = raw.get().as_ptr();
|
||||
let value = Json::from_validated_raw(raw);
|
||||
assert_eq!(value.as_str().as_ptr(), allocation);
|
||||
let text = value.into_text();
|
||||
assert_eq!(text.as_ptr(), allocation);
|
||||
assert_eq!(text, source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_arrays_enforce_combined_value_and_separator_budgets() {
|
||||
let mut values = vec![Json::null().unwrap(); document::MAX_VALUES - 1];
|
||||
assert!(Json::array(&values).is_ok());
|
||||
values.push(Json::null().unwrap());
|
||||
assert!(Json::array(&values).is_err());
|
||||
for extra in [0, 1] {
|
||||
let values = [
|
||||
Json::integer(0).unwrap(),
|
||||
Json::string(&"x".repeat(document::MAX_BYTES - 6 + extra)).unwrap(),
|
||||
];
|
||||
let encoded = Json::array(&values);
|
||||
if extra == 0 {
|
||||
let encoded = encoded.unwrap();
|
||||
assert_eq!(encoded.as_str().len(), document::MAX_BYTES);
|
||||
assert!(Json::array(&[encoded]).is_err());
|
||||
} else {
|
||||
assert!(encoded.is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic_output_keeps_byte_limits_and_counts_empty_containers() {
|
||||
let allowed = vec!["x".repeat(document::MAX_BYTES - 4)];
|
||||
assert_eq!(
|
||||
Json::encode(&allowed).unwrap().as_str().len(),
|
||||
document::MAX_BYTES
|
||||
);
|
||||
assert!(Json::encode(&vec!["x".repeat(document::MAX_BYTES - 3)]).is_err());
|
||||
assert!(Json::encode(&vec![Vec::<i64>::new(); document::MAX_VALUES - 1]).is_ok());
|
||||
assert!(Json::encode(&vec![Vec::<i64>::new(); document::MAX_VALUES]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validated_fragments_combine_with_serde_fields_and_keep_reusable_budgets() {
|
||||
let fragment = Json::parse(" \n{\"雪\":[1e9999,-0,\"\\u96ea\"]}\t ").unwrap();
|
||||
let nested = Json::encode_with(|output| {
|
||||
output.object(|output| {
|
||||
output.field_encoded("\"a\\\"雪\":", |output| output.json(&fragment))?;
|
||||
output.field("b", false, |output| output.value(&vec![Some("雪\n"), None]))
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
nested.as_str(),
|
||||
"{\"a\\\"雪\":{\"雪\":[1e9999,-0,\"\\u96ea\"]},\"b\":[\"雪\\n\",null]}"
|
||||
);
|
||||
assert_eq!(
|
||||
Json::array(std::slice::from_ref(&nested)).unwrap().as_str(),
|
||||
format!("[{}]", nested.as_str())
|
||||
);
|
||||
assert_eq!(fragment.as_str(), " \n{\"雪\":[1e9999,-0,\"\\u96ea\"]}\t ");
|
||||
let allowed =
|
||||
Json::parse(&("[".repeat(document::MAX_DEPTH - 1) + &"]".repeat(document::MAX_DEPTH - 1)))
|
||||
.unwrap();
|
||||
let full = Json::array(&[allowed]).unwrap();
|
||||
assert!(Json::array(&[full]).is_err());
|
||||
let full =
|
||||
Json::encode_with(|output| output.value(&vec![0; document::MAX_VALUES - 1])).unwrap();
|
||||
assert!(Json::array(&[full]).is_err());
|
||||
let allowed = Json::string(&"x".repeat(document::MAX_BYTES - 4)).unwrap();
|
||||
let full = Json::array(&[allowed]).unwrap();
|
||||
assert_eq!(full.as_str().len(), document::MAX_BYTES);
|
||||
assert!(Json::array(&[full]).is_err());
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
use dever_runtime::wire::{self, document};
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit strict-Unicode versus syntax-only performance diagnostic"]
|
||||
fn escaped_string_validation_cost() {
|
||||
use serde::{Deserializer, de::IgnoredAny};
|
||||
use std::{hint::black_box, time::Instant};
|
||||
let source = serde_json::to_string(&"雪\"\\\n".repeat(1024)).unwrap();
|
||||
println!("case,sample,implementation,iterations,nanoseconds,checksum");
|
||||
for sample in 0..15 {
|
||||
for strict in if sample % 2 == 0 {
|
||||
[true, false]
|
||||
} else {
|
||||
[false, true]
|
||||
} {
|
||||
let started = Instant::now();
|
||||
for _ in 0..1000 {
|
||||
let source = black_box(source.as_str());
|
||||
if strict {
|
||||
let mut decoder = serde_json::Deserializer::from_str(source);
|
||||
decoder.deserialize_str(IgnoredAny).unwrap();
|
||||
decoder.end().unwrap();
|
||||
} else {
|
||||
black_box(
|
||||
serde_json::from_str::<&serde_json::value::RawValue>(source).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
let elapsed = started.elapsed().as_nanos();
|
||||
let label = if strict {
|
||||
"rust_unicode"
|
||||
} else {
|
||||
"rust_syntax_only"
|
||||
};
|
||||
println!(
|
||||
"escaped_string,{sample},{label},1000,{elapsed},{}",
|
||||
source.len() * 1000
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_text_preserves_unicode_validation_and_original_spelling() {
|
||||
use wire::json::Json;
|
||||
for unit in 0..=u16::MAX {
|
||||
let source = format!("\"\\u{unit:04x}\"");
|
||||
assert_eq!(
|
||||
Json::parse(&source).is_ok(),
|
||||
serde_json::from_str::<String>(&source).is_ok(),
|
||||
"{source}"
|
||||
);
|
||||
}
|
||||
for text in [
|
||||
"",
|
||||
"雪🙂",
|
||||
"\\u1234",
|
||||
"\\\"\\\\",
|
||||
"\\\\\"\\\"",
|
||||
"\"\"雪\"",
|
||||
"\u{0}\n\t",
|
||||
] {
|
||||
let source = serde_json::to_string(text).unwrap();
|
||||
for source in [source.clone(), format!(" \n{source}\t ")] {
|
||||
let parsed = Json::parse(&source).unwrap();
|
||||
assert_eq!(parsed.as_str(), source);
|
||||
assert_eq!(parsed.as_text().unwrap(), text);
|
||||
assert_eq!(
|
||||
Json::array(&[parsed]).unwrap().into_text(),
|
||||
format!("[{}]", source.trim())
|
||||
);
|
||||
}
|
||||
}
|
||||
for source in [
|
||||
r#"{"雪":0,"\u96ea":1}"#,
|
||||
r#"["\ud800"]"#,
|
||||
r#"{"field":"\udc00"}"#,
|
||||
r#"{"\ud800":0}"#,
|
||||
r#""\ud800\u0061""#,
|
||||
] {
|
||||
assert!(Json::parse(source).is_err(), "{source}");
|
||||
}
|
||||
for source in [
|
||||
r#""\ud83d\ude42""#,
|
||||
r#"{"value":[true,false,null,-0,1e9999]}"#,
|
||||
] {
|
||||
assert_eq!(Json::parse(source).unwrap().as_str(), source);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shaped_input_keeps_array_syntax_and_limits() {
|
||||
for count in [0, 16, document::MAX_VALUES - 1] {
|
||||
let source = format!("[{}]", vec!["0"; count].join(","));
|
||||
assert_eq!(
|
||||
wire::decode_shaped::<Vec<i64>>(&source),
|
||||
wire::decode(&source)
|
||||
);
|
||||
assert_eq!(
|
||||
wire::decode_shaped::<Vec<i64>>(&source).unwrap().len(),
|
||||
count
|
||||
);
|
||||
}
|
||||
for source in [
|
||||
"[1,]",
|
||||
"[null]",
|
||||
"[true]",
|
||||
"[{}]",
|
||||
"[[1]]",
|
||||
"[1.5]",
|
||||
"[1] null",
|
||||
"[9223372036854775808]",
|
||||
"[1e9999]",
|
||||
] {
|
||||
assert!(wire::decode_shaped::<Vec<i64>>(source).is_err(), "{source}");
|
||||
}
|
||||
let source = format!("[{}]", vec!["0"; document::MAX_VALUES].join(","));
|
||||
assert!(
|
||||
wire::decode_shaped::<Vec<i64>>(&source)
|
||||
.unwrap_err()
|
||||
.contains("value")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shaped_input_has_identical_acceptance_across_the_proven_byte_budget() {
|
||||
for size in [
|
||||
document::MAX_VALUES - 1,
|
||||
document::MAX_VALUES,
|
||||
document::MAX_VALUES + 1,
|
||||
] {
|
||||
let source = format!("[\"{}\"]", "x".repeat(size - 4));
|
||||
assert_eq!(source.len(), size);
|
||||
assert_eq!(
|
||||
wire::decode_shaped::<Vec<String>>(&source),
|
||||
wire::decode(&source)
|
||||
);
|
||||
let source = format!("[\"{}\\ud800\"]", "x".repeat(size - 10));
|
||||
assert!(wire::decode_shaped::<Vec<String>>(&source).is_err());
|
||||
}
|
||||
let source = format!("[\"{}\"]", "x".repeat(document::MAX_BYTES - 4));
|
||||
assert!(wire::decode_shaped::<Vec<String>>(&source).is_ok());
|
||||
assert!(wire::decode_shaped::<Vec<String>>(&(source + " ")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_pass_budget_counts_nested_containers_and_optional_values_once() {
|
||||
use serde::Deserialize;
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Fields {
|
||||
first: Vec<Option<i64>>,
|
||||
second: Option<Vec<Option<i64>>>,
|
||||
}
|
||||
for extra in [0, 1] {
|
||||
let source = format!(
|
||||
"{{\"first\":[{}],\"second\":[null]}}",
|
||||
vec!["null"; document::MAX_VALUES - 4 + extra].join(",")
|
||||
);
|
||||
assert_eq!(wire::decode::<Fields>(&source).is_ok(), extra == 0);
|
||||
assert_eq!(wire::decode_shaped::<Fields>(&source).is_ok(), extra == 0);
|
||||
}
|
||||
let padding = " ".repeat(document::MAX_VALUES);
|
||||
for source in [
|
||||
r#"{"first":[],"second":null}"#,
|
||||
r#"{"first":[null,1],"second":[2,null]}"#,
|
||||
r#"{"first":[],"second":null,"second":[]}"#,
|
||||
r#"{"first":[],"fi\u0072st":[]}"#,
|
||||
r#"{"first":[],"unknown":{}}"#,
|
||||
] {
|
||||
let source = format!("{padding}{source}");
|
||||
let normal = wire::decode::<Fields>(&source);
|
||||
let shaped = wire::decode_shaped::<Fields>(&source);
|
||||
assert_eq!(normal.is_ok(), shaped.is_ok());
|
||||
assert_eq!(normal.ok(), shaped.ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_pass_budget_preserves_serde_numeric_decoding() {
|
||||
let padding = " ".repeat(document::MAX_VALUES);
|
||||
for source in [
|
||||
"[0,-0,-0.0,1,9223372036854775807,18446744073709551615]",
|
||||
"[0.1,1.2345678901234567,1e-308,5e-324,1.7976931348623157e308]",
|
||||
] {
|
||||
let source = format!("{padding}{source}");
|
||||
let normal = wire::decode::<Vec<f64>>(&source).unwrap();
|
||||
let shaped = wire::decode_shaped::<Vec<f64>>(&source).unwrap();
|
||||
assert_eq!(
|
||||
normal
|
||||
.iter()
|
||||
.map(|value| value.to_bits())
|
||||
.collect::<Vec<_>>(),
|
||||
shaped
|
||||
.iter()
|
||||
.map(|value| value.to_bits())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
for source in ["[1e9999]", "[null]", "[\"1\"]", "[NaN]"] {
|
||||
assert!(wire::decode_shaped::<Vec<f64>>(&format!("{padding}{source}")).is_err());
|
||||
}
|
||||
let source = format!("{padding}[\"1.2300\",null,\"-1E-6176\"]");
|
||||
assert_eq!(
|
||||
wire::decode_shaped::<Vec<Option<dever_runtime::value::decimal::Decimal>>>(&source),
|
||||
wire::decode(&source)
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct JsonFields {
|
||||
payload: wire::json::Json,
|
||||
note: Option<String>,
|
||||
nested: Option<JsonRecord>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct JsonRecord {
|
||||
payload: Option<wire::json::Json>,
|
||||
count: i64,
|
||||
}
|
||||
|
||||
fn decode_json_fields(source: &str) -> Result<JsonFields, String> {
|
||||
wire::decode_json(source, |value: &JsonFields, budget| {
|
||||
budget.open()?;
|
||||
budget.value(3)?;
|
||||
budget.json(&value.payload)?;
|
||||
if let Some(nested) = &value.nested {
|
||||
budget.open()?;
|
||||
budget.value(2)?;
|
||||
if let Some(payload) = &nested.payload {
|
||||
budget.json(payload)?;
|
||||
}
|
||||
budget.close();
|
||||
}
|
||||
budget.close();
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_input_budget_distinguishes_omitted_fields_from_explicit_null_at_limit() {
|
||||
for count in [document::MAX_VALUES - 4, document::MAX_VALUES - 2] {
|
||||
let payload = vec!["0"; count].join(",");
|
||||
for suffix in ["", ",\"note\":null", ",\"note\":null,\"nested\":null"] {
|
||||
let source = format!("{{\"payload\":[{payload}]{suffix}}}");
|
||||
let expected = wire::decode::<JsonFields>(&source);
|
||||
let actual = decode_json_fields(&source);
|
||||
assert_eq!(actual.is_ok(), expected.is_ok(), "{count}/{suffix}");
|
||||
assert_eq!(actual.ok(), expected.ok());
|
||||
}
|
||||
}
|
||||
let payload = vec!["0"; document::MAX_VALUES - 2].join(",");
|
||||
assert!(decode_json_fields(&format!("{{\"payload\":[{payload}]}}")).is_ok());
|
||||
assert!(decode_json_fields(&format!("{{\"payload\":[{payload}],\"note\":null}}")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_input_budget_combines_fixed_records_and_multiple_json_subtrees() {
|
||||
for extra in [0, 1] {
|
||||
let payload = vec!["0"; document::MAX_VALUES - 6 + extra].join(",");
|
||||
let source =
|
||||
format!("{{\"payload\":[{payload}],\"nested\":{{\"payload\":[0],\"count\":0}}}}");
|
||||
let actual = decode_json_fields(&source);
|
||||
assert_eq!(actual.is_ok(), extra == 0);
|
||||
assert_eq!(actual.ok(), wire::decode::<JsonFields>(&source).ok());
|
||||
}
|
||||
let payload = vec!["0"; document::MAX_VALUES - 4].join(",");
|
||||
for (field, accepted) in [("", true), (",\"payload\":null", false)] {
|
||||
let source = format!("{{\"payload\":[{payload}],\"nested\":{{\"count\":0{field}}}}}");
|
||||
assert_eq!(decode_json_fields(&source).is_ok(), accepted);
|
||||
assert_eq!(wire::decode::<JsonFields>(&source).is_ok(), accepted);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_input_budget_counts_empty_containers_and_relative_subtree_depth() {
|
||||
for (nested, parent_depth) in [(false, 1), (true, 2)] {
|
||||
for extra in [0, 1] {
|
||||
let depth = document::MAX_DEPTH - parent_depth + extra;
|
||||
let payload = "[".repeat(depth) + &"]".repeat(depth);
|
||||
let source = if nested {
|
||||
format!("{{\"payload\":null,\"nested\":{{\"payload\":{payload},\"count\":0}}}}")
|
||||
} else {
|
||||
format!("{{\"payload\":{payload}}}")
|
||||
};
|
||||
assert_eq!(decode_json_fields(&source).is_ok(), extra == 0);
|
||||
assert_eq!(wire::decode::<JsonFields>(&source).is_ok(), extra == 0);
|
||||
}
|
||||
}
|
||||
for source in [
|
||||
r#"{"payload":null,"nested":null}"#,
|
||||
r#"{"payload":{},"nested":{"count":0}}"#,
|
||||
r#"{"payload":[],"nested":{"payload":null,"count":0}}"#,
|
||||
] {
|
||||
assert_eq!(
|
||||
decode_json_fields(source).unwrap(),
|
||||
wire::decode(source).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_input_keeps_raw_spelling_duplicate_keys_and_strict_unicode() {
|
||||
let payload = r#"{ "value": [1e9999,-0,1.00e+2,"\u96ea","\ud83d\ude42"] }"#;
|
||||
let source = format!("{{\"payload\":{payload}}}");
|
||||
let decoded = decode_json_fields(&source).unwrap();
|
||||
assert_eq!(decoded.payload.as_str(), payload);
|
||||
assert_eq!(
|
||||
wire::json::Json::array(&[decoded.payload])
|
||||
.unwrap()
|
||||
.as_str(),
|
||||
format!("[{payload}]")
|
||||
);
|
||||
for source in [
|
||||
r#"{"payload":null,"payl\u006fad":0}"#,
|
||||
r#"{"payload":null,"note":null,"note":null}"#,
|
||||
r#"{"payload":{"雪":0,"\u96ea":1}}"#,
|
||||
r#"{"payload":"\ud800"}"#,
|
||||
r#"{"payload":null,"note":"\udc00"}"#,
|
||||
r#"{"payload":null,"nested":{"count":0,"count":1}}"#,
|
||||
r#"{"payload":null,"extra":0}"#,
|
||||
r#"{"payload":null} {}"#,
|
||||
r#"{"payload":null,}"#,
|
||||
] {
|
||||
assert!(decode_json_fields(source).is_err(), "{source}");
|
||||
assert!(wire::decode::<JsonFields>(source).is_err(), "{source}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_input_limits_original_bytes_before_deserializing() {
|
||||
let prefix = r#"{"payload":null,"note":""#;
|
||||
let source = format!(
|
||||
"{prefix}雪{}\"}}",
|
||||
" ".repeat(document::MAX_BYTES - prefix.len() - 5)
|
||||
);
|
||||
assert_eq!(source.len(), document::MAX_BYTES);
|
||||
assert!(decode_json_fields(&source).is_ok());
|
||||
let oversized = source + " ";
|
||||
let mut checked = false;
|
||||
let result = wire::decode_json::<JsonFields>(&oversized, |_, _| {
|
||||
checked = true;
|
||||
Ok(())
|
||||
});
|
||||
assert!(result.unwrap_err().contains("16 MiB"));
|
||||
assert!(!checked);
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
use super::Json;
|
||||
use std::{hint::black_box, time::Instant};
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit release performance probe"]
|
||||
fn document_string_cost() {
|
||||
use crate::wire::document::{self, Document, Value};
|
||||
type Decode = fn(&str) -> Document;
|
||||
let operations: [(&str, Decode); 2] = [
|
||||
("dever", |source| document::parse(source).unwrap()),
|
||||
// 独立 Serde 解码保留相同的单节点结果和字节预算。
|
||||
("rust", |source| {
|
||||
assert!(source.len() <= document::MAX_BYTES);
|
||||
Document {
|
||||
root: 0,
|
||||
nodes: vec![Value::String(serde_json::from_str(source).unwrap())],
|
||||
}
|
||||
}),
|
||||
];
|
||||
println!("\ncase,sample,implementation,iterations,elapsed_ns,checksum");
|
||||
for (name, text) in [
|
||||
("short", "hello".to_owned()),
|
||||
("ascii", "a".repeat(8192)),
|
||||
("unicode", "雪🙂".repeat(1024)),
|
||||
("escaped", "雪\"\\\n".repeat(1024)),
|
||||
] {
|
||||
let source = serde_json::to_string(&text).unwrap();
|
||||
let expected = Document {
|
||||
root: 0,
|
||||
nodes: vec![Value::String(text.clone())],
|
||||
};
|
||||
for (_, operation) in operations {
|
||||
assert_eq!(operation(&source), expected);
|
||||
for _ in 0..100 {
|
||||
black_box(operation(&source));
|
||||
}
|
||||
}
|
||||
let iterations = if name == "short" { 10000 } else { 1000 };
|
||||
for sample in 0..15 {
|
||||
for offset in 0..operations.len() {
|
||||
let (implementation, operation) = operations[(sample + offset) % operations.len()];
|
||||
let started = Instant::now();
|
||||
let mut checksum = 0;
|
||||
for _ in 0..iterations {
|
||||
let parsed = black_box(operation(black_box(&source)));
|
||||
let Value::String(value) = &parsed.nodes[parsed.root] else {
|
||||
panic!("expected string node");
|
||||
};
|
||||
checksum += value.len();
|
||||
}
|
||||
let elapsed = started.elapsed().as_nanos();
|
||||
assert_eq!(checksum, text.len() * iterations);
|
||||
println!(
|
||||
"document_string_{name},{sample},{implementation},{iterations},{elapsed},{checksum}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit release performance probe"]
|
||||
fn encoded_fragment_cost() {
|
||||
type Encode = fn(&str, bool) -> String;
|
||||
println!("\ncase,sample,implementation,iterations,elapsed_ns,checksum");
|
||||
for bytes in [64, 4096, 65536] {
|
||||
let text = "a".repeat(bytes);
|
||||
for nested in [false, true] {
|
||||
let operations: [(&str, Encode); 4] = [
|
||||
("proven", |text, nested| {
|
||||
let value = Json::encode_proven(&(text, 7i64), 3, 1).unwrap();
|
||||
if nested {
|
||||
Json::array(&[value]).unwrap().into_text()
|
||||
} else {
|
||||
value.into_text()
|
||||
}
|
||||
}),
|
||||
("shaped", |text, nested| {
|
||||
let value = Json::encode(&(text, 7i64)).unwrap();
|
||||
if nested {
|
||||
Json::array(&[value]).unwrap().into_text()
|
||||
} else {
|
||||
value.into_text()
|
||||
}
|
||||
}),
|
||||
// 一次写完用于观察融合输出的成本,不能当作中间 Json 的包装开销。
|
||||
("rust_direct", |text, nested| {
|
||||
assert!(text.len() <= (crate::wire::document::MAX_BYTES - 32) / 6);
|
||||
if nested {
|
||||
serde_json::to_string(&[(text, 7i64)]).unwrap()
|
||||
} else {
|
||||
serde_json::to_string(&(text, 7i64)).unwrap()
|
||||
}
|
||||
}),
|
||||
// 与 Dever 一样先持有编码结果,再用标准库写入外层数组。
|
||||
("rust_materialized", |text, nested| {
|
||||
assert!(text.len() <= (crate::wire::document::MAX_BYTES - 32) / 6);
|
||||
let value = serde_json::to_string(&(text, 7i64)).unwrap();
|
||||
if nested {
|
||||
let mut output = String::with_capacity(value.len() + 2);
|
||||
output.push('[');
|
||||
output.push_str(&value);
|
||||
output.push(']');
|
||||
output
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}),
|
||||
];
|
||||
let expected = operations[2].1(&text, nested);
|
||||
for (_, operation) in operations {
|
||||
assert_eq!(operation(&text, nested), expected);
|
||||
for _ in 0..100 {
|
||||
black_box(operation(&text, nested));
|
||||
}
|
||||
}
|
||||
let iterations = if bytes == 64 { 10_000 } else { 1000 };
|
||||
for sample in 0..15 {
|
||||
for offset in 0..operations.len() {
|
||||
let (name, operation) = operations[(sample + offset) % operations.len()];
|
||||
let started = Instant::now();
|
||||
let mut checksum = 0;
|
||||
for _ in 0..iterations {
|
||||
checksum += black_box(operation(black_box(&text), nested)).len();
|
||||
}
|
||||
let elapsed = started.elapsed().as_nanos();
|
||||
assert_eq!(checksum, expected.len() * iterations);
|
||||
println!(
|
||||
"encoded_{bytes}_{nested},{sample},{name},{iterations},{elapsed},{checksum}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit release performance probe"]
|
||||
fn command_json_decode_cost() {
|
||||
use crate::wire;
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Input {
|
||||
source: Json,
|
||||
}
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Native {
|
||||
source: Box<serde_json::value::RawValue>,
|
||||
}
|
||||
type Decoder = fn(&str) -> usize;
|
||||
let decoders: [(&str, Decoder); 3] = [
|
||||
("before", |source| {
|
||||
wire::decode::<Input>(source).unwrap().source.as_str().len()
|
||||
}),
|
||||
("after", |source| {
|
||||
wire::decode_json(source, |value: &Input, budget| {
|
||||
budget.open()?;
|
||||
budget.value(1)?;
|
||||
budget.json(&value.source)?;
|
||||
budget.close();
|
||||
Ok(())
|
||||
})
|
||||
.unwrap()
|
||||
.source
|
||||
.as_str()
|
||||
.len()
|
||||
}),
|
||||
// 原生 RawValue 不做去重和结构预算,只提供语法解码成本下界。
|
||||
("rust_syntax_only", |source| {
|
||||
serde_json::from_str::<Native>(source)
|
||||
.unwrap()
|
||||
.source
|
||||
.get()
|
||||
.len()
|
||||
}),
|
||||
];
|
||||
println!("\ncase,sample,implementation,iterations,elapsed_ns,checksum");
|
||||
for (name, text) in [
|
||||
("short", "a".repeat(64)),
|
||||
("plain_65536", "a".repeat(65536)),
|
||||
("escaped", "雪\"\\\n".repeat(1024)),
|
||||
] {
|
||||
let payload = serde_json::to_string(&text).unwrap();
|
||||
let source = format!("{{\"source\":{payload}}}");
|
||||
let iterations = if name == "short" { 10_000 } else { 1000 };
|
||||
for (_, decode) in decoders {
|
||||
for _ in 0..100 {
|
||||
assert_eq!(black_box(decode(&source)), payload.len());
|
||||
}
|
||||
}
|
||||
for sample in 0..15 {
|
||||
for offset in 0..decoders.len() {
|
||||
let (label, decode) = decoders[(sample + offset) % decoders.len()];
|
||||
let started = Instant::now();
|
||||
let mut checksum = 0;
|
||||
for _ in 0..iterations {
|
||||
checksum += black_box(decode(black_box(&source)));
|
||||
}
|
||||
let elapsed = started.elapsed().as_nanos();
|
||||
assert_eq!(checksum, payload.len() * iterations);
|
||||
println!("command_json_{name},{sample},{label},{iterations},{elapsed},{checksum}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit release performance probe"]
|
||||
fn parse_object_keys_cost() {
|
||||
println!("\ncase,sample,iterations,elapsed_ns,checksum");
|
||||
for (name, suffix) in [
|
||||
("short", String::new()),
|
||||
("long", "field".repeat(32)),
|
||||
("escaped", "雪\"\\\n".repeat(16)),
|
||||
] {
|
||||
let fields = (0..256)
|
||||
.map(|index| (format!("key_{index:03}_{suffix}"), index))
|
||||
.collect::<std::collections::BTreeMap<_, _>>();
|
||||
let source = serde_json::to_string(&fields).unwrap();
|
||||
assert_eq!(Json::parse(&source).unwrap().as_str(), source);
|
||||
for _ in 0..100 {
|
||||
black_box(Json::parse(&source).unwrap());
|
||||
}
|
||||
for sample in 0..15 {
|
||||
let started = Instant::now();
|
||||
let mut checksum = 0;
|
||||
for _ in 0..1000 {
|
||||
let parsed = Json::parse(black_box(&source)).unwrap();
|
||||
checksum += black_box(parsed.as_str().len());
|
||||
}
|
||||
let elapsed = started.elapsed().as_nanos();
|
||||
assert_eq!(checksum, source.len() * 1000);
|
||||
println!("json_keys_{name},{sample},1000,{elapsed},{checksum}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit release performance probe"]
|
||||
fn parse_text_json_cost() {
|
||||
println!("\ncase,sample,iterations,elapsed_ns,checksum");
|
||||
for (name, text) in [
|
||||
("ascii", "a".repeat(8192)),
|
||||
("unicode", "雪🙂".repeat(1024)),
|
||||
("escaped", "雪\"\\\n".repeat(1024)),
|
||||
] {
|
||||
let source = serde_json::to_string(&text).unwrap();
|
||||
assert_eq!(Json::parse(&source).unwrap().as_text().unwrap(), text);
|
||||
for _ in 0..100 {
|
||||
black_box(Json::parse(&source).unwrap());
|
||||
}
|
||||
for sample in 0..15 {
|
||||
let started = Instant::now();
|
||||
let mut checksum = 0;
|
||||
for _ in 0..1000 {
|
||||
let parsed = Json::parse(black_box(&source)).unwrap();
|
||||
checksum += black_box(parsed.as_str().len());
|
||||
}
|
||||
let elapsed = started.elapsed().as_nanos();
|
||||
assert_eq!(checksum, source.len() * 1000);
|
||||
println!("json_parse_{name},{sample},1000,{elapsed},{checksum}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit release performance probe"]
|
||||
fn deserialize_raw_json_cost() {
|
||||
println!("\ncase,sample,iterations,elapsed_ns,checksum");
|
||||
let values = (0..256).collect::<Vec<i64>>();
|
||||
let text = serde_json::to_string(&"雪\"\\".repeat(512)).unwrap();
|
||||
let array = format!(r#"{{"numbers":{values:?},"huge":1e9999}}"#);
|
||||
let nested = "[".repeat(32) + &array + &"]".repeat(32);
|
||||
for (name, source) in [("text", text), ("array", array), ("nested", nested)] {
|
||||
let parsed: Json = serde_json::from_str(&source).unwrap();
|
||||
assert_eq!(parsed.as_str(), source);
|
||||
for _ in 0..100 {
|
||||
black_box(serde_json::from_str::<Json>(&source).unwrap());
|
||||
}
|
||||
for sample in 0..15 {
|
||||
let started = Instant::now();
|
||||
let mut checksum = 0;
|
||||
for _ in 0..1000 {
|
||||
let parsed: Json = serde_json::from_str(black_box(&source)).unwrap();
|
||||
checksum += black_box(parsed.as_str().len());
|
||||
}
|
||||
let elapsed = started.elapsed().as_nanos();
|
||||
assert_eq!(checksum, source.len() * 1000);
|
||||
println!("json_deserialize_{name},{sample},1000,{elapsed},{checksum}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit release performance probe"]
|
||||
fn property_lookup_cost() {
|
||||
println!("\ncase,sample,iterations,elapsed_ns,checksum");
|
||||
for (name, count, suffix) in [
|
||||
("small", 4, ""),
|
||||
("wide", 256, ""),
|
||||
("escaped", 256, "雪\\\""),
|
||||
] {
|
||||
let names = (0..count)
|
||||
.map(|index| format!("key_{index:03}_{suffix}"))
|
||||
.collect::<Vec<_>>();
|
||||
let fields = names
|
||||
.iter()
|
||||
.map(|key| format!("{}:1e9999", serde_json::to_string(key).unwrap()))
|
||||
.collect::<Vec<_>>();
|
||||
let source = format!("{{{}}}", fields.join(","));
|
||||
let value = Json::parse(&source).unwrap();
|
||||
let keys = [
|
||||
names[0].as_str(),
|
||||
&names[count / 2],
|
||||
&names[count - 1],
|
||||
"missing",
|
||||
];
|
||||
let lookup = || {
|
||||
keys.iter()
|
||||
.map(|key| {
|
||||
value
|
||||
.property(black_box(key))
|
||||
.unwrap()
|
||||
.map_or(0, |child| black_box(child.as_str().len()))
|
||||
})
|
||||
.sum::<usize>()
|
||||
};
|
||||
assert_eq!(lookup(), 18);
|
||||
for _ in 0..100 {
|
||||
black_box(lookup());
|
||||
}
|
||||
for sample in 0..15 {
|
||||
let started = Instant::now();
|
||||
let mut checksum = 0;
|
||||
for _ in 0..1000 {
|
||||
checksum += black_box(lookup());
|
||||
}
|
||||
let elapsed = started.elapsed().as_nanos();
|
||||
assert_eq!(checksum, 18000);
|
||||
println!("json_property_{name},{sample},1000,{elapsed},{checksum}");
|
||||
}
|
||||
assert_eq!(value.as_str(), source);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user