Qt 6.x
The Qt SDK
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) 2022 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 <QCoreApplication>
6#include <QDir>
7#include <QHash>
8#include <QRegularExpression>
9#include <QSystemSemaphore>
10#include <QXmlStreamReader>
11
12#include <algorithm>
13#include <chrono>
14#include <functional>
15#include <thread>
16
17#include <shellquote_shared.h>
18
19#ifdef Q_CC_MSVC
20#define popen _popen
21#define QT_POPEN_READ "rb"
22#define pclose _pclose
23#else
24#define QT_POPEN_READ "r"
25#endif
26
27using namespace Qt::StringLiterals;
28
29static bool checkJunit(const QByteArray &data) {
30 QXmlStreamReader reader{data};
31 while (!reader.atEnd()) {
32 reader.readNext();
33
34 if (!reader.isStartElement())
35 continue;
36
37 if (reader.name() == QStringLiteral("error"))
38 return false;
39
40 const QString type = reader.attributes().value(QStringLiteral("type")).toString();
41 if (reader.name() == QStringLiteral("failure")) {
42 if (type == QStringLiteral("fail") || type == QStringLiteral("xpass"))
43 return false;
44 }
45 }
46
47 // Fail if there's an error after reading through all the xml output
48 return !reader.hasError();
49}
50
51static bool checkTxt(const QByteArray &data) {
52 if (data.indexOf("\nFAIL! : "_L1) >= 0)
53 return false;
54 if (data.indexOf("\nXPASS : "_L1) >= 0)
55 return false;
56 // Look for "********* Finished testing of tst_QTestName *********"
57 static const QRegularExpression testTail("\\*+ +Finished testing of .+ +\\*+"_L1);
58 return testTail.match(QLatin1StringView(data)).hasMatch();
59}
60
61static bool checkCsv(const QByteArray &data) {
62 // The csv format is only suitable for benchmarks,
63 // so this is not much useful to determine test failure/success.
64 // FIXME: warn the user early on about this.
66 return true;
67}
68
69static bool checkXml(const QByteArray &data) {
70 QXmlStreamReader reader{data};
71 while (!reader.atEnd()) {
72 reader.readNext();
73 const QString type = reader.attributes().value(QStringLiteral("type")).toString();
74 const bool isIncident = (reader.name() == QStringLiteral("Incident"));
75 if (reader.isStartElement() && isIncident) {
76 if (type == QStringLiteral("fail") || type == QStringLiteral("xpass"))
77 return false;
78 }
79 }
80
81 // Fail if there's an error after reading through all the xml output
82 return !reader.hasError();
83}
84
85static bool checkLightxml(const QByteArray &data) {
86 // lightxml intentionally skips the root element, which technically makes it
87 // not valid XML. We'll add that ourselves for the purpose of validation.
88 QByteArray newData = data;
89 newData.prepend("<root>");
90 newData.append("</root>");
91 return checkXml(newData);
92}
93
94static bool checkTeamcity(const QByteArray &data) {
95 if (data.indexOf("' message='Failure! |[Loc: ") >= 0)
96 return false;
97 const QList<QByteArray> lines = data.trimmed().split('\n');
98 if (lines.isEmpty())
99 return false;
100 return lines.last().startsWith("##teamcity[testSuiteFinished "_L1);
101}
102
103static bool checkTap(const QByteArray &data) {
104 // This will still report blacklisted fails because QTest with TAP
105 // is not putting any data about that.
106 if (data.indexOf("\nnot ok ") >= 0)
107 return false;
108
109 static const QRegularExpression testTail("ok [0-9]* - cleanupTestCase\\(\\)"_L1);
110 return testTail.match(QLatin1StringView(data)).hasMatch();
111}
112
113struct Options
114{
115 bool helpRequested = false;
116 bool verbose = false;
117 bool skipAddInstallRoot = false;
118 std::chrono::seconds timeout{480}; // 8 minutes
128 int sdkVersion = -1;
129 int pid = -1;
130 bool showLogcatOutput = false;
131 const QHash<QString, std::function<bool(const QByteArray &)>> checkFiles = {
132 {QStringLiteral("txt"), checkTxt},
133 {QStringLiteral("csv"), checkCsv},
134 {QStringLiteral("xml"), checkXml},
135 {QStringLiteral("lightxml"), checkLightxml},
136 {QStringLiteral("xunitxml"), checkJunit},
137 {QStringLiteral("junitxml"), checkJunit},
138 {QStringLiteral("teamcity"), checkTeamcity},
139 {QStringLiteral("tap"), checkTap},
140 };
141};
142
144
145static bool execCommand(const QString &command, QByteArray *output = nullptr, bool verbose = false)
146{
147 if (verbose)
148 fprintf(stdout, "Execute %s.\n", command.toUtf8().constData());
149 FILE *process = popen(command.toUtf8().constData(), QT_POPEN_READ);
150
151 if (!process) {
152 fprintf(stderr, "Cannot execute command %s.\n", qPrintable(command));
153 return false;
154 }
155 char buffer[512];
156 while (fgets(buffer, sizeof(buffer), process)) {
157 if (output)
158 output->append(buffer);
159 if (verbose)
160 fprintf(stdout, "%s", buffer);
161 }
162
163 fflush(stdout);
164 fflush(stderr);
165
166 return pclose(process) == 0;
167}
168
169static bool parseOptions()
170{
172 int i = 1;
173 for (; i < arguments.size(); ++i) {
174 const QString &argument = arguments.at(i);
175 if (argument.compare(QStringLiteral("--adb"), Qt::CaseInsensitive) == 0) {
176 if (i + 1 == arguments.size())
178 else
180 } else if (argument.compare(QStringLiteral("--path"), Qt::CaseInsensitive) == 0) {
181 if (i + 1 == arguments.size())
183 else
185 } else if (argument.compare(QStringLiteral("--make"), Qt::CaseInsensitive) == 0) {
186 if (i + 1 == arguments.size())
188 else
190 } else if (argument.compare(QStringLiteral("--apk"), Qt::CaseInsensitive) == 0) {
191 if (i + 1 == arguments.size())
193 else
195 } else if (argument.compare(QStringLiteral("--activity"), Qt::CaseInsensitive) == 0) {
196 if (i + 1 == arguments.size())
198 else
200 } else if (argument.compare(QStringLiteral("--skip-install-root"), Qt::CaseInsensitive) == 0) {
202 } else if (argument.compare(QStringLiteral("--show-logcat"), Qt::CaseInsensitive) == 0) {
204 } else if (argument.compare(QStringLiteral("--timeout"), Qt::CaseInsensitive) == 0) {
205 if (i + 1 == arguments.size())
207 else
208 g_options.timeout = std::chrono::seconds{arguments.at(++i).toInt()};
209 } else if (argument.compare(QStringLiteral("--help"), Qt::CaseInsensitive) == 0) {
211 } else if (argument.compare(QStringLiteral("--verbose"), Qt::CaseInsensitive) == 0) {
212 g_options.verbose = true;
213 } else if (argument.compare(QStringLiteral("--"), Qt::CaseInsensitive) == 0) {
214 ++i;
215 break;
216 } else {
218 }
219 }
220 for (;i < arguments.size(); ++i)
222
224 return false;
225
226 QString serial = qEnvironmentVariable("ANDROID_DEVICE_SERIAL");
227 if (!serial.isEmpty())
228 g_options.adbCommand += QStringLiteral(" -s %1").arg(serial);
229 return true;
230}
231
232static void printHelp()
233{
234 fprintf(stderr, "Syntax: %s <options> -- [TESTARGS] \n"
235 "\n"
236 " Creates an Android package in a temp directory <destination> and\n"
237 " runs it on the default emulator/device or on the one specified by\n"
238 " \"ANDROID_DEVICE_SERIAL\" environment variable.\n"
239 "\n"
240 " Mandatory arguments:\n"
241 " --path <path>: The path where androiddeployqt builds the android package.\n"
242 "\n"
243 " --apk <apk path>: The test apk path. The apk has to exist already, if it\n"
244 " does not exist the make command must be provided for building the apk.\n"
245 "\n"
246 " Optional arguments:\n"
247 " --make <make cmd>: make command, needed to install the qt library.\n"
248 " For Qt 5.14+ this can be \"make apk\".\n"
249 "\n"
250 " --adb <adb cmd>: The Android ADB command. If missing the one from\n"
251 " $PATH will be used.\n"
252 "\n"
253 " --activity <acitvity>: The Activity to run. If missing the first\n"
254 " activity from AndroidManifest.qml file will be used.\n"
255 "\n"
256 " --timeout <seconds>: Timeout to run the test. Default is 5 minutes.\n"
257 "\n"
258 " --skip-install-root: Do not append INSTALL_ROOT=... to the make command.\n"
259 "\n"
260 " --show-logcat: Print Logcat output to stdout.\n"
261 "\n"
262 " -- Arguments that will be passed to the test application.\n"
263 "\n"
264 " --verbose: Prints out information during processing.\n"
265 "\n"
266 " --help: Displays this information.\n\n",
268 );
269}
270
271static QString packageNameFromAndroidManifest(const QString &androidManifestPath)
272{
273 QFile androidManifestXml(androidManifestPath);
274 if (androidManifestXml.open(QIODevice::ReadOnly)) {
275 QXmlStreamReader reader(&androidManifestXml);
276 while (!reader.atEnd()) {
277 reader.readNext();
278 if (reader.isStartElement() && reader.name() == QStringLiteral("manifest"))
279 return reader.attributes().value(QStringLiteral("package")).toString();
280 }
281 }
282 return {};
283}
284
285static QString activityFromAndroidManifest(const QString &androidManifestPath)
286{
287 QFile androidManifestXml(androidManifestPath);
288 if (androidManifestXml.open(QIODevice::ReadOnly)) {
289 QXmlStreamReader reader(&androidManifestXml);
290 while (!reader.atEnd()) {
291 reader.readNext();
292 if (reader.isStartElement() && reader.name() == QStringLiteral("activity"))
293 return reader.attributes().value(QStringLiteral("android:name")).toString();
294 }
295 }
296 return {};
297}
298
300{
301 if (file.isEmpty())
302 file = QStringLiteral("-");
303 if (format.isEmpty())
304 format = QStringLiteral("txt");
305
307}
308
309static bool parseTestArgs()
310{
311 QRegularExpression oldFormats{QStringLiteral("^-(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$")};
312 QRegularExpression newLoggingFormat{QStringLiteral("^(.*),(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$")};
313
315 QString logType;
316 QStringList unhandledArgs;
317 for (int i = 0; i < g_options.testArgsList.size(); ++i) {
318 const QString &arg = g_options.testArgsList[i].trimmed();
319 if (arg == QStringLiteral("--"))
320 continue;
321 if (arg == QStringLiteral("-o")) {
322 if (i >= g_options.testArgsList.size() - 1)
323 return false; // missing file argument
324
325 const auto &filePath = g_options.testArgsList[++i];
326 const auto match = newLoggingFormat.match(filePath);
327 if (!match.hasMatch()) {
328 file = filePath;
329 } else {
330 const auto capturedTexts = match.capturedTexts();
331 setOutputFile(capturedTexts.at(1), capturedTexts.at(2));
332 }
333 } else {
334 auto match = oldFormats.match(arg);
335 if (match.hasMatch()) {
336 logType = match.capturedTexts().at(1);
337 } else {
338 unhandledArgs << QStringLiteral(" \\\"%1\\\"").arg(arg);
339 }
340 }
341 }
342 if (g_options.outFiles.isEmpty() || !file.isEmpty() || !logType.isEmpty())
343 setOutputFile(file, logType);
344
345 for (const auto &format : g_options.outFiles.keys())
346 g_options.testArgs += QStringLiteral(" -o output.%1,%1").arg(format);
347
348 g_options.testArgs += unhandledArgs.join(u' ');
349
350 g_options.testArgs = QStringLiteral("shell am start -e applicationArguments \"%1\" -n %2/%3")
352 .arg(g_options.package)
353 .arg(g_options.activity);
354
355 return true;
356}
357
358static bool isRunning() {
360 if (!execCommand(QStringLiteral("%1 shell \"ps | grep ' %2'\"").arg(g_options.adbCommand,
362
363 return false;
364 }
365 return output.indexOf(QLatin1StringView(" " + g_options.package.toUtf8())) > -1;
366}
367
368static bool waitToFinish()
369{
370 using clock = std::chrono::system_clock;
371 auto start = clock::now();
372 // wait to start
373 while (!isRunning()) {
374 std::this_thread::sleep_for(std::chrono::milliseconds(100));
375 if ((clock::now() - start) > std::chrono::seconds{10})
376 return false;
377 }
378
379 if (g_options.sdkVersion > 23) { // pidof is broken in SDK 23, non-existent before
381 const QString command(QStringLiteral("%1 shell pidof -s %2")
384 bool ok = false;
385 int pid = output.toInt(&ok); // If we got more than one pid, fail.
386 if (ok) {
387 g_options.pid = pid;
388 } else {
389 fprintf(stderr,
390 "Unable to obtain the PID of the running unit test. Command \"%s\" "
391 "returned \"%s\"\n",
392 command.toUtf8().constData(), output.constData());
393 fflush(stderr);
394 }
395 }
396
397 // Wait to finish
398 while (isRunning()) {
399 std::this_thread::sleep_for(std::chrono::milliseconds(250));
400 if (g_options.timeout >= std::chrono::seconds::zero()
401 && (clock::now() - start) > g_options.timeout)
402 return false;
403 }
404 return true;
405}
406
407static void obtainSDKVersion()
408{
409 // SDK version is necessary, as in SDK 23 pidof is broken, so we cannot obtain the pid.
410 // Also, Logcat cannot filter by pid in SDK 23, so we don't offer the --show-logcat option.
412 const QString command(
413 QStringLiteral("%1 shell getprop ro.build.version.sdk").arg(g_options.adbCommand));
415 bool ok = false;
416 int sdkVersion = output.toInt(&ok);
417 if (ok) {
418 g_options.sdkVersion = sdkVersion;
419 } else {
420 fprintf(stderr,
421 "Unable to obtain the SDK version of the target. Command \"%s\" "
422 "returned \"%s\"\n",
423 command.toUtf8().constData(), output.constData());
424 fflush(stderr);
425 }
426}
427
428static bool pullFiles()
429{
430 bool ret = true;
431 for (auto it = g_options.outFiles.constBegin(); it != g_options.outFiles.end(); ++it) {
432 // Get only stdout from cat and get rid of stderr and fail later if the output is empty
433 const QString catCmd = QStringLiteral("cat files/output.%1 2> /dev/null").arg(it.key());
434
436 if (!execCommand(QStringLiteral("%1 shell 'run-as %2 %3'")
438 // Cannot find output file. Check in path related to current user
439 QByteArray userId;
440 execCommand(QStringLiteral("%1 shell cmd activity get-current-user")
441 .arg(g_options.adbCommand), &userId);
442 const QString userIdSimplified(QString::fromUtf8(userId).simplified());
443 if (!execCommand(QStringLiteral("%1 shell 'run-as %2 --user %3 %4'")
444 .arg(g_options.adbCommand, g_options.package, userIdSimplified, catCmd),
445 &output)) {
446 return false;
447 }
448 }
449
450 if (output.isEmpty()) {
451 fprintf(stderr, "Failed to get the test output from the target. Either the output "
452 "is empty or androidtestrunner failed to retrieve it.\n");
453 return false;
454 }
455
456 auto checkerIt = g_options.checkFiles.find(it.key());
457 ret = ret && checkerIt != g_options.checkFiles.end() && checkerIt.value()(output);
458 if (it.value() == QStringLiteral("-")){
459 fprintf(stdout, "%s", output.constData());
460 fflush(stdout);
461 } else {
462 QFile out{it.value()};
463 if (!out.open(QIODevice::WriteOnly))
464 return false;
465 out.write(output);
466 }
467 }
468 return ret;
469}
470
472{
474 {
475 runner.acquire();
476 }
478 {
479 runner.release();
480 }
481 QSystemSemaphore runner{ QSystemSemaphore::platformSafeKey(u"androidtestrunner"_s),
482 1, QSystemSemaphore::Open };
483};
484
485int main(int argc, char *argv[])
486{
487 QCoreApplication a(argc, argv);
488 if (!parseOptions()) {
489 printHelp();
490 return 1;
491 }
492
494 fprintf(stderr,
495 "It is required to provide a make command with the \"--make\" parameter "
496 "to generate the apk.\n");
497 return 1;
498 }
499 if (!execCommand(g_options.makeCommand, nullptr, true)) {
501 // we need to run make INSTALL_ROOT=path install to install the application file(s) first
502 if (!execCommand(QStringLiteral("%1 INSTALL_ROOT=%2 install")
504 return 1;
505 }
506 } else {
507 if (!execCommand(QStringLiteral("%1")
509 return 1;
510 }
511 }
512 }
513
515 fprintf(stderr,
516 "No apk \"%s\" found after running the make command. Check the provided path and "
517 "the make command.\n",
519 return 1;
520 }
521
523
524 RunnerLocker lock; // do not install or run packages while another test is running
525 if (!execCommand(QStringLiteral("%1 install -r -g %2")
527 return 1;
528 }
529
530 QString manifest = g_options.buildPath + QStringLiteral("/AndroidManifest.xml");
534
535 // parseTestArgs depends on g_options.package
536 if (!parseTestArgs())
537 return 1;
538
539 // start the tests
541 nullptr, g_options.verbose)
542 && waitToFinish();
543
544 // get logcat output
546 if (g_options.sdkVersion <= 23) {
547 fprintf(stderr, "Cannot show logcat output on Android 23 and below.\n");
548 fflush(stderr);
549 } else if (g_options.pid > 0) {
550 fprintf(stdout, "Logcat output:\n");
551 res &= execCommand(QStringLiteral("%1 logcat -d --pid=%2")
553 .arg(g_options.pid),
554 nullptr, true);
555 fprintf(stdout, "End Logcat output.\n");
556 }
557 }
558
559 if (res)
560 res &= pullFiles();
562 nullptr, g_options.verbose);
563 fflush(stdout);
564 return res ? 0 : 1;
565}
\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:216
const char * constData() const noexcept
Returns a pointer to the const data stored in the byte array.
Definition qbytearray.h:122
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()
static QString toNativeSeparators(const QString &pathName)
Definition qdir.cpp:929
\inmodule QtCore
Definition qfile.h:93
bool open(OpenMode flags) override
Opens the file using OpenMode mode, returning true if successful; otherwise false.
Definition qfile.cpp:881
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:818
iterator find(const Key &key)
Returns an iterator pointing to the item with the key in the hash.
Definition qhash.h:1258
const_iterator constBegin() const noexcept
Returns a const \l{STL-style iterators}{STL-style iterator} pointing to the first item in the hash.
Definition qhash.h:1205
QList< Key > keys() const
Returns a list containing all the keys in the hash, in an arbitrary order.
Definition qhash.h:1076
iterator end() noexcept
Returns an \l{STL-style iterators}{STL-style iterator} pointing to the imaginary item after the last ...
Definition qhash.h:1206
bool isEmpty() const noexcept
Returns true if the hash contains no items; otherwise returns false.
Definition qhash.h:926
Definition qlist.h:74
qsizetype size() const noexcept
Definition qlist.h:386
bool isEmpty() const noexcept
Definition qlist.h:390
T & last()
Definition qlist.h:631
const_reference at(qsizetype i) const noexcept
Definition qlist.h:429
bool hasMatch() const
Returns true if the regular expression matched against the subject string, or false otherwise.
\inmodule QtCore \reentrant
QRegularExpressionMatch match(const QString &subject, qsizetype offset=0, MatchType matchType=NormalMatch, MatchOptions matchOptions=NoMatchOption) const
Attempts to match the regular expression against the given subject string, starting at the position o...
\inmodule QtCore
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:127
static QString fromUtf8(QByteArrayView utf8)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qstring.cpp:5857
bool isEmpty() const
Returns true if the string has no characters; otherwise returns false.
Definition qstring.h:1083
QString trimmed() const &
Definition qstring.h:380
QByteArray toUtf8() const &
Definition qstring.h:563
int toInt(bool *ok=nullptr) const
Returns the variant as an int if the variant has userType() \l QMetaType::Int, \l QMetaType::Bool,...
int main()
[0]
QSet< QString >::iterator it
QList< QVariant > arguments
@ CaseInsensitive
return ret
GLboolean GLboolean GLboolean GLboolean a
[7]
GLbitfield GLuint64 timeout
[4]
GLenum GLuint buffer
GLenum type
GLint GLsizei GLsizei GLenum GLenum GLsizei void * data
GLuint start
GLint GLsizei GLsizei GLenum format
GLuint res
SSL_CTX int(*) void arg)
#define qPrintable(string)
Definition qstring.h:1391
#define QStringLiteral(str)
static bool waitToFinish()
Definition main.cpp:368
#define QT_POPEN_READ
Definition main.cpp:24
static void setOutputFile(QString file, QString format)
Definition main.cpp:299
static void obtainSDKVersion()
Definition main.cpp:407
static bool parseTestArgs()
Definition main.cpp:309
static QString packageNameFromAndroidManifest(const QString &androidManifestPath)
Definition main.cpp:271
static bool checkJunit(const QByteArray &data)
Definition main.cpp:29
static bool checkLightxml(const QByteArray &data)
Definition main.cpp:85
static bool checkXml(const QByteArray &data)
Definition main.cpp:69
static Options g_options
Definition main.cpp:143
static QString activityFromAndroidManifest(const QString &androidManifestPath)
Definition main.cpp:285
static bool isRunning()
Definition main.cpp:358
static bool checkTap(const QByteArray &data)
Definition main.cpp:103
static bool checkCsv(const QByteArray &data)
Definition main.cpp:61
static bool checkTeamcity(const QByteArray &data)
Definition main.cpp:94
static bool parseOptions()
Definition main.cpp:169
static bool execCommand(const QString &command, QByteArray *output=nullptr, bool verbose=false)
Definition main.cpp:145
static void printHelp()
Definition main.cpp:232
static bool pullFiles()
Definition main.cpp:428
static bool checkTxt(const QByteArray &data)
Definition main.cpp:51
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
static QString shellQuote(const QString &arg)
QFile file
[0]
QTextStream out(stdout)
[7]
QReadWriteLock lock
[0]
QAction * at
QDBusArgument argument
QString makeCommand
Definition main.cpp:121
QString testArgs
Definition main.cpp:126
const QHash< QString, std::function< bool(const QByteArray &)> > checkFiles
Definition main.cpp:131
QString adbCommand
Definition main.cpp:120
bool helpRequested
Definition main.cpp:121
bool showLogcatOutput
Definition main.cpp:130
std::chrono::seconds timeout
Definition main.cpp:118
bool skipAddInstallRoot
Definition main.cpp:117
int sdkVersion
Definition main.cpp:128
QString package
Definition main.cpp:122
bool verbose
Definition main.cpp:122
QString apkPath
Definition main.cpp:207
QStringList testArgsList
Definition main.cpp:124
QHash< QString, QString > outFiles
Definition main.cpp:125
int pid
Definition main.cpp:129
QString activity
Definition main.cpp:123
QString buildPath
Definition main.cpp:119
QSystemSemaphore runner
Definition main.cpp:481
~RunnerLocker()
Definition main.cpp:477