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: ``, exp: `ok."".z -> p.k -> ok."".z -> l.a -> ok."".z ok."": { q -> + -> z z: label } ok: { "": { i } } (ok."".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."".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 `, }, { name: "update_relative_import-1", text: `x: @../meow y `, path: "../meow", newPath: go2.Pointer("../woof"), exp: `x: @../woof y `, }, { name: "update_relative_import-2", text: `x: @../meow y `, path: "../meow", newPath: go2.Pointer("woof"), exp: `x: @woof y `, }, { name: "update_relative_import-3", text: `x: @../meow y `, path: "../meow", newPath: go2.Pointer("../meow/woof"), exp: `x: @../meow/woof y `, }, { name: "update_relative_import-4", text: `x: @../meow y `, path: "../meow", newPath: go2.Pointer("../g/woof"), exp: `x: @../g/woof y `, }, } 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) } }) } }