package rollouts import ( _ "embed" "fmt" "testing" "github.com/stretchr/testify/assert" ) //go:embed testdata/simple-graph.json var simpleGraph []byte //go:embed testdata/simple-plan.json var simplePlan []byte //go:embed testdata/bad-edge-plan.json var badEdgePlan []byte //go:embed testdata/bad-edge-graph.json var badEdgeGraph []byte var fakeGraphID = "abc1234-def" var fakePlanID = "abc1234-def" func testSimpleEdges(edges []RolloutGraphEdge) error { if len(edges) != 7 { return fmt.Errorf("expected 7 edges, got %d", len(edges)) } return nil } func testSimpleNodes(nodes map[NodeKey]RolloutGraphNode) error { if len(nodes) != 7 { return fmt.Errorf("expected 7 nodes, got %d", len(nodes)) } // tg1 tg1 := nodes["tg1"] if tg1 == nil { return fmt.Errorf("expected node with key %s, got nil", "tg1") } if len(tg1.GetDependsOn()) != 0 { return fmt.Errorf("expected first node, tg1, to have no dependencies, got %d", len(tg1.GetDependsOn())) } if len(tg1.GetNext()) != 1 { return fmt.Errorf("expected first node, tg1, to have 1 next node, got %d", len(tg1.GetNext())) } // tg2 tg2 := nodes["tg2"] if tg2 == nil { return fmt.Errorf("expected node with key %s, got nil", "tg2") } if len(tg2.GetDependsOn()) != 1 { return fmt.Errorf("expected node tg2, to have 1 dependencies, got %d", len(tg2.GetDependsOn())) } if len(tg2.GetNext()) != 1 { return fmt.Errorf("expected node tg2, to have 1 next node, got %d", len(tg2.GetNext())) } // tg3 tg3 := nodes["tg3"] if tg3 == nil { return fmt.Errorf("expected node with key %s, got nil", "tg3") } if len(tg3.GetDependsOn()) != 1 { return fmt.Errorf("expected node tg3, to have 1 dependencies, got %d", len(tg3.GetDependsOn())) } if len(tg3.GetNext()) != 1 { return fmt.Errorf("expected node tg3, to have 1 next node, got %d", len(tg3.GetNext())) } // tg4 tg4 := nodes["tg4"] if tg4 == nil { return fmt.Errorf("expected node with key %s, got nil", "tg4") } if len(tg4.GetDependsOn()) != 1 { return fmt.Errorf("expected node tg4, to have 1 dependencies, got %d", len(tg4.GetDependsOn())) } if len(tg4.GetNext()) != 0 { return fmt.Errorf("expected terminal node, tg4, to have 0 next nodes, got %d", len(tg4.GetNext())) } // tg5 tg5 := nodes["tg5"] if tg5 == nil { return fmt.Errorf("expected node with key %s, got nil", "tg5") } if len(tg5.GetDependsOn()) != 1 { return fmt.Errorf("expected node tg5, to have 1 dependencies, got %d", len(tg5.GetDependsOn())) } if len(tg5.GetNext()) != 0 { return fmt.Errorf("expected terminal node, tg5, to have 0 next nodes, got %d", len(tg5.GetNext())) } // g1 g1 := nodes["g1"] if g1 == nil { return fmt.Errorf("expected node with key %s, got nil", "g1") } if len(g1.GetDependsOn()) != 1 { return fmt.Errorf("expected node g1 to have 1 dependencies, got %d", len(g1.GetDependsOn())) } if len(g1.GetNext()) != 2 { return fmt.Errorf("expected node g1 to have 2 next nodes, got %d", len(g1.GetNext())) } // g2 g2 := nodes["g2"] if g2 == nil { return fmt.Errorf("expected node with key %s, got nil", "g2") } if len(g2.GetDependsOn()) != 2 { return fmt.Errorf("expected node g2 to have 2 dependencies, got %d", len(g2.GetDependsOn())) } if len(g2.GetNext()) != 2 { return fmt.Errorf("expected node g2 to have 2 next nodes, got %d", len(g2.GetNext())) } return nil } func TestCreateRolloutPlanFromJSON(t *testing.T) { plan, err := NewRolloutPlanFromJSON(simplePlan) fmt.Printf("%+v\n", plan) if err != nil { t.Fatalf("failed to create test plan from JSON: %v", err) } if len(plan.Initial) != 1 { t.Fatalf("expecting Initial nodes to be len 1 but got %d", len(plan.Initial)) } assert.Equal(t, plan.ID, fakePlanID) nodesErr := testSimpleNodes(plan.Nodes) if nodesErr != nil { t.Fatal(nodesErr) } edgesErr := testSimpleEdges(plan.Edges) if edgesErr != nil { t.Fatal(edgesErr) } } func TestRolloutPlanJSONBadEdge(t *testing.T) { plan, err := NewRolloutPlanFromJSON(badEdgePlan) fmt.Printf("%+v\n", plan) if err != nil { t.Logf("successful error: %s", err) fmt.Println(err) } else { t.Fatal("didn't log error") } } func TestRolloutGraphJSONBadEdge(t *testing.T) { graph, err := NewRolloutGraphFromJSON(badEdgeGraph) fmt.Printf("%+v\n", graph) if err != nil { t.Logf("successful error: %s", err) fmt.Println(err) } else { t.Fatal("didn't log error") } } func TestCreateRolloutGraphFromJSON(t *testing.T) { rollout, err := NewRolloutGraphFromJSON(simpleGraph) if err != nil { t.Fatalf("failed create test rollout graph from JSON: %v", err) } assert.Equal(t, rollout.ID, fakeGraphID) nodesErr := testSimpleNodes(rollout.Nodes) if nodesErr != nil { t.Fatal(nodesErr) } edgesErr := testSimpleEdges(rollout.Edges) if edgesErr != nil { t.Fatal(edgesErr) } } //func TestRolloutWaitsForAllInputs(t *testing.T) { // // TODO(dk185217): Do something with this test other than skip. just skipping to keep logspam down // // t.SkipNow() // // for the canonical example graph: // // S->TG_1, TG_1->G_1, G_1->TG_2, G_1->TG_3, TG_2->G_2, TG_3->G_2, G_2->TG_4, G_2->TG_5 // // G_2 is the first node with multiple inputs. Test that G_2 does not execute until TG_2 and TG_3 finish // // if a node has multiple inputs, it must wait until all those inputs are done // // before being processed. in the case of a finish node for eg, we might get: (from test fmt logs) // // updating Current to: [Finish,Finish] // // Processing current step of graph: { Complete: false, Current: [Finish,Finish] } // // executing node: Finish // // executing node: Finish // // updating Current to: [] // // in this case, it is incorrect to execute the finish node because all of its inputs are not independently // // checked before adding it to the list of next nodes // tt := &testTimer{ // fakeTime: time.Now(), // } // // create a rollout plan // plan, _ := NewRolloutPlanFromJSON(simplePlan) // testState := map[string]string{} // labels := map[string][]string{ // "dev": {"dev0", "dev1", "dev2"}, // "staging-east": {"stage0-east", "stage1-east"}, // "staging-west": {"stage2-west", "stage3-west"}, // "prod-us": {"prod0-east", "prod1-east", "prod2-west", "prod3-west"}, // "prod-global": {"prod4", "prod5"}, // } // rolloutDriver := NewInMemDriver(fakeBannerID, testState, labels, false) // // create a rollout from the plan (TODO) // rollout := NewRolloutGraph(plan, rolloutDriver, WithTimeSource(tt)) // // start the rollout // i := 0 // ctx := context.Background() // for { // done, err := rollout.ProcessStep(ctx) // if err != nil { // t.Fatalf("failed to process rollout step: %v", err) // } // if done { // break // } // // nodes should not be in-progress if their dependencies have not finished // for _, currKey := range rollout.Current { // curr := rollout.Nodes[currKey] // allDepsComplete := true // var incompleteDep RolloutGraphNode // for _, depOfCurr := range curr.GetDependsOn() { // if depOfCurr.GetState() != Complete { // allDepsComplete = false // incompleteDep = depOfCurr // } // } // if !allDepsComplete && curr.GetState() == InProgress { // t.Fatalf("node %s was started without finishing its deps first. incomplete dep: %s", // curr.GetKey(), incompleteDep.GetKey()) // } // } // // fast forward into the future. 2 seconds per step // fmt.Println("fast forwarding time...") // tt.fakeTime = tt.fakeTime.Add(2001 * time.Millisecond) // // simulate an approval. might take... 11 iterations? // if i > 11 && !rolloutDriver.approved { // fmt.Println("approving...") // rolloutDriver.approved = true // } // i++ // if i > 100 { // t.Fatal("took too long") // } // } //} //func TestRolloutGraphConstraints(t *testing.T) { // plan := RolloutPlan{Name: "test-rollout-graph-constraints"} // rolloutDriver := NewInMemDriver("", nil, nil, false) // rollout := NewRolloutGraph(plan, rolloutDriver) // // check if edges and node dependencies are wired up correctly // numDeps := 0 // for _, n := range rollout.Nodes { // numDeps += len(n.GetDependsOn()) // } // if numDeps != len(rollout.Edges) { // t.Fatalf("number of edges did not match total number of dependent relations. edges: %v, deps: %v", // len(rollout.Edges), numDeps) // } //} //type testTimer struct { // fakeTime time.Time //} //func (t *testTimer) Now() time.Time { // return t.fakeTime //}