5#include <QtCore/QCoreApplication>
6#include <QtCore/QDeadlineTimer>
9#include <QtCore/QProcess>
10#include <QtCore/QProcessEnvironment>
11#include <QtCore/QRegularExpression>
12#include <QtCore/QSystemSemaphore>
13#include <QtCore/QThread>
14#include <QtCore/QXmlStreamReader>
19#if defined(Q_OS_WIN32)
28 QXmlStreamReader reader{
data};
29 while (!reader.atEnd()) {
32 if (!reader.isStartElement())
35 if (reader.name() ==
"error"_L1)
38 const QString type = reader.attributes().value(
"type"_L1).toString();
39 if (reader.name() ==
"failure"_L1) {
40 if (
type ==
"fail"_L1 ||
type ==
"xpass"_L1)
46 return !reader.hasError();
50 if (
data.indexOf(
"\nFAIL! : "_L1) >= 0)
52 if (
data.indexOf(
"\nXPASS : "_L1) >= 0)
68 QXmlStreamReader reader{
data};
69 while (!reader.atEnd()) {
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)
80 return !reader.hasError();
93 if (
data.indexOf(
"' message='Failure! |[Loc: ") >= 0)
95 const QList<QByteArray> lines =
data.trimmed().split(
'\n');
98 return lines.last().startsWith(
"##teamcity[testSuiteFinished "_L1);
104 if (
data.indexOf(
"\nnot ok ") >= 0)
156 const auto command =
program +
" "_L1 +
args.join(u
' ');
159 qDebug(
"Execute %s.", command.toUtf8().constData());
163 if (!process.waitForStarted()) {
170 const int FinishTimeout =
program.endsWith(
"adb"_L1) ? 30000 :
g_options.timeoutSecs * 1000;
171 if (!process.waitForFinished(FinishTimeout)) {
176 const auto stdOut = process.readAllStandardOutput();
181 qDebug() << stdOut.constData();
183 return process.exitCode() == 0;
194 auto args = QProcess::splitCommand(command);
264 g_options.adbCommand +=
" -s %1"_L1.arg(serial);
278 qWarning(
"Syntax: %s <options> -- [TESTARGS] \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"
283 " Mandatory arguments:\n"
284 " --path <path>: The path where androiddeployqt builds the android package.\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"
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"
293 " --adb <adb cmd>: The Android ADB command. If missing the one from\n"
294 " $PATH will be used.\n"
296 " --activity <acitvity>: The Activity to run. If missing the first\n"
297 " activity from AndroidManifest.qml file will be used.\n"
299 " --timeout <seconds>: Timeout to run the test. Default is 10 minutes.\n"
301 " --skip-install-root: Do not append INSTALL_ROOT=... to the make command.\n"
303 " --show-logcat: Print Logcat output to stdout.\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"
308 " -- Arguments that will be passed to the test application.\n"
310 " --verbose: Prints out information during processing.\n"
312 " --help: Displays this information.\n",
319 QFile androidManifestXml(androidManifestPath);
321 QXmlStreamReader reader(&androidManifestXml);
322 while (!reader.atEnd()) {
324 if (reader.isStartElement() && reader.name() ==
"manifest"_L1)
325 return reader.attributes().value(
"package"_L1).toString();
333 QFile androidManifestXml(androidManifestPath);
335 QXmlStreamReader reader(&androidManifestXml);
336 while (!reader.atEnd()) {
338 if (reader.isStartElement() && reader.name() ==
"activity"_L1)
339 return reader.attributes().value(
"android:name"_L1).toString();
357 QRegularExpression oldFormats{
"^-(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
358 QRegularExpression newLoggingFormat{
"^(.*),(txt|csv|xunitxml|junitxml|xml|lightxml|teamcity|tap)$"_L1};
363 for (
int i = 0;
i <
g_options.testArgsList.size(); ++
i) {
367 if (
arg ==
"-o"_L1) {
371 const auto &filePath =
g_options.testArgsList[++
i];
372 const auto match = newLoggingFormat.match(filePath);
373 if (!
match.hasMatch()) {
376 const auto capturedTexts =
match.capturedTexts();
381 if (
match.hasMatch()) {
382 logType =
match.capturedTexts().at(1);
389 quotedArg.
replace(
"'"_L1,
"\'"_L1);
391 unhandledArgs <<
" \\\"%1\\\""_L1.
arg(quotedArg);
395 if (
g_options.outFiles.isEmpty() || !
file.isEmpty() || !logType.isEmpty())
400 testAppArgs +=
"-o output.%1,%1 "_L1.
arg(
format);
402 testAppArgs += unhandledArgs.join(u
' ').
trimmed();
403 testAppArgs =
"\"%1\""_L1.
arg(testAppArgs.trimmed());
410 if (
var.startsWith(
"QTEST_"_L1))
411 testEnvVars +=
"%1 "_L1.arg(
var);
414 if (!testEnvVars.isEmpty()) {
416 testEnvVars =
"-e extraenvvars \"%4\""_L1.arg(testEnvVars);
419 g_options.amStarttestArgs = {
"shell"_L1,
"am"_L1,
"start"_L1,
420 "-n"_L1, activityName,
421 "-e"_L1,
"applicationArguments"_L1, testAppArgs,
434 const QList<QByteArray> lines =
output.split(u
'\n');
435 if (lines.size() < 1)
438 QList<QByteArray> columns = lines.first().simplified().replace(u
'\t', u
' ').split(u
' ');
439 if (columns.size() < 3)
444 int pid = columns.at(1).toInt(&
ok);
469 }
while (!startDeadline.hasExpired() && !
g_testInfo.isTestRunnerInterrupted.load());
477 }
while (!finishedDeadline.hasExpired() && !
g_testInfo.isTestRunnerInterrupted.load());
479 if (finishedDeadline.hasExpired())
480 qWarning() <<
"Timed out while waiting for the test to finish";
488 const QStringList versionArgs = {
"shell"_L1,
"getprop"_L1,
"ro.build.version.sdk"_L1 };
495 qCritical() <<
"Unable to obtain the SDK version of the target.";
504 const QStringList userIdArgs = {
"shell"_L1,
"cmd"_L1,
"activity"_L1,
"get-current-user"_L1};
506 qCritical() <<
"Error: failed to retrieve the user ID";
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(
522 qCritical() <<
"Error: failed to retrieve the test's output.%1 file."_L1.arg(outSuffix);
527 qCritical() <<
"Error: the test's output.%1 is empty."_L1.arg(outSuffix);
531 auto checkerIt =
g_options.checkFiles.find(outSuffix);
533 if (
it.value() ==
"-"_L1) {
549 logcatArgs <<
"-t"_L1 << formattedTime;
555 qCritical() <<
"Error: failed to fetch logcat of the test";
559 if (logcat.isEmpty()) {
560 qWarning() <<
"The retrieved logcat is empty";
564 qDebug() <<
"****** Begin logcat output ******";
565 qDebug().noquote() << logcat;
566 qDebug() <<
"****** End logcat output ******";
571 const QStringList abiArgs = {
"shell"_L1,
"getprop"_L1,
"ro.product.cpu.abi"_L1 };
574 qWarning() <<
"Warning: failed to get the device abi, fallback to first libs dir";
583 bool useNdkStack =
false;
584 auto libsPath =
"%1/libs/"_L1.arg(
g_options.buildPath);
590 if (!subDirs.isEmpty())
591 abi = subDirs.first();
594 if (!abi.isEmpty()) {
598 qWarning() <<
"Warning: failed to get the libs abi, ndk-stack cannot be used.";
601 qWarning() <<
"Warning: ndk-stack path not provided and couldn't be deduced "
602 "using the ANDROID_NDK_ROOT environment variable.";
605 QProcess adbCrashProcess;
606 QProcess ndkStackProcess;
609 adbCrashProcess.setStandardOutputProcess(&ndkStackProcess);
610 ndkStackProcess.start(
g_options.ndkStackPath, {
"-sym"_L1, libsPath });
613 const QStringList adbCrashArgs = {
"logcat"_L1,
"-b"_L1,
"crash"_L1,
"-t"_L1, formattedTime };
614 adbCrashProcess.start(
g_options.adbCommand, adbCrashArgs);
616 if (!adbCrashProcess.waitForStarted()) {
617 qCritical() <<
"Error: failed to run adb logcat crash command.";
621 if (useNdkStack && !ndkStackProcess.waitForStarted()) {
622 qCritical() <<
"Error: failed to run ndk-stack command.";
626 if (!adbCrashProcess.waitForFinished()) {
627 qCritical() <<
"Error: adb command timed out.";
631 if (useNdkStack && !ndkStackProcess.waitForFinished()) {
632 qCritical() <<
"Error: ndk-stack command timed out.";
636 const QByteArray crash = useNdkStack ? ndkStackProcess.readAllStandardOutput()
637 : adbCrashProcess.readAllStandardOutput();
638 if (crash.isEmpty()) {
639 qWarning() <<
"The retrieved crash logcat is empty";
643 qDebug() <<
"****** Begin logcat crash buffer output ******";
644 qDebug().noquote() << crash;
645 qDebug() <<
"****** End logcat crash buffer output ******";
651 "%m-%d %H:%M:%S.000"_L1 :
"%Y-%m-%d %H:%M:%S.%3N"_L1;
653 QStringList dateArgs = {
"shell"_L1,
"date"_L1,
"+'%1'"_L1.arg(timeFormat) };
656 qWarning() <<
"Date/time adb command failed";
686 QSystemSemaphore
semaphore { QSystemSemaphore::platformSafeKey(u
"androidtestrunner"_s),
687 1, QSystemSemaphore::Open };
694 std::signal(
signal, SIG_DFL);
702 g_testInfo.isTestRunnerInterrupted.store(
true);
705int main(
int argc,
char *argv[])
717 qCritical() <<
"It is required to provide a make command with the \"--make\" parameter "
718 "to generate the apk.";
735 qCritical(
"No apk \"%s\" found after running the make command. "
736 "Check the provided path and the make command.",
784 if (
g_testInfo.isTestRunnerInterrupted.load()) {
785 qCritical() <<
"The androidtestrunner was interrupted and the was test cleaned up.";
789 return success ? 0 : 1;
QByteArray & prepend(char c)
This is an overloaded member function, provided for convenience. It differs from the above function o...
QByteArray simplified() const &
QByteArray & append(char c)
This is an overloaded member function, provided for convenience. It differs from the above function o...
static QStringList arguments()
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...
static QChar separator()
Returns the native directory separator: "/" under Unix and "\\" under Windows.
static QString toNativeSeparators(const QString &pathName)
bool exists() const
This is an overloaded member function, provided for convenience. It differs from the above function o...
qsizetype size() const noexcept
bool removeOne(const AT &t)
const_reference at(qsizetype i) const noexcept
static QProcessEnvironment systemEnvironment()
\inmodule QtCore \reentrant
\macro QT_RESTRICTED_CAST_FROM_ASCII
QString & replace(qsizetype i, qsizetype len, QChar after)
bool isEmpty() const noexcept
Returns true if the string has no characters; otherwise returns false.
static QString fromUtf8(QByteArrayView utf8)
This is an overloaded member function, provided for convenience. It differs from the above function o...
QString arg(qlonglong a, int fieldwidth=0, int base=10, QChar fillChar=u' ') const
static QString number(int, int base=10)
This is an overloaded member function, provided for convenience. It differs from the above function o...
QString trimmed() const &
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,...
QSet< QString >::iterator it
QList< QVariant > arguments
GLboolean GLboolean GLboolean GLboolean a
[7]
GLint GLsizei GLsizei GLenum GLenum GLsizei void * data
GLint GLsizei GLsizei GLenum format
#define qPrintable(string)
static QString getCurrentTimeString()
void printLogcatCrashBuffer(const QString &formattedTime)
static void setOutputFile(QString file, QString format)
static bool parseTestArgs()
static QString packageNameFromAndroidManifest(const QString &androidManifestPath)
static bool checkJunit(const QByteArray &data)
static bool checkLightxml(const QByteArray &data)
static bool checkXml(const QByteArray &data)
static QString activityFromAndroidManifest(const QString &androidManifestPath)
static TestInfo g_testInfo
static bool checkTap(const QByteArray &data)
static bool checkCsv(const QByteArray &data)
static void obtainSdkVersion()
static bool execAdbCommand(const QStringList &args, QByteArray *output=nullptr, bool verbose=true)
void sigHandler(int signal)
static bool checkTeamcity(const QByteArray &data)
static bool parseOptions()
TestRunnerSystemSemaphore testRunnerLock
static void waitForStartedAndFinished()
static QString getDeviceABI()
static bool execCommand(const QString &program, const QStringList &args, QByteArray *output=nullptr, bool verbose=false)
static bool uninstallTestPackage()
static bool checkTxt(const QByteArray &data)
void printLogcat(const QString &formattedTime)
QString qEnvironmentVariable(const char *varName, const QString &defaultValue)
static bool match(const uchar *found, uint foundLen, const char *target, uint targetLen)
QT_BEGIN_NAMESPACE typedef uchar * output
QTextStream out(stdout)
[7]
QStringList amStarttestArgs
QHash< QString, QString > outFiles
const QHash< QString, std::function< bool(const QByteArray &)> checkFiles)
std::atomic< bool > isTestRunnerInterrupted
std::atomic< bool > isPackageInstalled
~TestRunnerSystemSemaphore()
std::atomic< bool > isAcquired
TestRunnerSystemSemaphore()
QSystemSemaphore semaphore