serde_nt 0.1.12

NestedText (de)serialization for serde, powered by nt_syntax
Documentation
//! 确定性变异测试:任何能被解析的值,写回后必须能再次解析且值不变。
//!
//! 变异的种子取自 `nt_syntax` 内的官方测试集快照。

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>,
}

/// 官方套件路径:`NT_OFFICIAL_TESTS` 优先,否则用 `nt_syntax` 内的内置快照
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")
  );
}