Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
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
16#include <atomic>
17#include <csignal>
18#include <functional>
19#if defined(Q_OS_WIN32)
20#include <process.h>
21#else
22#include <unistd.h>
23#endif
24
25using namespace Qt::StringLiterals;
26
27static bool checkJunit(const QByteArray &data) {
28 QXmlStreamReader reader{data};
29 while (!reader.atEnd()) {
30 reader.readNext();
31
32 if (!reader.isStartElement())
33 continue;
34
35 if (reader.name() == "error"_L1)
36 return false;
37
38 const QString type = reader.attributes().value("type"_L1).toString();
39 if (reader.name() == "failure"_L1) {
40 if (type == "fail"_L1 || type == "xpass"_L1)
41 return false;
42 }
43 }
44
45 // Fail if there's an error after reading through all the xml output
46 return !reader.hasError();
47}
48
49static bool checkTxt(const QByteArray &data) {
50 if (data.indexOf("\nFAIL! : "_L1) >= 0)
51 return false;
52 if (data.indexOf("\nXPASS : "_L1) >= 0)
53 return false;
54 // Look for "********* Finished testing of tst_QTestName *********"
55 static const QRegularExpression testTail("\\*+ +Finished testing of .+ +\\*+"_L1);
56 return testTail.match(QLatin1StringView(data)).hasMatch();
57}
58
59static bool checkCsv(const QByteArray &data) {
60 // The csv format is only suitable for benchmarks,
61 // so this is not much useful to determine test failure/success.
62 // FIXME: warn the user early on about this.
64 return true;
65}
66
67static bool checkXml(const QByteArray &data) {
68 QXmlStreamReader reader{data};
69 while (!reader.atEnd()) {
70 reader.readNext();
71 const QString type = reader.attributes().value("type"_L1).toString();
72 const bool isIncident = (reader.name() == "Incident"_L1);
73 if (reader.isStartElement() && isIncident) {
74 if (type == "fail"_L1 || type == "xpass"_L1)
75 return false;
76 }
77 }
78
79 // Fail if there's an error after reading through all the xml output
80 return !reader.hasError();
81}
82
83static bool checkLightxml(const QByteArray &data) {
84 // lightxml intentionally skips the root element, which technically makes it
85 // not valid XML. We'll add that ourselves for the purpose of validation.
86 QByteArray newData = data;
87 newData.prepend("<root>");
88 newData.append("</root>");
89 return checkXml(newData);
90}
91
92static bool checkTeamcity(const QByteArray &data) {
93 if (data.indexOf("' message='Failure! |[Loc: ") >= 0)
94 return false;
95 const QList<QByteArray> lines = data.trimmed().split('\n');
96 if (lines.isEmpty())
97 return false;
98 return lines.last().startsWith("##teamcity[testSuiteFinished "_L1);
99}
100
101static bool checkTap(const QByteArray &data) {
102 // This will still report blacklisted fails because QTest with TAP
103 // is not putting any data about that.
104 if (data.indexOf("\nnot ok ") >= 0)
105 return false;
106
107 static const QRegularExpression testTail("ok [0-9]* - cleanupTestCase\\(\\)"_L1);
108 return testTail.match(QLatin1StringView(data)).hasMatch();
109}
110
111struct Options
112{
113 bool helpRequested = false;
114 bool verbose = false;
115 bool skipAddInstallRoot = false;
116 int timeoutSecs = 600; // 10 minutes
123 QHash<QString, QString> outFiles;
127 bool showLogcatOutput = false;
128 const QHash<QString, std::function<bool(const QByteArray &)>> checkFiles = {
129 {"txt"_L1, checkTxt},
130 {"csv"_L1, checkCsv},
131 {"xml"_L1, checkXml},
132 {"lightxml"_L1, checkLightxml},
133 {"xunitxml"_L1, checkJunit},
134 {"junitxml"_L1, checkJunit},
135 {"teamcity"_L1, checkTeamcity},
136 {"tap"_L1, checkTap},
137 };
138};
139
141
143{
144 int sdkVersion = -1;
145 int pid = -1;
146
147 std::atomic<bool> isPackageInstalled { false };
148 std::atomic<bool> isTestRunnerInterrupted { false };
149};
150
152
153static bool execCommand(const QString &program, const QStringList &args,
154 QByteArray *output = nullptr, bool verbose = false)
155{
156 const auto command = program + " "_L1 + args.join(u' ');
157
158 if (verbose && g_options.verbose)
159 qDebug("Execute %s.", command.toUtf8().constData());
160
161 QProcess process;
162 process.start(program, args);
163 if (!process.waitForStarted()) {
164 qCritical("Cannot execute command %s.", qPrintable(command));
165 return false;
166 }
167
168 // If the command is not adb, for example, make or ninja, it can take more that
169 // QProcess::waitForFinished() 30 secs, so for that use a higher timeout.
170 const int FinishTimeout = program.endsWith("adb"_L1) ? 30000 : g_options.timeoutSecs * 1000;
171 if (!process.waitForFinished(FinishTimeout)) {
172 qCritical("Execution of command %s timed out.", qPrintable(command));
173 return false;
174 }
175
176 const auto stdOut = process.readAllStandardOutput();
177 if (output)
178 output->append(stdOut);
179
180 if (verbose && g_options.verbose)
181 qDebug() << stdOut.constData();
182
183 return process.exitCode() == 0;
184}
185
186static bool execAdbCommand(const QStringList &args, QByteArray *output = nullptr,
187 bool verbose = true)
188{
189 return execCommand(g_options.adbCommand, args, output, verbose);
190}
191
192static bool execCommand(const QString &command, QByteArray *output = nullptr, bool verbose = true)
193{
194 auto args = QProcess::splitCommand(command);
195 const auto program = args.first();
197 return execCommand(program, args, output, verbose);
198}
199
200static bool parseOptions()
201{
203 int i = 1;
204 for (; i < arguments.size(); ++i) {
205 const QString &argument = arguments.at(i);
206 if (argument.compare("--adb"_L1, Qt::CaseInsensitive) == 0) {
207 if (i + 1 == arguments.size())
208 g_options.helpRequested = true;
209 else
210 g_options.adbCommand = arguments.at(++i);
211 } else if (argument.compare("--path"_L1, Qt::CaseInsensitive) == 0) {
212 if (i + 1 == arguments.size())
213 g_options.helpRequested = true;
214 else
215 g_options.buildPath = arguments.at(++i);
216 } else if (argument.compare("--make"_L1, Qt::CaseInsensitive) == 0) {
217 if (i + 1 == arguments.size())
218 g_options.helpRequested = true;
219 else
220 g_options.makeCommand = arguments.at(++i);
221 } else if (argument.compare("--apk"_L1, Qt::CaseInsensitive) == 0) {
222 if (i + 1 == arguments.size())
223 g_options.helpRequested = true;
224 else
225 g_options.apkPath = arguments.at(++i);
226 } else if (argument.compare("--activity"_L1, Qt::CaseInsensitive) == 0) {
227 if (i + 1 == arguments.size())
228 g_options.helpRequested = true;
229 else
230 g_options.activity = arguments.at(++i);
231 } else if (argument.compare("--skip-install-root"_L1, Qt::CaseInsensitive) == 0) {
232 g_options.skipAddInstallRoot = true;
233 } else if (argument.compare("--show-logcat"_L1, Qt::CaseInsensitive) == 0) {
234 g_options.showLogcatOutput = true;
235 } else if (argument.compare("--ndk-stack"_L1, Qt::CaseInsensitive) == 0) {
236 if (i + 1 == arguments.size())
237 g_options.helpRequested = true;
238 else
239 g_options.ndkStackPath = arguments.at(++i);
240 } else if (argument.compare("--timeout"_L1, Qt::CaseInsensitive) == 0) {
241 if (i + 1 == arguments.size())
242 g_options.helpRequested = true;
243 else
244 g_options.timeoutSecs = arguments.at(++i).toInt();
245 } else if (argument.compare("--help"_L1, Qt::CaseInsensitive) == 0) {
246 g_options.helpRequested = true;
247 } else if (argument.compare("--verbose"_L1, Qt::CaseInsensitive) == 0) {
248 g_options.verbose = true;
249 } else if (argument.compare("--"_L1, Qt::CaseInsensitive) == 0) {
250 ++i;
251 break;
252 } else {
253 g_options.testArgsList << arguments.at(i);
254 }
255 }
256 for (;i < arguments.size(); ++i)
257 g_options.testArgsList << arguments.at(i);
258
259 if (g_options.helpRequested || g_options.buildPath.isEmpty() || g_options.apkPath.isEmpty())
260 return false;
261
262 QString serial = qEnvironmentVariable("ANDROID_DEVICE_SERIAL");
263 if (!serial.isEmpty())
264 g_options.adbCommand += " -s %1"_L1.arg(serial);
265
266 if (g_options.ndkStackPath.isEmpty()) {
267 const QString ndkPath = qEnvironmentVariable("ANDROID_NDK_ROOT");
268 const QString ndkStackPath = ndkPath + QDir::separator() + "ndk-stack"_L1;
269 if (QFile::exists(ndkStackPath))
270 g_options.ndkStackPath = ndkStackPath;
271 }
272
273 return true;
274}
275
276static void printHelp()
277{
278 qWarning( "Syntax: %s <options> -- [TESTARGS] \n"
279 "\n"
280 " Runs an Android test on the default emulator/device or on the one\n"
281 " specified by \"ANDROID_DEVICE_SERIAL\" environment variable.\n"
282 "\n"
283 " Mandatory arguments:\n"
284 " --path <path>: The path where androiddeployqt builds the android package.\n"
285 "\n"
286 " --apk <apk path>: The test apk path. The apk has to exist already, if it\n"
287 " does not exist the make command must be provided for building the apk.\n"
288 "\n"
289 " Optional arguments:\n"
290 " --make <make cmd>: make command, needed to install the qt library.\n"
291 " For Qt 6, this can be \"cmake --build . --target <target>_make_apk\".\n"
292 "\n"
293 " --adb <adb cmd>: The Android ADB command. If missing the one from\n"
294 " $PATH will be used.\n"
295 "\n"
296 " --activity <acitvity>: The Activity to run. If missing the first\n"
297 " activity from AndroidManifest.qml file will be used.\n"
298 "\n"
299 " --timeout <seconds>: Timeout to run the test. Default is 10 minutes.\n"
300 "\n"
301 " --skip-install-root: Do not append INSTALL_ROOT=... to the make command.\n"
302 "\n"
303 " --show-logcat: Print Logcat output to stdout.\n"
304 "\n"
305 " --ndk-stack: Path to ndk-stack tool that symbolizes crash stacktraces.\n"
306 " By default, ANDROID_NDK_ROOT env var is used to deduce the tool path.\n"
307 "\n"
308 " -- Arguments that will be passed to the test application.\n"
309 "\n"
310 " --verbose: Prints out information during processing.\n"
311 "\n"
312 " --help: Displays this information.\n",
314 );
315}
316
317static QString packageNameFromAndroidManifest(const QString &androidManifestPath)
318{
319 QFile androidManifestXml(androidManifestPath);
320 if (androidManifestXml.open(QIODevice::ReadOnly)) {
321 QXmlStreamReader reader(&androidManifestXml);
322 while (!reader.atEnd()) {
323 reader.readNext();
324 if (reader.isStartElement() && reader.name() == "manifest"_L1)
325 return reader.attributes().value("package"_L1).toString();
326 }
327 }
328 return {};
329}
330
331static QString activityFromAndroidManifest(const QString &androidManifestPath)
332{
333 QFile androidManifestXml(androidManifestPath);
334 if (androidManifestXml.open(QIODevice::ReadOnly)) {
335 QXmlStreamReader reader(&androidManifestXml);
336 while (!reader.atEnd()) {
337 reader.readNext();
338 if (reader.isStartElement() && reader.name() == "activity"_L1)
339 return reader.attributes().value("android:name"_L1).toString();
340 }
341 }
342 return {};
343}
344
346{
347 if (file.isEmpty())
348 file = "-"_L1;
349 if (format.isEmpty())
350 format = "txt"_L1;
351
352 g_options.outFiles[format] = file;
353}
354
355static bool parseTestArgs()
356{
357 QRegularExpression oldFormats{"^-(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
358 QRegularExpression newLoggingFormat{"^(.*),(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
359
361 QString logType;
362 QStringList unhandledArgs;
363 for (int i = 0; i < g_options.testArgsList.size(); ++i) {
364 const QString &arg = g_options.testArgsList[i].trimmed();
365 if (arg == "--"_L1)
366 continue;
367 if (arg == "-o"_L1) {
368 if (i >= g_options.testArgsList.size() - 1)
369 return false; // missing file argument
370
371 const auto &filePath = g_options.testArgsList[++i];
372 const auto match = newLoggingFormat.match(filePath);
373 if (!match.hasMatch()) {
374 file = filePath;
375 } else {
376 const auto capturedTexts = match.capturedTexts();
377 setOutputFile(capturedTexts.at(1), capturedTexts.at(2));
378 }
379 } else {
380 auto match = oldFormats.match(arg);
381 if (match.hasMatch()) {
382 logType = match.capturedTexts().at(1);
383 } else {
384 // Use triple literal quotes so that QProcess::splitCommand() in androidjnimain.cpp
385 // keeps quotes characters inside the string.
386 QString quotedArg = QString(arg).replace("\""_L1, "\\\"\\\"\\\""_L1);
387 // Escape single quotes so they don't interfere with the shell command,
388 // and so they get passed to the app as single quote inside the string.
389 quotedArg.replace("'"_L1, "\'"_L1);
390 // Add escaped double quote character so that args with spaces are treated as one.
391 unhandledArgs << " \\\"%1\\\""_L1.arg(quotedArg);
392 }
393 }
394 }
395 if (g_options.outFiles.isEmpty() || !file.isEmpty() || !logType.isEmpty())
396 setOutputFile(file, logType);
397
398 QString testAppArgs;
399 for (const auto &format : g_options.outFiles.keys())
400 testAppArgs += "-o output.%1,%1 "_L1.arg(format);
401
402 testAppArgs += unhandledArgs.join(u' ').trimmed();
403 testAppArgs = "\"%1\""_L1.arg(testAppArgs.trimmed());
404 const QString activityName = "%1/%2"_L1.arg(g_options.package).arg(g_options.activity);
405
406 // Pass over any testlib env vars if set
407 QString testEnvVars;
408 const QStringList envVarsList = QProcessEnvironment::systemEnvironment().toStringList();
409 for (const QString &var : envVarsList) {
410 if (var.startsWith("QTEST_"_L1))
411 testEnvVars += "%1 "_L1.arg(var);
412 }
413
414 if (!testEnvVars.isEmpty()) {
415 testEnvVars = QString::fromUtf8(testEnvVars.trimmed().toUtf8().toBase64());
416 testEnvVars = "-e extraenvvars \"%4\""_L1.arg(testEnvVars);
417 }
418
419 g_options.amStarttestArgs = { "shell"_L1, "am"_L1, "start"_L1,
420 "-n"_L1, activityName,
421 "-e"_L1, "applicationArguments"_L1, testAppArgs,
422 testEnvVars
423 };
424
425 return true;
426}
427
428static bool obtainPid() {
430 const QStringList psArgs = { "shell"_L1, "ps | grep ' %1'"_L1.arg(g_options.package) };
431 if (!execAdbCommand(psArgs, &output, false))
432 return false;
433
434 const QList<QByteArray> lines = output.split(u'\n');
435 if (lines.size() < 1)
436 return false;
437
438 QList<QByteArray> columns = lines.first().simplified().replace(u'\t', u' ').split(u' ');
439 if (columns.size() < 3)
440 return false;
441
442 if (g_testInfo.pid == -1) {
443 bool ok = false;
444 int pid = columns.at(1).toInt(&ok);
445 if (ok)
446 g_testInfo.pid = pid;
447 }
448
449 return true;
450}
451
452static bool isRunning() {
454 const QStringList psArgs = { "shell"_L1, "ps | grep ' %1'"_L1.arg(g_options.package) };
455 if (!execAdbCommand(psArgs, &output, false))
456 return false;
457
458 return output.indexOf(QLatin1StringView(" " + g_options.package.toUtf8())) > -1;
459}
460
462{
463 // wait to start and set PID
464 QDeadlineTimer startDeadline(10000);
465 do {
466 if (obtainPid())
467 break;
468 QThread::msleep(100);
469 } while (!startDeadline.hasExpired() && !g_testInfo.isTestRunnerInterrupted.load());
470
471 // Wait to finish
472 QDeadlineTimer finishedDeadline(g_options.timeoutSecs * 1000);
473 do {
474 if (!isRunning())
475 break;
476 QThread::msleep(250);
477 } while (!finishedDeadline.hasExpired() && !g_testInfo.isTestRunnerInterrupted.load());
478
479 if (finishedDeadline.hasExpired())
480 qWarning() << "Timed out while waiting for the test to finish";
481}
482
483static void obtainSdkVersion()
484{
485 // SDK version is necessary, as in SDK 23 pidof is broken, so we cannot obtain the pid.
486 // Also, Logcat cannot filter by pid in SDK 23, so we don't offer the --show-logcat option.
488 const QStringList versionArgs = { "shell"_L1, "getprop"_L1, "ro.build.version.sdk"_L1 };
489 execAdbCommand(versionArgs, &output, false);
490 bool ok = false;
491 int sdkVersion = output.toInt(&ok);
492 if (ok)
493 g_testInfo.sdkVersion = sdkVersion;
494 else
495 qCritical() << "Unable to obtain the SDK version of the target.";
496}
497
498static bool pullFiles()
499{
500 bool ret = true;
501 QByteArray userId;
502 // adb get-current-user command is available starting from API level 26.
503 if (g_testInfo.sdkVersion >= 26) {
504 const QStringList userIdArgs = {"shell"_L1, "cmd"_L1, "activity"_L1, "get-current-user"_L1};
505 if (!execAdbCommand(userIdArgs, &userId, false)) {
506 qCritical() << "Error: failed to retrieve the user ID";
507 return false;
508 }
509 } else {
510 userId = "0";
511 }
512
513 for (auto it = g_options.outFiles.constBegin(); it != g_options.outFiles.end(); ++it) {
514 // Get only stdout from cat and get rid of stderr and fail later if the output is empty
515 const QString outSuffix = it.key();
516 const QString catCmd = "cat files/output.%1 2> /dev/null"_L1.arg(outSuffix);
517 const QStringList fullCatArgs = { "shell"_L1, "run-as %1 --user %2 %3"_L1.arg(
518 g_options.package, QString::fromUtf8(userId.simplified()), catCmd) };
519
521 if (!execAdbCommand(fullCatArgs, &output, false)) {
522 qCritical() << "Error: failed to retrieve the test's output.%1 file."_L1.arg(outSuffix);
523 return false;
524 }
525
526 if (output.isEmpty()) {
527 qCritical() << "Error: the test's output.%1 is empty."_L1.arg(outSuffix);
528 return false;
529 }
530
531 auto checkerIt = g_options.checkFiles.find(outSuffix);
532 ret &= (checkerIt != g_options.checkFiles.end() && checkerIt.value()(output));
533 if (it.value() == "-"_L1) {
534 qDebug() << output.constData();
535 } else {
536 QFile out{it.value()};
537 if (!out.open(QIODevice::WriteOnly))
538 return false;
539 out.write(output);
540 }
541 }
542 return ret;
543}
544
545void printLogcat(const QString &formattedTime)
546{
547 QStringList logcatArgs = { "logcat"_L1 };
548 if (g_testInfo.sdkVersion <= 23 || g_testInfo.pid == -1)
549 logcatArgs << "-t"_L1 << formattedTime;
550 else
551 logcatArgs << "-d"_L1 << "--pid=%1"_L1.arg(QString::number(g_testInfo.pid));
552
553 QByteArray logcat;
554 if (!execAdbCommand(logcatArgs, &logcat, false)) {
555 qCritical() << "Error: failed to fetch logcat of the test";
556 return;
557 }
558
559 if (logcat.isEmpty()) {
560 qWarning() << "The retrieved logcat is empty";
561 return;
562 }
563
564 qDebug() << "****** Begin logcat output ******";
565 qDebug().noquote() << logcat;
566 qDebug() << "****** End logcat output ******";
567}
568
570{
571 const QStringList abiArgs = { "shell"_L1, "getprop"_L1, "ro.product.cpu.abi"_L1 };
572 QByteArray abi;
573 if (!execAdbCommand(abiArgs, &abi, false)) {
574 qWarning() << "Warning: failed to get the device abi, fallback to first libs dir";
575 return {};
576 }
577
578 return QString::fromUtf8(abi.simplified());
579}
580
581void printLogcatCrashBuffer(const QString &formattedTime)
582{
583 bool useNdkStack = false;
584 auto libsPath = "%1/libs/"_L1.arg(g_options.buildPath);
585
586 if (!g_options.ndkStackPath.isEmpty()) {
587 QString abi = getDeviceABI();
588 if (abi.isEmpty()) {
590 if (!subDirs.isEmpty())
591 abi = subDirs.first();
592 }
593
594 if (!abi.isEmpty()) {
595 libsPath += abi;
596 useNdkStack = true;
597 } else {
598 qWarning() << "Warning: failed to get the libs abi, ndk-stack cannot be used.";
599 }
600 } else {
601 qWarning() << "Warning: ndk-stack path not provided and couldn't be deduced "
602 "using the ANDROID_NDK_ROOT environment variable.";
603 }
604
605 QProcess adbCrashProcess;
606 QProcess ndkStackProcess;
607
608 if (useNdkStack) {
609 adbCrashProcess.setStandardOutputProcess(&ndkStackProcess);
610 ndkStackProcess.start(g_options.ndkStackPath, { "-sym"_L1, libsPath });
611 }
612
613 const QStringList adbCrashArgs = { "logcat"_L1, "-b"_L1, "crash"_L1, "-t"_L1, formattedTime };
614 adbCrashProcess.start(g_options.adbCommand, adbCrashArgs);
615
616 if (!adbCrashProcess.waitForStarted()) {
617 qCritical() << "Error: failed to run adb logcat crash command.";
618 return;
619 }
620
621 if (useNdkStack && !ndkStackProcess.waitForStarted()) {
622 qCritical() << "Error: failed to run ndk-stack command.";
623 return;
624 }
625
626 if (!adbCrashProcess.waitForFinished()) {
627 qCritical() << "Error: adb command timed out.";
628 return;
629 }
630
631 if (useNdkStack && !ndkStackProcess.waitForFinished()) {
632 qCritical() << "Error: ndk-stack command timed out.";
633 return;
634 }
635
636 const QByteArray crash = useNdkStack ? ndkStackProcess.readAllStandardOutput()
637 : adbCrashProcess.readAllStandardOutput();
638 if (crash.isEmpty()) {
639 qWarning() << "The retrieved crash logcat is empty";
640 return;
641 }
642
643 qDebug() << "****** Begin logcat crash buffer output ******";
644 qDebug().noquote() << crash;
645 qDebug() << "****** End logcat crash buffer output ******";
646}
647
649{
650 const QString timeFormat = (g_testInfo.sdkVersion <= 23) ?
651 "%m-%d %H:%M:%S.000"_L1 : "%Y-%m-%d %H:%M:%S.%3N"_L1;
652
653 QStringList dateArgs = { "shell"_L1, "date"_L1, "+'%1'"_L1.arg(timeFormat) };
655 if (!execAdbCommand(dateArgs, &output, false)) {
656 qWarning() << "Date/time adb command failed";
657 return {};
658 }
659
660 return QString::fromUtf8(output.simplified());
661}
662
664{
665 return execAdbCommand({ "uninstall"_L1, g_options.package }, nullptr);
666}
667
669{
672
673 void acquire() { isAcquired.store(semaphore.acquire()); }
674
675 void release()
676 {
677 bool expected = true;
678 // NOTE: There's still could be tiny time gap between the compare_exchange_strong() call
679 // and release() call where the thread could be interrupted, if that's ever an issue,
680 // this code could be checked and improved further.
681 if (isAcquired.compare_exchange_strong(expected, false))
682 isAcquired.store(!semaphore.release());
683 }
684
685 std::atomic<bool> isAcquired { false };
686 QSystemSemaphore semaphore { QSystemSemaphore::platformSafeKey(u"androidtestrunner"_s),
687 1, QSystemSemaphore::Open };
688};
689
691
693{
694 std::signal(signal, SIG_DFL);
696 // Ideally we shouldn't be doing such calls from a signal handler,
697 // and we can't use QSocketNotifier because this tool doesn't spin
698 // a main event loop. Since, there's no other alternative to do this,
699 // let's do the cleanup anyway.
700 if (!g_testInfo.isPackageInstalled.load())
701 _exit(-1);
702 g_testInfo.isTestRunnerInterrupted.store(true);
703}
704
705int main(int argc, char *argv[])
706{
707 std::signal(SIGINT, sigHandler);
708 std::signal(SIGTERM, sigHandler);
709
710 QCoreApplication a(argc, argv);
711 if (!parseOptions()) {
712 printHelp();
713 return 1;
714 }
715
716 if (g_options.makeCommand.isEmpty()) {
717 qCritical() << "It is required to provide a make command with the \"--make\" parameter "
718 "to generate the apk.";
719 return 1;
720 }
721 if (!execCommand(g_options.makeCommand, nullptr, true)) {
722 if (!g_options.skipAddInstallRoot) {
723 // we need to run make INSTALL_ROOT=path install to install the application file(s) first
724 if (!execCommand("%1 INSTALL_ROOT=%2 install"_L1.arg(g_options.makeCommand,
725 QDir::toNativeSeparators(g_options.buildPath)), nullptr)) {
726 return 1;
727 }
728 } else {
729 if (!execCommand(g_options.makeCommand, nullptr))
730 return 1;
731 }
732 }
733
734 if (!QFile::exists(g_options.apkPath)) {
735 qCritical("No apk \"%s\" found after running the make command. "
736 "Check the provided path and the make command.",
737 qPrintable(g_options.apkPath));
738 return 1;
739 }
740
742
743 QString manifest = g_options.buildPath + "/AndroidManifest.xml"_L1;
744 g_options.package = packageNameFromAndroidManifest(manifest);
745 if (g_options.activity.isEmpty())
746 g_options.activity = activityFromAndroidManifest(manifest);
747
748 // parseTestArgs depends on g_options.package
749 if (!parseTestArgs())
750 return 1;
751
752 // do not install or run packages while another test is running
754
755 const QStringList installArgs = { "install"_L1, "-r"_L1, "-g"_L1, g_options.apkPath };
756 g_testInfo.isPackageInstalled.store(execAdbCommand(installArgs, nullptr));
757 if (!g_testInfo.isPackageInstalled)
758 return 1;
759
760 const QString formattedTime = getCurrentTimeString();
761
762 // start the tests
763 bool success = execAdbCommand(g_options.amStarttestArgs, nullptr);
764
766
767 if (success) {
768 success &= pullFiles();
769 if (g_options.showLogcatOutput)
770 printLogcat(formattedTime);
771 }
772
773 // If we have a failure, attempt to print both logcat and the crash buffer which
774 // includes the crash stacktrace that is not included in the default logcat.
775 if (!success) {
776 printLogcat(formattedTime);
777 printLogcatCrashBuffer(formattedTime);
778 }
779
780 success &= uninstallTestPackage();
781
783
784 if (g_testInfo.isTestRunnerInterrupted.load()) {
785 qCritical() << "The androidtestrunner was interrupted and the was test cleaned up.";
786 return 1;
787 }
788
789 return success ? 0 : 1;
790}
\inmodule QtCore
Definition qbytearray.h:57
QByteArray & prepend(char c)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qbytearray.h:280
QByteArray simplified() const &
Definition qbytearray.h:266
QByteArray & append(char c)
This is an overloaded member function, provided for convenience. It differs from the above function o...
\inmodule QtCore
static QStringList arguments()
\inmodule QtCore
\inmodule QtCore
Definition qdir.h:20
QStringList entryList(Filters filters=NoFilter, SortFlags sort=NoSort) const
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qdir.cpp:1365
static QChar separator()
Returns the native directory separator: "/" under Unix and "\\" under Windows.
Definition qdir.h:209
static QString toNativeSeparators(const QString &pathName)
Definition qdir.cpp:929
@ NoDotAndDotDot
Definition qdir.h:44
@ Dirs
Definition qdir.h:22
\inmodule QtCore
Definition qfile.h:93
bool exists() const
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qfile.cpp:351
\inmodule QtCore
Definition qhash.h:820
qsizetype size() const noexcept
Definition qlist.h:397
T & first()
Definition qlist.h:645
bool removeOne(const AT &t)
Definition qlist.h:598
const_reference at(qsizetype i) const noexcept
Definition qlist.h:446
static QProcessEnvironment systemEnvironment()
\inmodule QtCore \reentrant
\inmodule QtCore
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:129
QString & replace(qsizetype i, qsizetype len, QChar after)
Definition qstring.cpp:3824
bool isEmpty() const noexcept
Returns true if the string has no characters; otherwise returns false.
Definition qstring.h:192
static QString fromUtf8(QByteArrayView utf8)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qstring.cpp:6018
QString arg(qlonglong a, int fieldwidth=0, int base=10, QChar fillChar=u' ') const
Definition qstring.cpp:8870
static QString number(int, int base=10)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qstring.cpp:8084
QString trimmed() const &
Definition qstring.h:447
static void msleep(unsigned long)
int toInt(bool *ok=nullptr) const
Returns the variant as an int if the variant has userType() \l QMetaType::Int, \l QMetaType::Bool,...
QJSValue expected
Definition qjsengine.cpp:12
int main()
[0]
QSet< QString >::iterator it
auto signal
QList< QVariant > arguments
@ CaseInsensitive
#define qCritical
Definition qlogging.h:167
#define qDebug
[1]
Definition qlogging.h:164
#define qWarning
Definition qlogging.h:166
return ret
GLboolean GLboolean GLboolean GLboolean a
[7]
GLint GLsizei GLsizei GLenum GLenum GLsizei void * data
GLenum type
GLuint program
GLint GLsizei GLsizei GLenum format
SSL_CTX int void * arg
#define qPrintable(string)
Definition qstring.h:1531
static QString getCurrentTimeString()
Definition main.cpp:648
void printLogcatCrashBuffer(const QString &formattedTime)
Definition main.cpp:581
static void setOutputFile(QString file, QString format)
Definition main.cpp:345
static bool parseTestArgs()
Definition main.cpp:355
static QString packageNameFromAndroidManifest(const QString &androidManifestPath)
Definition main.cpp:317
static bool checkJunit(const QByteArray &data)
Definition main.cpp:27
static bool checkLightxml(const QByteArray &data)
Definition main.cpp:83
static bool checkXml(const QByteArray &data)
Definition main.cpp:67
static Options g_options
Definition main.cpp:140
static QString activityFromAndroidManifest(const QString &androidManifestPath)
Definition main.cpp:331
static TestInfo g_testInfo
Definition main.cpp:151
static bool isRunning()
Definition main.cpp:452
static bool checkTap(const QByteArray &data)
Definition main.cpp:101
static bool checkCsv(const QByteArray &data)
Definition main.cpp:59
static void obtainSdkVersion()
Definition main.cpp:483
static bool execAdbCommand(const QStringList &args, QByteArray *output=nullptr, bool verbose=true)
Definition main.cpp:186
void sigHandler(int signal)
Definition main.cpp:692
static bool checkTeamcity(const QByteArray &data)
Definition main.cpp:92
static bool parseOptions()
Definition main.cpp:200
TestRunnerSystemSemaphore testRunnerLock
Definition main.cpp:690
static void waitForStartedAndFinished()
Definition main.cpp:461
static void printHelp()
Definition main.cpp:276
static QString getDeviceABI()
Definition main.cpp:569
static bool execCommand(const QString &program, const QStringList &args, QByteArray *output=nullptr, bool verbose=false)
Definition main.cpp:153
static bool uninstallTestPackage()
Definition main.cpp:663
static bool obtainPid()
Definition main.cpp:428
static bool pullFiles()
Definition main.cpp:498
static bool checkTxt(const QByteArray &data)
Definition main.cpp:49
void printLogcat(const QString &formattedTime)
Definition main.cpp:545
QString qEnvironmentVariable(const char *varName, const QString &defaultValue)
#define Q_UNUSED(x)
static bool match(const uchar *found, uint foundLen, const char *target, uint targetLen)
QT_BEGIN_NAMESPACE typedef uchar * output
QFile file
[0]
QTextStream out(stdout)
[7]
QAction * at
QDBusArgument argument
QJSValueList args
QString makeCommand
Definition main.cpp:119
QStringList amStarttestArgs
Definition main.cpp:124
QString adbCommand
Definition main.cpp:118
bool helpRequested
Definition main.cpp:123
int timeoutSecs
Definition main.cpp:116
bool showLogcatOutput
Definition main.cpp:127
bool skipAddInstallRoot
Definition main.cpp:115
QString package
Definition main.cpp:120
bool verbose
Definition main.cpp:124
QString ndkStackPath
Definition main.cpp:126
QString apkPath
Definition main.cpp:210
QStringList testArgsList
Definition main.cpp:122
QHash< QString, QString > outFiles
Definition main.cpp:123
const QHash< QString, std::function< bool(const QByteArray &)> checkFiles)
Definition main.cpp:128
QString activity
Definition main.cpp:121
QString buildPath
Definition main.cpp:117
std::atomic< bool > isTestRunnerInterrupted
Definition main.cpp:148
int sdkVersion
Definition main.cpp:144
int pid
Definition main.cpp:145
std::atomic< bool > isPackageInstalled
Definition main.cpp:147
std::atomic< bool > isAcquired
Definition main.cpp:685
QSystemSemaphore semaphore
Definition main.cpp:686