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

source code of qtbase/src/tools/androidtestrunner/main.cpp