Skip to content

Commit 2ba8273

Browse files
committed
Make plan.String() deterministic for usability in tests
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
1 parent 7500747 commit 2ba8273

File tree

3 files changed

+134
-135
lines changed

3 files changed

+134
-135
lines changed

pkg/compose/plan.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package compose
1818

1919
import (
2020
"fmt"
21+
"sort"
2122
"strconv"
2223
"strings"
2324
"time"
@@ -139,9 +140,14 @@ func (p *Plan) addNode(op Operation, group string, deps ...*PlanNode) *PlanNode
139140
func (p *Plan) String() string {
140141
var sb strings.Builder
141142
for _, node := range p.Nodes {
142-
deps := make([]string, len(node.DependsOn))
143+
depIDs := make([]int, len(node.DependsOn))
143144
for i, d := range node.DependsOn {
144-
deps[i] = strconv.Itoa(d.ID)
145+
depIDs[i] = d.ID
146+
}
147+
sort.Ints(depIDs)
148+
deps := make([]string, len(depIDs))
149+
for i, id := range depIDs {
150+
deps[i] = strconv.Itoa(id)
145151
}
146152
fmt.Fprintf(&sb, "[%s] -> #%d %s, %s, %s",
147153
strings.Join(deps, ","),

pkg/compose/reconcile.go

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ func reconcile(_ context.Context, project *types.Project, observed *ObservedStat
9494

9595
// reconcileNetworks adds plan nodes for network creation or recreation.
9696
func (r *reconciler) reconcileNetworks() error {
97-
for key, desired := range r.project.Networks {
97+
for _, key := range sortedKeys(r.project.Networks) {
98+
desired := r.project.Networks[key]
9899
if desired.External {
99100
continue
100101
}
@@ -187,7 +188,8 @@ func (r *reconciler) planRecreateNetwork(key string, nw *types.NetworkConfig) er
187188

188189
// reconcileVolumes adds plan nodes for volume creation or recreation.
189190
func (r *reconciler) reconcileVolumes() error {
190-
for key, desired := range r.project.Volumes {
191+
for _, key := range sortedKeys(r.project.Volumes) {
192+
desired := r.project.Volumes[key]
191193
if desired.External {
192194
continue
193195
}
@@ -285,10 +287,11 @@ func (r *reconciler) planRecreateVolume(key string, vol *types.VolumeConfig) {
285287
}
286288

287289
// servicesUsingNetwork returns the names of services that reference the given
288-
// compose network key.
290+
// compose network key, sorted for deterministic plan output.
289291
func (r *reconciler) servicesUsingNetwork(networkKey string) []string {
290292
var names []string
291-
for _, svc := range r.project.Services {
293+
for _, key := range sortedKeys(r.project.Services) {
294+
svc := r.project.Services[key]
292295
if _, ok := svc.Networks[networkKey]; ok {
293296
names = append(names, svc.Name)
294297
}
@@ -297,10 +300,11 @@ func (r *reconciler) servicesUsingNetwork(networkKey string) []string {
297300
}
298301

299302
// servicesUsingVolume returns the names of services that mount the given
300-
// compose volume key.
303+
// compose volume key, sorted for deterministic plan output.
301304
func (r *reconciler) servicesUsingVolume(volumeKey string) []string {
302305
var names []string
303-
for _, svc := range r.project.Services {
306+
for _, key := range sortedKeys(r.project.Services) {
307+
svc := r.project.Services[key]
304308
for _, v := range svc.Volumes {
305309
if v.Source == volumeKey {
306310
names = append(names, svc.Name)
@@ -338,10 +342,13 @@ func (r *reconciler) reconcileContainers() error {
338342
// dependencies are reconciled before the services that depend on them.
339343
func (r *reconciler) visitInDependencyOrder(g *Graph) error {
340344
visited := map[string]bool{}
345+
// Sort vertex keys for deterministic plan output in tests
346+
keys := sortedKeys(g.Vertices)
341347
for {
342348
// Find a vertex whose all children are visited
343349
var next *Vertex
344-
for _, v := range g.Vertices {
350+
for _, k := range keys {
351+
v := g.Vertices[k]
345352
if visited[v.Key] {
346353
continue
347354
}
@@ -501,7 +508,7 @@ func (r *reconciler) mustRecreate(expected types.ServiceConfig, oc ObservedConta
501508

502509
// hasNetworkMismatch checks if the container is not connected to all expected networks.
503510
func (r *reconciler) hasNetworkMismatch(expected types.ServiceConfig, oc ObservedContainer) bool {
504-
for net := range expected.Networks {
511+
for _, net := range sortedKeys(expected.Networks) {
505512
expectedID := ""
506513
if obs, ok := r.observed.Networks[net]; ok {
507514
expectedID = obs.ID
@@ -636,7 +643,8 @@ func (r *reconciler) planStopDependents(service types.ServiceConfig) []*PlanNode
636643
// references, plus the last node of dependency services.
637644
func (r *reconciler) infrastructureDeps(service types.ServiceConfig) []*PlanNode {
638645
var deps []*PlanNode
639-
for net := range service.Networks {
646+
// Sort map keys for deterministic plan output in tests
647+
for _, net := range sortedKeys(service.Networks) {
640648
if node, ok := r.networkNodes[net]; ok {
641649
deps = append(deps, node)
642650
}
@@ -648,7 +656,7 @@ func (r *reconciler) infrastructureDeps(service types.ServiceConfig) []*PlanNode
648656
}
649657
}
650658
}
651-
for depName := range service.DependsOn {
659+
for _, depName := range sortedKeys(service.DependsOn) {
652660
if node, ok := r.serviceNodes[depName]; ok {
653661
deps = append(deps, node)
654662
}
@@ -704,5 +712,16 @@ func (r *reconciler) observedSummaries(serviceName string) []container.Summary {
704712
return result
705713
}
706714

715+
// sortedKeys returns the keys of a map sorted alphabetically.
716+
// This ensures deterministic iteration order for reproducible plan output in tests.
717+
func sortedKeys[V any](m map[string]V) []string {
718+
keys := make([]string, 0, len(m))
719+
for k := range m {
720+
keys = append(keys, k)
721+
}
722+
sort.Strings(keys)
723+
return keys
724+
}
725+
707726
// serviceLabel is a package-level shorthand for the service label key.
708727
const serviceLabel = "com.docker.compose.service"

0 commit comments

Comments
 (0)