| // Copyright 2022 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" |
| "encoding/json" |
| "fmt" |
| "regexp" |
| "strconv" |
| "strings" |
| "time" |
| "unicode" |
| |
| "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/firmware/reporters" |
| |
| "go.chromium.org/tast/core/ctxutil" |
| "go.chromium.org/tast/core/errors" |
| "go.chromium.org/tast/core/ssh" |
| "go.chromium.org/tast/core/testing" |
| "go.chromium.org/tast/core/testing/hwdep" |
| ) |
| |
| func init() { |
| testing.AddTest(&testing.Test{ |
| Func: FlagsPreservation, |
| Desc: "Checks that flag values are preserved over different power cycles", |
| Contacts: []string{ |
| "chromeos-faft@google.com", |
| "arthur.chuang@cienet.com", |
| }, |
| BugComponent: "b:792402", // ChromeOS > Platform > Enablement > Firmware > FAFT |
| Attr: []string{"group:firmware", "firmware_bios", "firmware_level4"}, |
| Requirements: []string{"sys-fw-0021-v01", "sys-fw-0024-v01", "sys-fw-0025-v01"}, |
| Fixture: fixture.DevModeGBB, |
| SoftwareDeps: []string{"crossystem"}, |
| HardwareDeps: hwdep.D(hwdep.ChromeEC(), hwdep.Battery()), |
| Timeout: 50 * time.Minute, |
| LacrosStatus: testing.LacrosVariantUnneeded, |
| }) |
| } |
| |
| type crossystemValues struct { |
| devBootUSBVal string |
| devBootAltfw string |
| fwUpdateTriesVal string |
| locIdxVal string |
| backupNvramRequest string |
| } |
| |
| type fwUpdaterVersions struct { |
| RO string `json:"ro"` |
| RW string `json:"rw"` |
| } |
| |
| type currentFwVersions struct { |
| ro string |
| rw string |
| } |
| |
| func FlagsPreservation(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) |
| } |
| |
| if err := h.RequireConfig(ctx); err != nil { |
| s.Fatal("Failed to create config: ", err) |
| } |
| |
| cs := crossystemValues{} |
| s.Log("Saving original crossystem values under evaluation to restore at the end of test") |
| csOriginal, err := readTargetCsVals(ctx, s, h, cs) |
| if err != nil { |
| s.Fatal("Failed to read initial crossystem values: ", err) |
| } |
| originalCrossystemMap := createCsMap(csOriginal) |
| |
| cleanupCtx := ctx |
| ctx, cancel := ctxutil.Shorten(ctx, 5*time.Minute) |
| defer cancel() |
| |
| defer func(ctx context.Context) { |
| // Run a cold reset first to ensure DUT connected. |
| s.Log("Cold resetting DUT at the end of test") |
| if err := h.Servo.SetPowerState(ctx, servo.PowerStateReset); err != nil { |
| s.Fatal("Failed to cold reset DUT at the end of test: ", err) |
| } |
| s.Log("Waiting for reconnection to DUT") |
| if err := h.WaitConnect(ctx); err != nil { |
| s.Fatal("Unable to reconnect to DUT: ", err) |
| } |
| s.Log("Restoring crossystem values to the original settings") |
| if err := setTargetCsVals(ctx, s, h, originalCrossystemMap); err != nil { |
| s.Fatal("Failed to restore crossystem values to the original settings: ", err) |
| } |
| }(cleanupCtx) |
| |
| // Target crossystem params and their values to be tested. |
| targetCrossystemMap := map[reporters.CrossystemParam]string{ |
| reporters.CrossystemParamDevBootUsb: "1", |
| reporters.CrossystemParamDevBootAltfw: "1", |
| reporters.CrossystemParamFWUpdatetries: "2", |
| reporters.CrossystemParamLocIdx: "3", |
| } |
| |
| s.Log("Setting crossystem params with target values") |
| if err := setTargetCsVals(ctx, s, h, targetCrossystemMap); err != nil { |
| s.Fatal("Failed to set target crossystem values: ", err) |
| } |
| |
| // On dirinboz and gumboz, running fw update allowed them to wake from a 45W charger. |
| // needFwUpdate first checks the ec, and then ap to find if an update is required. |
| // It will return true as soon as it detects that fw updater has a newer version, either |
| // for ro, or for rw. In case that the dut doesn't wake up during power cycles, include |
| // information from needFwUpdate for debugging purposes. |
| needFwUpdate := func() string { |
| for _, programmer := range []firmware.FWType{firmware.ECFirmware, firmware.APFirmware} { |
| if val, err := compareForFwUpdate(ctx, h, programmer); err != nil { |
| s.Log("Failed to determine if fw update is required: ", err) |
| return "unknown" |
| } else if val { |
| return "need to run firmware update mode recovery" |
| } |
| } |
| return "all fw versions are up to date" |
| } |
| fwStatus := needFwUpdate() |
| for _, powerDisruption := range []string{ |
| "powerCycleByReboot", |
| "powerCycleByPressingPowerKey", |
| "powerCycleByRemovingBattery", |
| } { |
| s.Log("Saving crossystem params and their values before a power-cycle") |
| csBefore, err := readTargetCsVals(ctx, s, h, cs) |
| if err != nil { |
| s.Fatal("Failed to read crossystem values before a power-cycle: ", err) |
| } |
| beforeCrossystemMap := createCsMap(csBefore) |
| |
| switch powerDisruption { |
| case "powerCycleByReboot": |
| s.Log("Power-cycling DUT with a reboot") |
| if err := h.Servo.SetPowerState(ctx, servo.PowerStateReset); err != nil { |
| s.Fatal("Failed to reboot DUT by servo: ", err) |
| } |
| case "powerCycleByPressingPowerKey": |
| // pressPowerBtn sends a press on the power button and waits for DUT to reach G3. |
| pressPowerBtn := func(pressDur time.Duration) error { |
| s.Logf("Power-cycling DUT by pressing power button for %s", pressDur) |
| if err := h.Servo.KeypressWithDuration(ctx, servo.PowerKey, servo.Dur(pressDur)); err != nil { |
| return errors.Wrap(err, "failed to press power through servo") |
| } |
| s.Log("Waiting for power state to become G3") |
| if err := h.WaitForPowerStates(ctx, firmware.PowerStateInterval, 2*time.Minute, "G3"); err != nil { |
| return errors.Wrap(err, "failed to get powerstate at G3") |
| } |
| return nil |
| } |
| // Retry powering off DUT with a longer press if the duration was less than |
| // 10 seconds on the power button, and the DUT remained at S0. |
| powerOff := h.Config.HoldPwrButtonPowerOff |
| if powerOff >= 10*time.Second { |
| if err := pressPowerBtn(powerOff); err != nil { |
| s.Fatal("Failed to power off DUT: ", err) |
| } |
| } else { |
| for powerOff < 10*time.Second { |
| err := pressPowerBtn(powerOff) |
| if err == nil { |
| break |
| } |
| powerState, _ := checkPowerState(ctx, h) |
| if powerState != "S0" || powerOff == 9*time.Second { |
| s.Fatal("Failed to power off DUT: ", err) |
| } |
| powerOff += 1 * time.Second |
| } |
| } |
| s.Log("Pressing on the power button to power on DUT") |
| if err := h.Servo.KeypressWithDuration(ctx, servo.PowerKey, servo.DurPress); err != nil { |
| s.Fatal("Failed to perform a tap on the power button: ", err) |
| } |
| case "powerCycleByRemovingBattery": |
| // Explicitly log servo type and dut connection type for debugging purposes. |
| s.Log("Logging servo type and dut connection type") |
| var validInfo = []string{"servoType", "dutConnectionType"} |
| for _, info := range validInfo { |
| if result, err := logInformation(ctx, h, info); err != nil { |
| s.Logf("Unable to find information on %s: %v", info, err) |
| } else { |
| s.Logf("%s: %s", info, result) |
| } |
| } |
| |
| // Dirinboz [zork] did not wake from battery cutoff with a 45W charger, |
| // but it did with a 65W. Also, when connected to a servo v4 board, the |
| // same 65W charger only outputs 60W to DUT. On servo v4.1, it output the |
| // full 65W. Log more information about how much power DUT receives from |
| // charger in the lab. |
| if err := checkMaxChargerPower(ctx, h); err != nil { |
| s.Fatal("Failed to check for max power: ", err) |
| } |
| |
| s.Log("Power-cycling DUT by disconnecting AC and removing battery") |
| if err := firmware.PollToSetChargerStatus(ctx, h, false); err != nil { |
| s.Fatal("Failed to remove charger: ", err) |
| } |
| |
| // When power is cut, there's a temporary drop in connection with the DUT. |
| // Wait for DUT to reconnect before proceeding to the next step. |
| waitConnectCtx, cancelWaitConnect := context.WithTimeout(ctx, 2*time.Minute) |
| defer cancelWaitConnect() |
| if err := s.DUT().WaitConnect(waitConnectCtx); err != nil { |
| s.Fatal("Failed to reconnect to DUT: ", err) |
| } |
| |
| // Log information on PD communication status after disconnecting charger. |
| if pdStatus, err := logInformation(ctx, h, "pdCommunication"); err != nil { |
| s.Log("Unable to check PD communication status: ", err) |
| } else { |
| s.Logf("PD communication status: %s", pdStatus) |
| } |
| |
| s.Log("Removing CCD watchdog") |
| if err := h.Servo.RemoveCCDWatchdogs(ctx); err != nil { |
| s.Fatal("Failed to remove watchdog for ccd: ", err) |
| } |
| |
| // Check if DUT is offline after battery cutoff at the end of test. If it is, |
| // first check if power state is in S5 or G3, and boot DUT to S0 by pressing |
| // on power button. If there's no response at all, disconnecting and then |
| // reconnecting charger seems to help with restoring the connection. |
| defer func(ctx context.Context) { |
| if !s.DUT().Connected(ctx) { |
| if err := checkDUTAsleepAndPressPwr(ctx, h); err != nil { |
| s.Logf("DUT completely offline: %v Attempting to restore connection", err) |
| if err := h.Servo.RemoveCCDWatchdogs(ctx); err != nil { |
| s.Fatal("Failed to remove watchdog for ccd: ", err) |
| } |
| s.Log("Sleeping for 5 seconds") |
| // GoBigSleepLint: To-do: depending on how the results turn out, we could |
| // extend the sleep in RemoveCCDWatchdogs(), instead of adding another sleep here. |
| if err := testing.Sleep(ctx, 5*time.Second); err != nil { |
| s.Fatal("Failed to sleep: ", err) |
| } |
| s.Log("Removing DUT's power") |
| if err := h.SetDUTPower(ctx, false); err != nil { |
| s.Fatal("Failed to set pd role: ", err) |
| } |
| s.Log("Sleeping for 60 seconds") |
| // GoBigSleepLint: Allow some time before reconnecting charger. |
| if err := testing.Sleep(ctx, 60*time.Second); err != nil { |
| s.Fatal("Failed to sleep: ", err) |
| } |
| s.Log("Reconnecting DUT's power") |
| if err := h.SetDUTPower(ctx, true); err != nil { |
| s.Fatal("Failed to set pd role: ", err) |
| } |
| } |
| if err := h.WaitConnect(ctx); err != nil { |
| s.Fatal("DUT did not wake up: ", err) |
| } |
| if hasCCD, err := h.Servo.HasCCD(ctx); err != nil { |
| s.Fatal("While checking if servo has a CCD connection: ", err) |
| } else if hasCCD { |
| if err := h.OpenCCD(ctx, true, true); err != nil { |
| s.Fatal("CCD not opened: ", err) |
| } |
| } |
| } |
| }(cleanupCtx) |
| |
| s.Log("Cutting off DUT's battery") |
| cmd := firmware.NewECTool(s.DUT(), firmware.ECToolNameMain) |
| if err := cmd.BatteryCutoff(ctx); err != nil { |
| s.Fatal("Failed to send the battery cutoff command: ", err) |
| } |
| |
| s.Log("Sleeping for 60 seconds") |
| // GoBigSleepLint: 60 seconds of sleep may be necessary in order for some batteries to |
| // be fully cut off, and for reducing complications in waking DUTs. |
| if err := testing.Sleep(ctx, 60*time.Second); err != nil { |
| s.Fatal("Failed to sleep: ", err) |
| } |
| |
| s.Log("Checking EC unresponsive") |
| if err := h.Servo.CheckUnresponsiveEC(ctx); err != nil { |
| s.Fatal("While verifying whether EC is unresponsive after cutting off DUT's battery: ", err) |
| } |
| |
| s.Log("Powering on DUT by reconnecting AC") |
| if err := firmware.PollToSetChargerStatus(ctx, h, true); err != nil { |
| s.Fatal("Failed to reconnect charger: ", err) |
| } |
| |
| s.Log("Waiting for battery to be connected") |
| if err := firmware.WaitForBatteryConnection(ctx, h, 1*time.Minute, 5*time.Second); err != nil { |
| s.Fatal("Failed to wait for battery connection: ", err) |
| } |
| |
| // After reconnecting AC, DUT should get into S0. |
| // If it's not in S0, try with a press on power. |
| s.Log("Waiting for powerstate S0") |
| if err := h.WaitForPowerStates(ctx, firmware.PowerStateInterval, 1*time.Minute, "S0"); err != nil { |
| s.Log("Failed to get power state at S0: ", err) |
| } |
| |
| pwrState, err := checkPowerState(ctx, h) |
| if err != nil { |
| s.Fatal("Failed to check power state: ", err) |
| } |
| s.Log("Powerstate: ", pwrState) |
| if pwrState != "S0" { |
| s.Logf("Pressing power button for %s to wake DUT into S0", h.Config.HoldPwrButtonPowerOn) |
| if err := h.Servo.KeypressWithDuration(ctx, servo.PowerKey, servo.Dur(h.Config.HoldPwrButtonPowerOn)); err != nil { |
| s.Fatal("Failed to press power button: ", err) |
| } |
| } |
| } |
| |
| s.Log("Waiting for DUT to power ON") |
| waitConnectCtx, cancelWaitConnect := context.WithTimeout(ctx, 8*time.Minute) |
| defer cancelWaitConnect() |
| |
| if err := h.WaitConnect(waitConnectCtx, firmware.SkipPDRoleSnk); err != nil { |
| // When reconnecting to the DUT fails from plugging in AC, |
| // check for its power state, charge state, and whether |
| // the battery is detected (present). Also, log gpio output |
| // indicating when the system power is good for AP to pwr up. |
| pwrState, _ := checkPowerState(ctx, h) |
| stateOfCharge, _ := checkChgstateBatt(ctx, h, "state_of_charge") |
| battPresent, _ := checkChgstateBatt(ctx, h, "is_present") |
| // To-do: EC_FCH_PWROK was configured for zork devices, for example, |
| // vilboz and dirinboz. Add more gpio names in the future if more |
| // models are to be checked. |
| pwrOkGpio, _ := grepGpio(ctx, h, "EC_FCH_PWROK") |
| s.Fatalf("Failed to reconnect to DUT [power state %s, battery %s, %s, gpio_pwrok %s, fw status: %s]: %v", |
| pwrState, stateOfCharge, battPresent, pwrOkGpio, fwStatus, err) |
| } |
| // Cr50 goes to sleep when the battery is disconnected, and when DUT wakes, |
| // CCD might be locked. Open CCD after waking DUT and before talking to the EC. |
| if hasCCD, err := h.Servo.HasCCD(ctx); err != nil { |
| s.Fatal("While checking if servo has a CCD connection: ", err) |
| } else if hasCCD { |
| if err := h.OpenCCD(ctx, true, true); err != nil { |
| s.Fatal("CCD not opened: ", err) |
| } |
| } |
| |
| s.Log("Saving crossystem params and their values after a power-cycle") |
| csAfter, err := readTargetCsVals(ctx, s, h, cs) |
| if err != nil { |
| s.Fatal("Failed to read crossystem values after a power-cycle: ", err) |
| } |
| afterCrossystemMap := createCsMap(csAfter) |
| |
| // Compare the before and after values in crossystemValues, except backupNvramRequest, |
| // which is only checked on DUTs using vboot1. |
| if ok, err := equal(ctx, beforeCrossystemMap, afterCrossystemMap); err != nil || !ok { |
| s.Fatal("Crossystem values are different after power-cycle: ", err) |
| } else if ok { |
| s.Log("Flags are preserved after power-cycle") |
| } |
| |
| } |
| } |
| |
| func createCsMap(cs *crossystemValues) map[reporters.CrossystemParam]string { |
| var csParamsMap = map[reporters.CrossystemParam]string{ |
| reporters.CrossystemParamDevBootUsb: cs.devBootUSBVal, |
| reporters.CrossystemParamDevBootAltfw: cs.devBootAltfw, |
| reporters.CrossystemParamFWUpdatetries: cs.fwUpdateTriesVal, |
| reporters.CrossystemParamLocIdx: cs.locIdxVal, |
| } |
| return csParamsMap |
| } |
| |
| func readTargetCsVals(ctx context.Context, s *testing.State, h *firmware.Helper, cs crossystemValues) (*crossystemValues, error) { |
| s.Log("Reading crossystem values under test") |
| var csParamsMap = map[reporters.CrossystemParam]*string{ |
| reporters.CrossystemParamDevBootUsb: &cs.devBootUSBVal, |
| reporters.CrossystemParamDevBootAltfw: &cs.devBootAltfw, |
| reporters.CrossystemParamFWUpdatetries: &cs.fwUpdateTriesVal, |
| reporters.CrossystemParamLocIdx: &cs.locIdxVal, |
| reporters.CrossystemParamBackupNvramRequest: &cs.backupNvramRequest, |
| } |
| for csKey, csVal := range csParamsMap { |
| current, err := h.Reporter.CrossystemParam(ctx, csKey) |
| if err != nil { |
| return nil, err |
| } |
| *csVal = current |
| } |
| return &cs, nil |
| } |
| |
| func setTargetCsVals(ctx context.Context, s *testing.State, h *firmware.Helper, targetMap map[reporters.CrossystemParam]string) error { |
| targetArgs := make([]string, len(targetMap)) |
| i := 0 |
| for targetKey, targetVal := range targetMap { |
| targetArgs[i] = fmt.Sprintf("%s=%s", targetKey, targetVal) |
| i++ |
| } |
| if err := h.DUT.Conn().CommandContext(ctx, "crossystem", targetArgs...).Run(); err != nil { |
| return errors.Wrapf(err, "running crossystem %s", strings.Join(targetArgs, " ")) |
| } |
| return nil |
| } |
| |
| func equal(ctx context.Context, mapBefore, mapAfter map[reporters.CrossystemParam]string) (bool, error) { |
| if len(mapBefore) != len(mapAfter) { |
| return false, errors.New("Lengths of maps under evaluation do not match") |
| } |
| for k, v := range mapBefore { |
| if elem, ok := mapAfter[k]; !ok || v != elem { |
| return false, errors.Errorf("found mismatch in key %q, values are different: one has %q, while the other has %q", k, mapBefore[k], mapAfter[k]) |
| } |
| } |
| return true, nil |
| } |
| |
| // logInformation logs some information for debugging purposes. |
| func logInformation(ctx context.Context, h *firmware.Helper, information string) (string, error) { |
| var ( |
| err error |
| dutConnection servo.DUTConnTypeValue |
| result string |
| ) |
| switch information { |
| case "servoType": |
| result, err = h.Servo.GetServoType(ctx) |
| case "dutConnectionType": |
| dutConnection, err = h.Servo.GetDUTConnectionType(ctx) |
| result = string(dutConnection) |
| case "pdCommunication": |
| result, err = h.Servo.GetPDCommunication(ctx) |
| default: |
| result = "Unknown information" |
| } |
| if err != nil { |
| return "", err |
| } |
| return result, nil |
| } |
| |
| // checkDUTAsleepAndPressPwr checks if DUT is at S5 or G3, and presses power button to boot it. |
| func checkDUTAsleepAndPressPwr(ctx context.Context, h *firmware.Helper) error { |
| shortCtx, cancelShortCtx := context.WithTimeout(ctx, 2*time.Minute) |
| defer cancelShortCtx() |
| testing.ContextLog(shortCtx, "Checking if power state is at S5 or G3") |
| if err := h.WaitForPowerStates(shortCtx, firmware.PowerStateInterval, 1*time.Minute, "S5", "G3"); err != nil { |
| return err |
| } |
| testing.ContextLogf(shortCtx, "Pressing power button for %s to wake DUT", h.Config.HoldPwrButtonPowerOn) |
| if err := h.Servo.KeypressWithDuration(shortCtx, servo.PowerKey, servo.Dur(h.Config.HoldPwrButtonPowerOn)); err != nil { |
| return errors.Wrap(err, "failed to press power button") |
| } |
| return nil |
| } |
| |
| // checkMaxChargerPower runs the local command 'power_supply_info' on DUT |
| // to check for max voltage and max current, and multiply them to get max power. |
| func checkMaxChargerPower(ctx context.Context, h *firmware.Helper) error { |
| // Regular expressions. |
| var ( |
| reMaxVoltage = regexp.MustCompile(`max\s+voltage\D*([^\n\r]*)`) |
| reMaxCurrnet = regexp.MustCompile(`max\s+current\D*([^\n\r]*)`) |
| ) |
| out, err := h.DUT.Conn().CommandContext(ctx, "power_supply_info").Output() |
| if err != nil { |
| return errors.Wrap(err, "failed to retrieve power supply info from DUT") |
| } |
| findMatch := func(maxPowerVar string, pattern *regexp.Regexp, scannedOut []byte) (float64, error) { |
| match := pattern.FindStringSubmatch(string(scannedOut)) |
| if len(match) < 2 { |
| return 0, errors.Errorf("did not find value for %s", maxPowerVar) |
| } |
| val, err := strconv.ParseFloat(match[1], 64) |
| if err != nil { |
| return 0, errors.Wrapf(err, "failed to parse for %s", maxPowerVar) |
| } |
| return val, nil |
| } |
| maxVoltage, err := findMatch("max_voltage", reMaxVoltage, out) |
| if err != nil { |
| return err |
| } |
| maxCurrent, err := findMatch("max_current", reMaxCurrnet, out) |
| if err != nil { |
| return err |
| } |
| testing.ContextLogf(ctx, "DUT receives max_voltage: %f, max_current: %f, max_power: %f", |
| maxVoltage, maxCurrent, maxVoltage*maxCurrent) |
| return nil |
| } |
| |
| // checkChgstateBatt runs ec command 'chgstate' and collects information |
| // from the battery section. |
| func checkChgstateBatt(ctx context.Context, h *firmware.Helper, attr string) (string, error) { |
| chgState, err := firmware.GetChargingState(ctx, h) |
| if err != nil { |
| return "unknown", errors.Wrap(err, "failed to get charging state") |
| } |
| key := "batt." + attr |
| return chgState[key], nil |
| } |
| |
| // grepGpio runs ec command 'gpioget' to check for a gpio's value. |
| func grepGpio(ctx context.Context, h *firmware.Helper, name string) (string, error) { |
| if err := h.Servo.RunECCommand(ctx, "chan save"); err != nil { |
| return "unknown", errors.Wrap(err, "failed to send 'chan save' to EC") |
| } |
| if err := h.Servo.RunECCommand(ctx, "chan 0"); err != nil { |
| return "unknown", errors.Wrap(err, "failed to send 'chan 0' to EC") |
| } |
| defer func() { |
| if err := h.Servo.RunECCommand(ctx, "chan restore"); err != nil { |
| testing.ContextLog(ctx, "Failed to send 'chan restore' to EC: ", err) |
| } |
| }() |
| match := fmt.Sprintf(`(?i)(0|1)[^\n\r]*\s%s`, name) |
| cmd := fmt.Sprintf("gpioget %s", name) |
| out, err := h.Servo.RunECCommandGetOutput(ctx, cmd, []string{match}) |
| if err != nil { |
| return "unknown", errors.Wrapf(err, "failed to run command %v", cmd) |
| } |
| return out[0][1], nil |
| } |
| |
| // checkPowerState checks for the dut's power state. |
| func checkPowerState(ctx context.Context, h *firmware.Helper) (string, error) { |
| testing.ContextLog(ctx, "Checking for the DUT's power state") |
| state, err := h.Servo.GetECSystemPowerState(ctx) |
| if err != nil { |
| return "unknown", err |
| } |
| return state, nil |
| } |
| |
| // checkUpdaterFirmware checks for the fw versions contained in the firmware updater archive |
| // for a specific programmer and dut model. |
| func checkUpdaterFirmware(ctx context.Context, h *firmware.Helper, programmer firmware.FWType) (fwUpdaterVersions, error) { |
| testing.ContextLogf(ctx, "Checking %s firmware updater version for %s", programmer, h.Model) |
| checkFirmwareUpdaterManifest := fmt.Sprintf( |
| "chromeos-firmwareupdate --manifest | jq -c .%s.%s.versions", h.Model, programmer) |
| out, err := h.DUT.Conn().CommandContext(ctx, "bash", "-c", checkFirmwareUpdaterManifest).Output(ssh.DumpLogOnError) |
| if err != nil { |
| return fwUpdaterVersions{}, errors.Wrap(err, "failed to check for firmware updater manifest") |
| } |
| var data fwUpdaterVersions |
| if err := json.Unmarshal(out, &data); err != nil { |
| return fwUpdaterVersions{}, errors.Wrap(err, "failed to parse JSON file") |
| } |
| return data, nil |
| } |
| |
| // checkCurrentFirmware checks for the current fw versions running on the dut. |
| // For ec, it runs 'ectool version' to find both the ro and rw versions. |
| // For ap, it runs 'crossystem ro_fwid', and 'crossystem fwid' to find ro and rw respectively. |
| func checkCurrentFirmware(ctx context.Context, h *firmware.Helper, programmer firmware.FWType) (currentFwVersions, error) { |
| var data currentFwVersions |
| switch programmer { |
| case firmware.ECFirmware: |
| ec := firmware.NewECTool(h.DUT, firmware.ECToolNameMain) |
| output, err := ec.Command(ctx, "version").Output(ssh.DumpLogOnError) |
| if err != nil { |
| return currentFwVersions{}, errors.Wrap(err, "failed to run 'ectool version' on DUT") |
| } |
| reROVersion := regexp.MustCompile(`RO version:\s*(\S+)\s`) |
| roVersion := reROVersion.FindSubmatch(output) |
| if len(roVersion) == 0 { |
| return data, errors.Errorf("failed to match regexp %s in ectool version output: %s", reROVersion, output) |
| } |
| reRWVersion := regexp.MustCompile(`RW version:\s*(\S+)\s`) |
| rwVersion := reRWVersion.FindSubmatch(output) |
| if len(rwVersion) == 0 { |
| return data, errors.Errorf("failed to match regexp %s in ectool version output: %s", reRWVersion, output) |
| } |
| data.ro = string(roVersion[1]) |
| data.rw = string(rwVersion[1]) |
| case firmware.APFirmware: |
| rofwid, err := firmware.GetFwVersion(ctx, h, reporters.CrossystemParamRoFwid) |
| if err != nil { |
| return data, errors.Wrap(err, "failed to get crosystem ro_fwid") |
| } |
| rwfwid, err := firmware.GetFwVersion(ctx, h, reporters.CrossystemParamFwid) |
| if err != nil { |
| return data, errors.Wrap(err, "failed to get crossystem fwid") |
| } |
| data.ro = rofwid |
| data.rw = rwfwid |
| default: |
| return data, errors.New("unknown programmer name") |
| } |
| return data, nil |
| } |
| |
| // checkUpdaterVersionBigger checks whether fw updater has newer versions (higher version numbers) than |
| // the current ones running on the dut. |
| func checkUpdaterVersionBigger(ctx context.Context, h *firmware.Helper, fwUpdater, current string, programmer firmware.FWType) (bool, error) { |
| var fwUpdaterIDs, currentIDs []string |
| testing.ContextLogf(ctx, "Full fw updater version value: %s, current fw version value: %s", fwUpdater, current) |
| switch programmer { |
| case firmware.ECFirmware: |
| // As an example, before parsed into ids, ec fw version is represented by |
| // the following format, "cret_v2.0.11733-88ee536526", where cret is the |
| // dut's model name. Use parseECVerIntoID to extract 2, 0, and 11733. |
| parseECVerIntoID := func(version string) string { |
| str := strings.TrimLeft(version, fmt.Sprintf("%s_v", h.Model)) |
| nums := strings.Split(str, "-") |
| id := nums[0] |
| return id |
| } |
| fwUpdaterIDs = strings.Split(parseECVerIntoID(fwUpdater), ".") |
| currentIDs = strings.Split(parseECVerIntoID(current), ".") |
| case firmware.APFirmware: |
| // As an example, before parsed into ids, host fw version is represented by |
| // the following format, "Google_Cret.13606.426.0". Use parseHostVerIntoID |
| // to extract 13606, 426, and 0. |
| parseHostVerIntoID := func(version string) string { |
| id := strings.TrimLeftFunc(version, func(r rune) bool { return !unicode.IsNumber(r) }) |
| return id |
| } |
| fwUpdaterIDs = strings.Split(parseHostVerIntoID(fwUpdater), ".") |
| currentIDs = strings.Split(parseHostVerIntoID(current), ".") |
| default: |
| return false, errors.New("unknown programmer name") |
| } |
| testing.ContextLogf(ctx, "Found fw updater id: %s, current id: %s", fwUpdaterIDs, currentIDs) |
| for i, val := range fwUpdaterIDs { |
| var idA, idB int |
| // As an example, say we have "fw updater id: [2 0 11733], current id: [2 0 11081]". |
| // Running fmt.Sscanf below would assign '2' to idA, and '2' to idB during the first |
| // iteration. In the second iteration, '0' to idA, and '0' to idB, and so on and so |
| // fourth, until finding out eventually that '11733' is greater than '11081'. |
| if _, err := fmt.Sscanf(val, "%d", &idA); err != nil { |
| return false, errors.Wrapf(err, "failed to sscanf %s", val) |
| } |
| if _, err := fmt.Sscanf(currentIDs[i], "%d", &idB); err != nil { |
| return false, errors.Wrapf(err, "failed to sscanf %s", currentIDs[i]) |
| } |
| if idA > idB { |
| testing.ContextLogf(ctx, "%v is bigger than %v", fwUpdaterIDs, currentIDs) |
| return true, nil |
| } |
| } |
| return false, nil |
| } |
| |
| // compareForFwUpdate returns true if either the ro or rw version number is found bigger from fw updater, |
| // denoting that the dut needs a fw update. |
| func compareForFwUpdate(ctx context.Context, h *firmware.Helper, programmer firmware.FWType) (bool, error) { |
| updater, err := checkUpdaterFirmware(ctx, h, programmer) |
| if err != nil { |
| return false, err |
| } |
| current, err := checkCurrentFirmware(ctx, h, programmer) |
| if err != nil { |
| return false, err |
| } |
| testing.ContextLogf(ctx, "Compaing %s ro fw versions", programmer) |
| updateRO, err := checkUpdaterVersionBigger(ctx, h, updater.RO, current.ro, programmer) |
| if err != nil { |
| return false, err |
| } |
| testing.ContextLogf(ctx, "Compaing %s rw fw versions", programmer) |
| updateRW, err := checkUpdaterVersionBigger(ctx, h, updater.RW, current.rw, programmer) |
| if err != nil { |
| return false, err |
| } |
| if updateRO || updateRW { |
| return true, nil |
| } |
| return false, nil |
| } |