blob: da80f0bca5881248339f8ee581ab1216e486fc1e [file] [log] [blame]
// Copyright 2019 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 backend
import (
"context"
"fmt"
"testing"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
"go.chromium.org/luci/appengine/tq"
"go.chromium.org/luci/appengine/tq/tqtesting"
"go.chromium.org/luci/common/clock/testclock"
"go.chromium.org/luci/gae/impl/memory"
"go.chromium.org/luci/gae/service/datastore"
swarmingpb "go.chromium.org/luci/swarming/proto/api_v2"
"go.chromium.org/luci/gce/api/config/v1"
"go.chromium.org/luci/gce/api/tasks/v1"
"go.chromium.org/luci/gce/appengine/model"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
var someTimeAgo = timestamppb.New(time.Date(2022, 1, 1, 1, 1, 1, 0, time.UTC))
func TestDeleteBot(t *testing.T) {
t.Parallel()
Convey("deleteBot", t, func() {
dsp := &tq.Dispatcher{}
registerTasks(dsp)
swr := &mockSwarmingBotsClient{}
c := withDispatcher(memory.Use(context.Background()), dsp)
c = withSwarming(c, func(context.Context, string) swarmingpb.BotsClient { return swr })
tqt := tqtesting.GetTestable(c, dsp)
tqt.CreateQueues()
Convey("invalid", func() {
Convey("nil", func() {
err := deleteBot(c, nil)
So(err, ShouldErrLike, "unexpected payload")
})
Convey("empty", func() {
err := deleteBot(c, &tasks.DeleteBot{})
So(err, ShouldErrLike, "ID is required")
})
Convey("hostname", func() {
err := deleteBot(c, &tasks.DeleteBot{
Id: "id",
})
So(err, ShouldErrLike, "hostname is required")
})
})
Convey("valid", func() {
Convey("missing", func() {
err := deleteBot(c, &tasks.DeleteBot{
Id: "id",
Hostname: "name",
})
So(err, ShouldBeNil)
})
Convey("error", func() {
swr.err = status.Errorf(codes.Internal, "boom")
So(datastore.Put(c, &model.VM{
ID: "id",
Created: 1,
Hostname: "name",
Lifetime: 1,
URL: "url",
}), ShouldBeNil)
err := deleteBot(c, &tasks.DeleteBot{
Id: "id",
Hostname: "name",
})
So(err, ShouldErrLike, "failed to delete bot")
v := &model.VM{
ID: "id",
}
So(datastore.Get(c, v), ShouldBeNil)
So(v.Created, ShouldEqual, 1)
So(v.Hostname, ShouldEqual, "name")
So(v.URL, ShouldEqual, "url")
})
Convey("deleted", func() {
swr.err = status.Errorf(codes.NotFound, "not found")
So(datastore.Put(c, &model.VM{
ID: "id",
Created: 1,
Hostname: "name",
Lifetime: 1,
URL: "url",
}), ShouldBeNil)
err := deleteBot(c, &tasks.DeleteBot{
Id: "id",
Hostname: "name",
})
So(err, ShouldBeNil)
v := &model.VM{
ID: "id",
}
So(datastore.Get(c, v), ShouldEqual, datastore.ErrNoSuchEntity)
})
Convey("deletes", func() {
swr.deleteBotResponse = &swarmingpb.DeleteResponse{}
So(datastore.Put(c, &model.VM{
ID: "id",
Created: 1,
Hostname: "name",
Lifetime: 1,
URL: "url",
}), ShouldBeNil)
err := deleteBot(c, &tasks.DeleteBot{
Id: "id",
Hostname: "name",
})
So(err, ShouldBeNil)
v := &model.VM{
ID: "id",
}
So(datastore.Get(c, v), ShouldEqual, datastore.ErrNoSuchEntity)
})
})
})
}
func TestDeleteVM(t *testing.T) {
t.Parallel()
Convey("deleteVM", t, func() {
c := memory.Use(context.Background())
Convey("deletes", func() {
So(datastore.Put(c, &model.VM{
ID: "id",
Hostname: "name",
}), ShouldBeNil)
So(deleteVM(c, "id", "name"), ShouldBeNil)
v := &model.VM{
ID: "id",
}
So(datastore.Get(c, v), ShouldEqual, datastore.ErrNoSuchEntity)
})
Convey("deleted", func() {
So(deleteVM(c, "id", "name"), ShouldBeNil)
})
Convey("replaced", func() {
So(datastore.Put(c, &model.VM{
ID: "id",
Hostname: "name-2",
}), ShouldBeNil)
So(deleteVM(c, "id", "name-1"), ShouldBeNil)
v := &model.VM{
ID: "id",
}
So(datastore.Get(c, v), ShouldBeNil)
So(v.Hostname, ShouldEqual, "name-2")
})
})
}
func TestManageBot(t *testing.T) {
t.Parallel()
Convey("manageBot", t, func() {
dsp := &tq.Dispatcher{}
registerTasks(dsp)
swr := &mockSwarmingBotsClient{}
c := withDispatcher(memory.Use(context.Background()), dsp)
c = withSwarming(c, func(context.Context, string) swarmingpb.BotsClient { return swr })
tqt := tqtesting.GetTestable(c, dsp)
tqt.CreateQueues()
Convey("invalid", func() {
Convey("nil", func() {
err := manageBot(c, nil)
So(err, ShouldErrLike, "unexpected payload")
})
Convey("empty", func() {
err := manageBot(c, &tasks.ManageBot{})
So(err, ShouldErrLike, "ID is required")
})
})
Convey("valid", func() {
So(datastore.Put(c, &model.Config{
ID: "config",
Config: &config.Config{
CurrentAmount: 1,
},
}), ShouldBeNil)
Convey("deleted", func() {
err := manageBot(c, &tasks.ManageBot{
Id: "id",
})
So(err, ShouldBeNil)
})
Convey("creating", func() {
So(datastore.Put(c, &model.VM{
ID: "id",
Config: "config",
}), ShouldBeNil)
err := manageBot(c, &tasks.ManageBot{
Id: "id",
})
So(err, ShouldBeNil)
})
Convey("error", func() {
swr.err = status.Errorf(codes.InvalidArgument, "unexpected error")
So(datastore.Put(c, &model.VM{
ID: "id",
Config: "config",
URL: "url",
}), ShouldBeNil)
err := manageBot(c, &tasks.ManageBot{
Id: "id",
})
So(err, ShouldErrLike, "failed to fetch bot")
})
Convey("missing", func() {
Convey("deadline", func() {
swr.err = status.Errorf(codes.NotFound, "not found")
So(datastore.Put(c, &model.VM{
ID: "id",
Config: "config",
Created: 1,
Hostname: "name",
Lifetime: 1,
URL: "url",
}), ShouldBeNil)
err := manageBot(c, &tasks.ManageBot{
Id: "id",
})
So(err, ShouldBeNil)
So(tqt.GetScheduledTasks(), ShouldHaveLength, 1)
So(tqt.GetScheduledTasks()[0].Payload, ShouldHaveSameTypeAs, &tasks.DestroyInstance{})
v := &model.VM{
ID: "id",
}
So(datastore.Get(c, v), ShouldBeNil)
})
Convey("drained & new bots", func() {
swr.err = status.Errorf(codes.NotFound, "not found")
So(datastore.Put(c, &model.VM{
ID: "id",
Config: "config",
Drained: true,
Hostname: "name",
URL: "url",
Created: time.Now().Unix() - 100,
}), ShouldBeNil)
err := manageBot(c, &tasks.ManageBot{
Id: "id",
})
So(err, ShouldBeNil)
// For now, won't destroy a instance if it's set to drained or newly created but
// hasn't connected to swarming yet
So(tqt.GetScheduledTasks(), ShouldHaveLength, 0)
})
Convey("timeout", func() {
swr.err = status.Errorf(codes.NotFound, "not found")
So(datastore.Put(c, &model.VM{
ID: "id",
Config: "config",
Created: 1,
Hostname: "name",
Timeout: 1,
URL: "url",
}), ShouldBeNil)
err := manageBot(c, &tasks.ManageBot{
Id: "id",
})
So(err, ShouldBeNil)
So(tqt.GetScheduledTasks(), ShouldHaveLength, 1)
So(tqt.GetScheduledTasks()[0].Payload, ShouldHaveSameTypeAs, &tasks.DestroyInstance{})
})
Convey("wait", func() {
swr.err = status.Errorf(codes.NotFound, "not found")
So(datastore.Put(c, &model.VM{
ID: "id",
Config: "config",
Hostname: "name",
URL: "url",
}), ShouldBeNil)
err := manageBot(c, &tasks.ManageBot{
Id: "id",
})
So(err, ShouldBeNil)
})
})
Convey("found", func() {
Convey("deleted but newly created", func() {
swr.getBotResponse = &swarmingpb.BotInfo{
BotId: "id",
Deleted: true,
}
So(datastore.Put(c, &model.VM{
ID: "id",
Config: "config",
Hostname: "name",
URL: "url",
Created: time.Now().Unix() - 10,
}), ShouldBeNil)
err := manageBot(c, &tasks.ManageBot{
Id: "id",
})
So(err, ShouldBeNil)
// Won't destroy the instance if it's a newly created VM
So(tqt.GetScheduledTasks(), ShouldHaveLength, 0)
})
Convey("dead but newly created", func() {
swr.getBotResponse = &swarmingpb.BotInfo{
BotId: "id",
FirstSeenTs: someTimeAgo,
IsDead: true,
}
So(datastore.Put(c, &model.VM{
ID: "id",
Config: "config",
Hostname: "name",
URL: "url",
Created: time.Now().Unix() - 10,
}), ShouldBeNil)
err := manageBot(c, &tasks.ManageBot{
Id: "id",
})
So(err, ShouldBeNil)
// won't destroy the instance if it's a newly created VM
So(tqt.GetScheduledTasks(), ShouldHaveLength, 0)
})
})
})
})
}
func TestTerminateBot(t *testing.T) {
t.Parallel()
Convey("terminateBot", t, func() {
dsp := &tq.Dispatcher{}
registerTasks(dsp)
swr := &mockSwarmingBotsClient{}
c := withDispatcher(memory.Use(context.Background()), dsp)
c = withSwarming(c, func(context.Context, string) swarmingpb.BotsClient { return swr })
tqt := tqtesting.GetTestable(c, dsp)
tqt.CreateQueues()
Convey("invalid", func() {
Convey("nil", func() {
err := terminateBot(c, nil)
So(err, ShouldErrLike, "unexpected payload")
})
Convey("empty", func() {
err := terminateBot(c, &tasks.TerminateBot{})
So(err, ShouldErrLike, "ID is required")
})
Convey("hostname", func() {
err := terminateBot(c, &tasks.TerminateBot{
Id: "id",
})
So(err, ShouldErrLike, "hostname is required")
})
})
Convey("valid", func() {
Convey("missing", func() {
err := terminateBot(c, &tasks.TerminateBot{
Id: "id",
Hostname: "name",
})
So(err, ShouldBeNil)
})
Convey("replaced", func() {
So(datastore.Put(c, &model.VM{
ID: "id",
Hostname: "new",
}), ShouldBeNil)
err := terminateBot(c, &tasks.TerminateBot{
Id: "id",
Hostname: "old",
})
So(err, ShouldBeNil)
v := &model.VM{
ID: "id",
}
So(datastore.Get(c, v), ShouldBeNil)
So(v.Hostname, ShouldEqual, "new")
})
Convey("error", func() {
swr.err = status.Errorf(codes.Internal, "internal error")
So(datastore.Put(c, &model.VM{
ID: "id",
Hostname: "name",
}), ShouldBeNil)
err := terminateBot(c, &tasks.TerminateBot{
Id: "id",
Hostname: "name",
})
So(err, ShouldErrLike, "failed to terminate bot")
v := &model.VM{
ID: "id",
}
So(datastore.Get(c, v), ShouldBeNil)
So(v.Hostname, ShouldEqual, "name")
})
Convey("terminates", func() {
c, _ = testclock.UseTime(c, testclock.TestRecentTimeUTC)
swr.terminateBotResponse = &swarmingpb.TerminateResponse{}
So(datastore.Put(c, &model.VM{
ID: "id",
Hostname: "name",
}), ShouldBeNil)
terminateTask := tasks.TerminateBot{
Id: "id",
Hostname: "name",
}
So(terminateBot(c, &terminateTask), ShouldBeNil)
So(swr.calls, ShouldEqual, 1)
Convey("wait 1 hour before sending another terminate task", func() {
c, _ = testclock.UseTime(c, testclock.TestRecentTimeUTC.Add(time.Hour-time.Second))
So(terminateBot(c, &terminateTask), ShouldBeNil)
So(swr.calls, ShouldEqual, 1)
c, _ = testclock.UseTime(c, testclock.TestRecentTimeUTC.Add(time.Hour))
So(terminateBot(c, &terminateTask), ShouldBeNil)
So(swr.calls, ShouldEqual, 2)
})
})
})
})
}
func TestInspectSwarming(t *testing.T) {
t.Parallel()
Convey("inspectSwarmingAsync", t, func() {
dsp := &tq.Dispatcher{}
registerTasks(dsp)
c := withDispatcher(memory.Use(context.Background()), dsp)
tqt := tqtesting.GetTestable(c, dsp)
tqt.CreateQueues()
datastore.GetTestable(c).Consistent(true)
Convey("none", func() {
err := inspectSwarmingAsync(c)
So(err, ShouldBeNil)
So(tqt.GetScheduledTasks(), ShouldHaveLength, 0)
})
Convey("one", func() {
So(datastore.Put(c, &model.Config{
ID: "config-1",
Config: &config.Config{
Swarming: "https://gce-swarming.appspot.com",
},
}), ShouldBeNil)
err := inspectSwarmingAsync(c)
So(err, ShouldBeNil)
So(tqt.GetScheduledTasks(), ShouldHaveLength, 1)
})
Convey("two", func() {
So(datastore.Put(c, &model.Config{
ID: "config-1",
Config: &config.Config{
Swarming: "https://gce-swarming.appspot.com",
},
}), ShouldBeNil)
So(datastore.Put(c, &model.Config{
ID: "config-2",
Config: &config.Config{
Swarming: "https://vmleaser-swarming.appspot.com",
},
}), ShouldBeNil)
err := inspectSwarmingAsync(c)
So(err, ShouldBeNil)
So(tqt.GetScheduledTasks(), ShouldHaveLength, 2)
})
})
Convey("inspectSwarming", t, func() {
dsp := &tq.Dispatcher{}
registerTasks(dsp)
swr := &mockSwarmingBotsClient{}
c := withDispatcher(memory.Use(context.Background()), dsp)
c = withSwarming(c, func(context.Context, string) swarmingpb.BotsClient { return swr })
tqt := tqtesting.GetTestable(c, dsp)
tqt.CreateQueues()
datastore.GetTestable(c).Consistent(true)
Convey("BadInputs", func() {
Convey("nil", func() {
err := inspectSwarming(c, nil)
So(err, ShouldNotBeNil)
})
Convey("empty", func() {
err := inspectSwarming(c, &tasks.InspectSwarming{})
So(err, ShouldNotBeNil)
})
})
Convey("Swarming error", func() {
swr.err = status.Errorf(codes.Internal, "internal server error")
err := inspectSwarming(c, &tasks.InspectSwarming{
Swarming: "https://gce-swarming.appspot.com",
})
So(err, ShouldNotBeNil)
})
Convey("Ignore non-gce bot", func() {
So(datastore.Put(c, &model.VM{
ID: "vm-1",
Hostname: "vm-1-abcd",
Swarming: "https://gce-swarming.appspot.com",
}), ShouldBeNil)
So(datastore.Put(c, &model.VM{
ID: "vm-2",
Hostname: "vm-2-abcd",
Swarming: "https://gce-swarming.appspot.com",
}), ShouldBeNil)
swr.listBotsResponse = &swarmingpb.BotInfoListResponse{
Cursor: "",
Items: []*swarmingpb.BotInfo{
{
BotId: "vm-1-abcd",
FirstSeenTs: someTimeAgo,
},
{
BotId: "vm-2-abcd",
FirstSeenTs: someTimeAgo,
},
// We don't have a record for this bot in datastore
{
BotId: "vm-3-abcd",
FirstSeenTs: someTimeAgo,
},
},
}
err := inspectSwarming(c, &tasks.InspectSwarming{
Swarming: "https://gce-swarming.appspot.com",
})
So(err, ShouldBeNil)
// ignoring vm-3-abcd as we didn't see it in datastore
So(tqt.GetScheduledTasks(), ShouldHaveLength, 1)
})
Convey("Delete dead or deleted bots", func() {
So(datastore.Put(c, &model.VM{
ID: "vm-1",
Hostname: "vm-1-abcd",
Swarming: "https://gce-swarming.appspot.com",
}), ShouldBeNil)
So(datastore.Put(c, &model.VM{
ID: "vm-2",
Hostname: "vm-2-abcd",
Swarming: "https://gce-swarming.appspot.com",
}), ShouldBeNil)
swr.listBotsResponse = &swarmingpb.BotInfoListResponse{
Cursor: "",
Items: []*swarmingpb.BotInfo{
{
BotId: "vm-1-abcd",
FirstSeenTs: someTimeAgo,
IsDead: true,
},
{
BotId: "vm-2-abcd",
FirstSeenTs: someTimeAgo,
Deleted: true,
},
},
}
err := inspectSwarming(c, &tasks.InspectSwarming{
Swarming: "https://gce-swarming.appspot.com",
})
So(err, ShouldBeNil)
So(tqt.GetScheduledTasks(), ShouldHaveLength, 2)
})
Convey("HappyPath-1", func() {
So(datastore.Put(c, &model.VM{
ID: "vm-1",
Hostname: "vm-1-abcd",
Swarming: "https://gce-swarming.appspot.com",
}), ShouldBeNil)
So(datastore.Put(c, &model.VM{
ID: "vm-2",
Hostname: "vm-2-abcd",
Swarming: "https://gce-swarming.appspot.com",
}), ShouldBeNil)
So(datastore.Put(c, &model.VM{
ID: "vm-3",
Hostname: "vm-3-abcd",
Swarming: "https://vmleaser-swarming.appspot.com",
}), ShouldBeNil)
swr.listBotsResponse = &swarmingpb.BotInfoListResponse{
Cursor: "",
Items: []*swarmingpb.BotInfo{
{
BotId: "vm-1-abcd",
FirstSeenTs: someTimeAgo,
},
{
BotId: "vm-2-abcd",
FirstSeenTs: someTimeAgo,
},
},
}
err := inspectSwarming(c, &tasks.InspectSwarming{
Swarming: "https://gce-swarming.appspot.com",
})
So(err, ShouldBeNil)
So(tqt.GetScheduledTasks(), ShouldHaveLength, 1)
})
Convey("HappyPath-2", func() {
So(datastore.Put(c, &model.VM{
ID: "vm-1",
Hostname: "vm-1-abcd",
Swarming: "https://gce-swarming.appspot.com",
}), ShouldBeNil)
So(datastore.Put(c, &model.VM{
ID: "vm-2",
Hostname: "vm-2-abcd",
Swarming: "https://gce-swarming.appspot.com",
}), ShouldBeNil)
So(datastore.Put(c, &model.VM{
ID: "vm-3",
Hostname: "vm-3-abcd",
Swarming: "https://vmleaser-swarming.appspot.com",
}), ShouldBeNil)
swr.listBotsResponse = &swarmingpb.BotInfoListResponse{
Cursor: "",
Items: []*swarmingpb.BotInfo{
{
BotId: "vm-3-abcd",
FirstSeenTs: someTimeAgo,
},
},
}
err := inspectSwarming(c, &tasks.InspectSwarming{
Swarming: "https://vmleaser-swarming.appspot.com",
})
So(err, ShouldBeNil)
So(tqt.GetScheduledTasks(), ShouldHaveLength, 1)
})
Convey("HappyPath-3-pagination", func() {
So(datastore.Put(c, &model.VM{
ID: "vm-1",
Hostname: "vm-1-abcd",
Swarming: "https://gce-swarming.appspot.com",
}), ShouldBeNil)
So(datastore.Put(c, &model.VM{
ID: "vm-2",
Hostname: "vm-2-abcd",
Swarming: "https://gce-swarming.appspot.com",
}), ShouldBeNil)
So(datastore.Put(c, &model.VM{
ID: "vm-3",
Hostname: "vm-3-abcd",
Swarming: "https://gce-swarming.appspot.com",
}), ShouldBeNil)
swr.listBotsResponse = &swarmingpb.BotInfoListResponse{
Cursor: "cursor",
Items: []*swarmingpb.BotInfo{
{
BotId: "vm-1-abcd",
FirstSeenTs: someTimeAgo,
},
{
BotId: "vm-2-abcd",
FirstSeenTs: someTimeAgo,
},
},
}
err := inspectSwarming(c, &tasks.InspectSwarming{
Swarming: "https://gce-swarming.appspot.com",
})
So(err, ShouldBeNil)
// One DeleteStaleSwarmingBots tasks and one inspectSwarming task with cursor
So(tqt.GetScheduledTasks(), ShouldHaveLength, 2)
})
})
}
func TestDeleteStaleSwarmingBot(t *testing.T) {
t.Parallel()
Convey("deleteStaleSwarmingBot", t, func() {
dsp := &tq.Dispatcher{}
registerTasks(dsp)
swr := &mockSwarmingBotsClient{}
c := withDispatcher(memory.Use(context.Background()), dsp)
c = withSwarming(c, func(context.Context, string) swarmingpb.BotsClient { return swr })
tqt := tqtesting.GetTestable(c, dsp)
tqt.CreateQueues()
datastore.GetTestable(c).Consistent(true)
Convey("BadInputs", func() {
Convey("nil", func() {
err := deleteStaleSwarmingBot(c, nil)
So(err, ShouldNotBeNil)
})
Convey("empty", func() {
err := deleteStaleSwarmingBot(c, &tasks.DeleteStaleSwarmingBot{})
So(err, ShouldNotBeNil)
})
Convey("missing timestamp", func() {
err := deleteStaleSwarmingBot(c, &tasks.DeleteStaleSwarmingBot{
Id: "id-1",
})
So(err, ShouldNotBeNil)
})
})
Convey("VM issues", func() {
Convey("Missing VM", func() {
// Don't err if the VM is missing, prob deleted already
err := deleteStaleSwarmingBot(c, &tasks.DeleteStaleSwarmingBot{
Id: "id-1",
FirstSeenTs: "onceUponATime",
})
So(err, ShouldBeNil)
})
Convey("Missing URL in VM", func() {
So(datastore.Put(c, &model.VM{
ID: "vm-3",
Hostname: "vm-3-abcd",
Swarming: "https://gce-swarming.appspot.com",
}), ShouldBeNil)
// Don't err if the URL in VM is missing
err := deleteStaleSwarmingBot(c, &tasks.DeleteStaleSwarmingBot{
Id: "id-1",
FirstSeenTs: "onceUponATime",
})
So(err, ShouldBeNil)
})
})
Convey("Swarming Issues", func() {
Convey("Failed to fetch", func() {
So(datastore.Put(c, &model.VM{
ID: "vm-3",
Hostname: "vm-3-abcd",
URL: "https://www.googleapis.com/compute/v1/projects/vmleaser/zones/us-numba1-c/instances/vm-3-abcd",
Swarming: "https://gce-swarming.appspot.com",
}), ShouldBeNil)
swr.err = status.Errorf(codes.NotFound, "not found")
err := deleteStaleSwarmingBot(c, &tasks.DeleteStaleSwarmingBot{
Id: "id-1",
FirstSeenTs: "onceUponATime",
})
So(err, ShouldBeNil)
})
})
Convey("Happy paths", func() {
Convey("Bot terminated", func() {
So(datastore.Put(c, &model.VM{
ID: "vm-3",
Hostname: "vm-3-abcd",
URL: "https://www.googleapis.com/compute/v1/projects/vmleaser/zones/us-numba1-c/instances/vm-3-abcd",
Swarming: "https://gce-swarming.appspot.com",
}), ShouldBeNil)
swr.getBotResponse = &swarmingpb.BotInfo{
BotId: "vm-3-abcd",
FirstSeenTs: someTimeAgo,
}
swr.listBotEventsResponse = &swarmingpb.BotEventsResponse{
Items: []*swarmingpb.BotEventResponse{
{EventType: "bot_terminate"},
},
}
err := deleteStaleSwarmingBot(c, &tasks.DeleteStaleSwarmingBot{
Id: "vm-3",
FirstSeenTs: "2019-03-13T00:12:29.882948",
})
So(err, ShouldBeNil)
So(tqt.GetScheduledTasks(), ShouldHaveLength, 1)
})
Convey("Bot retirement", func() {
So(datastore.Put(c, &model.VM{
ID: "vm-3",
Hostname: "vm-3-abcd",
URL: "https://www.googleapis.com/compute/v1/projects/vmleaser/zones/us-numba1-c/instances/vm-3-abcd",
Swarming: "https://gce-swarming.appspot.com",
Lifetime: 99,
Created: time.Now().Unix() - 100,
}), ShouldBeNil)
swr.getBotResponse = &swarmingpb.BotInfo{
BotId: "vm-3-abcd",
FirstSeenTs: someTimeAgo,
}
swr.listBotEventsResponse = &swarmingpb.BotEventsResponse{
Items: []*swarmingpb.BotEventResponse{},
}
err := deleteStaleSwarmingBot(c, &tasks.DeleteStaleSwarmingBot{
Id: "vm-3",
FirstSeenTs: "2019-03-13T00:12:29.882948",
})
So(err, ShouldBeNil)
So(tqt.GetScheduledTasks(), ShouldHaveLength, 1)
})
Convey("Bot drained", func() {
So(datastore.Put(c, &model.VM{
ID: "vm-3",
Hostname: "vm-3-abcd",
URL: "https://www.googleapis.com/compute/v1/projects/vmleaser/zones/us-numba1-c/instances/vm-3-abcd",
Swarming: "https://gce-swarming.appspot.com",
Lifetime: 100000000,
Created: time.Now().Unix(),
Drained: true,
}), ShouldBeNil)
swr.getBotResponse = &swarmingpb.BotInfo{
BotId: "vm-3-abcd",
FirstSeenTs: someTimeAgo,
}
swr.listBotEventsResponse = &swarmingpb.BotEventsResponse{
Items: []*swarmingpb.BotEventResponse{},
}
err := deleteStaleSwarmingBot(c, &tasks.DeleteStaleSwarmingBot{
Id: "vm-3",
FirstSeenTs: "2019-03-13T00:12:29.882948",
})
So(err, ShouldBeNil)
So(tqt.GetScheduledTasks(), ShouldHaveLength, 1)
})
})
})
}
type mockSwarmingBotsClient struct {
err error
calls int
deleteBotResponse *swarmingpb.DeleteResponse
getBotResponse *swarmingpb.BotInfo
listBotEventsResponse *swarmingpb.BotEventsResponse
terminateBotResponse *swarmingpb.TerminateResponse
listBotsResponse *swarmingpb.BotInfoListResponse
swarmingpb.BotsClient // "implements" remaining RPCs by nil panicking
}
func handleCall[R any](mc *mockSwarmingBotsClient, method string, resp *R) (*R, error) {
mc.calls++
if mc.err != nil {
return nil, mc.err
}
if resp == nil {
panic(fmt.Sprintf("unexpected call to %s", method))
}
return resp, nil
}
func (mc *mockSwarmingBotsClient) GetBot(context.Context, *swarmingpb.BotRequest, ...grpc.CallOption) (*swarmingpb.BotInfo, error) {
return handleCall(mc, "GetBot", mc.getBotResponse)
}
func (mc *mockSwarmingBotsClient) TerminateBot(context.Context, *swarmingpb.TerminateRequest, ...grpc.CallOption) (*swarmingpb.TerminateResponse, error) {
return handleCall(mc, "TerminateBot", mc.terminateBotResponse)
}
func (mc *mockSwarmingBotsClient) DeleteBot(context.Context, *swarmingpb.BotRequest, ...grpc.CallOption) (*swarmingpb.DeleteResponse, error) {
return handleCall(mc, "DeleteBot", mc.deleteBotResponse)
}
func (mc *mockSwarmingBotsClient) ListBotEvents(context.Context, *swarmingpb.BotEventsRequest, ...grpc.CallOption) (*swarmingpb.BotEventsResponse, error) {
return handleCall(mc, "ListBotEvents", mc.listBotEventsResponse)
}
func (mc *mockSwarmingBotsClient) ListBots(context.Context, *swarmingpb.BotsRequest, ...grpc.CallOption) (*swarmingpb.BotInfoListResponse, error) {
return handleCall(mc, "ListBots", mc.listBotsResponse)
}