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