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
19#include <atomic>
20#include <csignal>
21#include <functional>
22#include <optional>
23#if defined(Q_OS_WIN32)
24#include <process.h>
25#else
26#include <unistd.h>
27#endif
28
29using namespace Qt::StringLiterals;
30
31#define EXIT_ERROR -1
32
33struct Options
34{
35 bool helpRequested = false;
36 bool verbose = false;
37 bool skipAddInstallRoot = false;
38 int timeoutSecs = 600; // 10 minutes
56 bool showLogcatOutput = false;
58};
59
61
63{
64 int sdkVersion = -1;
65 int pid = -1;
67
68 std::atomic<bool> isPackageInstalled { false };
69 std::atomic<bool> isTestRunnerInterrupted { false };
70};
71
73
74static bool execCommand(const QString &program, const QStringList &args,
75 QByteArray *output = nullptr, bool verbose = false)
76{
77 const auto command = program + " "_L1 + args.join(u' ');
78
79 if (verbose && g_options.verbose)
80 fprintf(stdout,"Execute %s.\n", command.toUtf8().constData());
81
82 QProcess process;
83 process.start(program, args);
84 if (!process.waitForStarted()) {
85 qCritical("Cannot execute command %s.", qPrintable(command));
86 return false;
87 }
88
89 // If the command is not adb, for example, make or ninja, it can take more that
90 // QProcess::waitForFinished() 30 secs, so for that use a higher timeout.
91 const int FinishTimeout = program.endsWith("adb"_L1) ? 30000 : g_options.timeoutSecs * 1000;
92 if (!process.waitForFinished(FinishTimeout)) {
93 qCritical("Execution of command %s timed out.", qPrintable(command));
94 return false;
95 }
96
97 const auto stdOut = process.readAllStandardOutput();
98 if (output)
99 output->append(stdOut);
100
101 if (verbose && g_options.verbose)
102 fprintf(stdout, "%s\n", stdOut.constData());
103
104 return process.exitCode() == 0;
105}
106
107static bool execAdbCommand(const QStringList &args, QByteArray *output = nullptr,
108 bool verbose = true)
109{
110 if (g_options.serial.isEmpty())
111 return execCommand(g_options.adbCommand, args, output, verbose);
112
113 QStringList argsWithSerial = {"-s"_L1, g_options.serial};
114 argsWithSerial.append(args);
115
116 return execCommand(g_options.adbCommand, argsWithSerial, output, verbose);
117}
118
119static bool execBundletoolCommand(const QStringList &args, QByteArray *output = nullptr,
120 bool verbose = true)
121{
122 QString java("java"_L1);
123 QStringList argsFull = QStringList() << "-jar"_L1 << g_options.bundletoolPath << args;
124 return execCommand(java, argsFull, output, verbose);
125}
126
127static void setPackagePath(const QString &path)
128{
129 if (!g_options.packagePath.isEmpty()) {
130 qCritical("Both --aab and --apk options provided. This is not supported.");
132 return;
133 }
134 g_options.packagePath = path;
135}
136
137static bool execCommand(const QString &command, QByteArray *output = nullptr, bool verbose = true)
138{
139 auto args = QProcess::splitCommand(command);
140 const auto program = args.first();
141 args.removeOne(program);
142 return execCommand(program, args, output, verbose);
143}
144
145static bool parseOptions()
146{
147 QStringList arguments = QCoreApplication::arguments();
148 int i = 1;
149 for (; i < arguments.size(); ++i) {
150 const QString &argument = arguments.at(i);
151 if (argument.compare("--adb"_L1, Qt::CaseInsensitive) == 0) {
152 if (i + 1 == arguments.size())
154 else
155 g_options.adbCommand = arguments.at(++i);
156 } else if (argument.compare("--bundletool"_L1, Qt::CaseInsensitive) == 0) {
157 if (i + 1 == arguments.size())
159 else
160 g_options.bundletoolPath = arguments.at(++i);
161 } else if (argument.compare("--path"_L1, Qt::CaseInsensitive) == 0) {
162 if (i + 1 == arguments.size())
164 else
165 g_options.buildPath = arguments.at(++i);
166 } else if (argument.compare("--manifest"_L1, Qt::CaseInsensitive) == 0) {
167 if (i + 1 == arguments.size())
169 else
170 g_options.manifestPath = arguments.at(++i);
171 } else if (argument.compare("--make"_L1, Qt::CaseInsensitive) == 0) {
172 if (i + 1 == arguments.size())
174 else
175 g_options.makeCommand = arguments.at(++i);
176 } else if (argument.compare("--apk"_L1, Qt::CaseInsensitive) == 0) {
177 if (i + 1 == arguments.size())
179 else
180 setPackagePath(arguments.at(++i));
181 } else if (argument.compare("--aab"_L1, Qt::CaseInsensitive) == 0) {
182 if (i + 1 == arguments.size())
184 else
185 setPackagePath(arguments.at(++i));
186 } else if (argument.compare("--activity"_L1, Qt::CaseInsensitive) == 0) {
187 if (i + 1 == arguments.size())
189 else
190 g_options.activity = arguments.at(++i);
191 } else if (argument.compare("--skip-install-root"_L1, Qt::CaseInsensitive) == 0) {
193 } else if (argument.compare("--show-logcat"_L1, Qt::CaseInsensitive) == 0) {
195 } else if (argument.compare("--ndk-stack"_L1, Qt::CaseInsensitive) == 0) {
196 if (i + 1 == arguments.size())
198 else
199 g_options.ndkStackPath = arguments.at(++i);
200 } else if (argument.compare("--timeout"_L1, Qt::CaseInsensitive) == 0) {
201 if (i + 1 == arguments.size())
203 else
204 g_options.timeoutSecs = arguments.at(++i).toInt();
205 } else if (argument.compare("--help"_L1, Qt::CaseInsensitive) == 0) {
207 } else if (argument.compare("--verbose"_L1, Qt::CaseInsensitive) == 0) {
208 g_options.verbose = true;
209 } else if (argument.compare("--pre-test-adb-command"_L1, Qt::CaseInsensitive) == 0) {
210 if (i + 1 == arguments.size())
212 else {
213 g_options.preTestRunAdbCommands += QProcess::splitCommand(arguments.at(++i));
214 }
215 } else if (argument.compare("--"_L1, Qt::CaseInsensitive) == 0) {
216 ++i;
217 break;
218 } else {
219 g_options.testArgsList << arguments.at(i);
220 }
221 }
222
224 // we need to run make INSTALL_ROOT=path install to install the application file(s) first
225 g_options.makeCommand = "%1 INSTALL_ROOT=%2 install"_L1
226 .arg(g_options.makeCommand)
227 .arg(QDir::toNativeSeparators(g_options.buildPath));
228 }
229
230 for (;i < arguments.size(); ++i)
231 g_options.testArgsList << arguments.at(i);
232
233 if (g_options.helpRequested || g_options.buildPath.isEmpty() || g_options.packagePath.isEmpty())
234 return false;
235
236 g_options.serial = qEnvironmentVariable("ANDROID_SERIAL");
237 if (g_options.serial.isEmpty())
238 g_options.serial = qEnvironmentVariable("ANDROID_DEVICE_SERIAL");
239
240 if (g_options.ndkStackPath.isEmpty()) {
241 const QString ndkPath = qEnvironmentVariable("ANDROID_NDK_ROOT");
242 const QString ndkStackPath = ndkPath + QDir::separator() + "ndk-stack"_L1;
243 if (QFile::exists(ndkStackPath))
244 g_options.ndkStackPath = ndkStackPath;
245 }
246
247 if (g_options.manifestPath.isEmpty())
248 g_options.manifestPath = g_options.buildPath + "/AndroidManifest.xml"_L1;
249
250 return true;
251}
252
253static void printHelp()
254{
255 qWarning("Syntax: %s <options> -- [TESTARGS] \n"
256 "\n"
257 " Runs a Qt for Android test on an emulator or a device. Specify a device\n"
258 " using the environment variables ANDROID_SERIAL or ANDROID_DEVICE_SERIAL.\n"
259 " Returns the number of failed tests, -1 on test runner deployment related\n"
260 " failures or zero on success."
261 "\n"
262 " Mandatory arguments:\n"
263 " --path <path>: The path where androiddeployqt builds the android package.\n"
264 "\n"
265 " --make <make cmd>: make command to create an APK, for example:\n"
266 " \"cmake --build <build-dir> --target <target>_make_apk\".\n"
267 "\n"
268 " --apk <apk path>: The test apk path. The apk has to exist already, if it\n"
269 " does not exist the make command must be provided for building the apk.\n"
270 "\n"
271 " --aab <aab path>: The test aab path. The aab has to exist already, if it\n"
272 " does not exist the make command must be provided for building the aab.\n"
273 "\n"
274 " Optional arguments:\n"
275 " --adb <adb cmd>: The Android ADB command. If missing the one from\n"
276 " $PATH will be used.\n"
277 "\n"
278 " --activity <acitvity>: The Activity to run. If missing the first\n"
279 " activity from AndroidManifest.qml file will be used.\n"
280 "\n"
281 " --timeout <seconds>: Timeout to run the test. Default is 10 minutes.\n"
282 "\n"
283 " --skip-install-root: Do not append INSTALL_ROOT=... to the make command.\n"
284 "\n"
285 " --show-logcat: Print Logcat output to stdout. If an ANR occurs during\n"
286 " the test run, logs from the system_server process are included.\n"
287 "\n"
288 " --ndk-stack: Path to ndk-stack tool that symbolizes crash stacktraces.\n"
289 " By default, ANDROID_NDK_ROOT env var is used to deduce the tool path.\n"
290 "\n"
291 " -- Arguments that will be passed to the test application.\n"
292 "\n"
293 " --verbose: Prints out information during processing.\n"
294 "\n"
295 " --pre-test-adb-command <command>: call the adb <command> after\n"
296 " installation and before the test run.\n"
297 "\n"
298 " --manifest <path>: Custom path to the AndroidManifest.xml.\n"
299 "\n"
300 " --bundletool <bundletool path>: The path to Android bundletool.\n"
301 " See https://developer.android.com/tools/bundletool for details.\n"
302 "\n"
303 " --help: Displays this information.\n",
304 qPrintable(QCoreApplication::arguments().at(0)));
305}
306
308{
309 QFile androidManifestXml(g_options.manifestPath);
310 if (!androidManifestXml.open(QIODevice::ReadOnly)) {
311 qCritical("Unable to read android manifest '%s'", qPrintable(g_options.manifestPath));
312 return false;
313 }
314
315 QXmlStreamReader reader(&androidManifestXml);
316 while (!reader.atEnd()) {
317 reader.readNext();
318 if (!reader.isStartElement())
319 continue;
320
321 if (reader.name() == "manifest"_L1)
322 g_options.package = reader.attributes().value("package"_L1).toString();
323 else if (reader.name() == "activity"_L1 && g_options.activity.isEmpty())
324 g_options.activity = reader.attributes().value("android:name"_L1).toString();
325 else if (reader.name() == "uses-permission"_L1)
326 g_options.permissions.append(reader.attributes().value("android:name"_L1).toString());
327 }
328 return true;
329}
330
331static void setOutputFile(QString file, QString format)
332{
333 if (format.isEmpty())
334 format = "txt"_L1;
335
336 if ((file.isEmpty() || file == u'-')) {
337 if (g_options.outFiles.contains(format)) {
338 file = g_options.outFiles.value(format);
339 } else {
340 file = "stdout.%1"_L1.arg(format);
341 g_options.outFiles[format] = file;
342 }
343 g_options.stdoutFileName = QFileInfo(file).fileName();
344 } else {
345 g_options.outFiles[format] = file;
346 }
347}
348
349static bool parseTestArgs()
350{
351 QRegularExpression oldFormats{"^-(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
352 QRegularExpression newLoggingFormat{"^(.*),(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
353
354 QString file;
355 QString logType;
356 QStringList unhandledArgs;
357 for (int i = 0; i < g_options.testArgsList.size(); ++i) {
358 const QString &arg = g_options.testArgsList[i].trimmed();
359 if (arg == "--"_L1)
360 continue;
361 if (arg == "-o"_L1) {
362 if (i >= g_options.testArgsList.size() - 1)
363 return false; // missing file argument
364
365 const auto &filePath = g_options.testArgsList[++i];
366 const auto match = newLoggingFormat.match(filePath);
367 if (!match.hasMatch()) {
368 file = filePath;
369 } else {
370 const auto capturedTexts = match.capturedTexts();
371 setOutputFile(capturedTexts.at(1), capturedTexts.at(2));
372 }
373 } else {
374 auto match = oldFormats.match(arg);
375 if (match.hasMatch()) {
376 logType = match.capturedTexts().at(1);
377 } else {
378 // Use triple literal quotes so that QProcess::splitCommand() in androidjnimain.cpp
379 // keeps quotes characters inside the string.
380 QString quotedArg = QString(arg).replace("\""_L1, "\\\"\\\"\\\""_L1);
381 // Escape single quotes so they don't interfere with the shell command,
382 // and so they get passed to the app as single quote inside the string.
383 quotedArg.replace("'"_L1, "\'"_L1);
384 // Add escaped double quote character so that args with spaces are treated as one.
385 unhandledArgs << " \\\"%1\\\""_L1.arg(quotedArg);
386 }
387 }
388 }
389 if (g_options.outFiles.isEmpty() || !file.isEmpty() || !logType.isEmpty())
390 setOutputFile(file, logType);
391
392 QString testAppArgs;
393 for (auto it = g_options.outFiles.constBegin(); it != g_options.outFiles.constEnd(); ++it)
394 testAppArgs += "-o %1,%2 "_L1.arg(QFileInfo(it.value()).fileName(), it.key());
395
396 testAppArgs += unhandledArgs.join(u' ').trimmed();
397 testAppArgs = "\"%1\""_L1.arg(testAppArgs.trimmed());
398 const QString activityName = "%1/%2"_L1.arg(g_options.package).arg(g_options.activity);
399
400 // Pass over any qt or testlib env vars if set
401 QString testEnvVars;
402 const QStringList envVarsList = QProcessEnvironment::systemEnvironment().toStringList();
403 for (const QString &var : envVarsList) {
404 if (var.startsWith("QTEST_"_L1) || var.startsWith("QT_"_L1))
405 testEnvVars += "%1 "_L1.arg(var);
406 }
407
408 if (!testEnvVars.isEmpty()) {
409 testEnvVars = QString::fromUtf8(testEnvVars.trimmed().toUtf8().toBase64());
410 testEnvVars = "-e extraenvvars \"%4\""_L1.arg(testEnvVars);
411 }
412
413 g_options.amStarttestArgs = { "shell"_L1, "am"_L1, "start"_L1,
414 "-n"_L1, activityName,
415 "-e"_L1, "applicationArguments"_L1, testAppArgs,
416 testEnvVars
417 };
418
419 return true;
420}
421
422static int getPid(const QString &package)
423{
424 QByteArray output;
425 const QStringList psArgs = { "shell"_L1, "ps | grep ' %1'"_L1.arg(package) };
426 if (!execAdbCommand(psArgs, &output, false))
427 return false;
428
429 const QList<QByteArray> lines = output.split(u'\n');
430 if (lines.size() < 1)
431 return false;
432
433 QList<QByteArray> columns = lines.first().simplified().replace(u'\t', u' ').split(u' ');
434 if (columns.size() < 3)
435 return false;
436
437 bool ok = false;
438 int pid = columns.at(1).toInt(&ok);
439 if (ok)
440 return pid;
441
442 return -1;
443}
444
445static QString runCommandAsUserArgs(const QString &cmd)
446{
447 return "run-as %1 --user %2 %3"_L1.arg(g_options.package, g_testInfo.userId, cmd);
448}
449
450static bool isRunning() {
451 if (g_testInfo.pid < 1)
452 return false;
453
454 QByteArray output;
455 const QStringList psArgs = { "shell"_L1, "ps"_L1, "-p"_L1, QString::number(g_testInfo.pid),
456 "|"_L1, "grep"_L1, "-o"_L1, " %1$"_L1.arg(g_options.package) };
457 bool psSuccess = false;
458 for (int i = 1; i <= 3; ++i) {
459 psSuccess = execAdbCommand(psArgs, &output, false);
460 if (psSuccess)
461 break;
462 QThread::msleep(250);
463 }
464
465 return psSuccess && output.trimmed() == g_options.package.toUtf8();
466}
467
468static void waitForStarted()
469{
470 // wait to start and set PID
471 QDeadlineTimer startDeadline(10000);
472 do {
473 g_testInfo.pid = getPid(g_options.package);
474 if (g_testInfo.pid > 0)
475 break;
476 QThread::msleep(100);
477 } while (!startDeadline.hasExpired() && !g_testInfo.isTestRunnerInterrupted.load());
478}
479
481{
482 const QString lsCmd = "ls files/%1"_L1.arg(g_options.stdoutFileName);
483 const QStringList adbLsCmd = { "shell"_L1, runCommandAsUserArgs(lsCmd) };
484
485 QDeadlineTimer deadline(5000);
486 do {
487 if (execAdbCommand(adbLsCmd, nullptr, false))
488 break;
489 QThread::msleep(100);
490 } while (!deadline.hasExpired() && !g_testInfo.isTestRunnerInterrupted.load());
491}
492
493static bool setupStdoutLogger()
494{
495 // Start tail to get results to stdout as soon as they're available
496 const QString tailPipeCmd = "tail -n +1 -f files/%1"_L1.arg(g_options.stdoutFileName);
497 const QStringList adbTailCmd = { "shell"_L1, runCommandAsUserArgs(tailPipeCmd) };
498
499 g_options.stdoutLogger.emplace();
500 g_options.stdoutLogger->setProcessChannelMode(QProcess::ForwardedOutputChannel);
501 g_options.stdoutLogger->start(g_options.adbCommand, adbTailCmd);
502
503 if (!g_options.stdoutLogger->waitForStarted()) {
504 qCritical() << "Error: failed to run adb command to fetch stdout test results.";
505 g_options.stdoutLogger = std::nullopt;
506 return false;
507 }
508
509 return true;
510}
511
512static bool stopStdoutLogger()
513{
514 if (!g_options.stdoutLogger.has_value()) {
515 // In case this ever happens, it setupStdoutLogger() wasn't called, whether
516 // that's on purpose or not, return true since what it does is achieved.
517 qCritical() << "Trying to stop the stdout logger process while it's been uninitialised";
518 return true;
519 }
520
521 if (g_options.stdoutLogger->state() == QProcess::NotRunning) {
522 // We expect the tail command to be running until we stop it, so if it's
523 // not running it might have been terminated outside of the test runner.
524 qCritical() << "The stdout logger process was terminated unexpectedly, "
525 "It might have been terminated by an external process";
526 return false;
527 }
528
529 g_options.stdoutLogger->terminate();
530
531 if (!g_options.stdoutLogger->waitForFinished()) {
532 qCritical() << "Error: adb test results tail command timed out.";
533 return false;
534 }
535
536 return true;
537}
538
539static void waitForFinished()
540{
541 // Wait to finish
542 QDeadlineTimer finishedDeadline(g_options.timeoutSecs * 1000);
543 do {
544 if (!isRunning())
545 break;
546 QThread::msleep(250);
547 } while (!finishedDeadline.hasExpired() && !g_testInfo.isTestRunnerInterrupted.load());
548
549 if (finishedDeadline.hasExpired())
550 qWarning() << "Timed out while waiting for the test to finish";
551}
552
553static void obtainSdkVersion()
554{
555 // SDK version is necessary, as in SDK 23 pidof is broken, so we cannot obtain the pid.
556 // Also, Logcat cannot filter by pid in SDK 23, so we don't offer the --show-logcat option.
557 QByteArray output;
558 const QStringList versionArgs = { "shell"_L1, "getprop"_L1, "ro.build.version.sdk"_L1 };
559 execAdbCommand(versionArgs, &output, false);
560 bool ok = false;
561 int sdkVersion = output.toInt(&ok);
562 if (ok)
563 g_testInfo.sdkVersion = sdkVersion;
564 else
565 qCritical() << "Unable to obtain the SDK version of the target.";
566}
567
569{
570 // adb get-current-user command is available starting from API level 26.
571 QByteArray userId;
572 if (g_testInfo.sdkVersion >= 26) {
573 const QStringList userIdArgs = {"shell"_L1, "cmd"_L1, "activity"_L1, "get-current-user"_L1};
574 if (!execAdbCommand(userIdArgs, &userId, false)) {
575 qCritical() << "Error: failed to retrieve the user ID";
576 userId.clear();
577 }
578 }
579
580 if (userId.isEmpty())
581 userId = "0";
582
583 return QString::fromUtf8(userId.simplified());
584}
585
587{
588 QByteArray output;
589 execAdbCommand({ "devices"_L1 }, &output, false);
590
591 QStringList devices;
592 for (const QByteArray &line : output.split(u'\n')) {
593 if (line.contains("\tdevice"_L1))
594 devices.append(QString::fromUtf8(line.split(u'\t').first()));
595 }
596
597 return devices;
598}
599
600static bool pullResults()
601{
602 for (auto it = g_options.outFiles.constBegin(); it != g_options.outFiles.constEnd(); ++it) {
603 const QString filePath = it.value();
604 const QString fileName = QFileInfo(filePath).fileName();
605 // Get only stdout from cat and get rid of stderr and fail later if the output is empty
606 const QString catCmd = "cat files/%1 2> /dev/null"_L1.arg(fileName);
607 const QStringList fullCatArgs = { "shell"_L1, runCommandAsUserArgs(catCmd) };
608
609 bool catSuccess = false;
610 QByteArray output;
611
612 for (int i = 1; i <= g_options.resultsPullRetries; ++i) {
613 catSuccess = execAdbCommand(fullCatArgs, &output, false);
614 if (!catSuccess)
615 continue;
616 else if (!output.isEmpty())
617 break;
618 }
619
620 if (!catSuccess) {
621 qCritical() << "Error: failed to retrieve the test result file %1."_L1.arg(fileName);
622 return false;
623 }
624
625 if (output.isEmpty()) {
626 qCritical() << "Error: the test result file %1 is empty."_L1.arg(fileName);
627 return false;
628 }
629
630 QFile out{filePath};
631 if (!out.open(QIODevice::WriteOnly)) {
632 qCritical() << "Error: failed to open %1 to write results to host."_L1.arg(filePath);
633 return false;
634 }
635 out.write(output);
636 }
637
638 return true;
639}
640
642{
643 QString libsPath = "%1/libs/"_L1.arg(g_options.buildPath);
644 const QStringList abiArgs = { "shell"_L1, "getprop"_L1, "ro.product.cpu.abi"_L1 };
645 QByteArray abi;
646 if (!execAdbCommand(abiArgs, &abi, false)) {
647 QStringList subDirs = QDir(libsPath).entryList(QDir::Dirs | QDir::NoDotAndDotDot);
648 if (!subDirs.isEmpty())
649 abi = subDirs.first().toUtf8();
650 }
651
652 abi = abi.trimmed();
653 if (abi.isEmpty())
654 qWarning() << "Failed to get the libs abi, falling to host architecture";
655
656 QString hostArch = QSysInfo::currentCpuArchitecture();
657 if (hostArch == "x86_64"_L1)
658 abi = "arm64-x86_64";
659 else if (hostArch == "arm64"_L1)
660 abi = "arm64-v8a";
661 else if (hostArch == "i386"_L1)
662 abi = "x86";
663 else
664 abi = "armeabi-v7a";
665
666 return libsPath + QString::fromUtf8(abi);
667}
668
669void printLogcatCrash(const QByteArray &logcat)
670{
671 // No crash report, do nothing
672 if (logcat.isEmpty())
673 return;
674
675 QByteArray crashLogcat(logcat);
676 if (!g_options.ndkStackPath.isEmpty()) {
677 QProcess ndkStackProc;
678 ndkStackProc.start(g_options.ndkStackPath, { "-sym"_L1, getAbiLibsPath() });
679
680 if (ndkStackProc.waitForStarted()) {
681 ndkStackProc.write(crashLogcat);
682 ndkStackProc.closeWriteChannel();
683
684 if (ndkStackProc.waitForReadyRead())
685 crashLogcat = ndkStackProc.readAllStandardOutput();
686
687 ndkStackProc.terminate();
688 if (!ndkStackProc.waitForFinished())
689 qCritical() << "Error: ndk-stack command timed out.";
690 } else {
691 qCritical() << "Error: failed to run ndk-stack command.";
692 return;
693 }
694 } else {
695 qWarning() << "Warning: ndk-stack path not provided and couldn't be deduced "
696 "using the ANDROID_NDK_ROOT environment variable.";
697 }
698
699 if (!crashLogcat.startsWith("********** Crash dump"))
700 qDebug() << "********** Crash dump: **********";
701 qDebug().noquote() << crashLogcat.trimmed();
702 qDebug() << "********** End crash dump **********";
703}
704
705void analyseLogcat(const QString &timeStamp, int *exitCode)
706{
707 QStringList logcatArgs = { "shell"_L1, "logcat"_L1, "-t"_L1, "'%1'"_L1.arg(timeStamp),
708 "-v"_L1, "brief"_L1 };
709
710 const bool useColor = qEnvironmentVariable("QTEST_ENVIRONMENT") != "ci"_L1;
711 if (useColor)
712 logcatArgs << "-v"_L1 << "color"_L1;
713
714 QByteArray logcat;
715 if (!execAdbCommand(logcatArgs, &logcat, false)) {
716 qCritical() << "Error: failed to fetch logcat of the test";
717 return;
718 }
719
720 if (logcat.isEmpty()) {
721 qWarning() << "The retrieved logcat is empty";
722 return;
723 }
724
725 const QByteArray crashMarker("*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***");
726 int crashMarkerIndex = logcat.indexOf(crashMarker);
727 QByteArray crashLogcat;
728
729 if (crashMarkerIndex != -1) {
730 crashLogcat = logcat.mid(crashMarkerIndex);
731 logcat = logcat.left(crashMarkerIndex);
732 }
733
734 // Check for ANRs
735 const bool anrOccurred = logcat.contains("ANR in %1"_L1.arg(g_options.package).toUtf8());
736 if (anrOccurred) {
737 // Treat a found ANR as a test failure.
738 *exitCode = *exitCode < 1 ? 1 : *exitCode;
739 qCritical("An ANR has occurred while running the test %s. The logcat will include "
740 "additional logs from the system_server process.",
741 qPrintable(g_options.package));
742 }
743
744 int systemServerPid = getPid("system_server"_L1);
745
746 static const QRegularExpression logcatRegEx{
747 "(?:^\\x1B\\[[0-9]+m)?" // color
748 "(\\w)/" // message type 1. capture
749 ".*" // source
750 "(\\‍(\\s*\\d*\\‍)):" // pid 2. capture
751 "\\s*"
752 ".*" // message
753 "(?:\\x1B\\[[0-9]+m)?" // color
754 "[\\n\\r]*$"_L1
755 };
756
757 QByteArrayList testLogcat;
758 for (const QByteArray &line : logcat.split(u'\n')) {
759 QRegularExpressionMatch match = logcatRegEx.match(QString::fromUtf8(line));
760 if (match.hasMatch()) {
761 const QString msgType = match.captured(1);
762 const QString pidStr = match.captured(2);
763 const int capturedPid = pidStr.mid(1, pidStr.size() - 2).trimmed().toInt();
764 if (capturedPid == g_testInfo.pid || msgType == u'F')
765 testLogcat.append(line);
766 else if (anrOccurred && capturedPid == systemServerPid)
767 testLogcat.append(line);
768 } else {
769 // If we can't match then just print everything
770 testLogcat.append(line);
771 }
772 }
773
774 // If we have a crash, attempt to print both logcat and the crash buffer which
775 // includes the crash stacktrace that is not included in the default logcat.
776 const bool testCrashed = *exitCode == EXIT_ERROR && !g_testInfo.isTestRunnerInterrupted.load();
777 if (testCrashed) {
778 qDebug() << "********** logcat dump **********";
779 qDebug().noquote() << testLogcat.join(u'\n').trimmed();
780 qDebug() << "********** End logcat dump **********";
781
782 if (!crashLogcat.isEmpty())
783 printLogcatCrash(crashLogcat);
784 }
785}
786
788{
789 const QString timeFormat = (g_testInfo.sdkVersion <= 23) ?
790 "%m-%d %H:%M:%S.000"_L1 : "%Y-%m-%d %H:%M:%S.%3N"_L1;
791
792 QStringList dateArgs = { "shell"_L1, "date"_L1, "+'%1'"_L1.arg(timeFormat) };
793 QByteArray output;
794 if (!execAdbCommand(dateArgs, &output, false)) {
795 qWarning() << "Date/time adb command failed";
796 return {};
797 }
798
799 return QString::fromUtf8(output.simplified());
800}
801
802static int testExitCode()
803{
804 QByteArray exitCodeOutput;
805 const QString exitCodeCmd = "cat files/qtest_last_exit_code 2> /dev/null"_L1;
806 if (!execAdbCommand({ "shell"_L1, runCommandAsUserArgs(exitCodeCmd) }, &exitCodeOutput, false)) {
807 qCritical() << "Failed to retrieve the test exit code.";
808 return EXIT_ERROR;
809 }
810
811 bool ok;
812 int exitCode = exitCodeOutput.toInt(&ok);
813
814 return ok ? exitCode : EXIT_ERROR;
815}
816
818{
819 return execAdbCommand({ "uninstall"_L1, g_options.package }, nullptr);
820}
821
823{
826
827 void acquire() { isAcquired.store(semaphore.acquire()); }
828
829 void release()
830 {
831 bool expected = true;
832 // NOTE: There's still could be tiny time gap between the compare_exchange_strong() call
833 // and release() call where the thread could be interrupted, if that's ever an issue,
834 // this code could be checked and improved further.
835 if (isAcquired.compare_exchange_strong(expected, false))
836 isAcquired.store(!semaphore.release());
837 }
838
839 std::atomic<bool> isAcquired { false };
842};
843
845
846void sigHandler(int signal)
847{
848 std::signal(signal, SIG_DFL);
850 // Ideally we shouldn't be doing such calls from a signal handler,
851 // and we can't use QSocketNotifier because this tool doesn't spin
852 // a main event loop. Since, there's no other alternative to do this,
853 // let's do the cleanup anyway.
854 if (!g_testInfo.isPackageInstalled.load())
855 _exit(-1);
856 g_testInfo.isTestRunnerInterrupted.store(true);
857}
858
859int main(int argc, char *argv[])
860{
861 std::signal(SIGINT, sigHandler);
862 std::signal(SIGTERM, sigHandler);
863
864 QCoreApplication a(argc, argv);
865 if (!parseOptions()) {
867 return EXIT_ERROR;
868 }
869
870 if (g_options.makeCommand.isEmpty()) {
871 qCritical() << "It is required to provide a make command with the \"--make\" parameter "
872 "to generate the apk.";
873 return EXIT_ERROR;
874 }
875
876 QByteArray buildOutput;
877 if (!execCommand(g_options.makeCommand, &buildOutput, true)) {
878 qCritical("The APK build command \"%s\" failed\n\n%s",
879 qPrintable(g_options.makeCommand), buildOutput.constData());
880 return EXIT_ERROR;
881 }
882
883 if (!QFile::exists(g_options.packagePath)) {
884 qCritical("No apk \"%s\" found after running the make command. "
885 "Check the provided path and the make command.",
886 qPrintable(g_options.packagePath));
887 return EXIT_ERROR;
888 }
889
890 const QStringList devices = runningDevices();
891 if (devices.isEmpty()) {
892 qCritical("No connected devices or running emulators can be found.");
893 return EXIT_ERROR;
894 } else if (!g_options.serial.isEmpty() && !devices.contains(g_options.serial)) {
895 qCritical("No connected device or running emulator with serial '%s' can be found.",
896 qPrintable(g_options.serial));
897 return EXIT_ERROR;
898 }
899
901
902 g_testInfo.userId = userId();
903
904 if (!QFile::exists(g_options.manifestPath)) {
905 qCritical("Unable to find '%s'.", qPrintable(g_options.manifestPath));
906 return EXIT_ERROR;
907 }
908
910 return EXIT_ERROR;
911
912 if (g_options.package.isEmpty()) {
913 qCritical("Unable to get package name for '%s'", qPrintable(g_options.packagePath));
914 return EXIT_ERROR;
915 }
916
917 // parseTestArgs depends on g_options.package
918 if (!parseTestArgs())
919 return EXIT_ERROR;
920
921 // do not install or run packages while another test is running
923
924 if (g_options.packagePath.endsWith(".apk"_L1)) {
925 const QStringList installArgs = { "install"_L1, "-r"_L1, g_options.packagePath };
926 g_testInfo.isPackageInstalled.store(execAdbCommand(installArgs, nullptr));
927 if (!g_testInfo.isPackageInstalled)
928 return EXIT_ERROR;
929 } else if (g_options.packagePath.endsWith(".aab"_L1)) {
930 QFileInfo aab(g_options.packagePath);
931 const auto apksFilePath = aab.absoluteDir().absoluteFilePath(aab.baseName() + ".apks"_L1);
932 if (!execBundletoolCommand({ "build-apks"_L1, "--bundle"_L1, g_options.packagePath,
933 "--output"_L1, apksFilePath, "--local-testing"_L1,
934 "--overwrite"_L1 }))
935 return EXIT_ERROR;
936
937 if (!execBundletoolCommand({ "install-apks"_L1, "--apks"_L1, apksFilePath }))
938 return EXIT_ERROR;
939 }
940
941 for (const auto &permission : g_options.permissions) {
942 if (!execAdbCommand({ "shell"_L1, "pm"_L1, "grant"_L1, g_options.package, permission },
943 nullptr)) {
944 qWarning("Unable to grant '%s' to '%s'. Probably the Android version mismatch.",
945 qPrintable(permission), qPrintable(g_options.package));
946 }
947 }
948
949 // Call additional adb command if set after installation and before starting the test
950 for (const auto &command : g_options.preTestRunAdbCommands) {
951 QByteArray output;
952 if (!execAdbCommand(command, &output)) {
953 qCritical("The pre test ADB command \"%s\" failed with output:\n%s",
954 qUtf8Printable(command.join(u' ')), output.constData());
955 return EXIT_ERROR;
956 }
957 }
958
959 // Pre test start
960 const QString formattedStartTime = getCurrentTimeString();
961
962 // Start the test
963 if (!execAdbCommand(g_options.amStarttestArgs, nullptr))
964 return EXIT_ERROR;
965
968
970 return EXIT_ERROR;
971
973
974 // Post test run
976 return EXIT_ERROR;
977
978 int exitCode = testExitCode();
979
981 analyseLogcat(formattedStartTime, &exitCode);
982
983 exitCode = pullResults() ? exitCode : EXIT_ERROR;
984
986 return EXIT_ERROR;
987
989
990 if (g_testInfo.isTestRunnerInterrupted.load()) {
991 qCritical() << "The androidtestrunner was interrupted and the was test cleaned up.";
992 return EXIT_ERROR;
993 }
994
995 return exitCode;
996}
#define EXIT_ERROR
Definition main.cpp:31
static QString userId()
Definition main.cpp:568
static QString runCommandAsUserArgs(const QString &cmd)
Definition main.cpp:445
static void waitForLoggingStarted()
Definition main.cpp:480
static bool setupStdoutLogger()
Definition main.cpp:493
static QString getCurrentTimeString()
Definition main.cpp:787
static int getPid(const QString &package)
Definition main.cpp:422
static QString getAbiLibsPath()
Definition main.cpp:641
static int testExitCode()
Definition main.cpp:802
static void setOutputFile(QString file, QString format)
Definition main.cpp:331
static bool parseTestArgs()
Definition main.cpp:349
static TestInfo g_testInfo
Definition main.cpp:72
static bool isRunning()
Definition main.cpp:450
static bool pullResults()
Definition main.cpp:600
static void obtainSdkVersion()
Definition main.cpp:553
static bool execAdbCommand(const QStringList &args, QByteArray *output=nullptr, bool verbose=true)
Definition main.cpp:107
void sigHandler(int signal)
Definition main.cpp:846
static void setPackagePath(const QString &path)
Definition main.cpp:127
static bool execBundletoolCommand(const QStringList &args, QByteArray *output=nullptr, bool verbose=true)
Definition main.cpp:119
static void waitForFinished()
Definition main.cpp:539
static bool parseOptions()
Definition main.cpp:145
void printLogcatCrash(const QByteArray &logcat)
Definition main.cpp:669
TestRunnerSystemSemaphore testRunnerLock
Definition main.cpp:844
void analyseLogcat(const QString &timeStamp, int *exitCode)
Definition main.cpp:705
static void printHelp()
Definition main.cpp:253
static bool execCommand(const QString &command, QByteArray *output=nullptr, bool verbose=true)
Definition main.cpp:137
static void waitForStarted()
Definition main.cpp:468
static bool execCommand(const QString &program, const QStringList &args, QByteArray *output=nullptr, bool verbose=false)
Definition main.cpp:74
static bool stopStdoutLogger()
Definition main.cpp:512
static bool uninstallTestPackage()
Definition main.cpp:817
static bool processAndroidManifest()
Definition main.cpp:307
static Options g_options
Definition main.cpp:60
static QStringList runningDevices()
Definition main.cpp:586
int main(int argc, char *argv[])
[ctor_close]
QString makeCommand
Definition main.cpp:45
QStringList amStarttestArgs
Definition main.cpp:52
std::optional< QProcess > stdoutLogger
Definition main.cpp:57
QHash< QString, QString > outFiles
Definition main.cpp:51
QString adbCommand
Definition main.cpp:42
bool helpRequested
Definition main.cpp:129
int timeoutSecs
Definition main.cpp:38
QStringList permissions
Definition main.cpp:48
bool showLogcatOutput
Definition main.cpp:56
QString bundletoolPath
Definition main.cpp:43
int resultsPullRetries
Definition main.cpp:39
QString stdoutFileName
Definition main.cpp:50
bool skipAddInstallRoot
Definition main.cpp:37
QList< QStringList > preTestRunAdbCommands
Definition main.cpp:55
QString package
Definition main.cpp:46
QString serial
Definition main.cpp:44
QString packagePath
Definition main.cpp:53
bool verbose
Definition main.cpp:130
QString ndkStackPath
Definition main.cpp:54
QStringList testArgsList
Definition main.cpp:49
QString manifestPath
Definition main.cpp:41
QString activity
Definition main.cpp:47
QString buildPath
Definition main.cpp:40
QString userId
Definition main.cpp:66
std::atomic< bool > isPackageInstalled
Definition main.cpp:68
int sdkVersion
Definition main.cpp:64
int pid
Definition main.cpp:65
std::atomic< bool > isTestRunnerInterrupted
Definition main.cpp:69
QSystemSemaphore semaphore
Definition main.cpp:840
std::atomic< bool > isAcquired
Definition main.cpp:839