1 package e2etests
2
3 import (
4 "context"
5 "encoding/xml"
6 "fmt"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11
12 "cdr.dev/slog"
13 "golang.org/x/tools/txtar"
14
15 trequire "github.com/stretchr/testify/require"
16
17 "oss.terrastruct.com/util-go/assert"
18 "oss.terrastruct.com/util-go/diff"
19 "oss.terrastruct.com/util-go/go2"
20
21 "oss.terrastruct.com/d2/d2compiler"
22 "oss.terrastruct.com/d2/d2graph"
23 "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
24 "oss.terrastruct.com/d2/d2layouts/d2elklayout"
25 "oss.terrastruct.com/d2/d2lib"
26 "oss.terrastruct.com/d2/d2plugin"
27 "oss.terrastruct.com/d2/d2renderers/d2animate"
28 "oss.terrastruct.com/d2/d2renderers/d2svg"
29 "oss.terrastruct.com/d2/d2target"
30 "oss.terrastruct.com/d2/lib/log"
31 "oss.terrastruct.com/d2/lib/textmeasure"
32 )
33
34 func TestE2E(t *testing.T) {
35 t.Parallel()
36
37 t.Run("sanity", testSanity)
38 t.Run("stable", testStable)
39 t.Run("regression", testRegression)
40 t.Run("patterns", testPatterns)
41 t.Run("todo", testTodo)
42 t.Run("measured", testMeasured)
43 t.Run("unicode", testUnicode)
44 t.Run("root", testRoot)
45 t.Run("themes", testThemes)
46 t.Run("txtar", testTxtar)
47 }
48
49 func testSanity(t *testing.T) {
50 tcs := []testCase{
51 {
52 name: "empty",
53 script: ``,
54 },
55 {
56 name: "basic",
57 script: `a -> b
58 `,
59 },
60 {
61 name: "1 to 2",
62 script: `a -> b
63 a -> c
64 `,
65 },
66 {
67 name: "child to child",
68 script: `a.b -> c.d
69 `,
70 },
71 {
72 name: "connection label",
73 script: `a -> b: hello
74 `,
75 },
76 }
77 runa(t, tcs)
78 }
79
80 func testTxtar(t *testing.T) {
81 var tcs []testCase
82 archive, err := txtar.ParseFile("./testdata/txtar.txt")
83 assert.Success(t, err)
84 for _, f := range archive.Files {
85 tcs = append(tcs, testCase{
86 name: f.Name,
87 script: string(f.Data),
88 })
89 }
90 runa(t, tcs)
91 }
92
93 type testCase struct {
94 name string
95
96 justDagre bool
97 testSerialization bool
98 script string
99 mtexts []*d2target.MText
100 assertions func(t *testing.T, diagram *d2target.Diagram)
101 skip bool
102 dagreFeatureError string
103 elkFeatureError string
104 expErr string
105 themeID *int64
106 }
107
108 func runa(t *testing.T, tcs []testCase) {
109 for _, tc := range tcs {
110 tc := tc
111 t.Run(tc.name, func(t *testing.T) {
112 if tc.skip {
113 t.Skip()
114 }
115 t.Parallel()
116
117 run(t, tc)
118 })
119 }
120 }
121
122
123
124 func serde(t *testing.T, tc testCase, ruler *textmeasure.Ruler) {
125 g, _, err := d2compiler.Compile("", strings.NewReader(tc.script), &d2compiler.CompileOptions{
126 UTF16Pos: false,
127 })
128 trequire.Nil(t, err)
129 if len(g.Objects) > 0 {
130 err = g.SetDimensions(nil, ruler, nil)
131 trequire.Nil(t, err)
132 }
133 b, err := d2graph.SerializeGraph(g)
134 trequire.Nil(t, err)
135 var newG d2graph.Graph
136 err = d2graph.DeserializeGraph(b, &newG)
137 trequire.Nil(t, err)
138 trequire.Nil(t, d2graph.CompareSerializedGraph(g, &newG))
139 }
140
141 func run(t *testing.T, tc testCase) {
142 ctx := context.Background()
143 ctx = log.WithTB(ctx, t, nil)
144 ctx = log.Leveled(ctx, slog.LevelDebug)
145
146 var ruler *textmeasure.Ruler
147 var err error
148 if tc.mtexts == nil {
149 ruler, err = textmeasure.NewRuler()
150 trequire.Nil(t, err)
151
152 serde(t, tc, ruler)
153 }
154
155 layoutsTested := []string{"dagre"}
156 if !tc.justDagre {
157 layoutsTested = append(layoutsTested, "elk")
158 }
159
160 layoutResolver := func(engine string) (d2graph.LayoutGraph, error) {
161 layout := d2dagrelayout.DefaultLayout
162 if strings.EqualFold(engine, "elk") {
163 layout = d2elklayout.DefaultLayout
164 }
165 if tc.testSerialization {
166 return func(ctx context.Context, g *d2graph.Graph) error {
167 bytes, err := d2graph.SerializeGraph(g)
168 if err != nil {
169 return err
170 }
171 err = d2graph.DeserializeGraph(bytes, g)
172 if err != nil {
173 return err
174 }
175 err = layout(ctx, g)
176 if err != nil {
177 return err
178 }
179 bytes, err = d2graph.SerializeGraph(g)
180 if err != nil {
181 return err
182 }
183 return d2graph.DeserializeGraph(bytes, g)
184 }, nil
185 }
186 return layout, nil
187 }
188
189 for _, layoutName := range layoutsTested {
190 var plugin d2plugin.Plugin
191 if layoutName == "dagre" {
192 plugin = &d2plugin.DagrePlugin
193 } else if layoutName == "elk" {
194
195 if tc.mtexts != nil {
196 continue
197 }
198 plugin = &d2plugin.ELKPlugin
199 }
200
201 compileOpts := &d2lib.CompileOptions{
202 Ruler: ruler,
203 MeasuredTexts: tc.mtexts,
204 Layout: go2.Pointer(layoutName),
205 LayoutResolver: layoutResolver,
206 }
207 renderOpts := &d2svg.RenderOpts{
208 Pad: go2.Pointer(int64(0)),
209 ThemeID: tc.themeID,
210
211
212 }
213
214 diagram, g, err := d2lib.Compile(ctx, tc.script, compileOpts, renderOpts)
215 if tc.expErr != "" {
216 assert.Error(t, err)
217 assert.ErrorString(t, err, tc.expErr)
218 return
219 } else {
220 assert.Success(t, err)
221 }
222
223 pluginInfo, err := plugin.Info(ctx)
224 assert.Success(t, err)
225
226 err = d2plugin.FeatureSupportCheck(pluginInfo, g)
227 switch layoutName {
228 case "dagre":
229 if tc.dagreFeatureError != "" {
230 assert.Error(t, err)
231 assert.ErrorString(t, err, tc.dagreFeatureError)
232 return
233 }
234 case "elk":
235 if tc.elkFeatureError != "" {
236 assert.Error(t, err)
237 assert.ErrorString(t, err, tc.elkFeatureError)
238 return
239 }
240 }
241 assert.Success(t, err)
242
243 if tc.assertions != nil {
244 t.Run("assertions", func(t *testing.T) {
245 tc.assertions(t, diagram)
246 })
247 }
248
249 dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestE2E/"), layoutName)
250 pathGotSVG := filepath.Join(dataPath, "sketch.got.svg")
251
252 if len(diagram.Layers) > 0 || len(diagram.Scenarios) > 0 || len(diagram.Steps) > 0 {
253 masterID, err := diagram.HashID()
254 assert.Success(t, err)
255 renderOpts.MasterID = masterID
256 }
257 boards, err := d2svg.RenderMultiboard(diagram, renderOpts)
258 assert.Success(t, err)
259
260 var svgBytes []byte
261 if len(boards) == 1 {
262 svgBytes = boards[0]
263 } else {
264 svgBytes, err = d2animate.Wrap(diagram, boards, *renderOpts, 1000)
265 assert.Success(t, err)
266 }
267
268 err = os.MkdirAll(dataPath, 0755)
269 assert.Success(t, err)
270 err = os.WriteFile(pathGotSVG, svgBytes, 0600)
271 assert.Success(t, err)
272
273
274 var xmlParsed interface{}
275 err = xml.Unmarshal(svgBytes, &xmlParsed)
276 assert.Success(t, err)
277
278 var err2 error
279 err = diff.TestdataJSON(filepath.Join(dataPath, "board"), diagram)
280 if os.Getenv("SKIP_SVG_CHECK") == "" {
281 err2 = diff.Testdata(filepath.Join(dataPath, "sketch"), ".svg", svgBytes)
282 }
283 assert.Success(t, err)
284 assert.Success(t, err2)
285 }
286 }
287
288 func mdTestScript(md string) string {
289 return fmt.Sprintf(`
290 md: |md
291 %s
292 |
293 a -> md -> b
294 `, md)
295 }
296
297 func loadFromFile(t *testing.T, name string) testCase {
298 fn := filepath.Join("testdata", "files", fmt.Sprintf("%s.d2", name))
299 d2Text, err := os.ReadFile(fn)
300 if err != nil {
301 t.Fatalf("failed to load test from file:%s. %s", name, err.Error())
302 }
303
304 return testCase{
305 name: name,
306 script: string(d2Text),
307 }
308 }
309
310 func loadFromFileWithOptions(t *testing.T, name string, options testCase) testCase {
311 tc := options
312 tc.name = name
313 tc.script = loadFromFile(t, name).script
314 return tc
315 }
316
View as plain text