golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/coordinator/coordinator_test.go (about)

     1  // Copyright 2015 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  	"bytes"
    11  	"io"
    12  	"log"
    13  	"net/http"
    14  	"net/http/httptest"
    15  	"reflect"
    16  	"strings"
    17  	"testing"
    18  	"time"
    19  
    20  	"golang.org/x/build/buildenv"
    21  	"golang.org/x/build/gerrit"
    22  	"golang.org/x/build/internal/buildgo"
    23  	"golang.org/x/build/internal/coordinator/pool"
    24  	"golang.org/x/build/maintner/maintnerd/apipb"
    25  )
    26  
    27  type Seconds float64
    28  
    29  func (s Seconds) Duration() time.Duration {
    30  	return time.Duration(float64(s) * float64(time.Second))
    31  }
    32  
    33  var fixedTestDuration = map[string]Seconds{
    34  	"go_test:a": 1,
    35  	"go_test:b": 1.5,
    36  	"go_test:c": 2,
    37  	"go_test:d": 2.50,
    38  	"go_test:e": 3,
    39  	"go_test:f": 3.5,
    40  	"go_test:g": 4,
    41  	"go_test:h": 4.5,
    42  	"go_test:i": 5,
    43  	"go_test:j": 5.5,
    44  	"go_test:k": 6.5,
    45  }
    46  
    47  func TestPartitionGoTests(t *testing.T) {
    48  	var in []string
    49  	for name := range fixedTestDuration {
    50  		in = append(in, name)
    51  	}
    52  	testDuration := func(builder, testName string) time.Duration {
    53  		if s, ok := fixedTestDuration[testName]; ok {
    54  			return s.Duration()
    55  		}
    56  		return 3 * time.Second
    57  	}
    58  	sets := partitionGoTests(testDuration, "", in)
    59  	want := [][]string{
    60  		{"go_test:a", "go_test:b", "go_test:c", "go_test:d", "go_test:e"},
    61  		{"go_test:f", "go_test:g"},
    62  		{"go_test:h", "go_test:i"},
    63  		{"go_test:j"},
    64  		{"go_test:k"},
    65  	}
    66  	if !reflect.DeepEqual(sets, want) {
    67  		t.Errorf(" got: %v\nwant: %v", sets, want)
    68  	}
    69  }
    70  
    71  func TestTryStatusJSON(t *testing.T) {
    72  	testCases := []struct {
    73  		desc   string
    74  		method string
    75  		ts     *trySet
    76  		tss    trySetState
    77  		status int
    78  		body   string
    79  	}{
    80  		{
    81  			"pre-flight CORS header",
    82  			"OPTIONS",
    83  			nil,
    84  			trySetState{},
    85  			http.StatusOK,
    86  			``,
    87  		},
    88  		{
    89  			"nil trySet",
    90  			"GET",
    91  			nil,
    92  			trySetState{},
    93  			http.StatusNotFound,
    94  			`{"success":false,"error":"TryBot result not found (already done, invalid, or not yet discovered from Gerrit). Check Gerrit for results."}` + "\n",
    95  		},
    96  		{"non-nil trySet",
    97  			"GET",
    98  			&trySet{
    99  				tryKey: tryKey{
   100  					Commit:   "deadbeef",
   101  					ChangeID: "Ifoo",
   102  				},
   103  			},
   104  			trySetState{
   105  				builds: []*buildStatus{
   106  					{
   107  						BuilderRev: buildgo.BuilderRev{Name: "linux"},
   108  						startTime:  time.Time{}.Add(24 * time.Hour),
   109  						done:       time.Time{}.Add(48 * time.Hour),
   110  						succeeded:  true,
   111  					},
   112  					{
   113  						BuilderRev: buildgo.BuilderRev{Name: "macOS"},
   114  						startTime:  time.Time{}.Add(24 * time.Hour),
   115  					},
   116  				},
   117  			},
   118  			http.StatusOK,
   119  			`{"success":true,"payload":{"changeId":"Ifoo","commit":"deadbeef","builds":[{"name":"linux","startTime":"0001-01-02T00:00:00Z","done":true,"succeeded":true},{"name":"macOS","startTime":"0001-01-02T00:00:00Z","done":false,"succeeded":false}]}}` + "\n"},
   120  	}
   121  
   122  	for _, tc := range testCases {
   123  		t.Run(tc.desc, func(t *testing.T) {
   124  			w := httptest.NewRecorder()
   125  			r, err := http.NewRequest(tc.method, "", nil)
   126  			if err != nil {
   127  				t.Fatalf("could not create http.Request: %v", err)
   128  			}
   129  			serveTryStatusJSON(w, r, tc.ts, tc.tss)
   130  			resp := w.Result()
   131  			hdr := "Access-Control-Allow-Origin"
   132  			if got, want := resp.Header.Get(hdr), "*"; got != want {
   133  				t.Errorf("unexpected %q header: got %q; want %q", hdr, got, want)
   134  			}
   135  			if got, want := resp.StatusCode, tc.status; got != want {
   136  				t.Errorf("response status code: got %d; want %d", got, want)
   137  			}
   138  			defer resp.Body.Close()
   139  			b, err := io.ReadAll(resp.Body)
   140  			if err != nil {
   141  				t.Fatalf("io.ReadAll: %v", err)
   142  			}
   143  			if got, want := string(b), tc.body; got != want {
   144  				t.Errorf("body: got\n%v\nwant\n%v", got, want)
   145  			}
   146  		})
   147  	}
   148  }
   149  
   150  func TestStagingClusterBuilders(t *testing.T) {
   151  	// Just test that it doesn't panic:
   152  	stagingClusterBuilders()
   153  }
   154  
   155  // Test that trybot on release-branch.go1.N branch of a golang.org/x repo
   156  // uses the Go revision from Go repository's release-branch.go1.N branch.
   157  // See golang.org/issue/28891.
   158  func TestIssue28891(t *testing.T) {
   159  	testingKnobSkipBuilds = true
   160  
   161  	work := &apipb.GerritTryWorkItem{ // Based on what maintapi's GoFindTryWork does for x/net CL 258478.
   162  		Project:   "net",
   163  		Branch:    "release-branch.go1.15",
   164  		ChangeId:  "I546597cedf3715e6617babcb3b62140bf1857a27",
   165  		Commit:    "a5fa9d4b7c91aa1c3fecbeb6358ec1127b910dd6",
   166  		GoCommit:  []string{"72ccabc99449b2cb5bb1438eb90244d55f7b02f5"},
   167  		GoBranch:  []string{"release-branch.go1.15"},
   168  		GoVersion: []*apipb.MajorMinor{{Major: 1, Minor: 15}},
   169  	}
   170  	ts := newTrySet(work)
   171  	if len(ts.builds) == 0 {
   172  		t.Fatal("no builders in try set, want at least 1")
   173  	}
   174  	for i, bs := range ts.builds {
   175  		const go115Revision = "72ccabc99449b2cb5bb1438eb90244d55f7b02f5"
   176  		if bs.BuilderRev.Rev != go115Revision {
   177  			t.Errorf("build[%d]: %s: x/net on release-branch.go1.15 branch should be tested with Go 1.15, but isn't", i, bs.NameAndBranch())
   178  		}
   179  	}
   180  }
   181  
   182  // Test that trybot on release-branch.go1.N-{suffix} branch of a golang.org/x repo
   183  // uses the Go revision from Go repository's release-branch.go1.N branch.
   184  // See golang.org/issue/42127.
   185  func TestIssue42127(t *testing.T) {
   186  	testingKnobSkipBuilds = true
   187  
   188  	work := &apipb.GerritTryWorkItem{ // Based on what maintapi's GoFindTryWork does for x/net CL 264058.
   189  		Project:   "net",
   190  		Branch:    "release-branch.go1.15-bundle",
   191  		ChangeId:  "I546597cedf3715e6617babcb3b62140bf1857a27",
   192  		Commit:    "abf26a14a65b111d492067f407f32455c5b1048c",
   193  		GoCommit:  []string{"72ccabc99449b2cb5bb1438eb90244d55f7b02f5"},
   194  		GoBranch:  []string{"release-branch.go1.15"},
   195  		GoVersion: []*apipb.MajorMinor{{Major: 1, Minor: 15}},
   196  	}
   197  	ts := newTrySet(work)
   198  	if len(ts.builds) == 0 {
   199  		t.Fatal("no builders in try set, want at least 1")
   200  	}
   201  	for i, bs := range ts.builds {
   202  		const go115Revision = "72ccabc99449b2cb5bb1438eb90244d55f7b02f5"
   203  		if bs.BuilderRev.Rev != go115Revision {
   204  			t.Errorf("build[%d]: %s: x/net on release-branch.go1.15-bundle branch should be tested with Go 1.15, but isn't", i, bs.NameAndBranch())
   205  		}
   206  	}
   207  }
   208  
   209  // Tests that TryBots run on branches of the x/ repositories, other than
   210  // "master" and "release-branch.go1.N". See golang.org/issue/37512.
   211  func TestXRepoBranches(t *testing.T) {
   212  	testingKnobSkipBuilds = true
   213  
   214  	work := &apipb.GerritTryWorkItem{ // Based on what maintapi's GoFindTryWork does for x/tools CL 227356.
   215  		Project:   "tools",
   216  		Branch:    "gopls-release-branch.0.4",
   217  		ChangeId:  "Ica799fcf117bf607c0c59f41b08a78552339dc53",
   218  		Commit:    "13af72af5ccdfe6f1e75b57b02cfde3bb0a77a76",
   219  		GoCommit:  []string{"9995c6b50aa55c1cc1236d1d688929df512dad53"},
   220  		GoBranch:  []string{"master"},
   221  		GoVersion: []*apipb.MajorMinor{{Major: 1, Minor: 17}},
   222  	}
   223  	ts := newTrySet(work)
   224  	for i, bs := range ts.builds {
   225  		v := bs.NameAndBranch()
   226  		t.Logf("build[%d]: %s", i, v)
   227  	}
   228  	if len(ts.builds) < 3 {
   229  		t.Fatalf("expected at least 3 builders, got %v", len(ts.builds))
   230  	}
   231  }
   232  
   233  // Test that when there are multiple SlowBot requests on the same patch set,
   234  // the latest request is used. See golang.org/issue/42084.
   235  func TestIssue42084(t *testing.T) {
   236  	testingKnobSkipBuilds = true
   237  
   238  	work := &apipb.GerritTryWorkItem{ // Based on what maintapi's GoFindTryWork does for CL 324763. TryMessage is set later.
   239  		Project:   "go",
   240  		Branch:    "master",
   241  		ChangeId:  "I023d5208374f867552ba68b45011f7990159868f",
   242  		Commit:    "dd38fd80c3667f891dbe06bd1d8ed153c2e208da",
   243  		Version:   1,
   244  		GoCommit:  []string{"9995c6b50aa55c1cc1236d1d688929df512dad53"},
   245  		GoBranch:  []string{"master"},
   246  		GoVersion: []*apipb.MajorMinor{{Major: 1, Minor: 17}},
   247  	}
   248  
   249  	// First, determine builds without try messages. Our target SlowBot shouldn't be included.
   250  	ts := newTrySet(work)
   251  	hasLinuxArmBuilder := false
   252  	for _, bs := range ts.builds {
   253  		v := bs.NameAndBranch()
   254  		if v == "linux-arm" {
   255  			hasLinuxArmBuilder = true
   256  		}
   257  	}
   258  	if hasLinuxArmBuilder {
   259  		// This test relies on linux-arm builder not being a default
   260  		// TryBot to provide coverage for issue 42084. If the build policy
   261  		// changes, need to pick another builder to use in this test.
   262  		t.Fatal("linux-arm builder was included even without TRY= message")
   263  	}
   264  
   265  	// Next, add try messages, and check that the SlowBot is now included.
   266  	work.TryMessage = []*apipb.TryVoteMessage{
   267  		{Message: "linux", AuthorId: 1234, Version: 1},
   268  		{Message: "linux-arm", AuthorId: 1234, Version: 1},
   269  	}
   270  	ts = newTrySet(work)
   271  	hasLinuxArmBuilder = false
   272  	for i, bs := range ts.builds {
   273  		v := bs.NameAndBranch()
   274  		t.Logf("build[%d]: %s", i, v)
   275  		if v == "linux-arm-aws" {
   276  			hasLinuxArmBuilder = true
   277  		}
   278  	}
   279  	if !hasLinuxArmBuilder {
   280  		t.Error("linux-arm SlowBot was not included")
   281  	}
   282  }
   283  
   284  func TestFindWork(t *testing.T) {
   285  	if testing.Short() {
   286  		t.Skip("skipping in short mode")
   287  	}
   288  	gce := pool.NewGCEConfiguration()
   289  	buildEnv := gce.BuildEnv()
   290  	defer func(old *buildenv.Environment) { gce.SetBuildEnv(old) }(buildEnv)
   291  	gce.SetBuildEnv(buildenv.Production)
   292  	defer func() { buildgo.TestHookSnapshotExists = nil }()
   293  	buildgo.TestHookSnapshotExists = func(br *buildgo.BuilderRev) bool {
   294  		if strings.Contains(br.Name, "android") {
   295  			log.Printf("snapshot check for %+v", br)
   296  		}
   297  		return false
   298  	}
   299  
   300  	addWorkTestHook = func(work buildgo.BuilderRev, d commitDetail) {
   301  		t.Logf("Got: %v, %+v", work, d)
   302  	}
   303  	defer func() { addWorkTestHook = nil }()
   304  
   305  	err := findWork()
   306  	if err != nil {
   307  		t.Error(err)
   308  	}
   309  }
   310  
   311  func TestBuildersJSON(t *testing.T) {
   312  	rec := httptest.NewRecorder()
   313  	handleBuilders(rec, httptest.NewRequest("GET", "https://farmer.tld/builders?mode=json", nil))
   314  	res := rec.Result()
   315  	if res.Header.Get("Content-Type") != "application/json" || res.StatusCode != 200 {
   316  		var buf bytes.Buffer
   317  		res.Write(&buf)
   318  		t.Error(buf.String())
   319  	}
   320  }
   321  
   322  func TestSlowBotsFromComments(t *testing.T) {
   323  	work := &apipb.GerritTryWorkItem{
   324  		Version: 2,
   325  		TryMessage: []*apipb.TryVoteMessage{
   326  			{
   327  				Version: 1,
   328  				Message: "ios",
   329  			},
   330  			{
   331  				Version: 2,
   332  				Message: "arm64, darwin aix ",
   333  			},
   334  			{
   335  				Version: 1,
   336  				Message: "aix",
   337  			},
   338  		},
   339  	}
   340  	slowBots, invalidSlowBots := slowBotsFromComments(work)
   341  	var got []string
   342  	for _, bc := range slowBots {
   343  		got = append(got, bc.Name)
   344  	}
   345  	want := []string{"aix-ppc64", "darwin-amd64-13", "linux-arm64"}
   346  	if !reflect.DeepEqual(got, want) {
   347  		t.Errorf("mismatch:\n got: %q\nwant: %q\n", got, want)
   348  	}
   349  
   350  	if len(invalidSlowBots) > 0 {
   351  		t.Errorf("mismatch invalidSlowBots:\n got: %d\nwant: 0", len(invalidSlowBots))
   352  	}
   353  }
   354  
   355  func TestSubreposFromComments(t *testing.T) {
   356  	work := &apipb.GerritTryWorkItem{
   357  		Version: 2,
   358  		TryMessage: []*apipb.TryVoteMessage{
   359  			{
   360  				Version: 2,
   361  				Message: "x/build, x/sync x/tools, x/sync, x/tools@freebsd-amd64-race",
   362  			},
   363  		},
   364  	}
   365  	got := xReposFromComments(work)
   366  	want := map[xRepoAndBuilder]bool{
   367  		{"build", ""}:                   true,
   368  		{"sync", ""}:                    true,
   369  		{"tools", ""}:                   true,
   370  		{"tools", "freebsd-amd64-race"}: true,
   371  	}
   372  	if !reflect.DeepEqual(got, want) {
   373  		t.Errorf("mismatch:\n got: %v\nwant: %v\n", got, want)
   374  	}
   375  }
   376  
   377  func TestBuildStatusFormat(t *testing.T) {
   378  	for i, tt := range []struct {
   379  		st   *buildStatus
   380  		want string
   381  	}{
   382  		{
   383  			st: &buildStatus{
   384  				trySet: &trySet{
   385  					tryKey: tryKey{
   386  						Project: "go",
   387  					},
   388  				},
   389  				BuilderRev: buildgo.BuilderRev{
   390  					Name:    "linux-amd64",
   391  					SubName: "tools",
   392  				},
   393  				commitDetail: commitDetail{
   394  					RevBranch: "master",
   395  				},
   396  			},
   397  			want: "(x/tools) linux-amd64",
   398  		},
   399  		{
   400  			st: &buildStatus{
   401  				trySet: &trySet{
   402  					tryKey: tryKey{
   403  						Project: "tools",
   404  					},
   405  				},
   406  				BuilderRev: buildgo.BuilderRev{
   407  					Name:    "linux-amd64",
   408  					SubName: "tools",
   409  				},
   410  				commitDetail: commitDetail{
   411  					RevBranch: "release-branch.go1.15",
   412  				},
   413  			},
   414  			want: "linux-amd64 (Go 1.15.x)",
   415  		},
   416  		{
   417  			st: &buildStatus{
   418  				trySet: &trySet{
   419  					tryKey: tryKey{
   420  						Project: "go",
   421  					},
   422  				},
   423  				BuilderRev: buildgo.BuilderRev{
   424  					Name:    "linux-amd64",
   425  					SubName: "tools",
   426  				},
   427  				commitDetail: commitDetail{
   428  					RevBranch: "master",
   429  				},
   430  			},
   431  			want: "(x/tools) linux-amd64",
   432  		},
   433  		{
   434  			st: &buildStatus{
   435  				BuilderRev: buildgo.BuilderRev{
   436  					Name: "darwin-amd64-13",
   437  				},
   438  				commitDetail: commitDetail{
   439  					RevBranch: "master",
   440  				},
   441  			},
   442  			want: "darwin-amd64-13",
   443  		},
   444  		{
   445  			st: &buildStatus{
   446  				BuilderRev: buildgo.BuilderRev{
   447  					Name: "darwin-amd64-13",
   448  				},
   449  				commitDetail: commitDetail{
   450  					RevBranch: "release-branch.go1.15",
   451  				},
   452  			},
   453  			want: "darwin-amd64-13 (Go 1.15.x)",
   454  		},
   455  	} {
   456  		if got := tt.st.NameAndBranch(); got != tt.want {
   457  			t.Errorf("%d: NameAndBranch = %q; want %q", i, got, tt.want)
   458  		}
   459  	}
   460  }
   461  
   462  // listPatchSetThreadsResponse is the response to
   463  // https://go-review.googlesource.com/changes/go~master~I92400996cb051ab30e99bfffafd91ff32a1e7087/comments
   464  var listPatchSetThreadsResponse = []byte(`)]}'
   465  {"/PATCHSET_LEVEL":[{"author":{"_account_id":5976,"name":"Go Bot","email":"gobot@golang.org","tags":["SERVICE_USER"]},"tag":"autogenerated:trybots~beginning","change_message_id":"af128c803aa192eb5c191c1567713f5b123d3adb","unresolved":true,"patch_set":1,"id":"aaf7aa39_658707c2","updated":"2021-04-27 18:20:09.000000000","message":"SlowBots beginning. Status page: https://farmer.golang.org/try?commit\u003d39ad506d","commit_id":"39ad506d874d4711015184f52585b4215c9b84cc"},{"author":{"_account_id":5976,"name":"Go Bot","email":"gobot@golang.org","tags":["SERVICE_USER"]},"tag":"autogenerated:trybots~beginning","change_message_id":"a00ee30c652a61afeb5ba7657e5823b2d56159ac","unresolved":true,"patch_set":1,"id":"cb0c9011_d26d6550","updated":"2021-04-27 07:10:41.000000000","message":"SlowBots beginning. Status page: https://farmer.golang.org/try?commit\u003d39ad506d","commit_id":"39ad506d874d4711015184f52585b4215c9b84cc"},{"author":{"_account_id":6365,"name":"Bryan C. Mills","email":"bcmills@google.com"},"change_message_id":"c3dc9da7c814efe691876649d8b1acd086d34921","unresolved":false,"patch_set":1,"id":"da1249e7_bc007148","updated":"2021-04-27 07:10:22.000000000","message":"TRY\u003dlongtest","commit_id":"39ad506d874d4711015184f52585b4215c9b84cc"},{"author":{"_account_id":6365,"name":"Bryan C. Mills","email":"bcmills@google.com"},"change_message_id":"1908c5ae7cd3fa1adc7579bfb4fa0798a33dafd2","unresolved":false,"patch_set":1,"id":"043375d0_558208b0","in_reply_to":"50a54b3c_f95f3567","updated":"2021-04-27 18:32:56.000000000","message":"#41863","commit_id":"39ad506d874d4711015184f52585b4215c9b84cc"},{"author":{"_account_id":6365,"name":"Bryan C. Mills","email":"bcmills@google.com"},"change_message_id":"061f7a6231e09027e96b7c81093b32db9cd6a6f3","unresolved":false,"patch_set":1,"id":"49f73b27_b0261bc8","in_reply_to":"aaf7aa39_658707c2","updated":"2021-04-27 19:17:53.000000000","message":"Ack","commit_id":"39ad506d874d4711015184f52585b4215c9b84cc"},{"author":{"_account_id":5976,"name":"Go Bot","email":"gobot@golang.org","tags":["SERVICE_USER"]},"tag":"autogenerated:trybots~failed","change_message_id":"d7e2ff4f58b281bc3120ad79a9c5bf7c87c0ec1b","unresolved":true,"patch_set":1,"id":"50a54b3c_f95f3567","in_reply_to":"c3e462db_b5e1efca","updated":"2021-04-27 07:22:38.000000000","message":"1 of 26 SlowBots failed.\nFailed on linux-arm64-aws: https://storage.googleapis.com/go-build-log/39ad506d/linux-arm64-aws_5dc1efb9.log\n\nConsult https://build.golang.org/ to see whether they are new failures. Keep in mind that TryBots currently test *exactly* your git commit, without rebasing. If your commit\u0027s git parent is old, the failure might\u0027ve already been fixed.\n\nSlowBot builds that ran:\n* linux-amd64-longtest\n","commit_id":"39ad506d874d4711015184f52585b4215c9b84cc"},{"author":{"_account_id":5976,"name":"Go Bot","email":"gobot@golang.org","tags":["SERVICE_USER"]},"tag":"autogenerated:trybots~happy","change_message_id":"9ed90c9e9b43e3c2c4d2b6ab8e8a121686d5da88","unresolved":false,"patch_set":1,"id":"13677404_911c1149","in_reply_to":"c3e462db_b5e1efca","updated":"2021-04-27 18:46:54.000000000","message":"SlowBots are happy.\n\nSlowBot builds that ran:\n* linux-amd64-longtest\n","commit_id":"39ad506d874d4711015184f52585b4215c9b84cc"},{"author":{"_account_id":5976,"name":"Go Bot","email":"gobot@golang.org","tags":["SERVICE_USER"]},"tag":"autogenerated:trybots~progress","change_message_id":"ab963b29aa95907e30e9f6a51ef1f080807bc8ab","unresolved":true,"patch_set":1,"id":"c3e462db_b5e1efca","in_reply_to":"cb0c9011_d26d6550","updated":"2021-04-27 07:18:11.000000000","message":"Build is still in progress... Status page: https://farmer.golang.org/try?commit\u003d39ad506d\nFailed on linux-arm64-aws: https://storage.googleapis.com/go-build-log/39ad506d/linux-arm64-aws_5dc1efb9.log\nOther builds still in progress; subsequent failure notices suppressed until final report.\n\nConsult https://build.golang.org/ to see whether they are new failures. Keep in mind that TryBots currently test *exactly* your git commit, without rebasing. If your commit\u0027s git parent is old, the failure might\u0027ve already been fixed.\n","commit_id":"39ad506d874d4711015184f52585b4215c9b84cc"}]}
   466  `)
   467  
   468  func TestListPatchSetThreads(t *testing.T) {
   469  	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   470  		w.Header().Set("Content-Type", "application/json; charset=UTF-8")
   471  		w.WriteHeader(200)
   472  		w.Write(listPatchSetThreadsResponse)
   473  	}))
   474  	defer s.Close()
   475  	gerritClient := gerrit.NewClient(s.URL, gerrit.NoAuth)
   476  	threads, err := listPatchSetThreads(gerritClient, "go~master~I92400996cb051ab30e99bfffafd91ff32a1e7087")
   477  	if err != nil {
   478  		t.Fatal(err)
   479  	}
   480  	var mostRecentTryBotThread string
   481  	for _, tr := range threads {
   482  		if tr.unresolved {
   483  			t.Errorf("thread %s is unresolved", tr.root.ID)
   484  		}
   485  		if tr.root.Tag == tryBotsTag("beginning") {
   486  			mostRecentTryBotThread = tr.root.ID
   487  		}
   488  		if tr.root != tr.thread[0] {
   489  			t.Errorf("the root is not the first comment in thread")
   490  		}
   491  	}
   492  	if mostRecentTryBotThread != "aaf7aa39_658707c2" {
   493  		t.Errorf("wrong most recent TryBot thread: got %s, want %s", mostRecentTryBotThread, "aaf7aa39_658707c2")
   494  	}
   495  }
   496  
   497  func TestInvalidSlowBots(t *testing.T) {
   498  	work := &apipb.GerritTryWorkItem{
   499  		Version: 2,
   500  		TryMessage: []*apipb.TryVoteMessage{
   501  			{
   502  				Version: 1,
   503  				Message: "aix, linux-mipps, amd64, freeebsd",
   504  			},
   505  		},
   506  	}
   507  	slowBots, invalidSlowBots := slowBotsFromComments(work)
   508  	var got []string
   509  	for _, bc := range slowBots {
   510  		got = append(got, bc.Name)
   511  	}
   512  	want := []string{"aix-ppc64", "linux-amd64"}
   513  	if !reflect.DeepEqual(got, want) {
   514  		t.Errorf("mismatch:\n got: %q\nwant: %q\n", got, want)
   515  	}
   516  
   517  	wantInvalid := []string{"linux-mipps", "freeebsd"}
   518  	if !reflect.DeepEqual(invalidSlowBots, wantInvalid) {
   519  		t.Errorf("mismatch:\n got: %q\nwant: %q\n", invalidSlowBots, wantInvalid)
   520  	}
   521  }