// Copyright 2020 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build goexperiment.jsonv2 package jsontext import ( "fmt" "slices" "strings" "testing" "unicode/utf8" ) func TestPointer(t *testing.T) { tests := []struct { in Pointer wantParent Pointer wantLast string wantTokens []string wantValid bool }{ {"", "", "", nil, true}, {"a", "", "a", []string{"a"}, false}, {"~", "", "~", []string{"~"}, false}, {"/a", "", "a", []string{"a"}, true}, {"/foo/bar", "/foo", "bar", []string{"foo", "bar"}, true}, {"///", "//", "", []string{"", "", ""}, true}, {"/~0~1", "", "~/", []string{"~/"}, true}, {"/\xde\xad\xbe\xef", "", "\xde\xad\xbe\xef", []string{"\xde\xad\xbe\xef"}, false}, } for _, tt := range tests { if got := tt.in.Parent(); got != tt.wantParent { t.Errorf("Pointer(%q).Parent = %q, want %q", tt.in, got, tt.wantParent) } if got := tt.in.LastToken(); got != tt.wantLast { t.Errorf("Pointer(%q).Last = %q, want %q", tt.in, got, tt.wantLast) } if strings.HasPrefix(string(tt.in), "/") { wantRoundtrip := tt.in if !utf8.ValidString(string(wantRoundtrip)) { // Replace bytes of invalid UTF-8 with Unicode replacement character. wantRoundtrip = Pointer([]rune(wantRoundtrip)) } if got := tt.in.Parent().AppendToken(tt.in.LastToken()); got != wantRoundtrip { t.Errorf("Pointer(%q).Parent().AppendToken(LastToken()) = %q, want %q", tt.in, got, tt.in) } in := tt.in for { if (in + "x").Contains(tt.in) { t.Errorf("Pointer(%q).Contains(%q) = true, want false", in+"x", tt.in) } if !in.Contains(tt.in) { t.Errorf("Pointer(%q).Contains(%q) = false, want true", in, tt.in) } if in == in.Parent() { break } in = in.Parent() } } if got := slices.Collect(tt.in.Tokens()); !slices.Equal(got, tt.wantTokens) { t.Errorf("Pointer(%q).Tokens = %q, want %q", tt.in, got, tt.wantTokens) } if got := tt.in.IsValid(); got != tt.wantValid { t.Errorf("Pointer(%q).IsValid = %v, want %v", tt.in, got, tt.wantValid) } } } func TestStateMachine(t *testing.T) { // To test a state machine, we pass an ordered sequence of operations and // check whether the current state is as expected. // The operation type is a union type of various possible operations, // which either call mutating methods on the state machine or // call accessor methods on state machine and verify the results. type operation any type ( // stackLengths checks the results of stateEntry.length accessors. stackLengths []int64 // appendTokens is sequence of token kinds to append where // none of them are expected to fail. // // For example: `[nft]` is equivalent to the following sequence: // // pushArray() // appendLiteral() // appendString() // appendNumber() // popArray() // appendTokens string // appendToken is a single token kind to append with the expected error. appendToken struct { kind Kind want error } // needDelim checks the result of the needDelim accessor. needDelim struct { next Kind want byte } ) // Each entry is a sequence of tokens to pass to the state machine. tests := []struct { label string ops []operation }{{ "TopLevelValues", []operation{ stackLengths{0}, needDelim{'n', 0}, appendTokens(`nft`), stackLengths{3}, needDelim{'"', 0}, appendTokens(`"0[]{}`), stackLengths{7}, }, }, { "ArrayValues", []operation{ stackLengths{0}, needDelim{'[', 0}, appendTokens(`[`), stackLengths{1, 0}, needDelim{'n', 0}, appendTokens(`nft`), stackLengths{1, 3}, needDelim{'"', ','}, appendTokens(`"0[]{}`), stackLengths{1, 7}, needDelim{']', 0}, appendTokens(`]`), stackLengths{1}, }, }, { "ObjectValues", []operation{ stackLengths{0}, needDelim{'{', 0}, appendTokens(`{`), stackLengths{1, 0}, needDelim{'"', 0}, appendTokens(`"`), stackLengths{1, 1}, needDelim{'n', ':'}, appendTokens(`n`), stackLengths{1, 2}, needDelim{'"', ','}, appendTokens(`"f"t`), stackLengths{1, 6}, appendTokens(`"""0"[]"{}`), stackLengths{1, 14}, needDelim{'}', 0}, appendTokens(`}`), stackLengths{1}, }, }, { "ObjectCardinality", []operation{ appendTokens(`{`), // Appending any kind other than string for object name is an error. appendToken{'n', ErrNonStringName}, appendToken{'f', ErrNonStringName}, appendToken{'t', ErrNonStringName}, appendToken{'0', ErrNonStringName}, appendToken{'{', ErrNonStringName}, appendToken{'[', ErrNonStringName}, appendTokens(`"`), // Appending '}' without first appending any value is an error. appendToken{'}', errMissingValue}, appendTokens(`"`), appendTokens(`}`), }, }, { "MismatchingDelims", []operation{ appendToken{'}', errMismatchDelim}, // appending '}' without preceding '{' appendTokens(`[[{`), appendToken{']', errMismatchDelim}, // appending ']' that mismatches preceding '{' appendTokens(`}]`), appendToken{'}', errMismatchDelim}, // appending '}' that mismatches preceding '[' appendTokens(`]`), appendToken{']', errMismatchDelim}, // appending ']' without preceding '[' }, }} for _, tt := range tests { t.Run(tt.label, func(t *testing.T) { // Flatten appendTokens to sequence of appendToken entries. var ops []operation for _, op := range tt.ops { if toks, ok := op.(appendTokens); ok { for _, k := range []byte(toks) { ops = append(ops, appendToken{Kind(k), nil}) } continue } ops = append(ops, op) } // Append each token to the state machine and check the output. var state stateMachine state.reset() var sequence []Kind for _, op := range ops { switch op := op.(type) { case stackLengths: var got []int64 for i := range state.Depth() { e := state.index(i) got = append(got, e.Length()) } want := []int64(op) if !slices.Equal(got, want) { t.Fatalf("%s: stack lengths mismatch:\ngot %v\nwant %v", sequence, got, want) } case appendToken: got := state.append(op.kind) if !equalError(got, op.want) { t.Fatalf("%s: append('%c') = %v, want %v", sequence, op.kind, got, op.want) } if got == nil { sequence = append(sequence, op.kind) } case needDelim: if got := state.needDelim(op.next); got != op.want { t.Fatalf("%s: needDelim('%c') = '%c', want '%c'", sequence, op.next, got, op.want) } default: panic(fmt.Sprintf("unknown operation: %T", op)) } } }) } } // append is a thin wrapper over the other append, pop, or push methods // based on the token kind. func (s *stateMachine) append(k Kind) error { switch k { case 'n', 'f', 't': return s.appendLiteral() case '"': return s.appendString() case '0': return s.appendNumber() case '{': return s.pushObject() case '}': return s.popObject() case '[': return s.pushArray() case ']': return s.popArray() default: panic(fmt.Sprintf("invalid token kind: '%c'", k)) } } func TestObjectNamespace(t *testing.T) { type operation any type ( insert struct { name string wantInserted bool } removeLast struct{} ) // Sequence of insert operations to perform (order matters). ops := []operation{ insert{`""`, true}, removeLast{}, insert{`""`, true}, insert{`""`, false}, // Test insertion of the same name with different formatting. insert{`"alpha"`, true}, insert{`"ALPHA"`, true}, // case-sensitive matching insert{`"alpha"`, false}, insert{`"\u0061\u006c\u0070\u0068\u0061"`, false}, // unescapes to "alpha" removeLast{}, // removes "ALPHA" insert{`"alpha"`, false}, removeLast{}, // removes "alpha" insert{`"alpha"`, true}, removeLast{}, // Bulk insert simple names. insert{`"alpha"`, true}, insert{`"bravo"`, true}, insert{`"charlie"`, true}, insert{`"delta"`, true}, insert{`"echo"`, true}, insert{`"foxtrot"`, true}, insert{`"golf"`, true}, insert{`"hotel"`, true}, insert{`"india"`, true}, insert{`"juliet"`, true}, insert{`"kilo"`, true}, insert{`"lima"`, true}, insert{`"mike"`, true}, insert{`"november"`, true}, insert{`"oscar"`, true}, insert{`"papa"`, true}, insert{`"quebec"`, true}, insert{`"romeo"`, true}, insert{`"sierra"`, true}, insert{`"tango"`, true}, insert{`"uniform"`, true}, insert{`"victor"`, true}, insert{`"whiskey"`, true}, insert{`"xray"`, true}, insert{`"yankee"`, true}, insert{`"zulu"`, true}, // Test insertion of invalid UTF-8. insert{`"` + "\ufffd" + `"`, true}, insert{`"` + "\ufffd" + `"`, false}, insert{`"\ufffd"`, false}, // unescapes to Unicode replacement character insert{`"\uFFFD"`, false}, // unescapes to Unicode replacement character insert{`"` + "\xff" + `"`, false}, // mangles as Unicode replacement character removeLast{}, insert{`"` + "\ufffd" + `"`, true}, // Test insertion of unicode characters. insert{`"☺☻☹"`, true}, insert{`"☺☻☹"`, false}, removeLast{}, insert{`"☺☻☹"`, true}, } // Execute the sequence of operations twice: // 1) on a fresh namespace and 2) on a namespace that has been reset. var ns objectNamespace wantNames := []string{} for _, reset := range []bool{false, true} { if reset { ns.reset() wantNames = nil } // Execute the operations and ensure the state is consistent. for i, op := range ops { switch op := op.(type) { case insert: gotInserted := ns.insertQuoted([]byte(op.name), false) if gotInserted != op.wantInserted { t.Fatalf("%d: objectNamespace{%v}.insert(%v) = %v, want %v", i, strings.Join(wantNames, " "), op.name, gotInserted, op.wantInserted) } if gotInserted { b, _ := AppendUnquote(nil, []byte(op.name)) wantNames = append(wantNames, string(b)) } case removeLast: ns.removeLast() wantNames = wantNames[:len(wantNames)-1] default: panic(fmt.Sprintf("unknown operation: %T", op)) } // Check that the namespace is consistent. gotNames := []string{} for i := range ns.length() { gotNames = append(gotNames, string(ns.getUnquoted(i))) } if !slices.Equal(gotNames, wantNames) { t.Fatalf("%d: objectNamespace = {%v}, want {%v}", i, strings.Join(gotNames, " "), strings.Join(wantNames, " ")) } } // Verify that we have not switched to using a Go map. if ns.mapNames != nil { t.Errorf("objectNamespace.mapNames = non-nil, want nil") } // Insert a large number of names. for i := range 64 { ns.InsertUnquoted([]byte(fmt.Sprintf(`name%d`, i))) } // Verify that we did switch to using a Go map. if ns.mapNames == nil { t.Errorf("objectNamespace.mapNames = nil, want non-nil") } } }