blob: 9f2d65a324d5deda93c5cc2946b5b64d596d62a7 [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package agent
import (
"context"
"sync"
"time"
"github.com/golang/protobuf/proto"
"google.golang.org/grpc"
"infra/appengine/drone-queen/api"
"infra/cmd/drone-agent/internal/agent/state"
"infra/cmd/drone-agent/internal/bot"
)
// newPersistentBot returns a FakeBot that does not exit when
// drained/terminated. The Stop method must be called to explicitly
// stop the bot.
func newPersistentBot() *bot.FakeBot {
b := bot.NewFakeBot()
b.DrainFunc = func(*bot.FakeBot) error { return nil }
b.TerminateFunc = func(*bot.FakeBot) error { return nil }
return b
}
// startFakeBot is a fake implementation of Agent.startBotFunc.
func startFakeBot(bot.Config) (bot.Bot, error) {
return bot.NewFakeBot(), nil
}
// stateSpyFactory implements a method that can be used as
// wrapStateFunc to inspect created states.
type stateSpyFactory struct {
states chan *stateSpy
}
func newStateSpyFactory() stateSpyFactory {
return stateSpyFactory{
// These channels need to have big enough buffers to
// capture events needed by tests. Events that
// overfill the channel buffers are discarded.
states: make(chan *stateSpy, 1),
}
}
// wrapState wraps a state in a stateSpy and sends it to the channel.
// This method is used as wrapStateFunc.
func (f stateSpyFactory) wrapState(s *state.State) stateInterface {
s2 := &stateSpy{
State: s,
// These channels need to have big enough buffers to
// capture events needed by tests. Events that
// overfill the channel buffers are discarded.
addedDUTs: make(chan string, 8),
terminatedDUTs: make(chan string, 8),
drainedDUTs: make(chan string, 8),
terminatedAll: make(chan struct{}, 1),
drainedAll: make(chan struct{}, 1),
blocked: make(chan struct{}, 1),
}
select {
case f.states <- s2:
default:
}
return s2
}
// stateSpy wraps a state and allows inspecting state manipulations.
type stateSpy struct {
*state.State
addedDUTs chan string
terminatedDUTs chan string
drainedDUTs chan string
terminatedAll chan struct{}
drainedAll chan struct{}
blocked chan struct{}
}
func (s *stateSpy) AddDUT(dutID string) {
s.State.AddDUT(dutID)
select {
case s.addedDUTs <- dutID:
default:
}
}
func (s *stateSpy) TerminateDUT(dutID string) {
s.State.TerminateDUT(dutID)
select {
case s.terminatedDUTs <- dutID:
default:
}
}
func (s *stateSpy) DrainDUT(dutID string) {
s.State.DrainDUT(dutID)
select {
case s.drainedDUTs <- dutID:
default:
}
}
func (s *stateSpy) TerminateAll() {
s.State.TerminateAll()
select {
case s.terminatedAll <- struct{}{}:
default:
}
}
func (s *stateSpy) DrainAll() {
s.State.DrainAll()
select {
case s.drainedAll <- struct{}{}:
default:
}
}
func (s *stateSpy) BlockDUTs() {
s.State.BlockDUTs()
select {
case s.blocked <- struct{}{}:
default:
}
}
// stubClient is a stub implementation of api.DroneClient. The
// response can be modified, but writers should use withLock if
// modifying the response concurrent with other users.
type stubClient struct {
m sync.Mutex
res *api.ReportDroneResponse
err error
}
var endOfTime = protoTime(time.Date(9999, 1, 2, 3, 4, 5, 6, time.UTC))
// newStubClient makes a new stubClient with sane default values.
// Tests MUST NOT rely on the exact drone UUID; the test should
// explicitly set a UUID.
func newStubClient() *stubClient {
return &stubClient{
res: &api.ReportDroneResponse{
Status: api.ReportDroneResponse_OK,
DroneUuid: "3679687c-b341-4422-ad6d-a935887ed6a7",
ExpirationTime: endOfTime,
},
}
}
// withLock calls the function with a lock. This is used in tests to
// modify the stubbed response.
func (c *stubClient) withLock(f func()) {
c.m.Lock()
f()
c.m.Unlock()
}
func (c *stubClient) ReportDrone(ctx context.Context, req *api.ReportDroneRequest, _ ...grpc.CallOption) (*api.ReportDroneResponse, error) {
c.m.Lock()
defer c.m.Unlock()
// Make a copy to prevent concurrent access.
res := proto.Clone(c.res).(*api.ReportDroneResponse)
return res, c.err
}
func (c *stubClient) ReleaseDuts(ctx context.Context, req *api.ReleaseDutsRequest, _ ...grpc.CallOption) (*api.ReleaseDutsResponse, error) {
return &api.ReleaseDutsResponse{}, nil
}
type spyClient struct {
*stubClient
reports chan *api.ReportDroneRequest
}
func newSpyClient() *spyClient {
return &spyClient{
stubClient: newStubClient(),
// These channels need to have big enough buffers to
// capture events needed by tests. Events that
// overfill the channel buffers are discarded.
reports: make(chan *api.ReportDroneRequest, 2),
}
}
func (c *spyClient) ReportDrone(ctx context.Context, req *api.ReportDroneRequest, o ...grpc.CallOption) (*api.ReportDroneResponse, error) {
select {
case c.reports <- req:
default:
}
return c.stubClient.ReportDrone(ctx, req, o...)
}