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