Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1// Copyright (C) 2019 BogDan Vatra <bogdan@kde.org>
2// Copyright (C) 2023 The Qt Company Ltd.
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
4
5#include <QtCore/QCoreApplication>
6#include <QtCore/QDeadlineTimer>
7#include <QtCore/qdebug.h>
8#include <QtCore/QDir>
9#include <QtCore/QHash>
10#include <QtCore/QProcess>
11#include <QtCore/QProcessEnvironment>
12#include <QtCore/QRegularExpression>
13#include <QtCore/QSystemSemaphore>
14#include <QtCore/QThread>
15#include <QtCore/QXmlStreamReader>
16#include <QtCore/QFileInfo>
17#include <QtCore/QSysInfo>
18
19#include <atomic>
20#include <csignal>
21#include <functional>
22#include <optional>
23#if defined(Q_OS_WIN32)
24#include <process.h>
25#else
26#include <unistd.h>
27#endif
28
29using namespace Qt::StringLiterals;
30
31
32// QTest-based test processes may exit with up to 127 for normal test failures
33static constexpr int HIGHEST_QTEST_EXITCODE = 127;
34// Something went wrong in androidtestrunner, in general
35static constexpr int EXIT_ERROR = 254;
36// More specific exit codes for failures in androidtestrunner:
37static constexpr int EXIT_NOEXITCODE = 253; // Failed to transfer exit code from device
38static constexpr int EXIT_ANR = 252; // Android ANR error (Application Not Responding)
39static constexpr int EXIT_NORESULTS = 251; // Failed to transfer result files from device
40
41
42struct Options
43{
44 bool helpRequested = false;
45 bool verbose = false;
46 bool skipAddInstallRoot = false;
47 int timeoutSecs = 600; // 10 minutes
65 bool showLogcatOutput = false;
67};
68
70
72{
73 int sdkVersion = -1;
74 int pid = -1;
76
77 std::atomic<bool> isPackageInstalled { false };
78 std::atomic<bool> isTestRunnerInterrupted { false };
79};
80
82
83// QTest-based processes return 0 if all tests PASSed, or the number of FAILs up to 127.
84// Other exitcodes signify abnormal termination and are system-dependent.
85static bool isTestExitCodeNormal(const int ec)
86{
87 return (ec >= 0 && ec <= HIGHEST_QTEST_EXITCODE);
88}
89
90static bool execCommand(const QString &program, const QStringList &args,
91 QByteArray *output = nullptr, bool verbose = false)
92{
93 const auto command = program + " "_L1 + args.join(u' ');
94
95 if (verbose && g_options.verbose)
96 fprintf(stdout,"Execute %s.\n", command.toUtf8().constData());
97
98 QProcess process;
99 process.start(program, args);
100 if (!process.waitForStarted()) {
101 qCritical("Cannot execute command %s.", qPrintable(command));
102 return false;
103 }
104
105 // If the command is not adb, for example, make or ninja, it can take more that
106 // QProcess::waitForFinished() 30 secs, so for that use a higher timeout.
107 const int FinishTimeout = program.endsWith("adb"_L1) ? 30000 : g_options.timeoutSecs * 1000;
108 if (!process.waitForFinished(FinishTimeout)) {
109 qCritical("Execution of command %s timed out.", qPrintable(command));
110 return false;
111 }
112
113 const auto stdOut = process.readAllStandardOutput();
114 if (output)
115 output->append(stdOut);
116
117 if (verbose && g_options.verbose)
118 fprintf(stdout, "%s\n", stdOut.constData());
119
120 return process.exitCode() == 0;
121}
122
123static bool execAdbCommand(const QStringList &args, QByteArray *output = nullptr,
124 bool verbose = true)
125{
126 if (g_options.serial.isEmpty())
127 return execCommand(g_options.adbCommand, args, output, verbose);
128
129 QStringList argsWithSerial = {"-s"_L1, g_options.serial};
130 argsWithSerial.append(args);
131
132 return execCommand(g_options.adbCommand, argsWithSerial, output, verbose);
133}
134
135static bool execBundletoolCommand(const QStringList &args, QByteArray *output = nullptr,
136 bool verbose = true)
137{
138 QString java("java"_L1);
139 QStringList argsFull = QStringList() << "-jar"_L1 << g_options.bundletoolPath << args;
140 return execCommand(java, argsFull, output, verbose);
141}
142
143static void setPackagePath(const QString &path)
144{
145 if (!g_options.packagePath.isEmpty()) {
146 qCritical("Both --aab and --apk options provided. This is not supported.");
148 return;
149 }
150 g_options.packagePath = path;
151}
152
153static bool execCommand(const QString &command, QByteArray *output = nullptr, bool verbose = true)
154{
155 auto args = QProcess::splitCommand(command);
156 const auto program = args.first();
157 args.removeOne(program);
158 return execCommand(program, args, output, verbose);
159}
160
161static bool parseOptions()
162{
163 QStringList arguments = QCoreApplication::arguments();
164 int i = 1;
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())
170 else
171 g_options.adbCommand = arguments.at(++i);
172 } else if (argument.compare("--bundletool"_L1, Qt::CaseInsensitive) == 0) {
173 if (i + 1 == arguments.size())
175 else
176 g_options.bundletoolPath = arguments.at(++i);
177 } else if (argument.compare("--path"_L1, Qt::CaseInsensitive) == 0) {
178 if (i + 1 == arguments.size())
180 else
181 g_options.buildPath = arguments.at(++i);
182 } else if (argument.compare("--manifest"_L1, Qt::CaseInsensitive) == 0) {
183 if (i + 1 == arguments.size())
185 else
186 g_options.manifestPath = arguments.at(++i);
187 } else if (argument.compare("--make"_L1, Qt::CaseInsensitive) == 0) {
188 if (i + 1 == arguments.size())
190 else
191 g_options.makeCommand = arguments.at(++i);
192 } else if (argument.compare("--apk"_L1, Qt::CaseInsensitive) == 0) {
193 if (i + 1 == arguments.size())
195 else
196 setPackagePath(arguments.at(++i));
197 } else if (argument.compare("--aab"_L1, Qt::CaseInsensitive) == 0) {
198 if (i + 1 == arguments.size())
200 else
201 setPackagePath(arguments.at(++i));
202 } else if (argument.compare("--activity"_L1, Qt::CaseInsensitive) == 0) {
203 if (i + 1 == arguments.size())
205 else
206 g_options.activity = arguments.at(++i);
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())
214 else
215 g_options.ndkStackPath = arguments.at(++i);
216 } else if (argument.compare("--timeout"_L1, Qt::CaseInsensitive) == 0) {
217 if (i + 1 == arguments.size())
219 else
220 g_options.timeoutSecs = arguments.at(++i).toInt();
221 } else if (argument.compare("--help"_L1, Qt::CaseInsensitive) == 0) {
223 } else if (argument.compare("--verbose"_L1, Qt::CaseInsensitive) == 0) {
224 g_options.verbose = true;
225 } else if (argument.compare("--pre-test-adb-command"_L1, Qt::CaseInsensitive) == 0) {
226 if (i + 1 == arguments.size())
228 else {
229 g_options.preTestRunAdbCommands += QProcess::splitCommand(arguments.at(++i));
230 }
231 } else if (argument.compare("--"_L1, Qt::CaseInsensitive) == 0) {
232 ++i;
233 break;
234 } else {
235 g_options.testArgsList << arguments.at(i);
236 }
237 }
238
240 // we need to run make INSTALL_ROOT=path install to install the application file(s) first
241 g_options.makeCommand = "%1 INSTALL_ROOT=%2 install"_L1
242 .arg(g_options.makeCommand)
243 .arg(QDir::toNativeSeparators(g_options.buildPath));
244 }
245
246 for (;i < arguments.size(); ++i)
247 g_options.testArgsList << arguments.at(i);
248
249 if (g_options.helpRequested || g_options.buildPath.isEmpty() || g_options.packagePath.isEmpty())
250 return false;
251
252 g_options.serial = qEnvironmentVariable("ANDROID_SERIAL");
253 if (g_options.serial.isEmpty())
254 g_options.serial = qEnvironmentVariable("ANDROID_DEVICE_SERIAL");
255
256 if (g_options.ndkStackPath.isEmpty()) {
257 const QString ndkPath = qEnvironmentVariable("ANDROID_NDK_ROOT");
258 const QString ndkStackPath = ndkPath + QDir::separator() + "ndk-stack"_L1;
259 if (QFile::exists(ndkStackPath))
260 g_options.ndkStackPath = ndkStackPath;
261 }
262
263 if (g_options.manifestPath.isEmpty())
264 g_options.manifestPath = g_options.buildPath + "/AndroidManifest.xml"_L1;
265
266 return true;
267}
268
269static void printHelp()
270{
271 qWarning("Syntax: %s <options> -- [TESTARGS] \n"
272 "\n"
273 " Runs a Qt for Android test on an emulator or a device. Specify a device\n"
274 " using the environment variables ANDROID_SERIAL or ANDROID_DEVICE_SERIAL.\n"
275 " Returns the number of failed tests, -1 on test runner deployment related\n"
276 " failures or zero on success."
277 "\n"
278 " Mandatory arguments:\n"
279 " --path <path>: The path where androiddeployqt builds the android package.\n"
280 "\n"
281 " --make <make cmd>: make command to create an APK, for example:\n"
282 " \"cmake --build <build-dir> --target <target>_make_apk\".\n"
283 "\n"
284 " --apk <apk path>: The test apk path. The apk has to exist already, if it\n"
285 " does not exist the make command must be provided for building the apk.\n"
286 "\n"
287 " --aab <aab path>: The test aab path. The aab has to exist already, if it\n"
288 " does not exist the make command must be provided for building the aab.\n"
289 "\n"
290 " Optional arguments:\n"
291 " --adb <adb cmd>: The Android ADB command. If missing the one from\n"
292 " $PATH will be used.\n"
293 "\n"
294 " --activity <acitvity>: The Activity to run. If missing the first\n"
295 " activity from AndroidManifest.qml file will be used.\n"
296 "\n"
297 " --timeout <seconds>: Timeout to run the test. Default is 10 minutes.\n"
298 "\n"
299 " --skip-install-root: Do not append INSTALL_ROOT=... to the make command.\n"
300 "\n"
301 " --show-logcat: Print Logcat output to stdout. If an ANR occurs during\n"
302 " the test run, logs from the system_server process are included.\n"
303 "\n"
304 " --ndk-stack: Path to ndk-stack tool that symbolizes crash stacktraces.\n"
305 " By default, ANDROID_NDK_ROOT env var is used to deduce the tool path.\n"
306 "\n"
307 " -- Arguments that will be passed to the test application.\n"
308 "\n"
309 " --verbose: Prints out information during processing.\n"
310 "\n"
311 " --pre-test-adb-command <command>: call the adb <command> after\n"
312 " installation and before the test run.\n"
313 "\n"
314 " --manifest <path>: Custom path to the AndroidManifest.xml.\n"
315 "\n"
316 " --bundletool <bundletool path>: The path to Android bundletool.\n"
317 " See https://developer.android.com/tools/bundletool for details.\n"
318 "\n"
319 " --help: Displays this information.\n",
320 qPrintable(QCoreApplication::arguments().at(0)));
321}
322
324{
325 QFile androidManifestXml(g_options.manifestPath);
326 if (!androidManifestXml.open(QIODevice::ReadOnly)) {
327 qCritical("Unable to read android manifest '%s'", qPrintable(g_options.manifestPath));
328 return false;
329 }
330
331 QXmlStreamReader reader(&androidManifestXml);
332 while (!reader.atEnd()) {
333 reader.readNext();
334 if (!reader.isStartElement())
335 continue;
336
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());
343 }
344 return true;
345}
346
348{
349 QByteArray output;
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");
353 return {};
354 }
355
356 /*
357 * Permissions section from this command look like:
358 *
359 * Permission [android.permission.INTERNET] (c8cafdc):
360 * sourcePackage=android
361 * uid=1000 gids=[3003] type=0 prot=normal|instant
362 * perm=PermissionInfo{5f5bfbb android.permission.INTERNET}
363 * flags=0x0
364 */
365 const static QRegularExpression regex("^\\s*Permission\\s+\\‍[([^\\‍]]+)\\‍]\\s+\\‍(([^)]+)\\‍):"_L1);
366 QStringList dangerousPermissions;
367 QString currentPerm;
368
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);
374 continue;
375 }
376
377 if (currentPerm.isEmpty())
378 continue;
379
380 int protIndex = line.indexOf("prot="_L1);
381 if (protIndex == -1)
382 continue;
383
384 QString protectionTypes = line.mid(protIndex + 5).trimmed();
385 if (protectionTypes.contains("dangerous"_L1, Qt::CaseInsensitive)) {
386 dangerousPermissions.append(currentPerm);
387 currentPerm.clear();
388 }
389 }
390
391 return dangerousPermissions;
392}
393
394static void setOutputFile(QString file, QString format)
395{
396 if (format.isEmpty())
397 format = "txt"_L1;
398
399 if ((file.isEmpty() || file == u'-')) {
400 if (g_options.outFiles.contains(format)) {
401 file = g_options.outFiles.value(format);
402 } else {
403 file = "stdout.%1"_L1.arg(format);
404 g_options.outFiles[format] = file;
405 }
406 g_options.stdoutFileName = QFileInfo(file).fileName();
407 } else {
408 g_options.outFiles[format] = file;
409 }
410}
411
412static bool parseTestArgs()
413{
414 QRegularExpression oldFormats{"^-(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
415 QRegularExpression newLoggingFormat{"^(.*),(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
416
417 QString file;
418 QString logType;
419 QStringList unhandledArgs;
420 for (int i = 0; i < g_options.testArgsList.size(); ++i) {
421 const QString &arg = g_options.testArgsList[i].trimmed();
422 if (arg == "--"_L1)
423 continue;
424 if (arg == "-o"_L1) {
425 if (i >= g_options.testArgsList.size() - 1)
426 return false; // missing file argument
427
428 const auto &filePath = g_options.testArgsList[++i];
429 const auto match = newLoggingFormat.match(filePath);
430 if (!match.hasMatch()) {
431 file = filePath;
432 } else {
433 const auto capturedTexts = match.capturedTexts();
434 setOutputFile(capturedTexts.at(1), capturedTexts.at(2));
435 }
436 } else {
437 auto match = oldFormats.match(arg);
438 if (match.hasMatch()) {
439 logType = match.capturedTexts().at(1);
440 } else {
441 // Use triple literal quotes so that QProcess::splitCommand() in androidjnimain.cpp
442 // keeps quotes characters inside the string.
443 QString quotedArg = QString(arg).replace("\""_L1, "\\\"\\\"\\\""_L1);
444 // Escape single quotes so they don't interfere with the shell command,
445 // and so they get passed to the app as single quote inside the string.
446 quotedArg.replace("'"_L1, "\'"_L1);
447 // Add escaped double quote character so that args with spaces are treated as one.
448 unhandledArgs << " \\\"%1\\\""_L1.arg(quotedArg);
449 }
450 }
451 }
452 if (g_options.outFiles.isEmpty() || !file.isEmpty() || !logType.isEmpty())
453 setOutputFile(file, logType);
454
455 QString testAppArgs;
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());
458
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);
462
463 // Pass over any qt or testlib env vars if set
464 QString testEnvVars;
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);
469 }
470
471 if (!testEnvVars.isEmpty()) {
472 testEnvVars = QString::fromUtf8(testEnvVars.trimmed().toUtf8().toBase64());
473 testEnvVars = "-e extraenvvars \"%4\""_L1.arg(testEnvVars);
474 }
475
476 g_options.amStarttestArgs = { "shell"_L1, "am"_L1, "start"_L1,
477 "-n"_L1, activityName,
478 "-e"_L1, "applicationArguments"_L1, testAppArgs,
479 testEnvVars
480 };
481
482 return true;
483}
484
485static int getPid(const QString &package)
486{
487 QByteArray output;
488 const QStringList psArgs = { "shell"_L1, "ps | grep ' %1'"_L1.arg(package) };
489 if (!execAdbCommand(psArgs, &output, false))
490 return false;
491
492 const QList<QByteArray> lines = output.split(u'\n');
493 if (lines.size() < 1)
494 return false;
495
496 QList<QByteArray> columns = lines.first().simplified().replace(u'\t', u' ').split(u' ');
497 if (columns.size() < 3)
498 return false;
499
500 bool ok = false;
501 int pid = columns.at(1).toInt(&ok);
502 if (ok)
503 return pid;
504
505 return -1;
506}
507
508static QString runCommandAsUserArgs(const QString &cmd)
509{
510 return "run-as %1 --user %2 %3"_L1.arg(g_options.package, g_testInfo.userId, cmd);
511}
512
513static bool isRunning() {
514 if (g_testInfo.pid < 1)
515 return false;
516
517 QByteArray output;
518 const QStringList psArgs = { "shell"_L1, "ps"_L1, "-p"_L1, QString::number(g_testInfo.pid),
519 "|"_L1, "grep"_L1, "-o"_L1, " %1$"_L1.arg(g_options.package) };
520 bool psSuccess = false;
521 for (int i = 1; i <= 3; ++i) {
522 psSuccess = execAdbCommand(psArgs, &output, false);
523 if (psSuccess)
524 break;
525 QThread::msleep(250);
526 }
527
528 return psSuccess && output.trimmed() == g_options.package.toUtf8();
529}
530
531static void waitForStarted()
532{
533 // wait to start and set PID
534 QDeadlineTimer startDeadline(10000);
535 do {
536 g_testInfo.pid = getPid(g_options.package);
537 if (g_testInfo.pid > 0)
538 break;
539 QThread::msleep(100);
540 } while (!startDeadline.hasExpired() && !g_testInfo.isTestRunnerInterrupted.load());
541}
542
544{
545 const QString lsCmd = "ls files/%1"_L1.arg(g_options.stdoutFileName);
546 const QStringList adbLsCmd = { "shell"_L1, runCommandAsUserArgs(lsCmd) };
547
548 QDeadlineTimer deadline(5000);
549 do {
550 if (execAdbCommand(adbLsCmd, nullptr, false))
551 break;
552 QThread::msleep(100);
553 } while (!deadline.hasExpired() && !g_testInfo.isTestRunnerInterrupted.load());
554}
555
556static bool setupStdoutLogger()
557{
558 // Start tail to get results to stdout as soon as they're available
559 const QString tailPipeCmd = "tail -n +1 -f files/%1"_L1.arg(g_options.stdoutFileName);
560 const QStringList adbTailCmd = { "shell"_L1, runCommandAsUserArgs(tailPipeCmd) };
561
562 g_options.stdoutLogger.emplace();
563 g_options.stdoutLogger->setProcessChannelMode(QProcess::ForwardedOutputChannel);
564 g_options.stdoutLogger->start(g_options.adbCommand, adbTailCmd);
565
566 if (!g_options.stdoutLogger->waitForStarted()) {
567 qCritical() << "Error: failed to run adb command to fetch stdout test results.";
568 g_options.stdoutLogger = std::nullopt;
569 return false;
570 }
571
572 return true;
573}
574
575static bool stopStdoutLogger()
576{
577 if (!g_options.stdoutLogger.has_value()) {
578 // In case this ever happens, it setupStdoutLogger() wasn't called, whether
579 // that's on purpose or not, return true since what it does is achieved.
580 qCritical() << "Trying to stop the stdout logger process while it's been uninitialised";
581 return true;
582 }
583
584 if (g_options.stdoutLogger->state() == QProcess::NotRunning) {
585 // We expect the tail command to be running until we stop it, so if it's
586 // not running it might have been terminated outside of the test runner.
587 qCritical() << "The stdout logger process was terminated unexpectedly, "
588 "It might have been terminated by an external process";
589 return false;
590 }
591
592 g_options.stdoutLogger->terminate();
593
594 if (!g_options.stdoutLogger->waitForFinished()) {
595 qCritical() << "Error: adb test results tail command timed out.";
596 return false;
597 }
598
599 return true;
600}
601
602static void waitForFinished()
603{
604 // Wait to finish
605 QDeadlineTimer finishedDeadline(g_options.timeoutSecs * 1000);
606 do {
607 if (!isRunning())
608 break;
609 QThread::msleep(250);
610 } while (!finishedDeadline.hasExpired() && !g_testInfo.isTestRunnerInterrupted.load());
611
612 if (finishedDeadline.hasExpired())
613 qWarning() << "Timed out while waiting for the test to finish";
614}
615
616static void obtainSdkVersion()
617{
618 // SDK version is necessary, as in SDK 23 pidof is broken, so we cannot obtain the pid.
619 // Also, Logcat cannot filter by pid in SDK 23, so we don't offer the --show-logcat option.
620 QByteArray output;
621 const QStringList versionArgs = { "shell"_L1, "getprop"_L1, "ro.build.version.sdk"_L1 };
622 execAdbCommand(versionArgs, &output, false);
623 bool ok = false;
624 int sdkVersion = output.toInt(&ok);
625 if (ok)
626 g_testInfo.sdkVersion = sdkVersion;
627 else
628 qCritical() << "Unable to obtain the SDK version of the target.";
629}
630
632{
633 // adb get-current-user command is available starting from API level 26.
634 QByteArray userId;
635 if (g_testInfo.sdkVersion >= 26) {
636 const QStringList userIdArgs = {"shell"_L1, "cmd"_L1, "activity"_L1, "get-current-user"_L1};
637 if (!execAdbCommand(userIdArgs, &userId, false)) {
638 qCritical() << "Error: failed to retrieve the user ID";
639 userId.clear();
640 }
641 }
642
643 if (userId.isEmpty())
644 userId = "0";
645
646 return QString::fromUtf8(userId.simplified());
647}
648
650{
651 QByteArray output;
652 execAdbCommand({ "devices"_L1 }, &output, false);
653
654 QStringList devices;
655 for (const QByteArray &line : output.split(u'\n')) {
656 if (line.contains("\tdevice"_L1))
657 devices.append(QString::fromUtf8(line.split(u'\t').first()));
658 }
659
660 return devices;
661}
662
663static bool pullResults()
664{
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();
668 // Get only stdout from cat and get rid of stderr and fail later if the output is empty
669 const QString catCmd = "cat files/%1 2> /dev/null"_L1.arg(fileName);
670 const QStringList fullCatArgs = { "shell"_L1, runCommandAsUserArgs(catCmd) };
671
672 bool catSuccess = false;
673 QByteArray output;
674
675 for (int i = 1; i <= g_options.resultsPullRetries; ++i) {
676 catSuccess = execAdbCommand(fullCatArgs, &output, false);
677 if (!catSuccess)
678 continue;
679 else if (!output.isEmpty())
680 break;
681 }
682
683 if (!catSuccess) {
684 qCritical() << "Error: failed to retrieve the test result file %1."_L1.arg(fileName);
685 return false;
686 }
687
688 if (output.isEmpty()) {
689 qCritical() << "Error: the test result file %1 is empty."_L1.arg(fileName);
690 return false;
691 }
692
693 QFile out{filePath};
694 if (!out.open(QIODevice::WriteOnly)) {
695 qCritical() << "Error: failed to open %1 to write results to host."_L1.arg(filePath);
696 return false;
697 }
698 out.write(output);
699 }
700
701 return true;
702}
703
705{
706 QString libsPath = "%1/libs/"_L1.arg(g_options.buildPath);
707 const QStringList abiArgs = { "shell"_L1, "getprop"_L1, "ro.product.cpu.abi"_L1 };
708 QByteArray abi;
709 if (!execAdbCommand(abiArgs, &abi, false)) {
710 QStringList subDirs = QDir(libsPath).entryList(QDir::Dirs | QDir::NoDotAndDotDot);
711 if (!subDirs.isEmpty())
712 abi = subDirs.first().toUtf8();
713 }
714
715 abi = abi.trimmed();
716 if (abi.isEmpty())
717 qWarning() << "Failed to get the libs abi, falling to host architecture";
718
719 QString hostArch = QSysInfo::currentCpuArchitecture();
720 if (hostArch == "x86_64"_L1)
721 abi = "arm64-x86_64";
722 else if (hostArch == "arm64"_L1)
723 abi = "arm64-v8a";
724 else if (hostArch == "i386"_L1)
725 abi = "x86";
726 else
727 abi = "armeabi-v7a";
728
729 return libsPath + QString::fromUtf8(abi);
730}
731
732void printLogcatCrash(const QByteArray &logcat)
733{
734 // No crash report, do nothing
735 if (logcat.isEmpty())
736 return;
737
738 QByteArray crashLogcat(logcat);
739 if (!g_options.ndkStackPath.isEmpty()) {
740 QProcess ndkStackProc;
741 ndkStackProc.start(g_options.ndkStackPath, { "-sym"_L1, getAbiLibsPath() });
742
743 if (ndkStackProc.waitForStarted()) {
744 ndkStackProc.write(crashLogcat);
745 ndkStackProc.closeWriteChannel();
746
747 if (ndkStackProc.waitForReadyRead())
748 crashLogcat = ndkStackProc.readAllStandardOutput();
749
750 ndkStackProc.terminate();
751 if (!ndkStackProc.waitForFinished())
752 qCritical() << "Error: ndk-stack command timed out.";
753 } else {
754 qCritical() << "Error: failed to run ndk-stack command.";
755 return;
756 }
757 } else {
758 qWarning() << "Warning: ndk-stack path not provided and couldn't be deduced "
759 "using the ANDROID_NDK_ROOT environment variable.";
760 }
761
762 if (!crashLogcat.startsWith("********** Crash dump")) {
763 qDebug() << "[androidtestrunner] ********** BEGIN crash dump **********";
764 qDebug().noquote() << crashLogcat.trimmed();
765 qDebug() << "[androidtestrunner] ********** END crash dump **********";
766 }
767}
768
769void analyseLogcat(const QString &timeStamp, int *exitCode)
770{
771 QStringList logcatArgs = { "shell"_L1, "logcat"_L1, "-t"_L1, "'%1'"_L1.arg(timeStamp),
772 "-v"_L1, "brief"_L1 };
773
774 const bool useColor = qEnvironmentVariable("QTEST_ENVIRONMENT") != "ci"_L1;
775 if (useColor)
776 logcatArgs << "-v"_L1 << "color"_L1;
777
778 QByteArray logcat;
779 if (!execAdbCommand(logcatArgs, &logcat, false)) {
780 qCritical() << "Error: failed to fetch logcat of the test";
781 return;
782 }
783
784 if (logcat.isEmpty()) {
785 qWarning() << "The retrieved logcat is empty";
786 return;
787 }
788
789 const QByteArray crashMarker("*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***");
790 int crashMarkerIndex = logcat.indexOf(crashMarker);
791 QByteArray crashLogcat;
792
793 if (crashMarkerIndex != -1) {
794 crashLogcat = logcat.mid(crashMarkerIndex);
795 logcat = logcat.left(crashMarkerIndex);
796 }
797
798 // Check for ANRs
799 const bool anrOccurred = logcat.contains("ANR in %1"_L1.arg(g_options.package).toUtf8());
800 if (anrOccurred) {
801 // Rather improbable, but if the test managed to return a non-crash exitcode then overwrite
802 // it to signify that something blew up. Same if we didn't manage to collect an exit code.
803 // Preserve all other exitcodes, they might be useful crash information from the device.
804 if (isTestExitCodeNormal(*exitCode) || *exitCode == EXIT_NOEXITCODE)
805 *exitCode = EXIT_ANR;
806 qCritical("[androidtestrunner] An ANR has occurred while running the test '%s';"
807 " consult logcat for additional logs from the system_server process",
808 qPrintable(g_options.package));
809 }
810
811 int systemServerPid = getPid("system_server"_L1);
812
813 static const QRegularExpression logcatRegEx{
814 "(?:^\\x1B\\‍[[0-9]+m)?" // color
815 "(\\w)/" // message type 1. capture
816 ".*" // source
817 "(\\‍(\\s*\\d*\\‍)):" // pid 2. capture
818 "\\s*"
819 ".*" // message
820 "(?:\\x1B\\‍[[0-9]+m)?" // color
821 "[\\n\\r]*$"_L1
822 };
823
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);
835 } else {
836 // If we can't match then just print everything
837 testLogcat.append(line);
838 }
839 }
840
841 // If we have an unpredictable exitcode, possibly a crash, attempt to print both logcat and the
842 // crash buffer which includes the crash stacktrace that is not included in the default logcat.
843 const bool testCrashed = ( !isTestExitCodeNormal(*exitCode)
844 && !g_testInfo.isTestRunnerInterrupted.load());
845 if (testCrashed) {
846 qDebug() << "[androidtestrunner] ********** BEGIN logcat dump **********";
847 qDebug().noquote() << testLogcat.join(u'\n').trimmed();
848 qDebug() << "[androidtestrunner] ********** END logcat dump **********";
849
850 if (!crashLogcat.isEmpty())
851 printLogcatCrash(crashLogcat);
852 }
853}
854
856{
857 const QString timeFormat = (g_testInfo.sdkVersion <= 23) ?
858 "%m-%d %H:%M:%S.000"_L1 : "%Y-%m-%d %H:%M:%S.%3N"_L1;
859
860 QStringList dateArgs = { "shell"_L1, "date"_L1, "+'%1'"_L1.arg(timeFormat) };
861 QByteArray output;
862 if (!execAdbCommand(dateArgs, &output, false)) {
863 qWarning() << "[androidtestrunner] ERROR in command: adb shell date";
864 return {};
865 }
866
867 return QString::fromUtf8(output.simplified());
868}
869
870static int testExitCode()
871{
872 QByteArray exitCodeOutput;
873 const QString exitCodeCmd = "cat files/qtest_last_exit_code 2> /dev/null"_L1;
874 if (!execAdbCommand({ "shell"_L1, runCommandAsUserArgs(exitCodeCmd) }, &exitCodeOutput, false)) {
875 qCritical() << "[androidtestrunner] ERROR in command: adb shell cat files/qtest_last_exit_code";
876 return EXIT_NOEXITCODE;
877 }
878 qDebug() << "[androidtestrunner] Test exitcode: " << exitCodeOutput;
879
880 bool ok;
881 int exitCode = exitCodeOutput.toInt(&ok);
882
883 return ok ? exitCode : EXIT_NOEXITCODE;
884}
885
887{
888 return execAdbCommand({ "uninstall"_L1, g_options.package }, nullptr);
889}
890
892{
895
896 void acquire() { isAcquired.store(semaphore.acquire()); }
897
898 void release()
899 {
900 bool expected = true;
901 // NOTE: There's still could be tiny time gap between the compare_exchange_strong() call
902 // and release() call where the thread could be interrupted, if that's ever an issue,
903 // this code could be checked and improved further.
904 if (isAcquired.compare_exchange_strong(expected, false))
905 isAcquired.store(!semaphore.release());
906 }
907
908 std::atomic<bool> isAcquired { false };
911};
912
914
915void sigHandler(int signal)
916{
917 std::signal(signal, SIG_DFL);
919 // Ideally we shouldn't be doing such calls from a signal handler,
920 // and we can't use QSocketNotifier because this tool doesn't spin
921 // a main event loop. Since, there's no other alternative to do this,
922 // let's do the cleanup anyway.
923 if (!g_testInfo.isPackageInstalled.load())
924 _exit(EXIT_ERROR);
925 g_testInfo.isTestRunnerInterrupted.store(true);
926}
927
928int main(int argc, char *argv[])
929{
930 std::signal(SIGINT, sigHandler);
931 std::signal(SIGTERM, sigHandler);
932
933 QCoreApplication a(argc, argv);
934 if (!parseOptions()) {
936 return EXIT_ERROR;
937 }
938
939 if (g_options.makeCommand.isEmpty()) {
940 qCritical() << "It is required to provide a make command with the \"--make\" parameter "
941 "to generate the apk.";
942 return EXIT_ERROR;
943 }
944
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());
949 return EXIT_ERROR;
950 }
951
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.",
955 qPrintable(g_options.packagePath));
956 return EXIT_ERROR;
957 }
958
959 const QStringList devices = runningDevices();
960 if (devices.isEmpty()) {
961 qCritical("No connected devices or running emulators can be found.");
962 return EXIT_ERROR;
963 } else if (!g_options.serial.isEmpty() && !devices.contains(g_options.serial)) {
964 qCritical("No connected device or running emulator with serial '%s' can be found.",
965 qPrintable(g_options.serial));
966 return EXIT_ERROR;
967 }
968
970
971 g_testInfo.userId = userId();
972
973 if (!QFile::exists(g_options.manifestPath)) {
974 qCritical("Unable to find '%s'.", qPrintable(g_options.manifestPath));
975 return EXIT_ERROR;
976 }
977
979 return EXIT_ERROR;
980
981 if (g_options.package.isEmpty()) {
982 qCritical("Unable to get package name for '%s'", qPrintable(g_options.packagePath));
983 return EXIT_ERROR;
984 }
985
986 // parseTestArgs depends on g_options.package
987 if (!parseTestArgs())
988 return EXIT_ERROR;
989
990 // do not install or run packages while another test is running
992
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));
996 if (!g_testInfo.isPackageInstalled)
997 return EXIT_ERROR;
998 } else if (g_options.packagePath.endsWith(".aab"_L1)) {
999 QFileInfo aab(g_options.packagePath);
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 }))
1004 return EXIT_ERROR;
1005
1006 if (!execBundletoolCommand({ "install-apks"_L1, "--apks"_L1, apksFilePath }))
1007 return EXIT_ERROR;
1008 }
1009
1010 const QStringList dangerousPermisisons = queryDangerousPermissions();
1011 for (const auto &permission : g_options.permissions) {
1012 if (!dangerousPermisisons.contains(permission))
1013 continue;
1014
1015 if (!execAdbCommand({ "shell"_L1, "pm"_L1, "grant"_L1, g_options.package, permission },
1016 nullptr)) {
1017 qWarning("Unable to grant '%s' to '%s'. Probably the Android version mismatch.",
1018 qPrintable(permission), qPrintable(g_options.package));
1019 }
1020 }
1021
1022 // Call additional adb command if set after installation and before starting the test
1023 for (const auto &command : g_options.preTestRunAdbCommands) {
1024 QByteArray output;
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());
1028 return EXIT_ERROR;
1029 }
1030 }
1031
1032 // Pre test start
1033 const QString formattedStartTime = getCurrentTimeString();
1034
1035 // Start the test
1036 if (!execAdbCommand(g_options.amStarttestArgs, nullptr))
1037 return EXIT_ERROR;
1038
1041
1043 return EXIT_ERROR;
1044
1046
1047 // Post test run
1048 if (!stopStdoutLogger())
1049 return EXIT_ERROR;
1050
1051 int exitCode = testExitCode();
1052
1054 analyseLogcat(formattedStartTime, &exitCode);
1055
1056 const bool pullRes = pullResults();
1057 if (!pullRes && isTestExitCodeNormal(exitCode))
1058 exitCode = EXIT_NORESULTS;
1059
1061 return EXIT_ERROR;
1062
1064
1065 if (g_testInfo.isTestRunnerInterrupted.load()) {
1066 qCritical() << "The androidtestrunner was interrupted and the was test cleaned up.";
1067 return EXIT_ERROR;
1068 }
1069
1070 return exitCode;
1071}
static QString userId()
Definition main.cpp:631
static QString runCommandAsUserArgs(const QString &cmd)
Definition main.cpp:508
static void waitForLoggingStarted()
Definition main.cpp:543
static bool setupStdoutLogger()
Definition main.cpp:556
static QString getCurrentTimeString()
Definition main.cpp:855
static int getPid(const QString &package)
Definition main.cpp:485
static QString getAbiLibsPath()
Definition main.cpp:704
static int testExitCode()
Definition main.cpp:870
static void setOutputFile(QString file, QString format)
Definition main.cpp:394
static bool parseTestArgs()
Definition main.cpp:412
static TestInfo g_testInfo
Definition main.cpp:81
static bool isRunning()
Definition main.cpp:513
static bool pullResults()
Definition main.cpp:663
static void obtainSdkVersion()
Definition main.cpp:616
static bool execAdbCommand(const QStringList &args, QByteArray *output=nullptr, bool verbose=true)
Definition main.cpp:123
void sigHandler(int signal)
Definition main.cpp:915
static void setPackagePath(const QString &path)
Definition main.cpp:143
static constexpr int EXIT_NOEXITCODE
Definition main.cpp:37
static bool execBundletoolCommand(const QStringList &args, QByteArray *output=nullptr, bool verbose=true)
Definition main.cpp:135
static constexpr int HIGHEST_QTEST_EXITCODE
Definition main.cpp:33
static void waitForFinished()
Definition main.cpp:602
static constexpr int EXIT_NORESULTS
Definition main.cpp:39
static bool parseOptions()
Definition main.cpp:161
void printLogcatCrash(const QByteArray &logcat)
Definition main.cpp:732
static bool isTestExitCodeNormal(const int ec)
Definition main.cpp:85
static QStringList queryDangerousPermissions()
Definition main.cpp:347
static constexpr int EXIT_ERROR
Definition main.cpp:35
TestRunnerSystemSemaphore testRunnerLock
Definition main.cpp:913
void analyseLogcat(const QString &timeStamp, int *exitCode)
Definition main.cpp:769
static void printHelp()
Definition main.cpp:269
static bool execCommand(const QString &command, QByteArray *output=nullptr, bool verbose=true)
Definition main.cpp:153
static void waitForStarted()
Definition main.cpp:531
static bool execCommand(const QString &program, const QStringList &args, QByteArray *output=nullptr, bool verbose=false)
Definition main.cpp:90
static bool stopStdoutLogger()
Definition main.cpp:575
static bool uninstallTestPackage()
Definition main.cpp:886
static bool processAndroidManifest()
Definition main.cpp:323
static constexpr int EXIT_ANR
Definition main.cpp:38
static Options g_options
Definition main.cpp:69
static QStringList runningDevices()
Definition main.cpp:649
int main(int argc, char *argv[])
[ctor_close]
QString makeCommand
Definition main.cpp:54
QStringList amStarttestArgs
Definition main.cpp:61
std::optional< QProcess > stdoutLogger
Definition main.cpp:66
QHash< QString, QString > outFiles
Definition main.cpp:60
QString adbCommand
Definition main.cpp:51
bool helpRequested
Definition main.cpp:129
int timeoutSecs
Definition main.cpp:47
QStringList permissions
Definition main.cpp:57
bool showLogcatOutput
Definition main.cpp:65
QString bundletoolPath
Definition main.cpp:52
int resultsPullRetries
Definition main.cpp:48
QString stdoutFileName
Definition main.cpp:59
bool skipAddInstallRoot
Definition main.cpp:46
QList< QStringList > preTestRunAdbCommands
Definition main.cpp:64
QString package
Definition main.cpp:55
QString serial
Definition main.cpp:53
QString packagePath
Definition main.cpp:62
bool verbose
Definition main.cpp:130
QString ndkStackPath
Definition main.cpp:63
QStringList testArgsList
Definition main.cpp:58
QString manifestPath
Definition main.cpp:50
QString activity
Definition main.cpp:56
QString buildPath
Definition main.cpp:49
QString userId
Definition main.cpp:75
std::atomic< bool > isPackageInstalled
Definition main.cpp:77
int sdkVersion
Definition main.cpp:73
int pid
Definition main.cpp:74
std::atomic< bool > isTestRunnerInterrupted
Definition main.cpp:78
QSystemSemaphore semaphore
Definition main.cpp:909
std::atomic< bool > isAcquired
Definition main.cpp:908