Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 8 additions & 23 deletions pkg/devcontainer/graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,17 +445,16 @@ func processNeighbors(

func (g *Graph[T]) sortNodeIDsWithPriority(priority map[string]int) ([]string, error) {
workingInDegree := copyInDegreeMap(g.inDegree)
ready := initializeQueue(workingInDegree)
sortedResult := make([]string, 0, len(g.nodes))

for len(ready) > 0 {
sortByPriority(ready, priority)

current := ready[0]
ready = ready[1:]
sortedResult = append(sortedResult, current)

processNeighborsUnordered(g.edges, current, workingInDegree, &ready)
for {
round := collectZeroInDegree(workingInDegree)
if len(round) == 0 {
break
}
sortByPriority(round, priority)
sortedResult = append(sortedResult, round...)
advanceRound(round, g.edges, workingInDegree)
}

if len(sortedResult) != len(g.nodes) {
Expand Down Expand Up @@ -483,20 +482,6 @@ func comparePriority(a, b string, priority map[string]int) bool {
return a < b
}

func processNeighborsUnordered(
edges map[string][]string,
currentNode string,
inDegree map[string]int,
queue *[]string,
) {
for _, neighbor := range edges[currentNode] {
inDegree[neighbor]--
if inDegree[neighbor] == 0 {
*queue = append(*queue, neighbor)
}
}
}

func circularDependencyError(inDegree map[string]int) error {
remaining := []string{}
for id, deg := range inDegree {
Expand Down
96 changes: 96 additions & 0 deletions pkg/devcontainer/graph/graph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -641,3 +641,99 @@ func (suite *GraphTestSuite) TestTopologicalSortAdvanced() {
})
}
}

func (suite *GraphTestSuite) TestSortWithPriorityRoundBased() {
testCases := []struct {
name string
setup func()
priority map[string]int
expected []string
}{
{
name: "round-based emits all zero-in-degree before advancing",
setup: suite.buildParallelChains,
priority: map[string]int{"B": 1, "A": 2},
expected: []string{"B", "A", "C", "D"},
},
{
name: "diamond graph groups siblings in same round",
setup: suite.buildDiamond,
priority: map[string]int{"C": 1, "B": 2},
expected: []string{"A", "C", "B", "D"},
},
{
name: "nodes without priority sort alphabetically after prioritized nodes",
setup: suite.buildIndependentXYZ,
priority: map[string]int{"Z": 1},
expected: []string{"Z", "X", "Y"},
},
{
name: "mid-round promotion prevented by round boundary",
setup: suite.buildMidRoundPromotion,
priority: map[string]int{"A": 1, "C": 2, "B": 3},
expected: []string{"A", "B", "C"},
},
{
name: "circular dependency returns error",
setup: suite.buildCycle,
priority: map[string]int{"A": 1},
expected: nil,
},
}

for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
tc.setup()

result, err := suite.graph.sortNodeIDsWithPriority(tc.priority)
if tc.expected == nil {
suite.Error(err)
suite.Contains(err.Error(), "circular dependency")
} else {
suite.NoError(err)
suite.Equal(tc.expected, result)
}
})
}
}

func (suite *GraphTestSuite) buildParallelChains() {
suite.Require().NoError(suite.graph.AddNode("A", "dataA"))
suite.Require().NoError(suite.graph.AddNode("B", "dataB"))
suite.Require().NoError(suite.graph.AddNode("C", "dataC"))
suite.Require().NoError(suite.graph.AddNode("D", "dataD"))
suite.Require().NoError(suite.graph.AddEdge("A", "C"))
suite.Require().NoError(suite.graph.AddEdge("B", "D"))
}

func (suite *GraphTestSuite) buildDiamond() {
suite.Require().NoError(suite.graph.AddNode("A", "dataA"))
suite.Require().NoError(suite.graph.AddNode("B", "dataB"))
suite.Require().NoError(suite.graph.AddNode("C", "dataC"))
suite.Require().NoError(suite.graph.AddNode("D", "dataD"))
suite.Require().NoError(suite.graph.AddEdge("A", "B"))
suite.Require().NoError(suite.graph.AddEdge("A", "C"))
suite.Require().NoError(suite.graph.AddEdge("B", "D"))
suite.Require().NoError(suite.graph.AddEdge("C", "D"))
}

func (suite *GraphTestSuite) buildIndependentXYZ() {
suite.Require().NoError(suite.graph.AddNode("X", "dataX"))
suite.Require().NoError(suite.graph.AddNode("Y", "dataY"))
suite.Require().NoError(suite.graph.AddNode("Z", "dataZ"))
}

func (suite *GraphTestSuite) buildMidRoundPromotion() {
suite.Require().NoError(suite.graph.AddNode("A", "dataA"))
suite.Require().NoError(suite.graph.AddNode("B", "dataB"))
suite.Require().NoError(suite.graph.AddNode("C", "dataC"))
suite.Require().NoError(suite.graph.AddEdge("A", "C"))
}

func (suite *GraphTestSuite) buildCycle() {
suite.Require().NoError(suite.graph.AddNode("A", "dataA"))
suite.Require().NoError(suite.graph.AddNode("B", "dataB"))
suite.Require().NoError(suite.graph.AddEdge("A", "B"))
suite.Require().NoError(suite.graph.AddEdge("B", "A"))
}
Loading