agent/unit/conditional_test.go
Rama main · réplica de lectura del repositorio original.
package unit_test
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/agent/unit"
"github.com/coder/coder/v2/testutil"
)
func TestManager_ConditionalDependencyValidation(t *testing.T) {
t.Parallel()
t.Run("InvalidArguments", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
require.NoError(t, manager.Register(unitA))
err := manager.AddConditionalDependency("", unitB, unit.RequirementSuccess)
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
err = manager.AddConditionalDependency(unitA, "", unit.RequirementSuccess)
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
err = manager.AddConditionalDependency(unitB, unitA, unit.RequirementSuccess)
require.ErrorIs(t, err, unit.ErrUnitNotFound)
err = manager.AddConditionalDependency(unitA, unitB, unit.RequirementSuccess)
require.ErrorIs(t, err, unit.ErrUnitNotFound)
require.NoError(t, manager.Register(unitB))
err = manager.AddConditionalDependency(unitA, unitB, "unknown")
require.ErrorIs(t, err, unit.ErrInvalidRequirement)
err = manager.AddConditionalDependency(unitA, unitB, "")
require.ErrorIs(t, err, unit.ErrInvalidRequirement)
})
t.Run("Cycle", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
require.NoError(t, manager.Register(unitA))
require.NoError(t, manager.Register(unitB))
require.NoError(t, manager.AddConditionalDependency(unitA, unitB, unit.RequirementSuccess))
err := manager.AddConditionalDependency(unitB, unitA, unit.RequirementCompletion)
require.ErrorIs(t, err, unit.ErrCycleDetected)
require.ErrorIs(t, err, unit.ErrFailedToAddDependency)
})
t.Run("SameRequirementTwiceIsOneEdge", func(t *testing.T) {
t.Parallel()
manager := conditionalManager(t, unit.RequirementSuccess)
require.NoError(t, manager.AddConditionalDependency(unitA, unitB, unit.RequirementSuccess))
evaluation, err := manager.Evaluate(unitA)
require.NoError(t, err)
require.Equal(t, unit.DecisionWaiting, evaluation.Decision)
require.Len(t, evaluation.UnmetDependencies, 1)
})
t.Run("ConflictingRequirementIsRejected", func(t *testing.T) {
t.Parallel()
manager := conditionalManager(t, unit.RequirementSuccess)
err := manager.AddConditionalDependency(unitA, unitB, unit.RequirementCompletion)
require.ErrorIs(t, err, unit.ErrConflictingRequirement)
// The first edge stands: a failed prerequisite still skips the dependent.
require.NoError(t, manager.UpdateOutcome(unitB, unit.OutcomeFailed))
evaluation, err := manager.Evaluate(unitA)
require.NoError(t, err)
require.Equal(t, unit.DecisionSkip, evaluation.Decision)
require.Equal(t, unit.RequirementSuccess, evaluation.UnmetDependencies[0].Requirement)
})
}
func TestManager_UpdateOutcome(t *testing.T) {
t.Parallel()
t.Run("Transitions", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
require.NoError(t, manager.Register(unitA))
snapshot, err := manager.Unit(unitA)
require.NoError(t, err)
require.Equal(t, unit.OutcomePending, snapshot.Outcome())
require.NoError(t, manager.UpdateOutcome(unitA, unit.OutcomeRunning))
snapshot, err = manager.Unit(unitA)
require.NoError(t, err)
require.Equal(t, unit.OutcomeRunning, snapshot.Outcome())
require.NoError(t, manager.UpdateOutcome(unitA, unit.OutcomeSucceeded))
snapshot, err = manager.Unit(unitA)
require.NoError(t, err)
require.Equal(t, unit.OutcomeSucceeded, snapshot.Outcome())
})
t.Run("InvalidArguments", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
require.NoError(t, manager.Register(unitA))
require.NoError(t, manager.UpdateOutcome(unitA, unit.OutcomeRunning))
err := manager.UpdateOutcome(unitA, unit.OutcomeRunning)
require.ErrorIs(t, err, unit.ErrSameOutcomeAlreadySet)
err = manager.UpdateOutcome(unitA, "unknown")
require.ErrorIs(t, err, unit.ErrInvalidOutcome)
err = manager.UpdateOutcome(unitA, unit.OutcomeNotRegistered)
require.ErrorIs(t, err, unit.ErrInvalidOutcome)
err = manager.UpdateOutcome(unitB, unit.OutcomeRunning)
require.ErrorIs(t, err, unit.ErrUnitNotFound)
err = manager.UpdateOutcome("", unit.OutcomeRunning)
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
})
t.Run("TerminalOutcomeIsFinal", func(t *testing.T) {
t.Parallel()
for _, terminal := range []unit.Outcome{
unit.OutcomeSucceeded,
unit.OutcomeFailed,
unit.OutcomeTimedOut,
unit.OutcomeSkipped,
unit.OutcomeCanceled,
} {
manager := unit.NewManager()
require.NoError(t, manager.Register(unitA))
require.NoError(t, manager.UpdateOutcome(unitA, terminal))
for _, next := range []unit.Outcome{
unit.OutcomeRunning,
unit.OutcomeSucceeded,
unit.OutcomeFailed,
unit.OutcomeCanceled,
} {
if next == terminal {
continue
}
err := manager.UpdateOutcome(unitA, next)
require.ErrorIs(t, err, unit.ErrOutcomeAlreadyTerminal, "%s -> %s", terminal, next)
}
snapshot, err := manager.Unit(unitA)
require.NoError(t, err)
require.Equal(t, terminal, snapshot.Outcome())
}
})
t.Run("CanceledWhilePendingOrRunning", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
require.NoError(t, manager.Register(unitA))
require.NoError(t, manager.Register(unitB))
require.NoError(t, manager.UpdateOutcome(unitA, unit.OutcomeCanceled))
require.NoError(t, manager.UpdateOutcome(unitB, unit.OutcomeRunning))
require.NoError(t, manager.UpdateOutcome(unitB, unit.OutcomeCanceled))
for _, id := range []unit.ID{unitA, unitB} {
snapshot, err := manager.Unit(id)
require.NoError(t, err)
require.Equal(t, unit.OutcomeCanceled, snapshot.Outcome())
}
})
}
func TestManager_ConditionalDependenciesPreserveStatusDependencies(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
require.NoError(t, manager.Register(unitA))
require.NoError(t, manager.Register(unitB))
require.NoError(t, manager.AddConditionalDependency(unitA, unitB, unit.RequirementSuccess))
ready, err := manager.IsReady(unitA)
require.NoError(t, err)
require.True(t, ready)
evaluation, err := manager.Evaluate(unitA)
require.NoError(t, err)
require.Equal(t, unit.DecisionWaiting, evaluation.Decision)
require.NoError(t, manager.AddDependency(unitA, unitB, unit.StatusStarted))
ready, err = manager.IsReady(unitA)
require.NoError(t, err)
require.False(t, ready)
require.NoError(t, manager.UpdateStatus(unitB, unit.StatusStarted))
ready, err = manager.IsReady(unitA)
require.NoError(t, err)
require.True(t, ready)
evaluation, err = manager.Evaluate(unitA)
require.NoError(t, err)
require.Equal(t, unit.DecisionWaiting, evaluation.Decision)
require.NoError(t, manager.UpdateOutcome(unitB, unit.OutcomeSucceeded))
evaluation, err = manager.Evaluate(unitA)
require.NoError(t, err)
require.Equal(t, unit.DecisionRunnable, evaluation.Decision)
snapshot, err := manager.Unit(unitB)
require.NoError(t, err)
require.Equal(t, unit.StatusStarted, snapshot.Status())
}
func TestOutcome_Terminal(t *testing.T) {
t.Parallel()
for _, test := range []struct {
name string
outcome unit.Outcome
terminal bool
}{
{name: "not registered", outcome: unit.OutcomeNotRegistered},
{name: "pending", outcome: unit.OutcomePending},
{name: "running", outcome: unit.OutcomeRunning},
{name: "succeeded", outcome: unit.OutcomeSucceeded, terminal: true},
{name: "failed", outcome: unit.OutcomeFailed, terminal: true},
{name: "timed out", outcome: unit.OutcomeTimedOut, terminal: true},
{name: "skipped", outcome: unit.OutcomeSkipped, terminal: true},
{name: "canceled", outcome: unit.OutcomeCanceled, terminal: true},
} {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, test.terminal, test.outcome.Terminal())
})
}
}
func TestOutcome_String(t *testing.T) {
t.Parallel()
require.Equal(t, "not registered", unit.OutcomeNotRegistered.String())
require.Equal(t, "timed_out", unit.OutcomeTimedOut.String())
}
// TestManager_EvaluateRequirementByOutcome pins the decision for every
// requirement and prerequisite outcome pair.
func TestManager_EvaluateRequirementByOutcome(t *testing.T) {
t.Parallel()
decisions := map[unit.Requirement]map[unit.Outcome]unit.Decision{
unit.RequirementSuccess: {
unit.OutcomePending: unit.DecisionWaiting,
unit.OutcomeRunning: unit.DecisionWaiting,
unit.OutcomeSucceeded: unit.DecisionRunnable,
unit.OutcomeFailed: unit.DecisionSkip,
unit.OutcomeTimedOut: unit.DecisionSkip,
unit.OutcomeSkipped: unit.DecisionSkip,
unit.OutcomeCanceled: unit.DecisionWaiting,
},
unit.RequirementCompletion: {
unit.OutcomePending: unit.DecisionWaiting,
unit.OutcomeRunning: unit.DecisionWaiting,
unit.OutcomeSucceeded: unit.DecisionRunnable,
unit.OutcomeFailed: unit.DecisionRunnable,
unit.OutcomeTimedOut: unit.DecisionRunnable,
unit.OutcomeSkipped: unit.DecisionRunnable,
unit.OutcomeCanceled: unit.DecisionWaiting,
},
}
for requirement, byOutcome := range decisions {
for outcome, want := range byOutcome {
t.Run(fmt.Sprintf("%s/%s", requirement, outcome), func(t *testing.T) {
t.Parallel()
manager := conditionalManager(t, requirement)
if outcome != unit.OutcomePending {
require.NoError(t, manager.UpdateOutcome(unitB, outcome))
}
evaluation, err := manager.Evaluate(unitA)
require.NoError(t, err)
require.Equal(t, want, evaluation.Decision)
if want == unit.DecisionRunnable {
require.Empty(t, evaluation.UnmetDependencies)
return
}
require.Equal(t, []unit.ConditionalDependency{{
Unit: unitA,
DependsOn: unitB,
Requirement: requirement,
CurrentOutcome: outcome,
}}, evaluation.UnmetDependencies)
})
}
}
}
func TestManager_Evaluate(t *testing.T) {
t.Parallel()
t.Run("NoDependenciesIsRunnable", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
require.NoError(t, manager.Register(unitA))
evaluation, err := manager.Evaluate(unitA)
require.NoError(t, err)
require.Equal(t, unit.DecisionRunnable, evaluation.Decision)
require.Empty(t, evaluation.UnmetDependencies)
})
t.Run("SuccessWaitsThenRuns", func(t *testing.T) {
t.Parallel()
manager := conditionalManager(t, unit.RequirementSuccess)
evaluation, err := manager.Evaluate(unitA)
require.NoError(t, err)
require.Equal(t, unit.DecisionWaiting, evaluation.Decision)
require.Equal(t, unit.OutcomePending, evaluation.UnmetDependencies[0].CurrentOutcome)
require.NoError(t, manager.UpdateOutcome(unitB, unit.OutcomeRunning))
evaluation, err = manager.Evaluate(unitA)
require.NoError(t, err)
require.Equal(t, unit.DecisionWaiting, evaluation.Decision)
require.Equal(t, unit.OutcomeRunning, evaluation.UnmetDependencies[0].CurrentOutcome)
require.NoError(t, manager.UpdateOutcome(unitB, unit.OutcomeSucceeded))
evaluation, err = manager.Evaluate(unitA)
require.NoError(t, err)
require.Equal(t, unit.DecisionRunnable, evaluation.Decision)
require.Empty(t, evaluation.UnmetDependencies)
})
t.Run("UnregisteredPrerequisiteIsRejected", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
require.NoError(t, manager.Register(unitA))
err := manager.AddConditionalDependency(unitA, unitB, unit.RequirementSuccess)
require.ErrorIs(t, err, unit.ErrUnitNotFound)
// No edge was added, so the dependent is not left waiting on it.
evaluation, err := manager.Evaluate(unitA)
require.NoError(t, err)
require.Equal(t, unit.DecisionRunnable, evaluation.Decision)
require.Empty(t, evaluation.UnmetDependencies)
})
t.Run("CanceledPrerequisiteKeepsWaiting", func(t *testing.T) {
t.Parallel()
for _, requirement := range []unit.Requirement{unit.RequirementSuccess, unit.RequirementCompletion} {
manager := conditionalManager(t, requirement)
require.NoError(t, manager.UpdateOutcome(unitB, unit.OutcomeRunning))
require.NoError(t, manager.UpdateOutcome(unitB, unit.OutcomeCanceled))
evaluation, err := manager.Evaluate(unitA)
require.NoError(t, err)
require.Equal(t, unit.DecisionWaiting, evaluation.Decision, "requirement %s", requirement)
require.Equal(t, unit.OutcomeCanceled, evaluation.UnmetDependencies[0].CurrentOutcome)
}
})
t.Run("MixedRequirements", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
for _, id := range []unit.ID{unitA, unitB, unitC} {
require.NoError(t, manager.Register(id))
}
require.NoError(t, manager.AddConditionalDependency(unitA, unitB, unit.RequirementSuccess))
require.NoError(t, manager.AddConditionalDependency(unitA, unitC, unit.RequirementCompletion))
require.NoError(t, manager.UpdateOutcome(unitB, unit.OutcomeSucceeded))
require.NoError(t, manager.UpdateOutcome(unitC, unit.OutcomeRunning))
evaluation, err := manager.Evaluate(unitA)
require.NoError(t, err)
require.Equal(t, unit.DecisionWaiting, evaluation.Decision)
require.Equal(t, []unit.ID{unitC}, conditionalDependencyIDs(evaluation.UnmetDependencies))
// A failed completion prerequisite releases rather than skips.
require.NoError(t, manager.UpdateOutcome(unitC, unit.OutcomeFailed))
evaluation, err = manager.Evaluate(unitA)
require.NoError(t, err)
require.Equal(t, unit.DecisionRunnable, evaluation.Decision)
})
t.Run("SkipIsDecidedOnFirstFailure", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
for _, id := range []unit.ID{unitA, unitB, unitC} {
require.NoError(t, manager.Register(id))
}
require.NoError(t, manager.AddConditionalDependency(unitA, unitB, unit.RequirementSuccess))
require.NoError(t, manager.AddConditionalDependency(unitA, unitC, unit.RequirementSuccess))
require.NoError(t, manager.UpdateOutcome(unitB, unit.OutcomeRunning))
require.NoError(t, manager.UpdateOutcome(unitC, unit.OutcomeFailed))
evaluation, err := manager.Evaluate(unitA)
require.NoError(t, err)
require.Equal(t, unit.DecisionSkip, evaluation.Decision)
require.Equal(t, []unit.ConditionalDependency{
{Unit: unitA, DependsOn: unitB, Requirement: unit.RequirementSuccess, CurrentOutcome: unit.OutcomeRunning},
{Unit: unitA, DependsOn: unitC, Requirement: unit.RequirementSuccess, CurrentOutcome: unit.OutcomeFailed},
}, evaluation.UnmetDependencies)
// The other prerequisite finishing later does not change the decision.
require.NoError(t, manager.UpdateOutcome(unitB, unit.OutcomeSucceeded))
evaluation, err = manager.Evaluate(unitA)
require.NoError(t, err)
require.Equal(t, unit.DecisionSkip, evaluation.Decision)
require.Equal(t, []unit.ID{unitC}, conditionalDependencyIDs(evaluation.UnmetDependencies))
})
t.Run("UnmetDependenciesAreSorted", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
for _, id := range []unit.ID{unitA, unitB, unitC, unitD} {
require.NoError(t, manager.Register(id))
}
require.NoError(t, manager.AddConditionalDependency(unitA, unitD, unit.RequirementCompletion))
require.NoError(t, manager.AddConditionalDependency(unitA, unitB, unit.RequirementSuccess))
require.NoError(t, manager.AddConditionalDependency(unitA, unitC, unit.RequirementCompletion))
require.NoError(t, manager.UpdateOutcome(unitB, unit.OutcomeFailed))
evaluation, err := manager.Evaluate(unitA)
require.NoError(t, err)
require.Equal(t, unit.DecisionSkip, evaluation.Decision)
require.Equal(t, []unit.ID{unitB, unitC, unitD}, conditionalDependencyIDs(evaluation.UnmetDependencies))
})
t.Run("SkippedPropagatesByRequirement", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
for _, id := range []unit.ID{unitA, unitB, unitC, unitD} {
require.NoError(t, manager.Register(id))
}
require.NoError(t, manager.AddConditionalDependency(unitB, unitA, unit.RequirementSuccess))
require.NoError(t, manager.AddConditionalDependency(unitC, unitB, unit.RequirementCompletion))
require.NoError(t, manager.AddConditionalDependency(unitD, unitB, unit.RequirementSuccess))
require.NoError(t, manager.UpdateOutcome(unitA, unit.OutcomeFailed))
evaluation, err := manager.Evaluate(unitB)
require.NoError(t, err)
require.Equal(t, unit.DecisionSkip, evaluation.Decision)
// The manager does not record the skip itself. Until the caller does,
// units downstream of B keep waiting.
for _, id := range []unit.ID{unitC, unitD} {
evaluation, err = manager.Evaluate(id)
require.NoError(t, err)
require.Equal(t, unit.DecisionWaiting, evaluation.Decision)
}
require.NoError(t, manager.UpdateOutcome(unitB, unit.OutcomeSkipped))
evaluation, err = manager.Evaluate(unitC)
require.NoError(t, err)
require.Equal(t, unit.DecisionRunnable, evaluation.Decision)
evaluation, err = manager.Evaluate(unitD)
require.NoError(t, err)
require.Equal(t, unit.DecisionSkip, evaluation.Decision)
})
t.Run("Validation", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
_, err := manager.Evaluate("")
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
_, err = manager.Evaluate(unitA)
require.ErrorIs(t, err, unit.ErrUnitNotFound)
})
}
func TestManager_WaitForDecision(t *testing.T) {
t.Parallel()
t.Run("ReturnsAtOnceWhenDecided", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
manager := conditionalManager(t, unit.RequirementSuccess)
require.NoError(t, manager.UpdateOutcome(unitB, unit.OutcomeSucceeded))
evaluation, err := manager.WaitForDecision(ctx, unitA)
require.NoError(t, err)
require.Equal(t, unit.DecisionRunnable, evaluation.Decision)
require.NoError(t, manager.Register(unitC))
evaluation, err = manager.WaitForDecision(ctx, unitC)
require.NoError(t, err)
require.Equal(t, unit.DecisionRunnable, evaluation.Decision)
})
t.Run("WakesWhenRunnable", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
manager := conditionalManager(t, unit.RequirementSuccess)
result := make(chan unit.Evaluation, 1)
errCh := make(chan error, 1)
go func() {
evaluation, err := manager.WaitForDecision(ctx, unitA)
result <- evaluation
errCh <- err
}()
require.NoError(t, manager.UpdateOutcome(unitB, unit.OutcomeSucceeded))
require.Equal(t, unit.DecisionRunnable, testutil.RequireReceive(ctx, t, result).Decision)
require.NoError(t, testutil.RequireReceive(ctx, t, errCh))
})
t.Run("WakesWhenSkipped", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
manager := conditionalManager(t, unit.RequirementSuccess)
result := make(chan unit.Evaluation, 1)
errCh := make(chan error, 1)
go func() {
evaluation, err := manager.WaitForDecision(ctx, unitA)
result <- evaluation
errCh <- err
}()
require.NoError(t, manager.UpdateOutcome(unitB, unit.OutcomeFailed))
require.Equal(t, unit.DecisionSkip, testutil.RequireReceive(ctx, t, result).Decision)
require.NoError(t, testutil.RequireReceive(ctx, t, errCh))
})
t.Run("WakesWhenEdgeAdded", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
manager := conditionalManager(t, unit.RequirementSuccess)
require.NoError(t, manager.Register(unitC))
require.NoError(t, manager.UpdateOutcome(unitC, unit.OutcomeFailed))
result := make(chan unit.Evaluation, 1)
go func() {
evaluation, _ := manager.WaitForDecision(ctx, unitA)
result <- evaluation
}()
// A new success edge to an already failed unit makes A skipped.
require.NoError(t, manager.AddConditionalDependency(unitA, unitC, unit.RequirementSuccess))
require.Equal(t, unit.DecisionSkip, testutil.RequireReceive(ctx, t, result).Decision)
})
t.Run("ContextCanceled", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
cancel()
manager := conditionalManager(t, unit.RequirementSuccess)
require.NoError(t, manager.UpdateOutcome(unitB, unit.OutcomeRunning))
_, err := manager.WaitForDecision(ctx, unitA)
require.ErrorIs(t, err, context.Canceled)
// Nothing changed because the wait was abandoned.
snapshot, err := manager.Unit(unitB)
require.NoError(t, err)
require.Equal(t, unit.OutcomeRunning, snapshot.Outcome())
evaluation, err := manager.Evaluate(unitA)
require.NoError(t, err)
require.Equal(t, unit.DecisionWaiting, evaluation.Decision)
})
t.Run("Validation", func(t *testing.T) {
t.Parallel()
manager := unit.NewManager()
_, err := manager.WaitForDecision(context.Background(), unitA)
require.ErrorIs(t, err, unit.ErrUnitNotFound)
_, err = manager.WaitForDecision(context.Background(), "")
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
})
}
func TestManager_WaitForDecision_ManyWaiters(t *testing.T) {
t.Parallel()
const count = 50
ctx := testutil.Context(t, testutil.WaitMedium)
manager := unit.NewManager()
dependents := make([]unit.ID, 0, count)
prerequisites := make([]unit.ID, 0, count)
for i := range count {
dependent := unit.ID(fmt.Sprintf("dependent-%02d", i))
prerequisite := unit.ID(fmt.Sprintf("prerequisite-%02d", i))
require.NoError(t, manager.Register(dependent))
require.NoError(t, manager.Register(prerequisite))
require.NoError(t, manager.AddConditionalDependency(dependent, prerequisite, unit.RequirementSuccess))
dependents = append(dependents, dependent)
prerequisites = append(prerequisites, prerequisite)
}
type result struct {
id unit.ID
evaluation unit.Evaluation
err error
}
results := make(chan result, count)
for _, id := range dependents {
go func() {
evaluation, err := manager.WaitForDecision(ctx, id)
results <- result{id: id, evaluation: evaluation, err: err}
}()
}
// One update releases exactly one waiter.
require.NoError(t, manager.UpdateOutcome(prerequisites[0], unit.OutcomeSucceeded))
first := testutil.RequireReceive(ctx, t, results)
require.NoError(t, first.err)
require.Equal(t, dependents[0], first.id)
require.Equal(t, unit.DecisionRunnable, first.evaluation.Decision)
select {
case extra := <-results:
t.Fatalf("unexpected result for %s", extra.id)
default:
}
// The rest wake one by one as their own prerequisite finishes.
released := map[unit.ID]bool{dependents[0]: true}
for i := 1; i < count; i++ {
require.NoError(t, manager.UpdateOutcome(prerequisites[i], unit.OutcomeFailed))
}
for i := 1; i < count; i++ {
got := testutil.RequireReceive(ctx, t, results)
require.NoError(t, got.err)
require.Equal(t, unit.DecisionSkip, got.evaluation.Decision, "unit %s", got.id)
require.False(t, released[got.id], "unit %s released twice", got.id)
released[got.id] = true
}
require.Len(t, released, count)
}
func conditionalManager(t *testing.T, requirement unit.Requirement) *unit.Manager {
t.Helper()
manager := unit.NewManager()
require.NoError(t, manager.Register(unitA))
require.NoError(t, manager.Register(unitB))
require.NoError(t, manager.AddConditionalDependency(unitA, unitB, requirement))
return manager
}
func conditionalDependencyIDs(dependencies []unit.ConditionalDependency) []unit.ID {
ids := make([]unit.ID, 0, len(dependencies))
for _, dependency := range dependencies {
ids = append(ids, dependency.DependsOn)
}
return ids
}