| // Copyright 2017 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. |
| |
| //go:build go1.16 && (linux || darwin) |
| // +build go1.16 |
| // +build linux darwin |
| |
| package main |
| |
| import ( |
| "bufio" |
| "bytes" |
| "context" |
| "net/http" |
| "net/http/httptest" |
| "regexp" |
| "strings" |
| "testing" |
| "time" |
| |
| "golang.org/x/build/internal/coordinator/remote" |
| "golang.org/x/build/internal/coordinator/schedule" |
| ) |
| |
| var durationTests = []struct { |
| in time.Duration |
| want string |
| }{ |
| {10*time.Second + 555*time.Millisecond, "10.6s"}, |
| {10*time.Second + 500*time.Millisecond, "10.5s"}, |
| {10*time.Second + 499*time.Millisecond, "10.5s"}, |
| {10*time.Second + 401*time.Millisecond, "10.4s"}, |
| {9*time.Second + 401*time.Millisecond, "9.4s"}, |
| {9*time.Second + 456*time.Millisecond, "9.46s"}, |
| {9*time.Second + 445*time.Millisecond, "9.45s"}, |
| {1 * time.Second, "1s"}, |
| {859*time.Millisecond + 445*time.Microsecond, "859.4ms"}, |
| {859*time.Millisecond + 460*time.Microsecond, "859.5ms"}, |
| } |
| |
| func TestFriendlyDuration(t *testing.T) { |
| for _, tt := range durationTests { |
| got := friendlyDuration(tt.in) |
| if got != tt.want { |
| t.Errorf("friendlyDuration(%v): got %s, want %s", tt.in, got, tt.want) |
| } |
| } |
| } |
| |
| func TestHandleStatus_HealthFormatting(t *testing.T) { |
| ctx, cancel := context.WithCancel(context.Background()) |
| defer cancel() |
| addHealthCheckers(ctx, http.NewServeMux(), nil) |
| addHealthChecker(http.NewServeMux(), &healthChecker{ |
| ID: "allgood", |
| Title: "All Good Test", |
| Check: func(*checkWriter) {}, |
| }) |
| addHealthChecker(http.NewServeMux(), &healthChecker{ |
| ID: "errortest", |
| Title: "Error Test", |
| Check: func(cw *checkWriter) { |
| cw.info("test-info") |
| cw.warn("test-warn") |
| cw.error("test-error") |
| }, |
| }) |
| |
| statusMu.Lock() |
| for k := range status { |
| delete(status, k) |
| } |
| for k := range tries { |
| delete(tries, k) |
| } |
| tryList = nil |
| statusMu.Unlock() |
| |
| rec := httptest.NewRecorder() |
| req := httptest.NewRequest("GET", "/", nil) |
| setSessionPool(remote.NewSessionPool(ctx)) |
| handleStatus(rec, req) |
| const pre = "<h2 id=health>Health" |
| const suf = "<h2 id=trybots>Active Trybot Runs" |
| got := rec.Body.String() |
| if i := strings.Index(got, pre); i != -1 { |
| got = got[i+len(pre):] |
| } else { |
| t.Fatalf("output didn't contain %q: %s", pre, got) |
| } |
| if i := strings.Index(got, suf); i != -1 { |
| got = got[:i] |
| } else { |
| t.Fatalf("output didn't contain %q: %s", suf, got) |
| } |
| for _, sub := range []string{ |
| `<a href="/status/macs">MacStadium Mac VMs</a> [`, |
| `<li>macstadium_host06a not yet connected</li>`, |
| `<a href="/status/allgood">All Good Test</a>: ok`, |
| `<li>test-info</li>`, |
| `<li><span style='color: orange'>test-warn</span></li>`, |
| `<li><span style='color: red'><b>test-error</b></span></li>`, |
| } { |
| if !strings.Contains(got, sub) { |
| t.Errorf("didn't find substring %q in output", sub) |
| } |
| } |
| if t.Failed() { |
| t.Logf("Got: %s", got) |
| } |
| } |
| |
| func TestStatusSched(t *testing.T) { |
| data := statusData{ |
| SchedState: schedule.SchedulerState{ |
| HostTypes: []schedule.SchedulerHostState{ |
| { |
| HostType: "no-special", |
| LastProgress: 5 * time.Minute, |
| Total: schedule.SchedulerWaitingState{Count: 10, Newest: 5 * time.Minute, Oldest: 61 * time.Minute}, |
| Regular: schedule.SchedulerWaitingState{Count: 1}, |
| }, |
| { |
| HostType: "with-try", |
| LastProgress: 5 * time.Minute, |
| Total: schedule.SchedulerWaitingState{Count: 3}, |
| Try: schedule.SchedulerWaitingState{Count: 1, Newest: 2 * time.Second, Oldest: 5 * time.Minute}, |
| Regular: schedule.SchedulerWaitingState{Count: 2}, |
| }, |
| { |
| HostType: "gomote-and-try", |
| Total: schedule.SchedulerWaitingState{Count: 6, Newest: 3 * time.Second, Oldest: 4 * time.Minute}, |
| Gomote: schedule.SchedulerWaitingState{Count: 2, Newest: 3 * time.Second, Oldest: 4 * time.Minute}, |
| Try: schedule.SchedulerWaitingState{Count: 1}, |
| Regular: schedule.SchedulerWaitingState{Count: 3}, |
| }, |
| }, |
| }, |
| } |
| buf := new(bytes.Buffer) |
| if err := statusTmpl.Execute(buf, data); err != nil { |
| t.Fatal(err) |
| } |
| |
| wantMatch := []string{ |
| `(?s)<li><b>no-special</b>: 10 waiting \(oldest 1h1m0s, newest 5m0s, progress 5m0s\)\s+</li>`, |
| `<li>try: 1 \(oldest 5m0s, newest 2s\)</li>`, |
| `(?s)<li><b>gomote-and-try</b>: 6 waiting \(oldest 4m0s, newest 3s\)`, // checks for no ", progress" |
| `<li>gomote: 2 \(oldest 4m0s, newest 3s\)</li>`, |
| } |
| for _, rx := range wantMatch { |
| matched, err := regexp.Match(rx, buf.Bytes()) |
| if err != nil { |
| t.Errorf("error matching %#q: %v", rx, err) |
| continue |
| } |
| if !matched { |
| t.Errorf("didn't match %#q", rx) |
| } |
| } |
| if t.Failed() { |
| t.Logf("Got: %s", section(buf.Bytes(), "sched")) |
| } |
| } |
| |
| // section returns the section of the status HTML page that starts |
| // with <h2 id=$section> and ends with any other ^<h2 line. |
| func section(in []byte, section string) []byte { |
| start := "<h2 id=" + section + ">" |
| bs := bufio.NewScanner(bytes.NewReader(in)) |
| var out bytes.Buffer |
| var foundStart bool |
| for bs.Scan() { |
| if foundStart { |
| if strings.HasPrefix(bs.Text(), "<h2") { |
| break |
| } |
| } else { |
| if strings.HasPrefix(bs.Text(), start) { |
| foundStart = true |
| } else { |
| continue |
| } |
| } |
| out.Write(bs.Bytes()) |
| out.WriteByte('\n') |
| } |
| return out.Bytes() |
| } |