199 lines
5.2 KiB
Go
199 lines
5.2 KiB
Go
package engine
|
|
|
|
import (
|
|
"strconv"
|
|
"testing"
|
|
)
|
|
|
|
func TestPathRoundTripsSemanticKeys(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
pointer string
|
|
}{
|
|
{name: "root", pointer: ""},
|
|
{name: "slash and tilde", pointer: "/a~1b/c~0d"},
|
|
{name: "empty key", pointer: "/"},
|
|
{name: "dotted key", pointer: "/outputs/a.b"},
|
|
{name: "numeric key", pointer: "/outputs/0"},
|
|
{name: "dot keys", pointer: "/./.."},
|
|
{name: "percent text", pointer: "/%2F/%7E"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
parsed, err := ParsePath(test.pointer)
|
|
if err != nil {
|
|
t.Fatalf("ParsePath() error = %v", err)
|
|
}
|
|
if got := parsed.String(); got != test.pointer {
|
|
t.Fatalf("String() = %q, want %q", got, test.pointer)
|
|
}
|
|
})
|
|
}
|
|
|
|
if got := Keys("a/b", "c~d", "", "..").String(); got != "/a~1b/c~0d//.." {
|
|
t.Fatalf("Keys().String() = %q", got)
|
|
}
|
|
if got := Keys("items").Index(12).Key("name").String(); got != "/items/12/name" {
|
|
t.Fatalf("indexed path = %q", got)
|
|
}
|
|
maximum := int(^uint(0) >> 1)
|
|
for _, index := range []int{100, maximum, -1, -maximum - 1} {
|
|
want := "/items/" + strconv.Itoa(index)
|
|
if got := Keys("items").Index(index).String(); got != want {
|
|
t.Fatalf("indexed path = %q, want %q", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPathContainmentMatchesPointerSpelling(t *testing.T) {
|
|
parse := func(pointer string) Path {
|
|
path, err := ParsePath(pointer)
|
|
if err != nil {
|
|
t.Fatalf("ParsePath(%q) error = %v", pointer, err)
|
|
}
|
|
return path
|
|
}
|
|
maximum := int(^uint(0) >> 1)
|
|
minimum := -maximum - 1
|
|
paths := []Path{
|
|
{},
|
|
Keys(""),
|
|
Keys("a"),
|
|
Keys("ab"),
|
|
Keys("a", "b"),
|
|
Keys("a/b"),
|
|
Keys("a~1b"),
|
|
Keys("~"),
|
|
Keys("0"),
|
|
Keys("01"),
|
|
Keys("+1"),
|
|
Keys("items").Index(0),
|
|
Keys("items").Index(maximum),
|
|
Keys("items").Index(minimum),
|
|
parse("/"),
|
|
parse("/a~1b"),
|
|
parse("/a~01b"),
|
|
parse("/~0"),
|
|
parse("/0"),
|
|
parse("/01"),
|
|
parse("/+1"),
|
|
parse("/items/0"),
|
|
parse("/items/" + strconv.Itoa(maximum)),
|
|
parse("/items/" + strconv.Itoa(minimum)),
|
|
Path{}.Index(-1),
|
|
}
|
|
for _, container := range paths {
|
|
for _, descendant := range paths {
|
|
got := pathContains(container, descendant)
|
|
want := pathContainsByPointer(container, descendant)
|
|
if got != want {
|
|
t.Errorf("pathContains(%q, %q) = %v, want %v", container.String(), descendant.String(), got, want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAppendUniquePathMatchesPointerSpelling(t *testing.T) {
|
|
parse := func(pointer string) Path {
|
|
path, err := ParsePath(pointer)
|
|
if err != nil {
|
|
t.Fatalf("ParsePath(%q) error = %v", pointer, err)
|
|
}
|
|
return path
|
|
}
|
|
paths := []Path{
|
|
Keys("a/b"), parse("/a~1b"),
|
|
Keys("a~1b"), parse("/a~01b"),
|
|
Keys("items").Index(0), parse("/items/0"), Keys("items", "0"),
|
|
Keys("items", "01"), parse("/items/01"),
|
|
Path{}.Index(-1), Keys("-1"),
|
|
Keys("a", "b"), Keys("a/b"),
|
|
}
|
|
var got, want []Path
|
|
for _, path := range paths {
|
|
got = appendUniquePath(got, path)
|
|
want = appendUniquePathByPointer(want, path)
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("unique path counts = %d, want %d", len(got), len(want))
|
|
}
|
|
for i := range got {
|
|
if got[i].String() != want[i].String() {
|
|
t.Fatalf("unique path %d = %q, want %q", i, got[i].String(), want[i].String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func pathContainsByPointer(container, descendant Path) bool {
|
|
if !container.valid() || !descendant.valid() || len(container.segments) > len(descendant.segments) {
|
|
return false
|
|
}
|
|
prefix := container.String()
|
|
if prefix == "" {
|
|
return true
|
|
}
|
|
value := descendant.String()
|
|
return value == prefix || len(value) > len(prefix) && value[:len(prefix)] == prefix && value[len(prefix)] == '/'
|
|
}
|
|
|
|
func appendUniquePathByPointer(paths []Path, path Path) []Path {
|
|
for _, existing := range paths {
|
|
if existing.String() == path.String() {
|
|
return paths
|
|
}
|
|
}
|
|
return append(paths, path.clone())
|
|
}
|
|
|
|
func TestPathPreservesSegmentIntentAndImmutability(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
key := Keys("items", "0")
|
|
index := Keys("items").Index(0)
|
|
parsed, err := ParsePath("/items/0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if key.String() != index.String() || index.String() != parsed.String() {
|
|
t.Fatal("equivalent textual paths should share a JSON Pointer spelling")
|
|
}
|
|
if key.segments[1].kind != pathSegmentKey {
|
|
t.Fatal("Keys numeric segment lost mapping-key intent")
|
|
}
|
|
if index.segments[1].kind != pathSegmentIndex {
|
|
t.Fatal("Index segment lost sequence intent")
|
|
}
|
|
if parsed.segments[1].kind != pathSegmentParsed {
|
|
t.Fatal("parsed numeric segment was resolved before traversal")
|
|
}
|
|
|
|
base := Keys("parent")
|
|
child := base.Key("child")
|
|
child.segments[0].key = "changed"
|
|
if got := base.String(); got != "/parent" {
|
|
t.Fatalf("builder mutated parent: %q", got)
|
|
}
|
|
|
|
negative := Path{}.Index(-1)
|
|
if negative.valid() {
|
|
t.Fatal("negative index must produce an invalid path")
|
|
}
|
|
}
|
|
|
|
func TestParsePathRejectsMalformedPointers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, pointer := range []string{"relative", "~0", "/bad~", "/bad~2", "/bad~01~"} {
|
|
if _, err := ParsePath(pointer); err == nil {
|
|
t.Errorf("ParsePath(%q) succeeded", pointer)
|
|
}
|
|
}
|
|
|
|
if parsed, err := ParsePath("/missing"); err != nil || !parsed.valid() {
|
|
t.Fatalf("valid missing path rejected: path=%v error=%v", parsed, err)
|
|
}
|
|
}
|