126 if (g_options.serial.isEmpty())
127 return execCommand(g_options.adbCommand, args, output, verbose);
129 QStringList argsWithSerial = {
"-s"_L1, g_options.serial};
130 argsWithSerial.append(args);
132 return execCommand(g_options.adbCommand, argsWithSerial, output, verbose);
163 QStringList arguments = QCoreApplication::arguments();
165 for (; i < arguments.size(); ++i) {
166 const QString &argument = arguments.at(i);
167 if (argument.compare(
"--adb"_L1, Qt::CaseInsensitive) == 0) {
168 if (i + 1 == arguments.size())
171 g_options.adbCommand = arguments.at(++i);
172 }
else if (argument.compare(
"--bundletool"_L1, Qt::CaseInsensitive) == 0) {
173 if (i + 1 == arguments.size())
176 g_options.bundletoolPath = arguments.at(++i);
177 }
else if (argument.compare(
"--path"_L1, Qt::CaseInsensitive) == 0) {
178 if (i + 1 == arguments.size())
182 }
else if (argument.compare(
"--manifest"_L1, Qt::CaseInsensitive) == 0) {
183 if (i + 1 == arguments.size())
186 g_options.manifestPath = arguments.at(++i);
187 }
else if (argument.compare(
"--make"_L1, Qt::CaseInsensitive) == 0) {
188 if (i + 1 == arguments.size())
191 g_options.makeCommand = arguments.at(++i);
192 }
else if (argument.compare(
"--apk"_L1, Qt::CaseInsensitive) == 0) {
193 if (i + 1 == arguments.size())
196 setPackagePath(arguments.at(++i));
197 }
else if (argument.compare(
"--aab"_L1, Qt::CaseInsensitive) == 0) {
198 if (i + 1 == arguments.size())
201 setPackagePath(arguments.at(++i));
202 }
else if (argument.compare(
"--activity"_L1, Qt::CaseInsensitive) == 0) {
203 if (i + 1 == arguments.size())
207 }
else if (argument.compare(
"--skip-install-root"_L1, Qt::CaseInsensitive) == 0) {
209 }
else if (argument.compare(
"--show-logcat"_L1, Qt::CaseInsensitive) == 0) {
211 }
else if (argument.compare(
"--ndk-stack"_L1, Qt::CaseInsensitive) == 0) {
212 if (i + 1 == arguments.size())
215 g_options.ndkStackPath = arguments.at(++i);
216 }
else if (argument.compare(
"--timeout"_L1, Qt::CaseInsensitive) == 0) {
217 if (i + 1 == arguments.size())
221 }
else if (argument.compare(
"--help"_L1, Qt::CaseInsensitive) == 0) {
223 }
else if (argument.compare(
"--verbose"_L1, Qt::CaseInsensitive) == 0) {
225 }
else if (argument.compare(
"--pre-test-adb-command"_L1, Qt::CaseInsensitive) == 0) {
226 if (i + 1 == arguments.size())
229 g_options.preTestRunAdbCommands += QProcess::splitCommand(arguments.at(++i));
231 }
else if (argument.compare(
"--"_L1, Qt::CaseInsensitive) == 0) {
235 g_options.testArgsList << arguments.at(i);
241 g_options.makeCommand =
"%1 INSTALL_ROOT=%2 install"_L1
242 .arg(g_options.makeCommand)
243 .arg(QDir::toNativeSeparators(g_options.buildPath));
246 for (;i < arguments.size(); ++i)
247 g_options.testArgsList << arguments.at(i);
252 g_options.serial = qEnvironmentVariable(
"ANDROID_SERIAL");
254 g_options.serial = qEnvironmentVariable(
"ANDROID_DEVICE_SERIAL");
257 const QString ndkPath = qEnvironmentVariable(
"ANDROID_NDK_ROOT");
258 const QString ndkStackPath = ndkPath + QDir::separator() +
"ndk-stack"_L1;
259 if (QFile::exists(ndkStackPath))
263 if (g_options.manifestPath.isEmpty())
264 g_options.manifestPath = g_options.buildPath +
"/AndroidManifest.xml"_L1;
325 QFile androidManifestXml(
g_options.manifestPath);
326 if (!androidManifestXml.open(QIODevice::ReadOnly)) {
327 qCritical(
"Unable to read android manifest '%s'", qPrintable(
g_options.manifestPath));
331 QXmlStreamReader reader(&androidManifestXml);
332 while (!reader.atEnd()) {
334 if (!reader.isStartElement())
337 if (reader.name() ==
"manifest"_L1)
338 g_options.package = reader.attributes().value(
"package"_L1).toString();
339 else if (reader.name() ==
"activity"_L1 && g_options.activity.isEmpty())
340 g_options.activity = reader.attributes().value(
"android:name"_L1).toString();
341 else if (reader.name() ==
"uses-permission"_L1)
342 g_options.permissions.append(reader.attributes().value(
"android:name"_L1).toString());
350 const QStringList args({
"shell"_L1,
"dumpsys"_L1,
"package"_L1,
"permissions"_L1 });
351 if (!execAdbCommand(args, &output,
false)) {
352 qWarning(
"Failed to query permissions via dumpsys");
357
358
359
360
361
362
363
364
365 const static QRegularExpression regex(
"^\\s*Permission\\s+\\[([^\\]]+)\\]\\s+\\(([^)]+)\\):"_L1);
366 QStringList dangerousPermissions;
369 const QStringList lines = QString::fromUtf8(output).split(u'\n');
370 for (
const QString &line : lines) {
371 QRegularExpressionMatch match = regex.match(line);
372 if (match.hasMatch()) {
373 currentPerm = match.captured(1);
377 if (currentPerm.isEmpty())
380 int protIndex = line.indexOf(
"prot="_L1);
384 QString protectionTypes = line.mid(protIndex + 5).trimmed();
385 if (protectionTypes.contains(
"dangerous"_L1, Qt::CaseInsensitive)) {
386 dangerousPermissions.append(currentPerm);
391 return dangerousPermissions;
414 QRegularExpression oldFormats{
"^-(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
415 QRegularExpression newLoggingFormat{
"^(.*),(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
419 QStringList unhandledArgs;
420 for (
int i = 0; i <
g_options.testArgsList.size(); ++i) {
421 const QString &arg =
g_options.testArgsList[i].trimmed();
424 if (arg ==
"-o"_L1) {
425 if (i >=
g_options.testArgsList.size() - 1)
428 const auto &filePath =
g_options.testArgsList[++i];
429 const auto match = newLoggingFormat.match(filePath);
430 if (!match.hasMatch()) {
433 const auto capturedTexts = match.capturedTexts();
434 setOutputFile(capturedTexts.at(1), capturedTexts.at(2));
437 auto match = oldFormats.match(arg);
438 if (match.hasMatch()) {
439 logType = match.capturedTexts().at(1);
443 QString quotedArg = QString(arg).replace(
"\""_L1,
"\\\"\\\"\\\""_L1);
446 quotedArg.replace(
"'"_L1,
"\'"_L1);
448 unhandledArgs <<
" \\\"%1\\\""_L1.arg(quotedArg);
452 if (
g_options.outFiles.isEmpty() || !file.isEmpty() || !logType.isEmpty())
453 setOutputFile(file, logType);
456 for (
auto it = g_options.outFiles.constBegin(); it != g_options.outFiles.constEnd(); ++it)
457 testAppArgs +=
"-o %1,%2 "_L1.arg(QFileInfo(it.value()).fileName(), it.key());
459 testAppArgs += unhandledArgs.join(u' ').trimmed();
460 testAppArgs =
"\"%1\""_L1.arg(testAppArgs.trimmed());
461 const QString activityName =
"%1/%2"_L1.arg(g_options.package).arg(g_options.activity);
465 const QStringList envVarsList = QProcessEnvironment::systemEnvironment().toStringList();
466 for (
const QString &var : envVarsList) {
467 if (var.startsWith(
"QTEST_"_L1) || var.startsWith(
"QT_"_L1))
468 testEnvVars +=
"%1 "_L1.arg(var);
471 if (!testEnvVars.isEmpty()) {
472 testEnvVars = QString::fromUtf8(testEnvVars.trimmed().toUtf8().toBase64());
473 testEnvVars =
"-e extraenvvars \"%4\""_L1.arg(testEnvVars);
476 g_options.amStarttestArgs = {
"shell"_L1,
"am"_L1,
"start"_L1,
477 "-n"_L1, activityName,
478 "-e"_L1,
"applicationArguments"_L1, testAppArgs,
665 for (
auto it =
g_options.outFiles.constBegin(); it !=
g_options.outFiles.constEnd(); ++it) {
666 const QString filePath = it.value();
667 const QString fileName = QFileInfo(filePath).fileName();
669 const QString catCmd =
"cat files/%1 2> /dev/null"_L1.arg(fileName);
670 const QStringList fullCatArgs = {
"shell"_L1, runCommandAsUserArgs(catCmd) };
672 bool catSuccess =
false;
676 catSuccess = execAdbCommand(fullCatArgs, &output,
false);
679 else if (!output.isEmpty())
684 qCritical() <<
"Error: failed to retrieve the test result file %1."_L1.arg(fileName);
688 if (output.isEmpty()) {
689 qCritical() <<
"Error: the test result file %1 is empty."_L1.arg(fileName);
694 if (!out.open(QIODevice::WriteOnly)) {
695 qCritical() <<
"Error: failed to open %1 to write results to host."_L1.arg(filePath);
735 if (logcat.isEmpty())
738 QByteArray crashLogcat(logcat);
740 QProcess ndkStackProc;
741 ndkStackProc.start(
g_options.ndkStackPath, {
"-sym"_L1, getAbiLibsPath() });
743 if (ndkStackProc.waitForStarted()) {
744 ndkStackProc.write(crashLogcat);
745 ndkStackProc.closeWriteChannel();
747 if (ndkStackProc.waitForReadyRead())
748 crashLogcat = ndkStackProc.readAllStandardOutput();
750 ndkStackProc.terminate();
751 if (!ndkStackProc.waitForFinished())
752 qCritical() <<
"Error: ndk-stack command timed out.";
754 qCritical() <<
"Error: failed to run ndk-stack command.";
758 qWarning() <<
"Warning: ndk-stack path not provided and couldn't be deduced "
759 "using the ANDROID_NDK_ROOT environment variable.";
762 if (!crashLogcat.startsWith(
"********** Crash dump")) {
763 qDebug() <<
"[androidtestrunner] ********** BEGIN crash dump **********";
764 qDebug().noquote() << crashLogcat.trimmed();
765 qDebug() <<
"[androidtestrunner] ********** END crash dump **********";
771 QStringList logcatArgs = {
"shell"_L1,
"logcat"_L1,
"-t"_L1,
"'%1'"_L1.arg(timeStamp),
772 "-v"_L1,
"brief"_L1 };
774 const bool useColor = qEnvironmentVariable(
"QTEST_ENVIRONMENT") !=
"ci"_L1;
776 logcatArgs <<
"-v"_L1 <<
"color"_L1;
779 if (!execAdbCommand(logcatArgs, &logcat,
false)) {
780 qCritical() <<
"Error: failed to fetch logcat of the test";
784 if (logcat.isEmpty()) {
785 qWarning() <<
"The retrieved logcat is empty";
789 const QByteArray crashMarker(
"*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***");
790 int crashMarkerIndex = logcat.indexOf(crashMarker);
791 QByteArray crashLogcat;
793 if (crashMarkerIndex != -1) {
794 crashLogcat = logcat.mid(crashMarkerIndex);
795 logcat = logcat.left(crashMarkerIndex);
799 const bool anrOccurred = logcat.contains(
"ANR in %1"_L1.arg(g_options.package).toUtf8());
806 qCritical(
"[androidtestrunner] An ANR has occurred while running the test '%s';"
807 " consult logcat for additional logs from the system_server process",
811 int systemServerPid = getPid(
"system_server"_L1);
813 static const QRegularExpression logcatRegEx{
814 "(?:^\\x1B\\[[0-9]+m)?"
817 "(\\(\\s*\\d*\\)):"
820 "(?:\\x1B\\[[0-9]+m)?"
824 QByteArrayList testLogcat;
825 for (
const QByteArray &line : logcat.split(u'\n')) {
826 QRegularExpressionMatch match = logcatRegEx.match(QString::fromUtf8(line));
827 if (match.hasMatch()) {
828 const QString msgType = match.captured(1);
829 const QString pidStr = match.captured(2);
830 const int capturedPid = pidStr.mid(1, pidStr.size() - 2).trimmed().toInt();
831 if (capturedPid == g_testInfo.pid || msgType == u'F')
832 testLogcat.append(line);
833 else if (anrOccurred && capturedPid == systemServerPid)
834 testLogcat.append(line);
837 testLogcat.append(line);
844 && !
g_testInfo.isTestRunnerInterrupted.load());
846 qDebug() <<
"[androidtestrunner] ********** BEGIN logcat dump **********";
847 qDebug().noquote() << testLogcat.join(u'\n').trimmed();
848 qDebug() <<
"[androidtestrunner] ********** END logcat dump **********";
850 if (!crashLogcat.isEmpty())
851 printLogcatCrash(crashLogcat);
928int main(
int argc,
char *argv[])
933 QCoreApplication a(argc, argv);
940 qCritical() <<
"It is required to provide a make command with the \"--make\" parameter "
941 "to generate the apk.";
945 QByteArray buildOutput;
946 if (!execCommand(g_options.makeCommand, &buildOutput,
true)) {
947 qCritical(
"The APK build command \"%s\" failed\n\n%s",
948 qPrintable(
g_options.makeCommand), buildOutput.constData());
952 if (!QFile::exists(g_options.packagePath)) {
953 qCritical(
"No apk \"%s\" found after running the make command. "
954 "Check the provided path and the make command.",
959 const QStringList devices = runningDevices();
960 if (devices.isEmpty()) {
961 qCritical(
"No connected devices or running emulators can be found.");
964 qCritical(
"No connected device or running emulator with serial '%s' can be found.",
971 g_testInfo.userId = userId();
973 if (!QFile::exists(g_options.manifestPath)) {
974 qCritical(
"Unable to find '%s'.", qPrintable(
g_options.manifestPath));
982 qCritical(
"Unable to get package name for '%s'", qPrintable(
g_options.packagePath));
993 if (
g_options.packagePath.endsWith(
".apk"_L1)) {
994 const QStringList installArgs = {
"install"_L1,
"-r"_L1, g_options.packagePath };
995 g_testInfo.isPackageInstalled.store(execAdbCommand(installArgs,
nullptr));
998 }
else if (
g_options.packagePath.endsWith(
".aab"_L1)) {
1000 const auto apksFilePath = aab.absoluteDir().absoluteFilePath(aab.baseName() +
".apks"_L1);
1001 if (!execBundletoolCommand({
"build-apks"_L1,
"--bundle"_L1, g_options.packagePath,
1002 "--output"_L1, apksFilePath,
"--local-testing"_L1,
1003 "--overwrite"_L1 }))
1006 if (!execBundletoolCommand({
"install-apks"_L1,
"--apks"_L1, apksFilePath }))
1010 const QStringList dangerousPermisisons = queryDangerousPermissions();
1011 for (
const auto &permission : g_options.permissions) {
1012 if (!dangerousPermisisons.contains(permission))
1015 if (!execAdbCommand({
"shell"_L1,
"pm"_L1,
"grant"_L1, g_options.package, permission },
1017 qWarning(
"Unable to grant '%s' to '%s'. Probably the Android version mismatch.",
1018 qPrintable(permission), qPrintable(g_options.package));
1023 for (
const auto &command : g_options.preTestRunAdbCommands) {
1025 if (!execAdbCommand(command, &output)) {
1026 qCritical(
"The pre test ADB command \"%s\" failed with output:\n%s",
1027 qUtf8Printable(command.join(u' ')), output.constData());
1033 const QString formattedStartTime = getCurrentTimeString();
1036 if (!execAdbCommand(g_options.amStarttestArgs,
nullptr))
1054 analyseLogcat(formattedStartTime, &exitCode);
1065 if (
g_testInfo.isTestRunnerInterrupted.load()) {
1066 qCritical() <<
"The androidtestrunner was interrupted and the was test cleaned up.";