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