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/QCommandLineParser>
6#include <QtCore/QCoreApplication>
7#include <QtCore/QDeadlineTimer>
8#include <QtCore/qdebug.h>
9#include <QtCore/QDir>
10#include <QtCore/QHash>
11#include <QtCore/QLockFile>
12#include <QtCore/QProcess>
13#include <QtCore/QProcessEnvironment>
14#include <QtCore/QRegularExpression>
15#include <QtCore/QSet>
16#include <QtCore/QStandardPaths>
17#include <QtCore/QThread>
18#include <QtCore/QXmlStreamReader>
19#include <QtCore/QFileInfo>
20#include <QtCore/QTemporaryFile>
21
22#include <atomic>
23#include <chrono>
24#include <csignal>
25#include <QtCore/qxpfunctional.h>
26#include <optional>
27
28#if defined(Q_OS_WIN32)
29#include <process.h>
30#else
31#include <unistd.h>
32#endif
33
34using namespace Qt::StringLiterals;
35
36
37// QTest-based test processes may exit with up to 127 for normal test failures
38static constexpr int HIGHEST_QTEST_EXITCODE = 127;
39// Something went wrong in androidtestrunner, in general
40static constexpr int EXIT_ERROR = 254;
41// More specific exit codes for failures in androidtestrunner:
42static constexpr int EXIT_NOEXITCODE = 253; // Failed to transfer exit code from device
43static constexpr int EXIT_ANR = 252; // Android ANR error (Application Not Responding)
44static constexpr int EXIT_NORESULTS = 251; // Failed to transfer result files from device
45static constexpr int EXIT_DEVICE_GONE = 250; // Device disconnected mid-test
46
47
48struct Options
49{
50 bool verbose = false;
51 bool skipAddInstallRoot = false;
52 int timeoutSecs = 600; // 10 minutes
70 bool showLogcatOutput = false;
72};
73
75
77{
78 int sdkVersion = -1;
79 int pid = -1;
81
82 std::atomic<bool> isPackageInstalled { false };
83 std::atomic<bool> isTestRunnerInterrupted { false };
84 std::atomic<bool> deviceGone { false };
86};
87
88// sigHandler reads/writes these from arbitrary signal contexts.
89static_assert(std::atomic<bool>::is_always_lock_free);
90static_assert(std::atomic<qint64>::is_always_lock_free);
91
93
94// QTest-based processes return 0 if all tests PASSed, or the number of FAILs up to 127.
95// Other exitcodes signify abnormal termination and are system-dependent.
96static bool isTestExitCodeNormal(const int ec)
97{
98 return (ec >= 0 && ec <= HIGHEST_QTEST_EXITCODE);
99}
100
101static QByteArray execCommand(const QString &program, const QStringList &args, bool verbose = false,
102 std::chrono::milliseconds timeout = std::chrono::milliseconds(-1))
103{
104 const auto command = program + " "_L1 + args.join(u' ');
105
106 if (verbose && g_options.verbose)
107 fprintf(stdout,"Execute %s.\n", command.toUtf8().constData());
108
109 QProcess process;
110 process.start(program, args);
111 if (!process.waitForStarted()) {
112 qCritical("Cannot execute command %s.", qPrintable(command));
113 return QByteArray();
114 }
115
116 const bool finished = timeout.count() < 0
117 ? process.waitForFinished()
118 : process.waitForFinished(static_cast<int>(timeout.count()));
119 if (!finished) {
120 qCritical("Execution of command %s timed out.", qPrintable(command));
121 process.kill();
122 process.waitForFinished();
123 // Beyond the kill's own "killed by signal", stderr may hint at the hang.
124 const QByteArray stdErr = process.readAllStandardError();
125 if (!stdErr.isEmpty())
126 qWarning().noquote() << stdErr.trimmed();
127 return QByteArray();
128 }
129
130 const auto stdOut = process.readAllStandardOutput();
131 const auto stdErr = process.readAllStandardError();
132
133 if (process.exitCode() != 0) {
134 // Surface why a notable (verbose) command failed; quiet polls stay quiet.
135 if (verbose && !stdOut.isEmpty())
136 qWarning().noquote() << stdOut.trimmed();
137 if (!stdErr.isEmpty())
138 qWarning().noquote() << stdErr.trimmed();
139 return QByteArray();
140 }
141
142 if (verbose && g_options.verbose)
143 fprintf(stdout, "%s\n", stdOut.constData());
144
145 // Non-null even when empty so a silent success is distinct from failure.
146 return stdOut.isNull() ? QByteArray("") : stdOut;
147}
148
149static QStringList adbArgsWithSerial(const QStringList &args)
150{
151 if (g_options.serial.isEmpty())
152 return args;
153 return QStringList{ "-s"_L1, g_options.serial } + args;
154}
155
156static QByteArray execAdbCommand(const QStringList &args, bool verbose = true)
157{
158 return execCommand(g_options.adbCommand, adbArgsWithSerial(args), verbose);
159}
160
161static QByteArray execBundletoolCommand(const QStringList &args, bool verbose = true)
162{
163 QString java("java"_L1);
164 QStringList argsFull = QStringList() << "-jar"_L1 << g_options.bundletoolPath << args;
165 return execCommand(java, argsFull, verbose);
166}
167
168static bool setPackagePath(const QString &path)
169{
170 if (!g_options.packagePath.isEmpty()) {
171 qCritical("Only one of --apk or --aab may be set, and only once.");
172 return false;
173 }
174 g_options.packagePath = path;
175 return true;
176}
177
178static bool collectPackagePaths(const QStringList &apkValues, const QStringList &aabValues)
179{
180 if (apkValues.size() > 1) {
181 qCritical("--apk specified %lld times; only one APK path is supported.",
182 static_cast<long long>(apkValues.size()));
183 return false;
184 }
185 if (aabValues.size() > 1) {
186 qCritical("--aab specified %lld times; only one AAB path is supported.",
187 static_cast<long long>(aabValues.size()));
188 return false;
189 }
190 if (!apkValues.isEmpty() && !setPackagePath(apkValues.first()))
191 return false;
192 if (!aabValues.isEmpty() && !setPackagePath(aabValues.first()))
193 return false;
194 return true;
195}
196
197static QByteArray execCommand(const QString &command, bool verbose = true,
198 std::chrono::milliseconds timeout = std::chrono::milliseconds(-1))
199{
200 auto args = QProcess::splitCommand(command);
201 const auto program = args.takeFirst();
202 return execCommand(program, args, verbose, timeout);
203}
204
205// Split argv into our and test options, then re-join as "<ours> -- <test args>".
206static QStringList splitOwnAndTestArgs(const QStringList &args,
207 const QSet<QString> &knownOpts,
208 const QSet<QString> &valueOpts)
209{
210 QStringList ownArgs;
211 QStringList testArgs;
212 ownArgs.reserve(args.size() + 1);
213 for (int i = 0; i < args.size(); ++i) {
214 const QString &arg = args.at(i);
215 if (i == 0) {
216 ownArgs << arg;
217 continue;
218 }
219 if (arg == "--"_L1) {
220 // Caller already split things explicitly, so forward the rest as-is.
221 for (int j = i + 1; j < args.size(); ++j)
222 testArgs << args.at(j);
223 break;
224 }
225 if (arg.startsWith("--"_L1) && arg.size() > 2) {
226 QString name = arg.mid(2);
227 const qsizetype eqIdx = name.indexOf(u'=');
228 if (eqIdx != -1)
229 name.truncate(eqIdx);
230 if (!knownOpts.contains(name)) {
231 qCritical("Unknown option '--%s'. Use '--' to forward test arguments.",
232 qPrintable(name));
233 return {};
234 }
235 ownArgs << arg;
236 if (eqIdx == -1 && valueOpts.contains(name)) {
237 if (i + 1 >= args.size()) {
238 qCritical("Option --%s requires a value.", qPrintable(name));
239 return {};
240 }
241 ownArgs << args.at(++i);
242 }
243 continue;
244 }
245 // Keep scanning, because CMake appends our flags after the test's flags.
246 testArgs << arg;
247 }
248
249 if (!testArgs.isEmpty()) {
250 ownArgs << "--"_L1;
251 ownArgs += testArgs;
252 }
253 return ownArgs;
254}
255
257{
258#ifdef Q_OS_WIN
259 const QStringList candidates = { "ndk-stack.cmd"_L1, "ndk-stack.bat"_L1, "ndk-stack"_L1 };
260#else
261 const QStringList candidates = { "ndk-stack"_L1 };
262#endif
263 for (const char *var : { "ANDROID_NDK_ROOT", "ANDROID_NDK_HOME" }) {
264 const QString ndkPath = qEnvironmentVariable(var);
265 if (ndkPath.isEmpty())
266 continue;
267 for (const QString &name : candidates) {
268 const QString path = ndkPath + QDir::separator() + name;
269 if (QFile::exists(path))
270 return path;
271 }
272 }
273 return {};
274}
275
276static bool parseOptions()
277{
278 QCommandLineParser parser;
279 parser.setApplicationDescription(
280 "Runs a Qt for Android test on an emulator or a device. Specify a "
281 "device via --serial, ANDROID_SERIAL or ANDROID_DEVICE_SERIAL.\n"
282 "\n"
283 "Exit codes:\n"
284 " 0-127 QTest exit code (number of failed test functions)\n"
285 " 250 EXIT_DEVICE_GONE device disconnected\n"
286 " 251 EXIT_NORESULTS result files could not be pulled\n"
287 " 252 EXIT_ANR Android Not Responding\n"
288 " 253 EXIT_NOEXITCODE exit code could not be read from device\n"
289 " 254 EXIT_ERROR generic runner failure"_L1);
290
291 QCommandLineOption pathOpt(
292 "path"_L1, "Path where the Android Gradle package is built."_L1, "path"_L1);
293 QCommandLineOption makeOpt(
294 "make"_L1, "make command to build the APK."_L1, "command"_L1);
295 QCommandLineOption apkOpt(
296 "apk"_L1, "Test APK path. Built via --make if absent."_L1, "path"_L1);
297 QCommandLineOption aabOpt(
298 "aab"_L1, "Test AAB path. Built via --make if absent."_L1, "path"_L1);
299 QCommandLineOption bundletoolOpt(
300 "bundletool"_L1, "Path to Android bundletool jar."_L1, "path"_L1);
301 QCommandLineOption manifestOpt(
302 "manifest"_L1, "Custom AndroidManifest.xml path."_L1, "path"_L1);
303 QCommandLineOption adbOpt(
304 "adb"_L1, "Path to adb. Falls back to $PATH."_L1, "command"_L1, "adb"_L1);
305 QCommandLineOption serialOpt(
306 "serial"_L1,
307 "Android device serial. Overrides ANDROID_SERIAL / ANDROID_DEVICE_SERIAL."_L1,
308 "serial"_L1);
309 QCommandLineOption activityOpt(
310 "activity"_L1, "Activity to run. Defaults to the first in the manifest."_L1, "name"_L1);
311 QCommandLineOption timeoutOpt(
312 "timeout"_L1, "Test timeout in seconds (default 600)."_L1, "seconds"_L1, "600"_L1);
313 QCommandLineOption preTestAdbOpt("pre-test-adb-command"_L1,
314 "adb command to run after install, before test."_L1, "command"_L1);
315 QCommandLineOption skipInstallRootOpt(
316 "skip-install-root"_L1,
317 "Don't append INSTALL_ROOT=... to --make. Only honored for make-family "
318 "tools (make, gmake, nmake, mingw32-make, jom)."_L1);
319 QCommandLineOption ndkStackOpt(
320 "ndk-stack"_L1, "Path to ndk-stack (default: ndk-stack under $ANDROID_NDK_ROOT "
321 "or $ANDROID_NDK_HOME)."_L1, "path"_L1);
322 QCommandLineOption showLogcatOpt(
323 "show-logcat"_L1, "Print logcat output (+ system_server on ANR)."_L1);
324 QCommandLineOption verboseOpt(
325 "verbose"_L1, "Print extra information."_L1);
326 QCommandLineOption helpOpt("help"_L1, "Show this help message."_L1);
327
328 const QList<QCommandLineOption> ourOptions = {
329 pathOpt, makeOpt, apkOpt, aabOpt,
330 bundletoolOpt, manifestOpt,
331 adbOpt, serialOpt,
332 activityOpt, timeoutOpt, preTestAdbOpt,
333 skipInstallRootOpt,
334 ndkStackOpt, showLogcatOpt, verboseOpt,
335 helpOpt,
336 };
337 parser.addOptions(ourOptions);
338 parser.addPositionalArgument("testargs"_L1, "Arguments forwarded to the test."_L1,
339 "[-- TESTARGS]"_L1);
340
341 QSet<QString> knownOpts;
342 QSet<QString> valueOpts;
343 for (const QCommandLineOption &opt : ourOptions) {
344 const bool takesValue = !opt.valueName().isEmpty();
345 for (const QString &name : opt.names()) {
346 knownOpts.insert(name);
347 if (takesValue)
348 valueOpts.insert(name);
349 }
350 }
351
352 const QStringList processedArgs =
353 splitOwnAndTestArgs(QCoreApplication::arguments(), knownOpts, valueOpts);
354 if (processedArgs.isEmpty())
355 return false;
356 if (!parser.parse(processedArgs)) {
357 qCritical("%s", qPrintable(parser.errorText()));
358 return false;
359 }
360 if (parser.isSet(helpOpt))
361 parser.showHelp(0);
362
363 g_options.buildPath = parser.value(pathOpt);
364 g_options.makeCommand = parser.value(makeOpt);
365 if (!collectPackagePaths(parser.values(apkOpt), parser.values(aabOpt)))
366 return false;
367 g_options.adbCommand = parser.value(adbOpt);
368 g_options.activity = parser.value(activityOpt);
369 g_options.serial = parser.value(serialOpt);
370 bool timeoutOk = false;
371 g_options.timeoutSecs = parser.value(timeoutOpt).toInt(&timeoutOk);
372 if (!timeoutOk || g_options.timeoutSecs <= 0) {
373 qCritical("--timeout must be a positive integer (got '%s').",
374 qPrintable(parser.value(timeoutOpt)));
375 return false;
376 }
377 g_options.skipAddInstallRoot = parser.isSet(skipInstallRootOpt);
378 g_options.showLogcatOutput = parser.isSet(showLogcatOpt);
379 g_options.ndkStackPath = parser.value(ndkStackOpt);
380 g_options.verbose = parser.isSet(verboseOpt);
381 g_options.manifestPath = parser.value(manifestOpt);
382 g_options.bundletoolPath = parser.value(bundletoolOpt);
383
384 for (const QString &cmd : parser.values(preTestAdbOpt))
385 g_options.preTestRunAdbCommands += QProcess::splitCommand(cmd);
386 g_options.testArgsList = parser.positionalArguments();
387
388 static const QStringList makeNames = {
389 "make"_L1, "gmake"_L1, "nmake"_L1, "mingw32-make"_L1, "jom"_L1,
390 };
391 const QStringList makeParts = QProcess::splitCommand(g_options.makeCommand);
392 const QString makeBaseName = QFileInfo(makeParts.value(0)).baseName();
393 const bool makeIsGnuMakeFamily = makeNames.contains(makeBaseName, Qt::CaseInsensitive);
394 if (!g_options.skipAddInstallRoot && makeIsGnuMakeFamily) {
395 g_options.makeCommand = "%1 INSTALL_ROOT=%2 install"_L1
396 .arg(g_options.makeCommand, QDir::toNativeSeparators(g_options.buildPath));
397 }
398
399 if (g_options.buildPath.isEmpty() || g_options.packagePath.isEmpty()) {
400 qCritical("--path and --apk (or --aab) are required.");
401 fputs(qPrintable(parser.helpText()), stderr);
402 return false;
403 }
404
405 if (!g_options.packagePath.endsWith(".apk"_L1)
406 && !g_options.packagePath.endsWith(".aab"_L1)) {
407 qCritical("Package path '%s' must end with .apk or .aab.",
408 qPrintable(g_options.packagePath));
409 return false;
410 }
411
412 if (g_options.packagePath.endsWith(".aab"_L1)) {
413 if (g_options.bundletoolPath.isEmpty()) {
414 qCritical("--aab requires --bundletool to locate the bundletool jar.");
415 return false;
416 }
417 if (!QFile::exists(g_options.bundletoolPath)) {
418 qCritical("--bundletool path '%s' does not exist.",
419 qPrintable(g_options.bundletoolPath));
420 return false;
421 }
422 }
423
424 if (g_options.serial.isEmpty())
425 g_options.serial = qEnvironmentVariable("ANDROID_SERIAL");
426 if (g_options.serial.isEmpty())
427 g_options.serial = qEnvironmentVariable("ANDROID_DEVICE_SERIAL");
428
429 if (g_options.ndkStackPath.isEmpty())
430 g_options.ndkStackPath = ndkStackPath();
431
432 return true;
433}
434
436{
437 static QString path;
438 if (!path.isEmpty())
439 return path;
440
441 QTemporaryFile initScript;
442 initScript.setAutoRemove(false);
443 if (!initScript.open())
444 return {};
445
446 initScript.write(
447 "gradle.projectsEvaluated {\n"
448 " def prop = gradle.rootProject.findProperty(\"property\")\n"
449 " def target = gradle.rootProject.findProject(':app') ?: gradle.rootProject\n"
450 " for (part in prop.tokenize('.')) {\n"
451 " target = target.\"${part}\"\n"
452 " }\n"
453 " println target\n"
454 "}\n"
455 "rootProject { tasks.register(\"printProjectProperty\") }\n");
456 initScript.close();
457
458 path = initScript.fileName();
459 qAddPostRoutine([] { QFile::remove(path); });
460 return path;
461}
462
463// Query a Gradle project's property using dot-separated path notation e.g. "android.namespace".
464static QString getGradleProjectProperty(const QString &androidBuildDir, const QString &property)
465{
466#ifdef Q_OS_WIN
467 QString gradlew = androidBuildDir + "/gradlew.bat"_L1;
468#else
469 QString gradlew = androidBuildDir + "/gradlew"_L1;
470#endif
471 if (!QFile::exists(gradlew))
472 return {};
473
474 const QString scriptPath = gradleInitScriptPath();
475 if (scriptPath.isEmpty())
476 return {};
477
478 QProcess process;
479 process.setWorkingDirectory(androidBuildDir);
480 process.start(gradlew, { "-q"_L1, "--init-script"_L1, scriptPath,
481 "-Pproperty="_L1 + property, "printProjectProperty"_L1 });
482
483 if (!process.waitForFinished()) {
484 qWarning("gradlew '%s' timed out.", qPrintable(property));
485 process.kill();
486 process.waitForFinished();
487 return {};
488 }
489 if (process.exitCode() != 0) {
490 qWarning("gradlew '%s' exited with %d:\n%s", qPrintable(property),
491 process.exitCode(), process.readAllStandardError().constData());
492 return {};
493 }
494 return QString::fromUtf8(process.readAllStandardOutput()).trimmed();
495}
496
498{
499 if (!g_options.manifestPath.isEmpty()) {
500 if (!QFile::exists(g_options.manifestPath)) {
501 qCritical("--manifest path '%s' does not exist.",
502 qPrintable(g_options.manifestPath));
503 return false;
504 }
505 } else {
506 const QStringList candidates = {
507 g_options.buildPath + "/AndroidManifest.xml"_L1,
508 g_options.buildPath + "/app/AndroidManifest.xml"_L1
509 };
510 for (const QString &candidate : candidates) {
511 if (QFile::exists(candidate)) {
512 g_options.manifestPath = candidate;
513 break;
514 }
515 }
516 }
517 if (g_options.manifestPath.isEmpty()) {
518 qCritical("Unable to find AndroidManifest.xml at '%s'.", qPrintable(g_options.buildPath));
519 return false;
520 }
521 QFile androidManifestXml(g_options.manifestPath);
522 if (!androidManifestXml.open(QIODevice::ReadOnly)) {
523 qCritical("Unable to read android manifest '%s'", qPrintable(g_options.manifestPath));
524 return false;
525 }
526
527 QXmlStreamReader reader(&androidManifestXml);
528 while (!reader.atEnd()) {
529 reader.readNext();
530 if (!reader.isStartElement())
531 continue;
532
533 if (reader.name() == "activity"_L1 && g_options.activity.isEmpty())
534 g_options.activity = reader.attributes().value("android:name"_L1).toString();
535 else if (reader.name() == "uses-permission"_L1)
536 g_options.permissions.append(reader.attributes().value("android:name"_L1).toString());
537 }
538 return true;
539}
540
542{
543 const QStringList args({ "shell"_L1, "dumpsys"_L1, "package"_L1, "permissions"_L1 });
544 const QByteArray output = execAdbCommand(args, false);
545 if (output.isNull()) {
546 qWarning("Failed to query permissions via dumpsys");
547 return {};
548 }
549
550 /*
551 * Permissions section from this command look like:
552 *
553 * Permission [android.permission.INTERNET] (c8cafdc):
554 * sourcePackage=android
555 * uid=1000 gids=[3003] type=0 prot=normal|instant
556 * perm=PermissionInfo{5f5bfbb android.permission.INTERNET}
557 * flags=0x0
558 */
559 const static QRegularExpression regex("^\\s*Permission\\s+\\‍[([^\\‍]]+)\\‍]\\s+\\‍([^)]+\\‍):"_L1);
560 QStringList dangerousPermissions;
561 QString currentPerm;
562
563 const QStringList lines = QString::fromUtf8(output).split(u'\n');
564 for (const QString &line : lines) {
565 QRegularExpressionMatch match = regex.match(line);
566 if (match.hasMatch()) {
567 currentPerm = match.captured(1);
568 continue;
569 }
570
571 if (currentPerm.isEmpty())
572 continue;
573
574 int protIndex = line.indexOf("prot="_L1);
575 if (protIndex == -1)
576 continue;
577
578 QString protectionTypes = line.mid(protIndex + 5).trimmed();
579 if (protectionTypes.contains("dangerous"_L1, Qt::CaseInsensitive))
580 dangerousPermissions.append(currentPerm);
581 currentPerm.clear();
582 }
583
584 return dangerousPermissions;
585}
586
587static QString deviceOutputFileName(const QString &format, const QString &hostPath)
588{
589 return hostPath.isEmpty() ? "stdout.%1"_L1.arg(format) : QFileInfo(hostPath).fileName();
590}
591
592static void setOutputFile(QString file, QString format)
593{
594 if (format.isEmpty())
595 format = "txt"_L1;
596 if (file == u'-')
597 file.clear();
598
599 if (file.isEmpty()) {
600 if (!g_options.outFiles.contains(format))
601 g_options.outFiles.insert(format, QString());
602 g_options.stdoutFileName = deviceOutputFileName(format, g_options.outFiles.value(format));
603 } else {
604 g_options.outFiles[format] = file;
605 }
606}
607
608static bool parseTestArgs()
609{
610 QRegularExpression oldFormats{"^-(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
611 QRegularExpression newLoggingFormat{"^(.*),(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
612
613 QString file;
614 QString logType;
615 QStringList unhandledArgs;
616 for (int i = 0; i < g_options.testArgsList.size(); ++i) {
617 const QString &arg = g_options.testArgsList[i].trimmed();
618 if (arg == "--"_L1)
619 continue;
620 if (arg == "-o"_L1 || arg == "--output"_L1) {
621 if (i >= g_options.testArgsList.size() - 1)
622 return false; // missing file argument
623
624 const auto &filePath = g_options.testArgsList[++i];
625 const auto match = newLoggingFormat.match(filePath);
626 if (!match.hasMatch()) {
627 file = filePath;
628 } else {
629 const auto capturedTexts = match.capturedTexts();
630 setOutputFile(capturedTexts.at(1), capturedTexts.at(2));
631 }
632 } else {
633 auto match = oldFormats.match(arg);
634 if (match.hasMatch()) {
635 logType = match.capturedTexts().at(1);
636 } else {
637 // Use triple literal quotes so that QProcess::splitCommand() in androidjnimain.cpp
638 // keeps quotes characters inside the string.
639 QString quotedArg = QString(arg).replace("\""_L1, "\\\"\\\"\\\""_L1);
640 // Add escaped double quote character so that args with spaces are treated as one.
641 unhandledArgs << " \\\"%1\\\""_L1.arg(quotedArg);
642 }
643 }
644 }
645 if (g_options.outFiles.isEmpty() || !file.isEmpty() || !logType.isEmpty())
646 setOutputFile(file, logType);
647
648 QString testAppArgs;
649 for (auto it = g_options.outFiles.constBegin(); it != g_options.outFiles.constEnd(); ++it)
650 testAppArgs += "-o %1,%2 "_L1.arg(deviceOutputFileName(it.key(), it.value()), it.key());
651
652 testAppArgs += unhandledArgs.join(u' ').trimmed();
653 testAppArgs = "\"%1\""_L1.arg(testAppArgs.trimmed());
654 const QString activityName = "%1/%2"_L1.arg(g_options.package).arg(g_options.activity);
655
656 // Pass over any qt or testlib env vars if set
657 QStringList testEnvVarArgs;
658 const QStringList envVarsList = QProcessEnvironment::systemEnvironment().toStringList();
659 for (const QString &var : envVarsList) {
660 if (!var.startsWith("QTEST_"_L1) && !var.startsWith("QT_"_L1))
661 continue;
662 const qsizetype index = var.indexOf(u'=');
663 if (index < 0)
664 continue;
665 const QString key = var.left(index);
666 QString escapedValue = var.mid(index + 1);
667 escapedValue.replace("'"_L1, "'\\''"_L1);
668 const QString value = "'%1'"_L1.arg(escapedValue);
669 testEnvVarArgs << "-e"_L1 << ("extraenvvars_"_L1 + key) << value;
670 }
671
672 g_options.amStarttestArgs = { "shell"_L1, "am"_L1, "start"_L1, "-W"_L1,
673 "-n"_L1, activityName,
674 "-e"_L1, "applicationArguments"_L1, testAppArgs };
675 g_options.amStarttestArgs.append(testEnvVarArgs);
676
677 return true;
678}
679
680static int getPid(const QString &package)
681{
682 const QByteArray output = execAdbCommand({ "shell"_L1, "pidof"_L1, "-s"_L1, package }, false);
683 if (output.isNull())
684 return -1;
685
686 bool ok = false;
687 // Android 9's toybox ignores -s and lists every match; a fork()ing test has two.
688 const int pid = output.simplified().split(' ').constFirst().toInt(&ok);
689 return ok && pid > 0 ? pid : -1;
690}
691
692static QString runCommandAsUserArgs(const QString &cmd)
693{
694 return "run-as %1 --user %2 %3"_L1.arg(g_options.package, g_testInfo.userId, cmd);
695}
696
697// Returns nullopt when `adb devices` itself failed, so callers don't confuse
698// a transient daemon hiccup with "device disconnected".
700{
701 const QByteArray output = execAdbCommand({ "devices"_L1 }, false);
702 if (output.isNull())
703 return std::nullopt;
704
705 QStringList devices;
706 for (const QByteArray &line : output.split(u'\n')) {
707 if (line.contains("\tdevice"_L1))
708 devices.append(QString::fromUtf8(line.split(u'\t').first()));
709 }
710
711 return devices;
712}
713
715{
716 const auto devices = runningDevices();
717 return devices && !devices->contains(g_options.serial);
718}
719
720static bool isRunning() {
721 if (g_testInfo.deviceGone.load())
722 return false;
723
724 if (getPid(g_options.package) > 0)
725 return true;
726
727 // No pid means the test exited, but adb failing looks the same; check the
728 // device list to tell them apart and flag a disconnect when warranted.
729 if (!g_options.serial.isEmpty() && deviceDisconnected())
730 g_testInfo.deviceGone.store(true);
731
732 return false;
733}
734
735static bool pollUntil(qxp::function_ref<bool() const> predicate,
736 QDeadlineTimer deadline,
737 std::chrono::nanoseconds interval)
738{
739 do {
740 if (predicate())
741 return true;
742 QThread::sleep(interval);
743 } while (!deadline.hasExpired() && !g_testInfo.isTestRunnerInterrupted.load());
744 if (g_testInfo.isTestRunnerInterrupted.load())
745 return false;
746 // Predicate may have flipped during the final sleep; check once more.
747 return predicate();
748}
749
750static void waitForStarted()
751{
752 using namespace std::chrono_literals;
753 // Grab the pid for logcat filtering if pidof catches it, but don't block
754 // on a short-lived process that waitForFinished tracks by presence anyway
755 pollUntil([]() {
756 const int pid = getPid(g_options.package);
757 if (pid > 0)
758 g_testInfo.pid = pid;
759 return pid > 0 || !isRunning();
760 }, QDeadlineTimer(10s), 100ms);
761}
762
764{
765 using namespace std::chrono_literals;
766 if (g_options.stdoutFileName.isEmpty())
767 return false;
768 const QString existsCmd = "head -c 0 files/%1 2>/dev/null"_L1.arg(g_options.stdoutFileName);
769 const QStringList adbExistsCmd = { "shell"_L1, runCommandAsUserArgs(existsCmd) };
770 auto fileExists = [&]() { return !execAdbCommand(adbExistsCmd, false).isNull(); };
771 // Wait for the output file, but stop early if the test exits first
772 pollUntil([&]() { return fileExists() || !isRunning(); }, QDeadlineTimer(5s), 25ms);
773 return fileExists();
774}
775
776static bool setupStdoutLogger()
777{
778 // Empty stdoutFileName means file-only output; nothing to stream live.
779 if (g_options.stdoutFileName.isEmpty())
780 return true;
781
782 const QString tailPipeCmd = "tail -n +1 -f 'files/%1'"_L1.arg(g_options.stdoutFileName);
783 const QStringList adbTailCmd = { "shell"_L1, runCommandAsUserArgs(tailPipeCmd) };
784
785 g_options.stdoutLogger.emplace();
786 g_options.stdoutLogger->setProcessChannelMode(QProcess::ForwardedOutputChannel);
787 g_options.stdoutLogger->start(g_options.adbCommand, adbArgsWithSerial(adbTailCmd));
788 g_testInfo.stdoutLoggerPid.store(g_options.stdoutLogger->processId());
789
790 if (!g_options.stdoutLogger->waitForStarted()) {
791 g_testInfo.stdoutLoggerPid.store(0);
792 qCritical() << "Error: failed to run adb command to fetch stdout test results.";
793 g_options.stdoutLogger = std::nullopt;
794 return false;
795 }
796
797 return true;
798}
799
800static bool stopStdoutLogger()
801{
802 if (!g_options.stdoutLogger.has_value())
803 return true;
804
805 if (g_options.stdoutLogger->state() == QProcess::NotRunning) {
806 // sigHandler already SIGTERM'd the logger; that's expected.
807 if (g_testInfo.isTestRunnerInterrupted.load())
808 return true;
809 qCritical() << "The stdout logger process was terminated unexpectedly, "
810 "It might have been terminated by an external process";
811 return false;
812 }
813
814 g_options.stdoutLogger->terminate();
815 g_testInfo.stdoutLoggerPid.store(0);
816
817 if (!g_options.stdoutLogger->waitForFinished(5000)) {
818 g_options.stdoutLogger->kill();
819 g_options.stdoutLogger->waitForFinished();
820 qCritical() << "Error: adb test results tail command timed out.";
821 return false;
822 }
823
824 return true;
825}
826
827static void waitForFinished()
828{
829 using namespace std::chrono_literals;
830 const bool finished = pollUntil([]() { return !isRunning(); },
831 QDeadlineTimer(g_options.timeoutSecs * 1s), 100ms);
832 if (!finished && !g_testInfo.isTestRunnerInterrupted.load())
833 qWarning() << "Timed out while waiting for the test to finish";
834}
835
836static void obtainSdkVersion()
837{
838 // Best-effort: SDK version gates userId() for multi-user, legacyDate formatting,
839 // and ApplicationExitInfo queries. Falling back to defaults is safe.
840 const QStringList versionArgs = { "shell"_L1, "getprop"_L1, "ro.build.version.sdk"_L1 };
841 const QByteArray output = execAdbCommand(versionArgs, false);
842 if (output.isNull()) {
843 qWarning() << "Unable to query the SDK version: adb getprop ro.build.version.sdk failed.";
844 return;
845 }
846 bool ok = false;
847 int sdkVersion = output.toInt(&ok);
848 if (ok)
849 g_testInfo.sdkVersion = sdkVersion;
850 else
851 qWarning("Unable to parse SDK version from adb getprop output: '%s'.", output.constData());
852}
853
855{
856 // adb get-current-user command is available starting from API level 26.
857 QByteArray userId;
858 if (g_testInfo.sdkVersion >= 26) {
859 const QStringList userIdArgs = {"shell"_L1, "cmd"_L1, "activity"_L1, "get-current-user"_L1};
860 userId = execAdbCommand(userIdArgs, false);
861 if (userId.isNull())
862 qCritical() << "Error: failed to retrieve the user ID";
863 }
864
865 if (userId.isEmpty())
866 userId = "0";
867
868 return QString::fromUtf8(userId.simplified());
869}
870
871static QByteArray adbReadAppFile(const QString &fileName, int retries,
872 std::chrono::milliseconds backoff)
873{
874 const QString catCmd = "cat files/%1 2> /dev/null"_L1.arg(fileName);
875 const QStringList args = { "shell"_L1, runCommandAsUserArgs(catCmd) };
876 while (retries > 0) {
877 const QByteArray output = execAdbCommand(args, false);
878 if (!output.isEmpty())
879 return output;
880 if (--retries)
881 QThread::msleep(backoff.count());
882 }
883 return QByteArray();
884}
885
886static bool pullResults()
887{
888 using namespace std::chrono_literals;
889 for (auto it = g_options.outFiles.constBegin(); it != g_options.outFiles.constEnd(); ++it) {
890 const QString filePath = it.value();
891 if (filePath.isEmpty())
892 continue; // stdout-streamed format so nothing to pull
893 const QString fileName = QFileInfo(filePath).fileName();
894
895 const QByteArray output = adbReadAppFile(fileName, g_options.resultsPullRetries, 200ms);
896 if (output.isNull()) {
898 g_testInfo.deviceGone.store(true);
899 qCritical() << "Error: failed to retrieve test result file %1 (missing or empty)."_L1
900 .arg(fileName);
901 return false;
902 }
903
904 QFile out{filePath};
905 if (!out.open(QIODevice::WriteOnly)) {
906 qCritical() << "Error: failed to open %1 to write results to host."_L1.arg(filePath);
907 return false;
908 }
909 if (out.write(output) != output.size()) {
910 qCritical() << "Error: short write of results to %1: %2"_L1
911 .arg(filePath).arg(out.errorString());
912 return false;
913 }
914 }
915
916 return true;
917}
918
920{
921 QString libsPath = "%1/libs/"_L1.arg(g_options.buildPath);
922 if (!QDir(libsPath).exists())
923 libsPath = "%1/app/libs/"_L1.arg(g_options.buildPath);
924 const QStringList abiArgs = { "shell"_L1, "getprop"_L1, "ro.product.cpu.abi"_L1 };
925 QByteArray abi = execAdbCommand(abiArgs, false).trimmed();
926 if (abi.isEmpty()) {
927 const QStringList subDirs = QDir(libsPath).entryList(QDir::Dirs | QDir::NoDotAndDotDot);
928 if (!subDirs.isEmpty())
929 abi = subDirs.first().toUtf8();
930 }
931
932 if (abi.isEmpty())
933 return {};
934
935 return libsPath + QString::fromUtf8(abi);
936}
937
938static void printLogcatCrash(const QByteArray &logcat)
939{
940 // No crash report, do nothing
941 if (logcat.isEmpty())
942 return;
943
944 QByteArray crashLogcat(logcat);
945 if (g_options.ndkStackPath.isEmpty()) {
946 qWarning() << "Warning: ndk-stack path not provided and couldn't be deduced "
947 "using the ANDROID_NDK_ROOT or ANDROID_NDK_HOME environment "
948 "variables.";
949 } else if (const QString libsPath = getAbiLibsPath(); libsPath.isEmpty()) {
950 qWarning() << "Warning: could not determine the device ABI, "
951 "skipping ndk-stack and printing the raw dump.";
952 } else {
953 QProcess ndkStackProc;
954 ndkStackProc.start(g_options.ndkStackPath, { "-sym"_L1, libsPath });
955
956 if (ndkStackProc.waitForStarted()) {
957 ndkStackProc.write(crashLogcat);
958 ndkStackProc.closeWriteChannel();
959
960 // Drain ndk-stack to completion so its output isn't truncated.
961 if (ndkStackProc.waitForFinished()) {
962 // Keep the raw dump if ndk-stack produced no output.
963 const QByteArray ndkOutput = ndkStackProc.readAllStandardOutput();
964 if (!ndkOutput.trimmed().isEmpty())
965 crashLogcat = ndkOutput;
966 } else {
967 qCritical() << "Error: ndk-stack command timed out.";
968 ndkStackProc.kill();
969 ndkStackProc.waitForFinished();
970 }
971 } else {
972 qCritical() << "Error: failed to run ndk-stack command.";
973 }
974 }
975
976 if (crashLogcat.startsWith("********** Crash dump")) {
977 qDebug().noquote() << crashLogcat.trimmed();
978 } else {
979 qDebug() << "[androidtestrunner] ********** BEGIN crash dump **********";
980 qDebug().noquote() << crashLogcat.trimmed();
981 qDebug() << "[androidtestrunner] ********** END crash dump **********";
982 }
983}
984
985// Shortened from debuggerd's full banner so emulator/oem builds that wrap
986// or truncate the line still slice cleanly.
987static constexpr auto crashBannerMarker = "*** *** *** *** *** *** *** ***";
988
989static QByteArray fetchLogcat(const QString &timeStamp, bool waitForDiagnostics)
990{
991 using namespace std::chrono_literals;
992 // Read all three default buffers explicitly: crashes land in crash,
993 // ANR notices in system, and the test's QtTestLib/Qt output in main.
994 QStringList logcatArgs = { "shell"_L1, "logcat"_L1,
995 "-b"_L1, "main,system,crash"_L1,
996 "-v"_L1, "brief"_L1 };
997 // Without a timestamp the time arg is useless; cap by line count instead.
998 if (!timeStamp.isEmpty())
999 logcatArgs << "-t"_L1 << "'%1'"_L1.arg(timeStamp);
1000 else
1001 logcatArgs << "-t"_L1 << "5000"_L1;
1002 const bool useColor = qEnvironmentVariable("QTEST_ENVIRONMENT") != "ci"_L1;
1003 if (useColor)
1004 logcatArgs << "-v"_L1 << "color"_L1;
1005
1006 QByteArray logcat = execAdbCommand(logcatArgs, false);
1007 if (logcat.isNull())
1008 qWarning() << "Warning: failed to fetch logcat of the test";
1009
1010 if (!waitForDiagnostics)
1011 return logcat;
1012
1013 const QByteArray anrMarker = "ANR in " + g_options.package.toUtf8();
1014 if (logcat.contains(crashBannerMarker) || logcat.contains(anrMarker))
1015 return logcat;
1016
1017 // Debuggerd banner and ANR notice land seconds after death; poll for them.
1018 QByteArray polled;
1019 constexpr auto timeout = 15s;
1020 const bool found = pollUntil([&]() {
1021 polled = execAdbCommand(logcatArgs, false);
1022 return !polled.isNull()
1023 && (polled.contains(crashBannerMarker) || polled.contains(anrMarker));
1024 }, QDeadlineTimer(timeout), 250ms);
1025 if (!polled.isEmpty())
1026 logcat = std::move(polled);
1027 if (!found && !g_testInfo.isTestRunnerInterrupted.load()) {
1028 qWarning().noquote() << QString::fromLatin1("[androidtestrunner] No crash banner or "
1029 "ANR marker found in logcat within %1s; output below may be incomplete.")
1030 .arg(timeout.count());
1031 }
1032 return logcat;
1033}
1034
1035static QByteArray takeCrashDump(QByteArray *logcat)
1036{
1037 const qsizetype idx = logcat->indexOf(crashBannerMarker);
1038 if (idx == -1)
1039 return {};
1040 QByteArray dump = logcat->mid(idx);
1041 *logcat = logcat->left(idx);
1042 return dump;
1043}
1044
1045static QByteArray filterTestLogcat(const QByteArray &logcat, int testPid, int systemServerPid)
1046{
1047 // No pid to anchor on: return the logcat unfiltered.
1048 if (testPid <= 0)
1049 return logcat;
1050
1051 static const QRegularExpression logcatRegEx{
1052 "(?:^\\x1B\\‍[[0-9;]*m)?" // color
1053 "(\\w)/" // message type 1. capture
1054 ".*?" // source (non-greedy so the pid capture group binds first)
1055 "(\\‍(\\s*\\d*\\‍)):" // pid 2. capture
1056 "\\s*"
1057 ".*" // message
1058 "(?:\\x1B\\‍[[0-9;]*m)?" // color
1059 "[\\n\\r]*$"_L1
1060 };
1061 QByteArrayList kept;
1062 for (const QByteArray &line : logcat.split(u'\n')) {
1063 QRegularExpressionMatch match = logcatRegEx.match(QString::fromUtf8(line));
1064 if (!match.hasMatch()) {
1065 // Unparseable line; keep it rather than drop silently.
1066 kept.append(line);
1067 continue;
1068 }
1069 const QString msgType = match.captured(1);
1070 const QString pidStr = match.captured(2);
1071 const int capturedPid = pidStr.mid(1, pidStr.size() - 2).trimmed().toInt();
1072 const bool isFatal = msgType == u'F';
1073 const bool isOurTest = capturedPid == testPid;
1074 const bool isAnrSource = systemServerPid > 0 && capturedPid == systemServerPid;
1075 if (isOurTest || isFatal || isAnrSource)
1076 kept.append(line);
1077 }
1078 return kept.join('\n');
1079}
1080
1081static void analyseLogcat(const QString &timeStamp, int *exitCode)
1082{
1083 const bool wasAbnormal = !isTestExitCodeNormal(*exitCode)
1084 && !g_testInfo.isTestRunnerInterrupted.load();
1085 QByteArray logcat = fetchLogcat(timeStamp, wasAbnormal);
1086 if (logcat.isEmpty()) {
1088 qWarning() << "The retrieved logcat is empty";
1089 return;
1090 }
1091
1092 const QByteArray crashDump = takeCrashDump(&logcat);
1093
1094 const bool anrOccurred = logcat.contains(
1095 "ANR in %1"_L1.arg(g_options.package).toUtf8());
1096 if (anrOccurred) {
1097 // ANR may fire after a clean exit code. Surface it via the exit code,
1098 // but preserve other abnormal codes.
1099 if (isTestExitCodeNormal(*exitCode) || *exitCode == EXIT_NOEXITCODE)
1100 *exitCode = EXIT_ANR;
1101 qCritical("[androidtestrunner] An ANR has occurred while running the test '%s';"
1102 " consult logcat for additional logs from the system_server process",
1103 qPrintable(g_options.package));
1104 }
1105
1106 const int systemServerPid = anrOccurred ? getPid("system_server"_L1) : -1;
1107 const QByteArray filtered = filterTestLogcat(logcat, g_testInfo.pid, systemServerPid);
1108
1109 // Print whenever the caller asked (--show-logcat) or the test exited
1110 // abnormally; the crash buffer is only meaningful on abnormal exit.
1111 const bool testCrashed = !isTestExitCodeNormal(*exitCode)
1112 && !g_testInfo.isTestRunnerInterrupted.load();
1113 if (g_options.showLogcatOutput || testCrashed) {
1114 qDebug() << "[androidtestrunner] ********** BEGIN logcat dump **********";
1115 qDebug().noquote() << filtered.trimmed();
1116 qDebug() << "[androidtestrunner] ********** END logcat dump **********";
1117 }
1118 if (testCrashed && !crashDump.isEmpty())
1119 printLogcatCrash(crashDump);
1120}
1121
1123{
1124 const bool legacyDate = g_testInfo.sdkVersion > 0 && g_testInfo.sdkVersion <= 23;
1125 const QString timeFormat = legacyDate ?
1126 "%m-%d %H:%M:%S.000"_L1 : "%Y-%m-%d %H:%M:%S.%3N"_L1;
1127
1128 QStringList dateArgs = { "shell"_L1, "date"_L1, "+'%1'"_L1.arg(timeFormat) };
1129 const QByteArray output = execAdbCommand(dateArgs, false);
1130 if (output.isNull()) {
1131 qWarning() << "[androidtestrunner] ERROR in command: adb shell date";
1132 return {};
1133 }
1134
1135 return QString::fromUtf8(output.simplified());
1136}
1137
1138static int testExitCode()
1139{
1140 using namespace std::chrono_literals;
1141 const QByteArray exitCodeOutput =
1142 adbReadAppFile(u"qtest_last_exit_code"_s, g_options.resultsPullRetries, 200ms);
1143 if (exitCodeOutput.isNull()) {
1145 g_testInfo.deviceGone.store(true);
1146 qCritical() << "[androidtestrunner] ERROR in command: adb shell cat"
1147 " files/qtest_last_exit_code";
1148 return EXIT_NOEXITCODE;
1149 }
1150 qDebug() << "[androidtestrunner] Test exitcode: " << exitCodeOutput;
1151
1152 bool ok;
1153 int exitCode = exitCodeOutput.toInt(&ok);
1154
1155 return ok ? exitCode : EXIT_NOEXITCODE;
1156}
1157
1159{
1160 return !execAdbCommand({ "uninstall"_L1, g_options.package }).isNull();
1161}
1162
1163
1164void sigHandler(int signal)
1165{
1166#if !defined(Q_OS_WIN32)
1167 // Reap the adb-tail subprocess so a second SIGINT doesn't orphan it.
1168 const qint64 loggerPid = g_testInfo.stdoutLoggerPid.exchange(0);
1169 if (loggerPid > 0)
1170 ::kill(static_cast<pid_t>(loggerPid), SIGTERM);
1171#endif
1172 std::signal(signal, SIG_DFL);
1173 if (!g_testInfo.isPackageInstalled.load())
1174 _exit(EXIT_ERROR);
1175 g_testInfo.isTestRunnerInterrupted.store(true);
1176}
1177
1178int main(int argc, char *argv[])
1179{
1180 using namespace std::chrono_literals;
1181 std::signal(SIGINT, sigHandler);
1182 std::signal(SIGTERM, sigHandler);
1183
1184 QCoreApplication a(argc, argv);
1185 if (!parseOptions())
1186 return EXIT_ERROR;
1187
1188 if (g_options.makeCommand.isEmpty()) {
1189 qCritical() << "It is required to provide a make command with the \"--make\" parameter "
1190 "to generate the apk.";
1191 return EXIT_ERROR;
1192 }
1193
1194 if (execCommand(g_options.makeCommand, true, g_options.timeoutSecs * 1s).isNull()) {
1195 qCritical("The APK build command \"%s\" failed.", qPrintable(g_options.makeCommand));
1196 return EXIT_ERROR;
1197 }
1198
1199 if (!QFile::exists(g_options.packagePath)) {
1200 qCritical("No package \"%s\" found after running the make command. "
1201 "Check the provided path and the make command.",
1202 qPrintable(g_options.packagePath));
1203 return EXIT_ERROR;
1204 }
1205
1206 const std::optional<QStringList> devices = runningDevices();
1207 if (!devices) {
1208 qCritical("Failed to query connected devices via 'adb devices'.");
1209 return EXIT_ERROR;
1210 } else if (devices->isEmpty()) {
1211 qCritical("No connected devices or running emulators can be found.");
1212 return EXIT_ERROR;
1213 } else if (!g_options.serial.isEmpty() && !devices->contains(g_options.serial)) {
1214 qCritical("No connected device or running emulator with serial '%s' can be found.",
1215 qPrintable(g_options.serial));
1216 return EXIT_ERROR;
1217 } else if (g_options.serial.isEmpty() && devices->size() == 1) {
1218 g_options.serial = devices->first();
1219 } else if (g_options.serial.isEmpty()) {
1220 qCritical("Multiple devices connected, set ANDROID_SERIAL or ANDROID_DEVICE_SERIAL.");
1221 return EXIT_ERROR;
1222 }
1223
1225
1226 g_testInfo.userId = userId();
1227
1229 return EXIT_ERROR;
1230
1231 const QString ns = getGradleProjectProperty(g_options.buildPath, "android.namespace"_L1);
1232 if (!ns.isEmpty())
1233 g_options.package = ns;
1234
1235 if (g_options.package.isEmpty()) {
1236 qCritical("Unable to get package name for '%s'", qPrintable(g_options.packagePath));
1237 return EXIT_ERROR;
1238 }
1239
1240 // parseTestArgs depends on g_options.package
1241 if (!parseTestArgs())
1242 return EXIT_ERROR;
1243
1244 // Per-user prefix so a shared TempLocation doesn't collide across users.
1245 const QString user = qEnvironmentVariable("USER",
1246 qEnvironmentVariable("USERNAME", u"default"_s));
1247 const QString lockName = u"androidtestrunner-%1-%2.lock"_s.arg(user, g_options.serial);
1248 const QDir tempDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation));
1249 QLockFile testRunnerLock(tempDir.absoluteFilePath(lockName));
1250 testRunnerLock.setStaleLockTime(0);
1251 if (!testRunnerLock.lock()) {
1252 qCritical("Failed to acquire test runner lock for '%s'.", qPrintable(g_options.serial));
1253 return EXIT_ERROR;
1254 }
1255
1256 if (g_options.packagePath.endsWith(".apk"_L1)) {
1257 const QStringList installArgs = { "install"_L1, "-r"_L1, g_options.packagePath };
1258 if (execAdbCommand(installArgs).isNull())
1259 return EXIT_ERROR;
1260 } else if (g_options.packagePath.endsWith(".aab"_L1)) {
1261 QFileInfo aab(g_options.packagePath);
1262 const auto apksFilePath = aab.absoluteDir().absoluteFilePath(aab.baseName() + ".apks"_L1);
1263 QStringList installApksArgs = { "install-apks"_L1, "--apks"_L1, apksFilePath };
1264 if (!g_options.serial.isEmpty())
1265 installApksArgs << "--device-id"_L1 << g_options.serial;
1266 if (execBundletoolCommand({ "build-apks"_L1, "--bundle"_L1, g_options.packagePath,
1267 "--output"_L1, apksFilePath, "--local-testing"_L1,
1268 "--overwrite"_L1 }).isNull()
1269 || execBundletoolCommand(installApksArgs).isNull())
1270 return EXIT_ERROR;
1271 }
1272 g_testInfo.isPackageInstalled.store(true);
1273
1274 const QStringList dangerousPermissions = queryDangerousPermissions();
1275 for (const auto &permission : g_options.permissions) {
1276 if (!dangerousPermissions.contains(permission))
1277 continue;
1278
1279 if (execAdbCommand({ "shell"_L1, "pm"_L1, "grant"_L1, "--user"_L1, g_testInfo.userId,
1280 g_options.package, permission }).isNull()) {
1281 qWarning("Unable to grant '%s' to '%s'. Probably the Android version mismatch.",
1282 qPrintable(permission), qPrintable(g_options.package));
1283 }
1284 }
1285
1286 // Call additional adb command if set after installation and before starting the test
1287 for (const auto &command : g_options.preTestRunAdbCommands) {
1288 if (execAdbCommand(command).isNull()) {
1289 qCritical("The pre test ADB command \"%s\" failed.",
1290 qUtf8Printable(command.join(u' ')));
1291 return EXIT_ERROR;
1292 }
1293 }
1294
1295 // Pre test start
1296 const QString formattedStartTime = getCurrentTimeString();
1297
1298 // Start the test
1299 if (execAdbCommand(g_options.amStarttestArgs).isNull()) {
1300 // am start -W never returns when the app crashes before main; treat the
1301 // failure as a launch-time crash and surface it from logcat.
1302 int exitCode = EXIT_NOEXITCODE;
1303 analyseLogcat(formattedStartTime, &exitCode);
1304 return exitCode;
1305 }
1306
1308
1310 qWarning("Continuing without live stdout streaming; result files are still pulled.");
1311
1313
1314 if (g_testInfo.deviceGone.load()) {
1315 qCritical("[androidtestrunner] Device '%s' became unreachable during the test, "
1316 "result transfer and uninstall skipped.", qPrintable(g_options.serial));
1318 return EXIT_DEVICE_GONE;
1319 }
1320
1321 // Post test run
1322 if (!stopStdoutLogger())
1323 return EXIT_ERROR;
1324
1325 int exitCode = testExitCode();
1326
1328 analyseLogcat(formattedStartTime, &exitCode);
1329
1330 const bool pullRes = pullResults();
1331 if (g_testInfo.deviceGone.load()) {
1332 qCritical("[androidtestrunner] Device '%s' became unreachable during cleanup, "
1333 "uninstall skipped.", qPrintable(g_options.serial));
1334 return EXIT_DEVICE_GONE;
1335 }
1336 if (!pullRes && isTestExitCodeNormal(exitCode))
1337 exitCode = EXIT_NORESULTS;
1338
1340 qWarning("Failed to uninstall test package '%s'. The test exit code is preserved.",
1341 qPrintable(g_options.package));
1342 }
1343
1344 if (g_testInfo.isTestRunnerInterrupted.load()) {
1345 qCritical() << "The androidtestrunner was interrupted and the test was cleaned up.";
1346 return EXIT_ERROR;
1347 }
1348
1349 return exitCode;
1350}
static QByteArray execCommand(const QString &command, bool verbose=true, std::chrono::milliseconds timeout=std::chrono::milliseconds(-1))
Definition main.cpp:197
static QByteArray execCommand(const QString &program, const QStringList &args, bool verbose=false, std::chrono::milliseconds timeout=std::chrono::milliseconds(-1))
Definition main.cpp:101
static QString userId()
Definition main.cpp:854
static QString runCommandAsUserArgs(const QString &cmd)
Definition main.cpp:692
static bool setupStdoutLogger()
Definition main.cpp:776
static QString getCurrentTimeString()
Definition main.cpp:1122
static int getPid(const QString &package)
Definition main.cpp:680
static QString getAbiLibsPath()
Definition main.cpp:919
static constexpr int EXIT_DEVICE_GONE
Definition main.cpp:45
static int testExitCode()
Definition main.cpp:1138
static void setOutputFile(QString file, QString format)
Definition main.cpp:592
static bool parseTestArgs()
Definition main.cpp:608
static QByteArray execBundletoolCommand(const QStringList &args, bool verbose=true)
Definition main.cpp:161
static QByteArray adbReadAppFile(const QString &fileName, int retries, std::chrono::milliseconds backoff)
Definition main.cpp:871
static QString deviceOutputFileName(const QString &format, const QString &hostPath)
Definition main.cpp:587
static QByteArray takeCrashDump(QByteArray *logcat)
Definition main.cpp:1035
static std::optional< QStringList > runningDevices()
Definition main.cpp:699
static QString gradleInitScriptPath()
Definition main.cpp:435
static bool deviceDisconnected()
Definition main.cpp:714
static TestInfo g_testInfo
Definition main.cpp:92
static bool isRunning()
Definition main.cpp:720
static bool pullResults()
Definition main.cpp:886
static void obtainSdkVersion()
Definition main.cpp:836
static bool collectPackagePaths(const QStringList &apkValues, const QStringList &aabValues)
Definition main.cpp:178
static constexpr auto crashBannerMarker
Definition main.cpp:987
static bool setPackagePath(const QString &path)
Definition main.cpp:168
static void analyseLogcat(const QString &timeStamp, int *exitCode)
Definition main.cpp:1081
static constexpr int EXIT_NOEXITCODE
Definition main.cpp:42
static constexpr int HIGHEST_QTEST_EXITCODE
Definition main.cpp:38
static void waitForFinished()
Definition main.cpp:827
static constexpr int EXIT_NORESULTS
Definition main.cpp:44
static bool parseOptions()
Definition main.cpp:276
static QString ndkStackPath()
Definition main.cpp:256
static bool isTestExitCodeNormal(const int ec)
Definition main.cpp:96
static QStringList queryDangerousPermissions()
Definition main.cpp:541
static constexpr int EXIT_ERROR
Definition main.cpp:40
static QString getGradleProjectProperty(const QString &androidBuildDir, const QString &property)
Definition main.cpp:464
static bool pollUntil(qxp::function_ref< bool() const > predicate, QDeadlineTimer deadline, std::chrono::nanoseconds interval)
Definition main.cpp:735
static QStringList splitOwnAndTestArgs(const QStringList &args, const QSet< QString > &knownOpts, const QSet< QString > &valueOpts)
Definition main.cpp:206
static QByteArray fetchLogcat(const QString &timeStamp, bool waitForDiagnostics)
Definition main.cpp:989
static QByteArray filterTestLogcat(const QByteArray &logcat, int testPid, int systemServerPid)
Definition main.cpp:1045
static QByteArray execAdbCommand(const QStringList &args, bool verbose=true)
Definition main.cpp:156
static void printLogcatCrash(const QByteArray &logcat)
Definition main.cpp:938
void sigHandler(int signal)
Definition main.cpp:1164
static bool waitForLoggingStarted()
Definition main.cpp:763
static void waitForStarted()
Definition main.cpp:750
static bool stopStdoutLogger()
Definition main.cpp:800
static QStringList adbArgsWithSerial(const QStringList &args)
Definition main.cpp:149
static bool uninstallTestPackage()
Definition main.cpp:1158
static bool processAndroidManifest()
Definition main.cpp:497
static constexpr int EXIT_ANR
Definition main.cpp:43
static Options g_options
Definition main.cpp:74
int main(int argc, char *argv[])
[ctor_close]
QString makeCommand
Definition main.cpp:59
QStringList amStarttestArgs
Definition main.cpp:66
std::optional< QProcess > stdoutLogger
Definition main.cpp:71
QStringList permissions
Definition main.cpp:62
QHash< QString, QString > outFiles
Definition main.cpp:65
QString adbCommand
Definition main.cpp:56
int timeoutSecs
Definition main.cpp:52
bool showLogcatOutput
Definition main.cpp:70
QString bundletoolPath
Definition main.cpp:57
int resultsPullRetries
Definition main.cpp:53
QString stdoutFileName
Definition main.cpp:64
bool skipAddInstallRoot
Definition main.cpp:51
QList< QStringList > preTestRunAdbCommands
Definition main.cpp:69
QString package
Definition main.cpp:60
QString serial
Definition main.cpp:58
QString packagePath
Definition main.cpp:67
bool verbose
Definition main.cpp:130
QString ndkStackPath
Definition main.cpp:68
QStringList testArgsList
Definition main.cpp:63
QString manifestPath
Definition main.cpp:55
QString activity
Definition main.cpp:61
QString buildPath
Definition main.cpp:54
QString userId
Definition main.cpp:80
std::atomic< bool > isPackageInstalled
Definition main.cpp:82
int sdkVersion
Definition main.cpp:78
std::atomic< bool > deviceGone
Definition main.cpp:84
int pid
Definition main.cpp:79
std::atomic< qint64 > stdoutLoggerPid
Definition main.cpp:85
std::atomic< bool > isTestRunnerInterrupted
Definition main.cpp:83