blob: e19273fea4b5e3e1fe1df2b62cd1a579229022fa [file] [log] [blame]
// 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"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
. "github.com/smartystreets/goconvey/convey"
"infra/cros/recovery/config"
"infra/cros/recovery/internal/execs"
"infra/cros/recovery/logger"
"infra/cros/recovery/tlw"
"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.DUTSetupTypeUnspecified,
buildbucket.TaskName(""),
nil,
false,
},
{
"default recovery",
tlw.DUTSetupTypeUnspecified,
buildbucket.Recovery,
nil,
false,
},
{
"default deploy",
tlw.DUTSetupTypeUnspecified,
buildbucket.Deploy,
nil,
false,
},
{
"default custom",
tlw.DUTSetupTypeUnspecified,
buildbucket.Custom,
nil,
false,
},
{
"cros no task",
tlw.DUTSetupTypeCros,
buildbucket.TaskName(""),
nil,
false,
},
{
"cros recovery",
tlw.DUTSetupTypeCros,
buildbucket.Recovery,
[]string{"servo", "cros", "chameleon", "bluetooth_peer", "wifi_router", "close"},
true,
},
{
"cros deploy",
tlw.DUTSetupTypeCros,
buildbucket.Deploy,
[]string{"servo", "cros", "chameleon", "bluetooth_peer", "wifi_router", "close"},
true,
},
{
"cros custom",
tlw.DUTSetupTypeCros,
buildbucket.Custom,
nil,
false,
},
{
"labstation no task",
tlw.DUTSetupTypeCros,
buildbucket.TaskName(""),
nil,
false,
},
{
"labstation recovery",
tlw.DUTSetupTypeLabstation,
buildbucket.Recovery,
[]string{"cros"},
true,
},
{
"labstation deploy",
tlw.DUTSetupTypeLabstation,
buildbucket.Deploy,
[]string{"cros"},
true,
},
{
"labstation custom",
tlw.DUTSetupTypeLabstation,
buildbucket.Custom,
nil,
false,
},
{
"android no task",
tlw.DUTSetupTypeAndroid,
buildbucket.TaskName(""),
nil,
false,
},
{
"android recovery",
tlw.DUTSetupTypeAndroid,
buildbucket.Recovery,
[]string{"android", "close"},
true,
},
{
"android deploy",
tlw.DUTSetupTypeAndroid,
buildbucket.Deploy,
[]string{"android", "close"},
true,
},
{
"android custom",
tlw.DUTSetupTypeAndroid,
buildbucket.Custom,
nil,
false,
},
{
"android no task",
tlw.DUTSetupTypeAndroid,
buildbucket.TaskName(""),
nil,
false,
},
{
"chromeos audit RPM",
tlw.DUTSetupTypeCros,
buildbucket.AuditRPM,
[]string{"servo", "cros", "close"},
true,
},
{
"chromeos audit USB-key",
tlw.DUTSetupTypeCros,
buildbucket.AuditUSB,
[]string{"servo", "cros", "close"},
true,
},
{
"labstation does not have audit RPM",
tlw.DUTSetupTypeLabstation,
buildbucket.AuditRPM,
nil,
false,
},
{
"android does not have audit RPM",
tlw.DUTSetupTypeAndroid,
buildbucket.AuditRPM,
nil,
false,
},
{
"cros deep recovery",
tlw.DUTSetupTypeCros,
buildbucket.DeepRecovery,
[]string{"servo_deep_repair", "cros_deep_repair", "servo", "cros", "chameleon", "bluetooth_peer", "wifi_router", "close"},
true,
},
{
"cros deep recovery",
tlw.DUTSetupTypeLabstation,
buildbucket.DeepRecovery,
[]string{"cros"},
true,
},
{
"cros browser DUT recovery",
tlw.DUTSetupTypeCrosBrowser,
buildbucket.Recovery,
[]string{"cros"},
true,
},
{
"cros browser DUT deep recovery",
tlw.DUTSetupTypeCrosBrowser,
buildbucket.DeepRecovery,
[]string{"cros"},
true,
},
{
"cros browser DUT deploy",
tlw.DUTSetupTypeCrosBrowser,
buildbucket.Deploy,
[]string{"cros"},
true,
},
{
"cros dry run",
tlw.DUTSetupTypeCrosBrowser,
buildbucket.DryRun,
nil,
true,
},
{
"android dry run",
tlw.DUTSetupTypeAndroid,
buildbucket.DryRun,
nil,
true,
},
{
"labstation dry run",
tlw.DUTSetupTypeLabstation,
buildbucket.DryRun,
nil,
true,
},
{
"cros post test",
tlw.DUTSetupTypeCros,
buildbucket.PostTest,
strings.Fields("servo cros chameleon bluetooth_peer wifi_router close"),
true,
},
{
"cros browser lightweight verifier",
tlw.DUTSetupTypeCrosBrowser,
buildbucket.PostTest,
nil,
false,
},
{
"android lightweight verifier",
tlw.DUTSetupTypeAndroid,
buildbucket.PostTest,
nil,
false,
},
{
"android labstation verifier",
tlw.DUTSetupTypeLabstation,
buildbucket.PostTest,
nil,
false,
},
}
// 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()
Convey("bad cases", t, func() {
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{}
Convey("fail when no plans in config", func() {
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 {
So(err.Error(), ShouldContainSubstring, "run dut \"test_dut\" plans:")
So(err.Error(), ShouldContainSubstring, "not found in configuration")
}
})
Convey("fail when one plan fail of plans fail", func() {
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 {
So(err.Error(), ShouldContainSubstring, "run plan \"servo\" for \"servo_host\":")
So(err.Error(), ShouldContainSubstring, "failed")
}
})
Convey("fail when bad action in the plan", func() {
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 {
So(err.Error(), ShouldContainSubstring, "run plan \"cros\" for \"test_dut\":")
So(err.Error(), ShouldContainSubstring, ": failed")
}
})
})
Convey("Happy path", t, func() {
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,
}
Convey("Run good plan", func() {
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)
}
})
Convey("Run all good plans", func() {
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)
}
})
Convey("Run all plans even one allow to fail", func() {
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)
}
})
Convey("Do not fail even if closing plan failed", func() {
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 {
tt := tt
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")
},
},
{
"TPM 0x54 recovery error",
func() *config.Configuration {
return config.FixTPM54Config()
},
},
{
"Battery cut-off",
func() *config.Configuration {
return config.FixBatteryCutOffConfig()
},
},
{
"Serial console enable plan",
func() *config.Configuration {
return config.EnableSerialConsoleConfig()
},
},
}
// 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 {
c := c
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)
}
}
})
}
}