use std::{
collections::BTreeMap,
env::var_os,
fs,
path::{Path, PathBuf},
};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use serde::Deserialize;
use sonic_rs::JsonContainerTrait;
#[derive(Deserialize)]
struct Suite {
load_tests: BTreeMap<String, Case>,
}
#[derive(Deserialize)]
struct Case {
load_in: Option<String>,
load_out: Option<sonic_rs::Value>,
load_err: Option<sonic_rs::Value>,
}
fn tests_path() -> Option<PathBuf> {
if let Some(p) = var_os("NT_OFFICIAL_TESTS") {
return Some(p.into());
}
let path =
Path::new(env!("CARGO_MANIFEST_DIR")).join("../nt_syntax/tests/official_tests/tests.json");
path.is_file().then_some(path)
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.0 >> 33
}
fn pick(&mut self, n: usize) -> usize {
(self.next() as usize) % n
}
}
const CH: &[char] = &[
' ', ':', '-', '>', '#', '[', ']', '{', '}', '\n', '\r', 'a', '1', '\t',
];
fn mutate(src: &str, rng: &mut Rng, rounds: usize) -> String {
let mut s = src.to_owned();
for _ in 0..rounds {
match rng.pick(5) {
0 => {
let mut end = rng.pick(s.len() + 1);
while !s.is_char_boundary(end) {
end -= 1;
}
s.truncate(end);
}
1 => {
let mut at = rng.pick(s.len() + 1);
while !s.is_char_boundary(at) {
at -= 1;
}
s.insert(at, CH[rng.pick(CH.len())]);
}
2 if !s.is_empty() => {
let mut at = rng.pick(s.len());
while !s.is_char_boundary(at) {
at -= 1;
}
s.remove(at);
}
3 if !s.is_empty() => {
let mut at = rng.pick(s.len());
while !s.is_char_boundary(at) {
at -= 1;
}
s.replace_range(at..at, &CH[rng.pick(CH.len())].to_string());
}
4 if s.contains('\n') => {
let line_li: Vec<&str> = s.split_inclusive('\n').collect();
let line = line_li[rng.pick(line_li.len())].to_owned();
drop(line_li);
s.push_str(&line);
}
_ => {}
}
}
s
}
#[test]
fn mutation_round_trip() {
let Some(path) = tests_path() else {
eprintln!("跳过:本包未捆绑 nt_syntax 的官方测试集(发布包情形)");
return;
};
let data = fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("读取官方测试集 {} 失败: {e}", path.display()));
let suite: Suite = sonic_rs::from_str(&data).unwrap();
let engine = STANDARD;
let mut rng = Rng(0x5eed);
let mut checked = 0usize;
let mut fail = Vec::new();
for (name, case) in &suite.load_tests {
let Some(b64) = &case.load_in else {
continue;
};
let has_err = case
.load_err
.as_ref()
.is_some_and(|v| v.as_object().is_none_or(|obj| !obj.is_empty()));
if case.load_out.is_none() || has_err {
continue;
}
let Ok(bytes) = engine.decode(b64) else {
continue;
};
let Ok(input) = String::from_utf8(bytes) else {
continue;
};
for _ in 0..30 {
let rounds = 1 + rng.pick(3);
let mutated = mutate(&input, &mut rng, rounds);
let Ok(Some(value)) = nt_syntax::parse(&mutated) else {
continue;
};
checked += 1;
let text = serde_nt::dumps(&value);
match nt_syntax::parse(&text) {
Ok(Some(back)) if back == value => {}
Ok(Some(_)) => fail.push(format!("{name}: 值变化\n{mutated}\n---\n{text}")),
Ok(None) => fail.push(format!("{name}: 写回为空\n{mutated}")),
Err(e) => fail.push(format!("{name}: 写回不可解析 {e}\n{mutated}\n---\n{text}")),
}
}
}
eprintln!("变异样本 {checked},失败 {}", fail.len());
assert!(
fail.is_empty(),
"变异失败:\n{}",
fail
.iter()
.take(5)
.cloned()
.collect::<Vec<_>>()
.join("\n\n")
);
}