You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
396 lines
11 KiB
396 lines
11 KiB
// 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") |
|
} |
|
} |
|
}
|
|
|