use std::cmp::Ordering;
use asupersync::lab::{LabConfig, LabRuntime};
use regex_lite::Regex;
use serde_json::Value;
pub const TEST_SEED: u64 = 42;
pub const TEST_SEED_ALT: u64 = 7;
pub const TEST_TIMESTAMP: &str = "2026-01-01T00:00:00Z";
pub const TEST_WORKSPACE_ID: &str = "wsp_test0000000000000000000000";
pub const TEST_MEMORY_ID: &str = "mem_test0000000000000000000000";
pub const TEST_AUDIT_ID: &str = "audit_test0000000000000000000000";
pub const TEST_PACK_ID: &str = "pack_test0000000000000000000000";
pub const TEST_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
pub const TEST_DEGRADATION_CODE: &str = "test_degraded";
pub type TestResult = Result<(), String>;
pub fn validate_json_schema_instance(value: &Value, schema: &Value) -> TestResult {
validate_json_schema_value(value, schema, schema, "$")
}
fn validate_json_schema_value(
value: &Value,
schema: &Value,
root_schema: &Value,
path: &str,
) -> TestResult {
if let Some(reference) = schema.get("$ref").and_then(Value::as_str) {
let pointer = reference
.strip_prefix('#')
.ok_or_else(|| format!("unsupported non-local $ref {reference}"))?;
let target = root_schema
.pointer(pointer)
.ok_or_else(|| format!("unresolved $ref {reference}"))?;
return validate_json_schema_value(value, target, root_schema, path);
}
if let Some(options) = schema.get("oneOf").and_then(Value::as_array) {
let matches = options
.iter()
.filter(|candidate| {
validate_json_schema_value(value, candidate, root_schema, path).is_ok()
})
.count();
if matches != 1 {
return Err(format!(
"{path} matched {matches} oneOf branches instead of exactly one"
));
}
}
if let Some(options) = schema.get("anyOf").and_then(Value::as_array)
&& !options.iter().any(|candidate| {
validate_json_schema_value(value, candidate, root_schema, path).is_ok()
})
{
return Err(format!("{path} did not match any anyOf branch"));
}
if let Some(all_of) = schema.get("allOf").and_then(Value::as_array) {
for candidate in all_of {
validate_json_schema_value(value, candidate, root_schema, path)?;
}
}
if let Some(expected) = schema.get("const")
&& !json_schema_values_equal(value, expected)
{
return Err(format!("{path} expected const {expected}, got {value}"));
}
if let Some(enum_values) = schema.get("enum").and_then(Value::as_array)
&& !enum_values
.iter()
.any(|candidate| json_schema_values_equal(candidate, value))
{
return Err(format!(
"{path} value {value} is not in enum {enum_values:?}"
));
}
if let Some(expected_types) = json_schema_types(schema)
&& !expected_types
.iter()
.any(|expected_type| json_schema_type_matches(value, expected_type))
{
return Err(format!(
"{path} expected type {expected_types:?}, got {}",
json_schema_type_name(value)
));
}
if let Some(string) = value.as_str() {
let length = string.chars().count();
if let Some(min_length) = schema.get("minLength").and_then(Value::as_u64)
&& length < min_length as usize
{
return Err(format!("{path} has fewer than {min_length} characters"));
}
if let Some(max_length) = schema.get("maxLength").and_then(Value::as_u64)
&& length > max_length as usize
{
return Err(format!("{path} has more than {max_length} characters"));
}
if let Some(pattern) = schema.get("pattern").and_then(Value::as_str) {
let regex = Regex::new(pattern).map_err(|error| {
format!("{path} schema has invalid pattern {pattern:?}: {error}")
})?;
if !regex.is_match(string) {
return Err(format!(
"{path} value {string:?} does not match {pattern:?}"
));
}
}
if schema.get("format").and_then(Value::as_str) == Some("date-time")
&& chrono::DateTime::parse_from_rfc3339(string).is_err()
{
return Err(format!(
"{path} value {string:?} is not an RFC 3339 date-time"
));
}
}
if let Some(number) = value.as_number() {
if let Some(minimum) = schema.get("minimum").and_then(Value::as_number) {
let ordering = compare_json_numbers(number, minimum).ok_or_else(|| {
format!("{path} could not compare value {number} with minimum {minimum}")
})?;
if ordering == Ordering::Less {
return Err(format!("{path} value {number} is below minimum {minimum}"));
}
}
if let Some(maximum) = schema.get("maximum").and_then(Value::as_number) {
let ordering = compare_json_numbers(number, maximum).ok_or_else(|| {
format!("{path} could not compare value {number} with maximum {maximum}")
})?;
if ordering == Ordering::Greater {
return Err(format!("{path} value {number} is above maximum {maximum}"));
}
}
if let Some(minimum) = schema.get("exclusiveMinimum").and_then(Value::as_number) {
let ordering = compare_json_numbers(number, minimum).ok_or_else(|| {
format!("{path} could not compare value {number} with exclusive minimum {minimum}")
})?;
if ordering.is_le() {
return Err(format!(
"{path} value {number} is not above exclusive minimum {minimum}"
));
}
}
if let Some(maximum) = schema.get("exclusiveMaximum").and_then(Value::as_number) {
let ordering = compare_json_numbers(number, maximum).ok_or_else(|| {
format!("{path} could not compare value {number} with exclusive maximum {maximum}")
})?;
if ordering.is_ge() {
return Err(format!(
"{path} value {number} is not below exclusive maximum {maximum}"
));
}
}
}
if let Some(object) = value.as_object() {
if let Some(min_properties) = schema.get("minProperties").and_then(Value::as_u64)
&& object.len() < min_properties as usize
{
return Err(format!("{path} has fewer than {min_properties} properties"));
}
if let Some(max_properties) = schema.get("maxProperties").and_then(Value::as_u64)
&& object.len() > max_properties as usize
{
return Err(format!("{path} has more than {max_properties} properties"));
}
if let Some(required) = schema.get("required").and_then(Value::as_array) {
for field in required {
let field = field
.as_str()
.ok_or_else(|| format!("{path} schema required entry is not a string"))?;
if !object.contains_key(field) {
return Err(format!("{path} missing required field {field}"));
}
}
}
let properties = schema.get("properties").and_then(Value::as_object);
for (key, child) in object {
let child_path = format!("{path}.{key}");
if let Some(property_schema) = properties.and_then(|entries| entries.get(key)) {
validate_json_schema_value(child, property_schema, root_schema, &child_path)?;
continue;
}
match schema.get("additionalProperties") {
Some(Value::Bool(false)) => {
return Err(format!("{path} contains unexpected field {key}"));
}
Some(Value::Object(property_schema)) => validate_json_schema_value(
child,
&Value::Object(property_schema.clone()),
root_schema,
&child_path,
)?,
Some(Value::Bool(true)) | None => {}
Some(other) => {
return Err(format!(
"{path} has unsupported additionalProperties schema {other}"
));
}
}
}
}
if let Some(array) = value.as_array() {
if let Some(min_items) = schema.get("minItems").and_then(Value::as_u64)
&& array.len() < min_items as usize
{
return Err(format!("{path} has fewer than {min_items} items"));
}
if let Some(max_items) = schema.get("maxItems").and_then(Value::as_u64)
&& array.len() > max_items as usize
{
return Err(format!("{path} has more than {max_items} items"));
}
if schema.get("uniqueItems").and_then(Value::as_bool) == Some(true) {
for (index, item) in array.iter().enumerate() {
if array[..index]
.iter()
.any(|existing| json_schema_values_equal(existing, item))
{
return Err(format!("{path}[{index}] duplicates an earlier item"));
}
}
}
if let Some(prefix_items) = schema.get("prefixItems").and_then(Value::as_array) {
for (index, item_schema) in prefix_items.iter().enumerate() {
if let Some(item) = array.get(index) {
validate_json_schema_value(
item,
item_schema,
root_schema,
&format!("{path}[{index}]"),
)?;
}
}
}
if let Some(item_schema) = schema.get("items") {
for (index, item) in array.iter().enumerate() {
validate_json_schema_value(
item,
item_schema,
root_schema,
&format!("{path}[{index}]"),
)?;
}
}
}
Ok(())
}
fn json_schema_types(schema: &Value) -> Option<Vec<&str>> {
match schema.get("type")? {
Value::String(single) => Some(vec![single.as_str()]),
Value::Array(values) => Some(values.iter().filter_map(Value::as_str).collect()),
_ => None,
}
}
fn json_schema_type_matches(value: &Value, expected: &str) -> bool {
match expected {
"null" => value.is_null(),
"boolean" => value.is_boolean(),
"number" => value.is_number(),
"integer" => value.as_number().is_some_and(json_schema_number_is_integer),
"string" => value.is_string(),
"array" => value.is_array(),
"object" => value.is_object(),
_ => false,
}
}
fn json_schema_type_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(number) if json_schema_number_is_integer(number) => "integer",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
#[derive(Debug)]
struct NormalizedJsonDecimal {
negative: bool,
digits: Vec<u8>,
exponent: SignedDecimalInteger,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct SignedDecimalInteger {
negative: bool,
digits: Vec<u8>,
}
impl SignedDecimalInteger {
fn zero() -> Self {
Self {
negative: false,
digits: vec![0],
}
}
fn parse(raw: &str) -> Option<Self> {
let negative = raw.starts_with('-');
let digits = raw
.strip_prefix('-')
.or_else(|| raw.strip_prefix('+'))
.unwrap_or(raw);
if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
let digits = digits
.bytes()
.skip_while(|digit| *digit == b'0')
.map(|digit| digit - b'0')
.collect::<Vec<_>>();
if digits.is_empty() {
return Some(Self::zero());
}
Some(Self { negative, digits })
}
fn from_usize(value: usize) -> Self {
Self {
negative: false,
digits: value
.to_string()
.bytes()
.map(|digit| digit - b'0')
.collect(),
}
}
fn is_zero(&self) -> bool {
self.digits == [0]
}
fn is_nonnegative(&self) -> bool {
!self.negative
}
fn compare_absolute(left: &[u8], right: &[u8]) -> Ordering {
left.len().cmp(&right.len()).then_with(|| left.cmp(right))
}
fn add_absolute(left: &[u8], right: &[u8]) -> Vec<u8> {
let mut output = Vec::with_capacity(left.len().max(right.len()) + 1);
let mut left_index = left.len();
let mut right_index = right.len();
let mut carry = 0_u8;
while left_index > 0 || right_index > 0 || carry != 0 {
let left_digit = if left_index > 0 {
left_index -= 1;
left[left_index]
} else {
0
};
let right_digit = if right_index > 0 {
right_index -= 1;
right[right_index]
} else {
0
};
let sum = left_digit + right_digit + carry;
output.push(sum % 10);
carry = sum / 10;
}
output.reverse();
output
}
fn subtract_absolute(larger: &[u8], smaller: &[u8]) -> Vec<u8> {
debug_assert!(Self::compare_absolute(larger, smaller) != Ordering::Less);
let mut output = Vec::with_capacity(larger.len());
let mut smaller_index = smaller.len();
let mut borrow = 0_i8;
for larger_digit in larger.iter().rev() {
let smaller_digit = if smaller_index > 0 {
smaller_index -= 1;
smaller[smaller_index] as i8
} else {
0
};
let mut difference = *larger_digit as i8 - borrow - smaller_digit;
if difference < 0 {
difference += 10;
borrow = 1;
} else {
borrow = 0;
}
output.push(difference as u8);
}
debug_assert_eq!(borrow, 0);
output.reverse();
let first_nonzero = output
.iter()
.position(|digit| *digit != 0)
.unwrap_or(output.len() - 1);
output[first_nonzero..].to_vec()
}
fn add_signed_usize(&mut self, negative: bool, value: usize) {
if value == 0 {
return;
}
let other = Self::from_usize(value);
if self.is_zero() {
self.negative = negative;
self.digits = other.digits;
return;
}
if self.negative == negative {
self.digits = Self::add_absolute(&self.digits, &other.digits);
return;
}
match Self::compare_absolute(&self.digits, &other.digits) {
Ordering::Greater => {
self.digits = Self::subtract_absolute(&self.digits, &other.digits);
}
Ordering::Equal => *self = Self::zero(),
Ordering::Less => {
self.digits = Self::subtract_absolute(&other.digits, &self.digits);
self.negative = negative;
}
}
}
fn add_usize(&mut self, value: usize) {
self.add_signed_usize(false, value);
}
fn subtract_usize(&mut self, value: usize) {
self.add_signed_usize(true, value);
}
fn compare(&self, other: &Self) -> Ordering {
if self.negative != other.negative {
return if self.negative {
Ordering::Less
} else {
Ordering::Greater
};
}
let ordering = Self::compare_absolute(&self.digits, &other.digits);
if self.negative {
ordering.reverse()
} else {
ordering
}
}
}
fn normalize_json_number(number: &serde_json::Number) -> Option<NormalizedJsonDecimal> {
let rendered = number.to_string();
let (mantissa, explicit_exponent) = rendered.find(['e', 'E']).map_or(
Some((rendered.as_str(), SignedDecimalInteger::zero())),
|index| {
SignedDecimalInteger::parse(&rendered[index + 1..])
.map(|exponent| (&rendered[..index], exponent))
},
)?;
if mantissa.is_empty() {
return None;
}
let (negative, mantissa) = mantissa
.strip_prefix('-')
.map_or((false, mantissa), |unsigned| (true, unsigned));
let (whole, fraction) = mantissa.split_once('.').unwrap_or((mantissa, ""));
if whole.is_empty()
|| !whole.bytes().all(|byte| byte.is_ascii_digit())
|| !fraction.bytes().all(|byte| byte.is_ascii_digit())
{
return None;
}
let mut digits = whole
.bytes()
.chain(fraction.bytes())
.skip_while(|byte| *byte == b'0')
.collect::<Vec<_>>();
if digits.is_empty() {
return Some(NormalizedJsonDecimal {
negative: false,
digits: vec![b'0'],
exponent: SignedDecimalInteger::zero(),
});
}
let mut exponent = explicit_exponent;
exponent.subtract_usize(fraction.len());
while digits.last() == Some(&b'0') {
digits.pop();
exponent.add_usize(1);
}
Some(NormalizedJsonDecimal {
negative,
digits,
exponent,
})
}
fn json_schema_number_is_integer(number: &serde_json::Number) -> bool {
normalize_json_number(number).is_some_and(|number| number.exponent.is_nonnegative())
}
fn json_schema_values_equal(left: &Value, right: &Value) -> bool {
match (left, right) {
(Value::Number(left), Value::Number(right)) => {
compare_json_numbers(left, right) == Some(Ordering::Equal)
}
(Value::Array(left), Value::Array(right)) => {
left.len() == right.len()
&& left
.iter()
.zip(right)
.all(|(left, right)| json_schema_values_equal(left, right))
}
(Value::Object(left), Value::Object(right)) => {
left.len() == right.len()
&& left.iter().all(|(key, left)| {
right
.get(key)
.is_some_and(|right| json_schema_values_equal(left, right))
})
}
_ => left == right,
}
}
fn compare_json_numbers(left: &serde_json::Number, right: &serde_json::Number) -> Option<Ordering> {
let left = normalize_json_number(left)?;
let right = normalize_json_number(right)?;
let left_is_zero = left.digits.as_slice() == b"0";
let right_is_zero = right.digits.as_slice() == b"0";
if left_is_zero || right_is_zero {
return Some(match (left_is_zero, right_is_zero) {
(true, true) => Ordering::Equal,
(true, false) if right.negative => Ordering::Greater,
(true, false) => Ordering::Less,
(false, true) if left.negative => Ordering::Less,
(false, true) => Ordering::Greater,
(false, false) => unreachable!("zero branch requires at least one zero"),
});
}
if left.negative != right.negative {
return Some(if left.negative {
Ordering::Less
} else {
Ordering::Greater
});
}
let mut left_magnitude = left.exponent.clone();
left_magnitude.add_usize(left.digits.len());
let mut right_magnitude = right.exponent.clone();
right_magnitude.add_usize(right.digits.len());
let magnitude_order = left_magnitude.compare(&right_magnitude);
let absolute_order = if magnitude_order == Ordering::Equal {
let width = left.digits.len().max(right.digits.len());
(0..width)
.map(|index| {
left.digits
.get(index)
.copied()
.unwrap_or(b'0')
.cmp(&right.digits.get(index).copied().unwrap_or(b'0'))
})
.find(|ordering| *ordering != Ordering::Equal)
.unwrap_or(Ordering::Equal)
} else {
magnitude_order
};
Some(if left.negative {
absolute_order.reverse()
} else {
absolute_order
})
}
pub fn ensure_equal<T: std::fmt::Debug + PartialEq>(
actual: &T,
expected: &T,
context: &str,
) -> TestResult {
if actual == expected {
Ok(())
} else {
Err(format!("{context}: expected {expected:?}, got {actual:?}"))
}
}
pub fn ensure(condition: bool, context: impl Into<String>) -> TestResult {
if condition {
Ok(())
} else {
Err(context.into())
}
}
pub fn ensure_contains(haystack: &str, needle: &str, context: &str) -> TestResult {
if haystack.contains(needle) {
Ok(())
} else {
Err(format!(
"{context}: expected {haystack:?} to contain {needle:?}"
))
}
}
pub fn ensure_at_least<T: std::fmt::Debug + PartialOrd>(
actual: T,
minimum: T,
context: &str,
) -> TestResult {
if actual >= minimum {
Ok(())
} else {
Err(format!(
"{context}: expected at least {minimum:?}, got {actual:?}"
))
}
}
pub fn ensure_ok<T, E: std::fmt::Debug>(result: &Result<T, E>, context: &str) -> TestResult {
match result {
Ok(_) => Ok(()),
Err(e) => Err(format!("{context}: expected Ok, got Err({e:?})")),
}
}
pub fn ensure_err<T: std::fmt::Debug, E>(result: &Result<T, E>, context: &str) -> TestResult {
match result {
Ok(v) => Err(format!("{context}: expected Err, got Ok({v:?})")),
Err(_) => Ok(()),
}
}
pub fn ensure_some<T>(option: &Option<T>, context: &str) -> TestResult {
match option {
Some(_) => Ok(()),
None => Err(format!("{context}: expected Some, got None")),
}
}
pub fn ensure_none<T: std::fmt::Debug>(option: &Option<T>, context: &str) -> TestResult {
match option {
None => Ok(()),
Some(v) => Err(format!("{context}: expected None, got Some({v:?})")),
}
}
#[must_use]
pub fn test_memory_id(n: u32) -> String {
format!("mem_test{n:022}") }
#[must_use]
pub fn test_workspace_id(n: u32) -> String {
format!("wsp_test{n:022}") }
#[must_use]
pub fn test_pack_id(n: u32) -> String {
format!("pack_test{n:022}") }
#[must_use]
pub fn test_audit_id(n: u32) -> String {
format!("audit_test{n:022}")
}
#[must_use]
pub fn test_hash(seed: u64) -> String {
format!("{seed:064x}")
}
#[must_use]
pub fn valid_id(prefix: &str, len: usize, seed: &str) -> String {
let head_len = prefix.len() + 1; assert!(
len > head_len,
"id length {len} is too short for prefix `{prefix}_`"
);
let body_len = len - head_len;
let mut body: String = seed
.chars()
.filter_map(|character| {
let lower = character.to_ascii_lowercase();
lower.is_ascii_alphanumeric().then_some(lower)
})
.take(body_len)
.collect();
while body.len() < body_len {
body.push('0');
}
format!("{prefix}_{body}")
}
#[must_use]
pub fn wsp(seed: &str) -> String {
valid_id("wsp", 30, seed)
}
#[must_use]
pub fn agt(seed: &str) -> String {
valid_id("agt", 30, seed)
}
#[must_use]
pub fn mem(seed: &str) -> String {
valid_id("mem", 30, seed)
}
#[must_use]
pub fn imp(seed: &str) -> String {
valid_id("imp", 30, seed)
}
#[must_use]
pub fn ep(seed: &str) -> String {
valid_id("ep", 30, seed)
}
#[must_use]
pub fn mdl(seed: &str) -> String {
valid_id("mdl", 30, seed)
}
#[must_use]
pub fn agi(seed: &str) -> String {
valid_id("agi", 30, seed)
}
#[must_use]
pub fn ahs(seed: &str) -> String {
valid_id("ahs", 30, seed)
}
#[must_use]
pub fn pack(seed: &str) -> String {
valid_id("pack", 31, seed)
}
#[must_use]
pub fn sess(seed: &str) -> String {
valid_id("sess", 31, seed)
}
#[must_use]
pub fn rule(seed: &str) -> String {
valid_id("rule", 31, seed)
}
#[must_use]
pub fn link(seed: &str) -> String {
valid_id("link", 31, seed)
}
#[must_use]
pub fn ev(seed: &str) -> String {
valid_id("ev", 29, seed)
}
#[must_use]
pub fn audit(seed: &str) -> String {
valid_id("audit", 32, seed)
}
#[must_use]
pub fn curate(seed: &str) -> String {
valid_id("curate", 33, seed)
}
#[must_use]
pub fn art(seed: &str) -> String {
let digest = blake3::hash(seed.as_bytes()).to_hex();
format!("art_{}", &digest.as_str()[..26])
}
#[must_use]
pub fn lab_runtime(seed: u64) -> LabRuntime {
LabRuntime::new(LabConfig::new(seed))
}
#[must_use]
pub fn default_lab_runtime() -> LabRuntime {
lab_runtime(TEST_SEED)
}
#[must_use]
pub fn chaos_lab_runtime(seed: u64) -> LabRuntime {
LabRuntime::new(LabConfig::new(seed).with_light_chaos())
}
pub fn assert_deterministic_runtimes(seed: u64) {
let first = lab_runtime(seed);
let second = lab_runtime(seed);
assert_eq!(
first.now(),
second.now(),
"Lab runtimes with seed {seed} must have identical start time"
);
assert_eq!(
first.steps(),
second.steps(),
"Lab runtimes with seed {seed} must have identical step count"
);
}
pub fn with_lab_runtime<F, R>(seed: u64, test_fn: F) -> R
where
F: FnOnce(&mut LabRuntime) -> R,
{
let mut runtime = lab_runtime(seed);
test_fn(&mut runtime)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn draft_2020_12_integer_accepts_integral_decimal_numbers() -> TestResult {
let schema = serde_json::json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "integer",
"minimum": 0,
"maximum": 18446744073709551615_u64
});
let maximum_decimal: Value = serde_json::from_str("18446744073709551615.0")
.map_err(|error| format!("parse u64::MAX decimal fixture: {error}"))?;
let maximum_exponent: Value = serde_json::from_str("184467440737095516150e-1")
.map_err(|error| format!("parse u64::MAX exponent fixture: {error}"))?;
for value in [
serde_json::json!(0.0),
serde_json::json!(1.0),
serde_json::json!(1e3),
serde_json::json!(u64::MAX),
maximum_decimal,
maximum_exponent,
] {
validate_json_schema_instance(&value, &schema).map_err(|error| {
format!("schema rejected integral JSON number {value}: {error}")
})?;
}
Ok(())
}
#[test]
fn draft_2020_12_uint64_schema_rejects_out_of_domain_numbers() -> TestResult {
let schema = serde_json::json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "integer",
"minimum": 0,
"maximum": 18446744073709551615_u64
});
let over_u64: Value = serde_json::from_str("18446744073709551616")
.map_err(|error| format!("parse over-u64 fixture: {error}"))?;
let over_u64_decimal: Value = serde_json::from_str("18446744073709551616.0")
.map_err(|error| format!("parse over-u64 decimal fixture: {error}"))?;
let near_boundary_fraction: Value = serde_json::from_str("18446744073709551614.5")
.map_err(|error| format!("parse near-boundary fractional fixture: {error}"))?;
let huge_exponent: Value = serde_json::from_str("1e9223372036854775808")
.map_err(|error| format!("parse huge-exponent fixture: {error}"))?;
let huge_negative: Value = serde_json::from_str("-1e9223372036854775808")
.map_err(|error| format!("parse huge-negative fixture: {error}"))?;
for value in [
serde_json::json!(-1),
serde_json::json!(1.5),
over_u64,
over_u64_decimal,
near_boundary_fraction,
huge_exponent.clone(),
] {
if validate_json_schema_instance(&value, &schema).is_ok() {
return Err(format!(
"uint64 schema accepted out-of-domain JSON number {value}"
));
}
}
let bounded_number_schema = serde_json::json!({
"type": "number",
"maximum": 18446744073709551615_u64
});
if validate_json_schema_instance(&huge_exponent, &bounded_number_schema).is_ok() {
return Err("bounded number schema accepted an extreme positive value".into());
}
let bounded_negative_schema = serde_json::json!({
"type": "number",
"minimum": -1
});
if validate_json_schema_instance(&huge_negative, &bounded_negative_schema).is_ok() {
return Err("bounded number schema accepted an extreme negative value".into());
}
Ok(())
}
#[test]
fn draft_2020_12_numeric_const_and_enum_use_mathematical_equality() -> TestResult {
let const_schema = serde_json::json!({ "const": 1 });
let enum_schema = serde_json::json!({ "enum": [0, 1] });
let nested_schema = serde_json::json!({ "const": { "value": [1] } });
let unique_schema = serde_json::json!({ "uniqueItems": true });
for raw in ["1", "1.0", "1e0"] {
let value: Value = serde_json::from_str(raw)
.map_err(|error| format!("parse numeric equality fixture {raw}: {error}"))?;
validate_json_schema_instance(&value, &const_schema)?;
validate_json_schema_instance(&value, &enum_schema)?;
}
let nested: Value = serde_json::from_str(r#"{"value":[1.0]}"#)
.map_err(|error| format!("parse nested numeric equality fixture: {error}"))?;
validate_json_schema_instance(&nested, &nested_schema)?;
for raw in ["[1,1.0]", "[1,1e0]", "[1.0,1e0]"] {
let value: Value = serde_json::from_str(raw)
.map_err(|error| format!("parse numeric uniqueness fixture {raw}: {error}"))?;
if validate_json_schema_instance(&value, &unique_schema).is_ok() {
return Err(format!(
"uniqueItems accepted mathematically duplicate numbers in {raw}"
));
}
}
let tiny_a: Value = serde_json::from_str("1e-9223372036854775808")
.map_err(|error| format!("parse first extreme exponent: {error}"))?;
let tiny_b: Value = serde_json::from_str("1e-9999999999999999999")
.map_err(|error| format!("parse second extreme exponent: {error}"))?;
let mut extreme_const_schema = serde_json::json!({ "const": null });
extreme_const_schema["const"] = tiny_a.clone();
validate_json_schema_instance(&tiny_a, &extreme_const_schema)?;
if validate_json_schema_instance(&tiny_b, &extreme_const_schema).is_ok() {
return Err("distinct extreme exponents collapsed under const equality".into());
}
validate_json_schema_instance(
&serde_json::Value::Array(vec![tiny_a, tiny_b]),
&unique_schema,
)?;
if validate_json_schema_instance(&serde_json::json!(2), &const_schema).is_ok()
|| validate_json_schema_instance(&serde_json::json!(2), &enum_schema).is_ok()
{
return Err("numeric const/enum accepted a mathematically distinct value".into());
}
Ok(())
}
#[test]
fn test_seed_constant_is_stable() -> TestResult {
ensure_equal(&TEST_SEED, &42, "TEST_SEED")
}
#[test]
fn test_timestamp_is_valid_rfc3339() -> TestResult {
chrono::DateTime::parse_from_rfc3339(TEST_TIMESTAMP)
.map_err(|e| format!("TEST_TIMESTAMP is not valid RFC 3339: {e}"))?;
Ok(())
}
#[test]
fn test_workspace_id_has_correct_length() -> TestResult {
ensure_equal(&TEST_WORKSPACE_ID.len(), &30, "workspace ID length")
}
#[test]
fn test_memory_id_has_correct_length() -> TestResult {
ensure_equal(&TEST_MEMORY_ID.len(), &30, "memory ID length")
}
#[test]
fn test_audit_id_has_correct_length() -> TestResult {
ensure_equal(&TEST_AUDIT_ID.len(), &32, "audit ID length")
}
#[test]
fn test_pack_id_has_correct_length() -> TestResult {
ensure_equal(&TEST_PACK_ID.len(), &31, "pack ID length")
}
#[test]
fn test_hash_has_correct_length() -> TestResult {
ensure_equal(&TEST_HASH.len(), &64, "hash length")
}
#[test]
fn lab_runtime_is_deterministic() -> TestResult {
let first = lab_runtime(TEST_SEED);
let second = lab_runtime(TEST_SEED);
ensure_equal(&first.now(), &second.now(), "lab runtime start time")?;
ensure_equal(&first.steps(), &second.steps(), "lab runtime step count")
}
#[test]
fn default_lab_runtime_uses_test_seed() -> TestResult {
let default = default_lab_runtime();
let explicit = lab_runtime(TEST_SEED);
ensure_equal(
&default.now(),
&explicit.now(),
"default runtime matches explicit",
)
}
#[test]
fn different_seeds_are_accepted() -> TestResult {
let _first = lab_runtime(TEST_SEED);
let _second = lab_runtime(TEST_SEED_ALT);
Ok(())
}
#[test]
fn with_lab_runtime_provides_mutable_access() {
let initial_steps = with_lab_runtime(TEST_SEED, |runtime| runtime.steps());
assert_eq!(initial_steps, 0, "fresh runtime has zero steps");
}
#[test]
fn assert_deterministic_runtimes_passes_for_same_seed() {
assert_deterministic_runtimes(TEST_SEED);
assert_deterministic_runtimes(TEST_SEED_ALT);
}
#[test]
fn ensure_equal_passes_for_equal_values() -> TestResult {
ensure_equal(&42, &42, "integers")?;
ensure_equal(&"hello", &"hello", "strings")
}
#[test]
fn ensure_equal_fails_for_unequal_values() -> TestResult {
let result = ensure_equal(&42, &43, "test");
ensure(result.is_err(), "should fail for unequal values")
}
#[test]
fn ensure_passes_for_true() -> TestResult {
ensure(true, "condition is true")
}
#[test]
fn ensure_fails_for_false() -> TestResult {
let result = ensure(false, "test");
ensure_equal(&result.is_err(), &true, "should fail for false")
}
#[test]
fn ensure_contains_finds_substring() -> TestResult {
ensure_contains("hello world", "world", "substring found")
}
#[test]
fn ensure_contains_fails_for_missing_substring() -> TestResult {
let result = ensure_contains("hello", "world", "test");
ensure(result.is_err(), "should fail for missing substring")
}
#[test]
fn ensure_at_least_passes_for_equal() -> TestResult {
ensure_at_least(5, 5, "equal values")
}
#[test]
fn ensure_at_least_passes_for_greater() -> TestResult {
ensure_at_least(10, 5, "greater value")
}
#[test]
fn ensure_at_least_fails_for_less() -> TestResult {
let result = ensure_at_least(3, 5, "test");
ensure(result.is_err(), "should fail for less than minimum")
}
#[test]
fn ensure_ok_passes_for_ok() -> TestResult {
let result: Result<i32, &str> = Ok(42);
ensure_ok(&result, "should be Ok")
}
#[test]
fn ensure_ok_fails_for_err() -> TestResult {
let result: Result<i32, &str> = Err("error");
let check = ensure_ok(&result, "test");
ensure(check.is_err(), "should fail for Err")
}
#[test]
fn ensure_err_passes_for_err() -> TestResult {
let result: Result<i32, &str> = Err("error");
ensure_err(&result, "should be Err")
}
#[test]
fn ensure_some_passes_for_some() -> TestResult {
ensure_some(&Some(42), "should be Some")
}
#[test]
fn ensure_none_passes_for_none() -> TestResult {
let none: Option<i32> = None;
ensure_none(&none, "should be None")
}
#[test]
fn test_memory_id_generates_correct_format() -> TestResult {
let id = test_memory_id(1);
ensure_equal(&id.len(), &30, "memory ID length")?;
ensure(id.starts_with("mem_test"), "starts with mem_test")
}
#[test]
fn test_memory_id_increments_correctly() -> TestResult {
let id1 = test_memory_id(1);
let id2 = test_memory_id(2);
ensure(id1 != id2, "different numbers produce different IDs")
}
#[test]
fn test_workspace_id_generates_correct_format() -> TestResult {
let id = test_workspace_id(1);
ensure_equal(&id.len(), &30, "workspace ID length")?;
ensure(id.starts_with("wsp_test"), "starts with wsp_test")
}
#[test]
fn test_pack_id_generates_correct_format() -> TestResult {
let id = test_pack_id(1);
ensure_equal(&id.len(), &31, "pack ID length")?;
ensure(id.starts_with("pack_test"), "starts with pack_test")
}
#[test]
fn test_audit_id_generates_correct_format() -> TestResult {
let id = test_audit_id(1);
ensure_equal(&id.len(), &32, "audit ID length")?;
ensure(id.starts_with("audit_test"), "starts with audit_test")
}
#[test]
fn test_hash_generates_correct_length() -> TestResult {
let hash = test_hash(12345);
ensure_equal(&hash.len(), &64, "hash length")
}
#[test]
fn test_hash_is_deterministic() -> TestResult {
let hash1 = test_hash(42);
let hash2 = test_hash(42);
ensure_equal(&hash1, &hash2, "same seed produces same hash")
}
}