194int main(
int argc,
char *argv[])
199 QCoreApplication app(argc, argv);
200 app.setApplicationName(u"harmonyostestrunner"_s);
201 app.setApplicationVersion(QString::fromLatin1(QT_VERSION_STR));
203 QCommandLineParser parser;
204 parser.setApplicationDescription(
205 u"Runs a single Qt auto test from an installed HarmonyOS test bundle HAP."_s);
206 parser.addHelpOption();
207 parser.addVersionOption();
209 parser.addPositionalArgument(
211 u"Path to the test shared library (e.g. /path/to/libtst_qobject.so)"_s);
213 QCommandLineOption bundleNameOpt(
215 u"HarmonyOS bundle name of the installed test HAP (env: QT_HARMONYOS_BUNDLE_NAME)"_s,
217 qEnvironmentVariable(
"QT_HARMONYOS_BUNDLE_NAME", u"org.qtproject.autotests"_s));
218 parser.addOption(bundleNameOpt);
220 QCommandLineOption abilityNameOpt(
222 u"HarmonyOS ability name inside the test HAP (env: QT_HARMONYOS_ABILITY_NAME)"_s,
224 qEnvironmentVariable(
"QT_HARMONYOS_ABILITY_NAME", u"QAbility"_s));
225 parser.addOption(abilityNameOpt);
227 QCommandLineOption hdcOpt(
229 u"Path to the hdc tool (env: QT_HARMONYOS_HDC)"_s,
231 qEnvironmentVariable(
"QT_HARMONYOS_HDC", u"hdc"_s));
232 parser.addOption(hdcOpt);
234 QCommandLineOption timeoutOpt(
236 u"Seconds to wait for a test to complete before aborting (env: QT_HARMONYOS_TEST_TIMEOUT)"_s,
238 qEnvironmentVariable(
"QT_HARMONYOS_TEST_TIMEOUT", u"300"_s));
239 parser.addOption(timeoutOpt);
241 QCommandLineOption noProgressTimeoutOpt(
242 u"no-progress-timeout"_s,
243 u"Seconds without a PASS/FAIL test case result before declaring the test hung "
244 u"(env: QT_HARMONYOS_NO_PROGRESS_TIMEOUT, 0 = disabled)"_s,
246 qEnvironmentVariable(
"QT_HARMONYOS_NO_PROGRESS_TIMEOUT", u"60"_s));
247 parser.addOption(noProgressTimeoutOpt);
249 QCommandLineOption deviceOpt(
251 u"hdc connect key (-t) for the target device — required when multiple devices "
252 u"are attached (env: QT_HARMONYOS_DEVICE)"_s,
254 qEnvironmentVariable(
"QT_HARMONYOS_DEVICE"));
255 parser.addOption(deviceOpt);
257 QCommandLineOption testConfigOpt(
259 u"Generated test bundle deployment settings, read for the system dialogs the runner has "
260 u"to answer (env: QT_HARMONYOS_TEST_CONFIG)"_s,
262 qEnvironmentVariable(
"QT_HARMONYOS_TEST_CONFIG"));
263 parser.addOption(testConfigOpt);
265 QCommandLineOption testEnvOpt(
267 u"Semicolon-separated NAME=VALUE pairs to set in the test environment "
268 u"(env: QT_HARMONYOS_TEST_ENV)"_s,
270 qEnvironmentVariable(
"QT_HARMONYOS_TEST_ENV"));
271 parser.addOption(testEnvOpt);
276 const QStringList positional = parser.positionalArguments();
277 if (positional.isEmpty()) {
278 fprintf(stderr,
"harmonyostestrunner: no test binary specified\n");
282 const QString testBinaryPath = positional.first();
283 const QString testLibName = QFileInfo(testBinaryPath).fileName();
285 const QStringList testArgs = positional.mid(1);
286 const QString bundleName = parser.value(bundleNameOpt);
287 const QString abilityName = parser.value(abilityNameOpt);
288 const Hdc hdc(parser.value(hdcOpt), parser.value(deviceOpt));
289 const int timeoutSecs = parser.value(timeoutOpt).toInt();
290 const int noProgressTimeoutSecs = parser.value(noProgressTimeoutOpt).toInt();
294 const QString runId = QString::number(QDateTime::currentMSecsSinceEpoch());
296 const QString appBase = u"/data/storage/el2/base/files"_s;
297 const QString appStdoutPath = appBase + u"/qt_stdout_"_s + runId + u".txt"_s;
298 const QString appExitCodePath = appBase + u"/qt_exitcode_"_s + runId + u".txt"_s;
300 const QString shellBase = u"/data/app/el2/100/base/"_s + bundleName + u"/files"_s;
301 const QString shellStdoutPath = shellBase + u"/qt_stdout_"_s + runId + u".txt"_s;
302 const QString shellExitCodePath = shellBase + u"/qt_exitcode_"_s + runId + u".txt"_s;
304 const QString bundleCheckOutput =
305 hdc.shell({u"bm"_s, u"dump"_s, u"-n"_s, bundleName});
306 if (bundleCheckOutput.contains(u"error"_s, Qt::CaseInsensitive)
307 || bundleCheckOutput.trimmed().isEmpty()) {
309 "harmonyostestrunner: bundle '%s' is not installed on the device.\n"
310 " Build and sign the test HAP, then install it with:\n"
311 " hdc install <path/to/autotests-signed.hap>\n",
312 qPrintable(bundleName));
316#if QT_CONFIG(systemsemaphore)
317 TestRunnerSystemSemaphore runnerLock(runnerLockKey(hdc.connectKey(), bundleName));
318 g_runnerLock = &runnerLock;
319 runnerLock.acquire();
322 forceStopBundle(hdc, bundleName);
323 waitForProcessDeath(hdc, bundleName);
326 QStringList aaStartCommand = {
329 u"-a"_s, abilityName,
330 u"--ps"_s, u"io.qt.appSharedLibNameOverride"_s, testLibName,
331 u"--ps"_s, u"io.qt.debug.redirectedStdoutPath"_s, appStdoutPath,
332 u"--ps"_s, u"io.qt.debug.exitCodePath"_s, appExitCodePath,
334 u"--pb"_s, u"io.qt.useDefaultUiAbilityInstanceInQt"_s, u"false"_s,
337 aaStartCommand << u"--pb"_s << u"io.qt.watchdogEnabled"_s << u"false"_s;
339 if (!testArgs.isEmpty()) {
340 const QString json = QString::fromUtf8(
341 QJsonDocument(QJsonArray::fromStringList(testArgs)).toJson(QJsonDocument::Compact));
342 aaStartCommand += {u"--ps"_s, u"io.qt.appArgsJson"_s, shellSingleQuote(json)};
345 const QString testEnv = parser.value(testEnvOpt);
346 if (!testEnv.isEmpty()) {
347 QJsonObject envVarsObject;
348 for (
const QString &pair : testEnv.split(u';', Qt::SkipEmptyParts)) {
349 const qsizetype eq = pair.indexOf(u'=');
351 fprintf(stderr,
"harmonyostestrunner: ignoring malformed --test-env entry "
352 "(expected NAME=VALUE): %s\n", qPrintable(pair));
355 envVarsObject.insert(pair.left(eq), pair.mid(eq + 1));
357 const QString json = QString::fromUtf8(
358 QJsonDocument(envVarsObject).toJson(QJsonDocument::Compact));
359 aaStartCommand += {u"--ps"_s, u"io.qt.envVarsJson"_s, shellSingleQuote(json)};
364 const QString aaOut = hdc.shell(aaStartCommand,
true);
365 if (aaOut.contains(u"error"_s, Qt::CaseInsensitive)
366 || aaOut.contains(u"failed"_s, Qt::CaseInsensitive)) {
367 fprintf(stderr,
"harmonyostestrunner: aa start: %s\n", qPrintable(aaOut.trimmed()));
371 if (!waitForProcessStart(hdc, bundleName, shellExitCodePath)) {
372 fprintf(stderr,
"harmonyostestrunner: %s: timed out waiting for process to start\n",
373 qPrintable(testLibName));
374#if QT_CONFIG(systemsemaphore)
375 runnerLock.release();
380 waitForStdoutFile(hdc, shellStdoutPath);
382 const std::unique_ptr<QProcess> stdoutLogger =
383 streamDeviceFileWhileAppRuns(hdc, shellStdoutPath, bundleName);
385 fprintf(stderr,
"harmonyostestrunner: warning: failed to start stdout logger, "
386 "output may be delayed\n");
391 const int pollIntervalMs = 500;
393 QElapsedTimer elapsed;
395 int testExitCode = -1;
396 bool completed =
false;
397 int aliveCheckCounter = 0;
398 qint64 lastTestProgressAt = -1;
399 qint64 lastHeartbeatSecs = 0;
400 BlockingTestDialogs blockingTestDialogs(parser.value(testConfigOpt), hdc);
401 std::chrono::nanoseconds lastOutputAt {};
404 && elapsed.elapsed() < qint64(timeoutSecs) * 1000)
407 const QByteArray chunk = stdoutLogger->readAllStandardOutput();
408 if (!chunk.isEmpty()) {
409 fwrite(chunk.constData(), 1,
static_cast<size_t>(chunk.size()), stdout);
414 lastOutputAt = elapsed.durationElapsed();
415 if (chunk.contains(
"PASS :") || chunk.contains(
"FAIL! :")
416 || chunk.contains(
"Totals:"))
417 lastTestProgressAt = elapsed.elapsed();
423 const QString exitContent =
424 readDeviceFile(hdc, shellExitCodePath);
426 const int code = exitContent.trimmed().toInt(&ok);
434 answerBlockingDialogsIfStalled(blockingTestDialogs, elapsed, lastOutputAt);
438 if (++aliveCheckCounter % 3 == 0 && !isProcessAlive(hdc, bundleName)) {
439 const QString exitContent =
440 readDeviceFile(hdc, shellExitCodePath);
442 const int code = exitContent.trimmed().toInt(&ok);
449 fprintf(stderr,
"harmonyostestrunner: %s: process exited without writing "
450 "exit code — likely crashed\n", qPrintable(testLibName));
456 if (noProgressTimeoutSecs > 0 && lastTestProgressAt >= 0
457 && elapsed.elapsed() - lastTestProgressAt
458 > qint64(noProgressTimeoutSecs) * 1000)
461 "harmonyostestrunner: %s: no test case progress for %d seconds "
462 "— main thread likely deadlocked, force-stopping\n",
463 qPrintable(testLibName), noProgressTimeoutSecs);
464 forceStopBundle(hdc, bundleName);
470 const qint64 elapsedSecs = elapsed.elapsed() / 1000;
471 if (elapsedSecs >= lastHeartbeatSecs + 30) {
472 fprintf(stderr,
"harmonyostestrunner: %s still running (%lld s elapsed)\n",
473 qPrintable(testLibName),
static_cast<
long long>(elapsedSecs));
474 lastHeartbeatSecs = elapsedSecs;
478 stdoutLogger->waitForReadyRead(pollIntervalMs);
480 QThread::msleep(pollIntervalMs);
488 "harmonyostestrunner: TIMEOUT — %s did not complete within %d seconds\n",
489 qPrintable(testLibName), timeoutSecs);
491 forceStopBundle(hdc, bundleName);
495 if (stdoutLogger->state() != QProcess::NotRunning) {
498 while (stdoutLogger->waitForReadyRead(200)) {
499 const QByteArray chunk = stdoutLogger->readAllStandardOutput();
502 fwrite(chunk.constData(), 1,
static_cast<size_t>(chunk.size()), stdout);
506 const QByteArray finalChunk = stdoutLogger->readAllStandardOutput();
507 if (!finalChunk.isEmpty()) {
508 fwrite(finalChunk.constData(), 1,
static_cast<size_t>(finalChunk.size()), stdout);
513#if QT_CONFIG(systemsemaphore)
514 runnerLock.release();
517 return completed ? testExitCode