74static bool execCommand(
const QString &program,
const QStringList &args,
75 QByteArray *output =
nullptr,
bool verbose =
false)
77 const auto command = program +
" "_L1 + args.join(u' ');
79 if (verbose && g_options.verbose)
80 fprintf(stdout,
"Execute %s.\n", command.toUtf8().constData());
83 process.start(program, args);
84 if (!process.waitForStarted()) {
85 qCritical(
"Cannot execute command %s.", qPrintable(command));
91 const int FinishTimeout = program.endsWith(
"adb"_L1) ? 30000 : g_options.timeoutSecs * 1000;
92 if (!process.waitForFinished(FinishTimeout)) {
93 qCritical(
"Execution of command %s timed out.", qPrintable(command));
97 const auto stdOut = process.readAllStandardOutput();
99 output->append(stdOut);
101 if (verbose && g_options.verbose)
102 fprintf(stdout,
"%s\n", stdOut.constData());
104 return process.exitCode() == 0;
110 if (g_options.serial.isEmpty())
111 return execCommand(g_options.adbCommand, args, output, verbose);
113 QStringList argsWithSerial = {
"-s"_L1, g_options.serial};
114 argsWithSerial.append(args);
116 return execCommand(g_options.adbCommand, argsWithSerial, output, verbose);
147 QStringList arguments = QCoreApplication::arguments();
149 for (; i < arguments.size(); ++i) {
150 const QString &argument = arguments.at(i);
151 if (argument.compare(
"--adb"_L1, Qt::CaseInsensitive) == 0) {
152 if (i + 1 == arguments.size())
155 g_options.adbCommand = arguments.at(++i);
156 }
else if (argument.compare(
"--bundletool"_L1, Qt::CaseInsensitive) == 0) {
157 if (i + 1 == arguments.size())
160 g_options.bundletoolPath = arguments.at(++i);
161 }
else if (argument.compare(
"--path"_L1, Qt::CaseInsensitive) == 0) {
162 if (i + 1 == arguments.size())
166 }
else if (argument.compare(
"--manifest"_L1, Qt::CaseInsensitive) == 0) {
167 if (i + 1 == arguments.size())
170 g_options.manifestPath = arguments.at(++i);
171 }
else if (argument.compare(
"--make"_L1, Qt::CaseInsensitive) == 0) {
172 if (i + 1 == arguments.size())
175 g_options.makeCommand = arguments.at(++i);
176 }
else if (argument.compare(
"--apk"_L1, Qt::CaseInsensitive) == 0) {
177 if (i + 1 == arguments.size())
180 setPackagePath(arguments.at(++i));
181 }
else if (argument.compare(
"--aab"_L1, Qt::CaseInsensitive) == 0) {
182 if (i + 1 == arguments.size())
185 setPackagePath(arguments.at(++i));
186 }
else if (argument.compare(
"--activity"_L1, Qt::CaseInsensitive) == 0) {
187 if (i + 1 == arguments.size())
191 }
else if (argument.compare(
"--skip-install-root"_L1, Qt::CaseInsensitive) == 0) {
193 }
else if (argument.compare(
"--show-logcat"_L1, Qt::CaseInsensitive) == 0) {
195 }
else if (argument.compare(
"--ndk-stack"_L1, Qt::CaseInsensitive) == 0) {
196 if (i + 1 == arguments.size())
199 g_options.ndkStackPath = arguments.at(++i);
200 }
else if (argument.compare(
"--timeout"_L1, Qt::CaseInsensitive) == 0) {
201 if (i + 1 == arguments.size())
205 }
else if (argument.compare(
"--help"_L1, Qt::CaseInsensitive) == 0) {
207 }
else if (argument.compare(
"--verbose"_L1, Qt::CaseInsensitive) == 0) {
209 }
else if (argument.compare(
"--pre-test-adb-command"_L1, Qt::CaseInsensitive) == 0) {
210 if (i + 1 == arguments.size())
213 g_options.preTestRunAdbCommands += QProcess::splitCommand(arguments.at(++i));
215 }
else if (argument.compare(
"--"_L1, Qt::CaseInsensitive) == 0) {
219 g_options.testArgsList << arguments.at(i);
225 g_options.makeCommand =
"%1 INSTALL_ROOT=%2 install"_L1
226 .arg(g_options.makeCommand)
227 .arg(QDir::toNativeSeparators(g_options.buildPath));
230 for (;i < arguments.size(); ++i)
231 g_options.testArgsList << arguments.at(i);
236 g_options.serial = qEnvironmentVariable(
"ANDROID_SERIAL");
238 g_options.serial = qEnvironmentVariable(
"ANDROID_DEVICE_SERIAL");
241 const QString ndkPath = qEnvironmentVariable(
"ANDROID_NDK_ROOT");
242 const QString ndkStackPath = ndkPath + QDir::separator() +
"ndk-stack"_L1;
243 if (QFile::exists(ndkStackPath))
247 if (g_options.manifestPath.isEmpty())
248 g_options.manifestPath = g_options.buildPath +
"/AndroidManifest.xml"_L1;
309 QFile androidManifestXml(
g_options.manifestPath);
310 if (!androidManifestXml.open(QIODevice::ReadOnly)) {
311 qCritical(
"Unable to read android manifest '%s'", qPrintable(
g_options.manifestPath));
315 QXmlStreamReader reader(&androidManifestXml);
316 while (!reader.atEnd()) {
318 if (!reader.isStartElement())
321 if (reader.name() ==
"manifest"_L1)
322 g_options.package = reader.attributes().value(
"package"_L1).toString();
323 else if (reader.name() ==
"activity"_L1 && g_options.activity.isEmpty())
324 g_options.activity = reader.attributes().value(
"android:name"_L1).toString();
325 else if (reader.name() ==
"uses-permission"_L1)
326 g_options.permissions.append(reader.attributes().value(
"android:name"_L1).toString());
351 QRegularExpression oldFormats{
"^-(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
352 QRegularExpression newLoggingFormat{
"^(.*),(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
356 QStringList unhandledArgs;
357 for (
int i = 0; i <
g_options.testArgsList.size(); ++i) {
358 const QString &arg =
g_options.testArgsList[i].trimmed();
361 if (arg ==
"-o"_L1) {
362 if (i >=
g_options.testArgsList.size() - 1)
365 const auto &filePath =
g_options.testArgsList[++i];
366 const auto match = newLoggingFormat.match(filePath);
367 if (!match.hasMatch()) {
370 const auto capturedTexts = match.capturedTexts();
371 setOutputFile(capturedTexts.at(1), capturedTexts.at(2));
374 auto match = oldFormats.match(arg);
375 if (match.hasMatch()) {
376 logType = match.capturedTexts().at(1);
380 QString quotedArg = QString(arg).replace(
"\""_L1,
"\\\"\\\"\\\""_L1);
383 quotedArg.replace(
"'"_L1,
"\'"_L1);
385 unhandledArgs <<
" \\\"%1\\\""_L1.arg(quotedArg);
389 if (
g_options.outFiles.isEmpty() || !file.isEmpty() || !logType.isEmpty())
390 setOutputFile(file, logType);
393 for (
auto it = g_options.outFiles.constBegin(); it != g_options.outFiles.constEnd(); ++it)
394 testAppArgs +=
"-o %1,%2 "_L1.arg(QFileInfo(it.value()).fileName(), it.key());
396 testAppArgs += unhandledArgs.join(u' ').trimmed();
397 testAppArgs =
"\"%1\""_L1.arg(testAppArgs.trimmed());
398 const QString activityName =
"%1/%2"_L1.arg(g_options.package).arg(g_options.activity);
402 const QStringList envVarsList = QProcessEnvironment::systemEnvironment().toStringList();
403 for (
const QString &var : envVarsList) {
404 if (var.startsWith(
"QTEST_"_L1) || var.startsWith(
"QT_"_L1))
405 testEnvVars +=
"%1 "_L1.arg(var);
408 if (!testEnvVars.isEmpty()) {
409 testEnvVars = QString::fromUtf8(testEnvVars.trimmed().toUtf8().toBase64());
410 testEnvVars =
"-e extraenvvars \"%4\""_L1.arg(testEnvVars);
413 g_options.amStarttestArgs = {
"shell"_L1,
"am"_L1,
"start"_L1,
414 "-n"_L1, activityName,
415 "-e"_L1,
"applicationArguments"_L1, testAppArgs,
602 for (
auto it =
g_options.outFiles.constBegin(); it !=
g_options.outFiles.constEnd(); ++it) {
603 const QString filePath = it.value();
604 const QString fileName = QFileInfo(filePath).fileName();
606 const QString catCmd =
"cat files/%1 2> /dev/null"_L1.arg(fileName);
607 const QStringList fullCatArgs = {
"shell"_L1, runCommandAsUserArgs(catCmd) };
609 bool catSuccess =
false;
613 catSuccess = execAdbCommand(fullCatArgs, &output,
false);
616 else if (!output.isEmpty())
621 qCritical() <<
"Error: failed to retrieve the test result file %1."_L1.arg(fileName);
625 if (output.isEmpty()) {
626 qCritical() <<
"Error: the test result file %1 is empty."_L1.arg(fileName);
631 if (!out.open(QIODevice::WriteOnly)) {
632 qCritical() <<
"Error: failed to open %1 to write results to host."_L1.arg(filePath);
672 if (logcat.isEmpty())
675 QByteArray crashLogcat(logcat);
677 QProcess ndkStackProc;
678 ndkStackProc.start(g_options.ndkStackPath, {
"-sym"_L1, getAbiLibsPath() });
680 if (ndkStackProc.waitForStarted()) {
681 ndkStackProc.write(crashLogcat);
682 ndkStackProc.closeWriteChannel();
684 if (ndkStackProc.waitForReadyRead())
685 crashLogcat = ndkStackProc.readAllStandardOutput();
687 ndkStackProc.terminate();
688 if (!ndkStackProc.waitForFinished())
689 qCritical() <<
"Error: ndk-stack command timed out.";
691 qCritical() <<
"Error: failed to run ndk-stack command.";
695 qWarning() <<
"Warning: ndk-stack path not provided and couldn't be deduced "
696 "using the ANDROID_NDK_ROOT environment variable.";
699 if (!crashLogcat.startsWith(
"********** Crash dump"))
700 qDebug() <<
"********** Crash dump: **********";
701 qDebug().noquote() << crashLogcat.trimmed();
702 qDebug() <<
"********** End crash dump **********";
707 QStringList logcatArgs = {
"shell"_L1,
"logcat"_L1,
"-t"_L1,
"'%1'"_L1.arg(timeStamp),
708 "-v"_L1,
"brief"_L1 };
710 const bool useColor = qEnvironmentVariable(
"QTEST_ENVIRONMENT") !=
"ci"_L1;
712 logcatArgs <<
"-v"_L1 <<
"color"_L1;
715 if (!execAdbCommand(logcatArgs, &logcat,
false)) {
716 qCritical() <<
"Error: failed to fetch logcat of the test";
720 if (logcat.isEmpty()) {
721 qWarning() <<
"The retrieved logcat is empty";
725 const QByteArray crashMarker(
"*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***");
726 int crashMarkerIndex = logcat.indexOf(crashMarker);
727 QByteArray crashLogcat;
729 if (crashMarkerIndex != -1) {
730 crashLogcat = logcat.mid(crashMarkerIndex);
731 logcat = logcat.left(crashMarkerIndex);
735 const bool anrOccurred = logcat.contains(
"ANR in %1"_L1.arg(g_options.package).toUtf8());
738 *exitCode = *exitCode < 1 ? 1 : *exitCode;
739 qCritical(
"An ANR has occurred while running the test %s. The logcat will include "
740 "additional logs from the system_server process.",
744 int systemServerPid = getPid(
"system_server"_L1);
746 static const QRegularExpression logcatRegEx{
747 "(?:^\\x1B\\[[0-9]+m)?"
750 "(\\(\\s*\\d*\\)):"
753 "(?:\\x1B\\[[0-9]+m)?"
757 QByteArrayList testLogcat;
758 for (
const QByteArray &line : logcat.split(u'\n')) {
759 QRegularExpressionMatch match = logcatRegEx.match(QString::fromUtf8(line));
760 if (match.hasMatch()) {
761 const QString msgType = match.captured(1);
762 const QString pidStr = match.captured(2);
763 const int capturedPid = pidStr.mid(1, pidStr.size() - 2).trimmed().toInt();
764 if (capturedPid == g_testInfo.pid || msgType == u'F')
765 testLogcat.append(line);
766 else if (anrOccurred && capturedPid == systemServerPid)
767 testLogcat.append(line);
770 testLogcat.append(line);
778 qDebug() <<
"********** logcat dump **********";
779 qDebug().noquote() << testLogcat.join(u'\n').trimmed();
780 qDebug() <<
"********** End logcat dump **********";
782 if (!crashLogcat.isEmpty())
783 printLogcatCrash(crashLogcat);
859int main(
int argc,
char *argv[])
864 QCoreApplication a(argc, argv);
871 qCritical() <<
"It is required to provide a make command with the \"--make\" parameter "
872 "to generate the apk.";
876 QByteArray buildOutput;
877 if (!execCommand(g_options.makeCommand, &buildOutput,
true)) {
878 qCritical(
"The APK build command \"%s\" failed\n\n%s",
879 qPrintable(
g_options.makeCommand), buildOutput.constData());
883 if (!QFile::exists(g_options.packagePath)) {
884 qCritical(
"No apk \"%s\" found after running the make command. "
885 "Check the provided path and the make command.",
890 const QStringList devices = runningDevices();
891 if (devices.isEmpty()) {
892 qCritical(
"No connected devices or running emulators can be found.");
895 qCritical(
"No connected device or running emulator with serial '%s' can be found.",
902 g_testInfo.userId = userId();
904 if (!QFile::exists(g_options.manifestPath)) {
905 qCritical(
"Unable to find '%s'.", qPrintable(
g_options.manifestPath));
913 qCritical(
"Unable to get package name for '%s'", qPrintable(
g_options.packagePath));
924 if (g_options.packagePath.endsWith(
".apk"_L1)) {
925 const QStringList installArgs = {
"install"_L1,
"-r"_L1, g_options.packagePath };
926 g_testInfo.isPackageInstalled.store(execAdbCommand(installArgs,
nullptr));
929 }
else if (g_options.packagePath.endsWith(
".aab"_L1)) {
931 const auto apksFilePath = aab.absoluteDir().absoluteFilePath(aab.baseName() +
".apks"_L1);
932 if (!execBundletoolCommand({
"build-apks"_L1,
"--bundle"_L1, g_options.packagePath,
933 "--output"_L1, apksFilePath,
"--local-testing"_L1,
937 if (!execBundletoolCommand({
"install-apks"_L1,
"--apks"_L1, apksFilePath }))
941 for (
const auto &permission : g_options.permissions) {
942 if (!execAdbCommand({
"shell"_L1,
"pm"_L1,
"grant"_L1, g_options.package, permission },
944 qWarning(
"Unable to grant '%s' to '%s'. Probably the Android version mismatch.",
945 qPrintable(permission), qPrintable(g_options.package));
950 for (
const auto &command : g_options.preTestRunAdbCommands) {
952 if (!execAdbCommand(command, &output)) {
953 qCritical(
"The pre test ADB command \"%s\" failed with output:\n%s",
954 qUtf8Printable(command.join(u' ')), output.constData());
960 const QString formattedStartTime = getCurrentTimeString();
963 if (!execAdbCommand(g_options.amStarttestArgs,
nullptr))
981 analyseLogcat(formattedStartTime, &exitCode);
990 if (
g_testInfo.isTestRunnerInterrupted.load()) {
991 qCritical() <<
"The androidtestrunner was interrupted and the was test cleaned up.";