blob: a3bfdd8e940762c0105f95facd0a5c55fde4279f [file]
// Copyright 2018 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 inventory
import (
"bytes"
"context"
"fmt"
"testing"
"github.com/golang/mock/gomock"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"go.chromium.org/luci/appengine/gaetesting"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/logging/gologger"
"go.chromium.org/luci/gae/service/datastore"
fleet "infra/appengine/crosskylabadmin/api/fleet/v1"
"infra/appengine/crosskylabadmin/app/clients"
"infra/appengine/crosskylabadmin/app/clients/mock"
"infra/appengine/crosskylabadmin/app/config"
"infra/appengine/crosskylabadmin/app/gitstore"
"infra/appengine/crosskylabadmin/app/gitstore/fakes"
"infra/libs/git"
"infra/libs/skylab/inventory"
"go.chromium.org/chromiumos/infra/proto/go/device"
)
type testFixture struct {
T *testing.T
C context.Context
Inventory *ServerImpl
FakeGerrit *fakes.GerritClient
FakeGitiles *fakes.GitilesClient
MockSwarming *mock.MockSwarmingClient
MockTracker *fleet.MockTrackerServer
}
type fakeGitClient struct {
getFile func(ctx context.Context, path string) (string, error)
}
func (f *fakeGitClient) GetFile(ctx context.Context, path string) (string, error) {
return f.getFile(ctx, path)
}
func (f *fakeGitClient) SwitchProject(ctx context.Context, project string) error {
return nil
}
func (tf *testFixture) setStableVersionFactory(stableVersionFileContent string) {
is := tf.Inventory
is.StableVersionGitClientFactory = func(c context.Context) (git.ClientInterface, error) {
gc := &fakeGitClient{}
gc.getFile = func(ctx context.Context, path string) (string, error) {
return stableVersionFileContent, nil
}
return gc, nil
}
}
// newTextFixture creates a new testFixture to be used in unittests.
//
// The function returns the created testFixture and a validation function that
// must be deferred by the caller.
//
// TODO(pprabhu) Deduplicate common setup code with frontend/test_common.go as
// part of moving all services to sub-packages. (See TODO in inventory.go)
func newTestFixture(t *testing.T) (testFixture, func()) {
return newTestFixtureWithContext(testingContext(), t)
}
func newTestFixtureWithContext(ctx context.Context, t *testing.T) (testFixture, func()) {
tf := testFixture{T: t, C: ctx}
mc := gomock.NewController(t)
tf.FakeGerrit = &fakes.GerritClient{}
tf.FakeGitiles = fakes.NewGitilesClient()
tf.MockSwarming = mock.NewMockSwarmingClient(mc)
tf.MockTracker = fleet.NewMockTrackerServer(mc)
tf.Inventory = &ServerImpl{
GerritFactory: func(context.Context, string) (gitstore.GerritClient, error) {
return tf.FakeGerrit, nil
},
GitilesFactory: func(context.Context, string) (gitstore.GitilesClient, error) {
return tf.FakeGitiles, nil
},
SwarmingFactory: func(context.Context, string) (clients.SwarmingClient, error) {
return tf.MockSwarming, nil
},
TrackerFactory: func() fleet.TrackerServer {
return tf.MockTracker
},
}
validate := func() {
mc.Finish()
}
return tf, validate
}
func testingContext() context.Context {
c := gaetesting.TestingContextWithAppID("dev~infra-crosskylabadmin")
c = config.Use(c, &config.Config{
AccessGroup: "fake-access-group",
Inventory: &config.Inventory{
GitilesHost: "some-gitiles-host",
GerritHost: "some-gerrit-host",
Project: "some-project",
Branch: "main",
LabDataPath: "data/skylab/lab.textpb",
InfrastructureDataPath: "data/skylab/server_db.textpb",
Environment: "ENVIRONMENT_STAGING",
DeviceConfigProject: "device-config-project",
DeviceConfigBranch: "main",
DeviceConfigPath: "deviceconfig/generated/device_configs.cfg",
},
Tasker: &config.Tasker{
BackgroundTaskExecutionTimeoutSecs: 3600,
BackgroundTaskExpirationSecs: 300,
},
Swarming: &config.Swarming{
Host: "https://fake-host.appspot.com",
BotPool: "ChromeOSSkylab",
FleetAdminTaskTag: "fake-tag",
LuciProjectTag: "fake-project",
},
StableVersionConfig: &config.StableVersionConfig{
GerritHost: "xxx-fake-gerrit-review.googlesource.com",
GitilesHost: "xxx-gitiles.googlesource.com",
Project: "xxx-project",
Branch: "xxx-branch",
StableVersionDataPath: "xxx-stable_version_data_path",
},
})
datastore.GetTestable(c).Consistent(true)
c = gologger.StdConfig.Use(c)
c = logging.SetLevel(c, logging.Debug)
return c
}
// testInventoryDut contains a subset of inventory fields for a DUT.
type testInventoryDut struct {
id string
hostname string
model string
pool string
}
// setGitilesDUTs sets up fake gitiles to return the inventory of
// duts provided.
// TODO(xixuan): remove this after per-file inventory is landed and tested.
func setGitilesDUTs(c context.Context, g *fakes.GitilesClient, duts []testInventoryDut) error {
return g.SetInventory(config.Get(c).Inventory, fakes.InventoryData{
Lab: inventoryBytesFromDUTs(duts),
})
}
func setSplitGitilesDuts(c context.Context, g *fakes.GitilesClient, duts []testInventoryDut) error {
dmap := make(map[string][]byte, len(duts))
for _, dut := range duts {
dmap[gitstore.InvPathForDut(dut.hostname)] = []byte(fmt.Sprintf(dutFmt, dut.hostname, dut.id, dut.pool, dut.model))
}
return g.SetSplitInventory(config.Get(c).Inventory, fakes.InventoryData{
Duts: dmap,
})
}
// dutFmt should follow the following rules:
// 1) entries should be in alphabetical order.
// 2) indent is 2 spaces, no tabs.
const dutFmt = `duts {
common {
environment: ENVIRONMENT_STAGING
hostname: "%s"
id: "%s"
labels {
critical_pools: %s
model: "%s"
}
}
}
`
func inventoryBytesFromDUTs(duts []testInventoryDut) []byte {
var ptext bytes.Buffer
for _, dut := range duts {
ptext.WriteString(fmt.Sprintf(dutFmt, dut.hostname, dut.id, dut.pool, dut.model))
}
return ptext.Bytes()
}
// testDeviceConfig contains a subset of device config fields.
type testDeviceConfig struct {
dcID DeviceConfigID
gpuFamily string
}
// setDeviceConfig sets up fake gitiles to return device configs.
func setDeviceConfigs(c context.Context, g *fakes.GitilesClient, configs []testDeviceConfig) error {
return g.SetDeviceConfigs(config.Get(c).Inventory, deviceConfigBytes(configs))
}
func deviceConfigBytes(configs []testDeviceConfig) []byte {
dcAll := device.AllConfigs{}
for _, dc := range configs {
c := device.Config{
Id: &device.ConfigId{
PlatformId: &device.PlatformId{
Value: dc.dcID.PlatformID,
},
ModelId: &device.ModelId{
Value: dc.dcID.ModelID,
},
VariantId: &device.VariantId{
Value: dc.dcID.VariantID,
},
BrandId: &device.BrandId{
Value: "",
},
},
GpuFamily: dc.gpuFamily,
}
dcAll.Configs = append(dcAll.Configs, &c)
}
marshaler := jsonpb.Marshaler{}
dc, _ := marshaler.MarshalToString(&dcAll)
return []byte(dc)
}
// testDutOnServer contains a subset of the fields in infrastructure servers.
type testInventoryServer struct {
hostname string
environment inventory.Environment
dutIDs []string
}
// setupInfraInventoryArchive sets up fake gitiles to return the inventory of
// duts provided.
func setupInfraInventoryArchive(c context.Context, g *fakes.GitilesClient, servers []testInventoryServer) error {
return g.SetInventory(config.Get(c).Inventory, fakes.InventoryData{
Infrastructure: inventoryBytesFromServers(servers),
})
}
func inventoryBytesFromServers(servers []testInventoryServer) []byte {
infra := &inventory.Infrastructure{
Servers: make([]*inventory.Server, 0, len(servers)),
}
for _, s := range servers {
status := inventory.Server_STATUS_PRIMARY
hostname := s.hostname
env := s.environment
server := &inventory.Server{
DutUids: []string{},
Environment: &env,
Hostname: &hostname,
Roles: []inventory.Server_Role{inventory.Server_ROLE_SKYLAB_DRONE},
Status: &status,
}
server.DutUids = append(server.DutUids, s.dutIDs...)
infra.Servers = append(infra.Servers, server)
}
return []byte(proto.MarshalTextString(infra))
}