Skip to content

Commit 5a54fec

Browse files
rohanKanojiamichael-valdron
authored andcommitted
fix (utils) : [CRW-8603] Use slice of string instead of apimachinery Set to merge events (devfile#1711)
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 <rohaan@redhat.com>
1 parent 15a97f6 commit 5a54fec

File tree

8 files changed

+197
-23
lines changed

8 files changed

+197
-23
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: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,31 @@
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
#
25-
# The command Ids in each event type are unordered sets.
26-
# Only event types are ordered; no guarantee is provided that
27-
# Commands inside an event type will be executed in the order
28-
# they appear in the list.
25+
# The command Ids in each event type are string slices.
26+
# While merging original insertion order of events would
27+
# be preserved.
2928
#
3029
# However in the test results, as it will happen usually,
31-
# command ids of a given event type will simply be ordered
32-
# in alphabetical order.
30+
# command ids of a given event type will be merged preserving
31+
# insertion order in source devfiles.
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: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//
2+
//
3+
// Copyright Red Hat
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
package overriding
18+
19+
import (
20+
"reflect"
21+
"testing"
22+
)
23+
24+
func TestUnionStrings(t *testing.T) {
25+
tests := []struct {
26+
name string
27+
a []string
28+
b []string
29+
expected []string
30+
}{
31+
{
32+
name: "both empty",
33+
a: []string{},
34+
b: []string{},
35+
expected: []string{},
36+
},
37+
{
38+
name: "first is nil, second is empty",
39+
a: nil,
40+
b: []string{},
41+
expected: []string{},
42+
},
43+
{
44+
name: "first is empty, second is nil",
45+
a: []string{},
46+
b: nil,
47+
expected: []string{},
48+
},
49+
{
50+
name: "both are nil",
51+
a: nil,
52+
b: nil,
53+
expected: []string{},
54+
},
55+
{
56+
name: "no overlap",
57+
a: []string{"x", "y"},
58+
b: []string{"a", "b"},
59+
expected: []string{"x", "y", "a", "b"},
60+
},
61+
{
62+
name: "partial overlap",
63+
a: []string{"a", "b"},
64+
b: []string{"b", "c"},
65+
expected: []string{"a", "b", "c"},
66+
},
67+
{
68+
name: "duplicate in same slice",
69+
a: []string{"a", "a", "b"},
70+
b: []string{"b", "c"},
71+
expected: []string{"a", "b", "c"},
72+
},
73+
{
74+
name: "only duplicates",
75+
a: []string{"a", "b"},
76+
b: []string{"a", "b"},
77+
expected: []string{"a", "b"},
78+
},
79+
{
80+
name: "preserves order",
81+
a: []string{"c", "a"},
82+
b: []string{"b", "a", "d"},
83+
expected: []string{"c", "a", "b", "d"},
84+
},
85+
}
86+
87+
for _, tt := range tests {
88+
t.Run(tt.name, func(t *testing.T) {
89+
actual := UnionStrings(tt.a, tt.b)
90+
91+
if actual == nil {
92+
t.Errorf("expected non-nil slice, got nil")
93+
}
94+
95+
if !reflect.DeepEqual(actual, tt.expected) {
96+
t.Errorf("UnionStrings(%v, %v) = %v; want %v", tt.a, tt.b, actual, tt.expected)
97+
}
98+
})
99+
}
100+
}

0 commit comments

Comments
 (0)