Skip to content

Commit 29009f6

Browse files
bobzhangclaude
andcommitted
test(json): address review — pin depth-1024 parse for both shapes, mutate at code-unit level
- The nesting-limit test now asserts that documents exactly 1024 levels deep parse successfully for BOTH arrays and objects (previously it only checked valid() for the array and only checked the 1025-deep failure for objects), and the 1025-deep variables are renamed too_deep_array / too_deep_object to reflect their depth. - The mutation-totality test now genuinely mutates single UTF-16 code units via code_units() instead of Char-level to_array(), so deleting or replacing a unit can split an astral surrogate pair, and the replacement unit is drawn from the full 16-bit range (including lone surrogates) — strictly stronger fuzzing that matches the test's name and doc comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent fc24c9c commit 29009f6

1 file changed

Lines changed: 33 additions & 20 deletions

File tree

json/quickcheck_adversarial_test.mbt

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -253,21 +253,32 @@ test "duplicate object keys: last occurrence wins" {
253253
}
254254

255255
///|
256-
/// Deleting or replacing one code unit of a valid document must keep the
257-
/// parser total: it either succeeds or raises a parse error (`valid` agrees
258-
/// with `parse`), and when the mutant still parses, the parsed value is a
259-
/// fixed point of restringify-and-reparse.
256+
/// Deleting or replacing one UTF-16 code unit of a valid document must keep
257+
/// the parser total: it either succeeds or raises a parse error (`valid`
258+
/// agrees with `parse`), and when the mutant still parses, the parsed value
259+
/// is a fixed point of restringify-and-reparse.
260+
///
261+
/// Mutating at the code-unit level (not the `Char` level) means an astral
262+
/// character can lose half of its surrogate pair, and the replacement unit —
263+
/// drawn from the full 16-bit range — can itself be a lone surrogate.
260264
test "parse stays total under single code-unit deletion and replacement" {
261-
@quickcheck.check((input : (ArbJson, Int, Char)) => {
262-
let (json, position, replacement) = input
263-
let chars = json.0.stringify().to_array()
264-
guard chars.length() > 0 else { return true }
265-
let idx = wrap_index(position, chars.length())
266-
let deleted = chars.copy()
267-
ignore(deleted.remove(idx))
268-
let replaced = chars.copy()
269-
replaced[idx] = replacement
270-
for mutant in [String::from_array(deleted), String::from_array(replaced)] {
265+
@quickcheck.check((input : (ArbJson, Int, Int)) => {
266+
let (json, position, raw_replacement) = input
267+
let units = json.0.stringify().code_units()
268+
guard units.length() > 0 else { return true }
269+
let idx = wrap_index(position, units.length())
270+
let replacement = wrap_index(raw_replacement, 0x10000)
271+
let deleted = StringBuilder(size_hint=units.length())
272+
let replaced = StringBuilder(size_hint=units.length())
273+
for i, unit in units {
274+
if i != idx {
275+
deleted.write_char(unit.to_int().unsafe_to_char())
276+
replaced.write_char(unit.to_int().unsafe_to_char())
277+
} else {
278+
replaced.write_char(replacement.unsafe_to_char())
279+
}
280+
}
281+
for mutant in [deleted.to_string(), replaced.to_string()] {
271282
guard parse_succeeds(mutant) == @json.valid(mutant) else { return false }
272283
if @json.valid(mutant) {
273284
let value = @json.parse(mutant)
@@ -284,8 +295,6 @@ test "parse stays total under single code-unit deletion and replacement" {
284295
/// both arrays and objects. Also pins that `stringify` itself is iterative
285296
/// and survives a 1024-deep tree on every backend.
286297
test "default nesting limit boundary at depth 1024" {
287-
let deep_array = "[".repeat(1024) + "0" + "]".repeat(1024)
288-
assert_true(@json.valid(deep_array))
289298
fn outcome(text : String) -> String {
290299
try {
291300
ignore(@json.parse(text))
@@ -296,10 +305,14 @@ test "default nesting limit boundary at depth 1024" {
296305
}
297306
}
298307

299-
let deeper_array = "[".repeat(1025) + "0" + "]".repeat(1025)
300-
assert_eq(outcome(deeper_array), "depth limit")
301-
let deep_object = "{\"k\":".repeat(1025) + "0" + "}".repeat(1025)
302-
assert_eq(outcome(deep_object), "depth limit")
308+
let deep_array = "[".repeat(1024) + "0" + "]".repeat(1024)
309+
assert_eq(outcome(deep_array), "parsed")
310+
let too_deep_array = "[".repeat(1025) + "0" + "]".repeat(1025)
311+
assert_eq(outcome(too_deep_array), "depth limit")
312+
let deep_object = "{\"k\":".repeat(1024) + "0" + "}".repeat(1024)
313+
assert_eq(outcome(deep_object), "parsed")
314+
let too_deep_object = "{\"k\":".repeat(1025) + "0" + "}".repeat(1025)
315+
assert_eq(outcome(too_deep_object), "depth limit")
303316
let mut tree : Json = Json::number(0.0)
304317
for _ in 0..<1024 {
305318
tree = Json::array([tree])

0 commit comments

Comments
 (0)