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