blob: 9064e0e4d741ffd646eb73bd4fd9bf2a14457865 [file] [log] [blame]
/*
* Copyright (C) 2018 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.game.qualification.testtype;
import com.android.game.qualification.ResultData;
import com.android.game.qualification.metric.GameQualificationMetricCollector;
import com.android.game.qualification.proto.ResultDataProto;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.game.qualification.ApkInfo;
import com.android.game.qualification.ApkListXmlParser;
import com.android.tradefed.config.Option;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.result.CollectingTestListener;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.IShardableTest;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import org.xml.sax.SAXException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.xml.parsers.ParserConfigurationException;
public class GameQualificationHostsideController implements IShardableTest, IDeviceTest {
// Package and class of the device side test.
private static final String PACKAGE = "com.android.game.qualification.device";
private static final String CLASS = PACKAGE + ".GameQualificationTest";
private static final String AJUR_RUNNER = "android.support.test.runner.AndroidJUnitRunner";
private static final long DEFAULT_TEST_TIMEOUT_MS = 10 * 60 * 1000L; //10min
private static final long DEFAULT_MAX_TIMEOUT_TO_OUTPUT_MS = 10 * 60 * 1000L; //10min
private ITestDevice mDevice;
private List<ApkInfo> mApks = null;
private File mApkInfoFile;
@Override
public void setDevice(ITestDevice device) {
mDevice = device;
}
@Override
public ITestDevice getDevice() {
return mDevice;
}
@Option(name = "apk-info",
description = "An XML file describing the list of APKs for qualifications.",
importance = Option.Importance.ALWAYS)
private String mApkInfoFileName;
@Option(name = "apk-dir",
description =
"Directory contains the APKs for qualifications. If --apk-info is not "
+ "specified and a file named 'apk-info.xml' exists in --apk-dir, that "
+ "file will be used as the apk-info.",
importance = Option.Importance.ALWAYS)
private String mApkDir;
private String getApkDir() {
if (mApkDir == null) {
mApkDir = System.getenv("ANDROID_PRODUCT_OUT") + "/data/app";
}
return mApkDir;
}
@Override
public Collection<IRemoteTest> split(int shardCountHint) {
initApkList();
List<IRemoteTest> shards = new ArrayList<>();
for(int i = 0; i < shardCountHint; i++) {
if (i >= mApks.size()) {
break;
}
List<ApkInfo> apkInfo = new ArrayList<>();
for(int j = i; j < mApks.size(); j += shardCountHint) {
apkInfo.add(mApks.get(j));
}
GameQualificationHostsideController shard = new GameQualificationHostsideController();
shard.mApks = apkInfo;
shard.mApkDir = getApkDir();
shards.add(shard);
}
return shards;
}
@Override
public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
Map<String, String> runMetrics = new HashMap<>();
initApkList();
getDevice().pushFile(mApkInfoFile, ApkInfo.APK_LIST_LOCATION);
for (ApkInfo apk : mApks) {
File apkFile = findApk(apk.getFileName());
getDevice().installPackage(apkFile, true);
GameQualificationMetricCollector.setAppLayerName(apk);
// Might seem counter-intuitive, but the easiest way to get per-package results is
// to put this call and the corresponding testRunEnd inside the for loop for now
listener.testRunStarted("gamequalification", mApks.size());
// TODO: Migrate to TF TestDescription when available
TestIdentifier identifier = new TestIdentifier(CLASS, "run[" + apk.getName() + "]");
Map<String, String> testMetrics = new HashMap<>();
// TODO: Populate metrics
listener.testStarted(identifier);
if (apkFile == null) {
listener.testFailed(
identifier,
String.format(
"Missing APK. Unable to find %s in %s.",
apk.getFileName(),
getApkDir()));
} else {
runDeviceTests(PACKAGE, CLASS, "run[" + apk.getName() + "]");
}
listener.testEnded(identifier, testMetrics);
ResultDataProto.Result resultData = retrieveResultData();
GameQualificationMetricCollector.setDeviceResultData(resultData);
listener.testRunEnded(0, runMetrics);
getDevice().uninstallPackage(apk.getPackageName());
}
}
private ResultDataProto.Result retrieveResultData() throws DeviceNotAvailableException {
File resultFile = getDevice().pullFileFromExternal(ResultData.RESULT_FILE_LOCATION);
if (resultFile != null) {
try (InputStream inputStream = new FileInputStream(resultFile)) {
ResultDataProto.Result data = ResultDataProto.Result.parseFrom(inputStream);
return data;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return null;
}
/** Find an apk in the apk-dir directory */
private File findApk(String filename) {
File file = new File(getApkDir(), filename);
if (file.exists()) {
return file;
}
// If a default sample app is named Sample.apk, it is outputted to
// $ANDROID_PRODUCT_OUT/data/app/Sample/Sample.apk.
file = new File(getApkDir(), Files.getNameWithoutExtension(filename) + "/" + filename);
if (file.exists()) {
return file;
}
return null;
}
private void initApkList() {
if (mApks != null) {
return;
}
// Find an apk info file. The priorities are:
// 1. Use the specified apk-info if available.
// 2. Use 'apk-info.xml' if there is one in the apk-dir directory.
// 3. Use the default apk-info.xml in res.
if (mApkInfoFileName != null) {
mApkInfoFile = new File(mApkInfoFileName);
} else {
mApkInfoFile = new File(getApkDir(), "apk-info.xml");
if (!mApkInfoFile.exists()) {
String resource = "/com/android/game/qualification/apk-info.xml";
try(InputStream inputStream = ApkInfo.class.getResourceAsStream(resource)) {
if (inputStream == null) {
throw new FileNotFoundException("Unable to find resource: " + resource);
}
mApkInfoFile = File.createTempFile("apk-info", ".xml");
try (OutputStream ostream = new FileOutputStream(mApkInfoFile)) {
ByteStreams.copy(inputStream, ostream);
}
mApkInfoFile.deleteOnExit();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
ApkListXmlParser parser = new ApkListXmlParser();
try {
mApks = parser.parse(mApkInfoFile);
} catch (IOException | ParserConfigurationException | SAXException e) {
throw new RuntimeException(e);
}
}
// TODO: Migrate to use BaseHostJUnit4Test when available.
/**
* Method to run an installed instrumentation package.
*
* @param pkgName the name of the package to run.
* @param testClassName the name of the test class to run.
* @param testMethodName the name of the method to run.
*/
private void runDeviceTests(String pkgName, String testClassName, String testMethodName)
throws DeviceNotAvailableException {
RemoteAndroidTestRunner testRunner =
new RemoteAndroidTestRunner(pkgName, AJUR_RUNNER, getDevice().getIDevice());
testRunner.setMethodName(testClassName, testMethodName);
testRunner.addInstrumentationArg(
"timeout_msec", Long.toString(DEFAULT_TEST_TIMEOUT_MS));
testRunner.setMaxTimeout(DEFAULT_MAX_TIMEOUT_TO_OUTPUT_MS, TimeUnit.MILLISECONDS);
CollectingTestListener listener = new CollectingTestListener();
getDevice().runInstrumentationTests(testRunner, listener);
}
}