github.com/weaveworks/common@v0.0.0-20230728070032-dd9e68f319d5/tools/runner/runner.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "net/url" 9 "os" 10 "os/exec" 11 "sort" 12 "strconv" 13 "strings" 14 "sync" 15 "time" 16 17 "github.com/mgutz/ansi" 18 "github.com/weaveworks/common/mflag" 19 ) 20 21 const ( 22 defaultSchedulerHost = "positive-cocoa-90213.appspot.com" 23 jsonContentType = "application/json" 24 ) 25 26 var ( 27 start = ansi.ColorCode("black+ub") 28 fail = ansi.ColorCode("red+b") 29 succ = ansi.ColorCode("green+b") 30 reset = ansi.ColorCode("reset") 31 32 schedulerHost = defaultSchedulerHost 33 useScheduler = false 34 runParallel = false 35 verbose = false 36 timeout = 180 // In seconds. Three minutes ought to be enough for any test 37 38 consoleLock = sync.Mutex{} 39 ) 40 41 type test struct { 42 name string 43 hosts int 44 } 45 46 type schedule struct { 47 Tests []string `json:"tests"` 48 } 49 50 type result struct { 51 test 52 errored bool 53 hosts []string 54 } 55 56 type tests []test 57 58 func (ts tests) Len() int { return len(ts) } 59 func (ts tests) Swap(i, j int) { ts[i], ts[j] = ts[j], ts[i] } 60 func (ts tests) Less(i, j int) bool { 61 if ts[i].hosts != ts[j].hosts { 62 return ts[i].hosts < ts[j].hosts 63 } 64 return ts[i].name < ts[j].name 65 } 66 67 func (ts *tests) pick(available int) (test, bool) { 68 // pick the first test that fits in the available hosts 69 for i, test := range *ts { 70 if test.hosts <= available { 71 *ts = append((*ts)[:i], (*ts)[i+1:]...) 72 return test, true 73 } 74 } 75 76 return test{}, false 77 } 78 79 func (t test) run(hosts []string) bool { 80 consoleLock.Lock() 81 fmt.Printf("%s>>> Running %s on %s%s\n", start, t.name, hosts, reset) 82 consoleLock.Unlock() 83 84 var out bytes.Buffer 85 86 cmd := exec.Command(t.name) 87 cmd.Env = os.Environ() 88 cmd.Stdout = &out 89 cmd.Stderr = &out 90 91 // replace HOSTS in env 92 for i, env := range cmd.Env { 93 if strings.HasPrefix(env, "HOSTS") { 94 cmd.Env[i] = fmt.Sprintf("HOSTS=%s", strings.Join(hosts, " ")) 95 break 96 } 97 } 98 99 start := time.Now() 100 var err error 101 102 c := make(chan error, 1) 103 go func() { c <- cmd.Run() }() 104 select { 105 case err = <-c: 106 case <-time.After(time.Duration(timeout) * time.Second): 107 err = fmt.Errorf("timed out") 108 } 109 110 duration := float64(time.Since(start)) / float64(time.Second) 111 112 consoleLock.Lock() 113 if err != nil { 114 fmt.Printf("%s>>> Test %s finished after %0.1f secs with error: %v%s\n", fail, t.name, duration, err, reset) 115 } else { 116 fmt.Printf("%s>>> Test %s finished with success after %0.1f secs%s\n", succ, t.name, duration, reset) 117 } 118 if err != nil || verbose { 119 fmt.Print(out.String()) 120 fmt.Println() 121 } 122 consoleLock.Unlock() 123 124 if err != nil && useScheduler { 125 updateScheduler(t.name, duration) 126 } 127 128 return err != nil 129 } 130 131 func updateScheduler(test string, duration float64) { 132 req := &http.Request{ 133 Method: "POST", 134 Host: schedulerHost, 135 URL: &url.URL{ 136 Opaque: fmt.Sprintf("/record/%s/%0.2f", url.QueryEscape(test), duration), 137 Scheme: "http", 138 Host: schedulerHost, 139 }, 140 Close: true, 141 } 142 if resp, err := http.DefaultClient.Do(req); err != nil { 143 fmt.Printf("Error updating scheduler: %v\n", err) 144 } else { 145 resp.Body.Close() 146 } 147 } 148 149 func getSchedule(tests []string) ([]string, error) { 150 var ( 151 userName = os.Getenv("CIRCLE_PROJECT_USERNAME") 152 project = os.Getenv("CIRCLE_PROJECT_REPONAME") 153 buildNum = os.Getenv("CIRCLE_BUILD_NUM") 154 testRun = userName + "-" + project + "-integration-" + buildNum 155 shardCount = os.Getenv("CIRCLE_NODE_TOTAL") 156 shardID = os.Getenv("CIRCLE_NODE_INDEX") 157 requestBody = &bytes.Buffer{} 158 ) 159 if err := json.NewEncoder(requestBody).Encode(schedule{tests}); err != nil { 160 return []string{}, err 161 } 162 url := fmt.Sprintf("http://%s/schedule/%s/%s/%s", schedulerHost, testRun, shardCount, shardID) 163 resp, err := http.Post(url, jsonContentType, requestBody) 164 if err != nil { 165 return []string{}, err 166 } 167 var sched schedule 168 if err := json.NewDecoder(resp.Body).Decode(&sched); err != nil { 169 return []string{}, err 170 } 171 return sched.Tests, nil 172 } 173 174 func getTests(testNames []string) (tests, error) { 175 var err error 176 if useScheduler { 177 testNames, err = getSchedule(testNames) 178 if err != nil { 179 return tests{}, err 180 } 181 } 182 tests := tests{} 183 for _, name := range testNames { 184 parts := strings.Split(strings.TrimSuffix(name, "_test.sh"), "_") 185 numHosts, err := strconv.Atoi(parts[len(parts)-1]) 186 if err != nil { 187 numHosts = 1 188 } 189 tests = append(tests, test{name, numHosts}) 190 fmt.Printf("Test %s needs %d hosts\n", name, numHosts) 191 } 192 return tests, nil 193 } 194 195 func summary(tests, failed tests) { 196 if len(failed) > 0 { 197 fmt.Printf("%s>>> Ran %d tests, %d failed%s\n", fail, len(tests), len(failed), reset) 198 for _, test := range failed { 199 fmt.Printf("%s>>> Fail %s%s\n", fail, test.name, reset) 200 } 201 } else { 202 fmt.Printf("%s>>> Ran %d tests, all succeeded%s\n", succ, len(tests), reset) 203 } 204 } 205 206 func parallel(ts tests, hosts []string) bool { 207 testsCopy := ts 208 sort.Sort(sort.Reverse(ts)) 209 resultsChan := make(chan result) 210 outstanding := 0 211 failed := tests{} 212 for len(ts) > 0 || outstanding > 0 { 213 // While we have some free hosts, try and schedule 214 // a test on them 215 for len(hosts) > 0 { 216 test, ok := ts.pick(len(hosts)) 217 if !ok { 218 break 219 } 220 testHosts := hosts[:test.hosts] 221 hosts = hosts[test.hosts:] 222 223 go func() { 224 errored := test.run(testHosts) 225 resultsChan <- result{test, errored, testHosts} 226 }() 227 outstanding++ 228 } 229 230 // Otherwise, wait for the test to finish and return 231 // the hosts to the pool 232 result := <-resultsChan 233 hosts = append(hosts, result.hosts...) 234 outstanding-- 235 if result.errored { 236 failed = append(failed, result.test) 237 } 238 } 239 summary(testsCopy, failed) 240 return len(failed) > 0 241 } 242 243 func sequential(ts tests, hosts []string) bool { 244 failed := tests{} 245 for _, test := range ts { 246 if test.run(hosts) { 247 failed = append(failed, test) 248 } 249 } 250 summary(ts, failed) 251 return len(failed) > 0 252 } 253 254 func main() { 255 mflag.BoolVar(&useScheduler, []string{"scheduler"}, false, "Use scheduler to distribute tests across shards") 256 mflag.BoolVar(&runParallel, []string{"parallel"}, false, "Run tests in parallel on hosts where possible") 257 mflag.BoolVar(&verbose, []string{"v"}, false, "Print output from all tests (Also enabled via DEBUG=1)") 258 mflag.StringVar(&schedulerHost, []string{"scheduler-host"}, defaultSchedulerHost, "Hostname of scheduler.") 259 mflag.IntVar(&timeout, []string{"timeout"}, 180, "Max time to run one test for, in seconds") 260 mflag.Parse() 261 262 if len(os.Getenv("DEBUG")) > 0 { 263 verbose = true 264 } 265 266 testArgs := mflag.Args() 267 tests, err := getTests(testArgs) 268 if err != nil { 269 fmt.Printf("Error parsing tests: %v (%v)\n", err, testArgs) 270 os.Exit(1) 271 } 272 273 hosts := strings.Fields(os.Getenv("HOSTS")) 274 maxHosts := len(hosts) 275 if maxHosts == 0 { 276 fmt.Print("No HOSTS specified.\n") 277 os.Exit(1) 278 } 279 280 var errored bool 281 if runParallel { 282 errored = parallel(tests, hosts) 283 } else { 284 errored = sequential(tests, hosts) 285 } 286 287 if errored { 288 os.Exit(1) 289 } 290 }