Skip to content

Commit 34448e5

Browse files
authored
Merge pull request #482 from Integration-Automation/feat/cross-platform-verification
Verify the platforms this project claims, and support more of them
2 parents a55c060 + f52662c commit 34448e5

107 files changed

Lines changed: 9808 additions & 1245 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/docker.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,54 @@ jobs:
241241
- name: Verify the portal handshake against liboeffis
242242
run: docker run --rm autocontrol-portal:ci
243243

244+
x11-verification:
245+
name: X11 backend against a real X server
246+
needs: build-image
247+
runs-on: ubuntu-22.04
248+
249+
steps:
250+
- uses: actions/checkout@v4
251+
252+
- name: Set up Docker Buildx
253+
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
254+
uses: docker/setup-buildx-action@v3 # NOSONAR githubactions:S7637
255+
256+
- name: Build the X11 verification image
257+
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
258+
uses: docker/build-push-action@v5 # NOSONAR githubactions:S7637
259+
with:
260+
context: .
261+
file: docker/Dockerfile.x11
262+
tags: autocontrol-x11:ci
263+
load: true
264+
cache-from: type=gha
265+
cache-to: type=gha,mode=max
266+
267+
# Wayland ended up with five jobs that read back what reached a real
268+
# peer. X11 — the older and more widely deployed of the two Linux
269+
# paths — had none: every X11 assertion in the suite is made against a
270+
# mock of python-Xlib, so nothing had confirmed that an injected event
271+
# reaches a client at all.
272+
#
273+
# Ground truth deliberately comes from other codebases than the one
274+
# under test. xev is a real X client that prints every event delivered
275+
# to its window, so a click is read back the way the ydotool job reads
276+
# its events off /dev/input/eventN — including `synthetic NO`, which is
277+
# what separates real server input from XSendEvent traffic that
278+
# toolkits discard. ImageMagick's `import` is an independent grabber,
279+
# in the role grim plays for Wayland, against a root window painted two
280+
# asymmetric colours so a wrong rectangle cannot look right.
281+
#
282+
# It runs twice: one monitor over the whole screen, then two RANDR
283+
# monitors side by side. There is deliberately no negative-origin pass
284+
# — on X11 the root window is the union of every monitor and always
285+
# starts at (0, 0), so the Wayland job's second layout has no analogue
286+
# here. That is a protocol difference, not an untested case.
287+
#
288+
# The container exits with the number of failed checks.
289+
- name: Verify the X11 backend against a real X server
290+
run: docker run --rm autocontrol-x11:ci
291+
244292
seat-verification:
245293
name: ydotool absolute move against a seat that consumes it
246294
needs: build-image

