blob: 0d677fbe280cb4b4703d46b8703828e8c6a1a47d [file] [log] [blame]
// Copyright 2021 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"
"strings"
"time"
"chromiumos/tast/common/servo"
"chromiumos/tast/errors"
"chromiumos/tast/remote/firmware/fixture"
"chromiumos/tast/testing"
"chromiumos/tast/testing/hwdep"
)
func init() {
testing.AddTest(&testing.Test{
Func: DeepSleep,
Desc: "Estimate battery life in deep sleep state, as a replacement for manual test 1.10.1",
Contacts: []string{"hc.tsai@cienet.com", "chromeos-firmware@google.com"},
Attr: []string{"group:firmware", "firmware_bringup"},
Vars: []string{"firmware.hibernate_time", "board", "model"},
HardwareDeps: hwdep.D(hwdep.Battery(), hwdep.ChromeEC()),
Timeout: 260 * time.Minute, // 4hrs 20mins
Fixture: fixture.NormalMode,
})
}
// DeepSleep has been tested to pass with Servo V4, Servo V4 + ServoMicro, Servo V4 + ServoMicro in dual V4 mode.
// Verified fail on SuzyQ because it charges the battery during the test.
func DeepSleep(ctx context.Context, s *testing.State) {
// requiredBatteryLife is the number of days the battery must last when in hibernate mode.
const requiredBatteryLife = 100 * 24 * time.Hour
// hibernateDelay is the time after the EC hibernate command where it still writes output.
const hibernateDelay = 1 * time.Second
// g3PollOptions is the time to wait for the DUT to reach G3 after power off.
g3PollOptions := testing.PollOptions{
Timeout: 30 * time.Second,
Interval: 3 * time.Second,
}
// postWakePollOptions is the time to wait for the battery after waking up from hibernate.
postWakePollOptions := testing.PollOptions{
Timeout: 60 * time.Second,
Interval: 250 * time.Millisecond,
}
// getChargerPollOptions is the time to retry the GetChargerAttached command. Unexpected EC uart logging can make it fail.
getChargerPollOptions := testing.PollOptions{
Timeout: 10 * time.Second,
Interval: 250 * time.Millisecond,
}
h := s.FixtValue().(*fixture.Value).Helper
if err := h.RequireServo(ctx); err != nil {
s.Fatal("Failed to init servo: ", err)
}
board, _ := s.Var("board")
model, _ := s.Var("model")
h.OverridePlatform(ctx, board, model)
if err := h.RequireConfig(ctx); err != nil {
s.Fatal("Failed to get config: ", err)
}
// By default the DUT hibernates for 4 hours. Reduce the duration by
// providing an optional variable "firmware.hibernate_time".
sleep := 4 * time.Hour
if v, ok := s.Var("firmware.hibernate_time"); ok {
var err error
if sleep, err = time.ParseDuration(v); err != nil {
s.Fatalf("Failed to parse duration %s: %v", v, err)
}
}
_, err := h.Servo.PreferDebugHeader(ctx)
if err != nil {
s.Fatal("PreferDebugHeader: ", err)
}
// Run this code in a func to make sure the power is on at the end.
var start time.Time
var mahStart, max int
func(ctx context.Context) {
s.Log("Stopping power supply")
if err := h.SetDUTPower(ctx, false); err != nil {
s.Fatal("Failed to remove charger: ", err)
}
defer func(ctx context.Context) {
s.Log("Connecting power supply")
if err := h.SetDUTPower(ctx, true); err != nil {
s.Fatal("Failed to attach charger: ", err)
}
}(ctx)
if err := testing.Poll(ctx, func(ctx context.Context) error {
if attached, err := h.Servo.GetChargerAttached(ctx); err != nil {
return err
} else if attached {
return errors.New("charger is still attached - use Servo V4 Type-C or supply RPM vars")
}
return nil
}, &getChargerPollOptions); err != nil {
s.Fatal("Check for charger failed: ", err)
}
s.Log("Pressing power button to make DUT into deep sleep mode")
if err := h.Servo.KeypressWithDuration(ctx, servo.PowerKey, servo.Dur(h.Config.HoldPwrButtonPowerOff)); err != nil {
s.Fatal("Failed to set a KeypressControl by servo: ", err)
}
h.DisconnectDUT(ctx)
s.Log("Waiting until power state is G3")
if err := testing.Poll(ctx, func(ctx context.Context) error {
state, err := h.Servo.GetECSystemPowerState(ctx)
if err != nil {
if strings.Contains(err.Error(), "Timed out waiting for interfaces to become available") {
return err
}
return testing.PollBreak(errors.Wrap(err, "failed to get power state"))
}
if state != "G3" {
return errors.New("power state is " + state)
}
return nil
}, &g3PollOptions); err != nil {
s.Fatal("Failed to wait power state to be G3: ", err)
}
max, err = h.Servo.GetBatteryFullChargeMAH(ctx)
if err != nil {
s.Fatal("Failed to get full charge mAh: ", err)
}
s.Logf("Battery max capacity: %dmAh", max)
mahStart, err = h.Servo.GetBatteryChargeMAH(ctx)
if err != nil {
s.Fatal("Failed to get charge mAh: ", err)
}
start = time.Now()
s.Logf("Battery charge: %dmAh", mahStart)
if h.Config.Hibernate {
s.Log("Hibernating")
if err = h.Servo.ECHibernate(ctx, servo.UseConsole); err != nil {
s.Fatal("Failed to run EC command: ", err)
}
} else {
s.Log("Skipping hibernate, because this DUT doesn't support it")
}
h.DisconnectDUT(ctx)
sleepUntil := time.Now().Add(sleep)
s.Logf("Sleeping for %s until %s", sleep, sleepUntil)
for time.Now().Before(sleepUntil) {
sleepDuration := time.Until(sleepUntil)
if sleepDuration > time.Minute {
sleepDuration = time.Minute
}
if err = testing.Sleep(ctx, sleepDuration); err != nil {
s.Fatal("Failed to sleep: ", err)
}
if _, err = h.Servo.Echo(ctx, "ping"); err != nil {
s.Log("Failed to ping servo, reconnecting: ", err)
err = h.ServoProxy.Reconnect(ctx)
if err != nil {
s.Log("Failed to reconnect to servo: ", err)
}
}
}
}(ctx)
// DUT will probably still booting at end of test.
// pre.NormalMode().Close() will cause an extra reboot here if we don't wait.
defer func() {
if err := h.Servo.KeypressWithDuration(ctx, servo.PowerKey, servo.DurTab); err != nil {
s.Fatal("Failed to set a KeypressControl by servo: ", err)
}
newCtx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
h.WaitConnect(newCtx)
}()
mahEnd := 0
if err := testing.Poll(ctx, func(ctx context.Context) error {
mahEnd, err = h.Servo.GetBatteryChargeMAH(ctx)
return err
}, &postWakePollOptions); err != nil {
s.Fatal("GetBatteryChargeMAH failed after retries, is DUT off?: ", err)
}
s.Logf("Battery charge: %dmAh", mahEnd)
var (
dur = time.Since(start)
usage = mahEnd - mahStart
)
s.Logf("Battery Usage: %dmAh in %s", usage, dur)
if usage > 0 {
days := float64(max) / (float64(usage) / dur.Seconds()) / (24 * time.Hour.Seconds())
s.Logf("Estimate Battery Life: %f day(s)", days)
if days < requiredBatteryLife.Hours()/24 {
s.Errorf("Estimate Battery Life(%f) less than 100 days", days)
}
} else {
// If less than 1 mAh is consumed during the test, we still won't know it passed unless we
// ran for long enough for it not to be a rounding error. This does assume that
// the battery is capable of reporting charge in increments of 1mAh, which might not
// be true.
minSleepTime := time.Duration(requiredBatteryLife.Nanoseconds() / int64(max))
if minSleepTime > dur {
s.Errorf("Inconclusive, please run test for at least %s", minSleepTime)
}
}
}