golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/coordinator/status_test.go (about) 1 // Copyright 2017 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 //go:build linux || darwin 6 7 package main 8 9 import ( 10 "bufio" 11 "bytes" 12 "context" 13 "net/http" 14 "net/http/httptest" 15 "regexp" 16 "strings" 17 "testing" 18 "time" 19 20 "golang.org/x/build/internal/coordinator/remote" 21 "golang.org/x/build/internal/coordinator/schedule" 22 ) 23 24 var durationTests = []struct { 25 in time.Duration 26 want string 27 }{ 28 {10*time.Second + 555*time.Millisecond, "10.6s"}, 29 {10*time.Second + 500*time.Millisecond, "10.5s"}, 30 {10*time.Second + 499*time.Millisecond, "10.5s"}, 31 {10*time.Second + 401*time.Millisecond, "10.4s"}, 32 {9*time.Second + 401*time.Millisecond, "9.4s"}, 33 {9*time.Second + 456*time.Millisecond, "9.46s"}, 34 {9*time.Second + 445*time.Millisecond, "9.45s"}, 35 {1 * time.Second, "1s"}, 36 {859*time.Millisecond + 445*time.Microsecond, "859.4ms"}, 37 {859*time.Millisecond + 460*time.Microsecond, "859.5ms"}, 38 } 39 40 func TestFriendlyDuration(t *testing.T) { 41 for _, tt := range durationTests { 42 got := friendlyDuration(tt.in) 43 if got != tt.want { 44 t.Errorf("friendlyDuration(%v): got %s, want %s", tt.in, got, tt.want) 45 } 46 } 47 } 48 49 func TestHandleStatus_HealthFormatting(t *testing.T) { 50 ctx, cancel := context.WithCancel(context.Background()) 51 defer cancel() 52 addHealthCheckers(ctx, http.NewServeMux(), nil) 53 addHealthChecker(http.NewServeMux(), &healthChecker{ 54 ID: "allgood", 55 Title: "All Good Test", 56 Check: func(*checkWriter) {}, 57 }) 58 addHealthChecker(http.NewServeMux(), &healthChecker{ 59 ID: "errortest", 60 Title: "Error Test", 61 Check: func(cw *checkWriter) { 62 cw.info("test-info") 63 cw.warn("test-warn") 64 cw.error("test-error") 65 }, 66 }) 67 68 statusMu.Lock() 69 for k := range status { 70 delete(status, k) 71 } 72 for k := range tries { 73 delete(tries, k) 74 } 75 tryList = nil 76 statusMu.Unlock() 77 78 rec := httptest.NewRecorder() 79 req := httptest.NewRequest("GET", "/", nil) 80 setSessionPool(remote.NewSessionPool(ctx)) 81 handleStatus(rec, req) 82 const pre = "<h2 id=health>Health" 83 const suf = "<h2 id=trybots>Active Trybot Runs" 84 got := rec.Body.String() 85 if i := strings.Index(got, pre); i != -1 { 86 got = got[i+len(pre):] 87 } else { 88 t.Fatalf("output didn't contain %q: %s", pre, got) 89 } 90 if i := strings.Index(got, suf); i != -1 { 91 got = got[:i] 92 } else { 93 t.Fatalf("output didn't contain %q: %s", suf, got) 94 } 95 for _, sub := range []string{ 96 `<a href="/status/allgood">All Good Test</a>: ok`, 97 `<li>test-info</li>`, 98 `<li><span style='color: orange'>test-warn</span></li>`, 99 `<li><span style='color: red'><b>test-error</b></span></li>`, 100 } { 101 if !strings.Contains(got, sub) { 102 t.Errorf("didn't find substring %q in output", sub) 103 } 104 } 105 if t.Failed() { 106 t.Logf("Got: %s", got) 107 } 108 } 109 110 func TestStatusSched(t *testing.T) { 111 data := statusData{ 112 SchedState: schedule.SchedulerState{ 113 HostTypes: []schedule.SchedulerHostState{ 114 { 115 HostType: "no-special", 116 LastProgress: 5 * time.Minute, 117 Total: schedule.SchedulerWaitingState{Count: 10, Newest: 5 * time.Minute, Oldest: 61 * time.Minute}, 118 Regular: schedule.SchedulerWaitingState{Count: 1}, 119 }, 120 { 121 HostType: "with-try", 122 LastProgress: 5 * time.Minute, 123 Total: schedule.SchedulerWaitingState{Count: 3}, 124 Try: schedule.SchedulerWaitingState{Count: 1, Newest: 2 * time.Second, Oldest: 5 * time.Minute}, 125 Regular: schedule.SchedulerWaitingState{Count: 2}, 126 }, 127 { 128 HostType: "gomote-and-try", 129 Total: schedule.SchedulerWaitingState{Count: 6, Newest: 3 * time.Second, Oldest: 4 * time.Minute}, 130 Gomote: schedule.SchedulerWaitingState{Count: 2, Newest: 3 * time.Second, Oldest: 4 * time.Minute}, 131 Try: schedule.SchedulerWaitingState{Count: 1}, 132 Regular: schedule.SchedulerWaitingState{Count: 3}, 133 }, 134 }, 135 }, 136 } 137 buf := new(bytes.Buffer) 138 if err := statusTmpl.Execute(buf, data); err != nil { 139 t.Fatal(err) 140 } 141 142 wantMatch := []string{ 143 `(?s)<li><b>no-special</b>: 10 waiting \(oldest 1h1m0s, newest 5m0s, progress 5m0s\)\s+</li>`, 144 `<li>try: 1 \(oldest 5m0s, newest 2s\)</li>`, 145 `(?s)<li><b>gomote-and-try</b>: 6 waiting \(oldest 4m0s, newest 3s\)`, // checks for no ", progress" 146 `<li>gomote: 2 \(oldest 4m0s, newest 3s\)</li>`, 147 } 148 for _, rx := range wantMatch { 149 matched, err := regexp.Match(rx, buf.Bytes()) 150 if err != nil { 151 t.Errorf("error matching %#q: %v", rx, err) 152 continue 153 } 154 if !matched { 155 t.Errorf("didn't match %#q", rx) 156 } 157 } 158 if t.Failed() { 159 t.Logf("Got: %s", section(buf.Bytes(), "sched")) 160 } 161 } 162 163 // section returns the section of the status HTML page that starts 164 // with <h2 id=$section> and ends with any other ^<h2 line. 165 func section(in []byte, section string) []byte { 166 start := "<h2 id=" + section + ">" 167 bs := bufio.NewScanner(bytes.NewReader(in)) 168 var out bytes.Buffer 169 var foundStart bool 170 for bs.Scan() { 171 if foundStart { 172 if strings.HasPrefix(bs.Text(), "<h2") { 173 break 174 } 175 } else { 176 if strings.HasPrefix(bs.Text(), start) { 177 foundStart = true 178 } else { 179 continue 180 } 181 } 182 out.Write(bs.Bytes()) 183 out.WriteByte('\n') 184 } 185 return out.Bytes() 186 }