| // 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") | |
| } | |
| } | |
| } |