-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathmain.go
More file actions
216 lines (184 loc) · 6.56 KB
/
main.go
File metadata and controls
216 lines (184 loc) · 6.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
package main
import (
"flag"
"fmt"
"os"
"strconv"
"strings"
"github.com/bmatcuk/doublestar"
)
var useCircleCI bool
var useJUnitXML bool
var useLineCount bool
var junitXMLPath string
var testFilePattern = ""
var excludeFilePattern = ""
var circleCIProjectPrefix = ""
var circleCIBranchName string
var splitIndex int
var splitTotal int
var circleCIAPIKey string
var bias string
func printMsg(msg string, args ...interface{}) {
if len(args) == 0 {
fmt.Fprint(os.Stderr, msg)
} else {
fmt.Fprintf(os.Stderr, msg, args...)
}
}
func fatalMsg(msg string, args ...interface{}) {
printMsg(msg, args...)
os.Exit(1)
}
func removeDeletedFiles(fileTimes map[string]float64, currentFileSet map[string]bool) {
for file := range fileTimes {
if !currentFileSet[file] {
delete(fileTimes, file)
}
}
}
func addNewFiles(fileTimes map[string]float64, currentFileSet map[string]bool) {
averageFileTime := 0.0
if len(fileTimes) > 0 {
for _, time := range fileTimes {
averageFileTime += time
}
averageFileTime /= float64(len(fileTimes))
} else {
averageFileTime = 1.0
}
for file := range currentFileSet {
if _, isSet := fileTimes[file]; isSet {
continue
}
if useCircleCI || useJUnitXML {
printMsg("missing file time for %s\n", file)
}
fileTimes[file] = averageFileTime
}
}
func parseFlags() {
flag.StringVar(&testFilePattern, "glob", "spec/**/*_spec.rb", "Glob pattern to find test files. Make sure to single-quote to avoid shell expansion.")
flag.StringVar(&excludeFilePattern, "exclude-glob", "", "Glob pattern to exclude test files. Make sure to single-quote.")
flag.IntVar(&splitIndex, "split-index", -1, "This test container's index (or set CIRCLE_NODE_INDEX)")
flag.IntVar(&splitTotal, "split-total", -1, "Total number of containers (or set CIRCLE_NODE_TOTAL)")
flag.StringVar(&circleCIAPIKey, "circleci-key", "", "CircleCI API key (or set CIRCLECI_API_KEY environment variable) - required to use CircleCI")
flag.StringVar(&circleCIProjectPrefix, "circleci-project", "", "CircleCI project name (e.g. github/leonid-shevtsov/split_tests) - required to use CircleCI")
flag.StringVar(&circleCIBranchName, "circleci-branch", "", "Current branch for CircleCI (or set CIRCLE_BRANCH) - required to use CircleCI")
flag.BoolVar(&useJUnitXML, "junit", false, "Use a JUnit XML report for test times")
flag.StringVar(&junitXMLPath, "junit-path", "", "Path to a JUnit XML report (leave empty to read from stdin; use glob pattern to load multiple files)")
flag.BoolVar(&useLineCount, "line-count", false, "Use line count to estimate test times")
flag.StringVar(&junitUpdateOldGlob, "junit-update", "", "Glob pattern for old JUnit XML files (for updating timings with sliding window)")
flag.StringVar(&junitUpdateNewGlob, "junit-new", "", "Glob pattern for new JUnit XML files (for updating timings with sliding window)")
flag.StringVar(&junitUpdateOutPath, "junit-out", "", "Output path for updated JUnit XML file (for updating timings with sliding window)")
var showHelp bool
flag.BoolVar(&showHelp, "help", false, "Show this help text")
flag.StringVar(&bias, "bias", "", "Set bias for specific splits (if one split is doing extra work like running a linter).\nFormat: [split_index]=[bias_in_seconds],[another_index]=[another_bias],...")
flag.Parse()
var err error
if circleCIAPIKey == "" {
circleCIAPIKey = os.Getenv("CIRCLECI_API_KEY")
}
if circleCIBranchName == "" {
circleCIBranchName = os.Getenv("CIRCLE_BRANCH")
}
if splitTotal == -1 {
splitTotal, err = strconv.Atoi(os.Getenv("CIRCLE_NODE_TOTAL"))
if err != nil {
splitIndex = -1
}
}
if splitIndex == -1 {
splitIndex, err = strconv.Atoi(os.Getenv("CIRCLE_NODE_INDEX"))
if err != nil {
splitIndex = -1
}
}
useCircleCI = circleCIAPIKey != ""
if showHelp {
printMsg("Splits test files into containers of even duration\n\n")
flag.PrintDefaults()
os.Exit(1)
}
if useCircleCI && (circleCIProjectPrefix == "" || circleCIBranchName == "") {
fatalMsg("Incomplete CircleCI configuration (set -circleci-key, -circleci-project, and -circleci-branch\n")
}
}
func main() {
parseFlags()
// If JUnit update mode is enabled, handle it separately and exit
if junitUpdateOldGlob != "" || junitUpdateNewGlob != "" || junitUpdateOutPath != "" {
updateJUnitTimings()
return
}
// Validate split parameters (not needed in update mode)
if splitTotal == 0 || splitIndex < 0 || splitIndex > splitTotal {
fatalMsg("-split-index and -split-total (and environment variables) are missing or invalid\n")
}
// We are not using filepath.Glob,
// because it doesn't support '**' (to match all files in all nested directories)
currentFiles, err := doublestar.Glob(testFilePattern)
if err != nil {
fatalMsg("failed to enumerate current file set: %v", err)
}
currentFileSet := make(map[string]bool)
for _, file := range currentFiles {
currentFileSet[file] = true
}
if excludeFilePattern != "" {
excludedFiles, err := doublestar.Glob(excludeFilePattern)
if err != nil {
fatalMsg("failed to enumerate excluded file set: %v", err)
}
for _, file := range excludedFiles {
delete(currentFileSet, file)
}
}
fileTimes := make(map[string]float64)
if useLineCount {
estimateFileTimesByLineCount(currentFileSet, fileTimes)
} else if useJUnitXML {
getFileTimesFromJUnitXML(fileTimes)
} else if useCircleCI {
getFileTimesFromCircleCI(fileTimes)
}
removeDeletedFiles(fileTimes, currentFileSet)
addNewFiles(fileTimes, currentFileSet)
var biases []float64
if bias != "" {
biases, err = parseBias(bias, splitTotal)
if err != nil {
fatalMsg("failed to parse bias: %v", err)
}
} else {
biases = make([]float64, splitTotal)
}
buckets, bucketTimes := splitFiles(biases, fileTimes, splitTotal)
if useCircleCI || useJUnitXML {
printMsg("expected test time: %0.1fs\n", bucketTimes[splitIndex])
}
fmt.Println(strings.Join(buckets[splitIndex], " "))
}
func parseBias(bias string, splitTotal int) ([]float64, error) {
declarations := strings.Split(bias, ",")
biases := make([]float64, splitTotal)
for _, declaration := range declarations {
parts := strings.Split(declaration, "=")
if len(parts) != 2 {
return nil, fmt.Errorf("not a valid bias declaration: %s", declaration)
}
index, err := strconv.Atoi(parts[0])
if err != nil {
return nil, fmt.Errorf("failed to parse bias index: %w", err)
}
if index < 0 || index >= splitTotal {
return nil, fmt.Errorf("bias index is not within the split number: %d", index)
}
biasSeconds, err := strconv.ParseFloat(parts[1], 64)
if err != nil {
return nil, fmt.Errorf("failed to parse bias time: %w", err)
}
biases[index] = biasSeconds
}
return biases, nil
}