102 std::chrono::milliseconds timeout = std::chrono::milliseconds(-1))
104 const auto command = program +
" "_L1 + args.join(u' ');
107 fprintf(stdout,
"Execute %s.\n", command.toUtf8().constData());
110 process.start(program, args);
111 if (!process.waitForStarted()) {
112 qCritical(
"Cannot execute command %s.", qPrintable(command));
116 const bool finished = timeout.count() < 0
117 ? process.waitForFinished()
118 : process.waitForFinished(
static_cast<
int>(timeout.count()));
120 qCritical(
"Execution of command %s timed out.", qPrintable(command));
122 process.waitForFinished();
124 const QByteArray stdErr = process.readAllStandardError();
125 if (!stdErr.isEmpty())
126 qWarning().noquote() << stdErr.trimmed();
130 const auto stdOut = process.readAllStandardOutput();
131 const auto stdErr = process.readAllStandardError();
133 if (process.exitCode() != 0) {
135 if (verbose && !stdOut.isEmpty())
136 qWarning().noquote() << stdOut.trimmed();
137 if (!stdErr.isEmpty())
138 qWarning().noquote() << stdErr.trimmed();
143 fprintf(stdout,
"%s\n", stdOut.constData());
146 return stdOut.isNull() ? QByteArray(
"") : stdOut;
278 QCommandLineParser parser;
279 parser.setApplicationDescription(
280 "Runs a Qt for Android test on an emulator or a device. Specify a "
281 "device via --serial, ANDROID_SERIAL or ANDROID_DEVICE_SERIAL.\n"
284 " 0-127 QTest exit code (number of failed test functions)\n"
285 " 250 EXIT_DEVICE_GONE device disconnected\n"
286 " 251 EXIT_NORESULTS result files could not be pulled\n"
287 " 252 EXIT_ANR Android Not Responding\n"
288 " 253 EXIT_NOEXITCODE exit code could not be read from device\n"
289 " 254 EXIT_ERROR generic runner failure"_L1);
291 QCommandLineOption pathOpt(
292 "path"_L1,
"Path where the Android Gradle package is built."_L1,
"path"_L1);
293 QCommandLineOption makeOpt(
294 "make"_L1,
"make command to build the APK."_L1,
"command"_L1);
295 QCommandLineOption apkOpt(
296 "apk"_L1,
"Test APK path. Built via --make if absent."_L1,
"path"_L1);
297 QCommandLineOption aabOpt(
298 "aab"_L1,
"Test AAB path. Built via --make if absent."_L1,
"path"_L1);
299 QCommandLineOption bundletoolOpt(
300 "bundletool"_L1,
"Path to Android bundletool jar."_L1,
"path"_L1);
301 QCommandLineOption manifestOpt(
302 "manifest"_L1,
"Custom AndroidManifest.xml path."_L1,
"path"_L1);
303 QCommandLineOption adbOpt(
304 "adb"_L1,
"Path to adb. Falls back to $PATH."_L1,
"command"_L1,
"adb"_L1);
305 QCommandLineOption serialOpt(
307 "Android device serial. Overrides ANDROID_SERIAL / ANDROID_DEVICE_SERIAL."_L1,
309 QCommandLineOption activityOpt(
310 "activity"_L1,
"Activity to run. Defaults to the first in the manifest."_L1,
"name"_L1);
311 QCommandLineOption timeoutOpt(
312 "timeout"_L1,
"Test timeout in seconds (default 600)."_L1,
"seconds"_L1,
"600"_L1);
313 QCommandLineOption preTestAdbOpt(
"pre-test-adb-command"_L1,
314 "adb command to run after install, before test."_L1,
"command"_L1);
315 QCommandLineOption skipInstallRootOpt(
316 "skip-install-root"_L1,
317 "Don't append INSTALL_ROOT=... to --make. Only honored for make-family "
318 "tools (make, gmake, nmake, mingw32-make, jom)."_L1);
319 QCommandLineOption ndkStackOpt(
320 "ndk-stack"_L1,
"Path to ndk-stack (default: ndk-stack under $ANDROID_NDK_ROOT "
321 "or $ANDROID_NDK_HOME)."_L1,
"path"_L1);
322 QCommandLineOption showLogcatOpt(
323 "show-logcat"_L1,
"Print logcat output (+ system_server on ANR)."_L1);
324 QCommandLineOption verboseOpt(
325 "verbose"_L1,
"Print extra information."_L1);
326 QCommandLineOption helpOpt(
"help"_L1,
"Show this help message."_L1);
328 const QList<QCommandLineOption> ourOptions = {
329 pathOpt, makeOpt, apkOpt, aabOpt,
330 bundletoolOpt, manifestOpt,
332 activityOpt, timeoutOpt, preTestAdbOpt,
334 ndkStackOpt, showLogcatOpt, verboseOpt,
337 parser.addOptions(ourOptions);
338 parser.addPositionalArgument(
"testargs"_L1,
"Arguments forwarded to the test."_L1,
341 QSet<QString> knownOpts;
342 QSet<QString> valueOpts;
343 for (
const QCommandLineOption &opt : ourOptions) {
344 const bool takesValue = !opt.valueName().isEmpty();
345 for (
const QString &name : opt.names()) {
346 knownOpts.insert(name);
348 valueOpts.insert(name);
352 const QStringList processedArgs =
353 splitOwnAndTestArgs(QCoreApplication::arguments(), knownOpts, valueOpts);
354 if (processedArgs.isEmpty())
356 if (!parser.parse(processedArgs)) {
357 qCritical(
"%s", qPrintable(parser.errorText()));
360 if (parser.isSet(helpOpt))
363 g_options.buildPath = parser.value(pathOpt);
364 g_options.makeCommand = parser.value(makeOpt);
365 if (!collectPackagePaths(parser.values(apkOpt), parser.values(aabOpt)))
367 g_options.adbCommand = parser.value(adbOpt);
368 g_options.activity = parser.value(activityOpt);
369 g_options.serial = parser.value(serialOpt);
370 bool timeoutOk =
false;
373 qCritical(
"--timeout must be a positive integer (got '%s').",
374 qPrintable(parser.value(timeoutOpt)));
379 g_options.ndkStackPath = parser.value(ndkStackOpt);
381 g_options.manifestPath = parser.value(manifestOpt);
382 g_options.bundletoolPath = parser.value(bundletoolOpt);
384 for (
const QString &cmd : parser.values(preTestAdbOpt))
385 g_options.preTestRunAdbCommands += QProcess::splitCommand(cmd);
386 g_options.testArgsList = parser.positionalArguments();
388 static const QStringList makeNames = {
389 "make"_L1,
"gmake"_L1,
"nmake"_L1,
"mingw32-make"_L1,
"jom"_L1,
391 const QStringList makeParts = QProcess::splitCommand(g_options.makeCommand);
392 const QString makeBaseName = QFileInfo(makeParts.value(0)).baseName();
393 const bool makeIsGnuMakeFamily = makeNames.contains(makeBaseName, Qt::CaseInsensitive);
395 g_options.makeCommand =
"%1 INSTALL_ROOT=%2 install"_L1
396 .arg(g_options.makeCommand, QDir::toNativeSeparators(g_options.buildPath));
400 qCritical(
"--path and --apk (or --aab) are required.");
401 fputs(qPrintable(parser.helpText()), stderr);
405 if (!
g_options.packagePath.endsWith(
".apk"_L1)
406 && !
g_options.packagePath.endsWith(
".aab"_L1)) {
407 qCritical(
"Package path '%s' must end with .apk or .aab.",
412 if (
g_options.packagePath.endsWith(
".aab"_L1)) {
413 if (
g_options.bundletoolPath.isEmpty()) {
414 qCritical(
"--aab requires --bundletool to locate the bundletool jar.");
417 if (!QFile::exists(g_options.bundletoolPath)) {
418 qCritical(
"--bundletool path '%s' does not exist.",
425 g_options.serial = qEnvironmentVariable(
"ANDROID_SERIAL");
427 g_options.serial = qEnvironmentVariable(
"ANDROID_DEVICE_SERIAL");
429 if (g_options.ndkStackPath.isEmpty())
430 g_options.ndkStackPath = ndkStackPath();
467 QString gradlew = androidBuildDir +
"/gradlew.bat"_L1;
469 QString gradlew = androidBuildDir +
"/gradlew"_L1;
471 if (!QFile::exists(gradlew))
474 const QString scriptPath = gradleInitScriptPath();
475 if (scriptPath.isEmpty())
479 process.setWorkingDirectory(androidBuildDir);
480 process.start(gradlew, {
"-q"_L1,
"--init-script"_L1, scriptPath,
481 "-Pproperty="_L1 + property,
"printProjectProperty"_L1 });
483 if (!process.waitForFinished()) {
484 qWarning(
"gradlew '%s' timed out.", qPrintable(property));
486 process.waitForFinished();
489 if (process.exitCode() != 0) {
490 qWarning(
"gradlew '%s' exited with %d:\n%s", qPrintable(property),
491 process.exitCode(), process.readAllStandardError().constData());
494 return QString::fromUtf8(process.readAllStandardOutput()).trimmed();
500 if (!QFile::exists(g_options.manifestPath)) {
501 qCritical(
"--manifest path '%s' does not exist.",
506 const QStringList candidates = {
507 g_options.buildPath +
"/AndroidManifest.xml"_L1,
508 g_options.buildPath +
"/app/AndroidManifest.xml"_L1
510 for (
const QString &candidate : candidates) {
511 if (QFile::exists(candidate)) {
512 g_options.manifestPath = candidate;
518 qCritical(
"Unable to find AndroidManifest.xml at '%s'.", qPrintable(
g_options.buildPath));
521 QFile androidManifestXml(
g_options.manifestPath);
522 if (!androidManifestXml.open(QIODevice::ReadOnly)) {
523 qCritical(
"Unable to read android manifest '%s'", qPrintable(
g_options.manifestPath));
527 QXmlStreamReader reader(&androidManifestXml);
528 while (!reader.atEnd()) {
530 if (!reader.isStartElement())
533 if (reader.name() ==
"activity"_L1 && g_options.activity.isEmpty())
534 g_options.activity = reader.attributes().value(
"android:name"_L1).toString();
535 else if (reader.name() ==
"uses-permission"_L1)
536 g_options.permissions.append(reader.attributes().value(
"android:name"_L1).toString());
610 QRegularExpression oldFormats{
"^-(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
611 QRegularExpression newLoggingFormat{
"^(.*),(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
615 QStringList unhandledArgs;
616 for (
int i = 0; i <
g_options.testArgsList.size(); ++i) {
617 const QString &arg =
g_options.testArgsList[i].trimmed();
620 if (arg ==
"-o"_L1 || arg ==
"--output"_L1) {
621 if (i >=
g_options.testArgsList.size() - 1)
624 const auto &filePath =
g_options.testArgsList[++i];
625 const auto match = newLoggingFormat.match(filePath);
626 if (!match.hasMatch()) {
629 const auto capturedTexts = match.capturedTexts();
630 setOutputFile(capturedTexts.at(1), capturedTexts.at(2));
633 auto match = oldFormats.match(arg);
634 if (match.hasMatch()) {
635 logType = match.capturedTexts().at(1);
639 QString quotedArg = QString(arg).replace(
"\""_L1,
"\\\"\\\"\\\""_L1);
641 unhandledArgs <<
" \\\"%1\\\""_L1.arg(quotedArg);
645 if (
g_options.outFiles.isEmpty() || !file.isEmpty() || !logType.isEmpty())
646 setOutputFile(file, logType);
649 for (
auto it = g_options.outFiles.constBegin(); it != g_options.outFiles.constEnd(); ++it)
650 testAppArgs +=
"-o %1,%2 "_L1.arg(deviceOutputFileName(it.key(), it.value()), it.key());
652 testAppArgs += unhandledArgs.join(u' ').trimmed();
653 testAppArgs =
"\"%1\""_L1.arg(testAppArgs.trimmed());
654 const QString activityName =
"%1/%2"_L1.arg(g_options.package).arg(g_options.activity);
657 QStringList testEnvVarArgs;
658 const QStringList envVarsList = QProcessEnvironment::systemEnvironment().toStringList();
659 for (
const QString &var : envVarsList) {
660 if (!var.startsWith(
"QTEST_"_L1) && !var.startsWith(
"QT_"_L1))
662 const qsizetype index = var.indexOf(u'=');
665 const QString key = var.left(index);
666 QString escapedValue = var.mid(index + 1);
667 escapedValue.replace(
"'"_L1,
"'\\''"_L1);
668 const QString value =
"'%1'"_L1.arg(escapedValue);
669 testEnvVarArgs <<
"-e"_L1 << (
"extraenvvars_"_L1 + key) << value;
672 g_options.amStarttestArgs = {
"shell"_L1,
"am"_L1,
"start"_L1,
"-W"_L1,
673 "-n"_L1, activityName,
674 "-e"_L1,
"applicationArguments"_L1, testAppArgs };
675 g_options.amStarttestArgs.append(testEnvVarArgs);
941 if (logcat.isEmpty())
944 QByteArray crashLogcat(logcat);
946 qWarning() <<
"Warning: ndk-stack path not provided and couldn't be deduced "
947 "using the ANDROID_NDK_ROOT or ANDROID_NDK_HOME environment "
949 }
else if (
const QString libsPath = getAbiLibsPath(); libsPath.isEmpty()) {
950 qWarning() <<
"Warning: could not determine the device ABI, "
951 "skipping ndk-stack and printing the raw dump.";
953 QProcess ndkStackProc;
954 ndkStackProc.start(
g_options.ndkStackPath, {
"-sym"_L1, libsPath });
956 if (ndkStackProc.waitForStarted()) {
957 ndkStackProc.write(crashLogcat);
958 ndkStackProc.closeWriteChannel();
961 if (ndkStackProc.waitForFinished()) {
963 const QByteArray ndkOutput = ndkStackProc.readAllStandardOutput();
964 if (!ndkOutput.trimmed().isEmpty())
965 crashLogcat = ndkOutput;
967 qCritical() <<
"Error: ndk-stack command timed out.";
969 ndkStackProc.waitForFinished();
972 qCritical() <<
"Error: failed to run ndk-stack command.";
976 if (crashLogcat.startsWith(
"********** Crash dump")) {
977 qDebug().noquote() << crashLogcat.trimmed();
979 qDebug() <<
"[androidtestrunner] ********** BEGIN crash dump **********";
980 qDebug().noquote() << crashLogcat.trimmed();
981 qDebug() <<
"[androidtestrunner] ********** END crash dump **********";
1051 static const QRegularExpression logcatRegEx{
1052 "(?:^\\x1B\\[[0-9;]*m)?"
1055 "(\\(\\s*\\d*\\)):"
1058 "(?:\\x1B\\[[0-9;]*m)?"
1061 QByteArrayList kept;
1062 for (
const QByteArray &line : logcat.split(u'\n')) {
1063 QRegularExpressionMatch match = logcatRegEx.match(QString::fromUtf8(line));
1064 if (!match.hasMatch()) {
1069 const QString msgType = match.captured(1);
1070 const QString pidStr = match.captured(2);
1071 const int capturedPid = pidStr.mid(1, pidStr.size() - 2).trimmed().toInt();
1072 const bool isFatal = msgType == u'F';
1073 const bool isOurTest = capturedPid == testPid;
1074 const bool isAnrSource = systemServerPid > 0 && capturedPid == systemServerPid;
1075 if (isOurTest || isFatal || isAnrSource)
1078 return kept.join(
'\n');
1084 && !
g_testInfo.isTestRunnerInterrupted.load();
1085 QByteArray logcat = fetchLogcat(timeStamp, wasAbnormal);
1086 if (logcat.isEmpty()) {
1088 qWarning() <<
"The retrieved logcat is empty";
1092 const QByteArray crashDump = takeCrashDump(&logcat);
1094 const bool anrOccurred = logcat.contains(
1095 "ANR in %1"_L1.arg(g_options.package).toUtf8());
1101 qCritical(
"[androidtestrunner] An ANR has occurred while running the test '%s';"
1102 " consult logcat for additional logs from the system_server process",
1106 const int systemServerPid = anrOccurred ? getPid(
"system_server"_L1) : -1;
1107 const QByteArray filtered = filterTestLogcat(logcat, g_testInfo.pid, systemServerPid);
1112 && !
g_testInfo.isTestRunnerInterrupted.load();
1114 qDebug() <<
"[androidtestrunner] ********** BEGIN logcat dump **********";
1115 qDebug().noquote() << filtered.trimmed();
1116 qDebug() <<
"[androidtestrunner] ********** END logcat dump **********";
1118 if (testCrashed && !crashDump.isEmpty())
1119 printLogcatCrash(crashDump);
1180 using namespace std::chrono_literals;
1184 QCoreApplication a(argc, argv);
1189 qCritical() <<
"It is required to provide a make command with the \"--make\" parameter "
1190 "to generate the apk.";
1194 if (execCommand(
g_options.makeCommand,
true, g_options.timeoutSecs * 1s).isNull()) {
1195 qCritical(
"The APK build command \"%s\" failed.", qPrintable(
g_options.makeCommand));
1199 if (!QFile::exists(g_options.packagePath)) {
1200 qCritical(
"No package \"%s\" found after running the make command. "
1201 "Check the provided path and the make command.",
1206 const std::optional<QStringList> devices = runningDevices();
1208 qCritical(
"Failed to query connected devices via 'adb devices'.");
1210 }
else if (devices->isEmpty()) {
1211 qCritical(
"No connected devices or running emulators can be found.");
1214 qCritical(
"No connected device or running emulator with serial '%s' can be found.",
1217 }
else if (
g_options.serial.isEmpty() && devices->size() == 1) {
1219 }
else if (
g_options.serial.isEmpty()) {
1220 qCritical(
"Multiple devices connected, set ANDROID_SERIAL or ANDROID_DEVICE_SERIAL.");
1226 g_testInfo.userId = userId();
1231 const QString ns = getGradleProjectProperty(g_options.buildPath,
"android.namespace"_L1);
1236 qCritical(
"Unable to get package name for '%s'", qPrintable(
g_options.packagePath));
1245 const QString user = qEnvironmentVariable(
"USER",
1246 qEnvironmentVariable(
"USERNAME", u"default"_s));
1247 const QString lockName = u"androidtestrunner-%1-%2.lock"_s.arg(user, g_options.serial);
1248 const QDir tempDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation));
1249 QLockFile testRunnerLock(tempDir.absoluteFilePath(lockName));
1250 testRunnerLock.setStaleLockTime(0);
1251 if (!testRunnerLock.lock()) {
1252 qCritical(
"Failed to acquire test runner lock for '%s'.", qPrintable(
g_options.serial));
1256 if (
g_options.packagePath.endsWith(
".apk"_L1)) {
1257 const QStringList installArgs = {
"install"_L1,
"-r"_L1, g_options.packagePath };
1258 if (execAdbCommand(installArgs).isNull())
1260 }
else if (
g_options.packagePath.endsWith(
".aab"_L1)) {
1262 const auto apksFilePath = aab.absoluteDir().absoluteFilePath(aab.baseName() +
".apks"_L1);
1263 QStringList installApksArgs = {
"install-apks"_L1,
"--apks"_L1, apksFilePath };
1264 if (!g_options.serial.isEmpty())
1265 installApksArgs <<
"--device-id"_L1 << g_options.serial;
1266 if (execBundletoolCommand({
"build-apks"_L1,
"--bundle"_L1, g_options.packagePath,
1267 "--output"_L1, apksFilePath,
"--local-testing"_L1,
1268 "--overwrite"_L1 }).isNull()
1269 || execBundletoolCommand(installApksArgs).isNull())
1274 const QStringList dangerousPermissions = queryDangerousPermissions();
1275 for (
const auto &permission : g_options.permissions) {
1276 if (!dangerousPermissions.contains(permission))
1279 if (execAdbCommand({
"shell"_L1,
"pm"_L1,
"grant"_L1,
"--user"_L1, g_testInfo.userId,
1280 g_options.package, permission }).isNull()) {
1281 qWarning(
"Unable to grant '%s' to '%s'. Probably the Android version mismatch.",
1282 qPrintable(permission), qPrintable(g_options.package));
1287 for (
const auto &command : g_options.preTestRunAdbCommands) {
1288 if (execAdbCommand(command).isNull()) {
1289 qCritical(
"The pre test ADB command \"%s\" failed.",
1290 qUtf8Printable(command.join(u' ')));
1296 const QString formattedStartTime = getCurrentTimeString();
1299 if (execAdbCommand(
g_options.amStarttestArgs).isNull()) {
1303 analyseLogcat(formattedStartTime, &exitCode);
1310 qWarning(
"Continuing without live stdout streaming; result files are still pulled.");
1315 qCritical(
"[androidtestrunner] Device '%s' became unreachable during the test, "
1316 "result transfer and uninstall skipped.", qPrintable(
g_options.serial));
1328 analyseLogcat(formattedStartTime, &exitCode);
1332 qCritical(
"[androidtestrunner] Device '%s' became unreachable during cleanup, "
1333 "uninstall skipped.", qPrintable(
g_options.serial));
1340 qWarning(
"Failed to uninstall test package '%s'. The test exit code is preserved.",
1344 if (
g_testInfo.isTestRunnerInterrupted.load()) {
1345 qCritical() <<
"The androidtestrunner was interrupted and the test was cleaned up.";