[gold] Add tests for baseline

I'm preparing to re-work several things in an effort to get the BT tracestore in.
I would like to have tests to back up those changes.

The change of master branch to be -1 is to make it the same
(expstore.MasterBranch is -1)

Bug: skia:9096
Change-Id: Ia6580832176459b0895aec11dc2c8df3051a03db
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/219781
Commit-Queue: Kevin Lubick <kjlubick@google.com>
Reviewed-by: Ben Wagner aka dogben <benjaminwagner@google.com>
diff --git a/gold-client/cmd/goldctl/cmd_imgtest.go b/gold-client/cmd/goldctl/cmd_imgtest.go
index 7f4a997..e62a040 100644
--- a/gold-client/cmd/goldctl/cmd_imgtest.go
+++ b/gold-client/cmd/goldctl/cmd_imgtest.go
@@ -166,7 +166,7 @@
 	ifErrLogExit(cmd, err)
 
 	validation := shared.Validation{}
-	issueID := validation.Int64Value("issue", i.flagIssueID, 0)
+	issueID := validation.Int64Value("issue", i.flagIssueID, types.MasterBranch)
 	patchsetID := validation.Int64Value("patchset", i.flagPatchsetID, 0)
 	jobID := validation.Int64Value("jobid", i.flagJobID, 0)
 	ifErrLogExit(cmd, validation.Errors())
diff --git a/gold-client/go/goldclient/goldclient.go b/gold-client/go/goldclient/goldclient.go
index 9d37c0c..3b18405 100644
--- a/gold-client/go/goldclient/goldclient.go
+++ b/gold-client/go/goldclient/goldclient.go
@@ -610,7 +610,7 @@
 // loadExpectations fetches the expectations from Gold to compare to tests.
 func (r *resultState) loadExpectations(httpClient HTTPClient) error {
 	urlPath := strings.Replace(shared.EXPECTATIONS_ROUTE, "{commit_hash}", r.SharedConfig.GitHash, 1)
-	if r.SharedConfig.Issue > 0 {
+	if !types.IsMasterBranch(r.SharedConfig.Issue) {
 		urlPath = fmt.Sprintf("%s?issue=%d", urlPath, r.SharedConfig.Issue)
 	}
 	url := fmt.Sprintf("%s/%s", r.GoldURL, strings.TrimLeft(urlPath, "/"))
@@ -665,7 +665,7 @@
 		fileName}
 	path := fmt.Sprintf("%s/%04d/%02d/%02d/%02d/%s/%d/%d/%s", segments...)
 
-	if r.SharedConfig.Issue > 0 {
+	if !types.IsMasterBranch(r.SharedConfig.Issue) {
 		path = "trybot/" + path
 	}
 	return fmt.Sprintf("%s/%s", r.Bucket, path)
diff --git a/gold-client/go/goldclient/goldclient_test.go b/gold-client/go/goldclient/goldclient_test.go
index a815016..1e181fa 100644
--- a/gold-client/go/goldclient/goldclient_test.go
+++ b/gold-client/go/goldclient/goldclient_test.go
@@ -55,7 +55,8 @@
 	assert.NotContains(t, knownHashes, "notInThere")
 }
 
-// Test data processing of the baseline input
+// TestLoadBaseline loads a baseline for an issue (testSharedConfig defaults to being
+// an configured for a tryjob).
 func TestLoadBaseline(t *testing.T) {
 	unittest.SmallTest(t)
 
@@ -79,13 +80,59 @@
 	assert.NoError(t, err)
 
 	// Check that the baseline was loaded correctly
-	baseline := goldClient.resultState.Expectations
-	assert.Len(t, baseline, 1, "only one test")
-	digests := baseline["ThisIsTheOnlyTest"]
+	bl := goldClient.resultState.Expectations
+	assert.Len(t, bl, 1, "only one test")
+	digests := bl["ThisIsTheOnlyTest"]
 	assert.Len(t, digests, 2, "two previously seen images")
 	assert.Equal(t, types.NEGATIVE, digests["badbadbad1325855590527db196112e0"])
 	assert.Equal(t, types.POSITIVE, digests["beef00d3a1527db19619ec12a4e0df68"])
 
+	assert.Equal(t, testIssueID, goldClient.resultState.SharedConfig.Issue)
+
+	knownHashes := goldClient.resultState.KnownHashes
+	assert.Empty(t, knownHashes, "No hashes loaded")
+}
+
+// TestLoadBaselineMaster loads the baseline for the master branch.
+func TestLoadBaselineMaster(t *testing.T) {
+	unittest.SmallTest(t)
+
+	wd, cleanup := testutils.TempDir(t)
+	defer cleanup()
+
+	auth, httpClient, uploader := makeMocks()
+	defer auth.AssertExpectations(t)
+	defer httpClient.AssertExpectations(t)
+	defer uploader.AssertExpectations(t)
+
+	hashesResp := httpResponse([]byte("none"), "200 OK", http.StatusOK)
+	httpClient.On("Get", "https://testing-gold.skia.org/json/hashes").Return(hashesResp, nil)
+
+	expectations := httpResponse([]byte(mockBaselineJSON), "200 OK", http.StatusOK)
+	httpClient.On("Get", "https://testing-gold.skia.org/json/expectations/commit/abcd1234").Return(expectations, nil)
+
+	goldClient, err := makeGoldClient(auth, false /*=passFail*/, false /*=uploadOnly*/, wd)
+	assert.NoError(t, err)
+	err = goldClient.SetSharedConfig(jsonio.GoldResults{
+		GitHash: "abcd1234",
+		Key: map[string]string{
+			"os":  "WinTest",
+			"gpu": "GPUTest",
+		},
+		Issue: types.MasterBranch,
+	})
+	assert.NoError(t, err)
+
+	// Check that the baseline was loaded correctly
+	bl := goldClient.resultState.Expectations
+	assert.Len(t, bl, 1, "only one test")
+	digests := bl["ThisIsTheOnlyTest"]
+	assert.Len(t, digests, 2, "two previously seen images")
+	assert.Equal(t, types.NEGATIVE, digests["badbadbad1325855590527db196112e0"])
+	assert.Equal(t, types.POSITIVE, digests["beef00d3a1527db19619ec12a4e0df68"])
+
+	assert.Equal(t, types.MasterBranch, goldClient.resultState.SharedConfig.Issue)
+
 	knownHashes := goldClient.resultState.KnownHashes
 	assert.Empty(t, knownHashes, "No hashes loaded")
 }
diff --git a/golden/go/baseline/baseline.go b/golden/go/baseline/baseline.go
index f03aca8..ac4dde1 100644
--- a/golden/go/baseline/baseline.go
+++ b/golden/go/baseline/baseline.go
@@ -6,7 +6,6 @@
 	"fmt"
 
 	"go.skia.org/infra/go/skerr"
-	"go.skia.org/infra/go/sklog"
 	"go.skia.org/infra/go/tiling"
 	"go.skia.org/infra/go/util"
 	"go.skia.org/infra/golden/go/tryjobstore"
@@ -25,8 +24,6 @@
 	}
 }
 
