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  }