CI integration
Most veriCue tests run headless with no extra work: Qt's offscreen platform plugin renders without a display server, and the pytest fixtures spawn your instrumented app automatically.
The essentials
Three environment variables cover most CI setups:
| Variable | Value | Purpose |
|---|---|---|
QT_QPA_PLATFORM | offscreen | Headless rendering for QWidget apps, no X11/Wayland needed |
VERICUE_TEST_APP | path to your app binary | Tells the pytest fixtures what to spawn |
MYAPP_VERICUE_TOKEN | a secret | Auth token, if your app sets one |
For a QWidget application, screenshots (screenshot, screenshot_compare) work fine offscreen - widgets render into the offscreen buffer.
offscreen is not universal - Qt Quick needs a GL context
The offscreen platform plugin cannot obtain an OpenGL context on a machine with no display server, so a Qt Quick / QML application typically fails to start (or renders nothing) under QT_QPA_PLATFORM=offscreen. The reliable recipe on a headless Linux runner is a virtual X server plus the normal xcb plugin:
sudo apt-get install -y xvfb libxkbcommon-x11-0 libgl1
export QT_QPA_PLATFORM=xcb
xvfb-run -a pytest tests/ui -vSoftware rasterization (LIBGL_ALWAYS_SOFTWARE=1, or Qt 6's QSG_RHI_BACKEND=software for scenes that do not need real GL) makes that path work on runners with no GPU. Treat offscreen as the default for QWidget suites and validate it for your own application before relying on it - the same caveat applies to any widget hosting a QOpenGLWidget.
Pytest fixtures
Installing the Python client (pip install vericue) auto-registers a pytest plugin with three fixtures:
vericue_test_app(module-scoped) - spawns the binary at$VERICUE_TEST_APPwithQT_QPA_PLATFORM=offscreen, passes--port 0and reads the actual port from the app'sVERICUE_PORT=<port>stdout line. Yields the port; terminates the app on module teardown.vericue_client- a connected, handshakenVeriCueClientagainst that app. Disconnects on test teardown. This is the one you want:pythonasync def test_login(vericue_client): await vericue_client.type_text("MainWindow/userField", "alice") await vericue_client.mouse_click("MainWindow/loginButton") props = await vericue_client.get_properties("MainWindow/statusLabel", ["text"]) assert props["text"] == "Logged in"vericue_client_factory- for tests needing several concurrent connections (e.g. multi-client scenarios).
For the port-discovery handoff to work in your own app, print the port after start():
printf("VERICUE_PORT=%d\n", server.serverPort());Transport in CI
The bundled fixtures use TCP on loopback with an OS-assigned port. That is deliberate: it needs no shared filesystem convention and it is the one code path that also works on Windows runners.
If your rig prefers local IPC - Linux and macOS only - start the app with startLocal(), print VERICUE_ENDPOINT=<path> the same way, and connect with connect_local() in your own fixture. Two things to watch on a runner: $XDG_RUNTIME_DIR is often unset in containers (veriCue then falls back to <tmp>/vericue-<uid>), and a deep workspace path can exceed the 107-byte UNIX-socket limit, so pass a short explicit endpoint if you hit that.
Reports
The plugin writes an HTML report and JUnit XML at session end. Configure via CLI (--vericue-report-html, --vericue-report-xml, --vericue-report-name) or pyproject.toml:
[tool.pytest.ini_options]
vericue_report_html = "reports/vericue.html"
vericue_report_xml = "reports/vericue.xml"
vericue_report_name = "MyApp UI suite"Most CI systems ingest the JUnit XML natively (GitHub checks, GitLab reports: junit:, Jenkins).
GitHub Actions
A complete workflow: build the app with veriCue enabled, run the UI suite headless, publish reports. This one assumes a QWidget application; for QML, swap QT_QPA_PLATFORM: offscreen for the xvfb-run + xcb recipe above.
name: ui-tests
on: [push, pull_request]
jobs:
ui-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
version: '6.7.*'
- name: Build app (veriCue embedded)
run: |
cmake -B build -DCMAKE_BUILD_TYPE=Release -DENABLE_VERICUE=ON
cmake --build build --parallel
- name: Install test deps
run: pip install vericue pytest pytest-asyncio
- name: Run UI tests
env:
QT_QPA_PLATFORM: offscreen
VERICUE_TEST_APP: build/my-app
run: |
pytest tests/ui -v \
--vericue-report-html reports/vericue.html \
--vericue-report-xml reports/vericue.xml
- name: Upload reports
if: always()
uses: actions/upload-artifact@v4
with:
name: ui-test-reports
path: reports/GitLab CI
ui-tests:
image: your-qt-build-image
script:
- cmake -B build -DENABLE_VERICUE=ON && cmake --build build --parallel
- pip install vericue pytest pytest-asyncio
- export QT_QPA_PLATFORM=offscreen
- export VERICUE_TEST_APP=build/my-app
- pytest tests/ui -v --vericue-report-xml reports/vericue.xml
artifacts:
when: always
reports:
junit: reports/vericue.xml
paths:
- reports/Parallel jobs
Because vericue_test_app starts the app with --port 0 (OS-assigned port), any number of jobs or pytest-xdist workers can run on the same machine without port collisions - each spawned app instance gets its own port and its own client connection.
Concurrent automation sessions in CI
With an organization key file (or trial), a session is one live client connection - it frees as soon as the connection closes. With floating licensing, a session is one running app process: each instrumented app checks out a lease at its first start()/startLocal() and returns it on exit, whether it serves one transport or both. Either way, a typical CI job - one app instance, one client - uses one session at a time. Size floating pools to the maximum number of simultaneous jobs, not the number of tests.

