| 1 | package plancontract |
| 2 | |
| 3 | import ( |
| 4 | "slices" |
| 5 | "testing" |
| 6 | ) |
| 7 | |
| 8 | func orderedTitles(p Plan) []string { |
| 9 | steps := p.Ordered() |
| 10 | out := make([]string, 0, len(steps)) |
| 11 | for _, s := range steps { |
| 12 | out = append(out, s.Title) |
| 13 | } |
| 14 | return out |
| 15 | } |
| 16 | |
| 17 | func TestOrderedGroupsSubStepsUnderTheirPhase(t *testing.T) { |
| 18 | p := Plan{Objective: "o", Steps: []Step{ |
| 19 | {ID: "p1", Title: "phase one"}, |
| 20 | {ID: "p2", Title: "phase two"}, |
| 21 | {ID: "b", ParentID: "p2", Title: "two-a"}, |
| 22 | {ID: "a", ParentID: "p1", Title: "one-a"}, |
| 23 | }}.Normalize() |
| 24 | |
| 25 | got := orderedTitles(p) |
| 26 | want := []string{"phase one", "one-a", "phase two", "two-a"} |
| 27 | if !slices.Equal(got, want) { |
| 28 | t.Fatalf("ordered = %v, want %v", got, want) |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | func TestOrderedRespectsSiblingDependencies(t *testing.T) { |
| 33 | p := Plan{Objective: "o", Steps: []Step{ |
| 34 | {ID: "p", Title: "phase"}, |
| 35 | {ID: "late", ParentID: "p", Title: "late", DependsOn: []string{"early"}}, |
| 36 | {ID: "early", ParentID: "p", Title: "early"}, |
| 37 | }}.Normalize() |
| 38 | |
| 39 | got := orderedTitles(p) |
| 40 | want := []string{"phase", "early", "late"} |
| 41 | if !slices.Equal(got, want) { |
| 42 | t.Fatalf("ordered = %v, want %v", got, want) |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | func TestOrderedIgnoresDependenciesAcrossPhases(t *testing.T) { |
| 47 | // A sub-step of phase two depending on a sub-step of phase one cannot |
| 48 | // reorder anything: the phases already order them. |
| 49 | p := Plan{Objective: "o", Steps: []Step{ |
| 50 | {ID: "p1", Title: "phase one"}, |
| 51 | {ID: "a", ParentID: "p1", Title: "one-a", DependsOn: []string{"b"}}, |
| 52 | {ID: "p2", Title: "phase two"}, |
| 53 | {ID: "b", ParentID: "p2", Title: "two-a"}, |
| 54 | }}.Normalize() |
| 55 | |
| 56 | got := orderedTitles(p) |
| 57 | want := []string{"phase one", "one-a", "phase two", "two-a"} |
| 58 | if !slices.Equal(got, want) { |
| 59 | t.Fatalf("ordered = %v, want %v", got, want) |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | func TestOrderedKeepsEveryStepThroughADependencyCycle(t *testing.T) { |
| 64 | p := Plan{Objective: "o", Steps: []Step{ |
| 65 | {ID: "a", Title: "a", DependsOn: []string{"b"}}, |
| 66 | {ID: "b", Title: "b", DependsOn: []string{"a"}}, |
| 67 | }}.Normalize() |
| 68 | |
| 69 | got := orderedTitles(p) |
| 70 | want := []string{"a", "b"} |
| 71 | if !slices.Equal(got, want) { |
| 72 | t.Fatalf("ordered = %v, want declared order %v", got, want) |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | func TestOrderedKeepsEveryStepOnUnnormalizedInput(t *testing.T) { |
| 77 | // Ordered must be total: a caller that skips Normalize gets the same steps, |
| 78 | // grouped the same way, never a silently shorter list. |
| 79 | raw := Plan{Objective: "o", Steps: []Step{ |
| 80 | {ID: "p", Title: "phase"}, |
| 81 | {ID: "child", ParentID: "p", Title: "child"}, |
| 82 | {ID: "grandchild", ParentID: "child", Title: "grandchild"}, |
| 83 | {ID: "orphan", ParentID: "ghost", Title: "orphan"}, |
| 84 | }} |
| 85 | |
| 86 | got := orderedTitles(raw) |
| 87 | if len(got) != len(raw.Steps) { |
| 88 | t.Fatalf("ordered dropped steps: %v", got) |
| 89 | } |
| 90 | if !slices.Equal(got, orderedTitles(raw.Normalize())) { |
| 91 | t.Fatalf("ordered disagrees with the normalized plan: %v vs %v", got, orderedTitles(raw.Normalize())) |
| 92 | } |
| 93 | } |
| 94 |