cmd/coordinator: support specifying a builder for x-repo slowbots

Add support for the syntax `x/repo@builder`, as suggested in #39201.

For golang/go#39201

Change-Id: Iac49169f12b90cbdfea626fcb4f5408358639760
Reviewed-on: https://go-review.googlesource.com/c/build/+/342712
Trust: Robert Findley <rfindley@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/cmd/coordinator/coordinator.go b/cmd/coordinator/coordinator.go
index 53d5e04..d817dd5 100644
--- a/cmd/coordinator/coordinator.go
+++ b/cmd/coordinator/coordinator.go
@@ -1376,14 +1376,26 @@
 	// For the Go project on the "master" branch,
 	// use the TRY= syntax to test against x repos.
 	if branch := key.Branch; key.Project == "go" && branch == "master" {
-		// linuxBuilder is the standard builder as it is the fastest and least expensive.
-		linuxBuilder := dashboard.Builders["linux-amd64"]
+		// customBuilder optionally specifies the builder to use for the build
+		// (empty string means to use the default builder).
+		addXrepo := func(project, customBuilder string) *buildStatus {
+			// linux-amd64 is the default builder as it is the fastest and least
+			// expensive.
+			builder := dashboard.Builders["linux-amd64"]
+			if customBuilder != "" {
+				b, ok := dashboard.Builders[customBuilder]
+				if !ok {
+					log.Printf("can't resolve requested builder %q", customBuilder)
+					return nil
+				}
+				builder = b
+			}
 
-		addXrepo := func(project string) *buildStatus {
 			if testingKnobSkipBuilds {
 				return nil
 			}
-			if !linuxBuilder.BuildsRepoTryBot(project, branch, branch) {
+			if !builder.BuildsRepoTryBot(project, branch, branch) {
+				log.Printf("builder %q isn't configured to build %q@%q as a trybot", builder.Name, project, branch)
 				return nil
 			}
 			rev, err := getRepoHead(project)
@@ -1392,7 +1404,7 @@
 				return nil
 			}
 			brev := buildgo.BuilderRev{
-				Name:    linuxBuilder.Name,
+				Name:    builder.Name,
 				Rev:     work.Commit,
 				SubName: project,
 				SubRev:  rev,
@@ -1407,17 +1419,17 @@
 		}
 
 		// First, add the opt-in x repos.
-		xrepos := xReposFromComments(work)
-		for project := range xrepos {
-			if bs := addXrepo(project); bs != nil {
+		repoBuilders := xReposFromComments(work)
+		for rb := range repoBuilders {
+			if bs := addXrepo(rb.Project, rb.Builder); bs != nil {
 				ts.xrepos = append(ts.xrepos, bs)
 			}
 		}
 
-		// Always include x/tools. See golang.org/issue/34348.
+		// Always include the default x/tools builder. See golang.org/issue/34348.
 		// Do not add it to the trySet's list of opt-in x repos, however.
-		if !xrepos["tools"] {
-			addXrepo("tools")
+		if haveDefaultToolsBuild := repoBuilders[xRepoAndBuilder{Project: "tools"}]; !haveDefaultToolsBuild {
+			addXrepo("tools", "")
 		}
 	}
 
@@ -4193,18 +4205,39 @@
 	return builders
 }
 
-// xReposFromComments looks at the TRY= comments from Gerrit (in
-// work) and returns any additional subrepos that should be tested.
-// The TRY= comments are expected to be of the format TRY=x/foo,
-// where foo is the name of the subrepo.
-func xReposFromComments(work *apipb.GerritTryWorkItem) map[string]bool {
-	xrepos := map[string]bool{}
+type xRepoAndBuilder struct {
+	Project string // "net", "tools", etc.
+	Builder string // Builder to use. Empty string means default builder.
+}
+
+func (rb xRepoAndBuilder) String() string {
+	if rb.Builder == "" {
+		return rb.Project
+	}
+	return rb.Project + "@" + rb.Builder
+}
+
+// xReposFromComments looks at the TRY= comments from Gerrit (in work) and
+// returns any additional subrepos that should be tested. The TRY= comments
+// are expected to be of the format TRY=x/foo or TRY=x/foo@builder where foo is
+// the name of the subrepo and builder is a builder name. If no builder is
+// provided, a default builder is used.
+func xReposFromComments(work *apipb.GerritTryWorkItem) map[xRepoAndBuilder]bool {
+	xrepos := make(map[xRepoAndBuilder]bool)
 	for _, term := range latestTryTerms(work) {
 		if len(term) < len("x/_") || term[:2] != "x/" {
 			continue
 		}
-		xrepo := term[2:]
-		xrepos[xrepo] = true
+		parts := strings.SplitN(term, "@", 2)
+		xrepo := parts[0][2:]
+		builder := "" // By convention, this means the default builder.
+		if len(parts) > 1 {
+			builder = parts[1]
+		}
+		xrepos[xRepoAndBuilder{
+			Project: xrepo,
+			Builder: builder,
+		}] = true
 	}
 	return xrepos
 }
@@ -4219,7 +4252,7 @@
 		return nil
 	}
 	return strings.FieldsFunc(tryMsg, func(c rune) bool {
-		return !unicode.IsLetter(c) && !unicode.IsNumber(c) && c != '-' && c != '_' && c != '/'
+		return !unicode.IsLetter(c) && !unicode.IsNumber(c) && c != '-' && c != '_' && c != '/' && c != '@'
 	})
 }
 
diff --git a/cmd/coordinator/coordinator_test.go b/cmd/coordinator/coordinator_test.go
index 8addd8b..e0592d5 100644
--- a/cmd/coordinator/coordinator_test.go
+++ b/cmd/coordinator/coordinator_test.go
@@ -365,15 +365,16 @@
 		TryMessage: []*apipb.TryVoteMessage{
 			{
 				Version: 2,
-				Message: "x/build, x/sync x/tools, x/sync",
+				Message: "x/build, x/sync x/tools, x/sync, x/tools@freebsd-amd64-race",
 			},
 		},
 	}
 	got := xReposFromComments(work)
-	want := map[string]bool{
-		"build": true,
-		"sync":  true,
-		"tools": true,
+	want := map[xRepoAndBuilder]bool{
+		{"build", ""}:                   true,
+		{"sync", ""}:                    true,
+		{"tools", ""}:                   true,
+		{"tools", "freebsd-amd64-race"}: true,
 	}
 	if !reflect.DeepEqual(got, want) {
 		t.Errorf("mismatch:\n got: %v\nwant: %v\n", got, want)