[sk] Add tests for `try`

Change-Id: I1517ebb98fc0b94fa0dbb80a615ac96c806e1105
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/399856
Reviewed-by: Joe Gregorio <jcgregorio@google.com>
Commit-Queue: Eric Boren <borenet@google.com>
diff --git a/sk/go/try/BUILD.bazel b/sk/go/try/BUILD.bazel
index ace9905..6953fbc 100644
--- a/sk/go/try/BUILD.bazel
+++ b/sk/go/try/BUILD.bazel
@@ -1,3 +1,4 @@
+load("//bazel/go:go_test.bzl", "go_test")
 load("@io_bazel_rules_go//go:def.bzl", "go_library")
 
 go_library(
@@ -15,3 +16,15 @@
         "@com_github_urfave_cli_v2//:cli",
     ],
 )
+
+go_test(
+    name = "try_test",
+    srcs = ["try_test.go"],
+    embed = [":try"],
+    deps = [
+        "//go/exec",
+        "//go/testutils/unittest",
+        "//go/util",
+        "@com_github_stretchr_testify//require",
+    ],
+)
diff --git a/sk/go/try/try.go b/sk/go/try/try.go
index b6f4bb0..2b5b983 100644
--- a/sk/go/try/try.go
+++ b/sk/go/try/try.go
@@ -4,6 +4,7 @@
 	"bufio"
 	"context"
 	"fmt"
+	"io"
 	"os"
 	"regexp"
 	"sort"
@@ -19,6 +20,14 @@
 	"go.skia.org/infra/task_scheduler/go/specs"
 )
 
+var (
+	// stdin is an abstraction of os.Stdin which is convenient for testing.
+	stdin io.Reader = os.Stdin
+
+	// tryjobs is an instance of tryJobReader which may be replaced for testing.
+	tryjobs tryJobReader = &tryJobReaderImpl{}
+)
+
 // Command returns a cli.Command instance which represents the "try" command.
 func Command() *cli.Command {
 	yFlag := "y"
@@ -34,6 +43,9 @@
 			},
 		},
 		Action: func(ctx *cli.Context) error {
+			if err := fixupIssue(ctx.Context); err != nil {
+				return err
+			}
 			return try(ctx.Context, ctx.Args().Slice(), ctx.Bool(yFlag))
 		},
 	}
