blob: ba6a9e4a4b93ee01eb0f3db8739323493df89ca3 [file] [log] [blame]
// Copyright 2022 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 (
"context"
"fmt"
"regexp"
"strconv"
"time"
"github.com/golang/protobuf/ptypes/empty"
"chromiumos/tast/common/servo"
"chromiumos/tast/errors"
"chromiumos/tast/remote/firmware"
"chromiumos/tast/remote/firmware/fixture"
"chromiumos/tast/testing"
"chromiumos/tast/testing/hwdep"
)
// Create enum to specify which tests need to be run
type wakeMethod int
const (
wakeByPowerBtn wakeMethod = iota
wakeByKeyboard
wakeByLid
wakeByUSBKeyboard
)
func init() {
testing.AddTest(&testing.Test{
Func: ECWakeSource,
Desc: "Test that DUT goes to G3 powerstate on shutdown",
Contacts: []string{"tij@google.com", "cros-fw-engprod@google.com"},
Attr: []string{"group:firmware", "firmware_unstable"},
Fixture: fixture.NormalMode,
HardwareDeps: hwdep.D(hwdep.ChromeEC()),
SoftwareDeps: []string{"chrome"},
LacrosStatus: testing.LacrosVariantUnneeded,
ServiceDeps: []string{"tast.cros.firmware.UtilsService"},
Timeout: 5 * time.Minute,
Params: []testing.Param{
{
Name: "power_btn",
ExtraHardwareDeps: hwdep.D(hwdep.InternalDisplay()),
Val: wakeByPowerBtn,
},
{
Name: "keypress",
ExtraHardwareDeps: hwdep.D(hwdep.Keyboard()),
Val: wakeByKeyboard,
},
{
Name: "lid",
ExtraHardwareDeps: hwdep.D(hwdep.Lid()),
Val: wakeByLid,
},
{
Name: "usb_keyboard",
Val: wakeByUSBKeyboard,
},
},
})
}
// time constants
const (
ecSuspendDelay time.Duration = 5 * time.Second
dutWakeDelay time.Duration = 5 * time.Second
lidSwitchDelay time.Duration = 2 * time.Second
powerdSuspendDelay time.Duration = 3 * time.Second
)
// regular expressions
const (
reGetTabletMode string = `\[\S+ tablet mode (enabled|disabled)\]`
reTabletModeNotAvailable string = `Command 'tabletmode' not found or ambiguous`
reCheckTabletMode string = `(` + reGetTabletMode + `|` + reTabletModeNotAvailable + `)`
reHasKsstate string = `Keyboard scan disable mask: (0x[0-9a-fA-F]{8})`
reNoKsstate string = `Command \'ksstate\' not found or ambiguous\.`
reGetKsstate string = `(` + reHasKsstate + `|` + reNoKsstate + `)`
)
func ECWakeSource(ctx context.Context, s *testing.State) {
h := s.FixtValue().(*fixture.Value).Helper
testType := s.Param().(wakeMethod)
if err := h.RequireServo(ctx); err != nil {
s.Fatal("Failed to connect to servo: ", err)
}
if err := h.RequireRPCUtils(ctx); err != nil {
s.Fatal("Requiring RPC utils: ", err)
}
// Create instance of chrome for login so that DUT suspends mode instead of shutting down.
s.Log("New Chrome service")
if _, err := h.RPCUtils.NewChrome(ctx, &empty.Empty{}); err != nil {
s.Fatal("Failed to create instance of chrome: ", err)
}
switch testType {
case wakeByPowerBtn:
// If the DUT has no internal display, pressing the power button in a suspended state
// would cause it to shut down instead of wake.
s.Log("Suspend DUT and wake using power key")
if err := testWakeWithPowerKey(ctx, h); err != nil {
s.Fatal("Failed to suspend and wake with power key: ", err)
}
case wakeByKeyboard:
s.Log("Suspend DUT and try to wake with keypress")
if err := testSuspendAndWakeWithInternalKeypress(ctx, h); err != nil {
s.Fatal("Failed to test waking with internal keypress: ", err)
}
case wakeByLid:
s.Log("Suspend DUT by closing lid and wake by opening lid")
if err := testSuspendAndWakeWithLid(ctx, h); err != nil {
s.Fatal("Failed to suspend and wake with lid: ", err)
}
s.Log("Suspend DUT then close lid, wake by opening lid")
if err := testWakeWithLid(ctx, h); err != nil {
s.Fatal("Failed to suspend and wake with lid: ", err)
}
case wakeByUSBKeyboard:
s.Log("Suspend DUT and wake using keypress from USB keyboard")
if err := testWakeWithUSBKeyboard(ctx, h); err != nil {
s.Fatal("Failed to suspend and wake with enter key on usb keyboard: ", err)
}
}
}
func testSuspendAndWakeWithLid(ctx context.Context, h *firmware.Helper) error {
bootID, err := h.Reporter.BootID(ctx)
if err != nil {
return errors.Wrap(err, "failed to get boot id")
}
// Delay by `dutWakeDelay` like with other suspend and wake tests.
if err := closeAndOpenLid(ctx, h, dutWakeDelay); err != nil {
return errors.Wrap(err, "failed to suspend and wake using lid")
}
newBootID, err := h.Reporter.BootID(ctx)
if err != nil {
return errors.Wrap(err, "failed to get boot id")
}
if newBootID != bootID {
return errors.New("suspend and wake test unexpectedly resulted in a reboot")
}
return nil
}
func testWakeWithLid(ctx context.Context, h *firmware.Helper) error {
bootID, err := h.Reporter.BootID(ctx)
if err != nil {
return errors.Wrap(err, "failed to get boot id")
}
testing.ContextLog(ctx, "Suspending DUT")
cmd := h.DUT.Conn().CommandContext(ctx, "powerd_dbus_suspend", fmt.Sprintf("--delay=%d", int(powerdSuspendDelay.Seconds())))
if err := cmd.Start(); err != nil {
return errors.Wrap(err, "failed to suspend DUT")
}
testing.ContextLogf(ctx, "Sleeping for %s seconds", ecSuspendDelay+powerdSuspendDelay)
if err := testing.Sleep(ctx, ecSuspendDelay+powerdSuspendDelay); err != nil {
return errors.Wrap(err, "failed to sleep waiting for suspend")
}
testing.ContextLog(ctx, "Checking for S0ix or S3 powerstate")
if err := h.WaitForPowerStates(ctx, 1*time.Second, 30*time.Second, "S0ix", "S3"); err != nil {
return errors.Wrap(err, "failed to get S0ix or S3 powerstate")
}
testing.ContextLogf(ctx, "Sleeping for %s", dutWakeDelay)
if err := testing.Sleep(ctx, dutWakeDelay); err != nil {
return err
}
testing.ContextLog(ctx, "Checking it remains suspended for S0ix or S3 powerstate")
if err := h.WaitForPowerStates(ctx, 1*time.Second, 30*time.Second, "S0ix", "S3"); err != nil {
return errors.Wrap(err, "failed to get S0ix or S3 powerstate")
}
// Delay by `lidSwitchDelay` to ensure lid is detected as closed.
// Additionally checks that closing lid keeps suspended power state.
if err := closeAndOpenLid(ctx, h, lidSwitchDelay); err != nil {
return errors.Wrap(err, "failed to wake from suspended by opening lid")
}
newBootID, err := h.Reporter.BootID(ctx)
if err != nil {
return errors.Wrap(err, "failed to get boot id")
}
if newBootID != bootID {
return errors.New("suspend and wake test unexpectedly resulted in a reboot")
}
return nil
}
func closeAndOpenLid(ctx context.Context, h *firmware.Helper, delay time.Duration) error {
if err := h.Servo.CloseLid(ctx); err != nil {
return err
}
testing.ContextLogf(ctx, "waiting %s for DUT to settle into suspend state", ecSuspendDelay)
if err := testing.Sleep(ctx, ecSuspendDelay); err != nil {
return err
}
testing.ContextLog(ctx, "Checking for S0ix or S3 powerstate")
if err := h.WaitForPowerStates(ctx, 1*time.Second, 60*time.Second, "S0ix", "S3"); err != nil {
return errors.Wrap(err, "failed to get S0ix or S3 powerstate")
}
testing.ContextLogf(ctx, "Sleeping for %s", delay)
if err := testing.Sleep(ctx, delay); err != nil {
return err
}
if err := h.Servo.OpenLid(ctx); err != nil {
return err
}
testing.ContextLog(ctx, "Waiting for S0 powerstate")
if err := h.WaitForPowerStates(ctx, 1*time.Second, 30*time.Second, "S0"); err != nil {
return errors.Wrap(err, "failed to get S0 powerstate")
}
return nil
}
func testWakeWithUSBKeyboard(ctx context.Context, h *firmware.Helper) error {
bootID, err := h.Reporter.BootID(ctx)
if err != nil {
return errors.Wrap(err, "failed to get boot id")
}
testing.ContextLog(ctx, "Enabling USB keyboard")
if err := h.Servo.SetOnOff(ctx, servo.USBKeyboard, servo.On); err != nil {
return errors.Wrapf(err, "failed to set %q to %q with servo", servo.USBKeyboard, servo.On)
}
testing.ContextLog(ctx, "Pressing enter key with USB keyboard")
if err := suspendDUTAndWakeWithKey(ctx, h, servo.USBEnter, true); err != nil {
return errors.Wrap(err, "failed to suspend and wake dut with enter key on usb keyboard")
}
testing.ContextLog(ctx, "Disabling USB keyboard")
if err := h.Servo.SetOnOff(ctx, servo.USBKeyboard, servo.Off); err != nil {
return errors.Wrapf(err, "failed to set %q to %q with servo", servo.USBKeyboard, servo.Off)
}
newBootID, err := h.Reporter.BootID(ctx)
if err != nil {
return errors.Wrap(err, "failed to get boot id")
}
if newBootID != bootID {
return errors.New("suspend and wake test unexpectedly resulted in a reboot")
}
return nil
}
func testSuspendAndWakeWithInternalKeypress(ctx context.Context, h *firmware.Helper) error {
var match []string
out, err := h.Servo.RunECCommandGetOutput(ctx, "tabletmode off", []string{reCheckTabletMode})
if err != nil {
// Not all devices support this cmd.
testing.ContextLog(ctx, "Failed to disable tabletmode: ", err)
} else if out != nil {
tabletModeAvailable := regexp.MustCompile(reGetTabletMode)
match = tabletModeAvailable.FindStringSubmatch(out[0][0])
}
testing.ContextLog(ctx, "Checking ksstate")
shouldWake := false
ksstateout, err := h.Servo.RunECCommandGetOutput(ctx, "ksstate", []string{reGetKsstate})
if err != nil {
return errors.Wrap(err, "failed to check for presence of ksstate")
} else if ksstateout != nil {
ksstatematch := regexp.MustCompile(reHasKsstate).FindStringSubmatch(ksstateout[0][0])
parsedMask, err := strconv.ParseInt(string(ksstatematch[1]), 0, 0)
testing.ContextLog(ctx, "keyboard scan disable mask: ", parsedMask)
if err != nil {
testing.ContextLog(ctx, "Failed to parse kb disable mask: ", ksstatematch)
} else {
// If parsedMask = 0, expect internal keyboard to not be disabled.
shouldWake = parsedMask == 0
}
}
if match == nil {
testing.ContextLog(ctx, "DUT does not support tablet mode")
// Whether or not it wakes on keypress depends on keyboard being enabled/disabled.
testing.ContextLog(ctx, "Suspend DUT and wake with emulated keypress")
if err := testWakeWithKeyboard(ctx, h, shouldWake); err != nil {
return errors.Wrap(err, "failed suspend and wake with keypress")
}
} else {
testing.ContextLog(ctx, "DUT supports tablet mode, disabling tablet mode")
out, err = h.Servo.RunECCommandGetOutput(ctx, "tabletmode off", []string{reGetTabletMode})
if err == nil {
testing.ContextLog(ctx, "current tabletmode state: ", out[0][1])
// If tabletmode is off, waking on keypress depends on keyboard being enabled/disabled.
testing.ContextLog(ctx, "Suspend DUT and wake with emulated keypress")
if err := testWakeWithKeyboard(ctx, h, shouldWake); err != nil {
return errors.Wrap(err, "failed suspend and wake with keypress with tablet mode disabled")
}
} else {
testing.ContextLog(ctx, "Failed to disable tablet mode")
}
testing.ContextLog(ctx, "Enabling tablet mode")
out, err = h.Servo.RunECCommandGetOutput(ctx, "tabletmode on", []string{reGetTabletMode})
if err == nil {
testing.ContextLog(ctx, "current tabletmode state: ", out[0][1])
// Device is in tablet mode, expect that keyboard is disabled and keypress does not wake device.
testing.ContextLog(ctx, "Suspend DUT and stay suspended after emulated keypress")
if err := testWakeWithKeyboard(ctx, h, false); err != nil {
return errors.Wrap(err, "failed suspend and stay suspended with keypress with tablet mode enabled")
}
} else {
testing.ContextLog(ctx, "Failed to enable tablet mode: ", err)
}
testing.ContextLog(ctx, "Resetting tablet mode to initial value")
if err = h.Servo.RunECCommand(ctx, "tabletmode reset"); err != nil {
testing.ContextLog(ctx, "Failed to run reset tablet mode: ", err)
}
}
return nil
}
func testWakeWithKeyboard(ctx context.Context, h *firmware.Helper, shouldWake bool) error {
bootID, err := h.Reporter.BootID(ctx)
if err != nil {
return errors.Wrap(err, "failed to get boot id")
}
// Disable USB keyboard to ensure internal keyboard is used.
testing.ContextLog(ctx, "Disabling USB keyboard")
if err := h.Servo.SetOnOff(ctx, servo.USBKeyboard, servo.Off); err != nil {
return errors.Wrapf(err, "failed to set %q to %q with servo", servo.USBKeyboard, servo.Off)
}
if err = suspendDUTAndWakeWithKey(ctx, h, servo.Enter, shouldWake); err != nil {
return errors.Wrap(err, "failed to suspend dut then wake/stay suspended")
}
newBootID, err := h.Reporter.BootID(ctx)
if err != nil {
return errors.Wrap(err, "failed to get boot id")
}
if newBootID != bootID {
return errors.New("suspend and wake test unexpectedly resulted in a reboot")
}
return nil
}
func testWakeWithPowerKey(ctx context.Context, h *firmware.Helper) error {
bootID, err := h.Reporter.BootID(ctx)
if err != nil {
return errors.Wrap(err, "failed to get boot id")
}
// Power button should always wake DUT.
if err := suspendDUTAndWakeWithKey(ctx, h, servo.PowerKey, true); err != nil {
return errors.Wrap(err, "failed to suspend and wake dut with power key")
}
testing.ContextLog(ctx, "Getting new boot ID")
newBootID, err := h.Reporter.BootID(ctx)
if err != nil {
return errors.Wrap(err, "failed to get boot id")
}
if newBootID != bootID {
return errors.New("suspend and wake test unexpectedly resulted in a reboot")
}
return nil
}
func suspendDUTAndWakeWithKey(ctx context.Context, h *firmware.Helper, wakeKey servo.KeypressControl, shouldWake bool) error {
testing.ContextLog(ctx, "Suspending DUT")
cmd := h.DUT.Conn().CommandContext(ctx, "powerd_dbus_suspend", fmt.Sprintf("--delay=%d", int(powerdSuspendDelay.Seconds())))
if err := cmd.Start(); err != nil {
return errors.Wrap(err, "failed to suspend DUT")
}
testing.ContextLogf(ctx, "Sleeping for %s seconds", ecSuspendDelay+powerdSuspendDelay)
if err := testing.Sleep(ctx, ecSuspendDelay+powerdSuspendDelay); err != nil {
return errors.Wrap(err, "failed to sleep waiting for suspend")
}
testing.ContextLog(ctx, "Checking for S0ix or S3 powerstate")
if err := h.WaitForPowerStates(ctx, 1*time.Second, 30*time.Second, "S0ix", "S3"); err != nil {
return errors.Wrap(err, "failed to get S0ix or S3 powerstate")
}
testing.ContextLogf(ctx, "Pressing %v key", string(wakeKey))
if err := h.Servo.KeypressWithDuration(ctx, wakeKey, servo.DurTab); err != nil {
return errors.Wrapf(err, "failed to press %v key on DUT", string(wakeKey))
}
// If shouldn't wake, DUT should remain suspended, and wake from suspended when power button is pressed.
testing.ContextLog(ctx, "Should device be awake after keypress: ", shouldWake)
if !shouldWake {
testing.ContextLogf(ctx, "Sleeping for %s", ecSuspendDelay)
if err := testing.Sleep(ctx, ecSuspendDelay); err != nil {
return err
}
testing.ContextLog(ctx, "Expect DUT to remain suspended, wait for S0ix or S3 powerstates")
if err := h.WaitForPowerStates(ctx, 2*time.Second, 90*time.Second, "S0ix", "S3"); err != nil {
return errors.Wrap(err, "failed to get one of S0ix or S3 powerstates")
}
// Wake DUT again since it remained suspended.
testing.ContextLog(ctx, "Pressing power key")
if err := h.Servo.KeypressWithDuration(ctx, servo.PowerKey, servo.DurPress); err != nil {
return errors.Wrap(err, "failed to wake DUT with power button")
}
}
testing.ContextLogf(ctx, "Sleeping for %s to wait for DUT to wake", ecSuspendDelay)
if err := testing.Sleep(ctx, ecSuspendDelay); err != nil {
return errors.Wrapf(err, "failed to sleep for %s waiting for power on", ecSuspendDelay)
}
// Both cases expect DUT to have woken.
testing.ContextLog(ctx, "Waiting for S0 powerstate")
if err := h.WaitForPowerStates(ctx, 1*time.Second, 30*time.Second, "S0"); err != nil {
return errors.Wrap(err, "failed to get S0 powerstate")
}
if err := h.WaitConnect(ctx); err != nil {
return errors.Wrap(err, "failed to reconnect to DUT")
}
return nil
}