Skip to content

Commit 5222ac3

Browse files
committed
chore(e2e): naively parallelize CI jobs by chunking alphabetically
Signed-off-by: Eric Stroczynski <ericstroczynski@gmail.com>
1 parent ab923b3 commit 5222ac3

File tree

3 files changed

+116
-3
lines changed

3 files changed

+116
-3
lines changed

.github/workflows/e2e-tests.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,19 @@ on:
99
workflow_dispatch:
1010
jobs:
1111
e2e-tests:
12+
strategy:
13+
matrix:
14+
parallel-id: [0, 1, 2, 3]
1215
runs-on: ubuntu-latest
1316
steps:
1417
- uses: actions/checkout@v1
1518
- uses: actions/setup-go@v2
1619
with:
1720
go-version: '~1.16'
18-
- run: make e2e-local E2E_NODES=2 ARTIFACTS_DIR=./artifacts/
21+
- run: make e2e-local E2E_TEST_CHUNK=${{ matrix.parallel-id }} E2E_TEST_NUM_CHUNKS=${{ strategy.job-total }} E2E_NODES=2 ARTIFACTS_DIR=./artifacts/
1922
- name: Archive Test Artifacts # test results, failed or not, are always uploaded.
2023
if: ${{ always() }}
2124
uses: actions/upload-artifact@v2
2225
with:
23-
name: e2e-test-output-${{(github.event.pull_request.head.sha||github.sha)}}-${{ github.run_id }}
26+
name: e2e-test-output-${{ (github.event.pull_request.head.sha || github.sha) }}-${{ github.run_id }}-${{ matrix.parallel-id }}
2427
path: ${{ github.workspace }}/bin/artifacts/*

Makefile

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,17 @@ setup-bare: clean e2e.namespace
126126
E2E_NODES ?= 1
127127
E2E_FLAKE_ATTEMPTS ?= 1
128128
E2E_TIMEOUT ?= 90m
129-
E2E_OPTS ?= $(if $(E2E_SEED),-seed '$(E2E_SEED)') $(if $(TEST),-focus '$(TEST)') -flakeAttempts $(E2E_FLAKE_ATTEMPTS) -nodes $(E2E_NODES) -timeout $(E2E_TIMEOUT) -v -randomizeSuites -race -trace -progress
129+
E2E_COND_OPTS := $(if $(E2E_SEED),-seed '$(E2E_SEED)')
130+
# Optionally run an individual chunk of e2e test specs.
131+
# Do not use this from the CLI; this is intended to be used by CI only.
132+
E2E_TEST_CHUNK ?= all
133+
E2E_TEST_NUM_CHUNKS ?= 4
134+
ifeq (all,$(E2E_TEST_CHUNK))
135+
E2E_COND_OPTS := $(E2E_COND_OPTS) $(if $(TEST),-focus '$(TEST)')
136+
else
137+
E2E_COND_OPTS := $(E2E_COND_OPTS) -focus "$(shell go run ./test/e2e/split/... -chunks $(E2E_TEST_NUM_CHUNKS) -print-chunk $(E2E_TEST_CHUNK) ./test/e2e)"
138+
endif
139+
E2E_OPTS ?= $(E2E_COND_OPTS) -flakeAttempts $(E2E_FLAKE_ATTEMPTS) -nodes $(E2E_NODES) -timeout $(E2E_TIMEOUT) -v -randomizeSuites -race -trace -progress
130140
E2E_INSTALL_NS ?= operator-lifecycle-manager
131141
E2E_TEST_NS ?= operators
132142

test/e2e/split/main.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"io/ioutil"
7+
"log"
8+
"math"
9+
"os"
10+
"path/filepath"
11+
"regexp"
12+
"sort"
13+
"strings"
14+
)
15+
16+
var topDescribeRE = regexp.MustCompile(`var _ = Describe\("(.+)", func\(.*`)
17+
18+
func main() {
19+
var numChunks, printChunk int
20+
flag.IntVar(&numChunks, "chunks", 1, "Number of chunks to create focus regexps for")
21+
flag.IntVar(&printChunk, "print-chunk", 0, "Chunk to print a regexp for")
22+
flag.Parse()
23+
24+
if printChunk >= numChunks {
25+
log.Fatalf("the chunk to print (%d) must be a smaller number than the number of chunks (%d)", printChunk, numChunks)
26+
}
27+
28+
dir := flag.Arg(0)
29+
30+
// Clean dir.
31+
var err error
32+
if dir, err = filepath.Abs(dir); err != nil {
33+
log.Fatal(err)
34+
}
35+
wd, err := os.Getwd()
36+
if err != nil {
37+
log.Fatal(err)
38+
}
39+
if dir, err = filepath.Rel(wd, dir); err != nil {
40+
log.Fatal(err)
41+
}
42+
43+
// Find all Ginkgo specs in dir's test files.
44+
// These can be grouped independently.
45+
describeTable := make(map[string]struct{})
46+
matches, err := filepath.Glob(filepath.Join(dir, "*_test.go"))
47+
if err != nil {
48+
log.Fatal(err)
49+
}
50+
for _, match := range matches {
51+
b, err := ioutil.ReadFile(match)
52+
if err != nil {
53+
log.Fatal(err)
54+
}
55+
specNames := topDescribeRE.FindAllSubmatch(b, -1)
56+
if len(specNames) == 0 {
57+
log.Printf("%s: found no top level describes, skipping", match)
58+
continue
59+
}
60+
for _, possibleNames := range specNames {
61+
if len(possibleNames) != 2 {
62+
log.Printf("%s: expected to find 2 submatch, found %d:", match, len(possibleNames))
63+
for _, name := range possibleNames {
64+
log.Printf("\t%s\n", string(name))
65+
}
66+
continue
67+
}
68+
describe := strings.TrimSpace(string(possibleNames[1]))
69+
describeTable[describe] = struct{}{}
70+
}
71+
}
72+
73+
describes := make([]string, len(describeTable))
74+
i := 0
75+
for describeKey := range describeTable {
76+
describes[i] = describeKey
77+
i++
78+
}
79+
sort.Strings(describes)
80+
81+
chunks := make([][]string, numChunks)
82+
interval := int(math.Ceil(float64(len(describes)) / float64(numChunks)))
83+
currIdx := 0
84+
for chunkIdx := 0; chunkIdx < numChunks; chunkIdx++ {
85+
nextIdx := int(math.Min(float64(currIdx+interval), float64(len(describes))))
86+
chunks[chunkIdx] = describes[currIdx:nextIdx]
87+
currIdx = nextIdx
88+
}
89+
90+
sb := strings.Builder{}
91+
sb.WriteString("(")
92+
sb.WriteString(chunks[printChunk][0])
93+
for _, test := range chunks[printChunk][1:] {
94+
sb.WriteString("|")
95+
sb.WriteString(test)
96+
}
97+
sb.WriteString(").*")
98+
99+
fmt.Println(sb.String())
100+
}

0 commit comments

Comments
 (0)