-// TODO(kjlubick): Add tests for GetBaselinesPerCommit.
-
 // GetBaselinesPerCommit calculates the baselines for each commit in the tile.
 // Of note, it only fills out the Positive matches - everything not seen is either untriaged
 // or negative.
@@ -76,7 +73,7 @@
 	}
 
 	// Iterate over all commits. If the tile is sparse we substitute the expectations with the
-	// expectations of the closest ancestor that has expecations. We also add the commits that
+	// expectations of the closest ancestor that has expectations. We also add the commits that
 	// have landed already, but are not captured in the current tile.
 	combined := allCommits
 	if len(extraCommits) > 0 {
@@ -86,7 +83,7 @@
 	}
 
 	ret := make(map[string]*Baseline, len(combined))
-	var currBL *Baseline = nil
+	var currBL *Baseline
 	for _, commit := range combined {
 		bl, ok := denseBaselines[commit.Hash]
 		if ok {
@@ -102,19 +99,15 @@
 				Filled:       len(bl),
 				Expectations: bl,
 				MD5:          md5Sum,
+				Issue:        types.MasterBranch,
 			}
 			currBL = ret[commit.Hash]
-		} else if currBL != nil {
+		} else {
 			// Make a copy of the baseline of the previous commit and update the commit information.
-			cpBL := *currBL
+			cpBL := currBL.Copy()
 			cpBL.StartCommit = commit
 			cpBL.EndCommit = commit
-			ret[commit.Hash] = &cpBL
-		} else {
-			// Reaching this point means the first in the dense tile does not align with the first
-			// commit of the sparse portion of the tile. This a sanity test and should only happen
-			// in the presence of a programming error or data corruption.
-			sklog.Errorf("Unable to get baseline for commit %s. It has not commits for immediate ancestors in tile.", commit.Hash)
+			ret[commit.Hash] = cpBL
 		}
 	}
 
@@ -123,7 +116,7 @@
 
 // GetBaselineForIssue returns the baseline for the given issue. This baseline
 // contains all triaged digests that are not in the master tile.
