blob: 3f02c6083110469589fc31b3a5297432ce8e054a [file] [log] [blame]
// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notify
import (
"context"
"fmt"
"strings"
"sync"
"testing"
"time"
grpc "google.golang.org/grpc"
"google.golang.org/protobuf/types/known/timestamppb"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/logging/memlogger"
"go.chromium.org/luci/common/testing/ftt"
"go.chromium.org/luci/common/testing/truth/assert"
"go.chromium.org/luci/common/testing/truth/should"
"go.chromium.org/luci/gae/impl/memory"
"go.chromium.org/luci/gae/service/datastore"
tspb "go.chromium.org/luci/tree_status/proto/v1"
notifypb "go.chromium.org/luci/luci_notify/api/config"
"go.chromium.org/luci/luci_notify/common"
"go.chromium.org/luci/luci_notify/config"
)
// fakeTreeStatusClient simulates the behaviour of a real tree status instance,
// but locally, in-memory.
type fakeTreeStatusClient struct {
statusForHosts map[string]treeStatus
mu sync.Mutex
}
func (ts *fakeTreeStatusClient) getStatus(c context.Context, treeName string) (*treeStatus, error) {
ts.mu.Lock()
defer ts.mu.Unlock()
status, exists := ts.statusForHosts[treeName]
if exists {
return &status, nil
}
return nil, errors.New(fmt.Sprintf("No status for treeName %s", treeName))
}
func (ts *fakeTreeStatusClient) postStatus(c context.Context, message string, treeName string, status config.TreeCloserStatus, closingBuilderName string) error {
ts.mu.Lock()
defer ts.mu.Unlock()
var messageStatus config.TreeCloserStatus
if strings.Contains(message, "close") {
messageStatus = config.Closed
} else {
messageStatus = config.Open
}
if messageStatus != status {
return errors.Reason("message status does not match provided status").Err()
}
ts.statusForHosts[treeName] = treeStatus{
username: "buildbot@chromium.org",
message: message,
status: status,
timestamp: time.Now(),
closingBuilderName: closingBuilderName,
}
return nil
}
func TestUpdateTrees(t *testing.T) {
ftt.Run("Test environment", t, func(t *ftt.Test) {
c := memory.Use(context.Background())
c = common.SetAppIDForTest(c, "luci-notify-test")
datastore.GetTestable(c).Consistent(true)
c = memlogger.Use(c)
log := logging.Get(c).(*memlogger.MemLogger)
project1 := &config.Project{Name: "chromium", TreeClosingEnabled: true}
project1Key := datastore.KeyForObj(c, project1)
builder1 := &config.Builder{ProjectKey: project1Key, ID: "ci/builder1"}
builder2 := &config.Builder{ProjectKey: project1Key, ID: "ci/builder2"}
builder3 := &config.Builder{ProjectKey: project1Key, ID: "ci/builder3"}
builder4 := &config.Builder{ProjectKey: project1Key, ID: "ci/builder4"}
builder7 := &config.Builder{ProjectKey: project1Key, ID: "buildbucket/dart/luci.dart.ci/dart-sdk-win"}
project2 := &config.Project{Name: "infra", TreeClosingEnabled: false}
project2Key := datastore.KeyForObj(c, project2)
builder5 := &config.Builder{ProjectKey: project2Key, ID: "ci/builder5"}
builder6 := &config.Builder{ProjectKey: project2Key, ID: "ci/builder6"}
assert.Loosely(t, datastore.Put(c, project1, builder1, builder2, builder3, builder4, project2, builder5, builder6), should.BeNil)
earlierTime := time.Now().AddDate(-1, 0, 0).UTC()
evenEarlierTime := time.Now().AddDate(-2, 0, 0).UTC()
cleanup := func() {
var treeClosers []*config.TreeCloser
assert.Loosely(t, datastore.GetAll(c, datastore.NewQuery("TreeClosers"), &treeClosers), should.BeNil)
datastore.Delete(c, treeClosers)
}
// Helper function for basic tests. Sets an initial tree state, adds two tree closers
// for the tree, and checks that updateTrees sets the tree to the correct state.
testUpdateTrees := func(initialTreeStatus, builder1Status, builder2Status, expectedStatus config.TreeCloserStatus) {
var statusMessage string
if initialTreeStatus == config.Open {
statusMessage = "Open for business"
} else {
statusMessage = "Closed up"
}
ts := fakeTreeStatusClient{
statusForHosts: map[string]treeStatus{
"chromium": {
username: botUsernames[0],
message: statusMessage,
status: initialTreeStatus,
timestamp: earlierTime,
},
},
}
assert.Loosely(t, datastore.Put(c, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder1),
TreeName: "chromium",
TreeCloser: notifypb.TreeCloser{},
Status: builder1Status,
Timestamp: time.Now().UTC(),
// In fully automatic operation, this should not be considered.
BuildCreateTime: evenEarlierTime,
}), should.BeNil)
assert.Loosely(t, datastore.Put(c, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder2),
TreeName: "chromium",
TreeCloser: notifypb.TreeCloser{},
Status: builder2Status,
Timestamp: time.Now().UTC(),
// In fully automatic operation, this should not be considered.
BuildCreateTime: evenEarlierTime,
}), should.BeNil)
defer cleanup()
assert.Loosely(t, updateTrees(c, &ts), should.BeNil)
status, err := ts.getStatus(c, "chromium")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, status.status, should.Equal(expectedStatus))
}
t.Run("Open, both TCs failing, closes", func(t *ftt.Test) {
testUpdateTrees(config.Open, config.Closed, config.Closed, config.Closed)
})
t.Run("Open, 1 failing & 1 passing TC, closes", func(t *ftt.Test) {
testUpdateTrees(config.Open, config.Closed, config.Open, config.Closed)
})
t.Run("Open, both TCs passing, stays open", func(t *ftt.Test) {
testUpdateTrees(config.Open, config.Open, config.Open, config.Open)
})
t.Run("Closed, both TCs failing, stays closed", func(t *ftt.Test) {
testUpdateTrees(config.Closed, config.Closed, config.Closed, config.Closed)
})
t.Run("Closed, 1 failing & 1 passing TC, stays closed", func(t *ftt.Test) {
testUpdateTrees(config.Closed, config.Closed, config.Open, config.Closed)
})
t.Run("Closed, both TCs, stays closed", func(t *ftt.Test) {
testUpdateTrees(config.Closed, config.Closed, config.Open, config.Closed)
})
t.Run("Closed manually, doesn't re-open", func(t *ftt.Test) {
ts := fakeTreeStatusClient{
statusForHosts: map[string]treeStatus{
"chromium": {
username: "somedev@chromium.org",
message: "Closed because of reasons",
status: config.Closed,
timestamp: earlierTime,
},
},
}
assert.Loosely(t, datastore.Put(c, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder1),
TreeName: "chromium",
TreeCloser: notifypb.TreeCloser{},
Status: config.Open,
Timestamp: time.Now().UTC(),
BuildCreateTime: time.Now().UTC(),
}), should.BeNil)
defer cleanup()
assert.Loosely(t, updateTrees(c, &ts), should.BeNil)
status, err := ts.getStatus(c, "chromium")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, status.status, should.Equal(config.Closed))
})
t.Run("Opened manually, stays open with no new failures", func(t *ftt.Test) {
ts := fakeTreeStatusClient{
statusForHosts: map[string]treeStatus{
"chromium": {
username: "somedev@chromium.org",
message: "Opened, because I feel like it",
status: config.Open,
timestamp: earlierTime,
},
},
}
assert.Loosely(t, datastore.Put(c, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder1),
TreeName: "chromium",
TreeCloser: notifypb.TreeCloser{},
Status: config.Closed,
Timestamp: evenEarlierTime,
BuildCreateTime: evenEarlierTime,
}), should.BeNil)
defer cleanup()
assert.Loosely(t, updateTrees(c, &ts), should.BeNil)
status, err := ts.getStatus(c, "chromium")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, status.status, should.Equal(config.Open))
assert.Loosely(t, status.message, should.Equal("Opened, because I feel like it"))
})
t.Run("Opened manually, stays open with new failures on previously started builds", func(t *ftt.Test) {
ts := fakeTreeStatusClient{
statusForHosts: map[string]treeStatus{
"chromium": {
username: "somedev@chromium.org",
message: "Opened, because I feel like it",
status: config.Open,
timestamp: earlierTime,
},
},
}
assert.Loosely(t, datastore.Put(c, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder1),
TreeName: "chromium",
TreeCloser: notifypb.TreeCloser{},
Status: config.Closed,
Timestamp: time.Now().UTC(),
BuildCreateTime: evenEarlierTime,
}), should.BeNil)
defer cleanup()
assert.Loosely(t, updateTrees(c, &ts), should.BeNil)
status, err := ts.getStatus(c, "chromium")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, status.status, should.Equal(config.Open))
assert.Loosely(t, status.message, should.Equal("Opened, because I feel like it"))
})
t.Run("Opened manually, closes on new failure", func(t *ftt.Test) {
ts := fakeTreeStatusClient{
statusForHosts: map[string]treeStatus{
"chromium": {
username: "somedev@chromium.org",
message: "Opened, because I feel like it",
status: config.Open,
timestamp: earlierTime,
},
},
}
assert.Loosely(t, datastore.Put(c, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder1),
TreeName: "chromium",
TreeCloser: notifypb.TreeCloser{},
Status: config.Closed,
Timestamp: time.Now().UTC(),
BuildCreateTime: time.Now().UTC(),
}), should.BeNil)
defer cleanup()
assert.Loosely(t, updateTrees(c, &ts), should.BeNil)
status, err := ts.getStatus(c, "chromium")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, status.status, should.Equal(config.Closed))
assert.Loosely(t, status.closingBuilderName, should.Equal("projects/chromium/buckets/ci/builders/builder1"))
})
t.Run("Opened manually, closes on new failure, with old builder ID", func(t *ftt.Test) {
ts := fakeTreeStatusClient{
statusForHosts: map[string]treeStatus{
"chromium": {
username: "somedev@chromium.org",
message: "Opened, because I feel like it",
status: config.Open,
timestamp: earlierTime,
},
},
}
assert.Loosely(t, datastore.Put(c, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder7),
TreeName: "chromium",
TreeCloser: notifypb.TreeCloser{},
Status: config.Closed,
Timestamp: time.Now().UTC(),
BuildCreateTime: time.Now().UTC(),
}), should.BeNil)
defer cleanup()
assert.Loosely(t, updateTrees(c, &ts), should.BeNil)
status, err := ts.getStatus(c, "chromium")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, status.status, should.Equal(config.Closed))
assert.Loosely(t, status.closingBuilderName, should.BeEmpty)
})
t.Run("Multiple trees", func(t *ftt.Test) {
ts := fakeTreeStatusClient{
statusForHosts: map[string]treeStatus{
"chromium": {
username: botUsernames[0],
message: "Closed up",
status: config.Closed,
timestamp: evenEarlierTime,
},
"v8": {
username: botUsernames[0],
message: "Open for business",
status: config.Open,
timestamp: evenEarlierTime,
},
},
}
assert.Loosely(t, datastore.Put(c, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder1),
TreeName: "chromium",
TreeCloser: notifypb.TreeCloser{},
Status: config.Open,
Timestamp: time.Now().UTC(),
}), should.BeNil)
assert.Loosely(t, datastore.Put(c, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder2),
TreeName: "chromium",
TreeCloser: notifypb.TreeCloser{},
Status: config.Open,
Timestamp: time.Now().UTC(),
}), should.BeNil)
assert.Loosely(t, datastore.Put(c, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder3),
TreeName: "chromium",
TreeCloser: notifypb.TreeCloser{},
Status: config.Open,
Timestamp: time.Now().UTC(),
}), should.BeNil)
assert.Loosely(t, datastore.Put(c, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder2),
TreeName: "v8",
TreeCloser: notifypb.TreeCloser{},
Status: config.Open,
Timestamp: time.Now().UTC(),
}), should.BeNil)
assert.Loosely(t, datastore.Put(c, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder3),
TreeName: "v8",
TreeCloser: notifypb.TreeCloser{},
Status: config.Open,
Timestamp: time.Now().UTC(),
}), should.BeNil)
assert.Loosely(t, datastore.Put(c, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder4),
TreeName: "v8",
TreeCloser: notifypb.TreeCloser{},
Status: config.Closed,
Timestamp: earlierTime,
Message: "Correct message",
}), should.BeNil)
defer cleanup()
assert.Loosely(t, updateTrees(c, &ts), should.BeNil)
status, err := ts.getStatus(c, "chromium")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, status.status, should.Equal(config.Open))
assert.Loosely(t, status.message, should.HavePrefix("Tree is open (Automatic: "))
status, err = ts.getStatus(c, "v8")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, status.status, should.Equal(config.Closed))
assert.Loosely(t, status.message, should.Equal("Tree is closed (Automatic: Correct message)"))
assert.Loosely(t, status.closingBuilderName, should.Equal("projects/chromium/buckets/ci/builders/builder4"))
})
t.Run("Doesn't open when build is older than last status update", func(t *ftt.Test) {
// This test replicates the likely state just after we've
// automatically closed the tree: the tree is closed with
// our username, and there is some failing TreeCloser older
// than the status update.
ts := fakeTreeStatusClient{
statusForHosts: map[string]treeStatus{
"chromium": {
username: botUsernames[0],
message: "Tree is closed (Automatic: some builder failed)",
status: config.Closed,
timestamp: earlierTime,
},
},
}
assert.Loosely(t, datastore.Put(c, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder1),
TreeName: "chromium",
TreeCloser: notifypb.TreeCloser{},
Status: config.Open,
Timestamp: evenEarlierTime,
BuildCreateTime: evenEarlierTime,
}, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder2),
TreeName: "chromium",
TreeCloser: notifypb.TreeCloser{},
Status: config.Closed,
Timestamp: evenEarlierTime,
BuildCreateTime: evenEarlierTime,
}), should.BeNil)
defer cleanup()
assert.Loosely(t, updateTrees(c, &ts), should.BeNil)
status, err := ts.getStatus(c, "chromium")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, status.status, should.Equal(config.Closed))
assert.Loosely(t, status.message, should.Equal("Tree is closed (Automatic: some builder failed)"))
})
t.Run("Doesn't open when a builder is still failing", func(t *ftt.Test) {
// This test replicates the likely state after we've automatically
// closed the tree, but some other builder has had a successful
// build.
ts := fakeTreeStatusClient{
statusForHosts: map[string]treeStatus{
"chromium": {
username: botUsernames[0],
message: "Tree is closed (Automatic: some builder failed)",
status: config.Closed,
timestamp: earlierTime,
},
},
}
assert.Loosely(t, datastore.Put(c, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder1),
TreeName: "chromium",
TreeCloser: notifypb.TreeCloser{},
Status: config.Open,
Timestamp: time.Now().UTC(),
BuildCreateTime: evenEarlierTime,
}, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder2),
TreeName: "chromium",
TreeCloser: notifypb.TreeCloser{},
Status: config.Closed,
Timestamp: evenEarlierTime,
BuildCreateTime: evenEarlierTime,
}), should.BeNil)
defer cleanup()
assert.Loosely(t, updateTrees(c, &ts), should.BeNil)
status, err := ts.getStatus(c, "chromium")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, status.status, should.Equal(config.Closed))
assert.Loosely(t, status.message, should.Equal("Tree is closed (Automatic: some builder failed)"))
})
t.Run("Multiple projects", func(t *ftt.Test) {
ts := fakeTreeStatusClient{
statusForHosts: map[string]treeStatus{
"chromium": {
username: botUsernames[0],
message: "Tree is closed (Automatic: some builder failed)",
status: config.Closed,
timestamp: earlierTime,
},
"infra": {
username: botUsernames[0],
message: "Tree is open (Automatic: Yes!)",
status: config.Open,
timestamp: earlierTime,
},
},
}
assert.Loosely(t, datastore.Put(c, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder1),
TreeName: "chromium",
TreeCloser: notifypb.TreeCloser{},
Status: config.Open,
Timestamp: time.Now().UTC(),
}, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder5),
TreeName: "infra",
TreeCloser: notifypb.TreeCloser{},
Status: config.Closed,
Timestamp: time.Now().UTC(),
Message: "Close it up!",
}), should.BeNil)
defer cleanup()
assert.Loosely(t, updateTrees(c, &ts), should.BeNil)
status, err := ts.getStatus(c, "chromium")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, status.status, should.Equal(config.Open))
assert.Loosely(t, status.message, should.HavePrefix("Tree is open (Automatic: "))
status, err = ts.getStatus(c, "infra")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, status.status, should.Equal(config.Open))
assert.Loosely(t, status.message, should.HavePrefix("Tree is open (Automatic: "))
hasExpectedLog := false
for _, log := range log.Messages() {
if log.Level == logging.Info {
hasExpectedLog = true
assert.Loosely(t, log.Msg, should.Equal(`Would update status for infra to "Tree is closed (Automatic: Close it up!)"`))
}
}
assert.Loosely(t, hasExpectedLog, should.BeTrue)
})
t.Run("Multiple projects, overlapping tree status hosts", func(t *ftt.Test) {
ts := fakeTreeStatusClient{
statusForHosts: map[string]treeStatus{
"chromium": {
username: botUsernames[0],
message: "Tree is open (Flake)",
status: config.Open,
timestamp: earlierTime,
},
"infra": {
username: botUsernames[0],
message: "Tree is closed (Automatic: Some builder failed)",
status: config.Closed,
timestamp: earlierTime,
},
},
}
assert.Loosely(t, datastore.Put(c, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder1),
TreeName: "chromium",
TreeCloser: notifypb.TreeCloser{},
Status: config.Open,
Timestamp: time.Now().UTC(),
}, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder5),
TreeName: "chromium",
TreeCloser: notifypb.TreeCloser{},
Status: config.Closed,
Timestamp: time.Now().UTC(),
}, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder2),
TreeName: "infra",
TreeCloser: notifypb.TreeCloser{},
Status: config.Open,
Timestamp: time.Now().UTC(),
}, &config.TreeCloser{
BuilderKey: datastore.KeyForObj(c, builder6),
TreeName: "infra",
TreeCloser: notifypb.TreeCloser{},
Status: config.Closed,
Timestamp: time.Now().UTC(),
}), should.BeNil)
defer cleanup()
assert.Loosely(t, updateTrees(c, &ts), should.BeNil)
status, err := ts.getStatus(c, "chromium")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, status.status, should.Equal(config.Open))
assert.Loosely(t, status.message, should.Equal("Tree is open (Flake)"))
status, err = ts.getStatus(c, "infra")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, status.status, should.Equal(config.Open))
assert.Loosely(t, status.message, should.HavePrefix("Tree is open (Automatic: "))
})
})
}
func TestHttpTreeStatusClient(t *testing.T) {
ftt.Run("Test environment for httpTreeStatusClient", t, func(t *ftt.Test) {
c := memory.Use(context.Background())
c = common.SetAppIDForTest(c, "luci-notify-test")
fakePrpcClient := &fakePRPCTreeStatusClient{}
ts := httpTreeStatusClient{fakePrpcClient}
t.Run("getStatus, open tree", func(t *ftt.Test) {
status, err := ts.getStatus(c, "chromium")
assert.Loosely(t, err, should.BeNil)
expectedTime := time.Date(2020, time.March, 31, 5, 33, 52, 682351000, time.UTC)
assert.Loosely(t, status, should.Resemble(&treeStatus{
username: "someone@google.com",
message: "Tree is throttled (win rel 32 appears to be a goma flake. the other builds seem to be charging ahead OK. will fully open / fully close if win32 does/doesn't improve)",
status: config.Closed,
timestamp: expectedTime,
}))
})
t.Run("getStatus, closed tree", func(t *ftt.Test) {
status, err := ts.getStatus(c, "v8")
assert.Loosely(t, err, should.BeNil)
expectedTime := time.Date(2020, time.April, 2, 15, 21, 39, 981072000, time.UTC)
assert.Loosely(t, status, should.Resemble(&treeStatus{
username: "someone-else@google.com",
message: "open (flake?)",
status: config.Open,
timestamp: expectedTime,
}))
})
t.Run("postStatus", func(t *ftt.Test) {
err := ts.postStatus(c, "open for business", "dart", config.Open, "projects/chromium/buckets/bucket/builders/builder")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, fakePrpcClient.latestStatus, should.NotBeNil)
assert.Loosely(t, fakePrpcClient.latestStatus.Message, should.Equal("open for business"))
assert.Loosely(t, fakePrpcClient.latestStatus.GeneralState, should.Equal(tspb.GeneralState_OPEN))
assert.Loosely(t, fakePrpcClient.latestStatus.ClosingBuilderName, should.Equal("projects/chromium/buckets/bucket/builders/builder"))
})
})
}
type fakePRPCTreeStatusClient struct {
latestStatus *tspb.Status
}
func (c *fakePRPCTreeStatusClient) ListStatus(ctx context.Context, in *tspb.ListStatusRequest, opts ...grpc.CallOption) (*tspb.ListStatusResponse, error) {
return nil, errors.Reason("Not implemented").Err()
}
func (c *fakePRPCTreeStatusClient) GetStatus(ctx context.Context, in *tspb.GetStatusRequest, opts ...grpc.CallOption) (*tspb.Status, error) {
if c.latestStatus != nil {
return c.latestStatus, nil
}
if in.Name == "trees/v8/status/latest" {
return &tspb.Status{
Name: in.Name,
GeneralState: tspb.GeneralState_OPEN,
Message: "open (flake?)",
CreateUser: "someone-else@google.com",
CreateTime: timestamppb.New(time.Date(2020, time.April, 2, 15, 21, 39, 981072000, time.UTC)),
}, nil
}
if in.Name == "trees/chromium/status/latest" {
return &tspb.Status{
Name: "trees/chromium/status/fallback",
GeneralState: tspb.GeneralState_CLOSED,
Message: "Tree is throttled (win rel 32 appears to be a goma flake. the other builds seem to be charging ahead OK. will fully open / fully close if win32 does/doesn't improve)",
CreateUser: "someone@google.com",
CreateTime: timestamppb.New(time.Date(2020, time.March, 31, 5, 33, 52, 682351000, time.UTC)),
}, nil
}
return nil, fmt.Errorf("status with name %q not found", in.Name)
}
func (c *fakePRPCTreeStatusClient) CreateStatus(ctx context.Context, in *tspb.CreateStatusRequest, opts ...grpc.CallOption) (*tspb.Status, error) {
c.latestStatus = in.Status
return in.Status, nil
}