1 package e2etests_cli
2
3 import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "os"
10 "path/filepath"
11 "regexp"
12 "strings"
13 "testing"
14 "time"
15
16 "nhooyr.io/websocket"
17
18 "oss.terrastruct.com/util-go/assert"
19 "oss.terrastruct.com/util-go/diff"
20 "oss.terrastruct.com/util-go/xmain"
21 "oss.terrastruct.com/util-go/xos"
22
23 "oss.terrastruct.com/d2/d2cli"
24 "oss.terrastruct.com/d2/lib/pptx"
25 "oss.terrastruct.com/d2/lib/xgif"
26 )
27
28 func TestCLI_E2E(t *testing.T) {
29 t.Parallel()
30
31 tca := []struct {
32 name string
33 serial bool
34 skipCI bool
35 skip bool
36 run func(t *testing.T, ctx context.Context, dir string, env *xos.Env)
37 }{
38 {
39 name: "hello_world_png",
40 skipCI: true,
41 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
42 writeFile(t, dir, "hello-world.d2", `x -> y`)
43 err := runTestMain(t, ctx, dir, env, "hello-world.d2", "hello-world.png")
44 assert.Success(t, err)
45 png := readFile(t, dir, "hello-world.png")
46 testdataIgnoreDiff(t, ".png", png)
47 },
48 },
49 {
50 name: "hello_world_png_pad",
51 skipCI: true,
52 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
53 writeFile(t, dir, "hello-world.d2", `x -> y`)
54 err := runTestMain(t, ctx, dir, env, "--pad=400", "hello-world.d2", "hello-world.png")
55 assert.Success(t, err)
56 png := readFile(t, dir, "hello-world.png")
57 testdataIgnoreDiff(t, ".png", png)
58 },
59 },
60 {
61 name: "center",
62 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
63 writeFile(t, dir, "hello-world.d2", `x -> y`)
64 err := runTestMain(t, ctx, dir, env, "--center=true", "hello-world.d2")
65 assert.Success(t, err)
66 svg := readFile(t, dir, "hello-world.svg")
67 assert.Testdata(t, ".svg", svg)
68 },
69 },
70 {
71 name: "flags-panic",
72 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
73 writeFile(t, dir, "hello-world.d2", `x -> y`)
74 err := runTestMain(t, ctx, dir, env, "layout", "dagre", "--dagre-nodesep", "50", "hello-world.d2")
75 assert.ErrorString(t, err, `failed to wait xmain test: e2etests-cli/d2: failed to unmarshal input to graph: `)
76 },
77 },
78 {
79 name: "empty-layer",
80 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
81 writeFile(t, dir, "empty-layer.d2", `layers: { x: {} }`)
82 err := runTestMain(t, ctx, dir, env, "empty-layer.d2")
83 assert.Success(t, err)
84 },
85 },
86 {
87 name: "layer-link",
88 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
89 writeFile(t, dir, "test.d2", `doh: { link: layers.test2 }; layers: { test2: @test2.d2 }`)
90 writeFile(t, dir, "test2.d2", `x: I'm a Mac { link: https://example.com }`)
91 err := runTestMain(t, ctx, dir, env, "test.d2", "layer-link.svg")
92 assert.Success(t, err)
93
94 assert.TestdataDir(t, filepath.Join(dir, "layer-link"))
95 },
96 },
97 {
98
99 name: "empty-base",
100 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
101 writeFile(t, dir, "empty-base.d2", `steps: {
102 1: {
103 a -> b
104 }
105 2: {
106 b -> d
107 c -> d
108 }
109 3: {
110 d -> e
111 }
112 }`)
113
114 err := runTestMain(t, ctx, dir, env, "--animate-interval=1400", "empty-base.d2")
115 assert.Success(t, err)
116 svg := readFile(t, dir, "empty-base.svg")
117 assert.Testdata(t, ".svg", svg)
118 assert.Equal(t, 3, getNumBoards(string(svg)))
119 },
120 },
121 {
122 name: "animation",
123 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
124 writeFile(t, dir, "animation.d2", `Chicken's plan: {
125 style.font-size: 35
126 near: top-center
127 shape: text
128 }
129
130 steps: {
131 1: {
132 Approach road
133 }
134 2: {
135 Approach road -> Cross road
136 }
137 3: {
138 Cross road -> Make you wonder why
139 }
140 }
141 `)
142 err := runTestMain(t, ctx, dir, env, "--animate-interval=1400", "animation.d2")
143 assert.Success(t, err)
144 svg := readFile(t, dir, "animation.svg")
145 assert.Testdata(t, ".svg", svg)
146 },
147 },
148 {
149 name: "vars-animation",
150 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
151 writeFile(t, dir, "animation.d2", `vars: {
152 d2-config: {
153 theme-id: 300
154 }
155 }
156 Chicken's plan: {
157 style.font-size: 35
158 near: top-center
159 shape: text
160 }
161
162 steps: {
163 1: {
164 Approach road
165 }
166 2: {
167 Approach road -> Cross road
168 }
169 3: {
170 Cross road -> Make you wonder why
171 }
172 }
173 `)
174 err := runTestMain(t, ctx, dir, env, "--animate-interval=1400", "animation.d2")
175 assert.Success(t, err)
176 svg := readFile(t, dir, "animation.svg")
177 assert.Testdata(t, ".svg", svg)
178 },
179 },
180 {
181 name: "linked-path",
182
183 skip: true,
184 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
185 writeFile(t, dir, "linked.d2", `cat: how does the cat go? {
186 link: layers.cat
187 }
188 layers: {
189 cat: {
190 home: {
191 link: _
192 }
193 the cat -> meow: goes
194
195 scenarios: {
196 big cat: {
197 the cat -> roar: goes
198 }
199 }
200 }
201 }
202 `)
203 err := runTestMain(t, ctx, dir, env, "linked.d2")
204 assert.Success(t, err)
205
206 assert.TestdataDir(t, filepath.Join(dir, "linked"))
207 },
208 },
209 {
210 name: "with-font",
211 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
212 writeFile(t, dir, "font.d2", `a: Why do computers get sick often?
213 b: Because their Windows are always open!
214 a -> b: italic font
215 `)
216 err := runTestMain(t, ctx, dir, env, "--font-bold=./RockSalt-Regular.ttf", "font.d2")
217 assert.Success(t, err)
218 svg := readFile(t, dir, "font.svg")
219 assert.Testdata(t, ".svg", svg)
220 },
221 },
222 {
223 name: "incompatible-animation",
224 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
225 writeFile(t, dir, "x.d2", `x -> y`)
226 err := runTestMain(t, ctx, dir, env, "--animate-interval=2", "x.d2", "x.png")
227 assert.ErrorString(t, err, `failed to wait xmain test: e2etests-cli/d2: bad usage: -animate-interval can only be used when exporting to SVG or GIF.
228 You provided: .png`)
229 },
230 },
231 {
232 name: "hello_world_png_sketch",
233 skipCI: true,
234 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
235 writeFile(t, dir, "hello-world.d2", `x -> y`)
236 err := runTestMain(t, ctx, dir, env, "--sketch", "hello-world.d2", "hello-world.png")
237 assert.Success(t, err)
238 png := readFile(t, dir, "hello-world.png")
239
240 testdataIgnoreDiff(t, ".png", png)
241 },
242 },
243 {
244 name: "target-root",
245 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
246 writeFile(t, dir, "target-root.d2", `title: {
247 label: Main Plan
248 }
249 scenarios: {
250 b: {
251 title.label: Backup Plan
252 }
253 }`)
254 err := runTestMain(t, ctx, dir, env, "--target", "", "target-root.d2", "target-root.svg")
255 assert.Success(t, err)
256 svg := readFile(t, dir, "target-root.svg")
257 assert.Testdata(t, ".svg", svg)
258 },
259 },
260 {
261 name: "target-b",
262 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
263 writeFile(t, dir, "target-b.d2", `title: {
264 label: Main Plan
265 }
266 scenarios: {
267 b: {
268 title.label: Backup Plan
269 }
270 }`)
271 err := runTestMain(t, ctx, dir, env, "--target", "b", "target-b.d2", "target-b.svg")
272 assert.Success(t, err)
273 svg := readFile(t, dir, "target-b.svg")
274 assert.Testdata(t, ".svg", svg)
275 },
276 },
277 {
278 name: "target-nested-with-special-chars",
279 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
280 writeFile(t, dir, "target-nested-with-special-chars.d2", `layers: {
281 a: {
282 layers: {
283 "x / y . z": {
284 mad
285 }
286 }
287 }
288 }`)
289 err := runTestMain(t, ctx, dir, env, "--target", `layers.a.layers."x / y . z"`, "target-nested-with-special-chars.d2", "target-nested-with-special-chars.svg")
290 assert.Success(t, err)
291 svg := readFile(t, dir, "target-nested-with-special-chars.svg")
292 assert.Testdata(t, ".svg", svg)
293 },
294 },
295 {
296 name: "target-invalid",
297 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
298 writeFile(t, dir, "target-invalid.d2", `x -> y`)
299 err := runTestMain(t, ctx, dir, env, "--target", "b", "target-invalid.d2", "target-invalid.svg")
300 assert.ErrorString(t, err, `failed to wait xmain test: e2etests-cli/d2: failed to compile target-invalid.d2: render target "b" not found`)
301 },
302 },
303 {
304 name: "target-nested-index",
305 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
306 writeFile(t, dir, "target-nested-index.d2", `a
307 layers: {
308 l1: {
309 b
310 layers: {
311 index: {
312 c
313 layers: {
314 l3: {
315 d
316 }
317 }
318 }
319 }
320 }
321 }`)
322 err := runTestMain(t, ctx, dir, env, "--target", `l1.index.l3`, "target-nested-index.d2", "target-nested-index.svg")
323 assert.Success(t, err)
324 svg := readFile(t, dir, "target-nested-index.svg")
325 assert.Testdata(t, ".svg", svg)
326 },
327 },
328 {
329 name: "target-nested-index2",
330 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
331 writeFile(t, dir, "target-nested-index2.d2", `a
332 layers: {
333 index: {
334 b
335 layers: {
336 nest1: {
337 c
338 scenarios: {
339 nest2: {
340 d
341 }
342 }
343 }
344 }
345 }
346 }`)
347 err := runTestMain(t, ctx, dir, env, "--target", `index.nest1.nest2`, "target-nested-index2.d2", "target-nested-index2.svg")
348 assert.Success(t, err)
349 svg := readFile(t, dir, "target-nested-index2.svg")
350 assert.Testdata(t, ".svg", svg)
351 },
352 },
353 {
354 name: "theme-override",
355 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
356 writeFile(t, dir, "theme-override.d2", `
357 direction: right
358 vars: {
359 d2-config: {
360 theme-overrides: {
361 B1: "#2E7D32"
362 B2: "#66BB6A"
363 B3: "#A5D6A7"
364 B4: "#C5E1A5"
365 B5: "#E6EE9C"
366 B6: "#FFF59D"
367
368 AA2: "#0D47A1"
369 AA4: "#42A5F5"
370 AA5: "#90CAF9"
371
372 AB4: "#F44336"
373 AB5: "#FFCDD2"
374
375 N1: "#2E2E2E"
376 N2: "#2E2E2E"
377 N3: "#595959"
378 N4: "#858585"
379 N5: "#B1B1B1"
380 N6: "#DCDCDC"
381 N7: "#DCDCDC"
382 }
383 dark-theme-overrides: {
384 B1: "#2E7D32"
385 B2: "#66BB6A"
386 B3: "#A5D6A7"
387 B4: "#C5E1A5"
388 B5: "#E6EE9C"
389 B6: "#FFF59D"
390
391 AA2: "#0D47A1"
392 AA4: "#42A5F5"
393 AA5: "#90CAF9"
394
395 AB4: "#F44336"
396 AB5: "#FFCDD2"
397
398 N1: "#2E2E2E"
399 N2: "#2E2E2E"
400 N3: "#595959"
401 N4: "#858585"
402 N5: "#B1B1B1"
403 N6: "#DCDCDC"
404 N7: "#DCDCDC"
405 }
406 }
407 }
408
409 logs: {
410 shape: page
411 style.multiple: true
412 }
413 user: User {shape: person}
414 network: Network {
415 tower: Cell Tower {
416 satellites: {
417 shape: stored_data
418 style.multiple: true
419 }
420
421 satellites -> transmitter
422 satellites -> transmitter
423 satellites -> transmitter
424 transmitter
425 }
426 processor: Data Processor {
427 storage: Storage {
428 shape: cylinder
429 style.multiple: true
430 }
431 }
432 portal: Online Portal {
433 UI
434 }
435
436 tower.transmitter -> processor: phone logs
437 }
438 server: API Server
439
440 user -> network.tower: Make call
441 network.processor -> server
442 network.processor -> server
443 network.processor -> server
444
445 server -> logs
446 server -> logs
447 server -> logs: persist
448
449 server -> network.portal.UI: display
450 user -> network.portal.UI: access {
451 style.stroke-dash: 3
452 }
453
454 costumes: {
455 shape: sql_table
456 id: int {constraint: primary_key}
457 silliness: int
458 monster: int
459 last_updated: timestamp
460 }
461
462 monsters: {
463 shape: sql_table
464 id: int {constraint: primary_key}
465 movie: string
466 weight: int
467 last_updated: timestamp
468 }
469
470 costumes.monster -> monsters.id
471 `)
472 err := runTestMain(t, ctx, dir, env, "theme-override.d2", "theme-override.svg")
473 assert.Success(t, err)
474 svg := readFile(t, dir, "theme-override.svg")
475 assert.Testdata(t, ".svg", svg)
476
477 assert.NotEqual(t, -1, strings.Index(string(svg), "#2E2E2E"))
478 },
479 },
480 {
481 name: "multiboard/life",
482 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
483 writeFile(t, dir, "life.d2", `x -> y
484 layers: {
485 core: {
486 belief
487 food
488 diet
489 }
490 broker: {
491 mortgage
492 realtor
493 }
494 stocks: {
495 TSX
496 NYSE
497 NASDAQ
498 }
499 }
500
501 scenarios: {
502 why: {
503 y -> x
504 }
505 }
506 `)
507 err := runTestMain(t, ctx, dir, env, "life.d2")
508 assert.Success(t, err)
509
510 assert.TestdataDir(t, filepath.Join(dir, "life"))
511 },
512 },
513 {
514 name: "multiboard/life_index_d2",
515 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
516 writeFile(t, dir, "life/index.d2", `x -> y
517 layers: {
518 core: {
519 belief
520 food
521 diet
522 }
523 broker: {
524 mortgage
525 realtor
526 }
527 stocks: {
528 TSX
529 NYSE
530 NASDAQ
531 }
532 }
533
534 scenarios: {
535 why: {
536 y -> x
537 }
538 }
539 `)
540 err := runTestMain(t, ctx, dir, env, "life")
541 assert.Success(t, err)
542
543 assert.TestdataDir(t, filepath.Join(dir, "life"))
544 },
545 },
546 {
547 name: "internal_linked_pdf",
548 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
549 writeFile(t, dir, "in.d2", `cat: how does the cat go? {
550 link: layers.cat
551 }
552 layers: {
553 cat: {
554 home: {
555 link: _
556 }
557 the cat -> meow: goes
558 }
559 }
560 `)
561 err := runTestMain(t, ctx, dir, env, "in.d2", "out.pdf")
562 assert.Success(t, err)
563
564 pdf := readFile(t, dir, "out.pdf")
565 testdataIgnoreDiff(t, ".pdf", pdf)
566 },
567 },
568 {
569 name: "export_ppt",
570 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
571 writeFile(t, dir, "x.d2", `x -> y`)
572 err := runTestMain(t, ctx, dir, env, "x.d2", "x.ppt")
573 assert.ErrorString(t, err, `failed to wait xmain test: e2etests-cli/d2: bad usage: D2 does not support ppt exports, did you mean "pptx"?`)
574 },
575 },
576 {
577 name: "how_to_solve_problems_pptx",
578 skipCI: true,
579 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
580 writeFile(t, dir, "in.d2", `how to solve a hard problem? {
581 link: steps.2
582 }
583 steps: {
584 1: {
585 w: write down the problem
586 }
587 2: {
588 w -> t
589 t: think really hard about it
590 }
591 3: {
592 t -> w2
593 w2: write down the solution
594 w2: {
595 link: https://d2lang.com
596 }
597 }
598 }
599 `)
600 err := runTestMain(t, ctx, dir, env, "in.d2", "how_to_solve_problems.pptx")
601 assert.Success(t, err)
602
603 file := readFile(t, dir, "how_to_solve_problems.pptx")
604 err = pptx.Validate(file, 4)
605 assert.Success(t, err)
606 },
607 },
608 {
609 name: "how_to_solve_problems_gif",
610 skipCI: true,
611 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
612 writeFile(t, dir, "in.d2", `how to solve a hard problem? {
613 link: steps.2
614 }
615 steps: {
616 1: {
617 w: write down the problem
618 }
619 2: {
620 w -> t
621 t: think really hard about it
622 }
623 3: {
624 t -> w2
625 w2: write down the solution
626 w2: {
627 link: https://d2lang.com
628 }
629 }
630 }
631 `)
632 err := runTestMain(t, ctx, dir, env, "--animate-interval=10", "in.d2", "how_to_solve_problems.gif")
633 assert.Success(t, err)
634
635 gifBytes := readFile(t, dir, "how_to_solve_problems.gif")
636 err = xgif.Validate(gifBytes, 4, 10)
637 assert.Success(t, err)
638 },
639 },
640 {
641 name: "one-layer-gif",
642 skipCI: true,
643 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
644 writeFile(t, dir, "in.d2", `x`)
645 err := runTestMain(t, ctx, dir, env, "--animate-interval=10", "in.d2", "out.gif")
646 assert.Success(t, err)
647
648 gifBytes := readFile(t, dir, "out.gif")
649 err = xgif.Validate(gifBytes, 1, 10)
650 assert.Success(t, err)
651 },
652 },
653 {
654 name: "stdin",
655 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
656 stdin := bytes.NewBufferString(`x -> y`)
657 stdout := &bytes.Buffer{}
658 tms := testMain(dir, env, "-")
659 tms.Stdin = stdin
660 tms.Stdout = stdout
661 tms.Start(t, ctx)
662 defer tms.Cleanup(t)
663 err := tms.Wait(ctx)
664 assert.Success(t, err)
665
666 assert.Testdata(t, ".svg", stdout.Bytes())
667 },
668 },
669 {
670 name: "abspath",
671 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
672 writeFile(t, dir, "hello-world.d2", `x -> y`)
673 err := runTestMain(t, ctx, dir, env, filepath.Join(dir, "hello-world.d2"))
674 assert.Success(t, err)
675 svg := readFile(t, dir, "hello-world.svg")
676 assert.Testdata(t, ".svg", svg)
677 },
678 },
679 {
680 name: "import",
681 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
682 writeFile(t, dir, "hello-world.d2", `x: @x; y: @y; ...@p`)
683 writeFile(t, dir, "x.d2", `shape: circle`)
684 writeFile(t, dir, "y.d2", `shape: square`)
685 writeFile(t, dir, "p.d2", `x -> y`)
686 err := runTestMain(t, ctx, dir, env, filepath.Join(dir, "hello-world.d2"))
687 assert.Success(t, err)
688 svg := readFile(t, dir, "hello-world.svg")
689 assert.Testdata(t, ".svg", svg)
690 },
691 },
692 {
693 name: "import_vars",
694 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
695 writeFile(t, dir, "hello-world.d2", `vars: { d2-config: @config }; x -> y`)
696 writeFile(t, dir, "config.d2", `theme-id: 200`)
697 err := runTestMain(t, ctx, dir, env, filepath.Join(dir, "hello-world.d2"))
698 assert.Success(t, err)
699 svg := readFile(t, dir, "hello-world.svg")
700 assert.Testdata(t, ".svg", svg)
701 },
702 },
703 {
704 name: "import_spread_nested",
705 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
706 writeFile(t, dir, "hello-world.d2", `...@x.y`)
707 writeFile(t, dir, "x.d2", `y: { jon; jan }`)
708 err := runTestMain(t, ctx, dir, env, filepath.Join(dir, "hello-world.d2"))
709 assert.Success(t, err)
710 svg := readFile(t, dir, "hello-world.svg")
711 assert.Testdata(t, ".svg", svg)
712 },
713 },
714 {
715 name: "chain_import",
716 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
717 writeFile(t, dir, "hello-world.d2", `...@x`)
718 writeFile(t, dir, "x.d2", `...@y`)
719 writeFile(t, dir, "y.d2", `meow`)
720 err := runTestMain(t, ctx, dir, env, filepath.Join(dir, "hello-world.d2"))
721 assert.Success(t, err)
722 svg := readFile(t, dir, "hello-world.svg")
723 assert.Testdata(t, ".svg", svg)
724 },
725 },
726 {
727 name: "board_import",
728 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
729 writeFile(t, dir, "hello-world.d2", `x.link: layers.x; layers: { x: @x }`)
730 writeFile(t, dir, "x.d2", `y.link: layers.y; layers: { y: @y }`)
731 writeFile(t, dir, "y.d2", `meow`)
732 err := runTestMain(t, ctx, dir, env, filepath.Join(dir, "hello-world.d2"))
733 assert.Success(t, err)
734 t.Run("hello-world-x-y", func(t *testing.T) {
735 svg := readFile(t, dir, "hello-world/x/y.svg")
736 assert.Testdata(t, ".svg", svg)
737 })
738 t.Run("hello-world-x", func(t *testing.T) {
739 svg := readFile(t, dir, "hello-world/x/index.svg")
740 assert.Testdata(t, ".svg", svg)
741 })
742 t.Run("hello-world", func(t *testing.T) {
743 svg := readFile(t, dir, "hello-world/index.svg")
744 assert.Testdata(t, ".svg", svg)
745 })
746 },
747 },
748 {
749 name: "vars-config",
750 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
751 writeFile(t, dir, "hello-world.d2", `vars: {
752 d2-config: {
753 sketch: true
754 layout-engine: elk
755 }
756 }
757 x -> y -> a.dream
758 it -> was -> all -> a.dream
759 i used to read
760 `)
761 env.Setenv("D2_THEME", "1")
762 err := runTestMain(t, ctx, dir, env, "--pad=10", "hello-world.d2")
763 assert.Success(t, err)
764 svg := readFile(t, dir, "hello-world.svg")
765 assert.Testdata(t, ".svg", svg)
766 },
767 },
768 {
769 name: "renamed-board",
770 skipCI: true,
771 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
772 writeFile(t, dir, "in.d2", `cat: how does the cat go? {
773 link: layers.cat
774 }
775 a.link: "https://www.google.com/maps/place/Smoked+Out+BBQ/@37.3848007,-121.9513887,17z/data=!3m1!4b1!4m6!3m5!1s0x808fc9182ad4d38d:0x8e2f39c3e927b296!8m2!3d37.3848007!4d-121.9492!16s%2Fg%2F11gjt85zvf"
776 label: blah
777 layers: {
778 cat: {
779 label: dog
780 home: {
781 link: _
782 }
783 the cat -> meow: goes
784 }
785 }
786 `)
787 err := runTestMain(t, ctx, dir, env, "in.d2", "out.pdf")
788 assert.Success(t, err)
789
790 pdf := readFile(t, dir, "out.pdf")
791 testdataIgnoreDiff(t, ".pdf", pdf)
792 },
793 },
794 {
795 name: "no-nav-pdf",
796 skipCI: true,
797 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
798 writeFile(t, dir, "in.d2", `cat: how does the cat go? {
799 link: layers.cat
800 }
801 a.link: "https://www.google.com/maps/place/Smoked+Out+BBQ/@37.3848007,-121.9513887,17z/data=!3m1!4b1!4m6!3m5!1s0x808fc9182ad4d38d:0x8e2f39c3e927b296!8m2!3d37.3848007!4d-121.9492!16s%2Fg%2F11gjt85zvf"
802 label: ""
803 layers: {
804 cat: {
805 label: dog
806 home: {
807 link: _
808 }
809 the cat -> meow: goes
810 }
811 }
812 `)
813 err := runTestMain(t, ctx, dir, env, "in.d2", "out.pdf")
814 assert.Success(t, err)
815
816 pdf := readFile(t, dir, "out.pdf")
817 testdataIgnoreDiff(t, ".pdf", pdf)
818 },
819 },
820 {
821 name: "no-nav-pptx",
822 skipCI: true,
823 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
824 writeFile(t, dir, "in.d2", `cat: how does the cat go? {
825 link: layers.cat
826 }
827 a.link: "https://www.google.com/maps/place/Smoked+Out+BBQ/@37.3848007,-121.9513887,17z/data=!3m1!4b1!4m6!3m5!1s0x808fc9182ad4d38d:0x8e2f39c3e927b296!8m2!3d37.3848007!4d-121.9492!16s%2Fg%2F11gjt85zvf"
828 label: ""
829 layers: {
830 cat: {
831 label: dog
832 home: {
833 link: _
834 }
835 the cat -> meow: goes
836 }
837 }
838 `)
839 err := runTestMain(t, ctx, dir, env, "in.d2", "out.pptx")
840 assert.Success(t, err)
841
842 file := readFile(t, dir, "out.pptx")
843
844 assert.Success(t, err)
845 testdataIgnoreDiff(t, ".pptx", file)
846 },
847 },
848 {
849 name: "basic-fmt",
850 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
851 writeFile(t, dir, "hello-world.d2", `x ---> y`)
852 err := runTestMainPersist(t, ctx, dir, env, "fmt", "hello-world.d2")
853 assert.Success(t, err)
854 got := readFile(t, dir, "hello-world.d2")
855 assert.Equal(t, "x -> y\n", string(got))
856 },
857 },
858 {
859 name: "fmt-multiple-files",
860 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
861 writeFile(t, dir, "foo.d2", `a ---> b`)
862 writeFile(t, dir, "bar.d2", `x ---> y`)
863 err := runTestMainPersist(t, ctx, dir, env, "fmt", "foo.d2", "bar.d2")
864 assert.Success(t, err)
865 gotFoo := readFile(t, dir, "foo.d2")
866 gotBar := readFile(t, dir, "bar.d2")
867 assert.Equal(t, "a -> b\n", string(gotFoo))
868 assert.Equal(t, "x -> y\n", string(gotBar))
869 },
870 },
871 {
872 name: "watch-regular",
873 serial: true,
874 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
875 writeFile(t, dir, "index.d2", `
876 a -> b
877 b.link: layers.cream
878
879 layers: {
880 cream: {
881 c -> b
882 }
883 }`)
884 stderr := &bytes.Buffer{}
885 tms := testMain(dir, env, "--watch", "--browser=0", "index.d2")
886 tms.Stderr = stderr
887
888 tms.Start(t, ctx)
889 defer func() {
890
891 err := tms.Signal(ctx, os.Interrupt)
892 assert.Success(t, err)
893 }()
894
895
896 urlRE := regexp.MustCompile(`127.0.0.1:([0-9]+)`)
897 watchURL, err := waitLogs(ctx, stderr, urlRE)
898 assert.Success(t, err)
899 stderr.Reset()
900
901
902 c, _, err := websocket.Dial(ctx, fmt.Sprintf("ws://%s/watch", watchURL), nil)
903 assert.Success(t, err)
904 defer c.CloseNow()
905
906
907 _, msg, err := c.Read(ctx)
908 assert.Success(t, err)
909 aRE := regexp.MustCompile(`href=\\"([^\"]*)\\"`)
910 match := aRE.FindSubmatch(msg)
911 assert.Equal(t, 2, len(match))
912 linkedPath := match[1]
913
914 err = getWatchPage(ctx, t, fmt.Sprintf("http://%s/%s", watchURL, linkedPath))
915 assert.Success(t, err)
916
917 successRE := regexp.MustCompile(`broadcasting update to 1 client`)
918 _, err = waitLogs(ctx, stderr, successRE)
919 assert.Success(t, err)
920 },
921 },
922 {
923 name: "watch-ok-link",
924 serial: true,
925 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
926
927
928
929 writeFile(t, dir, "index.d2", `
930 a -> b
931 b.link: cream
932
933 layers: {
934 cream: {
935 c -> b
936 }
937 }`)
938 stderr := &bytes.Buffer{}
939 tms := testMain(dir, env, "--watch", "--browser=0", "index.d2")
940 tms.Stderr = stderr
941
942 tms.Start(t, ctx)
943 defer func() {
944
945 err := tms.Signal(ctx, os.Interrupt)
946 assert.Success(t, err)
947 }()
948
949
950 urlRE := regexp.MustCompile(`127.0.0.1:([0-9]+)`)
951 watchURL, err := waitLogs(ctx, stderr, urlRE)
952 assert.Success(t, err)
953
954 stderr.Reset()
955
956
957 c, _, err := websocket.Dial(ctx, fmt.Sprintf("ws://%s/watch", watchURL), nil)
958 assert.Success(t, err)
959 defer c.CloseNow()
960
961
962 _, msg, err := c.Read(ctx)
963 assert.Success(t, err)
964 aRE := regexp.MustCompile(`href=\\"([^\"]*)\\"`)
965 match := aRE.FindSubmatch(msg)
966 assert.Equal(t, 2, len(match))
967 linkedPath := match[1]
968
969 err = getWatchPage(ctx, t, fmt.Sprintf("http://%s/%s", watchURL, linkedPath))
970 assert.Success(t, err)
971
972 successRE := regexp.MustCompile(`broadcasting update to 1 client`)
973 _, err = waitLogs(ctx, stderr, successRE)
974 assert.Success(t, err)
975 },
976 },
977 {
978 name: "watch-bad-link",
979 serial: true,
980 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
981
982 writeFile(t, dir, "index.d2", `
983 a -> b
984 b.link: dream
985
986 layers: {
987 cream: {
988 c -> b
989 }
990 }`)
991 stderr := &bytes.Buffer{}
992 tms := testMain(dir, env, "--watch", "--browser=0", "index.d2")
993 tms.Stderr = stderr
994
995 tms.Start(t, ctx)
996 defer func() {
997
998 err := tms.Signal(ctx, os.Interrupt)
999 assert.Success(t, err)
1000 }()
1001
1002
1003 urlRE := regexp.MustCompile(`127.0.0.1:([0-9]+)`)
1004 watchURL, err := waitLogs(ctx, stderr, urlRE)
1005 assert.Success(t, err)
1006 stderr.Reset()
1007
1008
1009 c, _, err := websocket.Dial(ctx, fmt.Sprintf("ws://%s/watch", watchURL), nil)
1010 assert.Success(t, err)
1011 defer c.CloseNow()
1012
1013
1014 _, msg, err := c.Read(ctx)
1015 assert.Success(t, err)
1016 aRE := regexp.MustCompile(`href=\\"([^\"]*)\\"`)
1017 match := aRE.FindSubmatch(msg)
1018 assert.Equal(t, 2, len(match))
1019 linkedPath := match[1]
1020
1021 err = getWatchPage(ctx, t, fmt.Sprintf("http://%s/%s", watchURL, linkedPath))
1022 assert.Success(t, err)
1023
1024 successRE := regexp.MustCompile(`broadcasting update to 1 client`)
1025 _, err = waitLogs(ctx, stderr, successRE)
1026 assert.Success(t, err)
1027 },
1028 },
1029 {
1030 name: "watch-imported-file",
1031 serial: true,
1032 run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
1033 writeFile(t, dir, "a.d2", `
1034 ...@b
1035 `)
1036 writeFile(t, dir, "b.d2", `
1037 x
1038 `)
1039 stderr := &bytes.Buffer{}
1040 tms := testMain(dir, env, "--watch", "--browser=0", "a.d2")
1041 tms.Stderr = stderr
1042
1043 tms.Start(t, ctx)
1044 defer func() {
1045 err := tms.Signal(ctx, os.Interrupt)
1046 assert.Success(t, err)
1047 }()
1048
1049
1050 doneRE := regexp.MustCompile(`successfully compiled a.d2`)
1051 _, err := waitLogs(ctx, stderr, doneRE)
1052 assert.Success(t, err)
1053 stderr.Reset()
1054
1055
1056 writeFile(t, dir, "b.d2", `
1057 x -> y
1058 `)
1059 bRE := regexp.MustCompile(`detected change in b.d2`)
1060 _, err = waitLogs(ctx, stderr, bRE)
1061 assert.Success(t, err)
1062 stderr.Reset()
1063
1064
1065 writeFile(t, dir, "a.d2", `
1066 ...@b
1067 hey
1068 `)
1069 writeFile(t, dir, "b.d2", `
1070 x
1071 hi
1072 `)
1073 bothRE := regexp.MustCompile(`detected change in a.d2, b.d2`)
1074 _, err = waitLogs(ctx, stderr, bothRE)
1075 assert.Success(t, err)
1076
1077
1078 _, err = waitLogs(ctx, stderr, doneRE)
1079 assert.Success(t, err)
1080 stderr.Reset()
1081
1082
1083 writeFile(t, dir, "a.d2", `
1084 a
1085 `)
1086 _, err = waitLogs(ctx, stderr, doneRE)
1087 assert.Success(t, err)
1088 stderr.Reset()
1089
1090
1091 writeFile(t, dir, "b.d2", `
1092 y
1093 `)
1094
1095
1096 writeFile(t, dir, "a.d2", `
1097 c
1098 `)
1099
1100 _, err = waitLogs(ctx, stderr, doneRE)
1101 assert.Success(t, err)
1102 },
1103 },
1104 }
1105
1106 ctx := context.Background()
1107 for _, tc := range tca {
1108 tc := tc
1109 t.Run(tc.name, func(t *testing.T) {
1110 if !tc.serial {
1111 t.Parallel()
1112 }
1113
1114 if tc.skipCI && os.Getenv("CI") != "" {
1115 t.SkipNow()
1116 }
1117 if tc.skip {
1118 t.SkipNow()
1119 }
1120
1121 ctx, cancel := context.WithTimeout(ctx, time.Minute*5)
1122 defer cancel()
1123
1124 dir, cleanup := assert.TempDir(t)
1125 defer cleanup()
1126
1127 env := xos.NewEnv(nil)
1128
1129 tc.run(t, ctx, dir, env)
1130 })
1131 }
1132 }
1133
1134
1135
1136 func testMain(dir string, env *xos.Env, args ...string) *xmain.TestState {
1137 return &xmain.TestState{
1138 Run: d2cli.Run,
1139 Env: env,
1140 Args: append([]string{"e2etests-cli/d2"}, args...),
1141 PWD: dir,
1142 }
1143 }
1144
1145 func runTestMain(tb testing.TB, ctx context.Context, dir string, env *xos.Env, args ...string) error {
1146 err := runTestMainPersist(tb, ctx, dir, env, args...)
1147 if err != nil {
1148 return err
1149 }
1150 removeD2Files(tb, dir)
1151 return nil
1152 }
1153
1154 func runTestMainPersist(tb testing.TB, ctx context.Context, dir string, env *xos.Env, args ...string) error {
1155 tms := testMain(dir, env, args...)
1156 tms.Start(tb, ctx)
1157 defer tms.Cleanup(tb)
1158 err := tms.Wait(ctx)
1159 if err != nil {
1160 return err
1161 }
1162 return nil
1163 }
1164
1165 func writeFile(tb testing.TB, dir, fp, data string) {
1166 tb.Helper()
1167 err := os.MkdirAll(filepath.Dir(filepath.Join(dir, fp)), 0755)
1168 assert.Success(tb, err)
1169 assert.WriteFile(tb, filepath.Join(dir, fp), []byte(data), 0644)
1170 }
1171
1172 func readFile(tb testing.TB, dir, fp string) []byte {
1173 tb.Helper()
1174 return assert.ReadFile(tb, filepath.Join(dir, fp))
1175 }
1176
1177 func removeD2Files(tb testing.TB, dir string) {
1178 ea, err := os.ReadDir(dir)
1179 assert.Success(tb, err)
1180
1181 for _, e := range ea {
1182 if e.IsDir() {
1183 removeD2Files(tb, filepath.Join(dir, e.Name()))
1184 continue
1185 }
1186 ext := filepath.Ext(e.Name())
1187 if ext == ".d2" {
1188 assert.Remove(tb, filepath.Join(dir, e.Name()))
1189 }
1190 }
1191 }
1192
1193 func testdataIgnoreDiff(tb testing.TB, ext string, got []byte) {
1194 _ = diff.Testdata(filepath.Join("testdata", tb.Name()), ext, got)
1195 }
1196
1197
1198
1199 func getNumBoards(svg string) int {
1200 return strings.Count(svg, `class="d2`)
1201 }
1202
1203 var errRE = regexp.MustCompile(`err:`)
1204
1205 func waitLogs(ctx context.Context, buf *bytes.Buffer, pattern *regexp.Regexp) (string, error) {
1206 ticker := time.NewTicker(10 * time.Millisecond)
1207 defer ticker.Stop()
1208 var match string
1209 for i := 0; i < 1000 && match == ""; i++ {
1210 select {
1211 case <-ticker.C:
1212 out := buf.String()
1213 match = pattern.FindString(out)
1214 errMatch := errRE.FindString(out)
1215 if errMatch != "" {
1216 return "", errors.New(buf.String())
1217 }
1218 case <-ctx.Done():
1219 ticker.Stop()
1220 return "", fmt.Errorf("could not match pattern in log. logs: %s", buf.String())
1221 }
1222 }
1223 if match == "" {
1224 return "", errors.New(buf.String())
1225 }
1226
1227 return match, nil
1228 }
1229
1230 func getWatchPage(ctx context.Context, t *testing.T, page string) error {
1231 req, err := http.NewRequestWithContext(ctx, "GET", page, nil)
1232 if err != nil {
1233 return err
1234 }
1235
1236 var httpClient = &http.Client{}
1237 resp, err := httpClient.Do(req)
1238 if err != nil {
1239 return err
1240 }
1241 defer resp.Body.Close()
1242 if resp.StatusCode != 200 {
1243 return fmt.Errorf("status code: %d", resp.StatusCode)
1244 }
1245 return nil
1246 }
1247
View as plain text