Skip to content

Commit 89dda20

Browse files
committedJan 9, 2023
correctly ensure deterministic spec order, even if specs are generated by iterating over a map
1 parent 7a2b242 commit 89dda20

File tree

6 files changed

+178
-21
lines changed

6 files changed

+178
-21
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package nondeterministic_fixture_test
2+
3+
import (
4+
. "github.com/onsi/ginkgo/v2"
5+
)
6+
7+
var cheat = func() {
8+
It("in", func() {
9+
10+
})
11+
12+
It("order", func() {
13+
14+
})
15+
}
16+
17+
var _ = Describe("ordered", Ordered, func() {
18+
It("always", func() {
19+
20+
})
21+
22+
It("runs", func() {
23+
24+
})
25+
26+
cheat()
27+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package nondeterministic_fixture_test
2+
3+
import (
4+
. "github.com/onsi/ginkgo/v2"
5+
)
6+
7+
var specGenerator = map[string]bool{
8+
"map-A": true,
9+
"map-B": true,
10+
"map-C": true,
11+
"map-D": true,
12+
"map-E": true,
13+
"map-F": true,
14+
"map-G": true,
15+
"map-H": true,
16+
"map-I": true,
17+
"map-J": true,
18+
}
19+
20+
var _ = Describe("some tests, that include a test generated by iterating over a map", func() {
21+
Describe("some tests", func() {
22+
It("runs A", func() {
23+
24+
})
25+
It("runs B", func() {
26+
27+
})
28+
})
29+
30+
When("iterating over a map", func() {
31+
for key := range specGenerator {
32+
It("runs "+key, func() {
33+
34+
})
35+
}
36+
})
37+
38+
It("runs some other tests", func() {
39+
40+
})
41+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package nondeterministic_fixture_test
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
. "github.com/onsi/ginkgo/v2"
8+
"github.com/onsi/ginkgo/v2/types"
9+
. "github.com/onsi/gomega"
10+
)
11+
12+
func TestNondeterministicFixture(t *testing.T) {
13+
RegisterFailHandler(Fail)
14+
RunSpecs(t, "NondeterministicFixture Suite")
15+
}
16+
17+
var _ = ReportAfterSuite("ensure all specs ran correctly", func(report types.Report) {
18+
specs := report.SpecReports.WithLeafNodeType(types.NodeTypeIt)
19+
orderedTexts := []string{}
20+
textCounts := map[string]int{}
21+
for _, spec := range specs {
22+
text := spec.FullText()
23+
textCounts[text] += 1
24+
if strings.HasPrefix(text, "ordered") {
25+
orderedTexts = append(orderedTexts, spec.LeafNodeText)
26+
}
27+
}
28+
29+
By("ensuring there are no duplicates")
30+
for text, count := range textCounts {
31+
Ω(count).Should(Equal(1), text)
32+
}
33+
34+
By("ensuring ordered specs are strictly preserved")
35+
Ω(orderedTexts).Should(Equal([]string{"always", "runs", "in", "order"}))
36+
})

‎integration/run_test.go

+14
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,20 @@ var _ = Describe("Running Specs", func() {
444444
})
445445
})
446446

447+
Context("when running a suite in parallel that may have specs that are not deterministically ordered", func() {
448+
BeforeEach(func() {
449+
fm.MountFixture("nondeterministic")
450+
})
451+
It("successfully generates a stable sort across all parallel processes and ensures exactly the correct specs are run", func() {
452+
By("Note: the assertions live in the ReportAfterSuite of the fixture. So we simply assert that we succeeded")
453+
session := startGinkgo(fm.PathTo("nondeterministic"), "--no-color", "--procs=3")
454+
Eventually(session).Should(gexec.Exit(0))
455+
456+
session = startGinkgo(fm.PathTo("nondeterministic"), "--no-color", "--procs=3", "--randomize-all")
457+
Eventually(session).Should(gexec.Exit(0))
458+
})
459+
})
460+
447461
Context("when there is a version mismatch between the cli and the test package", func() {
448462
It("emits a useful error and tries running", func() {
449463
fm.MountFixture(("version_mismatch"))

‎internal/ordering.go

+9-15
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ func (s *SortableSpecs) Len() int { return len(s.Indexes) }
2626
func (s *SortableSpecs) Swap(i, j int) { s.Indexes[i], s.Indexes[j] = s.Indexes[j], s.Indexes[i] }
2727
func (s *SortableSpecs) Less(i, j int) bool {
2828
a, b := s.Specs[s.Indexes[i]], s.Specs[s.Indexes[j]]
29+
30+
firstOrderedA := a.Nodes.FirstNodeMarkedOrdered()
31+
firstOrderedB := b.Nodes.FirstNodeMarkedOrdered()
32+
if firstOrderedA.ID == firstOrderedB.ID && !firstOrderedA.IsZero() {
33+
// strictly preserve order in ordered containers. ID will track this as IDs are generated monotonically
34+
return a.FirstNodeWithType(types.NodeTypeIt).ID < b.FirstNodeWithType(types.NodeTypeIt).ID
35+
}
36+
2937
aCLs := a.Nodes.WithType(types.NodeTypesForContainerAndIt).CodeLocations()
3038
bCLs := b.Nodes.WithType(types.NodeTypesForContainerAndIt).CodeLocations()
3139
for i := 0; i < len(aCLs) && i < len(bCLs); i++ {
@@ -97,7 +105,6 @@ func OrderSpecs(specs Specs, suiteConfig types.SuiteConfig) (GroupedSpecIndices,
97105
// we shuffle outermost containers. so we need to form shufflable groupings of GroupIDs
98106
shufflableGroupingIDs := []uint{}
99107
shufflableGroupingIDToGroupIDs := map[uint][]uint{}
100-
shufflableGroupingsIDToSortKeys := map[uint]string{}
101108

102109
// for each execution group we're going to have to pick a node to represent how the
103110
// execution group is grouped for shuffling:
@@ -106,7 +113,7 @@ func OrderSpecs(specs Specs, suiteConfig types.SuiteConfig) (GroupedSpecIndices,
106113
nodeTypesToShuffle = types.NodeTypeIt
107114
}
108115

109-
//so, fo reach execution group:
116+
//so, for each execution group:
110117
for _, groupID := range executionGroupIDs {
111118
// pick out a representative spec
112119
representativeSpec := specs[executionGroups[groupID][0]]
@@ -121,22 +128,9 @@ func OrderSpecs(specs Specs, suiteConfig types.SuiteConfig) (GroupedSpecIndices,
121128
if len(shufflableGroupingIDToGroupIDs[shufflableGroupingNode.ID]) == 1 {
122129
// record the shuffleable group ID
123130
shufflableGroupingIDs = append(shufflableGroupingIDs, shufflableGroupingNode.ID)
124-
// and record the sort key to use
125-
shufflableGroupingsIDToSortKeys[shufflableGroupingNode.ID] = shufflableGroupingNode.CodeLocation.String()
126131
}
127132
}
128133

129-
// now we sort the shufflable groups by the sort key. We use the shufflable group nodes code location and break ties using its node id
130-
sort.SliceStable(shufflableGroupingIDs, func(i, j int) bool {
131-
keyA := shufflableGroupingsIDToSortKeys[shufflableGroupingIDs[i]]
132-
keyB := shufflableGroupingsIDToSortKeys[shufflableGroupingIDs[j]]
133-
if keyA == keyB {
134-
return shufflableGroupingIDs[i] < shufflableGroupingIDs[j]
135-
} else {
136-
return keyA < keyB
137-
}
138-
})
139-
140134
// now we permute the sorted shufflable grouping IDs and build the ordered Groups
141135
orderedGroups := GroupedSpecIndices{}
142136
permutation := r.Perm(len(shufflableGroupingIDs))

‎internal/ordering_test.go

+51-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package internal_test
22

33
import (
44
"strings"
5+
"time"
56

67
. "github.com/onsi/ginkgo/v2"
78
. "github.com/onsi/gomega"
@@ -131,7 +132,6 @@ var _ = Describe("OrderSpecs", func() {
131132
S(con2, N("G", ntIt, CL("file_B", 15))),
132133
S(con2, N("H", ntIt, CL("file_B", 20))),
133134
}
134-
135135
})
136136

137137
It("always generates a consistent randomization when given the same seed", func() {
@@ -279,15 +279,16 @@ var _ = Describe("OrderSpecs", func() {
279279
Describe("presorting-specs", func() {
280280
BeforeEach(func() {
281281
conA0 := N(ntCon, CL("file-A", 1))
282-
conA1 := N(ntCon, CL("file-A", 4))
282+
conA1 := N(ntCon, Ordered, CL("file-A", 4))
283283
conA2 := N(ntCon, CL("file-A", 10))
284284
conB0 := N(ntCon, CL("file-B", 1))
285285
conC0 := N(ntCon, CL("file-C", 1))
286286
specs = Specs{
287287
S(conA0, N("A", ntIt, CL("file-A", 2))),
288288
S(conA0, N("B", ntIt, CL("file-A", 3))),
289-
S(conA0, conA1, N("C", ntIt, CL("file-A", 5))),
290-
S(conA0, conA1, N("D", ntIt, CL("file-A", 6))),
289+
// C and D are generated by a helper function in a different file. if we aren't careful they would sort after E. But conA1 is an Ordered container so its important things run in the correct order
290+
S(conA0, conA1, N("C", ntIt, CL("file-Z", 100))),
291+
S(conA0, conA1, N("D", ntIt, CL("file-Z", 99))),
291292
S(conA0, conA1, N(ntCon, CL("file-A", 7)), N("E", ntIt, CL("file-A", 8))),
292293
S(conA0, N("F", ntIt, CL("file-A", 9))),
293294
S(conA0, conA2, N("G", ntIt, CL("file-A", 11))),
@@ -306,15 +307,59 @@ var _ = Describe("OrderSpecs", func() {
306307
conf.RandomizeAllSpecs = false
307308
})
308309

309-
It("ensures a deterministic order for specs that are defined at the same line without messing with the natural order of specs and containers", func() {
310+
It("ensures a deterministic order for specs that are defined at the same line without messing with the natural order of specs and containers; it also ensures ordered containers run in the correct order - even if specs are generated in a helper function at a different line", func() {
310311
conf.RandomSeed = 1 // this happens to sort conA0 ahead of conB0 - other than that, though, we are actually testing SortableSpecs
311312
groupedSpecIndices, serialSpecIndices := internal.OrderSpecs(specs, conf)
312313
Ω(serialSpecIndices).Should(BeEmpty())
313314

314315
Ω(getTexts(specs, groupedSpecIndices).Join()).Should(Equal("ABCDEFGHB-ZB-YB-BB-CB-DB-AC-AC-BC-CC-DC-EC-F"))
315-
316316
})
317317
})
318318

319+
Describe("presorting-specs with randomize-all enabled", func() {
320+
generateSpecs := func() Specs {
321+
conA0 := N(ntCon, CL("file-A", 1))
322+
conA1 := N(ntCon, CL("file-A", 4))
323+
conA2 := N(ntCon, CL("file-A", 10))
324+
conB0 := N(ntCon, CL("file-B", 1))
325+
conC0 := N(ntCon, CL("file-C", 1))
326+
specs := Specs{
327+
S(conA0, N("A", ntIt, CL("file-A", 2))),
328+
S(conA0, N("B", ntIt, CL("file-A", 3))),
329+
S(conA0, conA1, N("C", ntIt, CL("file-A", 5))),
330+
S(conA0, conA1, N("D", ntIt, CL("file-A", 6))),
331+
S(conA0, conA1, N(ntCon, CL("file-A", 7)), N("E", ntIt, CL("file-A", 8))),
332+
S(conA0, N("F", ntIt, CL("file-A", 9))),
333+
S(conA0, conA2, N("G", ntIt, CL("file-A", 11))),
334+
S(conA0, conA2, N("H", ntIt, CL("file-A", 12))),
335+
S(conB0, N("B-Z", ntIt, CL("file-B", 2))),
336+
S(conB0, N("B-Y", ntIt, CL("file-B", 3))),
337+
S(conB0, N("B-D", ntIt, CL("file-B", 4))),
338+
S(conB0, N("B-C", ntIt, CL("file-B", 4))),
339+
S(conB0, N("B-B", ntIt, CL("file-B", 4))),
340+
S(conB0, N("B-A", ntIt, CL("file-B", 5))),
341+
}
342+
343+
for key := range map[string]bool{"C-A": true, "C-B": true, "C-C": true, "C-D": true, "C-E": true, "C-F": true} {
344+
specs = append(specs, S(conC0, N(key, ntIt, CL("file-C", 2)))) // normally this would be totally non-deterministic
345+
}
346+
return specs
347+
}
348+
349+
It("ensures a deterministic order for specs that are defined at the same line", func() {
350+
conf.RandomSeed = time.Now().Unix()
351+
conf.RandomizeAllSpecs = true
352+
353+
specsA := generateSpecs()
354+
specsB := generateSpecs()
355+
groupedSpecIndicesA, serialSpecIndices := internal.OrderSpecs(specsA, conf)
356+
Ω(serialSpecIndices).Should(BeEmpty())
357+
groupedSpecIndicesB, serialSpecIndices := internal.OrderSpecs(specsB, conf)
358+
Ω(serialSpecIndices).Should(BeEmpty())
359+
360+
Ω(getTexts(specsA, groupedSpecIndicesA).Join()).Should(Equal(getTexts(specsB, groupedSpecIndicesB).Join()))
361+
362+
}, MustPassRepeatedly(5))
363+
})
319364
})
320365
})

0 commit comments

Comments
 (0)
Please sign in to comment.