d2/d2format/format_test.go

927 lines
14 KiB
Go
Raw Normal View History

package d2format_test
import (
"fmt"
"strings"
"testing"
2022-12-01 19:32:57 +00:00
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2parser"
)
func TestPrint(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
in string
exp string
}{
{
name: "basic",
in: `
x -> y
`,
exp: `x -> y
`,
},
{
name: "complex",
in: `
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}
}
meow <- diagrams.id
}
D2 AST Parser {
shape: class
+prevRune : rune
prevColumn : int
+eatSpace(eatNewlines bool): (rune, error)
unreadRune()
\#scanKey(r rune): (k Key, _ error)
}
"""dmaskkldsamkld """
"""
dmaskdmasl
mdlkasdaskml
daklsmdakms
"""
bs: |
dmasmdkals
dkmsamdklsa
|
bs2: | mdsalldkams|
y-->q: meow
x->y->z
meow: {
x: |` + "`" + `
meow
meow
` + "`" + `| {
}
}
"meow\t": ok
`,
exp: `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}
}
2023-06-20 00:33:41 +00:00
meow <- diagrams.id
2024-10-22 20:18:12 +00:00
steps: {
shape: sql_table
id: {type: int; constraint: primary_key}
representation: {type: jsonb}
diagram: int {constraint: foreign_key}
}
}
D2 AST Parser: {
shape: class
+prevRune: rune
prevColumn: int
+eatSpace(eatNewlines bool): (rune, error)
unreadRune()
\#scanKey(r rune): (k Key, _ error)
}
""" dmaskkldsamkld """
"""
dmaskdmasl
mdlkasdaskml
daklsmdakms
"""
bs: |md
dmasmdkals
dkmsamdklsa
|
bs2: |md mdsalldkams |
y -> q: meow
x -> y -> z
meow: {
x: |` + "`" + `md
meow
meow
` + "`" + `|
}
"meow\t": ok
`,
},
{
name: "block_comment",
in: `
"""
D2 AST Parser2: {
shape: class
reader: io.RuneReader
readerPos: d2ast.Position
lookahead: "[]rune"
lookaheadPos: d2ast.Position
peek() (r rune, eof bool)
-rewind(): ()
+commit()
\#peekn(n int) (s string, eof bool)
}
"""
`,
exp: `"""
D2 AST Parser2: {
shape: class
reader: io.RuneReader
readerPos: d2ast.Position
lookahead: "[]rune"
lookaheadPos: d2ast.Position
peek() (r rune, eof bool)
-rewind(): ()
+commit()
\#peekn(n int) (s string, eof bool)
}
"""
`,
},
{
name: "block_string_indent",
in: `
parent: {
example_code: |` + "`" + `go
package fs
type FS interface {
Open(name string) (File, error)
}
type File interface {
Stat() (FileInfo, error)
Read([]byte) (int, error)
Close() error
}
var (
ErrInvalid = errInvalid() // "invalid argument"
ErrPermission = errPermission() // "permission denied"
ErrExist = errExist() // "file already exists"
ErrNotExist = errNotExist() // "file does not exist"
ErrClosed = errClosed() // "file already closed"
)
` + "`" + `|}`,
exp: `parent: {
example_code: |` + "`" + `go
package fs
type FS interface {
Open(name string) (File, error)
}
type File interface {
Stat() (FileInfo, error)
Read([]byte) (int, error)
Close() error
}
var (
ErrInvalid = errInvalid() // "invalid argument"
ErrPermission = errPermission() // "permission denied"
ErrExist = errExist() // "file already exists"
ErrNotExist = errNotExist() // "file does not exist"
ErrClosed = errClosed() // "file already closed"
)
` + "`" + `|
}
`,
},
{
// This one we test that the common indent is stripped before the correct indent is
// applied.
name: "block_string_indent_2",
in: `
parent: {
example_code: |` + "`" + `go
package fs
type FS interface {
Open(name string) (File, error)
}
type File interface {
Stat() (FileInfo, error)
Read([]byte) (int, error)
Close() error
}
var (
ErrInvalid = errInvalid() // "invalid argument"
ErrPermission = errPermission() // "permission denied"
ErrExist = errExist() // "file already exists"
ErrNotExist = errNotExist() // "file does not exist"
ErrClosed = errClosed() // "file already closed"
)
` + "`" + `|}`,
exp: `parent: {
example_code: |` + "`" + `go
package fs
type FS interface {
Open(name string) (File, error)
}
type File interface {
Stat() (FileInfo, error)
Read([]byte) (int, error)
Close() error
}
var (
ErrInvalid = errInvalid() // "invalid argument"
ErrPermission = errPermission() // "permission denied"
ErrExist = errExist() // "file already exists"
ErrNotExist = errNotExist() // "file does not exist"
ErrClosed = errClosed() // "file already closed"
)
` + "`" + `|
}
`,
},
{
// This one we test that the common indent is stripped before the correct indent is
// applied even when there's too much indent.
name: "block_string_indent_3",
in: `
parent: {
example_code: |` + "`" + `go
package fs
type FS interface {
Open(name string) (File, error)
}
type File interface {
Stat() (FileInfo, error)
Read([]byte) (int, error)
Close() error
}
var (
ErrInvalid = errInvalid() // "invalid argument"
ErrPermission = errPermission() // "permission denied"
ErrExist = errExist() // "file already exists"
ErrNotExist = errNotExist() // "file does not exist"
ErrClosed = errClosed() // "file already closed"
)
` + "`" + `|}`,
exp: `parent: {
example_code: |` + "`" + `go
package fs
type FS interface {
Open(name string) (File, error)
}
type File interface {
Stat() (FileInfo, error)
Read([]byte) (int, error)
Close() error
}
var (
ErrInvalid = errInvalid() // "invalid argument"
ErrPermission = errPermission() // "permission denied"
ErrExist = errExist() // "file already exists"
ErrNotExist = errNotExist() // "file does not exist"
ErrClosed = errClosed() // "file already closed"
)
` + "`" + `|
}
`,
},
{
// This one has 3 space indent and whitespace only lines.
name: "block_string_uneven_indent",
in: `
parent: {
example_code: |` + "`" + `go
package fs
type FS interface {
Open(name string) (File, error)
}
type File interface {
Stat() (FileInfo, error)
Read([]byte) (int, error)
Close() error
}
var (
ErrInvalid = errInvalid() // "invalid argument"
ErrPermission = errPermission() // "permission denied"
ErrExist = errExist() // "file already exists"
ErrNotExist = errNotExist() // "file does not exist"
ErrClosed = errClosed() // "file already closed"
)
` + "`" + `|}`,
exp: `parent: {
example_code: |` + "`" + `go
package fs
type FS interface {
Open(name string) (File, error)
}
type File interface {
Stat() (FileInfo, error)
Read([]byte) (int, error)
Close() error
}
var (
ErrInvalid = errInvalid() // "invalid argument"
ErrPermission = errPermission() // "permission denied"
ErrExist = errExist() // "file already exists"
ErrNotExist = errNotExist() // "file does not exist"
ErrClosed = errClosed() // "file already closed"
)
` + "`" + `|
}
`,
},
{
// This one has 3 space indent and large whitespace only lines.
name: "block_string_uneven_indent_2",
in: `
parent: {
example_code: |` + "`" + `go
package fs
type FS interface {
Open(name string) (File, error)
}
` + "`" + `|}`,
exp: `parent: {
example_code: |` + "`" + `go
package fs
type FS interface {
Open(name string) (File, error)
}
` + "`" + `|
}
`,
},
{
name: "block_comment_indent",
in: `
parent: {
"""
hello
""" }`,
exp: `parent: {
"""
hello
"""
}
`,
},
{
name: "scalars",
in: `x: null
y: true
z: 343`,
exp: `x: null
y: true
z: 343
`,
},
{
name: "substitution",
in: `x: ${ok}; y: [...${yes}]`,
exp: `x: ${ok}; y: [...${yes}]
`,
},
{
name: "line_comment_block",
in: `# wsup
# hello
# The Least Successful Collector`,
exp: `# wsup
# hello
# The Least Successful Collector
`,
},
{
name: "inline_comment",
in: `hello: x # soldier
more`,
exp: `hello: x # soldier
more
`,
},
{
name: "array_one_line",
in: `a: [1;2;3;4]`,
exp: `a: [1; 2; 3; 4]
`,
},
{
name: "array",
in: `a: [
hi # Fraud is the homage that force pays to reason.
1
2
3
4
5; 6; 7
]`,
exp: `a: [
hi # Fraud is the homage that force pays to reason.
1
2
3
4
5
6
7
]
`,
},
{
name: "ampersand",
in: `&scenario: red`,
exp: `&scenario: red
`,
},
{
name: "complex_edge",
in: `pre.(src -> dst -> more)[3].post`,
exp: `pre.(src -> dst -> more)[3].post
`,
},
{
name: "edge_index_glob",
in: `(x -> y)[*]`,
exp: `(x -> y)[*]
`,
},
{
name: "bidirectional",
in: `x<>y`,
exp: `x <-> y
`,
},
{
name: "empty_map",
in: `x: {}
`,
exp: `x
2022-12-07 16:28:40 +00:00
`,
},
{
name: "leading_space_comments",
in: `# foo
# foobar
# baz
x -> y
#foo
y
`,
exp: `# foo
# foobar
# baz
x -> y
# foo
y
`,
},
{
name: "less_than_edge#955",
in: `
x <= y
`,
exp: `x <- = y
`,
},
{
name: "import/1",
in: `
x: @file.d2
`,
exp: `x: @file
`,
},
{
name: "import/2",
in: `
x: @file."d2"
`,
exp: `x: @file."d2"
`,
},
{
name: "import/3",
in: `
x: @./file
`,
exp: `x: @file
`,
},
{
name: "import/4",
in: `
x: @../file
`,
exp: `x: @../file
2023-06-10 02:31:57 +00:00
`,
},
{
name: "import/4",
in: `
x: @"x/../file"
`,
exp: `x: @file
2023-06-20 00:33:41 +00:00
`,
},
{
name: "layers_scenarios_steps_bottom_simple",
2023-06-20 18:29:46 +00:00
in: `layers: {
2023-06-20 00:33:41 +00:00
b: {
2023-06-20 18:29:46 +00:00
e
2023-06-20 00:33:41 +00:00
scenarios: {
2024-10-22 20:18:12 +00:00
p: {
x
2023-06-20 00:33:41 +00:00
}
}
}
2023-06-20 20:07:34 +00:00
steps: {
a
}
2023-06-20 00:33:41 +00:00
}
`,
2023-06-20 18:29:46 +00:00
exp: `layers: {
2023-06-20 00:33:41 +00:00
b: {
e
2024-10-22 20:18:12 +00:00
2023-06-20 00:33:41 +00:00
scenarios: {
p: {
x
}
}
}
2024-10-22 20:18:12 +00:00
2023-06-20 20:07:34 +00:00
steps: {
a
}
2023-06-20 00:33:41 +00:00
}
`,
},
{
name: "layers_scenarios_steps_bottom_complex",
in: `a
scenarios: {
scenario-1: {
steps: {
step-1: {
Test
}
step-2
}
non-step
}
}
layers: {
Test super nested: {
base-layer
layers: {
layers: {
grand-child-layer: {
grand-child-board
}
}
layer-board
}
last-layer
}
}
b
steps: {
1: {
step-1-content
}
}
scenarios: {
scenario-2: {
scenario-2-content
}
}
c
d
2023-06-20 18:29:46 +00:00
only-layers: {
layers: {
X
Y
}
2023-06-20 20:07:34 +00:00
layers: {
Z
}
2023-06-20 18:29:46 +00:00
}
2023-06-20 00:33:41 +00:00
`,
exp: `a
b
c
d
2023-06-20 18:29:46 +00:00
only-layers: {
layers: {
X
Y
}
2024-10-22 20:18:12 +00:00
2023-06-20 20:07:34 +00:00
layers: {
Z
}
2023-06-20 18:29:46 +00:00
}
2023-06-20 00:33:41 +00:00
layers: {
Test super nested: {
base-layer
last-layer
2024-10-22 20:18:12 +00:00
2023-06-20 00:33:41 +00:00
layers: {
layer-board
2024-10-22 20:18:12 +00:00
2023-06-20 00:33:41 +00:00
layers: {
grand-child-layer: {
grand-child-board
}
}
}
}
}
scenarios: {
scenario-1: {
non-step
2024-10-22 20:18:12 +00:00
2023-06-20 00:33:41 +00:00
steps: {
step-1: {
Test
}
step-2
}
}
}
scenarios: {
scenario-2: {
scenario-2-content
}
}
steps: {
1: {
step-1-content
}
}
2023-09-22 21:52:16 +00:00
`,
},
{
name: "substitution_mid_string",
in: `vars: {
test: hello
}
mybox: {
label: prefix${test}suffix
}
`,
exp: `vars: {
test: hello
}
mybox: {
label: prefix${test}suffix
}
2024-07-18 19:11:43 +00:00
`,
},
{
name: "not-filter",
in: `jacob: {
shape: circle
}
jeremy: {
shape: rectangle
}
*: {
!&shape: rectangle
label: I'm not a rectangle
}`,
exp: `jacob: {
shape: circle
}
jeremy: {
shape: rectangle
}
*: {
!&shape: rectangle
label: I'm not a rectangle
}
2024-09-15 16:43:10 +00:00
`,
},
{
name: "lowercase-reserved",
in: `jacob: {
SHAPE: circle
}
jeremy.SHAPE: rectangle
alice.STYLE.fill: red
bob.style.FILL: red
carmen.STYLE.FILL: red
coop: {
STYLE: {
FILL: blue
}
}
`,
exp: `jacob: {
shape: circle
}
jeremy.shape: rectangle
alice.style.fill: red
bob.style.fill: red
carmen.style.fill: red
coop: {
style: {
fill: blue
}
}
2024-10-27 05:21:30 +00:00
`,
},
{
name: "remove-empty-boards",
in: `k
layers
scenarios: {}
steps: asdf
`,
exp: `k
`,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ast, err := d2parser.Parse(fmt.Sprintf("%s.d2", t.Name()), strings.NewReader(tc.in), nil)
if err != nil {
t.Fatal(err)
}
2022-12-01 19:32:57 +00:00
assert.String(t, tc.exp, d2format.Format(ast))
})
}
}
func TestEdge(t *testing.T) {
t.Parallel()
mk, err := d2parser.ParseMapKey(`(x -> y)[0]`)
if err != nil {
t.Fatal(err)
}
if len(mk.Edges) != 1 {
t.Fatalf("expected one edge: %#v", mk.Edges)
}
2022-12-01 19:32:57 +00:00
assert.String(t, `x -> y`, d2format.Format(mk.Edges[0]))
assert.String(t, `[0]`, d2format.Format(mk.EdgeIndex))
}