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