blob: d178e6c08d19c3c57d00d413c00a1a7eaf506f27 [file] [log] [blame]
// Copyright 2020 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 firmware
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
gossh "golang.org/x/crypto/ssh"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"chromiumos/tast/common/servo"
"chromiumos/tast/dut"
"chromiumos/tast/errors"
"chromiumos/tast/lsbrelease"
"chromiumos/tast/remote/firmware/reporters"
"chromiumos/tast/remote/firmware/rpm"
"chromiumos/tast/rpc"
fwpb "chromiumos/tast/services/cros/firmware"
"chromiumos/tast/ssh/linuxssh"
"chromiumos/tast/testing"
)
// Helper tracks several firmware-related objects. The recommended way to initialize the helper is to use firmware.fixture:
//
// import (
// ...
// "chromiumos/tast/remote/firmware/fixture"
// )
//
// func init() {
// testing.AddTest(&testing.Test{
// ...
// Fixture: fixture.NormalMode,
// })
// }
//
// func MyTest(ctx context.Context, s *testing.State) {
// h := s.FixtValue().(*fixture.Value).Helper
//
// if err := h.RequireServo(ctx); err != nil {
// s.Fatal("Failed to init servo: ", err)
// }
// ...
// }
type Helper struct {
// BiosServiceClient provides bios related services such as GBBFlags manipulation.
BiosServiceClient fwpb.BiosServiceClient
// Board contains the DUT's board, as reported by the Platform RPC.
// Currently, this is based on /etc/lsb-release's CHROMEOS_RELEASE_BOARD.
Board string
// Config contains a variety of platform-specific attributes.
Config *Config
// cfgFilepath is the full path to the data directory containing fw-testing-configs JSON files.
// Any tests requiring a Config should set cfgFilepath to s.DataPath(firmware.ConfigFile) during NewHelper.
cfgFilepath string
// These vars track whether the DUT's on-board image, and the USB images are known to have up-to-date Tast host files.
dutInternalStorageHasTastFiles bool
dutUsbHasTastFiles bool
// DUT is used for communicating with the device under test.
DUT *dut.DUT
// hostFilesTmpDir is a temporary directory on the test server holding a copy of Tast host files.
hostFilesTmpDir string
// Model contains the DUT's model, as reported by the Platform RPC.
// Currently, this is based on cros_config / name.
Model string
// Reporter reports various info from the DUT.
Reporter *reporters.Reporter
// RPCClient is a direct client connection to the Tast gRPC server hosted on the DUT.
RPCClient *rpc.Client
// disallowServices prevents RequireRPCClient from working if set.
disallowServices bool
// rpcHint is needed in order to create an RPC client connection.
rpcHint *testing.RPCHint
// RPCUtils allows the Helper to call the firmware utils RPC service.
RPCUtils fwpb.UtilsServiceClient
// Servo allows us to send commands to a servo device.
Servo *servo.Servo
// servoHostPort is the address and port of the machine acting as the servo host, normally provided via the "servo" command-line variable.
servoHostPort string
keyFile string
keyDir string
// ServoProxy wraps the Servo object, and communicates with the servod instance.
ServoProxy *servo.Proxy
// RPM is a remote power management client. Only valid in the test lab.
RPM *rpm.RPM
// dutHostname is the real name of the dut, even if tast is connected to a forwarded port.
dutHostname string
// powerunitHostname, powerunitOutlet, hydraHostname identify the managed power outlet for the DUT.
powerunitHostname, powerunitOutlet, hydraHostname string
}
// WaitConnectOption includes situations to wait to connect from.
type WaitConnectOption string
const (
// FromHibernation alerts WaitConnect to skip
// on setting servo control while DUT is still
// in the process of waking up from hibernation.
FromHibernation WaitConnectOption = "hibernation"
)
// NewHelper creates a new Helper object with info from testing.State.
// For tests that do not use a certain Helper aspect (e.g. RPC or Servo), it is OK to pass null-values (nil or "").
func NewHelper(d *dut.DUT, rpcHint *testing.RPCHint, cfgFilepath, servoHostPort, dutHostname, powerunitHostname, powerunitOutlet, hydraHostname string) *Helper {
return &Helper{
cfgFilepath: cfgFilepath,
DUT: d,
keyFile: d.KeyFile(),
keyDir: d.KeyDir(),
Reporter: reporters.New(d),
rpcHint: rpcHint,
servoHostPort: servoHostPort,
dutHostname: dutHostname,
powerunitHostname: powerunitHostname,
powerunitOutlet: powerunitOutlet,
hydraHostname: hydraHostname,
}
}
// NewHelperWithoutDUT creates a new Helper object with info from testing.State. The resulting Helper will be unable to ssh to the DUT.
func NewHelperWithoutDUT(cfgFilepath, servoHostPort, keyFile, keyDir string) *Helper {
return &Helper{
cfgFilepath: cfgFilepath,
keyFile: keyFile,
keyDir: keyDir,
servoHostPort: servoHostPort,
}
}
// Close shuts down any firmware objects associated with the Helper.
// Generally, tests should defer Close() immediately after initializing a Helper.
func (h *Helper) Close(ctx context.Context) error {
var allErrors []error
if h.hostFilesTmpDir != "" {
if err := os.RemoveAll(h.hostFilesTmpDir); err != nil {
allErrors = append(allErrors, errors.Wrap(err, "removing server's copy of Tast host files"))
}
h.hostFilesTmpDir = ""
}
if err := h.CloseRPCConnection(ctx); err != nil {
isIgnorable := false
for rootErr := err; rootErr != nil && !isIgnorable; rootErr = errors.Unwrap(rootErr) {
// The gRPC Canceled error just means the connection is already closed.
if st, ok := status.FromError(rootErr); ok && st.Code() == codes.Canceled {
isIgnorable = true
}
}
if !isIgnorable {
allErrors = append(allErrors, errors.Wrap(err, "closing rpc connection"))
}
}
if err := h.CloseServo(ctx); err != nil {
allErrors = append(allErrors, errors.Wrap(err, "closing servo"))
}
if len(allErrors) > 0 {
for err := range allErrors[1:] {
testing.ContextLog(ctx, "Suppressed error: ", err)
}
return allErrors[0]
}
return nil
}
// EnsureDUTBooted checks the power state, and attempts to boot the DUT if it is off.
func (h *Helper) EnsureDUTBooted(ctx context.Context) error {
if h.DUT != nil && h.DUT.Connected(ctx) {
return nil
}
if err := h.RequireServo(ctx); err != nil {
return errors.Wrap(err, "could not connect to servo")
}
if hasEC, err := h.Servo.HasControl(ctx, string(servo.ECSystemPowerState)); err != nil {
testing.ContextLog(ctx, "Error checking for chrome ec: ", err)
} else if hasEC {
state, err := h.Servo.GetECSystemPowerState(ctx)
if err != nil {
testing.ContextLog(ctx, "Error getting power state: ", err)
}
if state == "S0" {
testing.ContextLog(ctx, "Waiting for DUT to finish booting")
// The machine is up, just wait for it to finish booting.
h.CloseRPCConnection(ctx)
waitBootCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
if err = h.WaitConnect(waitBootCtx); err == nil {
return nil
}
// If WaitConnect didn't work, let it reset.
}
}
testing.ContextLog(ctx, "Connecting power")
if err := h.SetDUTPower(ctx, true); err != nil {
testing.ContextLog(ctx, "Failed to connect charger: ", err)
}
// Cr50 goes to sleep during hibernation and battery cutoff, and when DUT wakes, CCD state might be locked.
if val, err := h.Servo.GetString(ctx, servo.GSCCCDLevel); err != nil {
testing.ContextLog(ctx, "Failed to get gsc_ccd_level: ", err)
} else if val != servo.Open {
testing.ContextLogf(ctx, "CCD is not open, got %q. Attempting to unlock", val)
if err := h.Servo.SetString(ctx, servo.CR50Testlab, servo.Open); err != nil {
testing.ContextLog(ctx, "Failed to unlock CCD: ", err)
}
}
testing.ContextLog(ctx, "Resetting DUT")
if err := h.Servo.SetPowerState(ctx, servo.PowerStateReset); err != nil {
testing.ContextLog(ctx, "Failed to reset DUT: ", err)
}
if err := h.DisconnectDUT(ctx); err != nil {
testing.ContextLog(ctx, "Error closing connections to DUT: ", err)
}
h.CloseRPCConnection(ctx)
return h.WaitConnect(ctx)
}
// DisallowServices prevents RequireRPCClient from being used for the lifetime of this Helper.
func (h *Helper) DisallowServices() {
h.disallowServices = true
}
// RequireRPCClient creates a client connection to the DUT's gRPC server, unless a connection already exists.
func (h *Helper) RequireRPCClient(ctx context.Context) error {
if h.disallowServices {
return errors.New("RPC services disabled by fixture")
}
if h.RPCClient != nil {
return nil
}
// rpcHint comes from testing.State, so it needs to be manually set in advance.
if h.rpcHint == nil {
return errors.New("cannot create RPC client connection without rpcHint")
}
testing.ContextLog(ctx, "Opening RPCClient connection")
var cl *rpc.Client
const rpcConnectTimeout = 5 * time.Minute
if err := testing.Poll(ctx, func(ctx context.Context) error {
if !h.DUT.Connected(ctx) {
if err := h.DUT.Connect(ctx); err != nil {
return err
}
}
var err error
cl, err = rpc.Dial(ctx, h.DUT, h.rpcHint)
return err
}, &testing.PollOptions{Timeout: rpcConnectTimeout}); err != nil {
return errors.Wrap(err, "dialing RPC connection")
}
h.RPCClient = cl
return nil
}
// RequireRPCUtils creates a firmware.UtilsServiceClient, unless one already exists.
func (h *Helper) RequireRPCUtils(ctx context.Context) error {
if h.RPCUtils != nil {
return nil
}
if err := h.RequireRPCClient(ctx); err != nil {
return errors.Wrap(err, "requiring RPC client")
}
h.RPCUtils = fwpb.NewUtilsServiceClient(h.RPCClient.Conn)
return nil
}
// RequireBiosServiceClient creates a firmware.BiosServiceClient, unless one already exists.
// You must add `SoftwareDeps: []string{"flashrom"},` to your `testing.Test` to use this.
func (h *Helper) RequireBiosServiceClient(ctx context.Context) error {
if h.BiosServiceClient != nil {
return nil
}
if err := h.RequireRPCClient(ctx); err != nil {
return errors.Wrap(err, "requiring RPC client")
}
h.BiosServiceClient = fwpb.NewBiosServiceClient(h.RPCClient.Conn)
return nil
}
// CloseRPCConnection shuts down the RPC client (if present), and removes any RPC clients that the Helper was tracking.
func (h *Helper) CloseRPCConnection(ctx context.Context) error {
defer func() {
h.RPCClient, h.RPCUtils, h.BiosServiceClient = nil, nil, nil
}()
if h.RPCClient != nil {
testing.ContextLog(ctx, "Closing RPCClient connection")
if err := h.RPCClient.Close(ctx); err != nil {
return errors.Wrap(err, "closing rpc client")
}
}
return nil
}
// DisconnectDUT shuts down all connections to the DUT. Call this after you have powered down the DUT.
func (h *Helper) DisconnectDUT(ctx context.Context) error {
rpcerr := h.CloseRPCConnection(ctx)
// Disconnect the dut even if the rpc connection failed to close.
if h.DUT != nil {
duterr := h.DUT.Disconnect(ctx)
if duterr != nil {
return duterr
}
}
return rpcerr
}
// RequirePlatform fetches the DUT's board and model and caches them, unless they have already been cached.
func (h *Helper) RequirePlatform(ctx context.Context) error {
if h.Board == "" {
board, err := h.Reporter.Board(ctx)
if err != nil {
return errors.Wrap(err, "getting DUT board")
}
h.Board = strings.ToLower(board)
}
if h.Model == "" {
model, err := h.Reporter.Model(ctx)
// Ignore error, as not all boards have a model.
if err == nil {
h.Model = strings.ToLower(model)
} else {
testing.ContextLogf(ctx, "Failed to get DUT model for board %s: %+v", h.Board, err)
}
}
return nil
}
// OverridePlatform sets board and model if the passed in params are not blank.
func (h *Helper) OverridePlatform(ctx context.Context, board, model string) {
if board != "" {
h.Board = strings.ToLower(board)
}
if model != "" {
h.Model = strings.ToLower(model)
}
}
// RequireConfig creates a firmware.Config, unless one already exists.
// This requires your test to have `Data: []string{firmware.ConfigFile},` in its `testing.Test` block.
func (h *Helper) RequireConfig(ctx context.Context) error {
if h.Config != nil {
return nil
}
if err := h.RequirePlatform(ctx); err != nil {
return errors.Wrap(err, "requiring DUT platform")
}
// cfgFilepath comes from testing.State, so it needs to be passed during NewHelper.
if h.cfgFilepath == "" {
return errors.New("cannot create firmware Config with a null Helper.cfgFilepath")
}
cfg, err := NewConfig(h.cfgFilepath, h.Board, h.Model)
if err != nil {
return errors.Wrapf(err, "during NewConfig with board=%s, model=%s", h.Board, h.Model)
}
h.Config = cfg
return nil
}
// RequireServo creates a servo.Servo, unless one already exists.
func (h *Helper) RequireServo(ctx context.Context) error {
if h.Servo != nil {
return nil
}
pxy, err := servo.NewProxy(ctx, h.servoHostPort, h.keyFile, h.keyDir)
if err != nil {
return errors.Wrap(err, "connecting to servo")
}
h.ServoProxy = pxy
h.Servo = pxy.Servo()
return nil
}
// CloseServo closes the connection to the servo, use RequireServo to open it again.
func (h *Helper) CloseServo(ctx context.Context) error {
defer func() {
h.ServoProxy = nil
h.Servo = nil
h.RPM = nil
}()
var err error
if h.RPM != nil {
if err = h.RPM.Close(ctx); err != nil {
err = errors.Wrap(err, "failed to close rpm client")
}
}
if h.ServoProxy != nil {
h.ServoProxy.Close(ctx)
}
return err
}
const (
// dutLocalRunner, dutLocalBundleDir, and dutLocalDataDir are paths on the DUT containing Tast host files.
dutLocalRunner = "/usr/local/bin/local_test_runner"
dutLocalBundleDir = "/usr/local/libexec/tast/"
dutLocalDataDir = "/usr/local/share/tast/"
// tmpLocalRunner, tmpLocalBundleDir, and tmpLocalDataDir are relative paths, within a tempdir on the server, to copies of Tast host files.
tmpLocalRunner = "local-runner"
tmpLocalBundleDir = "local-bundle-dir/"
tmpLocalDataDir = "local-data-dir/"
)
// DoesServerHaveTastHostFiles determines whether the test server has a copy of Tast host files.
func (h *Helper) DoesServerHaveTastHostFiles() bool {
return h.hostFilesTmpDir != ""
}
// CopyTastFilesFromDUT retrieves Tast host files from the DUT and stores them locally for later use.
// This allows the test server to re-push Tast files to the DUT if a different OS image is booted mid-test.
func (h *Helper) CopyTastFilesFromDUT(ctx context.Context) error {
if h.DoesServerHaveTastHostFiles() {
return errors.New("cannot copy Tast files from DUT twice")
}
// Create temp dir to hold copied Tast files.
tmpDir, err := ioutil.TempDir("", "tast-host-files-copy")
if err != nil {
return err
}
h.hostFilesTmpDir = tmpDir
// Copy files from DUT onto test server.
testing.ContextLogf(ctx, "Copying Tast host files to test server at %s", tmpDir)
for dutSrc, serverDst := range map[string]string{
dutLocalRunner: filepath.Join(tmpDir, tmpLocalRunner),
dutLocalBundleDir: filepath.Join(tmpDir, tmpLocalBundleDir),
dutLocalDataDir: filepath.Join(tmpDir, tmpLocalDataDir),
} {
// Only copy the file if it exists.
if err = h.DUT.Conn().CommandContext(ctx, "test", "-x", dutSrc).Run(); err == nil {
if err = linuxssh.GetFile(ctx, h.DUT.Conn(), dutSrc, serverDst, linuxssh.PreserveSymlinks); err != nil {
return errors.Wrapf(err, "copying local Tast file %s from DUT", dutSrc)
}
} else if _, ok := err.(*gossh.ExitError); !ok {
return errors.Wrapf(err, "checking for existence of %s", dutSrc)
}
}
return nil
}
// SyncTastFilesToDUT copies the test server's copy of Tast host files back onto the DUT. This is only necessary if you want to use gRPC services.
// TODO(gredelston): When Autotest SSP tarballs contain local Tast test bundles, refactor this code
// so that it pushes Tast files to the DUT via the same means as the upstream Tast framework.
// As of the time of this writing, that is not possible; see http://g/tast-owners/sBhC1w-ET8g.
func (h *Helper) SyncTastFilesToDUT(ctx context.Context) error {
if !h.DoesServerHaveTastHostFiles() {
return errors.New("must copy Tast files from DUT before syncing back onto DUT")
}
fileMap := map[string]string{
filepath.Join(h.hostFilesTmpDir, tmpLocalRunner): dutLocalRunner,
filepath.Join(h.hostFilesTmpDir, tmpLocalBundleDir): dutLocalBundleDir,
filepath.Join(h.hostFilesTmpDir, tmpLocalDataDir): dutLocalDataDir,
}
for key := range fileMap {
if _, err := os.Stat(key); os.IsNotExist(err) {
delete(fileMap, key)
}
}
testing.ContextLog(ctx, "Syncing Tast files from test server onto DUT: ", fileMap)
if _, err := linuxssh.PutFiles(ctx, h.DUT.Conn(), fileMap, linuxssh.DereferenceSymlinks); err != nil {
return errors.Wrap(err, "failed syncing Tast files from test server onto DUT")
}
return nil
}
// SetupUSBKey prepares the USB disk for a test. (Borrowed from Tauto's firmware_test.py)
// It checks the setup of USB disk and a valid ChromeOS test image inside.
// Downloads the test image if the image isn't the right version.
// Will break the DUT if it is currently booted off the USB drive in recovery mode.
func (h *Helper) SetupUSBKey(ctx context.Context, cloudStorage *testing.CloudStorage) (retErr error) {
testing.ContextLog(ctx, "Validating image usbkey on servo")
// Power cycling the USB key helps to make it visible to the host.
if err := h.Servo.SetUSBMuxState(ctx, servo.USBMuxOff); err != nil {
return errors.Wrap(err, "failed to power off usbkey")
}
// This call is super slow.
usbdev, err := h.Servo.GetStringTimeout(ctx, servo.ImageUSBKeyDev, time.Second*90)
if err != nil {
return errors.Wrap(err, "servo call image_usbkey_dev failed")
}
if usbdev == "" {
return errors.New("no USB key detected")
}
var fdiskOutput []byte
// Verify that the device really exists on the servo host.
err = testing.Poll(ctx, func(ctx context.Context) error {
fdiskOutput, err = h.ServoProxy.OutputCommand(ctx, true, "fdisk", "-l", usbdev)
return err
}, &testing.PollOptions{
Timeout: 10 * time.Second,
Interval: 1 * time.Second,
})
if err != nil {
return errors.Wrapf(err, "validate usb key at %q", usbdev)
}
testing.ContextLogf(ctx, "Output from fdisk -l %q: %s", usbdev, fdiskOutput)
testing.ContextLog(ctx, "Checking ChromeOS image name on usbkey")
mountPath := fmt.Sprintf("/media/servo_usb/%d", h.ServoProxy.GetPort())
// Unmount whatever might be mounted.
h.ServoProxy.RunCommandQuiet(ctx, true, "umount", "-q", mountPath)
// ChromeOS root fs is in /dev/sdx3.
mountSrc := usbdev + "3"
if err = h.ServoProxy.RunCommand(ctx, true, "mkdir", "-p", mountPath); err != nil {
return errors.Wrapf(err, "mkdir failed at %q", mountPath)
}
var lsb map[string]string
// Failures here are a bad USB image, so don't fail, just write the new image.
func() {
if err = h.ServoProxy.RunCommand(ctx, true, "mount", "-o", "ro", mountSrc, mountPath); err != nil {
testing.ContextLogf(ctx, "Mount of %q failed at %q", mountSrc, mountPath)
}
defer h.ServoProxy.RunCommand(ctx, true, "umount", mountPath)
output, err := h.ServoProxy.OutputCommand(ctx, true, "cat", fmt.Sprintf("%s/etc/lsb-release", mountPath))
if err != nil {
testing.ContextLog(ctx, "Failed to read lsb-release")
}
lsb, err = lsbrelease.Parse(bytes.NewReader(output))
if err != nil {
testing.ContextLog(ctx, "Failed to parse lsb-release")
}
}()
releaseBuilderPath := lsb[lsbrelease.BuilderPath]
dutBuilderPath, err := h.Reporter.BuilderPath(ctx)
if err != nil {
return errors.Wrap(err, "failed to get DUT builder path")
}
if strings.Contains(dutBuilderPath, "-postsubmit") {
testing.ContextLogf(ctx, "Current build on DUT (%s) is not a release image, using %s from USB stick", dutBuilderPath, releaseBuilderPath)
return nil
}
if !strings.Contains(lsb[lsbrelease.ReleaseTrack], "test") {
testing.ContextLog(ctx, "The image on usbkey is not a test image")
releaseBuilderPath = ""
}
if releaseBuilderPath == dutBuilderPath {
return nil
}
if cloudStorage == nil {
testing.ContextLogf(ctx, "User requested no USB image download, using %s even though it differs from DUT %s", releaseBuilderPath, dutBuilderPath)
return nil
}
testing.ContextLogf(ctx, "Current build on USB (%s) differs from DUT (%s), proceed with download", releaseBuilderPath, dutBuilderPath)
// Copying the behavior from src/third_party/hdctools/servo/drv/usb_downloader.py.
// Write the chromiumos_test_image.bin straight over /dev/sdx (usbdev).
// That code expects a url to a unpacked chromiumos_test_image.bin, but cloudStorage.Open doesn't handle devserver artifacts like `test_image`,
// so we need to manually untar the file and write it over the usb device.
// TODO if needed, recovery images are at .../recovery_image.tar.xz.
testImageURL := "build-artifact:///chromiumos_test_image.tar.xz"
// TODO(b/217635723): Revisit later when we have a solution for accessing dev servers on non-DUT machines.
dataURL, err := cloudStorage.Stage(ctx, testImageURL)
if err != nil {
return errors.Wrapf(err, "failed to download test image %s", dutBuilderPath)
}
if dataURL.Scheme != "http" && dataURL.Scheme != "https" {
return errors.Errorf("CloudStorage url is not http(s): %q", dataURL)
}
testing.ContextLog(ctx, "Flashing test OS image to USB")
// If it did have tast files, it won't shortly.
h.dutUsbHasTastFiles = false
// On my computer with a servo v4, this takes 7 minutes.
if err := h.Servo.SetStringTimeout(ctx, servo.DownloadImageToUSBDev, dataURL.String(), time.Hour); err != nil {
return errors.Wrapf(err, "failed to flash os image %q to USB %q", testImageURL, usbdev)
}
testing.ContextLogf(ctx, "Successfully flashed %q from %q", usbdev, testImageURL)
return nil
}
// WaitForPowerStates polls for DUT to get to a specific powerstate
func (h *Helper) WaitForPowerStates(ctx context.Context, interval, timeout time.Duration, powerStates ...string) error {
// Try reading the power state from the EC.
err := testing.Poll(ctx, func(c context.Context) error {
currPowerState, err := h.Servo.GetECSystemPowerState(ctx)
if err != nil {
return errors.Wrap(err, "failed to check powerstate")
}
if !comparePowerStates(currPowerState, powerStates...) {
return errors.Errorf("Power state = %s", currPowerState)
}
return nil
}, &testing.PollOptions{Timeout: timeout, Interval: interval})
if err != nil {
return errors.Errorf("failed to get one of %v power state: %s", powerStates, err)
}
return nil
}
func comparePowerStates(currState string, expectedStates ...string) bool {
for _, state := range expectedStates {
if currState == state {
return true
}
}
return false
}
func (h *Helper) waitDutS0(ctx context.Context) error {
const reconnectRetryDelay = time.Second
testing.ContextLog(ctx, "Waiting DUT to reach S0")
if err := h.WaitForPowerStates(ctx, reconnectRetryDelay, 1*time.Minute, "S0"); err != nil {
return errors.Wrap(err, "wait for S0")
}
testing.ContextLog(ctx, "Sleeping 20s for boot to finish")
if err := testing.Sleep(ctx, 20*time.Second); err != nil {
return errors.Wrap(err, "sleep 20s")
}
return nil
}
// wcOptsContain determines whether a slice of WaitConnectOption contains a specific Option.
func wcOptsContain(opts []WaitConnectOption, contained WaitConnectOption) bool {
for _, v := range opts {
if v == contained {
return true
}
}
return false
}
// WaitConnect is similar to DUT.WaitConnect, except that it works with RO EC firmware.
// Pass a context with a deadline if you don't want to wait forever.
// If --var noSSH=true is set, this degrades to waiting for S0 + a sleep.
func (h *Helper) WaitConnect(ctx context.Context, opts ...WaitConnectOption) error {
const reconnectRetryDelay = time.Second
if err := h.RequireServo(ctx); err != nil {
return errors.Wrap(err, "failed to connect to servo")
}
if h.DUT == nil {
return h.waitDutS0(ctx)
}
testing.ContextLogf(ctx, "Waiting for %s to connect", h.DUT.HostName())
for {
// SetDUTPDDataRole would fail when DUT is still in the process
// of waking up from hibernation.
if !wcOptsContain(opts, FromHibernation) {
if err := h.Servo.SetDUTPDDataRole(ctx, servo.DFP); err != nil {
testing.ContextLogf(ctx, "Failed to set pd data role to DFP: %s", err)
}
}
err := h.DUT.Connect(ctx)
if err == nil {
return nil
}
select {
case <-time.After(reconnectRetryDelay):
break
case <-ctx.Done():
if err.Error() == ctx.Err().Error() {
return err
}
return errors.Wrapf(err, "context error = %v", ctx.Err())
}
}
}
// RequireRPM creates the RPM client in h.RPM.
func (h *Helper) RequireRPM(ctx context.Context) error {
if h.RPM != nil {
return nil
}
if err := h.RequireServo(ctx); err != nil {
return err
}
var err error
if h.ServoProxy.Proxied() {
h.RPM, err = rpm.NewLabRPM(ctx, h.ServoProxy, h.dutHostname, h.powerunitHostname, h.powerunitOutlet, h.hydraHostname)
} else {
h.RPM, err = rpm.NewLabRPM(ctx, nil, h.dutHostname, h.powerunitHostname, h.powerunitOutlet, h.hydraHostname)
}
if err != nil {
return errors.Wrap(err, "new rpm client")
}
return nil
}
// SetDUTPower turns the DUT's power on or off. Uses servo v4 pd role if possible, and falls back to RPM.
// To use RPM the command line vars `powerunitHostname` and `powerunitOutlet` must be set.
// `dutHostname` can be used to override the DUT's hostname, if ssh and rpm have different names.
// For plugs attached to hyrda, also set var `hydraHostname`.
func (h *Helper) SetDUTPower(ctx context.Context, powerOn bool) error {
// Try servo SetPDRole (servo v4 type C). The servo is slightly evil though, and will report that it has this control even for Type-A.
connectionType := ""
hasControl, err := h.Servo.HasControl(ctx, string(servo.PDRole))
if err != nil {
return errors.Wrap(err, "checking for control")
}
if hasControl {
connectionType, err = h.Servo.GetString(ctx, "root.dut_connection_type")
if err != nil {
return errors.Wrap(err, "getting connection type")
}
}
if connectionType == "type-c" {
role := servo.PDRoleSnk
if powerOn {
role = servo.PDRoleSrc
}
if err := h.Servo.SetPDRole(ctx, role); err != nil {
return errors.Wrap(err, "set pd role")
}
testing.ContextLogf(ctx, "SetPDRole: %q", role)
return nil
}
// Try rpm client
if h.powerunitHostname != "" {
if err := h.RequireRPM(ctx); err != nil {
return err
}
powerState := rpm.Off
if powerOn {
powerState = rpm.On
}
if ok, err := h.RPM.SetPower(ctx, powerState); err != nil {
return errors.Wrap(err, "set power via rpm")
} else if !ok {
return errors.Errorf("rpm client did not set power state to %s", powerState)
}
return nil
}
return errors.New("servo does not support pd role and no rpm vars provided")
}