mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
ce627bf23f
closes: https://github.com/coder/coder/issues/10352 closes: https://github.com/coder/internal/issues/1094 closes: https://github.com/coder/internal/issues/1095 In this pull request, we enable a new set of experimental cli commands grouped under `coder exp sync`. These commands allow any process acting within a coder workspace to inform the coder agent of its requirements and execution progress. The coder agent will then relay this information to other processes that have subscribed. These commands are: ``` # Check if this feature is enabled in your environment coder exp sync ping # express that your unit depends on another coder exp sync want <unit> <dependency_unit> # express that your unit intends to start a portion of the script that requires # other units to have completed first. This command blocks until all dependencies have been met coder exp sync start <unit> # express that your unit has completes its work, allowing dependent units to begin their execution coder exp sync complete <unit> ``` Example: In order to automatically run claude code in a new workspace, it must first have a git repository cloned. The scripts responsible for cloning the repository and for running claude code would coordinate in the following way: ```bash # Script A: Claude code # Inform the agent that the claude script wants the git script. # That is, the git script must have completed before the claude script can begin its execution coder exp sync want claude git # Inform the agent that we would now like to begin execution of claude. # This command will block until the git script (and any other defined dependencies) # have completed coder exp sync start claude # Now we run claude code and any other commands we need claude ... # Once our script has completed, we inform the agent, so that any scripts that depend on this one # may begin their execution coder exp sync complete claude ``` ```bash # Script B: Git # Because the git script does not have any dependencies, we can simply inform the agent that we # intend to start coder exp sync start git git clone ssh://git@github.com/coder/coder # Once the repository have been cloned, we inform the agent that this script is complete, so that # scripts that depend on it may begin their execution. coder exp sync complete git ``` Notes: * Unit names (ie. `claude` and `git`) given as input to the sync commands are arbitrary strings. You do not have to conform to specific identifiers. We recommend naming your scripts descriptively, but succinctly. * Scripts unit names should be well documented. Other scripts will need to know the names you've chosen in order to depend on yours. Therefore, you --------- Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
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.True(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")
|
|
})
|
|
}
|