pub(crate) mod json;
pub(crate) mod markers;
pub(crate) mod queue;
pub(crate) mod rmcp;
pub(crate) mod rules;
pub(crate) mod strategies;
pub(crate) mod vectors;
use std::path::{Path, PathBuf};
use proptest::test_runner::Config;
use serde_json::Value;
use crate::wire;
#[derive(Debug)]
pub(crate) struct TempHome(PathBuf);
impl TempHome {
pub(crate) fn new(name: &str) -> Self {
let dir = std::env::temp_dir().join(format!("agentcat-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create the temp home");
Self(dir)
}
pub(crate) fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempHome {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
const SHOWN_BYTES: usize = 40;
pub(crate) fn assert_same_json_text(actual: &Value, expected: &Value, context: &str) {
let actual = wire::stringify(actual);
let expected = wire::stringify(&markers::decode_utf16(expected.clone()));
if actual == expected {
return;
}
let at = actual
.bytes()
.zip(expected.bytes())
.position(|(a, e)| a != e)
.unwrap_or_else(|| actual.len().min(expected.len()));
panic!(
"{context}: JSON texts differ at byte {at}\n actual: {}\n expected: {}",
around(&actual, at),
around(&expected, at),
);
}
fn around(text: &str, at: usize) -> String {
let mut start = at.saturating_sub(SHOWN_BYTES / 2);
while !text.is_char_boundary(start) {
start -= 1;
}
let mut end = (start + SHOWN_BYTES).min(text.len());
while !text.is_char_boundary(end) {
end += 1;
}
format!("{:?} (bytes {start}..{end})", &text[start..end])
}
pub(crate) fn config() -> Config {
Config::default()
}
const SHRINK_ITERS: u32 = 64;
const SHRINK_MS: u32 = 60_000;
pub(crate) fn config_capped(max: u32) -> Config {
let mut config = Config::with_cases(config().cases.min(max));
config.max_shrink_iters = config.max_shrink_iters().min(SHRINK_ITERS);
config.max_shrink_time = match config.max_shrink_time {
0 => SHRINK_MS,
ms => ms.min(SHRINK_MS),
};
config
}
mod tests {
use proptest::test_runner::Config;
use serde_json::json;
use super::{around, assert_same_json_text, config, config_capped};
fn panic_message(f: impl FnOnce() + std::panic::UnwindSafe) -> String {
let payload = std::panic::catch_unwind(f).unwrap_err();
payload.downcast_ref::<String>().unwrap().clone()
}
#[test]
fn assert_same_json_text_passes_equal_values() {
let value = json!({"a": [1, "x", null], "b": {"c": true}});
assert_same_json_text(&value, &value, "same");
}
#[test]
fn assert_same_json_text_drops_the_half_character_of_a_utf16_marker() {
assert_same_json_text(&json!(["a"]), &json!([{"$utf16": [97, 55357]}]), "cut");
}
#[test]
fn assert_same_json_text_names_the_context_and_the_first_difference() {
let message = panic_message(|| assert_same_json_text(&json!("abc"), &json!("abd"), "ctx"));
let lines = [
"ctx: JSON texts differ at byte 3",
r#" actual: "\"abc\"" (bytes 0..5)"#,
r#" expected: "\"abd\"" (bytes 0..5)"#,
];
assert_eq!(message, lines.join("\n"));
}
#[test]
fn assert_same_json_text_puts_a_missing_tail_at_the_end_of_the_shorter_text() {
let message = panic_message(|| assert_same_json_text(&json!(1), &json!(12), "tail"));
assert!(
message.starts_with("tail: JSON texts differ at byte 1\n"),
"{message}"
);
}
#[test]
fn around_shows_about_40_bytes_widened_to_character_boundaries() {
let text = format!("\"{}", "世".repeat(20));
assert_eq!(
around(&text, 31),
format!("{:?} (bytes 10..52)", "世".repeat(14))
);
assert_eq!(around("ab", 1), format!("{:?} (bytes 0..2)", "ab"));
}
#[test]
fn config_is_the_proptest_default() {
assert_eq!(config().cases, Config::default().cases);
}
#[test]
fn config_capped_takes_the_smaller_case_count() {
assert_eq!(config_capped(1).cases, 1);
assert_eq!(config_capped(u32::MAX).cases, Config::default().cases);
}
#[test]
fn config_capped_caps_shrinking() {
let config = config_capped(1_000);
assert!(
config.max_shrink_iters() <= 64,
"{}",
config.max_shrink_iters()
);
assert!(
(1..=60_000).contains(&config.max_shrink_time),
"{}",
config.max_shrink_time
);
}
}