From 706d61ef8bd81aafcb4cfbb310412d89c70ec23e Mon Sep 17 00:00:00 2001 From: Rodolfo Hansen Date: Fri, 18 Sep 2026 14:52:32 +0200 Subject: [PATCH 1/3] =?UTF-8?q?test(jsoniter):=20structural=20scanner=20or?= =?UTF-8?q?acle=20replaces=20the=20no-throw=20prefix=20property=20?= =?UTF-8?q?=E2=80=94=20kills=20the=20end-of-input=20guard=20cluster?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prefix property drove every skip*/find* guard already; its oracle was `Try(...).isSuccess`, i.e. "did not throw". Every mutant in the cluster turns a Miss into a wrong-but-non-throwing Span, so the oracle was blind to all of them. Replaced in place with a model-generator + three-part oracle: (a) ABSOLUTE — the generator returns the rendered children alongside the document, so each top-level step`s expected span text is known without a second parse. A purely differential oracle compares two runs of the SAME mutated code and passes any uniformly-wrong scanner. (b) BOUNDS — 0 <= start <= end <= len on both `find` and `findAll`. (c) DIFFERENTIAL — R1/R2/R3 between prefix and full. Separators became ", " and child counts became frequency-weighted so empty containers occur in value position and a post-comma-space cut is reachable. Exception = failure now, so the no-throw guarantee is kept for free. Subsumed and deleted: section 8`s rootHit/negHit/untermOk clauses, and literalCases rows 1-6 (valid + truncated literals). Widened the existing numberGen exponent alternatives with digit-less "e"/"E". Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0194EHFR4NamCpTHiqy7B74V --- .../jsoniter/JsonScannerRobustnessSpec.scala | 222 ++++++++++++------ 1 file changed, 149 insertions(+), 73 deletions(-) diff --git a/jsoniter/src/test/scala/dev/constructive/eo/jsoniter/JsonScannerRobustnessSpec.scala b/jsoniter/src/test/scala/dev/constructive/eo/jsoniter/JsonScannerRobustnessSpec.scala index 79dd96e2..9dbbc251 100644 --- a/jsoniter/src/test/scala/dev/constructive/eo/jsoniter/JsonScannerRobustnessSpec.scala +++ b/jsoniter/src/test/scala/dev/constructive/eo/jsoniter/JsonScannerRobustnessSpec.scala @@ -6,8 +6,9 @@ import org.scalacheck.{Gen, Prop} import org.specs2.ScalaCheck import org.specs2.mutable.Specification -/** Grammar-sweep and robustness specs for [[JsonPathScanner]] — truncation-safety across every - * prefix of a generated document, literal/number/object/array skip grammars, and `findAll` fan-out +/** Grammar-sweep and robustness specs for [[JsonPathScanner]] — a structural scanner property that + * checks every prefix of a generated document against a model (absolute spans, in-bounds spans, + * prefix/full agreement), plus literal/number/object/array skip grammars and `findAll` fan-out * over mixed steps. Complements [[JsoniterPrismSpec]] / [[JsoniterTraversalSpec]], which exercise * the optic-level surface rather than the scanner's byte-level dispatch. See per-block `covers` * comments for `JsonPathScanner.scala` line ranges targeted. @@ -19,76 +20,159 @@ class JsonScannerRobustnessSpec extends Specification with ScalaCheck: private def text(b: Array[Byte], span: JsonPathScanner.Span): String = new String(b.slice(span.start, span.end), "UTF-8") - // ----- 1: truncation safety ------------------------------------------- - - private def genValue(depth: Int): Gen[String] = - val leaves = Gen.oneOf( - Gen.const("true"), - Gen.const("false"), - Gen.const("null"), - Gen.choose(-100, 100).map(_.toString), - Gen.oneOf("a", "bb", "").map(s => s""""$s""""), - ) - if depth <= 0 then leaves + // ----- 1: structural scanner property ---------------------------------- + + /** A generated document together with the rendered text of its top-level children. Carrying the + * children lets the oracle state what each top-level step MUST resolve to without re-parsing — + * an absolute check that a purely differential prefix-vs-full oracle cannot make, because a + * uniformly-wrong scanner agrees with itself. + */ + final private case class DocModel(text: String, kind: Char, kids: List[String]) + + private val genLeaf: Gen[String] = Gen.oneOf( + Gen.const("true"), + Gen.const("false"), + Gen.const("null"), + Gen.choose(-100, 100).map(_.toString), + Gen.oneOf("a", "bb", "").map(s => s""""$s""""), + ) + + // Child counts are frequency-weighted rather than uniform so the two structural boundaries both + // occur often: 0 children puts `{}` / `[]` in VALUE position (the empty-container fast paths at + // JsonPathScanner 130/159/206/245), >= 2 children force a sibling to be SKIPPED before the + // target is reached (the find*Loop / skip*Loop advance arms). + private val genKidCount: Gen[Int] = Gen.frequency((2, 0), (3, 1), (3, 2), (2, 3)) + + // Members and elements are joined with ", " — the space after the comma is load-bearing: it is + // the only way a prefix cut can leave `skipObjectLoop` at a position whose whitespace run reaches + // end-of-input (line 264 advances to `np + 1` without a skipWs, unlike line 181). + private def genModel(depth: Int): Gen[DocModel] = + val leaf = genLeaf.map(DocModel(_, 'l', Nil)) + if depth <= 0 then leaf else + val kids = genKidCount.flatMap(n => Gen.listOfN(n, genModel(depth - 1).map(_.text))) Gen.oneOf( - leaves, - Gen - .choose(0, 3) - .flatMap(n => Gen.listOfN(n, genValue(depth - 1))) - .map(_.mkString("[", ",", "]")), - Gen - .choose(0, 3) - .flatMap(n => Gen.listOfN(n, genValue(depth - 1))) - .map(vs => vs.zipWithIndex.map((v, i) => s""""k$i":$v""").mkString("{", ",", "}")), + leaf, + kids.map(ks => DocModel(ks.mkString("[", ", ", "]"), 'a', ks)), + kids.map(ks => + DocModel( + ks.zipWithIndex.map((v, i) => s""""k$i":$v""").mkString("{", ", ", "}"), + 'o', + ks, + ) + ), ) - private val genDoc: Gen[String] = genValue(3) - private val repPaths: List[List[PathStep]] = List( Nil, List(PathStep.Field("k0")), + List(PathStep.Field("k1")), List(PathStep.Index(0)), List(PathStep.Wildcard), List(PathStep.Field("k0"), PathStep.Index(1)), ) - "find/findAll never throw on any prefix of a generated document" >> { - // covers: every skip*/find* function in JsonPathScanner.scala — a mid-document cut must - // resolve to Miss/partial-Nil at any byte boundary, never an exception (bounds checks at - // lines 142/166/226/260/278/300/etc). - Prop.forAll(genDoc) { doc => - val docBytes = bytes(doc) - @tailrec def loop(len: Int): Boolean = - if len > docBytes.length then true + /** Absolute oracle: what the scanner must return for the WHOLE document, stated from the model + * rather than from a second run of the scanner. + */ + private def absoluteViolations(model: DocModel, full: Array[Byte]): List[String] = + val root = JsonPathScanner.find(full, Nil) + val rootV = + if root == JsonPathScanner.Span(0, full.length) then Nil + else List(s"root: $root != Span(0,${full.length})") + val kidV = model.kind match + case 'o' => + val hits = model.kids.zipWithIndex.flatMap { (kid, i) => + val sp = JsonPathScanner.find(full, List(PathStep.Field(s"k$i"))) + if sp.isHit && text(full, sp) == kid then Nil else List(s"$$.k$i -> $sp, want '$kid'") + } + val absent = JsonPathScanner.find(full, List(PathStep.Field("zz"))) + hits ::: (if absent.isHit then List(s"$$.zz -> $absent, want Miss") else Nil) + case 'a' => + val hits = model.kids.zipWithIndex.flatMap { (kid, i) => + val sp = JsonPathScanner.find(full, List(PathStep.Index(i))) + if sp.isHit && text(full, sp) == kid then Nil else List(s"$$[$i] -> $sp, want '$kid'") + } + val oob = List(model.kids.length, -1) + .filter(i => JsonPathScanner.find(full, List(PathStep.Index(i))).isHit) + .map(i => s"$$[$i] hit, want Miss") + val starTexts = + JsonPathScanner.findAll(full, List(PathStep.Wildcard)).map(s => text(full, s)) + val starV = + if starTexts == model.kids then Nil else List(s"$$[*] -> $starTexts, want ${model.kids}") + hits ::: oob ::: starV + case _ => + val f = JsonPathScanner.find(full, List(PathStep.Field("k0"))) + val i = JsonPathScanner.find(full, List(PathStep.Index(0))) + (if f.isHit then List(s"leaf $$.k0 -> $f, want Miss") else Nil) ::: + (if i.isHit then List(s"leaf $$[0] -> $i, want Miss") else Nil) + rootV ::: kidV + + /** Bounds + differential oracle over every prefix `full.take(len)`, `len` enumerated exhaustively + * so byte-exact cuts (the last byte of a literal, the space after a comma) are straddled by + * construction rather than sampled. + * + * - bounds: every returned span satisfies `0 <= start <= end <= len`, on BOTH surfaces. + * - R1: a prefix hit is a truncation of the full hit (same start, end no further). + * - R2: when the full span fits entirely inside the prefix, the prefix answer IS the full + * answer. (The converse — "must be a Miss otherwise" — is false: the scanner is permissive + * about truncated numbers, so `{"k0":12` resolves `$.k0` to the Hit `1`.) + * - R3: `findAll` agrees on every span that ends strictly inside the prefix. + */ + private def prefixViolations(full: Array[Byte]): List[String] = + repPaths.flatMap { p => + val sf = JsonPathScanner.find(full, p) + val af = JsonPathScanner.findAll(full, p) + @tailrec def loop(len: Int, acc: List[String]): List[String] = + if len > full.length then acc else - val prefix = docBytes.take(len) - val stepsOk = repPaths.forall { p => - scala.util.Try(JsonPathScanner.find(prefix, p)).isSuccess && - scala.util.Try(JsonPathScanner.findAll(prefix, p)).isSuccess - } - if !stepsOk then false else loop(len + 1) - loop(0) + val pre = full.take(len) + val sp = JsonPathScanner.find(pre, p) + val ap = JsonPathScanner.findAll(pre, p) + val spans = (if sp.isHit then List(sp) else Nil) ::: ap + val v1 = + if spans.forall(s => s.start >= 0 && s.start <= s.end && s.end <= len) then Nil + else List(s"bounds len=$len p=$p sp=$sp ap=$ap") + val v2 = + if !sp.isHit || (sf.isHit && sp.start == sf.start && sp.end <= sf.end) then Nil + else List(s"R1 len=$len p=$p sp=$sp sf=$sf") + val v3 = + if !(sf.isHit && sf.end <= len) || sp == sf then Nil + else List(s"R2 len=$len p=$p sp=$sp sf=$sf") + val v4 = + if ap.filter(_.end < len) == af.filter(_.end < len) then Nil + else List(s"R3 len=$len p=$p ap=$ap af=$af") + loop(len + 1, v4 ::: v3 ::: v2 ::: v1 ::: acc) + loop(0, Nil) + } + + "scanner: absolute spans, in-bounds spans, and prefix/full agreement at every cut" >> { + // covers: JsonPathScanner.scala 104:12 (walkAll pushes a Span(pos,-1)), 245:8 + 245:14 + // (skipObject's `{}` fast path), 254:29 (skipObjectLoop's `||` un-short-circuited at + // end-of-input), 348:28 + 348:48 (`matches` width guard) — plus every skip*/find* bounds guard + // the previous `Try(...).isSuccess` oracle could only observe as "did not throw". An exception + // now fails the property directly, so the no-throw guarantee is kept for free. + Prop.forAll(genModel(3)) { model => + val full = bytes(model.text) + val all = absoluteViolations(model, full) ::: prefixViolations(full) + Prop.propBoolean(all.isEmpty) :| all.take(3).mkString(" | ") } } // ----- 2: literal sweep ------------------------------------------------- + // Valid and truncated literals are reached by the section-1 generator (literal leaves, every + // prefix); a CORRUPTED interior byte is not a prefix of any well-formed document, so these rows + // stay as pinned examples. private val literalCases: List[(String, Boolean)] = List( - ("""{"a":true,"b":1}""", true), - ("""{"a":false,"b":1}""", true), - ("""{"a":null,"b":1}""", true), - ("""{"a":tru""", false), - ("""{"a":fals""", false), - ("""{"a":nul""", false), ("""{"a":txue,"b":1}""", false), ("""{"a":falze,"b":1}""", false), ("""{"a":nudl,"b":1}""", false), ("""{"a":true ,"b":1}trailing""", true), ) - "literal skip: true/false/null valid/truncated/wrong-char/trailing, never throws" >> { - // covers: skipLiteral dispatch + width checks (lines 312-317). + "literal skip: wrong-char literals Miss, whitespace-before-comma and trailing bytes Hit" >> { + // covers: skipLiteral dispatch + width checks (lines 303-309). val allOk = literalCases.forall { case (doc, expectHit) => val r = scala.util.Try(JsonPathScanner.find(bytes(doc), List(PathStep.Field("b")))) @@ -99,16 +183,18 @@ class JsonScannerRobustnessSpec extends Specification with ScalaCheck: // ----- 3: number-format sweep ------------------------------------------- + // "e" / "E" with no exponent digits: malformed, but the scanner is documented as permissive and + // must not derail — it is what forces the exponent-sign guard (line 323) to be read. private val numberGen: Gen[String] = for sign <- Gen.oneOf("", "-") intPart <- Gen.oneOf("0", "7", "123") frac <- Gen.oneOf("", ".5", ".0") - exp <- Gen.oneOf("", "e1", "e+1", "e-1", "E2", "E+2", "E-2") + exp <- Gen.oneOf("", "e1", "e+1", "e-1", "E2", "E+2", "E-2", "e", "E") yield s"$sign$intPart$frac$exp" "number skip: sign/fraction/exponent grammar sweep — $.after always Hits" >> { - // covers: skipNumber sign/frac/exponent-sign branches (lines 322-334). + // covers: skipNumber sign/frac/exponent-sign branches (lines 314-326), incl. 323:12. Prop.forAll(numberGen) { num => val doc = bytes(s"""{"n":$num,"after":1}""") JsonPathScanner.find(doc, List(PathStep.Field("after"))).isHit @@ -150,11 +236,11 @@ class JsonScannerRobustnessSpec extends Specification with ScalaCheck: } } - // Malformed or truncated scalar/object inputs that must resolve to a no-throw Miss. - // (Missing-]/-array truncation is already exercised by the section-1 truncation property, - // which drives k0[idx] into every prefix of a generated array — skipArray + findArray guards.) + // Malformed inputs a document generator cannot produce: each has broken syntax whose bytes + // nonetheless spell a resolvable member/element, so the mis-parse a dropped guard causes lands + // exactly on the probed path instead of being rejected by a later guard. private val malformedNoThrowCases: List[(String, List[PathStep])] = List( - // truncated numbers — skipNumber/skipDigits end-of-buffer guards (lines 322-338) + // truncated numbers — skipNumber/skipDigits end-of-buffer guards (lines 314-331) ("""{"n":1e+""", List(PathStep.Field("after"))), ("""{"n":1.""", List(PathStep.Field("after"))), ("""{"n":-""", List(PathStep.Field("after"))), @@ -164,11 +250,13 @@ class JsonScannerRobustnessSpec extends Specification with ScalaCheck: ("""{"a" 1,"target":2}""", List(PathStep.Field("target"))), ) - "malformed number/object inputs never throw, resolve to Miss" >> { + "malformed number/object inputs never throw, resolve to Miss on both surfaces" >> { val allOk = malformedNoThrowCases.forall { case (doc, path) => - val r = scala.util.Try(JsonPathScanner.find(bytes(doc), path)) - r.isSuccess && !r.get.isHit + val b = bytes(doc) + val one = scala.util.Try(JsonPathScanner.find(b, path)) + val many = scala.util.Try(JsonPathScanner.findAll(b, path)) + one.isSuccess && !one.get.isHit && many.isSuccess && many.get.isEmpty } allOk must beTrue } @@ -184,7 +272,7 @@ class JsonScannerRobustnessSpec extends Specification with ScalaCheck: List((0, 0), (1, 0), (1, 1), (2, 0), (2, 1), (2, 2), (5, 0), (5, 2), (5, 4), (5, 5), (5, -1)) "array scan: length x index sweep — Hit iff 0<=idx> { - // covers: findArrayElementLoop (lines 220-234), skipArray (lines 276-292). + // covers: findArrayElementLoop (lines 212-226), skipArray (lines 268-284). Prop.forAll(Gen.oneOf(arraySwScenarios)) { case (len, idx) => val doc = buildArrayDoc(len) @@ -227,21 +315,9 @@ class JsonScannerRobustnessSpec extends Specification with ScalaCheck: // ----- 8: small pinned examples -------------------------------------------- - "small examples: root Nil-path, negative number, unterminated string, key-length mismatch" >> { - // covers: find() start boundary (lines 49-51), skipNumber '-' branch (line 323), skipString - // unterminated guard (line 308), stringEqualsAscii length guard (lines 197-198). - val root = bytes("""{"a":1}""") - val rootHit = JsonPathScanner.find(root, Nil).isHit must beTrue - - val neg = bytes("""{"x":-5,"y":1}""") - val negHit = JsonPathScanner.find(neg, List(PathStep.Field("y"))).isHit must beTrue - - val unterminated = bytes("""{"a":"oops""") - val untermR = scala.util.Try(JsonPathScanner.find(unterminated, List(PathStep.Field("a")))) - val untermOk = (untermR.isSuccess must beTrue).and(untermR.get.isHit must beFalse) - + "small example: a key-length mismatch must not match" >> { + // covers: stringEqualsAscii length guard (line 197). The generator's keys and probes are all + // two bytes wide, so the length half of that guard never fires with a mismatch under it. val lenMismatch = bytes("""{"ab":1,"target":2}""") - val lenOk = JsonPathScanner.find(lenMismatch, List(PathStep.Field("abc"))).isHit must beFalse - - rootHit.and(negHit).and(untermOk).and(lenOk) + JsonPathScanner.find(lenMismatch, List(PathStep.Field("abc"))).isHit must beFalse } From 5e38dcc41a5cad75d6470b30467478301470db67 Mon Sep 17 00:00:00 2001 From: Rodolfo Hansen Date: Fri, 18 Sep 2026 14:52:32 +0200 Subject: [PATCH 2/3] =?UTF-8?q?test(jsoniter):=20nine=20malformed=20consta?= =?UTF-8?q?nts=20for=20the=20guards=20no=20generator=20can=20reach=20?= =?UTF-8?q?=E2=80=94=20kills=2011=20mutants=20in=209=20data=20rows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each row is a byte layout whose broken syntax is followed by bytes that accidentally spell the probed member/element, so a dropped guard mis-parses onto the target instead of being rejected by a later guard: 1"target":2 a step applied to a non-container value (78/85/108/113/118) {"a":} a value position holding no value at all (240:41) {Xk0":1} key-quote guard; skipString finds the CLOSING quote (168) {"k0"1 2} colon guard (173:8) ...nested twins the same two inside a SKIPPED value (254:8, 258:8) The existing malformed block`s oracle widened from `!find(...).isHit` to also require `findAll(...) == Nil`, which is what makes the walkAll guards observable; the widening covers the six pre-existing rows at no extra lines. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0194EHFR4NamCpTHiqy7B74V --- .../jsoniter/JsonScannerRobustnessSpec.scala | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/jsoniter/src/test/scala/dev/constructive/eo/jsoniter/JsonScannerRobustnessSpec.scala b/jsoniter/src/test/scala/dev/constructive/eo/jsoniter/JsonScannerRobustnessSpec.scala index 9dbbc251..79f017d0 100644 --- a/jsoniter/src/test/scala/dev/constructive/eo/jsoniter/JsonScannerRobustnessSpec.scala +++ b/jsoniter/src/test/scala/dev/constructive/eo/jsoniter/JsonScannerRobustnessSpec.scala @@ -248,6 +248,24 @@ class JsonScannerRobustnessSpec extends Specification with ScalaCheck: // unquoted key / missing colon — findFieldValueLoop quote/colon guards (lines 168, 173) ("""{a:1,"target":2}""", List(PathStep.Field("target"))), ("""{"a" 1,"target":2}""", List(PathStep.Field("target"))), + // covers: 78:12, 85:12, 108:12, 113:12, 118:12 — a step applied to a NON-container value must + // Miss even when the bytes that follow spell a valid member/element. Dropping the container + // check makes `find` resolve `$.target` to `2` and `$[0]` / `$[*]` to `"target"`. + ("""1"target":2""", List(PathStep.Field("target"))), + ("""1"target":2""", List(PathStep.Index(0))), + ("""1"target":2""", List(PathStep.Wildcard)), + // covers: 240:41 — with the digit-range `&&` flipped to `||` every byte starts a number, so + // skipValue resolves the missing value to the zero-length Span(5,5) instead of -1. + ("""{"a":}""", List(PathStep.Field("a"))), + // covers: 168:8, 168:29, 173:8 — findFieldValueLoop's key-shape and colon guards, with the + // bytes laid out so the mis-parse lands exactly on the probed key: in `{Xk0":1}` a dropped + // quote check makes skipString find the CLOSING quote, so the compared key IS `k0`. + ("""{Xk0":1}""", List(PathStep.Field("k0"))), + ("""{"k0"1 2}""", List(PathStep.Field("k0"))), + // covers: 254:8, 258:8 — the skipObjectLoop twins of the two rows above, nested inside a value + // that must be skipped over on the way to a later member. + ("""{"a":{Xk0":1}, "target":2}""", List(PathStep.Field("target"))), + ("""{"a":{"k0"1 2}, "target":2}""", List(PathStep.Field("target"))), ) "malformed number/object inputs never throw, resolve to Miss on both surfaces" >> { From a2867e682a4816109f22d5b87881d0ecb27305e7 Mon Sep 17 00:00:00 2001 From: Rodolfo Hansen Date: Fri, 18 Sep 2026 14:59:49 +0200 Subject: [PATCH 3/3] =?UTF-8?q?test(jsoniter):=20delete=20the=20object-cou?= =?UTF-8?q?nt=20and=20array-index=20sweeps=20=E2=80=94=20subsumed,=200=20k?= =?UTF-8?q?ills=20lost?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new absolute oracle asserts each top-level child`s span text by index on every generated document, which is exactly what these two enumerated sweeps were: a target-position sweep plus absent-key / index-past-end / negative-index Miss rows plus a skip-over-a-sibling assertion. Measured, not assumed: stryker before the deletion 328K/58S, after 328K/58S — zero regressions by mutant key (file, line, column, mutator, replacement). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0194EHFR4NamCpTHiqy7B74V --- .../jsoniter/JsonScannerRobustnessSpec.scala | 63 +------------------ 1 file changed, 2 insertions(+), 61 deletions(-) diff --git a/jsoniter/src/test/scala/dev/constructive/eo/jsoniter/JsonScannerRobustnessSpec.scala b/jsoniter/src/test/scala/dev/constructive/eo/jsoniter/JsonScannerRobustnessSpec.scala index 79f017d0..b0c24939 100644 --- a/jsoniter/src/test/scala/dev/constructive/eo/jsoniter/JsonScannerRobustnessSpec.scala +++ b/jsoniter/src/test/scala/dev/constructive/eo/jsoniter/JsonScannerRobustnessSpec.scala @@ -201,41 +201,6 @@ class JsonScannerRobustnessSpec extends Specification with ScalaCheck: } } - // ----- 4: object sweep --------------------------------------------------- - - private def buildObject(count: Int, targetIdx: Option[Int]): (Array[Byte], Option[String]) = - val value = "999" - val fields: Vector[(String, String)] = (0 until count).map { i => - if targetIdx.contains(i) then ("target", value) else (s"f$i", i.toString) - }.toVector - (bytes(fields.map((k, v) => s""""$k":$v""").mkString("{", ",", "}")), targetIdx.map(_ => value)) - - // (count, target index) — enumerated so every count x position combo that makes sense occurs. - private val objectScenarios: List[(Int, Option[Int])] = List( - (0, None), - (1, Some(0)), - (1, None), - (2, Some(0)), - (2, Some(1)), - (2, None), - (5, Some(0)), - (5, Some(2)), - (5, Some(4)), - (5, None), - ) - - "object scan: member-count x target-position sweep — Hit iff present, span decodes correctly" >> { - // covers: findFieldValueLoop key-match / skip-and-advance (lines 165-182). - Prop.forAll(Gen.oneOf(objectScenarios)) { - case (count, targetIdx) => - val (doc, expected) = buildObject(count, targetIdx) - val span = JsonPathScanner.find(doc, List(PathStep.Field("target"))) - val hitOk = span.isHit == expected.isDefined - val valueOk = expected.forall(v => text(doc, span) == v) - hitOk && valueOk - } - } - // Malformed inputs a document generator cannot produce: each has broken syntax whose bytes // nonetheless spell a resolvable member/element, so the mis-parse a dropped guard causes lands // exactly on the probed path instead of being rejected by a later guard. @@ -279,31 +244,7 @@ class JsonScannerRobustnessSpec extends Specification with ScalaCheck: allOk must beTrue } - // ----- 5: array sweep ----------------------------------------------------- - - private def buildArrayDoc(len: Int): Array[Byte] = - val elems = (0 until len).map(i => (i * 10).toString).mkString(",") - bytes(s"""{"arr":[$elems],"after":1}""") - - // (length, target index) — includes first/mid/last/just-past/negative-oob. - private val arraySwScenarios: List[(Int, Int)] = - List((0, 0), (1, 0), (1, 1), (2, 0), (2, 1), (2, 2), (5, 0), (5, 2), (5, 4), (5, 5), (5, -1)) - - "array scan: length x index sweep — Hit iff 0<=idx> { - // covers: findArrayElementLoop (lines 212-226), skipArray (lines 268-284). - Prop.forAll(Gen.oneOf(arraySwScenarios)) { - case (len, idx) => - val doc = buildArrayDoc(len) - val span = JsonPathScanner.find(doc, List(PathStep.Field("arr"), PathStep.Index(idx))) - val expectHit = idx >= 0 && idx < len - val hitOk = span.isHit == expectHit - val valueOk = !expectHit || text(doc, span) == (idx * 10).toString - val afterOk = JsonPathScanner.find(doc, List(PathStep.Field("after"))).isHit - hitOk && valueOk && afterOk - } - } - - // ----- 6: findAll mixed-step ----------------------------------------------- + // ----- 4: findAll mixed-step ----------------------------------------------- private val mixedDoc = bytes( """{"rows":[{"xs":[1,2]},{"xs":[3,4,5]},{"xs":[]},{"xs":[9,BAD,7]}]}""" @@ -331,7 +272,7 @@ class JsonScannerRobustnessSpec extends Specification with ScalaCheck: valuesOk.and(row0Ok).and(topOk).and(noThrow must beTrue) } - // ----- 8: small pinned examples -------------------------------------------- + // ----- 5: small pinned examples -------------------------------------------- "small example: a key-length mismatch must not match" >> { // covers: stringEqualsAscii length guard (line 197). The generator's keys and probes are all