Skip to content

Commit bb4d09e

Browse files
committed
fix (utils) : [CRW-8603] Use slice of string instead of apimachinery Set to merge events
We were previously using apimachinery Set that is internally based on Map, an unordered data structure. Due to this we were losing the original insertion order defined in the devfile while merging it, List() call returns the elements in non-deterministic order, as it internally ranges over the map. Use string slice to preserve insertion order of all event types. Signed-off-by: Rohan Kumar <[email protected]>
1 parent 22740cb commit bb4d09e

File tree

8 files changed

+177
-18
lines changed

8 files changed

+177
-18
lines changed

pkg/utils/overriding/merging.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -108,19 +108,19 @@ func MergeDevWorkspaceTemplateSpec(
108108
}
109109
}
110110

111-
preStartCommands := sets.String{}
112-
postStartCommands := sets.String{}
113-
preStopCommands := sets.String{}
114-
postStopCommands := sets.String{}
111+
var preStartCommands []string
112+
var postStartCommands []string
113+
var preStopCommands []string
114+
var postStopCommands []string
115115
for _, content := range allContents {
116116
if content.Events != nil {
117117
if result.Events == nil {
118118
result.Events = &dw.Events{}
119119
}
120-
preStartCommands = preStartCommands.Union(sets.NewString(content.Events.PreStart...))
121-
postStartCommands = postStartCommands.Union(sets.NewString(content.Events.PostStart...))
122-
preStopCommands = preStopCommands.Union(sets.NewString(content.Events.PreStop...))
123-
postStopCommands = postStopCommands.Union(sets.NewString(content.Events.PostStop...))
120+
preStartCommands = UnionStrings(preStartCommands, content.Events.PreStart)
121+
postStartCommands = UnionStrings(postStartCommands, content.Events.PostStart)
122+
preStopCommands = UnionStrings(preStopCommands, content.Events.PreStop)
123+
postStopCommands = UnionStrings(postStopCommands, content.Events.PostStop)
124124
}
125125

126126
if len(content.Variables) > 0 {
@@ -147,10 +147,10 @@ func MergeDevWorkspaceTemplateSpec(
147147
}
148148

149149
if result.Events != nil {
150-
result.Events.PreStart = preStartCommands.List()
151-
result.Events.PostStart = postStartCommands.List()
152-
result.Events.PreStop = preStopCommands.List()
153-
result.Events.PostStop = postStopCommands.List()
150+
result.Events.PreStart = preStartCommands
151+
result.Events.PostStart = postStartCommands
152+
result.Events.PreStop = preStopCommands
153+
result.Events.PostStop = postStopCommands
154154
}
155155

156156
return &result, nil

pkg/utils/overriding/merging_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,24 @@ func TestMergingOnlyParent(t *testing.T) {
134134
assert.Equal(t, &expectedDWT, gotDWT)
135135
}
136136
}
137+
138+
func TestMergePostStartInOrder(t *testing.T) {
139+
// Given
140+
baseFile := "test-fixtures/merges/multiple-post-start/main.yaml"
141+
parentFile := "test-fixtures/merges/multiple-post-start/plugin.yaml"
142+
resultFile := "test-fixtures/merges/multiple-post-start/result.yaml"
143+
144+
baseDWT := dw.DevWorkspaceTemplateSpecContent{}
145+
parentDWT := dw.DevWorkspaceTemplateSpecContent{}
146+
expectedDWT := dw.DevWorkspaceTemplateSpecContent{}
147+
148+
readFileToStruct(t, baseFile, &baseDWT)
149+
readFileToStruct(t, parentFile, &parentDWT)
150+
readFileToStruct(t, resultFile, &expectedDWT)
151+
152+
// When + Then
153+
gotDWT, err := MergeDevWorkspaceTemplateSpec(&baseDWT, &parentDWT)
154+
if assert.NoError(t, err) {
155+
assert.Equal(t, &expectedDWT, gotDWT)
156+
}
157+
}
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
events:
22
preStart:
3-
- "preStartFromMainContent"
43
- "preStartFromParent"
54
- "preStartFromPlugin"
5+
- "preStartFromMainContent"
66
preStop:
7-
- "preStopFromMainContent"
87
- "preStopFromParent"
98
- "preStopFromPlugin"
9+
- "preStopFromMainContent"
1010
postStart:
11-
- "postStartFromMainContent"
1211
- "postStartFromParent"
1312
- "postStartFromPlugin"
13+
- "postStartFromMainContent"
1414
postStop:
15-
- "postStopFromMainContent"
1615
- "postStopFromParent"
1716
- "postStopFromPlugin"
17+
- "postStopFromMainContent"
1818

1919
# Note:
2020
#
2121
# The command Id are merged *per-event type*
2222
# from the commands of the corresponding event type
23-
# in parent, plugins and devfile main content.
23+
# in parent, plugins and devfile main content.
2424
#
2525
# The command Ids in each event type are unordered sets.
2626
# Only event types are ordered; no guarantee is provided that
@@ -29,4 +29,4 @@ events:
2929
#
3030
# However in the test results, as it will happen usually,
3131
# command ids of a given event type will simply be ordered
32-
# in alphabetical order.
32+
# in alphabetical order.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
parent:
2+
uri: "anyParent"
3+
components:
4+
- plugin:
5+
uri: "aCustomLocation"
6+
name: "the-only-plugin"
7+
events:
8+
postStart:
9+
- "init-che-code-command"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
events:
2+
postStart:
3+
- "event-one"
4+
- "event-two"
5+
- "event-three"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
events:
2+
preStart: []
3+
preStop: []
4+
postStop: []
5+
postStart:
6+
- "event-one"
7+
- "event-two"
8+
- "event-three"
9+
- "init-che-code-command"

pkg/utils/overriding/utils.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,34 @@ func handleUnmarshal(j []byte) (map[string]interface{}, error) {
3232
}
3333
return m, nil
3434
}
35+
36+
// UnionStrings returns the union of two string slices, preserving the order
37+
// of first occurrence and removing duplicates.
38+
//
39+
// Elements from the first slice `a` are added in order. Elements from the second
40+
// slice `b` are appended only if they do not already exist in `a`.
41+
//
42+
// The returned slice is guaranteed to be non-nil, even if both input slices are nil.
43+
//
44+
// Example:
45+
//
46+
// UnionStrings([]string{"a", "b"}, []string{"b", "c"})
47+
// // Returns: []string{"a", "b", "c"}
48+
func UnionStrings(a, b []string) []string {
49+
alreadyExistsInList := make(map[string]bool)
50+
result := make([]string, 0)
51+
52+
for _, s := range a {
53+
if !alreadyExistsInList[s] {
54+
alreadyExistsInList[s] = true
55+
result = append(result, s)
56+
}
57+
}
58+
for _, s := range b {
59+
if !alreadyExistsInList[s] {
60+
alreadyExistsInList[s] = true
61+
result = append(result, s)
62+
}
63+
}
64+
return result
65+
}

