1 package d2exporter_test
2
3 import (
4 "context"
5 "path/filepath"
6 "strings"
7 "testing"
8
9 "cdr.dev/slog"
10
11 tassert "github.com/stretchr/testify/assert"
12
13 "oss.terrastruct.com/util-go/assert"
14 "oss.terrastruct.com/util-go/diff"
15 "oss.terrastruct.com/util-go/go2"
16
17 "oss.terrastruct.com/d2/d2compiler"
18 "oss.terrastruct.com/d2/d2exporter"
19 "oss.terrastruct.com/d2/d2graph"
20 "oss.terrastruct.com/d2/d2layouts"
21 "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
22 "oss.terrastruct.com/d2/d2lib"
23 "oss.terrastruct.com/d2/d2target"
24 "oss.terrastruct.com/d2/lib/geo"
25 "oss.terrastruct.com/d2/lib/log"
26 "oss.terrastruct.com/d2/lib/textmeasure"
27 )
28
29 type testCase struct {
30 name string
31 dsl string
32
33 assertions func(t *testing.T, d *d2target.Diagram)
34 }
35
36 func TestExport(t *testing.T) {
37 t.Parallel()
38
39 t.Run("shape", testShape)
40 t.Run("connection", testConnection)
41 t.Run("label", testLabel)
42 t.Run("theme", testTheme)
43 }
44
45 func testShape(t *testing.T) {
46 tcs := []testCase{
47 {
48 name: "basic",
49 dsl: `x
50 `,
51 },
52 {
53 name: "synonyms",
54 dsl: `x: {shape: circle}
55 y: {shape: square}
56 `,
57 },
58 {
59 name: "text_color",
60 dsl: `x: |md yo | { style.font-color: red }
61 `,
62 },
63 {
64 name: "border-radius",
65 dsl: `Square: "" { style.border-radius: 5 }
66 `,
67 },
68 {
69 name: "image_dimensions",
70
71 dsl: `hey: "" {
72 icon: https://icons.terrastruct.com/essentials/004-picture.svg
73 shape: image
74 width: 200
75 height: 230
76 }
77 `,
78 assertions: func(t *testing.T, d *d2target.Diagram) {
79 if d.Shapes[0].Width != 200 {
80 t.Fatalf("expected width 200, got %v", d.Shapes[0].Width)
81 }
82 if d.Shapes[0].Height != 230 {
83 t.Fatalf("expected height 230, got %v", d.Shapes[0].Height)
84 }
85 },
86 },
87 {
88 name: "sequence_group_position",
89
90 dsl: `hey {
91 shape: sequence_diagram
92 a
93 b
94 group: {
95 a -> b
96 }
97 }
98 `,
99 assertions: func(t *testing.T, d *d2target.Diagram) {
100 tassert.Equal(t, "hey.group", d.Shapes[3].ID)
101 tassert.Equal(t, "INSIDE_TOP_LEFT", d.Shapes[3].LabelPosition)
102 },
103 },
104 }
105
106 runa(t, tcs)
107 }
108
109 func testConnection(t *testing.T) {
110 tcs := []testCase{
111 {
112 name: "basic",
113 dsl: `x -> y
114 `,
115 },
116 {
117 name: "stroke-dash",
118 dsl: `x -> y: { style.stroke-dash: 4 }
119 `,
120 },
121 {
122 name: "arrowhead",
123 dsl: `x -> y: {
124 source-arrowhead: If you've done six impossible things before breakfast, why not round it
125 target-arrowhead: {
126 label: A man with one watch knows what time it is.
127 shape: diamond
128 style.filled: true
129 }
130 }
131 `,
132 },
133 {
134
135
136 name: "theme_stroke-dash",
137 dsl: `x -> y: { style.stroke-dash: 0 }
138 x -> y
139 `,
140 },
141 }
142
143 runa(t, tcs)
144 }
145
146 func testLabel(t *testing.T) {
147 tcs := []testCase{
148 {
149 name: "basic_shape",
150 dsl: `x: yo
151 `,
152 },
153 {
154 name: "shape_font_color",
155 dsl: `x: yo { style.font-color: red }
156 `,
157 },
158 {
159 name: "connection_font_color",
160 dsl: `x -> y: yo { style.font-color: red }
161 `,
162 },
163 }
164
165 runa(t, tcs)
166 }
167
168 func testTheme(t *testing.T) {
169 tcs := []testCase{
170 {
171 name: "shape_without_bold",
172 dsl: `x: {
173 style.bold: false
174 }
175 `,
176 },
177 {
178 name: "shape_with_italic",
179 dsl: `x: {
180 style.italic: true
181 }
182 `,
183 },
184 {
185 name: "connection_without_italic",
186 dsl: `x -> y: asdf { style.italic: false }
187 `,
188 },
189 {
190 name: "connection_with_italic",
191 dsl: `x -> y: asdf {
192 style.italic: true
193 }
194 `,
195 },
196 {
197 name: "connection_with_bold",
198 dsl: `x -> y: asdf {
199 style.bold: true
200 }
201 `,
202 },
203 }
204
205 runa(t, tcs)
206 }
207
208 func runa(t *testing.T, tcs []testCase) {
209 for _, tc := range tcs {
210 tc := tc
211 t.Run(tc.name, func(t *testing.T) {
212 t.Parallel()
213
214 run(t, tc)
215 })
216 }
217 }
218
219 func run(t *testing.T, tc testCase) {
220 ctx := context.Background()
221 ctx = log.WithTB(ctx, t, nil)
222 ctx = log.Leveled(ctx, slog.LevelDebug)
223
224 g, config, err := d2compiler.Compile("", strings.NewReader(tc.dsl), &d2compiler.CompileOptions{
225 UTF16Pos: true,
226 })
227 if err != nil {
228 t.Fatal(err)
229 }
230
231 ruler, err := textmeasure.NewRuler()
232 assert.JSON(t, nil, err)
233
234 err = g.SetDimensions(nil, ruler, nil)
235 assert.JSON(t, nil, err)
236
237 graphInfo := d2layouts.NestedGraphInfo(g.Root)
238 err = d2layouts.LayoutNested(ctx, g, graphInfo, d2dagrelayout.DefaultLayout, d2layouts.DefaultRouter)
239 if err != nil {
240 t.Fatal(err)
241 }
242
243 got, err := d2exporter.Export(ctx, g, nil)
244 if err != nil {
245 t.Fatal(err)
246 }
247 if got != nil {
248 got.Config = config
249 }
250
251 if tc.assertions != nil {
252 t.Run("assertions", func(t *testing.T) {
253 tc.assertions(t, got)
254 })
255 }
256
257
258 for i := range got.Shapes {
259 got.Shapes[i].Pos = d2target.Point{}
260 got.Shapes[i].Width = 0
261 got.Shapes[i].Height = 0
262 got.Shapes[i].LabelWidth = 0
263 got.Shapes[i].LabelHeight = 0
264 got.Shapes[i].LabelPosition = ""
265 }
266 for i := range got.Connections {
267 got.Connections[i].Route = []*geo.Point{}
268 got.Connections[i].LabelWidth = 0
269 got.Connections[i].LabelHeight = 0
270 got.Connections[i].LabelPosition = ""
271 }
272
273 err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2exporter", t.Name()), got)
274 assert.Success(t, err)
275 }
276
277
278 func TestHashID(t *testing.T) {
279 ctx := context.Background()
280 ctx = log.WithTB(ctx, t, nil)
281 ctx = log.Leveled(ctx, slog.LevelDebug)
282
283 aString := `
284 vars: {
285 d2-config: {
286 theme-id: 3
287 }
288 }
289 a -> b
290 `
291
292 bString := `
293 vars: {
294 d2-config: {
295 theme-id: 4
296 }
297 }
298 a -> b
299 `
300
301 da, err := compile(ctx, aString)
302 assert.JSON(t, nil, err)
303
304 db, err := compile(ctx, bString)
305 assert.JSON(t, nil, err)
306
307 hashA, err := da.HashID()
308 assert.JSON(t, nil, err)
309
310 hashB, err := db.HashID()
311 assert.JSON(t, nil, err)
312
313 assert.NotEqual(t, hashA, hashB)
314 }
315
316 func layoutResolver(engine string) (d2graph.LayoutGraph, error) {
317 return d2dagrelayout.DefaultLayout, nil
318 }
319
320 func compile(ctx context.Context, d2 string) (*d2target.Diagram, error) {
321 ruler, _ := textmeasure.NewRuler()
322 opts := &d2lib.CompileOptions{
323 Ruler: ruler,
324 LayoutResolver: layoutResolver,
325 Layout: go2.Pointer("dagre"),
326 }
327 d, _, e := d2lib.Compile(ctx, d2, opts, nil)
328 return d, e
329 }
330
View as plain text