diff options
Diffstat (limited to 'tools/tradefed-host/src/com/android/afwtest')
32 files changed, 3710 insertions, 0 deletions
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/TestConfig.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/TestConfig.java new file mode 100644 index 0000000..1b1dbbc --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/TestConfig.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2016 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.afwtest.tradefed; + +import com.android.tradefed.log.LogUtil.CLog; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * A singleton class that reads properties from afw-test.props and provides interfaces + * to get test configurations. + */ +public class TestConfig { + /** + * Property key of factory reset timeout in minutes. + */ + public static final String KEY_FACTORY_RESET_TIMEOUT_MIN = "factory_reset_timeout_min"; + + /** + * Property key of Timeout size, used in the test configuration file. + */ + public static final String KEY_TIMEOUT_SIZE = "timeout_size"; + + /** + * Mapping from timeout size to its integer value. + */ + private static final Map<String, Integer> TIMEOUT_SIZE_MAPPING = + new HashMap<String, Integer>() {{ + put("S", 1); + put("M", 2); + put("L", 3); + put("XL", 5); + put("XXL", 8); + }}; + + /** + * Property key of test timeout in minute, used in the test configuration file. + */ + public static final String KEY_TEST_TIMEOUT_MIN = "test_timeout_min"; + + /** + * Singleton of this class. + */ + private static TestConfig sTestConfig; + + // Stores configurations loaded from a file. + private final Properties mProps; + + /** + * Creates a new object by loading configurations from file. + * + * @param configFile test configuration file + */ + private TestConfig(File configFile) throws IOException { + mProps = new Properties(); + mProps.load(new FileInputStream(configFile)); + } + + /** + * Inits a {@link TestConfig} from a file. + * + * @param configFile configuration file to read + */ + public static void init(File configFile) throws IOException { + sTestConfig = new TestConfig(configFile); + } + + /** + * Gets the singleton of this class. + * + * @return {@link TestConfig} singleton, null if it's not initialized. + */ + public static TestConfig getInstance() { + return sTestConfig; + } + + /** + * Gets the property of a specific key. + * + * @param key property key + * @return propery value of the given key + * If the key doesn't exist in the configuration file, null will be returned; + * If the value of the key is empty, empty string will be returned + */ + public String getProperty(String key) { + return mProps.getProperty(key); + } + + /** + * Gets the property of a specific key. + * + * @param key property key + * @param defaultValue default value if given key is not found + * @return property value of the given key; + * if given key not found, {@link #defaultValue} is returned + */ + public String getProperty(String key, String defaultValue) { + return mProps.getProperty(key, defaultValue); + } + + /** + * Gets factory reset timeout in minute. + * + * @return timeout in minute or -1 if either timeout is not specified or invalid + */ + public int getFactoryResetTimeoutMin() { + return getFactoryResetTimeoutMin(-1); + } + + /** + * Gets factory reset timeout in minute. + * + * @param defaultValue default value if test timeout not specified + * @return factory reset timeout in minute + */ + public int getFactoryResetTimeoutMin(int defaultValue) { + String value = mProps.getProperty(KEY_FACTORY_RESET_TIMEOUT_MIN); + if (value == null || value.isEmpty()) { + return defaultValue; + } + + return Integer.valueOf(value); + } + + /** + * Gets timeout size value from props file. + * + * <p>Possible size values are strings "S", "M", "L", "XL" or "XXL".</p> + * + * @return integer value of the timeout size. + */ + public int getTimeoutSize() { + // Default to M + String timeoutSize = getProperty(KEY_TIMEOUT_SIZE, "M"); + if (!TIMEOUT_SIZE_MAPPING.containsKey(timeoutSize)) { + CLog.w("Invalid timeout size, defaulting to M"); + timeoutSize = "M"; + } + + return TIMEOUT_SIZE_MAPPING.get(timeoutSize); + } + + /** + * Gets test timeout in minute. + * + * @return test timeout in minute or -1 if either timeout is not specified or invalid + */ + public int getTestTimeoutMin() { + return getTestTimeoutMin(-1); + } + + /** + * Gets test timeout in minute. + * + * @param defaultValue default value if test timeout not specified + * @return test timeout in minute + */ + public int getTestTimeoutMin(int defaultValue) { + String value = mProps.getProperty(KEY_TEST_TIMEOUT_MIN); + if (value == null || value.isEmpty()) { + return defaultValue; + } + + return Integer.valueOf(value); + } +} + diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestAdbRootOptionPreparer.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestAdbRootOptionPreparer.java new file mode 100644 index 0000000..74c4480 --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestAdbRootOptionPreparer.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2016 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.afwtest.tradefed.targetprep; + +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.config.Option; +import com.android.tradefed.config.OptionClass; +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.targetprep.ITargetCleaner; +import com.android.tradefed.targetprep.TargetSetupError; + +/** + * A {@link ITargetPreparer} that sets enable-root option of the testing device. + */ +@OptionClass(alias = "afw-test-adb-root-option") +public class AfwTestAdbRootOptionPreparer implements ITargetCleaner { + + @Option(name = "enable-root-option", + description = "Whether to enable adb root option on the testing device.") + private boolean mEnableAdbRootOption = true; + + /** + * Original adb root option. + */ + private Boolean mOriginalRootOption = null; + + /** + * {@inheritDoc} + */ + @Override + public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError, + DeviceNotAvailableException { + mOriginalRootOption = device.getOptions().isEnableAdbRoot(); + device.getOptions().setEnableAdbRoot(mEnableAdbRootOption); + } + + /** + * {@inheritDoc} + */ + @Override + public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e) + throws DeviceNotAvailableException { + if (mOriginalRootOption != null) { + device.getOptions().setEnableAdbRoot(mOriginalRootOption); + } + } +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestAppUninstaller.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestAppUninstaller.java new file mode 100644 index 0000000..599e993 --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestAppUninstaller.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2016 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.afwtest.tradefed.targetprep; + +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.config.Option; +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.targetprep.ITargetCleaner; +import com.android.tradefed.targetprep.TargetSetupError; + +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * A {@link ITargetPreparer} that helps uninstalling apps before or after the test. + */ +@OptionClass(alias="afw-test-app-uninstaller") +public class AfwTestAppUninstaller implements ITargetCleaner { + + @Option(name = "before-test", description = "packages to uninstall before test") + private List<String> mPackageNamesBeforeTest = new LinkedList<>(); + + @Option(name = "after-test", description = "packages to uninstall after test") + private List<String> mPackageNamesAfterTest = new LinkedList<>(); + + @Option(name = "reboot-before-uninstall", + description = "if reboot the device before uinstallation") + private boolean mRebootBeforeUninstall = false; + + /** + * {@inheritDoc} + */ + @Override + public void setUp(ITestDevice device, IBuildInfo buildInfo) + throws TargetSetupError, DeviceNotAvailableException { + String result = uninstall(device, mPackageNamesBeforeTest); + + if (result != null) { + throw new TargetSetupError(String.format("Failed to uninstall %s on %s", + result, device.getSerialNumber())); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e) + throws DeviceNotAvailableException { + + String result = uninstall(device, mPackageNamesAfterTest); + + if (result != null) { + throw new RuntimeException(String.format("Failed to uninstall %s on %s", + result, device.getSerialNumber())); + } + } + + /** + * Uninstalls list of packages. + * + * @param device the testing device + * @param packages packages to uninstall + * @return {@code null} if all given packages are uninstalled, or the name of the first package + * that failed to be uninstalled + */ + private String uninstall(ITestDevice device, List<String> packages) + throws DeviceNotAvailableException { + Set<String> installedPackages = device.getInstalledPackageNames(); + installedPackages.retainAll(packages); + + if (installedPackages.isEmpty()) { + return null; + } + + if (mRebootBeforeUninstall) { + device.reboot(); + } + + for (String pkgName : installedPackages) { + String result = device.uninstallPackage(pkgName); + if (result != null) { + return pkgName; + } + + CLog.i(String.format("Successfully uninstalled %s on %s", + pkgName, device.getSerialNumber())); + } + + return null; + } +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestEncryptDevice.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestEncryptDevice.java new file mode 100644 index 0000000..7de4ace --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestEncryptDevice.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2016 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.afwtest.tradefed.targetprep; + +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.config.Option; +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.targetprep.ITargetPreparer; +import com.android.tradefed.targetprep.TargetSetupError; +import com.android.tradefed.device.CollectingOutputReceiver; +import com.android.ddmlib.NullOutputReceiver; + + +import java.util.concurrent.TimeUnit; + +/** + * A {@link ITargetPreparer} that encrypts the testing device. + * + * <p>Requires a device where 'adb root' is possible, typically a userdebug build type.</p> + * + * <p>If the device is already encrypted, nothing will be done. </p> + */ +@OptionClass(alias = "afw-test-encrypt-device") +public class AfwTestEncryptDevice extends AfwTestTargetPreparer implements ITargetPreparer { + + @Option(name = "inplace", + description = "Whether to encrypt the device inplace or by wipe") + private Boolean mInplace = true; + + @Option(name = "timeout-secs", + description = "Timeout in seconds.") + private Long mTimeoutSecs = TimeUnit.MINUTES.toSeconds(5); + + @Option(name = "attempts", + description = "Number of attempts to encrypt the device") + private int mAttempts = 3; + + /** Adb command timeout, in milliseconds. */ + private static final int CMD_TIMEOUT = (int)TimeUnit.MINUTES.toMillis(2); + + /** The password for encrypting and decrypting the device. */ + private static final String ENCRYPTION_PASSWORD = "android"; + + /** Encrypting with inplace can take up to 2 hours. */ + private static final int ENCRYPTION_INPLACE_TIMEOUT_MIN = 2 * 60; + + /** Encrypting with wipe can take up to 20 minutes. */ + private static final long ENCRYPTION_WIPE_TIMEOUT_MIN = 20; + + /** + * {@inheritDoc} + */ + @Override + public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError, + DeviceNotAvailableException { + + for (int i = 0; i < mAttempts; ++i) { + CLog.i(String.format("Encrypting device %s: #%d", device.getSerialNumber(), i+1)); + + // Exit from loop if the device is encrypted successfully + if (encryptDevice(device)) { + break; + } + + // The device may become unavailable, wait for it. + device.waitForDeviceAvailable(TimeUnit.SECONDS.toMillis(10)); + } + + // Fail the target preparer if encryption failed + if (!device.isDeviceEncrypted()) { + throw new TargetSetupError( + "Failed to encrypt device " + device.getSerialNumber()); + } + + device.waitForDeviceAvailable(TimeUnit.SECONDS.toMillis(mTimeoutSecs) * getTimeoutSize()); + CLog.i(String.format("Device %s is encrypted successfully", device.getSerialNumber())); + + postDeviceEncryption(device); + } + + /** + * Encrypts the device. + * + * @param device test device + * @return {@code true} if device was encrypted successfully; {@code false} otherwise + */ + protected boolean encryptDevice(ITestDevice device) throws DeviceNotAvailableException { + if (!device.isEncryptionSupported()) { + throw new UnsupportedOperationException(String.format("Can't encrypt device %s: " + + "encryption not supported", device.getSerialNumber())); + } + + if (device.isDeviceEncrypted()) { + CLog.d("Device %s is already encrypted, skipping...", device.getSerialNumber()); + return true; + } + + String encryptMethod; + long timeout; + if (mInplace) { + encryptMethod = "inplace"; + timeout = ENCRYPTION_INPLACE_TIMEOUT_MIN; + } else { + encryptMethod = "wipe"; + timeout = ENCRYPTION_WIPE_TIMEOUT_MIN; + } + + CLog.i("Encrypting device %s via %s", device.getSerialNumber(), encryptMethod); + + // enable crypto takes one of the following formats: + // cryptfs enablecrypto <wipe|inplace> <passwd> + // cryptfs enablecrypto <wipe|inplace> default|password|pin|pattern [passwd] + // Try the first one first, if it outputs "500 <process_id> Usage: ...", try the second. + CollectingOutputReceiver receiver = new CollectingOutputReceiver(); + String command = String.format("vdc cryptfs enablecrypto %s \"%s\"", encryptMethod, + ENCRYPTION_PASSWORD); + device.executeShellCommand(command, receiver, timeout, TimeUnit.MINUTES, 1); + if (receiver.getOutput().split(":")[0].matches("500 \\d+ Usage")) { + command = String.format("vdc cryptfs enablecrypto %s default", encryptMethod); + device.executeShellCommand(command, + new NullOutputReceiver(), timeout, TimeUnit.MINUTES, 1); + } + + device.waitForDeviceNotAvailable(CMD_TIMEOUT); + device.waitForDeviceOnline(); + + return device.isDeviceEncrypted(); + } + + /** + * Actions after encryption. + * + * @param device test device + */ + protected void postDeviceEncryption(ITestDevice device) throws DeviceNotAvailableException { + // By default do nothing. + } +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestEnvDumper.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestEnvDumper.java new file mode 100644 index 0000000..226dbb7 --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestEnvDumper.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2016 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.afwtest.tradefed.targetprep; + +import com.google.common.base.Joiner; + +import com.android.afwtest.tradefed.utils.TimeUtil; +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.config.Option; +import com.android.tradefed.config.OptionClass; +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.device.PackageInfo; +import com.android.tradefed.log.LogUtil.CLog; +import com.android.tradefed.targetprep.ITargetCleaner; +import com.android.tradefed.targetprep.TargetSetupError; +import com.android.tradefed.util.StreamUtil; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A target preparer that dumps testing environment info to a text file. + * + * <p> + * The file is saved at: + * ${CTS_ROOT}/android-cts/repository/logs/{given-prefix}_EnvDump_{TimeStamp}.txt. + * Generally {given-prefix} should be the test apk file name. + * </p> + */ +@OptionClass(alias = "afw-test-env-dumper") +public class AfwTestEnvDumper extends AfwTestTargetPreparer implements ITargetCleaner { + + private static final String DUMP_FILE_NAME_SUFFIX = "EnvDump"; + private static final String GET_ANDROID_BUILD_FINGERPRINT_CMD = + "getprop ro.build.fingerprint"; + + @Option(name = "file-name-prefix", + description = "Dump file name prefix, suggested to be test apk file name", + mandatory = true) + private String mFileNamePrefix = null; + + /** + * {@inheritDoc} + */ + @Override + public void setUp(ITestDevice device, IBuildInfo buildInfo) + throws TargetSetupError, + DeviceNotAvailableException { + // Do nothing, dump after the test only + } + + /** + * {@inheritDoc} + */ + @Override + public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e) + throws DeviceNotAvailableException { + + try { + String str = getEnv(device); + + // Creates the dump file + File dumpFile = getDumpFile(buildInfo); + + OutputStream os = new FileOutputStream(dumpFile); + os.write(str.getBytes()); + StreamUtil.flushAndCloseStream(os); + + CLog.i(String.format("Dumped file: %s", dumpFile.getAbsolutePath())); + } catch (IOException exception) { + CLog.e("Failed to dump app versions to file", e); + } + } + + /** + * Gets environment configurations as string. + * + * @param device testing device + * @return environment configuration as string + */ + protected String getEnv(ITestDevice device) throws DeviceNotAvailableException { + String androidBuild = getAndroidBuildFingerprint(device); + String appVersions = getAppVersions(device); + + return Joiner.on(System.lineSeparator()).join(androidBuild, appVersions); + } + + /** + * Gets device's Android build fingerprint. + * + * @param device testing device + * @return device's Android build fingerprint. + * @throws DeviceNotAvailableException if connection to the device was lost during execution of + * the command. + */ + private String getAndroidBuildFingerprint(ITestDevice device) + throws DeviceNotAvailableException { + return device.executeShellCommand(GET_ANDROID_BUILD_FINGERPRINT_CMD); + } + + /** + * Gets a {@link File} to dump environment info. + * + * @param buildInfo reference to {@link IBuildInfo} + * @return {@link File} object of a unique file + */ + private File getDumpFile(IBuildInfo buildInfo) throws FileNotFoundException { + return new File(getCtsBuildHelper(buildInfo).getLogsDir(), + String.format("%s_%s_%s.txt", + mFileNamePrefix, + DUMP_FILE_NAME_SUFFIX, + TimeUtil.getResultTimestamp())); + } + + /** + * Gets versions of all {@link App} as a string. + * + * @param device reference to {@link ITestDevice} + * @return versions of all {@link App} as string + */ + private String getAppVersions(ITestDevice device) throws DeviceNotAvailableException { + Set<String> installPkgs = device.getInstalledPackageNames(); + + List<String> appVersions = new ArrayList<String>(); + + for (Map.Entry<String, String> pkg: getPackagesToDump().entrySet()) { + String pkgName = pkg.getKey(); + String appName = pkg.getValue(); + // Gets versions of install app only + if (installPkgs.contains(pkgName)) { + PackageInfo pkgInfo = device.getAppPackageInfo(pkgName); + String appVer = String.format("%s: Ver %s", appName, pkgInfo.getVersionName()); + + // Log it to tradefed console for debugging purpose + CLog.i(appVer); + + appVersions.add(appVer); + } + } + + return Joiner.on("\n").join(appVersions.iterator()); + } + + + /** + * Gets set of packages whose info should be dumped. + * + * @return a {@link Map} with key=full package name and value=app representation name + */ + protected Map<String, String> getPackagesToDump() { + Map<String, String> pkgs = new HashMap<String, String>(); + + pkgs.put("com.google.android.gms", "Google Mobile Service"); + pkgs.put("com.android.managedprovisioning", "Managed Provisioning"); + pkgs.put("com.afwsamples.testdpc", "TestDPC"); + + return pkgs; + } +} + diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestFactoryReset.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestFactoryReset.java new file mode 100644 index 0000000..5ee82fa --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestFactoryReset.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2016 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.afwtest.tradefed.targetprep; + +import com.android.afwtest.tradefed.utils.TimeUtil; +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.config.Option; +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.targetprep.ITargetPreparer; +import com.android.tradefed.targetprep.TargetSetupError; + +import java.util.concurrent.TimeUnit; + +/** + * A {@link ITargetPreparer} that factory resets the device by sending an intent + * to package com.android.afwtest.deviceadmin (AfwThDeviceAdmin.apk). + * + * <p>Requires a device where 'adb' is available after factory reset, typically + * a userdebug build type. + * </p> + */ +@OptionClass(alias = "afw-test-factory-reset") +public class AfwTestFactoryReset extends AfwTestTargetPreparer implements ITargetPreparer { + + @Option(name = "use-fastboot", + description = "Whether to use 'fastboot format' to do factory reset") + private boolean mUseFastboot = false; + + @Option(name = "timeout-secs", + description = "Factory reset timeout, in seconds.") + private Long mTimeoutSecs = TimeUnit.MINUTES.toSeconds(15); + + @Option(name = "attempts", + description = "Number of attempts to start factory reset") + private int mAttempts = 2; + + @Option (name = "wipe-protection-data", + description = "If factory reset protection data should be wiped") + private boolean mWipeProtectionData = false; + + // Command string to change system locale to en_US + private static final String CHANGE_LOCALE_TO_EN_US_CMD = + "am instrument -w -e locale en_US com.android.afwtest.systemutil/.ChangeLocale"; + + // After sending the factory reset intent out, time allowed for + // the device to start rebooting + private static final long REBOOT_WAIT_TIME_MS = TimeUnit.MINUTES.toMillis(2); + + /** + * {@inheritDoc} + */ + @Override + public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError, + DeviceNotAvailableException { + if (getUseFastboot(device)) { + CLog.i("Factory resetting device via fastboot"); + wipeDevice(device); + } else { + CLog.i("Factory resetting device via DeviceAdmin app"); + doFactoryReset(device); + } + + // Wait for the device to be available + long factoryResetTimeoutMs = getFactoryResetTimeout(); + CLog.i(String.format("Waiting for factory reset to finish: timeout=%d seconds", + factoryResetTimeoutMs / 1000)); + device.waitForDeviceAvailable(factoryResetTimeoutMs); + + // re-enable adb root + enableAdbRoot(device); + + CLog.i(String.format("Device %s is factory reset successfully", device.getSerialNumber())); + + postFactoryReset(device); + } + + /** + * Gets if configured to use fastboot to do factory reset. + * + * @return {@code true} if use fastboot, {@code false} otherwise + */ + protected boolean getUseFastboot(ITestDevice device) throws DeviceNotAvailableException { + return mUseFastboot; + } + + /** + * Executes actions after factory reset. + */ + protected void postFactoryReset(ITestDevice device) + throws DeviceNotAvailableException, TargetSetupError { + // Sync time with the host + TimeUtil.syncHostTime(device); + + CLog.i("Disabling auto-rotate"); + device.executeShellCommand("settings put system accelerometer_rotation 0"); + + // Change system locale to en_US + CLog.i("Changing system locale to en_US"); + String result = device.executeShellCommand(CHANGE_LOCALE_TO_EN_US_CMD); + if (result == null || !result.contains("result=SUCCESS")) { + CLog.e(result); + } else { + CLog.i("System locale changed to en_US"); + } + } + + /** + * Gets configured factory reset time out, in ms. + * + * @return factory reset timeout, in ms + */ + private long getFactoryResetTimeout() { + long result = 0; + // Get from afw-test.props file + if (getTestConfig() != null) { + result = TimeUnit.MINUTES.toMillis(getTestConfig().getFactoryResetTimeoutMin(0)); + } + // If not specified in afw-test.props, use mTimeoutSecs + if (result == 0) { + result = TimeUnit.SECONDS.toMillis(mTimeoutSecs); + } + + return result; + } + + /** + * Does factory reset with device admin. + * + * @param device test device + */ + private void doFactoryReset(ITestDevice device) + throws DeviceNotAvailableException, TargetSetupError { + int count = 0; + boolean factoryResetStarted; + do { + count++; + CLog.i(String.format("Factory resetting device %s, #%d", + device.getSerialNumber(), count)); + + CLog.i(device.executeShellCommand(getFactoryResetCommand())); + factoryResetStarted = device.waitForDeviceNotAvailable(REBOOT_WAIT_TIME_MS); + } while (count < mAttempts && !factoryResetStarted); + + // Wait for the device to reboot + if (!factoryResetStarted) { + throw new TargetSetupError( + "Failed to start factory reset on device " + device.getSerialNumber()); + } + } + + /** + * Gets factory reset command string. + */ + private String getFactoryResetCommand() { + String result = "am start -n com.android.afwtest.deviceadmin/.FactoryResetActivity"; + if (mWipeProtectionData) { + result += " --ez afwtest.wipe.protection.data true"; + } + return result; + } +} + diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestGoogleAccountRemover.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestGoogleAccountRemover.java new file mode 100644 index 0000000..aee6e9d --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestGoogleAccountRemover.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2016 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.afwtest.tradefed.targetprep; + +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.config.Option; +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.targetprep.ITargetCleaner; +import com.android.tradefed.targetprep.TargetSetupError; +import com.android.tradefed.util.RunUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.TimeUnit; + +/** + * Target preparer to remove Google accounts. + * + * <p>Requires a device where 'adb root' is possible, typically a userdebug build.</p> + * + * <p>Requires com.anroid.afwtest.systemutil to be pre-installed as privileged app.</p> + */ +@OptionClass(alias = "afw-test-google-account-remover") +public class AfwTestGoogleAccountRemover extends AfwTestTargetPreparer implements ITargetCleaner { + + /** + * Package name of the system util app. + */ + private static final String SYSTEM_UTIL_PKG_NAME = "com.android.afwtest.systemutil"; + + /** + * Action to remove Google account. + */ + private static final String REMOVE_GOOGLE_ACCOUNT_CLASS = ".RemoveGoogleAccount"; + + /** + * Waiting time for system util app. + */ + private static final long SYSTEM_UTIL_PKG_WAIT_TIME_MS = TimeUnit.MINUTES.toMillis(3); + + @Option(name = "google-account", + description = "Google accounts to remove, remove all Google accounts if not none given") + private Collection<String> mGoogleAccounts = new ArrayList<String>(); + + @Option(name = "remove-before-test", + description = "Remove given Google account or all Google accounts before test.") + private boolean mRemoveBeforeTest = true; + + @Option(name = "remove-after-test", + description = "Remove given Google account or all Google accounts after the test.") + private boolean mRemoveAfterTest = true; + + @Option(name = "attempts", + description = "Number of attempts") + private int mAttempts = 3; + + /** + * {@inheritDoc} + */ + @Override + public void setUp(ITestDevice device, IBuildInfo buildInfo) + throws TargetSetupError, DeviceNotAvailableException { + if (waitForAppPkgInfo(device, SYSTEM_UTIL_PKG_NAME, SYSTEM_UTIL_PKG_WAIT_TIME_MS) == null) { + throw new TargetSetupError(String.format("%s not installed successfully.", + SYSTEM_UTIL_PKG_NAME)); + } + + if (mRemoveBeforeTest) { + doRemoval(device); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e) + throws DeviceNotAvailableException { + if (waitForAppPkgInfo(device, SYSTEM_UTIL_PKG_NAME, SYSTEM_UTIL_PKG_WAIT_TIME_MS) == null) { + throw new RuntimeException(String.format("%s not installed successfully.", + SYSTEM_UTIL_PKG_NAME)); + } + + if (mRemoveAfterTest) { + try { + doRemoval(device); + } catch (TargetSetupError ex) { + throw new RuntimeException(ex); + } + } + } + + /** + * Help function to remove requested Google accounts. + * + * @param device test device + */ + private void doRemoval(ITestDevice device) + throws TargetSetupError, DeviceNotAvailableException { + if (mGoogleAccounts.isEmpty()) { + removeAllAccounts(device); + } else { + for (String account : mGoogleAccounts) { + removeAccount(device, account); + } + } + } + + /** + * Remove all Google accounts. + * + * @param device test device + */ + private void removeAllAccounts(ITestDevice device) + throws TargetSetupError, DeviceNotAvailableException { + String removeCmd = String.format("am instrument -w %s/%s", + SYSTEM_UTIL_PKG_NAME, + REMOVE_GOOGLE_ACCOUNT_CLASS); + + if (executeRemoveAccountCommand(device, removeCmd)) { + CLog.i(String.format("All Google accounts were removed from device %s.", + device.getSerialNumber())); + } else { + throw new TargetSetupError( + String.format("Failed to remove all Google accounts from device %s.", + device.getSerialNumber())); + } + } + + /** + * Remove a Google account. + * + * @param device test device + * @param account account to remove + */ + private void removeAccount(ITestDevice device, String account) + throws TargetSetupError, DeviceNotAvailableException { + String removeCmd = String.format("am instrument -w -e account \"%s\" %s/%s", + account, SYSTEM_UTIL_PKG_NAME, REMOVE_GOOGLE_ACCOUNT_CLASS); + if (executeRemoveAccountCommand(device, removeCmd)) { + CLog.i(String.format("Google account %s was removed from device %s.", + account, device.getSerialNumber())); + } else { + throw new TargetSetupError( + String.format("Failed to remove Google account %s from device %s.", + account, device.getSerialNumber())); + } + } + + /** + * Executes shell command to remove Google account. + * + * @param device test device + * @param cmd command to execute + * @return {@code true} if success, {@code false} otherwise + */ + private boolean executeRemoveAccountCommand(ITestDevice device, String cmd) + throws DeviceNotAvailableException { + CLog.v(cmd); + for (int i = 0; i < mAttempts; ++i) { + CLog.i(String.format("Removing account on device %s, #%d", + device.getSerialNumber(), i + 1)); + + String result = device.executeShellCommand(cmd); + // expected result format is (on success): + // + // INSTRUMENTATION_RESULT: result=SUCCESS + // INSTRUMENTATION_CODE: -1 + // + if (result != null && result.contains("result=SUCCESS")) { + return true; + } else { + CLog.e(String.format("Failed to remove account from device %s: %s.", + device.getSerialNumber(), result)); + // Sleep for 15 seconds before next attempt + RunUtil.getDefault().sleep(TimeUnit.SECONDS.toMillis(15)); + } + } + + // OK, failed after all attempts + return false; + } +} + diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestManagedProfileCreator.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestManagedProfileCreator.java new file mode 100644 index 0000000..1bf5a92 --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestManagedProfileCreator.java @@ -0,0 +1,89 @@ +package com.android.afwtest.tradefed.targetprep; + +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.config.Option; +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.targetprep.ITargetCleaner; +import com.android.tradefed.targetprep.ITargetPreparer; +import com.android.tradefed.targetprep.TargetSetupError; + +/** + * A {@link ITargetPreparer} that creates a managed profile in the testing device. + */ +@OptionClass(alias = "afw-test-create-managed-profile") +public class AfwTestManagedProfileCreator extends AfwTestTargetPreparer + implements ITargetCleaner { + + @Option(name = "remove-after-test", + description = "Remove work profile after test.") + private boolean mRemoveAfterTest = true; + + @Option(name = "profile-owner-component", + description = "Profile owner component to set; optional") + private String mProfileOwnerComponent = null; + + @Option(name = "profile-owner-apk", + description = "Profile owner apk path; optional") + private String mProfileOwnerApk = null; + + /** UserID for user in managed profile.*/ + private int mManagedProfileUserId; + + /** + * {@inheritDoc} + */ + @Override + public void setUp(ITestDevice device, IBuildInfo buildInfo) + throws TargetSetupError, DeviceNotAvailableException { + + String pmCommand = "pm create-user --profileOf 0 --managed " + + "TestProfile_" + System.currentTimeMillis(); + String pmCommandOutput = device.executeShellCommand(pmCommand); + + // Extract the id of the new user. + String[] pmCmdTokens = pmCommandOutput.split("\\s+"); + if (!pmCmdTokens[0].contains("Success:")) { + throw new TargetSetupError("Managed profile creation failed."); + } + mManagedProfileUserId = Integer.parseInt(pmCmdTokens[pmCmdTokens.length-1]); + + // Start managed profile user. + device.startUser(mManagedProfileUserId); + + CLog.i(String.format("New user created: %d", mManagedProfileUserId)); + + if (mProfileOwnerComponent != null && mProfileOwnerApk != null) { + device.installPackageForUser( + getApk(buildInfo, mProfileOwnerApk), true, mManagedProfileUserId); + String command = String.format("dpm set-profile-owner --user %d %s", + mManagedProfileUserId, mProfileOwnerComponent); + String commandOutput = device.executeShellCommand(command); + String[] cmdTokens = commandOutput.split("\\s+"); + if (!cmdTokens[0].contains("Success:")) { + throw new RuntimeException(String.format("Fail to setup %s as profile owner.", + mProfileOwnerComponent)); + } + + CLog.i(String.format("%s was set as profile owner of user %d", + mProfileOwnerComponent, mManagedProfileUserId)); + } + + // Reboot device to create the apps in managed profile. + device.reboot(); + device.waitForDeviceAvailable(); + } + + /** + * {@inheritDoc} + */ + @Override + public void tearDown(ITestDevice testDevice, IBuildInfo buildInfo, Throwable throwable) + throws DeviceNotAvailableException { + if (mRemoveAfterTest) { + testDevice.removeUser(mManagedProfileUserId); + } + } +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestPrivAppInstaller.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestPrivAppInstaller.java new file mode 100644 index 0000000..303b31d --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestPrivAppInstaller.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2016 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.afwtest.tradefed.targetprep; + +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.config.Option; +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.log.LogUtil.CLog; +import com.android.tradefed.targetprep.ITargetCleaner; +import com.android.tradefed.targetprep.TargetSetupError; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Target preparer to install an app as privileged app by pushing it to /system/priv-app. + * + * <p>Requires a device where 'adb root' is possible, typically a userdebug build.</p> + */ +public class AfwTestPrivAppInstaller extends AfwTestTargetPreparer implements ITargetCleaner { + + /** + * System folder for privileged apps and forlder for permissions. + */ + private static final String PRIV_APP_DIR = "/system/priv-app"; + private static final String PERMISSIONS_APP_DIR = "/etc/permissions"; + + @Option(name = "app-filename", + description = "app to install") + private Collection<String> mApps = new ArrayList<String>(); + + @Option(name = "permission-file", + description = "Permissions file required for the app") + private Collection<String> mPermissionsFiles = new ArrayList<String>(); + + @Option(name = "cleanup", + description = "true to delete the apk in teardown") + private boolean mCleanup = true; + + /** + * {@inheritDoc} + */ + @Override + public void setUp(ITestDevice device, IBuildInfo buildInfo) + throws TargetSetupError, DeviceNotAvailableException { + + if (mApps.isEmpty()) { + return; + } + + // Enable adb root and remount system partition + enableAdbRoot(device); + device.remountSystemWritable(); + + for (String app : mApps) { + String privApp = String.format("%s/%s", PRIV_APP_DIR, app); + if (device.pushFile(getApk(buildInfo, app), privApp)) { + CLog.i(String.format("Privileged app installed: %s", privApp)); + } else { + throw new TargetSetupError(String.format("Failed to install %s", privApp)); + } + } + + // Copy permission files (Required for API 25) + for (String file : mPermissionsFiles) { + String permissionFile = String.format("%s/%s", PERMISSIONS_APP_DIR, file); + if (device.pushFile(getApk(buildInfo, file), permissionFile)) { + CLog.i(String.format("Permissions file copied: %s", permissionFile)); + } else { + throw new TargetSetupError(String.format("Failed to copy %s", permissionFile)); + } + } + + // Reboot to activate the installed apps + device.reboot(); + device.waitForDeviceAvailable(); + // Enable adb root again + enableAdbRoot(device); + } + + /** + * {@inheritDoc} + */ + @Override + public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e) + throws DeviceNotAvailableException { + if (!mCleanup || mApps.isEmpty()) { + return; + } + + if (!device.enableAdbRoot()) { + throw new RuntimeException( + String.format("Failed to enable adb root: %s", device.getSerialNumber())); + } + + device.remountSystemWritable(); + + for (String app : mApps) { + device.executeShellCommand(String.format("rm %s/%s", PRIV_APP_DIR, app)); + } + } +} + diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestPullExternalFile.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestPullExternalFile.java new file mode 100644 index 0000000..7a068b2 --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestPullExternalFile.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2016 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.afwtest.tradefed.targetprep; + +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.config.Option; +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.targetprep.ITargetCleaner; +import com.android.tradefed.targetprep.TargetSetupError; +import com.android.tradefed.util.FileUtil; + +import java.io.File; + +/** + * A target cleaner to copy file from the external storage of the device to host after the + * test execution. + */ +@OptionClass(alias = "afw-test-pull-external-file") +public class AfwTestPullExternalFile extends AfwTestTargetPreparer implements ITargetCleaner { + + @Option(name = "remote-file", + description = "File to copy from external storage.", + mandatory = true) + private String mRemoteFile = null; + + @Option(name = "local-file", + description = "File path relative to android-cts/repository.", + mandatory = true) + private String mLocalFile = null; + + /** + * {@inheritDoc} + */ + @Override + public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError, + DeviceNotAvailableException { + // Do nothing. + } + + /** + * {@inheritDoc} + */ + @Override + public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e) + throws DeviceNotAvailableException { + + try { + File localFile = new File(getRepositoryDir(buildInfo), mLocalFile); + if (localFile.getParentFile().mkdirs()) { + localFile.createNewFile(); + } + FileUtil.copyFile(device.pullFileFromExternal(mRemoteFile), localFile); + } catch (DeviceNotAvailableException e1) { + throw e1; + } catch (Exception e2) { + // Log error but not failing the target preparer. + CLog.e(String.format("Failed to copy remote file.", e2)); + } + } +} + diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestSyncTime.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestSyncTime.java new file mode 100644 index 0000000..8f4fbcd --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestSyncTime.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2016 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.afwtest.tradefed.targetprep; + +import com.android.afwtest.tradefed.utils.TimeUtil; +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.config.OptionClass; +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.targetprep.ITargetPreparer; +import com.android.tradefed.targetprep.TargetSetupError; + +/** + * A {@link ITargetPreparer} that syncs device time with the running host. + * + * <p>Requires a device where 'adb root' is possible, typically a userdebug build</p> + */ +@OptionClass(alias = "afw-test-sync-time") +public class AfwTestSyncTime extends AfwTestTargetPreparer implements ITargetPreparer { + + /** + * {@inheritDoc} + */ + @Override + public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError, + DeviceNotAvailableException { + enableAdbRoot(device); + TimeUtil.syncHostTime(device); + } +} + diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestTargetPreparer.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestTargetPreparer.java new file mode 100644 index 0000000..684fe1f --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestTargetPreparer.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2016 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.afwtest.tradefed.targetprep; + +import com.android.afwtest.tradefed.TestConfig; +import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.device.PackageInfo; +import com.android.tradefed.log.LogUtil.CLog; +import com.android.tradefed.targetprep.TargetSetupError; +import com.android.tradefed.util.CommandResult; +import com.android.tradefed.util.CommandStatus; +import com.android.tradefed.util.RunUtil; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.concurrent.TimeUnit; + + +/** + * Abstract class to keep common functionality. + */ +public abstract class AfwTestTargetPreparer { + + private CompatibilityBuildHelper mBuildHelper; + private Long mTimeoutSecs = TimeUnit.MINUTES.toSeconds(5); + + /** + * Gets an instance of {@link CompatibilityBuildHelper}. + * + * @param buildInfo reference to {@link IBuildInfo} + * @return reference to {@link CompatibilityBuildHelper} instance + */ + protected CompatibilityBuildHelper getCtsBuildHelper(IBuildInfo buildInfo) { + if (mBuildHelper == null) { + mBuildHelper = new CompatibilityBuildHelper(buildInfo); + } + return mBuildHelper; + } + + /** + * Gets repository directory. + * + * @param buildInfo reference to {@link IBuildInfo} + * @return File path to repository folder. + */ + protected File getRepositoryDir(IBuildInfo buildInfo) throws FileNotFoundException { + return getCtsBuildHelper(buildInfo).getDir(); + } + + /** + * Gets apk file from testcases dir. + * + * @param buildInfo reference to {@link IBuildInfo} + * @param fileName apk file name + * @return {@link File} of the apk + */ + protected File getApk(IBuildInfo buildInfo, String fileName) throws TargetSetupError { + try { + return new File(getCtsBuildHelper(buildInfo).getTestsDir(), fileName); + } catch (FileNotFoundException e) { + throw new TargetSetupError(String.format("Couldn't get apk: %s", fileName), e); + } + } + + /** + * Enable adb root. + * + * @param device test device + * @throws TargetSetupError if failed to enable adb root + * @throws DeviceNotAvailableException if test device becomes unavailable + */ + protected void enableAdbRoot(ITestDevice device) + throws TargetSetupError, DeviceNotAvailableException { + // Enable adb root + if (!device.enableAdbRoot()) { + throw new TargetSetupError( + String.format("Failed to enable adb root: %s", device.getSerialNumber())); + } + } + + /** + * Waits and gets certain app package info. + * + * @param device test device + * @param pkgName app package name + * @param timeoutMs timeout in ms + * @return App package info of given package if found, null otherwise + * @throws DeviceNotAvailableException if test device becomes unavailable + */ + protected PackageInfo waitForAppPkgInfo(ITestDevice device, String pkgName, long timeoutMs) + throws DeviceNotAvailableException { + + long threadSleepTimeMs = TimeUnit.SECONDS.toMillis(5); + + PackageInfo pkgInfo = device.getAppPackageInfo(pkgName); + + while (pkgInfo == null && timeoutMs > 0) { + RunUtil.getDefault().sleep(threadSleepTimeMs); + timeoutMs -= threadSleepTimeMs; + + pkgInfo = device.getAppPackageInfo(pkgName); + } + + return pkgInfo; + } + + /** + * Gets {@link TestConfig} instance. + * + * @return {@link TestConfig} instance or null if not found; + */ + protected TestConfig getTestConfig() { + return TestConfig.getInstance(); + } + + /** + * Gets timeout size from config file. + * + * @return timeout size in integer + */ + protected int getTimeoutSize() { + TestConfig testConfig = TestConfig.getInstance(); + + // Target prepares should still work without test config file; + if (testConfig == null) { + return 1; + } + + return TestConfig.getInstance().getTimeoutSize(); + } + + /** + * Wipes device via fastboot. + * + * <p> + * Refers to com.android.tradefed.targetprep.DeviceWiper + * </p> + * + * @param device test device + */ + protected void wipeDevice(ITestDevice device) + throws DeviceNotAvailableException, TargetSetupError { + + CLog.i(String.format("Wiping device %s".format(device.getSerialNumber()))); + + device.rebootIntoBootloader(); + fastbootFormat(device, "cache"); + fastbootFormat(device, "userdata"); + device.executeFastbootCommand("reboot"); + } + + + /** + * Performs fastboot erase/format operation on certain partition + * + * @param device test device + * @param partition android partition, e.g. userdata, cache, system + */ + protected void fastbootFormat(ITestDevice device, String partition) + throws DeviceNotAvailableException, TargetSetupError { + + CLog.i(String.format("Attempting: fastboot format %s", partition)); + CommandResult r = device.executeLongFastbootCommand("format", partition); + if (r.getStatus() != CommandStatus.SUCCESS) { + throw new TargetSetupError( + String.format("format %s failed: %s", partition, r.getStderr())); + } + } + + /** + * "Soft" reboot the device by adb shell stop/start. + * + * @param device test device + */ + protected void softReboot(ITestDevice device) + throws DeviceNotAvailableException, TargetSetupError { + + CLog.i(String.format("Soft reboot device %s".format(device.getSerialNumber()))); + + device.executeShellCommand("stop"); + device.executeShellCommand("start"); + device.waitForDeviceAvailable(); + // enableAdbRoot() will check if device.isEnableRoot() + device.enableAdbRoot(); + } +} + diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestUserRemover.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestUserRemover.java new file mode 100644 index 0000000..c21c8e6 --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestUserRemover.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2016 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.afwtest.tradefed.targetprep; + +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.config.Option; +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.targetprep.ITargetCleaner; +import com.android.tradefed.targetprep.TargetSetupError; +import com.android.tradefed.util.RunUtil; + +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +/** + * A target preparer that can remove all non-primary users (user id not 0). + */ +@OptionClass(alias = "afw-test-user-remover") +public class AfwTestUserRemover implements ITargetCleaner { + + @Option(name = "remove-users-before-test", + description = "Remove all non-primary users before the test.") + private boolean mRemoveUsersBeforeTest = false; + + @Option(name = "remove-users-after-test", + description = "Remove all non-primary users after the test.") + private boolean mRemoveUsersAfterTest = true; + + /** + * {@inheritDoc} + */ + @Override + public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError, + DeviceNotAvailableException { + if (mRemoveUsersBeforeTest && !removeUsers(device)) { + throw new TargetSetupError( + "Failed to remove all non-primary users on device " + device.getSerialNumber()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e) + throws DeviceNotAvailableException { + if (mRemoveUsersAfterTest && !removeUsers(device)) { + throw new RuntimeException( + "Failed to remove all non-primary users on device " + device.getSerialNumber()); + } + } + + /** + * Remove all non-primary users. + * + * @param device the testing device + * @return {@code true} if all non-primary users are removed, {@code false} otherwise + */ + private boolean removeUsers(ITestDevice device) throws DeviceNotAvailableException { + // Ensure package manager is running + device.waitForDeviceAvailable(); + + ArrayList<Integer> users = device.listUsers(); + + if (users == null) { + return true; + } + + boolean success = true; + + for (Integer userId : users) { + if (userId == 0) { + // Can't remove user 0, so don't try. + continue; + } + + if (device.removeUser(userId)) { + CLog.i(String.format("Successfully removed user %d on %s", + userId, + device.getSerialNumber())); + } else { + CLog.e(String.format("Failed to remove user %d on %s", + userId, + device.getSerialNumber())); + success = false; + } + } + + RunUtil.getDefault().sleep(TimeUnit.SECONDS.toMillis(60)); + + // Make sure device is still available + device.waitForDeviceAvailable(); + + return success; + } +} + diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestWifiPreparer.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestWifiPreparer.java new file mode 100644 index 0000000..c071bd1 --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestWifiPreparer.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2016 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.afwtest.tradefed.targetprep; + +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.config.Option; +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.targetprep.ITargetCleaner; +import com.android.tradefed.targetprep.TargetSetupError; +import com.android.tradefed.util.RunUtil; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/** + * A {@link ITargetPreparer} that connects to wifi network with configurations read from file. + */ +@OptionClass(alias = "afw-test-wifi") +public class AfwTestWifiPreparer extends AfwTestTargetPreparer implements ITargetCleaner { + + /** + * Package name of the system util app. + */ + private static final String UTIL_PKG_NAME = "com.android.afwtest.util"; + + /** Shell command template for connect/disconnect wifi. */ + private static final String ADB_SHELL_CMD_TEMPL = + "am instrument -w %s com.android.afwtest.util/.Wifi"; + + @Option(name = "wifi-config-file", + description = "The file name containing wifi configuration.") + private String mConfigFileName = null; + + @Option(name = "wifi-ssid-key", + description = "The key of wifi ssid in the config file.") + private String mWifiSsidKey = null; + + @Option(name = "wifi-security-type-key", + description = "The key of wifi security type in the config file.") + private String mWifiSecurityType = null; + + @Option(name = "wifi-password-key", + description = "The key of wifi password in the config file.") + private String mWifiPasswordKey = null; + + @Option(name = "wifi-attempts", + description = "Number of attempts to connect to wifi network.") + private int mWifiAttempts = 5; + + @Option(name = "disconnect-wifi-after-test", + description = "Disconnect from wifi network after test completes.") + private boolean mDisconnectWifiAfterTest = true; + + /** Whether to connect with AfwThUtil app. */ + private boolean mWifiConnectedWithUtilApp = false; + + /** + * {@inheritDoc} + */ + @Override + public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError, + DeviceNotAvailableException { + + WifiConfig wifiConfig = getWifiConfig(buildInfo); + + // tradefed doesn't support WEP wifi; connect to it by AfwThUtil + if ("WEP".equals(wifiConfig.getSecurityType())) { + mWifiConnectedWithUtilApp = true; + connectWifiWithUtilApp(device, + wifiConfig.getSSID(), + wifiConfig.getSecurityType(), + wifiConfig.getSecurityKey()); + return; + } + + // Implement retry. + for (int i = 0; i < mWifiAttempts; ++i) { + try { + if (device.connectToWifiNetworkIfNeeded( + wifiConfig.getSSID(), wifiConfig.getSecurityKey())) { + return; + } + } catch (Exception e) { + CLog.e(e); + } + boolean lastAttempt = (i + 1) == mWifiAttempts; + if (!lastAttempt) { + RunUtil.getDefault().sleep(device.getOptions().getWifiRetryWaitTime()); + } + } + + throw new TargetSetupError(String.format("Failed to connect to wifi network %s on %s", + wifiConfig.getSSID(), device.getSerialNumber())); + } + + /** + * Loads configuration of Wi-Fi to be used. + * + * @param buildInfo data about the build under test. + * @return loaded Wi-Fi configuration. + */ + protected WifiConfig getWifiConfig(IBuildInfo buildInfo) throws TargetSetupError { + if (mConfigFileName == null) { + throw new TargetSetupError("wifi-config-file not specified"); + } + + if (mWifiSsidKey == null) { + throw new TargetSetupError("wifi-ssid-key not specified"); + } + + File configFile; + try { + configFile = new File(getCtsBuildHelper(buildInfo).getTestsDir(), mConfigFileName); + } catch (FileNotFoundException e) { + throw new TargetSetupError("Couldn't find tests directory"); + } + Properties props; + try { + props = readProperties(configFile); + } catch (IOException e) { + throw new TargetSetupError( + String.format("Failed to read prop file: %s", configFile.getAbsolutePath())); + } + + String wifiSsid = props.getProperty(mWifiSsidKey, ""); + String wifiSecurityType = props.getProperty(mWifiSecurityType, ""); + String wifiPassword = props.getProperty(mWifiPasswordKey, ""); + + if (wifiSsid.isEmpty()) { + throw new TargetSetupError(String.format( + "%s not specified in file %s", mWifiSsidKey, configFile.getAbsolutePath())); + } + + return new WifiConfig(wifiSsid, wifiSecurityType, wifiPassword); + } + + /** + * {@inheritDoc} + */ + @Override + public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e) + throws DeviceNotAvailableException { + + if (mDisconnectWifiAfterTest && device.isWifiEnabled()) { + if (mWifiConnectedWithUtilApp) { + disconnectWifiWithUtilApp(device); + } else { + if (device.disconnectFromWifi()) { + CLog.i("Successfully disconnected from wifi network on %s", + device.getSerialNumber()); + } else { + CLog.w("Failed to disconnect from wifi network on %s", + device.getSerialNumber()); + } + } + } + } + + /** + * Connects to wifi with AfwThUtil app. + * + * @param device test device + * @param wifiSSID WIFI SSID + * @param securityType WIFI security type + * @param password WIFI password + */ + private void connectWifiWithUtilApp(ITestDevice device, + String wifiSSID, + String securityType, + String password) + throws TargetSetupError, DeviceNotAvailableException { + String args = String.format("-e action connect -e ssid %s", wifiSSID); + if (securityType != null && !securityType.isEmpty()) { + args = String.format("%s -e security_type %s", args, securityType); + } + if (password != null && !password.isEmpty()) { + args = String.format("%s -e password %s", args, password); + } + + String cmdStr = String.format(ADB_SHELL_CMD_TEMPL, args); + CLog.i(String.format("Shell: %s", cmdStr)); + String result = device.executeShellCommand(cmdStr); + if (result != null && result.contains("result=SUCCESS")) { + CLog.i(String.format( + "Successfully connected to wifi network %s(security_type=%s) on %s", + wifiSSID, + securityType, + device.getSerialNumber())); + } else { + throw new TargetSetupError(String.format( + "Failed to connected to wifi network %s(security_type=%s) on %s: %s", + wifiSSID, + securityType, + device.getSerialNumber(), + result)); + } + } + + /** + * Disconnects from Wifi with AfwThUtil app. + * + * @param device test device + */ + private void disconnectWifiWithUtilApp(ITestDevice device) throws DeviceNotAvailableException { + String cmdStr = String.format(ADB_SHELL_CMD_TEMPL, "-e action disconnect"); + CLog.i(String.format("Shell: %s", cmdStr)); + String result = device.executeShellCommand(cmdStr); + if (result != null && result.contains("result=SUCCESS")) { + CLog.i(String.format("Successfully disconnected from wifi network on %s", + device.getSerialNumber())); + } else { + CLog.w(String.format("Failed to disconnect from wifi network on %s: %s", + device.getSerialNumber(), + result)); + } + } + + /** + * Help function to read {@link Properties} from a file. + * + * @param file {@link File} to read properties from + * @return {@link Properties} read from given file + * + * @throws IOException if read failed + */ + private static Properties readProperties(File file) throws IOException { + InputStream input = null; + + try { + input = new FileInputStream(file); + Properties props = new Properties(); + props.load(input); + return props; + } finally { + if (input != null) { + input.close(); + } + } + } + + /** + * Helper class to hold Wi-Fi configuration. + */ + protected final static class WifiConfig { + private final String mSSID; + private final String mSecurityType; + private final String mSecurityKey; + + /** + * Constructs new instance of Wi-Fi configuration holder. + * + * @param ssid SSID of Wi-Fi access point. + * @param securityType security type used by Wi-Fi access point. + * @param securityKey security key used by Wi-Fi access point. + */ + protected WifiConfig(String ssid, String securityType, String securityKey) { + this.mSSID = ssid; + this.mSecurityType = securityType; + this.mSecurityKey = securityKey; + } + + /** + * Returns SSID of Wi-Fi access point. + */ + protected String getSSID() { + return mSSID; + } + + /** + * Returns security type used by Wi-Fi access point. + */ + protected String getSecurityType() { + return mSecurityType; + } + + /** + * Returns security key used by Wi-Fi access point. + */ + protected String getSecurityKey() { + return mSecurityKey; + } + } +} + diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/AfwTestMetricsCollectorTargetPreparer.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/AfwTestMetricsCollectorTargetPreparer.java new file mode 100644 index 0000000..10f3adb --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/AfwTestMetricsCollectorTargetPreparer.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2017 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.afwtest.tradefed.targetprep.metrics; + +import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.BuildFingerprint; +import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.DeviceInfo; +import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics; +import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.InvocationMetric; + +import com.android.afwtest.tradefed.utils.ReflectionUtils; +import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.config.Option; +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.log.LogUtil.CLog; +import com.android.tradefed.targetprep.BuildError; +import com.android.tradefed.targetprep.ITargetCleaner; +import com.android.tradefed.targetprep.TargetSetupError; +import com.google.protobuf.nano.MessageNano; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Consumer; +import javax.annotation.concurrent.GuardedBy; + +/** {@link ITargetCleaner} to collect metrics during test invocation. */ +public class AfwTestMetricsCollectorTargetPreparer implements ITargetCleaner { + + @Option( + name = "collector-class-names", + description = "Class names of metrics collectors to use." + ) + private final List<String> mCollectorClassNames = new ArrayList<>(); + + // List of metrics collectors. + private final List<MetricsCollector> mCollectors = new ArrayList<>(); + + // Common list of collected metrics. + @GuardedBy("mMetricsLock") + private final List<InvocationMetric> mMetrics = new ArrayList<>(); + + // Lock to synchronize access to common list of collected metrics + // from multiple metrics collectors. + // Lock is used instead of synchronized collections to ensure fairness of the access. + private final ReadWriteLock mMetricsLock = new ReentrantReadWriteLock(true); + + /** {@inheritDoc} */ + @Override + public void setUp(ITestDevice device, IBuildInfo buildInfo) + throws TargetSetupError, BuildError, DeviceNotAvailableException { + if (!isCollectionEnabled()) { + return; + } + + try { + createCollectors(); + for (MetricsCollector collector : mCollectors) { + collector.init(device, buildInfo, this::addMetricToList); + } + } catch (MetricsCollectorException e) { + throw new TargetSetupError( + "Exception was thrown during metrics collectors initialization", e, null); + } + } + + /** {@inheritDoc} */ + @Override + public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e) + throws DeviceNotAvailableException { + if (!isCollectionEnabled()) { + return; + } + + for (MetricsCollector collector : mCollectors) { + collector.destroy(); + } + try { + writeMetrics(device, buildInfo); + } catch (MetricsCollectorException ex) { + CLog.e(ex); + } + } + + /** + * Checks if metrics collection is enabled for current invocation. + * + * @return {@code true} if metrics collection is enabled, {@code false} otherwise. + */ + protected boolean isCollectionEnabled() { + return true; + } + + /** + * {@link Consumer} passed to collectors to ensure synchronized access to the list of collected + * metrics. + * + * @param metric metric to add to the list of collected metrics. + */ + private void addMetricToList(InvocationMetric metric) { + mMetricsLock.writeLock().lock(); + mMetrics.add(metric); + mMetricsLock.writeLock().unlock(); + } + + /** + * Collects information about the device on which invocation happened. + * + * @param device current {@link ITestDevice}. + * @param buildInfo current {@link IBuildInfo}. + * @return information about the device on which invocation happened. + */ + private DeviceInfo getDeviceInfo(ITestDevice device, IBuildInfo buildInfo) { + final Map<String, String> buildAttrs = buildInfo.getBuildAttributes(); + BuildFingerprint buildFingerprint = new BuildFingerprint(); + buildFingerprint.productBrand = buildAttrs.get("cts:build_brand"); + buildFingerprint.productName = buildAttrs.get("cts:build_product"); + buildFingerprint.productDevice = buildAttrs.get("cts:build_device"); + buildFingerprint.platformVersion = buildAttrs.get("cts:build_version_release"); + buildFingerprint.release = buildAttrs.get("cts:build_id"); + buildFingerprint.versionIncremental = buildAttrs.get("cts:build_version_incremental"); + buildFingerprint.type = buildAttrs.get("cts:build_type"); + buildFingerprint.tags = buildAttrs.get("cts:build_tags"); + + DeviceInfo deviceInfo = new DeviceInfo(); + deviceInfo.buildFingerprint = buildFingerprint; + deviceInfo.apiLevel = Long.parseLong(buildAttrs.get("cts:build_version_sdk")); + try { + deviceInfo.gmsVersion = + device.getAppPackageInfo("com.google.android.gms").getVersionName(); + } catch (DeviceNotAvailableException e) { + CLog.e(e); + deviceInfo.gmsVersion = "N/A"; + } + + return deviceInfo; + } + + /** + * Writes collected metrics to file. + * + * @param buildInfo current {@link IBuildInfo}. + * @throws MetricsCollectorException if any error has occurred upon writing collected metrics. + */ + private void writeMetrics(ITestDevice device, IBuildInfo buildInfo) + throws MetricsCollectorException { + CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(buildInfo); + FileOutputStream os = null; + try { + InvocationMetrics metrics = new InvocationMetrics(); + metrics.id = UUID.randomUUID().toString(); + metrics.timestampMillis = System.currentTimeMillis(); + metrics.deviceInfo = getDeviceInfo(device, buildInfo); + + mMetricsLock.readLock().lock(); + metrics.metrics = mMetrics.toArray(InvocationMetric.emptyArray()); + mMetricsLock.readLock().unlock(); + + if (metrics.metrics.length == 0) { + return; + } + + File logsDir = buildHelper.getLogsDir(); + // createTempFile generates unique file name. + File resultFile = File.createTempFile("provisioning_metrics_", ".pbuf", logsDir); + os = new FileOutputStream(resultFile); + os.write(MessageNano.toByteArray(metrics)); + } catch (IOException e) { + throw new MetricsCollectorException("Cannot save collected metrics.", e); + } finally { + if (os != null) { + try { + os.close(); + } catch (IOException ignore) { + } + } + } + } + + /** + * Creates instances of metrics collectors whose class names are specified in + * 'collector-class-names' option. + * + * @throws MetricsCollectorException if an exception occurs during collectors instantiation. + */ + private void createCollectors() throws MetricsCollectorException { + for (String className : mCollectorClassNames) { + try { + mCollectors.add(ReflectionUtils.getInstanceOf(className, MetricsCollector.class)); + } catch (ReflectiveOperationException e) { + throw new MetricsCollectorException(e); + } + } + } +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/LogcatMetricsCollector.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/LogcatMetricsCollector.java new file mode 100644 index 0000000..79feb60 --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/LogcatMetricsCollector.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2017 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.afwtest.tradefed.targetprep.metrics; + +import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics; +import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.InvocationMetric; +import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.MetricValue; +import static com.android.internal.logging.nano.MetricsProto.MetricsEvent; + +import com.android.afwtest.tradefed.utils.StreamDiscarder; +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.device.ITestDevice; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Collector of the metrics from logcat events. */ +public class LogcatMetricsCollector implements MetricsCollector { + + // Logcat process from which events are read. + private Process mLogcatProcess; + + // Thread of the event parser. + private Thread mParserMain; + + // Thread to read and discard logcat's stderr to prevent pipe buffer overflow. + private Thread mLogcatErrDiscarder; + + // Map of logcat events to collect to AfW TH specific metric types. + private final Map<Integer, Integer> mEventsToCollect = new HashMap<>(); + + // Initializer of set of logcat event to collect as metrics. + { + mEventsToCollect.put( + MetricsEvent.PROVISIONING_TOTAL_TASK_TIME_MS, + InvocationMetrics.TOTAL_PROVISIONING_TIME_MS); + } + + /** {@inheritDoc} */ + @Override + public void init( + ITestDevice iTestDevice, + IBuildInfo iBuildInfo, + Consumer<InvocationMetric> metricsConsumer) + throws MetricsCollectorException { + + try { + startLogcat(iTestDevice.getSerialNumber()); + mParserMain = + new Thread( + new LogcatEventParserMain( + mLogcatProcess.getInputStream(), + (eventId, value) -> { + if (mEventsToCollect.containsKey(eventId)) { + final InvocationMetric metric = new InvocationMetric(); + final MetricValue metricValue = new MetricValue(); + + metricValue.setStringValue(value); + + metric.metricType = mEventsToCollect.get(eventId); + metric.setScalarValue(metricValue); + metricsConsumer.accept(metric); + } + })); + mParserMain.start(); + mLogcatErrDiscarder = new Thread(new StreamDiscarder(mLogcatProcess.getErrorStream())); + mLogcatErrDiscarder.start(); + } catch (Exception e) { + destroy(); + throw new MetricsCollectorException( + String.format("Failed to initialize %s.", getClass().getName()), e); + } + } + + /** {@inheritDoc} */ + @Override + public void destroy() { + if (mLogcatProcess != null) { + mLogcatProcess.destroy(); + try { + mLogcatProcess.waitFor(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + if (mParserMain != null) { + try { + mParserMain.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + if (mLogcatErrDiscarder != null) { + try { + mLogcatErrDiscarder.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + /** + * Starts new logcat process to capture events from device. + * + * @param serial serial number of a device. + * @throws MetricsCollectorException if could not create new logcat process. + */ + private void startLogcat(String serial) throws MetricsCollectorException { + ProcessBuilder pb = new ProcessBuilder("adb", "-s", serial, "logcat", "-b", "events"); + try { + mLogcatProcess = pb.start(); + } catch (IOException e) { + throw new MetricsCollectorException("Could not start logcat to capture events.", e); + } + } + + /** {@link Runnable} for a thread to parse logcat output. */ + private static final class LogcatEventParserMain implements Runnable { + + // Sample event string: + // 03-01 14:30:12.634 788 2845 I sysui_action: [325,4706] + private static final Pattern EVENT_PATTERN = + Pattern.compile("sysui_action: \\[(\\d+),(.+)\\]"); + + private final InputStream mStream; + private final BiConsumer<Integer, String> mEventConsumer; + + LogcatEventParserMain(InputStream stream, BiConsumer<Integer, String> eventConsumer) { + mStream = stream; + mEventConsumer = eventConsumer; + } + + @Override + public void run() { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(mStream))) { + String line; + while ((line = reader.readLine()) != null) { + Matcher matcher = EVENT_PATTERN.matcher(line); + if (!matcher.find()) { + continue; + } + mEventConsumer.accept(Integer.parseInt(matcher.group(1)), matcher.group(2)); + } + } catch (IOException e) { + throw new RuntimeException("Cannot read logcat output.", e); + } + } + } +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MemoryMetricsCollector.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MemoryMetricsCollector.java new file mode 100644 index 0000000..7b87341 --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MemoryMetricsCollector.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2017 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.afwtest.tradefed.targetprep.metrics; + +import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.InvocationMetric; + +import com.android.afwtest.tradefed.targetprep.metrics.memory.MemoryByUserGrouper; +import com.android.afwtest.tradefed.targetprep.metrics.memory.MemoryCollectorTask; +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.device.ITestDevice; + +import java.util.Arrays; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Collector of memory usage metrics. + */ +public class MemoryMetricsCollector implements MetricsCollector { + + /** + * Executor to run scheduled metric collection task. + */ + private ScheduledExecutorService mExecutor; + + /** + * {@inheritDoc} + */ + @Override + public void init(ITestDevice testDevice, IBuildInfo iBuildInfo, + Consumer<InvocationMetric> metricsConsumer) + throws MetricsCollectorException { + + AtomicLong tickCounter = new AtomicLong(0L); + + mExecutor = new ScheduledThreadPoolExecutor(1); + mExecutor.scheduleWithFixedDelay(getCollectorTask(testDevice, metricsConsumer, + tickCounter::getAndIncrement), 0, 500, TimeUnit.MILLISECONDS); + } + + /** + * {@inheritDoc} + */ + @Override + public void destroy() { + mExecutor.shutdownNow(); + } + + /** + * Gets {@link Runnable} to process a single iteration of memory information collection. + * + * @param testDevice current {@link ITestDevice}. + * @param metricsConsumer {@link Consumer} to emit collected metrics. + * @param tickCounter tick counter. + * @return {@link Runnable} to process a single iteration of memory information collection. + */ + protected Runnable getCollectorTask(ITestDevice testDevice, + Consumer<InvocationMetric> metricsConsumer, + Supplier<Long> tickCounter) { + return new MemoryCollectorTask(testDevice, metricsConsumer, tickCounter, + Arrays.asList(new MemoryByUserGrouper())); + } +}
\ No newline at end of file diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsCollector.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsCollector.java new file mode 100644 index 0000000..d28c837 --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsCollector.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2017 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.afwtest.tradefed.targetprep.metrics; + +import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.InvocationMetric; + +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.device.ITestDevice; + +import java.util.function.Consumer; + +/** Collects metrics from a device during test invocation. */ +public interface MetricsCollector { + + /** + * Initializes metrics collector. + * + * @param iTestDevice current {@link ITestDevice}. + * @param iBuildInfo current {@link IBuildInfo}. + * @param metricsConsumer {@link Consumer} to emit collected metrics. + * @throws MetricsCollectorException if an error occurred during initialization. + */ + void init( + ITestDevice iTestDevice, + IBuildInfo iBuildInfo, + Consumer<InvocationMetric> metricsConsumer) + throws MetricsCollectorException; + + /** Cleans up metrics collector. */ + void destroy(); +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsCollectorException.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsCollectorException.java new file mode 100644 index 0000000..5d4da4e --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsCollectorException.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2017 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.afwtest.tradefed.targetprep.metrics; + +/** + * Custom checked exception to wrap exceptions from com.android.afwtest.tradefed.targetprep.metrics + * package. + */ +public final class MetricsCollectorException extends Exception { + + public MetricsCollectorException() { + super(); + } + + public MetricsCollectorException(String message) { + super(message); + } + + public MetricsCollectorException(String message, Throwable cause) { + super(message, cause); + } + + public MetricsCollectorException(Throwable cause) { + super(cause); + } + + protected MetricsCollectorException( + String message, + Throwable cause, + boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsUtils.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsUtils.java new file mode 100644 index 0000000..e9e58ab --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsUtils.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2017 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.afwtest.tradefed.targetprep.metrics; + +import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.MetricValueMap; +import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.MetricValue; + +/** + * Utility class containing methods related to metrics collection classes. + */ +public final class MetricsUtils { + + /** Prevent class from instantiation. */ + private MetricsUtils() { + } + + /** + * Creates an instance of {@link MetricValueMap.ValuesEntry} from given key and value. + * + * @param key key of the entry to add. + * @param value value of the entry to add. + * @return created instance of {@link MetricValueMap.ValuesEntry}. + */ + public static MetricValueMap.ValuesEntry toMetricValueMapEntry(String key, long value) { + MetricValue metricValue = new MetricValue(); + metricValue.setUintValue(value); + + MetricValueMap.ValuesEntry valuesEntry = new MetricValueMap.ValuesEntry(); + valuesEntry.key = key; + valuesEntry.value = metricValue; + + return valuesEntry; + } +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryByProcessGrouper.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryByProcessGrouper.java new file mode 100644 index 0000000..6b5190e --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryByProcessGrouper.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2017 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.afwtest.tradefed.targetprep.metrics.memory; + +import static com.android.afwtest.tradefed.targetprep.metrics.MetricsUtils.toMetricValueMapEntry; +import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics; +import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.MetricValueMap; +import static com.android.afwtest.tradefed.utils.AdbShellUtils.ProcessInfo; + +import static java.util.stream.Collectors.toList; + +import java.util.Collection; +import java.util.Map; + +/** + * {@link MemoryInfoGrouper} grouping memory data into a map of process name to total PSS consumed + * by the process. + */ +public class MemoryByProcessGrouper implements MemoryInfoGrouper { + + private Collection<String> mProcesses; + + public MemoryByProcessGrouper(Collection<String> processes) { + mProcesses = processes; + } + + @Override + public Collection<MetricValueMap.ValuesEntry> group(Collection<ProcessInfo> ps, + Map<Long, Long> memoryInfo) { + return ps.stream() + .filter(processInfo -> mProcesses.contains(processInfo.getName())) + .map(processInfo -> + toMetricValueMapEntry(processInfo.getName(), memoryInfo.getOrDefault( + processInfo.getPID(), 0L))) + .collect(toList()); + } + + @Override + public int getMetricType() { + return InvocationMetrics.PSS_BY_PROCESS; + } +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryByUserGrouper.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryByUserGrouper.java new file mode 100644 index 0000000..a5a45dc --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryByUserGrouper.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2017 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.afwtest.tradefed.targetprep.metrics.memory; + +import static com.android.afwtest.tradefed.targetprep.metrics.MetricsUtils.toMetricValueMapEntry; +import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics; +import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.MetricValueMap; +import static com.android.afwtest.tradefed.utils.AdbShellUtils.ProcessInfo; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.summingLong; +import static java.util.stream.Collectors.toList; + +import java.util.Collection; +import java.util.Map; + +/** + * {@link MemoryInfoGrouper} grouping memory data into a map of user id to total PSS consumed by all + * the processes run by the user. + */ +public class MemoryByUserGrouper implements MemoryInfoGrouper { + + /** + * {@inheritDoc} + */ + @Override + public Collection<MetricValueMap.ValuesEntry> group(Collection<ProcessInfo> ps, + Map<Long, Long> memoryInfo) { + return ps.stream() + .collect( + groupingBy(ProcessInfo::getAndroidUserId, + summingLong(processInfo -> + memoryInfo.getOrDefault( + processInfo.getPID(), 0L)))) + .entrySet().stream() + .map(e -> toMetricValueMapEntry("user_" + e.getKey(), e.getValue())) + .collect(toList()); + } + + /** + * {@inheritDoc} + */ + @Override + public int getMetricType() { + return InvocationMetrics.TOTAL_PSS_BY_USER; + } +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryCollectorTask.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryCollectorTask.java new file mode 100644 index 0000000..5d10d26 --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryCollectorTask.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2017 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.afwtest.tradefed.targetprep.metrics.memory; + +import static com.android.afwtest.tradefed.targetprep.metrics.MetricsUtils.toMetricValueMapEntry; +import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.InvocationMetric; +import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.MetricValueMap; + +import com.android.afwtest.tradefed.utils.AdbShellUtils; +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * {@link Runnable} of the task that is executed in equal time intervals and collects memory + * usage information. + */ +public class MemoryCollectorTask implements Runnable { + + /** + * {@link MemoryInfoGrouper} collection to be used to emit metrics. + */ + private Collection<MemoryInfoGrouper> mMemoryInfoGroupers; + + /** + * Current {@link ITestDevice}. + */ + private ITestDevice mTestDevice; + + /** + * {@link Consumer} of collected information. + */ + private Consumer<InvocationMetric> mConsumer; + + /** + * Tick counter. + */ + private Supplier<Long> mTickCounter; + + /** + * Constructs a new instance of the task. + * + * @param testDevice current {@link ITestDevice}. + * @param consumer {@link Consumer} of collected information. + * @param tickCounter tick counter. + * @param groupers {@link MemoryInfoGrouper} collection to be used to emit metrics. + */ + public MemoryCollectorTask(ITestDevice testDevice, + Consumer<InvocationMetric> consumer, + Supplier<Long> tickCounter, + Collection<MemoryInfoGrouper> groupers) { + mTestDevice = testDevice; + mConsumer = consumer; + mTickCounter = tickCounter; + mMemoryInfoGroupers = groupers; + } + + /** + * {@inheritDoc} + */ + @Override + public void run() { + try { + Set<AdbShellUtils.ProcessInfo> ps = AdbShellUtils.getPS(mTestDevice); + Map<Long, Long> memoryInfo = AdbShellUtils.getMemoryInfo(mTestDevice); + MetricValueMap.ValuesEntry tickEntry = + toMetricValueMapEntry("tick", mTickCounter.get()); + + mMemoryInfoGroupers + .stream() + .map(grouper -> { + Collection<MetricValueMap.ValuesEntry> entries = + grouper.group(ps, memoryInfo); + entries.add(tickEntry); + + MetricValueMap.ValuesEntry[] entriesArray = + entries.toArray(new MetricValueMap.ValuesEntry[entries.size()]); + + MetricValueMap mapValue = new MetricValueMap(); + mapValue.values = entriesArray; + + InvocationMetric metric = new InvocationMetric(); + metric.metricType = grouper.getMetricType(); + metric.setMapValue(mapValue); + + return metric; + }).forEach(mConsumer); + + } catch (DeviceNotAvailableException ignore) {} + } +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryInfoGrouper.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryInfoGrouper.java new file mode 100644 index 0000000..d1cfa95 --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryInfoGrouper.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017 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.afwtest.tradefed.targetprep.metrics.memory; + +import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.MetricValueMap; +import static com.android.afwtest.tradefed.utils.AdbShellUtils.ProcessInfo; + +import java.util.Collection; +import java.util.Map; + +/** + * An object to group memory and process information collected from the device into + * performance metrics. + */ +public interface MemoryInfoGrouper { + + /** + * Groups memory and process information collected from the device into + * performance metrics. + * + * @param ps process information. + * @param memoryInfo memory information, map of process id to total PSS consumed be the process. + * @return {@link Collection} of performance metrics. + */ + Collection<MetricValueMap.ValuesEntry> group( + Collection<ProcessInfo> ps, + Map<Long, Long> memoryInfo); + + /** + * Gets the type of metrics emitted by the grouper. + * + * @return the type of metrics emitted by the grouper. + */ + int getMetricType(); +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/metrics.proto b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/metrics.proto new file mode 100644 index 0000000..bd3cde9 --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/metrics.proto @@ -0,0 +1,100 @@ +// Copyright (C) 2017 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. + +syntax = "proto2"; + +option java_package = "com.android.afwtest.tradefed.targetprep.metrics"; + +package com_android_afwtest_tradefed_targetprep_metrics; + +// Message representing Android Build Fingerprint. +// google/bullhead/bullhead:7.1.1/N4F26O/3582057:user/release-keys +message BuildFingerprint { + // google + required string product_brand = 1; + // bullhead + required string product_name = 2; + // bullhead + required string product_device = 3; + // 7.1.1 + required string platform_version = 4; + // N4F26O + required string release = 5; + // 3582057 + required string version_incremental = 6; + // user + required string type = 7; + // release-keys + optional string tags = 8; +} + +// Message describing device on which invocation happened. +message DeviceInfo { + // Fingerprint of the build on the device. + required BuildFingerprint build_fingerprint = 1; + + // Version of Google Play Services installed on the device. + required string gms_version = 2; + + // API level of the device. + required int64 api_level = 3; +} + +// Message describing collection of metrics collected during an invocation. +message InvocationMetrics { + + // Enum of supported metric types. + enum MetricType { + UNKNOWN = 1; + TOTAL_PROVISIONING_TIME_MS = 2; + TOTAL_PSS_BY_USER = 3; + PSS_BY_PROCESS = 4; + } + + // Value of metric. + message MetricValue { + oneof value { + uint64 uint_value = 1; + sint64 sint_value = 2; + string string_value = 3; + } + } + + // Wrapper object to bypass limitation of not being able to have map inside + // oneof. + message MetricValueMap { + map<string, MetricValue> values = 1; + } + + // Message describing a single metric collected during an invocation. + message InvocationMetric { + required MetricType metric_type = 1; + oneof metric_value { + MetricValue scalar_value = 2; + MetricValueMap map_value = 3; + } + } + + // Unique ID. + required string id = 1; + + // Timestamp of the invocation in milliseconds. + required int64 timestamp_millis = 2; + + // Information about the device on which invocation happened. + required DeviceInfo device_info = 3; + + // Collection of metrics. + repeated InvocationMetric metrics = 4; +}
\ No newline at end of file diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/AfwAndroidJUnitTest.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/AfwAndroidJUnitTest.java new file mode 100644 index 0000000..bfa3e29 --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/AfwAndroidJUnitTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2016 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.afwtest.tradefed.testtype; + +import com.android.afwtest.tradefed.TestConfig; +import com.android.ddmlib.Log; +import com.android.tradefed.testtype.AndroidJUnitTest; + +import java.util.concurrent.TimeUnit; + +/** + * Test class for Android for Work Test Harness JUnit tests. + */ +public class AfwAndroidJUnitTest extends AndroidJUnitTest { + + private static final String LOG_TAG = "afwtest.AfwAndroidJUnitTest"; + + // Default execution timeout for afw test packages + private static final long TEST_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(10); + + // 1 min in milliseconds + private static final long ONE_MIN_MS = TimeUnit.MINUTES.toMillis(1); + + /** + * {@inheritDoc} + */ + public AfwAndroidJUnitTest() { + super(); + + long testTimeout = + TimeUnit.MINUTES.toMillis(TestConfig.getInstance().getTestTimeoutMin(0)); + if (testTimeout == 0) { + testTimeout = TEST_TIMEOUT_MS + + TestConfig.getInstance().getTimeoutSize() * ONE_MIN_MS * 5; + } + Log.i(LOG_TAG, String.format("Instrumentation test timeout = %d seconds", + TimeUnit.MILLISECONDS.toSeconds(testTimeout))); + setTestTimeout((int) testTimeout); + } +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/AfwTest.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/AfwTest.java new file mode 100644 index 0000000..b19ea25 --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/AfwTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2016 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.afwtest.tradefed.testtype; + +import com.android.afwtest.tradefed.TestConfig; +import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; +import com.android.compatibility.common.tradefed.testtype.IModuleRepo; +import com.android.compatibility.common.tradefed.testtype.CompatibilityTest; +import com.android.compatibility.common.tradefed.testtype.ModuleRepo; +import com.android.tradefed.build.IBuildInfo; +import com.android.tradefed.config.Option; +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.util.FileUtil; + +import java.io.File; +import java.io.IOException; + +/** + * Customized {@link CompatibilityTest} for running afw-test tests. + * <p>Supports running all the tests contained in an afw-test plan, or individual test packages. + * </p> + */ +@OptionClass(alias = "compatibility") +public class AfwTest extends CompatibilityTest { + + private static final String CMD_DISABLE_PKG_VERIFICATION = + "settings put global package_verifier_enable 0"; + + @Option(name = "afw-test-config-file", + description = "Test harness config file to replace afw-test.props.") + private String mConfigFile = null; + + @Option(name = "screenshot", + description = "Take a screenshot on every test") + private boolean mScreenshot = false; + + /** + * A {@link CompatibilityBuildHelper} instance to access afw-test build info, such as the + * absolute paths of the test plan file and test case apks. + */ + protected CompatibilityBuildHelper mAfwTestBuild = null; + + /** + * Internal {@link ITestInvocationListener} wrapping one provided by framework with additional + * listeners specific for {@link AfwTest}. + */ + protected ITestInvocationListener mInternalListener = null; + + /** + * A {@link ITestDevice} instance representing a device under test. + */ + protected ITestDevice mDevice = null; + + /** + * {@inheritDoc} + */ + public AfwTest() { + this(1 /*totalShards*/, new ModuleRepo() /*moduleRepo*/, 0 /*shardIndex*/); + } + + /** + * {@inheritDoc} + */ + public AfwTest(int totalShards, IModuleRepo moduleRepo, Integer shardIndex) { + super(totalShards, moduleRepo, shardIndex); + } + + /** + * {@inheritDoc} + */ + @Override + public void setBuild(IBuildInfo build) { + super.setBuild(build); + + mAfwTestBuild = new CompatibilityBuildHelper(build); + } + + /** + * {@inheritDoc} + */ + @Override + public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { + mInternalListener = listener; + + beforeTest(); + super.run(mInternalListener); + afterTest(); + } + + /** + * Executes general preparation steps before running the test. + * + * @throws DeviceNotAvailableException if connection to the device is lost. + */ + protected void beforeTest() throws DeviceNotAvailableException { + CLog.i("Running afw-test"); + + mDevice = getDevice(); + if (mDevice == null) { + throw new IllegalArgumentException("Device not found"); + } + + try { + // Replace afw-test.props with a customized file if there is. + if (mConfigFile != null) { + FileUtil.copyFile(new File(mConfigFile), getTestConfigFile()); + } + + // Init TestConfig singleton. + TestConfig.init(getTestConfigFile()); + } catch (IOException e) { + throw new RuntimeException(e); + } + + // Disable package verification. + // This is necessary because CtsTest.run() will push TestDeviceSetup.apk + // to the testing device which will trigger package verification dialog. + mDevice.executeShellCommand(CMD_DISABLE_PKG_VERIFICATION); + + if (mScreenshot) { + mInternalListener = new ScreenshotListener(mInternalListener, mDevice); + } + } + + /** + * Executes general tear down steps after running the test. + * + * @throws DeviceNotAvailableException if connection to the device is lost. + */ + protected void afterTest() throws DeviceNotAvailableException { + // Disable package verification again; it might be enabled by some tests. + mDevice.executeShellCommand(CMD_DISABLE_PKG_VERIFICATION); + } + + /** + * Reads test configuration file, afw-test.props. + * + * @return {@link File} object keeping loaded content from afw-test.props + */ + protected File getTestConfigFile() throws IOException { + return new File(mAfwTestBuild.getTestsDir(), "afw-test.props"); + } +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/ScreenshotListener.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/ScreenshotListener.java new file mode 100644 index 0000000..e48dd65 --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/ScreenshotListener.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2016 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.afwtest.tradefed.testtype; + +import com.android.ddmlib.testrunner.TestIdentifier; +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.log.LogUtil.CLog; +import com.android.tradefed.result.FileInputStreamSource; +import com.android.tradefed.result.ITestInvocationListener; +import com.android.tradefed.result.InputStreamSource; +import com.android.tradefed.result.LogDataType; +import com.android.tradefed.result.ResultForwarder; + +import java.io.File; +import java.util.Map; + +/** + * Test invocation listener to capture device screenshot after the test + */ +public class ScreenshotListener extends ResultForwarder { + + private static final String UIAUTOMATOR = "/system/bin/uiautomator"; + private static final String UIAUTOMATOR_DUMP_COMMAND = "dump"; + private static final String UIDUMP_DEVICE_PATH = "/data/local/tmp/uidump.xml"; + + final private ITestDevice mDevice; + private boolean testFailed = false; + + public ScreenshotListener(ITestInvocationListener listener, ITestDevice device) { + super(listener); + mDevice = device; + } + + /** + * {@inheritDoc} + */ + @Override + public void testEnded(TestIdentifier test, Map<String, String> testMetrics) { + super.testEnded(test, testMetrics); + CLog.i("Capturing device screenshot after test %s", test.toString()); + try { + final InputStreamSource screenSource = mDevice.getScreenshot("PNG", + /* rescale */ !testFailed); + super.testLog(String.format("%s-screenshot", test.toString()), LogDataType.PNG, + screenSource); + screenSource.cancel(); + } catch (DeviceNotAvailableException e) { + CLog.e(e); + CLog.e("Device %s became unavailable while capturing screenshot", + mDevice.getSerialNumber()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void testFailed(TestIdentifier test, String trace) { + super.testFailed(test, trace); + + testFailed = true; + + CLog.i("Dumping device ui layout after test %s", test.toString()); + try { + mDevice.executeShellCommand( + String.format("%s %s %s", UIAUTOMATOR, UIAUTOMATOR_DUMP_COMMAND, + UIDUMP_DEVICE_PATH)); + final File dumpFile = mDevice.pullFile(UIDUMP_DEVICE_PATH); + final InputStreamSource dumpSource = new FileInputStreamSource(dumpFile, + /* deleteFileOnCancel */ true); + super.testLog(String.format("%s-uidump", test.toString()), LogDataType.XML, dumpSource); + dumpSource.cancel(); + } catch (DeviceNotAvailableException e) { + CLog.e(e); + CLog.e("Device %s became unavailable while capturing ui layout dump", + mDevice.getSerialNumber()); + } + } +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/AdbShellUtils.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/AdbShellUtils.java new file mode 100644 index 0000000..50e7706 --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/AdbShellUtils.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2017 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.afwtest.tradefed.utils; + +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility class containing methods related to executing various ADB Shell commands. + */ +public final class AdbShellUtils { + + /** Prevent class from instantiation. */ + private AdbShellUtils() { + } + + /** + * Gets process list currently running on the device grouped by user. + * + * @param testDevice device to get process list from. + * @return {@link Map} of user ids to a {@link Set} of process ids run by the user. + * @throws DeviceNotAvailableException if device became unavailable during the execution of + * the method. + */ + public static Set<ProcessInfo> getPS(ITestDevice testDevice) + throws DeviceNotAvailableException { + Set<ProcessInfo> result = new HashSet<>(); + String psOutput = testDevice.executeShellCommand("ps -e"); + for (String line : psOutput.split("\n")) { + if (line.contains("USER")) { + continue; + } + + String[] columns = line.split("\\s+"); + String user = columns[0]; + long pid = Long.parseLong(columns[1]); + long ppid = Long.parseLong(columns[2]); + String name = columns[8]; + + result.add(new ProcessInfo(user, pid, ppid, name)); + } + return result; + } + + /** + * Gets memory consumption information about processes currently running on the device. + * + * @param testDevice device to get memory information from. + * @return {@link Map} of process ids to total PSS of the process. + * @throws DeviceNotAvailableException if device became unavailable during the execution of + * the method. + */ + public static Map<Long, Long> getMemoryInfo(ITestDevice testDevice) + throws DeviceNotAvailableException { + Map<Long, Long> result = new HashMap<>(); + String miOutput = testDevice.executeShellCommand("su system dumpsys meminfo -c"); + for (String line : miOutput.split("\n")) { + String[] columns = line.split(","); + if (columns[0].equals("proc")) { + long pid = Long.parseLong(columns[3]); + long pss = Long.parseLong(columns[4]); + result.put(pid, pss); + } + } + return result; + } + + /** + * Class representing information about process retrieved from ps command. + */ + public static final class ProcessInfo { + + /** + * {@link Pattern} of Android user name that encodes user id and activity id. + */ + private static final Pattern APP_PROCESS_USER_PATTERN = Pattern.compile("u(\\d+)_a\\d+"); + + private final String mUser; + private final long mPID; + private final long mPPID; + private final String mName; + + private ProcessInfo(String user, long pid, long ppid, String name) { + this.mUser = user; + this.mPID = pid; + this.mPPID = ppid; + this.mName = name; + } + + /** + * Gets user name running the process. + * + * @return user name running the process. + */ + public String getUser() { + return mUser; + } + + /** + * Gets the id of the process. + * + * @return the id of the process. + */ + public long getPID() { + return mPID; + } + + /** + * Gets the id of the parent of the process. + * + * @return the id of the parent of the process. + */ + public long getPPID() { + return mPPID; + } + + /** + * Get the name of the process. + * + * @return the name of the process. + */ + public String getName() { + return mName; + } + + /** + * Gets the id if Android user running the process, or -1 of the process is run by + * the system. + * + * @return the id if Android user running the process, or -1 of the process is run by + * the system. + */ + public long getAndroidUserId() { + long userId = -1; + Matcher matcher = APP_PROCESS_USER_PATTERN.matcher(mName); + if (matcher.find()) { + userId = Long.parseLong(matcher.group(1)); + } + return userId; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ProcessInfo that = (ProcessInfo) o; + return mPID == that.mPID && + mPPID == that.mPPID && + Objects.equals(mUser, that.mUser) && + Objects.equals(mName, that.mName); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hash(mUser, mPID, mPPID, mName); + } + } +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/ReflectionUtils.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/ReflectionUtils.java new file mode 100644 index 0000000..e51aa1b --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/ReflectionUtils.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017 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.afwtest.tradefed.utils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +/** Set of utility methods for reflection operations. */ +public final class ReflectionUtils { + + /** Prevent class from instantiation. */ + private ReflectionUtils() {} + + /** + * Creates instance of specified class. + * + * @param className name of the class to instantiate. + * @param clazz {@link Class} extended by specified class to cast result to. + * @param args arguments of constructor to invoke. + * @return instance of specified class. + * @throws ClassNotFoundException if specified class cannot be found in classpath. + * @throws NoSuchMethodException if no constructor can be found suitable for specified + * constructor arguments. + * @throws IllegalAccessException if access to suitable constructor is forbidden. + * @throws InstantiationException if the class that declares the underlying constructor + * represents an abstract class. + * @throws InvocationTargetException if the underlying constructor throws an exception. + * @throws IllegalArgumentException if class specified by name is not assignable to the class + * specified by class instance. + */ + @SuppressWarnings("unchecked") + public static <T> T getInstanceOf(String className, Class<T> clazz, Object... args) + throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, + InstantiationException, InvocationTargetException { + + Class<?> resolvedClass = clazz.getClassLoader().loadClass(className); + if (!clazz.isAssignableFrom(resolvedClass)) { + throw new IllegalArgumentException( + String.format( + "An instance of %s cannot be assigned to %s.", + resolvedClass.getName(), clazz.getName())); + } + Constructor<T> constructor = + (Constructor<T>) resolvedClass.getConstructor(getClassesOf(args)); + return constructor.newInstance(args); + } + + /** + * Gets classes of specified objects. + * + * @param objects objects to get classes of. + * @return classes of specified objects. + */ + private static Class<?>[] getClassesOf(Object... objects) { + Class<?>[] result = new Class[objects.length]; + for (int i = 0; i < objects.length; i++) { + result[i] = objects[i].getClass(); + } + return result; + } +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/StreamDiscarder.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/StreamDiscarder.java new file mode 100644 index 0000000..dd4191c --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/StreamDiscarder.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2017 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.afwtest.tradefed.utils; + +import com.android.tradefed.log.LogUtil.CLog; + +import java.io.IOException; +import java.io.InputStream; + +/** + * {@link Runnable} that reads and discards data from specified stream. + */ +public final class StreamDiscarder implements Runnable { + + private static final int BATCH_SIZE = 1024; + + private final InputStream mStream; + private final byte[] buffer; + + /** + * Constructs {@link StreamDiscarder} instance. + * + * @param is {@link InputStream} to read and discard data from. + */ + public StreamDiscarder(InputStream is) { + mStream = is; + buffer = new byte[BATCH_SIZE]; + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("StatementWithEmptyBody") + public void run() { + try { + while (!Thread.interrupted() && mStream.read(buffer) > 0) {} + } catch (IOException e) { + CLog.e(e); + } + } +} diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/TimeUtil.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/TimeUtil.java new file mode 100644 index 0000000..d5730ce --- /dev/null +++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/TimeUtil.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2016 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.afwtest.tradefed.utils; + +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.log.LogUtil.CLog; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Time related utils. + */ +public class TimeUtil { + + /** + * Return the current timestamp in a compressed format, e.g.: 2015.11.25_11.42.12 + */ + public static String getResultTimestamp() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd_HH.mm.ss"); + return dateFormat.format(new Date()); + } + + /** + * Sync device time with host. + * + * @param device test device + */ + public static void syncHostTime(ITestDevice device) throws DeviceNotAvailableException { + CLog.i(String.format("Before time sync: %s", device.executeShellCommand("date -u"))); + + Date date = new Date(); + String dateString = null; + if (device.getApiLevel() < 23) { + // set date in epoch format + dateString = Long.toString(date.getTime() / 1000); //ms to s + } else { + // set date with POSIX like params + SimpleDateFormat sdf = new java.text.SimpleDateFormat("MMddHHmmyyyy.ss"); + sdf.setTimeZone(java.util.TimeZone.getTimeZone("UTC")); + dateString = sdf.format(date); + } + // best effort, no verification + String cmd = String.format("date -u %s", dateString); + CLog.i(String.format("Setting time: %s", cmd)); + CLog.i(device.executeShellCommand(cmd)); + + CLog.i(String.format("After time sync: %s", device.executeShellCommand("date -u"))); + } + +} |