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