@@ -43,10 +55,7 @@
 // triggers the try jobs selected by the user.
 func try(ctx context.Context, jobRequests []string, triggerWithoutPrompt bool) error {
 	// Setup.
-	if err := fixupIssue(ctx); err != nil {
-		return err
-	}
-	jobs, err := getTryJobs(ctx)
+	jobs, err := tryjobs.getTryJobs(ctx)
 	if err != nil {
 		return err
 	}
@@ -98,7 +107,8 @@
 	jobsToTrigger := filteredJobs
 	if !triggerWithoutPrompt {
 		fmt.Printf("Do you want to trigger these jobs? (y/n or i for interactive): ")
-		read, err := bufio.NewReader(os.Stdin).ReadString('\n')
+		reader := bufio.NewReader(stdin)
+		read, err := reader.ReadString('\n')
 		if err != nil {
 			return err
 		}
@@ -111,7 +121,7 @@
 			for bucket, jobList := range filteredJobs {
 				for _, job := range jobList {
 					fmt.Printf("Trigger %s? (y/n): ", job)
-					trigger, err := bufio.NewReader(os.Stdin).ReadString('\n')
+					trigger, err := reader.ReadString('\n')
 					if err != nil {
 						return err
 					}
@@ -176,12 +186,20 @@
 	return nil
 }
 
-// getTryJobs reads tasks.json from the current repo and returns a
-// map[string][]string of Buildbucket bucket names to try job names.
-// TODO(borenet): This assumes that the current repo is associated with the
-// skia.primary bucket. This will work for most repos but it would be better to
-// look up the correct bucket to use.
-func getTryJobs(ctx context.Context) (map[string][]string, error) {
+// tryJobReader provides an abstraction for reading the available set of try
+// jobs to facilitate testing.
+type tryJobReader interface {
+	// getTryJobs reads tasks.json from the current repo and returns a
+	// map[string][]string of Buildbucket bucket names to try job names.
+	getTryJobs(context.Context) (map[string][]string, error)
+}
+
+// tryJobReaderImpl is the default tryJobReader implementation which reads from
+// the tasks.json file in the current repo.
+type tryJobReaderImpl struct{}
+
+// GetTryJobs implements tryJobReader.
+func (r *tryJobReaderImpl) getTryJobs(ctx context.Context) (map[string][]string, error) {
 	repoRoot, err := repo_root.GetLocal()
 	if err != nil {
 		return nil, err
@@ -194,6 +212,9 @@
 	for name := range tasksCfg.Jobs {
 		jobs = append(jobs, name)
 	}
+	// TODO(borenet): This assumes that the current repo is associated with the
+	// skia.primary bucket. This will work for most repos but it would be better
+	// to look up the correct bucket to use.
 	return map[string][]string{
 		"skia/skia.primary": jobs,
 	}, nil
diff --git a/sk/go/try/try_test.go b/sk/go/try/try_test.go
new file mode 100644
index 0000000..fec3e3a
--- /dev/null
+++ b/sk/go/try/try_test.go
@@ -0,0 +1,63 @@
+package try
+
+import (
+	"context"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"go.skia.org/infra/go/exec"
+	"go.skia.org/infra/go/testutils/unittest"
+	"go.skia.org/infra/go/util"
+)
+
+// mockTryJobReader is a mock implementation of tryJobReader used for testing.
+type mockTryJobReader struct {
+	jobs map[string][]string
+}
+
+// getTryJobs implements tryJobReader.
+func (r *mockTryJobReader) getTryJobs(ctx context.Context) (map[string][]string, error) {
+	return r.jobs, nil
+}
+
+func TestTry(t *testing.T) {
+	unittest.SmallTest(t)
+
+	mockCmd := exec.CommandCollector{}
+	ctx := exec.NewContext(context.Background(), mockCmd.Run)
+	bucket := "skia/skia.primary"
+	tryjobs = &mockTryJobReader{
+		jobs: map[string][]string{
+			bucket: {
+				"my-job",
+				"another-job",
+			},
+		},
+	}
+	tryCmdPrefix := []string{"cl", "try", "-B", bucket}
+
+	check := func(jobs []string, noPrompt bool, input string, expectTriggered []string) {
+		mockCmd.ClearCommands()
+		stdin = strings.NewReader(input)
+		require.NoError(t, try(ctx, jobs, noPrompt))
+		triggeredJobs := []string{}
+		for _, cmd := range mockCmd.Commands() {
+			if len(cmd.Args) > len(tryCmdPrefix) && util.SSliceEqual(cmd.Args[:len(tryCmdPrefix)], tryCmdPrefix) {
+				for i := len(tryCmdPrefix); i < len(cmd.Args); i++ {
+					arg := cmd.Args[i]
+					if arg != "-b" {
+						triggeredJobs = append(triggeredJobs, arg)
+					}
+				}
+			}
+		}
+		require.Equal(t, expectTriggered, triggeredJobs)
+	}
+	check([]string{"my-job"}, true, "", []string{"my-job"})
+	check([]string{"my-job"}, false, "y\n", []string{"my-job"})
+	check([]string{".*-job"}, true, "", []string{"another-job", "my-job"})
+	check([]string{"my-job"}, false, "n\n", []string{})
+	check([]string{".*-job"}, false, "i\ny\ny\n", []string{"another-job", "my-job"})
+	check([]string{".*-job"}, false, "i\nn\ny\n", []string{"my-job"})
+}