Refactor testing into docker file

This means a decent scale refactor but ends with our testing being much simpler

	Key changes:
		* single Dockerfile for all services
		* tests moved into docker up so don't need to be run locally
		* configurations moved to environment
This commit is contained in:
Marc Di Luzio 2020-06-11 18:16:11 +01:00
parent 99da6c5d67
commit 14424c16ca
13 changed files with 171 additions and 107 deletions

View file

@ -6,9 +6,11 @@ COPY . .
RUN go mod download
# For /usr/share/dict/words
RUN apt-get update && apt-get install -y wamerican
RUN apt-get -q update && apt-get -qy install wamerican
# Build both executables
RUN go build -o rove-server -ldflags="-X 'github.com/mdiluz/rove/pkg/version.Version=$(git describe --always --long --dirty --tags)'" cmd/rove-server/main.go
RUN go build -o rove-accountant -ldflags="-X 'github.com/mdiluz/rove/pkg/version.Version=$(git describe --always --long --dirty --tags)'" cmd/rove-accountant/main.go
CMD [ "./rove-server" ]

View file

@ -12,22 +12,9 @@ gen:
protoc --proto_path pkg/accounts --go_out=plugins=grpc:pkg/accounts/ --go_opt=paths=source_relative pkg/accounts/accounts.proto
test:
go mod download
go build ./...
docker-compose up --build --exit-code-from=rove-tests --abort-on-container-exit
# Run the server and shut it down again to ensure our docker-compose works
ROVE_ARGS="--quit 1" docker-compose up --build --exit-code-from=rove-server --abort-on-container-exit
# Run the server and shut it down again to ensure our docker-compose works
docker-compose up -d
# Run tests with coverage and integration tags
go test -v ./... --tags=integration -cover -coverprofile=/tmp/c.out -count 1
#
docker-compose down
# Convert the coverage data to html
go tool cover -html=/tmp/c.out -o /tmp/coverage.html
# TODO: Collect and convert the coverage data to html
# go tool cover -html=/tmp/c.out -o /tmp/coverage.html
.PHONY: install test

View file

@ -1,11 +0,0 @@
FROM golang:latest
LABEL maintainer="Marc Di Luzio <marc.diluzio@gmail.com>"
WORKDIR /app
COPY . .
RUN go mod download
RUN go build -o rove-accountant -ldflags="-X 'github.com/mdiluz/rove/pkg/version.Version=$(git describe --always --long --dirty --tags)'" cmd/rove-accountant/main.go
CMD [ "./rove-accountant" ]

View file

