...

Source file src/oss.terrastruct.com/d2/d2parser/parse_test.go

Documentation: oss.terrastruct.com/d2/d2parser

     1  package d2parser_test
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"strings"
     7  	"testing"
     8  
     9  	"oss.terrastruct.com/util-go/assert"
    10  	"oss.terrastruct.com/util-go/diff"
    11  
    12  	"oss.terrastruct.com/d2/d2ast"
    13  	"oss.terrastruct.com/d2/d2format"
    14  	"oss.terrastruct.com/d2/d2parser"
    15  )
    16  
    17  type testCase struct {
    18  	name   string
    19  	text   string
    20  	assert func(t testing.TB, ast *d2ast.Map, err error)
    21  }
    22  
    23  // TODO: next step for parser is writing as many tests and grouping them nicely
    24  // TODO: add assertions
    25  // to layout *all* expected behavior.
    26  func TestParse(t *testing.T) {
    27  	t.Parallel()
    28  
    29  	var testCases = []testCase{
    30  		{
    31  			name: "empty",
    32  			text: ``,
    33  		},
    34  		{
    35  			name: "semicolons",
    36  			text: `;;;;;`,
    37  		},
    38  		{
    39  			name: "bad_curly",
    40  			text: `;;;};;;`,
    41  		},
    42  		{
    43  			name: "one_line_comment",
    44  			text: `
    45  # hello
    46  `,
    47  		},
    48  		{
    49  			name: "multiline_comment",
    50  			text: `
    51  
    52    # hello
    53  # world
    54  # earth
    55  #
    56  #globe
    57   # very good
    58     # not so bad
    59  #
    60  #yes indeed
    61  #The good (I am convinced, for one)
    62  #Is but the bad one leaves undone.
    63  #Once your reputation's done
    64  #You can live a life of fun.
    65  #    -- Wilhelm Busch
    66  
    67  
    68  `,
    69  		},
    70  		{
    71  			name: "one_line_block_comment",
    72  			text: `
    73  """ dmaslkmdlksa """
    74  `,
    75  		},
    76  		{
    77  			name: "block_comment",
    78  			text: `
    79  """ dmaslkmdlksa
    80  
    81  dasmlkdas
    82  mkdlasdmkas
    83    dmsakldmklsadsa
    84  
    85  	dsmakldmaslk
    86  	damklsdmklas
    87  
    88  	echo hi
    89  x   """
    90  
    91  """ ok
    92  meow
    93  """
    94  `,
    95  		},
    96  		{
    97  			name: "key",
    98  			text: `
    99  x
   100  `,
   101  		},
   102  		{
   103  			name: "edge",
   104  			text: `
   105  x -> y
   106  `,
   107  		},
   108  		{
   109  			name: "multiple_edges",
   110  			text: `
   111  x -> y -> z
   112  `,
   113  		},
   114  		{
   115  			name: "key_with_edge",
   116  			text: `
   117  x.(z->q)
   118  `,
   119  		},
   120  		{
   121  			name: "edge_key",
   122  			text: `
   123  x.(z->q)[343].hola: false
   124  `,
   125  		},
   126  		{
   127  			name: "subst",
   128  			text: `
   129  x -> y: ${meow.ok}
   130  `,
   131  		},
   132  		{
   133  			name: "primary",
   134  			text: `
   135  x -> y: ${meow.ok} {
   136  	label: |
   137  "Hi, I'm Preston A. Mantis, president of Consumers Retail Law Outlet. As you
   138  can see by my suit and the fact that I have all these books of equal height
   139  on the shelves behind me, I am a trained legal attorney. Do you have a car
   140  or a job?  Do you ever walk around?  If so, you probably have the makings of
   141  an excellent legal case.  Although of course every case is different, I
   142  would definitely say that based on my experience and training, there's no
   143  reason why you shouldn't come out of this thing with at least a cabin
   144  cruiser.
   145  
   146  "Remember, at the Preston A. Mantis Consumers Retail Law Outlet, our motto
   147  is: 'It is very difficult to disprove certain kinds of pain.'"
   148  		-- Dave Barry, "Pain and Suffering"
   149  |
   150  }
   151  `,
   152  		},
   153  		{
   154  			name: "()_keys",
   155  			text: `
   156  my_fn() -> wowa()
   157  meow.(x -> y -> z)[3].shape: "all hail corn"
   158  `,
   159  		},
   160  		{
   161  			name: "errs",
   162  			text: `
   163  --: meow]]] ` + `
   164  meow][: ok ` + `
   165  ok: "dmsadmakls"    dsamkldkmsa   ` + `
   166   ` + `
   167  s.shape: orochimaru       ` + `
   168  x.shape: dasdasdas       ` + `
   169  
   170  wow:
   171  
   172  : ` + `
   173   ` + `
   174  []
   175  
   176    {}
   177  
   178  """
   179  wsup
   180  """
   181  
   182  '
   183  
   184  meow: ${ok}
   185  meow.(x->)[:
   186  x -> x
   187  
   188  x: [][]𐀀𐀀𐀀𐀀𐀀𐀀
   189  `,
   190  		},
   191  		{
   192  			name: "block_string",
   193  			text: `
   194  x: ||
   195  meow
   196  meo
   197  # ok
   198      code
   199  yes
   200  ||
   201  x: || meow
   202  meo
   203  # ok
   204      code
   205  yes ||
   206  
   207  # compat
   208  x: |` + "`" + `
   209  meow
   210  meow
   211  meow
   212  ` + "`" + `| {
   213  }
   214  `,
   215  		},
   216  		{
   217  			name: "trailing_whitespace",
   218  			text: `
   219  s.shape: orochimaru       ` + `
   220  `,
   221  		},
   222  		{
   223  			name: "table_and_class",
   224  			text: `
   225  sql_example: sql_example {
   226    board: {
   227      shape: sql_table
   228      id: int {constraint: primary_key}
   229      frame: int {constraint: foreign_key}
   230      diagram: int {constraint: foreign_key}
   231      board_objects: jsonb
   232      last_updated: timestamp with time zone
   233      last_thumbgen: timestamp with time zone
   234      dsl: text
   235    }
   236  
   237    # Normal.
   238    board.diagram -> diagrams.id
   239  
   240    # Self referential.
   241    diagrams.id -> diagrams.representation
   242  
   243    # SrcArrow test.
   244    diagrams.id <- views.diagram
   245    diagrams.id <-> steps.diagram
   246  
   247    diagrams: {
   248      shape: sql_table
   249      id: {type: int, constraint: primary_key}
   250      representation: {type: jsonb}
   251    }
   252  
   253    views: {
   254      shape: sql_table
   255      id: {type: int, constraint: primary_key}
   256      representation: {type: jsonb}
   257      diagram: int {constraint: foreign_key}
   258    }
   259  
   260    # steps: {
   261    # shape: sql_table
   262    # id: {type: int, constraint: primary_key}
   263    # representation: {type: jsonb}
   264    # diagram: int {constraint: foreign_key}
   265    # }
   266    # Uncomment to make autolayout panic:
   267    meow <- diagrams.id
   268  }
   269  
   270  D2 AST Parser: {
   271    shape: class
   272  
   273    +prevRune: rune
   274    prevColumn: int
   275  
   276    +eatSpace(eatNewlines bool): (rune, error)
   277    unreadRune()
   278  
   279    \#scanKey(r rune): (k Key, _ error)
   280  }
   281  `,
   282  		},
   283  		{
   284  			name: "missing_map_value",
   285  			text: `
   286  x:
   287  			`,
   288  		},
   289  		{
   290  			name: "edge_line_continuation",
   291  			text: `
   292  super long shape id here --\
   293    -> super long shape id even longer here
   294  		   `,
   295  		},
   296  		{
   297  			name: "edge_line_continuation_2",
   298  			text: `
   299  super long shape id here --\
   300  > super long shape id even longer here
   301  	 `,
   302  		},
   303  		{
   304  			name: "field_line_continuation",
   305  			text: `
   306  meow \
   307  	ok \
   308  		super: yes \
   309  		wow so cool
   310    \
   311  xd \
   312  \
   313    ok does it work: hopefully
   314  	 `,
   315  		},
   316  		{
   317  			name: "block_with_delims",
   318  			text: `
   319  a: ||
   320    |pipe|
   321  ||
   322  
   323  """
   324  b: ""
   325  """
   326  `,
   327  		},
   328  		{
   329  			name: "block_one_line",
   330  			text: `
   331  a: |   hello  |
   332  """   hello  """
   333  `,
   334  		},
   335  		{
   336  			name: "block_trailing_space",
   337  			text: `
   338  x: |
   339  	meow   ` + `
   340  |
   341  """   hello    ` + `
   342  """
   343  `,
   344  		},
   345  		{
   346  			name: "block_edge_case",
   347  			text: `
   348  x: | meow   ` + `
   349    hello
   350  yes
   351  |
   352  `,
   353  		},
   354  		{
   355  			name: "single_quote_block_string",
   356  			text: `
   357  x: |'
   358  	bs
   359  '|
   360  not part of block string
   361  `,
   362  		},
   363  		{
   364  			name: "edge_group_value",
   365  			text: `
   366  q.(x -> y).z: (rawr)
   367  `,
   368  		},
   369  		{
   370  			name: "less_than_edge#955",
   371  			text: `
   372  x <= y
   373  `,
   374  		},
   375  		{
   376  			name: "merged_shapes_#322",
   377  			text: `
   378  a-
   379  b-
   380  c-
   381  `,
   382  		},
   383  		{
   384  			name: "not-amper",
   385  			text: `
   386  &k: amper
   387  !&k: not amper
   388  `,
   389  			assert: func(t testing.TB, ast *d2ast.Map, err error) {
   390  				assert.Success(t, err)
   391  				assert.True(t, ast.Nodes[0].MapKey.Ampersand)
   392  				assert.True(t, ast.Nodes[1].MapKey.NotAmpersand)
   393  			},
   394  		},
   395  		{
   396  			name: "whitespace_range",
   397  			text: ` a -> b -> c `,
   398  			assert: func(t testing.TB, ast *d2ast.Map, err error) {
   399  				assert.Equal(t, "1:2", ast.Nodes[0].MapKey.Edges[0].Src.Range.Start.String())
   400  				assert.Equal(t, "1:3", ast.Nodes[0].MapKey.Edges[0].Src.Range.End.String())
   401  				assert.Equal(t, "1:7", ast.Nodes[0].MapKey.Edges[0].Dst.Range.Start.String())
   402  				assert.Equal(t, "1:8", ast.Nodes[0].MapKey.Edges[0].Dst.Range.End.String())
   403  				assert.Equal(t, "1:12", ast.Nodes[0].MapKey.Edges[1].Dst.Range.Start.String())
   404  				assert.Equal(t, "1:13", ast.Nodes[0].MapKey.Edges[1].Dst.Range.End.String())
   405  			},
   406  		},
   407  		{
   408  			name: "utf16-input",
   409  			text: "\xff\xfex\x00 \x00-\x00>\x00 \x00y\x00\r\x00\n\x00",
   410  			assert: func(t testing.TB, ast *d2ast.Map, err error) {
   411  				assert.Success(t, err)
   412  				assert.Equal(t, "x -> y\n", d2format.Format(ast))
   413  			},
   414  		},
   415  	}
   416  
   417  	t.Run("import", testImport)
   418  
   419  	runa(t, testCases)
   420  }
   421  
   422  func testImport(t *testing.T) {
   423  	t.Parallel()
   424  
   425  	tca := []testCase{
   426  		{
   427  			text: "x: @file",
   428  			assert: func(t testing.TB, ast *d2ast.Map, err error) {
   429  				assert.Success(t, err)
   430  				assert.Equal(t, "file", ast.Nodes[0].MapKey.Value.Import.Path[0].Unbox().ScalarString())
   431  			},
   432  		},
   433  		{
   434  			text: "x: @file.d2",
   435  			assert: func(t testing.TB, ast *d2ast.Map, err error) {
   436  				assert.Success(t, err)
   437  				assert.Equal(t, "file", ast.Nodes[0].MapKey.Value.Import.Path[0].Unbox().ScalarString())
   438  			},
   439  		},
   440  		{
   441  			text: "...@file.d2",
   442  			assert: func(t testing.TB, ast *d2ast.Map, err error) {
   443  				assert.Success(t, err)
   444  				assert.True(t, ast.Nodes[0].Import.Spread)
   445  				assert.Equal(t, "file", ast.Nodes[0].Import.Path[0].Unbox().ScalarString())
   446  			},
   447  		},
   448  		{
   449  			text: "x: [...@file.d2]",
   450  			assert: func(t testing.TB, ast *d2ast.Map, err error) {
   451  				assert.Success(t, err)
   452  				imp := ast.Nodes[0].MapKey.Value.Array.Nodes[0].Import
   453  				assert.True(t, imp.Spread)
   454  				assert.Equal(t, "file", imp.Path[0].Unbox().ScalarString())
   455  			},
   456  		},
   457  		{
   458  			text: "...@\"file\".d2",
   459  			assert: func(t testing.TB, ast *d2ast.Map, err error) {
   460  				assert.Success(t, err)
   461  				assert.True(t, ast.Nodes[0].Import.Spread)
   462  				assert.Equal(t, "file", ast.Nodes[0].Import.Path[0].Unbox().ScalarString())
   463  				assert.Equal(t, "d2", ast.Nodes[0].Import.Path[1].Unbox().ScalarString())
   464  			},
   465  		},
   466  		{
   467  			text: "...@file.\"d2\"",
   468  			assert: func(t testing.TB, ast *d2ast.Map, err error) {
   469  				assert.Success(t, err)
   470  				assert.True(t, ast.Nodes[0].Import.Spread)
   471  				assert.Equal(t, "file", ast.Nodes[0].Import.Path[0].Unbox().ScalarString())
   472  				assert.Equal(t, "d2", ast.Nodes[0].Import.Path[1].Unbox().ScalarString())
   473  			},
   474  		},
   475  		{
   476  			text: "...@../file",
   477  			assert: func(t testing.TB, ast *d2ast.Map, err error) {
   478  				assert.Success(t, err)
   479  				assert.True(t, ast.Nodes[0].Import.Spread)
   480  				assert.Equal(t, "../file", ast.Nodes[0].Import.PathWithPre())
   481  			},
   482  		},
   483  		{
   484  			text: "@file",
   485  			assert: func(t testing.TB, ast *d2ast.Map, err error) {
   486  				assert.ErrorString(t, err, "d2/testdata/d2parser/TestParse/import/#07.d2:1:1: @file is not a valid import, did you mean ...@file?")
   487  			},
   488  		},
   489  		{
   490  			text: "...@./../.././file",
   491  			assert: func(t testing.TB, ast *d2ast.Map, err error) {
   492  				assert.Success(t, err)
   493  				assert.True(t, ast.Nodes[0].Import.Spread)
   494  				assert.Equal(t, "../../file", ast.Nodes[0].Import.PathWithPre())
   495  			},
   496  		},
   497  		{
   498  			text: "meow: ...@file",
   499  			assert: func(t testing.TB, ast *d2ast.Map, err error) {
   500  				assert.ErrorString(t, err, "d2/testdata/d2parser/TestParse/import/#09.d2:1:7: unquoted strings cannot begin with ...@ as that's import spread syntax")
   501  			},
   502  		},
   503  	}
   504  
   505  	runa(t, tca)
   506  }
   507  
   508  func runa(t *testing.T, tca []testCase) {
   509  	for _, tc := range tca {
   510  		tc := tc
   511  		t.Run(tc.name, func(t *testing.T) {
   512  			t.Parallel()
   513  
   514  			d2Path := fmt.Sprintf("d2/testdata/d2parser/%v.d2", t.Name())
   515  			opts := &d2parser.ParseOptions{}
   516  			ast, err := d2parser.Parse(d2Path, strings.NewReader(tc.text), opts)
   517  
   518  			if tc.assert != nil {
   519  				tc.assert(t, ast, err)
   520  			}
   521  
   522  			got := struct {
   523  				AST *d2ast.Map `json:"ast"`
   524  				Err error      `json:"err"`
   525  			}{
   526  				AST: ast,
   527  				Err: err,
   528  			}
   529  
   530  			err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2parser", t.Name()), got)
   531  			assert.Success(t, err)
   532  		})
   533  	}
   534  }
   535  

View as plain text