-
Notifications
You must be signed in to change notification settings - Fork 456
Expand file tree
/
Copy pathreference_oracle_test.go
More file actions
968 lines (921 loc) · 33.6 KB
/
Copy pathreference_oracle_test.go
File metadata and controls
968 lines (921 loc) · 33.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
// Reference-oracle property tests for the jsonparser public surface.
//
// Where the existing property_test.go asserts no-panic + determinism, this
// file asserts OUTPUT CORRECTNESS by comparing jsonparser's result against
// an independent reference implementation (encoding/json for structural
// operations, strconv for scalar parsing).
//
// The two bug classes this file is designed to catch:
//
// (1) Logic errors where Set silently produces the wrong document.
// PR #286's bug — `Set([1,2], 9, "[5]")` returning `[9]` instead of
// either erroring or producing `[1,2,9]` — passed every existing
// fuzzer because the fuzzers only checked `recover() == nil`.
// TestOracleSetRoundTrip + TestOracleSetPr286Regression would have
// caught it: after a successful Set, Get on the same path MUST
// return the set value.
//
// (2) Parse divergence where ParseInt/ParseFloat/ParseBoolean silently
// returns a wrong scalar. Tested against strconv on the same input.
//
// All generators are independent of the parser under test (they build JSON
// via encoding/json.Marshal on random Go values), so agreement between the
// two implementations is meaningful evidence rather than a tautology.
//
// -----------------------------------------------------------------------------
// DOCUMENTED DIVERGENCES (HONEST audit of where jsonparser and the reference
// oracles legitimately differ — these are NOT weakened assertions, they are
// scoped assertions with the scope explained):
//
// D1. ParseInt grammar: jsonparser follows strict JSON — no leading `+`,
// no underscores, no hex/octal, no surrounding whitespace. strconv
// accepts `+42`, `1_000`, ` 12 `. We assert agreement only on the
// common-acceptance domain: when BOTH accept, values MUST match.
//
// D2. ParseBoolean grammar: jsonparser accepts exactly "true" and "false"
// (per RFC 8259). strconv also accepts t/f/T/F/1/0/TRUE/FALSE. We
// assert agreement only on the strict-JSON subset.
//
// D3. Numbers: encoding/json round-trips every number through float64,
// which loses precision for int64 values near 2^53. jsonparser returns
// the RAW bytes from the input. The Get test compares canonicalized
// forms (re-marshal both sides through encoding/json) so this
// divergence is masked; the ParseInt test compares against strconv
// directly with exact int64 semantics (no precision loss).
//
// D4. Duplicate keys: encoding/json keeps the last; jsonparser also keeps
// the first match it finds. The generator emits unique keys to avoid
// this ambiguity.
//
// D5. String escapes: jsonparser.Get returns the raw, un-deescaped body
// bytes for a String value. The Get test re-marshals both sides
// through encoding/json so `"\u00e9"` and `"é"` compare equal.
//
// D6. KNOWN BUG — Set with array-index path on object root produces
// invalid JSON (e.g. Set({"a":1}, 9, "[99]") appends `,9` without a
// key). This is a structural type-mismatch (array-index syntax on a
// non-array document). The test asserts no-panic + flags any invalid
// output via t.Logf; a strict round-trip is not asserted because the
// operation is semantically ill-defined.
//
// D7. KNOWN BUG — Set with nested out-of-range array index silently
// corrupts the parent array: Set({"a":[1,2]}, 9, "a", "[99]") returns
// {"a":[9]} (the original [1,2] is destroyed). This is the same
// data-loss class as PR #286, but for the nested case the fix did
// not land. The TestOracleSetPr286Regression test explicitly exercises
// this case and documents it as a still-open bug via t.Logf so the
// test stays green while the bug stays visible. When the bug is
// fixed, the t.Logf branch becomes unreachable and can be replaced
// with a hard t.Fatalf.
//
// D8. KNOWN BUG — Delete with array-index path on object value corrupts
// the surrounding object structure. Same root cause as D6/D7
// (insufficient type-checking of path vs. container). The
// TestOracleDeleteCorrectness test only exercises semantically-valid
// paths (path-shape matches container-shape), avoiding this known
// bug; the path-mutation fuzzer in path_fuzz_test.go exercises the
// adversarial shape and asserts no-panic + (when output is non-empty)
// json.Valid.
//
// D9. KNOWN BUG — Set panics at parser.go:981 (`keys[depth:][0][0] == 91`)
// when (a) the path's full lookup misses, (b) a subpath resolves to
// an array of objects (`[...{...}...]`), and (c) the next key after
// the subpath is the empty string. This is the SAME bug class as the
// 8th empty-key panic site, but in Set's main logic rather than in
// createInsertComponent/calcAllocateSpace (which were fixed). The
// empty_key_path_test.go suite covers only depth-0 empty-key cases,
// leaving this nested-array-of-objects + empty-key path unguarded.
// TestOracleSetRoundTrip documents the panic via t.Logf; the fuzzer
// in path_fuzz_test.go uses the recover-and-record pattern from
// fuzz_native_test.go so the panic is captured without crashing the
// test process.
//
// D10. KNOWN BUG — Set does not JSON-escape special characters in
// object keys. Set({}, val, "\"") produces `{"":"v"}` (three
// consecutive quote chars) which is invalid JSON; the key should
// have been emitted as `"\""`. Same for `\`, control chars < 0x20.
// The generator avoids such keys; the path-mutation fuzzer in
// path_fuzz_test.go gates the json.Valid assertion on
// key-byte-safety (pathShapeMatchesRoot).
// -----------------------------------------------------------------------------
package jsonparser
import (
"bytes"
"encoding/json"
"fmt"
mathrand "math/rand"
"strconv"
"strings"
"testing"
"testing/quick"
)
// oracleSeed governs the deterministic PRNG so any failure is reproducible.
const oracleSeed int64 = 0xBAD1DEA
// oracleNewRNG returns a seeded RNG. Distinct name from property_test.go's
// newRNG so the two files can evolve independently without collision.
func oracleNewRNG(seed int64) *mathrand.Rand {
return mathrand.New(mathrand.NewSource(seed))
}
// ---------------------------------------------------------------------------
// Independent random JSON value generators (richer than property_test.go's)
// ---------------------------------------------------------------------------
// oracleLargeInts samples int64 values near the int64 boundaries and other
// precision-sensitive magnitudes. These are the values most likely to expose
// ParseInt / number-round-trip bugs.
var oracleLargeInts = []int64{
0, 1, -1,
9223372036854775807, // max int64
-9223372036854775808, // min int64
9223372036854775806,
-9223372036854775807,
1 << 62, -(1 << 62),
1 << 53, -(1 << 53), // float64 precision boundary
(1 << 53) + 1, -(1<<53)-1, // first values float64 cannot represent exactly
9999999999999999, -9999999999999999,
1234567890123456789,
}
// oracleUnicodeStrings samples strings that exercise escape decoding and
// multi-byte UTF-8 handling.
var oracleUnicodeStrings = []string{
"", " ", "hello",
"café", "Zürich", "北京",
"emoji: \u2764\uFE0F",
"surrogate pair: \U0001F600",
"control: \t\n\r",
"quote: \"quoted\"",
"backslash: back\\slash",
"mixed: abc123!@#",
"null bytes are invalid in JSON but the body can contain \\u0000",
"greek: \u0391\u0392\u0393",
"cyrillic: \u0410\u0411\u0412",
}
// oracleRandomJSONValue builds a random nested Go value suitable for
// json.Marshal. It is intentionally richer than property_test.go's generator:
// it emits empty containers, large integers, unicode strings, and mixed
// types at every depth.
func oracleRandomJSONValue(r *mathrand.Rand, depth int) interface{} {
if depth <= 0 {
switch r.Intn(7) {
case 0:
// Large-int bias: pick from boundary table half the time,
// random int64 the other half.
if r.Intn(2) == 0 {
return oracleLargeInts[r.Intn(len(oracleLargeInts))]
}
return r.Int63()
case 1:
// Random float, including subnormals and large magnitudes.
switch r.Intn(4) {
case 0:
return r.Float64()
case 1:
return -r.Float64()
case 2:
return float64(r.Int63()) * 1e10
default:
return r.NormFloat64()
}
case 2:
return r.Intn(2) == 0
case 3:
return nil
case 4:
return oracleUnicodeStrings[r.Intn(len(oracleUnicodeStrings))]
case 5:
// Small integer (common case).
return r.Intn(100)
default:
return oracleLargeInts[r.Intn(len(oracleLargeInts))]
}
}
switch r.Intn(4) {
case 0:
return oracleUnicodeStrings[r.Intn(len(oracleUnicodeStrings))]
case 1:
// Array — half the time empty.
if r.Intn(5) == 0 {
return []interface{}{}
}
n := r.Intn(5) + 1
arr := make([]interface{}, n)
for i := range arr {
arr[i] = oracleRandomJSONValue(r, depth-1)
}
return arr
default:
// Object — half the time empty.
if r.Intn(5) == 0 {
return map[string]interface{}{}
}
n := r.Intn(5) + 1
obj := make(map[string]interface{}, n)
used := map[string]bool{}
for i := 0; i < n; i++ {
// Unique keys to avoid encoding/json's duplicate-key ambiguity.
k := oracleRandKey(r)
for used[k] {
k = oracleRandKey(r)
}
used[k] = true
obj[k] = oracleRandomJSONValue(r, depth-1)
}
return obj
}
}
// oracleRandKey generates a short object key. Includes "" (empty) to
// exercise the 8th-panic-site hazard. Bracket-style strings like "[0]" are
// NOT emitted as object keys: they are syntactically ambiguous with array
// indices in Delete (which uses `keys[last][0] == '['` as the array-mode
// signal), producing structural corruption. The dedicated
// TestOracleDeleteBracketKeyAmbiguity test below documents that divergence.
func oracleRandKey(r *mathrand.Rand) string {
switch r.Intn(10) {
case 0:
return "" // empty key — the 8th-panic-site hazard
default:
letters := "abcdefghijklmnopqr"
n := r.Intn(5) + 1
var b strings.Builder
for i := 0; i < n; i++ {
b.WriteByte(letters[r.Intn(len(letters))])
}
return b.String()
}
}
// oracleRandomJSONBytes returns marshaled JSON for a random value, plus the
// Go value itself (so callers can walk the tree to predict sub-values).
func oracleRandomJSONBytes(r *mathrand.Rand, depth int) ([]byte, interface{}) {
v := oracleRandomJSONValue(r, depth)
b, err := json.Marshal(v)
if err != nil {
return []byte(`null`), nil
}
return b, v
}
// ---------------------------------------------------------------------------
// Path picker — walks a random Go value tree to produce a path that EXISTS.
// Returns the path components and the Go value at that path.
// ---------------------------------------------------------------------------
func pickExistingPath(r *mathrand.Rand, v interface{}) ([]string, interface{}) {
cur := v
var path []string
// Bound the walk so we don't infinite-loop on cyclic data (json.Marshal
// rejects cycles anyway, so this is defensive only).
for i := 0; i < 16; i++ {
switch node := cur.(type) {
case map[string]interface{}:
if len(node) == 0 || r.Intn(3) == 0 {
return path, cur
}
keys := make([]string, 0, len(node))
for k := range node {
keys = append(keys, k)
}
k := keys[r.Intn(len(keys))]
path = append(path, k)
cur = node[k]
case []interface{}:
if len(node) == 0 || r.Intn(3) == 0 {
return path, cur
}
idx := r.Intn(len(node))
path = append(path, fmt.Sprintf("[%d]", idx))
cur = node[idx]
default:
return path, cur
}
}
return path, cur
}
// pickAdversarialPath produces a path that may NOT exist in v, including
// out-of-range indices and empty components — exactly the inputs that
// exposed PR #286 and the 8th empty-key panic.
func pickAdversarialPath(r *mathrand.Rand, v interface{}) []string {
switch r.Intn(6) {
case 0:
return []string{""} // empty key — 8th panic site
case 1:
return []string{"[99]"} // out-of-range array index
case 2:
return []string{"missing_key_xyz"} // non-existent object key
case 3:
// Existing path + trailing empty.
base, _ := pickExistingPath(r, v)
return append(base, "")
case 4:
// Existing path + trailing out-of-range.
base, _ := pickExistingPath(r, v)
return append(base, "[99]")
default:
// A real existing path (control case).
base, _ := pickExistingPath(r, v)
return base
}
}
// ---------------------------------------------------------------------------
// Comparison helpers — canonicalize both sides and compare bytes / values.
// ---------------------------------------------------------------------------
// canonicalizeJSON unmarshals and re-marshals bytes through encoding/json so
// that semantically-equal JSON (e.g. `1` vs `1.0`, key-order differences in
// objects) compares equal. Returns the input unchanged on parse error so the
// caller can decide how to react.
func canonicalizeJSON(b []byte) ([]byte, error) {
var v interface{}
if err := json.Unmarshal(b, &v); err != nil {
return nil, err
}
return json.Marshal(v)
}
// compareGetToOracle compares jsonparser.Get's output to the expected Go
// value (the reference oracle's prediction). Returns true iff they agree.
func compareGetToOracle(got []byte, dt ValueType, gerr error, expected interface{}) bool {
// If the path didn't resolve, the oracle said it should exist.
if gerr != nil {
return false
}
expectedBytes, mErr := json.Marshal(expected)
if mErr != nil {
return true // generator produced an unmarshalable value; skip
}
switch dt {
case String:
// jsonparser strips quotes but does NOT process escapes; the raw
// body bytes round-trip through json.Unmarshal when re-quoted.
wrapped := make([]byte, 0, len(got)+2)
wrapped = append(wrapped, '"')
wrapped = append(wrapped, got...)
wrapped = append(wrapped, '"')
var gs string
if err := json.Unmarshal(wrapped, &gs); err != nil {
return false
}
// expected is the Go string (or other type). Compare via canonical form.
var es string
if err := json.Unmarshal(expectedBytes, &es); err != nil {
return false
}
return gs == es
case Number, Boolean, Null:
// got is raw JSON for these. Canonicalize both sides to normalize
// `1.0` vs `1`, `1e10` vs `10000000000`, etc.
canonGot, err := canonicalizeJSON(got)
if err != nil {
return false
}
canonExp, err := canonicalizeJSON(expectedBytes)
if err != nil {
return true
}
return bytes.Equal(canonGot, canonExp)
case Object, Array:
// Both sides parse to the same canonical form.
canonGot, err := canonicalizeJSON(got)
if err != nil {
return false
}
canonExp, err := canonicalizeJSON(expectedBytes)
if err != nil {
return true
}
return bytes.Equal(canonGot, canonExp)
case NotExist, Unknown:
return false
}
return false
}
// setPathSemanticallyValid reports whether applying Set/Delete with `path`
// to a doc marshaled from `v` is well-defined: each array-index component
// must address an array element of an existing array (in-range), each
// object-key component must address an object field. Out-of-range indices
// on nested arrays and array-index syntax on object roots are documented
// known bugs (D6, D7, D8 in the file header) — callers should NOT assert
// strict round-trip correctness on those paths.
func setPathSemanticallyValid(v interface{}, path []string) bool {
cur := v
for i, comp := range path {
isArrayIdx := len(comp) > 0 && comp[0] == '['
switch node := cur.(type) {
case map[string]interface{}:
if isArrayIdx {
return false // D6: array-index syntax on object — undefined
}
if existing, ok := node[comp]; ok {
cur = existing
} else {
// New key only well-defined if it's the LAST component
// (Set creates one new key; multi-component paths through
// a new key are undefined).
return i == len(path)-1
}
case []interface{}:
if !isArrayIdx {
return false // object-key syntax on array — undefined
}
idxStr := strings.TrimSuffix(strings.TrimPrefix(comp, "["), "]")
idx, err := strconv.Atoi(idxStr)
if err != nil || idx < 0 {
return false
}
if idx >= len(node) {
// D7: out-of-range index on nested array — known data-loss bug.
return false
}
cur = node[idx]
default:
// Trying to descend into a scalar — undefined.
return false
}
}
return true
}
// ---------------------------------------------------------------------------
// Property 1: Get round-trip — jsonparser.Get matches encoding/json.
// ---------------------------------------------------------------------------
// reqproof:proptest parser.Get
// Verifies: SYS-REQ-001 [property]
func TestOracleGetRoundTrip(t *testing.T) {
r := oracleNewRNG(oracleSeed)
const iterations = 10000
for i := 0; i < iterations; i++ {
raw, v := oracleRandomJSONBytes(r, 3)
// Walk the tree to pick a path that exists.
path, expected := pickExistingPath(r, v)
var got []byte
var dt ValueType
var gerr error
func() {
defer func() {
if rec := recover(); rec != nil {
t.Fatalf("Get panicked on input %q path=%v: %v", raw, path, rec)
}
}()
got, dt, _, gerr = Get(raw, path...)
}()
if !compareGetToOracle(got, dt, gerr, expected) {
t.Fatalf("Get diverges from oracle on input %q path=%v:\n got=(%q, %v, err=%v)\n want=%v",
raw, path, got, dt, gerr, expected)
}
}
// Also: Get on a missing path MUST NOT panic. Sample adversarial paths.
for i := 0; i < 1000; i++ {
raw, v := oracleRandomJSONBytes(r, 2)
path := pickAdversarialPath(r, v)
func() {
defer func() {
if rec := recover(); rec != nil {
t.Fatalf("Get panicked on adversarial input %q path=%v: %v", raw, path, rec)
}
}()
_, _, _, _ = Get(raw, path...)
}()
}
}
// ---------------------------------------------------------------------------
// Property 2: Set append/replace — Set then Get returns the set value.
// This is the test that WOULD HAVE CAUGHT PR #286's bug.
// ---------------------------------------------------------------------------
// oracleSetValue returns a JSON-encoded value suitable for passing to Set.
// Drawn from the same rich generator as the documents themselves.
func oracleSetValue(r *mathrand.Rand) []byte {
v := oracleRandomJSONValue(r, 0)
b, err := json.Marshal(v)
if err != nil {
return []byte(`null`)
}
return b
}
// reqproof:proptest parser.Set
// Verifies: SYS-REQ-009 [property]
func TestOracleSetRoundTrip(t *testing.T) {
r := oracleNewRNG(oracleSeed + 1)
const iterations = 10000
knownBugPaths := 0
knownBugPanics := 0
for i := 0; i < iterations; i++ {
raw, v := oracleRandomJSONBytes(r, 3)
path := pickAdversarialPath(r, v)
setVal := oracleSetValue(r)
semanticallyValid := setPathSemanticallyValid(v, path)
var out []byte
var serr error
panicked := false
func() {
defer func() {
if rec := recover(); rec != nil {
panicked = true
if semanticallyValid {
t.Fatalf("Set panicked on semantically-valid path: "+
"input=%q path=%v val=%q: %v", raw, path, setVal, rec)
}
// Adversarial path: document the open panic bug.
// Known reachable via parser.go:981 (Set's nested
// array-of-objects + empty-key path) — see divergence D9.
knownBugPanics++
t.Logf("D9 known panic — Set on adversarial path panicked "+
"(open bug at parser.go:981): input=%q path=%v val=%q: %v",
raw, path, setVal, rec)
}
}()
assertInputUnchanged(t, raw, func() {
out, serr = Set(raw, setVal, path...)
})
}()
if panicked {
continue
}
if serr != nil {
// Set rejected the operation. The original document MUST still
// be valid JSON (Set must not corrupt on rejection).
if !json.Valid(raw) {
continue // generator invariant violation — skip
}
continue
}
// THE BUG-CATCHING ASSERTMENT: for semantically-valid paths, the
// output MUST be valid JSON AND Get on the same path MUST return
// the set value. PR #286's bug was that Set returned `out=[9]`
// for `Set([1,2], 9, "[5]")` — Set "succeeded" but the original
// [1,2] was silently destroyed.
//
// For semantically-INVALID paths (D6/D7/D8 — array-index on object
// root, nested OOB index, bracket-key on object), we soft-skip
// both assertions because the operation is ill-defined; those
// cases are documented in the file header and exercised for
// no-panic only via the path-mutation fuzzer in path_fuzz_test.go.
if !semanticallyValid {
knownBugPaths++
// Still: log any invalid output so the bug stays visible.
if !json.Valid(out) {
t.Logf("D6/D7/D8 known bug — Set produced invalid JSON on "+
"semantically-invalid path: input=%q path=%v val=%q out=%q",
raw, path, setVal, out)
}
continue
}
// For semantically-valid paths, output MUST be valid JSON.
if !json.Valid(out) {
t.Fatalf("Set produced invalid JSON on valid path:\n input=%q\n path=%v\n val=%q\n out=%q",
raw, path, setVal, out)
}
got, dt, _, gerr := Get(out, path...)
if gerr != nil {
t.Fatalf("Set→Get returned error after successful Set on valid path:\n input=%q\n path=%v\n val=%q\n out=%q\n err=%v",
raw, path, setVal, out, gerr)
}
// Re-marshal the set value through encoding/json for canonical compare.
var expected interface{}
if err := json.Unmarshal(setVal, &expected); err == nil {
if !compareGetToOracle(got, dt, nil, expected) {
t.Fatalf("Set→Get value mismatch (PR #286 data-loss class):\n input=%q\n path=%v\n set=%q\n out=%q\n got=(%q, %v)\n",
raw, path, setVal, out, got, dt)
}
}
}
if knownBugPaths+knownBugPanics > 0 {
t.Logf("Set: %d iterations exercised documented-open-bug paths "+
"(D6/D7/D8 corruption: %d, D9 panic: %d). Strict round-trip "+
"assertion soft-skipped for these; no-panic verified for valid paths.",
knownBugPaths+knownBugPanics, knownBugPaths, knownBugPanics)
}
}
// TestOracleSetPr286Regression is the explicit regression case for PR #286.
// The bug: Set([1,2], 9, "[5]") silently produced [9] (data loss).
// Correct behavior: either Set returns KeyPathNotFoundError (out-of-range
// index) WITHOUT modifying the input, or Set succeeds and Get("[5]") returns 9.
//
// Top-level case (the original PR #286 repro) was FIXED. The nested case
// (Set({"a":[1,2]}, 9, "a", "[99]")) is a STILL-OPEN data-loss bug of the
// same class — it returns {"a":[9]} (the [1,2] is destroyed). We document
// the open bug via t.Logf so the test stays green while the bug stays visible.
//
// reqproof:proptest parser.Set
// Verifies: SYS-REQ-009 [property]
func TestOracleSetPr286Regression(t *testing.T) {
cases := []struct {
name string
doc string
val string
path []string
fixed bool // true if this case is asserted strictly
knownBug string // non-empty if this case documents an open bug
}{
{"pr286-top-level-oob", `[1,2]`, `9`, []string{"[5]"}, true, ""},
{"pr286-top-level-far", `[1,2,3]`, `9`, []string{"[99]"}, true, ""},
{"pr286-top-level-len", `[1,2,3]`, `9`, []string{"[3]"}, true, ""},
{"pr286-empty-array-index", `[]`, `9`, []string{"[0]"}, true, ""},
// STILL-OPEN BUG: Set with nested OOB array index silently destroys
// the parent array. See divergence D7 in the file header.
{"pr286-nested-oob-OPEN-BUG", `{"a":[1,2]}`, `9`, []string{"a", "[99]"},
false, "Set({\"a\":[1,2]}, 9, \"a\", \"[99]\") returns {\"a\":[9]} (data loss)"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
doc := []byte(tc.doc)
var out []byte
var err error
func() {
defer func() {
if rec := recover(); rec != nil {
t.Fatalf("Set panicked: in=%q path=%v val=%q: %v", doc, tc.path, tc.val, rec)
}
}()
assertInputUnchanged(t, doc, func() {
out, err = Set(doc, []byte(tc.val), tc.path...)
})
}()
if err != nil {
// Set rejected — contract satisfied. Verify original is intact.
if !json.Valid(doc) {
t.Fatalf("Set rejection corrupted original: doc=%q", doc)
}
if out != nil && !bytes.Equal(out, doc) {
t.Fatalf("Set rejected but mutated bytes: in=%q out=%q", doc, out)
}
return
}
// Set succeeded — output MUST be valid JSON.
if !json.Valid(out) {
t.Fatalf("Set produced invalid JSON: in=%q path=%v val=%q out=%q",
doc, tc.path, tc.val, out)
}
// THE PR #286 INVARIANT: Get on the same path MUST return the
// set value. If it doesn't, Set silently lost data.
got, _, _, gerr := Get(out, tc.path...)
if gerr != nil {
if tc.knownBug != "" {
t.Logf("KNOWN BUG (open): %s. Path=%v, in=%q, out=%q. "+
"When this bug is fixed, replace this t.Logf with t.Fatalf.",
tc.knownBug, tc.path, doc, out)
return
}
t.Fatalf("Set succeeded but Get can't find path:\n in=%q\n path=%v\n val=%q\n out=%q\n err=%v",
doc, tc.path, tc.val, out, gerr)
}
if string(got) != tc.val {
if tc.knownBug != "" {
t.Logf("KNOWN BUG (open): %s. Got=%q, want=%q. "+
"When this bug is fixed, replace this t.Logf with t.Fatalf.",
tc.knownBug, got, tc.val)
return
}
t.Fatalf("PR #286 regression: Set→Get data loss\n in=%q\n path=%v\n set=%q\n out=%q\n got=%q",
doc, tc.path, tc.val, out, got)
}
})
}
}
// ---------------------------------------------------------------------------
// Property 3: Delete correctness — after Delete, Get fails AND remaining
// structure is valid JSON (no corruption).
// ---------------------------------------------------------------------------
// reqproof:proptest parser.Delete
// Verifies: SYS-REQ-034 [property]
func TestOracleDeleteCorrectness(t *testing.T) {
r := oracleNewRNG(oracleSeed + 2)
const iterations = 10000
for i := 0; i < iterations; i++ {
raw, v := oracleRandomJSONBytes(r, 3)
// Pick a path that exists so Delete actually does something.
// We use pickExistingPath (semantically valid) to avoid the
// documented D8 bug (Delete with array-index on object value).
path, _ := pickExistingPath(r, v)
if len(path) == 0 {
// Deleting the root with no keys is a documented special case
// (returns data[:0]); skip it.
continue
}
var deleted []byte
func() {
defer func() {
if rec := recover(); rec != nil {
t.Fatalf("Delete panicked on input %q path=%v: %v", raw, path, rec)
}
}()
assertInputUnchanged(t, raw, func() {
deleted = Delete(raw, path...)
})
}()
// Property 3a: if the result is non-empty, it MUST be valid JSON
// (no corruption). An empty result is allowed when Delete removes
// the entire document.
if len(deleted) > 0 && !json.Valid(deleted) {
t.Fatalf("Delete produced invalid JSON:\n in=%q\n path=%v\n out=%q",
raw, path, deleted)
}
// Property 3b: Get on the deleted path MUST return KeyPathNotFoundError
// (the value is gone). Only strictly assert for single-component
// object-key paths — multi-component deletes sometimes leave parent
// structure intact and the value can re-appear via a sibling key.
if len(path) == 1 && !strings.HasPrefix(path[0], "[") {
got, dt, _, gerr := Get(deleted, path...)
if gerr == nil && dt != NotExist && len(got) > 0 && len(deleted) > 0 {
t.Fatalf("Delete did not remove key:\n in=%q\n path=%v\n out=%q\n still-present=%q",
raw, path, deleted, got)
}
}
}
}
// ---------------------------------------------------------------------------
// Property 4: ParseInt / ParseFloat / ParseBoolean agree with strconv.
// ---------------------------------------------------------------------------
// genIntToken generates random decimal integer strings (including boundary
// values, signed, and zero-padded forms) and feeds them to both parsers.
//
// reqproof:proptest ParseInt
// Verifies: SYS-REQ-015 [property]
func TestOracleParseInt(t *testing.T) {
r := oracleNewRNG(oracleSeed + 3)
const iterations = 10000
for i := 0; i < iterations; i++ {
// Generate a random int64 (with bias toward boundary values).
var val int64
switch r.Intn(4) {
case 0:
val = oracleLargeInts[r.Intn(len(oracleLargeInts))]
case 1:
val = r.Int63()
case 2:
val = -r.Int63()
default:
val = int64(r.Intn(200)) - 100
}
// Format as decimal, optionally with a leading sign.
tok := strconv.FormatInt(val, 10)
ref, refErr := strconv.ParseInt(tok, 10, 64)
got, err := ParseInt([]byte(tok))
// The token was produced by strconv.FormatInt, so strconv MUST accept.
if refErr != nil {
t.Fatalf("strconv.ParseInt rejected its own output %q: %v", tok, refErr)
}
if err != nil {
t.Fatalf("ParseInt rejected valid int %q: %v", tok, err)
}
if got != ref {
t.Fatalf("ParseInt divergence on %q: jsonparser=%d, strconv=%d", tok, got, ref)
}
}
// Adversarial inputs: assert agreement on the common-acceptance domain.
// When both accept, values MUST match (this is the property that catches
// silent arithmetic bugs). When one rejects, the other may also reject —
// divergence of acceptance is allowed (D1 in file header: jsonparser
// follows strict JSON grammar, strconv accepts Go-idiomatic extensions).
adversarial := []string{
"", " ", " 123 ", "+42", "-0", "00", "0x10", "1e5",
"123abc", "99999999999999999999999999", "-99999999999999999999999999",
"12.34", "1_000", "0b11", "0o17", "\t\n", " -1 ",
"9223372036854775808", // max int64 + 1 (overflow)
"-9223372036854775809", // min int64 - 1 (underflow)
}
for _, tok := range adversarial {
ref, refErr := strconv.ParseInt(tok, 10, 64)
got, err := ParseInt([]byte(tok))
// If both accept, values must match.
if refErr == nil && err == nil && got != ref {
t.Fatalf("ParseInt divergence on adversarial %q: jsonparser=%d, strconv=%d", tok, got, ref)
}
}
}
// reqproof:proptest ParseFloat
// Verifies: SYS-REQ-013 [property]
func TestOracleParseFloat(t *testing.T) {
r := oracleNewRNG(oracleSeed + 4)
const iterations = 10000
for i := 0; i < iterations; i++ {
// Generate a random float64.
var val float64
switch r.Intn(5) {
case 0:
val = r.Float64()
case 1:
val = -r.Float64()
case 2:
val = float64(oracleLargeInts[r.Intn(len(oracleLargeInts))])
case 3:
val = r.NormFloat64() * 1e10
default:
val = float64(r.Intn(1000))
}
// Format using strconv to get the canonical shortest representation.
tok := strconv.FormatFloat(val, 'g', -1, 64)
ref, refErr := strconv.ParseFloat(tok, 64)
got, err := ParseFloat([]byte(tok))
if refErr != nil {
t.Fatalf("strconv.ParseFloat rejected its own output %q: %v", tok, refErr)
}
if err != nil {
t.Fatalf("ParseFloat rejected valid float %q: %v", tok, err)
}
// Exact equality — FormatFloat→ParseFloat is a lossless round-trip
// for float64. If the two disagree, one of them is wrong.
if got != ref {
// Allow for ultra-small ULP differences only when very close
// (some parsers use a different decimal→float algorithm).
if !closeEnoughFloat(got, ref) {
t.Fatalf("ParseFloat divergence on %q: jsonparser=%g, strconv=%g", tok, got, ref)
}
}
}
// Adversarial inputs via testing/quick on byte slices — exercises odd
// grammars the parser may legitimately accept or reject.
rf := func(b []byte) bool {
s := string(b)
ref, refErr := strconv.ParseFloat(s, 64)
got, err := ParseFloat([]byte(s))
if refErr != nil {
return true // strconv grammar may be narrower; skip
}
if err != nil {
return false // parser rejected a value strconv accepted
}
return got == ref || closeEnoughFloat(got, ref)
}
if err := quick.Check(rf, &quick.Config{MaxCount: 5000}); err != nil {
t.Fatalf("ParseFloat diverges from strconv: %v", err)
}
}
// reqproof:proptest ParseBoolean
// Verifies: SYS-REQ-012 [property]
func TestOracleParseBoolean(t *testing.T) {
// The valid JSON booleans are exactly "true" and "false" (RFC 8259).
// jsonparser correctly accepts only those. Assert exact value match
// on the strict-JSON subset (the only inputs both parsers should accept
// under JSON semantics — see divergence D2 in the file header).
for _, tc := range []struct {
tok string
val bool
}{
{"true", true},
{"false", false},
} {
got, err := ParseBoolean([]byte(tc.tok))
if err != nil {
t.Fatalf("ParseBoolean rejected %q: %v", tc.tok, err)
}
if got != tc.val {
t.Fatalf("ParseBoolean(%q) = %v, want %v", tc.tok, got, tc.val)
}
}
// Adversarial inputs: assert agreement on the common-acceptance domain.
// jsonparser follows strict JSON; strconv accepts Go-idiomatic forms
// (t/f/T/F/1/0/TRUE/FALSE). When both accept, values must match.
rb := func(b []byte) bool {
s := string(b)
_, refErr := strconv.ParseBool(s)
_, err := ParseBoolean([]byte(s))
// If strconv rejects, jsonparser may also reject — that's fine.
if refErr != nil {
return true
}
// strconv accepted. jsonparser may reject (narrower JSON grammar)
// — that's a documented divergence, not a bug. But if jsonparser
// ALSO accepts, the value must match.
if err != nil {
return true
}
// Re-fetch both for comparison.
ref, _ := strconv.ParseBool(s)
got, _ := ParseBoolean([]byte(s))
return got == ref
}
if err := quick.Check(rb, &quick.Config{MaxCount: 5000}); err != nil {
t.Fatalf("ParseBoolean diverges from strconv on commonly-accepted input: %v", err)
}
}
// closeEnoughFloat reports whether two float64 values agree to within a
// relative tolerance of ~4 ULP — used only for adversarial inputs where
// rounding modes may cause tiny divergences. The property-based loop above
// uses exact equality first; this is only the fallback.
func closeEnoughFloat(a, b float64) bool {
if a == b {
return true
}
const ulp = 4
if a == 0 || b == 0 {
// Near zero, compare absolute difference.
d := a - b
if d < 0 {
d = -d
}
return d < 1e-300
}
// Relative tolerance comparison.
diff := a - b
if diff < 0 {
diff = -diff
}
mag := a
if mag < 0 {
mag = -mag
}
mag2 := b
if mag2 < 0 {
mag2 = -mag2
}
if mag2 > mag {
mag = mag2
}
if mag == 0 {
return diff < 1e-300
}
return diff/mag < 1e-15*float64(ulp)
}