Ctrl K
ComunidadGuías…

agent/unit/manager_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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")
	})
}