...

Source file src/oss.terrastruct.com/d2/d2layouts/d2sequence/layout_test.go

Documentation: oss.terrastruct.com/d2/d2layouts/d2sequence

     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  	// │   n1   │              │   n2   │
    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  		// just set some position as if it had been properly placed
    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  	// asserts that actors were placed in the expected x order and at y=0
    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  	// assert that edges were placed in y order and have the endpoints at their actors
    88  	// uses `nEdges` because Layout creates some vertical edges to represent the actor lifeline
    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  			// left to right
    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  	// check label positions
   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  	//   │  a  │                 │  b  │
   156  	//   └──┬──┘                 └──┬──┘
   157  	//      ├┐────────────────────►┌┤
   158  	//   t1 ││                     ││ t1
   159  	//      ├┘◄────────────────────└┤
   160  	//      ├┐──────────────────────►
   161  	//   t2 ││                      │
   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  		// just set some position as if it had been properly placed
   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  	// check properties
   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  	// Y diff of the 2 first edges
   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  	// check positions
   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  	// check routes
   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  	// |     ┌─────┐    container    ┌─────┐    |
   284  	// |     │  a  │                 │  b  │    |            ┌─────┐
   285  	// |     └──┬──┘                 └──┬──┘    ├────edge1───┤  c  │
   286  	// |        ├┐───────sdEdge1──────►┌┤       |            └─────┘
   287  	// |     t1 ││                     ││ t1    |
   288  	// |        ├┘◄──────sdEdge2───────└┤       |
   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  		// just set some position as if it had been properly placed
   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