-// Note: CommitDelta, Total and Filled are not relevant for an issue baseline since
+// Note: Total and Filled are not relevant for an issue baseline since
 // the concept of traces doesn't really make sense for a single commit.
 func GetBaselineForIssue(issueID int64, tryjobs []*tryjobstore.Tryjob, tryjobResults [][]*tryjobstore.TryjobResult, exp types.Expectations, commits []*tiling.Commit) (*Baseline, error) {
 	startCommit := commits[len(commits)-1]
diff --git a/golden/go/baseline/baseline_test.go b/golden/go/baseline/baseline_test.go
new file mode 100644
index 0000000..634557f
--- /dev/null
+++ b/golden/go/baseline/baseline_test.go
@@ -0,0 +1,203 @@
+package baseline_test
+
+import (
+	"testing"
+	"time"
+
+	assert "github.com/stretchr/testify/require"
+	"go.skia.org/infra/go/testutils/unittest"
+	"go.skia.org/infra/go/tiling"
+	"go.skia.org/infra/golden/go/baseline"
+	"go.skia.org/infra/golden/go/mocks"
+	data "go.skia.org/infra/golden/go/testutils/data_three_devices"
+	"go.skia.org/infra/golden/go/tryjobstore"
+	"go.skia.org/infra/golden/go/types"
+)
+
+func TestGetBaselinesPerCommitSunnyDay(t *testing.T) {
+	unittest.SmallTest(t)
+
+	mti := &mocks.TileInfo{}
+	defer mti.AssertExpectations(t)
+
+	mti.On("AllCommits").Return(makeSparseCommits())
+	mti.On("DataCommits").Return(data.MakeTestCommits())
+	mti.On("GetTile", types.ExcludeIgnoredTraces).Return(data.MakeTestTile())
+
+	exp := data.MakeTestExpectations()
+
+	bm, err := baseline.GetBaselinesPerCommit(exp, mti, makeExtraCommits())
+	assert.NoError(t, err)
+
+	extraCommits := makeExtraCommits()
+	sparseCommits := makeSparseCommits()
+
+	assert.Equal(t, map[string]*baseline.Baseline{
+		data.FirstCommitHash: {
+			StartCommit:  sparseCommits[0],
+			EndCommit:    sparseCommits[0],
+			Total:        6,
+			Filled:       1,
+			Issue:        types.MasterBranch,
+			Expectations: betaOnly,
+			MD5:          BetaMD5,
+		},
+		EmptyCommitHash: {
+			StartCommit:  sparseCommits[1],
+			EndCommit:    sparseCommits[1],
+			Total:        6,
+			Filled:       1,
+			Issue:        types.MasterBranch,
+			Expectations: betaOnly,
+			MD5:          BetaMD5,
+		},
+		data.SecondCommitHash: {
+			StartCommit:  sparseCommits[2],
+			EndCommit:    sparseCommits[2],
+			Total:        6,
+			Filled:       1,
+			Issue:        types.MasterBranch,
+			Expectations: betaOnly,
+			MD5:          BetaMD5,
+		},
+		data.ThirdCommitHash: {
+			StartCommit:  sparseCommits[3],
+			EndCommit:    sparseCommits[3],
+			Total:        6,
+			Filled:       2,
+			Issue:        types.MasterBranch,
+			Expectations: both,
+			MD5:          BothMD5,
+		},
+		ExtraCommitHash: {
+			StartCommit:  extraCommits[0],
+			EndCommit:    extraCommits[0],
+			Total:        6,
+			Filled:       2,
+			Issue:        types.MasterBranch,
+			Expectations: both,
+			MD5:          BothMD5,
+		},
+	}, bm)
+}
+
+func TestGetBaselineForIssueSunnyDay(t *testing.T) {
+	unittest.SmallTest(t)
+
+	issue := int64(117)
+
+	// There were 2 tryjobs, one run when the CL had a parent of
+	// SecondCommitHash and the other when it was rebased onto
+	// ThirdCommitHash.
+	tryjobs := []*tryjobstore.Tryjob{
+		{
+			MasterCommit: data.SecondCommitHash,
+		},
+		{
+			MasterCommit: data.ThirdCommitHash,
+		},
+	}
+
+	tryjobResults := make([][]*tryjobstore.TryjobResult, len(tryjobs))
+	tryjobResults[0] = []*tryjobstore.TryjobResult{
+		{
+			TestName: data.AlphaTest,
+			Digest:   data.AlphaBad1Digest,
+		},
+		{
+			TestName: data.BetaTest,
+			Digest:   data.BetaGood1Digest,
+		},
+	}
+	tryjobResults[1] = []*tryjobstore.TryjobResult{
+		{
+			TestName: data.AlphaTest,
+			Digest:   data.AlphaGood1Digest,
+		},
+		{
+			TestName: data.BetaTest,
+			Digest:   data.BetaUntriaged1Digest,
+		},
+	}
+
+	b, err := baseline.GetBaselineForIssue(issue, tryjobs, tryjobResults,
+		data.MakeTestExpectations(), makeSparseCommits())
+	assert.NoError(t, err)
+
+	sparseCommits := makeSparseCommits()
+	assert.Equal(t, &baseline.Baseline{
+		StartCommit:  sparseCommits[2],
+		EndCommit:    sparseCommits[3],
+		Total:        0,
+		Filled:       0,
+		Issue:        issue,
+		Expectations: both,
+		MD5:          BothMD5,
+	}, b)
+}
+
+func makeSparseCommits() []*tiling.Commit {
+	// Four commits, with completely arbitrary data.
+	return []*tiling.Commit{
+		{
+			Hash:       data.FirstCommitHash,
+			CommitTime: time.Date(2019, time.April, 26, 12, 0, 3, 0, time.UTC).Unix(),
+			Author:     data.FirstCommitAuthor,
+		},
+		{
+			Hash:       EmptyCommitHash,
+			CommitTime: time.Date(2019, time.April, 26, 12, 5, 5, 0, time.UTC).Unix(),
+			Author:     data.FirstCommitAuthor,
+		},
+		{
+			Hash:       data.SecondCommitHash,
+			CommitTime: time.Date(2019, time.April, 26, 12, 10, 18, 0, time.UTC).Unix(),
+			Author:     data.SecondCommitAuthor,
+		},
+		{
+			Hash:       data.ThirdCommitHash,
+			CommitTime: time.Date(2019, time.April, 26, 13, 10, 8, 0, time.UTC).Unix(),
+			Author:     data.ThirdCommitAuthor,
+		},
+	}
+}
+
+func makeExtraCommits() []*tiling.Commit {
+	// One extra commit, completely arbitrary data.
+	return []*tiling.Commit{
+		{
+			Hash:       ExtraCommitHash,
+			CommitTime: time.Date(2019, time.April, 27, 5, 17, 1, 0, time.UTC).Unix(),
+			Author:     data.SecondCommitAuthor,
+		},
+	}
+}
+
+const (
+	EmptyCommitHash = "eea258b693f2fc53501ac341f3029860b3b57a10"
+	ExtraCommitHash = "8181bc0cb0ff1c83e5839aac0e8b13dc0157047a"
+
+	BetaMD5 = "fefa5ea9f0e646258516ed6cecff06c6"
+	BothMD5 = "821b673f7f240953aecf3c8fa32d5655"
+)
+
+var (
+	// For all but the last commit, BetaGood1Digest is the only
+	// positive digest seen.
+	betaOnly = types.Expectations{
+		data.BetaTest: {
+			data.BetaGood1Digest: types.POSITIVE,
+		},
+	}
+
+	// For the last commit, we see AlphaGood1Digest, so it is added
+	// to the Expectations.
+	both = types.Expectations{
+		data.AlphaTest: {
+			data.AlphaGood1Digest: types.POSITIVE,
+		},
+		data.BetaTest: {
+			data.BetaGood1Digest: types.POSITIVE,
+		},
+	}
+)
diff --git a/golden/go/baseline/gcs_baseliner/baseliner.go b/golden/go/baseline/gcs_baseliner/baseliner.go
index f181c9b..8ade84e 100644
--- a/golden/go/baseline/gcs_baseliner/baseliner.go
+++ b/golden/go/baseline/gcs_baseliner/baseliner.go
@@ -19,6 +19,7 @@
 	"go.skia.org/infra/golden/go/shared"
 	"go.skia.org/infra/golden/go/storage"
 	"go.skia.org/infra/golden/go/tryjobstore"
+	"go.skia.org/infra/golden/go/types"
 	"golang.org/x/sync/errgroup"
 )
 
@@ -212,7 +213,7 @@
 
 // FetchBaseline implements the baseline.Baseliner interface.
 func (b *BaselinerImpl) FetchBaseline(commitHash string, issueID int64, issueOnly bool) (*baseline.Baseline, error) {
-	isIssue := issueID > baseline.MasterBranch
+	isIssue := !types.IsMasterBranch(issueID)
 
 	var masterBaseline *baseline.Baseline
 	var issueBaseline *baseline.Baseline
@@ -350,7 +351,7 @@
 	}
 
 	// We did not find it in the cache so lets load it from GCS.
