534 lines
9.8 KiB
Go
534 lines
9.8 KiB
Go
package d2parser_test
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"oss.terrastruct.com/util-go/assert"
|
|
"oss.terrastruct.com/util-go/diff"
|
|
|
|
"oss.terrastruct.com/d2/d2ast"
|
|
"oss.terrastruct.com/d2/d2format"
|
|
"oss.terrastruct.com/d2/d2parser"
|
|
)
|
|
|
|
type testCase struct {
|
|
name string
|
|
text string
|
|
utf16 bool
|
|
assert func(t testing.TB, ast *d2ast.Map, err error)
|
|
}
|
|
|
|
// TODO: next step for parser is writing as many tests and grouping them nicely
|
|
// TODO: add assertions
|
|
// to layout *all* expected behavior.
|
|
func TestParse(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var testCases = []testCase{
|
|
{
|
|
name: "empty",
|
|
text: ``,
|
|
},
|
|
{
|
|
name: "semicolons",
|
|
text: `;;;;;`,
|
|
},
|
|
{
|
|
name: "bad_curly",
|
|
text: `;;;};;;`,
|
|
},
|
|
{
|
|
name: "one_line_comment",
|
|
text: `
|
|
# hello
|
|
`,
|
|
},
|
|
{
|
|
name: "multiline_comment",
|
|
text: `
|
|
|
|
# hello
|
|
# world
|
|
# earth
|
|
#
|
|
#globe
|
|
# very good
|
|
# not so bad
|
|
#
|
|
#yes indeed
|
|
#The good (I am convinced, for one)
|
|
#Is but the bad one leaves undone.
|
|
#Once your reputation's done
|
|
#You can live a life of fun.
|
|
# -- Wilhelm Busch
|
|
|
|
|
|
`,
|
|
},
|
|
{
|
|
name: "one_line_block_comment",
|
|
text: `
|
|
""" dmaslkmdlksa """
|
|
`,
|
|
},
|
|
{
|
|
name: "block_comment",
|
|
text: `
|
|
""" dmaslkmdlksa
|
|
|
|
dasmlkdas
|
|
mkdlasdmkas
|
|
dmsakldmklsadsa
|
|
|
|
dsmakldmaslk
|
|
damklsdmklas
|
|
|
|
echo hi
|
|
x """
|
|
|
|
""" ok
|
|
meow
|
|
"""
|
|
`,
|
|
},
|
|
{
|
|
name: "key",
|
|
text: `
|
|
x
|
|
`,
|
|
},
|
|
{
|
|
name: "edge",
|
|
text: `
|
|
x -> y
|
|
`,
|
|
},
|
|
{
|
|
name: "multiple_edges",
|
|
text: `
|
|
x -> y -> z
|
|
`,
|
|
},
|
|
{
|
|
name: "key_with_edge",
|
|
text: `
|
|
x.(z->q)
|
|
`,
|
|
},
|
|
{
|
|
name: "edge_key",
|
|
text: `
|
|
x.(z->q)[343].hola: false
|
|
`,
|
|
},
|
|
{
|
|
name: "subst",
|
|
text: `
|
|
x -> y: ${meow.ok}
|
|
`,
|
|
},
|
|
{
|
|
name: "primary",
|
|
text: `
|
|
x -> y: ${meow.ok} {
|
|
label: |
|
|
"Hi, I'm Preston A. Mantis, president of Consumers Retail Law Outlet. As you
|
|
can see by my suit and the fact that I have all these books of equal height
|
|
on the shelves behind me, I am a trained legal attorney. Do you have a car
|
|
or a job? Do you ever walk around? If so, you probably have the makings of
|
|
an excellent legal case. Although of course every case is different, I
|
|
would definitely say that based on my experience and training, there's no
|
|
reason why you shouldn't come out of this thing with at least a cabin
|
|
cruiser.
|
|
|
|
"Remember, at the Preston A. Mantis Consumers Retail Law Outlet, our motto
|
|
is: 'It is very difficult to disprove certain kinds of pain.'"
|
|
-- Dave Barry, "Pain and Suffering"
|
|
|
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
name: "()_keys",
|
|
text: `
|
|
my_fn() -> wowa()
|
|
meow.(x -> y -> z)[3].shape: "all hail corn"
|
|
`,
|
|
},
|
|
{
|
|
name: "errs",
|
|
text: `
|
|
--: meow]]] ` + `
|
|
meow][: ok ` + `
|
|
ok: "dmsadmakls" dsamkldkmsa ` + `
|
|
` + `
|
|
s.shape: orochimaru ` + `
|
|
x.shape: dasdasdas ` + `
|
|
|
|
wow:
|
|
|
|
: ` + `
|
|
` + `
|
|
[]
|
|
|
|
{}
|
|
|
|
"""
|
|
wsup
|
|
"""
|
|
|
|
'
|
|
|
|
meow: ${ok}
|
|
meow.(x->)[:
|
|
x -> x
|
|
|
|
x: [][]𐀀𐀀𐀀𐀀𐀀𐀀
|
|
`,
|
|
},
|
|
{
|
|
name: "block_string",
|
|
text: `
|
|
x: ||
|
|
meow
|
|
meo
|
|
# ok
|
|
code
|
|
yes
|
|
||
|
|
x: || meow
|
|
meo
|
|
# ok
|
|
code
|
|
yes ||
|
|
|
|
# compat
|
|
x: |` + "`" + `
|
|
meow
|
|
meow
|
|
meow
|
|
` + "`" + `| {
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
name: "trailing_whitespace",
|
|
text: `
|
|
s.shape: orochimaru ` + `
|
|
`,
|
|
},
|
|
{
|
|
name: "table_and_class",
|
|
text: `
|
|
sql_example: sql_example {
|
|
board: {
|
|
shape: sql_table
|
|
id: int {constraint: primary_key}
|
|
frame: int {constraint: foreign_key}
|
|
diagram: int {constraint: foreign_key}
|
|
board_objects: jsonb
|
|
last_updated: timestamp with time zone
|
|
last_thumbgen: timestamp with time zone
|
|
dsl: text
|
|
}
|
|
|
|
# Normal.
|
|
board.diagram -> diagrams.id
|
|
|
|
# Self referential.
|
|
diagrams.id -> diagrams.representation
|
|
|
|
# SrcArrow test.
|
|
diagrams.id <- views.diagram
|
|
diagrams.id <-> steps.diagram
|
|
|
|
diagrams: {
|
|
shape: sql_table
|
|
id: {type: int, constraint: primary_key}
|
|
representation: {type: jsonb}
|
|
}
|
|
|
|
views: {
|
|
shape: sql_table
|
|
id: {type: int, constraint: primary_key}
|
|
representation: {type: jsonb}
|
|
diagram: int {constraint: foreign_key}
|
|
}
|
|
|
|
# steps: {
|
|
# shape: sql_table
|
|
# id: {type: int, constraint: primary_key}
|
|
# representation: {type: jsonb}
|
|
# diagram: int {constraint: foreign_key}
|
|
# }
|
|
# Uncomment to make autolayout panic:
|
|
meow <- diagrams.id
|
|
}
|
|
|
|
D2 AST Parser: {
|
|
shape: class
|
|
|
|
+prevRune: rune
|
|
prevColumn: int
|
|
|
|
+eatSpace(eatNewlines bool): (rune, error)
|
|
unreadRune()
|
|
|
|
\#scanKey(r rune): (k Key, _ error)
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
name: "missing_map_value",
|
|
text: `
|
|
x:
|
|
`,
|
|
},
|
|
{
|
|
name: "edge_line_continuation",
|
|
text: `
|
|
super long shape id here --\
|
|
-> super long shape id even longer here
|
|
`,
|
|
},
|
|
{
|
|
name: "edge_line_continuation_2",
|
|
text: `
|
|
super long shape id here --\
|
|
> super long shape id even longer here
|
|
`,
|
|
},
|
|
{
|
|
name: "field_line_continuation",
|
|
text: `
|
|
meow \
|
|
ok \
|
|
super: yes \
|
|
wow so cool
|
|
\
|
|
xd \
|
|
\
|
|
ok does it work: hopefully
|
|
`,
|
|
},
|
|
{
|
|
name: "block_with_delims",
|
|
text: `
|
|
a: ||
|
|
|pipe|
|
|
||
|
|
|
|
"""
|
|
b: ""
|
|
"""
|
|
`,
|
|
},
|
|
{
|
|
name: "block_one_line",
|
|
text: `
|
|
a: | hello |
|
|
""" hello """
|
|
`,
|
|
},
|
|
{
|
|
name: "block_trailing_space",
|
|
text: `
|
|
x: |
|
|
meow ` + `
|
|
|
|
|
""" hello ` + `
|
|
"""
|
|
`,
|
|
},
|
|
{
|
|
name: "block_edge_case",
|
|
text: `
|
|
x: | meow ` + `
|
|
hello
|
|
yes
|
|
|
|
|
`,
|
|
},
|
|
{
|
|
name: "single_quote_block_string",
|
|
text: `
|
|
x: |'
|
|
bs
|
|
'|
|
|
not part of block string
|
|
`,
|
|
},
|
|
{
|
|
name: "edge_group_value",
|
|
text: `
|
|
q.(x -> y).z: (rawr)
|
|
`,
|
|
},
|
|
{
|
|
name: "less_than_edge#955",
|
|
text: `
|
|
x <= y
|
|
`,
|
|
},
|
|
{
|
|
name: "merged_shapes_#322",
|
|
text: `
|
|
a-
|
|
b-
|
|
c-
|
|
`,
|
|
},
|
|
{
|
|
name: "whitespace_range",
|
|
text: ` a -> b -> c `,
|
|
assert: func(t testing.TB, ast *d2ast.Map, err error) {
|
|
assert.Equal(t, "1:2", ast.Nodes[0].MapKey.Edges[0].Src.Range.Start.String())
|
|
assert.Equal(t, "1:3", ast.Nodes[0].MapKey.Edges[0].Src.Range.End.String())
|
|
assert.Equal(t, "1:7", ast.Nodes[0].MapKey.Edges[0].Dst.Range.Start.String())
|
|
assert.Equal(t, "1:8", ast.Nodes[0].MapKey.Edges[0].Dst.Range.End.String())
|
|
assert.Equal(t, "1:12", ast.Nodes[0].MapKey.Edges[1].Dst.Range.Start.String())
|
|
assert.Equal(t, "1:13", ast.Nodes[0].MapKey.Edges[1].Dst.Range.End.String())
|
|
},
|
|
},
|
|
{
|
|
name: "utf16-input",
|
|
utf16: true,
|
|
text: "\xff\xfex\x00 \x00-\x00>\x00 \x00y\x00\r\x00\n\x00",
|
|
assert: func(t testing.TB, ast *d2ast.Map, err error) {
|
|
assert.Success(t, err)
|
|
assert.Equal(t, "x -> y\n", d2format.Format(ast))
|
|
},
|
|
},
|
|
{
|
|
name: "errors/utf16-input",
|
|
text: "\xff\xfex\x00 \x00-\x00>\x00 \x00y\x00\r\x00\n\x00",
|
|
assert: func(t testing.TB, ast *d2ast.Map, err error) {
|
|
assert.ErrorString(t, err, `d2/testdata/d2parser/TestParse/errors/utf16-input.d2:1:13: invalid text beginning unquoted key`)
|
|
},
|
|
},
|
|
}
|
|
|
|
t.Run("import", testImport)
|
|
|
|
runa(t, testCases)
|
|
}
|
|
|
|
func testImport(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tca := []testCase{
|
|
{
|
|
text: "x: @file",
|
|
assert: func(t testing.TB, ast *d2ast.Map, err error) {
|
|
assert.Success(t, err)
|
|
assert.Equal(t, "file", ast.Nodes[0].MapKey.Value.Import.Path[0].Unbox().ScalarString())
|
|
},
|
|
},
|
|
{
|
|
text: "x: @file.d2",
|
|
assert: func(t testing.TB, ast *d2ast.Map, err error) {
|
|
assert.Success(t, err)
|
|
assert.Equal(t, "file", ast.Nodes[0].MapKey.Value.Import.Path[0].Unbox().ScalarString())
|
|
},
|
|
},
|
|
{
|
|
text: "...@file.d2",
|
|
assert: func(t testing.TB, ast *d2ast.Map, err error) {
|
|
assert.Success(t, err)
|
|
assert.True(t, ast.Nodes[0].Import.Spread)
|
|
assert.Equal(t, "file", ast.Nodes[0].Import.Path[0].Unbox().ScalarString())
|
|
},
|
|
},
|
|
{
|
|
text: "x: [...@file.d2]",
|
|
assert: func(t testing.TB, ast *d2ast.Map, err error) {
|
|
assert.Success(t, err)
|
|
imp := ast.Nodes[0].MapKey.Value.Array.Nodes[0].Import
|
|
assert.True(t, imp.Spread)
|
|
assert.Equal(t, "file", imp.Path[0].Unbox().ScalarString())
|
|
},
|
|
},
|
|
{
|
|
text: "...@\"file\".d2",
|
|
assert: func(t testing.TB, ast *d2ast.Map, err error) {
|
|
assert.Success(t, err)
|
|
assert.True(t, ast.Nodes[0].Import.Spread)
|
|
assert.Equal(t, "file", ast.Nodes[0].Import.Path[0].Unbox().ScalarString())
|
|
assert.Equal(t, "d2", ast.Nodes[0].Import.Path[1].Unbox().ScalarString())
|
|
},
|
|
},
|
|
{
|
|
text: "...@file.\"d2\"",
|
|
assert: func(t testing.TB, ast *d2ast.Map, err error) {
|
|
assert.Success(t, err)
|
|
assert.True(t, ast.Nodes[0].Import.Spread)
|
|
assert.Equal(t, "file", ast.Nodes[0].Import.Path[0].Unbox().ScalarString())
|
|
assert.Equal(t, "d2", ast.Nodes[0].Import.Path[1].Unbox().ScalarString())
|
|
},
|
|
},
|
|
{
|
|
text: "...@../file",
|
|
assert: func(t testing.TB, ast *d2ast.Map, err error) {
|
|
assert.Success(t, err)
|
|
assert.True(t, ast.Nodes[0].Import.Spread)
|
|
assert.Equal(t, "../file", ast.Nodes[0].Import.PathWithPre())
|
|
},
|
|
},
|
|
{
|
|
text: "@file",
|
|
assert: func(t testing.TB, ast *d2ast.Map, err error) {
|
|
assert.ErrorString(t, err, "d2/testdata/d2parser/TestParse/import/#07.d2:1:1: @file is not a valid import, did you mean ...@file?")
|
|
},
|
|
},
|
|
{
|
|
text: "...@./../.././file",
|
|
assert: func(t testing.TB, ast *d2ast.Map, err error) {
|
|
assert.Success(t, err)
|
|
assert.True(t, ast.Nodes[0].Import.Spread)
|
|
assert.Equal(t, "../../file", ast.Nodes[0].Import.PathWithPre())
|
|
},
|
|
},
|
|
{
|
|
text: "meow: ...@file",
|
|
assert: func(t testing.TB, ast *d2ast.Map, err error) {
|
|
assert.ErrorString(t, err, "d2/testdata/d2parser/TestParse/import/#09.d2:1:7: unquoted strings cannot begin with ...@ as that's import spread syntax")
|
|
},
|
|
},
|
|
}
|
|
|
|
runa(t, tca)
|
|
}
|
|
|
|
func runa(t *testing.T, tca []testCase) {
|
|
for _, tc := range tca {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
d2Path := fmt.Sprintf("d2/testdata/d2parser/%v.d2", t.Name())
|
|
opts := &d2parser.ParseOptions{}
|
|
if tc.utf16 {
|
|
opts.UTF16Input = true
|
|
}
|
|
ast, err := d2parser.Parse(d2Path, strings.NewReader(tc.text), opts)
|
|
|
|
if tc.assert != nil {
|
|
tc.assert(t, ast, err)
|
|
}
|
|
|
|
got := struct {
|
|
AST *d2ast.Map `json:"ast"`
|
|
Err error `json:"err"`
|
|
}{
|
|
AST: ast,
|
|
Err: err,
|
|
}
|
|
|
|
err = diff.TestdataJSON(filepath.Join("..", "testdata", "d2parser", t.Name()), got)
|
|
assert.Success(t, err)
|
|
})
|
|
}
|
|
}
|