...

Source file src/oss.terrastruct.com/d2/e2etests/e2e_test.go

Documentation: oss.terrastruct.com/d2/e2etests

     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  	// if the test is just testing a render/style thing, no need to exercise both engines
    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  // serde exercises serializing and deserializing the graph
   123  // We want to run all the steps leading up to serialization in the course of regular layout
   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  			// If measured texts exists, we are specifically exercising text measurements, no need to run on both layouts
   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  			// To compare deltas at a fixed scale
   211  			// Scale: go2.Pointer(1.),
   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  		// Check that it's valid SVG
   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