d2/d2oracle/edit_test.go
2025-04-30 11:21:07 -06:00

9840 lines
133 KiB
Go

package d2oracle_test
import (
"fmt"
"path/filepath"
"strconv"
"strings"
"testing"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/diff"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/util-go/mapfs"
"oss.terrastruct.com/util-go/xjson"
"oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2oracle"
"oss.terrastruct.com/d2/d2target"
)
// TODO: make assertions less specific
// TODO: move n objects and n edges assertions as fields on test instead of as callback
func TestCreate(t *testing.T) {
t.Parallel()
testCases := []struct {
boardPath []string
name string
text string
fsTexts map[string]string
key string
expKey string
expErr string
exp string
assertions func(t *testing.T, g *d2graph.Graph)
}{
{
name: "base",
text: ``,
key: `square`,
expKey: `square`,
exp: `square
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 1 {
t.Fatalf("expected 1 objects: %#v", g.Objects)
}
if g.Objects[0].ID != "square" {
t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0])
}
if g.Objects[0].Label.MapKey.Value.Unbox() != nil {
t.Fatalf("expected g.Objects[0].Label.Node.Value.Unbox() == nil: %#v", g.Objects[0].Label.MapKey.Value)
}
if d2format.Format(g.Objects[0].Label.MapKey.Key) != "square" {
t.Fatalf("expected g.Objects[0].Label.Node.Key to be square: %#v", g.Objects[0].Label.MapKey.Key)
}
},
},
{
name: "gen_key_suffix",
text: `"x "
`,
key: `"x "`,
expKey: `x 2`,
exp: `"x "
x 2
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 2 {
t.Fatalf("unexpected objects length: %#v", g.Objects)
}
if g.Objects[1].ID != `x 2` {
t.Fatalf("bad object ID: %#v", g.Objects[1])
}
},
},
{
name: "nested",
text: ``,
key: `b.c.square`,
expKey: `b.c.square`,
exp: `b.c.square
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 3 {
t.Fatalf("unexpected objects length: %#v", g.Objects)
}
if g.Objects[2].AbsID() != "b.c.square" {
t.Fatalf("bad absolute ID: %#v", g.Objects[2].AbsID())
}
if d2format.Format(g.Objects[2].Label.MapKey.Key) != "b.c.square" {
t.Fatalf("bad mapkey: %#v", g.Objects[2].Label.MapKey.Key)
}
if g.Objects[2].Label.MapKey.Value.Unbox() != nil {
t.Fatalf("expected nil mapkey value: %#v", g.Objects[2].Label.MapKey.Value)
}
},
},
{
name: "gen_key",
text: `square`,
key: `square`,
expKey: `square 2`,
exp: `square
square 2
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
if g.Objects[1].ID != "square 2" {
t.Fatalf("expected g.Objects[1].ID to be square 2: %#v", g.Objects[1])
}
if g.Objects[1].Label.MapKey.Value.Unbox() != nil {
t.Fatalf("expected g.Objects[1].Label.Node.Value.Unbox() == nil: %#v", g.Objects[1].Label.MapKey.Value)
}
if d2format.Format(g.Objects[1].Label.MapKey.Key) != "square 2" {
t.Fatalf("expected g.Objects[1].Label.Node.Key to be square 2: %#v", g.Objects[1].Label.MapKey.Key)
}
},
},
{
name: "gen_key_nested",
text: `x.y.z.square`,
key: `x.y.z.square`,
expKey: `x.y.z.square 2`,
exp: `x.y.z.square
x.y.z.square 2
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 5 {
t.Fatalf("unexpected objects length: %#v", g.Objects)
}
if g.Objects[4].ID != "square 2" {
t.Fatalf("unexpected object id: %#v", g.Objects[4])
}
},
},
{
name: "scope",
text: `x.y.z: {
}`,
key: `x.y.z.square`,
expKey: `x.y.z.square`,
exp: `x.y.z: {
square
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 4 {
t.Fatalf("expected 4 objects: %#v", g.Objects)
}
if g.Objects[3].ID != "square" {
t.Fatalf("expected g.Objects[3].ID to be square: %#v", g.Objects[3])
}
if g.Objects[3].Label.MapKey.Value.Unbox() != nil {
t.Fatalf("expected g.Objects[3].Label.Node.Value.Unbox() == nil: %#v", g.Objects[3].Label.MapKey.Value)
}
if d2format.Format(g.Objects[3].Label.MapKey.Key) != "square" {
t.Fatalf("expected g.Objects[3].Label.Node.Key to be square: %#v", g.Objects[3].Label.MapKey.Key)
}
},
},
{
name: "gen_key_scope",
text: `x.y.z: {
square
}`,
key: `x.y.z.square`,
expKey: `x.y.z.square 2`,
exp: `x.y.z: {
square
square 2
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 5 {
t.Fatalf("expected 5 objects: %#v", g.Objects)
}
if g.Objects[4].ID != "square 2" {
t.Fatalf("expected g.Objects[4].ID to be square 2: %#v", g.Objects[4])
}
if g.Objects[4].Label.MapKey.Value.Unbox() != nil {
t.Fatalf("expected g.Objects[4].Label.Node.Value.Unbox() == nil: %#v", g.Objects[4].Label.MapKey.Value)
}
if d2format.Format(g.Objects[4].Label.MapKey.Key) != "square 2" {
t.Fatalf("expected g.Objects[4].Label.Node.Key to be square 2: %#v", g.Objects[4].Label.MapKey.Key)
}
},
},
{
name: "gen_key_n",
text: `x.y.z: {
square
square 2
square 3
square 4
square 5
square 6
square 7
square 8
square 9
square 10
}`,
key: `x.y.z.square`,
expKey: `x.y.z.square 11`,
exp: `x.y.z: {
square
square 2
square 3
square 4
square 5
square 6
square 7
square 8
square 9
square 10
square 11
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 14 {
t.Fatalf("expected 14 objects: %#v", g.Objects)
}
if g.Objects[13].ID != "square 11" {
t.Fatalf("expected g.Objects[13].ID to be square 11: %#v", g.Objects[13])
}
if d2format.Format(g.Objects[13].Label.MapKey.Key) != "square 11" {
t.Fatalf("expected g.Objects[13].Label.Node.Key to be square 11: %#v", g.Objects[13].Label.MapKey.Key)
}
},
},
{
name: "edge",
text: ``,
key: `x -> y`,
expKey: `(x -> y)[0]`,
exp: `x -> y
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
if g.Edges[0].Src.ID != "x" {
t.Fatalf("expected g.Edges[0].Src.ID == x: %#v", g.Edges[0].Src.ID)
}
if g.Edges[0].Dst.ID != "y" {
t.Fatalf("expected g.Edges[0].Dst.ID == y: %#v", g.Edges[0].Dst.ID)
}
},
},
{
name: "edge_nested",
text: ``,
key: `container.(x -> y)`,
expKey: `container.(x -> y)[0]`,
exp: `container.(x -> y)
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 3 {
t.Fatalf("unexpected objects: %#v", g.Objects)
}
if len(g.Edges) != 1 {
t.Fatalf("unexpected edges: %#v", g.Edges)
}
},
},
{
name: "edge_scope",
text: `container: {
}`,
key: `container.(x -> y)`,
expKey: `container.(x -> y)[0]`,
exp: `container: {
x -> y
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 3 {
t.Fatalf("expected 3 objects: %#v", g.Objects)
}
},
},
{
name: "edge_scope_flat",
text: `container: {
}`,
key: `container.x -> container.y`,
expKey: `container.(x -> y)[0]`,
exp: `container: {
x -> y
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 3 {
t.Fatalf("expected 3 objects: %#v", g.Objects)
}
},
},
{
name: "edge_scope_nested",
text: `x.y`,
key: `x.y.z -> x.y.q`,
expKey: `x.y.(z -> q)[0]`,
exp: `x.y: {
z -> q
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 4 {
t.Fatalf("unexpected objects: %#v", g.Objects)
}
if len(g.Edges) != 1 {
t.Fatalf("unexpected edges: %#v", g.Edges)
}
},
},
{
name: "edge_unique",
text: `x -> y
hello.(x -> y)
hello.(x -> y)
`,
key: `hello.(x -> y)`,
expKey: `hello.(x -> y)[2]`,
exp: `x -> y
hello.(x -> y)
hello.(x -> y)
hello.(x -> y)
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 5 {
t.Fatalf("expected 5 objects: %#v", g.Objects)
}
if len(g.Edges) != 4 {
t.Fatalf("expected 4 edges: %#v", g.Edges)
}
},
},
{
name: "container",
text: `b`,
key: `b.q`,
expKey: `b.q`,
exp: `b: {
q
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
},
},
{
name: "container_edge",
text: `b`,
key: `b.x -> b.y`,
expKey: `b.(x -> y)[0]`,
exp: `b: {
x -> y
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 3 {
t.Fatalf("expected 3 objects: %#v", g.Objects)
}
},
},
{
name: "container_edge_label",
text: `b: zoom`,
key: `b.x -> b.y`,
expKey: `b.(x -> y)[0]`,
exp: `b: zoom {
x -> y
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 3 {
t.Fatalf("expected 3 objects: %#v", g.Objects)
}
},
},
{
name: "make_scope_multiline",
text: `rawr: {shape: circle}
`,
key: `rawr.orange`,
expKey: `rawr.orange`,
exp: `rawr: {
shape: circle
orange
}
`,
},
{
name: "make_scope_multiline_spacing_1",
text: `before
rawr: {shape: circle}
after
`,
key: `rawr.orange`,
expKey: `rawr.orange`,
exp: `before
rawr: {
shape: circle
orange
}
after
`,
},
{
name: "make_scope_multiline_spacing_2",
text: `before
rawr: {shape: circle}
after
`,
key: `rawr.orange`,
expKey: `rawr.orange`,
exp: `before
rawr: {
shape: circle
orange
}
after
`,
},
{
name: "layers-basic",
text: `a
layers: {
x: {
a
}
}
`,
key: `b`,
boardPath: []string{"x"},
expKey: `b`,
exp: `a
layers: {
x: {
a
b
}
}
`,
},
{
name: "add_layer/1",
text: `b`,
key: `layers.c`,
expKey: `layers.c`,
exp: `b
layers: {
c
}
`,
},
{
name: "add_layer/2",
text: `b
layers: {
c: {
x
}
}`,
key: `layers.b`,
expKey: `layers.b`,
exp: `b
layers: {
c: {
x
}
b
}
`,
},
{
name: "add_layer/3",
text: `b
layers: {
c: {
d
}
}
`,
key: `layers.c`,
boardPath: []string{"c"},
expKey: `layers.c`,
exp: `b
layers: {
c: {
d
layers: {
c
}
}
}
`,
},
{
name: "add_layer/4",
text: `b
layers: {
c
}
`,
key: `d`,
boardPath: []string{"c"},
expKey: `d`,
exp: `b
layers: {
c: {
d
}
}
`,
},
{
name: "add_layer/5",
text: `classes: {
a: {
style.stroke: red
}
}
b
layers: {
c
}
`,
key: `d`,
boardPath: []string{"c"},
expKey: `d`,
exp: `classes: {
a: {
style.stroke: red
}
}
b
layers: {
c: {
d
}
}
`,
},
{
name: "layers-edge",
text: `a
layers: {
x: {
a
}
}
`,
key: `a -> b`,
boardPath: []string{"x"},
expKey: `(a -> b)[0]`,
exp: `a
layers: {
x: {
a
a -> b
}
}
`,
},
{
name: "layers-edge-duplicate",
text: `a -> b
layers: {
x: {
a -> b
}
}
`,
key: `a -> b`,
boardPath: []string{"x"},
expKey: `(a -> b)[1]`,
exp: `a -> b
layers: {
x: {
a -> b
a -> b
}
}
`,
},
{
name: "scenarios-basic",
text: `a
b
scenarios: {
x: {
a
}
}
`,
key: `c`,
boardPath: []string{"x"},
expKey: `c`,
exp: `a
b
scenarios: {
x: {
a
c
}
}
`,
},
{
name: "scenarios-edge",
text: `a
b
scenarios: {
x: {
a
}
}
`,
key: `a -> b`,
boardPath: []string{"x"},
expKey: `(a -> b)[0]`,
exp: `a
b
scenarios: {
x: {
a
a -> b
}
}
`,
},
{
name: "scenarios-edge-inherited",
text: `a -> b
scenarios: {
x: {
a
}
}
`,
key: `a -> b`,
boardPath: []string{"x"},
expKey: `(a -> b)[1]`,
exp: `a -> b
scenarios: {
x: {
a
a -> b
}
}
`,
},
{
name: "steps-basic",
text: `a
d
steps: {
x: {
b
}
}
`,
key: `c`,
boardPath: []string{"x"},
expKey: `c`,
exp: `a
d
steps: {
x: {
b
c
}
}
`,
},
{
name: "steps-edge",
text: `a
d
steps: {
x: {
b
}
}
`,
key: `d -> b`,
boardPath: []string{"x"},
expKey: `(d -> b)[0]`,
exp: `a
d
steps: {
x: {
b
d -> b
}
}
`,
},
{
name: "steps-conflict",
text: `a
d
steps: {
x: {
b
}
}
`,
key: `d`,
boardPath: []string{"x"},
expKey: `d 2`,
exp: `a
d
steps: {
x: {
b
d 2
}
}
`,
},
{
name: "image-edge",
text: `...@k
a.b: {
icon: https://icons.terrastruct.com/essentials/004-picture.svg
shape: image
}
`,
fsTexts: map[string]string{
"k.d2": `
a: {
b
c
}
`,
},
key: `a.b -> a.c`,
boardPath: []string{},
expKey: `a.(b -> c)[0]`,
exp: `...@k
a.b: {
icon: https://icons.terrastruct.com/essentials/004-picture.svg
shape: image
}
a.(b -> c)
`,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var newKey string
et := editTest{
text: tc.text,
fsTexts: tc.fsTexts,
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
var err error
g, newKey, err = d2oracle.Create(g, tc.boardPath, tc.key)
return g, err
},
exp: tc.exp,
expErr: tc.expErr,
assertions: func(t *testing.T, g *d2graph.Graph) {
if newKey != tc.expKey {
t.Fatalf("expected %q but got %q", tc.expKey, newKey)
}
if tc.assertions != nil {
tc.assertions(t, g)
}
},
}
et.run(t)
})
}
}
func TestSet(t *testing.T) {
t.Parallel()
testCases := []struct {
boardPath []string
name string
text string
fsTexts map[string]string
key string
tag *string
value *string
expErr string
exp string
assertions func(t *testing.T, g *d2graph.Graph)
}{
{
name: "base",
text: ``,
key: `square`,
exp: `square
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 1 {
t.Fatalf("expected 1 objects: %#v", g.Objects)
}
if g.Objects[0].ID != "square" {
t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0])
}
if g.Objects[0].Label.MapKey.Value.Unbox() != nil {
t.Fatalf("expected g.Objects[0].Label.Node.Value.Unbox() == nil: %#v", g.Objects[0].Label.MapKey.Value)
}
if d2format.Format(g.Objects[0].Label.MapKey.Key) != "square" {
t.Fatalf("expected g.Objects[0].Label.Node.Key to be square: %#v", g.Objects[0].Label.MapKey.Key)
}
},
},
{
name: "edge",
text: `x -> y: one`,
key: `(x -> y)[0]`,
value: go2.Pointer(`two`),
exp: `x -> y: two
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
if g.Edges[0].Src.ID != "x" {
t.Fatalf("expected g.Edges[0].Src.ID == x: %#v", g.Edges[0].Src.ID)
}
if g.Edges[0].Dst.ID != "y" {
t.Fatalf("expected g.Edges[0].Dst.ID == y: %#v", g.Edges[0].Dst.ID)
}
if g.Edges[0].Label.Value != "two" {
t.Fatalf("expected g.Edges[0].Label.Value == two: %#v", g.Edges[0].Label.Value)
}
},
},
{
name: "shape",
text: `square`,
key: `square.shape`,
value: go2.Pointer(`square`),
exp: `square: {shape: square}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 1 {
t.Fatalf("expected 1 objects: %#v", g.Objects)
}
if g.Objects[0].ID != "square" {
t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0])
}
if g.Objects[0].Shape.Value != d2target.ShapeSquare {
t.Fatalf("expected g.Objects[0].Shape.Value == square: %#v", g.Objects[0].Shape.Value)
}
},
},
{
name: "replace_shape",
text: `square.shape: square`,
key: `square.shape`,
value: go2.Pointer(`circle`),
exp: `square.shape: circle
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 1 {
t.Fatalf("expected 1 objects: %#v", g.Objects)
}
if g.Objects[0].ID != "square" {
t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0])
}
if g.Objects[0].Shape.Value != d2target.ShapeCircle {
t.Fatalf("expected g.Objects[0].Shape.Value == circle: %#v", g.Objects[0].Shape.Value)
}
},
},
{
name: "new_style",
text: `square
`,
key: `square.style.opacity`,
value: go2.Pointer(`0.2`),
exp: `square: {style.opacity: 0.2}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.AST.Nodes) != 1 {
t.Fatal(g.AST)
}
if len(g.Objects) != 1 {
t.Fatalf("expected 1 object but got %#v", len(g.Objects))
}
f, err := strconv.ParseFloat(g.Objects[0].Style.Opacity.Value, 64)
if err != nil || f != 0.2 {
t.Fatalf("expected g.Objects[0].Map.Nodes[0].MapKey.Value.Number.Value.Float64() == 0.2: %#v", f)
}
},
},
{
name: "inline_style",
text: `square: {style.opacity: 0.2}
`,
key: `square.style.fill`,
value: go2.Pointer(`red`),
exp: `square: {
style.opacity: 0.2
style.fill: red
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.AST.Nodes) != 1 {
t.Fatal(g.AST)
}
},
},
{
name: "expanded_map_style",
text: `square: {
style: {
opacity: 0.1
}
}
`,
key: `square.style.opacity`,
value: go2.Pointer(`0.2`),
exp: `square: {
style: {
opacity: 0.2
}
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.AST.Nodes) != 1 {
t.Fatal(g.AST)
}
if len(g.AST.Nodes[0].MapKey.Value.Map.Nodes) != 1 {
t.Fatalf("expected 1 node within square but got %v", len(g.AST.Nodes[0].MapKey.Value.Map.Nodes))
}
f, err := strconv.ParseFloat(g.Objects[0].Style.Opacity.Value, 64)
if err != nil || f != 0.2 {
t.Fatal(err, f)
}
},
},
{
name: "replace_style",
text: `square.style.opacity: 0.1
`,
key: `square.style.opacity`,
value: go2.Pointer(`0.2`),
exp: `square.style.opacity: 0.2
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.AST.Nodes) != 1 {
t.Fatal(g.AST)
}
f, err := strconv.ParseFloat(g.Objects[0].Style.Opacity.Value, 64)
if err != nil || f != 0.2 {
t.Fatal(err, f)
}
},
},
{
name: "replace_style_edgecase",
text: `square.style.fill: orange
`,
key: `square.style.opacity`,
value: go2.Pointer(`0.2`),
exp: `square.style.fill: orange
square.style.opacity: 0.2
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.AST.Nodes) != 2 {
t.Fatal(g.AST)
}
f, err := strconv.ParseFloat(g.Objects[0].Style.Opacity.Value, 64)
if err != nil || f != 0.2 {
t.Fatal(err, f)
}
},
},
{
name: "set_position",
text: `square
`,
key: `square.top`,
value: go2.Pointer(`200`),
exp: `square: {top: 200}
`,
},
{
name: "labeled_set_position",
text: `hey.label: what
`,
key: `hey.top`,
value: go2.Pointer(`200`),
exp: `hey.label: what
hey.top: 200
`,
},
{
name: "replace_position",
text: `square: {
width: 100
top: 32
left: 44
}
`,
key: `square.top`,
value: go2.Pointer(`200`),
exp: `square: {
width: 100
top: 200
left: 44
}
`,
},
{
name: "set_dimensions",
text: `square
`,
key: `square.width`,
value: go2.Pointer(`200`),
exp: `square: {width: 200}
`,
},
{
name: "replace_dimensions",
text: `square: {
width: 100
}
`,
key: `square.width`,
value: go2.Pointer(`200`),
exp: `square: {
width: 200
}
`,
},
{
name: "set_tooltip",
text: `square
`,
key: `square.tooltip`,
value: go2.Pointer(`y`),
exp: `square: {tooltip: y}
`,
},
{
name: "replace_tooltip",
text: `square: {
tooltip: x
}
`,
key: `square.tooltip`,
value: go2.Pointer(`y`),
exp: `square: {
tooltip: y
}
`,
},
{
name: "replace_link",
text: `square: {
link: https://google.com
}
`,
key: `square.link`,
value: go2.Pointer(`https://apple.com`),
exp: `square: {
link: https://apple.com
}
`,
},
{
name: "replace_arrowhead",
text: `x -> y: {
target-arrowhead.shape: diamond
}
`,
key: `(x -> y)[0].target-arrowhead.shape`,
value: go2.Pointer(`circle`),
exp: `x -> y: {
target-arrowhead.shape: circle
}
`,
},
{
name: "replace_arrowhead_map",
text: `x -> y: {
target-arrowhead: {
shape: diamond
}
}
`,
key: `(x -> y)[0].target-arrowhead.shape`,
value: go2.Pointer(`circle`),
exp: `x -> y: {
target-arrowhead: {
shape: circle
}
}
`,
},
{
name: "replace_edge_style_map",
text: `x -> y: {
style: {
stroke-dash: 3
}
}
`,
key: `(x -> y)[0].style.stroke-dash`,
value: go2.Pointer(`4`),
exp: `x -> y: {
style: {
stroke-dash: 4
}
}
`,
},
{
name: "replace_edge_style",
text: `x -> y: {
style.stroke-width: 1
style.stroke-dash: 4
}
`,
key: `(x -> y)[0].style.stroke-dash`,
value: go2.Pointer(`3`),
exp: `x -> y: {
style.stroke-width: 1
style.stroke-dash: 3
}
`,
},
{
name: "set_fill_pattern",
text: `square`,
key: `square.style.fill-pattern`,
value: go2.Pointer(`grain`),
exp: `square: {style.fill-pattern: grain}
`,
},
{
name: "replace_fill_pattern",
text: `square: {
style.fill-pattern: lines
}
`,
key: `square.style.fill-pattern`,
value: go2.Pointer(`grain`),
exp: `square: {
style.fill-pattern: grain
}
`,
},
{
name: "classes-style",
text: `classes: {
a: {
style.fill: red
}
}
b.class: a
`,
key: `b.style.fill`,
value: go2.Pointer(`green`),
exp: `classes: {
a: {
style.fill: red
}
}
b.class: a
b.style.fill: green
`,
},
{
name: "dupe-classes-style",
text: `classes: {
a: {
style.fill: red
}
}
b.class: a
b.style.fill: red
`,
key: `b.style.fill`,
value: go2.Pointer(`green`),
exp: `classes: {
a: {
style.fill: red
}
}
b.class: a
b.style.fill: green
`,
},
{
name: "unapplied-classes-style",
text: `classes: {
a: {
style.fill: red
}
}
b.style.fill: red
`,
key: `b.style.fill`,
value: go2.Pointer(`green`),
exp: `classes: {
a: {
style.fill: red
}
}
b.style.fill: green
`,
},
{
name: "unapplied-classes-style-2",
text: `classes: {
a: {
style.fill: red
}
}
b
`,
key: `b.style.fill`,
value: go2.Pointer(`green`),
exp: `classes: {
a: {
style.fill: red
}
}
b: {style.fill: green}
`,
},
{
name: "class-with-label",
text: `classes: {
user: {
label: ""
}
}
a.class: user
`,
key: `a.style.opacity`,
value: go2.Pointer(`0.5`),
exp: `classes: {
user: {
label: ""
}
}
a.class: user
a.style.opacity: 0.5
`,
},
{
name: "edge-class-with-label",
text: `classes: {
user: {
label: ""
}
}
a -> b: {
class: user
}
`,
key: `(a -> b)[0].style.opacity`,
value: go2.Pointer(`0.5`),
exp: `classes: {
user: {
label: ""
}
}
a -> b: {
class: user
style.opacity: 0.5
}
`,
},
{
name: "var-with-label",
text: `vars: {
user: ""
}
a: ${user}
`,
key: `a.style.opacity`,
value: go2.Pointer(`0.5`),
exp: `vars: {
user: ""
}
a: ${user} {style.opacity: 0.5}
`,
},
{
name: "glob-with-label",
text: `*.label: ""
a
`,
key: `a.style.opacity`,
value: go2.Pointer(`0.5`),
exp: `*.label: ""
a
a.style.opacity: 0.5
`,
},
{
name: "label_unset",
text: `square: "Always try to do things in chronological order; it's less confusing that way."
`,
key: `square.label`,
value: nil,
exp: `square
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 1 {
t.Fatalf("expected 1 objects: %#v", g.Objects)
}
if g.Objects[0].ID != "square" {
t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0])
}
if g.Objects[0].Shape.Value == d2target.ShapeSquare {
t.Fatalf("expected g.Objects[0].Shape.Value == square: %#v", g.Objects[0].Shape.Value)
}
},
},
{
name: "label",
text: `square`,
key: `square.label`,
value: go2.Pointer(`Always try to do things in chronological order; it's less confusing that way.`),
exp: `square: "Always try to do things in chronological order; it's less confusing that way."
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 1 {
t.Fatalf("expected 1 objects: %#v", g.Objects)
}
if g.Objects[0].ID != "square" {
t.Fatalf("expected g.Objects[0].ID to be square: %#v", g.Objects[0])
}
if g.Objects[0].Shape.Value == d2target.ShapeSquare {
t.Fatalf("expected g.Objects[0].Shape.Value == square: %#v", g.Objects[0].Shape.Value)
}
},
},
{
name: "label_replace",
text: `square: I am deeply CONCERNED and I want something GOOD for BREAKFAST!`,
key: `square`,
value: go2.Pointer(`Always try to do things in chronological order; it's less confusing that way.`),
exp: `square: "Always try to do things in chronological order; it's less confusing that way."
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.AST.Nodes) != 1 {
t.Fatal(g.AST)
}
if len(g.Objects) != 1 {
t.Fatal(g.Objects)
}
if g.Objects[0].ID != "square" {
t.Fatal(g.Objects[0])
}
if g.Objects[0].Label.Value == "I am deeply CONCERNED and I want something GOOD for BREAKFAST!" {
t.Fatal(g.Objects[0].Label.Value)
}
},
},
{
name: "map_key_missing",
text: `a -> b`,
key: `a`,
value: go2.Pointer(`Never offend people with style when you can offend them with substance.`),
exp: `a -> b
a: Never offend people with style when you can offend them with substance.
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
},
},
{
name: "nested_alex",
text: `this: {
label: do
test -> here: asdf
}`,
key: `this.here`,
value: go2.Pointer(`How much of their influence on you is a result of your influence on them?
A conference is a gathering of important people who singly can do nothing`),
exp: `this: {
label: do
test -> here: asdf
here: "How much of their influence on you is a result of your influence on them?\nA conference is a gathering of important people who singly can do nothing"
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 3 {
t.Fatalf("expected 3 objects: %#v", g.Objects)
}
if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
},
},
{
name: "label_primary",
text: `oreo: {
q -> z
}`,
key: `oreo`,
value: go2.Pointer(`QOTD: "It's been Monday all week today."`),
exp: `oreo: 'QOTD: "It''s been Monday all week today."' {
q -> z
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 3 {
t.Fatalf("expected 3 objects: %#v", g.Objects)
}
if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
},
},
{
name: "edge_index_nested",
text: `oreo: {
q -> z
}`,
key: `(oreo.q -> oreo.z)[0]`,
value: go2.Pointer(`QOTD`),
exp: `oreo: {
q -> z: QOTD
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 3 {
t.Fatalf("expected 3 objects: %#v", g.Objects)
}
if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
},
},
{
name: "edge_index_case",
text: `Square: {
Square -> Square 2
}
z: {
x -> y
}
`,
key: `Square.(Square -> Square 2)[0]`,
value: go2.Pointer(`two`),
exp: `Square: {
Square -> Square 2: two
}
z: {
x -> y
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 6 {
t.Fatalf("expected 6 objects: %#v", g.Objects)
}
if len(g.Edges) != 2 {
t.Fatalf("expected 2 edges: %#v", g.Edges)
}
if g.Edges[0].Label.Value != "two" {
t.Fatalf("expected g.Edges[0].Label.Value == two: %#v", g.Edges[0].Label.Value)
}
},
},
{
name: "icon",
text: `meow
`,
key: `meow.icon`,
value: go2.Pointer(`https://icons.terrastruct.com/essentials/087-menu.svg`),
exp: `meow: {icon: https://icons.terrastruct.com/essentials/087-menu.svg}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 1 {
t.Fatal(g.Objects)
}
if g.Objects[0].Icon.String() != "https://icons.terrastruct.com/essentials/087-menu.svg" {
t.Fatal(g.Objects[0].Icon.String())
}
},
},
{
name: "edge_chain",
text: `oreo: {
q -> z -> p: wsup
}`,
key: `(oreo.q -> oreo.z)[0]`,
value: go2.Pointer(`QOTD:
"It's been Monday all week today."`),
exp: `oreo: {
q -> z -> p: wsup
(q -> z)[0]: "QOTD:\n \"It's been Monday all week today.\""
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 4 {
t.Fatalf("expected 4 objects: %#v", g.Objects)
}
if len(g.Edges) != 2 {
t.Fatalf("expected 2 edges: %#v", g.Edges)
}
},
},
{
name: "edge_nested_label_set",
text: `oreo: {
q -> z: wsup
}`,
key: `(oreo.q -> oreo.z)[0].label`,
value: go2.Pointer(`yo`),
exp: `oreo: {
q -> z: yo
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 3 {
t.Fatalf("expected 3 objects: %#v", g.Objects)
}
if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
if g.Edges[0].Src.ID != "q" {
t.Fatal(g.Edges[0].Src.ID)
}
},
},
{
name: "shape_nested_style_set",
text: `x
`,
key: `x.style.opacity`,
value: go2.Pointer(`0.4`),
exp: `x: {style.opacity: 0.4}
`,
},
{
name: "edge_nested_style_set",
text: `oreo: {
q -> z: wsup
}
`,
key: `(oreo.q -> oreo.z)[0].style.opacity`,
value: go2.Pointer(`0.4`),
exp: `oreo: {
q -> z: wsup {style.opacity: 0.4}
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.JSON(t, 3, len(g.Objects))
assert.JSON(t, 1, len(g.Edges))
assert.JSON(t, "q", g.Edges[0].Src.ID)
assert.JSON(t, "0.4", g.Edges[0].Style.Opacity.Value)
},
},
{
name: "edge_chain_append_style",
text: `x -> y -> z
`,
key: `(x -> y)[0].style.animated`,
value: go2.Pointer(`true`),
exp: `x -> y -> z
(x -> y)[0].style.animated: true
`,
},
{
name: "edge_chain_existing_style",
text: `x -> y -> z
(y -> z)[0].style.opacity: 0.4
`,
key: `(y -> z)[0].style.animated`,
value: go2.Pointer(`true`),
exp: `x -> y -> z
(y -> z)[0].style.opacity: 0.4
(y -> z)[0].style.animated: true
`,
},
{
name: "edge_key_and_key",
text: `a
a.b -> a.c
`,
key: `a.(b -> c)[0].style.animated`,
value: go2.Pointer(`true`),
exp: `a
a.b -> a.c: {style.animated: true}
`,
},
{
name: "edge_label",
text: `a -> b: "yo"
`,
key: `(a -> b)[0].style.animated`,
value: go2.Pointer(`true`),
exp: `a -> b: "yo" {style.animated: true}
`,
},
{
name: "edge_append_style",
text: `x -> y
`,
key: `(x -> y)[0].style.animated`,
value: go2.Pointer(`true`),
exp: `x -> y: {style.animated: true}
`,
},
{
name: "edge_set_arrowhead",
text: `x -> y
`,
key: `(x -> y)[0].target-arrowhead.shape`,
value: go2.Pointer(`diamond`),
exp: `x -> y: {target-arrowhead.shape: diamond}
`,
},
{
name: "edge-arrowhead-filled/1",
text: `x -> y
`,
key: `(x -> y)[0].target-arrowhead.style.filled`,
value: go2.Pointer(`true`),
exp: `x -> y: {target-arrowhead.style.filled: true}
`,
},
{
name: "edge-arrowhead-filled/2",
text: `x -> y: {
target-arrowhead: * {
shape: diamond
}
}
`,
key: `(x -> y)[0].target-arrowhead.style.filled`,
value: go2.Pointer(`true`),
exp: `x -> y: {
target-arrowhead: * {
shape: diamond
style.filled: true
}
}
`,
},
{
name: "edge-arrowhead-filled/3",
text: `x -> y: {
target-arrowhead.shape: diamond
}
`,
key: `(x -> y)[0].target-arrowhead.style.filled`,
value: go2.Pointer(`true`),
exp: `x -> y: {
target-arrowhead.shape: diamond
target-arrowhead.style.filled: true
}
`,
},
{
name: "edge-arrowhead-filled/4",
text: `x -> y: {
target-arrowhead.shape: diamond
target-arrowhead.style.filled: true
}
`,
key: `(x -> y)[0].target-arrowhead.style.filled`,
value: go2.Pointer(`false`),
exp: `x -> y: {
target-arrowhead.shape: diamond
target-arrowhead.style.filled: false
}
`,
},
{
name: "edge-arrowhead-filled/5",
text: `x -> y: {
target-arrowhead.shape: diamond
target-arrowhead.style: {
filled: false
}
}
`,
key: `(x -> y)[0].target-arrowhead.style.filled`,
value: go2.Pointer(`true`),
exp: `x -> y: {
target-arrowhead.shape: diamond
target-arrowhead.style: {
filled: true
}
}
`,
},
{
name: "edge_replace_arrowhead",
text: `x -> y: {target-arrowhead.shape: circle}
`,
key: `(x -> y)[0].target-arrowhead.shape`,
value: go2.Pointer(`diamond`),
exp: `x -> y: {target-arrowhead.shape: diamond}
`,
},
{
name: "edge_replace_arrowhead_indexed",
text: `x -> y
(x -> y)[0].target-arrowhead.shape: circle
`,
key: `(x -> y)[0].target-arrowhead.shape`,
value: go2.Pointer(`diamond`),
exp: `x -> y
(x -> y)[0].target-arrowhead.shape: diamond
`,
},
{
name: "edge_merge_arrowhead",
text: `x -> y: {
target-arrowhead: {
label: 1
}
}
`,
key: `(x -> y)[0].target-arrowhead.shape`,
value: go2.Pointer(`diamond`),
exp: `x -> y: {
target-arrowhead: {
label: 1
shape: diamond
}
}
`,
},
{
name: "edge_merge_style",
text: `x -> y: {
style: {
opacity: 0.4
}
}
`,
key: `(x -> y)[0].style.animated`,
value: go2.Pointer(`true`),
exp: `x -> y: {
style: {
opacity: 0.4
animated: true
}
}
`,
},
{
name: "edge_flat_merge_arrowhead",
text: `x -> y -> z
(x -> y)[0].target-arrowhead.shape: diamond
`,
key: `(x -> y)[0].target-arrowhead.shape`,
value: go2.Pointer(`circle`),
exp: `x -> y -> z
(x -> y)[0].target-arrowhead.shape: circle
`,
},
{
name: "edge_index_merge_style",
text: `x -> y -> z
(x -> y)[0].style.opacity: 0.4
`,
key: `(x -> y)[0].style.opacity`,
value: go2.Pointer(`0.5`),
exp: `x -> y -> z
(x -> y)[0].style.opacity: 0.5
`,
},
{
name: "edge_chain_nested_set",
text: `oreo: {
q -> z -> p: wsup
}`,
key: `(oreo.q -> oreo.z)[0].style.opacity`,
value: go2.Pointer(`0.4`),
exp: `oreo: {
q -> z -> p: wsup
(q -> z)[0].style.opacity: 0.4
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 4 {
t.Fatalf("expected 4 objects: %#v", g.Objects)
}
if len(g.Edges) != 2 {
t.Fatalf("expected 2 edges: %#v", g.Edges)
}
if g.Edges[0].Src.ID != "q" {
t.Fatal(g.Edges[0].Src.ID)
}
if g.Edges[0].Style.Opacity.Value != "0.4" {
t.Fatal(g.Edges[0].Style.Opacity.Value)
}
},
},
{
name: "block_string_oneline",
text: ``,
key: `x`,
tag: go2.Pointer("md"),
value: go2.Pointer(`|||what's up|||`),
exp: `x: ||||md |||what's up||| ||||
`,
},
{
name: "block_string_multiline",
text: ``,
key: `x`,
tag: go2.Pointer("md"),
value: go2.Pointer(`# header
He has not acquired a fortune; the fortune has acquired him.
He has not acquired a fortune; the fortune has acquired him.`),
exp: `x: |md
# header
He has not acquired a fortune; the fortune has acquired him.
He has not acquired a fortune; the fortune has acquired him.
|
`,
},
// TODO: pass
/*
{
name: "oneline_constraint",
text: `My Table: {
shape: sql_table
column: int
}
`,
key: `My Table.column.constraint`,
value: utils.Pointer("PK"),
exp: `My Table: {
shape: sql_table
column: int {constraint: PK}
}
`,
},
*/
// TODO: pass
/*
{
name: "oneline_style",
text: `foo: bar
`,
key: `foo.style_fill`,
value: utils.Pointer("red"),
exp: `foo: bar {style_fill: red}
`,
},
*/
{
name: "errors/bad_tag",
text: `x.icon: hello
`,
key: "x.icon",
tag: go2.Pointer("one two"),
value: go2.Pointer(`three
four
five
six
`),
expErr: `failed to set "x.icon" to "one two" "\"three\\nfour\\nfive\\nsix\\n\"": spaces are not allowed in blockstring tags`,
},
{
name: "layers-usable-ref-style",
text: `a
layers: {
x: {
a
}
}
`,
key: `a.style.opacity`,
value: go2.Pointer(`0.2`),
boardPath: []string{"x"},
exp: `a
layers: {
x: {
a: {style.opacity: 0.2}
}
}
`,
},
{
name: "layers-unusable-ref-style",
text: `a
layers: {
x: {
b
}
}
`,
key: `a.style.opacity`,
value: go2.Pointer(`0.2`),
boardPath: []string{"x"},
exp: `a
layers: {
x: {
b
a.style.opacity: 0.2
}
}
`,
},
{
name: "scenarios-usable-ref-style",
text: `a: outer
scenarios: {
x: {
a: inner
}
}
`,
key: `a.style.opacity`,
value: go2.Pointer(`0.2`),
boardPath: []string{"x"},
exp: `a: outer
scenarios: {
x: {
a: inner {style.opacity: 0.2}
}
}
`,
},
{
name: "scenarios-multiple",
text: `a
scenarios: {
x: {
b
a.style.fill: red
}
}
`,
key: `a.style.opacity`,
value: go2.Pointer(`0.2`),
boardPath: []string{"x"},
exp: `a
scenarios: {
x: {
b
a.style.fill: red
a.style.opacity: 0.2
}
}
`,
},
{
name: "scenarios-nested-usable-ref-style",
text: `a: {
b: outer
}
scenarios: {
x: {
a: {
b: inner
}
}
}
`,
key: `a.b.style.opacity`,
value: go2.Pointer(`0.2`),
boardPath: []string{"x"},
exp: `a: {
b: outer
}
scenarios: {
x: {
a: {
b: inner {style.opacity: 0.2}
}
}
}
`,
},
{
name: "scenarios-unusable-ref-style",
text: `a
scenarios: {
x: {
b
}
}
`,
key: `a.style.opacity`,
value: go2.Pointer(`0.2`),
boardPath: []string{"x"},
exp: `a
scenarios: {
x: {
b
a.style.opacity: 0.2
}
}
`,
},
{
name: "scenarios-label-primary",
text: `a: {
style.opacity: 0.2
}
scenarios: {
x: {
a: {
style.opacity: 0.3
}
}
}
`,
key: `a`,
value: go2.Pointer(`b`),
boardPath: []string{"x"},
exp: `a: {
style.opacity: 0.2
}
scenarios: {
x: {
a: b {
style.opacity: 0.3
}
}
}
`,
},
{
name: "scenarios-label-primary-missing",
text: `a: {
style.opacity: 0.2
}
scenarios: {
x: {
b
}
}
`,
key: `a`,
value: go2.Pointer(`b`),
boardPath: []string{"x"},
exp: `a: {
style.opacity: 0.2
}
scenarios: {
x: {
b
a: b
}
}
`,
},
{
name: "scenarios-edge-set",
text: `a -> b
scenarios: {
x: {
c
}
}
`,
key: `(a -> b)[0].style.opacity`,
value: go2.Pointer(`0.2`),
boardPath: []string{"x"},
exp: `a -> b
scenarios: {
x: {
c
(a -> b)[0].style.opacity: 0.2
}
}
`,
},
{
name: "scenarios-existing-edge-set",
text: `a -> b
scenarios: {
x: {
a -> b
c
}
}
`,
key: `(a -> b)[1].style.opacity`,
value: go2.Pointer(`0.2`),
boardPath: []string{"x"},
exp: `a -> b
scenarios: {
x: {
a -> b: {style.opacity: 0.2}
c
}
}
`,
},
{
name: "scenarios-arrowhead",
text: `a -> b: {
target-arrowhead.shape: triangle
}
x -> y
scenarios: {
x: {
(a -> b)[0]: {
target-arrowhead.shape: circle
}
c -> d
}
}
`,
key: `(a -> b)[0].target-arrowhead.shape`,
value: go2.Pointer(`diamond`),
boardPath: []string{"x"},
exp: `a -> b: {
target-arrowhead.shape: triangle
}
x -> y
scenarios: {
x: {
(a -> b)[0]: {
target-arrowhead.shape: diamond
}
c -> d
}
}
`,
},
{
name: "import/1",
text: `x: {
...@meow.x
y
}
`,
fsTexts: map[string]string{
"meow.d2": `x: {
style.fill: blue
}
`,
},
key: `x.style.stroke`,
value: go2.Pointer(`red`),
exp: `x: {
...@meow.x
y
style.stroke: red
}
`,
},
{
name: "import/2",
text: `x: {
...@meow.x
y
}
`,
fsTexts: map[string]string{
"meow.d2": `x: {
style.fill: blue
}
`,
},
key: `x.style.fill`,
value: go2.Pointer(`red`),
exp: `x: {
...@meow.x
y
style.fill: red
}
`,
},
{
name: "import/3",
text: `x: {
...@meow.x
y
style.fill: red
}
`,
fsTexts: map[string]string{
"meow.d2": `x: {
style.fill: blue
}
`,
},
key: `x.style.fill`,
value: go2.Pointer(`yellow`),
exp: `x: {
...@meow.x
y
style.fill: yellow
}
`,
},
{
name: "import/4",
text: `...@yo
a`,
fsTexts: map[string]string{
"yo.d2": `b`,
},
key: `b.style.fill`,
value: go2.Pointer(`red`),
exp: `...@yo
a
b.style.fill: red
`,
},
{
name: "import/5",
text: `a
x: {
...@yo
}`,
fsTexts: map[string]string{
"yo.d2": `b`,
},
key: `x.b.style.fill`,
value: go2.Pointer(`red`),
exp: `a
x: {
...@yo
b.style.fill: red
}
`,
},
{
name: "import/6",
text: `a
x: @yo`,
fsTexts: map[string]string{
"yo.d2": `b`,
},
key: `x.b.style.fill`,
value: go2.Pointer(`red`),
exp: `a
x: @yo
x.b.style.fill: red
`,
},
{
name: "import/7",
text: `...@yo
b.style.fill: red`,
fsTexts: map[string]string{
"yo.d2": `b`,
},
key: `b.style.opacity`,
value: go2.Pointer("0.5"),
exp: `...@yo
b.style.fill: red
b.style.opacity: 0.5
`,
},
{
name: "import/8",
text: `a
layers: {
x: @yo
}`,
boardPath: []string{"x"},
fsTexts: map[string]string{
"yo.d2": `b`,
},
key: `b.style.fill`,
value: go2.Pointer(`red`),
exp: `a
layers: {
x: {
...@yo
b.style.fill: red
}
}
`,
},
{
name: "import/9",
text: `...@yo
`,
fsTexts: map[string]string{
"yo.d2": `a -> b`,
},
key: `(a -> b)[0].style.stroke`,
value: go2.Pointer(`red`),
exp: `...@yo
(a -> b)[0].style.stroke: red
`,
},
{
name: "import/10",
text: `heyn
layers: {
man: {...@meow}
}
`,
fsTexts: map[string]string{
"meow.d2": `layers: {
1: {
asdf
}
}
`,
},
boardPath: []string{"man", "1"},
key: `asdf.link`,
value: go2.Pointer(`_._`),
expErr: `failed to set "asdf.link" to "\"_._\"": board [man 1] cannot be modified through this file`,
},
{
name: "label-near/1",
text: `x
`,
key: `x.label.near`,
value: go2.Pointer(`bottom-right`),
exp: `x: {label.near: bottom-right}
`,
},
{
name: "label-near/2",
text: `x.label.near: bottom-left
`,
key: `x.label.near`,
value: go2.Pointer(`bottom-right`),
exp: `x.label.near: bottom-right
`,
},
{
name: "label-near/3",
text: `x: {
label.near: bottom-left
}
`,
key: `x.label.near`,
value: go2.Pointer(`bottom-right`),
exp: `x: {
label.near: bottom-right
}
`,
},
{
name: "label-near/4",
text: `x: {
label: hi {
near: bottom-left
}
}
`,
key: `x.label.near`,
value: go2.Pointer(`bottom-right`),
exp: `x: {
label: hi {
near: bottom-right
}
}
`,
},
{
name: "label-near/5",
text: `x: hi {
label: {
near: bottom-left
}
}
`,
key: `x.label.near`,
value: go2.Pointer(`bottom-right`),
exp: `x: hi {
label: {
near: bottom-right
}
}
`,
},
{
name: "glob-field/1",
text: `*.style.fill: red
a
b
`,
key: `a.style.fill`,
value: go2.Pointer(`blue`),
exp: `*.style.fill: red
a: {style.fill: blue}
b
`,
},
{
name: "glob-field/2",
text: `(* -> *)[*].style.stroke: red
a -> b
a -> b
`,
key: `(a -> b)[0].style.stroke`,
value: go2.Pointer(`blue`),
exp: `(* -> *)[*].style.stroke: red
a -> b: {style.stroke: blue}
a -> b
`,
},
{
name: "glob-field/3",
text: `(* -> *)[*].style.stroke: red
a -> b: {style.stroke: blue}
a -> b
`,
key: `(a -> b)[0].style.stroke`,
value: go2.Pointer(`green`),
exp: `(* -> *)[*].style.stroke: red
a -> b: {style.stroke: green}
a -> b
`,
},
{
name: "nested-edge-chained/1",
text: `a: {
b: {
c
}
}
x -> a.b -> a.b.c
`,
key: `(a.b -> a.b.c)[0].style.stroke`,
value: go2.Pointer(`green`),
exp: `a: {
b: {
c
}
}
x -> a.b -> a.b.c
(a.b -> a.b.c)[0].style.stroke: green
`,
},
{
name: "nested-edge-chained/2",
text: `z: {
a: {
b: {
c
}
}
x -> a.b -> a.b.c
}
`,
key: `(z.a.b -> z.a.b.c)[0].style.stroke`,
value: go2.Pointer(`green`),
exp: `z: {
a: {
b: {
c
}
}
x -> a.b -> a.b.c
(a.b -> a.b.c)[0].style.stroke: green
}
`,
},
{
name: "edge-comment",
text: `x -> y: {
# hi
style.stroke: blue
}
`,
key: `(x -> y)[0].style.stroke`,
value: go2.Pointer(`green`),
exp: `x -> y: {
# hi
style.stroke: green
}
`,
},
{
name: "scenario-child",
text: `a -> b
scenarios: {
x: {
hi
}
}
`,
key: `(a -> b)[0].style.stroke-width`,
value: go2.Pointer(`3`),
boardPath: []string{"x"},
exp: `a -> b
scenarios: {
x: {
hi
(a -> b)[0].style.stroke-width: 3
}
}
`,
},
{
name: "scenario-grandchild",
text: `a -> b
scenarios: {
x: {
scenarios: {
c: {
(a -> b)[0].style.bold: true
}
}
}
}
`,
key: `(a -> b)[0].style.stroke-width`,
value: go2.Pointer(`3`),
boardPath: []string{"x", "c"},
exp: `a -> b
scenarios: {
x: {
scenarios: {
c: {
(a -> b)[0].style.bold: true
(a -> b)[0].style.stroke-width: 3
}
}
}
}
`,
},
{
name: "step-connection",
text: `steps: {
1: {
Modules -- Metricbeat: {
style.stroke-width: 1
}
}
}
`,
key: `Metricbeat.style.stroke`,
value: go2.Pointer(`red`),
boardPath: []string{"1"},
exp: `steps: {
1: {
Modules -- Metricbeat: {
style.stroke-width: 1
}
Metricbeat.style.stroke: red
}
}
`,
},
{
name: "set-style-in-layer",
text: `hey
layers: {
k: {
b: {style.stroke: "#969db4"}
}
}
layers: {
x: {
y
}
}
`,
boardPath: []string{"x"},
key: `y.style.fill`,
value: go2.Pointer(`#ff0000`),
exp: `hey
layers: {
k: {
b: {style.stroke: "#969db4"}
}
}
layers: {
x: {
y: {style.fill: "#ff0000"}
}
}
`,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
et := editTest{
text: tc.text,
fsTexts: tc.fsTexts,
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
return d2oracle.Set(g, tc.boardPath, tc.key, tc.tag, tc.value)
},
exp: tc.exp,
expErr: tc.expErr,
assertions: tc.assertions,
}
et.run(t)
})
}
}
func TestReconnectEdge(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
boardPath []string
text string
edgeKey string
newSrc string
newDst string
expErr string
exp string
assertions func(t *testing.T, g *d2graph.Graph)
}{
{
name: "basic",
text: `a
b
c
a -> b
`,
edgeKey: `(a -> b)[0]`,
newDst: "c",
exp: `a
b
c
a -> c
`,
},
{
name: "src",
text: `a
b
c
a -> b
`,
edgeKey: `(a -> b)[0]`,
newSrc: "c",
exp: `a
b
c
c -> b
`,
},
{
name: "both",
text: `a
b
c
a -> b
`,
edgeKey: `(a -> b)[0]`,
newSrc: "b",
newDst: "a",
exp: `a
b
c
b -> a
`,
},
{
name: "contained",
text: `a.x -> a.y
a.z`,
edgeKey: `a.(x -> y)[0]`,
newDst: "a.z",
exp: `a.x -> a.z
a.y
a.z
`,
},
{
name: "scope_outer",
text: `a: {
x -> y
}
b`,
edgeKey: `(a.x -> a.y)[0]`,
newDst: "b",
exp: `a: {
x -> _.b
y
}
b
`,
},
{
name: "scope_inner",
text: `a: {
x -> y
z: {
b
}
}`,
edgeKey: `(a.x -> a.y)[0]`,
newDst: "a.z.b",
exp: `a: {
x -> z.b
y
z: {
b
}
}
`,
},
{
name: "loop",
text: `a -> a
b`,
edgeKey: `(a -> a)[0]`,
newDst: "b",
exp: `a -> b
b
`,
},
{
name: "preserve_old_obj",
text: `a -> b
(a -> b)[0].style.stroke: red
c`,
edgeKey: `(a -> b)[0]`,
newSrc: "a",
newDst: "c",
exp: `a -> c
b
(a -> c)[0].style.stroke: red
c
`,
},
{
name: "middle_chain",
text: `a -> b -> c
x`,
edgeKey: `(a -> b)[0]`,
newDst: "x",
exp: `b -> c
a -> x
x
`,
},
{
name: "middle_chain_src",
text: `a -> b -> c
x`,
edgeKey: `(b -> c)[0]`,
newSrc: "x",
exp: `a -> b
x -> c
x
`,
},
{
name: "middle_chain_both",
text: `a -> b -> c -> d
x`,
edgeKey: `(b -> c)[0]`,
newSrc: "x",
newDst: "x",
exp: `a -> b
c -> d
x -> x
x
`,
},
{
name: "middle_chain_first",
text: `a -> b -> c -> d
x`,
edgeKey: `(a -> b)[0]`,
newSrc: "x",
exp: `a
x -> b -> c -> d
x
`,
},
{
name: "middle_chain_last",
text: `a -> b -> c -> d
x`,
edgeKey: `(c -> d)[0]`,
newDst: "x",
exp: `a -> b -> c -> x
d
x
`,
},
// These _3 and _4 match the delta tests
{
name: "in_chain_3",
text: `a -> b -> a -> c
`,
edgeKey: "(a -> b)[0]",
newDst: "c",
exp: `b -> a -> c
a -> c
`,
},
{
name: "in_chain_4",
text: `a -> c -> a -> c
b
`,
edgeKey: "(a -> c)[0]",
newDst: "b",
exp: `c -> a -> c
a -> b
b
`,
},
{
name: "indexed_ref",
text: `a -> b
x
(a -> b)[0].style.stroke: red
`,
edgeKey: `(a -> b)[0]`,
newDst: "x",
exp: `a -> x
b
x
(a -> x)[0].style.stroke: red
`,
},
{
name: "reverse",
text: `a -> b
`,
edgeKey: `(a -> b)[0]`,
newSrc: "b",
newDst: "a",
exp: `b -> a
`,
},
{
name: "second_index",
text: `a -> b: {
style.stroke: blue
}
a -> b: {
style.stroke: red
}
x
`,
edgeKey: `(a -> b)[1]`,
newDst: "x",
exp: `a -> b: {
style.stroke: blue
}
a -> x: {
style.stroke: red
}
x
`,
},
{
name: "nonexistant_edge",
text: `a -> b
`,
edgeKey: `(b -> a)[0]`,
newDst: "a",
expErr: "edge not found",
},
{
name: "nonexistant_obj",
text: `a -> b
`,
edgeKey: `(a -> b)[0]`,
newDst: "x",
expErr: "newDst not found",
},
{
name: "layers-basic",
text: `a
layers: {
x: {
b
c
a -> b
}
}
`,
boardPath: []string{"x"},
edgeKey: `(a -> b)[0]`,
newDst: "c",
exp: `a
layers: {
x: {
b
c
a -> c
}
}
`,
},
{
name: "scenarios-basic",
text: `a
scenarios: {
x: {
b
c
a -> b
}
}
`,
boardPath: []string{"x"},
edgeKey: `(a -> b)[0]`,
newDst: "c",
exp: `a
scenarios: {
x: {
b
c
a -> c
}
}
`,
},
{
name: "scenarios-outer-scope",
text: `a
scenarios: {
x: {
d -> b
}
}
`,
boardPath: []string{"x"},
edgeKey: `(d -> b)[0]`,
newDst: "a",
exp: `a
scenarios: {
x: {
d -> a
b
}
}
`,
},
{
name: "scenarios-chain",
text: `a -> b -> c
scenarios: {
x: {
d
}
}
`,
boardPath: []string{"x"},
edgeKey: `(a -> b)[0]`,
newDst: "d",
expErr: `operation would modify AST outside of given scope`,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
et := editTest{
text: tc.text,
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
var newSrc *string
var newDst *string
if tc.newSrc != "" {
newSrc = &tc.newSrc
}
if tc.newDst != "" {
newDst = &tc.newDst
}
return d2oracle.ReconnectEdge(g, tc.boardPath, tc.edgeKey, newSrc, newDst)
},
exp: tc.exp,
expErr: tc.expErr,
assertions: tc.assertions,
}
et.run(t)
})
}
}
func TestRename(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
boardPath []string
text string
fsTexts map[string]string
key string
newName string
expErr string
exp string
assertions func(t *testing.T, g *d2graph.Graph)
}{
{
name: "flat",
text: `nerve-gift-earther
`,
key: `nerve-gift-earther`,
newName: `---`,
exp: `"---"
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 1 {
t.Fatalf("expected one object: %#v", g.Objects)
}
if g.Objects[0].ID != `"---"` {
t.Fatalf("unexpected object id: %q", g.Objects[0].ID)
}
},
},
{
name: "generated",
text: `Square
`,
key: `Square`,
newName: `Square`,
exp: `Square
`,
},
{
name: "generated-conflict",
text: `Square
Square 2
`,
key: `Square 2`,
newName: `Square`,
exp: `Square
Square 2
`,
},
{
name: "near",
text: `x: {
near: y
}
y
`,
key: `y`,
newName: `z`,
exp: `x: {
near: z
}
z
`,
},
{
name: "conflict",
text: `lalal
la
`,
key: `lalal`,
newName: `la`,
exp: `la 2
la
`,
},
{
name: "conflict 2",
text: `1.2.3: {
4
5
}
`,
key: "1.2.3.4",
newName: "5",
exp: `1.2.3: {
5 2
5
}
`,
},
{
name: "conflict_with_dots",
text: `"a.b"
y
`,
key: "y",
newName: "a.b",
exp: `"a.b"
"a.b 2"
`,
},
{
name: "conflict_with_numbers",
text: `1
Square
`,
key: `Square`,
newName: `1`,
exp: `1
1 2
`,
},
{
name: "nested",
text: `x.y.z.q.nerve-gift-earther
x.y.z.q: {
nerve-gift-earther
}
`,
key: `x.y.z.q.nerve-gift-earther`,
newName: `nerve-gift-jingler`,
exp: `x.y.z.q.nerve-gift-jingler
x.y.z.q: {
nerve-gift-jingler
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 5 {
t.Fatalf("expected five objects: %#v", g.Objects)
}
if g.Objects[4].AbsID() != "x.y.z.q.nerve-gift-jingler" {
t.Fatalf("unexpected object absolute id: %q", g.Objects[4].AbsID())
}
},
},
{
name: "edges",
text: `q.z -> p.k -> q.z -> l.a -> q.z
q: {
q -> + -> z
z: label
}
`,
key: `q.z`,
newName: `%%%`,
exp: `q.%%% -> p.k -> q.%%% -> l.a -> q.%%%
q: {
q -> + -> %%%
%%%: label
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 8 {
t.Fatalf("expected eight objects: %#v", g.Objects)
}
if g.Objects[1].AbsID() != "q.%%%" {
t.Fatalf("unexpected object absolute ID: %q", g.Objects[1].AbsID())
}
},
},
{
name: "container",
text: `ok.q.z -> p.k -> ok.q.z -> l.a -> ok.q.z
ok.q: {
q -> + -> z
z: label
}
ok: {
q: {
i
}
}
(ok.q.z -> p.k)[0]: "furbling, v.:"
more.(ok.q.z -> p.k): "furbling, v.:"
`,
key: `ok.q`,
newName: `<gosling>`,
exp: `ok."<gosling>".z -> p.k -> ok."<gosling>".z -> l.a -> ok."<gosling>".z
ok."<gosling>": {
q -> + -> z
z: label
}
ok: {
"<gosling>": {
i
}
}
(ok."<gosling>".z -> p.k)[0]: "furbling, v.:"
more.(ok.q.z -> p.k): "furbling, v.:"
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 16 {
t.Fatalf("expected 16 objects: %#v", g.Objects)
}
if g.Objects[2].AbsID() != `ok."<gosling>".z` {
t.Fatalf("unexpected object absolute ID: %q", g.Objects[1].AbsID())
}
},
},
{
name: "complex_edge_1",
text: `a.b.(x -> y).style.animated
`,
key: "a.b",
newName: "ooo",
exp: `a.ooo.(x -> y).style.animated
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 4 {
t.Fatalf("expected 4 objects: %#v", g.Objects)
}
if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
},
},
{
name: "complex_edge_2",
text: `a.b.(x -> y).style.animated
`,
key: "a.b.x",
newName: "papa",
exp: `a.b.(papa -> y).style.animated
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 4 {
t.Fatalf("expected 4 objects: %#v", g.Objects)
}
if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
},
},
/* TODO: handle edge keys
{
name: "complex_edge_3",
text: `a.b.(x -> y).q.z
`,
key: "a.b.(x -> y)[0].q",
newName: "zoink",
exp: `a.b.(x -> y).zoink.z
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 4 {
t.Fatalf("expected 4 objects: %#v", g.Objects)
}
if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
},
},
*/
{
name: "arrows",
text: `x -> y
`,
key: "(x -> y)[0]",
newName: "(x <- y)[0]",
exp: `x <- y
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
if !g.Edges[0].SrcArrow || g.Edges[0].DstArrow {
t.Fatalf("expected src arrow and no dst arrow: %#v", g.Edges[0])
}
},
},
{
name: "arrows_complex",
text: `a.b.(x -- y).style.animated
`,
key: "a.b.(x -- y)[0]",
newName: "(x <-> y)[0]",
exp: `a.b.(x <-> y).style.animated
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 4 {
t.Fatalf("expected 4 objects: %#v", g.Objects)
}
if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
if !g.Edges[0].SrcArrow || !g.Edges[0].DstArrow {
t.Fatalf("expected src arrow and dst arrow: %#v", g.Edges[0])
}
},
},
{
name: "arrows_chain",
text: `x -> y -> z -> q
`,
key: "(x -> y)[0]",
newName: "(x <-> y)[0]",
exp: `x <-> y -> z -> q
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 4 {
t.Fatalf("expected 4 objects: %#v", g.Objects)
}
if len(g.Edges) != 3 {
t.Fatalf("expected 3 edges: %#v", g.Edges)
}
if !g.Edges[0].SrcArrow || !g.Edges[0].DstArrow {
t.Fatalf("expected src arrow and dst arrow: %#v", g.Edges[0])
}
},
},
{
name: "arrows_trim_common",
text: `x.(x -> y -> z -> q)
`,
key: "(x.x -> x.y)[0]",
newName: "(x.x <-> x.y)[0]",
exp: `x.(x <-> y -> z -> q)
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 5 {
t.Fatalf("expected 5 objects: %#v", g.Objects)
}
if len(g.Edges) != 3 {
t.Fatalf("expected 3 edges: %#v", g.Edges)
}
if !g.Edges[0].SrcArrow || !g.Edges[0].DstArrow {
t.Fatalf("expected src arrow and dst arrow: %#v", g.Edges[0])
}
},
},
{
name: "arrows_trim_common_2",
text: `x.x -> x.y -> x.z -> x.q)
`,
key: "(x.x -> x.y)[0]",
newName: "(x.x <-> x.y)[0]",
exp: `x.x <-> x.y -> x.z -> x.q)
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 5 {
t.Fatalf("expected 5 objects: %#v", g.Objects)
}
if len(g.Edges) != 3 {
t.Fatalf("expected 3 edges: %#v", g.Edges)
}
if !g.Edges[0].SrcArrow || !g.Edges[0].DstArrow {
t.Fatalf("expected src arrow and dst arrow: %#v", g.Edges[0])
}
},
},
{
name: "errors/empty_key",
text: ``,
key: "",
expErr: `failed to rename "" to "": empty map key: ""`,
},
{
name: "errors/nonexistent",
text: ``,
key: "1.2.3.4",
newName: "bic",
expErr: `failed to rename "1.2.3.4" to "bic": key does not exist`,
},
{
name: "errors/reserved_keys",
text: `x.icon: hello
`,
key: "x.icon",
newName: "near",
expErr: `failed to rename "x.icon" to "near": cannot rename to reserved keyword: "near"`,
},
{
name: "layers-basic",
text: `x
layers: {
y: {
a
}
}
`,
boardPath: []string{"y"},
key: "a",
newName: "b",
exp: `x
layers: {
y: {
b
}
}
`,
},
{
name: "scenarios-basic",
text: `x
scenarios: {
y: {
a
}
}
`,
boardPath: []string{"y"},
key: "a",
newName: "b",
exp: `x
scenarios: {
y: {
b
}
}
`,
},
{
name: "scenarios-conflict",
text: `x
scenarios: {
y: {
a
}
}
`,
boardPath: []string{"y"},
key: "a",
newName: "x",
exp: `x
scenarios: {
y: {
x 2
}
}
`,
},
{
name: "scenarios-scope-err",
text: `x
scenarios: {
y: {
a
}
}
`,
boardPath: []string{"y"},
key: "x",
newName: "b",
expErr: `failed to rename "x" to "b": operation would modify AST outside of given scope`,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
et := editTest{
text: tc.text,
fsTexts: tc.fsTexts,
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
objectsBefore := len(g.Objects)
var err error
g, _, err = d2oracle.Rename(g, tc.boardPath, tc.key, tc.newName)
if err == nil {
objectsAfter := len(g.Objects)
if objectsBefore != objectsAfter {
t.Log(d2format.Format(g.AST))
return nil, fmt.Errorf("rename cannot destroy or create objects: found %d objects before and %d objects after", objectsBefore, objectsAfter)
}
}
return g, err
},
exp: tc.exp,
expErr: tc.expErr,
assertions: tc.assertions,
}
et.run(t)
})
}
}
func TestMove(t *testing.T) {
t.Parallel()
testCases := []struct {
skip bool
name string
boardPath []string
text string
fsTexts map[string]string
key string
newKey string
includeDescendants bool
expErr string
exp string
assertions func(t *testing.T, g *d2graph.Graph)
}{
{
name: "basic",
text: `a
`,
key: `a`,
newKey: `b`,
exp: `b
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.JSON(t, len(g.Objects), 1)
assert.JSON(t, g.Objects[0].ID, "b")
},
},
{
name: "basic_nested",
text: `a: {
b
}
`,
key: `a.b`,
newKey: `a.c`,
exp: `a: {
c
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.JSON(t, len(g.Objects), 2)
assert.JSON(t, g.Objects[1].ID, "c")
},
},
{
name: "duplicate",
text: `a: {
b: {
shape: cylinder
}
}
a: {
b: {
shape: cylinder
}
}
`,
key: `a.b`,
newKey: `b`,
exp: `a
a
b: {
shape: cylinder
}
`,
},
{
name: "duplicate_generated",
text: `x
x 2
x 3: {
x 3
x 4
}
x 4
y
`,
key: `x 3`,
newKey: `y.x 3`,
exp: `x
x 2
x 3
x 5
x 4
y: {
x 3
}
`,
},
{
name: "rename_2",
text: `a: {
b 2
y 2
}
b 2
x
`,
key: `a`,
newKey: `x.a`,
exp: `b
y 2
b 2
x: {
a
}
`,
},
{
name: "parentheses",
text: `x -> y (z)
z: ""
`,
key: `"y (z)"`,
newKey: `z.y (z)`,
exp: `x -> z.y (z)
z: ""
`,
},
{
name: "middle_container_generated_conflict",
text: `a.Square.Text 3 -> a.Square.Text 2
a.Square -> a.Text
a: {
Text
Square: {
Text 2
Text 3
}
Square
Text 2
}
`,
key: `a.Square`,
newKey: `Square`,
exp: `a.Text 3 -> a.Text 4
Square -> a.Text
a: {
Text
Text 4
Text 3
Text 2
}
Square
`,
},
{
name: "into_container_existing_map",
text: `a: {
b
}
c
`,
key: `c`,
newKey: `a.c`,
exp: `a: {
b
c
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.JSON(t, len(g.Objects), 3)
assert.JSON(t, "a", g.Objects[0].ID)
assert.JSON(t, 2, len(g.Objects[0].Children))
},
},
{
name: "into_container_with_flat_keys",
text: `a
c: {
style.opacity: 0.4
style.fill: "#FFFFFF"
style.stroke: "#FFFFFF"
}
`,
key: `c`,
newKey: `a.c`,
exp: `a: {
c: {
style.opacity: 0.4
style.fill: "#FFFFFF"
style.stroke: "#FFFFFF"
}
}
`,
},
{
name: "into_container_nonexisting_map",
text: `a
c
`,
key: `c`,
newKey: `a.c`,
exp: `a: {
c
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.JSON(t, len(g.Objects), 2)
assert.JSON(t, "a", g.Objects[0].ID)
assert.JSON(t, 1, len(g.Objects[0].Children))
},
},
{
name: "basic_out_of_container",
text: `a: {
b
}
`,
key: `a.b`,
newKey: `b`,
exp: `a
b
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.JSON(t, len(g.Objects), 2)
assert.JSON(t, "a", g.Objects[0].ID)
assert.JSON(t, 0, len(g.Objects[0].Children))
},
},
{
name: "out_of_newline_container",
text: `"a\n": {
b
}
`,
key: `"a\n".b`,
newKey: `b`,
exp: `"a\n"
b
`,
},
{
name: "partial_slice",
text: `a: {
b
}
a.b
`,
key: `a.b`,
newKey: `b`,
exp: `a
b
`,
},
{
name: "partial_edge_slice",
text: `a: {
b
}
a.b -> c
`,
key: `a.b`,
newKey: `b`,
exp: `a
b -> c
b
`,
},
{
name: "full_edge_slice",
text: `a: {
b: {
c
}
b.c -> d
}
a.b.c -> a.d
`,
key: `a.b.c`,
newKey: `c`,
exp: `a: {
b
_.c -> d
}
c -> a.d
c
`,
},
{
name: "full_slice",
text: `a: {
b: {
c
}
b.c
}
a.b.c
`,
key: `a.b.c`,
newKey: `c`,
exp: `a: {
b
}
c
`,
},
{
name: "slice_style",
text: `a: {
b
}
a.b.icon: https://icons.terrastruct.com/essentials/142-target.svg
`,
key: `a.b`,
newKey: `b`,
exp: `a
a
b
b.icon: https://icons.terrastruct.com/essentials/142-target.svg
`,
},
{
name: "between_containers",
text: `a: {
b
}
c
`,
key: `a.b`,
newKey: `c.b`,
exp: `a
c: {
b
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.JSON(t, len(g.Objects), 3)
assert.JSON(t, "a", g.Objects[0].ID)
assert.JSON(t, 0, len(g.Objects[0].Children))
assert.JSON(t, "c", g.Objects[1].ID)
assert.JSON(t, 1, len(g.Objects[1].Children))
},
},
{
name: "hoist_container_children",
text: `a: {
b
c
}
d
`,
key: `a`,
newKey: `d.a`,
exp: `b
c
d: {
a
}
`,
},
{
name: "middle_container",
text: `x: {
y: {
z
}
}
`,
key: `x.y`,
newKey: `y`,
exp: `x: {
z
}
y
`,
},
{
// a.b does not move from its scope, just extends path
name: "extend_stationary_path",
text: `a.b
a: {
b
c
}
`,
key: `a.b`,
newKey: `a.c.b`,
exp: `a.c.b
a: {
c: {
b
}
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.JSON(t, len(g.Objects), 3)
},
},
{
name: "extend_map",
text: `a.b: {
e
}
a: {
b
c
}
`,
key: `a.b`,
newKey: `a.c.b`,
exp: `a: {
e
}
a: {
c: {
b
}
}
`,
},
{
name: "into_container_with_flat_style",
text: `x.style.border-radius: 5
y
`,
key: `y`,
newKey: `x.y`,
exp: `x: {
style.border-radius: 5
y
}
`,
},
{
name: "flat_between_containers",
text: `a.b
c
`,
key: `a.b`,
newKey: `c.b`,
exp: `a
c: {
b
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.JSON(t, len(g.Objects), 3)
},
},
{
name: "underscore-connection",
text: `a: {
b
_.c.d -> b
}
c: {
d
}
`,
key: `a.b`,
newKey: `c.b`,
exp: `a: {
_.c.d -> _.c.b
}
c: {
d
b
}
`,
},
{
name: "nested-underscore-move-out",
text: `guitar: {
books: {
_._.pipe
}
}
`,
key: `pipe`,
newKey: `guitar.pipe`,
exp: `guitar: {
books
pipe
}
`,
},
{
name: "flat_middle_container",
text: `a.b.c
d
`,
key: `a.b`,
newKey: `d.b`,
exp: `a.c
d: {
b
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.JSON(t, len(g.Objects), 4)
},
},
{
name: "flat_merge",
text: `a.b
c.d: meow
`,
key: `a.b`,
newKey: `c.b`,
exp: `a
c: {
d: meow
b
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
assert.JSON(t, len(g.Objects), 4)
},
},
{
name: "flat_reparent_with_value",
text: `a.b: "yo"
`,
key: `a.b`,
newKey: `b`,
exp: `a
b: "yo"
`,
},
{
name: "flat_reparent_with_map_value",
text: `a.b: {
shape: hexagon
}
`,
key: `a.b`,
newKey: `b`,
exp: `a
b: {
shape: hexagon
}
`,
},
{
name: "flat_reparent_with_mixed_map_value",
text: `a.b: {
# this is reserved
shape: hexagon
# this is not
c
}
`,
key: `a.b`,
newKey: `b`,
exp: `a: {
# this is not
c
}
b: {
# this is reserved
shape: hexagon
}
`,
},
{
name: "flat_style",
text: `a.style.opacity: 0.4
a.style.fill: black
b
`,
key: `a`,
newKey: `b.a`,
exp: `b: {
a.style.opacity: 0.4
a.style.fill: black
}
`,
},
{
name: "flat_nested_merge",
text: `a.b.c.d.e
p.q.b.m.o
`,
key: `a.b.c`,
newKey: `p.q.z`,
exp: `a.b.d.e
p.q: {
b.m.o
z
}
`,
},
{
// We open up only the most nested
name: "flat_nested_merge_multiple_refs",
text: `a: {
b.c.d
e.f
e.g
}
a.b.c
a.b.c.q
`,
key: `a.e`,
newKey: `a.b.c.e`,
exp: `a: {
b.c: {
d
e
}
f
g
}
a.b.c
a.b.c.q
`,
},
{
// TODO
skip: true,
// Choose to move to a reference that is less nested but has an existing map
name: "less_nested_map",
text: `a: {
b: {
c
}
}
a.b.c: {
d
}
e
`,
key: `e`,
newKey: `a.b.c.e`,
exp: `a: {
b: {
c
}
}
a.b.c: {
d
e
}
`,
},
{
name: "invalid-near",
text: `x: {
near: y
}
y
`,
key: `y`,
newKey: `x.y`,
exp: `x: {
near: y
y
}
`,
expErr: `failed to move: "y" to "x.y": failed to recompile:
x: {
near: x.y
y
}
d2/testdata/d2oracle/TestMove/invalid-near.d2:2:9: near keys cannot be set to an descendant`,
},
{
name: "near",
text: `x: {
near: y
}
a
y
`,
key: `y`,
newKey: `a.y`,
exp: `x: {
near: a.y
}
a: {
y
}
`,
},
{
name: "flat_near",
text: `x.near: y
a
y
`,
key: `y`,
newKey: `a.y`,
exp: `x.near: a.y
a: {
y
}
`,
},
{
name: "container_near",
text: `x: {
y: {
near: x.a.b.z
}
a.b.z
}
y
`,
key: `x.a.b`,
newKey: `y.a`,
exp: `x: {
y: {
near: x.a.z
}
a.z
}
y: {
a
}
`,
},
{
name: "nhooyr_one",
text: `a: {
b.c
}
d
`,
key: `a.b`,
newKey: `d.q`,
exp: `a: {
c
}
d: {
q
}
`,
},
{
name: "nhooyr_two",
text: `a: {
b.c -> meow
}
d: {
x
}
`,
key: `a.b`,
newKey: `d.b`,
exp: `a: {
c -> meow
}
d: {
x
b
}
`,
},
{
name: "unique_name",
text: `a: {
b
}
a.b
c: {
b
}
`,
key: `c.b`,
newKey: `a.b`,
exp: `a: {
b
b 2
}
a.b
c
`,
},
{
name: "unique_name_with_references",
text: `a: {
b
}
d -> c.b
c: {
b
}
`,
key: `c.b`,
newKey: `a.b`,
exp: `a: {
b
b 2
}
d -> a.b 2
c
`,
},
{
name: "map_transplant",
text: `a: {
b
style: {
opacity: 0.4
}
c
label: "yo"
}
d
`,
key: `a`,
newKey: `d.a`,
exp: `b
c
d: {
a: {
style: {
opacity: 0.4
}
label: "yo"
}
}
`,
},
{
name: "map_with_label",
text: `a: "yo" {
c
}
d
`,
key: `a`,
newKey: `d.a`,
exp: `c
d: {
a: "yo"
}
`,
},
{
name: "underscore_merge",
text: `a: {
_.b: "yo"
}
b: "what"
c
`,
key: `b`,
newKey: `c.b`,
exp: `a
c: {
b: "yo"
b: "what"
}
`,
},
{
name: "underscore_children",
text: `a: {
_.b
}
b
`,
key: `b`,
newKey: `c`,
exp: `a: {
_.c
}
c
`,
},
{
name: "underscore_transplant",
text: `a: {
b: {
_.c
}
}
`,
key: `a.c`,
newKey: `c`,
exp: `a: {
b
}
c
`,
},
{
name: "underscore_split",
text: `a: {
b: {
_.c.f
}
}
`,
key: `a.c`,
newKey: `c`,
exp: `a: {
b: {
_.f
}
}
c
`,
},
{
name: "underscore_edge_container_1",
text: `a: {
_.b -> c
}
`,
key: `b`,
newKey: `a.b`,
exp: `a: {
b -> c
}
`,
},
{
name: "underscore_edge_container_2",
text: `a: {
_.b -> c
}
`,
key: `b`,
newKey: `a.c.b`,
exp: `a: {
c.b -> c
}
`,
},
{
name: "underscore_edge_container_3",
text: `a: {
_.b -> c
}
`,
key: `b`,
newKey: `d`,
exp: `a: {
_.d -> c
}
`,
},
{
name: "underscore_edge_container_4",
text: `a: {
_.b -> c
}
`,
key: `b`,
newKey: `a.f`,
exp: `a: {
f -> c
}
`,
},
{
name: "underscore_edge_container_5",
text: `a: {
_.b -> _.c
}
`,
key: `b`,
newKey: `c.b`,
exp: `a: {
_.c.b -> _.c
}
`,
},
{
name: "underscore_edge_container_6",
text: `x: {
_.y.a -> _.y.b
}
`,
key: `y`,
newKey: `x.y`,
includeDescendants: true,
exp: `x: {
y.a -> y.b
}
`,
},
{
name: "underscore_edge_split",
text: `a: {
b: {
_.c.f -> yo
}
}
`,
key: `a.c`,
newKey: `c`,
exp: `a: {
b: {
_.f -> yo
}
}
c
`,
},
{
name: "underscore_split_out",
text: `a: {
b: {
_.c.f
}
c: {
e
}
}
`,
key: `a.c.f`,
newKey: `a.c.e.f`,
exp: `a: {
b: {
_.c
}
c: {
e: {
f
}
}
}
`,
},
{
name: "underscore_edge_children",
text: `a: {
_.b -> c
}
b
`,
key: `b`,
newKey: `c`,
exp: `a: {
_.c -> c
}
c
`,
},
{
name: "move_container_children",
text: `b: {
p
q
}
a
d
`,
key: `b`,
newKey: `d.b`,
exp: `p
q
a
d: {
b
}
`,
},
{
name: "move_container_conflict_children",
text: `x: {
a
b
}
a
d
`,
key: `x`,
newKey: `d.x`,
exp: `a 2
b
a
d: {
x
}
`,
},
{
name: "edge_conflict",
text: `x.y.a -> x.y.b
y
`,
key: `x`,
newKey: `y.x`,
exp: `y 2.a -> y 2.b
y: {
x
}
`,
},
{
name: "edge_basic",
text: `a -> b
`,
key: `a`,
newKey: `c`,
exp: `c -> b
`,
},
{
name: "edge_nested_basic",
text: `a: {
b -> c
}
`,
key: `a.b`,
newKey: `a.d`,
exp: `a: {
d -> c
}
`,
},
{
name: "edge_into_container",
text: `a: {
d
}
b -> c
`,
key: `b`,
newKey: `a.b`,
exp: `a: {
d
}
a.b -> c
`,
},
{
name: "edge_out_of_container",
text: `a: {
b -> c
}
`,
key: `a.b`,
newKey: `b`,
exp: `a: {
_.b -> c
}
`,
},
{
name: "connected_nested",
text: `x -> y.z
`,
key: `y.z`,
newKey: `z`,
exp: `x -> z
y
`,
},
{
name: "chain_connected_nested",
text: `y.z -> x -> y.z
`,
key: `y.z`,
newKey: `z`,
exp: `z -> x -> z
y
`,
},
{
name: "chain_connected_nested_no_extra_create",
text: `y.b -> x -> y.z
`,
key: `y.z`,
newKey: `z`,
exp: `y.b -> x -> z
`,
},
{
name: "edge_across_containers",
text: `a: {
b -> c
}
d
`,
key: `a.b`,
newKey: `d.b`,
exp: `a: {
_.d.b -> c
}
d
`,
},
{
name: "move_out_of_edge",
text: `a.b.c -> d.e.f
`,
key: `a.b`,
newKey: `q`,
exp: `a.c -> d.e.f
q
`,
},
{
name: "move_out_of_nested_edge",
text: `a.b.c -> d.e.f
`,
key: `a.b`,
newKey: `d.e.q`,
exp: `a.c -> d.e.f
d.e: {
q
}
`,
},
{
name: "append_multiple_styles",
text: `a: {
style: {
opacity: 0.4
}
}
a: {
style: {
fill: "red"
}
}
d
`,
key: `a`,
newKey: `d.a`,
exp: `d: {
a: {
style: {
opacity: 0.4
}
}
a: {
style: {
fill: "red"
}
}
}
`,
},
{
name: "move_into_key_with_value",
text: `a: meow
b
`,
key: `b`,
newKey: `a.b`,
exp: `a: meow {
b
}
`,
},
{
name: "gnarly_1",
text: `a.b.c -> d.e.f
b: meow {
p: "eyy"
q
p.p -> q.q
}
b.p.x -> d
`,
key: `b`,
newKey: `d.b`,
exp: `a.b.c -> d.e.f
d: {
b: meow
}
p: "eyy"
q
p.p -> q.q
p.x -> d
`,
},
{
name: "reuse_map",
text: `a: {
b: {
hey
}
b.yo
}
k
`,
key: `k`,
newKey: `a.b.k`,
exp: `a: {
b: {
hey
k
}
b.yo
}
`,
},
{
// TODO the heuristic for splitting open new maps should be only if the key has no existing maps and it also has either zero or one children. if it has two children or more then we should not be opening a map and just append the key at the most nested map.
// first loop over explicit references from first to last.
//
// explicit ref means its the leaf disregarding reserved fields.
// implicit ref means there is a shape declared after the target element.
//
// then loop over the implicit references and only if there is no explicit ref do you need to add the implicit ref to the scope but only if appended == false (which would be set when looping through explicit refs).
skip: true,
name: "merge_nested_flat",
text: `a: {
b.c
b.d
b.e.g
}
k
`,
key: `k`,
newKey: `a.b.k`,
exp: `a: {
b.c
b.d
b.e.g
b.k
}
`,
},
{
name: "merge_nested_maps",
text: `a: {
b.c
b.d
b.e.g
b.d: {
o
}
}
k
`,
key: `k`,
newKey: `a.b.k`,
exp: `a: {
b.c
b.d
b.e.g
b: {
d: {
o
}
k
}
}
`,
},
{
name: "merge_reserved",
text: `a: {
b.c
b.label: "yo"
b.label: "hi"
b.e.g
}
k
`,
key: `k`,
newKey: `a.b.k`,
exp: `a: {
b.c
b.label: "yo"
b.label: "hi"
b: {
e.g
k
}
}
`,
},
{
name: "multiple_nesting_levels",
text: `a: {
b: {
c
c.g
}
b.c.d
x
}
a.b.c.f
`,
key: `a.x`,
newKey: `a.b.c.x`,
exp: `a: {
b: {
c
c: {
g
x
}
}
b.c.d
}
a.b.c.f
`,
},
{
name: "edge_chain_basic",
text: `a -> b -> c
`,
key: `a`,
newKey: `d`,
exp: `d -> b -> c
`,
},
{
name: "edge_chain_into_container",
text: `a -> b -> c
d
`,
key: `a`,
newKey: `d.a`,
exp: `d.a -> b -> c
d
`,
},
{
name: "edge_chain_out_container",
text: `a: {
b -> c -> d
}
`,
key: `a.c`,
newKey: `c`,
exp: `a: {
b -> _.c -> d
}
`,
},
{
name: "edge_chain_circular",
text: `a: {
b -> c -> b
}
`,
key: `a.b`,
newKey: `b`,
exp: `a: {
_.b -> c -> _.b
}
`,
},
{
name: "container_multiple_refs_with_underscore",
text: `a
b: {
_.a
}
`,
key: `a`,
newKey: `b.a`,
exp: `b: {
a
}
`,
},
{
name: "container_conflicts_generated",
text: `Square 2: "" {
Square: ""
}
Square: ""
Square 3
`,
key: `Square 2`,
newKey: `Square 3.Square 2`,
exp: `Square 2: ""
Square: ""
Square 3: {
Square 2: ""
}
`,
},
{
name: "include_descendants_flat_1",
text: `x.y
z
`,
key: `x`,
newKey: `z.x`,
includeDescendants: true,
exp: `z: {
x.y
}
`,
},
{
name: "include_descendants_flat_2",
text: `a.x.y
a.z
`,
key: `a.x`,
newKey: `a.z.x`,
includeDescendants: true,
exp: `a
a.z: {
x.y
}
`,
},
{
name: "include_descendants_flat_3",
text: `a.x.y
a.z
`,
key: `a.x`,
newKey: `x`,
includeDescendants: true,
exp: `a
a.z
x.y
`,
},
{
name: "include_descendants_flat_4",
text: `a.x.y
a.z
`,
key: `a.x.y`,
newKey: `y`,
includeDescendants: true,
exp: `a.x
a.z
y
`,
},
{
name: "include_descendants_map_1",
text: `x: {
y
}
z
`,
key: `x`,
newKey: `z.x`,
includeDescendants: true,
exp: `z: {
x: {
y
}
}
`,
},
{
name: "include_descendants_map_2",
text: `x: {
y: {
c
}
y.b
}
x.y.b
z
`,
key: `x.y`,
newKey: `a`,
includeDescendants: true,
exp: `x
x
z
a: {
c
}
a.b
`,
},
{
name: "include_descendants_grandchild",
text: `x: {
y.a
y: {
b
}
}
z
`,
key: `x`,
newKey: `z.x`,
includeDescendants: true,
exp: `z: {
x: {
y.a
y: {
b
}
}
}
`,
},
{
name: "include_descendants_sql",
text: `x: {
shape: sql_table
a: b
}
z
`,
key: `x`,
newKey: `z.x`,
includeDescendants: true,
exp: `z: {
x: {
shape: sql_table
a: b
}
}
`,
},
{
name: "include_descendants_edge_child",
text: `x: {
a -> b
}
z
`,
key: `x`,
newKey: `z.x`,
includeDescendants: true,
exp: `z: {
x: {
a -> b
}
}
`,
},
{
name: "include_descendants_edge_ref_1",
text: `x
z
x.a -> x.b
`,
key: `x`,
newKey: `z.x`,
includeDescendants: true,
exp: `z: {
x
}
z.x.a -> z.x.b
`,
},
{
name: "include_descendants_edge_ref_2",
text: `x -> y.z
`,
key: `y.z`,
newKey: `z`,
includeDescendants: true,
exp: `x -> z
y
`,
},
{
name: "include_descendants_edge_ref_3",
text: `x -> y.z.a
`,
key: `y.z`,
newKey: `z`,
includeDescendants: true,
exp: `x -> z.a
y
`,
},
{
name: "include_descendants_edge_ref_4",
text: `x -> y.z.a
b
`,
key: `y.z`,
newKey: `b.z`,
includeDescendants: true,
exp: `x -> b.z.a
b
y
`,
},
{
name: "include_descendants_edge_ref_5",
text: `foo: {
x -> y.z.a
b
}
`,
key: `foo.y.z`,
newKey: `foo.b.z`,
includeDescendants: true,
exp: `foo: {
x -> b.z.a
b
y
}
`,
},
{
name: "include_descendants_edge_ref_6",
text: `x -> y
z
`,
key: `y`,
newKey: `z.y`,
includeDescendants: true,
exp: `x -> z.y
z
`,
},
{
name: "include_descendants_edge_ref_7",
text: `d.t -> d.np.s
`,
key: `d.np.s`,
newKey: `d.s`,
includeDescendants: true,
exp: `d.t -> d.s
d.np
`,
},
{
name: "include_descendants_nested_1",
text: `y.z
b
`,
key: `y.z`,
newKey: `b.z`,
includeDescendants: true,
exp: `y
b: {
z
}
`,
},
{
name: "include_descendants_nested_2",
text: `y.z
y.b
`,
key: `y.z`,
newKey: `y.b.z`,
includeDescendants: true,
exp: `y
y.b: {
z
}
`,
},
{
name: "include_descendants_underscore",
text: `github.code -> local.dev
github: {
_.local.dev -> _.aws.workflows
_.aws: {
workflows
}
}
`,
key: `aws.workflows`,
newKey: `github.workflows`,
includeDescendants: true,
exp: `github.code -> local.dev
github: {
_.local.dev -> workflows
_.aws
workflows
}
`,
},
{
name: "include_descendants_underscore_2",
text: `a: {
b: {
_.c
}
}
`,
key: `a.b`,
newKey: `b`,
includeDescendants: true,
exp: `a
b: {
_.a.c
}
`,
},
{
name: "include_descendants_underscore_3",
text: `a: {
b: {
_.c -> d
_.c -> _.d
}
}
`,
key: `a.b`,
newKey: `b`,
includeDescendants: true,
exp: `a
b: {
_.a.c -> d
_.a.c -> _.a.d
}
`,
},
{
name: "include_descendants_edge_ref_underscore",
text: `x
z
x.a -> x.b
b: {
_.x.a -> _.x.b
}
`,
key: `x`,
newKey: `z.x`,
includeDescendants: true,
exp: `z: {
x
}
z.x.a -> z.x.b
b: {
_.z.x.a -> _.z.x.b
}
`,
},
{
name: "include_descendants_near",
text: `x.y
z
a.near: x.y
`,
key: `x`,
newKey: `z.x`,
includeDescendants: true,
exp: `z: {
x.y
}
a.near: z.x.y
`,
},
{
name: "include_descendants_conflict",
text: `x.y
z.x
`,
key: `x`,
newKey: `z.x`,
includeDescendants: true,
exp: `z: {
x
x 2.y
}
`,
},
{
name: "include_descendants_non_conflict",
text: `x.y
z.x
y
`,
key: `x`,
newKey: `z.x`,
includeDescendants: true,
exp: `z: {
x
x 2.y
}
y
`,
},
{
name: "nested_reserved_2",
text: `A.B.C.shape: circle
`,
key: `A.B.C`,
newKey: `C`,
exp: `A.B
C.shape: circle
`,
},
{
name: "nested_reserved_3",
text: `A.B.C.shape: circle
A.B: {
C
D
}
`,
key: `A.B.C`,
newKey: `A.B.D.C`,
exp: `A.B
A.B: {
D: {
C.shape: circle
C
}
}
`,
},
{
name: "include_descendants_nested_reserved_2",
text: `A.B.C.shape: circle
`,
key: `A.B.C`,
newKey: `C`,
includeDescendants: true,
exp: `A.B
C.shape: circle
`,
},
{
name: "include_descendants_nested_reserved_3",
text: `A.B.C.shape: circle
`,
key: `A.B`,
newKey: `C`,
includeDescendants: true,
exp: `A
C.C.shape: circle
`,
},
{
name: "include_descendants_move_out",
text: `a.b: {
c: {
d
}
}
`,
key: `a.b`,
newKey: `b`,
includeDescendants: true,
exp: `a
b: {
c: {
d
}
}
`,
},
{
name: "include_descendants_underscore_regression",
text: `x: {
_.a
}
a
`,
key: `a`,
newKey: `x.a`,
includeDescendants: true,
exp: `x: {
a
}
`,
},
{
name: "include_descendants_underscore_regression_2",
text: `x: {
_.a.b
}
`,
key: `a`,
newKey: `x.a`,
includeDescendants: true,
exp: `x: {
a.b
}
`,
},
{
name: "layers-basic",
text: `a
layers: {
x: {
b
c
}
}
`,
key: `c`,
newKey: `b.c`,
boardPath: []string{"x"},
exp: `a
layers: {
x: {
b: {
c
}
}
}
`,
},
{
name: "scenarios-out-of-scope",
text: `a
scenarios: {
x: {
b
c
}
}
`,
key: `a`,
newKey: `b.a`,
boardPath: []string{"x"},
expErr: `failed to move: "a" to "b.a": operation would modify AST outside of given scope`,
},
}
for _, tc := range testCases {
if tc.skip {
continue
}
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
et := editTest{
text: tc.text,
fsTexts: tc.fsTexts,
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
objectsBefore := len(g.Objects)
var err error
g, err = d2oracle.Move(g, tc.boardPath, tc.key, tc.newKey, tc.includeDescendants)
if err == nil {
objectsAfter := len(g.Objects)
if objectsBefore != objectsAfter {
t.Log(d2format.Format(g.AST))
return nil, fmt.Errorf("move cannot destroy or create objects: found %d objects before and %d objects after", objectsBefore, objectsAfter)
}
}
return g, err
},
exp: tc.exp,
expErr: tc.expErr,
assertions: tc.assertions,
}
et.run(t)
})
}
}
func TestDelete(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
boardPath []string
text string
fsTexts map[string]string
key string
expErr string
exp string
assertions func(t *testing.T, g *d2graph.Graph)
}{
{
name: "flat",
text: `nerve-gift-earther
`,
key: `nerve-gift-earther`,
exp: ``,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 0 {
t.Fatalf("expected zero objects: %#v", g.Objects)
}
},
},
{
name: "edge_identical_child",
text: `x.x.y.z -> x.y.b
`,
key: `x`,
exp: `x.y.z -> y.b
`,
},
{
name: "duplicate_generated",
text: `x
x 2
x 3: {
x 3
x 4
}
x 4
y
`,
key: `x 3`,
exp: `x
x 2
x 3
x 5
x 4
y
`,
},
{
name: "table_refs",
text: `a: {
shape: sql_table
b
}
c: {
shape: sql_table
d
}
a.b
a.b -> c.d
`,
key: `a`,
exp: `c: {
shape: sql_table
d
}
c.d
`,
},
{
name: "class_refs",
text: `a: {
shape: class
b: int
}
a.b
`,
key: `a`,
exp: ``,
},
{
name: "edge_both_identical_childs",
text: `x.x.y.z -> x.x.b
`,
key: `x`,
exp: `x.y.z -> x.b
`,
},
{
name: "edge_conflict",
text: `x.y.a -> x.y.b
y
`,
key: `x`,
exp: `y 2.a -> y 2.b
y
`,
},
{
name: "underscore_remove",
text: `x: {
_.y
_.a -> _.b
_.c -> d
}
`,
key: `x`,
exp: `y
a -> b
c -> d
`,
},
{
name: "underscore_linked",
text: `k
layers: {
x: {
a
b: {link: _}
}
}
`,
key: `b`,
boardPath: []string{"x"},
exp: `k
layers: {
x: {
a
}
}
`,
},
{
name: "underscore_no_conflict",
text: `x: {
y: {
_._.z
}
z
}
`,
key: `x.y`,
exp: `x: {
_.z
z
}
`,
},
{
name: "nested_underscore_update",
text: `guitar: {
books: {
_._.pipe
}
}
`,
key: `guitar`,
exp: `books: {
_.pipe
}
`,
},
{
name: "only-underscore",
text: `guitar: {
books: {
_._.pipe
}
}
`,
key: `pipe`,
exp: `guitar: {
books
}
`,
},
{
name: "only-underscore-nested",
text: `guitar: {
books: {
_._.pipe: {
a
}
}
}
`,
key: `pipe`,
exp: `guitar: {
books
}
a
`,
},
{
name: "node_in_edge",
text: `x -> y -> z -> q -> p
z.ok: {
what's up
}
`,
key: `z`,
exp: `x -> y
q -> p
ok: {
what's up
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 6 {
t.Fatalf("expected 6 objects: %#v", g.Objects)
}
if len(g.Edges) != 2 {
t.Fatalf("expected two edges: %#v", g.Edges)
}
},
},
{
name: "node_in_edge_last",
text: `x -> y -> z -> q -> a.b.p
a.b.p: {
what's up
}
`,
key: `a.b.p`,
exp: `x -> y -> z -> q
a.b: {
what's up
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 7 {
t.Fatalf("expected 7 objects: %#v", g.Objects)
}
if len(g.Edges) != 3 {
t.Fatalf("expected three edges: %#v", g.Edges)
}
},
},
{
name: "children",
text: `p: {
what's up
x -> y
}
`,
key: `p`,
exp: `what's up
x -> y
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 3 {
t.Fatalf("expected 3 objects: %#v", g.Objects)
}
if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
},
},
{
name: "hoist_children",
text: `a: {
b: {
c
}
}
`,
key: `a.b`,
exp: `a: {
c
}
`,
},
{
name: "hoist_edge_children",
text: `a: {
b
c -> d
}
`,
key: `a`,
exp: `b
c -> d
`,
},
{
name: "children_conflicts",
text: `p: {
x
}
x
`,
key: `p`,
exp: `x 2
x
`,
},
{
name: "edge_map_style",
text: `x -> y: { style.stroke: red }
`,
key: `(x -> y)[0].style.stroke`,
exp: `x -> y
`,
},
{
// Just checks that removing an object removes the arrowhead field too
name: "breakup_arrowhead",
text: `x -> y: {
target-arrowhead.shape: diamond
}
(x -> y)[0].source-arrowhead: {
shape: diamond
}
`,
key: `x`,
exp: `y
`,
},
{
name: "arrowhead",
text: `x -> y: {
target-arrowhead.shape: diamond
}
`,
key: `(x -> y)[0].target-arrowhead`,
exp: `x -> y
`,
},
{
name: "arrowhead_shape",
text: `x -> y: {
target-arrowhead.shape: diamond
}
`,
key: `(x -> y)[0].target-arrowhead.shape`,
exp: `x -> y
`,
},
{
name: "arrowhead_label",
text: `x -> y: {
target-arrowhead.shape: diamond
target-arrowhead.label: 1
}
`,
key: `(x -> y)[0].target-arrowhead.label`,
exp: `x -> y: {
target-arrowhead.shape: diamond
}
`,
},
{
name: "arrowhead_map",
text: `x -> y: {
target-arrowhead: {
shape: diamond
}
}
`,
key: `(x -> y)[0].target-arrowhead.shape`,
exp: `x -> y
`,
},
{
name: "edge-only-style",
text: `x -> y: {
style.stroke: red
}
`,
key: `(x -> y)[0].style.stroke`,
exp: `x -> y
`,
},
{
name: "edge_key_style",
text: `x -> y
(x -> y)[0].style.stroke: red
`,
key: `(x -> y)[0].style.stroke`,
exp: `x -> y
`,
},
{
name: "nested_edge_key_style",
text: `a: {
x -> y
}
a.(x -> y)[0].style.stroke: red
`,
key: `a.(x -> y)[0].style.stroke`,
exp: `a: {
x -> y
}
`,
},
{
name: "multiple_flat_style",
text: `x.style.opacity: 0.4
x.style.fill: red
`,
key: `x.style.fill`,
exp: `x.style.opacity: 0.4
`,
},
{
name: "edge_flat_style",
text: `A -> B
A.style.stroke-dash: 5
`,
key: `A`,
exp: `B
`,
},
{
name: "flat_reserved",
text: `A -> B
A.style.stroke-dash: 5
`,
key: `A.style.stroke-dash`,
exp: `A -> B
`,
},
{
name: "singular_flat_style",
text: `x.style.fill: red
`,
key: `x.style.fill`,
exp: `x
`,
},
{
name: "nested_flat_style",
text: `x: {
style.fill: red
}
`,
key: `x.style.fill`,
exp: `x
`,
},
{
name: "multiple_map_styles",
text: `x: {
style: {
opacity: 0.4
fill: red
}
}
`,
key: `x.style.fill`,
exp: `x: {
style: {
opacity: 0.4
}
}
`,
},
{
name: "singular_map_style",
text: `x: {
style: {
fill: red
}
}
`,
key: `x.style.fill`,
exp: `x
`,
},
{
name: "delete_near",
text: `x: {
near: y
}
y
`,
key: `x.near`,
exp: `x
y
`,
},
{
name: "delete_container_of_near",
text: `direction: down
first input -> start game -> game loop
game loop: {
direction: down
input -> increase bird top velocity
move bird -> move pipes -> render
render -> no collision -> wait 16 milliseconds -> move bird
render -> collision detected -> game over
no collision.near: game loop.collision detected
}
`,
key: `game loop`,
exp: `direction: down
first input -> start game
input -> increase bird top velocity
move bird -> move pipes -> render
render -> no collision -> wait 16 milliseconds -> move bird
render -> collision detected -> game over
no collision.near: collision detected
`,
},
{
name: "delete_tooltip",
text: `x: {
tooltip: yeah
}
`,
key: `x.tooltip`,
exp: `x
`,
},
{
name: "delete_link",
text: `x.link: https://google.com
`,
key: `x.link`,
exp: `x
`,
},
{
name: "delete_icon",
text: `y.x: {
link: https://google.com
icon: https://google.com/memes.jpeg
}
`,
key: `y.x.icon`,
exp: `y.x: {
link: https://google.com
}
`,
},
{
name: "delete_redundant_flat_near",
text: `x
y
`,
key: `x.near`,
exp: `x
y
`,
},
{
name: "delete_needed_flat_near",
text: `x.near: y
y
`,
key: `x.near`,
exp: `x
y
`,
},
{
name: "children_no_self_conflict",
text: `x: {
x
}
`,
key: `x`,
exp: `x
`,
},
{
name: "near",
text: `x: {
near: y
}
y
`,
key: `y`,
exp: `x
`,
},
{
name: "container_near",
text: `x: {
y: {
near: x.z
}
z
a: {
near: x.z
}
}
`,
key: `x`,
exp: `y: {
near: z
}
z
a: {
near: z
}
`,
},
{
name: "multi_near",
text: `Starfish: {
API
Bluefish: {
near: Starfish.API
}
Yo: {
near: Blah
}
}
Blah
`,
key: `Starfish`,
exp: `API
Bluefish: {
near: API
}
Yo: {
near: Blah
}
Blah
`,
},
{
name: "children_nested_conflicts",
text: `p: {
x: {
y
}
}
x
`,
key: `p`,
exp: `x 2: {
y
}
x
`,
},
{
name: "children_referenced_conflicts",
text: `p: {
x
}
x
p.x: "hi"
`,
key: `p`,
exp: `x 2
x
x 2: "hi"
`,
},
{
name: "children_flat_conflicts",
text: `p.x
x
p.x: "hi"
`,
key: `p`,
exp: `x 2
x
x 2: "hi"
`,
},
{
name: "children_edges_flat_conflicts",
text: `p.x -> p.y -> p.z
x
z
p.x: "hi"
p.z: "ey"
`,
key: `p`,
exp: `x 2 -> y -> z 2
x
z
x 2: "hi"
z 2: "ey"
`,
},
{
name: "children_nested_referenced_conflicts",
text: `p: {
x.y
}
x
p.x: "hi"
p.x.y: "hey"
`,
key: `p`,
exp: `x 2.y
x
x 2: "hi"
x 2.y: "hey"
`,
},
{
name: "children_edge_conflicts",
text: `p: {
x -> y
}
x
p.x: "hi"
`,
key: `p`,
exp: `x 2 -> y
x
x 2: "hi"
`,
},
{
name: "children_multiple_conflicts",
text: `p: {
x -> y
x
y
}
x
y
p.x: "hi"
`,
key: `p`,
exp: `x 2 -> y 2
x 2
y 2
x
y
x 2: "hi"
`,
},
{
name: "multi_path_map_conflict",
text: `x.y: {
z
}
x: {
z
}
`,
key: `x.y`,
exp: `x: {
z 2
}
x: {
z
}
`,
},
{
name: "multi_path_map_no_conflict",
text: `x.y: {
z
}
x: {
z
}
`,
key: `x`,
exp: `y: {
z
}
z
`,
},
{
name: "children_scope",
text: `x.q: {
p: {
what's up
x -> y
}
}
`,
key: `x.q.p`,
exp: `x.q: {
what's up
x -> y
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 5 {
t.Fatalf("expected 5 objects: %#v", g.Objects)
}
if len(g.Edges) != 1 {
t.Fatalf("expected 1 edge: %#v", g.Edges)
}
},
},
{
name: "children_order",
text: `c: {
before
y: {
congo
}
after
}
`,
key: `c.y`,
exp: `c: {
before
congo
after
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 4 {
t.Fatalf("expected 4 objects: %#v", g.Objects)
}
},
},
{
name: "edge_first",
text: `l.p.d: {x -> p -> y -> z}
`,
key: `l.p.d.(x -> p)[0]`,
exp: `l.p.d: {x; p -> y -> z}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 7 {
t.Fatalf("expected 7 objects: %#v", g.Objects)
}
if len(g.Edges) != 2 {
t.Fatalf("unexpected edges: %#v", g.Objects)
}
},
},
{
name: "multiple_flat_middle_container",
text: `a.b.c
a.b.d
`,
key: `a.b`,
exp: `a.c
a.d
`,
},
{
name: "edge_middle",
text: `l.p.d: {x -> y -> z -> q -> p}
`,
key: `l.p.d.(z -> q)[0]`,
exp: `l.p.d: {x -> y -> z; q -> p}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 8 {
t.Fatalf("expected 8 objects: %#v", g.Objects)
}
if len(g.Edges) != 3 {
t.Fatalf("expected three edges: %#v", g.Edges)
}
},
},
{
name: "edge_last",
text: `l.p.d: {x -> y -> z -> q -> p}
`,
key: `l.p.d.(q -> p)[0]`,
exp: `l.p.d: {x -> y -> z -> q; p}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 8 {
t.Fatalf("expected 8 objects: %#v", g.Objects)
}
if len(g.Edges) != 3 {
t.Fatalf("expected three edges: %#v", g.Edges)
}
},
},
{
name: "key_with_edges",
text: `hello.meow -> hello.bark
`,
key: `hello.(meow -> bark)[0]`,
exp: `hello.meow
hello.bark
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 3 {
t.Fatalf("expected three objects: %#v", g.Objects)
}
if len(g.Edges) != 0 {
t.Fatalf("expected zero edges: %#v", g.Edges)
}
},
},
{
name: "key_with_edges_2",
text: `hello.meow -> hello.bark
`,
key: `hello.meow`,
exp: `hello.bark
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
},
},
{
name: "key_with_edges_3",
text: `hello.(meow -> bark)
`,
key: `hello.meow`,
exp: `hello.bark
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
},
},
{
name: "key_with_edges_4",
text: `hello.(meow -> bark)
`,
key: `(hello.meow -> hello.bark)[0]`,
exp: `hello.meow
hello.bark
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 3 {
t.Fatalf("expected three objects: %#v", g.Objects)
}
if len(g.Edges) != 0 {
t.Fatalf("expected zero edges: %#v", g.Edges)
}
},
},
{
name: "nested",
text: `a.b.c.d
`,
key: `a.b`,
exp: `a.c.d
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 3 {
t.Fatalf("expected 3 objects: %#v", g.Objects)
}
},
},
{
name: "nested_2",
text: `a.b.c.d
`,
key: `a.b.c.d`,
exp: `a.b.c
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 3 {
t.Fatalf("expected 3 objects: %#v", g.Objects)
}
},
},
{
name: "order_1",
text: `x -> p -> y -> z
`,
key: `p`,
exp: `x
y -> z
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 3 {
t.Fatalf("expected 3 objects: %#v", g.Objects)
}
},
},
{
name: "order_2",
text: `p -> y -> z
`,
key: `y`,
exp: `p
z
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
},
},
{
name: "order_3",
text: `y -> p -> y -> z
`,
key: `y`,
exp: `p
z
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
},
},
{
name: "order_4",
text: `y -> p
`,
key: `p`,
exp: `y
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 1 {
t.Fatalf("expected 1 object: %#v", g.Objects)
}
},
},
{
name: "order_5",
text: `x: {
a -> b -> c
q -> p
}
`,
key: `x.a`,
exp: `x: {
b -> c
q -> p
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 5 {
t.Fatalf("expected 5 objects: %#v", g.Objects)
}
},
},
{
name: "order_6",
text: `x: {
lol
}
x.p.q.z
`,
key: `x.p.q.z`,
exp: `x: {
lol
}
x.p.q
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 4 {
t.Fatalf("expected 4 objects: %#v", g.Objects)
}
},
},
{
name: "order_7",
text: `x: {
lol
}
x.p.q.more
x.p.q.z
`,
key: `x.p.q.z`,
exp: `x: {
lol
}
x.p.q.more
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 5 {
t.Fatalf("expected 5 objects: %#v", g.Objects)
}
},
},
{
name: "order_8",
text: `x -> y
bark
y -> x
zebra
x -> q
kang
`,
key: `x`,
exp: `bark
y
zebra
q
kang
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 5 {
t.Fatalf("expected 5 objects: %#v", g.Objects)
}
},
},
{
name: "empty_map",
text: `c: {
y: {
congo
}
}
`,
key: `c.y.congo`,
exp: `c: {
y
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
},
},
{
name: "edge_common",
text: `x.a -> x.y
`,
key: "x",
exp: `a -> y
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
if len(g.Edges) != 1 {
t.Fatalf("unexpected edges: %#v", g.Edges)
}
},
},
{
name: "edge_common_2",
text: `x.(a -> y)
`,
key: "x",
exp: `a -> y
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 2 {
t.Fatalf("expected 2 objects: %#v", g.Objects)
}
if len(g.Edges) != 1 {
t.Fatalf("unexpected edges: %#v", g.Edges)
}
},
},
{
name: "edge_common_3",
text: `x.(a -> y)
`,
key: "(x.a -> x.y)[0]",
exp: `x.a
x.y
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 3 {
t.Fatalf("expected 3 objects: %#v", g.Objects)
}
if len(g.Edges) != 0 {
t.Fatalf("unexpected edges: %#v", g.Edges)
}
},
},
{
name: "edge_common_4",
text: `x.a -> x.y
`,
key: "x.(a -> y)[0]",
exp: `x.a
x.y
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
if len(g.Objects) != 3 {
t.Fatalf("expected 3 objects: %#v", g.Objects)
}
if len(g.Edges) != 0 {
t.Fatalf("unexpected edges: %#v", g.Edges)
}
},
},
{
name: "edge_decrement",
text: `a -> b
a -> b
a -> b
a -> b
a -> b
(a -> b)[0]: zero
(a -> b)[1]: one
(a -> b)[2]: two
(a -> b)[3]: three
(a -> b)[4]: four
`,
key: `(a -> b)[2]`,
exp: `a -> b
a -> b
a -> b
a -> b
(a -> b)[0]: zero
(a -> b)[1]: one
(a -> b)[2]: three
(a -> b)[3]: four
`,
},
{
name: "shape_class",
text: `D2 Parser: {
shape: class
# Default visibility is + so no need to specify.
+reader: io.RuneReader
readerPos: d2ast.Position
# Private field.
-lookahead: "[]rune"
# Protected field.
# We have to escape the # to prevent the line from being parsed as a comment.
\#lookaheadPos: d2ast.Position
+peek(): (r rune, eof bool)
rewind()
commit()
\#peekn(n int): (s string, eof bool)
}
"github.com/terrastruct/d2parser.git" -> D2 Parser
`,
key: `D2 Parser`,
exp: `"github.com/terrastruct/d2parser.git"
`,
},
// TODO: delete disks.id as it's redundant
{
name: "shape_sql_table",
text: `cloud: {
disks: {
shape: sql_table
id: int {constraint: primary_key}
}
blocks: {
shape: sql_table
id: int {constraint: primary_key}
disk: int {constraint: foreign_key}
blob: blob
}
blocks.disk -> disks.id
AWS S3 Vancouver -> disks
}
`,
key: "cloud.blocks",
exp: `cloud: {
disks: {
shape: sql_table
id: int {constraint: primary_key}
}
disks.id
AWS S3 Vancouver -> disks
}
`,
},
{
name: "nested_reserved",
text: `x.y.z: {
label: Sweet April showers do spring May flowers.
icon: bingo
near: x.y.jingle
shape: parallelogram
style: {
stroke: red
}
}
x.y.jingle
`,
key: "x.y.z",
exp: `x.y
x.y.jingle
`,
},
{
name: "only_delete_obj_reserved",
text: `A: {style.stroke: "#000e3d"}
B
A -> B: {style.stroke: "#2b50c2"}
`,
key: `A.style.stroke`,
exp: `A
B
A -> B: {style.stroke: "#2b50c2"}
`,
},
{
name: "only_delete_edge_reserved",
text: `A: {style.stroke: "#000e3d"}
B
A -> B: {style.stroke: "#2b50c2"}
`,
key: `(A->B)[0].style.stroke`,
exp: `A: {style.stroke: "#000e3d"}
B
A -> B
`,
},
{
name: "width",
text: `x: {
width: 200
}
`,
key: `x.width`,
exp: `x
`,
},
{
name: "left",
text: `x: {
left: 200
}
`,
key: `x.left`,
exp: `x
`,
},
{
name: "conflicts_generated",
text: `Text 4
Square: {
Text 4: {
Text 2
}
Text
}
`,
key: `Square`,
exp: `Text 4
Text 2: {
Text 2
}
Text
`,
},
{
name: "conflicts_generated_continued",
text: `Text 4
Text: {
Text 2
}
Text 2
`,
key: `Text`,
exp: `Text 4
Text
Text 2
`,
},
{
name: "conflicts_generated_3",
text: `x: {
Square 2
Square 3
}
Square 2
Square
`,
key: `x`,
exp: `Square 4
Square 3
Square 2
Square
`,
},
{
name: "drop_value",
text: `a.b.c: "c label"
`,
key: `a.b.c`,
exp: `a.b
`,
},
{
name: "drop_value_with_primary",
text: `a.b: hello {
shape: circle
}
`,
key: `a.b`,
exp: `a
`,
},
{
name: "save_map",
text: `a.b: {
shape: circle
}
`,
key: `a`,
exp: `b: {
shape: circle
}
`,
},
{
name: "save_map_with_primary",
text: `a.b: hello {
shape: circle
}
`,
key: `a`,
exp: `b: hello {
shape: circle
}
`,
},
{
name: "chaos_1",
text: `cm: {shape: cylinder}
cm <-> cm: {source-arrowhead.shape: cf-one-required}
mt: z
cdpdxz
bymdyk: hdzuj {shape: class}
bymdyk <-> bymdyk
cm
cm <-> bymdyk: {
source-arrowhead.shape: cf-many-required
target-arrowhead.shape: arrow
}
bymdyk <-> cdpdxz
bymdyk -> cm: nk {
target-arrowhead.shape: diamond
target-arrowhead.label: 1
}
`,
key: `bymdyk`,
exp: `cm: {shape: cylinder}
cm <-> cm: {source-arrowhead.shape: cf-one-required}
mt: z
cdpdxz
cm
`,
},
{
name: "layers-basic",
text: `a
layers: {
x: {
b
c
}
}
`,
key: `c`,
boardPath: []string{"x"},
exp: `a
layers: {
x: {
b
}
}
`,
},
{
name: "scenarios-basic",
text: `a
scenarios: {
x: {
b
c
}
}
`,
key: `c`,
boardPath: []string{"x"},
exp: `a
scenarios: {
x: {
b
}
}
`,
},
{
name: "scenarios-inherited",
text: `a
scenarios: {
x: {
b
c
}
}
`,
key: `a`,
boardPath: []string{"x"},
exp: `a
scenarios: {
x: {
b
c
a: null
}
}
`,
},
{
name: "scenarios-edge-inherited",
text: `a -> b
scenarios: {
x: {
b
c
}
}
`,
key: `(a -> b)[0]`,
boardPath: []string{"x"},
exp: `a -> b
scenarios: {
x: {
b
c
(a -> b)[0]: null
}
}
`,
},
{
name: "import/1",
text: `...@meow
y
`,
fsTexts: map[string]string{
"meow.d2": `x: {
a
}
`,
},
key: `x`,
exp: `...@meow
y
x: null
`,
},
{
name: "import/2",
text: `...@meow
scenarios: {
y: {
c
}
}
`,
fsTexts: map[string]string{
"meow.d2": `x: {
a
}
`,
},
boardPath: []string{"y"},
key: `x`,
exp: `...@meow
scenarios: {
y: {
c
x: null
}
}
`,
},
{
name: "import/3",
text: `...@meow
`,
fsTexts: map[string]string{
"meow.d2": `a -> b
`,
},
key: `(a -> b)[0]`,
exp: `...@meow
(a -> b)[0]: null
`,
},
{
name: "import/4",
text: `...@meow
`,
fsTexts: map[string]string{
"meow.d2": `a.link: https://google.com
`,
},
key: `a.link`,
exp: `...@meow
a.link: null
`,
},
{
name: "import/5",
text: `...@meow
`,
fsTexts: map[string]string{
"meow.d2": `a -> b: {
target-arrowhead: 1
}
`,
},
key: `(a -> b)[0].target-arrowhead`,
exp: `...@meow
(a -> b)[0].target-arrowhead: null
`,
},
{
name: "import/6",
text: `...@meow
`,
fsTexts: map[string]string{
"meow.d2": `a.style.fill: red
`,
},
key: `a.style.fill`,
exp: `...@meow
a.style.fill: null
`,
},
{
name: "import/7",
text: `...@meow
a.label.near: center-center
`,
fsTexts: map[string]string{
"meow.d2": `a
`,
},
key: `a.label.near`,
exp: `...@meow
`,
},
{
name: "import/8",
text: `...@meow
(a -> b)[0].style.stroke: red
`,
fsTexts: map[string]string{
"meow.d2": `a -> b
`,
},
key: `(a -> b)[0].style.stroke`,
exp: `...@meow
`,
},
{
name: "label-near/1",
text: `yes: {label.near: center-center}
`,
key: `yes.label.near`,
exp: `yes
`,
},
{
name: "label-near/2",
text: `yes.label.near: center-center
`,
key: `yes.label.near`,
exp: `yes
`,
},
{
name: "connection-glob",
text: `* -> *
a
b
`,
key: `(a -> b)[0]`,
exp: `* -> *
a
b
(a -> b)[0]: null
`,
},
{
name: "glob-child/1",
text: `*.b
a
`,
key: `a.b`,
exp: `*.b
a
a.b: null
`,
},
{
name: "delete-imported-layer-obj",
text: `layers: {
x: {
...@meow
}
}
`,
fsTexts: map[string]string{
"meow.d2": `a
`,
},
boardPath: []string{"x"},
key: `a`,
exp: `layers: {
x: {
...@meow
a: null
}
}
`,
},
{
name: "delete-not-layer-obj",
text: `b.style.fill: red
layers: {
x: {
a
}
}
`,
key: `b.style.fill`,
exp: `b
layers: {
x: {
a
}
}
`,
},
{
name: "delete-layer-obj",
text: `layers: {
x: {
a
}
}
`,
boardPath: []string{"x"},
key: `a`,
exp: `layers: {
x
}
`,
},
{
name: "delete-layer-style",
text: `layers: {
x: {
a.style.fill: red
}
}
`,
boardPath: []string{"x"},
key: `a.style.fill`,
exp: `layers: {
x: {
a
}
}
`,
},
{
name: "edge-out-layer",
text: `x: {
a -> b
}
`,
key: `x.(a -> b)[0].style.stroke`,
exp: `x: {
a -> b
}
`,
},
{
name: "edge-in-layer",
text: `layers: {
test: {
x: {
a -> b
}
}
}
`,
boardPath: []string{"test"},
key: `x.(a -> b)[0].style.stroke`,
exp: `layers: {
test: {
x: {
a -> b
}
}
}
`,
},
{
name: "label-near-in-layer",
text: `layers: {
x: {
y: {
label.near: center-center
}
a
}
}
`,
boardPath: []string{"x"},
key: `y`,
exp: `layers: {
x: {
a
}
}
`,
},
{
name: "update-near-in-layer",
text: `layers: {
x: {
y: {
near: a
}
a
}
}
`,
boardPath: []string{"x"},
key: `y`,
exp: `layers: {
x: {
a
}
}
`,
},
{
name: "edge-with-glob",
text: `x -> y
y
(* -> *)[*].style.opacity: 0.8
`,
key: `(x -> y)[0]`,
exp: `x
y
(* -> *)[*].style.opacity: 0.8
`,
},
{
name: "layer-delete-complex-object",
text: `k
layers: {
x: {
a: "b" {
top: 184
left: 180
}
j
}
}
`,
key: `a`,
boardPath: []string{"x"},
exp: `k
layers: {
x: {
j
}
}
`,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
et := editTest{
text: tc.text,
fsTexts: tc.fsTexts,
testFunc: func(g *d2graph.Graph) (*d2graph.Graph, error) {
return d2oracle.Delete(g, tc.boardPath, tc.key)
},
exp: tc.exp,
expErr: tc.expErr,
assertions: tc.assertions,
}
et.run(t)
})
}
}
type editTest struct {
text string
fsTexts map[string]string
testFunc func(*d2graph.Graph) (*d2graph.Graph, error)
exp string
expErr string
assertions func(*testing.T, *d2graph.Graph)
}
func (tc editTest) run(t *testing.T) {
var tfs *mapfs.FS
d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name())
if tc.fsTexts != nil {
tc.fsTexts["index.d2"] = tc.text
d2Path = "index.d2"
var err error
tfs, err = mapfs.New(tc.fsTexts)
assert.Success(t, err)
t.Cleanup(func() {
assert.Success(t, tfs.Close())
})
}
g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), &d2compiler.CompileOptions{
FS: tfs,
})
assert.Success(t, err)
g, err = tc.testFunc(g)
if tc.expErr != "" {
if err == nil {
t.Fatalf("expected error with: %q", tc.expErr)
}
ds, err := diff.Strings(tc.expErr, err.Error())
if err != nil {
t.Fatal(err)
}
if ds != "" {
t.Fatalf("unexpected error: %s", ds)
}
} else if err != nil {
t.Fatal(err)
}
if tc.expErr == "" {
if tc.assertions != nil {
t.Run("assertions", func(t *testing.T) {
tc.assertions(t, g)
})
}
newText := d2format.Format(g.AST)
ds, err := diff.Strings(tc.exp, newText)
if err != nil {
t.Fatal(err)
}
if ds != "" {
t.Fatalf("tc.exp != newText:\n%s", ds)
}
}
got := struct {
Graph *d2graph.Graph `json:"graph"`
Err string `json:"err"`
}{
Graph: g,
Err: fmt.Sprintf("%#v", err),
}
err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2oracle", t.Name()), got)
assert.Success(t, err)
}
func TestReconnectEdgeIDDeltas(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
boardPath []string
text string
edge string
newSrc string
newDst string
exp string
expErr string
}{
{
name: "basic",
text: `a -> b
x
`,
edge: "(a -> b)[0]",
newDst: "x",
exp: `{
"(a -> b)[0]": "(a -> x)[0]"
}`,
},
{
name: "both",
text: `a
b
c
a -> b
`,
edge: `(a -> b)[0]`,
newSrc: "b",
newDst: "a",
exp: `{
"(a -> b)[0]": "(b -> a)[0]"
}`,
},
{
name: "contained",
text: `a.x -> a.y
a.z
`,
edge: "a.(x -> y)[0]",
newDst: "a.z",
exp: `{
"a.(x -> y)[0]": "a.(x -> z)[0]"
}`,
},
{
name: "second_index",
text: `a -> b
a -> b
c
`,
edge: "(a -> b)[1]",
newDst: "c",
exp: `{
"(a -> b)[1]": "(a -> c)[0]"
}`,
},
{
name: "old_sibling_decrement",
text: `a -> b
a -> b
c
`,
edge: "(a -> b)[0]",
newDst: "c",
exp: `{
"(a -> b)[0]": "(a -> c)[0]",
"(a -> b)[1]": "(a -> b)[0]"
}`,
},
{
name: "new_sibling_increment",
text: `a -> b
c -> b
a -> b
`,
edge: "(c -> b)[0]",
newSrc: "a",
exp: `{
"(a -> b)[1]": "(a -> b)[2]",
"(c -> b)[0]": "(a -> b)[1]"
}`,
},
{
name: "increment_and_decrement",
text: `a -> b
c -> b
c -> b
a -> b
`,
edge: "(c -> b)[0]",
newSrc: "a",
exp: `{
"(a -> b)[1]": "(a -> b)[2]",
"(c -> b)[0]": "(a -> b)[1]",
"(c -> b)[1]": "(c -> b)[0]"
}`,
},
{
name: "in_chain",
text: `a -> b -> a -> b
c
`,
edge: "(a -> b)[0]",
newDst: "c",
exp: `{
"(a -> b)[0]": "(a -> c)[0]",
"(a -> b)[1]": "(a -> b)[0]"
}`,
},
{
name: "in_chain_2",
text: `a -> b -> a -> b
c
`,
edge: "(a -> b)[1]",
newDst: "c",
exp: `{
"(a -> b)[1]": "(a -> c)[0]"
}`,
},
{
name: "in_chain_3",
text: `a -> b -> a -> c
`,
edge: "(a -> b)[0]",
newDst: "c",
exp: `{
"(a -> b)[0]": "(a -> c)[1]"
}`,
},
{
name: "in_chain_4",
text: `a -> c -> a -> c
b
`,
edge: "(a -> c)[0]",
newDst: "b",
exp: `{
"(a -> c)[0]": "(a -> b)[0]",
"(a -> c)[1]": "(a -> c)[0]"
}`,
},
{
name: "scenarios-outer-scope",
text: `a
scenarios: {
x: {
d -> b
}
}
`,
boardPath: []string{"x"},
edge: `(d -> b)[0]`,
newDst: "a",
exp: `{
"(d -> b)[0]": "(d -> a)[0]"
}`,
},
{
name: "scenarios-second",
text: `g
a -> b
d
scenarios: {
x: {
d -> b
}
}
`,
boardPath: []string{"x"},
edge: `(d -> b)[0]`,
newSrc: "a",
exp: `{
"(d -> b)[0]": "(a -> b)[1]"
}`,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name())
g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil)
if err != nil {
t.Fatal(err)
}
var newSrc *string
var newDst *string
if tc.newSrc != "" {
newSrc = &tc.newSrc
}
if tc.newDst != "" {
newDst = &tc.newDst
}
deltas, err := d2oracle.ReconnectEdgeIDDeltas(g, tc.boardPath, tc.edge, newSrc, newDst)
if tc.expErr != "" {
if err == nil {
t.Fatalf("expected error with: %q", tc.expErr)
}
ds, err := diff.Strings(tc.expErr, err.Error())
if err != nil {
t.Fatal(err)
}
if ds != "" {
t.Fatalf("unexpected error: %s", ds)
}
} else if err != nil {
t.Fatal(err)
}
if hasRepeatedValue(deltas) {
t.Fatalf("deltas set more than one value equal to another: %s", string(xjson.Marshal(deltas)))
}
ds, err := diff.Strings(tc.exp, string(xjson.Marshal(deltas)))
if err != nil {
t.Fatal(err)
}
if ds != "" {
t.Fatalf("unexpected deltas: %s", ds)
}
})
}
}
func TestMoveIDDeltas(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
text string
key string
newKey string
includeDescendants bool
exp string
expErr string
}{
{
name: "rename",
text: `x
`,
key: "x",
newKey: "y",
exp: `{
"x": "y"
}`,
},
{
name: "rename_identical",
text: `Square
`,
key: "Square",
newKey: "Square",
exp: `{}`,
},
{
name: "children_no_self_conflict",
text: `x: {
x
}
y
`,
key: `x`,
newKey: `y.x`,
exp: `{
"x": "y.x",
"x.x": "x"
}`,
},
{
name: "into_container",
text: `x
y
x -> z
`,
key: "x",
newKey: "y.x",
exp: `{
"(x -> z)[0]": "(y.x -> z)[0]",
"x": "y.x"
}`,
},
{
name: "out_container",
text: `x: {
y
}
x.y -> z
`,
key: "x.y",
newKey: "y",
exp: `{
"(x.y -> z)[0]": "(y -> z)[0]",
"x.y": "y"
}`,
},
{
name: "container_with_edge",
text: `x {
a
b
a -> b
}
y
`,
key: "x",
newKey: "y.x",
exp: `{
"x": "y.x",
"x.(a -> b)[0]": "(a -> b)[0]",
"x.a": "a",
"x.b": "b"
}`,
},
{
name: "out_conflict",
text: `x: {
y
}
y
x.y -> z
`,
key: "x.y",
newKey: "y",
exp: `{
"(x.y -> z)[0]": "(y 2 -> z)[0]",
"x.y": "y 2"
}`,
},
{
name: "into_conflict",
text: `x: {
y
}
y
x.y -> z
`,
key: "y",
newKey: "x.y",
exp: `{
"y": "x.y 2"
}`,
},
{
name: "move_container",
text: `x: {
a
b
}
y
x.a -> x.b
x.a -> x.b
`,
key: "x",
newKey: "y.x",
exp: `{
"x": "y.x",
"x.(a -> b)[0]": "(a -> b)[0]",
"x.(a -> b)[1]": "(a -> b)[1]",
"x.a": "a",
"x.b": "b"
}`,
},
{
name: "conflicts",
text: `x: {
a
b
}
a
y
x.a -> x.b
`,
key: "x",
newKey: "y.x",
exp: `{
"x": "y.x",
"x.(a -> b)[0]": "(a 2 -> b)[0]",
"x.a": "a 2",
"x.b": "b"
}`,
},
{
name: "container_conflicts_generated",
text: `Square 2: "" {
Square: ""
}
Square: ""
Square 3
`,
key: `Square 2`,
newKey: `Square 3.Square 2`,
exp: `{
"Square 2": "Square 3.Square 2",
"Square 2.Square": "Square 2"
}`,
},
{
name: "duplicate_generated",
text: `x
x 2
x 3: {
x 3
x 4
}
x 4
y
`,
key: `x 3`,
newKey: `y.x 3`,
exp: `{
"x 3": "y.x 3",
"x 3.x 3": "x 3",
"x 3.x 4": "x 5"
}`,
},
{
name: "include_descendants_flat",
text: `x.y
z
`,
key: `x`,
newKey: `z.x`,
includeDescendants: true,
exp: `{
"x": "z.x",
"x.y": "z.x.y"
}`,
},
{
name: "include_descendants_map",
text: `x: {
y
}
z
`,
key: `x`,
newKey: `z.x`,
includeDescendants: true,
exp: `{
"x": "z.x",
"x.y": "z.x.y"
}`,
},
{
name: "include_descendants_conflict",
text: `x.y
z.x
`,
key: `x`,
newKey: `z.x`,
includeDescendants: true,
exp: `{
"x": "z.x 2",
"x.y": "z.x 2.y"
}`,
},
{
name: "include_descendants_non_conflict",
text: `x.y
z.x
y
`,
key: `x`,
newKey: `z.x`,
includeDescendants: true,
exp: `{
"x": "z.x 2",
"x.y": "z.x 2.y"
}`,
},
{
name: "include_descendants_edge_ref",
text: `x -> y.z
`,
key: `y.z`,
newKey: `z`,
includeDescendants: true,
exp: `{
"(x -> y.z)[0]": "(x -> z)[0]",
"y.z": "z"
}`,
},
{
name: "include_descendants_edge_ref_2",
text: `x -> y.z
`,
key: `y.z`,
newKey: `z`,
includeDescendants: true,
exp: `{
"(x -> y.z)[0]": "(x -> z)[0]",
"y.z": "z"
}`,
},
{
name: "include_descendants_edge_ref_3",
text: `x -> y.z.a
`,
key: `y.z`,
newKey: `z`,
includeDescendants: true,
exp: `{
"(x -> y.z.a)[0]": "(x -> z.a)[0]",
"y.z": "z",
"y.z.a": "z.a"
}`,
},
{
name: "include_descendants_edge_ref_4",
text: `x -> y.z.a
b
`,
key: `y.z`,
newKey: `b.z`,
includeDescendants: true,
exp: `{
"(x -> y.z.a)[0]": "(x -> b.z.a)[0]",
"y.z": "b.z",
"y.z.a": "b.z.a"
}`,
},
{
name: "include_descendants_underscore_2",
text: `a: {
b: {
_.c
}
}
`,
key: `a.b`,
newKey: `b`,
includeDescendants: true,
exp: `{
"a.b": "b"
}`,
},
{
name: "include_descendants_underscore_3",
text: `a: {
b: {
_.c -> d
_.c -> _.d
}
}
`,
key: `a.b`,
newKey: `b`,
includeDescendants: true,
exp: `{
"a.(c -> b.d)[0]": "(a.c -> b.d)[0]",
"a.b": "b",
"a.b.d": "b.d"
}`,
},
{
name: "include_descendants_edge_ref_underscore",
text: `x
z
x.a -> x.b
b: {
_.x.a -> _.x.b
}
`,
key: `x`,
newKey: `z.x`,
includeDescendants: true,
exp: `{
"x": "z.x",
"x.(a -> b)[0]": "z.x.(a -> b)[0]",
"x.(a -> b)[1]": "z.x.(a -> b)[1]",
"x.a": "z.x.a",
"x.b": "z.x.b"
}`,
},
{
name: "include_descendants_sql_table",
text: `x: {
shape: sql_table
a: b
}
z
`,
key: `x`,
newKey: `z.x`,
includeDescendants: true,
exp: `{
"x": "z.x"
}`,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name())
g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil)
if err != nil {
t.Fatal(err)
}
deltas, err := d2oracle.MoveIDDeltas(g, tc.key, tc.newKey, tc.includeDescendants)
if tc.expErr != "" {
if err == nil {
t.Fatalf("expected error with: %q", tc.expErr)
}
ds, err := diff.Strings(tc.expErr, err.Error())
if err != nil {
t.Fatal(err)
}
if ds != "" {
t.Fatalf("unexpected error: %s", ds)
}
} else if err != nil {
t.Fatal(err)
}
if hasRepeatedValue(deltas) {
t.Fatalf("deltas set more than one value equal to another: %s", string(xjson.Marshal(deltas)))
}
ds, err := diff.Strings(tc.exp, string(xjson.Marshal(deltas)))
if err != nil {
t.Fatal(err)
}
if ds != "" {
t.Fatalf("unexpected deltas: %s", ds)
}
})
}
}
func TestDeleteIDDeltas(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
boardPath []string
text string
key string
exp string
expErr string
}{
{
name: "delete_node",
text: `x.y.p -> x.y.q
x.y.z.w.e.p.l
x.y.z.1.2.3.4
x.y.3.4.5.6
x.y.3.4.6.7
x.y.3.4.6.7 -> x.y.3.4.5.6
x.y.z.w.e.p.l -> x.y.z.1.2.3.4
`,
key: "x.y",
exp: `{
"x.y.(p -> q)[0]": "x.(p -> q)[0]",
"x.y.3": "x.3",
"x.y.3.4": "x.3.4",
"x.y.3.4.(6.7 -> 5.6)[0]": "x.3.4.(6.7 -> 5.6)[0]",
"x.y.3.4.5": "x.3.4.5",
"x.y.3.4.5.6": "x.3.4.5.6",
"x.y.3.4.6": "x.3.4.6",
"x.y.3.4.6.7": "x.3.4.6.7",
"x.y.p": "x.p",
"x.y.q": "x.q",
"x.y.z": "x.z",
"x.y.z.(w.e.p.l -> 1.2.3.4)[0]": "x.z.(w.e.p.l -> 1.2.3.4)[0]",
"x.y.z.1": "x.z.1",
"x.y.z.1.2": "x.z.1.2",
"x.y.z.1.2.3": "x.z.1.2.3",
"x.y.z.1.2.3.4": "x.z.1.2.3.4",
"x.y.z.w": "x.z.w",
"x.y.z.w.e": "x.z.w.e",
"x.y.z.w.e.p": "x.z.w.e.p",
"x.y.z.w.e.p.l": "x.z.w.e.p.l"
}`,
},
{
name: "children_no_self_conflict",
text: `x: {
x
}
`,
key: `x`,
exp: `{
"x.x": "x"
}`,
},
{
name: "duplicate_generated",
text: `x
x 2
x 3: {
x 3
x 4
}
x 4
y
`,
key: `x 3`,
exp: `{
"x 3.x 3": "x 3",
"x 3.x 4": "x 5"
}`,
},
{
name: "nested-height",
text: `x: {
a -> b
height: 200
}
`,
key: `x.height`,
exp: `null`,
},
{
name: "edge-style",
text: `x <-> y: {
target-arrowhead: circle
source-arrowhead: diamond
}
`,
key: `(x <-> y)[0].target-arrowhead`,
exp: `null`,
},
{
name: "only-reserved",
text: `guitar: {
books: {
_._.pipe: {
a
}
}
}
`,
key: `pipe`,
exp: `{
"pipe.a": "a"
}`,
},
{
name: "delete_container_with_conflicts",
text: `x {
a
b
}
a
b
c
x.a -> c
`,
key: "x",
exp: `{
"(x.a -> c)[0]": "(a 2 -> c)[0]",
"x.a": "a 2",
"x.b": "b 2"
}`,
},
{
name: "multiword",
text: `Starfish: {
API
}
Starfish.API
`,
key: "Starfish",
exp: `{
"Starfish.API": "API"
}`,
},
{
name: "delete_container_with_edge",
text: `x {
a
b
a -> b
}
`,
key: "x",
exp: `{
"x.(a -> b)[0]": "(a -> b)[0]",
"x.a": "a",
"x.b": "b"
}`,
},
{
name: "delete_edge_field",
text: `a -> b
a -> b
`,
key: "(a -> b)[0].style.opacity",
exp: "null",
},
{
name: "delete_edge",
text: `x.y.z.w.e.p.l -> x.y.z.1.2.3.4
x.y.z.w.e.p.l -> x.y.z.1.2.3.4
x.y.z.w.e.p.l -> x.y.z.1.2.3.4
x.y.z.w.e.p.l -> x.y.z.1.2.3.4
x.y.z.w.e.p.l -> x.y.z.1.2.3.4
x.y.z.w.e.p.l -> x.y.z.1.2.3.4
x.y.z.w.e.p.l -> x.y.z.1.2.3.4
(x.y.z.w.e.p.l -> x.y.z.1.2.3.4)[0]: meow
(x.y.z.w.e.p.l -> x.y.z.1.2.3.4)[1]: meow
(x.y.z.w.e.p.l -> x.y.z.1.2.3.4)[2]: meow
(x.y.z.w.e.p.l -> x.y.z.1.2.3.4)[3]: meow
(x.y.z.w.e.p.l -> x.y.z.1.2.3.4)[4]: meow
(x.y.z.w.e.p.l -> x.y.z.1.2.3.4)[5]: meow
(x.y.z.w.e.p.l -> x.y.z.1.2.3.4)[6]: meow
`,
key: "(x.y.z.w.e.p.l -> x.y.z.1.2.3.4)[1]",
exp: `{
"x.y.z.(w.e.p.l -> 1.2.3.4)[2]": "x.y.z.(w.e.p.l -> 1.2.3.4)[1]",
"x.y.z.(w.e.p.l -> 1.2.3.4)[3]": "x.y.z.(w.e.p.l -> 1.2.3.4)[2]",
"x.y.z.(w.e.p.l -> 1.2.3.4)[4]": "x.y.z.(w.e.p.l -> 1.2.3.4)[3]",
"x.y.z.(w.e.p.l -> 1.2.3.4)[5]": "x.y.z.(w.e.p.l -> 1.2.3.4)[4]",
"x.y.z.(w.e.p.l -> 1.2.3.4)[6]": "x.y.z.(w.e.p.l -> 1.2.3.4)[5]"
}`,
},
{
name: "delete_generated_id_conflicts",
text: `Text 2: {
Text
Text 3
}
Text
`,
key: "Text 2",
exp: `{
"Text 2.Text": "Text 2",
"Text 2.Text 3": "Text 3"
}`,
},
{
name: "delete_generated_id_conflicts_2",
text: `Text 4
Square: {
Text 4: {
Text 2
}
Text
}
`,
key: "Square",
exp: `{
"Square.Text": "Text",
"Square.Text 4": "Text 2",
"Square.Text 4.Text 2": "Text 2.Text 2"
}`,
},
{
name: "delete_generated_id_conflicts_2_continued",
text: `Text 4
Text: {
Text 2
}
Text 2
`,
key: "Text",
exp: `{
"Text.Text 2": "Text"
}`,
},
{
name: "conflicts_generated_3",
text: `x: {
Square 2
Square 3
}
Square 2
Square
`,
key: `x`,
exp: `{
"x.Square 2": "Square 4",
"x.Square 3": "Square 3"
}`,
},
{
name: "scenarios-basic",
text: `x
scenarios: {
y: {
a
}
}
`,
boardPath: []string{"y"},
key: `a`,
exp: `{}`,
},
{
name: "scenarios-parent",
text: `x
scenarios: {
y: {
a.x
}
}
`,
boardPath: []string{"y"},
key: `a`,
exp: `{
"a.x": "x 2"
}`,
},
{
name: "layers-parent",
text: `x
layers: {
y: {
a.x
}
}
`,
boardPath: []string{"y"},
key: `a`,
exp: `{
"a.x": "x"
}`,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name())
g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil)
if err != nil {
t.Fatal(err)
}
deltas, err := d2oracle.DeleteIDDeltas(g, tc.boardPath, tc.key)
if tc.expErr != "" {
if err == nil {
t.Fatalf("expected error with: %q", tc.expErr)
}
ds, err := diff.Strings(tc.expErr, err.Error())
if err != nil {
t.Fatal(err)
}
if ds != "" {
t.Fatalf("unexpected error: %s", ds)
}
} else if err != nil {
t.Fatal(err)
}
if hasRepeatedValue(deltas) {
t.Fatalf("deltas set more than one value equal to another: %s", string(xjson.Marshal(deltas)))
}
ds, err := diff.Strings(tc.exp, string(xjson.Marshal(deltas)))
if err != nil {
t.Fatal(err)
}
if ds != "" {
t.Fatalf("unexpected deltas: %s", ds)
}
})
}
}
func hasRepeatedValue(m map[string]string) bool {
seen := make(map[string]struct{}, len(m))
for _, v := range m {
if _, ok := seen[v]; ok {
return true
}
seen[v] = struct{}{}
}
return false
}
func TestRenameIDDeltas(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
boardPath []string
text string
key string
newName string
exp string
expErr string
}{
{
name: "rename_node",
text: `x.y.p -> x.y.q
x.y.z.w.e.p.l
x.y.z.1.2.3.4
x.y.3.4.5.6
x.y.3.4.6.7
x.y.3.4.6.7 -> x.y.3.4.5.6
x.y.z.w.e.p.l -> x.y.z.1.2.3.4
`,
key: "x.y",
newName: "papa",
exp: `{
"x.y": "x.papa",
"x.y.(p -> q)[0]": "x.papa.(p -> q)[0]",
"x.y.3": "x.papa.3",
"x.y.3.4": "x.papa.3.4",
"x.y.3.4.(6.7 -> 5.6)[0]": "x.papa.3.4.(6.7 -> 5.6)[0]",
"x.y.3.4.5": "x.papa.3.4.5",
"x.y.3.4.5.6": "x.papa.3.4.5.6",
"x.y.3.4.6": "x.papa.3.4.6",
"x.y.3.4.6.7": "x.papa.3.4.6.7",
"x.y.p": "x.papa.p",
"x.y.q": "x.papa.q",
"x.y.z": "x.papa.z",
"x.y.z.(w.e.p.l -> 1.2.3.4)[0]": "x.papa.z.(w.e.p.l -> 1.2.3.4)[0]",
"x.y.z.1": "x.papa.z.1",
"x.y.z.1.2": "x.papa.z.1.2",
"x.y.z.1.2.3": "x.papa.z.1.2.3",
"x.y.z.1.2.3.4": "x.papa.z.1.2.3.4",
"x.y.z.w": "x.papa.z.w",
"x.y.z.w.e": "x.papa.z.w.e",
"x.y.z.w.e.p": "x.papa.z.w.e.p",
"x.y.z.w.e.p.l": "x.papa.z.w.e.p.l"
}`,
},
{
name: "rename_conflict",
text: `x
y
`,
key: "x",
newName: "y",
exp: `{
"x": "y 2"
}`,
},
{
name: "generated-conflict",
text: `Square
Square 2
`,
key: `Square 2`,
newName: `Square`,
exp: `{}`,
},
{
name: "rename_conflict_with_dots",
text: `"a.b"
y
`,
key: "y",
newName: "a.b",
exp: `{
"y": "\"a.b 2\""
}`,
},
{
name: "rename_conflict_with_numbers",
text: `1
Square
`,
key: `Square`,
newName: `1`,
exp: `{
"Square": "1 2"
}`,
},
{
name: "rename_identical",
text: `Square
`,
key: "Square",
newName: "Square",
exp: `{}`,
},
{
name: "rename_edge",
text: `x.y.z.w.e.p.l -> x.y.z.1.2.3.4
x.y.z.w.e.p.l -> x.y.z.1.2.3.4
x.y.z.w.e.p.l -> x.y.z.1.2.3.4
x.y.z.w.e.p.l -> x.y.z.1.2.3.4
x.y.z.w.e.p.l -> x.y.z.1.2.3.4
x.y.z.w.e.p.l -> x.y.z.1.2.3.4
x.y.z.w.e.p.l -> x.y.z.1.2.3.4
(x.y.z.w.e.p.l -> x.y.z.1.2.3.4)[0]: meow
(x.y.z.w.e.p.l -> x.y.z.1.2.3.4)[1]: meow
(x.y.z.w.e.p.l -> x.y.z.1.2.3.4)[2]: meow
(x.y.z.w.e.p.l -> x.y.z.1.2.3.4)[3]: meow
(x.y.z.w.e.p.l -> x.y.z.1.2.3.4)[4]: meow
(x.y.z.w.e.p.l -> x.y.z.1.2.3.4)[5]: meow
(x.y.z.w.e.p.l -> x.y.z.1.2.3.4)[6]: meow
`,
key: "(x.y.z.w.e.p.l -> x.y.z.1.2.3.4)[1]",
newName: "(x.y.z.w.e.p.l <-> x.y.z.1.2.3.4)[1]",
exp: `{
"x.y.z.(w.e.p.l -> 1.2.3.4)[1]": "x.y.z.(w.e.p.l <-> 1.2.3.4)[1]"
}`,
},
{
name: "layers-basic",
text: `x
layers: {
y: {
a
}
}
`,
boardPath: []string{"y"},
key: "a",
newName: "b",
exp: `{
"a": "b"
}`,
},
{
name: "scenarios-conflict",
text: `x
scenarios: {
y: {
a
}
}
`,
boardPath: []string{"y"},
key: "a",
newName: "x",
exp: `{
"a": "x 2"
}`,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
d2Path := fmt.Sprintf("d2/testdata/d2oracle/%v.d2", t.Name())
g, _, err := d2compiler.Compile(d2Path, strings.NewReader(tc.text), nil)
if err != nil {
t.Fatal(err)
}
deltas, err := d2oracle.RenameIDDeltas(g, tc.boardPath, tc.key, tc.newName)
if tc.expErr != "" {
if err == nil {
t.Fatalf("expected error with: %q", tc.expErr)
}
ds, err := diff.Strings(tc.expErr, err.Error())
if err != nil {
t.Fatal(err)
}
if ds != "" {
t.Fatalf("unexpected error: %s", ds)
}
} else if err != nil {
t.Fatal(err)
}
if hasRepeatedValue(deltas) {
t.Fatalf("deltas set more than one value equal to another: %s", string(xjson.Marshal(deltas)))
}
ds, err := diff.Strings(tc.exp, string(xjson.Marshal(deltas)))
if err != nil {
t.Fatal(err)
}
if ds != "" {
t.Fatalf("unexpected deltas: %s", ds)
}
})
}
}
func TestUpdateImport(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
boardPath []string
text string
fsTexts map[string]string
path string
newPath *string
expErr string
exp string
assertions func(t *testing.T, g *d2graph.Graph)
}{
{
name: "remove_import",
text: `x: @meow
y
`,
path: "meow",
newPath: nil,
exp: `x
y
`,
},
{
name: "remove_spread_import",
text: `x
...@meow
y`,
path: "meow",
newPath: nil,
exp: `x
y
`,
},
{
name: "update_import",
text: `x: @meow
y
`,
path: "meow",
newPath: go2.Pointer("woof"),
exp: `x: @woof
y
`,
},
{
name: "update_import_with_dir",
text: `x: @foo/meow
y
`,
path: "foo/meow",
newPath: go2.Pointer("bar/woof"),
exp: `x: @bar/woof
y
`,
},
{
name: "update_spread_import",
text: `x
...@meow
y
`,
path: "meow",
newPath: go2.Pointer("woof"),
exp: `x
...@woof
y
`,
},
{
name: "no_matching_import",
text: `x: @cat
y
`,
path: "meow",
newPath: go2.Pointer("woof"),
exp: `x: @cat
y
`,
},
{
name: "nested_import",
text: `container: {
x: @meow
y
}
`,
path: "meow",
newPath: go2.Pointer("woof"),
exp: `container: {
x: @woof
y
}
`,
},
{
name: "remove_nested_import",
text: `container: {
x: @meow
y
}
`,
path: "meow",
newPath: nil,
exp: `container: {
x
y
}
`,
},
{
name: "multiple_imports",
text: `x: @meow
y: @meow
z
`,
path: "meow",
newPath: go2.Pointer("woof"),
exp: `x: @woof
y: @woof
z
`,
},
{
name: "mixed_imports",
text: `x: @meow
y
...@meow
z
`,
path: "meow",
newPath: go2.Pointer("woof"),
exp: `x: @woof
y
...@woof
z
`,
},
{
name: "in_layer",
text: `x
layers: {
y: {
z: @meow
}
}
`,
path: "meow",
newPath: go2.Pointer("woof"),
exp: `x
layers: {
y: {
z: @woof
}
}
`,
},
{
name: "layer_import",
text: `x
layers: {
y: {
...@meow
}
}
`,
path: "meow",
newPath: go2.Pointer("woof"),
exp: `x
layers: {
y: {
...@woof
}
}
`,
},
{
name: "update_directory_import",
text: `x: @foo/bar
y: @foo/baz
z
`,
path: "foo/",
newPath: go2.Pointer("woof/"),
exp: `x: @woof/bar
y: @woof/baz
z
`,
},
{
name: "remove_directory_import",
text: `x: @foo/bar
y: @foo/baz
z
`,
path: "foo/",
newPath: nil,
exp: `x
y
z
`,
},
{
name: "update_deep_directory_paths",
text: `x: @foo/bar/baz
y: @foo/qux/quux
z
`,
path: "foo/",
newPath: go2.Pointer("woof/"),
exp: `x: @woof/bar/baz
y: @woof/qux/quux
z
`,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := d2oracle.UpdateImport(tc.text, tc.path, tc.newPath)
if err != nil {
t.Fatal(err)
}
if got != tc.exp {
t.Fatalf("tc.exp != newText:\n%s", got)
}
})
}
}