/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ota.tests; import com.android.tradefed.build.IBuildInfo; import com.android.tradefed.config.ConfigurationException; import com.android.tradefed.config.IConfiguration; import com.android.tradefed.config.IConfigurationReceiver; import com.android.tradefed.config.Option; import com.android.tradefed.config.Option.Importance; import com.android.tradefed.config.OptionClass; import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.device.ITestDevice; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.result.ITestInvocationListener; import com.android.tradefed.result.InputStreamSource; import com.android.tradefed.result.LogDataType; import com.android.tradefed.result.SnapshotInputStreamSource; import com.android.tradefed.targetprep.BuildError; import com.android.tradefed.targetprep.ITargetPreparer; import com.android.tradefed.targetprep.TargetSetupError; import com.android.tradefed.testtype.IBuildReceiver; import com.android.tradefed.testtype.IDeviceTest; import com.android.tradefed.testtype.IRemoteTest; import com.android.tradefed.testtype.IResumableTest; import com.android.tradefed.testtype.IShardableTest; import com.android.tradefed.util.FileUtil; import com.android.tradefed.util.IRunUtil; import com.android.tradefed.util.RunUtil; import junit.framework.Assert; import junit.framework.AssertionFailedError; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; /** * A test that will perform repeated flash + install OTA actions on a device. * <p/> * adb must have root. * <p/> * Expects a {@link OtaDeviceBuildInfo}. * <p/> * Note: this test assumes that the {@link ITargetPreparer}s included in this test's * {@link IConfiguration} will flash the device back to a baseline build, and prepare the device to * receive the OTA to a new build. */ @OptionClass(alias = "ota-stability") public class SideloadOtaStabilityTest implements IDeviceTest, IBuildReceiver, IConfigurationReceiver, IShardableTest, IResumableTest { private static final String CACHE_OTA_PATH = "/cache/recovery/otatest/update.zip"; private static final String RECOVERY_COMMAND_PATH = "/cache/recovery/command"; private OtaDeviceBuildInfo mOtaDeviceBuild; private IConfiguration mConfiguration; private ITestDevice mDevice; @Option(name = "run-name", description = "The name of the ota stability test run. Used to report metrics.") private String mRunName = "ota-stability"; @Option(name = "iterations", description = "Number of ota stability 'flash + wait for ota' iterations to run.") private int mIterations = 20; @Option(name = "shards", description = "Optional number of shards to split test into. " + "Iterations will be split evenly among shards.", importance = Importance.IF_UNSET) private Integer mShards = null; @Option(name = "resume", description = "Resume the ota test run if an device setup error " + "stopped the previous test run.") private boolean mResumeMode = false; @Option(name = "max-install-time", description = "The maximum time to wait for an ota to install in seconds.") private int mMaxInstallOnlineTimeSec = 5 * 60; /** controls if this test should be resumed. Only used if mResumeMode is enabled */ private boolean mResumable = true; /** * {@inheritDoc} */ @Override public void setConfiguration(IConfiguration configuration) { mConfiguration = configuration; } /** * {@inheritDoc} */ @Override public void setBuild(IBuildInfo buildInfo) { mOtaDeviceBuild = (OtaDeviceBuildInfo)buildInfo; } /** * {@inheritDoc} */ @Override public void setDevice(ITestDevice device) { mDevice = device; } /** * {@inheritDoc} */ @Override public ITestDevice getDevice() { return mDevice; } /** * Set the run name */ void setRunName(String runName) { mRunName = runName; } /** * Return the number of iterations. * <p/> * Exposed for unit testing */ public int getIterations() { return mIterations; } /** * Set the iterations */ void setIterations(int iterations) { mIterations = iterations; } /** * Set the number of shards */ void setShards(int shards) { mShards = shards; } /** * {@inheritDoc} */ @Override public Collection<IRemoteTest> split() { if (mShards == null || mShards <= 1) { return null; } Collection<IRemoteTest> shards = new ArrayList<IRemoteTest>(mShards); int remainingIterations = mIterations; for (int i = mShards; i > 0; i--) { SideloadOtaStabilityTest testShard = new SideloadOtaStabilityTest(); // device and configuration will be set by test invoker testShard.setRunName(mRunName); // attempt to divide iterations evenly among shards with no remainder int iterationsForShard = remainingIterations / i; if (iterationsForShard > 0) { testShard.setIterations(iterationsForShard); remainingIterations -= iterationsForShard; shards.add(testShard); } } return shards; } /** * {@inheritDoc} */ @Override public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { // started run, turn to off mResumable = false; checkFields(); String expectedBuildId = mOtaDeviceBuild.getOtaBuild().getOtaPackageVersion(); CLog.i("Starting OTA sideload test from %s to %s, for %d iterations", mOtaDeviceBuild.getDeviceImageVersion(), expectedBuildId, mIterations); long startTime = System.currentTimeMillis(); listener.testRunStarted(mRunName, 0); int actualIterations = 0; try { File otaFile = mOtaDeviceBuild.getOtaBuild().getOtaPackageFile(); while (actualIterations < mIterations) { if (actualIterations != 0) { // don't need to flash device on first iteration flashDevice(); } installOta(listener, otaFile, expectedBuildId); actualIterations++; CLog.i("Device %s successfully OTA-ed to build %s. Iteration: %d of %d", mDevice.getSerialNumber(), expectedBuildId, actualIterations, mIterations); } } catch (AssertionFailedError error) { CLog.e(error); } catch (TargetSetupError e) { CLog.i("Encountered TargetSetupError, marking this test as resumable"); mResumable = true; CLog.e(e); // throw up an exception so this test can be resumed Assert.fail(e.toString()); } catch (BuildError e) { CLog.e(e); } catch (ConfigurationException e) { CLog.e(e); } finally { Map<String, String> metrics = new HashMap<String, String>(1); metrics.put("iterations", Integer.toString(actualIterations)); metrics.put("failed_iterations", Integer.toString(mIterations - actualIterations)); long endTime = System.currentTimeMillis() - startTime; listener.testRunEnded(endTime, metrics); } } /** * Flash the device back to baseline build. * <p/> * Currently does this by re-running {@link ITargetPreparer#setUp(ITestDevice, IBuildInfo)} * * @throws DeviceNotAvailableException * @throws BuildError * @throws TargetSetupError * @throws ConfigurationException */ private void flashDevice() throws TargetSetupError, BuildError, DeviceNotAvailableException, ConfigurationException { // assume the target preparers will flash the device back to device build for (ITargetPreparer preparer : mConfiguration.getTargetPreparers()) { preparer.setUp(mDevice, mOtaDeviceBuild); } } /** * Get the {@link IRunUtil} instance to use. * <p/> * Exposed so unit tests can mock. */ IRunUtil getRunUtil() { return RunUtil.getDefault(); } private void checkFields() { if (mDevice == null) { throw new IllegalArgumentException("missing device"); } if (mConfiguration == null) { throw new IllegalArgumentException("missing configuration"); } if (mOtaDeviceBuild == null) { throw new IllegalArgumentException("missing build info"); } } /** * {@inheritDoc} */ @Override public boolean isResumable() { return mResumeMode && mResumable; } private void installOta(ITestInvocationListener listener, File otaFile, String expectedBuildId) throws DeviceNotAvailableException { Assert.assertTrue(mDevice.pushFile(otaFile, CACHE_OTA_PATH)); String installOtaCmd = String.format("--update_package=%s", CACHE_OTA_PATH); mDevice.pushString(installOtaCmd, RECOVERY_COMMAND_PATH); try { mDevice.rebootIntoRecovery(); } catch (DeviceNotAvailableException e) { CLog.e("Device %s did not boot into recovery", mDevice.getSerialNumber()); throw e; } try { mDevice.waitForDeviceOnline(mMaxInstallOnlineTimeSec * 1000); } catch (DeviceNotAvailableException e) { CLog.e("Device %s did not come back online after recovery", mDevice.getSerialNumber()); sendRecoveryLog(listener); throw e; } try { mDevice.waitForDeviceAvailable(); } catch (DeviceNotAvailableException e) { CLog.e("Device %s did not boot up successfully after installing OTA", mDevice.getSerialNumber()); throw e; } Assert.assertEquals("build id does not equal expected value after OTA", mDevice.getBuildId(), expectedBuildId); // TODO: check baseband, bootloader versions as well } private void sendRecoveryLog(ITestInvocationListener listener) throws DeviceNotAvailableException { File destFile = null; InputStreamSource destSource = null; try { // get recovery log destFile = FileUtil.createTempFile("recovery", "log"); boolean gotFile = mDevice.pullFile("/tmp/recovery.log", destFile); if (gotFile) { destSource = new SnapshotInputStreamSource(new FileInputStream(destFile)); listener.testLog("recovery_log", LogDataType.TEXT, destSource); } } catch (IOException e) { CLog.e("Failed to get recovery log from device %s", mDevice.getSerialNumber()); CLog.e(e); } finally { if (destSource != null) { destSource.cancel(); } FileUtil.deleteFile(destFile); } } }