-	ret, err := b.gStorageClient.ReadBaseline(commitHash, baseline.MasterBranch)
+	ret, err := b.gStorageClient.ReadBaseline(commitHash, types.MasterBranch)
 	if err != nil {
 		return nil, err
 	}
diff --git a/golden/go/baseline/gcs_baseliner/baseliner_test.go b/golden/go/baseline/gcs_baseliner/baseliner_test.go
index d886fa0..703740a 100644
--- a/golden/go/baseline/gcs_baseliner/baseliner_test.go
+++ b/golden/go/baseline/gcs_baseliner/baseliner_test.go
@@ -24,12 +24,12 @@
 	mgs := makeMockGCSStorage()
 	defer mgs.AssertExpectations(t)
 
-	mgs.On("ReadBaseline", testCommitHash, baseline.MasterBranch).Return(three_devices.MakeTestBaseline(), nil).Once()
+	mgs.On("ReadBaseline", testCommitHash, types.MasterBranch).Return(three_devices.MakeTestBaseline(), nil).Once()
 
 	baseliner, err := New(mgs, nil, nil, nil)
 	assert.NoError(t, err)
 
-	b, err := baseliner.FetchBaseline(testCommitHash, baseline.MasterBranch, false)
+	b, err := baseliner.FetchBaseline(testCommitHash, types.MasterBranch, false)
 	assert.NoError(t, err)
 
 	deepequal.AssertDeepEqual(t, three_devices.MakeTestBaseline(), b)
@@ -73,7 +73,7 @@
 	defer mgs.AssertExpectations(t)
 
 	// mock the master baseline
-	mgs.On("ReadBaseline", testCommitHash, int64(0)).Return(three_devices.MakeTestBaseline(), nil).Once()
+	mgs.On("ReadBaseline", testCommitHash, types.MasterBranch).Return(three_devices.MakeTestBaseline(), nil).Once()
 	// mock the expectations that a user would have applied to their CL (that
 	// are not live on master yet).
 	mgs.On("ReadBaseline", "", testIssueID).Return(additionalTriages, nil).Once()
@@ -108,7 +108,7 @@
 	}, b.Expectations)
 
 	// Ensure that reading the issue branch does not impact the master branch
-	b, err = baseliner.FetchBaseline(testCommitHash, baseline.MasterBranch, false)
+	b, err = baseliner.FetchBaseline(testCommitHash, types.MasterBranch, false)
 	assert.NoError(t, err)
 	assert.Equal(t, three_devices.MakeTestBaseline(), b)
 }
@@ -122,13 +122,13 @@
 	defer mgs.AssertExpectations(t)
 
 	// ReadBaseline should only be called once despite multiple requests below
-	mgs.On("ReadBaseline", testCommitHash, baseline.MasterBranch).Return(three_devices.MakeTestBaseline(), nil).Once()
+	mgs.On("ReadBaseline", testCommitHash, types.MasterBranch).Return(three_devices.MakeTestBaseline(), nil).Once()
 
 	baseliner, err := New(mgs, nil, nil, nil)
 	assert.NoError(t, err)
 
 	for i := 0; i < 10; i++ {
-		b, err := baseliner.FetchBaseline(testCommitHash, baseline.MasterBranch, false)
+		b, err := baseliner.FetchBaseline(testCommitHash, types.MasterBranch, false)
 		assert.NoError(t, err)
 		assert.NotNil(t, b)
 		deepequal.AssertDeepEqual(t, three_devices.MakeTestBaseline(), b)
diff --git a/golden/go/baseline/types.go b/golden/go/baseline/types.go
index 740909b..1c000f1 100644
--- a/golden/go/baseline/types.go
+++ b/golden/go/baseline/types.go
@@ -18,10 +18,6 @@
 	// EndCommit is the commit for which this baseline was collected.
 	EndCommit *tiling.Commit `json:"endCommit"`
 
-	// CommitDelta is the difference in index within the commits of a tile.
-	// TODO(kjlubick) Appears to be unread and unwritten to.
-	CommitDelta int `json:"commitDelta"`
-
 	// Total is the total number of traces that were iterated when generating the baseline.
 	Total int `json:"total"`
 
@@ -35,7 +31,7 @@
 	// with only the positive digests of the current commit.
 	Expectations types.Expectations `json:"master"`
 
-	// Issue indicates the Gerrit issue of this baseline. 0 indicates the master branch.
+	// Issue indicates the Gerrit issue of this baseline. -1 indicates the master branch.
 	Issue int64
 }
 
@@ -60,8 +56,6 @@
 	}
 }
 