@ -2,7 +2,6 @@ package main
import (
"context"
"flag"
"fmt"
"log"
"net"
@ -17,8 +16,8 @@ import (
"google.golang.org/grpc"
)
var address = flag.String("address", "", "port to server the accountant on")
var data = flag.String("data", os.TempDir(), "path to persistent data")
var address = os.Getenv("HOST_ADDRESS")
var data = os.Getenv("DATA_PATH")
// accountantServer is the internal object to manage the requests
type accountantServer struct {
@ -83,14 +82,12 @@ func (a *accountantServer) GetValue(_ context.Context, in *accounts.DataKey) (*a
// main
func main() {
flag.Parse()
// Verify the input
if len(*address) == 0 {
log.Fatal("No address set with --address")
if len(address) == 0 {
log.Fatal("No address set with $HOST_ADDRESS")
}
persistence.SetPath(*data)
persistence.SetPath(data)
// Initialise and load the accountant
accountant := &internal.Accountant{}
@ -99,7 +96,7 @@ func main() {
}
// Set up the RPC server and register
lis, err := net.Listen("tcp", *address)
lis, err := net.Listen("tcp", address)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
@ -118,7 +115,7 @@ func main() {
}()
// Serve the RPC server
fmt.Printf("Serving accountant on %s\n", *address)
fmt.Printf("Serving accountant on %s\n", address)
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to server gRPC: %s", err)
}

View file

@ -1,7 +1,8 @@
// +build integration
package internal
import (
"fmt"
"os"
"testing"
@ -11,30 +12,17 @@ import (
"github.com/stretchr/testify/assert"
)
// To be set by the main function
var serv rove.Server
const (
defaultAddress = "localhost:80"
)
func TestMain(m *testing.M) {
s := NewServer()
if err := s.Initialise(true); err != nil {
fmt.Println(err)
os.Exit(1)
}
serv = rove.Server(s.Addr())
go s.Run()
fmt.Printf("Test server hosted on %s", serv)
code := m.Run()
if err := s.StopAndClose(); err != nil {
fmt.Println(err)
os.Exit(1)
}
os.Exit(code)
var serv = func() rove.Server {
var address = os.Getenv("ROVE_SERVER_ADDRESS")
if len(address) == 0 {
address = defaultAddress
}
return rove.Server(address)
}()
func TestServer_Status(t *testing.T) {
status, err := serv.Status()

View file

@ -6,11 +6,13 @@ import (
"fmt"
"io"
"net/http"
"time"
"github.com/google/uuid"
"github.com/mdiluz/rove/pkg/accounts"
"github.com/mdiluz/rove/pkg/rove"
"github.com/mdiluz/rove/pkg/version"
"google.golang.org/grpc"
)
// Handler describes a function that handles any incoming request and can respond
@ -87,8 +89,10 @@ func HandleRegister(s *Server, vars map[string]string, b io.ReadCloser, w io.Wri
response.Error = "Cannot register empty name"
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
reg := accounts.RegisterInfo{Name: data.Name}
if acc, err := s.accountant.Register(context.Background(), &reg); err != nil {
if acc, err := s.accountant.Register(ctx, &reg, grpc.WaitForReady(true)); err != nil {
response.Error = err.Error()
} else if !acc.Success {
@ -125,11 +129,13 @@ func HandleCommand(s *Server, vars map[string]string, b io.ReadCloser, w io.Writ
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
key := accounts.DataKey{Account: id, Key: "rover"}
if len(id) == 0 {
response.Error = "No account ID provided"
} else if resp, err := s.accountant.GetValue(context.Background(), &key); err != nil {
} else if resp, err := s.accountant.GetValue(ctx, &key); err != nil {
response.Error = fmt.Sprintf("Provided account has no rover: %s", err)
} else if !resp.Success {
@ -155,12 +161,14 @@ func HandleRadar(s *Server, vars map[string]string, b io.ReadCloser, w io.Writer
Success: false,
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
id := vars["account"]
key := accounts.DataKey{Account: id, Key: "rover"}
if len(id) == 0 {
response.Error = "No account ID provided"
} else if resp, err := s.accountant.GetValue(context.Background(), &key); err != nil {
} else if resp, err := s.accountant.GetValue(ctx, &key); err != nil {
response.Error = fmt.Sprintf("Provided account has no rover: %s", err)
} else if !resp.Success {
@ -191,12 +199,14 @@ func HandleRover(s *Server, vars map[string]string, b io.ReadCloser, w io.Writer
Success: false,
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
id := vars["account"]
key := accounts.DataKey{Account: id, Key: "rover"}
if len(id) == 0 {
response.Error = "No account ID provided"
} else if resp, err := s.accountant.GetValue(context.Background(), &key); err != nil {
} else if resp, err := s.accountant.GetValue(ctx, &key); err != nil {
response.Error = fmt.Sprintf("Provided account has no rover: %s", err)
} else if !resp.Success {

View file

@ -1,3 +1,5 @@
// +build integration
package internal
import (
@ -9,6 +11,7 @@ import (
"path"
"testing"
"github.com/google/uuid"
"github.com/mdiluz/rove/pkg/accounts"
"github.com/mdiluz/rove/pkg/game"
"github.com/mdiluz/rove/pkg/rove"
@ -39,7 +42,7 @@ func TestHandleStatus(t *testing.T) {
}
func TestHandleRegister(t *testing.T) {
data := rove.RegisterData{Name: "one"}
data := rove.RegisterData{Name: uuid.New().String()}
b, err := json.Marshal(data)
if err != nil {
t.Error(err)
@ -57,22 +60,24 @@ func TestHandleRegister(t *testing.T) {
json.NewDecoder(response.Body).Decode(&status)
if status.Success != true {
t.Errorf("got false for /register")
t.Errorf("got false for /register: %s", status.Error)
}
}
func TestHandleCommand(t *testing.T) {
name := uuid.New().String()
s := NewServer()
s.Initialise(false) // Leave the world empty with no obstacles
reg := accounts.RegisterInfo{Name: "test"}
reg := accounts.RegisterInfo{Name: name}
acc, err := s.accountant.Register(context.Background(), &reg)
assert.NoError(t, err)
assert.True(t, acc.Success)
assert.NotNil(t, acc)
assert.True(t, acc.Success, acc.Error)
assert.NoError(t, err, "Error registering account")
// Spawn the rover rover for the account
_, inst, err := s.SpawnRoverForAccount("test")
_, inst, err := s.SpawnRoverForAccount(name)
assert.NoError(t, s.world.WarpRover(inst, vector.Vector{}))
attribs, err := s.world.RoverAttributes(inst)
@ -91,7 +96,7 @@ func TestHandleCommand(t *testing.T) {
b, err := json.Marshal(data)
assert.NoError(t, err, "Error marshalling data")
request, _ := http.NewRequest(http.MethodPost, path.Join("/", "test", "/command"), bytes.NewReader(b))
request, _ := http.NewRequest(http.MethodPost, path.Join("/", name, "/command"), bytes.NewReader(b))
response := httptest.NewRecorder()
s.router.ServeHTTP(response, request)
@ -118,16 +123,17 @@ func TestHandleCommand(t *testing.T) {
}
func TestHandleRadar(t *testing.T) {
name := uuid.New().String()
s := NewServer()
s.Initialise(false) // Spawn a clean world
reg := accounts.RegisterInfo{Name: "test"}
reg := accounts.RegisterInfo{Name: name}
acc, err := s.accountant.Register(context.Background(), &reg)
assert.NoError(t, err)
assert.True(t, acc.Success)
assert.True(t, acc.Success, acc.Error)
assert.NoError(t, err, "Error registering account")
// Spawn the rover rover for the account
attrib, id, err := s.SpawnRoverForAccount("test")
attrib, id, err := s.SpawnRoverForAccount(name)
assert.NoError(t, err)
// Warp this rover to 0,0
@ -143,7 +149,7 @@ func TestHandleRadar(t *testing.T) {
assert.NoError(t, s.world.Atlas.SetTile(rockPos, game.TileRock))
assert.NoError(t, s.world.Atlas.SetTile(emptyPos, game.TileEmpty))
request, _ := http.NewRequest(http.MethodGet, path.Join("/", "test", "/radar"), nil)
request, _ := http.NewRequest(http.MethodGet, path.Join("/", name, "/radar"), nil)
response := httptest.NewRecorder()
s.router.ServeHTTP(response, request)
@ -175,18 +181,19 @@ func TestHandleRadar(t *testing.T) {
}
func TestHandleRover(t *testing.T) {
name := uuid.New().String()
s := NewServer()
s.Initialise(true)
reg := accounts.RegisterInfo{Name: "test"}
reg := accounts.RegisterInfo{Name: name}
acc, err := s.accountant.Register(context.Background(), &reg)
assert.NoError(t, err)
assert.True(t, acc.Success)
assert.True(t, acc.Success, acc.Error)
// Spawn one rover for the account
attribs, _, err := s.SpawnRoverForAccount("test")
attribs, _, err := s.SpawnRoverForAccount(name)
assert.NoError(t, err)
request, _ := http.NewRequest(http.MethodGet, path.Join("/", "test", "/rover"), nil)
request, _ := http.NewRequest(http.MethodGet, path.Join("/", name, "/rover"), nil)
response := httptest.NewRecorder()
s.router.ServeHTTP(response, request)

View file

@ -3,11 +3,11 @@ package internal
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"sync"
"time"
@ -20,7 +20,7 @@ import (
"google.golang.org/grpc"
)
var accountantAddress = flag.String("accountant", "", "address of the accountant to connect to")
var accountantAddress = os.Getenv("ACCOUNTANT_ADDRESS")
const (
// PersistentData will allow the server to load and save it's state
@ -116,12 +116,12 @@ func (s *Server) Initialise(fillWorld bool) (err error) {
s.sync.Add(1)
// Connect to the accountant
fmt.Printf("Dialing accountant on %s\n", *accountantAddress)
clientConn, err := grpc.Dial(*accountantAddress, grpc.WithInsecure())
fmt.Printf("Dialing accountant on %s\n", accountantAddress)
s.clientConn, err = grpc.Dial(accountantAddress, grpc.WithInsecure())
if err != nil {
return err
}
s.accountant = accounts.NewAccountantClient(clientConn)
s.accountant = accounts.NewAccountantClient(s.clientConn)
// Spawn a border on the default world
if err := s.world.SpawnWorld(fillWorld); err != nil {
@ -139,6 +139,7 @@ func (s *Server) Initialise(fillWorld bool) (err error) {
}
// Start the listen
fmt.Printf("Listening on %s\n", s.server.Addr)
if s.listener, err = net.Listen("tcp", s.server.Addr); err != nil {
return err
}

View file

@ -3,8 +3,10 @@ package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"strconv"
"syscall"
"time"
@ -15,9 +17,15 @@ import (
var ver = flag.Bool("version", false, "Display version number")
var quit = flag.Int("quit", 0, "Quit after n seconds, useful for testing")
var address = flag.String("address", "", "The address to host on, automatically selected if empty")
var data = flag.String("data", "", "Directory to store persistant data, no storage if empty")
var tick = flag.Int("tick", 5, "Number of minutes per server tick (0 for no tick)")
// Address to host the server on, automatically selected if empty
var address = os.Getenv("HOST_ADDRESS")
// Path for persistent storage
var data = os.Getenv("DATA_PATH")
// The tick rate of the server in seconds
var tick = os.Getenv("TICK_RATE")
func InnerMain() {
flag.Parse()
@ -31,13 +39,23 @@ func InnerMain() {
fmt.Printf("Initialising version %s...\n", version.Version)
// Set the persistence path
persistence.SetPath(*data)
persistence.SetPath(data)
// Convert the tick rate
tickRate := 5
if len(tick) > 0 {
var err error
tickRate, err = strconv.Atoi(tick)
if err != nil {
log.Fatalf("TICK_RATE not set to valid int: %s", err)
}
}
// Create the server data
s := internal.NewServer(
internal.OptionAddress(*address),
internal.OptionAddress(address),
internal.OptionPersistentData(),
internal.OptionTick(*tick))
internal.OptionTick(tickRate))
// Initialise the server
if err := s.Initialise(true); err != nil {
@ -74,6 +92,5 @@ func InnerMain() {
}
func main() {
flag.Parse()
InnerMain()
}

View file

@ -12,9 +12,17 @@ import (
"github.com/stretchr/testify/assert"
)
var address = "localhost:80"
const (
defaultAddress = "localhost:80"
)
func Test_InnerMain(t *testing.T) {
var address = os.Getenv("ROVE_SERVER_ADDRESS")
if len(address) == 0 {
address = defaultAddress
}
// Set up the flags to act locally and use a temporary file
flag.Set("data", path.Join(os.TempDir(), uuid.New().String()))

View file

@ -4,24 +4,45 @@ volumes:
persistent-data:
services:
rove-server:
build:
context: .
dockerfile: cmd/rove-server/Dockerfile
image: rove-server:latest
ports:
- "80:80"
command: ./rove-server --address ":80" --data=/mnt/rove-server ${ROVE_ARGS} --accountant "rove-accountant:8081"
volumes:
- persistent-data:/mnt/rove-server:rw
rove-accountant:
build:
context: .
dockerfile: cmd/rove-accountant/Dockerfile
image: rove-accountant:latest
dockerfile: Dockerfile
image: rove:latest
ports:
- "8081:8081"
command: ./rove-accountant --address ":8081" --data=/mnt/rove-server
environment:
- HOST_ADDRESS=:8081
- DATA_PATH=/mnt/rove-server
volumes:
- persistent-data:/mnt/rove-server:rw
command: [ ./rove-accountant ]
rove-server:
depends_on: [ rove-accountant ]
build:
context: .
dockerfile: Dockerfile
image: rove:latest
ports:
- "80:80"
environment:
- HOST_ADDRESS=:80
- DATA_PATH=/mnt/rove-server
- ACCOUNTANT_ADDRESS=rove-accountant:8081
volumes:
- persistent-data:/mnt/rove-server:rw
rove-tests:
depends_on: [ rove-server ]
build:
context: .
dockerfile: Dockerfile
image: rove:latest
environment:
- ACCOUNTANT_ADDRESS=rove-accountant:8081
- ROVE_SERVER_ADDRESS=rove-server:80
command: [ "go", "test", "-v", "./...", "--tags=integration", "-cover", "-coverprofile=/tmp/c.out", "-count", "1" ]

1
go.mod
View file

@ -6,6 +6,7 @@ require (
github.com/golang/protobuf v1.4.2
github.com/google/uuid v1.1.1
github.com/gorilla/mux v1.7.4
github.com/onsi/ginkgo v1.12.3 // indirect
github.com/robfig/cron v1.2.0
github.com/stretchr/testify v1.6.0
github.com/tjarratt/babble v0.0.0-20191209142150-eecdf8c2339d

36
go.sum
View file

@ -8,6 +8,9 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -23,11 +26,22 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.12.3 h1:+RYp9QczoWz9zfUyLP/5SLXQVhfr6gZOoKGfQqHuLZQ=
github.com/onsi/ginkgo v1.12.3/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@ -45,22 +59,37 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -79,7 +108,14 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=