mod common;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use common::oracle::{same, to_json};
const EXPECT_ACCEPT: usize = 95; const EXPECT_REJECT: usize = 188; const EXPECT_IMPL: usize = 35; const EXPECT_CASES: usize = 318;
const MISSING: &str = "\
nst/JSONTestSuite corpus not found.
It is third-party and deliberately not vendored. Fetch it (pinned commit,
idempotent) and re-run:
sh test/fetch-jsontestsuite.sh # or: make json-test-suite
This fails rather than skips on purpose: a conformance suite that quietly
does not run reports a green tick that is a lie.";
fn repo_root() -> &'static Path {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("rs/ has a parent")
}
fn corpus_dir() -> PathBuf {
repo_root()
.join("test")
.join("jsontestsuite")
.join("test_parsing")
}
fn corpus() -> &'static Vec<String> {
static CASES: OnceLock<Vec<String>> = OnceLock::new();
CASES.get_or_init(|| {
let script = repo_root().join("test").join("fetch-jsontestsuite.sh");
match Command::new("sh").arg(&script).status() {
Ok(status) if status.success() => {}
Ok(status) => eprintln!(
"warning: {} exited {status}; the conformance tests will fail",
script.display()
),
Err(error) => eprintln!(
"warning: could not run {} ({error}); the conformance tests will fail",
script.display()
),
}
let dir = corpus_dir();
let entries = fs::read_dir(&dir).unwrap_or_else(|_| panic!("{MISSING}"));
let mut names: Vec<String> = entries
.filter_map(|entry| entry.ok())
.filter(|entry| entry.file_type().map(|t| t.is_file()).unwrap_or(false))
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.filter(|name| name.ends_with(".json"))
.collect();
names.sort();
assert!(!names.is_empty(), "{MISSING}");
names
})
}
fn cases(prefix: &'static str) -> impl Iterator<Item = &'static String> {
corpus().iter().filter(move |name| name.starts_with(prefix))
}
fn source(name: &str) -> Vec<u8> {
fs::read(corpus_dir().join(name)).unwrap_or_else(|e| panic!("read {name}: {e}"))
}
fn ours(src: &[u8]) -> Result<serde_json::Value, String> {
let text = std::str::from_utf8(src).map_err(|e| format!("not valid UTF-8: {e}"))?;
tabnas_json::parse(text)
.map(|value| to_json(&value))
.map_err(|error| {
assert!(
!error.code.is_empty(),
"rejected without an error code: {error}"
);
error.code
})
}
fn oracle(src: &[u8]) -> Result<serde_json::Value, String> {
serde_json::from_slice::<serde_json::Value>(src).map_err(|e| e.to_string())
}
#[test]
fn the_corpus_is_the_pinned_one() {
let (y, n, i) = (
cases("y_").count(),
cases("n_").count(),
cases("i_").count(),
);
assert!(
y == EXPECT_ACCEPT
&& n == EXPECT_REJECT
&& i == EXPECT_IMPL
&& corpus().len() == EXPECT_CASES,
"corpus has been narrowed or replaced under {}: \
y_={y} n_={n} i_={i} total={}, want y_={EXPECT_ACCEPT} n_={EXPECT_REJECT} \
i_={EXPECT_IMPL} total={EXPECT_CASES} -- \
re-run `sh test/fetch-jsontestsuite.sh --force`",
corpus_dir().display(),
corpus().len()
);
}
#[test]
fn must_accept() {
let mut failures = Vec::new();
for name in cases("y_") {
let src = source(name);
let want = match oracle(&src) {
Ok(want) => want,
Err(e) => {
failures.push(format!("{name}: serde_json rejected a y_ case: {e}"));
continue;
}
};
match ours(&src) {
Ok(got) if same(&got, &want) => {}
Ok(got) => failures.push(format!("{name}: parse = {got}, serde_json = {want}")),
Err(code) => failures.push(format!("{name}: must-accept case rejected: {code}")),
}
}
report(failures);
}
#[test]
fn must_reject() {
let mut failures = Vec::new();
for name in cases("n_") {
let src = source(name);
if let Ok(value) = ours(&src) {
failures.push(format!("{name}: must-reject case accepted: {value}"));
}
if oracle(&src).is_ok() {
failures.push(format!("{name}: serde_json accepted an n_ case"));
}
}
report(failures);
}
struct Divergence {
case: &'static str,
why: &'static str,
ours: Option<&'static str>,
oracle_accepts: bool,
}
const KNOWN_DIVERGENCES: &[Divergence] = &[
Divergence {
case: "i_number_very_big_negative_int",
why: "serde_json is 1 ulp off str::parse::<f64>; the engine matches std",
ours: Some("[-2.374623746732769e+47]"),
oracle_accepts: true,
},
Divergence {
case: "i_object_key_lone_2nd_surrogate",
why: "lone surrogate: U+FFFD, not a rejection",
ours: Some("{\"\u{FFFD}\":0.0}"),
oracle_accepts: false,
},
Divergence {
case: "i_string_1st_surrogate_but_2nd_missing",
why: "lone surrogate: U+FFFD, not a rejection",
ours: Some("[\"\u{FFFD}\"]"),
oracle_accepts: false,
},
Divergence {
case: "i_string_1st_valid_surrogate_2nd_invalid",
why: "lone surrogate: U+FFFD, not a rejection",
ours: Some("[\"\u{FFFD}\u{1234}\"]"),
oracle_accepts: false,
},
Divergence {
case: "i_string_incomplete_surrogate_and_escape_valid",
why: "lone surrogate: U+FFFD, not a rejection",
ours: Some("[\"\u{FFFD}\\n\"]"),
oracle_accepts: false,
},
Divergence {
case: "i_string_incomplete_surrogate_pair",
why: "lone surrogate: U+FFFD, not a rejection",
ours: Some("[\"\u{FFFD}a\"]"),
oracle_accepts: false,
},
Divergence {
case: "i_string_incomplete_surrogates_escape_valid",
why: "lone surrogate: U+FFFD, not a rejection",
ours: Some("[\"\u{FFFD}\u{FFFD}\\n\"]"),
oracle_accepts: false,
},
Divergence {
case: "i_string_invalid_lonely_surrogate",
why: "lone surrogate: U+FFFD, not a rejection",
ours: Some("[\"\u{FFFD}\"]"),
oracle_accepts: false,
},
Divergence {
case: "i_string_invalid_surrogate",
why: "lone surrogate: U+FFFD, not a rejection",
ours: Some("[\"\u{FFFD}abc\"]"),
oracle_accepts: false,
},
Divergence {
case: "i_string_inverted_surrogates_U+1D11E",
why: "lone surrogate: U+FFFD, not a rejection",
ours: Some("[\"\u{FFFD}\u{FFFD}\"]"),
oracle_accepts: false,
},
Divergence {
case: "i_string_lone_second_surrogate",
why: "lone surrogate: U+FFFD, not a rejection",
ours: Some("[\"\u{FFFD}\"]"),
oracle_accepts: false,
},
];
fn known(name: &str) -> Option<&'static Divergence> {
let stem = name.strip_suffix(".json").unwrap_or(name);
KNOWN_DIVERGENCES.iter().find(|d| d.case == stem)
}
#[test]
fn implementation_defined() {
let mut failures = Vec::new();
for name in cases("i_") {
let src = source(name);
let ours = ours(&src);
let oracle = oracle(&src);
let agrees = match (&ours, &oracle) {
(Ok(got), Ok(want)) => same(got, want),
(Err(_), Err(_)) => true,
_ => false,
};
let Some(known) = known(name) else {
if !agrees {
failures.push(format!(
"{name}: {}\n (a NEW divergence from the platform oracle; \
fix it, or add it to KNOWN_DIVERGENCES with the reason \
and the expected result)",
describe(&ours, &oracle)
));
}
continue;
};
if agrees {
failures.push(format!(
"{name}: now agrees with serde_json, but is still listed in \
KNOWN_DIVERGENCES as {:?}; remove the entry",
known.why
));
continue;
}
let got = ours.as_ref().ok().map(|value| value.to_string());
if got.as_deref() != known.ours {
failures.push(format!(
"{name}: diverges as expected, but not in the recorded way.\n \
recorded: {:?}\n actual: {:?}\n ({})",
known.ours, got, known.why
));
}
if oracle.is_ok() != known.oracle_accepts {
failures.push(format!(
"{name}: the oracle now {} it, where the entry records {}",
if oracle.is_ok() { "accepts" } else { "rejects" },
if known.oracle_accepts {
"accept"
} else {
"reject"
}
));
}
}
report(failures);
}
fn describe(
ours: &Result<serde_json::Value, String>,
oracle: &Result<serde_json::Value, String>,
) -> String {
match (ours, oracle) {
(Ok(got), Ok(want)) => format!("parse = {got}, serde_json = {want}"),
(Ok(got), Err(e)) => format!("parse accepted ({got}), serde_json rejected: {e}"),
(Err(code), Ok(want)) => format!("parse rejected ({code}), serde_json = {want}"),
(Err(a), Err(b)) => format!("both rejected ({a} / {b})"),
}
}
fn report(failures: Vec<String>) {
if failures.is_empty() {
return;
}
panic!(
"{} conformance case(s) failed:\n{}",
failures.len(),
failures.join("\n")
);
}