2022-11-03 13:54:49 +00:00
package d2ast_test
import (
"encoding/json"
"math/big"
math_rand "math/rand"
"reflect"
"strconv"
"strings"
"testing"
"oss.terrastruct.com/xrand"
"oss.terrastruct.com/diff"
2022-12-01 18:48:01 +00:00
"oss.terrastruct.com/util-go/go2"
2022-11-03 13:54:49 +00:00
"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 )
diff . AssertJSONEq ( t , ` "0:1:1" ` , p )
p = p . Advance ( '\n' , false )
diff . AssertJSONEq ( t , ` "1:0:2" ` , p )
p = p . Advance ( 'è' , false )
diff . AssertJSONEq ( t , ` "1:2:4" ` , p )
p = p . Advance ( '𐀀' , false )
diff . AssertJSONEq ( t , ` "1:6:8" ` , p )
p = p . Subtract ( '𐀀' , false )
diff . AssertJSONEq ( t , ` "1:2:4" ` , p )
p = p . Subtract ( 'è' , false )
diff . AssertJSONEq ( t , ` "1:0:2" ` , p )
} )
t . Run ( "UTF-16" , func ( t * testing . T ) {
t . Parallel ( )
var p d2ast . Position
p = p . Advance ( 'a' , true )
diff . AssertJSONEq ( t , ` "0:1:1" ` , p )
p = p . Advance ( '\n' , true )
diff . AssertJSONEq ( t , ` "1:0:2" ` , p )
p = p . Advance ( 'è' , true )
diff . AssertJSONEq ( t , ` "1:1:3" ` , p )
p = p . Advance ( '𐀀' , true )
diff . AssertJSONEq ( t , ` "1:3:5" ` , p )
p = p . Subtract ( '𐀀' , true )
diff . AssertJSONEq ( t , ` "1:1:3" ` , p )
p = p . Subtract ( 'è' , true )
diff . AssertJSONEq ( 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" ` ,
} ,
} ,
} ,
} ,
} ,
} ,
} ,
} ,
}
diff . AssertJSONEq ( 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 )
diff . AssertStringEq ( t , tc . exp , d2format . Format ( ast ) )
} )
}
}