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
4#include <QtCore/qcoreapplication.h>
5#include <QtCore/qcommandlineparser.h>
6#include <QtCore/qdatetime.h>
7#include <QtCore/qfileinfo.h>
8#include <QtCore/qprocess.h>
9#include <QtCore/qthread.h>
10#include <QtCore/qelapsedtimer.h>
11#if QT_CONFIG(systemsemaphore)
12# include <QtCore/qsystemsemaphore.h>
13#endif
14
15#include <atomic>
16#include <csignal>
17#include <cstdio>
18
19using namespace Qt::StringLiterals;
20
21// HarmonyOS sandbox: the app's writable /data/storage/el2/base/files/ maps to
22// /data/app/el2/100/base/<bundleName>/files/ in the hdc shell namespace, which
23// shell can read but not write (SELinux). "tail -f" / inotify are blocked on
24// that path too, so stdout is streamed via a device-side wc/tail polling loop
25// that uses only plain read() syscalls.
26
27// QTest normal exit code range is 0-127; use 251-254 for runner-level failures.
28static constexpr int EXIT_ERROR = 254;
29static constexpr int EXIT_NOEXITCODE = 253;
30static constexpr int EXIT_CRASH = 252;
31static constexpr int EXIT_TIMEOUT = 251;
32
33// --device / QT_HARMONYOS_DEVICE, prepended as `-t <key>` to every hdc call.
35
36static QString runHdc(const QString &hdcPath, const QStringList &args,
37 bool printOnFailure = false)
38{
39 QProcess proc;
40 QStringList allArgs;
41 if (!g_hdcConnectKey.isEmpty())
42 allArgs << u"-t"_s << g_hdcConnectKey;
43 allArgs += args;
44 proc.start(hdcPath, allArgs);
45 if (!proc.waitForStarted(5000)) {
46 if (printOnFailure)
47 fprintf(stderr, "harmonyostestrunner: failed to start hdc: %s\n",
48 qPrintable(hdcPath));
49 return {};
50 }
51 proc.waitForFinished(30000);
52 if (printOnFailure) {
53 const QByteArray err = proc.readAllStandardError();
54 if (!err.isEmpty())
55 fprintf(stderr, "hdc %s stderr: %s\n", qPrintable(allArgs.join(u' ')),
56 err.constData());
57 }
58 return QString::fromUtf8(proc.readAllStandardOutput());
59}
60
61#if QT_CONFIG(systemsemaphore)
62struct TestRunnerSystemSemaphore
63{
64 TestRunnerSystemSemaphore() {}
65 ~TestRunnerSystemSemaphore() { release(); }
66
67 // Acquire with a 30 s deadline: if a previous runner died -9 without
68 // releasing, reset the semaphore via Create-mode and retry.
69 void acquire()
70 {
71 std::atomic<bool> acquireResult { false };
72 QThread *worker = QThread::create([this, &acquireResult]() {
73 acquireResult.store(semaphore.acquire());
74 });
75 worker->start();
76 if (!worker->wait(30000)) {
77 fprintf(stderr, "harmonyostestrunner: semaphore stuck (previous runner "
78 "may have crashed) — resetting\n");
79 {
80 QSystemSemaphore reset{
81 QSystemSemaphore::platformSafeKey(u"harmonyostestrunner"_s),
82 1, QSystemSemaphore::Create
83 };
84 } // destructor unblocks the worker
85 worker->wait(5000);
86 }
87 delete worker;
88 isAcquired.store(acquireResult.load());
89 }
90
91 void release()
92 {
93 bool expected = true;
94 if (isAcquired.compare_exchange_strong(expected, false))
95 isAcquired.store(!semaphore.release());
96 }
97
98 std::atomic<bool> isAcquired { false };
99 QSystemSemaphore semaphore {
100 QSystemSemaphore::platformSafeKey(u"harmonyostestrunner"_s),
101 1, QSystemSemaphore::Open
102 };
103};
104
105static TestRunnerSystemSemaphore g_runnerLock;
106#endif // QT_CONFIG(systemsemaphore)
107
108static std::atomic<bool> g_interrupted { false };
109
110static void sigHandler(int sig)
111{
112 std::signal(sig, SIG_DFL);
113 // Not async-signal-safe; best effort so Ctrl-C doesn't strand the semaphore.
114 // The next runner's 30 s deadline resets it anyway.
115#if QT_CONFIG(systemsemaphore)
116 g_runnerLock.release();
117#endif
118 g_interrupted.store(true);
119}
120
121static bool isProcessAlive(const QString &hdc, const QString &bundleName)
122{
123 return !runHdc(hdc, {u"shell"_s, u"pidof"_s, bundleName}).trimmed().isEmpty();
124}
125
126// Without this, OHOS may deliver the new test's Want to the still-dying
127// previous process via onNewWant instead of creating a fresh one.
128static void waitForProcessDeath(const QString &hdc, const QString &bundleName,
129 int timeoutSecs = 15)
130{
131 const int pollMs = 200;
132 const int maxIterations = (timeoutSecs * 1000) / pollMs;
133 for (int i = 0; i < maxIterations; ++i) {
134 if (g_interrupted.load())
135 return;
136 if (!isProcessAlive(hdc, bundleName))
137 return;
138 QThread::msleep(pollMs);
139 }
140 fprintf(stderr, "harmonyostestrunner: warning: bundle process still alive after %d s "
141 "force-stop wait — proceeding anyway\n", timeoutSecs);
142}
143
144static bool waitForProcessStart(const QString &hdc, const QString &bundleName,
145 const QString &shellExitCodePath, int timeoutSecs = 30)
146{
147 const int pollMs = 250;
148 const int maxIterations = (timeoutSecs * 1000) / pollMs;
149 for (int i = 0; i < maxIterations; ++i) {
150 if (g_interrupted.load())
151 return false;
152 if (isProcessAlive(hdc, bundleName))
153 return true;
154 // Fast test may have finished before pidof could see it.
155 const QString exitContent = runHdc(hdc, {u"shell"_s, u"cat"_s, shellExitCodePath});
156 bool ok = false;
157 exitContent.trimmed().toInt(&ok);
158 if (ok)
159 return true;
160 QThread::msleep(pollMs);
161 }
162 return false;
163}
164
165static bool waitForStdoutFile(const QString &hdc, const QString &shellStdoutPath,
166 int timeoutSecs = 10)
167{
168 const int pollMs = 100;
169 const int maxIterations = (timeoutSecs * 1000) / pollMs;
170 for (int i = 0; i < maxIterations; ++i) {
171 if (g_interrupted.load())
172 return false;
173 const QString out = runHdc(hdc, {u"shell"_s, u"cat"_s, shellStdoutPath});
174 if (!out.contains(u"No such file or directory"_s))
175 return true;
176 QThread::msleep(pollMs);
177 }
178 return false;
179}
180
181// wc -c + tail -c +N runs device-side; both use plain read() syscalls (SELinux
182// permits, unlike inotify), and one persistent hdc connection avoids per-poll
183// reconnect overhead.
184static bool setupStdoutLogger(QProcess &stdoutLogger, const QString &hdc,
185 const QString &shellStdoutPath)
186{
187 const QString loop =
188 u"sz=0; f='"_s + shellStdoutPath
189 + u"'; while true; do"
190 " new=$(wc -c < \"$f\" 2>/dev/null);"
191 " if [ \"${new:-0}\" -gt \"$sz\" ]; then"
192 " tail -c +$((sz+1)) \"$f\" 2>/dev/null; sz=$new;"
193 " fi; sleep 0.1; done"_s;
194
195 // SeparateChannels so we can scan stdout for PASS/FAIL while forwarding it.
196 stdoutLogger.setProcessChannelMode(QProcess::SeparateChannels);
197 QStringList args;
198 if (!g_hdcConnectKey.isEmpty())
199 args << u"-t"_s << g_hdcConnectKey;
200 args << u"shell"_s << loop;
201 stdoutLogger.start(hdc, args);
202 return stdoutLogger.waitForStarted(5000);
203}
204
205int main(int argc, char *argv[])
206{
207 std::signal(SIGINT, sigHandler);
208 std::signal(SIGTERM, sigHandler);
209
210 QCoreApplication app(argc, argv);
211 app.setApplicationName(u"harmonyostestrunner"_s);
212 app.setApplicationVersion(QString::fromLatin1(QT_VERSION_STR));
213
214 QCommandLineParser parser;
215 parser.setApplicationDescription(
216 u"Runs a single Qt auto test from an installed HarmonyOS test bundle HAP."_s);
217 parser.addHelpOption();
218 parser.addVersionOption();
219
220 parser.addPositionalArgument(
221 u"test-binary"_s,
222 u"Path to the test shared library (e.g. /path/to/libtst_qobject.so)"_s);
223
224 QCommandLineOption bundleNameOpt(
225 u"bundle-name"_s,
226 u"HarmonyOS bundle name of the installed test HAP (env: QT_HARMONYOS_BUNDLE_NAME)"_s,
227 u"name"_s,
228 qEnvironmentVariable("QT_HARMONYOS_BUNDLE_NAME", u"org.qtproject.autotests"_s));
229 parser.addOption(bundleNameOpt);
230
231 QCommandLineOption abilityNameOpt(
232 u"ability-name"_s,
233 u"HarmonyOS ability name inside the test HAP (env: QT_HARMONYOS_ABILITY_NAME)"_s,
234 u"name"_s,
235 qEnvironmentVariable("QT_HARMONYOS_ABILITY_NAME", u"QAbility"_s));
236 parser.addOption(abilityNameOpt);
237
238 QCommandLineOption hdcOpt(
239 u"hdc"_s,
240 u"Path to the hdc tool (env: QT_HARMONYOS_HDC)"_s,
241 u"path"_s,
242 qEnvironmentVariable("QT_HARMONYOS_HDC", u"hdc"_s));
243 parser.addOption(hdcOpt);
244
245 QCommandLineOption timeoutOpt(
246 u"timeout"_s,
247 u"Seconds to wait for a test to complete before aborting (env: QT_HARMONYOS_TEST_TIMEOUT)"_s,
248 u"seconds"_s,
249 qEnvironmentVariable("QT_HARMONYOS_TEST_TIMEOUT", u"300"_s));
250 parser.addOption(timeoutOpt);
251
252 QCommandLineOption noProgressTimeoutOpt(
253 u"no-progress-timeout"_s,
254 u"Seconds without a PASS/FAIL test case result before declaring the test hung "
255 u"(env: QT_HARMONYOS_NO_PROGRESS_TIMEOUT, 0 = disabled)"_s,
256 u"seconds"_s,
257 qEnvironmentVariable("QT_HARMONYOS_NO_PROGRESS_TIMEOUT", u"60"_s));
258 parser.addOption(noProgressTimeoutOpt);
259
260 QCommandLineOption deviceOpt(
261 u"device"_s,
262 u"hdc connect key (-t) for the target device — required when multiple devices "
263 u"are attached (env: QT_HARMONYOS_DEVICE)"_s,
264 u"key"_s,
265 qEnvironmentVariable("QT_HARMONYOS_DEVICE"));
266 parser.addOption(deviceOpt);
267
268 parser.process(app);
269
270 g_hdcConnectKey = parser.value(deviceOpt);
271
272 const QStringList positional = parser.positionalArguments();
273 if (positional.isEmpty()) {
274 fprintf(stderr, "harmonyostestrunner: no test binary specified\n");
275 parser.showHelp(EXIT_ERROR);
276 }
277
278 const QString testBinaryPath = positional.first();
279 const QString testLibName = QFileInfo(testBinaryPath).fileName();
280 // Extra positionals (test function names, -v2, etc.) forwarded to the test.
281 const QStringList testArgs = positional.mid(1);
282 const QString bundleName = parser.value(bundleNameOpt);
283 const QString abilityName = parser.value(abilityNameOpt);
284 const QString hdc = parser.value(hdcOpt);
285 const int timeoutSecs = parser.value(timeoutOpt).toInt();
286 const int noProgressTimeoutSecs = parser.value(noProgressTimeoutOpt).toInt();
287
288 // Unique per-run ID: shell can't delete files from the app sandbox (SELinux),
289 // so uniqueness is the only way to avoid matching stale files.
290 const QString runId = QString::number(QDateTime::currentMSecsSinceEpoch());
291
292 const QString appBase = u"/data/storage/el2/base/files"_s;
293 const QString appStdoutPath = appBase + u"/qt_stdout_"_s + runId + u".txt"_s;
294 const QString appExitCodePath = appBase + u"/qt_exitcode_"_s + runId + u".txt"_s;
295
296 const QString shellBase = u"/data/app/el2/100/base/"_s + bundleName + u"/files"_s;
297 const QString shellStdoutPath = shellBase + u"/qt_stdout_"_s + runId + u".txt"_s;
298 const QString shellExitCodePath = shellBase + u"/qt_exitcode_"_s + runId + u".txt"_s;
299
300 const QString bundleCheckOutput =
301 runHdc(hdc, {u"shell"_s, u"bm"_s, u"dump"_s, u"-n"_s, bundleName});
302 if (bundleCheckOutput.contains(u"error"_s, Qt::CaseInsensitive)
303 || bundleCheckOutput.trimmed().isEmpty()) {
304 fprintf(stderr,
305 "harmonyostestrunner: bundle '%s' is not installed on the device.\n"
306 " Build and sign the test HAP, then install it with:\n"
307 " hdc install <path/to/autotests-signed.hap>\n",
308 qPrintable(bundleName));
309 return EXIT_ERROR;
310 }
311
312#if QT_CONFIG(systemsemaphore)
313 g_runnerLock.acquire();
314#endif
315
316 runHdc(hdc, {u"shell"_s, u"aa"_s, u"force-stop"_s, bundleName});
317 waitForProcessDeath(hdc, bundleName);
318
319 // --ps for string want.parameters, --pb for boolean. Do NOT use -e: that
320 // adds entities (no values) and makes QTest see an empty argv[1].
321 QStringList aaStartArgs = {
322 u"shell"_s, u"aa"_s, u"start"_s,
323 u"-b"_s, bundleName,
324 u"-a"_s, abilityName,
325 u"--ps"_s, u"io.qt.appSharedLibNameOverride"_s, testLibName,
326 u"--ps"_s, u"io.qt.debug.redirectedStdoutPath"_s, appStdoutPath,
327 u"--ps"_s, u"io.qt.debug.exitCodePath"_s, appExitCodePath,
328 // Without this the platform pushes an empty argv[1], which QTest reads
329 // as a test function name and fails with "Function not found: ".
330 u"--pb"_s, u"io.qt.useUriAsArg"_s, u"false"_s,
331 // Keep main alive across window destroy/create for visual tests.
332 u"--pb"_s, u"io.qt.useDefaultUiAbilityInstanceInQt"_s, u"false"_s,
333 };
334
335 aaStartArgs << u"--pb"_s << u"io.qt.watchdogEnabled"_s << u"false"_s;
336
337 if (!testArgs.isEmpty()) {
338 QString json = u"["_s;
339 for (int i = 0; i < testArgs.size(); ++i) {
340 if (i > 0)
341 json += u","_s;
342 QString escaped = testArgs.at(i);
343 escaped.replace(u'\\', u"\\\\"_s);
344 escaped.replace(u'"', u"\\\""_s);
345 json += u"\""_s + escaped + u"\""_s;
346 }
347 json += u"]"_s;
348 aaStartArgs += {u"--ps"_s, u"io.qt.appArgsJson"_s, json};
349 }
350
351 // aa start prints errors (screen locked, ability not found, ...) to stdout.
352 {
353 const QString aaOut = runHdc(hdc, aaStartArgs, /*printOnFailure=*/true);
354 if (aaOut.contains(u"error"_s, Qt::CaseInsensitive)
355 || aaOut.contains(u"failed"_s, Qt::CaseInsensitive)) {
356 fprintf(stderr, "harmonyostestrunner: aa start: %s\n", qPrintable(aaOut.trimmed()));
357 }
358 }
359
360 if (!waitForProcessStart(hdc, bundleName, shellExitCodePath)) {
361 fprintf(stderr, "harmonyostestrunner: %s: timed out waiting for process to start\n",
362 qPrintable(testLibName));
363#if QT_CONFIG(systemsemaphore)
364 g_runnerLock.release();
365#endif
366 return EXIT_ERROR;
367 }
368
369 waitForStdoutFile(hdc, shellStdoutPath);
370
371 QProcess stdoutLogger;
372 const bool hasStdoutLogger = setupStdoutLogger(stdoutLogger, hdc, shellStdoutPath);
373 if (!hasStdoutLogger) {
374 fprintf(stderr, "harmonyostestrunner: warning: failed to start stdout logger, "
375 "output may be delayed\n");
376 }
377
378 // hdc shell always returns 0; can't use `test -f`. Cat the file and check
379 // whether the contents parse as int.
380 const int pollIntervalMs = 500;
381
382 QElapsedTimer elapsed;
383 elapsed.start();
384 int testExitCode = -1;
385 bool completed = false;
386 int aliveCheckCounter = 0;
387 qint64 lastTestProgressAt = -1;
388 qint64 lastHeartbeatSecs = 0;
389
390 while (!g_interrupted.load()
391 && elapsed.elapsed() < qint64(timeoutSecs) * 1000)
392 {
393 if (hasStdoutLogger) {
394 const QByteArray chunk = stdoutLogger.readAllStandardOutput();
395 if (!chunk.isEmpty()) {
396 fwrite(chunk.constData(), 1, static_cast<size_t>(chunk.size()), stdout);
397 fflush(stdout);
398 // Markers match QPlainTestLogger output in qtestlog.cpp. If
399 // QTest's format ever changes, the no-progress watchdog silently
400 // stops firing — hung tests only trip the overall timeout.
401 if (chunk.contains("PASS :") || chunk.contains("FAIL! :")
402 || chunk.contains("Totals:"))
403 lastTestProgressAt = elapsed.elapsed();
404 }
405 }
406
407 // Primary completion signal: exit-code file becomes parseable as int.
408 {
409 const QString exitContent =
410 runHdc(hdc, {u"shell"_s, u"cat"_s, shellExitCodePath});
411 bool ok = false;
412 const int code = exitContent.trimmed().toInt(&ok);
413 if (ok) {
414 testExitCode = code;
415 completed = true;
416 break;
417 }
418 }
419
420 // Liveness check every ~1.5 s. Re-read the exit-code file to cover the
421 // race where the process exits cleanly between checks.
422 if (++aliveCheckCounter % 3 == 0 && !isProcessAlive(hdc, bundleName)) {
423 const QString exitContent =
424 runHdc(hdc, {u"shell"_s, u"cat"_s, 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 fprintf(stderr, "harmonyostestrunner: %s: process exited without writing "
434 "exit code — likely crashed\n", qPrintable(testLibName));
435 testExitCode = EXIT_NOEXITCODE;
436 completed = true;
437 break;
438 }
439
440 if (noProgressTimeoutSecs > 0 && lastTestProgressAt >= 0
441 && elapsed.elapsed() - lastTestProgressAt
442 > qint64(noProgressTimeoutSecs) * 1000)
443 {
444 fprintf(stderr,
445 "harmonyostestrunner: %s: no test case progress for %d seconds "
446 "— main thread likely deadlocked, force-stopping\n",
447 qPrintable(testLibName), noProgressTimeoutSecs);
448 runHdc(hdc, {u"shell"_s, u"aa"_s, u"force-stop"_s, bundleName});
449 testExitCode = EXIT_CRASH;
450 completed = true;
451 break;
452 }
453
454 const qint64 elapsedSecs = elapsed.elapsed() / 1000;
455 if (elapsedSecs >= lastHeartbeatSecs + 30) {
456 fprintf(stderr, "harmonyostestrunner: %s still running (%lld s elapsed)\n",
457 qPrintable(testLibName), static_cast<long long>(elapsedSecs));
458 lastHeartbeatSecs = elapsedSecs;
459 }
460
461 if (hasStdoutLogger)
462 stdoutLogger.waitForReadyRead(pollIntervalMs);
463 else
464 QThread::msleep(pollIntervalMs);
465 }
466
467 if (hasStdoutLogger) {
468 if (stdoutLogger.state() != QProcess::NotRunning) {
469 // Drain bytes still in flight after the test finished — the 100 ms
470 // device-side loop and hdc transport both add latency.
471 while (stdoutLogger.waitForReadyRead(200)) {
472 const QByteArray chunk = stdoutLogger.readAllStandardOutput();
473 if (chunk.isEmpty())
474 break;
475 fwrite(chunk.constData(), 1, static_cast<size_t>(chunk.size()), stdout);
476 fflush(stdout);
477 }
478 stdoutLogger.terminate();
479 stdoutLogger.waitForFinished(5000);
480 }
481 const QByteArray finalChunk = stdoutLogger.readAllStandardOutput();
482 if (!finalChunk.isEmpty()) {
483 fwrite(finalChunk.constData(), 1, static_cast<size_t>(finalChunk.size()), stdout);
484 fflush(stdout);
485 }
486 }
487
488 if (!completed) {
489 if (g_interrupted.load()) {
490 runHdc(hdc, {u"shell"_s, u"aa"_s, u"force-stop"_s, bundleName});
491#if QT_CONFIG(systemsemaphore)
492 g_runnerLock.release();
493#endif
494 return EXIT_ERROR;
495 }
496 fprintf(stderr,
497 "harmonyostestrunner: TIMEOUT — %s did not complete within %d seconds\n",
498 qPrintable(testLibName), timeoutSecs);
499 runHdc(hdc, {u"shell"_s, u"aa"_s, u"force-stop"_s, bundleName});
500#if QT_CONFIG(systemsemaphore)
501 g_runnerLock.release();
502#endif
503 return EXIT_TIMEOUT;
504 }
505
506#if QT_CONFIG(systemsemaphore)
507 g_runnerLock.release();
508#endif
509 return testExitCode;
510}
static constexpr int EXIT_NOEXITCODE
Definition main.cpp:38
static constexpr int EXIT_ERROR
Definition main.cpp:36
static QString runHdc(const QString &hdcPath, const QStringList &args, bool printOnFailure=false)
Definition main.cpp:36
static bool setupStdoutLogger(QProcess &stdoutLogger, const QString &hdc, const QString &shellStdoutPath)
Definition main.cpp:184
static constexpr int EXIT_CRASH
Definition main.cpp:30
static bool isProcessAlive(const QString &hdc, const QString &bundleName)
Definition main.cpp:121
static std::atomic< bool > g_interrupted
Definition main.cpp:108
static void sigHandler(int sig)
Definition main.cpp:110
static bool waitForStdoutFile(const QString &hdc, const QString &shellStdoutPath, int timeoutSecs=10)
Definition main.cpp:165
static constexpr int EXIT_TIMEOUT
Definition main.cpp:31
static QString g_hdcConnectKey
Definition main.cpp:34
static bool waitForProcessStart(const QString &hdc, const QString &bundleName, const QString &shellExitCodePath, int timeoutSecs=30)
Definition main.cpp:144
static void waitForProcessDeath(const QString &hdc, const QString &bundleName, int timeoutSecs=15)
Definition main.cpp:128
int main(int argc, char *argv[])
[ctor_close]