blob: 3e92ff2c053aa047767626def5a5fe372806ebfd [file] [log] [blame]
// Copyright 2024 The ChromiumOS Authors
// 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"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"go.chromium.org/tast-tests/cros/common/servo"
"go.chromium.org/tast-tests/cros/remote/firmware"
"go.chromium.org/tast-tests/cros/remote/firmware/fixture"
"go.chromium.org/tast-tests/cros/remote/powercontrol"
"go.chromium.org/tast/core/errors"
"go.chromium.org/tast/core/ssh/linuxssh"
"go.chromium.org/tast/core/testing"
"go.chromium.org/tast/core/testing/hwdep"
)
const (
// Default number of boot iterations. Can be overridden by var.
defaultDevBootKeyboardStressIterations = 100
)
func init() {
testing.AddTest(&testing.Test{
Func: DevBootKeyboardStress,
Desc: "Check keyboard is enabled during developer firmware screen",
Contacts: []string{
"chromeos-faft@google.com",
"ecgh@chromium.org",
},
BugComponent: "b:792402", // ChromeOS > Platform > Enablement > Firmware > FAFT
Attr: []string{"group:firmware", "firmware_unstable"},
Fixture: fixture.DevMode,
Timeout: 3 * time.Hour,
HardwareDeps: hwdep.D(hwdep.ChromeEC(), hwdep.X86()),
LacrosStatus: testing.LacrosVariantUnneeded,
Vars: []string{"iterations"},
})
}
// DevBootKeyboardStress test checks that the EC thinks the keyboard is
// enabled while depthcharge shows the dev mode screen before booting
// the OS. This test is only intended to run on x86 devices using the
// 8042 EC keyboard and with depthcharge configured to show the dev
// mode screen for the default 30 seconds.
func DevBootKeyboardStress(ctx context.Context, s *testing.State) {
h := s.FixtValue().(*fixture.Value).Helper
if err := h.RequireConfig(ctx); err != nil {
s.Fatal("Require config")
}
if err := h.RequireServo(ctx); err != nil {
s.Fatal("Require servo")
}
iterations := defaultDevBootKeyboardStressIterations
if iterStr, ok := s.Var("iterations"); ok {
iterInt, err := strconv.Atoi(iterStr)
if err != nil {
s.Fatal("Failed to parse iterations value: ", iterStr)
}
iterations = iterInt
}
if !checkKeyboard(ctx, s, h.Servo) {
s.Fatal("8042 keyboard must be enabled to start")
}
closeUART, err := h.Servo.EnableUARTCapture(ctx, servo.ECUARTCapture)
if err != nil {
s.Fatal("Failed to capture EC UART: ", err)
}
defer func() {
if err := closeUART(ctx); err != nil {
s.Fatal("Failed to cancel capture EC UART: ", err)
}
}()
outDir, ok := testing.ContextOutDir(ctx)
if !ok {
s.Fatal("Failed to get output dir: ", err)
}
// Use timestamp to separate logs from Tast retries (b/297937181).
outSubDir := time.Now().Format("20060102-150405")
s.Logf("Starting %d reboot attempts ", iterations)
for i := 0; i < iterations; i++ {
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()
ecOutput, err := doDevBootKeyboardStressIteration(ctx, s, h, i)
if err != nil {
s.Errorf("Error on attempt %d: %v", i, err)
// GoBigSleepLint: Wait after unexpected error.
testing.Sleep(ctx, 10*time.Second)
// Try a power button push since it might power the DUT up.
h.Servo.KeypressWithDuration(ctx, servo.PowerKey, servo.DurShortPress)
// GoBigSleepLint: Wait after unexpected error.
testing.Sleep(ctx, 10*time.Second)
errorOutput, _ := h.GetECConsoleOutputWithComment(ctx, "after error")
ecOutput = ecOutput + errorOutput
}
logDir := filepath.Join(outDir, outSubDir, strconv.Itoa(i))
os.MkdirAll(logDir, 0755)
os.WriteFile(filepath.Join(logDir, "ec_console.txt"), []byte(ecOutput), 0666)
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
defer cancel()
fwLog, err := h.Reporter.GetCBMEMLogs(ctx)
if err != nil {
s.Error("Failed to read FW logs: ", err)
}
os.WriteFile(filepath.Join(logDir, "fw_log.txt"), []byte(fwLog), 0666)
// Check that log shows waiting at dev screen.
matches := regexp.MustCompile(`developer_mode_action: Booting default target after 30s`).FindAllStringSubmatch(fwLog, -1)
if len(matches) != 1 {
s.Error("Failed to find dev screen line in fw log: ", matches)
}
timestamps, err := h.Reporter.GetCBMEMTimestamps(ctx)
if err != nil {
s.Error("Failed to read FW timestamps: ", err)
}
os.WriteFile(filepath.Join(logDir, "timestamps.txt"), []byte(timestamps), 0666)
linuxssh.GetFile(ctx, h.DUT.Conn(), "/sys/fs/pstore/", filepath.Join(logDir, "pstore"), linuxssh.PreserveSymlinks)
linuxssh.GetFile(ctx, h.DUT.Conn(), "/var/log/eventlog.txt", filepath.Join(logDir, "eventlog.txt"), linuxssh.PreserveSymlinks)
}
}
func doDevBootKeyboardStressIteration(ctx context.Context, s *testing.State, h *firmware.Helper, i int) (string, error) {
var ecOutput strings.Builder
bootID, err := h.Reporter.BootID(ctx)
if err != nil {
return ecOutput.String(), errors.Wrap(err, "failed to get boot id")
}
s.Logf("Reboot attempt %d (boot id %s)", i, bootID)
msg := fmt.Sprintf("DevBootKeyboardStress iteration %d boot id %s", i, bootID)
if out, err := h.DUT.Conn().CommandContext(ctx, "logger", msg).CombinedOutput(); err != nil {
return ecOutput.String(), errors.Errorf("failed to write to log: %s", out)
}
// Sync to preserve /var/log/messages for the current boot in case of unexpected crash.
if out, err := h.DUT.Conn().CommandContext(ctx, "sync").CombinedOutput(); err != nil {
return ecOutput.String(), errors.Errorf("failed to sync filesystem: %s", out)
}
if err := powercontrol.Shutdown(ctx, h.DUT); err != nil {
return ecOutput.String(), errors.Wrap(err, "failed to shutdown")
}
if err := h.WaitForPowerStates(ctx, firmware.PowerStateInterval, 30*time.Second, "S5", "G3"); err != nil {
return ecOutput.String(), errors.Wrap(err, "failed to get S5/G3 powerstate")
}
out, err := h.GetECConsoleOutputWithComment(ctx, msg)
if err != nil {
return ecOutput.String(), errors.Wrap(err, "failed to get EC console")
}
ecOutput.WriteString(out)
s.Log("Power on to developer firmware screen")
if err := h.Servo.KeypressWithDuration(ctx, servo.PowerKey, servo.DurShortPress); err != nil {
return ecOutput.String(), errors.Wrap(err, "failed to press power")
}
// Check the keyboard is enabled while the developer screen is up.
// GoBigSleepLint: Wait long enough for screen to be shown.
testing.Sleep(ctx, 20*time.Second)
checkKeyboard(ctx, s, h.Servo)
// GoBigSleepLint: Check keyboard again while screen is still shown.
testing.Sleep(ctx, 5*time.Second)
checkKeyboard(ctx, s, h.Servo)
// GoBigSleepLint: Wait for screen to timeout and boot to OS.
testing.Sleep(ctx, 25*time.Second)
devScreenOutput, err := h.GetECConsoleOutputWithComment(ctx, "done waiting for boot")
if err != nil {
return ecOutput.String(), errors.Wrap(err, "failed to get EC console")
}
ecOutput.WriteString(devScreenOutput)
// Expect to see "scancode set to 2" twice (at start and end of dev screen).
matches := regexp.MustCompile(`scancode set to (\d+)`).FindAllStringSubmatch(devScreenOutput, -1)
if len(matches) != 2 {
s.Error("Failed to find 2 scancode lines: ", matches)
}
for _, v := range matches {
if v[1] != "2" {
s.Error("Wrong scancode: ", matches)
}
}
connectCtx, cancel := context.WithTimeout(ctx, h.Config.DelayRebootToPing)
defer cancel()
if err := h.WaitConnect(connectCtx); err != nil {
return ecOutput.String(), errors.Wrap(err, "device did not come back up")
}
if newBootID, err := h.Reporter.BootID(ctx); err != nil {
return ecOutput.String(), errors.Wrap(err, "failed to get boot id")
} else if newBootID == bootID {
return ecOutput.String(), errors.New("boot id did not change")
} else {
bootID = newBootID
}
out, err = h.GetECConsoleOutputWithComment(ctx, "done waiting for connect")
if err != nil {
return ecOutput.String(), errors.Wrap(err, "failed to get EC console")
}
ecOutput.WriteString(out)
return ecOutput.String(), nil
}
func checkKeyboard(ctx context.Context, s *testing.State, svo *servo.Servo) bool {
kbEn, err := svo.RunECCommandGetOutput(ctx, "8042 kbd", []string{`Enabled: (\d)`})
if err != nil {
s.Error("Failed 8042 kbd: ", err)
return false
}
if len(kbEn) == 0 || kbEn[0][1] != "1" {
s.Error("Keyboard not enabled: ", kbEn)
return false
}
s.Log("Keyboard is enabled")
return true
}