69static bool execCommand(
const QString &program,
const QStringList &args,
70 QByteArray *output =
nullptr,
bool verbose =
false)
72 const auto command = program +
" "_L1 + args.join(u' ');
74 if (verbose && g_options.verbose)
75 fprintf(stdout,
"Execute %s.\n", command.toUtf8().constData());
78 process.start(program, args);
79 if (!process.waitForStarted()) {
80 qCritical(
"Cannot execute command %s.", qPrintable(command));
86 const int FinishTimeout = program.endsWith(
"adb"_L1) ? 30000 : g_options.timeoutSecs * 1000;
87 if (!process.waitForFinished(FinishTimeout)) {
88 qCritical(
"Execution of command %s timed out.", qPrintable(command));
92 const auto stdOut = process.readAllStandardOutput();
94 output->append(stdOut);
96 if (verbose && g_options.verbose)
97 fprintf(stdout,
"%s\n", stdOut.constData());
99 return process.exitCode() == 0;
105 if (g_options.serial.isEmpty())
106 return execCommand(g_options.adbCommand, args, output, verbose);
108 QStringList argsWithSerial = {
"-s"_L1, g_options.serial};
109 argsWithSerial.append(args);
111 return execCommand(g_options.adbCommand, argsWithSerial, output, verbose);
114static bool execCommand(
const QString &command, QByteArray *output =
nullptr,
bool verbose =
true)
116 auto args = QProcess::splitCommand(command);
117 const auto program = args.first();
118 args.removeOne(program);
119 return execCommand(program, args, output, verbose);
124 QStringList arguments = QCoreApplication::arguments();
126 for (; i < arguments.size(); ++i) {
127 const QString &argument = arguments.at(i);
128 if (argument.compare(
"--adb"_L1, Qt::CaseInsensitive) == 0) {
129 if (i + 1 == arguments.size())
132 g_options.adbCommand = arguments.at(++i);
133 }
else if (argument.compare(
"--path"_L1, Qt::CaseInsensitive) == 0) {
134 if (i + 1 == arguments.size())
138 }
else if (argument.compare(
"--make"_L1, Qt::CaseInsensitive) == 0) {
139 if (i + 1 == arguments.size())
142 g_options.makeCommand = arguments.at(++i);
143 }
else if (argument.compare(
"--apk"_L1, Qt::CaseInsensitive) == 0) {
144 if (i + 1 == arguments.size())
148 }
else if (argument.compare(
"--activity"_L1, Qt::CaseInsensitive) == 0) {
149 if (i + 1 == arguments.size())
153 }
else if (argument.compare(
"--skip-install-root"_L1, Qt::CaseInsensitive) == 0) {
155 }
else if (argument.compare(
"--show-logcat"_L1, Qt::CaseInsensitive) == 0) {
157 }
else if (argument.compare(
"--ndk-stack"_L1, Qt::CaseInsensitive) == 0) {
158 if (i + 1 == arguments.size())
161 g_options.ndkStackPath = arguments.at(++i);
162 }
else if (argument.compare(
"--timeout"_L1, Qt::CaseInsensitive) == 0) {
163 if (i + 1 == arguments.size())
167 }
else if (argument.compare(
"--help"_L1, Qt::CaseInsensitive) == 0) {
169 }
else if (argument.compare(
"--verbose"_L1, Qt::CaseInsensitive) == 0) {
171 }
else if (argument.compare(
"--"_L1, Qt::CaseInsensitive) == 0) {
175 g_options.testArgsList << arguments.at(i);
178 for (;i < arguments.size(); ++i)
179 g_options.testArgsList << arguments.at(i);
184 g_options.serial = qEnvironmentVariable(
"ANDROID_SERIAL");
186 g_options.serial = qEnvironmentVariable(
"ANDROID_DEVICE_SERIAL");
189 const QString ndkPath = qEnvironmentVariable(
"ANDROID_NDK_ROOT");
190 const QString ndkStackPath = ndkPath + QDir::separator() +
"ndk-stack"_L1;
191 if (QFile::exists(ndkStackPath))
245 QFile androidManifestXml(androidManifestPath);
246 if (androidManifestXml.open(QIODevice::ReadOnly)) {
247 QXmlStreamReader reader(&androidManifestXml);
248 while (!reader.atEnd()) {
250 if (reader.isStartElement() && reader.name() ==
"manifest"_L1)
251 return reader.attributes().value(
"package"_L1).toString();
259 QFile androidManifestXml(androidManifestPath);
260 if (androidManifestXml.open(QIODevice::ReadOnly)) {
261 QXmlStreamReader reader(&androidManifestXml);
262 while (!reader.atEnd()) {
264 if (reader.isStartElement() && reader.name() ==
"activity"_L1)
265 return reader.attributes().value(
"android:name"_L1).toString();
273 if (format.isEmpty())
276 if ((file.isEmpty() || file == u'-')) {
277 if (
g_options.outFiles.contains(format)) {
280 file =
"stdout.%1"_L1.arg(format);
283 g_options.stdoutFileName = QFileInfo(file).fileName();
291 QRegularExpression oldFormats{
"^-(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
292 QRegularExpression newLoggingFormat{
"^(.*),(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
296 QStringList unhandledArgs;
297 for (
int i = 0; i <
g_options.testArgsList.size(); ++i) {
298 const QString &arg = g_options.testArgsList[i].trimmed();
301 if (arg ==
"-o"_L1) {
302 if (i >=
g_options.testArgsList.size() - 1)
305 const auto &filePath =
g_options.testArgsList[++i];
306 const auto match = newLoggingFormat.match(filePath);
307 if (!match.hasMatch()) {
310 const auto capturedTexts = match.capturedTexts();
311 setOutputFile(capturedTexts.at(1), capturedTexts.at(2));
314 auto match = oldFormats.match(arg);
315 if (match.hasMatch()) {
316 logType = match.capturedTexts().at(1);
320 QString quotedArg = QString(arg).replace(
"\""_L1,
"\\\"\\\"\\\""_L1);
323 quotedArg.replace(
"'"_L1,
"\'"_L1);
325 unhandledArgs <<
" \\\"%1\\\""_L1.arg(quotedArg);
329 if (
g_options.outFiles.isEmpty() || !file.isEmpty() || !logType.isEmpty())
330 setOutputFile(file, logType);
333 for (
auto it = g_options.outFiles.constBegin(); it != g_options.outFiles.constEnd(); ++it)
334 testAppArgs +=
"-o %1,%2 "_L1.arg(QFileInfo(it.value()).fileName(), it.key());
336 testAppArgs += unhandledArgs.join(u' ').trimmed();
337 testAppArgs =
"\"%1\""_L1.arg(testAppArgs.trimmed());
338 const QString activityName =
"%1/%2"_L1.arg(g_options.package).arg(g_options.activity);
342 const QStringList envVarsList = QProcessEnvironment::systemEnvironment().toStringList();
343 for (
const QString &var : envVarsList) {
344 if (var.startsWith(
"QTEST_"_L1) || var.startsWith(
"QT_"_L1))
345 testEnvVars +=
"%1 "_L1.arg(var);
348 if (!testEnvVars.isEmpty()) {
349 testEnvVars = QString::fromUtf8(testEnvVars.trimmed().toUtf8().toBase64());
350 testEnvVars =
"-e extraenvvars \"%4\""_L1.arg(testEnvVars);
353 g_options.amStarttestArgs = {
"shell"_L1,
"am"_L1,
"start"_L1,
354 "-n"_L1, activityName,
355 "-e"_L1,
"applicationArguments"_L1, testAppArgs,
365 const QStringList psArgs = {
"shell"_L1,
"ps | grep ' %1'"_L1.arg(package) };
366 if (!execAdbCommand(psArgs, &output,
false))
369 const QList<QByteArray> lines = output.split(u'\n');
370 if (lines.size() < 1)
373 QList<QByteArray> columns = lines.first().simplified().replace(u'\t', u' ').split(u' ');
374 if (columns.size() < 3)
378 int pid = columns.at(1).toInt(&ok);
395 const QStringList psArgs = {
"shell"_L1,
"ps"_L1,
"-p"_L1, QString::number(g_testInfo.pid),
396 "|"_L1,
"grep"_L1,
"-o"_L1,
" %1$"_L1.arg(g_options.package) };
397 bool psSuccess =
false;
398 for (
int i = 1; i <= 3; ++i) {
399 psSuccess = execAdbCommand(psArgs, &output,
false);
402 QThread::msleep(250);
405 return psSuccess && output.trimmed() ==
g_options.package.toUtf8();
422 const QString lsCmd =
"ls files/%1"_L1.arg(g_options.stdoutFileName);
423 const QStringList adbLsCmd = {
"shell"_L1, runCommandAsUserArgs(lsCmd) };
425 QDeadlineTimer deadline(5000);
427 if (execAdbCommand(adbLsCmd,
nullptr,
false))
429 QThread::msleep(100);
430 }
while (!deadline.hasExpired() && !
g_testInfo.isTestRunnerInterrupted.load());
436 const QString tailPipeCmd =
"tail -n +1 -f files/%1"_L1.arg(g_options.stdoutFileName);
437 const QStringList adbTailCmd = {
"shell"_L1, runCommandAsUserArgs(tailPipeCmd) };
440 g_options.stdoutLogger->setProcessChannelMode(QProcess::ForwardedOutputChannel);
443 if (!
g_options.stdoutLogger->waitForStarted()) {
444 qCritical() <<
"Error: failed to run adb command to fetch stdout test results.";
542 for (
auto it =
g_options.outFiles.constBegin(); it !=
g_options.outFiles.constEnd(); ++it) {
543 const QString filePath = it.value();
544 const QString fileName = QFileInfo(filePath).fileName();
546 const QString catCmd =
"cat files/%1 2> /dev/null"_L1.arg(fileName);
547 const QStringList fullCatArgs = {
"shell"_L1, runCommandAsUserArgs(catCmd) };
549 bool catSuccess =
false;
553 catSuccess = execAdbCommand(fullCatArgs, &output,
false);
556 else if (!output.isEmpty())
561 qCritical() <<
"Error: failed to retrieve the test result file %1."_L1.arg(fileName);
565 if (output.isEmpty()) {
566 qCritical() <<
"Error: the test result file %1 is empty."_L1.arg(fileName);
571 if (!out.open(QIODevice::WriteOnly)) {
572 qCritical() <<
"Error: failed to open %1 to write results to host."_L1.arg(filePath);
583 QString libsPath =
"%1/libs/"_L1.arg(g_options.buildPath);
584 const QStringList abiArgs = {
"shell"_L1,
"getprop"_L1,
"ro.product.cpu.abi"_L1 };
586 if (!execAdbCommand(abiArgs, &abi,
false)) {
587 QStringList subDirs = QDir(libsPath).entryList(QDir::Dirs | QDir::NoDotAndDotDot);
588 if (!subDirs.isEmpty())
589 abi = subDirs.first().toUtf8();
594 qWarning() <<
"Failed to get the libs abi, falling to host architecture";
596 QString hostArch = QSysInfo::currentCpuArchitecture();
597 if (hostArch ==
"x86_64"_L1)
598 abi =
"arm64-x86_64";
599 else if (hostArch ==
"arm64"_L1)
601 else if (hostArch ==
"i386"_L1)
606 return libsPath + QString::fromUtf8(abi);
612 if (logcat.isEmpty())
615 QByteArray crashLogcat(logcat);
617 QProcess ndkStackProc;
618 ndkStackProc.start(g_options.ndkStackPath, {
"-sym"_L1, getAbiLibsPath() });
620 if (ndkStackProc.waitForStarted()) {
621 ndkStackProc.write(crashLogcat);
622 ndkStackProc.closeWriteChannel();
624 if (ndkStackProc.waitForReadyRead())
625 crashLogcat = ndkStackProc.readAllStandardOutput();
627 ndkStackProc.terminate();
628 if (!ndkStackProc.waitForFinished())
629 qCritical() <<
"Error: ndk-stack command timed out.";
631 qCritical() <<
"Error: failed to run ndk-stack command.";
635 qWarning() <<
"Warning: ndk-stack path not provided and couldn't be deduced "
636 "using the ANDROID_NDK_ROOT environment variable.";
639 if (!crashLogcat.startsWith(
"********** Crash dump"))
640 qDebug() <<
"********** Crash dump: **********";
641 qDebug().noquote() << crashLogcat.trimmed();
642 qDebug() <<
"********** End crash dump **********";
647 QStringList logcatArgs = {
"shell"_L1,
"logcat"_L1,
"-t"_L1,
"'%1'"_L1.arg(timeStamp),
648 "-v"_L1,
"brief"_L1 };
650 const bool useColor = qEnvironmentVariable(
"QTEST_ENVIRONMENT") !=
"ci"_L1;
652 logcatArgs <<
"-v"_L1 <<
"color"_L1;
655 if (!execAdbCommand(logcatArgs, &logcat,
false)) {
656 qCritical() <<
"Error: failed to fetch logcat of the test";
660 if (logcat.isEmpty()) {
661 qWarning() <<
"The retrieved logcat is empty";
665 const QByteArray crashMarker(
"*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***");
666 int crashMarkerIndex = logcat.indexOf(crashMarker);
667 QByteArray crashLogcat;
669 if (crashMarkerIndex != -1) {
670 crashLogcat = logcat.mid(crashMarkerIndex);
671 logcat = logcat.left(crashMarkerIndex);
675 const bool anrOccurred = logcat.contains(
"ANR in %1"_L1.arg(g_options.package).toUtf8());
678 *exitCode = *exitCode < 1 ? 1 : *exitCode;
679 qCritical(
"An ANR has occurred while running the test %s. The logcat will include "
680 "additional logs from the system_server process.",
684 int systemServerPid = getPid(
"system_server"_L1);
686 static const QRegularExpression logcatRegEx{
687 "(?:^\\x1B\\[[0-9]+m)?"
690 "(\\(\\s*\\d*\\)):"
693 "(?:\\x1B\\[[0-9]+m)?"
697 QByteArrayList testLogcat;
698 for (
const QByteArray &line : logcat.split(u'\n')) {
699 QRegularExpressionMatch match = logcatRegEx.match(QString::fromUtf8(line));
700 if (match.hasMatch()) {
701 const QString msgType = match.captured(1);
702 const QString pidStr = match.captured(2);
703 const int capturedPid = pidStr.mid(1, pidStr.size() - 2).trimmed().toInt();
704 if (capturedPid == g_testInfo.pid || msgType == u'F')
705 testLogcat.append(line);
706 else if (anrOccurred && capturedPid == systemServerPid)
707 testLogcat.append(line);
710 testLogcat.append(line);
717 qDebug() <<
"********** logcat dump **********";
718 qDebug().noquote() << testLogcat.join(u'\n').trimmed();
719 qDebug() <<
"********** End logcat dump **********";
722 if (!crashLogcat.isEmpty() && *exitCode != 0)
723 printLogcatCrash(crashLogcat);
798int main(
int argc,
char *argv[])
803 QCoreApplication a(argc, argv);
810 qCritical() <<
"It is required to provide a make command with the \"--make\" parameter "
811 "to generate the apk.";
814 if (!execCommand(g_options.makeCommand,
nullptr,
true)) {
817 if (!execCommand(
"%1 INSTALL_ROOT=%2 install"_L1.arg(g_options.makeCommand,
818 QDir::toNativeSeparators(g_options.buildPath)),
nullptr)) {
822 if (!execCommand(g_options.makeCommand,
nullptr))
827 if (!QFile::exists(g_options.apkPath)) {
828 qCritical(
"No apk \"%s\" found after running the make command. "
829 "Check the provided path and the make command.",
834 const QStringList devices = runningDevices();
835 if (devices.isEmpty()) {
836 qCritical(
"No connected devices or running emulators can be found.");
839 qCritical(
"No connected device or running emulator with serial '%s' can be found.",
846 g_testInfo.userId = userId();
848 QString manifest = g_options.buildPath +
"/AndroidManifest.xml"_L1;
849 g_options.package = packageNameFromAndroidManifest(manifest);
850 if (g_options.activity.isEmpty())
851 g_options.activity = activityFromAndroidManifest(manifest);
860 const QStringList installArgs = {
"install"_L1,
"-r"_L1,
"-g"_L1, g_options.apkPath };
861 g_testInfo.isPackageInstalled.store(execAdbCommand(installArgs,
nullptr));
866 const QString formattedStartTime = getCurrentTimeString();
869 if (!execAdbCommand(g_options.amStarttestArgs,
nullptr))
886 analyseLogcat(formattedStartTime, &exitCode);
895 if (
g_testInfo.isTestRunnerInterrupted.load()) {
896 qCritical() <<
"The androidtestrunner was interrupted and the was test cleaned up.";