Skip to content

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:

VariableValuePurpose
QT_QPA_PLATFORMoffscreenHeadless rendering for QWidget apps, no X11/Wayland needed
VERICUE_TEST_APPpath to your app binaryTells the pytest fixtures what to spawn
MYAPP_VERICUE_TOKENa secretAuth 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:

bash
sudo apt-get install -y xvfb libxkbcommon-x11-0 libgl1
export QT_QPA_PLATFORM=xcb
xvfb-run -a pytest tests/ui -v

Software 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_APP with QT_QPA_PLATFORM=offscreen, passes --port 0 and reads the actual port from the app's VERICUE_PORT=<port> stdout line. Yields the port; terminates the app on module teardown.

  • vericue_client - a connected, handshaken VeriCueClient against that app. Disconnects on test teardown. This is the one you want:

    python
    async 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():

cpp
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:

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.

yaml
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

yaml
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.

Released under a commercial licence. Privacy · Terms