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

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