211int main(
int argc,
char *argv[])
216 QCoreApplication app(argc, argv);
217 app.setApplicationName(u"harmonyostestrunner"_s);
218 app.setApplicationVersion(QString::fromLatin1(QT_VERSION_STR));
220 QCommandLineParser parser;
221 parser.setApplicationDescription(
222 u"Runs a single Qt auto test from an installed HarmonyOS test bundle HAP."_s);
223 parser.addHelpOption();
224 parser.addVersionOption();
226 parser.addPositionalArgument(
228 u"Path to the test shared library (e.g. /path/to/libtst_qobject.so)"_s);
230 QCommandLineOption bundleNameOpt(
232 u"HarmonyOS bundle name of the installed test HAP (env: QT_HARMONYOS_BUNDLE_NAME)"_s,
234 qEnvironmentVariable(
"QT_HARMONYOS_BUNDLE_NAME", u"org.qtproject.autotests"_s));
235 parser.addOption(bundleNameOpt);
237 QCommandLineOption abilityNameOpt(
239 u"HarmonyOS ability name inside the test HAP (env: QT_HARMONYOS_ABILITY_NAME)"_s,
241 qEnvironmentVariable(
"QT_HARMONYOS_ABILITY_NAME", u"QAbility"_s));
242 parser.addOption(abilityNameOpt);
244 QCommandLineOption hdcOpt(
246 u"Path to the hdc tool (env: QT_HARMONYOS_HDC)"_s,
248 qEnvironmentVariable(
"QT_HARMONYOS_HDC", u"hdc"_s));
249 parser.addOption(hdcOpt);
251 QCommandLineOption timeoutOpt(
253 u"Seconds to wait for a test to complete before aborting (env: QT_HARMONYOS_TEST_TIMEOUT)"_s,
255 qEnvironmentVariable(
"QT_HARMONYOS_TEST_TIMEOUT", u"300"_s));
256 parser.addOption(timeoutOpt);
258 QCommandLineOption noProgressTimeoutOpt(
259 u"no-progress-timeout"_s,
260 u"Seconds without a PASS/FAIL test case result before declaring the test hung "
261 u"(env: QT_HARMONYOS_NO_PROGRESS_TIMEOUT, 0 = disabled)"_s,
263 qEnvironmentVariable(
"QT_HARMONYOS_NO_PROGRESS_TIMEOUT", u"60"_s));
264 parser.addOption(noProgressTimeoutOpt);
266 QCommandLineOption deviceOpt(
268 u"hdc connect key (-t) for the target device — required when multiple devices "
269 u"are attached (env: QT_HARMONYOS_DEVICE)"_s,
271 qEnvironmentVariable(
"QT_HARMONYOS_DEVICE"));
272 parser.addOption(deviceOpt);
278 const QStringList positional = parser.positionalArguments();
279 if (positional.isEmpty()) {
280 fprintf(stderr,
"harmonyostestrunner: no test binary specified\n");
284 const QString testBinaryPath = positional.first();
285 const QString testLibName = QFileInfo(testBinaryPath).fileName();
287 const QStringList testArgs = positional.mid(1);
288 const QString bundleName = parser.value(bundleNameOpt);
289 const QString abilityName = parser.value(abilityNameOpt);
290 const QString hdc = parser.value(hdcOpt);
291 const int timeoutSecs = parser.value(timeoutOpt).toInt();
292 const int noProgressTimeoutSecs = parser.value(noProgressTimeoutOpt).toInt();
296 const QString runId = QString::number(QDateTime::currentMSecsSinceEpoch());
298 const QString appBase = u"/data/storage/el2/base/files"_s;
299 const QString appStdoutPath = appBase + u"/qt_stdout_"_s + runId + u".txt"_s;
300 const QString appExitCodePath = appBase + u"/qt_exitcode_"_s + runId + u".txt"_s;
302 const QString shellBase = u"/data/app/el2/100/base/"_s + bundleName + u"/files"_s;
303 const QString shellStdoutPath = shellBase + u"/qt_stdout_"_s + runId + u".txt"_s;
304 const QString shellExitCodePath = shellBase + u"/qt_exitcode_"_s + runId + u".txt"_s;
306 const QString bundleCheckOutput =
307 runHdc(hdc, {u"shell"_s, u"bm"_s, u"dump"_s, u"-n"_s, bundleName});
308 if (bundleCheckOutput.contains(u"error"_s, Qt::CaseInsensitive)
309 || bundleCheckOutput.trimmed().isEmpty()) {
311 "harmonyostestrunner: bundle '%s' is not installed on the device.\n"
312 " Build and sign the test HAP, then install it with:\n"
313 " hdc install <path/to/autotests-signed.hap>\n",
314 qPrintable(bundleName));
318#if QT_CONFIG(systemsemaphore)
319 TestRunnerSystemSemaphore runnerLock(runnerLockKey(g_hdcConnectKey, bundleName));
320 g_runnerLock = &runnerLock;
321 runnerLock.acquire();
324 runHdc(hdc, {u"shell"_s, u"aa"_s, u"force-stop"_s, bundleName});
325 waitForProcessDeath(hdc, bundleName);
329 QStringList aaStartArgs = {
330 u"shell"_s, u"aa"_s, u"start"_s,
332 u"-a"_s, abilityName,
333 u"--ps"_s, u"io.qt.appSharedLibNameOverride"_s, testLibName,
334 u"--ps"_s, u"io.qt.debug.redirectedStdoutPath"_s, appStdoutPath,
335 u"--ps"_s, u"io.qt.debug.exitCodePath"_s, appExitCodePath,
338 u"--pb"_s, u"io.qt.useUriAsArg"_s, u"false"_s,
340 u"--pb"_s, u"io.qt.useDefaultUiAbilityInstanceInQt"_s, u"false"_s,
343 aaStartArgs << u"--pb"_s << u"io.qt.watchdogEnabled"_s << u"false"_s;
345 if (!testArgs.isEmpty()) {
346 QString json = u"["_s;
347 for (
int i = 0; i < testArgs.size(); ++i) {
350 QString escaped = testArgs.at(i);
351 escaped.replace(u'\\', u"\\\\"_s);
352 escaped.replace(u'"', u"\\\""_s);
353 json += u"\""_s + escaped + u"\""_s;
356 aaStartArgs += {u"--ps"_s, u"io.qt.appArgsJson"_s, json};
361 const QString aaOut = runHdc(hdc, aaStartArgs,
true);
362 if (aaOut.contains(u"error"_s, Qt::CaseInsensitive)
363 || aaOut.contains(u"failed"_s, Qt::CaseInsensitive)) {
364 fprintf(stderr,
"harmonyostestrunner: aa start: %s\n", qPrintable(aaOut.trimmed()));
368 if (!waitForProcessStart(hdc, bundleName, shellExitCodePath)) {
369 fprintf(stderr,
"harmonyostestrunner: %s: timed out waiting for process to start\n",
370 qPrintable(testLibName));
371#if QT_CONFIG(systemsemaphore)
372 runnerLock.release();
377 waitForStdoutFile(hdc, shellStdoutPath);
379 QProcess stdoutLogger;
380 const bool hasStdoutLogger = setupStdoutLogger(stdoutLogger, hdc, shellStdoutPath);
381 if (!hasStdoutLogger) {
382 fprintf(stderr,
"harmonyostestrunner: warning: failed to start stdout logger, "
383 "output may be delayed\n");
388 const int pollIntervalMs = 500;
390 QElapsedTimer elapsed;
392 int testExitCode = -1;
393 bool completed =
false;
394 int aliveCheckCounter = 0;
395 qint64 lastTestProgressAt = -1;
396 qint64 lastHeartbeatSecs = 0;
399 && elapsed.elapsed() < qint64(timeoutSecs) * 1000)
401 if (hasStdoutLogger) {
402 const QByteArray chunk = stdoutLogger.readAllStandardOutput();
403 if (!chunk.isEmpty()) {
404 fwrite(chunk.constData(), 1,
static_cast<size_t>(chunk.size()), stdout);
409 if (chunk.contains(
"PASS :") || chunk.contains(
"FAIL! :")
410 || chunk.contains(
"Totals:"))
411 lastTestProgressAt = elapsed.elapsed();
417 const QString exitContent =
418 runHdc(hdc, {u"shell"_s, u"cat"_s, shellExitCodePath});
420 const int code = exitContent.trimmed().toInt(&ok);
430 if (++aliveCheckCounter % 3 == 0 && !isProcessAlive(hdc, bundleName)) {
431 const QString exitContent =
432 runHdc(hdc, {u"shell"_s, u"cat"_s, shellExitCodePath});
434 const int code = exitContent.trimmed().toInt(&ok);
441 fprintf(stderr,
"harmonyostestrunner: %s: process exited without writing "
442 "exit code — likely crashed\n", qPrintable(testLibName));
448 if (noProgressTimeoutSecs > 0 && lastTestProgressAt >= 0
449 && elapsed.elapsed() - lastTestProgressAt
450 > qint64(noProgressTimeoutSecs) * 1000)
453 "harmonyostestrunner: %s: no test case progress for %d seconds "
454 "— main thread likely deadlocked, force-stopping\n",
455 qPrintable(testLibName), noProgressTimeoutSecs);
456 runHdc(hdc, {u"shell"_s, u"aa"_s, u"force-stop"_s, bundleName});
462 const qint64 elapsedSecs = elapsed.elapsed() / 1000;
463 if (elapsedSecs >= lastHeartbeatSecs + 30) {
464 fprintf(stderr,
"harmonyostestrunner: %s still running (%lld s elapsed)\n",
465 qPrintable(testLibName),
static_cast<
long long>(elapsedSecs));
466 lastHeartbeatSecs = elapsedSecs;
470 stdoutLogger.waitForReadyRead(pollIntervalMs);
472 QThread::msleep(pollIntervalMs);
475 if (hasStdoutLogger) {
476 if (stdoutLogger.state() != QProcess::NotRunning) {
479 while (stdoutLogger.waitForReadyRead(200)) {
480 const QByteArray chunk = stdoutLogger.readAllStandardOutput();
483 fwrite(chunk.constData(), 1,
static_cast<size_t>(chunk.size()), stdout);
486 stdoutLogger.terminate();
487 stdoutLogger.waitForFinished(5000);
489 const QByteArray finalChunk = stdoutLogger.readAllStandardOutput();
490 if (!finalChunk.isEmpty()) {
491 fwrite(finalChunk.constData(), 1,
static_cast<size_t>(finalChunk.size()), stdout);
498 runHdc(hdc, {u"shell"_s, u"aa"_s, u"force-stop"_s, bundleName});
499#if QT_CONFIG(systemsemaphore)
500 runnerLock.release();
505 "harmonyostestrunner: TIMEOUT — %s did not complete within %d seconds\n",
506 qPrintable(testLibName), timeoutSecs);
507 runHdc(hdc, {u"shell"_s, u"aa"_s, u"force-stop"_s, bundleName});
508#if QT_CONFIG(systemsemaphore)
509 runnerLock.release();
514#if QT_CONFIG(systemsemaphore)
515 runnerLock.release();