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 const int pid = output.simplified().toInt(&ok);
680 return ok && pid > 0 ? pid : -1;
681}
682
683static QString runCommandAsUserArgs(const QString &cmd)
684{
685 return "run-as %1 --user %2 %3"_L1.arg(g_options.package, g_testInfo.userId, cmd);
686}
687
688// Returns nullopt when `adb devices` itself failed, so callers don't confuse
689// a transient daemon hiccup with "device disconnected".
691{
692 const QByteArray output = execAdbCommand({ "devices"_L1 }, false);
693 if (output.isNull())
694 return std::nullopt;
695
696 QStringList devices;
697 for (const QByteArray &line : output.split(u'\n')) {
698 if (line.contains("\tdevice"_L1))
699 devices.append(QString::fromUtf8(line.split(u'\t').first()));
700 }
701
702 return devices;
703}
704
706{
707 const auto devices = runningDevices();
708 return devices && !devices->contains(g_options.serial);
709}
710
711static bool isRunning() {
712 if (g_testInfo.deviceGone.load())
713 return false;
714
715 const QStringList pidofArgs = { "shell"_L1, "pidof"_L1, "-s"_L1, g_options.package };
716 const QByteArray output = execAdbCommand(pidofArgs, false);
717
718 // pidof exits 1 (with empty stdout) when the process is gone, but adb
719 // itself failing also looks like that; check the device list to tell
720 // them apart and flag a disconnect when warranted.
721 if (output.isNull()) {
722 if (!g_options.serial.isEmpty() && deviceDisconnected())
723 g_testInfo.deviceGone.store(true);
724 return false;
725 }
726
727 bool ok = false;
728 return output.simplified().toInt(&ok) > 0 && ok;
729}
730
731static bool pollUntil(qxp::function_ref<bool() const> predicate,
732 QDeadlineTimer deadline,
733 std::chrono::nanoseconds interval)
734{
735 do {
736 if (predicate())
737 return true;
738 QThread::sleep(interval);
739 } while (!deadline.hasExpired() && !g_testInfo.isTestRunnerInterrupted.load());
740 if (g_testInfo.isTestRunnerInterrupted.load())
741 return false;
742 // Predicate may have flipped during the final sleep; check once more.
743 return predicate();
744}
745
746static void waitForStarted()
747{
748 using namespace std::chrono_literals;
749 // Grab the pid for logcat filtering if pidof catches it, but don't block
750 // on a short-lived process that waitForFinished tracks by presence anyway
751 pollUntil([]() {
752 const int pid = getPid(g_options.package);
753 if (pid > 0)
754 g_testInfo.pid = pid;
755 return pid > 0 || !isRunning();
756 }, QDeadlineTimer(10s), 100ms);
757}
758
760{
761 using namespace std::chrono_literals;
762 if (g_options.stdoutFileName.isEmpty())
763 return false;
764 const QString existsCmd = "head -c 0 files/%1 2>/dev/null"_L1.arg(g_options.stdoutFileName);
765 const QStringList adbExistsCmd = { "shell"_L1, runCommandAsUserArgs(existsCmd) };
766 auto fileExists = [&]() { return !execAdbCommand(adbExistsCmd, false).isNull(); };
767 // Wait for the output file, but stop early if the test exits first
768 pollUntil([&]() { return fileExists() || !isRunning(); }, QDeadlineTimer(5s), 25ms);
769 return fileExists();
770}
771
772static bool setupStdoutLogger()
773{
774 // Empty stdoutFileName means file-only output; nothing to stream live.
775 if (g_options.stdoutFileName.isEmpty())
776 return true;
777
778 const QString tailPipeCmd = "tail -n +1 -f 'files/%1'"_L1.arg(g_options.stdoutFileName);
779 const QStringList adbTailCmd = { "shell"_L1, runCommandAsUserArgs(tailPipeCmd) };
780
781 g_options.stdoutLogger.emplace();
782 g_options.stdoutLogger->setProcessChannelMode(QProcess::ForwardedOutputChannel);
783 g_options.stdoutLogger->start(g_options.adbCommand, adbArgsWithSerial(adbTailCmd));
784 g_testInfo.stdoutLoggerPid.store(g_options.stdoutLogger->processId());
785
786 if (!g_options.stdoutLogger->waitForStarted()) {
787 g_testInfo.stdoutLoggerPid.store(0);
788 qCritical() << "Error: failed to run adb command to fetch stdout test results.";
789 g_options.stdoutLogger = std::nullopt;
790 return false;
791 }
792
793 return true;
794}
795
796static bool stopStdoutLogger()
797{
798 if (!g_options.stdoutLogger.has_value())
799 return true;
800
801 if (g_options.stdoutLogger->state() == QProcess::NotRunning) {
802 // sigHandler already SIGTERM'd the logger; that's expected.
803 if (g_testInfo.isTestRunnerInterrupted.load())
804 return true;
805 qCritical() << "The stdout logger process was terminated unexpectedly, "
806 "It might have been terminated by an external process";
807 return false;
808 }
809
810 g_options.stdoutLogger->terminate();
811 g_testInfo.stdoutLoggerPid.store(0);
812
813 if (!g_options.stdoutLogger->waitForFinished(5000)) {
814 g_options.stdoutLogger->kill();
815 g_options.stdoutLogger->waitForFinished();
816 qCritical() << "Error: adb test results tail command timed out.";
817 return false;
818 }
819
820 return true;
821}
822
823static void waitForFinished()
824{
825 using namespace std::chrono_literals;
826 const bool finished = pollUntil([]() { return !isRunning(); },
827 QDeadlineTimer(g_options.timeoutSecs * 1s), 100ms);
828 if (!finished && !g_testInfo.isTestRunnerInterrupted.load())
829 qWarning() << "Timed out while waiting for the test to finish";
830}
831
832static void obtainSdkVersion()
833{
834 // Best-effort: SDK version gates userId() for multi-user, legacyDate formatting,
835 // and ApplicationExitInfo queries. Falling back to defaults is safe.
836 const QStringList versionArgs = { "shell"_L1, "getprop"_L1, "ro.build.version.sdk"_L1 };
837 const QByteArray output = execAdbCommand(versionArgs, false);
838 if (output.isNull()) {
839 qWarning() << "Unable to query the SDK version: adb getprop ro.build.version.sdk failed.";
840 return;
841 }
842 bool ok = false;
843 int sdkVersion = output.toInt(&ok);
844 if (ok)
845 g_testInfo.sdkVersion = sdkVersion;
846 else
847 qWarning("Unable to parse SDK version from adb getprop output: '%s'.", output.constData());
848}
849
851{
852 // adb get-current-user command is available starting from API level 26.
853 QByteArray userId;
854 if (g_testInfo.sdkVersion >= 26) {
855 const QStringList userIdArgs = {"shell"_L1, "cmd"_L1, "activity"_L1, "get-current-user"_L1};
856 userId = execAdbCommand(userIdArgs, false);
857 if (userId.isNull())
858 qCritical() << "Error: failed to retrieve the user ID";
859 }
860
861 if (userId.isEmpty())
862 userId = "0";
863
864 return QString::fromUtf8(userId.simplified());
865}
866
867static QByteArray adbReadAppFile(const QString &fileName, int retries,
868 std::chrono::milliseconds backoff)
869{
870 const QString catCmd = "cat files/%1 2> /dev/null"_L1.arg(fileName);
871 const QStringList args = { "shell"_L1, runCommandAsUserArgs(catCmd) };
872 while (retries > 0) {
873 const QByteArray output = execAdbCommand(args, false);
874 if (!output.isEmpty())
875 return output;
876 if (--retries)
877 QThread::msleep(backoff.count());
878 }
879 return QByteArray();
880}
881
882static bool pullResults()
883{
884 using namespace std::chrono_literals;
885 for (auto it = g_options.outFiles.constBegin(); it != g_options.outFiles.constEnd(); ++it) {
886 const QString filePath = it.value();
887 if (filePath.isEmpty())
888 continue; // stdout-streamed format so nothing to pull
889 const QString fileName = QFileInfo(filePath).fileName();
890
891 const QByteArray output = adbReadAppFile(fileName, g_options.resultsPullRetries, 200ms);
892 if (output.isNull()) {
894 g_testInfo.deviceGone.store(true);
895 qCritical() << "Error: failed to retrieve test result file %1 (missing or empty)."_L1
896 .arg(fileName);
897 return false;
898 }
899
900 QFile out{filePath};
901 if (!out.open(QIODevice::WriteOnly)) {
902 qCritical() << "Error: failed to open %1 to write results to host."_L1.arg(filePath);
903 return false;
904 }
905 if (out.write(output) != output.size()) {
906 qCritical() << "Error: short write of results to %1: %2"_L1
907 .arg(filePath).arg(out.errorString());
908 return false;
909 }
910 }
911
912 return true;
913}
914
916{
917 QString libsPath = "%1/libs/"_L1.arg(g_options.buildPath);
918 if (!QDir(libsPath).exists())
919 libsPath = "%1/app/libs/"_L1.arg(g_options.buildPath);
920 const QStringList abiArgs = { "shell"_L1, "getprop"_L1, "ro.product.cpu.abi"_L1 };
921 QByteArray abi = execAdbCommand(abiArgs, false).trimmed();
922 if (abi.isEmpty()) {
923 const QStringList subDirs = QDir(libsPath).entryList(QDir::Dirs | QDir::NoDotAndDotDot);
924 if (!subDirs.isEmpty())
925 abi = subDirs.first().toUtf8();
926 }
927
928 if (abi.isEmpty())
929 return {};
930
931 return libsPath + QString::fromUtf8(abi);
932}
933
934static void printLogcatCrash(const QByteArray &logcat)
935{
936 // No crash report, do nothing
937 if (logcat.isEmpty())
938 return;
939
940 QByteArray crashLogcat(logcat);
941 if (g_options.ndkStackPath.isEmpty()) {
942 qWarning() << "Warning: ndk-stack path not provided and couldn't be deduced "
943 "using the ANDROID_NDK_ROOT environment variable.";
944 } else if (const QString libsPath = getAbiLibsPath(); libsPath.isEmpty()) {
945 qWarning() << "Warning: could not determine the device ABI, "
946 "skipping ndk-stack and printing the raw dump.";
947 } else {
948 QProcess ndkStackProc;
949 ndkStackProc.start(g_options.ndkStackPath, { "-sym"_L1, libsPath });
950
951 if (ndkStackProc.waitForStarted()) {
952 ndkStackProc.write(crashLogcat);
953 ndkStackProc.closeWriteChannel();
954
955 // Drain ndk-stack to completion so its output isn't truncated.
956 if (ndkStackProc.waitForFinished()) {
957 // Keep the raw dump if ndk-stack produced no output.
958 const QByteArray ndkOutput = ndkStackProc.readAllStandardOutput();
959 if (!ndkOutput.trimmed().isEmpty())
960 crashLogcat = ndkOutput;
961 } else {
962 qCritical() << "Error: ndk-stack command timed out.";
963 ndkStackProc.kill();
964 ndkStackProc.waitForFinished();
965 }
966 } else {
967 qCritical() << "Error: failed to run ndk-stack command.";
968 }
969 }
970
971 if (crashLogcat.startsWith("********** Crash dump")) {
972 qDebug().noquote() << crashLogcat.trimmed();
973 } else {
974 qDebug() << "[androidtestrunner] ********** BEGIN crash dump **********";
975 qDebug().noquote() << crashLogcat.trimmed();
976 qDebug() << "[androidtestrunner] ********** END crash dump **********";
977 }
978}
979
980// Shortened from debuggerd's full banner so emulator/oem builds that wrap
981// or truncate the line still slice cleanly.
982static constexpr auto crashBannerMarker = "*** *** *** *** *** *** *** ***";
983
984static QByteArray fetchLogcat(const QString &timeStamp, bool waitForDiagnostics)
985{
986 using namespace std::chrono_literals;
987 // Read all three default buffers explicitly: crashes land in crash,
988 // ANR notices in system, and the test's QtTestLib/Qt output in main.
989 QStringList logcatArgs = { "shell"_L1, "logcat"_L1,
990 "-b"_L1, "main,system,crash"_L1,
991 "-v"_L1, "brief"_L1 };
992 // Without a timestamp the time arg is useless; cap by line count instead.
993 if (!timeStamp.isEmpty())
994 logcatArgs << "-t"_L1 << "'%1'"_L1.arg(timeStamp);
995 else
996 logcatArgs << "-t"_L1 << "5000"_L1;
997 const bool useColor = qEnvironmentVariable("QTEST_ENVIRONMENT") != "ci"_L1;
998 if (useColor)
999 logcatArgs << "-v"_L1 << "color"_L1;
1000
1001 QByteArray logcat = execAdbCommand(logcatArgs, false);
1002 if (logcat.isNull())
1003 qWarning() << "Warning: failed to fetch logcat of the test";
1004
1005 if (!waitForDiagnostics)
1006 return logcat;
1007
1008 const QByteArray anrMarker = "ANR in " + g_options.package.toUtf8();
1009 if (logcat.contains(crashBannerMarker) || logcat.contains(anrMarker))
1010 return logcat;
1011
1012 // Debuggerd banner and ANR notice land seconds after death; poll for them.
1013 QByteArray polled;
1014 constexpr auto timeout = 15s;
1015 const bool found = pollUntil([&]() {
1016 polled = execAdbCommand(logcatArgs, false);
1017 return !polled.isNull()
1018 && (polled.contains(crashBannerMarker) || polled.contains(anrMarker));
1019 }, QDeadlineTimer(timeout), 250ms);
1020 if (!polled.isEmpty())
1021 logcat = std::move(polled);
1022 if (!found && !g_testInfo.isTestRunnerInterrupted.load()) {
1023 qWarning().noquote() << QString::fromLatin1("[androidtestrunner] No crash banner or "
1024 "ANR marker found in logcat within %1s; output below may be incomplete.")
1025 .arg(timeout.count());
1026 }
1027 return logcat;
1028}
1029
1030static QByteArray takeCrashDump(QByteArray *logcat)
1031{
1032 const qsizetype idx = logcat->indexOf(crashBannerMarker);
1033 if (idx == -1)
1034 return {};
1035 QByteArray dump = logcat->mid(idx);
1036 *logcat = logcat->left(idx);
1037 return dump;
1038}
1039
1040static QByteArray filterTestLogcat(const QByteArray &logcat, int testPid, int systemServerPid)
1041{
1042 // No pid to anchor on: return the logcat unfiltered.
1043 if (testPid <= 0)
1044 return logcat;
1045
1046 static const QRegularExpression logcatRegEx{
1047 "(?:^\\x1B\\‍[[0-9;]*m)?" // color
1048 "(\\w)/" // message type 1. capture
1049 ".*?" // source (non-greedy so the pid capture group binds first)
1050 "(\\‍(\\s*\\d*\\‍)):" // pid 2. capture
1051 "\\s*"
1052 ".*" // message
1053 "(?:\\x1B\\‍[[0-9;]*m)?" // color
1054 "[\\n\\r]*$"_L1
1055 };
1056 QByteArrayList kept;
1057 for (const QByteArray &line : logcat.split(u'\n')) {
1058 QRegularExpressionMatch match = logcatRegEx.match(QString::fromUtf8(line));
1059 if (!match.hasMatch()) {
1060 // Unparseable line; keep it rather than drop silently.
1061 kept.append(line);
1062 continue;
1063 }
1064 const QString msgType = match.captured(1);
1065 const QString pidStr = match.captured(2);
1066 const int capturedPid = pidStr.mid(1, pidStr.size() - 2).trimmed().toInt();
1067 const bool isFatal = msgType == u'F';
1068 const bool isOurTest = capturedPid == testPid;
1069 const bool isAnrSource = systemServerPid > 0 && capturedPid == systemServerPid;
1070 if (isOurTest || isFatal || isAnrSource)
1071 kept.append(line);
1072 }
1073 return kept.join('\n');
1074}
1075
1076static void analyseLogcat(const QString &timeStamp, int *exitCode)
1077{
1078 const bool wasAbnormal = !isTestExitCodeNormal(*exitCode)
1079 && !g_testInfo.isTestRunnerInterrupted.load();
1080 QByteArray logcat = fetchLogcat(timeStamp, wasAbnormal);
1081 if (logcat.isEmpty()) {
1083 qWarning() << "The retrieved logcat is empty";
1084 return;
1085 }
1086
1087 const QByteArray crashDump = takeCrashDump(&logcat);
1088
1089 const bool anrOccurred = logcat.contains(
1090 "ANR in %1"_L1.arg(g_options.package).toUtf8());
1091 if (anrOccurred) {
1092 // ANR may fire after a clean exit code. Surface it via the exit code,
1093 // but preserve other abnormal codes.
1094 if (isTestExitCodeNormal(*exitCode) || *exitCode == EXIT_NOEXITCODE)
1095 *exitCode = EXIT_ANR;
1096 qCritical("[androidtestrunner] An ANR has occurred while running the test '%s';"
1097 " consult logcat for additional logs from the system_server process",
1098 qPrintable(g_options.package));
1099 }
1100
1101 const int systemServerPid = anrOccurred ? getPid("system_server"_L1) : -1;
1102 const QByteArray filtered = filterTestLogcat(logcat, g_testInfo.pid, systemServerPid);
1103
1104 // Print whenever the caller asked (--show-logcat) or the test exited
1105 // abnormally; the crash buffer is only meaningful on abnormal exit.
1106 const bool testCrashed = !isTestExitCodeNormal(*exitCode)
1107 && !g_testInfo.isTestRunnerInterrupted.load();
1108 if (g_options.showLogcatOutput || testCrashed) {
1109 qDebug() << "[androidtestrunner] ********** BEGIN logcat dump **********";
1110 qDebug().noquote() << filtered.trimmed();
1111 qDebug() << "[androidtestrunner] ********** END logcat dump **********";
1112 }
1113 if (testCrashed && !crashDump.isEmpty())
1114 printLogcatCrash(crashDump);
1115}
1116
1118{
1119 const bool legacyDate = g_testInfo.sdkVersion > 0 && g_testInfo.sdkVersion <= 23;
1120 const QString timeFormat = legacyDate ?
1121 "%m-%d %H:%M:%S.000"_L1 : "%Y-%m-%d %H:%M:%S.%3N"_L1;
1122
1123 QStringList dateArgs = { "shell"_L1, "date"_L1, "+'%1'"_L1.arg(timeFormat) };
1124 const QByteArray output = execAdbCommand(dateArgs, false);
1125 if (output.isNull()) {
1126 qWarning() << "[androidtestrunner] ERROR in command: adb shell date";
1127 return {};
1128 }
1129
1130 return QString::fromUtf8(output.simplified());
1131}
1132
1133static int testExitCode()
1134{
1135 using namespace std::chrono_literals;
1136 const QByteArray exitCodeOutput =
1137 adbReadAppFile(u"qtest_last_exit_code"_s, g_options.resultsPullRetries, 200ms);
1138 if (exitCodeOutput.isNull()) {
1140 g_testInfo.deviceGone.store(true);
1141 qCritical() << "[androidtestrunner] ERROR in command: adb shell cat"
1142 " files/qtest_last_exit_code";
1143 return EXIT_NOEXITCODE;
1144 }
1145 qDebug() << "[androidtestrunner] Test exitcode: " << exitCodeOutput;
1146
1147 bool ok;
1148 int exitCode = exitCodeOutput.toInt(&ok);
1149
1150 return ok ? exitCode : EXIT_NOEXITCODE;
1151}
1152
1154{
1155 return !execAdbCommand({ "uninstall"_L1, g_options.package }).isNull();
1156}
1157
1158
1159void sigHandler(int signal)
1160{
1161#if !defined(Q_OS_WIN32)
1162 // Reap the adb-tail subprocess so a second SIGINT doesn't orphan it.
1163 const qint64 loggerPid = g_testInfo.stdoutLoggerPid.exchange(0);
1164 if (loggerPid > 0)
1165 ::kill(static_cast<pid_t>(loggerPid), SIGTERM);
1166#endif
1167 std::signal(signal, SIG_DFL);
1168 if (!g_testInfo.isPackageInstalled.load())
1169 _exit(EXIT_ERROR);
1170 g_testInfo.isTestRunnerInterrupted.store(true);
1171}
1172
1173int main(int argc, char *argv[])
1174{
1175 using namespace std::chrono_literals;
1176 std::signal(SIGINT, sigHandler);
1177 std::signal(SIGTERM, sigHandler);
1178
1179 QCoreApplication a(argc, argv);
1180 if (!parseOptions())
1181 return EXIT_ERROR;
1182
1183 if (g_options.makeCommand.isEmpty()) {
1184 qCritical() << "It is required to provide a make command with the \"--make\" parameter "
1185 "to generate the apk.";
1186 return EXIT_ERROR;
1187 }
1188
1189 if (execCommand(g_options.makeCommand, true, g_options.timeoutSecs * 1s).isNull()) {
1190 qCritical("The APK build command \"%s\" failed.", qPrintable(g_options.makeCommand));
1191 return EXIT_ERROR;
1192 }
1193
1194 if (!QFile::exists(g_options.packagePath)) {
1195 qCritical("No package \"%s\" found after running the make command. "
1196 "Check the provided path and the make command.",
1197 qPrintable(g_options.packagePath));
1198 return EXIT_ERROR;
1199 }
1200
1201 const std::optional<QStringList> devices = runningDevices();
1202 if (!devices) {
1203 qCritical("Failed to query connected devices via 'adb devices'.");
1204 return EXIT_ERROR;
1205 } else if (devices->isEmpty()) {
1206 qCritical("No connected devices or running emulators can be found.");
1207 return EXIT_ERROR;
1208 } else if (!g_options.serial.isEmpty() && !devices->contains(g_options.serial)) {
1209 qCritical("No connected device or running emulator with serial '%s' can be found.",
1210 qPrintable(g_options.serial));
1211 return EXIT_ERROR;
1212 } else if (g_options.serial.isEmpty() && devices->size() == 1) {
1213 g_options.serial = devices->first();
1214 } else if (g_options.serial.isEmpty()) {
1215 qCritical("Multiple devices connected, set ANDROID_SERIAL or ANDROID_DEVICE_SERIAL.");
1216 return EXIT_ERROR;
1217 }
1218
1220
1221 g_testInfo.userId = userId();
1222
1224 return EXIT_ERROR;
1225
1226 const QString ns = getGradleProjectProperty(g_options.buildPath, "android.namespace"_L1);
1227 if (!ns.isEmpty())
1228 g_options.package = ns;
1229
1230 if (g_options.package.isEmpty()) {
1231 qCritical("Unable to get package name for '%s'", qPrintable(g_options.packagePath));
1232 return EXIT_ERROR;
1233 }
1234
1235 // parseTestArgs depends on g_options.package
1236 if (!parseTestArgs())
1237 return EXIT_ERROR;
1238
1239 // Per-user prefix so a shared TempLocation doesn't collide across users.
1240 const QString user = qEnvironmentVariable("USER",
1241 qEnvironmentVariable("USERNAME", u"default"_s));
1242 const QString lockName = u"androidtestrunner-%1-%2.lock"_s.arg(user, g_options.serial);
1243 const QDir tempDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation));
1244 QLockFile testRunnerLock(tempDir.absoluteFilePath(lockName));
1245 testRunnerLock.setStaleLockTime(0);
1246 if (!testRunnerLock.lock()) {
1247 qCritical("Failed to acquire test runner lock for '%s'.", qPrintable(g_options.serial));
1248 return EXIT_ERROR;
1249 }
1250
1251 if (g_options.packagePath.endsWith(".apk"_L1)) {
1252 const QStringList installArgs = { "install"_L1, "-r"_L1, g_options.packagePath };
1253 if (execAdbCommand(installArgs).isNull())
1254 return EXIT_ERROR;
1255 } else if (g_options.packagePath.endsWith(".aab"_L1)) {
1256 QFileInfo aab(g_options.packagePath);
1257 const auto apksFilePath = aab.absoluteDir().absoluteFilePath(aab.baseName() + ".apks"_L1);
1258 QStringList installApksArgs = { "install-apks"_L1, "--apks"_L1, apksFilePath };
1259 if (!g_options.serial.isEmpty())
1260 installApksArgs << "--device-id"_L1 << g_options.serial;
1261 if (execBundletoolCommand({ "build-apks"_L1, "--bundle"_L1, g_options.packagePath,
1262 "--output"_L1, apksFilePath, "--local-testing"_L1,
1263 "--overwrite"_L1 }).isNull()
1264 || execBundletoolCommand(installApksArgs).isNull())
1265 return EXIT_ERROR;
1266 }
1267 g_testInfo.isPackageInstalled.store(true);
1268
1269 const QStringList dangerousPermissions = queryDangerousPermissions();
1270 for (const auto &permission : g_options.permissions) {
1271 if (!dangerousPermissions.contains(permission))
1272 continue;
1273
1274 if (execAdbCommand({ "shell"_L1, "pm"_L1, "grant"_L1, "--user"_L1, g_testInfo.userId,
1275 g_options.package, permission }).isNull()) {
1276 qWarning("Unable to grant '%s' to '%s'. Probably the Android version mismatch.",
1277 qPrintable(permission), qPrintable(g_options.package));
1278 }
1279 }
1280
1281 // Call additional adb command if set after installation and before starting the test
1282 for (const auto &command : g_options.preTestRunAdbCommands) {
1283 if (execAdbCommand(command).isNull()) {
1284 qCritical("The pre test ADB command \"%s\" failed.",
1285 qUtf8Printable(command.join(u' ')));
1286 return EXIT_ERROR;
1287 }
1288 }
1289
1290 // Pre test start
1291 const QString formattedStartTime = getCurrentTimeString();
1292
1293 // Start the test
1294 if (execAdbCommand(g_options.amStarttestArgs).isNull()) {
1295 // am start -W never returns when the app crashes before main; treat the
1296 // failure as a launch-time crash and surface it from logcat.
1297 int exitCode = EXIT_NOEXITCODE;
1298 analyseLogcat(formattedStartTime, &exitCode);
1299 return exitCode;
1300 }
1301
1303
1305 qWarning("Continuing without live stdout streaming; result files are still pulled.");
1306
1308
1309 if (g_testInfo.deviceGone.load()) {
1310 qCritical("[androidtestrunner] Device '%s' became unreachable during the test, "
1311 "result transfer and uninstall skipped.", qPrintable(g_options.serial));
1313 return EXIT_DEVICE_GONE;
1314 }
1315
1316 // Post test run
1317 if (!stopStdoutLogger())
1318 return EXIT_ERROR;
1319
1320 int exitCode = testExitCode();
1321
1323 analyseLogcat(formattedStartTime, &exitCode);
1324
1325 const bool pullRes = pullResults();
1326 if (g_testInfo.deviceGone.load()) {
1327 qCritical("[androidtestrunner] Device '%s' became unreachable during cleanup, "
1328 "uninstall skipped.", qPrintable(g_options.serial));
1329 return EXIT_DEVICE_GONE;
1330 }
1331 if (!pullRes && isTestExitCodeNormal(exitCode))
1332 exitCode = EXIT_NORESULTS;
1333
1335 qWarning("Failed to uninstall test package '%s'. The test exit code is preserved.",
1336 qPrintable(g_options.package));
1337 }
1338
1339 if (g_testInfo.isTestRunnerInterrupted.load()) {
1340 qCritical() << "The androidtestrunner was interrupted and the test was cleaned up.";
1341 return EXIT_ERROR;
1342 }
1343
1344 return exitCode;
1345}
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:850
static QString runCommandAsUserArgs(const QString &cmd)
Definition main.cpp:683
static bool setupStdoutLogger()
Definition main.cpp:772
static QString getCurrentTimeString()
Definition main.cpp:1117
static int getPid(const QString &package)
Definition main.cpp:672
static QString getAbiLibsPath()
Definition main.cpp:915
static constexpr int EXIT_DEVICE_GONE
Definition main.cpp:45
static int testExitCode()
Definition main.cpp:1133
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:867
static QString deviceOutputFileName(const QString &format, const QString &hostPath)
Definition main.cpp:579
static QByteArray takeCrashDump(QByteArray *logcat)
Definition main.cpp:1030
static std::optional< QStringList > runningDevices()
Definition main.cpp:690
static QString gradleInitScriptPath()
Definition main.cpp:427
static bool deviceDisconnected()
Definition main.cpp:705
static TestInfo g_testInfo
Definition main.cpp:92
static bool isRunning()
Definition main.cpp:711
static bool pullResults()
Definition main.cpp:882
static void obtainSdkVersion()
Definition main.cpp:832
static bool collectPackagePaths(const QStringList &apkValues, const QStringList &aabValues)
Definition main.cpp:178
static constexpr auto crashBannerMarker
Definition main.cpp:982
static bool setPackagePath(const QString &path)
Definition main.cpp:168
static void analyseLogcat(const QString &timeStamp, int *exitCode)
Definition main.cpp:1076
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:823
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:731
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:984
static QByteArray filterTestLogcat(const QByteArray &logcat, int testPid, int systemServerPid)
Definition main.cpp:1040
static QByteArray execAdbCommand(const QStringList &args, bool verbose=true)
Definition main.cpp:156
static void printLogcatCrash(const QByteArray &logcat)
Definition main.cpp:934
void sigHandler(int signal)
Definition main.cpp:1159
static bool waitForLoggingStarted()
Definition main.cpp:759
static void waitForStarted()
Definition main.cpp:746
static bool stopStdoutLogger()
Definition main.cpp:796
static QStringList adbArgsWithSerial(const QStringList &args)
Definition main.cpp:149
static bool uninstallTestPackage()
Definition main.cpp:1153
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