-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmain.go
344 lines (313 loc) · 8.83 KB
/
main.go
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"context"
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"os/exec"
"os/signal"
"regexp"
"strings"
"github.com/mknyszek/goswarm/gomote"
"golang.org/x/sync/errgroup"
)
var (
instances uint
clean cleanMode = cleanOff
verbosity uint
deflakes uint
env stringSetVar
errMatch string
keepGoing bool
)
func init() {
flag.UintVar(&instances, "i", 10, "number of instances to run in parallel")
flag.Var(&env, "e", "an environment variable to use on the gomote of the form VAR=value, may be specified multiple times")
flag.StringVar(&errMatch, "match", "", "stop only if a failure's output matches this regexp")
flag.Var(&clean, "clean", "off=do not clean up instances, start=clean up existing gomotes of the provided instance type at startup, exit=clean up instances created by goswarm on exit")
flag.UintVar(&verbosity, "v", 2, "verbosity level: 0 is quiet, 2 is the maximum")
flag.UintVar(&deflakes, "deflake", 5, "number of times to retry basic gomote operations")
flag.BoolVar(&keepGoing, "keep-going", false, "keep testing on remaining instances after finding a matching failure")
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "goswarm creates a pool of gomotes and executes a command on them until one of them fails.\n\n")
fmt.Fprintf(flag.CommandLine.Output(), "Note that goswarm does not tear down gomotes.\n\n")
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [instance type] [command]\n", os.Args[0])
flag.PrintDefaults()
}
}
type stringSetVar []string
func (s *stringSetVar) String() string {
return strings.Join(*s, ", ")
}
func (s *stringSetVar) Set(c string) error {
*s = append(*s, c)
return nil
}
type cleanMode string
const (
cleanOff cleanMode = "off" // do not clean up.
cleanStart cleanMode = "start" // clean up old instances before starting.
cleanExit cleanMode = "exit" // clean up instances created by goswarm on exit.
)
func (c *cleanMode) String() string {
if c == nil {
return ""
}
return string(*c)
}
func (c *cleanMode) Set(s string) error {
switch cleanMode(s) {
case cleanOff:
*c = cleanOff
case cleanStart:
*c = cleanStart
case cleanExit:
*c = cleanExit
default:
return fmt.Errorf("unknown clean mode %q", s)
}
return nil
}
func main() {
flag.Parse()
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
func validateInstanceType(ctx context.Context, typ string) error {
typs, err := gomote.InstanceTypes(ctx)
if err != nil {
return err
}
for _, it := range typs {
if typ == it {
return nil
}
}
return fmt.Errorf("invalid instance type: %s", typ)
}
func cleanUpInstances(ctx context.Context, typ string) error {
insts, err := gomote.List(ctx)
if err != nil {
return err
}
for _, inst := range insts {
if inst.Type != typ {
continue
}
log.Printf("Destroying instance %s...", inst.Name)
if err := gomote.Destroy(ctx, inst.Name); err != nil {
return err
}
}
return nil
}
var errStop = errors.New("stop execution due to matching failure")
func run() error {
// No arguments is always wrong.
if flag.NArg() == 0 {
return fmt.Errorf("expected an instance type, followed by a command")
}
if verbosity == 0 {
// Quiet mode.
log.SetOutput(io.Discard)
}
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
// We have at least an instance type, so validate that
// and clean up instances if asked.
typ := flag.Arg(0)
if err := validateInstanceType(ctx, typ); err != nil {
return err
}
if clean == cleanStart {
if err := cleanUpInstances(ctx, typ); err != nil {
return fmt.Errorf("cleaning up instances: %v", err)
}
}
if flag.NArg() == 1 {
// No command, so nothing more to do.
// Surface an error if -clean was not passed.
if clean != cleanStart {
return fmt.Errorf("expected a command")
}
return nil
}
var errRegexp *regexp.Regexp
if errMatch != "" {
r, err := regexp.Compile(errMatch)
if err != nil {
return fmt.Errorf("compiling regexp: %v", err)
}
errRegexp = r
}
eg, ctx := errgroup.WithContext(ctx)
for i := 0; i < int(instances); i++ {
eg.Go(func() error {
return runOneInstance(ctx, typ, errRegexp)
})
}
err := eg.Wait()
if err == errStop {
err = nil
}
return err
}
type testStatus int
const (
testExecutionError testStatus = iota // tests did not run due to external error
testPass // tests passed
testFailUnmatched // tests failed but did not match regexp
testFailMatched // test failed and match regexp
)
// Run testing in a single instance.
//
// Returns errStop to halt all testing.
func runOneInstance(ctx context.Context, typ string, errRegexp *regexp.Regexp) error {
// Create instance.
var inst string
err := retry(func() error {
i, err := gomote.Create(ctx, typ)
inst = i
return err
}, deflakes)
if err != nil {
log.Printf("Aborting instance creation due to too many errors: %v", unwrap(err))
return nil
}
log.Printf("Created instance %s...", inst)
if clean == cleanExit {
defer func() {
log.Printf("Destroying instance %s...", inst)
if err := gomote.Destroy(context.Background(), inst); err != nil {
log.Printf("Error destroying instance %s: %v", inst, err)
}
}()
}
// Push GOROOT to instance.
// N.B. GOROOT is implicitly passed to gomote via the environment.
err = retry(func() error { return gomote.Push(ctx, inst) }, deflakes)
if err != nil {
log.Printf("Giving up on %s due to too many errors while pushing: %v", inst, unwrap(err))
return nil
}
log.Printf("Pushed to %s.", inst)
// Run command in a loop.
cmd := flag.Args()[1:]
for {
status, err := runOneTest(ctx, inst, cmd, errRegexp)
if err != nil {
return err
}
switch status {
case testPass, testFailUnmatched:
continue
case testFailMatched:
if keepGoing {
// Stop testing on this instance, but return
// nil so others keep testing.
return nil
}
// Abort all testing and exit.
return errStop
default:
panic(fmt.Sprintf("unexpected status %s", status))
}
}
}
// runOneTest runs cmd on inst. It returns an error if there is a matching
// failure (or there is an internal gomote issue).
//
// If the test runs, the test status and a nil error are returned. Otherwise
// testExecutionError is returned with the error.
func runOneTest(ctx context.Context, inst string, cmd []string, errRegexp *regexp.Regexp) (testStatus, error) {
log.Printf("Running command on %s.", inst)
results, err := gomote.Run(ctx, inst, env, cmd...)
select {
case <-ctx.Done():
// Context canceled. Return nil.
return testExecutionError, context.Canceled
default:
}
if err == nil {
return testPass, nil
}
_, ok := err.(*exec.ExitError)
if !ok {
// Failed in some other way.
return testExecutionError, err
}
if bytes.Contains(results, []byte(inst)) {
return testExecutionError, fmt.Errorf("lost builder %q", inst)
}
if errRegexp != nil && !errRegexp.Match(results) {
// Only consider failures that match the regexp
// "real" failures. But if our verbosity level
// is high enough, dump the failure anyway.
f, err := os.CreateTemp("", inst)
if err != nil {
return testExecutionError, fmt.Errorf("failed to write output from %s to temp file: %w", inst, err)
}
defer f.Close()
if _, err := f.Write(results); err != nil {
return testExecutionError, fmt.Errorf("Failed to write output from %s to %s: %w", inst, f.Name(), err)
}
f.Close()
if verbosity < 2 {
log.Printf("Unmatched failure on %s.", inst)
} else {
log.Printf("Unmatched failure on %s:\n%s", inst, string(results))
}
log.Printf("Wrote output of %s to %s.", inst, f.Name())
return testFailUnmatched, nil
}
log.Printf("Discovered failure on %s.", inst)
outName := inst + ".out"
if err := os.WriteFile(outName, results, 0o644); err != nil {
log.Printf("Dumping output from %s:\n%s", inst, string(results))
return testExecutionError, fmt.Errorf("failed to write output: %v\n", err)
}
log.Printf("Wrote output of %s to %s.", inst, outName)
tarName := inst + ".tar.gz"
f, err := os.Create(tarName)
if err != nil {
return testExecutionError, fmt.Errorf("failed to create archive for %s: %v", inst, err)
}
defer f.Close()
if err := gomote.Get(ctx, inst, f); err != nil {
return testExecutionError, fmt.Errorf("failed to download archive for %s: %v", inst, err)
}
log.Printf("Downloaded archive of %s to %s.", inst, tarName)
return testFailMatched, nil
}
func retry(f func() error, retries uint) error {
i := 0
loop:
err := f()
if err == nil {
return nil
}
i++
if i < int(retries) {
goto loop
}
return err
}
func unwrap(err error) error {
r, ok := err.(*exec.ExitError)
if !ok {
return err
}
if len(r.Stderr) == 0 {
return fmt.Errorf("%v: <no output>", err)
}
return fmt.Errorf("%v: <stderr>: %s", err, string(r.Stderr))
}