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))
265 const QStringList manifestCandidates = {
266 g_options.buildPath +
"/AndroidManifest.xml"_L1,
267 g_options.buildPath +
"/app/AndroidManifest.xml"_L1,
269 for (
const QString &candidate : manifestCandidates) {
270 if (QFile::exists(candidate)) {
271 g_options.manifestPath = candidate;
366 QString gradlew = androidBuildDir +
"/gradlew.bat"_L1;
368 QString gradlew = androidBuildDir +
"/gradlew"_L1;
370 if (!QFile::exists(gradlew))
373 const QString scriptPath = gradleInitScriptPath();
374 if (scriptPath.isEmpty())
378 process.setWorkingDirectory(androidBuildDir);
379 process.start(gradlew, {
"-q"_L1,
"--init-script"_L1, scriptPath,
380 "-Pproperty="_L1 + property,
"printProjectProperty"_L1 });
382 if (!process.waitForFinished(30000))
385 return QString::fromUtf8(process.readAllStandardOutput()).trimmed();
413 const QStringList args({
"shell"_L1,
"dumpsys"_L1,
"package"_L1,
"permissions"_L1 });
414 if (!execAdbCommand(args, &output,
false)) {
415 qWarning(
"Failed to query permissions via dumpsys");
420
421
422
423
424
425
426
427
428 const static QRegularExpression regex(
"^\\s*Permission\\s+\\[([^\\]]+)\\]\\s+\\(([^)]+)\\):"_L1);
429 QStringList dangerousPermissions;
432 const QStringList lines = QString::fromUtf8(output).split(u'\n');
433 for (
const QString &line : lines) {
434 QRegularExpressionMatch match = regex.match(line);
435 if (match.hasMatch()) {
436 currentPerm = match.captured(1);
440 if (currentPerm.isEmpty())
443 int protIndex = line.indexOf(
"prot="_L1);
447 QString protectionTypes = line.mid(protIndex + 5).trimmed();
448 if (protectionTypes.contains(
"dangerous"_L1, Qt::CaseInsensitive)) {
449 dangerousPermissions.append(currentPerm);
454 return dangerousPermissions;
477 QRegularExpression oldFormats{
"^-(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
478 QRegularExpression newLoggingFormat{
"^(.*),(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
482 QStringList unhandledArgs;
483 for (
int i = 0; i <
g_options.testArgsList.size(); ++i) {
484 const QString &arg =
g_options.testArgsList[i].trimmed();
487 if (arg ==
"-o"_L1) {
488 if (i >=
g_options.testArgsList.size() - 1)
491 const auto &filePath =
g_options.testArgsList[++i];
492 const auto match = newLoggingFormat.match(filePath);
493 if (!match.hasMatch()) {
496 const auto capturedTexts = match.capturedTexts();
497 setOutputFile(capturedTexts.at(1), capturedTexts.at(2));
500 auto match = oldFormats.match(arg);
501 if (match.hasMatch()) {
502 logType = match.capturedTexts().at(1);
506 QString quotedArg = QString(arg).replace(
"\""_L1,
"\\\"\\\"\\\""_L1);
509 quotedArg.replace(
"'"_L1,
"\'"_L1);
511 unhandledArgs <<
" \\\"%1\\\""_L1.arg(quotedArg);
515 if (
g_options.outFiles.isEmpty() || !file.isEmpty() || !logType.isEmpty())
516 setOutputFile(file, logType);
519 for (
auto it = g_options.outFiles.constBegin(); it != g_options.outFiles.constEnd(); ++it)
520 testAppArgs +=
"-o %1,%2 "_L1.arg(QFileInfo(it.value()).fileName(), it.key());
522 testAppArgs += unhandledArgs.join(u' ').trimmed();
523 testAppArgs =
"\"%1\""_L1.arg(testAppArgs.trimmed());
524 const QString activityName =
"%1/%2"_L1.arg(g_options.package).arg(g_options.activity);
528 const QStringList envVarsList = QProcessEnvironment::systemEnvironment().toStringList();
529 for (
const QString &var : envVarsList) {
530 if (var.startsWith(
"QTEST_"_L1) || var.startsWith(
"QT_"_L1))
531 testEnvVars +=
"%1 "_L1.arg(var);
534 if (!testEnvVars.isEmpty()) {
535 testEnvVars = QString::fromUtf8(testEnvVars.trimmed().toUtf8().toBase64());
536 testEnvVars =
"-e extraenvvars \"%4\""_L1.arg(testEnvVars);
539 g_options.amStarttestArgs = {
"shell"_L1,
"am"_L1,
"start"_L1,
540 "-n"_L1, activityName,
541 "-e"_L1,
"applicationArguments"_L1, testAppArgs,
728 for (
auto it =
g_options.outFiles.constBegin(); it !=
g_options.outFiles.constEnd(); ++it) {
729 const QString filePath = it.value();
730 const QString fileName = QFileInfo(filePath).fileName();
732 const QString catCmd =
"cat files/%1 2> /dev/null"_L1.arg(fileName);
733 const QStringList fullCatArgs = {
"shell"_L1, runCommandAsUserArgs(catCmd) };
735 bool catSuccess =
false;
739 catSuccess = execAdbCommand(fullCatArgs, &output,
false);
742 else if (!output.isEmpty())
747 qCritical() <<
"Error: failed to retrieve the test result file %1."_L1.arg(fileName);
751 if (output.isEmpty()) {
752 qCritical() <<
"Error: the test result file %1 is empty."_L1.arg(fileName);
757 if (!out.open(QIODevice::WriteOnly)) {
758 qCritical() <<
"Error: failed to open %1 to write results to host."_L1.arg(filePath);
798 if (logcat.isEmpty())
801 QByteArray crashLogcat(logcat);
803 QProcess ndkStackProc;
804 ndkStackProc.start(
g_options.ndkStackPath, {
"-sym"_L1, getAbiLibsPath() });
806 if (ndkStackProc.waitForStarted()) {
807 ndkStackProc.write(crashLogcat);
808 ndkStackProc.closeWriteChannel();
810 if (ndkStackProc.waitForReadyRead())
811 crashLogcat = ndkStackProc.readAllStandardOutput();
813 ndkStackProc.terminate();
814 if (!ndkStackProc.waitForFinished())
815 qCritical() <<
"Error: ndk-stack command timed out.";
817 qCritical() <<
"Error: failed to run ndk-stack command.";
821 qWarning() <<
"Warning: ndk-stack path not provided and couldn't be deduced "
822 "using the ANDROID_NDK_ROOT environment variable.";
825 if (!crashLogcat.startsWith(
"********** Crash dump")) {
826 qDebug() <<
"[androidtestrunner] ********** BEGIN crash dump **********";
827 qDebug().noquote() << crashLogcat.trimmed();
828 qDebug() <<
"[androidtestrunner] ********** END crash dump **********";
834 QStringList logcatArgs = {
"shell"_L1,
"logcat"_L1,
"-t"_L1,
"'%1'"_L1.arg(timeStamp),
835 "-v"_L1,
"brief"_L1 };
837 const bool useColor = qEnvironmentVariable(
"QTEST_ENVIRONMENT") !=
"ci"_L1;
839 logcatArgs <<
"-v"_L1 <<
"color"_L1;
842 if (!execAdbCommand(logcatArgs, &logcat,
false)) {
843 qCritical() <<
"Error: failed to fetch logcat of the test";
847 if (logcat.isEmpty()) {
848 qWarning() <<
"The retrieved logcat is empty";
852 const QByteArray crashMarker(
"*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***");
853 int crashMarkerIndex = logcat.indexOf(crashMarker);
854 QByteArray crashLogcat;
856 if (crashMarkerIndex != -1) {
857 crashLogcat = logcat.mid(crashMarkerIndex);
858 logcat = logcat.left(crashMarkerIndex);
862 const bool anrOccurred = logcat.contains(
"ANR in %1"_L1.arg(g_options.package).toUtf8());
869 qCritical(
"[androidtestrunner] An ANR has occurred while running the test '%s';"
870 " consult logcat for additional logs from the system_server process",
874 int systemServerPid = getPid(
"system_server"_L1);
876 static const QRegularExpression logcatRegEx{
877 "(?:^\\x1B\\[[0-9]+m)?"
880 "(\\(\\s*\\d*\\)):"
883 "(?:\\x1B\\[[0-9]+m)?"
887 QByteArrayList testLogcat;
888 for (
const QByteArray &line : logcat.split(u'\n')) {
889 QRegularExpressionMatch match = logcatRegEx.match(QString::fromUtf8(line));
890 if (match.hasMatch()) {
891 const QString msgType = match.captured(1);
892 const QString pidStr = match.captured(2);
893 const int capturedPid = pidStr.mid(1, pidStr.size() - 2).trimmed().toInt();
894 if (capturedPid == g_testInfo.pid || msgType == u'F')
895 testLogcat.append(line);
896 else if (anrOccurred && capturedPid == systemServerPid)
897 testLogcat.append(line);
900 testLogcat.append(line);
907 && !
g_testInfo.isTestRunnerInterrupted.load());
909 qDebug() <<
"[androidtestrunner] ********** BEGIN logcat dump **********";
910 qDebug().noquote() << testLogcat.join(u'\n').trimmed();
911 qDebug() <<
"[androidtestrunner] ********** END logcat dump **********";
913 if (!crashLogcat.isEmpty())
914 printLogcatCrash(crashLogcat);
999int main(
int argc,
char *argv[])
1004 QCoreApplication a(argc, argv);
1011 qCritical() <<
"It is required to provide a make command with the \"--make\" parameter "
1012 "to generate the apk.";
1016 QByteArray buildOutput;
1017 if (!execCommand(g_options.makeCommand, &buildOutput,
true)) {
1018 qCritical(
"The APK build command \"%s\" failed\n\n%s",
1019 qPrintable(
g_options.makeCommand), buildOutput.constData());
1023 if (!QFile::exists(g_options.packagePath)) {
1024 qCritical(
"No apk \"%s\" found after running the make command. "
1025 "Check the provided path and the make command.",
1030 const QStringList devices = runningDevices();
1031 if (devices.isEmpty()) {
1032 qCritical(
"No connected devices or running emulators can be found.");
1035 qCritical(
"No connected device or running emulator with serial '%s' can be found.",
1042 g_testInfo.userId = userId();
1044 if (!QFile::exists(g_options.manifestPath)) {
1045 qCritical(
"Unable to find '%s'.", qPrintable(
g_options.manifestPath));
1052 const QString ns = getGradleProjectProperty(g_options.buildPath,
"android.namespace"_L1);
1057 qCritical(
"Unable to get package name for '%s'", qPrintable(
g_options.packagePath));
1068 if (
g_options.packagePath.endsWith(
".apk"_L1)) {
1069 const QStringList installArgs = {
"install"_L1,
"-r"_L1, g_options.packagePath };
1070 g_testInfo.isPackageInstalled.store(execAdbCommand(installArgs,
nullptr));
1073 }
else if (
g_options.packagePath.endsWith(
".aab"_L1)) {
1075 const auto apksFilePath = aab.absoluteDir().absoluteFilePath(aab.baseName() +
".apks"_L1);
1076 if (!execBundletoolCommand({
"build-apks"_L1,
"--bundle"_L1, g_options.packagePath,
1077 "--output"_L1, apksFilePath,
"--local-testing"_L1,
1078 "--overwrite"_L1 }))
1081 if (!execBundletoolCommand({
"install-apks"_L1,
"--apks"_L1, apksFilePath }))
1087 const QStringList dangerousPermisisons = queryDangerousPermissions();
1088 for (
const auto &permission : g_options.permissions) {
1089 if (!dangerousPermisisons.contains(permission))
1092 if (!execAdbCommand({
"shell"_L1,
"pm"_L1,
"grant"_L1, g_options.package, permission },
1094 qWarning(
"Unable to grant '%s' to '%s'. Probably the Android version mismatch.",
1095 qPrintable(permission), qPrintable(g_options.package));
1100 for (
const auto &command : g_options.preTestRunAdbCommands) {
1102 if (!execAdbCommand(command, &output)) {
1103 qCritical(
"The pre test ADB command \"%s\" failed with output:\n%s",
1104 qUtf8Printable(command.join(u' ')), output.constData());
1110 const QString formattedStartTime = getCurrentTimeString();
1113 if (!execAdbCommand(g_options.amStarttestArgs,
nullptr))
1131 analyseLogcat(formattedStartTime, &exitCode);
1142 if (
g_testInfo.isTestRunnerInterrupted.load()) {
1143 qCritical() <<
"The androidtestrunner was interrupted and the was test cleaned up.";