pkg/utils/overriding/utils_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package overriding
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func TestUnionStrings(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
a []string
12+
b []string
13+
expected []string
14+
}{
15+
{
16+
name: "both empty",
17+
a: []string{},
18+
b: []string{},
19+
expected: []string{},
20+
},
21+
{
22+
name: "first is nil, second is empty",
23+
a: nil,
24+
b: []string{},
25+
expected: []string{},
26+
},
27+
{
28+
name: "first is empty, second is nil",
29+
a: []string{},
30+
b: nil,
31+
expected: []string{},
32+
},
33+
{
34+
name: "both are nil",
35+
a: nil,
36+
b: nil,
37+
expected: []string{},
38+
},
39+
{
40+
name: "no overlap",
41+
a: []string{"x", "y"},
42+
b: []string{"a", "b"},
43+
expected: []string{"x", "y", "a", "b"},
44+
},
45+
{
46+
name: "partial overlap",
47+
a: []string{"a", "b"},
48+
b: []string{"b", "c"},
49+
expected: []string{"a", "b", "c"},
50+
},
51+
{
52+
name: "duplicate in same slice",
53+
a: []string{"a", "a", "b"},
54+
b: []string{"b", "c"},
55+
expected: []string{"a", "b", "c"},
56+
},
57+
{
58+
name: "only duplicates",
59+
a: []string{"a", "b"},
60+
b: []string{"a", "b"},
61+
expected: []string{"a", "b"},
62+
},
63+
{
64+
name: "preserves order",
65+
a: []string{"c", "a"},
66+
b: []string{"b", "a", "d"},
67+
expected: []string{"c", "a", "b", "d"},
68+
},
69+
}
70+
71+
for _, tt := range tests {
72+
t.Run(tt.name, func(t *testing.T) {
73+
actual := UnionStrings(tt.a, tt.b)
74+
75+
if actual == nil {
76+
t.Errorf("expected non-nil slice, got nil")
77+
}
78+
79+
if !reflect.DeepEqual(actual, tt.expected) {
80+
t.Errorf("UnionStrings(%v, %v) = %v; want %v", tt.a, tt.b, actual, tt.expected)
81+
}
82+
})
83+
}
84+
}

0 commit comments

Comments
 (0)