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