blob: 0bd6f18129df2351936c1e362a8db33b028d0afa [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"
"regexp"
"strconv"
"strings"
"time"
"chromiumos/tast/common/servo"
"chromiumos/tast/errors"
"chromiumos/tast/remote/firmware"
"chromiumos/tast/remote/firmware/fixture"
"chromiumos/tast/ssh"
"chromiumos/tast/testing"
"chromiumos/tast/testing/hwdep"
)
func init() {
testing.AddTest(&testing.Test{
Func: ECChargingState,
Desc: "Verify enabling and disabling write protect works as expected",
Contacts: []string{"tij@google.com", "cros-fw-engprod@google.com"},
Attr: []string{"group:firmware", "firmware_unstable"},
SoftwareDeps: []string{"crossystem", "flashrom"},
ServiceDeps: []string{"tast.cros.firmware.BiosService", "tast.cros.firmware.UtilsService"},
HardwareDeps: hwdep.D(hwdep.ChromeEC(), hwdep.Battery()),
Fixture: fixture.NormalMode,
Timeout: 20 * time.Minute,
})
}
const (
fullBatteryPercent = 95
statusFullyCharged = 0x20
statusDischarging = 0x40
statusTerminateChargeAlarm = 0x4000
statusAlarmMask = (0xFF00 & ^statusTerminateChargeAlarm)
)
type batteryState struct {
status int64
params int64
charge int64
}
func ECChargingState(ctx context.Context, s *testing.State) {
h := s.FixtValue().(*fixture.Value).Helper
if err := h.RequireServo(ctx); err != nil {
s.Fatal("Failed to connect to servo: ", err)
}
s.Log("Set servo role to source")
if err := h.SetDUTPower(ctx, true); err != nil {
s.Fatal("Failed to set servo role to source: ", err)
}
if err := suspendDUTAndCheckCharger(ctx, h, false); err != nil {
s.Fatal("Failed to suspend DUT and check if charger is attached: ", err)
}
if err := suspendDUTAndCheckCharger(ctx, h, true); err != nil {
s.Fatal("Failed to suspend DUT and check if charger is attached: ", err)
}
battery, err := getECBatteryStatus(ctx, h)
if err != nil {
s.Fatal("Failed to get current battery status: ", err)
}
if battery.status&statusDischarging == 0 {
s.Fatal("Incorrect battery state, expected Discharging, got status: ", battery.status)
}
if err := compareHostAndECBatteryStatus(ctx, h, battery); err != nil {
s.Fatal("Host and EC battery state mismatch: ", err)
}
if err := suspendDUTAndCheckCharger(ctx, h, true); err != nil {
s.Fatal("Failed to suspend DUT and check if charger is attached: ", err)
}
battery, err = getECBatteryStatus(ctx, h)
if err != nil {
s.Fatal("Failed to get current battery status: ", err)
}
if battery.status&statusFullyCharged == 0 && battery.status&statusDischarging != 0 {
s.Fatal("Incorrect battery state, expected Charging/Fully Charged, got status: ", battery.status)
}
if err := compareHostAndECBatteryStatus(ctx, h, battery); err != nil {
s.Fatal("Host and EC battery state mismatch: ", err)
}
fullChargeTimeout := 15 * time.Minute
fullChargeInterval := 1 * time.Minute
s.Logf("Wait for DUT to reach fully charged state up to %s minutes", fullChargeTimeout)
if err := testing.Poll(ctx, func(ctx context.Context) error {
battery, err = getECBatteryStatus(ctx, h)
if err != nil {
return errors.Wrap(err, "error getting battery status")
} else if battery.status&statusFullyCharged == 0 {
return errors.Errorf("expected DUT to be fully charged, actual charge level was: %d", battery.charge)
}
return nil
}, &testing.PollOptions{Timeout: fullChargeTimeout, Interval: fullChargeInterval}); err != nil {
s.Fatal("Failed to poll for fully charged battery level in DUT")
}
}
func compareHostAndECBatteryStatus(ctx context.Context, h *firmware.Helper, ecBatt *batteryState) error {
testing.ContextLog(ctx, "Get battery status reported by kernel")
out, err := h.DUT.Conn().CommandContext(ctx, "power_supply_info").Output(ssh.DumpLogOnError)
if err != nil {
return errors.Wrap(err, "failed to get power supply info")
}
// Matches the state property under the device battery.
reBattState := regexp.MustCompile(`(?s)Device: Battery.*state:(?-s)\s+(\S+(\s\S+)*)\s`)
match := reBattState.FindSubmatch(out)
if match == nil {
return errors.Errorf("failed to parse battery state in output: %s", string(out))
}
testing.ContextLog(ctx, "Kernel reported battery status: ", string(match[1]))
switch strings.ToLower(string(match[1])) {
case "fully charged":
if ecBatt.status&statusFullyCharged == 0 && ecBatt.charge < fullBatteryPercent {
return errors.Errorf("Kernel reports battery status to be fully charged, but actual state was %v instead", ecBatt.status)
}
return nil
case "charging":
if ecBatt.status&statusDischarging != 0 {
return errors.Errorf("Kernel reports battery status to be charging, but actual state was %v instead", ecBatt.status)
}
return nil
case "not charging", "discharging":
if ecBatt.status&statusDischarging == 0 {
return errors.Errorf("Kernel reports battery status to be discharging, but actual state was %v instead", ecBatt.status)
}
return nil
}
return nil
}
func getECBatteryStatus(ctx context.Context, h *firmware.Helper) (*batteryState, error) {
batteryPattern := []string{`Status:\s*0x([0-9a-f]+).*Param flags:\s*([0-9a-f]+).*Charge:\s+(\d+)\s+`}
testing.ContextLog(ctx, "Get battery status reported by ec")
if err := h.Servo.RunECCommand(ctx, "chan save"); err != nil {
return nil, errors.Wrap(err, "failed to send 'chan save' to EC")
}
if err := h.Servo.RunECCommand(ctx, "chan 0"); err != nil {
return nil, errors.Wrap(err, "failed to send 'chan 0' to EC")
}
var status, params, charge int64
retry := 5
// Get battery info from EC, retry in case output is corrupted/interrupted.
for i := 0; ; i++ {
out, err := h.Servo.RunECCommandGetOutput(ctx, "battery", batteryPattern)
if err != nil {
if i >= retry {
return nil, errors.Wrap(err, "failed to run 'battery' command in ec console")
}
continue
} else if i >= retry {
return nil, errors.Errorf("failed to get battery info within %d retries", retry)
} else if len(out[0]) < 4 {
continue
}
status, err = strconv.ParseInt(out[0][1], 16, 64)
if err != nil {
return nil, errors.Wrap(err, "failed to parse battery status as int")
}
params, err = strconv.ParseInt(out[0][2], 16, 64)
if err != nil {
return nil, errors.Wrap(err, "failed to parse battery params as int")
}
charge, err = strconv.ParseInt(out[0][3], 10, 64)
if err != nil {
return nil, errors.Wrap(err, "failed to parse battery charge as int")
}
break
}
if err := h.Servo.RunECCommand(ctx, "chan 0xffffffff"); err != nil {
return nil, errors.Wrap(err, "failed to send 'chan 0xffffffff' to EC")
}
if err := h.Servo.RunECCommand(ctx, "chan restore"); err != nil {
return nil, errors.Wrap(err, "failed to send 'chan restore' to EC")
}
testing.ContextLog(ctx, "Verify battery does not throw any unexpected alarms")
if status&statusAlarmMask != 0 {
return nil, errors.Errorf("Battery should not throw alarms, status: %v", status)
}
if (status & (statusTerminateChargeAlarm | statusFullyCharged)) == statusTerminateChargeAlarm {
return nil, errors.Errorf("Battery raising terminate charge alarm non-full, status: %v", status)
}
return &batteryState{
status: status,
params: params,
charge: charge,
}, nil
}
func suspendDUTAndCheckCharger(ctx context.Context, h *firmware.Helper, expectChargerAttached bool) error {
ecSuspendDelay := 3 * time.Second
testing.ContextLog(ctx, "Suspending DUT")
cmd := h.DUT.Conn().CommandContext(ctx, "powerd_dbus_suspend")
if err := cmd.Start(); err != nil {
return errors.Wrap(err, "failed to suspend DUT")
}
testing.ContextLogf(ctx, "Sleeping for %s", 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, firmware.PowerStateInterval, firmware.PowerStateTimeout, "S0ix", "S3"); err != nil {
return errors.Wrap(err, "failed to get S0ix or S3 powerstate")
}
testing.ContextLog(ctx, "Wait for DUT to disconnect")
if err := h.DisconnectDUT(ctx); err != nil {
return errors.Wrap(err, "failed to disconnect DUT")
}
srcOrSnk := "source"
if !expectChargerAttached {
srcOrSnk = "sink"
}
testing.ContextLogf(ctx, "Set servo role to %s", srcOrSnk)
if err := h.SetDUTPower(ctx, expectChargerAttached); err != nil {
return errors.Wrapf(err, "failed to set servo role to %s", srcOrSnk)
}
// Verify that DUT charger is in expected state.
if err := testing.Poll(ctx, func(ctx context.Context) error {
ok, err := h.Servo.GetChargerAttached(ctx)
if err != nil {
return errors.Wrap(err, "error checking whether charger is attached")
} else if ok != expectChargerAttached {
return errors.Errorf("expected charger attached state: %v", expectChargerAttached)
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second, Interval: 1 * time.Second}); err != nil {
return errors.Wrap(err, "failed to check if charger is attached")
}
testing.ContextLog(ctx, "Power on DUT with power key")
if err := h.Servo.KeypressWithDuration(ctx, servo.PowerKey, servo.DurPress); err != nil {
return errors.Wrap(err, "failed to press power key on DUT")
}
testing.ContextLog(ctx, "Wait for DUT to reach S0 powerstate")
if err := h.WaitForPowerStates(ctx, firmware.PowerStateInterval, firmware.PowerStateTimeout, "S0"); err != nil {
return errors.Wrap(err, "DUT failed to reach S0 after power button pressed")
}
testing.ContextLog(ctx, "Wait for DUT to connect")
if err := h.WaitConnect(ctx); err != nil {
return errors.Wrap(err, "failed to connect to DUT")
}
return nil
}