.github/workflows/platform-smoke.yml

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,31 @@ jobs:
1414
strategy:
1515
fail-fast: false
1616
matrix:
17-
os: [windows-2022, ubuntu-22.04, macos-14]
17+
# arm64 is not a rounding error on the desktop any more, and the
18+
# dependency set is where it shows. macos-14 is already arm64;
19+
# ubuntu-22.04-arm adds Linux, and it passes.
20+
#
21+
# windows-11-arm is deliberately absent, and it was measured rather
22+
# than assumed. Two dependencies have no win_arm64 wheel, and both
23+
# have to go before the runner is worth adding back:
24+
#
25+
# opencv-python — no win_arm64 wheel in any version, so pip falls
26+
# back to building from source and CMake cannot
27+
# configure for ARM64. Twelve minutes, then failure.
28+
# cryptography — wheels stop at 46.0.3; 46.0.4 onwards ship none.
29+
# Our floor is >=48.0.1 and that is a security floor
30+
# (GHSA-537c-gmf6-5ccf), so it cannot be lowered.
31+
#
32+
# Neither is a CI problem to work around — the package genuinely
33+
# cannot be installed on Windows arm64 today. Re-check without a
34+
# runner, in about ten seconds:
35+
#
36+
# pip install --dry-run --only-binary=:all: --platform win_arm64 \
37+
# --python-version 3.12 --target /tmp/probe \
38+
# 'opencv-python>=4.8,<6' 'cryptography>=48.0.1'
39+
#
40+
# Recorded in Progress.md; add the runner back when both resolve.
41+
os: [windows-2022, ubuntu-22.04, macos-14, ubuntu-22.04-arm]
1842
python-version: ["3.10", "3.14"]
1943
runs-on: ${{ matrix.os }}
2044
steps:
@@ -49,3 +73,118 @@ jobs:
4973
name: platform-smoke-${{ matrix.os }}-${{ matrix.python-version }}
5074
path: platform-smoke.zip
5175
if-no-files-found: warn
76+
77+
freebsd:
78+
name: The X11 backend driving input on a real FreeBSD
79+
runs-on: ubuntu-22.04
80+
timeout-minutes: 30
81+
82+
steps:
83+
- uses: actions/checkout@v4
84+
85+
# The X11 backend was gated on sys.platform being linux/linux2, so it
86+
# refused to load on a FreeBSD desktop that runs the same X server, the
87+
# same python-Xlib and the same code. Relaxing that guard is only worth
88+
# something if a BSD actually runs it, and no hosted runner is one — so
89+
# this boots a real FreeBSD VM inside the runner.
90+
#
91+
# For a while it could only check the *decision*, because importing
92+
# anything under je_auto_control ran the facade and the facade imported
93+
# OpenCV and cryptography at module scope. Neither publishes a FreeBSD
94+
# wheel and building them from ports had not finished after fifty
95+
# minutes, so utils/platform_id was loaded by file path and the backend
96+
# itself went untested.
97+
#
98+
# That was the wrong thing to work around. Moving a mouse needs neither
99+
# package, and the facade no longer insists on them — they are imported
100+
# by the functions that use them, which test_facade_import_is_light.py
101+
# keeps true. What is left for this VM is python-Xlib and defusedxml,
102+
# both pure Python, plus an X server. So the whole backend runs here now
103+
# and the reads come back off the server itself: query_pointer for the
104+
# cursor, its button mask for the buttons, query_keymap for the keys.
105+
# nosemgrep: yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha.third-party-action-not-pinned-to-commit-sha
106+
- uses: vmactions/freebsd-vm@v1 # NOSONAR githubactions:S7637
107+
with:
108+
release: "14.2"
109+
usesh: true
110+
prepare: |
111+
pkg install -y python311 xorg-vfbserver
112+
run: |
113+
set -eu
114+
echo "uname: $(uname -a)"
115+
116+
# pip comes from ensurepip, not from pkg: FreeBSD 14.2's repository
117+
# has no py311-pip (the flavoured port names are not dependable
118+
# here, while python311 itself is). These are the only dependencies
119+
# the facade still needs, all pure Python, at the versions
120+
# pyproject pins and by their PyPI names.
121+
#
122+
# --no-deps is the point of this job rather than a detail: it is
123+
# what proves nothing heavy is being dragged in behind the
124+
# verification. six is therefore named explicitly — python-Xlib
125+
# 0.33 imports it from Xlib.display, and with --no-deps nothing
126+
# else would install it.
127+
#
128+
# py311-sqlite3 is deliberately not installed either. FreeBSD
129+
# packages sqlite3 apart from python311, this VM is the only
130+
# machine in CI that does, and it is what caught ten subsystems
131+
# importing it at module scope — which made `import
132+
# je_auto_control` fail outright on a stock FreeBSD. Adding the
133+
# package here would make that regression invisible again.
134+
python3.11 -m ensurepip --upgrade
135+
python3.11 -m pip install --no-deps \
136+
python-xlib==0.33 six defusedxml==0.7.1
137+
138+
# The backend connects to a display at import time, so the server
139+
# has to be up first. 1280x1024 because the verification drives the
140+
# cursor to the far corner and reads it back.
141+
Xvfb :99 -screen 0 1280x1024x24 &
142+
xvfb_pid=$!
143+
trap 'kill "$xvfb_pid" 2>/dev/null || true' EXIT
144+
waited=0
145+
while [ ! -e /tmp/.X11-unix/X99 ]; do
146+
waited=$((waited + 1))
147+
if [ "$waited" -gt 100 ]; then
148+
echo "Xvfb never created /tmp/.X11-unix/X99" >&2
149+
exit 1
150+
fi
151+
sleep 0.1
152+
done
153+
154+
DISPLAY=:99 PYTHONPATH="$(pwd)" python3.11 test/verify/freebsd_verify.py
155+
macos-capabilities:
156+
name: What a real macOS runner permits
157+
runs-on: macos-14
158+
159+
steps:
160+
- uses: actions/checkout@v4
161+
162+
- uses: actions/setup-python@v5
163+
with:
164+
python-version: "3.12"
165+
166+
- run: python -m pip install -e . # NOSONAR githubactions:S8544 # reason: installs the checked-out project itself, there is no upstream version to lock
167+
168+
# macOS is the one supported platform with no container to put it in,
169+
# and every macOS row in docs/CAPABILITY_MATRIX.md said
170+
# "implementation": the code was there and nothing had run it on a Mac.
171+
#
172+
# Two of these capabilities are gated by TCC — macOS asks a *user* to
173+
# grant Screen Recording and Accessibility, and a CI runner has no user
174+
# to ask. Which of them a runner grants is not something to guess at,
175+
# and guessing is how the Wayland work twice recorded a desktop's
176+
# refusal as a container's limitation.
177+
#
178+
# Measured first, in --measure mode, and the answer was a surprise: a
179+
# macos-14 runner grants BOTH Screen Recording and Accessibility, so
180+
# every capability works — capture returns real pixels rather than the
181+
# black rectangle a refusal produces, CGEventPost moves the cursor and
182+
# the move reads back exactly, and the AX walk returns real elements.
183+
# The usual assumption that CI cannot exercise a TCC-gated macOS API is
184+
# simply wrong for this runner.
185+
#
186+
# So the flag is off and this is a gate now: EXPECTED in the script
187+
# holds what was measured, and a capability appearing or disappearing
188+
# turns this red and names which one.
189+
- name: Verify the macOS backend against a real window server
190+
run: python test/verify/macos_verify.py

