package modules
import (
"html/template"
"strings"
"testing"
)
// lines builds the Line slice annotateStructure expects, mirroring what
// blobMod produces before the structure pass runs.
func lines(raw string) []Line {
out := []Line{}
for i, t := range strings.Split(raw, "\n") {
out = append(out, Line{N: i + 1, Text: template.HTML(t)})
}
return out
}
const sampleYAML = `defaults: &defaults
adapter: postgres
pool: 5
development:
<<: *defaults
database: dev
test:
<<: *defaults
database: test
hosts:
- a
- b
`
func TestFoldRangesYAML(t *testing.T) {
ls := lines(sampleYAML)
annotateStructure(ls, []byte(sampleYAML), "yaml")
want := map[int]int{
1: 3, // defaults: block ends at "pool: 5", not the blank line after
5: 7, // development:
9: 14, // test:, stopping short of the empty line 15
12: 14, // hosts:
}
for _, l := range ls {
if got, ok := want[l.N]; ok {
if l.FoldTo != got {
t.Errorf("line %d: FoldTo = %d, want %d", l.N, l.FoldTo, got)
}
} else if l.FoldTo != 0 {
t.Errorf("line %d: FoldTo = %d, want none", l.N, l.FoldTo)
}
}
if ls[1].Depth != 1 {
t.Errorf("line 2 depth = %d, want 1", ls[1].Depth)
}
if ls[13].Depth != 2 {
t.Errorf("line 14 depth = %d, want 2", ls[13].Depth)
}
}
func TestKeysAndAnchors(t *testing.T) {
ls := lines(sampleYAML)
annotateStructure(ls, []byte(sampleYAML), "yaml")
if ls[0].Key != "defaults" {
t.Errorf("line 1 key = %q, want %q", ls[0].Key, "defaults")
}
if ls[13].Key != "" {
t.Errorf("line 14 (sequence item) key = %q, want none", ls[13].Key)
}
// The anchor is reported on the line the &token is written on, not on
// the line the mapping it names begins.
a := ls[0].Anchor
if a == nil {
t.Fatal("line 1: no anchor")
}
if a.Name != "defaults" || a.Uses != 2 {
t.Errorf("anchor = %+v, want {defaults 2}", *a)
}
for _, n := range []int{6, 10} {
r := ls[n-1].Ref
if r == nil {
t.Fatalf("line %d: no ref", n)
}
if r.Name != "defaults" || r.Line != 1 {
t.Errorf("line %d ref = %+v, want defaults@1", n, *r)
}
if !strings.Contains(r.Preview, "adapter: postgres") {
t.Errorf("line %d preview = %q, want the resolved mapping", n, r.Preview)
}
}
}
// Anchors defined in the second document of a stream are still resolved:
// yaml.Unmarshal would only ever see the first.
func TestMultiDocumentAnchors(t *testing.T) {
const src = `---
kind: First
---
base: &b
x: 1
copy: *b
`
ls := lines(src)
annotateStructure(ls, []byte(src), "yaml")
if ls[3].Anchor == nil || ls[3].Anchor.Name != "b" {
t.Fatalf("line 4 anchor = %+v, want b", ls[3].Anchor)
}
if r := ls[5].Ref; r == nil || r.Line != 4 {
t.Fatalf("line 6 ref = %+v, want b@4", r)
}
}
func TestFoldRangesJSON(t *testing.T) {
const src = `{
"name": "gsg",
"deps": {
"a": 1
},
"list": [
2
]
}`
ls := lines(src)
annotateStructure(ls, []byte(src), "json")
if ls[0].FoldTo != 8 {
t.Errorf("line 1 FoldTo = %d, want 8", ls[0].FoldTo)
}
if ls[2].FoldTo != 4 {
t.Errorf("line 3 FoldTo = %d, want 4", ls[2].FoldTo)
}
if ls[5].FoldTo != 7 {
t.Errorf("line 6 FoldTo = %d, want 7", ls[5].FoldTo)
}
if ls[1].Key != "name" {
t.Errorf("line 2 key = %q, want %q", ls[1].Key, "name")
}
// No anchors outside YAML, and a brace inside a string is not structure.
if ls[1].Anchor != nil || ls[1].Ref != nil {
t.Error("JSON line carries anchor/ref data")
}
}
// Minified JSON has no indentation to fold on; it must degrade to a plain
// view rather than producing bogus ranges.
func TestMinifiedJSONHasNoFolds(t *testing.T) {
const src = `{"a":{"b":[1,2]},"c":3}`
ls := lines(src)
annotateStructure(ls, []byte(src), "json")
if ls[0].FoldTo != 0 {
t.Errorf("FoldTo = %d, want 0", ls[0].FoldTo)
}
}
// Nested arrays of objects are the shape a path query like
// obj1.property3.items[4].val is written against.
func TestJSONPaths(t *testing.T) {
const src = `{
"obj1": {
"property3": {
"items": [
{
"val": 0
},
{
"val": 1
}
]
}
}
}`
ls := lines(src)
annotateStructure(ls, []byte(src), "json")
want := map[int]string{
2: "obj1",
3: "obj1.property3",
4: "obj1.property3.items",
5: "obj1.property3.items[0]",
6: "obj1.property3.items[0].val",
8: "obj1.property3.items[1]",
9: "obj1.property3.items[1].val",
}
for _, l := range ls {
if got, ok := want[l.N]; ok {
if l.Path != got {
t.Errorf("line %d Path = %q, want %q", l.N, l.Path, got)
}
} else if l.Path != "" {
t.Errorf("line %d Path = %q, want none", l.N, l.Path)
}
}
}
// A flow-style row carries several paths at once; the deepest is kept, so
// a query naming the final member still resolves. The shallow ones stay
// reachable when a container query prefix-matches the deep path.
func TestJSONFlowPathKeepsDeepest(t *testing.T) {
const src = `{"a": {"b": 1}}`
ls := lines(src)
annotateStructure(ls, []byte(src), "json")
if ls[0].Path != "a.b" {
t.Errorf("Path = %q, want a.b", ls[0].Path)
}
}
func TestYAMLPaths(t *testing.T) {
ls := lines(sampleYAML)
annotateStructure(ls, []byte(sampleYAML), "yaml")
want := map[int]string{
1: "defaults",
2: "defaults.adapter",
3: "defaults.pool",
5: "development",
6: "development", // merge key keeps the parent path
7: "development.database",
9: "test",
10: "test",
11: "test.database",
12: "test.hosts",
13: "test.hosts[0]",
14: "test.hosts[1]",
}
for _, l := range ls {
if got, ok := want[l.N]; ok {
if l.Path != got {
t.Errorf("line %d Path = %q, want %q", l.N, l.Path, got)
}
} else if l.Path != "" {
t.Errorf("line %d Path = %q, want none", l.N, l.Path)
}
}
}
func TestYAMLSequenceOfMappingsPaths(t *testing.T) {
const src = `obj1:
property3:
items:
- val: 0
- val: 1
`
ls := lines(src)
annotateStructure(ls, []byte(src), "yaml")
if ls[3].Path != "obj1.property3.items[0].val" {
t.Errorf("line 4 Path = %q", ls[3].Path)
}
if ls[4].Path != "obj1.property3.items[1].val" {
t.Errorf("line 5 Path = %q", ls[4].Path)
}
}
func TestPathKeyQuoting(t *testing.T) {
tests := []struct{ parent, key, want string }{
{"", "obj1", "obj1"},
{"obj1", "items", "obj1.items"},
{"obj1", "a.b", `obj1["a.b"]`},
{"", "weird key", `["weird key"]`},
{"obj1", `quo"te`, `obj1["quo\"te"]`},
}
for _, tc := range tests {
if got := joinPath(tc.parent, tc.key); got != tc.want {
t.Errorf("joinPath(%q, %q) = %q, want %q", tc.parent, tc.key, got, tc.want)
}
}
if got := joinIndex("obj1.items", 4); got != "obj1.items[4]" {
t.Errorf("joinIndex = %q", got)
}
}