Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
5#include "hdc.h"
6#include "shellhelpers.h"
7
8#include <QtCore/qcoreapplication.h>
9#include <QtCore/qcommandlineparser.h>
10#include <QtCore/qdatetime.h>
11#include <QtCore/qfileinfo.h>
12#include <QtCore/qprocess.h>
13#include <QtCore/qthread.h>
14#include <QtCore/qelapsedtimer.h>
15#include <QtCore/qjsonarray.h>
16#include <QtCore/qjsondocument.h>
17#include <QtCore/qjsonobject.h>
18#if QT_CONFIG(systemsemaphore)
19# include <QtCore/qsystemsemaphore.h>
20# include <QtCore/qtipccommon.h>
21#endif
22
23#include <atomic>
24#include <chrono>
25#include <csignal>
26#include <cstdio>
27#include <memory>
28
29using namespace Qt::StringLiterals;
30using namespace std::chrono_literals;
31
32// HarmonyOS sandbox: the app's writable /data/storage/el2/base/files/ maps to
33// /data/app/el2/100/base/<bundleName>/files/ in the hdc shell namespace, which
34// shell can read but not write (SELinux). "tail -f" / inotify are blocked on
35// that path too, so stdout is streamed via a device-side wc/tail polling loop
36// that uses only plain read() syscalls.
37
38// QTest normal exit code range is 0-127; use 251-254 for runner-level failures.
39static constexpr int EXIT_ERROR = 254;
40static constexpr int EXIT_NOEXITCODE = 253;
41static constexpr int EXIT_CRASH = 252;
42static constexpr int EXIT_TIMEOUT = 251;
43
44static QString shellSingleQuote(const QString &value)
45{
46 QString escaped = value;
47 escaped.replace(u"'"_s, uR"('\'')"_s);
48 return u"'"_s + escaped + u"'"_s;
49}
50
51static constexpr auto quietBeforeDialogCheck = 3s;
52static constexpr auto quietBeforeDialogCheckWhileStarting = 1s;
53static constexpr auto dialogsExpectedWhileStarting = 10s;
54
55#if QT_CONFIG(systemsemaphore)
56struct TestRunnerSystemSemaphore
57{
58 explicit TestRunnerSystemSemaphore(const QString &key)
59 : nativeKey(QSystemSemaphore::platformSafeKey(key)),
60 semaphore(nativeKey, 1, QSystemSemaphore::Open)
61 {}
62 ~TestRunnerSystemSemaphore() { release(); }
63
64 // Acquire with a 30 s deadline: if a previous runner died -9 without
65 // releasing, reset the semaphore via Create-mode and retry.
66 void acquire()
67 {
68 std::atomic<bool> acquireResult { false };
69 QThread *worker = QThread::create([this, &acquireResult]() {
70 acquireResult.store(semaphore.acquire());
71 });
72 worker->start();
73 if (!worker->wait(30000)) {
74 fprintf(stderr, "harmonyostestrunner: semaphore stuck (previous runner "
75 "may have crashed) — resetting\n");
76 {
77 QSystemSemaphore reset{ nativeKey, 1, QSystemSemaphore::Create };
78 } // destructor unblocks the worker
79 worker->wait(5000);
80 }
81 delete worker;
82 isAcquired.store(acquireResult.load());
83 }
84
85 void release()
86 {
87 bool expected = true;
88 if (isAcquired.compare_exchange_strong(expected, false))
89 isAcquired.store(!semaphore.release());
90 }
91
92 std::atomic<bool> isAcquired { false };
93 QNativeIpcKey nativeKey;
94 QSystemSemaphore semaphore;
95};
96
97static TestRunnerSystemSemaphore *g_runnerLock = nullptr;
98
99static QString runnerLockKey(const QString &deviceKey, const QString &bundleName)
100{
101 const QString device = deviceKey.isEmpty() ? u"local"_s : deviceKey;
102 return u"harmonyostestrunner_"_s + device + u'_' + bundleName;
103}
104#endif // QT_CONFIG(systemsemaphore)
105
106static std::atomic<bool> g_interrupted { false };
107
108static void sigHandler(int sig)
109{
110 std::signal(sig, SIG_DFL);
111 // Not async-signal-safe; best effort so Ctrl-C doesn't strand the semaphore.
112 // The next runner's 30 s deadline resets it anyway.
113#if QT_CONFIG(systemsemaphore)
114 if (g_runnerLock)
115 g_runnerLock->release();
116#endif
117 g_interrupted.store(true);
118}
119
120// Without this, OHOS may deliver the new test's Want to the still-dying
121// previous process via onNewWant instead of creating a fresh one.
122static void waitForProcessDeath(const Hdc &hdc, const QString &bundleName,
123 int timeoutSecs = 15)
124{
125 const int pollMs = 200;
126 const int maxIterations = (timeoutSecs * 1000) / pollMs;
127 for (int i = 0; i < maxIterations; ++i) {
128 if (g_interrupted.load())
129 return;
130 if (!isProcessAlive(hdc, bundleName))
131 return;
132 QThread::msleep(pollMs);
133 }
134 fprintf(stderr, "harmonyostestrunner: warning: bundle process still alive after %d s "
135 "force-stop wait — proceeding anyway\n", timeoutSecs);
136}
137
138static bool waitForProcessStart(const Hdc &hdc, const QString &bundleName,
139 const QString &shellExitCodePath, int timeoutSecs = 30)
140{
141 const int pollMs = 250;
142 const int maxIterations = (timeoutSecs * 1000) / pollMs;
143 for (int i = 0; i < maxIterations; ++i) {
144 if (g_interrupted.load())
145 return false;
146 if (isProcessAlive(hdc, bundleName))
147 return true;
148 // Fast test may have finished before pidof could see it.
149 const QString exitContent = readDeviceFile(hdc, shellExitCodePath);
150 bool ok = false;
151 exitContent.trimmed().toInt(&ok);
152 if (ok)
153 return true;
154 QThread::msleep(pollMs);
155 }
156 return false;
157}
158
159static bool waitForStdoutFile(const Hdc &hdc, const QString &shellStdoutPath,
160 int timeoutSecs = 10)
161{
162 const int pollMs = 100;
163 const int maxIterations = (timeoutSecs * 1000) / pollMs;
164 for (int i = 0; i < maxIterations; ++i) {
165 if (g_interrupted.load())
166 return false;
167 const QString out = readDeviceFile(hdc, shellStdoutPath);
168 if (!out.contains(u"No such file or directory"_s))
169 return true;
170 QThread::msleep(pollMs);
171 }
172 return false;
173}
174
175static void answerBlockingDialogsIfStalled(BlockingTestDialogs &blockingTestDialogs,
176 const QElapsedTimer &elapsed, std::chrono::nanoseconds lastOutputAt)
177{
178 static std::chrono::nanoseconds lastDialogCheckAt {};
179
180 if (blockingTestDialogs.isEmpty())
181 return;
182
183 const std::chrono::nanoseconds now = elapsed.durationElapsed();
184 const auto quietRequired = now < dialogsExpectedWhileStarting
185 ? quietBeforeDialogCheckWhileStarting
186 : quietBeforeDialogCheck;
187 if (now - lastOutputAt < quietRequired || now - lastDialogCheckAt < quietRequired)
188 return;
189
190 blockingTestDialogs.answerVisibleDialog();
191 lastDialogCheckAt = now;
192}
193
194int main(int argc, char *argv[])
195{
196 std::signal(SIGINT, sigHandler);
197 std::signal(SIGTERM, sigHandler);
198
199 QCoreApplication app(argc, argv);
200 app.setApplicationName(u"harmonyostestrunner"_s);
201 app.setApplicationVersion(QString::fromLatin1(QT_VERSION_STR));
202
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();
208
209 parser.addPositionalArgument(
210 u"test-binary"_s,
211 u"Path to the test shared library (e.g. /path/to/libtst_qobject.so)"_s);
212
213 QCommandLineOption bundleNameOpt(
214 u"bundle-name"_s,
215 u"HarmonyOS bundle name of the installed test HAP (env: QT_HARMONYOS_BUNDLE_NAME)"_s,
216 u"name"_s,
217 qEnvironmentVariable("QT_HARMONYOS_BUNDLE_NAME", u"org.qtproject.autotests"_s));
218 parser.addOption(bundleNameOpt);
219
220 QCommandLineOption abilityNameOpt(
221 u"ability-name"_s,
222 u"HarmonyOS ability name inside the test HAP (env: QT_HARMONYOS_ABILITY_NAME)"_s,
223 u"name"_s,
224 qEnvironmentVariable("QT_HARMONYOS_ABILITY_NAME", u"QAbility"_s));
225 parser.addOption(abilityNameOpt);
226
227 QCommandLineOption hdcOpt(
228 u"hdc"_s,
229 u"Path to the hdc tool (env: QT_HARMONYOS_HDC)"_s,
230 u"path"_s,
231 qEnvironmentVariable("QT_HARMONYOS_HDC", u"hdc"_s));
232 parser.addOption(hdcOpt);
233
234 QCommandLineOption timeoutOpt(
235 u"timeout"_s,
236 u"Seconds to wait for a test to complete before aborting (env: QT_HARMONYOS_TEST_TIMEOUT)"_s,
237 u"seconds"_s,
238 qEnvironmentVariable("QT_HARMONYOS_TEST_TIMEOUT", u"300"_s));
239 parser.addOption(timeoutOpt);
240
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,
245 u"seconds"_s,
246 qEnvironmentVariable("QT_HARMONYOS_NO_PROGRESS_TIMEOUT", u"60"_s));
247 parser.addOption(noProgressTimeoutOpt);
248
249 QCommandLineOption deviceOpt(
250 u"device"_s,
251 u"hdc connect key (-t) for the target device — required when multiple devices "
252 u"are attached (env: QT_HARMONYOS_DEVICE)"_s,
253 u"key"_s,
254 qEnvironmentVariable("QT_HARMONYOS_DEVICE"));
255 parser.addOption(deviceOpt);
256
257 QCommandLineOption testConfigOpt(
258 u"test-config"_s,
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,
261 u"path"_s,
262 qEnvironmentVariable("QT_HARMONYOS_TEST_CONFIG"));
263 parser.addOption(testConfigOpt);
264
265 QCommandLineOption testEnvOpt(
266 u"test-env"_s,
267 u"Semicolon-separated NAME=VALUE pairs to set in the test environment "
268 u"(env: QT_HARMONYOS_TEST_ENV)"_s,
269 u"vars"_s,
270 qEnvironmentVariable("QT_HARMONYOS_TEST_ENV"));
271 parser.addOption(testEnvOpt);
272
273 parser.process(app);
274
275
276 const QStringList positional = parser.positionalArguments();
277 if (positional.isEmpty()) {
278 fprintf(stderr, "harmonyostestrunner: no test binary specified\n");
279 parser.showHelp(EXIT_ERROR);
280 }
281
282 const QString testBinaryPath = positional.first();
283 const QString testLibName = QFileInfo(testBinaryPath).fileName();
284 // Extra positionals (test function names, -v2, etc.) forwarded to the test.
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();
291
292 // Unique per-run ID: shell can't delete files from the app sandbox (SELinux),
293 // so uniqueness is the only way to avoid matching stale files.
294 const QString runId = QString::number(QDateTime::currentMSecsSinceEpoch());
295
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;
299
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;
303
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()) {
308 fprintf(stderr,
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));
313 return EXIT_ERROR;
314 }
315
316#if QT_CONFIG(systemsemaphore)
317 TestRunnerSystemSemaphore runnerLock(runnerLockKey(hdc.connectKey(), bundleName));
318 g_runnerLock = &runnerLock;
319 runnerLock.acquire();
320#endif
321
322 forceStopBundle(hdc, bundleName);
323 waitForProcessDeath(hdc, bundleName);
324
325 // --ps for string want.parameters, --pb for boolean.
326 QStringList aaStartCommand = {
327 u"aa"_s, u"start"_s,
328 u"-b"_s, bundleName,
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,
333 // Keep main alive across window destroy/create for visual tests.
334 u"--pb"_s, u"io.qt.useDefaultUiAbilityInstanceInQt"_s, u"false"_s,
335 };
336
337 aaStartCommand << u"--pb"_s << u"io.qt.watchdogEnabled"_s << u"false"_s;
338
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)};
343 }
344
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'=');
350 if (eq < 0) {
351 fprintf(stderr, "harmonyostestrunner: ignoring malformed --test-env entry "
352 "(expected NAME=VALUE): %s\n", qPrintable(pair));
353 continue;
354 }
355 envVarsObject.insert(pair.left(eq), pair.mid(eq + 1));
356 }
357 const QString json = QString::fromUtf8(
358 QJsonDocument(envVarsObject).toJson(QJsonDocument::Compact));
359 aaStartCommand += {u"--ps"_s, u"io.qt.envVarsJson"_s, shellSingleQuote(json)};
360 }
361
362 // aa start prints errors (screen locked, ability not found, ...) to stdout.
363 {
364 const QString aaOut = hdc.shell(aaStartCommand, /*printOnFailure=*/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()));
368 }
369 }
370
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();
376#endif
377 return EXIT_ERROR;
378 }
379
380 waitForStdoutFile(hdc, shellStdoutPath);
381
382 const std::unique_ptr<QProcess> stdoutLogger =
383 streamDeviceFileWhileAppRuns(hdc, shellStdoutPath, bundleName);
384 if (!stdoutLogger) {
385 fprintf(stderr, "harmonyostestrunner: warning: failed to start stdout logger, "
386 "output may be delayed\n");
387 }
388
389 // hdc shell always returns 0; can't use `test -f`. Cat the file and check
390 // whether the contents parse as int.
391 const int pollIntervalMs = 500;
392
393 QElapsedTimer elapsed;
394 elapsed.start();
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 {};
402
403 while (!g_interrupted.load()
404 && elapsed.elapsed() < qint64(timeoutSecs) * 1000)
405 {
406 if (stdoutLogger) {
407 const QByteArray chunk = stdoutLogger->readAllStandardOutput();
408 if (!chunk.isEmpty()) {
409 fwrite(chunk.constData(), 1, static_cast<size_t>(chunk.size()), stdout);
410 fflush(stdout);
411 // Markers match QPlainTestLogger output in qtestlog.cpp. If
412 // QTest's format ever changes, the no-progress watchdog silently
413 // stops firing — hung tests only trip the overall timeout.
414 lastOutputAt = elapsed.durationElapsed();
415 if (chunk.contains("PASS :") || chunk.contains("FAIL! :")
416 || chunk.contains("Totals:"))
417 lastTestProgressAt = elapsed.elapsed();
418 }
419 }
420
421 // Primary completion signal: exit-code file becomes parseable as int.
422 {
423 const QString exitContent =
424 readDeviceFile(hdc, shellExitCodePath);
425 bool ok = false;
426 const int code = exitContent.trimmed().toInt(&ok);
427 if (ok) {
428 testExitCode = code;
429 completed = true;
430 break;
431 }
432 }
433
434 answerBlockingDialogsIfStalled(blockingTestDialogs, elapsed, lastOutputAt);
435
436 // Liveness check every ~1.5 s. Re-read the exit-code file to cover the
437 // race where the process exits cleanly between checks.
438 if (++aliveCheckCounter % 3 == 0 && !isProcessAlive(hdc, bundleName)) {
439 const QString exitContent =
440 readDeviceFile(hdc, shellExitCodePath);
441 bool ok = false;
442 const int code = exitContent.trimmed().toInt(&ok);
443 if (ok) {
444 testExitCode = code;
445 completed = true;
446 break;
447 }
448
449 fprintf(stderr, "harmonyostestrunner: %s: process exited without writing "
450 "exit code — likely crashed\n", qPrintable(testLibName));
451 testExitCode = EXIT_NOEXITCODE;
452 completed = true;
453 break;
454 }
455
456 if (noProgressTimeoutSecs > 0 && lastTestProgressAt >= 0
457 && elapsed.elapsed() - lastTestProgressAt
458 > qint64(noProgressTimeoutSecs) * 1000)
459 {
460 fprintf(stderr,
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);
465 testExitCode = EXIT_CRASH;
466 completed = true;
467 break;
468 }
469
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;
475 }
476
477 if (stdoutLogger)
478 stdoutLogger->waitForReadyRead(pollIntervalMs);
479 else
480 QThread::msleep(pollIntervalMs);
481 }
482
483 const bool interrupted = g_interrupted.load();
484
485 if (!completed) {
486 if (!interrupted) {
487 fprintf(stderr,
488 "harmonyostestrunner: TIMEOUT — %s did not complete within %d seconds\n",
489 qPrintable(testLibName), timeoutSecs);
490 }
491 forceStopBundle(hdc, bundleName);
492 }
493
494 if (stdoutLogger) {
495 if (stdoutLogger->state() != QProcess::NotRunning) {
496 // Drain bytes still in flight after the test finished — the 100 ms
497 // device-side loop and hdc transport both add latency.
498 while (stdoutLogger->waitForReadyRead(200)) {
499 const QByteArray chunk = stdoutLogger->readAllStandardOutput();
500 if (chunk.isEmpty())
501 break;
502 fwrite(chunk.constData(), 1, static_cast<size_t>(chunk.size()), stdout);
503 fflush(stdout);
504 }
505 }
506 const QByteArray finalChunk = stdoutLogger->readAllStandardOutput();
507 if (!finalChunk.isEmpty()) {
508 fwrite(finalChunk.constData(), 1, static_cast<size_t>(finalChunk.size()), stdout);
509 fflush(stdout);
510 }
511 }
512
513#if QT_CONFIG(systemsemaphore)
514 runnerLock.release();
515#endif
516
517 return completed ? testExitCode
518 : interrupted ? EXIT_ERROR : EXIT_TIMEOUT;
519}
static constexpr int EXIT_NOEXITCODE
Definition main.cpp:42
static constexpr int EXIT_ERROR
Definition main.cpp:40
static bool waitForProcessStart(const Hdc &hdc, const QString &bundleName, const QString &shellExitCodePath, int timeoutSecs=30)
Definition main.cpp:138
static void waitForProcessDeath(const Hdc &hdc, const QString &bundleName, int timeoutSecs=15)
Definition main.cpp:122
static constexpr auto quietBeforeDialogCheck
Definition main.cpp:51
static QString shellSingleQuote(const QString &value)
Definition main.cpp:44
static constexpr int EXIT_CRASH
Definition main.cpp:41
static std::atomic< bool > g_interrupted
Definition main.cpp:106
static constexpr auto quietBeforeDialogCheckWhileStarting
Definition main.cpp:52
static void sigHandler(int sig)
Definition main.cpp:108
static constexpr auto dialogsExpectedWhileStarting
Definition main.cpp:53
static bool waitForStdoutFile(const Hdc &hdc, const QString &shellStdoutPath, int timeoutSecs=10)
Definition main.cpp:159
static constexpr int EXIT_TIMEOUT
Definition main.cpp:42
static void answerBlockingDialogsIfStalled(BlockingTestDialogs &blockingTestDialogs, const QElapsedTimer &elapsed, std::chrono::nanoseconds lastOutputAt)
Definition main.cpp:175
int main(int argc, char *argv[])
[ctor_close]