104 std::chrono::milliseconds timeout = std::chrono::milliseconds(-1))
106 const auto command = program +
" "_L1 + args.join(u' ');
109 fprintf(stdout,
"Execute %s.\n", command.toUtf8().constData());
112 process.start(program, args);
113 if (!process.waitForStarted()) {
114 qCritical(
"Cannot execute command %s.", qPrintable(command));
118 const bool finished = timeout.count() < 0
119 ? process.waitForFinished()
120 : process.waitForFinished(
static_cast<
int>(timeout.count()));
122 qCritical(
"Execution of command %s timed out.", qPrintable(command));
124 process.waitForFinished();
126 const QByteArray stdErr = process.readAllStandardError();
127 if (!stdErr.isEmpty())
128 qWarning().noquote() << stdErr.trimmed();
132 const auto stdOut = process.readAllStandardOutput();
133 const auto stdErr = process.readAllStandardError();
135 if (process.exitCode() != 0) {
137 if (verbose && !stdOut.isEmpty())
138 qWarning().noquote() << stdOut.trimmed();
139 if (!stdErr.isEmpty())
140 qWarning().noquote() << stdErr.trimmed();
145 fprintf(stdout,
"%s\n", stdOut.constData());
148 return stdOut.isNull() ? QByteArray(
"") : stdOut;
280 QCommandLineParser parser;
281 parser.setApplicationDescription(
282 "Runs a Qt for Android test on an emulator or a device. Specify a "
283 "device via --serial, ANDROID_SERIAL or ANDROID_DEVICE_SERIAL.\n"
286 " 0-127 QTest exit code (number of failed test functions)\n"
287 " 250 EXIT_DEVICE_GONE device disconnected\n"
288 " 251 EXIT_NORESULTS result files could not be pulled\n"
289 " 252 EXIT_ANR Android Not Responding\n"
290 " 253 EXIT_NOEXITCODE exit code could not be read from device\n"
291 " 254 EXIT_ERROR generic runner failure"_L1);
293 QCommandLineOption pathOpt(
294 "path"_L1,
"Path where the Android Gradle package is built."_L1,
"path"_L1);
295 QCommandLineOption makeOpt(
296 "make"_L1,
"make command to build the APK."_L1,
"command"_L1);
297 QCommandLineOption apkOpt(
298 "apk"_L1,
"Test APK path. Built via --make if absent."_L1,
"path"_L1);
299 QCommandLineOption aabOpt(
300 "aab"_L1,
"Test AAB path. Built via --make if absent."_L1,
"path"_L1);
301 QCommandLineOption bundletoolOpt(
302 "bundletool"_L1,
"Path to Android bundletool jar."_L1,
"path"_L1);
303 QCommandLineOption manifestOpt(
304 "manifest"_L1,
"Custom AndroidManifest.xml path."_L1,
"path"_L1);
305 QCommandLineOption adbOpt(
306 "adb"_L1,
"Path to adb. Falls back to $PATH."_L1,
"command"_L1,
"adb"_L1);
307 QCommandLineOption serialOpt(
309 "Android device serial. Overrides ANDROID_SERIAL / ANDROID_DEVICE_SERIAL."_L1,
311 QCommandLineOption activityOpt(
312 "activity"_L1,
"Activity to run. Defaults to the first in the manifest."_L1,
"name"_L1);
313 QCommandLineOption timeoutOpt(
314 "timeout"_L1,
"Test timeout in seconds (default 600)."_L1,
"seconds"_L1,
"600"_L1);
315 QCommandLineOption preTestAdbOpt(
"pre-test-adb-command"_L1,
316 "adb command to run after install, before test."_L1,
"command"_L1);
317 QCommandLineOption skipInstallRootOpt(
318 "skip-install-root"_L1,
319 "Don't append INSTALL_ROOT=... to --make. Only honored for make-family "
320 "tools (make, gmake, nmake, mingw32-make, jom)."_L1);
321 QCommandLineOption ndkStackOpt(
322 "ndk-stack"_L1,
"Path to ndk-stack (default: ndk-stack under $ANDROID_NDK_ROOT "
323 "or $ANDROID_NDK_HOME)."_L1,
"path"_L1);
324 QCommandLineOption showLogcatOpt(
325 "show-logcat"_L1,
"Print logcat output (+ system_server on ANR)."_L1);
326 QCommandLineOption verboseOpt(
327 "verbose"_L1,
"Print extra information."_L1);
328 QCommandLineOption helpOpt(
"help"_L1,
"Show this help message."_L1);
330 const QList<QCommandLineOption> ourOptions = {
331 pathOpt, makeOpt, apkOpt, aabOpt,
332 bundletoolOpt, manifestOpt,
334 activityOpt, timeoutOpt, preTestAdbOpt,
336 ndkStackOpt, showLogcatOpt, verboseOpt,
339 parser.addOptions(ourOptions);
340 parser.addPositionalArgument(
"testargs"_L1,
"Arguments forwarded to the test."_L1,
343 QSet<QString> knownOpts;
344 QSet<QString> valueOpts;
345 for (
const QCommandLineOption &opt : ourOptions) {
346 const bool takesValue = !opt.valueName().isEmpty();
347 for (
const QString &name : opt.names()) {
348 knownOpts.insert(name);
350 valueOpts.insert(name);
354 const QStringList processedArgs =
355 splitOwnAndTestArgs(QCoreApplication::arguments(), knownOpts, valueOpts);
356 if (processedArgs.isEmpty())
358 if (!parser.parse(processedArgs)) {
359 qCritical(
"%s", qPrintable(parser.errorText()));
362 if (parser.isSet(helpOpt))
365 g_options.buildPath = parser.value(pathOpt);
366 g_options.makeCommand = parser.value(makeOpt);
367 if (!collectPackagePaths(parser.values(apkOpt), parser.values(aabOpt)))
369 g_options.adbCommand = parser.value(adbOpt);
370 g_options.activity = parser.value(activityOpt);
371 g_options.serial = parser.value(serialOpt);
372 bool timeoutOk =
false;
375 qCritical(
"--timeout must be a positive integer (got '%s').",
376 qPrintable(parser.value(timeoutOpt)));
381 g_options.ndkStackPath = parser.value(ndkStackOpt);
383 g_options.manifestPath = parser.value(manifestOpt);
384 g_options.bundletoolPath = parser.value(bundletoolOpt);
386 for (
const QString &cmd : parser.values(preTestAdbOpt))
387 g_options.preTestRunAdbCommands += QProcess::splitCommand(cmd);
388 g_options.testArgsList = parser.positionalArguments();
390 static const QStringList makeNames = {
391 "make"_L1,
"gmake"_L1,
"nmake"_L1,
"mingw32-make"_L1,
"jom"_L1,
393 const QStringList makeParts = QProcess::splitCommand(g_options.makeCommand);
394 const QString makeBaseName = QFileInfo(makeParts.value(0)).baseName();
395 const bool makeIsGnuMakeFamily = makeNames.contains(makeBaseName, Qt::CaseInsensitive);
397 g_options.makeCommand =
"%1 INSTALL_ROOT=%2 install"_L1
398 .arg(g_options.makeCommand, QDir::toNativeSeparators(g_options.buildPath));
402 qCritical(
"--path and --apk (or --aab) are required.");
403 fputs(qPrintable(parser.helpText()), stderr);
407 if (!
g_options.packagePath.endsWith(
".apk"_L1)
408 && !
g_options.packagePath.endsWith(
".aab"_L1)) {
409 qCritical(
"Package path '%s' must end with .apk or .aab.",
414 if (
g_options.packagePath.endsWith(
".aab"_L1)) {
415 if (
g_options.bundletoolPath.isEmpty()) {
416 qCritical(
"--aab requires --bundletool to locate the bundletool jar.");
419 if (!QFile::exists(g_options.bundletoolPath)) {
420 qCritical(
"--bundletool path '%s' does not exist.",
427 g_options.serial = qEnvironmentVariable(
"ANDROID_SERIAL");
429 g_options.serial = qEnvironmentVariable(
"ANDROID_DEVICE_SERIAL");
469 QString gradlew = androidBuildDir +
"/gradlew.bat"_L1;
471 QString gradlew = androidBuildDir +
"/gradlew"_L1;
473 if (!QFile::exists(gradlew))
476 const QString scriptPath = gradleInitScriptPath();
477 if (scriptPath.isEmpty())
481 process.setWorkingDirectory(androidBuildDir);
482 process.start(gradlew, {
"-q"_L1,
"--init-script"_L1, scriptPath,
483 "-Pproperty="_L1 + property,
"printProjectProperty"_L1 });
485 if (!process.waitForFinished()) {
486 qWarning(
"gradlew '%s' timed out.", qPrintable(property));
488 process.waitForFinished();
491 if (process.exitCode() != 0) {
492 qWarning(
"gradlew '%s' exited with %d:\n%s", qPrintable(property),
493 process.exitCode(), process.readAllStandardError().constData());
496 return QString::fromUtf8(process.readAllStandardOutput()).trimmed();
502 if (!QFile::exists(g_options.manifestPath)) {
503 qCritical(
"--manifest path '%s' does not exist.",
508 const QStringList candidates = {
509 g_options.buildPath +
"/AndroidManifest.xml"_L1,
510 g_options.buildPath +
"/app/AndroidManifest.xml"_L1
512 for (
const QString &candidate : candidates) {
513 if (QFile::exists(candidate)) {
514 g_options.manifestPath = candidate;
520 qCritical(
"Unable to find AndroidManifest.xml at '%s'.", qPrintable(
g_options.buildPath));
523 QFile androidManifestXml(
g_options.manifestPath);
524 if (!androidManifestXml.open(QIODevice::ReadOnly)) {
525 qCritical(
"Unable to read android manifest '%s'", qPrintable(
g_options.manifestPath));
529 QXmlStreamReader reader(&androidManifestXml);
530 while (!reader.atEnd()) {
532 if (!reader.isStartElement())
535 if (reader.name() ==
"activity"_L1 && g_options.activity.isEmpty())
536 g_options.activity = reader.attributes().value(
"android:name"_L1).toString();
537 else if (reader.name() ==
"uses-permission"_L1)
538 g_options.permissions.append(reader.attributes().value(
"android:name"_L1).toString());
545 const QStringList args({
"shell"_L1,
"dumpsys"_L1,
"package"_L1,
"permissions"_L1 });
546 const QByteArray output = execAdbCommand(args,
false);
547 if (output.isNull()) {
548 qWarning(
"Failed to query permissions via dumpsys");
553
554
555
556
557
558
559
560
561 const static QRegularExpression regex(
"^\\s*Permission\\s+\\[([^\\]]+)\\]\\s+\\([^)]+\\):"_L1);
562 QStringList dangerousPermissions;
565 const QStringList lines = QString::fromUtf8(output).split(u'\n');
566 for (
const QString &line : lines) {
567 QRegularExpressionMatch match = regex.match(line);
568 if (match.hasMatch()) {
569 currentPerm = match.captured(1);
573 if (currentPerm.isEmpty())
576 int protIndex = line.indexOf(
"prot="_L1);
580 QString protectionTypes = line.mid(protIndex + 5).trimmed();
581 if (protectionTypes.contains(
"dangerous"_L1, Qt::CaseInsensitive))
582 dangerousPermissions.append(currentPerm);
586 return dangerousPermissions;
612 QRegularExpression oldFormats{
"^-(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
613 QRegularExpression newLoggingFormat{
"^(.*),(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
617 QStringList unhandledArgs;
618 for (
int i = 0; i <
g_options.testArgsList.size(); ++i) {
619 const QString &arg =
g_options.testArgsList[i].trimmed();
622 if (arg ==
"-o"_L1 || arg ==
"--output"_L1) {
623 if (i >=
g_options.testArgsList.size() - 1)
626 const auto &filePath =
g_options.testArgsList[++i];
627 const auto match = newLoggingFormat.match(filePath);
628 if (!match.hasMatch()) {
631 const auto capturedTexts = match.capturedTexts();
632 setOutputFile(capturedTexts.at(1), capturedTexts.at(2));
635 auto match = oldFormats.match(arg);
636 if (match.hasMatch()) {
637 logType = match.capturedTexts().at(1);
641 QString quotedArg = QString(arg).replace(
"\""_L1,
"\\\"\\\"\\\""_L1);
643 unhandledArgs <<
" \\\"%1\\\""_L1.arg(quotedArg);
647 if (
g_options.outFiles.isEmpty() || !file.isEmpty() || !logType.isEmpty())
648 setOutputFile(file, logType);
651 for (
auto it =
g_options.outFiles.constBegin(); it !=
g_options.outFiles.constEnd(); ++it)
652 testAppArgs +=
"-o %1,%2 "_L1.arg(deviceOutputFileName(it.key(), it.value()), it.key());
654 testAppArgs += unhandledArgs.join(u' ').trimmed();
655 testAppArgs =
"\"%1\""_L1.arg(testAppArgs.trimmed());
656 const QString activityName =
"%1/%2"_L1.arg(g_options.package).arg(g_options.activity);
659 QStringList testEnvVarArgs;
660 const QStringList envVarsList = QProcessEnvironment::systemEnvironment().toStringList();
661 for (
const QString &var : envVarsList) {
662 if (!var.startsWith(
"QTEST_"_L1) && !var.startsWith(
"QT_"_L1))
664 const qsizetype index = var.indexOf(u'=');
667 const QString key = var.left(index);
668 QString escapedValue = var.mid(index + 1);
669 escapedValue.replace(
"'"_L1,
"'\\''"_L1);
670 const QString value =
"'%1'"_L1.arg(escapedValue);
671 testEnvVarArgs <<
"-e"_L1 << (
"extraenvvars_"_L1 + key) << value;
674 g_options.amStarttestArgs = {
"shell"_L1,
"am"_L1,
"start"_L1,
"-W"_L1,
675 "-n"_L1, activityName,
676 "-e"_L1,
"applicationArguments"_L1, testAppArgs };
677 g_options.amStarttestArgs.append(testEnvVarArgs);
943 if (logcat.isEmpty())
946 QByteArray crashLogcat(logcat);
948 qWarning() <<
"Warning: ndk-stack path not provided and couldn't be deduced "
949 "using the ANDROID_NDK_ROOT or ANDROID_NDK_HOME environment "
951 }
else if (
const QString libsPath = getAbiLibsPath(); libsPath.isEmpty()) {
952 qWarning() <<
"Warning: could not determine the device ABI, "
953 "skipping ndk-stack and printing the raw dump.";
955 QProcess ndkStackProc;
956 ndkStackProc.start(
g_options.ndkStackPath, {
"-sym"_L1, libsPath });
958 if (ndkStackProc.waitForStarted()) {
959 ndkStackProc.write(crashLogcat);
960 ndkStackProc.closeWriteChannel();
963 if (ndkStackProc.waitForFinished()) {
965 const QByteArray ndkOutput = ndkStackProc.readAllStandardOutput();
966 if (!ndkOutput.trimmed().isEmpty())
967 crashLogcat = ndkOutput;
969 qCritical() <<
"Error: ndk-stack command timed out.";
971 ndkStackProc.waitForFinished();
974 qCritical() <<
"Error: failed to run ndk-stack command.";
978 if (crashLogcat.startsWith(
"********** Crash dump")) {
979 qDebug().noquote() << crashLogcat.trimmed();
981 qDebug() <<
"[androidtestrunner] ********** BEGIN crash dump **********";
982 qDebug().noquote() << crashLogcat.trimmed();
983 qDebug() <<
"[androidtestrunner] ********** END crash dump **********";
993 using namespace std::chrono_literals;
996 QStringList logcatArgs = {
"shell"_L1,
"logcat"_L1,
997 "-b"_L1,
"main,system,crash"_L1,
998 "-v"_L1,
"brief"_L1 };
1000 if (!timeStamp.isEmpty())
1001 logcatArgs <<
"-t"_L1 <<
"'%1'"_L1.arg(timeStamp);
1003 logcatArgs <<
"-t"_L1 <<
"5000"_L1;
1004 const bool useColor = !QTestCiDetect::ciInTestEnvironment();
1006 logcatArgs <<
"-v"_L1 <<
"color"_L1;
1008 QByteArray logcat = execAdbCommand(logcatArgs,
false);
1009 if (logcat.isNull())
1010 qWarning() <<
"Warning: failed to fetch logcat of the test";
1012 if (!waitForDiagnostics)
1015 const QByteArray anrMarker =
"ANR in " +
g_options.package.toUtf8();
1021 constexpr auto timeout = 15s;
1023 polled = execAdbCommand(logcatArgs,
false);
1024 return !polled.isNull()
1025 && (polled.contains(crashBannerMarker) || polled.contains(anrMarker));
1026 }, QDeadlineTimer(timeout), 250ms);
1027 if (!polled.isEmpty())
1028 logcat =
std::move(polled);
1029 if (!found && !
g_testInfo.isTestRunnerInterrupted.load()) {
1030 qWarning().noquote() << QString::fromLatin1(
"[androidtestrunner] No crash banner or "
1031 "ANR marker found in logcat within %1s; output below may be incomplete.")
1032 .arg(timeout.count());
1053 static const QRegularExpression logcatRegEx{
1054 "(?:^\\x1B\\[[0-9;]*m)?"
1057 "(\\(\\s*\\d*\\)):"
1060 "(?:\\x1B\\[[0-9;]*m)?"
1063 QByteArrayList kept;
1064 for (
const QByteArray &line : logcat.split(u'\n')) {
1065 QRegularExpressionMatch match = logcatRegEx.match(QString::fromUtf8(line));
1066 if (!match.hasMatch()) {
1071 const QString msgType = match.captured(1);
1072 const QString pidStr = match.captured(2);
1073 const int capturedPid = pidStr.mid(1, pidStr.size() - 2).trimmed().toInt();
1074 const bool isFatal = msgType == u'F';
1075 const bool isOurTest = capturedPid == testPid;
1076 const bool isAnrSource = systemServerPid > 0 && capturedPid == systemServerPid;
1077 if (isOurTest || isFatal || isAnrSource)
1080 return kept.join(
'\n');
1086 && !
g_testInfo.isTestRunnerInterrupted.load();
1087 QByteArray logcat = fetchLogcat(timeStamp, wasAbnormal);
1088 if (logcat.isEmpty()) {
1090 qWarning() <<
"The retrieved logcat is empty";
1094 const QByteArray crashDump = takeCrashDump(&logcat);
1096 const bool anrOccurred = logcat.contains(
1097 "ANR in %1"_L1.arg(g_options.package).toUtf8());
1103 qCritical(
"[androidtestrunner] An ANR has occurred while running the test '%s';"
1104 " consult logcat for additional logs from the system_server process",
1108 const int systemServerPid = anrOccurred ? getPid(
"system_server"_L1) : -1;
1109 const QByteArray filtered = filterTestLogcat(logcat, g_testInfo.pid, systemServerPid);
1114 && !
g_testInfo.isTestRunnerInterrupted.load();
1116 qDebug() <<
"[androidtestrunner] ********** BEGIN logcat dump **********";
1117 qDebug().noquote() << filtered.trimmed();
1118 qDebug() <<
"[androidtestrunner] ********** END logcat dump **********";
1120 if (testCrashed && !crashDump.isEmpty())
1121 printLogcatCrash(crashDump);
1182 using namespace std::chrono_literals;
1186 QCoreApplication a(argc, argv);
1191 qCritical() <<
"It is required to provide a make command with the \"--make\" parameter "
1192 "to generate the apk.";
1196 if (execCommand(
g_options.makeCommand,
true, g_options.timeoutSecs * 1s).isNull()) {
1197 qCritical(
"The APK build command \"%s\" failed.", qPrintable(
g_options.makeCommand));
1201 if (!QFile::exists(g_options.packagePath)) {
1202 qCritical(
"No package \"%s\" found after running the make command. "
1203 "Check the provided path and the make command.",
1208 const std::optional<QStringList> devices = runningDevices();
1210 qCritical(
"Failed to query connected devices via 'adb devices'.");
1212 }
else if (devices->isEmpty()) {
1213 qCritical(
"No connected devices or running emulators can be found.");
1216 qCritical(
"No connected device or running emulator with serial '%s' can be found.",
1219 }
else if (
g_options.serial.isEmpty() && devices->size() == 1) {
1221 }
else if (
g_options.serial.isEmpty()) {
1222 qCritical(
"Multiple devices connected, set ANDROID_SERIAL or ANDROID_DEVICE_SERIAL.");
1233 const QString ns = getGradleProjectProperty(g_options.buildPath,
"android.namespace"_L1);
1238 qCritical(
"Unable to get package name for '%s'", qPrintable(
g_options.packagePath));
1247 const QString user = qEnvironmentVariable(
"USER",
1248 qEnvironmentVariable(
"USERNAME", u"default"_s));
1249 const QString lockName = u"androidtestrunner-%1-%2.lock"_s.arg(user, g_options.serial);
1250 const QDir tempDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation));
1251 QLockFile testRunnerLock(tempDir.absoluteFilePath(lockName));
1252 testRunnerLock.setStaleLockTime(0);
1253 if (!testRunnerLock.lock()) {
1254 qCritical(
"Failed to acquire test runner lock for '%s'.", qPrintable(
g_options.serial));
1258 if (
g_options.packagePath.endsWith(
".apk"_L1)) {
1259 const QStringList installArgs = {
"install"_L1,
"-r"_L1, g_options.packagePath };
1260 if (execAdbCommand(installArgs).isNull())
1262 }
else if (
g_options.packagePath.endsWith(
".aab"_L1)) {
1264 const auto apksFilePath = aab.absoluteDir().absoluteFilePath(aab.baseName() +
".apks"_L1);
1265 QStringList installApksArgs = {
"install-apks"_L1,
"--apks"_L1, apksFilePath };
1266 if (!g_options.serial.isEmpty())
1267 installApksArgs <<
"--device-id"_L1 << g_options.serial;
1268 if (execBundletoolCommand({
"build-apks"_L1,
"--bundle"_L1, g_options.packagePath,
1269 "--output"_L1, apksFilePath,
"--local-testing"_L1,
1270 "--overwrite"_L1 }).isNull()
1271 || execBundletoolCommand(installApksArgs).isNull())
1276 const QStringList dangerousPermissions = queryDangerousPermissions();
1277 for (
const auto &permission : g_options.permissions) {
1278 if (!dangerousPermissions.contains(permission))
1281 if (execAdbCommand({
"shell"_L1,
"pm"_L1,
"grant"_L1,
"--user"_L1, g_testInfo.userId,
1282 g_options.package, permission }).isNull()) {
1283 qWarning(
"Unable to grant '%s' to '%s'. Probably the Android version mismatch.",
1284 qPrintable(permission), qPrintable(g_options.package));
1289 for (
const auto &command : g_options.preTestRunAdbCommands) {
1290 if (execAdbCommand(command).isNull()) {
1291 qCritical(
"The pre test ADB command \"%s\" failed.",
1292 qUtf8Printable(command.join(u' ')));
1298 const QString formattedStartTime = getCurrentTimeString();
1301 if (execAdbCommand(
g_options.amStarttestArgs).isNull()) {
1305 analyseLogcat(formattedStartTime, &exitCode);
1312 qWarning(
"Continuing without live stdout streaming; result files are still pulled.");
1317 qCritical(
"[androidtestrunner] Device '%s' became unreachable during the test, "
1318 "result transfer and uninstall skipped.", qPrintable(
g_options.serial));
1330 analyseLogcat(formattedStartTime, &exitCode);
1334 qCritical(
"[androidtestrunner] Device '%s' became unreachable during cleanup, "
1335 "uninstall skipped.", qPrintable(
g_options.serial));
1342 qWarning(
"Failed to uninstall test package '%s'. The test exit code is preserved.",
1346 if (
g_testInfo.isTestRunnerInterrupted.load()) {
1347 qCritical() <<
"The androidtestrunner was interrupted and the test was cleaned up.";