| /* |
| * Copyright (C) 2015 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.tradefed.testtype; |
| |
| import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner; |
| import com.android.tradefed.config.ConfigurationDescriptor; |
| import com.android.tradefed.config.ConfigurationException; |
| import com.android.tradefed.config.Option; |
| import com.android.tradefed.config.OptionClass; |
| import com.android.tradefed.config.OptionCopier; |
| import com.android.tradefed.device.DeviceNotAvailableException; |
| import com.android.tradefed.device.ITestDevice; |
| import com.android.tradefed.invoker.TestInformation; |
| import com.android.tradefed.invoker.tracing.CloseableTraceScope; |
| import com.android.tradefed.log.LogUtil.CLog; |
| import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; |
| import com.android.tradefed.result.FailureDescription; |
| import com.android.tradefed.result.FileInputStreamSource; |
| import com.android.tradefed.result.ITestInvocationListener; |
| import com.android.tradefed.result.LogDataType; |
| import com.android.tradefed.result.proto.TestRecordProto.FailureStatus; |
| import com.android.tradefed.targetprep.BuildError; |
| import com.android.tradefed.targetprep.TargetSetupError; |
| import com.android.tradefed.targetprep.TestAppInstallSetup; |
| import com.android.tradefed.testtype.suite.params.InstantAppHandler; |
| import com.android.tradefed.util.ArrayUtil; |
| import com.android.tradefed.util.CommandResult; |
| import com.android.tradefed.util.FileUtil; |
| import com.android.tradefed.util.ListInstrumentationParser; |
| import com.android.tradefed.util.ResourceUtil; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| |
| import org.junit.runner.notification.RunListener; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.lang.reflect.InvocationTargetException; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.LinkedHashSet; |
| import java.util.Set; |
| import java.util.regex.Pattern; |
| import java.util.regex.PatternSyntaxException; |
| |
| /** |
| * A Test that runs an instrumentation test package on given device using the |
| * android.support.test.runner.AndroidJUnitRunner. |
| */ |
| @OptionClass(alias = "android-junit") |
| public class AndroidJUnitTest extends InstrumentationTest |
| implements IRuntimeHintProvider, |
| ITestFileFilterReceiver, |
| ITestFilterReceiver, |
| ITestAnnotationFilterReceiver, |
| IShardableTest { |
| |
| /** instrumentation test runner argument key used for including a class/test */ |
| private static final String INCLUDE_CLASS_INST_ARGS_KEY = "class"; |
| /** instrumentation test runner argument key used for excluding a class/test */ |
| private static final String EXCLUDE_CLASS_INST_ARGS_KEY = "notClass"; |
| /** instrumentation test runner argument key used for including a package */ |
| private static final String INCLUDE_PACKAGE_INST_ARGS_KEY = "package"; |
| /** instrumentation test runner argument key used for excluding a package */ |
| private static final String EXCLUDE_PACKAGE_INST_ARGS_KEY = "notPackage"; |
| /** instrumentation test runner argument key used for including a test regex */ |
| private static final String INCLUDE_REGEX_INST_ARGS_KEY = "tests_regex"; |
| /** instrumentation test runner argument key used for adding annotation filter */ |
| private static final String ANNOTATION_INST_ARGS_KEY = "annotation"; |
| /** instrumentation test runner argument key used for adding notAnnotation filter */ |
| private static final String NOT_ANNOTATION_INST_ARGS_KEY = "notAnnotation"; |
| /** instrumentation test runner argument used for adding testFile filter */ |
| private static final String TEST_FILE_INST_ARGS_KEY = "testFile"; |
| /** instrumentation test runner argument used for adding notTestFile filter */ |
| private static final String NOT_TEST_FILE_INST_ARGS_KEY = "notTestFile"; |
| /** instrumentation test runner argument used to specify the shardIndex of the test */ |
| private static final String SHARD_INDEX_INST_ARGS_KEY = "shardIndex"; |
| /** instrumentation test runner argument used to specify the total number of shards */ |
| private static final String NUM_SHARD_INST_ARGS_KEY = "numShards"; |
| /** |
| * instrumentation test runner argument used to enable the new {@link RunListener} order on |
| * device side. |
| */ |
| public static final String NEW_RUN_LISTENER_ORDER_KEY = "newRunListenerMode"; |
| |
| public static final String USE_TEST_STORAGE_SERVICE = "useTestStorageService"; |
| |
| /** Options from the collector side helper library. */ |
| public static final String INCLUDE_COLLECTOR_FILTER_KEY = "include-filter-group"; |
| |
| public static final String EXCLUDE_COLLECTOR_FILTER_KEY = "exclude-filter-group"; |
| |
| private static final String INCLUDE_FILE = "includes.txt"; |
| private static final String EXCLUDE_FILE = "excludes.txt"; |
| |
| @Option(name = "runtime-hint", |
| isTimeVal=true, |
| description="The hint about the test's runtime.") |
| private long mRuntimeHint = 60000;// 1 minute |
| |
| @Option( |
| name = "include-filter", |
| description = "The include filters of the test name to run.", |
| requiredForRerun = true) |
| private Set<String> mIncludeFilters = new LinkedHashSet<>(); |
| |
| @Option( |
| name = "exclude-filter", |
| description = "The exclude filters of the test name to run.", |
| requiredForRerun = true) |
| private Set<String> mExcludeFilters = new LinkedHashSet<>(); |
| |
| @Option( |
| name = "include-annotation", |
| description = "The annotation class name of the test name to run, can be repeated", |
| requiredForRerun = true) |
| private Set<String> mIncludeAnnotation = new HashSet<>(); |
| |
| @Option( |
| name = "exclude-annotation", |
| description = "The notAnnotation class name of the test name to run, can be repeated", |
| requiredForRerun = true) |
| private Set<String> mExcludeAnnotation = new HashSet<>(); |
| |
| @Option(name = "test-file-include-filter", |
| description="A file containing a list of line separated test classes and optionally" |
| + " methods to include") |
| private File mIncludeTestFile = null; |
| |
| @Option(name = "test-file-exclude-filter", |
| description="A file containing a list of line separated test classes and optionally" |
| + " methods to exclude") |
| private File mExcludeTestFile = null; |
| |
| @Option(name = "test-filter-dir", |
| description="The device directory path to which the test filtering files are pushed") |
| private String mTestFilterDir = "/data/local/tmp/ajur"; |
| |
| @Option( |
| name = "test-storage-dir", |
| description = "The device directory path where test storage read files.") |
| private String mTestStorageInternalDir = "/sdcard/googletest/test_runfiles"; |
| |
| @Option( |
| name = "use-test-storage", |
| description = |
| "If set to true, we will push filters to the test storage instead of disk.") |
| private boolean mUseTestStorage = true; |
| |
| @Option( |
| name = "ajur-max-shard", |
| description = |
| "The maximum number of shard we want to allow the AJUR test to shard into") |
| private Integer mMaxShard = 4; |
| |
| @Option( |
| name = "device-listeners", |
| description = |
| "Specify device side instrumentation listeners to be added for the run. " |
| + "Can be repeated. Note that while the ordering here is followed for " |
| + "now, future versions of AndroidJUnitRunner might not preserve the " |
| + "listener ordering." |
| ) |
| private List<String> mExtraDeviceListeners = new ArrayList<>(); |
| |
| @Option( |
| name = "use-new-run-listener-order", |
| description = "Enables the new RunListener Order for AJUR." |
| ) |
| // Default to true as it is harmless if not supported. |
| private boolean mNewRunListenerOrderMode = true; |
| |
| private String mDeviceIncludeFile = null; |
| private String mDeviceExcludeFile = null; |
| private int mTotalShards = 0; |
| private int mShardIndex = 0; |
| // Flag to avoid re-sharding a test that already was. |
| private boolean mIsSharded = false; |
| |
| public AndroidJUnitTest() { |
| super(); |
| setEnforceFormat(true); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public long getRuntimeHint() { |
| return mRuntimeHint; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void addIncludeFilter(String filter) { |
| mIncludeFilters.add(filter); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void addAllIncludeFilters(Set<String> filters) { |
| mIncludeFilters.addAll(filters); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void addExcludeFilter(String filter) { |
| mExcludeFilters.add(filter); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void addAllExcludeFilters(Set<String> filters) { |
| mExcludeFilters.addAll(filters); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void clearIncludeFilters() { |
| mIncludeFilters.clear(); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public Set<String> getIncludeFilters() { |
| return mIncludeFilters; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public Set<String> getExcludeFilters() { |
| return mExcludeFilters; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void clearExcludeFilters() { |
| mExcludeFilters.clear(); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void setIncludeTestFile(File testFile) { |
| mIncludeTestFile = testFile; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public File getIncludeTestFile() { |
| return mIncludeTestFile; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void setExcludeTestFile(File testFile) { |
| mExcludeTestFile = testFile; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public File getExcludeTestFile() { |
| return mExcludeTestFile; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void addIncludeAnnotation(String annotation) { |
| mIncludeAnnotation.add(annotation); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void addAllIncludeAnnotation(Set<String> annotations) { |
| mIncludeAnnotation.addAll(annotations); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void addExcludeAnnotation(String excludeAnnotation) { |
| mExcludeAnnotation.add(excludeAnnotation); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void addAllExcludeAnnotation(Set<String> excludeAnnotations) { |
| mExcludeAnnotation.addAll(excludeAnnotations); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public Set<String> getIncludeAnnotations() { |
| return mIncludeAnnotation; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public Set<String> getExcludeAnnotations() { |
| return mExcludeAnnotation; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void clearIncludeAnnotations() { |
| mIncludeAnnotation.clear(); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void clearExcludeAnnotations() { |
| mExcludeAnnotation.clear(); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void run(TestInformation testInfo, final ITestInvocationListener listener) |
| throws DeviceNotAvailableException { |
| if (getDevice() == null) { |
| throw new IllegalArgumentException("Device has not been set"); |
| } |
| if (mUseTestStorage) { |
| // Check if we are a parameterized module |
| List<String> params = |
| getConfiguration() |
| .getConfigurationDescription() |
| .getMetaData(ConfigurationDescriptor.ACTIVE_PARAMETER_KEY); |
| if (params != null && params.contains(InstantAppHandler.INSTANT_APP_ID)) { |
| mUseTestStorage = false; |
| CLog.d("Disable test storage on instant app module."); |
| } else if (isTestRunningOnSdkSandbox(testInfo)) { |
| // SDK sandboxes don't have access to the test ContentProvider. |
| mUseTestStorage = false; |
| CLog.d("Disable test storage for SDK sandbox instrumentation tests."); |
| } else { |
| mUseTestStorage = getDevice().checkApiLevelAgainstNextRelease(34); |
| if (!mUseTestStorage) { |
| CLog.d("Disabled test storage as it's not supported on that branch."); |
| } |
| } |
| } |
| |
| boolean pushedFile = false; |
| try (CloseableTraceScope filter = new CloseableTraceScope("push_filter_files")) { |
| // if mIncludeTestFile is set, perform filtering with this file |
| if (mIncludeTestFile != null && mIncludeTestFile.length() > 0) { |
| mDeviceIncludeFile = mTestFilterDir.replaceAll("/$", "") + "/" + INCLUDE_FILE; |
| pushTestFile(mIncludeTestFile, mDeviceIncludeFile, listener); |
| if (mUseTestStorage) { |
| pushTestFile( |
| mIncludeTestFile, |
| mTestStorageInternalDir + mDeviceIncludeFile, |
| listener); |
| } |
| pushedFile = true; |
| // If an explicit include file filter is provided, do not use the package |
| setTestPackageName(null); |
| } |
| |
| // if mExcludeTestFile is set, perform filtering with this file |
| if (mExcludeTestFile != null && mExcludeTestFile.length() > 0) { |
| mDeviceExcludeFile = mTestFilterDir.replaceAll("/$", "") + "/" + EXCLUDE_FILE; |
| pushTestFile(mExcludeTestFile, mDeviceExcludeFile, listener); |
| if (mUseTestStorage) { |
| pushTestFile( |
| mExcludeTestFile, |
| mTestStorageInternalDir + mDeviceExcludeFile, |
| listener); |
| } |
| pushedFile = true; |
| } |
| } |
| TestAppInstallSetup serviceInstaller = null; |
| if (mUseTestStorage) { |
| File testServices = null; |
| try (CloseableTraceScope serviceInstall = |
| new CloseableTraceScope("install_service_apk")) { |
| testServices = FileUtil.createTempFile("services", ".apk"); |
| boolean extracted = |
| ResourceUtil.extractResourceAsFile( |
| "/test-services-normalized.apk", testServices); |
| if (extracted) { |
| serviceInstaller = new TestAppInstallSetup(); |
| // Service apk needs force-queryable |
| serviceInstaller.setForceQueryable(true); |
| serviceInstaller.addTestFile(testServices); |
| if (testInfo != null |
| && testInfo.properties().containsKey(RUN_TESTS_AS_USER_KEY)) { |
| serviceInstaller.setUserId( |
| Integer.parseInt(testInfo.properties().get(RUN_TESTS_AS_USER_KEY))); |
| } |
| serviceInstaller.setUp(testInfo); |
| // Turn off battery optimization for androidx.test.services |
| CommandResult dumpsys = |
| getDevice() |
| .executeShellV2Command( |
| "dumpsys deviceidle whitelist +androidx.test.services"); |
| CLog.d("stdout: %s\nstderr: %s", dumpsys.getStdout(), dumpsys.getStderr()); |
| } else { |
| throw new IOException("Failed to extract test-services.apk"); |
| } |
| } catch (IOException | TargetSetupError | BuildError e) { |
| CLog.e(e); |
| mUseTestStorage = false; |
| } finally { |
| FileUtil.deleteFile(testServices); |
| } |
| } |
| if (mTotalShards > 0 && !isShardable() && mShardIndex != 0) { |
| // If not shardable, only first shard can run. |
| CLog.i("%s is not shardable.", getRunnerName()); |
| return; |
| } |
| super.run(testInfo, listener); |
| if (serviceInstaller != null) { |
| try (CloseableTraceScope serviceTeardown = |
| new CloseableTraceScope("service_teardown")) { |
| serviceInstaller.tearDown(testInfo, null); |
| } |
| } |
| if (pushedFile) { |
| // Remove the directory where the files where pushed |
| removeTestFilterDir(); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| protected void setRunnerArgs(IRemoteAndroidTestRunner runner) { |
| super.setRunnerArgs(runner); |
| |
| // if mIncludeTestFile is set, perform filtering with this file |
| if (mDeviceIncludeFile != null) { |
| runner.addInstrumentationArg(TEST_FILE_INST_ARGS_KEY, mDeviceIncludeFile); |
| } |
| |
| // if mExcludeTestFile is set, perform filtering with this file |
| if (mDeviceExcludeFile != null) { |
| runner.addInstrumentationArg(NOT_TEST_FILE_INST_ARGS_KEY, mDeviceExcludeFile); |
| } |
| |
| // Split filters into class, notClass, package and notPackage |
| List<String> classArg = new ArrayList<String>(); |
| List<String> notClassArg = new ArrayList<String>(); |
| List<String> packageArg = new ArrayList<String>(); |
| List<String> notPackageArg = new ArrayList<String>(); |
| List<String> regexArg = new ArrayList<String>(); |
| for (String test : mIncludeFilters) { |
| if (isRegex(test)) { |
| regexArg.add(test); |
| } else if (isClassOrMethod(test)) { |
| classArg.add(test); |
| } else { |
| packageArg.add(test); |
| } |
| } |
| for (String test : mExcludeFilters) { |
| // tests_regex doesn't support exclude-filter. Therefore, only check if the filter is |
| // for class/method or package. |
| if (isClassOrMethod(test)) { |
| notClassArg.add(test); |
| } else { |
| notPackageArg.add(test); |
| } |
| } |
| if (!classArg.isEmpty()) { |
| runner.addInstrumentationArg(INCLUDE_CLASS_INST_ARGS_KEY, |
| ArrayUtil.join(",", classArg)); |
| } |
| if (!notClassArg.isEmpty()) { |
| runner.addInstrumentationArg(EXCLUDE_CLASS_INST_ARGS_KEY, |
| ArrayUtil.join(",", notClassArg)); |
| } |
| if (!packageArg.isEmpty()) { |
| runner.addInstrumentationArg(INCLUDE_PACKAGE_INST_ARGS_KEY, |
| ArrayUtil.join(",", packageArg)); |
| } |
| if (!notPackageArg.isEmpty()) { |
| runner.addInstrumentationArg(EXCLUDE_PACKAGE_INST_ARGS_KEY, |
| ArrayUtil.join(",", notPackageArg)); |
| } |
| if (!regexArg.isEmpty()) { |
| String regexFilter; |
| if (regexArg.size() == 1) { |
| regexFilter = regexArg.get(0); |
| } else { |
| Collections.sort(regexArg); |
| regexFilter = "\"(" + ArrayUtil.join("|", regexArg) + ")\""; |
| } |
| runner.addInstrumentationArg(INCLUDE_REGEX_INST_ARGS_KEY, regexFilter); |
| } |
| if (!mIncludeAnnotation.isEmpty()) { |
| runner.addInstrumentationArg(ANNOTATION_INST_ARGS_KEY, |
| ArrayUtil.join(",", mIncludeAnnotation)); |
| } |
| if (!mExcludeAnnotation.isEmpty()) { |
| runner.addInstrumentationArg(NOT_ANNOTATION_INST_ARGS_KEY, |
| ArrayUtil.join(",", mExcludeAnnotation)); |
| } |
| if (mTotalShards > 0 && isShardable()) { |
| runner.addInstrumentationArg(SHARD_INDEX_INST_ARGS_KEY, Integer.toString(mShardIndex)); |
| runner.addInstrumentationArg(NUM_SHARD_INST_ARGS_KEY, Integer.toString(mTotalShards)); |
| } |
| if (mNewRunListenerOrderMode) { |
| runner.addInstrumentationArg( |
| NEW_RUN_LISTENER_ORDER_KEY, Boolean.toString(mNewRunListenerOrderMode)); |
| } |
| if (mUseTestStorage) { |
| runner.addInstrumentationArg( |
| USE_TEST_STORAGE_SERVICE, Boolean.toString(mUseTestStorage)); |
| } |
| // Add the listeners received from Options |
| addDeviceListeners(mExtraDeviceListeners); |
| } |
| |
| /** |
| * Push the testFile to the requested destination. This should only be called for a non-null |
| * testFile |
| * |
| * @param testFile file to be pushed from the host to the device. |
| * @param destination the path on the device to which testFile is pushed |
| * @param listener {@link ITestInvocationListener} to report failures. |
| */ |
| private void pushTestFile(File testFile, String destination, ITestInvocationListener listener) |
| throws DeviceNotAvailableException { |
| if (!testFile.canRead() || !testFile.isFile()) { |
| String message = String.format("Cannot read test file %s", testFile.getAbsolutePath()); |
| reportEarlyFailure(listener, message); |
| throw new IllegalArgumentException(message); |
| } |
| ITestDevice device = getDevice(); |
| try { |
| CLog.d("Attempting to push filters to %s", destination); |
| boolean filterDirExists = device.doesFileExist(mTestFilterDir); |
| if (!device.pushFile(testFile, destination, true)) { |
| String message = |
| String.format( |
| "Failed to push file %s to %s for %s in pushTestFile", |
| testFile.getAbsolutePath(), destination, device.getSerialNumber()); |
| reportEarlyFailure(listener, message); |
| throw new RuntimeException(message); |
| } |
| // in case the folder was created as 'root' we make is usable. |
| if (!filterDirExists) { |
| device.executeShellCommand( |
| String.format("chown -R shell:shell %s", mTestFilterDir)); |
| boolean filterExists = device.doesFileExist(destination); |
| if (!filterExists) { |
| CLog.e("Filter '%s' wasn't found on device after pushing.", destination); |
| } |
| } |
| } catch (DeviceNotAvailableException e) { |
| reportEarlyFailure(listener, e.getMessage()); |
| throw e; |
| } |
| try (FileInputStreamSource source = new FileInputStreamSource(testFile)) { |
| listener.testLog("filter-" + testFile.getName(), LogDataType.TEXT, source); |
| } |
| } |
| |
| private void removeTestFilterDir() throws DeviceNotAvailableException { |
| getDevice().deleteFile(mTestFilterDir); |
| } |
| |
| private void reportEarlyFailure(ITestInvocationListener listener, String errorMessage) { |
| listener.testRunStarted("AndroidJUnitTest_setupError", 0); |
| FailureDescription failure = FailureDescription.create(errorMessage); |
| failure.setFailureStatus(FailureStatus.INFRA_FAILURE); |
| listener.testRunFailed(failure); |
| listener.testRunEnded(0, new HashMap<String, Metric>()); |
| } |
| |
| /** |
| * Return if a string is the name of a Class or a Method. |
| */ |
| @VisibleForTesting |
| public boolean isClassOrMethod(String filter) { |
| if (filter.contains("#")) { |
| return true; |
| } |
| String[] parts = filter.split("\\."); |
| if (parts.length > 0) { |
| // FIXME Assume java package names starts with lowercase and class names start with |
| // uppercase. |
| // Return true iff the first character of the last word is uppercase |
| // com.android.foobar.Test |
| return Character.isUpperCase(parts[parts.length - 1].charAt(0)); |
| } |
| return false; |
| } |
| |
| /** Return if a string is a regex for filter. */ |
| @VisibleForTesting |
| public boolean isRegex(String filter) { |
| if (isParameterizedTest(filter)) { |
| return false; |
| } |
| |
| // If filter contains any special regex character, return true. |
| // Throw RuntimeException if the regex is invalid. |
| if (Pattern.matches(".*[\\?\\*\\^\\$\\(\\)\\[\\]\\{\\}\\|\\\\].*", filter)) { |
| try { |
| Pattern.compile(filter); |
| } catch (PatternSyntaxException e) { |
| CLog.e("Filter %s is not a valid regular expression string.", filter); |
| throw new RuntimeException(e); |
| } |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** Return if a string is a parameterized test. */ |
| @VisibleForTesting |
| public boolean isParameterizedTest(String filter) { |
| // If filter contains '#', '[', ']' and must ends with ']'. Only numbers, a-Z, -, _, |
| // [, ], (, ), and . are allowed between []. |
| if (Pattern.matches(".*#.*\\[[0-9a-zA-Z,\\-_.\\[\\]\\(\\)]*\\]$", filter)) { |
| CLog.i("Filter %s is a parameterized string.", filter); |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Helper to return if the runner is one that support sharding. |
| */ |
| private boolean isShardable() { |
| // Edge toward shardable if no explicit runner specified. The runner will be determined |
| // later and if not shardable only the first shard will run. |
| if (getRunnerName() == null) { |
| return true; |
| } |
| return ListInstrumentationParser.SHARDABLE_RUNNERS.contains(getRunnerName()); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public Collection<IRemoteTest> split(int shardCount) { |
| if (!isShardable()) { |
| return null; |
| } |
| if (mMaxShard != null) { |
| shardCount = Math.min(shardCount, mMaxShard); |
| } |
| if (!mIsSharded && shardCount > 1) { |
| mIsSharded = true; |
| Collection<IRemoteTest> shards = new ArrayList<>(shardCount); |
| for (int index = 0; index < shardCount; index++) { |
| shards.add(getTestShard(shardCount, index)); |
| } |
| return shards; |
| } |
| return null; |
| } |
| |
| private IRemoteTest getTestShard(int shardCount, int shardIndex) { |
| AndroidJUnitTest shard; |
| // ensure we handle runners that extend AndroidJUnitRunner |
| try { |
| shard = this.getClass().getDeclaredConstructor().newInstance(); |
| } catch (InstantiationException |
| | IllegalAccessException |
| | InvocationTargetException |
| | NoSuchMethodException e) { |
| throw new RuntimeException(e); |
| } |
| try { |
| OptionCopier.copyOptions(this, shard); |
| } catch (ConfigurationException e) { |
| CLog.e("Failed to copy instrumentation options: %s", e.getMessage()); |
| } |
| shard.mShardIndex = shardIndex; |
| shard.mTotalShards = shardCount; |
| shard.mIsSharded = true; |
| shard.setAbi(getAbi()); |
| // We approximate the runtime of each shard to be equal since we can't know. |
| shard.mRuntimeHint = mRuntimeHint / shardCount; |
| return shard; |
| } |
| } |