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;
258 QCommandLineParser parser;
259 parser.setApplicationDescription(
260 "Runs a Qt for Android test on an emulator or a device. Specify a "
261 "device via --serial, ANDROID_SERIAL or ANDROID_DEVICE_SERIAL.\n"
264 " 0-127 QTest exit code (number of failed test functions)\n"
265 " 250 EXIT_DEVICE_GONE device disconnected\n"
266 " 251 EXIT_NORESULTS result files could not be pulled\n"
267 " 252 EXIT_ANR Android Not Responding\n"
268 " 253 EXIT_NOEXITCODE exit code could not be read from device\n"
269 " 254 EXIT_ERROR generic runner failure"_L1);
271 QCommandLineOption pathOpt(
272 "path"_L1,
"Path where the Android Gradle package is built."_L1,
"path"_L1);
273 QCommandLineOption makeOpt(
274 "make"_L1,
"make command to build the APK."_L1,
"command"_L1);
275 QCommandLineOption apkOpt(
276 "apk"_L1,
"Test APK path. Built via --make if absent."_L1,
"path"_L1);
277 QCommandLineOption aabOpt(
278 "aab"_L1,
"Test AAB path. Built via --make if absent."_L1,
"path"_L1);
279 QCommandLineOption bundletoolOpt(
280 "bundletool"_L1,
"Path to Android bundletool jar."_L1,
"path"_L1);
281 QCommandLineOption manifestOpt(
282 "manifest"_L1,
"Custom AndroidManifest.xml path."_L1,
"path"_L1);
283 QCommandLineOption adbOpt(
284 "adb"_L1,
"Path to adb. Falls back to $PATH."_L1,
"command"_L1,
"adb"_L1);
285 QCommandLineOption serialOpt(
287 "Android device serial. Overrides ANDROID_SERIAL / ANDROID_DEVICE_SERIAL."_L1,
289 QCommandLineOption activityOpt(
290 "activity"_L1,
"Activity to run. Defaults to the first in the manifest."_L1,
"name"_L1);
291 QCommandLineOption timeoutOpt(
292 "timeout"_L1,
"Test timeout in seconds (default 600)."_L1,
"seconds"_L1,
"600"_L1);
293 QCommandLineOption preTestAdbOpt(
"pre-test-adb-command"_L1,
294 "adb command to run after install, before test."_L1,
"command"_L1);
295 QCommandLineOption skipInstallRootOpt(
296 "skip-install-root"_L1,
297 "Don't append INSTALL_ROOT=... to --make. Only honored for make-family "
298 "tools (make, gmake, nmake, mingw32-make, jom)."_L1);
299 QCommandLineOption ndkStackOpt(
300 "ndk-stack"_L1,
"Path to ndk-stack (default: $ANDROID_NDK_ROOT/ndk-stack)."_L1,
"path"_L1);
301 QCommandLineOption showLogcatOpt(
302 "show-logcat"_L1,
"Print logcat output (+ system_server on ANR)."_L1);
303 QCommandLineOption verboseOpt(
304 "verbose"_L1,
"Print extra information."_L1);
305 QCommandLineOption helpOpt(
"help"_L1,
"Show this help message."_L1);
307 const QList<QCommandLineOption> ourOptions = {
308 pathOpt, makeOpt, apkOpt, aabOpt,
309 bundletoolOpt, manifestOpt,
311 activityOpt, timeoutOpt, preTestAdbOpt,
313 ndkStackOpt, showLogcatOpt, verboseOpt,
316 parser.addOptions(ourOptions);
317 parser.addPositionalArgument(
"testargs"_L1,
"Arguments forwarded to the test."_L1,
320 QSet<QString> knownOpts;
321 QSet<QString> valueOpts;
322 for (
const QCommandLineOption &opt : ourOptions) {
323 const bool takesValue = !opt.valueName().isEmpty();
324 for (
const QString &name : opt.names()) {
325 knownOpts.insert(name);
327 valueOpts.insert(name);
331 const QStringList processedArgs =
332 splitOwnAndTestArgs(QCoreApplication::arguments(), knownOpts, valueOpts);
333 if (processedArgs.isEmpty())
335 if (!parser.parse(processedArgs)) {
336 qCritical(
"%s", qPrintable(parser.errorText()));
339 if (parser.isSet(helpOpt))
342 g_options.buildPath = parser.value(pathOpt);
343 g_options.makeCommand = parser.value(makeOpt);
344 if (!collectPackagePaths(parser.values(apkOpt), parser.values(aabOpt)))
346 g_options.adbCommand = parser.value(adbOpt);
347 g_options.activity = parser.value(activityOpt);
348 g_options.serial = parser.value(serialOpt);
349 bool timeoutOk =
false;
352 qCritical(
"--timeout must be a positive integer (got '%s').",
353 qPrintable(parser.value(timeoutOpt)));
358 g_options.ndkStackPath = parser.value(ndkStackOpt);
360 g_options.manifestPath = parser.value(manifestOpt);
361 g_options.bundletoolPath = parser.value(bundletoolOpt);
363 for (
const QString &cmd : parser.values(preTestAdbOpt))
364 g_options.preTestRunAdbCommands += QProcess::splitCommand(cmd);
365 g_options.testArgsList = parser.positionalArguments();
367 static const QStringList makeNames = {
368 "make"_L1,
"gmake"_L1,
"nmake"_L1,
"mingw32-make"_L1,
"jom"_L1,
370 const QStringList makeParts = QProcess::splitCommand(g_options.makeCommand);
371 const QString makeBaseName = QFileInfo(makeParts.value(0)).baseName();
372 const bool makeIsGnuMakeFamily = makeNames.contains(makeBaseName, Qt::CaseInsensitive);
374 g_options.makeCommand =
"%1 INSTALL_ROOT=%2 install"_L1
375 .arg(g_options.makeCommand, QDir::toNativeSeparators(g_options.buildPath));
379 qCritical(
"--path and --apk (or --aab) are required.");
380 fputs(qPrintable(parser.helpText()), stderr);
384 if (!
g_options.packagePath.endsWith(
".apk"_L1)
385 && !
g_options.packagePath.endsWith(
".aab"_L1)) {
386 qCritical(
"Package path '%s' must end with .apk or .aab.",
391 if (
g_options.packagePath.endsWith(
".aab"_L1)) {
392 if (
g_options.bundletoolPath.isEmpty()) {
393 qCritical(
"--aab requires --bundletool to locate the bundletool jar.");
396 if (!QFile::exists(g_options.bundletoolPath)) {
397 qCritical(
"--bundletool path '%s' does not exist.",
404 g_options.serial = qEnvironmentVariable(
"ANDROID_SERIAL");
406 g_options.serial = qEnvironmentVariable(
"ANDROID_DEVICE_SERIAL");
409 const QString ndkPath = qEnvironmentVariable(
"ANDROID_NDK_ROOT");
411 const QStringList candidates = {
"ndk-stack.cmd"_L1,
"ndk-stack.bat"_L1,
"ndk-stack"_L1 };
413 const QStringList candidates = {
"ndk-stack"_L1 };
415 for (
const QString &name : candidates) {
416 const QString path = ndkPath + QDir::separator() + name;
417 if (QFile::exists(path)) {
418 g_options.ndkStackPath = path;
459 QString gradlew = androidBuildDir +
"/gradlew.bat"_L1;
461 QString gradlew = androidBuildDir +
"/gradlew"_L1;
463 if (!QFile::exists(gradlew))
466 const QString scriptPath = gradleInitScriptPath();
467 if (scriptPath.isEmpty())
471 process.setWorkingDirectory(androidBuildDir);
472 process.start(gradlew, {
"-q"_L1,
"--init-script"_L1, scriptPath,
473 "-Pproperty="_L1 + property,
"printProjectProperty"_L1 });
475 if (!process.waitForFinished()) {
476 qWarning(
"gradlew '%s' timed out.", qPrintable(property));
478 process.waitForFinished();
481 if (process.exitCode() != 0) {
482 qWarning(
"gradlew '%s' exited with %d:\n%s", qPrintable(property),
483 process.exitCode(), process.readAllStandardError().constData());
486 return QString::fromUtf8(process.readAllStandardOutput()).trimmed();
492 if (!QFile::exists(g_options.manifestPath)) {
493 qCritical(
"--manifest path '%s' does not exist.",
498 const QStringList candidates = {
499 g_options.buildPath +
"/AndroidManifest.xml"_L1,
500 g_options.buildPath +
"/app/AndroidManifest.xml"_L1
502 for (
const QString &candidate : candidates) {
503 if (QFile::exists(candidate)) {
504 g_options.manifestPath = candidate;
510 qCritical(
"Unable to find AndroidManifest.xml at '%s'.", qPrintable(
g_options.buildPath));
513 QFile androidManifestXml(
g_options.manifestPath);
514 if (!androidManifestXml.open(QIODevice::ReadOnly)) {
515 qCritical(
"Unable to read android manifest '%s'", qPrintable(
g_options.manifestPath));
519 QXmlStreamReader reader(&androidManifestXml);
520 while (!reader.atEnd()) {
522 if (!reader.isStartElement())
525 if (reader.name() ==
"activity"_L1 && g_options.activity.isEmpty())
526 g_options.activity = reader.attributes().value(
"android:name"_L1).toString();
527 else if (reader.name() ==
"uses-permission"_L1)
528 g_options.permissions.append(reader.attributes().value(
"android:name"_L1).toString());
602 QRegularExpression oldFormats{
"^-(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
603 QRegularExpression newLoggingFormat{
"^(.*),(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
607 QStringList unhandledArgs;
608 for (
int i = 0; i <
g_options.testArgsList.size(); ++i) {
609 const QString &arg =
g_options.testArgsList[i].trimmed();
612 if (arg ==
"-o"_L1 || arg ==
"--output"_L1) {
613 if (i >=
g_options.testArgsList.size() - 1)
616 const auto &filePath =
g_options.testArgsList[++i];
617 const auto match = newLoggingFormat.match(filePath);
618 if (!match.hasMatch()) {
621 const auto capturedTexts = match.capturedTexts();
622 setOutputFile(capturedTexts.at(1), capturedTexts.at(2));
625 auto match = oldFormats.match(arg);
626 if (match.hasMatch()) {
627 logType = match.capturedTexts().at(1);
631 QString quotedArg = QString(arg).replace(
"\""_L1,
"\\\"\\\"\\\""_L1);
633 unhandledArgs <<
" \\\"%1\\\""_L1.arg(quotedArg);
637 if (
g_options.outFiles.isEmpty() || !file.isEmpty() || !logType.isEmpty())
638 setOutputFile(file, logType);
641 for (
auto it = g_options.outFiles.constBegin(); it != g_options.outFiles.constEnd(); ++it)
642 testAppArgs +=
"-o %1,%2 "_L1.arg(deviceOutputFileName(it.key(), it.value()), it.key());
644 testAppArgs += unhandledArgs.join(u' ').trimmed();
645 testAppArgs =
"\"%1\""_L1.arg(testAppArgs.trimmed());
646 const QString activityName =
"%1/%2"_L1.arg(g_options.package).arg(g_options.activity);
649 QStringList testEnvVarArgs;
650 const QStringList envVarsList = QProcessEnvironment::systemEnvironment().toStringList();
651 for (
const QString &var : envVarsList) {
652 if (!var.startsWith(
"QTEST_"_L1) && !var.startsWith(
"QT_"_L1))
654 const qsizetype index = var.indexOf(u'=');
657 const QString key = var.left(index);
658 QString escapedValue = var.mid(index + 1);
659 escapedValue.replace(
"'"_L1,
"'\\''"_L1);
660 const QString value =
"'%1'"_L1.arg(escapedValue);
661 testEnvVarArgs <<
"-e"_L1 << (
"extraenvvars_"_L1 + key) << value;
664 g_options.amStarttestArgs = {
"shell"_L1,
"am"_L1,
"start"_L1,
"-W"_L1,
665 "-n"_L1, activityName,
666 "-e"_L1,
"applicationArguments"_L1, testAppArgs };
667 g_options.amStarttestArgs.append(testEnvVarArgs);
937 if (logcat.isEmpty())
940 QByteArray crashLogcat(logcat);
942 qWarning() <<
"Warning: ndk-stack path not provided and couldn't be deduced "
943 "using the ANDROID_NDK_ROOT environment variable.";
944 }
else if (
const QString libsPath = getAbiLibsPath(); libsPath.isEmpty()) {
945 qWarning() <<
"Warning: could not determine the device ABI, "
946 "skipping ndk-stack and printing the raw dump.";
948 QProcess ndkStackProc;
949 ndkStackProc.start(
g_options.ndkStackPath, {
"-sym"_L1, libsPath });
951 if (ndkStackProc.waitForStarted()) {
952 ndkStackProc.write(crashLogcat);
953 ndkStackProc.closeWriteChannel();
956 if (ndkStackProc.waitForFinished()) {
958 const QByteArray ndkOutput = ndkStackProc.readAllStandardOutput();
959 if (!ndkOutput.trimmed().isEmpty())
960 crashLogcat = ndkOutput;
962 qCritical() <<
"Error: ndk-stack command timed out.";
964 ndkStackProc.waitForFinished();
967 qCritical() <<
"Error: failed to run ndk-stack command.";
971 if (crashLogcat.startsWith(
"********** Crash dump")) {
972 qDebug().noquote() << crashLogcat.trimmed();
974 qDebug() <<
"[androidtestrunner] ********** BEGIN crash dump **********";
975 qDebug().noquote() << crashLogcat.trimmed();
976 qDebug() <<
"[androidtestrunner] ********** END crash dump **********";
1175 using namespace std::chrono_literals;
1179 QCoreApplication a(argc, argv);
1184 qCritical() <<
"It is required to provide a make command with the \"--make\" parameter "
1185 "to generate the apk.";
1189 if (execCommand(
g_options.makeCommand,
true, g_options.timeoutSecs * 1s).isNull()) {
1190 qCritical(
"The APK build command \"%s\" failed.", qPrintable(
g_options.makeCommand));
1194 if (!QFile::exists(g_options.packagePath)) {
1195 qCritical(
"No package \"%s\" found after running the make command. "
1196 "Check the provided path and the make command.",
1201 const std::optional<QStringList> devices = runningDevices();
1203 qCritical(
"Failed to query connected devices via 'adb devices'.");
1205 }
else if (devices->isEmpty()) {
1206 qCritical(
"No connected devices or running emulators can be found.");
1209 qCritical(
"No connected device or running emulator with serial '%s' can be found.",
1212 }
else if (
g_options.serial.isEmpty() && devices->size() == 1) {
1214 }
else if (
g_options.serial.isEmpty()) {
1215 qCritical(
"Multiple devices connected, set ANDROID_SERIAL or ANDROID_DEVICE_SERIAL.");
1221 g_testInfo.userId = userId();
1226 const QString ns = getGradleProjectProperty(g_options.buildPath,
"android.namespace"_L1);
1231 qCritical(
"Unable to get package name for '%s'", qPrintable(
g_options.packagePath));
1240 const QString user = qEnvironmentVariable(
"USER",
1241 qEnvironmentVariable(
"USERNAME", u"default"_s));
1242 const QString lockName = u"androidtestrunner-%1-%2.lock"_s.arg(user, g_options.serial);
1243 const QDir tempDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation));
1244 QLockFile testRunnerLock(tempDir.absoluteFilePath(lockName));
1245 testRunnerLock.setStaleLockTime(0);
1246 if (!testRunnerLock.lock()) {
1247 qCritical(
"Failed to acquire test runner lock for '%s'.", qPrintable(
g_options.serial));
1251 if (
g_options.packagePath.endsWith(
".apk"_L1)) {
1252 const QStringList installArgs = {
"install"_L1,
"-r"_L1, g_options.packagePath };
1253 if (execAdbCommand(installArgs).isNull())
1255 }
else if (
g_options.packagePath.endsWith(
".aab"_L1)) {
1257 const auto apksFilePath = aab.absoluteDir().absoluteFilePath(aab.baseName() +
".apks"_L1);
1258 QStringList installApksArgs = {
"install-apks"_L1,
"--apks"_L1, apksFilePath };
1259 if (!g_options.serial.isEmpty())
1260 installApksArgs <<
"--device-id"_L1 << g_options.serial;
1261 if (execBundletoolCommand({
"build-apks"_L1,
"--bundle"_L1, g_options.packagePath,
1262 "--output"_L1, apksFilePath,
"--local-testing"_L1,
1263 "--overwrite"_L1 }).isNull()
1264 || execBundletoolCommand(installApksArgs).isNull())
1269 const QStringList dangerousPermissions = queryDangerousPermissions();
1270 for (
const auto &permission : g_options.permissions) {
1271 if (!dangerousPermissions.contains(permission))
1274 if (execAdbCommand({
"shell"_L1,
"pm"_L1,
"grant"_L1,
"--user"_L1, g_testInfo.userId,
1275 g_options.package, permission }).isNull()) {
1276 qWarning(
"Unable to grant '%s' to '%s'. Probably the Android version mismatch.",
1277 qPrintable(permission), qPrintable(g_options.package));
1282 for (
const auto &command : g_options.preTestRunAdbCommands) {
1283 if (execAdbCommand(command).isNull()) {
1284 qCritical(
"The pre test ADB command \"%s\" failed.",
1285 qUtf8Printable(command.join(u' ')));
1291 const QString formattedStartTime = getCurrentTimeString();
1294 if (execAdbCommand(
g_options.amStarttestArgs).isNull()) {
1298 analyseLogcat(formattedStartTime, &exitCode);
1305 qWarning(
"Continuing without live stdout streaming; result files are still pulled.");
1310 qCritical(
"[androidtestrunner] Device '%s' became unreachable during the test, "
1311 "result transfer and uninstall skipped.", qPrintable(
g_options.serial));
1323 analyseLogcat(formattedStartTime, &exitCode);
1327 qCritical(
"[androidtestrunner] Device '%s' became unreachable during cleanup, "
1328 "uninstall skipped.", qPrintable(
g_options.serial));
1335 qWarning(
"Failed to uninstall test package '%s'. The test exit code is preserved.",
1339 if (
g_testInfo.isTestRunnerInterrupted.load()) {
1340 qCritical() <<
"The androidtestrunner was interrupted and the test was cleaned up.";