1 package d2sequence_test
2
3 import (
4 "context"
5 "strings"
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9
10 "oss.terrastruct.com/d2/d2compiler"
11 "oss.terrastruct.com/d2/d2graph"
12 "oss.terrastruct.com/d2/d2layouts/d2sequence"
13 "oss.terrastruct.com/d2/d2target"
14 "oss.terrastruct.com/d2/lib/geo"
15 "oss.terrastruct.com/d2/lib/label"
16 "oss.terrastruct.com/d2/lib/log"
17 "oss.terrastruct.com/d2/lib/shape"
18 )
19
20 func TestBasicSequenceDiagram(t *testing.T) {
21
22
23
24
25
26
27
28
29
30
31
32
33 input := `
34 shape: sequence_diagram
35 n1 -> n2: left to right
36 n2 -> n1: right to left
37 n1 -> n2
38 n2 -> n1
39 `
40 g, _, err := d2compiler.Compile("", strings.NewReader(input), nil)
41 assert.Nil(t, err)
42
43 n1, has := g.Root.HasChild([]string{"n1"})
44 assert.True(t, has)
45 n2, has := g.Root.HasChild([]string{"n2"})
46 assert.True(t, has)
47
48 n1.Box = geo.NewBox(nil, 100, 100)
49 n2.Box = geo.NewBox(nil, 30, 30)
50
51 nEdges := len(g.Edges)
52
53 ctx := log.WithTB(context.Background(), t, nil)
54 d2sequence.Layout(ctx, g, func(ctx context.Context, g *d2graph.Graph) error {
55
56 for _, obj := range g.Objects {
57 obj.TopLeft = geo.NewPoint(0, 0)
58 }
59
60 for _, edge := range g.Edges {
61 edge.Route = []*geo.Point{geo.NewPoint(1, 1)}
62 }
63 return nil
64 })
65
66
67 actors := []*d2graph.Object{
68 g.Objects[0],
69 g.Objects[1],
70 }
71 for i := 1; i < len(actors); i++ {
72 if actors[i].TopLeft.X < actors[i-1].TopLeft.X {
73 t.Fatalf("expected actor[%d].TopLeft.X > actor[%d].TopLeft.X", i, i-1)
74 }
75 actorBottom := actors[i].TopLeft.Y + actors[i].Height
76 prevActorBottom := actors[i-1].TopLeft.Y + actors[i-1].Height
77 if actorBottom != prevActorBottom {
78 t.Fatalf("expected actor[%d] and actor[%d] to be at the same bottom y", i, i-1)
79 }
80 }
81
82 nExpectedEdges := nEdges + len(actors)
83 if len(g.Edges) != nExpectedEdges {
84 t.Fatalf("expected %d edges, got %d", nExpectedEdges, len(g.Edges))
85 }
86
87
88
89 for i := 0; i < nEdges; i++ {
90 edge := g.Edges[i]
91 if len(edge.Route) != 2 {
92 t.Fatalf("expected edge[%d] to have only 2 points", i)
93 }
94 if edge.Route[0].Y != edge.Route[1].Y {
95 t.Fatalf("expected edge[%d] to be a horizontal line", i)
96 }
97 if edge.Src.TopLeft.X < edge.Dst.TopLeft.X {
98
99 if edge.Route[0].X != edge.Src.Center().X {
100 t.Fatalf("expected edge[%d] x to be at the actor center", i)
101 }
102
103 if edge.Route[1].X != edge.Dst.Center().X {
104 t.Fatalf("expected edge[%d] x to be at the actor center", i)
105 }
106 } else {
107 if edge.Route[0].X != edge.Src.Center().X {
108 t.Fatalf("expected edge[%d] x to be at the actor center", i)
109 }
110
111 if edge.Route[1].X != edge.Dst.Center().X {
112 t.Fatalf("expected edge[%d] x to be at the actor center", i)
113 }
114 }
115 if i > 0 {
116 prevEdge := g.Edges[i-1]
117 if edge.Route[0].Y < prevEdge.Route[0].Y {
118 t.Fatalf("expected edge[%d].TopLeft.Y > edge[%d].TopLeft.Y", i, i-1)
119 }
120 }
121 }
122
123 lastSequenceEdge := g.Edges[nEdges-1]
124 for i := nEdges; i < nExpectedEdges; i++ {
125 edge := g.Edges[i]
126 if len(edge.Route) != 2 {
127 t.Fatalf("expected lifeline edge[%d] to have only 2 points", i)
128 }
129 if edge.Route[0].X != edge.Route[1].X {
130 t.Fatalf("expected lifeline edge[%d] to be a vertical line", i)
131 }
132 if edge.Route[0].X != edge.Src.Center().X {
133 t.Fatalf("expected lifeline edge[%d] x to be at the actor center", i)
134 }
135 if edge.Route[0].Y != edge.Src.Height+edge.Src.TopLeft.Y {
136 t.Fatalf("expected lifeline edge[%d] to start at the bottom of the source actor", i)
137 }
138 if edge.Route[1].Y < lastSequenceEdge.Route[0].Y {
139 t.Fatalf("expected lifeline edge[%d] to end after the last sequence edge", i)
140 }
141 }
142
143
144 if *g.Edges[0].LabelPosition != label.InsideMiddleCenter.String() {
145 t.Fatalf("expected edge label to be placed on %s, got %s", label.InsideMiddleCenter, *g.Edges[0].LabelPosition)
146 }
147
148 if *g.Edges[1].LabelPosition != label.InsideMiddleCenter.String() {
149 t.Fatalf("expected edge label to be placed on %s, got %s", label.InsideMiddleCenter, *g.Edges[0].LabelPosition)
150 }
151 }
152
153 func TestSpansSequenceDiagram(t *testing.T) {
154
155
156
157
158
159
160
161
162
163
164 input := `
165 shape: sequence_diagram
166 a: { shape: person }
167 b
168
169 a.t1: {
170 shape: diamond
171 label: label
172 }
173 a.t1 -> b.t1
174 b.t1 -> a.t1
175
176 a.t2 -> b
177 b -> a.t2`
178
179 ctx := log.WithTB(context.Background(), t, nil)
180 g, _, err := d2compiler.Compile("", strings.NewReader(input), nil)
181 assert.Nil(t, err)
182
183 g.Root.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram}
184
185 a, has := g.Root.HasChild([]string{"a"})
186 assert.True(t, has)
187
188 a_t1, has := a.HasChild([]string{"t1"})
189 assert.True(t, has)
190
191 a_t2, has := a.HasChild([]string{"t2"})
192 assert.True(t, has)
193
194 b, has := g.Root.HasChild([]string{"b"})
195 assert.True(t, has)
196 b.Box = geo.NewBox(nil, 30, 30)
197
198 b_t1, has := b.HasChild([]string{"t1"})
199 assert.True(t, has)
200
201 a.Box = geo.NewBox(nil, 100, 100)
202 a_t1.Box = geo.NewBox(nil, 100, 100)
203 a_t2.Box = geo.NewBox(nil, 100, 100)
204 b.Box = geo.NewBox(nil, 30, 30)
205 b_t1.Box = geo.NewBox(nil, 100, 100)
206
207 d2sequence.Layout(ctx, g, func(ctx context.Context, g *d2graph.Graph) error {
208
209 for _, obj := range g.Objects {
210 obj.TopLeft = geo.NewPoint(0, 0)
211 }
212
213 for _, edge := range g.Edges {
214 edge.Route = []*geo.Point{geo.NewPoint(1, 1)}
215 }
216 return nil
217 })
218
219
220 assert.Equal(t, strings.ToLower(shape.PERSON_TYPE), strings.ToLower(a.Shape.Value))
221
222 if a_t1.Label.Value != "" {
223 t.Fatalf("expected no label for span, got %s", a_t1.Label.Value)
224 }
225
226 if a_t1.Shape.Value != shape.SQUARE_TYPE {
227 t.Fatalf("expected square shape for span, got %s", a_t1.Shape.Value)
228 }
229
230 if a_t1.Height != b_t1.Height {
231 t.Fatalf("expected a.t1 and b.t1 to have the same height, got %.5f and %.5f", a_t1.Height, b_t1.Height)
232 }
233
234 for _, span := range []*d2graph.Object{a_t1, a_t2, b_t1} {
235 if span.ZIndex != d2sequence.SPAN_Z_INDEX {
236 t.Fatalf("expected span ZIndex=%d, got %d", d2sequence.SPAN_Z_INDEX, span.ZIndex)
237 }
238 }
239
240
241 expectedHeight := g.Edges[1].Route[0].Y - g.Edges[0].Route[0].Y + (2 * d2sequence.SPAN_MESSAGE_PAD)
242 if a_t1.Height != expectedHeight {
243 t.Fatalf("expected a.t1 height to be %.5f, got %.5f", expectedHeight, a_t1.Height)
244 }
245
246 if a_t1.Width != d2sequence.SPAN_BASE_WIDTH {
247 t.Fatalf("expected span width to be %.5f, got %.5f", d2sequence.SPAN_BASE_WIDTH, a_t1.Width)
248 }
249
250
251 if a.Center().X != a_t1.Center().X {
252 t.Fatal("expected a_t1.X = a.X")
253 }
254 if a.Center().X != a_t2.Center().X {
255 t.Fatal("expected a_t2.X = a.X")
256 }
257 if b.Center().X != b_t1.Center().X {
258 t.Fatal("expected b_t1.X = b.X")
259 }
260 if a_t1.TopLeft.Y != b_t1.TopLeft.Y {
261 t.Fatal("expected a.t1 and b.t1 to be placed at the same Y")
262 }
263 if a_t1.TopLeft.Y+d2sequence.SPAN_MESSAGE_PAD != g.Edges[0].Route[0].Y {
264 t.Fatal("expected a.t1 to be placed at the same Y of the first message")
265 }
266
267
268 if g.Edges[0].Route[0].X != a_t1.TopLeft.X+a_t1.Width {
269 t.Fatal("expected the first message to start on a.t1 top right X")
270 }
271
272 if g.Edges[0].Route[1].X != b_t1.TopLeft.X {
273 t.Fatal("expected the first message to end on b.t1 top left X")
274 }
275
276 if g.Edges[2].Route[1].X != b.Center().X {
277 t.Fatal("expected the third message to end on b.t1 center X")
278 }
279 }
280
281 func TestNestedSequenceDiagrams(t *testing.T) {
282
283
284
285
286
287
288
289
290 input := `container: {
291 shape: sequence_diagram
292 a: { shape: person }
293 b
294 a.t1 -> b.t1: sequence diagram edge 1
295 b.t1 -> a.t1: sequence diagram edge 2
296 }
297 c
298 container -> c: edge 1
299 `
300 ctx := log.WithTB(context.Background(), t, nil)
301 g, _, err := d2compiler.Compile("", strings.NewReader(input), nil)
302 assert.Nil(t, err)
303
304 container, has := g.Root.HasChild([]string{"container"})
305 assert.True(t, has)
306 container.Box = geo.NewBox(nil, 500, 500)
307
308 a, has := container.HasChild([]string{"a"})
309 assert.True(t, has)
310 a.Box = geo.NewBox(nil, 100, 100)
311
312 a_t1, has := a.HasChild([]string{"t1"})
313 assert.True(t, has)
314 a_t1.Box = geo.NewBox(nil, 100, 100)
315
316 b, has := container.HasChild([]string{"b"})
317 assert.True(t, has)
318 b.Box = geo.NewBox(nil, 30, 30)
319
320 b_t1, has := b.HasChild([]string{"t1"})
321 assert.True(t, has)
322 b_t1.Box = geo.NewBox(nil, 100, 100)
323
324 c := g.Root.EnsureChild([]string{"c"})
325 c.Box = geo.NewBox(nil, 100, 100)
326 c.Shape = d2graph.Scalar{Value: d2target.ShapeSquare}
327
328 layoutFn := func(ctx context.Context, g *d2graph.Graph) error {
329 if len(g.Objects) != 2 {
330 t.Fatal("expected only diagram objects for layout")
331 }
332 for _, obj := range g.Objects {
333 if obj == a || obj == a_t1 || obj == b || obj == b_t1 {
334 t.Fatal("expected to have removed all sequence diagram objects")
335 }
336 }
337 if len(container.ChildrenArray) != 0 {
338 t.Fatalf("expected no `container` children, got %d", len(container.ChildrenArray))
339 }
340
341 if len(container.Children) != len(container.ChildrenArray) {
342 t.Fatal("container children mismatch")
343 }
344
345 assert.Equal(t, 1, len(g.Edges))
346
347
348 for _, obj := range g.Objects {
349 obj.TopLeft = geo.NewPoint(0, 0)
350 }
351
352 for _, edge := range g.Edges {
353 edge.Route = []*geo.Point{geo.NewPoint(1, 1)}
354 }
355
356 return nil
357 }
358
359 if err = d2sequence.Layout(ctx, g, layoutFn); err != nil {
360 t.Fatal(err)
361 }
362
363 if len(g.Edges) != 5 {
364 t.Fatal("expected graph to have all edges and lifelines after layout")
365 }
366
367 for _, obj := range g.Objects {
368 if obj.TopLeft == nil {
369 t.Fatal("expected to have placed all objects")
370 }
371 }
372 for _, edge := range g.Edges {
373 if len(edge.Route) == 0 {
374 t.Fatal("expected to have routed all edges")
375 }
376 }
377 }
378
379 func TestSelfEdges(t *testing.T) {
380 g := d2graph.NewGraph()
381 g.Root.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram}
382 n1 := g.Root.EnsureChild([]string{"n1"})
383 n1.Box = geo.NewBox(nil, 100, 100)
384
385 g.Edges = []*d2graph.Edge{
386 {
387 Src: n1,
388 Dst: n1,
389 Index: 0,
390 Attributes: d2graph.Attributes{
391 Label: d2graph.Scalar{Value: "left to right"},
392 },
393 },
394 }
395
396 ctx := log.WithTB(context.Background(), t, nil)
397 d2sequence.Layout(ctx, g, func(ctx context.Context, g *d2graph.Graph) error {
398 return nil
399 })
400
401 route := g.Edges[0].Route
402 if len(route) != 4 {
403 t.Fatalf("expected route to have 4 points, got %d", len(route))
404 }
405
406 if route[0].X != route[3].X {
407 t.Fatalf("route does not end at the same actor, start at %.5f, end at %.5f", route[0].X, route[3].X)
408 }
409
410 if route[3].Y-route[0].Y != d2sequence.MIN_MESSAGE_DISTANCE*1.5 {
411 t.Fatalf("expected route height to be %.5f, got %.5f", d2sequence.MIN_MESSAGE_DISTANCE*1.5, route[3].Y-route[0].Y)
412 }
413 }
414
415 func TestSequenceToDescendant(t *testing.T) {
416 g := d2graph.NewGraph()
417 g.Root.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram}
418 a := g.Root.EnsureChild([]string{"a"})
419 a.Box = geo.NewBox(nil, 100, 100)
420 a.Attributes = d2graph.Attributes{
421 Shape: d2graph.Scalar{Value: shape.PERSON_TYPE},
422 }
423 a_t1 := a.EnsureChild([]string{"t1"})
424 a_t1.Box = geo.NewBox(nil, 16, 80)
425
426 g.Edges = []*d2graph.Edge{
427 {
428 Src: a,
429 Dst: a_t1,
430 Index: 0,
431 }, {
432 Src: a_t1,
433 Dst: a,
434 Index: 0,
435 },
436 }
437
438 ctx := log.WithTB(context.Background(), t, nil)
439 d2sequence.Layout(ctx, g, func(ctx context.Context, g *d2graph.Graph) error {
440 return nil
441 })
442
443 route1 := g.Edges[0].Route
444 if len(route1) != 4 {
445 t.Fatal("expected route with 4 points")
446 }
447 if route1[0].X != a.Center().X {
448 t.Fatal("expected route to start at `a` lifeline")
449 }
450 if route1[3].X != a_t1.TopLeft.X+a_t1.Width {
451 t.Fatal("expected route to end at `a.t1` right side")
452 }
453
454 route2 := g.Edges[1].Route
455 if len(route2) != 4 {
456 t.Fatal("expected route with 4 points")
457 }
458 if route2[0].X != a_t1.TopLeft.X+a_t1.Width {
459 t.Fatal("expected route to start at `a.t1` right side")
460 }
461 if route2[3].X != a.Center().X {
462 t.Fatal("expected route to end at `a` lifeline")
463 }
464 }
465
View as plain text