.github/workflows/quality.yml

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,26 @@ jobs:
6060
run: bandit -r je_auto_control/ -c pyproject.toml
6161

6262
pytest-headless:
63-
runs-on: windows-2022
63+
# The suite ran on Windows alone for its whole life, so every
64+
# platform assumption it holds went unmeasured on the two operating
65+
# systems the project also claims to support. Linux and macOS are
66+
# here to measure them; they carry the two ends of the supported
67+
# Python range rather than all five, because what differs between
68+
# 3.10 and 3.14 is Python and what differs here is the OS.
69+
runs-on: ${{ matrix.os }}
6470
strategy:
6571
fail-fast: false
6672
matrix:
67-
python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ]
73+
include:
74+
- { os: windows-2022, python-version: "3.10" }
75+
- { os: windows-2022, python-version: "3.11" }
76+
- { os: windows-2022, python-version: "3.12" }
77+
- { os: windows-2022, python-version: "3.13" }
78+
- { os: windows-2022, python-version: "3.14" }
79+
- { os: ubuntu-22.04, python-version: "3.10" }
80+
- { os: ubuntu-22.04, python-version: "3.14" }
81+
- { os: macos-14, python-version: "3.10" }
82+
- { os: macos-14, python-version: "3.14" }
6883
steps:
6984
- uses: actions/checkout@v4
7085

@@ -74,33 +89,66 @@ jobs:
7489
python-version: ${{ matrix.python-version }}
7590
cache: "pip"
7691

