d2/e2etests-cli/main_test.go

719 lines
18 KiB
Go
Raw Normal View History

package e2etests_cli
import (
"bytes"
"context"
2023-10-31 22:52:48 +00:00
"fmt"
"net/http"
2023-03-03 04:29:56 +00:00
"os"
"path/filepath"
2023-10-31 22:52:48 +00:00
"regexp"
2023-06-01 05:25:43 +00:00
"strings"
"testing"
"time"
2023-10-31 22:52:48 +00:00
"github.com/davecgh/go-spew/spew"
"nhooyr.io/websocket"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/diff"
"oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/util-go/xos"
2023-06-06 23:59:15 +00:00
"oss.terrastruct.com/d2/d2cli"
"oss.terrastruct.com/d2/lib/pptx"
"oss.terrastruct.com/d2/lib/xgif"
)
func TestCLI_E2E(t *testing.T) {
t.Parallel()
tca := []struct {
name string
skipCI bool
2023-03-31 03:39:01 +00:00
skip bool
run func(t *testing.T, ctx context.Context, dir string, env *xos.Env)
}{
{
name: "hello_world_png",
skipCI: true,
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
2023-03-03 04:02:36 +00:00
writeFile(t, dir, "hello-world.d2", `x -> y`)
err := runTestMain(t, ctx, dir, env, "hello-world.d2", "hello-world.png")
assert.Success(t, err)
2023-03-03 04:02:36 +00:00
png := readFile(t, dir, "hello-world.png")
testdataIgnoreDiff(t, ".png", png)
2023-03-03 04:02:36 +00:00
},
},
{
name: "hello_world_png_pad",
skipCI: true,
2023-03-03 04:02:36 +00:00
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "hello-world.d2", `x -> y`)
2023-03-03 04:09:05 +00:00
err := runTestMain(t, ctx, dir, env, "--pad=400", "hello-world.d2", "hello-world.png")
assert.Success(t, err)
png := readFile(t, dir, "hello-world.png")
testdataIgnoreDiff(t, ".png", png)
2023-03-03 04:09:05 +00:00
},
},
2023-03-18 05:29:51 +00:00
{
name: "center",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "hello-world.d2", `x -> y`)
err := runTestMain(t, ctx, dir, env, "--center=true", "hello-world.d2")
assert.Success(t, err)
svg := readFile(t, dir, "hello-world.svg")
assert.Testdata(t, ".svg", svg)
},
},
2023-04-30 04:24:03 +00:00
{
name: "flags-panic",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "hello-world.d2", `x -> y`)
err := runTestMain(t, ctx, dir, env, "layout", "dagre", "--dagre-nodesep", "50", "hello-world.d2")
assert.ErrorString(t, err, `failed to wait xmain test: e2etests-cli/d2: failed to unmarshal input to graph: `)
},
},
2023-04-07 17:50:13 +00:00
{
name: "empty-layer",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "empty-layer.d2", `layers: { x: {} }`)
err := runTestMain(t, ctx, dir, env, "empty-layer.d2")
assert.Success(t, err)
},
},
2023-07-27 06:10:05 +00:00
{
name: "layer-link",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "test.d2", `doh: { link: layers.test2 }; layers: { test2: @test2.d2 }`)
writeFile(t, dir, "test2.d2", `x: I'm a Mac { link: https://example.com }`)
err := runTestMain(t, ctx, dir, env, "test.d2", "layer-link.svg")
assert.Success(t, err)
assert.TestdataDir(t, filepath.Join(dir, "layer-link"))
},
},
2023-06-01 05:25:43 +00:00
{
// Skip the empty base board so the animation doesn't show blank for 1400ms
name: "empty-base",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "empty-base.d2", `steps: {
1: {
a -> b
}
2: {
b -> d
c -> d
}
3: {
d -> e
}
}`)
err := runTestMain(t, ctx, dir, env, "--animate-interval=1400", "empty-base.d2")
assert.Success(t, err)
svg := readFile(t, dir, "empty-base.svg")
assert.Testdata(t, ".svg", svg)
assert.Equal(t, 3, getNumBoards(string(svg)))
},
},
2023-03-23 20:37:28 +00:00
{
name: "animation",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "animation.d2", `Chicken's plan: {
style.font-size: 35
near: top-center
shape: text
}
2023-07-14 20:08:26 +00:00
steps: {
1: {
Approach road
}
2: {
Approach road -> Cross road
}
3: {
Cross road -> Make you wonder why
}
}
`)
err := runTestMain(t, ctx, dir, env, "--animate-interval=1400", "animation.d2")
assert.Success(t, err)
svg := readFile(t, dir, "animation.svg")
assert.Testdata(t, ".svg", svg)
},
},
{
name: "vars-animation",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "animation.d2", `vars: {
d2-config: {
theme-id: 300
}
}
Chicken's plan: {
style.font-size: 35
near: top-center
shape: text
}
2023-03-23 20:37:28 +00:00
steps: {
1: {
Approach road
}
2: {
Approach road -> Cross road
}
3: {
Cross road -> Make you wonder why
}
}
`)
err := runTestMain(t, ctx, dir, env, "--animate-interval=1400", "animation.d2")
assert.Success(t, err)
svg := readFile(t, dir, "animation.svg")
assert.Testdata(t, ".svg", svg)
},
},
2023-03-31 00:37:55 +00:00
{
name: "linked-path",
2023-03-31 03:39:01 +00:00
// TODO tempdir is random, resulting in different test results each time with the links
skip: true,
2023-03-31 00:37:55 +00:00
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "linked.d2", `cat: how does the cat go? {
link: layers.cat
}
layers: {
cat: {
home: {
link: _
}
the cat -> meow: goes
scenarios: {
big cat: {
the cat -> roar: goes
}
}
}
}
`)
err := runTestMain(t, ctx, dir, env, "linked.d2")
assert.Success(t, err)
assert.TestdataDir(t, filepath.Join(dir, "linked"))
},
},
2023-03-30 00:20:41 +00:00
{
name: "with-font",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "font.d2", `a: Why do computers get sick often?
b: Because their Windows are always open!
a -> b: italic font
`)
err := runTestMain(t, ctx, dir, env, "--font-bold=./RockSalt-Regular.ttf", "font.d2")
assert.Success(t, err)
svg := readFile(t, dir, "font.svg")
assert.Testdata(t, ".svg", svg)
},
},
2023-03-23 20:37:28 +00:00
{
name: "incompatible-animation",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "x.d2", `x -> y`)
err := runTestMain(t, ctx, dir, env, "--animate-interval=2", "x.d2", "x.png")
2023-04-14 13:52:27 +00:00
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.
2023-03-23 20:37:28 +00:00
You provided: .png`)
},
},
2023-03-03 04:09:05 +00:00
{
name: "hello_world_png_sketch",
skipCI: true,
2023-03-03 04:09:05 +00:00
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "hello-world.d2", `x -> y`)
err := runTestMain(t, ctx, dir, env, "--sketch", "hello-world.d2", "hello-world.png")
2023-03-03 04:02:36 +00:00
assert.Success(t, err)
png := readFile(t, dir, "hello-world.png")
// https://github.com/terrastruct/d2/pull/963#pullrequestreview-1323089392
testdataIgnoreDiff(t, ".png", png)
},
},
2023-03-03 04:29:56 +00:00
{
name: "multiboard/life",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "life.d2", `x -> y
layers: {
core: {
belief
food
diet
}
broker: {
mortgage
realtor
}
stocks: {
TSX
NYSE
NASDAQ
}
}
scenarios: {
why: {
y -> x
}
}
`)
err := runTestMain(t, ctx, dir, env, "life.d2")
assert.Success(t, err)
assert.TestdataDir(t, filepath.Join(dir, "life"))
},
},
{
name: "multiboard/life_index_d2",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
2023-03-03 04:29:56 +00:00
writeFile(t, dir, "life/index.d2", `x -> y
layers: {
core: {
belief
food
diet
}
broker: {
mortgage
realtor
}
stocks: {
TSX
NYSE
NASDAQ
}
}
scenarios: {
why: {
y -> x
}
}
`)
err := runTestMain(t, ctx, dir, env, "life")
assert.Success(t, err)
assert.TestdataDir(t, filepath.Join(dir, "life"))
},
},
2023-03-03 06:07:41 +00:00
{
name: "internal_linked_pdf",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "in.d2", `cat: how does the cat go? {
link: layers.cat
}
layers: {
cat: {
home: {
link: _
}
the cat -> meow: goes
}
}
`)
err := runTestMain(t, ctx, dir, env, "in.d2", "out.pdf")
assert.Success(t, err)
pdf := readFile(t, dir, "out.pdf")
testdataIgnoreDiff(t, ".pdf", pdf)
},
},
2023-04-14 22:44:01 +00:00
{
name: "export_ppt",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "x.d2", `x -> y`)
err := runTestMain(t, ctx, dir, env, "x.d2", "x.ppt")
assert.ErrorString(t, err, `failed to wait xmain test: e2etests-cli/d2: bad usage: D2 does not support ppt exports, did you mean "pptx"?`)
},
},
2023-04-06 18:16:32 +00:00
{
2023-04-10 19:29:27 +00:00
name: "how_to_solve_problems_pptx",
skipCI: true,
2023-04-06 18:16:32 +00:00
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
2023-04-10 20:16:01 +00:00
writeFile(t, dir, "in.d2", `how to solve a hard problem? {
link: steps.2
}
2023-04-06 18:16:32 +00:00
steps: {
1: {
w: write down the problem
}
2: {
w -> t
t: think really hard about it
}
3: {
t -> w2
w2: write down the solution
2023-04-10 20:16:01 +00:00
w2: {
link: https://d2lang.com
}
2023-04-06 18:16:32 +00:00
}
}
`)
err := runTestMain(t, ctx, dir, env, "in.d2", "how_to_solve_problems.pptx")
assert.Success(t, err)
file := readFile(t, dir, "how_to_solve_problems.pptx")
err = pptx.Validate(file, 4)
assert.Success(t, err)
},
},
2023-04-14 13:52:27 +00:00
{
name: "how_to_solve_problems_gif",
skipCI: true,
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "in.d2", `how to solve a hard problem? {
link: steps.2
}
steps: {
1: {
w: write down the problem
}
2: {
w -> t
t: think really hard about it
}
3: {
t -> w2
w2: write down the solution
w2: {
link: https://d2lang.com
}
}
}
`)
err := runTestMain(t, ctx, dir, env, "--animate-interval=10", "in.d2", "how_to_solve_problems.gif")
assert.Success(t, err)
gifBytes := readFile(t, dir, "how_to_solve_problems.gif")
err = xgif.Validate(gifBytes, 4, 10)
assert.Success(t, err)
},
},
2023-04-28 01:48:48 +00:00
{
name: "one-layer-gif",
skipCI: true,
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "in.d2", `x`)
err := runTestMain(t, ctx, dir, env, "--animate-interval=10", "in.d2", "out.gif")
assert.Success(t, err)
gifBytes := readFile(t, dir, "out.gif")
err = xgif.Validate(gifBytes, 1, 10)
assert.Success(t, err)
},
},
{
name: "stdin",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
stdin := bytes.NewBufferString(`x -> y`)
stdout := &bytes.Buffer{}
tms := testMain(dir, env, "-")
tms.Stdin = stdin
tms.Stdout = stdout
tms.Start(t, ctx)
defer tms.Cleanup(t)
err := tms.Wait(ctx)
assert.Success(t, err)
assert.Testdata(t, ".svg", stdout.Bytes())
},
},
{
name: "abspath",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "hello-world.d2", `x -> y`)
err := runTestMain(t, ctx, dir, env, filepath.Join(dir, "hello-world.d2"))
2023-06-06 20:33:56 +00:00
assert.Success(t, err)
svg := readFile(t, dir, "hello-world.svg")
assert.Testdata(t, ".svg", svg)
},
},
{
name: "import",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "hello-world.d2", `x: @x; y: @y; ...@p`)
writeFile(t, dir, "x.d2", `shape: circle`)
writeFile(t, dir, "y.d2", `shape: square`)
writeFile(t, dir, "p.d2", `x -> y`)
err := runTestMain(t, ctx, dir, env, filepath.Join(dir, "hello-world.d2"))
assert.Success(t, err)
svg := readFile(t, dir, "hello-world.svg")
assert.Testdata(t, ".svg", svg)
},
},
2023-07-14 20:08:26 +00:00
{
name: "import_vars",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "hello-world.d2", `vars: { d2-config: @config }; x -> y`)
writeFile(t, dir, "config.d2", `theme-id: 200`)
err := runTestMain(t, ctx, dir, env, filepath.Join(dir, "hello-world.d2"))
assert.Success(t, err)
svg := readFile(t, dir, "hello-world.svg")
assert.Testdata(t, ".svg", svg)
},
},
{
name: "import_spread_nested",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "hello-world.d2", `...@x.y`)
writeFile(t, dir, "x.d2", `y: { jon; jan }`)
err := runTestMain(t, ctx, dir, env, filepath.Join(dir, "hello-world.d2"))
2023-06-06 23:59:15 +00:00
assert.Success(t, err)
svg := readFile(t, dir, "hello-world.svg")
assert.Testdata(t, ".svg", svg)
},
},
{
name: "chain_import",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "hello-world.d2", `...@x`)
writeFile(t, dir, "x.d2", `...@y`)
writeFile(t, dir, "y.d2", `meow`)
err := runTestMain(t, ctx, dir, env, filepath.Join(dir, "hello-world.d2"))
2023-06-07 00:24:31 +00:00
assert.Success(t, err)
svg := readFile(t, dir, "hello-world.svg")
assert.Testdata(t, ".svg", svg)
},
},
{
name: "board_import",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "hello-world.d2", `x.link: layers.x; layers: { x: @x }`)
writeFile(t, dir, "x.d2", `y.link: layers.y; layers: { y: @y }`)
writeFile(t, dir, "y.d2", `meow`)
err := runTestMain(t, ctx, dir, env, filepath.Join(dir, "hello-world.d2"))
assert.Success(t, err)
2023-06-07 23:23:20 +00:00
t.Run("hello-world-x-y", func(t *testing.T) {
svg := readFile(t, dir, "hello-world/x/y.svg")
assert.Testdata(t, ".svg", svg)
})
t.Run("hello-world-x", func(t *testing.T) {
svg := readFile(t, dir, "hello-world/x/index.svg")
assert.Testdata(t, ".svg", svg)
})
t.Run("hello-world", func(t *testing.T) {
svg := readFile(t, dir, "hello-world/index.svg")
assert.Testdata(t, ".svg", svg)
})
},
},
2023-07-14 20:08:26 +00:00
{
name: "vars-config",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "hello-world.d2", `vars: {
d2-config: {
sketch: true
layout-engine: elk
}
}
x -> y -> a.dream
it -> was -> all -> a.dream
i used to read
`)
env.Setenv("D2_THEME", "1")
err := runTestMain(t, ctx, dir, env, "--pad=10", "hello-world.d2")
assert.Success(t, err)
svg := readFile(t, dir, "hello-world.svg")
assert.Testdata(t, ".svg", svg)
},
},
2023-08-02 18:38:53 +00:00
{
name: "basic-fmt",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "hello-world.d2", `x ---> y`)
err := runTestMainPersist(t, ctx, dir, env, "fmt", "hello-world.d2")
assert.Success(t, err)
got := readFile(t, dir, "hello-world.d2")
assert.Equal(t, "x -> y\n", string(got))
},
},
{
name: "fmt-multiple-files",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "foo.d2", `a ---> b`)
writeFile(t, dir, "bar.d2", `x ---> y`)
err := runTestMainPersist(t, ctx, dir, env, "fmt", "foo.d2", "bar.d2")
assert.Success(t, err)
gotFoo := readFile(t, dir, "foo.d2")
gotBar := readFile(t, dir, "bar.d2")
assert.Equal(t, "a -> b\n", string(gotFoo))
assert.Equal(t, "x -> y\n", string(gotBar))
},
},
2023-10-31 22:52:48 +00:00
{
name: "watch",
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
writeFile(t, dir, "index.d2", `
a -> b
b.link: cream
layers: {
cream: {
c -> b
}
}`)
stderr := &bytes.Buffer{}
tms := testMain(dir, env, "--watch", "--browser=0", "index.d2")
tms.Stderr = stderr
doneChan := make(chan struct{}, 1)
tms.Start(t, ctx)
defer tms.Cleanup(t)
go tms.Wait(ctx)
ticker := time.NewTicker(100 * time.Millisecond)
urlRE := regexp.MustCompile(`127.0.0.1:([0-9]+)`)
compiled := false
go func() {
var url string
for i := 0; i < 10 && url == ""; i++ {
select {
case <-ticker.C:
out := string(stderr.Bytes())
url = urlRE.FindString(out)
compiled, _ = regexp.MatchString(`failed to recompile`, out)
println("\033[1;31m--- DEBUG:", compiled, "\033[m")
case <-ctx.Done():
ticker.Stop()
return
}
}
if url != "" {
c, _, err := websocket.Dial(ctx, fmt.Sprintf("ws://%s/watch", url), nil)
assert.Success(t, err)
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://%s/cream", url), nil)
assert.Success(t, err)
var httpClient = &http.Client{}
resp, err := httpClient.Do(req)
assert.Success(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
time.Sleep(1000)
spew.Dump(string(stderr.Bytes()))
_, _, err = c.Read(ctx)
spew.Dump(err)
defer c.Close(websocket.StatusNormalClosure, "")
}
doneChan <- struct{}{}
}()
<-doneChan
err := tms.Signal(ctx, os.Interrupt)
assert.Error(t, err)
},
},
}
ctx := context.Background()
for _, tc := range tca {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if tc.skipCI && os.Getenv("CI") != "" {
t.SkipNow()
}
2023-03-31 03:39:01 +00:00
if tc.skip {
t.SkipNow()
}
ctx, cancel := context.WithTimeout(ctx, time.Minute*5)
defer cancel()
dir, cleanup := assert.TempDir(t)
defer cleanup()
env := xos.NewEnv(nil)
tc.run(t, ctx, dir, env)
})
}
}
// We do not run the CLI in its own process even though that makes it not truly e2e to
// test whether we're cleaning up state correctly.
func testMain(dir string, env *xos.Env, args ...string) *xmain.TestState {
return &xmain.TestState{
Run: d2cli.Run,
Env: env,
Args: append([]string{"e2etests-cli/d2"}, args...),
PWD: dir,
}
}
func runTestMain(tb testing.TB, ctx context.Context, dir string, env *xos.Env, args ...string) error {
2023-08-02 18:38:53 +00:00
err := runTestMainPersist(tb, ctx, dir, env, args...)
if err != nil {
return err
}
removeD2Files(tb, dir)
return nil
}
func runTestMainPersist(tb testing.TB, ctx context.Context, dir string, env *xos.Env, args ...string) error {
tms := testMain(dir, env, args...)
tms.Start(tb, ctx)
defer tms.Cleanup(tb)
2023-03-03 04:29:56 +00:00
err := tms.Wait(ctx)
if err != nil {
return err
}
return nil
}
2023-03-03 04:02:36 +00:00
func writeFile(tb testing.TB, dir, fp, data string) {
tb.Helper()
2023-03-03 04:29:56 +00:00
err := os.MkdirAll(filepath.Dir(filepath.Join(dir, fp)), 0755)
assert.Success(tb, err)
2023-03-03 04:02:36 +00:00
assert.WriteFile(tb, filepath.Join(dir, fp), []byte(data), 0644)
}
func readFile(tb testing.TB, dir, fp string) []byte {
tb.Helper()
return assert.ReadFile(tb, filepath.Join(dir, fp))
}
2023-03-03 04:29:56 +00:00
func removeD2Files(tb testing.TB, dir string) {
ea, err := os.ReadDir(dir)
assert.Success(tb, err)
for _, e := range ea {
if e.IsDir() {
removeD2Files(tb, filepath.Join(dir, e.Name()))
continue
}
ext := filepath.Ext(e.Name())
if ext == ".d2" {
assert.Remove(tb, filepath.Join(dir, e.Name()))
}
}
}
func testdataIgnoreDiff(tb testing.TB, ext string, got []byte) {
_ = diff.Testdata(filepath.Join("testdata", tb.Name()), ext, got)
}
2023-06-01 05:25:43 +00:00
// getNumBoards gets the number of boards in an SVG file through a non-robust pattern search
// If the renderer changes, this must change
func getNumBoards(svg string) int {
return strings.Count(svg, `class="d2`)
}