-const MasterBranch = int64(0)
-
 // Baseliner is an interface wrapping the functionality to save and fetch baselines.
 type Baseliner interface {
 	// CanWriteBaseline returns true if this instance was configured to write baseline files.
diff --git a/golden/go/expstorage/ds_expstore/ds_expstore.go b/golden/go/expstorage/ds_expstore/ds_expstore.go
index fe250d9..6f89fbe 100644
--- a/golden/go/expstorage/ds_expstore/ds_expstore.go
+++ b/golden/go/expstorage/ds_expstore/ds_expstore.go
@@ -120,7 +120,7 @@
 	blobStore := dsutil.NewBlobStore(client, ds.EXPECTATIONS_BLOB_ROOT, ds.EXPECTATIONS_BLOB)
 
 	store := &DSExpStore{
-		issueID:         expstorage.MasterBranch,
+		issueID:         types.MasterBranch,
 		changeKind:      ds.MASTER_EXP_CHANGE,
 		eventExpChange:  expstorage.EV_EXPSTORAGE_CHANGED,
 		globalEvent:     true,
@@ -581,7 +581,7 @@
 			Filter("OK =", true).
 			KeysOnly()
 
-		if c.issueID > 0 {
+		if !types.IsMasterBranch(c.issueID) {
 			q = q.Filter("IssueID =", c.issueID)
 		}
 
diff --git a/golden/go/expstorage/fs_expstore/fs_expstore.go b/golden/go/expstorage/fs_expstore/fs_expstore.go
index ceb0773..4f967dd 100644
--- a/golden/go/expstorage/fs_expstore/fs_expstore.go
+++ b/golden/go/expstorage/fs_expstore/fs_expstore.go
@@ -131,7 +131,7 @@
 		eventBus:       eventBus,
 		eventExpChange: expstorage.EV_EXPSTORAGE_CHANGED,
 		globalEvent:    true,
-		issue:          expstorage.MasterBranch,
+		issue:          types.MasterBranch,
 		mode:           mode,
 	}
 	// pre-load the cache. This simplifies the mutex handling in Get().
@@ -144,7 +144,7 @@
 
 // ForIssue implements the ExpectationsStore interface.
 func (f *Store) ForIssue(id int64) expstorage.ExpectationsStore {
-	if id == expstorage.MasterBranch {
+	if types.IsMasterBranch(id) {
 		// It is invalid to re-request the master branch
 		return nil
 	}
@@ -160,7 +160,7 @@
 
 // Get implements the ExpectationsStore interface.
 func (f *Store) Get() (types.Expectations, error) {
-	if f.issue == expstorage.MasterBranch {
+	if f.issue == types.MasterBranch {
 		defer metrics2.NewTimer("gold_get_expectations", map[string]string{"master_branch": "true"}).Stop()
 		f.cacheMutex.RLock()
 		defer f.cacheMutex.RUnlock()
@@ -174,7 +174,7 @@
 // based on the current state. It is expected the caller has taken care of any mutex grabbing.
 func (f *Store) getMasterExpectations() (types.Expectations, error) {
 	if f.cache == nil {
-		c, err := f.loadExpectationsSharded(expstorage.MasterBranch, masterShards)
+		c, err := f.loadExpectationsSharded(types.MasterBranch, masterShards)
 		if err != nil {
 			return nil, skerr.Fmt("could not load master expectations from firestore: %s", err)
 		}
diff --git a/golden/go/expstorage/fs_expstore/fs_expstore_test.go b/golden/go/expstorage/fs_expstore/fs_expstore_test.go
index e31e1ac..6d22a0a 100644
--- a/golden/go/expstorage/fs_expstore/fs_expstore_test.go
+++ b/golden/go/expstorage/fs_expstore/fs_expstore_test.go
@@ -466,11 +466,11 @@
 
 	meb.On("Publish", expstorage.EV_EXPSTORAGE_CHANGED, &expstorage.EventExpectationChange{
 		TestChanges: change1,
-		IssueID:     expstorage.MasterBranch,
+		IssueID:     types.MasterBranch,
 	}, /*global=*/ true).Once()
 	meb.On("Publish", expstorage.EV_EXPSTORAGE_CHANGED, &expstorage.EventExpectationChange{
 		TestChanges: change2,
-		IssueID:     expstorage.MasterBranch,
+		IssueID:     types.MasterBranch,
 	}, /*global=*/ true).Once()
 
 	ctx := context.Background()
@@ -547,11 +547,11 @@
 
 	meb.On("Publish", expstorage.EV_EXPSTORAGE_CHANGED, &expstorage.EventExpectationChange{
 		TestChanges: change,
-		IssueID:     expstorage.MasterBranch,
+		IssueID:     types.MasterBranch,
 	}, /*global=*/ true).Once()
 	meb.On("Publish", expstorage.EV_EXPSTORAGE_CHANGED, &expstorage.EventExpectationChange{
 		TestChanges: expectedUndo,
-		IssueID:     expstorage.MasterBranch,
+		IssueID:     types.MasterBranch,
 	}, /*global=*/ true).Once()
 
 	ctx := context.Background()
diff --git a/golden/go/expstorage/mem_expstore/mem_expstore.go b/golden/go/expstorage/mem_expstore/mem_expstore.go
index 1143963..06e727a 100644
--- a/golden/go/expstorage/mem_expstore/mem_expstore.go
+++ b/golden/go/expstorage/mem_expstore/mem_expstore.go
@@ -53,7 +53,7 @@
 	if m.eventBus != nil {
 		m.eventBus.Publish(expstorage.EV_EXPSTORAGE_CHANGED, &expstorage.EventExpectationChange{
 			TestChanges: changedTests,
-			IssueID:     expstorage.MasterBranch,
+			IssueID:     types.MasterBranch,
 		}, true)
 	}
 
@@ -81,7 +81,7 @@
 	if m.eventBus != nil {
 		m.eventBus.Publish(expstorage.EV_EXPSTORAGE_CHANGED, &expstorage.EventExpectationChange{
 			TestChanges: changedDigests,
-			IssueID:     expstorage.MasterBranch,
+			IssueID:     types.MasterBranch,
 		}, true)
 	}
 
diff --git a/golden/go/expstorage/types.go b/golden/go/expstorage/types.go
index eb59aaf..b1f09bb 100644
--- a/golden/go/expstorage/types.go
+++ b/golden/go/expstorage/types.go
@@ -17,10 +17,6 @@
 	// EV_TRYJOB_EXP_CHANGED is the event type that is fired when the expectations
 	// for an issue change. It sends an instance of *TryjobExpChange.
 	EV_TRYJOB_EXP_CHANGED = "expstorage:tryjob-exp-change"
-
-	// MasterBranch is the value used for IssueID when we dealing with the
-	// master branch. Any other IssueID < 0 should be ignored.
-	MasterBranch = int64(-1)
 )
 
 func init() {
diff --git a/golden/go/jsonio/jsonio.go b/golden/go/jsonio/jsonio.go
index f9d8003..6c06053 100644
--- a/golden/go/jsonio/jsonio.go
+++ b/golden/go/jsonio/jsonio.go
@@ -131,7 +131,10 @@
 func (r *rawGoldResults) parseValidate() (*GoldResults, []string) {
 	jn := rawGoldResultsJsonMap
 	var errs []string
-	issueValid := (r.Issue == "") || (r.Issue != "" && r.BuildBucketID != "" && r.Patchset != "")
+	mb := strconv.FormatInt(types.MasterBranch, 10)
+	lmb := strconv.FormatInt(types.LegacyMasterBranch, 10)
+	issueValid := (r.Issue == "") || (r.Issue == mb) || (r.Issue == lmb) ||
+		(r.Issue != "" && r.BuildBucketID != "" && r.Patchset != "")
 	addErrMessage(&errs, issueValid, "fields '%s', '%s' must not be empty if field '%s' contains a value", jn["Patchset"], jn["BuildBucketID"], jn["Issue"])
 
 	f := []string{"Issue", r.Issue, "Patchset", r.Patchset, "BuildBucketID", r.BuildBucketID}
@@ -149,10 +152,15 @@
 			Builder: r.Builder,
 			TaskID:  r.TaskID,
 		}
-		// If there was no error we can just parse the strings to int64.
-		ret.Issue, _ = strconv.ParseInt(r.Issue, 10, 64)
-		ret.BuildBucketID, _ = strconv.ParseInt(r.BuildBucketID, 10, 64)
-		ret.Patchset, _ = strconv.ParseInt(r.Patchset, 10, 64)
+		if r.Issue == "" {
+			ret.Issue = types.MasterBranch
+		} else {
+			// If there was no error we can just parse the strings to int64.
+			ret.Issue, _ = strconv.ParseInt(r.Issue, 10, 64)
+			ret.BuildBucketID, _ = strconv.ParseInt(r.BuildBucketID, 10, 64)
+			ret.Patchset, _ = strconv.ParseInt(r.Patchset, 10, 64)
+		}
+
 	}
 	return ret, errs
 }
@@ -174,8 +182,9 @@
 	addErrMessage(&errMsg, regExHexadecimal.MatchString(g.GitHash), "field '%s' must be hexadecimal. Received '%s'", jn["GitHash"], g.GitHash)
 	addErrMessage(&errMsg, len(g.Key) > 0 && hasNonEmptyKV(g.Key), "field '%s' must not be empty and must not have empty keys or values", jn["Key"])
 
-	validIssue := g.Issue == 0 || (g.Issue > 0 && g.Patchset > 0 && g.BuildBucketID > 0)
-	addErrMessage(&errMsg, validIssue, "fields '%s', '%s', '%s' must all be zero or all not be zero", jn["Issue"], jn["Patchset"], jn["BuildBucketID"])
+	validIssue := types.IsMasterBranch(g.Issue) ||
+		(!types.IsMasterBranch(g.Issue) && g.Patchset > 0 && g.BuildBucketID > 0)
+	addErrMessage(&errMsg, validIssue, "fields '%s', '%s', '%s' must all be set or all not be set", jn["Issue"], jn["Patchset"], jn["BuildBucketID"])
 
 	if !ignoreResults {
 		addErrMessage(&errMsg, len(g.Results) > 0, "field '%s' must not be empty.", jn["Results"])
diff --git a/golden/go/jsonio/jsonio_test.go b/golden/go/jsonio/jsonio_test.go
index b9eef88..aed685f 100644
--- a/golden/go/jsonio/jsonio_test.go
+++ b/golden/go/jsonio/jsonio_test.go
@@ -9,11 +9,126 @@
 
 	assert "github.com/stretchr/testify/require"
 	"go.skia.org/infra/go/testutils/unittest"
+	"go.skia.org/infra/golden/go/types"
 )
 
-var (
-	testJSON = `
-	{
+func TestValidate(t *testing.T) {
+	unittest.SmallTest(t)
+
+	empty := &GoldResults{}
+	errMsgs, err := empty.Validate(false)
+	assert.Error(t, err)
+	assertErrorFields(t, errMsgs,
+		"gitHash",
+		"key",
+		"results")
+	assert.NotNil(t, errMsgs)
+
+	wrongResults := &GoldResults{
+		GitHash: "aaa27ef254ad66609606c7af0730ee062b25edf9",
+		Key:     map[string]string{"param1": "value1"},
+	}
+	errMsgs, err = wrongResults.Validate(false)
+	assert.Error(t, err)
+	assertErrorFields(t, errMsgs, "results")
+
+	wrongResults.Results = []*Result{}
+	errMsgs, err = wrongResults.Validate(false)
+	assert.Error(t, err)
+	assertErrorFields(t, errMsgs, "results")
+
+	wrongResults.Results = []*Result{
+		{Key: map[string]string{}},
+	}
+	errMsgs, err = wrongResults.Validate(false)
+	assert.Error(t, err)
+	assertErrorFields(t, errMsgs, "results")
+
+	// Now ignore the results in the validation.
+	errMsgs, err = wrongResults.Validate(true)
+	assert.NoError(t, err)
+	assert.Equal(t, []string(nil), errMsgs)
+
+	// Check that the Validate accounts for both MasterBranch and
+	// LegacyMasterBranch values.
+	legacyMaster := &GoldResults{
+		GitHash: "aaa27ef254ad66609606c7af0730ee062b25edf9",
+		Key:     map[string]string{"param1": "value1"},
+
+		Issue: types.LegacyMasterBranch,
+	}
+	_, err = legacyMaster.Validate(true)
+	assert.NoError(t, err)
+
+	master := &GoldResults{
+		GitHash: "aaa27ef254ad66609606c7af0730ee062b25edf9",
+		Key:     map[string]string{"param1": "value1"},
+
+		Issue: types.MasterBranch,
+	}
+	_, err = master.Validate(true)
+	assert.NoError(t, err)
+}
+
+func TestParseGoldResults(t *testing.T) {
+	unittest.SmallTest(t)
+	r := testParse(t, testJSON)
+
+	// Make sure some key fields come out correctly, i.e. are converted correctly from string to int.
+	assert.Equal(t, "c4711517219f333c1116f47706eb57b51b5f8fc7", r.GitHash)
+	assert.Equal(t, "Xb0VhENPSRFGnf2elVQd", r.TaskID)
+	assert.Equal(t, int64(12345), r.Issue)
+	assert.Equal(t, int64(10), r.Patchset)
+	assert.Equal(t, int64(549340494940393), r.BuildBucketID)
+
+	r = testParse(t, legacyMasterBranchJSON)
+	assert.Equal(t, types.LegacyMasterBranch, r.Issue)
+
+	r = testParse(t, masterBranchJSON)
+	assert.Equal(t, types.MasterBranch, r.Issue)
+
+	r = testParse(t, emptyMasterBranchJSON)
+	assert.Equal(t, types.MasterBranch, r.Issue)
+}
+
+func TestGenJson(t *testing.T) {
+	unittest.SmallTest(t)
+
+	// Test parsing the test JSON.
+	goldResults := testParse(t, testJSON)
+
+	// For good measure we validate.
+	_, err := goldResults.Validate(false)
+	assert.NoError(t, err)
+
+	// Encode and decode the results.
+	var buf bytes.Buffer
+	assert.NoError(t, json.NewEncoder(&buf).Encode(goldResults))
+	newGoldResults := testParse(t, buf.String())
+	assert.Equal(t, goldResults, newGoldResults)
+}
+
+func testParse(t *testing.T, jsonStr string) *GoldResults {
+	buf := bytes.NewBuffer([]byte(jsonStr))
+
+	ret, errMsg, err := ParseGoldResults(buf)
+	assert.NoError(t, err)
+	assert.Nil(t, errMsg)
+	return ret
+}
+
+func assertErrorFields(t *testing.T, errMsgs []string, expectedFields ...string) {
+	for _, msg := range errMsgs {
+		found := false
+		for _, ef := range expectedFields {
+			found = found || strings.Contains(msg, ef)
+		}
+		assert.True(t, found, fmt.Sprintf("Could not find %v in msg: %s", expectedFields, msg))
+	}
+}
+
+const (
+	testJSON = `{
 		"gitHash" : "c4711517219f333c1116f47706eb57b51b5f8fc7",
 		"key" : {
 			 "arch" : "arm64",
@@ -69,91 +184,29 @@
 					}
 			 }
 		]
- }`
+}`
+
+	legacyMasterBranchJSON = `{
+		"gitHash" : "c4711517219f333c1116f47706eb57b51b5f8fc7",
+		"key" : {
+			"arch" : "arm64"
+		},
+		"issue": "0"
+	}`
+
+	masterBranchJSON = `{
+		"gitHash" : "c4711517219f333c1116f47706eb57b51b5f8fc7",
+		"key" : {
+			"arch" : "arm64"
+		},
+		"issue": "-1"
+	}`
+
+	emptyMasterBranchJSON = `{
+		"gitHash" : "c4711517219f333c1116f47706eb57b51b5f8fc7",
+		"key" : {
+			"arch" : "arm64"
+		},
+		"issue": ""
+	}`
 )
-
-func TestValidate(t *testing.T) {
-	unittest.SmallTest(t)
-
-	empty := &GoldResults{}
-	errMsgs, err := empty.Validate(false)
-	assert.Error(t, err)
-	assertErrorFields(t, errMsgs,
-		"gitHash",
-		"key",
-		"results")
-	assert.NotNil(t, errMsgs)
-
-	wrongResults := &GoldResults{
-		GitHash: "a1b2c3d4e5f6a7b8c9d0e1f2",
-		Key:     map[string]string{"param1": "value1"},
-	}
-	errMsgs, err = wrongResults.Validate(false)
-	assert.Error(t, err)
-	assertErrorFields(t, errMsgs, "results")
-
-	wrongResults.Results = []*Result{}
-	errMsgs, err = wrongResults.Validate(false)
-	assert.Error(t, err)
-	assertErrorFields(t, errMsgs, "results")
-
-	wrongResults.Results = []*Result{
-		{Key: map[string]string{}},
-	}
-	errMsgs, err = wrongResults.Validate(false)
-	assert.Error(t, err)
-	assertErrorFields(t, errMsgs, "results")
-
-	// Now ignore the results in the validation.
-	errMsgs, err = wrongResults.Validate(true)
-	assert.NoError(t, err)
-	assert.Equal(t, []string(nil), errMsgs)
-}
-
-func TestParseGoldResults(t *testing.T) {
-	unittest.SmallTest(t)
-	r := testParse(t, testJSON)
-
-	// Make sure some key fields come out correctly, i.e. are converted correctly from string to int.
-	assert.Equal(t, "c4711517219f333c1116f47706eb57b51b5f8fc7", r.GitHash)
-	assert.Equal(t, "Xb0VhENPSRFGnf2elVQd", r.TaskID)
-	assert.Equal(t, int64(12345), r.Issue)
-	assert.Equal(t, int64(10), r.Patchset)
-	assert.Equal(t, int64(549340494940393), r.BuildBucketID)
-}
-
-func TestGenJson(t *testing.T) {
-	unittest.SmallTest(t)
-
-	// Test parsing the test JSON.
-	goldResults := testParse(t, testJSON)
-
-	// For good measure we validate.
-	_, err := goldResults.Validate(false)
-	assert.NoError(t, err)
-
-	// Encode and decode the results.
-	var buf bytes.Buffer
-	assert.NoError(t, json.NewEncoder(&buf).Encode(goldResults))
-	newGoldResults := testParse(t, buf.String())
-	assert.Equal(t, goldResults, newGoldResults)
-}
-
-func testParse(t *testing.T, jsonStr string) *GoldResults {
-	buf := bytes.NewBuffer([]byte(jsonStr))
-
-	ret, errMsg, err := ParseGoldResults(buf)
-	assert.NoError(t, err)
-	assert.Nil(t, errMsg)
-	return ret
-}
-
-func assertErrorFields(t *testing.T, errMsgs []string, expectedFields ...string) {
-	for _, msg := range errMsgs {
-		found := false
-		for _, ef := range expectedFields {
-			found = found || strings.Contains(msg, ef)
-		}
-		assert.True(t, found, fmt.Sprintf("Could not find %v in msg: %s", expectedFields, msg))
-	}
-}
diff --git a/golden/go/search/new_search.go b/golden/go/search/new_search.go
index 4cf8891..edb246c 100644
--- a/golden/go/search/new_search.go
+++ b/golden/go/search/new_search.go
@@ -104,7 +104,7 @@
 	// for the majority of queries.
 	getRefDiffs := !q.NoDiff
 
-	isTryjobSearch := q.Issue > 0
+	isTryjobSearch := !types.IsMasterBranch(q.Issue)
 
 	// Get the expectations and the current index, which we assume constant
 	// for the duration of this query.
@@ -274,7 +274,7 @@
 func (s *SearchAPI) getExpectationsFromQuery(q *Query) (ExpSlice, error) {
 	ret := make(ExpSlice, 0, 2)
 
-	if (q != nil) && (q.Issue > 0) {
+	if q != nil && !types.IsMasterBranch(q.Issue) {
 		issueExpStore := s.storages.ExpectationsStore.ForIssue(q.Issue)
 		tjExp, err := issueExpStore.Get()
 		if err != nil {
diff --git a/golden/go/search/utils_test.go b/golden/go/search/utils_test.go
index e03c24c..8cf6179 100644
--- a/golden/go/search/utils_test.go
+++ b/golden/go/search/utils_test.go
@@ -40,7 +40,7 @@
 	// issues. This requires to refresh the set of input queries.
 
 	// Ignore queries for gerrit issues right now.
-	if q.Issue > 0 {
+	if !types.IsMasterBranch(q.Issue) {
 		return 0
 	}
 
diff --git a/golden/go/storage/gcsclient.go b/golden/go/storage/gcsclient.go
index 4fb6b86..aa3658d 100644
--- a/golden/go/storage/gcsclient.go
+++ b/golden/go/storage/gcsclient.go
@@ -100,7 +100,7 @@
 	}
 
 	// We need a valid end commit or issue.
-	if b.EndCommit == nil && b.Issue <= 0 {
+	if b.EndCommit == nil && types.IsMasterBranch(b.Issue) {
 		return "", skerr.Fmt("Received empty end commit and no issue. Cannot write baseline")
 	}
 
@@ -143,11 +143,11 @@
 }
 
 // getBaselinePath returns the baseline path in GCS for the given issueID.
-// If issueID <= 0 it returns the path for the master baseline.
+// This can also return the master baseline.
 func (g *ClientImpl) getBaselinePath(commitHash string, issueID int64) string {
 	// Change the output file based on whether it's the master branch or a Gerrit issue.
 	var outPath string
-	if issueID > baseline.MasterBranch {
+	if !types.IsMasterBranch(issueID) {
 		outPath = fmt.Sprintf("issue_%d.json", issueID)
 	} else if commitHash != "" {
 		outPath = fmt.Sprintf("master_%s.json", commitHash)
diff --git a/golden/go/tryjobstore/tryjobstore.go b/golden/go/tryjobstore/tryjobstore.go
index c84060d..b76687f 100644
--- a/golden/go/tryjobstore/tryjobstore.go
+++ b/golden/go/tryjobstore/tryjobstore.go
@@ -262,7 +262,7 @@
 	if tryjob == nil {
 		// make sure we have the necessary information if there are no data to be written directly.
 		if (buildBucketID == 0) || (newValFn == nil) {
-			return sklog.FmtErrorf("Id and newValFn cannot be nil when no tryjob is provided. Update not possible")
+			return skerr.Fmt("ID and newValFn cannot be nil when no tryjob is provided. Update not possible")
 		}
 		tryjob = &Tryjob{}
 	} else {
@@ -283,7 +283,7 @@
 func (c *cloudTryjobStore) GetTryjobs(issueID int64, patchsetIDs []int64, filterDup bool, loadResults bool) ([]*Tryjob, [][]*TryjobResult, error) {
 	flatTryjobKeys, tryjobsMap, err := c.getTryjobsForIssue(issueID, patchsetIDs, false, filterDup)
 	if err != nil {
-		return nil, nil, err
+		return nil, nil, skerr.Fmt("could not locate tryjobs for issue %d: %s", issueID, err)
 	}
 
 	// Flatten the Tryjobs map and make sure the element in keys matches.
@@ -301,7 +301,7 @@
 		var err error
 		results, err = c.GetTryjobResults(tryjobs)
 		if err != nil {
-			return nil, nil, err
+			return nil, nil, skerr.Fmt("could not get tryjob results for issue %d: %s", issueID, err)
 		}
 	}
 
diff --git a/golden/go/types/types.go b/golden/go/types/types.go
index 10ca302..00047f9 100644
--- a/golden/go/types/types.go
+++ b/golden/go/types/types.go
@@ -25,8 +25,19 @@
 
 	// MAXIMUM_NAME_LENGTH is the maximum length in bytes a test name can be.
 	MAXIMUM_NAME_LENGTH = 256
+
+	MasterBranch = int64(-1)
+	// There was a time when 0 meant MasterBranch in some places and -1 was used in others.
+	// It should all be -1 now, but we have this in place to work around some old code.
+	// TODO(kjlubick): remove this on/after Aug 2019 once the 0 has faded away from the
+	// latest and greatest.
+	LegacyMasterBranch = int64(0)
 )
 
+func IsMasterBranch(b int64) bool {
+	return b == MasterBranch || b == LegacyMasterBranch
+}
+
 // Label for classifying digests.
 type Label int
 
diff --git a/golden/go/web/web.go b/golden/go/web/web.go
index 6cf8077..6d36da2 100644
--- a/golden/go/web/web.go
+++ b/golden/go/web/web.go
@@ -268,7 +268,7 @@
 		return
 	}
 
-	if issueID <= 0 {
+	if types.IsMasterBranch(issueID) || issueID < 0 {
 		httputils.ReportError(w, r, fmt.Errorf("Issue id is <= 0"), "Valid issue ID required.")
 		return
 	}
@@ -308,7 +308,7 @@
 		return
 	}
 
-	if (query.Issue > 0) || (query.BlameGroupID != "") {
+	if !types.IsMasterBranch(query.Issue) || (query.BlameGroupID != "") {
 		msg := "Search query cannot contain blame or issue information."
 		httputils.ReportError(w, r, errors.New(msg), msg)
 		return
@@ -1153,7 +1153,7 @@
 func (wh *WebHandlers) JsonBaselineHandler(w http.ResponseWriter, r *http.Request) {
 	defer metrics2.FuncTimer().Stop()
 	commitHash := ""
-	issueID := int64(0)
+	issueID := types.MasterBranch
 	issueOnly := false
 	var err error
 
@@ -1204,7 +1204,7 @@
 		return
 	}
 
-	issueID := int64(0)
+	issueID := types.MasterBranch
 	var err error
 	issueIDStr, ok := mux.Vars(r)["id"]
 	if ok {