mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
relates to: https://github.com/coder/internal/issues/1094 This is number 2 of 5 pull requests in an effort to add agent script ordering. It adds a drpc API that is exposed via a local socket. This API serves access to a lightweight DAG based dependency manager that was inspired by systemd. In follow-up PRs: * This unit manager will be plumbed into the workspace agent struct. * CLI commands will use this agentsocket api to express dependencies between coder scripts I used an LLM to produce some of these changes, but I have conducted thorough self review and consider this contribution to be ready for an external reviewer.
744 lines
21 KiB
Go
744 lines
21 KiB
Go
package unit_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/agent/unit"
|
|
)
|
|
|
|
const (
|
|
unitA unit.ID = "serviceA"
|
|
unitB unit.ID = "serviceB"
|
|
unitC unit.ID = "serviceC"
|
|
unitD unit.ID = "serviceD"
|
|
)
|
|
|
|
func TestManager_UnitValidation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("Empty Unit Name", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
err := manager.Register("")
|
|
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
|
|
err = manager.AddDependency("", unitA, unit.StatusStarted)
|
|
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
|
|
err = manager.AddDependency(unitA, "", unit.StatusStarted)
|
|
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
|
|
dependencies, err := manager.GetAllDependencies("")
|
|
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
|
|
require.Len(t, dependencies, 0)
|
|
unmetDependencies, err := manager.GetUnmetDependencies("")
|
|
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
|
|
require.Len(t, unmetDependencies, 0)
|
|
err = manager.UpdateStatus("", unit.StatusStarted)
|
|
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
|
|
isReady, err := manager.IsReady("")
|
|
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
|
|
require.False(t, isReady)
|
|
u, err := manager.Unit("")
|
|
require.ErrorIs(t, err, unit.ErrUnitIDRequired)
|
|
assert.Equal(t, unit.Unit{}, u)
|
|
})
|
|
}
|
|
|
|
func TestManager_Register(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("RegisterNewUnit", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Given: a unit is registered
|
|
err := manager.Register(unitA)
|
|
require.NoError(t, err)
|
|
|
|
// Then: the unit should be ready (no dependencies)
|
|
u, err := manager.Unit(unitA)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, unitA, u.ID())
|
|
assert.Equal(t, unit.StatusPending, u.Status())
|
|
isReady, err := manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.True(t, isReady)
|
|
})
|
|
|
|
t.Run("RegisterDuplicateUnit", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Given: a unit is registered
|
|
err := manager.Register(unitA)
|
|
require.NoError(t, err)
|
|
|
|
// Newly registered units have StatusPending. We update the unit status to StatusStarted,
|
|
// so we can later assert that it is not overwritten back to StatusPending by the second
|
|
// register call
|
|
manager.UpdateStatus(unitA, unit.StatusStarted)
|
|
|
|
// When: the unit is registered again
|
|
err = manager.Register(unitA)
|
|
|
|
// Then: a descriptive error should be returned
|
|
require.ErrorIs(t, err, unit.ErrUnitAlreadyRegistered)
|
|
|
|
// Then: the unit status should not be overwritten
|
|
u, err := manager.Unit(unitA)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, unit.StatusStarted, u.Status())
|
|
isReady, err := manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.True(t, isReady)
|
|
})
|
|
|
|
t.Run("RegisterMultipleUnits", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Given: multiple units are registered
|
|
unitIDs := []unit.ID{unitA, unitB, unitC}
|
|
for _, unit := range unitIDs {
|
|
err := manager.Register(unit)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Then: all units should be ready initially
|
|
for _, unitID := range unitIDs {
|
|
u, err := manager.Unit(unitID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, unit.StatusPending, u.Status())
|
|
isReady, err := manager.IsReady(unitID)
|
|
require.NoError(t, err)
|
|
assert.True(t, isReady)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestManager_AddDependency(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("AddDependencyBetweenRegisteredUnits", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Given: units A and B are registered
|
|
err := manager.Register(unitA)
|
|
require.NoError(t, err)
|
|
err = manager.Register(unitB)
|
|
require.NoError(t, err)
|
|
|
|
// Given: Unit A depends on Unit B being unit.StatusStarted
|
|
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
// Then: Unit A should not be ready (depends on B)
|
|
u, err := manager.Unit(unitA)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, unit.StatusPending, u.Status())
|
|
isReady, err := manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.False(t, isReady)
|
|
|
|
// Then: Unit B should still be ready (no dependencies)
|
|
u, err = manager.Unit(unitB)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, unit.StatusPending, u.Status())
|
|
isReady, err = manager.IsReady(unitB)
|
|
require.NoError(t, err)
|
|
assert.True(t, isReady)
|
|
|
|
// When: Unit B is started
|
|
err = manager.UpdateStatus(unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
// Then: Unit A should be ready, because its dependency is now in the desired state.
|
|
isReady, err = manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.True(t, isReady)
|
|
|
|
// When: Unit B is stopped
|
|
err = manager.UpdateStatus(unitB, unit.StatusPending)
|
|
require.NoError(t, err)
|
|
|
|
// Then: Unit A should no longer be ready, because its dependency is not in the desired state.
|
|
isReady, err = manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.False(t, isReady)
|
|
})
|
|
|
|
t.Run("AddDependencyByAnUnregisteredDependentUnit", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Given Unit B is registered
|
|
err := manager.Register(unitB)
|
|
require.NoError(t, err)
|
|
|
|
// Given Unit A depends on Unit B being started
|
|
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
|
|
|
|
// Then: a descriptive error communicates that the dependency cannot be added
|
|
// because the dependent unit must be registered first.
|
|
require.ErrorIs(t, err, unit.ErrUnitNotFound)
|
|
})
|
|
|
|
t.Run("AddDependencyOnAnUnregisteredUnit", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Given unit A is registered
|
|
err := manager.Register(unitA)
|
|
require.NoError(t, err)
|
|
|
|
// Given Unit B is not yet registered
|
|
// And Unit A depends on Unit B being started
|
|
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
// Then: The dependency should be visible in Unit A's status
|
|
dependencies, err := manager.GetAllDependencies(unitA)
|
|
require.NoError(t, err)
|
|
require.Len(t, dependencies, 1)
|
|
assert.Equal(t, unitB, dependencies[0].DependsOn)
|
|
assert.Equal(t, unit.StatusStarted, dependencies[0].RequiredStatus)
|
|
assert.False(t, dependencies[0].IsSatisfied)
|
|
|
|
u, err := manager.Unit(unitB)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, unit.StatusNotRegistered, u.Status())
|
|
|
|
// Then: Unit A should not be ready, because it depends on Unit B
|
|
isReady, err := manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.False(t, isReady)
|
|
|
|
// When: Unit B is registered
|
|
err = manager.Register(unitB)
|
|
require.NoError(t, err)
|
|
|
|
// Then: Unit A should still not be ready.
|
|
// Unit B is not registered, but it has not been started as required by the dependency.
|
|
isReady, err = manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.False(t, isReady)
|
|
|
|
// When: Unit B is started
|
|
err = manager.UpdateStatus(unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
// Then: Unit A should be ready, because its dependency is now in the desired state.
|
|
isReady, err = manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.True(t, isReady)
|
|
})
|
|
|
|
t.Run("AddDependencyCreatesACyclicDependency", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Register units
|
|
err := manager.Register(unitA)
|
|
require.NoError(t, err)
|
|
err = manager.Register(unitB)
|
|
require.NoError(t, err)
|
|
err = manager.Register(unitC)
|
|
require.NoError(t, err)
|
|
err = manager.Register(unitD)
|
|
require.NoError(t, err)
|
|
|
|
// A depends on B
|
|
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
// B depends on C
|
|
err = manager.AddDependency(unitB, unitC, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
// C depends on D
|
|
err = manager.AddDependency(unitC, unitD, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
// Try to make D depend on A (creates indirect cycle)
|
|
err = manager.AddDependency(unitD, unitA, unit.StatusStarted)
|
|
require.ErrorIs(t, err, unit.ErrCycleDetected)
|
|
})
|
|
|
|
t.Run("UpdatingADependency", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Given units A and B are registered
|
|
err := manager.Register(unitA)
|
|
require.NoError(t, err)
|
|
err = manager.Register(unitB)
|
|
require.NoError(t, err)
|
|
|
|
// Given Unit A depends on Unit B being unit.StatusStarted
|
|
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
// When: The dependency is updated to unit.StatusComplete
|
|
err = manager.AddDependency(unitA, unitB, unit.StatusComplete)
|
|
require.NoError(t, err)
|
|
|
|
// Then: Unit A should only have one dependency, and it should be unit.StatusComplete
|
|
dependencies, err := manager.GetAllDependencies(unitA)
|
|
require.NoError(t, err)
|
|
require.Len(t, dependencies, 1)
|
|
assert.Equal(t, unit.StatusComplete, dependencies[0].RequiredStatus)
|
|
})
|
|
}
|
|
|
|
func TestManager_UpdateStatus(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("UpdateStatusTriggersReadinessRecalculation", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Given units A and B are registered
|
|
err := manager.Register(unitA)
|
|
require.NoError(t, err)
|
|
err = manager.Register(unitB)
|
|
require.NoError(t, err)
|
|
|
|
// Given Unit A depends on Unit B being unit.StatusStarted
|
|
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
// Then: Unit A should not be ready (depends on B)
|
|
u, err := manager.Unit(unitA)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, unit.StatusPending, u.Status())
|
|
isReady, err := manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.False(t, isReady)
|
|
|
|
// When: Unit B is started
|
|
err = manager.UpdateStatus(unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
// Then: Unit A should be ready, because its dependency is now in the desired state.
|
|
u, err = manager.Unit(unitA)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, unit.StatusPending, u.Status())
|
|
isReady, err = manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.True(t, isReady)
|
|
})
|
|
|
|
t.Run("UpdateStatusWithUnregisteredUnit", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Given Unit A is not registered
|
|
// When: Unit A is updated to unit.StatusStarted
|
|
err := manager.UpdateStatus(unitA, unit.StatusStarted)
|
|
|
|
// Then: a descriptive error communicates that the unit must be registered first.
|
|
require.ErrorIs(t, err, unit.ErrUnitNotFound)
|
|
})
|
|
|
|
t.Run("LinearChainDependencies", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Given units A, B, and C are registered
|
|
err := manager.Register(unitA)
|
|
require.NoError(t, err)
|
|
err = manager.Register(unitB)
|
|
require.NoError(t, err)
|
|
err = manager.Register(unitC)
|
|
require.NoError(t, err)
|
|
|
|
// Create chain: A depends on B being "started", B depends on C being "completed"
|
|
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
err = manager.AddDependency(unitB, unitC, unit.StatusComplete)
|
|
require.NoError(t, err)
|
|
|
|
// Then: only Unit C should be ready (no dependencies)
|
|
u, err := manager.Unit(unitC)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, unit.StatusPending, u.Status())
|
|
isReady, err := manager.IsReady(unitC)
|
|
require.NoError(t, err)
|
|
assert.True(t, isReady)
|
|
|
|
u, err = manager.Unit(unitB)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, unit.StatusPending, u.Status())
|
|
isReady, err = manager.IsReady(unitB)
|
|
require.NoError(t, err)
|
|
assert.False(t, isReady)
|
|
|
|
u, err = manager.Unit(unitA)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, unit.StatusPending, u.Status())
|
|
isReady, err = manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.False(t, isReady)
|
|
|
|
// When: Unit C is completed
|
|
err = manager.UpdateStatus(unitC, unit.StatusComplete)
|
|
require.NoError(t, err)
|
|
|
|
// Then: Unit B should be ready, because its dependency is now in the desired state.
|
|
u, err = manager.Unit(unitB)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, unit.StatusPending, u.Status())
|
|
isReady, err = manager.IsReady(unitB)
|
|
require.NoError(t, err)
|
|
assert.True(t, isReady)
|
|
|
|
u, err = manager.Unit(unitA)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, unit.StatusPending, u.Status())
|
|
isReady, err = manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.False(t, isReady)
|
|
|
|
u, err = manager.Unit(unitB)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, unit.StatusPending, u.Status())
|
|
isReady, err = manager.IsReady(unitB)
|
|
require.NoError(t, err)
|
|
assert.True(t, isReady)
|
|
|
|
// When: Unit B is started
|
|
err = manager.UpdateStatus(unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
// Then: Unit A should be ready, because its dependency is now in the desired state.
|
|
u, err = manager.Unit(unitA)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, unit.StatusPending, u.Status())
|
|
isReady, err = manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.True(t, isReady)
|
|
})
|
|
}
|
|
|
|
func TestManager_GetUnmetDependencies(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("GetUnmetDependenciesForUnitWithNoDependencies", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Given: Unit A is registered
|
|
err := manager.Register(unitA)
|
|
require.NoError(t, err)
|
|
|
|
// Given: Unit A has no dependencies
|
|
// Then: Unit A should have no unmet dependencies
|
|
unmet, err := manager.GetUnmetDependencies(unitA)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, unmet)
|
|
})
|
|
|
|
t.Run("GetUnmetDependenciesForUnitWithUnsatisfiedDependencies", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
err := manager.Register(unitA)
|
|
require.NoError(t, err)
|
|
err = manager.Register(unitB)
|
|
require.NoError(t, err)
|
|
|
|
// Given: Unit A depends on Unit B being unit.StatusStarted
|
|
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
unmet, err := manager.GetUnmetDependencies(unitA)
|
|
require.NoError(t, err)
|
|
require.Len(t, unmet, 1)
|
|
|
|
assert.Equal(t, unitA, unmet[0].Unit)
|
|
assert.Equal(t, unitB, unmet[0].DependsOn)
|
|
assert.Equal(t, unit.StatusStarted, unmet[0].RequiredStatus)
|
|
assert.False(t, unmet[0].IsSatisfied)
|
|
})
|
|
|
|
t.Run("GetUnmetDependenciesForUnitWithSatisfiedDependencies", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Given: Unit A and Unit B are registered
|
|
err := manager.Register(unitA)
|
|
require.NoError(t, err)
|
|
err = manager.Register(unitB)
|
|
require.NoError(t, err)
|
|
|
|
// Given: Unit A depends on Unit B being unit.StatusStarted
|
|
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
// When: Unit B is started
|
|
err = manager.UpdateStatus(unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
// Then: Unit A should have no unmet dependencies
|
|
unmet, err := manager.GetUnmetDependencies(unitA)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, unmet)
|
|
})
|
|
|
|
t.Run("GetUnmetDependenciesForUnregisteredUnit", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// When: Unit A is requested
|
|
unmet, err := manager.GetUnmetDependencies(unitA)
|
|
|
|
// Then: a descriptive error communicates that the unit must be registered first.
|
|
require.ErrorIs(t, err, unit.ErrUnitNotFound)
|
|
assert.Nil(t, unmet)
|
|
})
|
|
}
|
|
|
|
func TestManager_MultipleDependencies(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("UnitWithMultipleDependencies", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Register all units
|
|
units := []unit.ID{unitA, unitB, unitC, unitD}
|
|
for _, unit := range units {
|
|
err := manager.Register(unit)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// A depends on B being unit.StatusStarted AND C being "started"
|
|
err := manager.AddDependency(unitA, unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
err = manager.AddDependency(unitA, unitC, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
// A should not be ready (depends on both B and C)
|
|
isReady, err := manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.False(t, isReady)
|
|
|
|
// Update B to unit.StatusStarted - A should still not be ready (needs C too)
|
|
err = manager.UpdateStatus(unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
isReady, err = manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.False(t, isReady)
|
|
|
|
// Update C to "started" - A should now be ready
|
|
err = manager.UpdateStatus(unitC, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
isReady, err = manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.True(t, isReady)
|
|
})
|
|
|
|
t.Run("ComplexDependencyChain", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Register all units
|
|
units := []unit.ID{unitA, unitB, unitC, unitD}
|
|
for _, unit := range units {
|
|
err := manager.Register(unit)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Create complex dependency graph:
|
|
// A depends on B being unit.StatusStarted AND C being "started"
|
|
err := manager.AddDependency(unitA, unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
err = manager.AddDependency(unitA, unitC, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
// B depends on D being "completed"
|
|
err = manager.AddDependency(unitB, unitD, unit.StatusComplete)
|
|
require.NoError(t, err)
|
|
// C depends on D being "completed"
|
|
err = manager.AddDependency(unitC, unitD, unit.StatusComplete)
|
|
require.NoError(t, err)
|
|
|
|
// Initially only D is ready
|
|
isReady, err := manager.IsReady(unitD)
|
|
require.NoError(t, err)
|
|
assert.True(t, isReady)
|
|
isReady, err = manager.IsReady(unitB)
|
|
require.NoError(t, err)
|
|
assert.False(t, isReady)
|
|
isReady, err = manager.IsReady(unitC)
|
|
require.NoError(t, err)
|
|
assert.False(t, isReady)
|
|
isReady, err = manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.False(t, isReady)
|
|
|
|
// Update D to "completed" - B and C should become ready
|
|
err = manager.UpdateStatus(unitD, unit.StatusComplete)
|
|
require.NoError(t, err)
|
|
|
|
isReady, err = manager.IsReady(unitB)
|
|
require.NoError(t, err)
|
|
assert.True(t, isReady)
|
|
isReady, err = manager.IsReady(unitC)
|
|
require.NoError(t, err)
|
|
assert.True(t, isReady)
|
|
isReady, err = manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.False(t, isReady)
|
|
|
|
// Update B to unit.StatusStarted - A should still not be ready (needs C)
|
|
err = manager.UpdateStatus(unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
isReady, err = manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.False(t, isReady)
|
|
|
|
// Update C to "started" - A should now be ready
|
|
err = manager.UpdateStatus(unitC, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
isReady, err = manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.True(t, isReady)
|
|
})
|
|
|
|
t.Run("DifferentStatusTypes", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Register units
|
|
err := manager.Register(unitA)
|
|
require.NoError(t, err)
|
|
err = manager.Register(unitB)
|
|
require.NoError(t, err)
|
|
err = manager.Register(unitC)
|
|
require.NoError(t, err)
|
|
|
|
// Given: Unit A depends on Unit B being unit.StatusStarted
|
|
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
// Given: Unit A depends on Unit C being "completed"
|
|
err = manager.AddDependency(unitA, unitC, unit.StatusComplete)
|
|
require.NoError(t, err)
|
|
|
|
// When: Unit B is started
|
|
err = manager.UpdateStatus(unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
// Then: Unit A should not be ready, because only one of its dependencies is in the desired state.
|
|
// It still requires Unit C to be completed.
|
|
isReady, err := manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.False(t, isReady)
|
|
|
|
// When: Unit C is completed
|
|
err = manager.UpdateStatus(unitC, unit.StatusComplete)
|
|
require.NoError(t, err)
|
|
|
|
// Then: Unit A should be ready, because both of its dependencies are in the desired state.
|
|
isReady, err = manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.True(t, isReady)
|
|
})
|
|
}
|
|
|
|
func TestManager_IsReady(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("IsReadyWithUnregisteredUnit", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Given: a unit is not registered
|
|
u, err := manager.Unit(unitA)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, unit.StatusNotRegistered, u.Status())
|
|
// Then: the unit is not ready
|
|
isReady, err := manager.IsReady(unitA)
|
|
require.NoError(t, err)
|
|
assert.False(t, isReady)
|
|
})
|
|
}
|
|
|
|
func TestManager_ToDOT(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("ExportSimpleGraph", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Register units
|
|
err := manager.Register(unitA)
|
|
require.NoError(t, err)
|
|
err = manager.Register(unitB)
|
|
require.NoError(t, err)
|
|
|
|
// Add dependency
|
|
err = manager.AddDependency(unitA, unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
|
|
dot, err := manager.ExportDOT("test")
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, dot)
|
|
assert.Contains(t, dot, "digraph")
|
|
})
|
|
|
|
t.Run("ExportComplexGraph", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
manager := unit.NewManager()
|
|
|
|
// Register all units
|
|
units := []unit.ID{unitA, unitB, unitC, unitD}
|
|
for _, unit := range units {
|
|
err := manager.Register(unit)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Create complex dependency graph
|
|
// A depends on B and C, B depends on D, C depends on D
|
|
err := manager.AddDependency(unitA, unitB, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
err = manager.AddDependency(unitA, unitC, unit.StatusStarted)
|
|
require.NoError(t, err)
|
|
err = manager.AddDependency(unitB, unitD, unit.StatusComplete)
|
|
require.NoError(t, err)
|
|
err = manager.AddDependency(unitC, unitD, unit.StatusComplete)
|
|
require.NoError(t, err)
|
|
|
|
dot, err := manager.ExportDOT("complex")
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, dot)
|
|
assert.Contains(t, dot, "digraph")
|
|
})
|
|
}
|