diff --git a/d2chaos/d2chaos.go b/d2chaos/d2chaos.go index 910c999351..144fd9ddb6 100644 --- a/d2chaos/d2chaos.go +++ b/d2chaos/d2chaos.go @@ -121,6 +121,15 @@ func (gs *dslGenState) node() error { gs.nodeShapes[nodeID] = randShape } + if gs.roll(25, 75) == 0 { + // 25% chance of adding a style + randStyle, randVal := gs.randStyle() + gs.g, err = d2oracle.Set(gs.g, nodeID+".style."+randStyle, nil, go2.Pointer(randVal)) + if err != nil { + return err + } + } + return nil } @@ -146,13 +155,21 @@ func (gs *dslGenState) edge() error { } } + srcArrowhead := "" srcArrow := "-" if gs.randBool() { srcArrow = "<" + if gs.roll(25, 75) == 0 { + srcArrowhead = gs.randArrowhead() + } } dstArrow := "-" + dstArrowhead := "" if gs.randBool() { dstArrow = ">" + if gs.roll(25, 75) == 0 { + dstArrowhead = gs.randArrowhead() + } if srcArrow == "<" { dstArrow = "->" } @@ -163,6 +180,37 @@ func (gs *dslGenState) edge() error { if err != nil { return err } + if srcArrowhead != "" { + gs.g, err = d2oracle.Set(gs.g, key+".source-arrowhead.shape", nil, go2.Pointer(srcArrowhead)) + if err != nil { + return err + } + if gs.randBool() { + gs.g, err = d2oracle.Set(gs.g, key+".source-arrowhead.label", nil, go2.Pointer("1")) + if err != nil { + return err + } + } + } + if dstArrowhead != "" { + gs.g, err = d2oracle.Set(gs.g, key+".target-arrowhead.shape", nil, go2.Pointer(dstArrowhead)) + if err != nil { + return err + } + if gs.randBool() { + gs.g, err = d2oracle.Set(gs.g, key+".target-arrowhead.label", nil, go2.Pointer("1")) + if err != nil { + return err + } + } + } + if gs.roll(25, 75) == 0 { + // 25% chance of adding a style + gs.g, err = d2oracle.Set(gs.g, key+".style.stroke", nil, go2.Pointer("blue")) + if err != nil { + return err + } + } if gs.randBool() { maxLen := 8 if complexIDs { @@ -262,6 +310,59 @@ func (gs *dslGenState) randStr(n int, inKey bool) string { return d2format.Format(as) } +var universalStyles = []string{ + "opacity", + "stroke", + "fill", + "stroke-width", + "stroke-dash", + "border-radius", +} + +var floatStyles = map[string]struct{}{ + "opacity": {}, +} + +var intStyles = map[string]struct{}{ + "stroke-width": {}, + "stroke-dash": {}, + "border-radius": {}, +} + +var colorStyles = map[string]struct{}{ + "stroke": {}, + "fill": {}, +} + +func (gs *dslGenState) randStyle() (string, string) { + style := universalStyles[gs.rand.Intn(len(universalStyles))] + if _, ok := floatStyles[style]; ok { + return style, fmt.Sprint(gs.rand.Float64()) + } + if _, ok := intStyles[style]; ok { + return style, fmt.Sprint(gs.rand.Intn(6)) + } + if _, ok := colorStyles[style]; ok { + return style, "blue" + } + return "", "" +} + +var arrowheads = []string{ + "arrow", + "diamond", + "circle", + "triangle", + "cf-one", + "cf-many", + "cf-one-required", + "cf-many-required", +} + +func (gs *dslGenState) randArrowhead() string { + return arrowheads[gs.rand.Intn(len(arrowheads))] +} + func (gs *dslGenState) randShape() string { for { s := shapes[gs.rand.Intn(len(shapes))] diff --git a/d2chaos/d2chaos_test.go b/d2chaos/d2chaos_test.go index ad7343b5a8..d6d2f80d63 100644 --- a/d2chaos/d2chaos_test.go +++ b/d2chaos/d2chaos_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io/ioutil" + "math/rand" "os" "path/filepath" "runtime/debug" @@ -13,10 +14,13 @@ import ( "github.com/stretchr/testify/assert" + "oss.terrastruct.com/d2/d2ast" "oss.terrastruct.com/d2/d2chaos" "oss.terrastruct.com/d2/d2compiler" "oss.terrastruct.com/d2/d2exporter" + "oss.terrastruct.com/d2/d2format" "oss.terrastruct.com/d2/d2layouts/d2dagrelayout" + "oss.terrastruct.com/d2/d2oracle" "oss.terrastruct.com/d2/lib/log" "oss.terrastruct.com/d2/lib/textmeasure" ) @@ -102,11 +106,6 @@ func test(t *testing.T, textPath, text string) { t.Fatal(err) } - g, err := d2compiler.Compile("", strings.NewReader(text), nil) - if err != nil { - t.Fatal(err) - } - t.Run("layout", func(t *testing.T) { defer func() { r := recover() @@ -114,7 +113,10 @@ func test(t *testing.T, textPath, text string) { t.Errorf("recovered layout engine panic: %#v\n%s", r, debug.Stack()) } }() - + g, err := d2compiler.Compile("", strings.NewReader(text), nil) + if err != nil { + t.Fatal(err) + } ctx := log.WithTB(context.Background(), t, nil) ruler, err := textmeasure.NewRuler() @@ -133,6 +135,110 @@ func test(t *testing.T, textPath, text string) { t.Fatal(err) } }) + // In a random order, delete every object one by one + t.Run("d2oracle.Delete", func(t *testing.T) { + g, err := d2compiler.Compile("", strings.NewReader(text), nil) + if err != nil { + t.Fatal(err) + } + key := "" + var lastAST *d2ast.Map + defer func() { + r := recover() + if r != nil { + t.Errorf("recovered d2oracle panic deleting %s: %#v\n%s\n%s", key, r, debug.Stack(), d2format.Format(lastAST)) + } + }() + rand.Shuffle(len(g.Objects), func(i, j int) { + g.Objects[i], g.Objects[j] = g.Objects[j], g.Objects[i] + }) + for _, obj := range g.Objects { + key = obj.AbsID() + lastAST = g.AST + g, err = d2oracle.Delete(g, key) + if err != nil { + t.Fatal(fmt.Errorf("Failed to delete %s in\n%s\n: %v", key, d2format.Format(lastAST), err)) + } + } + }) + // In a random order, move every nested object one level up + t.Run("d2oracle.MoveOut", func(t *testing.T) { + g, err := d2compiler.Compile("", strings.NewReader(text), nil) + if err != nil { + t.Fatal(err) + } + key := "" + var lastAST *d2ast.Map + defer func() { + r := recover() + if r != nil { + t.Errorf("recovered d2oracle panic moving out %s: %#v\n%s\n%s", key, r, debug.Stack(), d2format.Format(lastAST)) + } + }() + rand.Shuffle(len(g.Objects), func(i, j int) { + g.Objects[i], g.Objects[j] = g.Objects[j], g.Objects[i] + }) + for _, obj := range g.Objects { + if obj.Parent == obj.Graph.Root { + continue + } + key = obj.AbsID() + lastAST = g.AST + g, err = d2oracle.Move(g, key, obj.Parent.AbsID()+"."+obj.ID) + if err != nil { + t.Fatal(fmt.Errorf("Failed to move %s in\n%s\n: %v", key, d2format.Format(lastAST), err)) + } + } + }) + // In a random order, move an object into another object + t.Run("d2oracle.MoveIn", func(t *testing.T) { + g, err := d2compiler.Compile("", strings.NewReader(text), nil) + if err != nil { + t.Fatal(err) + } + for _, obj := range g.Objects { + if obj.Attributes.Shape.Value == "sequence_diagram" { + return + } + } + containerKey := "" + key := "" + var lastAST *d2ast.Map + defer func() { + r := recover() + if r != nil { + t.Errorf("recovered d2oracle panic moving %s into %s: %#v\n%s\n%s", key, containerKey, r, debug.Stack(), d2format.Format(lastAST)) + } + }() + OUTER: + for i := 1; i < len(g.Objects); i++ { + g, err := d2compiler.Compile("", strings.NewReader(text), nil) + if err != nil { + t.Fatal(err) + } + rand.Shuffle(len(g.Objects), func(i, j int) { + g.Objects[i], g.Objects[j] = g.Objects[j], g.Objects[i] + }) + obj := g.Objects[1] + container := g.Objects[0] + if obj == container || obj.Parent == container { + continue + } + // Skip ancestors of the container chosen + for curr := container; curr != nil; curr = curr.Parent { + if curr == obj { + continue OUTER + } + } + key = obj.AbsID() + containerKey = container.AbsID() + lastAST = g.AST + _, err = d2oracle.Move(g, key, containerKey+"."+obj.ID) + if err != nil { + t.Fatal(fmt.Errorf("Failed to move %s into %s in\n%s\n: %v", key, containerKey, d2format.Format(lastAST), err)) + } + } + }) } func testPinned(t *testing.T, outDir string) {