1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tradefed.testtype; 18 19 import com.android.tradefed.config.ConfigurationException; 20 import com.android.tradefed.config.OptionCopier; 21 import com.android.tradefed.device.DeviceNotAvailableException; 22 import com.android.tradefed.log.LogUtil.CLog; 23 import com.android.tradefed.result.CollectingTestListener; 24 import com.android.tradefed.result.FilteredResultForwarder; 25 import com.android.tradefed.result.ITestInvocationListener; 26 import com.android.tradefed.result.TestDescription; 27 import com.android.tradefed.util.FileUtil; 28 29 import com.google.common.annotations.VisibleForTesting; 30 31 import java.io.BufferedWriter; 32 import java.io.File; 33 import java.io.FileWriter; 34 import java.io.IOException; 35 import java.util.Collection; 36 import java.util.LinkedHashMap; 37 import java.util.Map; 38 39 /** 40 * Runs a set of instrumentation tests by specifying a list of line separated test classes and 41 * methods in a file pushed to device (expected format: com.android.foo.FooClassName#testMethodName) 42 * <p> 43 * Note: Requires a runner that supports test execution from a file. Will default to serial tests 44 * execution via {@link InstrumentationSerialTest} if any issues with file creation are encountered 45 * or if all tests in the created file fail to successfully finish execution. 46 */ 47 class InstrumentationFileTest implements IRemoteTest { 48 49 // on device test folder location where the test file should be saved 50 private static final String ON_DEVICE_TEST_DIR_LOCATION = "/data/local/tmp/"; 51 /** Key that matches the -e package option for instrumentation. */ 52 private static final String PACKAGE_ARG_KEY = "package"; 53 54 private InstrumentationTest mInstrumentationTest = null; 55 56 /** the set of tests to run */ 57 private final Collection<TestDescription> mTests; 58 59 private String mFilePathOnDevice = null; 60 61 private int mAttemps; 62 63 private int mMaxAttemps; 64 65 private boolean mRetrySerially; 66 67 /** 68 * Creates a {@link InstrumentationFileTest}. 69 * 70 * @param instrumentationTest {@link InstrumentationTest} used to configure this class 71 * @param testsToRun a {@link Collection} of tests to run. Note this {@link Collection} will be 72 * used as is (ie a reference to the testsToRun object will be kept). 73 */ InstrumentationFileTest( InstrumentationTest instrumentationTest, Collection<TestDescription> testsToRun, boolean retrySerially, int maxAttempts)74 InstrumentationFileTest( 75 InstrumentationTest instrumentationTest, 76 Collection<TestDescription> testsToRun, 77 boolean retrySerially, 78 int maxAttempts) 79 throws ConfigurationException { 80 // reuse the InstrumentationTest class to perform actual test run 81 mInstrumentationTest = createInstrumentationTest(); 82 // copy all options from the original InstrumentationTest 83 OptionCopier.copyOptions(instrumentationTest, mInstrumentationTest); 84 mInstrumentationTest.setDevice(instrumentationTest.getDevice()); 85 mInstrumentationTest.setForceAbi(instrumentationTest.getForceAbi()); 86 mInstrumentationTest.setReRunUsingTestFile(true); 87 // no need to rerun when executing tests one by one 88 mInstrumentationTest.setRerunMode(false); 89 mInstrumentationTest.setIsRerun(true); 90 // keep local copy of tests to be run 91 mTests = testsToRun; 92 mAttemps = 0; 93 mRetrySerially = retrySerially; 94 mMaxAttemps = maxAttempts; 95 } 96 97 /** 98 * {@inheritDoc} 99 */ 100 @Override run(final ITestInvocationListener listener)101 public void run(final ITestInvocationListener listener) throws DeviceNotAvailableException { 102 if (mInstrumentationTest.getDevice() == null) { 103 throw new IllegalArgumentException("Device has not been set"); 104 } 105 // reuse InstrumentationTest class to perform actual test run 106 writeTestsToFileAndRun(mTests, listener); 107 } 108 109 110 /** 111 * Creates a file based on the {@link Collection} of tests to run. Upon successful file creation 112 * will push the file onto the test device and attempt to run them via {@link 113 * InstrumentationTest}. If something goes wrong, will default to serial test execution. 114 * 115 * @param tests a {@link Collection} of tests to run 116 * @param listener the test result listener 117 * @throws DeviceNotAvailableException 118 */ writeTestsToFileAndRun( Collection<TestDescription> tests, final ITestInvocationListener listener)119 private void writeTestsToFileAndRun( 120 Collection<TestDescription> tests, final ITestInvocationListener listener) 121 throws DeviceNotAvailableException { 122 mAttemps += 1; 123 if (mMaxAttemps > 0 && mAttemps <= mMaxAttemps) { 124 CLog.d("Try to run tests from file for the %d/%d attempts", 125 mAttemps, mMaxAttemps); 126 } else if (mMaxAttemps > 0) { 127 if (mRetrySerially) { 128 CLog.d("Running tests from file exceeded max attempts." 129 + " Try to run tests serially."); 130 reRunTestsSerially(mInstrumentationTest, listener); 131 } else { 132 CLog.d("Running tests from file exceeded max attempts. Ignore the rest tests"); 133 return; 134 } 135 } 136 File testFile = null; 137 try { 138 // create and populate test file 139 testFile = FileUtil.createTempFile( 140 "tf_testFile_" + InstrumentationFileTest.class.getCanonicalName(), ".txt"); 141 try (BufferedWriter bw = new BufferedWriter(new FileWriter(testFile))) { 142 // Remove parameterized tests to only re-run their base method. 143 Collection<TestDescription> uniqueMethods = createRerunSet(tests); 144 145 for (TestDescription testToRun : uniqueMethods) { 146 // We use getTestNameNoParams to avoid attempting re-running individual 147 // parameterized tests. Instead ask the base method to re-run them all. 148 bw.write( 149 String.format( 150 "%s#%s", 151 testToRun.getClassName(), 152 testToRun.getTestNameWithoutParams())); 153 bw.newLine(); 154 } 155 CLog.d("Test file %s was successfully created", testFile.getAbsolutePath()); 156 } 157 // push test file to the device and run 158 mFilePathOnDevice = ON_DEVICE_TEST_DIR_LOCATION + testFile.getName(); 159 if (pushFileToTestDevice(testFile, mFilePathOnDevice)) { 160 // Unset package name if any just in case to avoid conflict with classname. 161 // Since at that point we explicitly request the class to rerun there is no need to 162 // keep any of the original package options. 163 mInstrumentationTest.setTestPackageName(null); 164 mInstrumentationTest.removeFromInstrumentationArg(PACKAGE_ARG_KEY); 165 mInstrumentationTest.setTestFilePathOnDevice(mFilePathOnDevice); 166 CLog.d("Test file %s was successfully pushed to %s on device", 167 testFile.getAbsolutePath(), mFilePathOnDevice); 168 runTests(mInstrumentationTest, listener); 169 } else { 170 if (mRetrySerially) { 171 CLog.e("Failed to push file to device, re-running tests serially"); 172 reRunTestsSerially(mInstrumentationTest, listener); 173 } else { 174 CLog.e("Failed to push file to device, ignore the rest of tests"); 175 } 176 } 177 } catch (IOException e) { 178 if (mRetrySerially) { 179 CLog.e("Failed to run tests from file, re-running tests serially: %s", 180 e.getMessage()); 181 reRunTestsSerially(mInstrumentationTest, listener); 182 } else { 183 CLog.e("Failed to push file to device, ignore the rest of tests"); 184 } 185 } finally { 186 // clean up test file, if it was created 187 FileUtil.deleteFile(testFile); 188 } 189 } 190 191 /** 192 * Run all tests from file. Attempt to re-run not finished tests. 193 * If all tests in file fail to run default to executing them serially. 194 */ runTests(InstrumentationTest runner, ITestInvocationListener listener)195 private void runTests(InstrumentationTest runner, ITestInvocationListener listener) 196 throws DeviceNotAvailableException { 197 CollectingTestListener testTracker = new CollectingTestListener(); 198 try { 199 runner.run(new FilteredResultForwarder(mTests, listener, testTracker)); 200 } finally { 201 deleteTestFileFromDevice(mFilePathOnDevice); 202 Collection<TestDescription> completedTests = 203 testTracker.getCurrentRunResults().getCompletedTests(); 204 if (mTests.removeAll(completedTests) && !mTests.isEmpty()) { 205 // re-run remaining tests from file 206 writeTestsToFileAndRun(mTests, listener); 207 } else if (!mTests.isEmpty()) { 208 if (mRetrySerially) { 209 CLog.e("all remaining tests failed to run from file, re-running tests serially"); 210 reRunTestsSerially(runner, listener); 211 } else { 212 CLog.e("all remaining tests failed to run from file, will be ignored"); 213 } 214 } 215 } 216 } 217 218 /** 219 * Re-runs remaining tests one-by-one 220 */ reRunTestsSerially(InstrumentationTest runner, ITestInvocationListener listener)221 private void reRunTestsSerially(InstrumentationTest runner, ITestInvocationListener listener) 222 throws DeviceNotAvailableException { 223 // clear file path arguments to ensure it won't get used. 224 runner.setTestFilePathOnDevice(null); 225 // enforce serial re-run 226 runner.setReRunUsingTestFile(false); 227 // Set tests to run 228 runner.setTestsToRun(mTests); 229 runner.run(listener); 230 } 231 232 /** 233 * Returns a new collection of {@link TestDescription} where only one instance of each 234 * parameterized method is in the list. 235 */ createRerunSet(Collection<TestDescription> tests)236 private Collection<TestDescription> createRerunSet(Collection<TestDescription> tests) { 237 Map<String, TestDescription> uniqueMethods = new LinkedHashMap<>(); 238 for (TestDescription test : tests) { 239 uniqueMethods.put(test.getTestNameWithoutParams(), test); 240 } 241 return uniqueMethods.values(); 242 } 243 244 /** 245 * Util method to push file to a device. Exposed for unit testing. 246 * 247 * @return if file was pushed to the device successfully 248 * @throws DeviceNotAvailableException 249 */ pushFileToTestDevice(File file, String destinationPath)250 boolean pushFileToTestDevice(File file, String destinationPath) 251 throws DeviceNotAvailableException { 252 return mInstrumentationTest.getDevice().pushFile(file, destinationPath); 253 } 254 255 /** 256 * Delete file from the device if it exists 257 */ deleteTestFileFromDevice(String pathToFile)258 void deleteTestFileFromDevice(String pathToFile) throws DeviceNotAvailableException { 259 if (mInstrumentationTest.getDevice().doesFileExist(pathToFile)) { 260 mInstrumentationTest.getDevice() 261 .executeShellCommand(String.format("rm %s", pathToFile)); 262 CLog.d("Removed test file from device: %s", pathToFile); 263 } 264 } 265 266 /** @return the {@link InstrumentationTest} to use. Exposed for unit testing. */ 267 @VisibleForTesting createInstrumentationTest()268 InstrumentationTest createInstrumentationTest() { 269 return new InstrumentationTest(); 270 } 271 } 272