127 if (g_options.serial.isEmpty())
128 return execCommand(g_options.adbCommand, args, output, verbose);
130 QStringList argsWithSerial = {
"-s"_L1, g_options.serial};
131 argsWithSerial.append(args);
133 return execCommand(g_options.adbCommand, argsWithSerial, output, verbose);
164 QStringList arguments = QCoreApplication::arguments();
166 for (; i < arguments.size(); ++i) {
167 const QString &argument = arguments.at(i);
168 if (argument.compare(
"--adb"_L1, Qt::CaseInsensitive) == 0) {
169 if (i + 1 == arguments.size())
172 g_options.adbCommand = arguments.at(++i);
173 }
else if (argument.compare(
"--bundletool"_L1, Qt::CaseInsensitive) == 0) {
174 if (i + 1 == arguments.size())
177 g_options.bundletoolPath = arguments.at(++i);
178 }
else if (argument.compare(
"--path"_L1, Qt::CaseInsensitive) == 0) {
179 if (i + 1 == arguments.size())
183 }
else if (argument.compare(
"--manifest"_L1, Qt::CaseInsensitive) == 0) {
184 if (i + 1 == arguments.size())
187 g_options.manifestPath = arguments.at(++i);
188 }
else if (argument.compare(
"--make"_L1, Qt::CaseInsensitive) == 0) {
189 if (i + 1 == arguments.size())
192 g_options.makeCommand = arguments.at(++i);
193 }
else if (argument.compare(
"--apk"_L1, Qt::CaseInsensitive) == 0) {
194 if (i + 1 == arguments.size())
197 setPackagePath(arguments.at(++i));
198 }
else if (argument.compare(
"--aab"_L1, Qt::CaseInsensitive) == 0) {
199 if (i + 1 == arguments.size())
202 setPackagePath(arguments.at(++i));
203 }
else if (argument.compare(
"--activity"_L1, Qt::CaseInsensitive) == 0) {
204 if (i + 1 == arguments.size())
208 }
else if (argument.compare(
"--skip-install-root"_L1, Qt::CaseInsensitive) == 0) {
210 }
else if (argument.compare(
"--show-logcat"_L1, Qt::CaseInsensitive) == 0) {
212 }
else if (argument.compare(
"--ndk-stack"_L1, Qt::CaseInsensitive) == 0) {
213 if (i + 1 == arguments.size())
216 g_options.ndkStackPath = arguments.at(++i);
217 }
else if (argument.compare(
"--timeout"_L1, Qt::CaseInsensitive) == 0) {
218 if (i + 1 == arguments.size())
222 }
else if (argument.compare(
"--help"_L1, Qt::CaseInsensitive) == 0) {
224 }
else if (argument.compare(
"--verbose"_L1, Qt::CaseInsensitive) == 0) {
226 }
else if (argument.compare(
"--pre-test-adb-command"_L1, Qt::CaseInsensitive) == 0) {
227 if (i + 1 == arguments.size())
230 g_options.preTestRunAdbCommands += QProcess::splitCommand(arguments.at(++i));
232 }
else if (argument.compare(
"--"_L1, Qt::CaseInsensitive) == 0) {
236 g_options.testArgsList << arguments.at(i);
242 g_options.makeCommand =
"%1 INSTALL_ROOT=%2 install"_L1
243 .arg(g_options.makeCommand)
244 .arg(QDir::toNativeSeparators(g_options.buildPath));
247 for (;i < arguments.size(); ++i)
248 g_options.testArgsList << arguments.at(i);
253 g_options.serial = qEnvironmentVariable(
"ANDROID_SERIAL");
255 g_options.serial = qEnvironmentVariable(
"ANDROID_DEVICE_SERIAL");
258 const QString ndkPath = qEnvironmentVariable(
"ANDROID_NDK_ROOT");
259 const QString ndkStackPath = ndkPath + QDir::separator() +
"ndk-stack"_L1;
260 if (QFile::exists(ndkStackPath))
353 QString gradlew = androidBuildDir +
"/gradlew.bat"_L1;
355 QString gradlew = androidBuildDir +
"/gradlew"_L1;
357 if (!QFile::exists(gradlew))
360 const QString scriptPath = gradleInitScriptPath();
361 if (scriptPath.isEmpty())
365 process.setWorkingDirectory(androidBuildDir);
366 process.start(gradlew, {
"-q"_L1,
"--init-script"_L1, scriptPath,
367 "-Pproperty="_L1 + property,
"printProjectProperty"_L1 });
369 if (!process.waitForFinished(30000))
372 return QString::fromUtf8(process.readAllStandardOutput()).trimmed();
378 if (!QFile::exists(g_options.manifestPath)) {
379 qCritical(
"--manifest path '%s' does not exist.",
384 const QStringList candidates = {
385 g_options.buildPath +
"/AndroidManifest.xml"_L1,
386 g_options.buildPath +
"/app/AndroidManifest.xml"_L1
388 for (
const QString &candidate : candidates) {
389 if (QFile::exists(candidate)) {
390 g_options.manifestPath = candidate;
396 qCritical(
"Unable to find AndroidManifest.xml at '%s'.", qPrintable(
g_options.buildPath));
399 QFile androidManifestXml(
g_options.manifestPath);
400 if (!androidManifestXml.open(QIODevice::ReadOnly)) {
401 qCritical(
"Unable to read android manifest '%s'", qPrintable(
g_options.manifestPath));
405 QXmlStreamReader reader(&androidManifestXml);
406 while (!reader.atEnd()) {
408 if (!reader.isStartElement())
411 if (reader.name() ==
"activity"_L1 && g_options.activity.isEmpty())
412 g_options.activity = reader.attributes().value(
"android:name"_L1).toString();
413 else if (reader.name() ==
"uses-permission"_L1)
414 g_options.permissions.append(reader.attributes().value(
"android:name"_L1).toString());
422 const QStringList args({
"shell"_L1,
"dumpsys"_L1,
"package"_L1,
"permissions"_L1 });
423 if (!execAdbCommand(args, &output,
false)) {
424 qWarning(
"Failed to query permissions via dumpsys");
429
430
431
432
433
434
435
436
437 const static QRegularExpression regex(
"^\\s*Permission\\s+\\[([^\\]]+)\\]\\s+\\(([^)]+)\\):"_L1);
438 QStringList dangerousPermissions;
441 const QStringList lines = QString::fromUtf8(output).split(u'\n');
442 for (
const QString &line : lines) {
443 QRegularExpressionMatch match = regex.match(line);
444 if (match.hasMatch()) {
445 currentPerm = match.captured(1);
449 if (currentPerm.isEmpty())
452 int protIndex = line.indexOf(
"prot="_L1);
456 QString protectionTypes = line.mid(protIndex + 5).trimmed();
457 if (protectionTypes.contains(
"dangerous"_L1, Qt::CaseInsensitive)) {
458 dangerousPermissions.append(currentPerm);
463 return dangerousPermissions;
486 QRegularExpression oldFormats{
"^-(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
487 QRegularExpression newLoggingFormat{
"^(.*),(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
491 QStringList unhandledArgs;
492 for (
int i = 0; i <
g_options.testArgsList.size(); ++i) {
493 const QString &arg =
g_options.testArgsList[i].trimmed();
496 if (arg ==
"-o"_L1) {
497 if (i >=
g_options.testArgsList.size() - 1)
500 const auto &filePath =
g_options.testArgsList[++i];
501 const auto match = newLoggingFormat.match(filePath);
502 if (!match.hasMatch()) {
505 const auto capturedTexts = match.capturedTexts();
506 setOutputFile(capturedTexts.at(1), capturedTexts.at(2));
509 auto match = oldFormats.match(arg);
510 if (match.hasMatch()) {
511 logType = match.capturedTexts().at(1);
515 QString quotedArg = QString(arg).replace(
"\""_L1,
"\\\"\\\"\\\""_L1);
517 unhandledArgs <<
" \\\"%1\\\""_L1.arg(quotedArg);
521 if (
g_options.outFiles.isEmpty() || !file.isEmpty() || !logType.isEmpty())
522 setOutputFile(file, logType);
525 for (
auto it = g_options.outFiles.constBegin(); it != g_options.outFiles.constEnd(); ++it)
526 testAppArgs +=
"-o %1,%2 "_L1.arg(QFileInfo(it.value()).fileName(), it.key());
528 testAppArgs += unhandledArgs.join(u' ').trimmed();
529 testAppArgs =
"\"%1\""_L1.arg(testAppArgs.trimmed());
530 const QString activityName =
"%1/%2"_L1.arg(g_options.package).arg(g_options.activity);
533 QStringList testEnvVarArgs;
534 const QStringList envVarsList = QProcessEnvironment::systemEnvironment().toStringList();
535 for (
const QString &var : envVarsList) {
536 if (!var.startsWith(
"QTEST_"_L1) && !var.startsWith(
"QT_"_L1))
538 const qsizetype index = var.indexOf(u'=');
541 const QString key = var.left(index);
542 QString escapedValue = var.mid(index + 1);
543 escapedValue.replace(
"'"_L1,
"'\\''"_L1);
544 const QString value =
"'%1'"_L1.arg(escapedValue);
545 testEnvVarArgs <<
"-e"_L1 << (
"extraenvvars_"_L1 + key) << value;
548 g_options.amStarttestArgs = {
"shell"_L1,
"am"_L1,
"start"_L1,
549 "-n"_L1, activityName,
550 "-e"_L1,
"applicationArguments"_L1, testAppArgs };
551 g_options.amStarttestArgs.append(testEnvVarArgs);
736 for (
auto it =
g_options.outFiles.constBegin(); it !=
g_options.outFiles.constEnd(); ++it) {
737 const QString filePath = it.value();
738 const QString fileName = QFileInfo(filePath).fileName();
740 const QString catCmd =
"cat files/%1 2> /dev/null"_L1.arg(fileName);
741 const QStringList fullCatArgs = {
"shell"_L1, runCommandAsUserArgs(catCmd) };
743 bool catSuccess =
false;
747 catSuccess = execAdbCommand(fullCatArgs, &output,
false);
750 else if (!output.isEmpty())
755 qCritical() <<
"Error: failed to retrieve the test result file %1."_L1.arg(fileName);
759 if (output.isEmpty()) {
760 qCritical() <<
"Error: the test result file %1 is empty."_L1.arg(fileName);
765 if (!out.open(QIODevice::WriteOnly)) {
766 qCritical() <<
"Error: failed to open %1 to write results to host."_L1.arg(filePath);
777 QString libsPath =
"%1/libs/"_L1.arg(g_options.buildPath);
778 const QStringList abiArgs = {
"shell"_L1,
"getprop"_L1,
"ro.product.cpu.abi"_L1 };
780 if (!execAdbCommand(abiArgs, &abi,
false)) {
781 QStringList subDirs = QDir(libsPath).entryList(QDir::Dirs | QDir::NoDotAndDotDot);
782 if (!subDirs.isEmpty())
783 abi = subDirs.first().toUtf8();
788 qWarning() <<
"Failed to get the libs abi, falling to host architecture";
790 QString hostArch = QSysInfo::currentCpuArchitecture();
791 if (hostArch ==
"x86_64"_L1)
792 abi =
"arm64-x86_64";
793 else if (hostArch ==
"arm64"_L1)
795 else if (hostArch ==
"i386"_L1)
800 return libsPath + QString::fromUtf8(abi);
806 if (logcat.isEmpty())
809 QByteArray crashLogcat(logcat);
811 QProcess ndkStackProc;
812 ndkStackProc.start(
g_options.ndkStackPath, {
"-sym"_L1, getAbiLibsPath() });
814 if (ndkStackProc.waitForStarted()) {
815 ndkStackProc.write(crashLogcat);
816 ndkStackProc.closeWriteChannel();
818 if (ndkStackProc.waitForReadyRead())
819 crashLogcat = ndkStackProc.readAllStandardOutput();
821 ndkStackProc.terminate();
822 if (!ndkStackProc.waitForFinished())
823 qCritical() <<
"Error: ndk-stack command timed out.";
825 qCritical() <<
"Error: failed to run ndk-stack command.";
829 qWarning() <<
"Warning: ndk-stack path not provided and couldn't be deduced "
830 "using the ANDROID_NDK_ROOT environment variable.";
833 if (!crashLogcat.startsWith(
"********** Crash dump")) {
834 qDebug() <<
"[androidtestrunner] ********** BEGIN crash dump **********";
835 qDebug().noquote() << crashLogcat.trimmed();
836 qDebug() <<
"[androidtestrunner] ********** END crash dump **********";
842 QStringList logcatArgs = {
"shell"_L1,
"logcat"_L1,
"-t"_L1,
"'%1'"_L1.arg(timeStamp),
843 "-v"_L1,
"brief"_L1 };
845 const bool useColor = qEnvironmentVariable(
"QTEST_ENVIRONMENT") !=
"ci"_L1;
847 logcatArgs <<
"-v"_L1 <<
"color"_L1;
850 if (!execAdbCommand(logcatArgs, &logcat,
false)) {
851 qCritical() <<
"Error: failed to fetch logcat of the test";
855 if (logcat.isEmpty()) {
856 qWarning() <<
"The retrieved logcat is empty";
860 const QByteArray crashMarker(
"*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***");
861 int crashMarkerIndex = logcat.indexOf(crashMarker);
862 QByteArray crashLogcat;
864 if (crashMarkerIndex != -1) {
865 crashLogcat = logcat.mid(crashMarkerIndex);
866 logcat = logcat.left(crashMarkerIndex);
870 const bool anrOccurred = logcat.contains(
"ANR in %1"_L1.arg(g_options.package).toUtf8());
877 qCritical(
"[androidtestrunner] An ANR has occurred while running the test '%s';"
878 " consult logcat for additional logs from the system_server process",
882 int systemServerPid = getPid(
"system_server"_L1);
884 static const QRegularExpression logcatRegEx{
885 "(?:^\\x1B\\[[0-9]+m)?"
888 "(\\(\\s*\\d*\\)):"
891 "(?:\\x1B\\[[0-9]+m)?"
895 QByteArrayList testLogcat;
896 for (
const QByteArray &line : logcat.split(u'\n')) {
897 QRegularExpressionMatch match = logcatRegEx.match(QString::fromUtf8(line));
898 if (match.hasMatch()) {
899 const QString msgType = match.captured(1);
900 const QString pidStr = match.captured(2);
901 const int capturedPid = pidStr.mid(1, pidStr.size() - 2).trimmed().toInt();
902 if (capturedPid == g_testInfo.pid || msgType == u'F')
903 testLogcat.append(line);
904 else if (anrOccurred && capturedPid == systemServerPid)
905 testLogcat.append(line);
908 testLogcat.append(line);
915 && !
g_testInfo.isTestRunnerInterrupted.load());
917 qDebug() <<
"[androidtestrunner] ********** BEGIN logcat dump **********";
918 qDebug().noquote() << testLogcat.join(u'\n').trimmed();
919 qDebug() <<
"[androidtestrunner] ********** END logcat dump **********";
921 if (!crashLogcat.isEmpty())
922 printLogcatCrash(crashLogcat);
1012 QCoreApplication a(argc, argv);
1019 qCritical() <<
"It is required to provide a make command with the \"--make\" parameter "
1020 "to generate the apk.";
1024 QByteArray buildOutput;
1025 if (!execCommand(g_options.makeCommand, &buildOutput,
true)) {
1026 qCritical(
"The APK build command \"%s\" failed\n\n%s",
1027 qPrintable(
g_options.makeCommand), buildOutput.constData());
1031 if (!QFile::exists(g_options.packagePath)) {
1032 qCritical(
"No apk \"%s\" found after running the make command. "
1033 "Check the provided path and the make command.",
1038 const QStringList devices = runningDevices();
1039 if (devices.isEmpty()) {
1040 qCritical(
"No connected devices or running emulators can be found.");
1043 qCritical(
"No connected device or running emulator with serial '%s' can be found.",
1046 }
else if (
g_options.serial.isEmpty() && devices.size() == 1) {
1048 }
else if (
g_options.serial.isEmpty()) {
1049 qCritical(
"Multiple devices connected, set ANDROID_SERIAL or ANDROID_DEVICE_SERIAL.");
1055 g_testInfo.userId = userId();
1060 const QString ns = getGradleProjectProperty(g_options.buildPath,
"android.namespace"_L1);
1065 qCritical(
"Unable to get package name for '%s'", qPrintable(
g_options.packagePath));
1076 if (
g_options.packagePath.endsWith(
".apk"_L1)) {
1077 const QStringList installArgs = {
"install"_L1,
"-r"_L1, g_options.packagePath };
1078 g_testInfo.isPackageInstalled.store(execAdbCommand(installArgs,
nullptr));
1081 }
else if (
g_options.packagePath.endsWith(
".aab"_L1)) {
1083 const auto apksFilePath = aab.absoluteDir().absoluteFilePath(aab.baseName() +
".apks"_L1);
1084 if (!execBundletoolCommand({
"build-apks"_L1,
"--bundle"_L1, g_options.packagePath,
1085 "--output"_L1, apksFilePath,
"--local-testing"_L1,
1086 "--overwrite"_L1 }))
1089 if (!execBundletoolCommand({
"install-apks"_L1,
"--apks"_L1, apksFilePath }))
1095 const QStringList dangerousPermisisons = queryDangerousPermissions();
1096 for (
const auto &permission : g_options.permissions) {
1097 if (!dangerousPermisisons.contains(permission))
1100 if (!execAdbCommand({
"shell"_L1,
"pm"_L1,
"grant"_L1, g_options.package, permission },
1102 qWarning(
"Unable to grant '%s' to '%s'. Probably the Android version mismatch.",
1103 qPrintable(permission), qPrintable(g_options.package));
1108 for (
const auto &command : g_options.preTestRunAdbCommands) {
1110 if (!execAdbCommand(command, &output)) {
1111 qCritical(
"The pre test ADB command \"%s\" failed with output:\n%s",
1112 qUtf8Printable(command.join(u' ')), output.constData());
1118 const QString formattedStartTime = getCurrentTimeString();
1121 if (!execAdbCommand(g_options.amStarttestArgs,
nullptr))
1139 analyseLogcat(formattedStartTime, &exitCode);
1150 if (
g_testInfo.isTestRunnerInterrupted.load()) {
1151 qCritical() <<
"The androidtestrunner was interrupted and the test was cleaned up.";