package dag import ( "bytes" "encoding/json" "strings" "testing" "github.com/hashicorp/terraform/tfdiags" ) func TestGraphDot_empty(t *testing.T) { var g Graph g.Add(1) g.Add(2) g.Add(3) actual := strings.TrimSpace(string(g.Dot(nil))) expected := strings.TrimSpace(testGraphDotEmptyStr) if actual != expected { t.Fatalf("bad: %s", actual) } } func TestGraphDot_basic(t *testing.T) { var g Graph g.Add(1) g.Add(2) g.Add(3) g.Connect(BasicEdge(1, 3)) actual := strings.TrimSpace(string(g.Dot(nil))) expected := strings.TrimSpace(testGraphDotBasicStr) if actual != expected { t.Fatalf("bad: %s", actual) } } func TestGraphDot_attrs(t *testing.T) { var g Graph g.Add(&testGraphNodeDotter{ Result: &DotNode{ Name: "foo", Attrs: map[string]string{"foo": "bar"}, }, }) actual := strings.TrimSpace(string(g.Dot(nil))) expected := strings.TrimSpace(testGraphDotAttrsStr) if actual != expected { t.Fatalf("bad: %s", actual) } } type testGraphNodeDotter struct{ Result *DotNode } func (n *testGraphNodeDotter) Name() string { return n.Result.Name } func (n *testGraphNodeDotter) DotNode(string, *DotOpts) *DotNode { return n.Result } const testGraphDotBasicStr = `digraph { compound = "true" newrank = "true" subgraph "root" { "[root] 1" -> "[root] 3" } } ` const testGraphDotEmptyStr = `digraph { compound = "true" newrank = "true" subgraph "root" { } }` const testGraphDotAttrsStr = `digraph { compound = "true" newrank = "true" subgraph "root" { "[root] foo" [foo = "bar"] } }` func TestGraphJSON_empty(t *testing.T) { var g Graph g.Add(1) g.Add(2) g.Add(3) js, err := g.MarshalJSON() if err != nil { t.Fatal(err) } actual := strings.TrimSpace(string(js)) expected := strings.TrimSpace(testGraphJSONEmptyStr) if actual != expected { t.Fatalf("bad: %s", actual) } } func TestGraphJSON_basic(t *testing.T) { var g Graph g.Add(1) g.Add(2) g.Add(3) g.Connect(BasicEdge(1, 3)) js, err := g.MarshalJSON() if err != nil { t.Fatal(err) } actual := strings.TrimSpace(string(js)) expected := strings.TrimSpace(testGraphJSONBasicStr) if actual != expected { t.Fatalf("bad: %s", actual) } } // record some graph transformations, and make sure we get the same graph when // they're replayed func TestGraphJSON_basicRecord(t *testing.T) { var g Graph var buf bytes.Buffer g.SetDebugWriter(&buf) g.Add(1) g.Add(2) g.Add(3) g.Connect(BasicEdge(1, 2)) g.Connect(BasicEdge(1, 3)) g.Connect(BasicEdge(2, 3)) (&AcyclicGraph{g}).TransitiveReduction() recorded := buf.Bytes() // the Walk doesn't happen in a determined order, so just count operations // for now to make sure we wrote stuff out. if len(bytes.Split(recorded, []byte{'\n'})) != 17 { t.Fatalf("bad: %s", recorded) } original, err := g.MarshalJSON() if err != nil { t.Fatal(err) } // replay the logs, and marshal the graph back out again m, err := decodeGraph(bytes.NewReader(buf.Bytes())) if err != nil { t.Fatal(err) } replayed, err := json.MarshalIndent(m, "", " ") if err != nil { t.Fatal(err) } if !bytes.Equal(original, replayed) { t.Fatalf("\noriginal: %s\nreplayed: %s", original, replayed) } } // Verify that Vertex and Edge annotations appear in the debug output func TestGraphJSON_debugInfo(t *testing.T) { var g Graph var buf bytes.Buffer g.SetDebugWriter(&buf) g.Add(1) g.Add(2) g.Add(3) g.Connect(BasicEdge(1, 2)) g.DebugVertexInfo(2, "2") g.DebugVertexInfo(3, "3") g.DebugEdgeInfo(BasicEdge(1, 2), "1|2") dec := json.NewDecoder(bytes.NewReader(buf.Bytes())) var found2, found3, foundEdge bool for dec.More() { var d streamDecode err := dec.Decode(&d) if err != nil { t.Fatal(err) } switch d.Type { case typeVertexInfo: va := &marshalVertexInfo{} err := json.Unmarshal(d.JSON, va) if err != nil { t.Fatal(err) } switch va.Info { case "2": if va.Vertex.Name != "2" { t.Fatalf("wrong vertex annotated 2: %#v", va) } found2 = true case "3": if va.Vertex.Name != "3" { t.Fatalf("wrong vertex annotated 3: %#v", va) } found3 = true default: t.Fatalf("unexpected annotation: %#v", va) } case typeEdgeInfo: ea := &marshalEdgeInfo{} err := json.Unmarshal(d.JSON, ea) if err != nil { t.Fatal(err) } switch ea.Info { case "1|2": if ea.Edge.Name != "1|2" { t.Fatalf("incorrect edge annotation: %#v\n", ea) } foundEdge = true default: t.Fatalf("unexpected edge Info: %#v", ea) } } } if !found2 { t.Fatal("annotation 2 not found") } if !found3 { t.Fatal("annotation 3 not found") } if !foundEdge { t.Fatal("edge annotation not found") } } // Verify that debug operations appear in the debug output func TestGraphJSON_debugOperations(t *testing.T) { var g Graph var buf bytes.Buffer g.SetDebugWriter(&buf) debugOp := g.DebugOperation("AddOne", "adding node 1") g.Add(1) debugOp.End("done adding node 1") // use an immediate closure to test defers func() { defer g.DebugOperation("AddTwo", "adding nodes 2 and 3").End("done adding 2 and 3") g.Add(2) defer g.DebugOperation("NestedAddThree", "second defer").End("done adding node 3") g.Add(3) }() g.Connect(BasicEdge(1, 2)) dec := json.NewDecoder(bytes.NewReader(buf.Bytes())) var ops []string for dec.More() { var d streamDecode err := dec.Decode(&d) if err != nil { t.Fatal(err) } if d.Type != typeOperation { continue } o := &marshalOperation{} err = json.Unmarshal(d.JSON, o) if err != nil { t.Fatal(err) } switch { case o.Begin == "AddOne": ops = append(ops, "BeginAddOne") case o.End == "AddOne": ops = append(ops, "EndAddOne") case o.Begin == "AddTwo": ops = append(ops, "BeginAddTwo") case o.End == "AddTwo": ops = append(ops, "EndAddTwo") case o.Begin == "NestedAddThree": ops = append(ops, "BeginAddThree") case o.End == "NestedAddThree": ops = append(ops, "EndAddThree") } } expectedOps := []string{ "BeginAddOne", "EndAddOne", "BeginAddTwo", "BeginAddThree", "EndAddThree", "EndAddTwo", } if strings.Join(ops, ",") != strings.Join(expectedOps, ",") { t.Fatalf("incorrect order of operations: %v", ops) } } // Verify that we can replay visiting each vertex in order func TestGraphJSON_debugVisits(t *testing.T) { var g Graph var buf bytes.Buffer g.SetDebugWriter(&buf) g.Add(1) g.Add(2) g.Add(3) g.Add(4) g.Connect(BasicEdge(2, 1)) g.Connect(BasicEdge(4, 2)) g.Connect(BasicEdge(3, 4)) err := (&AcyclicGraph{g}).Walk(func(v Vertex) tfdiags.Diagnostics { g.DebugVisitInfo(v, "basic walk") return nil }) if err != nil { t.Fatal(err) } var visited []string dec := json.NewDecoder(bytes.NewReader(buf.Bytes())) for dec.More() { var d streamDecode err := dec.Decode(&d) if err != nil { t.Fatal(err) } if d.Type != typeVisitInfo { continue } o := &marshalVertexInfo{} err = json.Unmarshal(d.JSON, o) if err != nil { t.Fatal(err) } visited = append(visited, o.Vertex.ID) } expected := []string{"1", "2", "4", "3"} if strings.Join(visited, "-") != strings.Join(expected, "-") { t.Fatalf("incorrect order of operations: %v", visited) } } const testGraphJSONEmptyStr = `{ "Type": "Graph", "Name": "root", "Vertices": [ { "ID": "1", "Name": "1" }, { "ID": "2", "Name": "2" }, { "ID": "3", "Name": "3" } ] }` const testGraphJSONBasicStr = `{ "Type": "Graph", "Name": "root", "Vertices": [ { "ID": "1", "Name": "1" }, { "ID": "2", "Name": "2" }, { "ID": "3", "Name": "3" } ], "Edges": [ { "Name": "1|3", "Source": "1", "Target": "3" } ] }`