d2/d2ast/d2ast_test.go
2022-12-01 11:48:49 -08:00

815 lines
18 KiB
Go

package d2ast_test
import (
"encoding/json"
"math/big"
math_rand "math/rand"
"reflect"
"strconv"
"strings"
"testing"
"oss.terrastruct.com/util-go/assert"
"oss.terrastruct.com/util-go/xrand"
"oss.terrastruct.com/util-go/diff"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2ast"
"oss.terrastruct.com/d2/d2format"
"oss.terrastruct.com/d2/d2parser"
)
func TestRange(t *testing.T) {
t.Parallel()
t.Run("String", func(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
r d2ast.Range
exp string
}{
{
name: "one_byte",
r: d2ast.Range{
Path: "/src/example.go",
Start: d2ast.Position{
Line: 10,
Column: 5,
Byte: 100,
},
End: d2ast.Position{
Line: 10,
Column: 6,
Byte: 100,
},
},
exp: "/src/example.go:11:6",
},
{
name: "more_than_one_byte",
r: d2ast.Range{
Path: "/src/example.go",
Start: d2ast.Position{
Line: 10,
Column: 5,
Byte: 100,
},
End: d2ast.Position{
Line: 10,
Column: 7,
Byte: 101,
},
},
exp: "/src/example.go:11:6",
},
{
name: "empty_path",
r: d2ast.Range{
Start: d2ast.Position{
Line: 10,
Column: 5,
Byte: 100,
},
End: d2ast.Position{
Line: 10,
Column: 7,
Byte: 101,
},
},
exp: "11:6",
},
{
name: "start_equal_end",
r: d2ast.Range{
Start: d2ast.Position{
Line: 10,
Column: 5,
Byte: 100,
},
End: d2ast.Position{
Line: 10,
Column: 5,
Byte: 100,
},
},
exp: "11:6",
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if tc.exp != tc.r.String() {
t.Fatalf("expected %q but got %q", tc.exp, tc.r.String())
}
})
}
})
t.Run("UnmarshalText", func(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
in string
exp d2ast.Range
errmsg string
}{
{
name: "success",
in: `"json_test.d2,1:1:0-5:1:50"`,
exp: d2ast.Range{Path: "json_test.d2", Start: d2ast.Position{Line: 1, Column: 1, Byte: 0}, End: d2ast.Position{Line: 5, Column: 1, Byte: 50}},
},
{
name: "err1",
in: `"json_test.d2-5:1:50"`,
errmsg: "missing Start field",
},
{
name: "err2",
in: `"json_test.d2"`,
errmsg: "missing End field",
},
{
name: "err3",
in: `"json_test.d2,1:1:0-5:150"`,
errmsg: "expected three fields",
},
{
name: "err4",
in: `"json_test.d2,1:10-5:1:50"`,
errmsg: "expected three fields",
},
{
name: "err5",
in: `"json_test.d2,a:1:0-5:1:50"`,
errmsg: `strconv.Atoi: parsing "a": invalid syntax`,
},
{
name: "err6",
in: `"json_test.d2,1:c:0-5:1:50"`,
errmsg: `strconv.Atoi: parsing "c": invalid syntax`,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var r d2ast.Range
err := json.Unmarshal([]byte(tc.in), &r)
if tc.errmsg != "" {
if err == nil {
t.Fatalf("expected error: %#v", err)
}
if !strings.Contains(err.Error(), tc.errmsg) {
t.Fatalf("error message does not contain %q: %q", tc.errmsg, err.Error())
}
} else {
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(tc.exp, r) {
t.Fatalf("expected %#v but got %#v", tc.exp, r)
}
}
})
}
})
t.Run("Advance", func(t *testing.T) {
t.Parallel()
t.Run("UTF-8", func(t *testing.T) {
t.Parallel()
var p d2ast.Position
p = p.Advance('a', false)
assert.StringJSON(t, `"0:1:1"`, p)
p = p.Advance('\n', false)
assert.StringJSON(t, `"1:0:2"`, p)
p = p.Advance('è', false)
assert.StringJSON(t, `"1:2:4"`, p)
p = p.Advance('𐀀', false)
assert.StringJSON(t, `"1:6:8"`, p)
p = p.Subtract('𐀀', false)
assert.StringJSON(t, `"1:2:4"`, p)
p = p.Subtract('è', false)
assert.StringJSON(t, `"1:0:2"`, p)
})
t.Run("UTF-16", func(t *testing.T) {
t.Parallel()
var p d2ast.Position
p = p.Advance('a', true)
assert.StringJSON(t, `"0:1:1"`, p)
p = p.Advance('\n', true)
assert.StringJSON(t, `"1:0:2"`, p)
p = p.Advance('è', true)
assert.StringJSON(t, `"1:1:3"`, p)
p = p.Advance('𐀀', true)
assert.StringJSON(t, `"1:3:5"`, p)
p = p.Subtract('𐀀', true)
assert.StringJSON(t, `"1:1:3"`, p)
p = p.Subtract('è', true)
assert.StringJSON(t, `"1:0:2"`, p)
})
})
}
func TestJSON(t *testing.T) {
t.Parallel()
m := &d2ast.Map{
Range: d2ast.Range{Path: "json_test.d2", Start: d2ast.Position{Line: 0, Column: 0, Byte: 0}, End: d2ast.Position{Line: 5, Column: 1, Byte: 50}},
Nodes: []d2ast.MapNodeBox{
{
Comment: &d2ast.Comment{
Value: `America was discovered by Amerigo Vespucci and was named after him, until
people got tired of living in a place called "Vespuccia" and changed its
name to "America".
-- Mike Harding, "The Armchair Anarchist's Almanac"`,
},
},
{
BlockComment: &d2ast.BlockComment{
Value: `America was discovered by Amerigo Vespucci and was named after him, until
people got tired of living in a place called "Vespuccia" and changed its
name to "America".
-- Mike Harding, "The Armchair Anarchist's Almanac"`,
},
},
{
Substitution: &d2ast.Substitution{
Spread: true,
Path: []*d2ast.StringBox{
{
BlockString: &d2ast.BlockString{
Quote: "|",
Tag: "text",
Value: `America was discovered by Amerigo Vespucci and was named after him, until
people got tired of living in a place called "Vespuccia" and changed its
name to "America".
-- Mike Harding, "The Armchair Anarchist's Almanac"`,
},
},
},
},
},
{
MapKey: &d2ast.Key{
Ampersand: true,
Key: &d2ast.KeyPath{
Path: []*d2ast.StringBox{
{
SingleQuotedString: &d2ast.SingleQuotedString{
Value: "before edges",
},
},
},
},
Edges: []*d2ast.Edge{
{
Src: &d2ast.KeyPath{
Path: []*d2ast.StringBox{
{
SingleQuotedString: &d2ast.SingleQuotedString{
Value: "src",
},
},
},
},
SrcArrow: "*",
Dst: &d2ast.KeyPath{
Path: []*d2ast.StringBox{
{
SingleQuotedString: &d2ast.SingleQuotedString{
Value: "dst",
},
},
},
},
DstArrow: ">",
},
{
Src: &d2ast.KeyPath{
Path: []*d2ast.StringBox{
{
SingleQuotedString: &d2ast.SingleQuotedString{
Value: "dst",
},
},
},
},
Dst: &d2ast.KeyPath{
Path: []*d2ast.StringBox{
{
SingleQuotedString: &d2ast.SingleQuotedString{
Value: "dst2",
},
},
},
},
},
},
EdgeIndex: &d2ast.EdgeIndex{
Glob: true,
},
EdgeKey: &d2ast.KeyPath{
Path: []*d2ast.StringBox{
{
SingleQuotedString: &d2ast.SingleQuotedString{
Value: "after edges",
},
},
},
},
Primary: d2ast.ScalarBox{
Null: &d2ast.Null{},
},
Value: d2ast.ValueBox{
Array: &d2ast.Array{
Nodes: []d2ast.ArrayNodeBox{
{
Boolean: &d2ast.Boolean{
Value: true,
},
},
{
Number: &d2ast.Number{
Raw: "0xFF",
Value: big.NewRat(15, 1),
},
},
{
UnquotedString: &d2ast.UnquotedString{
Value: []d2ast.InterpolationBox{
{
String: go2.Pointer("no quotes needed"),
},
},
},
},
{
UnquotedString: &d2ast.UnquotedString{
Value: []d2ast.InterpolationBox{
{
Substitution: &d2ast.Substitution{},
},
},
},
},
{
DoubleQuotedString: &d2ast.DoubleQuotedString{
Value: []d2ast.InterpolationBox{
{
String: go2.Pointer("no quotes needed"),
},
},
},
},
{
SingleQuotedString: &d2ast.SingleQuotedString{
Value: "rawr",
},
},
{
BlockString: &d2ast.BlockString{
Quote: "|",
Tag: "text",
Value: `America was discovered by Amerigo Vespucci and was named after him, until
people got tired of living in a place called "Vespuccia" and changed its
name to "America".
-- Mike Harding, "The Armchair Anarchist's Almanac"`,
},
},
},
},
},
},
},
},
}
assert.StringJSON(t, `{
"range": "json_test.d2,0:0:0-5:1:50",
"nodes": [
{
"comment": {
"range": ",0:0:0-0:0:0",
"value": "America was discovered by Amerigo Vespucci and was named after him, until\npeople got tired of living in a place called \"Vespuccia\" and changed its\nname to \"America\".\n\t\t-- Mike Harding, \"The Armchair Anarchist's Almanac\""
}
},
{
"block_comment": {
"range": ",0:0:0-0:0:0",
"value": "America was discovered by Amerigo Vespucci and was named after him, until\npeople got tired of living in a place called \"Vespuccia\" and changed its\nname to \"America\".\n\t\t-- Mike Harding, \"The Armchair Anarchist's Almanac\""
}
},
{
"substitution": {
"range": ",0:0:0-0:0:0",
"spread": true,
"path": [
{
"block_string": {
"range": ",0:0:0-0:0:0",
"quote": "|",
"tag": "text",
"value": "America was discovered by Amerigo Vespucci and was named after him, until\n\tpeople got tired of living in a place called \"Vespuccia\" and changed its\n\tname to \"America\".\n\t-- Mike Harding, \"The Armchair Anarchist's Almanac\""
}
}
]
}
},
{
"map_key": {
"range": ",0:0:0-0:0:0",
"ampersand": true,
"key": {
"range": ",0:0:0-0:0:0",
"path": [
{
"single_quoted_string": {
"range": ",0:0:0-0:0:0",
"raw": "",
"value": "before edges"
}
}
]
},
"edges": [
{
"range": ",0:0:0-0:0:0",
"src": {
"range": ",0:0:0-0:0:0",
"path": [
{
"single_quoted_string": {
"range": ",0:0:0-0:0:0",
"raw": "",
"value": "src"
}
}
]
},
"src_arrow": "*",
"dst": {
"range": ",0:0:0-0:0:0",
"path": [
{
"single_quoted_string": {
"range": ",0:0:0-0:0:0",
"raw": "",
"value": "dst"
}
}
]
},
"dst_arrow": ">"
},
{
"range": ",0:0:0-0:0:0",
"src": {
"range": ",0:0:0-0:0:0",
"path": [
{
"single_quoted_string": {
"range": ",0:0:0-0:0:0",
"raw": "",
"value": "dst"
}
}
]
},
"src_arrow": "",
"dst": {
"range": ",0:0:0-0:0:0",
"path": [
{
"single_quoted_string": {
"range": ",0:0:0-0:0:0",
"raw": "",
"value": "dst2"
}
}
]
},
"dst_arrow": ""
}
],
"edge_index": {
"range": ",0:0:0-0:0:0",
"int": null,
"glob": true
},
"edge_key": {
"range": ",0:0:0-0:0:0",
"path": [
{
"single_quoted_string": {
"range": ",0:0:0-0:0:0",
"raw": "",
"value": "after edges"
}
}
]
},
"primary": {
"null": {
"range": ",0:0:0-0:0:0"
}
},
"value": {
"array": {
"range": ",0:0:0-0:0:0",
"nodes": [
{
"boolean": {
"range": ",0:0:0-0:0:0",
"value": true
}
},
{
"number": {
"range": ",0:0:0-0:0:0",
"raw": "0xFF",
"value": "15"
}
},
{
"unquoted_string": {
"range": ",0:0:0-0:0:0",
"value": [
{
"string": "no quotes needed"
}
]
}
},
{
"unquoted_string": {
"range": ",0:0:0-0:0:0",
"value": [
{
"substitution": {
"range": ",0:0:0-0:0:0",
"spread": false,
"path": null
}
}
]
}
},
{
"double_quoted_string": {
"range": ",0:0:0-0:0:0",
"value": [
{
"string": "no quotes needed"
}
]
}
},
{
"single_quoted_string": {
"range": ",0:0:0-0:0:0",
"raw": "",
"value": "rawr"
}
},
{
"block_string": {
"range": ",0:0:0-0:0:0",
"quote": "|",
"tag": "text",
"value": "America was discovered by Amerigo Vespucci and was named after him, until\n\t\t\tpeople got tired of living in a place called \"Vespuccia\" and changed its\n\t\t\tname to \"America\".\n\t\t\t-- Mike Harding, \"The Armchair Anarchist's Almanac\""
}
}
]
}
}
}
}
]
}`, m)
}
func testRawStringKey(t *testing.T, key string) {
ast := d2ast.RawString(key, true)
enc := d2format.Format(ast)
k, err := d2parser.ParseKey(enc)
if err != nil {
t.Fatal(err)
}
if len(k.Path) != 1 {
t.Fatalf("unexpected key length: %#v", k.Path)
}
err = diff.Runes(key, k.Path[0].Unbox().ScalarString())
if err != nil {
t.Fatal(err)
}
}
func testRawStringValue(t *testing.T, value string) {
ast := d2ast.RawString(value, false)
enc := d2format.Format(ast)
v, err := d2parser.ParseValue(enc)
if err != nil {
t.Fatal(err)
}
ps, ok := v.(d2ast.Scalar)
if !ok {
t.Fatalf("unexpected value type: %#v", v)
}
err = diff.Runes(value, ps.ScalarString())
if err != nil {
t.Fatal(err)
}
}
func TestRawString(t *testing.T) {
t.Parallel()
t.Run("chaos", func(t *testing.T) {
t.Parallel()
t.Run("pinned", func(t *testing.T) {
t.Parallel()
pinnedTestCases := []struct {
name string
str string
}{
{
name: "1",
str: "\U000b64cd\U0008b732\U0009632c\U000983f8\U000f42d4\U000c4749\U00041723\uf584蝉\U00100cd5\U0003325d\U0003e4d2\U0007ff0e\U000e03d8\U000b0431\U00042053\U0001b3ea𠒹\U0006d9cf\U000c5b1c\U00019a3c\U000f3c3d\U0004acedଶ\U0009da18\U0001a0bb\U000b6bfd\U00015ebd\U00088c5a녈\U00078277\U000eaa58\U0009266b\U000d85ae\U000d6ce8譊𣱡\U0008ac84\U000a722f\U000d3d35\U00072581\U000c3423\U000a1753\U00082014\U0001bde6\U0010bf47炏\U000423fa\U0007df70\U00088aaf\U00074e5e\U000ee80b\U000e3d53\U0003f542\U0001ad9f\U00031408\U000cce7e\U00082172\u202f",
},
{
name: "2",
str: "'\"Tc\U000d148d\U000dd61a\U0007cf68OO\U000b87a9\U000c073a\U000e7828n\U00068a9fc\U0004fbf5\x041\\'''",
},
{
name: "3",
str: "\r\U00057d53\x01'\U00042e5a\U0007be73T\U000fb916\x01\U000e0e4afL]\U000474d1\x15\U00083bc0\fbT\ue09bs{vP\U000b3d33\x0f\U0007ad13\x10\U00098b38\x1d\U000cf9da\n ",
},
}
for _, tc := range pinnedTestCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("key", func(t *testing.T) {
t.Parallel()
testRawStringKey(t, tc.str)
})
t.Run("value", func(t *testing.T) {
t.Parallel()
testRawStringValue(t, tc.str)
})
})
}
})
for i := 0; i < 1000; i++ {
i := i
t.Run(strconv.Itoa(i), func(t *testing.T) {
t.Parallel()
s := xrand.String(math_rand.Intn(99), nil)
t.Logf("testing: %q", s)
t.Run("key", func(t *testing.T) {
t.Parallel()
testRawStringKey(t, s)
})
t.Run("value", func(t *testing.T) {
t.Parallel()
testRawStringValue(t, s)
})
})
}
})
testCases := []struct {
name string
str string
exp string
inKey bool
}{
{
name: "empty",
str: ``,
exp: `""`,
},
{
name: "null",
str: `null`,
exp: `"null"`,
},
{
name: "simple",
str: `wearisome_condition_of_humanity`,
exp: `wearisome_condition_of_humanity`,
},
{
name: "specials_double",
str: `'#;#;#'`,
exp: `"'#;#;#'"`,
},
{
name: "specials_single_quote",
str: `"cambridge"`,
exp: `'"cambridge"'`,
},
{
name: "specials_single_dollar",
str: `$bingo`,
exp: `'$bingo'`,
},
{
name: "not_key_specials",
str: `------`,
exp: `------`,
},
{
name: "key_specials_double",
str: `-----`,
exp: `"-----"`,
inKey: true,
},
{
name: "key_specials_single",
str: `"cambridge"`,
exp: `'"cambridge"'`,
inKey: true,
},
{
name: "key_specials_unquoted",
str: `square-2`,
exp: `square-2`,
inKey: true,
},
{
name: "multiline",
str: `||||yes
yes
yes
yes
||||`,
exp: `"||||yes\nyes\nyes\nyes\n||||"`,
inKey: true,
},
{
name: "leading_whitespace",
str: ` yoho_park `,
exp: `" yoho_park "`,
},
{
name: "leading_whitespace_newlines",
str: ` yoho
_park `,
exp: `" yoho\n_park "`,
},
{
name: "leading_space_double_quotes_and_newlines",
str: ` "yoho"
_park `,
exp: `" \"yoho\"\n_park "`,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ast := d2ast.RawString(tc.str, tc.inKey)
assert.String(t, tc.exp, d2format.Format(ast))
})
}
}