blob: 06d9fbc61e9d78c29014332ed7e6d030f2873c6e [file] [log] [blame] [edit]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package recovery
import (
"context"
"encoding/base64"
"errors"
"io"
"testing"
"github.com/google/go-cmp/cmp"
"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/infra/cros/recovery/config"
"go.chromium.org/infra/cros/recovery/internal/execs"
"go.chromium.org/infra/cros/recovery/karte"
"go.chromium.org/infra/cros/recovery/logger"
"go.chromium.org/infra/cros/recovery/logger/metrics"
"go.chromium.org/infra/cros/recovery/scopes"
"go.chromium.org/infra/cros/recovery/tlw"
"go.chromium.org/infra/libs/skylab/buildbucket"
)
// Test cases for TestDUTPlans
var dutPlansCases = []struct {
name string
setupType tlw.DUTSetupType
taskName buildbucket.TaskName
expPlanNames []string
ok bool
}{
{
"default no task",
tlw.DUTSetupType_UNSPECIFIED,
buildbucket.TaskName(""),
nil,
false,
},
{
"default recovery",
tlw.DUTSetupType_UNSPECIFIED,
buildbucket.Recovery,
nil,
false,
},
{
"default MH recovery",
tlw.DUTSetupType_UNSPECIFIED,
buildbucket.MHRecovery,
nil,
false,
},
{
"default deploy",
tlw.DUTSetupType_UNSPECIFIED,
buildbucket.Deploy,
nil,
false,
},
{
"mh deploy",
tlw.DUTSetupType_UNSPECIFIED,
buildbucket.MHDeploy,
nil,
false,
},
{
"default custom",
tlw.DUTSetupType_UNSPECIFIED,
buildbucket.Custom,
nil,
false,
},
{
"cros no task",
tlw.DUTSetupType_CROS,
buildbucket.TaskName(""),
nil,
false,
},
{
"cros recovery",
tlw.DUTSetupType_CROS,
buildbucket.Recovery,
[]string{
config.PlanCrOSBase,
config.PlanDolos,
config.PlanAMT,
config.PlanServo,
config.PlanBluetoothPeer,
config.PlanWifiRouter,
config.PlanCrOS,
config.PlanChameleon,
config.PlanHMR,
config.PlanPASIT,
config.PlanClosing},
true,
},
{
"cros clank recovery",
tlw.DUTSetupType_CLANK_ONLY,
buildbucket.Recovery,
[]string{
config.PlanCrOSBase,
config.PlanServo,
config.PlanCrOS,
config.PlanClosing},
true,
},
{
"cros android recovery",
tlw.DUTSetupType_CROS_ANDROID,
buildbucket.Recovery,
[]string{
config.PlanCrOSBase,
config.PlanDolos,
config.PlanAMT,
config.PlanServo,
config.PlanBluetoothPeer,
config.PlanWifiRouter,
config.PlanCrOS,
config.PlanChameleon,
config.PlanHMR,
config.PlanPASIT,
config.PlanClosing},
true,
},
{
"cros verify",
tlw.DUTSetupType_CROS,
buildbucket.Verify,
[]string{
config.PlanCrOSBase,
config.PlanCrOS},
true,
},
{
"cros clank verify",
tlw.DUTSetupType_CLANK_ONLY,
buildbucket.Verify,
[]string{
config.PlanCrOSBase,
config.PlanCrOS},
true,
},
{
"cros android verify",
tlw.DUTSetupType_CROS_ANDROID,
buildbucket.Verify,
[]string{
config.PlanCrOSBase,
config.PlanCrOS},
true,
},
{
"MH recovery",
tlw.DUTSetupType_CROS,
buildbucket.MHRecovery,
[]string{
config.PlanCrOSBase,
config.PlanDolos,
config.PlanAMT,
config.PlanServo,
config.PlanBluetoothPeer,
config.PlanWifiRouter,
config.PlanCrOS,
config.PlanChameleon,
config.PlanHMR,
config.PlanPASIT,
config.PlanClosing},
true,
},
{
"MH recovery",
tlw.DUTSetupType_CLANK_ONLY,
buildbucket.MHRecovery,
[]string{
config.PlanCrOSBase,
config.PlanServo,
config.PlanCrOS,
config.PlanClosing},
true,
}, {
"MH recovery",
tlw.DUTSetupType_CROS_ANDROID,
buildbucket.MHRecovery,
[]string{
config.PlanCrOSBase,
config.PlanDolos,
config.PlanAMT,
config.PlanServo,
config.PlanBluetoothPeer,
config.PlanWifiRouter,
config.PlanCrOS,
config.PlanChameleon,
config.PlanHMR,
config.PlanPASIT,
config.PlanClosing},
true,
},
{
"cros deploy",
tlw.DUTSetupType_CROS,
buildbucket.Deploy,
[]string{
config.PlanCrOSBase,
config.PlanServoFwUpdate,
config.PlanDolos,
config.PlanAMT,
config.PlanServo,
config.PlanCrOSDeploy,
config.PlanCrOS,
config.PlanChameleon,
config.PlanBluetoothPeer,
config.PlanWifiRouter,
config.PlanHMR,
config.PlanPASIT,
config.PlanClosing,
},
true,
},
{
"mh deploy",
tlw.DUTSetupType_CROS,
buildbucket.MHDeploy,
[]string{
config.PlanCrOSBase,
config.PlanServoFwUpdate,
config.PlanDolos,
config.PlanAMT,
config.PlanServo,
config.PlanCrOSDeploy,
config.PlanCrOS,
config.PlanChameleon,
config.PlanBluetoothPeer,
config.PlanWifiRouter,
config.PlanHMR,
config.PlanPASIT,
config.PlanClosing,
},
true,
},
{
"cros custom",
tlw.DUTSetupType_CROS,
buildbucket.Custom,
nil,
false,
},
{
"labstation no task",
tlw.DUTSetupType_CROS,
buildbucket.TaskName(""),
nil,
false,
},
{
"labstation recovery",
tlw.DUTSetupType_LABSTATION,
buildbucket.Recovery,
[]string{config.PlanCrOS},
true,
},
{
"labstation deploy",
tlw.DUTSetupType_LABSTATION,
buildbucket.Deploy,
[]string{config.PlanCrOS},
true,
},
{
"labstation custom",
tlw.DUTSetupType_LABSTATION,
buildbucket.Custom,
nil,
false,
},
{
"android no task",
tlw.DUTSetupType_ANDROID,
buildbucket.TaskName(""),
nil,
false,
},
{
"android recovery",
tlw.DUTSetupType_ANDROID,
buildbucket.Recovery,
[]string{config.PlanAndroid, config.PlanClosing},
true,
},
{
"android deploy",
tlw.DUTSetupType_ANDROID,
buildbucket.Deploy,
[]string{config.PlanAndroid, config.PlanClosing},
true,
},
{
"android custom",
tlw.DUTSetupType_ANDROID,
buildbucket.Custom,
nil,
false,
},
{
"android no task",
tlw.DUTSetupType_ANDROID,
buildbucket.TaskName(""),
nil,
false,
},
{
"chromeos audit RPM",
tlw.DUTSetupType_CROS,
buildbucket.AuditRPM,
[]string{
config.PlanCrOSBase,
config.PlanServo,
config.PlanCrOSAudit,
config.PlanClosing,
},
true,
},
{
"chromeos audit USB-key",
tlw.DUTSetupType_CROS,
buildbucket.AuditUSB,
[]string{
config.PlanCrOSBase,
config.PlanServo,
config.PlanCrOSAudit,
config.PlanClosing,
},
true,
},
{
"chromeos audit storage",
tlw.DUTSetupType_CROS,
buildbucket.AuditStorage,
[]string{
config.PlanCrOSBase,
config.PlanCrOSAudit,
},
true,
},
{
"labstation does not have audit RPM",
tlw.DUTSetupType_LABSTATION,
buildbucket.AuditRPM,
nil,
false,
},
{
"android does not have audit RPM",
tlw.DUTSetupType_ANDROID,
buildbucket.AuditRPM,
nil,
false,
},
{
"cros deep recovery",
tlw.DUTSetupType_CROS,
buildbucket.DeepRecovery,
[]string{
config.PlanCrOSBase,
config.PlanDolos,
config.PlanAMT,
config.PlanServoDeepRepair,
config.PlanCrOSDeepRepair,
config.PlanServo,
config.PlanBluetoothPeer,
config.PlanWifiRouter,
config.PlanCrOS,
config.PlanChameleon,
config.PlanHMR,
config.PlanPASIT,
config.PlanClosing,
},
true,
},
{
"cros android deep recovery",
tlw.DUTSetupType_CROS_ANDROID,
buildbucket.DeepRecovery,
[]string{
config.PlanCrOSBase,
config.PlanDolos,
config.PlanAMT,
config.PlanServoDeepRepair,
config.PlanCrOSDeepRepair,
config.PlanServo,
config.PlanBluetoothPeer,
config.PlanWifiRouter,
config.PlanCrOS,
config.PlanChameleon,
config.PlanHMR,
config.PlanPASIT,
config.PlanClosing,
},
true,
},
{
"cros deep recovery",
tlw.DUTSetupType_LABSTATION,
buildbucket.DeepRecovery,
[]string{config.PlanCrOS},
true,
},
{
"cros browser DUT recovery",
tlw.DUTSetupType_CROS_BROWSER,
buildbucket.Recovery,
[]string{
config.PlanCrOSBase,
config.PlanServo,
config.PlanCrOS,
config.PlanClosing,
},
true,
},
{
"cros browser DUT deep recovery",
tlw.DUTSetupType_CROS_BROWSER,
buildbucket.DeepRecovery,
[]string{
config.PlanCrOSBase,
config.PlanServoDeepRepair,
config.PlanCrOSDeepRepair,
config.PlanServo,
config.PlanCrOS,
config.PlanClosing,
},
true,
},
{
"cros browser DUT deploy",
tlw.DUTSetupType_CROS_BROWSER,
buildbucket.Deploy,
[]string{
config.PlanCrOSBase,
config.PlanServoFwUpdate,
config.PlanServo,
config.PlanCrOSDeploy,
config.PlanCrOS,
config.PlanClosing,
},
true,
},
{
"cros dry run",
tlw.DUTSetupType_CROS_BROWSER,
buildbucket.DryRun,
nil,
true,
},
{
"android dry run",
tlw.DUTSetupType_ANDROID,
buildbucket.DryRun,
nil,
true,
},
{
"labstation dry run",
tlw.DUTSetupType_LABSTATION,
buildbucket.DryRun,
nil,
true,
},
{
"cros post test",
tlw.DUTSetupType_CROS,
buildbucket.PostTest,
[]string{
config.PlanCrOSBase,
config.PlanDolos,
config.PlanAMT,
config.PlanServo,
config.PlanBluetoothPeer,
config.PlanWifiRouter,
config.PlanCrOS,
config.PlanChameleon,
config.PlanHMR,
config.PlanPASIT,
config.PlanClosing,
},
true,
},
{
"cros browser lightweight verifier",
tlw.DUTSetupType_CROS_BROWSER,
buildbucket.PostTest,
nil,
false,
},
{
"android lightweight verifier",
tlw.DUTSetupType_ANDROID,
buildbucket.PostTest,
nil,
false,
},
{
"android labstation verifier",
tlw.DUTSetupType_LABSTATION,
buildbucket.PostTest,
nil,
false,
},
{
"cros dual cros-android labqual",
tlw.DUTSetupType_CROS_ANDROID,
buildbucket.Labqual,
[]string{
config.PlanServo,
config.PlanCrOSBase,
config.PlanCrOS,
config.PlanClosing},
true,
},
}
func mockDut() *tlw.Dut {
return &tlw.Dut{
Name: "dut-name",
Chromeos: &tlw.ChromeOS{
Servo: &tlw.ServoHost{
Name: "servo-host",
},
BluetoothPeers: []*tlw.BluetoothPeer{
{Name: "bluetooth-peers1"},
{Name: "bluetooth-peers2"},
{Name: "bluetooth-peers3"},
},
WifiRouters: []*tlw.WifiRouterHost{
{Name: "wifi-router1"},
{Name: "wifi-router2"},
{Name: "wifi-router3"},
},
Chameleon: &tlw.Chameleon{
Name: "chameleon",
},
HumanMotionRobot: &tlw.HumanMotionRobot{
Name: "hmr",
},
Dolos: &tlw.Dolos{
Hostname: "dolos",
},
AmtManager: &tlw.AMTManager{
Hostname: "amt",
},
Pasit: &tlw.Pasit{
Hostname: "pasit",
},
},
Android: &tlw.Android{
AssociatedHostname: "android-name",
},
}
}
// TestLoadConfiguration tests default configuration used for recovery flow is loading right and parsibale without any issue.
//
// Goals:
// 1. Parsed without any issue
// 2. plan using only existing execs
// 3. configuration contain all required plans in order.
func TestLoadConfiguration(t *testing.T) {
t.Parallel()
for _, c := range dutPlansCases {
cs := c
t.Run(cs.name, func(t *testing.T) {
ctx := context.Background()
args := &RunArgs{}
if c.taskName != "" {
args.TaskName = c.taskName
}
dut := &tlw.Dut{SetupType: c.setupType}
got, err := loadConfiguration(ctx, dut, args)
if cs.ok {
if err != nil {
t.Errorf("encountered unexpected error %q in test %q", err, cs.name)
}
if !cmp.Equal(got.GetPlanNames(), cs.expPlanNames) {
t.Errorf("%q ->want: %v\n got: %v: %s", cs.name, cs.expPlanNames, got.GetPlanNames(), err)
}
if _, err := config.Validate(ctx, got, execs.Exist); err != nil {
t.Errorf("%q -> fail to validate configuration with error: %s", cs.name, err)
}
} else {
if err == nil {
t.Errorf("%q -> expected to finish with error but passed", cs.name)
}
if len(got.GetPlanNames()) != 0 {
t.Errorf("%q -> want: %v\n got: %v", cs.name, cs.expPlanNames, got.GetPlanNames())
}
}
})
}
}
// TestParsedDefaultConfiguration tests default configurations are loading right and parsibale without any issue.
//
// Goals:
// 1. Parsed without any issue
// 2. plan using only existing execs
// 3. configuration contain all required plans in order.
func TestParsedDefaultConfiguration(t *testing.T) {
t.Parallel()
for _, c := range dutPlansCases {
cs := c
t.Run(cs.name, func(t *testing.T) {
ctx := context.Background()
got, err := ParsedDefaultConfiguration(ctx, c.taskName, c.setupType)
if cs.ok {
if !cmp.Equal(got.GetPlanNames(), cs.expPlanNames) {
t.Errorf("%q ->want: %v\n got: %v", cs.name, cs.expPlanNames, got.GetPlanNames())
}
} else {
if err == nil {
t.Errorf("%q -> expected to finish with error but passed", cs.name)
}
if len(got.GetPlanNames()) != 0 {
t.Errorf("%q -> want: %v\n got: %v", cs.name, cs.expPlanNames, got.GetPlanNames())
}
}
})
}
}
func TestRunDUTPlan(t *testing.T) {
t.Parallel()
ftt.Run("bad cases", t, func(t *ftt.Test) {
ctx := context.Background()
dut := &tlw.Dut{
Name: "test_dut",
Chromeos: &tlw.ChromeOS{
Servo: &tlw.ServoHost{
Name: "servo_host",
},
},
}
args := &RunArgs{
Logger: logger.NewLogger(),
}
execArgs := &execs.RunArgs{
DUT: dut,
Logger: args.Logger,
}
c := &config.Configuration{}
t.Run("fail when no plans in config", func(t *ftt.Test) {
c.Plans = map[string]*config.Plan{
"something": nil,
}
c.PlanNames = []string{"my_plan"}
err := runDUTPlans(ctx, dut, c, args)
if err == nil {
t.Errorf("Expected fail but passed")
} else {
assert.Loosely(t, err.Error(), should.ContainSubstring("run dut \"test_dut\" plans:"))
assert.Loosely(t, err.Error(), should.ContainSubstring("not found in configuration"))
}
})
t.Run("fail when one plan fail of plans fail", func(t *ftt.Test) {
c.Plans = map[string]*config.Plan{
config.PlanServo: {
CriticalActions: []string{"sample_fail"},
Actions: map[string]*config.Action{
"sample_fail": {
ExecName: "sample_fail",
},
},
},
config.PlanCrOS: {
CriticalActions: []string{"sample_pass"},
Actions: map[string]*config.Action{
"sample_pass": {
ExecName: "sample_pass",
},
},
},
}
c.PlanNames = []string{config.PlanServo, config.PlanCrOS}
err := runDUTPlans(ctx, dut, c, args)
if err == nil {
t.Errorf("Expected fail but passed")
} else {
assert.Loosely(t, err.Error(), should.ContainSubstring("run plan \"servo\" for \"servo_host\":"))
assert.Loosely(t, err.Error(), should.ContainSubstring("failed"))
}
})
t.Run("fail when bad action in the plan", func(t *ftt.Test) {
plan := &config.Plan{
CriticalActions: []string{"sample_fail"},
Actions: map[string]*config.Action{
"sample_fail": {
ExecName: "sample_fail",
},
},
}
err := runDUTPlanPerResource(ctx, "test_dut", config.PlanCrOS, plan, execArgs, nil)
if err == nil {
t.Errorf("Expected fail but passed")
} else {
assert.Loosely(t, err.Error(), should.ContainSubstring("run plan \"cros\" for \"test_dut\":"))
assert.Loosely(t, err.Error(), should.ContainSubstring(": failed"))
}
})
})
ftt.Run("Happy path", t, func(t *ftt.Test) {
ctx := context.Background()
dut := &tlw.Dut{
Name: "test_dut",
Chromeos: &tlw.ChromeOS{
Servo: &tlw.ServoHost{
Name: "servo_host",
},
},
}
args := &RunArgs{
Logger: logger.NewLogger(),
}
execArgs := &execs.RunArgs{
DUT: dut,
}
t.Run("Run good plan", func(t *ftt.Test) {
plan := &config.Plan{
CriticalActions: []string{"sample_pass"},
Actions: map[string]*config.Action{
"sample_pass": {
ExecName: "sample_pass",
},
},
}
if err := runDUTPlanPerResource(ctx, "DUT3", config.PlanCrOS, plan, execArgs, nil); err != nil {
t.Errorf("Expected pass but failed: %s", err)
}
})
t.Run("Run all good plans", func(t *ftt.Test) {
c := &config.Configuration{
Plans: map[string]*config.Plan{
config.PlanCrOS: {
CriticalActions: []string{"sample_pass"},
Actions: map[string]*config.Action{
"sample_pass": {
ExecName: "sample_pass",
},
},
},
config.PlanServo: {
CriticalActions: []string{"sample_pass"},
Actions: map[string]*config.Action{
"sample_pass": {
ExecName: "sample_pass",
},
},
},
},
}
if err := runDUTPlans(ctx, dut, c, args); err != nil {
t.Errorf("Expected pass but failed: %s", err)
}
})
t.Run("Run all plans even one allow to fail", func(t *ftt.Test) {
c := &config.Configuration{
Plans: map[string]*config.Plan{
config.PlanCrOS: {
CriticalActions: []string{"sample_fail"},
Actions: map[string]*config.Action{
"sample_fail": {
ExecName: "sample_fail",
},
},
AllowFail: true,
},
config.PlanServo: {
CriticalActions: []string{"sample_pass"},
Actions: map[string]*config.Action{
"sample_pass": {
ExecName: "sample_pass",
},
},
},
},
}
if err := runDUTPlans(ctx, dut, c, args); err != nil {
t.Errorf("Expected pass but failed: %s", err)
}
})
t.Run("Do not fail even if closing plan failed", func(t *ftt.Test) {
c := &config.Configuration{
Plans: map[string]*config.Plan{
config.PlanCrOS: {
CriticalActions: []string{},
},
config.PlanServo: {
CriticalActions: []string{},
},
config.PlanClosing: {
CriticalActions: []string{"sample_fail"},
Actions: map[string]*config.Action{
"sample_fail": {
ExecName: "sample_fail",
},
},
},
},
}
if err := runDUTPlans(ctx, dut, c, args); err != nil {
t.Errorf("Expected pass but failed: %s", err)
}
})
})
}
// TestVerify is a smoke test for the verify method.
func TestVerify(t *testing.T) {
t.Parallel()
cases := []struct {
name string
in *RunArgs
good bool
}{
{
"nil",
nil,
false,
},
{
"empty",
&RunArgs{},
false,
},
{
"missing tlw client",
&RunArgs{
UnitName: "a",
LogRoot: "b",
},
false,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
expected := tt.good
e := tt.in.verify()
actual := (e == nil)
if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf("unexpected diff (-want +got): %s", diff)
}
})
}
}
// Test cases for TestDUTPlans
var customConfigurationTestCases = []struct {
name string
getConfig func() *config.Configuration
}{
{
"Reserve DUT",
func() *config.Configuration {
return config.ReserveDutConfig()
},
},
{
"Recover CBI With Contents From Inventory",
func() *config.Configuration {
return config.RecoverCBIFromInventoryConfig()
},
},
{
"Custom dowload image to USB drive",
func() *config.Configuration {
return config.DownloadImageToServoUSBDrive("image_path", "image_name")
},
},
{
"Battery cut-off",
func() *config.Configuration {
return config.FixBatteryCutOffConfig()
},
},
{
"Serial console enable plan",
func() *config.Configuration {
return config.EnableSerialConsoleConfig()
},
},
{
"Update fw targets plan",
func() *config.Configuration {
return config.SetFwTargets("ec-target", "ap-target")
},
},
{
"BTpeer provision plan",
func() *config.Configuration {
return config.ProvisionBtpeerConfig("test_url")
},
},
{
"ChromeOS custom provision",
func() *config.Configuration {
return config.ClassicProvisionConfig([]string{"test_url"})
},
},
{
"AndroidOS custom provision",
func() *config.Configuration {
return config.AndroidOSProvisionConfig([]string{"test_url"})
},
},
}
// TestOtherConfigurations tests other known configurations used anywhere.
//
// Goals:
// 1. Parsed without any issue
// 2. plan using only existing execs
// 3. configuration contain all required plans in order.
func TestOtherConfigurations(t *testing.T) {
t.Parallel()
for _, c := range customConfigurationTestCases {
cs := c
t.Run(cs.name, func(t *testing.T) {
ctx := context.Background()
configuration := cs.getConfig()
if _, err := config.Validate(ctx, configuration, execs.Exist); err != nil {
t.Errorf("%q -> fail to validate configuration with error: %s", cs.name, err)
}
})
}
}
// Testing dutPlans method.
func TestGetConfiguration(t *testing.T) {
t.Parallel()
cases := []struct {
name string
in string
isNull bool
}{
{
"no Data",
"",
true,
},
{
"Some data",
`{
"Field":"something",
"number': 765
}`,
false,
},
{
"strange data",
"!@#$%^&*()__)(*&^%$#retyuihjo{:>\"?{",
false,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
t.Parallel()
a := &RunArgs{}
b64 := base64.StdEncoding
buf := make([]byte, b64.EncodedLen(len(c.in)))
b64.Encode(buf, []byte(c.in))
err := a.UseConfigBase64(string(buf))
if err != nil {
panic(err.Error())
}
r := a.configReader
if err != nil {
t.Errorf("Case %s: %s", c.name, err)
}
if c.isNull {
if r != nil {
t.Errorf("Case %s: expected nil", c.name)
}
} else {
got := []byte{}
err := errors.New("config reader cannot be nil")
if r != nil {
got, err = io.ReadAll(r)
}
if err != nil {
t.Errorf("Case %s: %s", c.name, err)
}
if !cmp.Equal(string(got), c.in) {
t.Errorf("got: %v\nwant: %v", string(got), c.in)
}
}
})
}
}
// TestcollectResourcesForPlan tests collectResourcesForPlan function.
func TestCollectResourcesForPlan(t *testing.T) {
t.Parallel()
cases := []struct {
plan string
out []string
}{
{config.PlanAndroid, []string{"dut-name"}},
{config.PlanCrOS, []string{"dut-name"}},
{config.PlanCrOSAudit, []string{"dut-name"}},
{config.PlanCrOSDeepRepair, []string{"dut-name"}},
{config.PlanServo, []string{"servo-host"}},
{config.PlanServoDeepRepair, []string{"servo-host"}},
{config.PlanChameleon, []string{"chameleon"}},
{config.PlanBluetoothPeer, []string{"bluetooth-peers1", "bluetooth-peers2", "bluetooth-peers3"}},
{config.PlanWifiRouter, []string{"wifi-router1", "wifi-router2", "wifi-router3"}},
{config.PlanHMR, []string{"hmr"}},
{config.PlanDolos, []string{"dolos"}},
{config.PlanAMT, []string{"amt"}},
{config.PlanPASIT, []string{"pasit"}},
{config.PlanClosing, []string{"dut-name"}},
{"cros_bla", []string{"dut-name"}},
{"servo_bla", []string{"servo-host"}},
{"", []string{"dut-name"}},
{"empty", []string{"dut-name"}},
}
for _, c := range cases {
t.Run(c.plan, func(t *testing.T) {
t.Parallel()
out := collectResourcesForPlan(c.plan, mockDut())
if len(out) == 0 {
t.Errorf("%s: did not get reources", c.plan)
}
if len(out) != len(c.out) {
t.Errorf("%s: did not get expeceted list of resource %d!= %d", c.plan, len(out), len(c.out))
}
for i, v := range c.out {
if !cmp.Equal(out[i], v) {
t.Errorf("%s: got:%v want: %v", c.plan, out[i], v)
}
}
})
}
}
func TestDeepCopyDUT(t *testing.T) {
c := context.Background()
ftt.Run("happy test", t, func(t *ftt.Test) {
c := scopes.WithConfigScope(c)
in := mockDut()
//TODO: object in the context is not readonly and can be changed over its lifetime
deepCopyDUT, err := deepCopyDUT(in)
assert.Loosely(t, err, should.BeNil)
// nothing should be changed during the copy.
assert.Loosely(t, deepCopyDUT, should.Match(in))
// save a deep copy of the original dut to the context
scopes.PutConfigParam(c, "original_dut_info", deepCopyDUT)
// changing original to make sure copy will not be changed
in.Name = "deep-copy-dut-name"
in.Chromeos.Servo.Name = "deep-copy-servo-host-name"
originalDut, ok := scopes.ReadConfigParam(c, "original_dut_info")
assert.Loosely(t, ok, should.BeTrue)
switch v := originalDut.(type) {
case *tlw.Dut:
assert.Loosely(t, in.GetChromeos().GetServo().GetName(), should.Equal("deep-copy-servo-host-name"))
assert.Loosely(t, in.GetChromeos().GetServo().GetName(), should.NotEqual(v.GetChromeos().GetServo().GetName()))
assert.Loosely(t, v, should.NotMatch(in))
default:
t.Errorf("ReadConfigParam(%q) unexpected type %T (%v)", "original_dut_info", v, v)
}
})
}
func TestRunClosingPlan(t *testing.T) {
t.Parallel()
ctx := context.Background()
const dutName = "test_dut"
testCases := []struct {
name string
initialAllowFail bool
initialAllowFailAfterRecovery bool
}{
{
name: "all false",
initialAllowFail: false,
initialAllowFailAfterRecovery: false,
},
{
name: "AllowFail true",
initialAllowFail: true,
initialAllowFailAfterRecovery: false,
},
{
name: "AllowFailAfterRecovery true",
initialAllowFail: false,
initialAllowFailAfterRecovery: true,
},
{
name: "all true",
initialAllowFail: true,
initialAllowFailAfterRecovery: true,
},
}
// Create sample execArgs.
execArgs := &execs.RunArgs{
DUT: mockDut(),
}
// Create a sample metricSaver.
metricSaver := func(metric *metrics.Action) error {
return nil
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create a sample configuration with a closing plan.
c := &config.Configuration{
Plans: map[string]*config.Plan{
config.PlanClosing: {
CriticalActions: []string{"action1", "action2"},
Actions: map[string]*config.Action{
"action1": {
ExecName: "exec1",
AllowFailAfterRecovery: tc.initialAllowFailAfterRecovery,
},
"action2": {
ExecName: "exec2",
AllowFailAfterRecovery: tc.initialAllowFailAfterRecovery,
},
},
AllowFail: tc.initialAllowFail,
},
},
}
// Call the function under test.
runClosingPlan(ctx, c, execArgs, metricSaver, dutName)
// Assert that the AllowFail field is set to true for the closing plan.
if !c.Plans[config.PlanClosing].AllowFail {
t.Errorf("runClosingPlan() did not set AllowFail to true for the closing plan")
}
// Assert that AllowFailAfterRecovery is set to true for all critical actions.
for _, actionName := range c.Plans[config.PlanClosing].CriticalActions {
action, ok := c.Plans[config.PlanClosing].Actions[actionName]
if !ok {
t.Errorf("runClosingPlan() critical action %q not found in actions map", actionName)
continue
}
if !action.AllowFailAfterRecovery {
t.Errorf("runClosingPlan() did not set AllowFailAfterRecovery to true for action %q", actionName)
}
}
})
}
}
func TestUpdateFailureInContext(t *testing.T) {
t.Parallel()
ctx := context.Background()
var failureCreated bool
failureSaver := func(failure *metrics.Failure) error {
failureCreated = true
return nil
}
ctx = karte.WithFailure(ctx, failureSaver)
fs := karte.GetFailureSaver(ctx)
if fs == nil {
t.Errorf("Expected to get failure saver from context, but got nil")
}
fs(&metrics.Failure{})
if !failureCreated {
t.Errorf("Expected failure to be created, but it was not")
}
}