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 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: "not-amper", text: ` &k: amper !&k: not amper `, assert: func(t testing.TB, ast *d2ast.Map, err error) { assert.Success(t, err) assert.True(t, ast.Nodes[0].MapKey.Ampersand) assert.True(t, ast.Nodes[1].MapKey.NotAmpersand) }, }, { 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", 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)) }, }, } 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{} 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) }) } }