Assam Boudjelthia 10a706df27 AndroidTestRunner: use QProcess instead of popen()
Using QProcess would make the test runner more robust when dealing
with quoted arguments since it won't be using the system shell and
handles quoting under the hood.

Fixes: QTBUG-105524
Fixes: QTQAINFRA-5703
Change-Id: Ib666ffea33302f1dfc7e8972bd7750f14065c4fc
Reviewed-by: Axel Spoerl <axel.spoerl@qt.io>
2023-11-30 17:26:26 +02:00

757 lines
26 KiB
C++

// Copyright (C) 2019 BogDan Vatra <bogdan@kde.org>
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QCoreApplication>
#include <QDir>
#include <QHash>
#include <QRegularExpression>
#include <QSystemSemaphore>
#include <QXmlStreamReader>
#include <algorithm>
#include <functional>
#include <atomic>
#include <csignal>
#include <QtCore/QDeadlineTimer>
#include <QtCore/QThread>
#include <QtCore/QProcessEnvironment>
#include <QtCore/QProcess>
using namespace Qt::StringLiterals;
static bool checkJunit(const QByteArray &data) {
QXmlStreamReader reader{data};
while (!reader.atEnd()) {
reader.readNext();
if (!reader.isStartElement())
continue;
if (reader.name() == QStringLiteral("error"))
return false;
const QString type = reader.attributes().value(QStringLiteral("type")).toString();
if (reader.name() == QStringLiteral("failure")) {
if (type == QStringLiteral("fail") || type == QStringLiteral("xpass"))
return false;
}
}
// Fail if there's an error after reading through all the xml output
return !reader.hasError();
}
static bool checkTxt(const QByteArray &data) {
if (data.indexOf("\nFAIL! : "_L1) >= 0)
return false;
if (data.indexOf("\nXPASS : "_L1) >= 0)
return false;
// Look for "********* Finished testing of tst_QTestName *********"
static const QRegularExpression testTail("\\*+ +Finished testing of .+ +\\*+"_L1);
return testTail.match(QLatin1StringView(data)).hasMatch();
}
static bool checkCsv(const QByteArray &data) {
// The csv format is only suitable for benchmarks,
// so this is not much useful to determine test failure/success.
// FIXME: warn the user early on about this.
Q_UNUSED(data);
return true;
}
static bool checkXml(const QByteArray &data) {
QXmlStreamReader reader{data};
while (!reader.atEnd()) {
reader.readNext();
const QString type = reader.attributes().value(QStringLiteral("type")).toString();
const bool isIncident = (reader.name() == QStringLiteral("Incident"));
if (reader.isStartElement() && isIncident) {
if (type == QStringLiteral("fail") || type == QStringLiteral("xpass"))
return false;
}
}
// Fail if there's an error after reading through all the xml output
return !reader.hasError();
}
static bool checkLightxml(const QByteArray &data) {
// lightxml intentionally skips the root element, which technically makes it
// not valid XML. We'll add that ourselves for the purpose of validation.
QByteArray newData = data;
newData.prepend("<root>");
newData.append("</root>");
return checkXml(newData);
}
static bool checkTeamcity(const QByteArray &data) {
if (data.indexOf("' message='Failure! |[Loc: ") >= 0)
return false;
const QList<QByteArray> lines = data.trimmed().split('\n');
if (lines.isEmpty())
return false;
return lines.last().startsWith("##teamcity[testSuiteFinished "_L1);
}
static bool checkTap(const QByteArray &data) {
// This will still report blacklisted fails because QTest with TAP
// is not putting any data about that.
if (data.indexOf("\nnot ok ") >= 0)
return false;
static const QRegularExpression testTail("ok [0-9]* - cleanupTestCase\\(\\)"_L1);
return testTail.match(QLatin1StringView(data)).hasMatch();
}
struct Options
{
bool helpRequested = false;
bool verbose = false;
bool skipAddInstallRoot = false;
int timeoutSecs = 600; // 10 minutes
QString buildPath;
QString adbCommand{QStringLiteral("adb")};
QString makeCommand;
QString package;
QString activity;
QStringList testArgsList;
QHash<QString, QString> outFiles;
QStringList amStarttestArgs;
QString apkPath;
QString ndkStackPath;
int sdkVersion = -1;
int pid = -1;
bool showLogcatOutput = false;
const QHash<QString, std::function<bool(const QByteArray &)>> checkFiles = {
{QStringLiteral("txt"), checkTxt},
{QStringLiteral("csv"), checkCsv},
{QStringLiteral("xml"), checkXml},
{QStringLiteral("lightxml"), checkLightxml},
{QStringLiteral("xunitxml"), checkJunit},
{QStringLiteral("junitxml"), checkJunit},
{QStringLiteral("teamcity"), checkTeamcity},
{QStringLiteral("tap"), checkTap},
};
};
static Options g_options;
static bool execCommand(const QString &program, const QStringList &args,
QByteArray *output = nullptr, bool verbose = false)
{
const auto command = program + " "_L1 + args.join(u' ');
if (verbose && g_options.verbose)
fprintf(stdout, "Execute %s.\n", command.toUtf8().constData());
QProcess process;
process.start(program, args);
if (!process.waitForStarted()) {
fprintf(stderr, "Cannot execute command %s.\n", qPrintable(command));
return false;
}
// If the command is not adb, for example, make or ninja, it can take more that
// QProcess::waitForFinished() 30 secs, so for that use a higher timeout.
const int FinishTimeout = program.endsWith("adb"_L1) ? 30000 : g_options.timeoutSecs * 1000;
if (!process.waitForFinished(FinishTimeout)) {
fprintf(stderr, "Execution of command %s timed out.\n", qPrintable(command));
return false;
}
const auto stdOut = process.readAllStandardOutput();
if (output)
output->append(stdOut);
if (verbose && g_options.verbose)
fprintf(stdout, "%s", stdOut.constData());
return process.exitCode() == 0;
}
static bool execAdbCommand(const QStringList &args, QByteArray *output = nullptr,
bool verbose = true)
{
return execCommand(g_options.adbCommand, args, output, verbose);
}
static bool execCommand(const QString &command, QByteArray *output = nullptr, bool verbose = true)
{
auto args = command.split(u' ');
const auto program = args.first();
args.removeOne(program);
return execCommand(program, args, output, verbose);
}
static bool parseOptions()
{
QStringList arguments = QCoreApplication::arguments();
int i = 1;
for (; i < arguments.size(); ++i) {
const QString &argument = arguments.at(i);
if (argument.compare(QStringLiteral("--adb"), Qt::CaseInsensitive) == 0) {
if (i + 1 == arguments.size())
g_options.helpRequested = true;
else
g_options.adbCommand = arguments.at(++i);
} else if (argument.compare(QStringLiteral("--path"), Qt::CaseInsensitive) == 0) {
if (i + 1 == arguments.size())
g_options.helpRequested = true;
else
g_options.buildPath = arguments.at(++i);
} else if (argument.compare(QStringLiteral("--make"), Qt::CaseInsensitive) == 0) {
if (i + 1 == arguments.size())
g_options.helpRequested = true;
else
g_options.makeCommand = arguments.at(++i);
} else if (argument.compare(QStringLiteral("--apk"), Qt::CaseInsensitive) == 0) {
if (i + 1 == arguments.size())
g_options.helpRequested = true;
else
g_options.apkPath = arguments.at(++i);
} else if (argument.compare(QStringLiteral("--activity"), Qt::CaseInsensitive) == 0) {
if (i + 1 == arguments.size())
g_options.helpRequested = true;
else
g_options.activity = arguments.at(++i);
} else if (argument.compare(QStringLiteral("--skip-install-root"), Qt::CaseInsensitive) == 0) {
g_options.skipAddInstallRoot = true;
} else if (argument.compare(QStringLiteral("--show-logcat"), Qt::CaseInsensitive) == 0) {
g_options.showLogcatOutput = true;
} else if (argument.compare("--ndk-stack"_L1, Qt::CaseInsensitive) == 0) {
if (i + 1 == arguments.size())
g_options.helpRequested = true;
else
g_options.ndkStackPath = arguments.at(++i);
} else if (argument.compare(QStringLiteral("--timeout"), Qt::CaseInsensitive) == 0) {
if (i + 1 == arguments.size())
g_options.helpRequested = true;
else
g_options.timeoutSecs = arguments.at(++i).toInt();
} else if (argument.compare(QStringLiteral("--help"), Qt::CaseInsensitive) == 0) {
g_options.helpRequested = true;
} else if (argument.compare(QStringLiteral("--verbose"), Qt::CaseInsensitive) == 0) {
g_options.verbose = true;
} else if (argument.compare(QStringLiteral("--"), Qt::CaseInsensitive) == 0) {
++i;
break;
} else {
g_options.testArgsList << arguments.at(i);
}
}
for (;i < arguments.size(); ++i)
g_options.testArgsList << arguments.at(i);
if (g_options.helpRequested || g_options.buildPath.isEmpty() || g_options.apkPath.isEmpty())
return false;
QString serial = qEnvironmentVariable("ANDROID_DEVICE_SERIAL");
if (!serial.isEmpty())
g_options.adbCommand += QStringLiteral(" -s %1").arg(serial);
if (g_options.ndkStackPath.isEmpty()) {
const QString ndkPath = qEnvironmentVariable("ANDROID_NDK_ROOT");
const QString ndkStackPath = ndkPath + QDir::separator() + "ndk-stack"_L1;
if (QFile::exists(ndkStackPath))
g_options.ndkStackPath = ndkStackPath;
}
return true;
}
static void printHelp()
{
fprintf(stderr, "Syntax: %s <options> -- [TESTARGS] \n"
"\n"
" Creates an Android package in a temp directory <destination> and\n"
" runs it on the default emulator/device or on the one specified by\n"
" \"ANDROID_DEVICE_SERIAL\" environment variable.\n"
"\n"
" Mandatory arguments:\n"
" --path <path>: The path where androiddeployqt builds the android package.\n"
"\n"
" --apk <apk path>: The test apk path. The apk has to exist already, if it\n"
" does not exist the make command must be provided for building the apk.\n"
"\n"
" Optional arguments:\n"
" --make <make cmd>: make command, needed to install the qt library.\n"
" For Qt 5.14+ this can be \"make apk\".\n"
"\n"
" --adb <adb cmd>: The Android ADB command. If missing the one from\n"
" $PATH will be used.\n"
"\n"
" --activity <acitvity>: The Activity to run. If missing the first\n"
" activity from AndroidManifest.qml file will be used.\n"
"\n"
" --timeout <seconds>: Timeout to run the test. Default is 10 minutes.\n"
"\n"
" --skip-install-root: Do not append INSTALL_ROOT=... to the make command.\n"
"\n"
" --show-logcat: Print Logcat output to stdout.\n"
"\n"
" --ndk-stack: Path to ndk-stack tool that symbolizes crash stacktraces.\n"
" By default, ANDROID_NDK_ROOT env var is used to deduce the tool path.\n"
"\n"
" -- Arguments that will be passed to the test application.\n"
"\n"
" --verbose: Prints out information during processing.\n"
"\n"
" --help: Displays this information.\n\n",
qPrintable(QCoreApplication::arguments().at(0))
);
}
static QString packageNameFromAndroidManifest(const QString &androidManifestPath)
{
QFile androidManifestXml(androidManifestPath);
if (androidManifestXml.open(QIODevice::ReadOnly)) {
QXmlStreamReader reader(&androidManifestXml);
while (!reader.atEnd()) {
reader.readNext();
if (reader.isStartElement() && reader.name() == QStringLiteral("manifest"))
return reader.attributes().value(QStringLiteral("package")).toString();
}
}
return {};
}
static QString activityFromAndroidManifest(const QString &androidManifestPath)
{
QFile androidManifestXml(androidManifestPath);
if (androidManifestXml.open(QIODevice::ReadOnly)) {
QXmlStreamReader reader(&androidManifestXml);
while (!reader.atEnd()) {
reader.readNext();
if (reader.isStartElement() && reader.name() == QStringLiteral("activity"))
return reader.attributes().value(QStringLiteral("android:name")).toString();
}
}
return {};
}
static void setOutputFile(QString file, QString format)
{
if (file.isEmpty())
file = QStringLiteral("-");
if (format.isEmpty())
format = QStringLiteral("txt");
g_options.outFiles[format] = file;
}
static bool parseTestArgs()
{
QRegularExpression oldFormats{QStringLiteral("^-(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$")};
QRegularExpression newLoggingFormat{QStringLiteral("^(.*),(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$")};
QString file;
QString logType;
QStringList unhandledArgs;
for (int i = 0; i < g_options.testArgsList.size(); ++i) {
const QString &arg = g_options.testArgsList[i].trimmed();
if (arg == QStringLiteral("--"))
continue;
if (arg == QStringLiteral("-o")) {
if (i >= g_options.testArgsList.size() - 1)
return false; // missing file argument
const auto &filePath = g_options.testArgsList[++i];
const auto match = newLoggingFormat.match(filePath);
if (!match.hasMatch()) {
file = filePath;
} else {
const auto capturedTexts = match.capturedTexts();
setOutputFile(capturedTexts.at(1), capturedTexts.at(2));
}
} else {
auto match = oldFormats.match(arg);
if (match.hasMatch()) {
logType = match.capturedTexts().at(1);
} else {
unhandledArgs << " %1"_L1.arg(arg);
}
}
}
if (g_options.outFiles.isEmpty() || !file.isEmpty() || !logType.isEmpty())
setOutputFile(file, logType);
QString testAppArgs;
for (const auto &format : g_options.outFiles.keys())
testAppArgs += "-o output.%1,%1 "_L1.arg(format);
testAppArgs += unhandledArgs.join(u' ').trimmed();
testAppArgs = "'%1'"_L1.arg(testAppArgs);
const QString activityName = "%1/%2"_L1.arg(g_options.package).arg(g_options.activity);
// Pass over any testlib env vars if set
QString testEnvVars;
const QStringList envVarsList = QProcessEnvironment::systemEnvironment().toStringList();
for (const QString &var : envVarsList) {
if (var.startsWith("QTEST_"_L1))
testEnvVars += "%1 "_L1.arg(var);
}
if (!testEnvVars.isEmpty()) {
testEnvVars = QString::fromUtf8(testEnvVars.trimmed().toUtf8().toBase64());
testEnvVars = "-e extraenvvars \"%4\""_L1.arg(testEnvVars);
}
g_options.amStarttestArgs = { "shell"_L1, "am"_L1, "start"_L1,
"-n"_L1, activityName,
"-e"_L1, "applicationArguments"_L1, testAppArgs,
testEnvVars
};
return true;
}
static bool obtainPid() {
QByteArray output;
const QStringList psArgs = { "shell"_L1, "ps | grep ' %1'"_L1.arg(g_options.package) };
if (!execAdbCommand(psArgs, &output, false))
return false;
const QList<QByteArray> lines = output.split(u'\n');
if (lines.size() < 1)
return false;
QList<QByteArray> columns = lines.first().simplified().replace(u'\t', u' ').split(u' ');
if (columns.size() < 3)
return false;
if (g_options.pid == -1) {
bool ok = false;
int pid = columns.at(1).toInt(&ok);
if (ok)
g_options.pid = pid;
}
return true;
}
static bool isRunning() {
QByteArray output;
const QStringList psArgs = { "shell"_L1, "ps | grep ' %1'"_L1.arg(g_options.package) };
if (!execAdbCommand(psArgs, &output, false))
return false;
return output.indexOf(QLatin1StringView(" " + g_options.package.toUtf8())) > -1;
}
static void waitForFinished()
{
// wait to start and set PID
QDeadlineTimer startDeadline(10000);
do {
if (obtainPid())
break;
QThread::msleep(100);
} while (!startDeadline.hasExpired());
// Wait to finish
QDeadlineTimer finishedDeadline(g_options.timeoutSecs * 1000);
do {
if (!isRunning())
break;
QThread::msleep(250);
} while (!finishedDeadline.hasExpired());
}
static void obtainSDKVersion()
{
// SDK version is necessary, as in SDK 23 pidof is broken, so we cannot obtain the pid.
// Also, Logcat cannot filter by pid in SDK 23, so we don't offer the --show-logcat option.
QByteArray output;
const QStringList versionArgs = { "shell"_L1, "getprop"_L1, "ro.build.version.sdk"_L1 };
execAdbCommand(versionArgs, &output, false);
bool ok = false;
int sdkVersion = output.toInt(&ok);
if (ok) {
g_options.sdkVersion = sdkVersion;
} else {
fprintf(stderr, "Unable to obtain the SDK version of the target.\n");
fflush(stderr);
}
}
static bool pullFiles()
{
bool ret = true;
QByteArray userId;
// adb get-current-user command is available starting from API level 26.
if (g_options.sdkVersion >= 26) {
const QStringList userIdArgs = {"shell"_L1, "cmd"_L1, "activity"_L1, "get-current-user"_L1};
if (!execAdbCommand(userIdArgs, &userId)) {
qCritical() << "Error: failed to retrieve the user ID";
return false;
}
} else {
userId = "0";
}
for (auto it = g_options.outFiles.constBegin(); it != g_options.outFiles.end(); ++it) {
// Get only stdout from cat and get rid of stderr and fail later if the output is empty
const QString outSuffix = it.key();
const QString catCmd = "cat files/output.%1 2> /dev/null"_L1.arg(outSuffix);
const QStringList fullCatArgs = { "shell"_L1, "run-as %1 --user %2 %3"_L1.arg(
g_options.package, QString::fromUtf8(userId.simplified()), catCmd) };
QByteArray output;
if (!execAdbCommand(fullCatArgs, &output, false)) {
qCritical() << "Error: failed to retrieve the test's output.%1 file."_L1.arg(outSuffix);
return false;
}
if (output.isEmpty()) {
qCritical() << "Error: the test's output.%1 is empty."_L1.arg(outSuffix);
return false;
}
auto checkerIt = g_options.checkFiles.find(outSuffix);
ret &= (checkerIt != g_options.checkFiles.end() && checkerIt.value()(output));
if (it.value() == "-"_L1) {
fprintf(stdout, "%s", output.constData());
fflush(stdout);
} else {
QFile out{it.value()};
if (!out.open(QIODevice::WriteOnly))
return false;
out.write(output);
}
}
return ret;
}
void printLogcat(const QString &formattedTime)
{
QStringList logcatArgs = { "logcat"_L1 };
if (g_options.sdkVersion <= 23 || g_options.pid == -1)
logcatArgs << "-t"_L1 << formattedTime;
else
logcatArgs << "-d"_L1 << "--pid=%1"_L1.arg(QString::number(g_options.pid));
QByteArray logcat;
if (!execAdbCommand(logcatArgs, &logcat, false)) {
qCritical() << "Error: failed to fetch logcat of the test";
return;
}
if (logcat.isEmpty()) {
qWarning() << "The retrieved logcat is empty";
return;
}
qDebug() << "****** Begin logcat output ******";
qDebug().noquote() << logcat;
qDebug() << "****** End logcat output ******";
}
static QString getDeviceABI()
{
const QStringList abiArgs = { "shell"_L1, "getprop"_L1, "ro.product.cpu.abi"_L1 };
QByteArray abi;
if (!execAdbCommand(abiArgs, &abi, false)) {
qWarning() << "Warning: failed to get the device abi, fallback to first libs dir";
return {};
}
return QString::fromUtf8(abi.simplified());
}
void printLogcatCrashBuffer(const QString &formattedTime)
{
bool useNdkStack = false;
auto libsPath = "%1/libs/"_L1.arg(g_options.buildPath);
if (!g_options.ndkStackPath.isEmpty()) {
QString abi = getDeviceABI();
if (abi.isEmpty()) {
QStringList subDirs = QDir(libsPath).entryList(QDir::Dirs | QDir::NoDotAndDotDot);
if (!subDirs.isEmpty())
abi = subDirs.first();
}
if (!abi.isEmpty()) {
libsPath += abi;
useNdkStack = true;
} else {
qWarning() << "Warning: failed to get the libs abi, ndk-stack cannot be used.";
}
} else {
qWarning() << "Warning: ndk-stack path not provided and couldn't be deduced "
"using the ANDROID_NDK_ROOT environment variable.";
}
QProcess adbCrashProcess;
QProcess ndkStackProcess;
if (useNdkStack) {
adbCrashProcess.setStandardOutputProcess(&ndkStackProcess);
ndkStackProcess.start(g_options.ndkStackPath, { "-sym"_L1, libsPath });
}
const QStringList adbCrashArgs = { "logcat"_L1, "-b"_L1, "crash"_L1, "-t"_L1, formattedTime };
adbCrashProcess.start(g_options.adbCommand, adbCrashArgs);
if (!adbCrashProcess.waitForStarted()) {
qCritical() << "Error: failed to run adb logcat crash command.";
return;
}
if (useNdkStack && !ndkStackProcess.waitForStarted()) {
qCritical() << "Error: failed to run ndk-stack command.";
return;
}
if (!adbCrashProcess.waitForFinished()) {
qCritical() << "Error: adb command timed out.";
return;
}
if (useNdkStack && !ndkStackProcess.waitForFinished()) {
qCritical() << "Error: ndk-stack command timed out.";
return;
}
const QByteArray crash = useNdkStack ? ndkStackProcess.readAllStandardOutput()
: adbCrashProcess.readAllStandardOutput();
if (crash.isEmpty()) {
qWarning() << "The retrieved crash logcat is empty";
return;
}
qDebug() << "****** Begin logcat crash buffer output ******";
qDebug().noquote() << crash;
qDebug() << "****** End logcat crash buffer output ******";
}
static QString getCurrentTimeString()
{
const QString timeFormat = (g_options.sdkVersion <= 23) ?
"%m-%d %H:%M:%S.000"_L1 : "%Y-%m-%d %H:%M:%S.%3N"_L1;
QStringList dateArgs = { "shell"_L1, "date"_L1, "+'%1'"_L1.arg(timeFormat) };
QByteArray output;
if (!execAdbCommand(dateArgs, &output, false)) {
qWarning() << "Date/time adb command failed";
return {};
}
return QString::fromUtf8(output.simplified());
}
struct TestRunnerSystemSemaphore
{
TestRunnerSystemSemaphore() { }
~TestRunnerSystemSemaphore() { release(); }
void acquire() { isAcquired.store(semaphore.acquire()); }
void release()
{
bool expected = true;
// NOTE: There's still could be tiny time gap between the compare_exchange_strong() call
// and release() call where the thread could be interrupted, if that's ever an issue,
// this code could be checked and improved further.
if (isAcquired.compare_exchange_strong(expected, false))
isAcquired.store(!semaphore.release());
}
std::atomic<bool> isAcquired { false };
QSystemSemaphore semaphore { QSystemSemaphore::platformSafeKey(u"androidtestrunner"_s),
1, QSystemSemaphore::Open };
};
TestRunnerSystemSemaphore testRunnerLock;
void sigHandler(int signal)
{
std::signal(signal, SIG_DFL);
testRunnerLock.release();
}
int main(int argc, char *argv[])
{
std::signal(SIGINT, sigHandler);
std::signal(SIGTERM, sigHandler);
QCoreApplication a(argc, argv);
if (!parseOptions()) {
printHelp();
return 1;
}
if (g_options.makeCommand.isEmpty()) {
fprintf(stderr,
"It is required to provide a make command with the \"--make\" parameter "
"to generate the apk.\n");
return 1;
}
if (!execCommand(g_options.makeCommand, nullptr, true)) {
if (!g_options.skipAddInstallRoot) {
// we need to run make INSTALL_ROOT=path install to install the application file(s) first
if (!execCommand(QStringLiteral("%1 INSTALL_ROOT=%2 install").arg(g_options.makeCommand,
QDir::toNativeSeparators(g_options.buildPath)), nullptr)) {
return 1;
}
} else {
if (!execCommand(g_options.makeCommand, nullptr))
return 1;
}
}
if (!QFile::exists(g_options.apkPath)) {
fprintf(stderr,
"No apk \"%s\" found after running the make command. Check the provided path and "
"the make command.\n",
qPrintable(g_options.apkPath));
return 1;
}
obtainSDKVersion();
// do not install or run packages while another test is running
testRunnerLock.acquire();
const QStringList installArgs = { "install"_L1, "-r"_L1, "-g"_L1, g_options.apkPath };
if (!execAdbCommand(installArgs, nullptr))
return 1;
QString manifest = g_options.buildPath + QStringLiteral("/AndroidManifest.xml");
g_options.package = packageNameFromAndroidManifest(manifest);
if (g_options.activity.isEmpty())
g_options.activity = activityFromAndroidManifest(manifest);
// parseTestArgs depends on g_options.package
if (!parseTestArgs())
return 1;
const QString formattedTime = getCurrentTimeString();
// start the tests
bool res = execAdbCommand(g_options.amStarttestArgs, nullptr);
waitForFinished();
if (res)
res &= pullFiles();
// If we have a failure, attempt to print both logcat and the crash buffer which
// includes the crash stacktrace that is not included in the default logcat.
if (!res) {
printLogcat(formattedTime);
printLogcatCrashBuffer(formattedTime);
} else if (g_options.showLogcatOutput) {
printLogcat(formattedTime);
}
res &= execAdbCommand({ "uninstall"_L1, g_options.package }, nullptr);
fflush(stdout);
testRunnerLock.release();
return res ? 0 : 1;
}