77-
- name: Install dependencies
92+
# Same set the container image installs, and for the same reasons:
93+
# the X11 backend connects to a display at import time, opencv and
94+
# PySide6 hard-require libGL/glib, and Qt's platform plugin needs
95+
# the xcb libraries. Without these the suite fails at collection
96+
# with a linker error rather than a test result.
97+
- name: Install X11 and Qt runtime libraries (Linux)
98+
if: runner.os == 'Linux'
7899
run: |
79-
python -m pip install --upgrade pip wheel
80-
# Install the editable package FIRST so its source dir is the
81-
# one Python sees on subsequent imports. We deliberately
82-
# avoid `pip install -r dev_requirements.txt` here because
83-
# that file pulls in `je_auto_control_dev` (a separate PyPI
84-
# package), which ships its own snapshot of `je_auto_control/`
85-
# straight into site-packages and masks the editable install
86-
# for any sub-package the snapshot doesn't include
87-
# (admin, usb, remote_desktop, vision, …).
88-
pip install -e .
89-
pip install --only-binary :all: ruff==0.15.22 bandit==1.9.4 pytest==9.1.1 pytest-timeout==2.4.0 pytest-rerunfailures==15.1 pytest-cov==7.0.0 PySide6==6.11.1
100+
sudo apt-get update
101+
sudo apt-get install -y --no-install-recommends \
102+
xvfb xauth x11-utils \
103+
libgl1 libegl1 libglib2.0-0 \
104+
libxkbcommon-x11-0 libdbus-1-3 \
105+
libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 \
106+
libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxcb-sync1 \
107+
libxcb-xfixes0 libxcb-xinerama0 libxcb-xkb1
108+
109+
# One command per step, each on a single line: a `run: |` block scalar
110+
# swallows NOSONAR markers, so a justification inside one is a comment
111+
# that reads as handled and suppresses nothing.
112+
- name: Upgrade the installer
113+
shell: bash
114+
run: python -m pip install --upgrade pip wheel # NOSONAR githubactions:S8544 # reason: pip and wheel are the installer; pinning them here would pin the tool that applies the pins below
115+
116+
# Install the editable package FIRST so its source dir is the one
117+
# Python sees on subsequent imports. We deliberately avoid
118+
# `pip install -r dev_requirements.txt` here because that file pulls in
119+
# `je_auto_control_dev` (a separate PyPI package), which ships its own
120+
# snapshot of `je_auto_control/` straight into site-packages and masks
121+
# the editable install for any sub-package the snapshot doesn't include
122+
# (admin, usb, remote_desktop, vision, …).
123+
- name: Install the project itself
124+
shell: bash
125+
run: pip install -e . # NOSONAR githubactions:S8544 githubactions:S8541 # reason: installs the checked-out project itself, so there is no upstream version to lock and no third-party setup script to run
126+
127+
- name: Install the test tooling
128+
shell: bash
129+
# Quoted: `--only-binary :all:` puts a colon-space inside the
130+
# scalar, which YAML reads as a mapping and refuses.
131+
run: "pip install --only-binary :all: ruff==0.15.22 bandit==1.9.4 pytest==9.1.1 pytest-timeout==2.4.0 pytest-rerunfailures==15.1 pytest-cov==7.0.0 PySide6==6.11.1"
90132

91133
# Paths come from `testpaths` in pyproject.toml. Do NOT pass an explicit
92134
# path here: an argument overrides testpaths, which previously meant the
93135
# flow_control tests were configured to run but silently never did.
136+
#
137+
# Linux runs under a real Xvfb rather than Qt's offscreen platform:
138+
# the X11 backend opens a display at import time, and offscreen
139+
# would hide exactly the breakage this job exists to find.
94140
- name: Run headless pytest suite
141+
shell: bash
95142
run: >-
143+
${{ runner.os == 'Linux' && 'xvfb-run -a -s "-screen 0 1280x800x24"' || '' }}
96144
pytest -v --tb=short --timeout=120
97145
--cov=je_auto_control --cov-report=term-missing
98146
--cov-report=xml --cov-fail-under=35
99147
100148
- name: Upload coverage report
101149
uses: actions/upload-artifact@v4
102150
with:
103-
name: coverage-${{ matrix.python-version }}
151+
name: coverage-${{ matrix.os }}-${{ matrix.python-version }}
104152
path: coverage.xml
105153

106154
typing-stable-api:

0 commit comments

Comments
 (0)