Merge pull request #5173 from Alexays/ci/smoke-plus
ci(smoke): sanitizer build + module/interaction/a11y/layout coverage
This commit is contained in:
+69
-28
@@ -1,15 +1,31 @@
|
|||||||
name: smoke
|
name: smoke
|
||||||
|
|
||||||
# Launch the real Waybar binary inside a headless sway compositor and verify it
|
# Launch the real Waybar binary (built with AddressSanitizer) inside a headless
|
||||||
# starts, renders, and matches a reference screenshot. Complements the unit
|
# compositor and exercise it: render, per-module matrix, pointer interaction,
|
||||||
|
# accessibility-tree assertions, and a layout matrix. Complements the unit
|
||||||
# tests and build jobs, which never actually run the bar.
|
# tests and build jobs, which never actually run the bar.
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
bless:
|
||||||
|
description: "Regenerate and commit the golden reference screenshot"
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
WAYBAR_BIN: ${{ github.workspace }}/build/waybar
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
smoke:
|
smoke:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -20,7 +36,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install --no-install-recommends -y \
|
sudo apt-get install --no-install-recommends -y \
|
||||||
sway grim imagemagick fonts-dejavu-core \
|
sway labwc grim imagemagick fonts-dejavu-core jq \
|
||||||
|
at-spi2-core python3-pyatspi dbus-x11 \
|
||||||
meson ninja-build g++ pkg-config scdoc wayland-protocols \
|
meson ninja-build g++ pkg-config scdoc wayland-protocols \
|
||||||
gobject-introspection libdbusmenu-gtk3-dev libevdev-dev libfmt-dev \
|
gobject-introspection libdbusmenu-gtk3-dev libevdev-dev libfmt-dev \
|
||||||
libgirepository1.0-dev libgtk-3-dev libgtkmm-3.0-dev libinput-dev \
|
libgirepository1.0-dev libgtk-3-dev libgtkmm-3.0-dev libinput-dev \
|
||||||
@@ -28,53 +45,77 @@ jobs:
|
|||||||
libpulse-dev libsigc++-2.0-dev libspdlog-dev libwayland-dev \
|
libpulse-dev libsigc++-2.0-dev libspdlog-dev libwayland-dev \
|
||||||
upower libxkbregistry-dev libxkbcommon-dev
|
upower libxkbregistry-dev libxkbcommon-dev
|
||||||
|
|
||||||
- name: Build & install Waybar
|
- name: Build Waybar with AddressSanitizer
|
||||||
run: |
|
run: |
|
||||||
meson setup build -Dman-pages=disabled
|
meson setup build -Dman-pages=disabled -Db_sanitize=address
|
||||||
ninja -C build
|
ninja -C build
|
||||||
sudo ninja -C build install
|
|
||||||
|
|
||||||
# --- Level 1: smoke with real modules ---
|
# --- Tier 0 + 2: render + sanitizer/critical gate ---
|
||||||
- name: Smoke — real modules load without crashing
|
- name: Render deterministic config
|
||||||
run: ./test/smoke/run.sh test/smoke/modules-config.jsonc
|
|
||||||
|
|
||||||
# --- Level 1 + 2: launch deterministic config and screenshot ---
|
|
||||||
- name: Launch + screenshot (deterministic config)
|
|
||||||
run: ./test/smoke/run.sh test/smoke/config.jsonc test/smoke/style.css bar.png
|
run: ./test/smoke/run.sh test/smoke/config.jsonc test/smoke/style.css bar.png
|
||||||
|
|
||||||
# --- Level 2: the bar actually rendered something ---
|
|
||||||
- name: Check the bar is not blank
|
- name: Check the bar is not blank
|
||||||
run: |
|
run: |
|
||||||
sd=$(convert bar.png -crop 1920x30+0+0 +repage -colorspace Gray \
|
sd=$(convert bar.png -crop 1920x30+0+0 +repage -colorspace Gray \
|
||||||
-format '%[fx:standard_deviation]' info:)
|
-format '%[fx:standard_deviation]' info:)
|
||||||
echo "bar strip stddev = $sd"
|
echo "bar strip stddev = $sd"
|
||||||
awk "BEGIN{ exit !($sd > 0.01) }" || {
|
awk "BEGIN{ exit !($sd > 0.01) }" || { echo "::error::bar looks blank"; exit 1; }
|
||||||
echo "::error::the top bar strip looks blank — Waybar rendered nothing"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Level 3: golden comparison against the committed reference ---
|
# --- Manual re-bless of the reference image ---
|
||||||
- name: Compare against reference screenshot
|
- name: Bless reference screenshot
|
||||||
|
if: github.event_name == 'workflow_dispatch' && inputs.bless
|
||||||
|
run: |
|
||||||
|
cp bar.png test/smoke/reference.png
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add test/smoke/reference.png
|
||||||
|
git commit -m "ci(smoke): re-bless reference screenshot [skip ci]"
|
||||||
|
git push
|
||||||
|
|
||||||
|
- name: Golden comparison
|
||||||
run: |
|
run: |
|
||||||
if [ -f test/smoke/reference.png ]; then
|
if [ -f test/smoke/reference.png ]; then
|
||||||
ae=$(compare -metric AE -fuzz 8% test/smoke/reference.png bar.png diff.png 2>&1 || true)
|
ae=$(compare -metric AE -fuzz 8% test/smoke/reference.png bar.png diff.png 2>&1 || true)
|
||||||
ae=$(printf '%s' "$ae" | grep -oE '^[0-9]+' || echo 0)
|
ae=$(printf '%s' "$ae" | grep -oE '^[0-9]+' || echo 0)
|
||||||
echo "differing pixels (fuzz 8%): $ae"
|
echo "differing pixels (fuzz 8%): $ae"
|
||||||
if [ "$ae" -gt 800 ]; then
|
[ "$ae" -gt 800 ] && { echo "::error::visual regression: $ae px (see diff.png artifact)"; exit 1; } || echo "✓ within tolerance"
|
||||||
echo "::error::visual regression: $ae pixels differ from the reference (threshold 800). See the 'diff.png' artifact."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "✓ within tolerance"
|
|
||||||
else
|
else
|
||||||
echo "::warning::No reference image yet. Download the 'waybar-screenshot' artifact from this run and commit bar.png as test/smoke/reference.png to enable golden comparison."
|
echo "::warning::No reference image; run this workflow via 'Run workflow' with bless=true to create it."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload screenshots
|
# --- Tier 1: per-module matrix ---
|
||||||
|
- name: Per-module render matrix
|
||||||
|
run: ./test/smoke/modules.sh module-shots
|
||||||
|
|
||||||
|
# --- Tier 2: pointer interaction (best-effort) ---
|
||||||
|
# Headless seats may not expose a pointer capability, so injected clicks
|
||||||
|
# can be dropped; keep this non-blocking while it is hardened.
|
||||||
|
- name: Interaction (click + format-alt)
|
||||||
|
continue-on-error: true
|
||||||
|
run: ./test/smoke/interact.sh interaction-shots
|
||||||
|
|
||||||
|
# --- Tier 2: accessibility assertions ---
|
||||||
|
- name: Accessibility (AT-SPI) assertions
|
||||||
|
run: dbus-run-session -- ./test/smoke/a11y.sh
|
||||||
|
|
||||||
|
# --- Tier 3: layout matrix (positions + HiDPI) ---
|
||||||
|
- name: Layout matrix (top/bottom/left + scale 2)
|
||||||
|
run: ./test/smoke/positions.sh layout-shots
|
||||||
|
|
||||||
|
# --- Tier 3: second compositor ---
|
||||||
|
- name: Launch under labwc
|
||||||
|
run: COMPOSITOR=labwc ./test/smoke/run.sh test/smoke/config.jsonc test/smoke/style.css labwc.png
|
||||||
|
|
||||||
|
- name: Upload screenshots & diffs
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: waybar-screenshot
|
name: waybar-smoke
|
||||||
path: |
|
path: |
|
||||||
bar.png
|
bar.png
|
||||||
diff.png
|
diff.png
|
||||||
|
labwc.png
|
||||||
|
module-shots/
|
||||||
|
interaction-shots/
|
||||||
|
layout-shots/
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
|
|||||||
+35
-20
@@ -1,34 +1,49 @@
|
|||||||
# Smoke / screenshot test
|
# Smoke / render / interaction tests
|
||||||
|
|
||||||
The [`smoke`](../../.github/workflows/smoke.yml) workflow launches the real
|
The [`smoke`](../../.github/workflows/smoke.yml) workflow builds Waybar **with
|
||||||
Waybar binary inside a **headless sway** compositor (software-rendered, no GPU)
|
AddressSanitizer** and runs the real binary inside a **headless** compositor
|
||||||
and checks three things:
|
(software-rendered, no GPU) to exercise it end to end. All helpers share
|
||||||
|
[`lib.sh`](lib.sh).
|
||||||
|
|
||||||
1. **It starts and stays alive** — with real modules (`clock`, `cpu`, `memory`,
|
## What it checks
|
||||||
`disk`) loaded, Waybar must not crash or log a fatal error.
|
|
||||||
([`modules-config.jsonc`](modules-config.jsonc))
|
| Tier | Check | Script |
|
||||||
2. **It renders** — a deterministic config ([`config.jsonc`](config.jsonc) +
|
| --- | --- | --- |
|
||||||
[`style.css`](style.css)) is captured with `grim`; the bar strip must not be
|
| 0 | Launches, stays alive, and logs **no ASan / `Gtk-CRITICAL` / crash** — for every launch below | [`lib.sh`](lib.sh) `assert_clean` |
|
||||||
blank.
|
| 1 | **Renders** — deterministic config screenshotted with `grim`, bar strip not blank | [`run.sh`](run.sh) |
|
||||||
3. **It looks right** — the screenshot is compared to `reference.png` with an 8%
|
| 1 | **Per-module matrix** — each headless-safe module rendered in isolation | [`modules.sh`](modules.sh) |
|
||||||
fuzz and an 800-pixel tolerance. Regressions fail the job, and the screenshot
|
| 2 | **Golden** — screenshot vs [`reference.png`](reference.png) (fuzz 8%, 800px) | workflow |
|
||||||
and diff are uploaded as artifacts.
|
| 2 | **Interaction** — inject pointer clicks via sway; assert `on-click` side effect + `format-alt` toggle | [`interact.sh`](interact.sh) |
|
||||||
|
| 2 | **Accessibility** — assert module labels via the AT-SPI tree (robust to fonts) | [`a11y.sh`](a11y.sh) / [`a11y.py`](a11y.py) |
|
||||||
|
| 3 | **Layout matrix** — top / bottom / left (vertical) + HiDPI (scale 2) | [`positions.sh`](positions.sh) |
|
||||||
|
| 3 | **Second compositor** — same launch under `labwc` | workflow (`COMPOSITOR=labwc`) |
|
||||||
|
|
||||||
|
AddressSanitizer runs under *all* of the above, so any memory bug in the real
|
||||||
|
render path aborts the run. The interaction step is best-effort
|
||||||
|
(`continue-on-error`): a headless seat exposes no pointer capability, so injected
|
||||||
|
clicks may be dropped — hardening it needs a virtual pointer (uinput/wlr-virtual-pointer).
|
||||||
|
|
||||||
## Updating the reference image
|
## Updating the reference image
|
||||||
|
|
||||||
`reference.png` must be generated by the CI environment (local rendering differs
|
`reference.png` must be generated by the CI environment (local rendering differs
|
||||||
from Ubuntu's). To (re)bless it:
|
from Ubuntu's). Two ways:
|
||||||
|
|
||||||
1. Push your change and open the run of the `smoke` workflow.
|
- **One click:** Actions → *smoke* → **Run workflow** with `bless = true`. The
|
||||||
2. Download the `waybar-screenshot` artifact.
|
job regenerates the screenshot and commits it to your branch.
|
||||||
3. Commit its `bar.png` as `test/smoke/reference.png`.
|
- **Manual:** download the `waybar-smoke` artifact from a run and commit its
|
||||||
|
`bar.png` as `test/smoke/reference.png`.
|
||||||
|
|
||||||
Do this whenever an **intended** visual change makes the golden comparison fail.
|
Do this whenever an **intended** visual change trips the golden comparison.
|
||||||
|
|
||||||
## Run locally
|
## Run locally
|
||||||
|
|
||||||
On a machine with `sway`, `grim` and `waybar`:
|
On a machine with `sway`, `grim`, `imagemagick` and Waybar built:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./test/smoke/run.sh test/smoke/config.jsonc test/smoke/style.css /tmp/bar.png
|
export WAYBAR_BIN=$PWD/build/waybar
|
||||||
|
./test/smoke/run.sh test/smoke/config.jsonc test/smoke/style.css /tmp/bar.png
|
||||||
|
./test/smoke/modules.sh /tmp/module-shots
|
||||||
|
./test/smoke/interact.sh /tmp/interaction-shots
|
||||||
|
./test/smoke/positions.sh /tmp/layout-shots
|
||||||
|
dbus-run-session -- ./test/smoke/a11y.sh
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Tier 2 — assert on Waybar's accessibility (AT-SPI) tree instead of pixels.
|
||||||
|
|
||||||
|
Walks the running Waybar's GTK accessibility tree and checks that the expected
|
||||||
|
module labels are exposed. This is robust to fonts/anti-aliasing (unlike the
|
||||||
|
golden image) and verifies actual widget content.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pyatspi
|
||||||
|
|
||||||
|
EXPECT = ["Waybar", "CI smoke test", "12:34"]
|
||||||
|
|
||||||
|
|
||||||
|
def collect(acc, out):
|
||||||
|
try:
|
||||||
|
name = acc.name or ""
|
||||||
|
except Exception:
|
||||||
|
name = ""
|
||||||
|
if name:
|
||||||
|
out.append(name)
|
||||||
|
try:
|
||||||
|
count = acc.childCount
|
||||||
|
except Exception:
|
||||||
|
count = 0
|
||||||
|
for i in range(count):
|
||||||
|
try:
|
||||||
|
child = acc.getChildAtIndex(i)
|
||||||
|
except Exception:
|
||||||
|
child = None
|
||||||
|
if child is not None:
|
||||||
|
collect(child, out)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
desktop = pyatspi.Registry.getDesktop(0)
|
||||||
|
texts, found = [], False
|
||||||
|
for i in range(desktop.childCount):
|
||||||
|
app = desktop.getChildAtIndex(i)
|
||||||
|
if app is None:
|
||||||
|
continue
|
||||||
|
if (app.name or "").lower().startswith("waybar"):
|
||||||
|
found = True
|
||||||
|
collect(app, texts)
|
||||||
|
print("AT-SPI labels seen on waybar:", texts)
|
||||||
|
if not found:
|
||||||
|
print("ERROR: waybar did not appear on the AT-SPI bus")
|
||||||
|
return 1
|
||||||
|
missing = [e for e in EXPECT if not any(e in t for t in texts)]
|
||||||
|
if missing:
|
||||||
|
print("ERROR: expected labels missing from AT-SPI tree:", missing)
|
||||||
|
return 1
|
||||||
|
print("OK: all expected labels present via AT-SPI")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Tier 2 — accessibility assertions. Must run under a D-Bus session so the
|
||||||
|
# AT-SPI bus is available, e.g.: dbus-run-session -- test/smoke/a11y.sh
|
||||||
|
set -euo pipefail
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "$DIR/lib.sh"
|
||||||
|
|
||||||
|
# Make sure GTK registers with the AT-SPI bridge.
|
||||||
|
unset NO_AT_BRIDGE
|
||||||
|
export GTK_A11Y=1
|
||||||
|
|
||||||
|
smoke::setup
|
||||||
|
smoke::start_compositor
|
||||||
|
trap smoke::stop EXIT
|
||||||
|
smoke::launch_waybar "$DIR/config.jsonc" "$DIR/style.css" 6
|
||||||
|
smoke::assert_alive
|
||||||
|
python3 "$DIR/a11y.py"
|
||||||
Executable
+81
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Tier 2 — interaction: inject real pointer input via sway and assert on the
|
||||||
|
# behaviour (on-click side effect, format-alt toggle), not on pixels alone.
|
||||||
|
#
|
||||||
|
# Usage: interact.sh [screenshot-dir]
|
||||||
|
set -euo pipefail
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "$DIR/lib.sh"
|
||||||
|
|
||||||
|
SHOTDIR="${1:-interaction-shots}"
|
||||||
|
mkdir -p "$SHOTDIR"
|
||||||
|
|
||||||
|
smoke::setup
|
||||||
|
smoke::start_compositor
|
||||||
|
trap smoke::stop EXIT
|
||||||
|
|
||||||
|
SEAT="$(swaymsg -t get_seats 2>/dev/null | jq -r '.[0].name' 2>/dev/null || echo seat0)"
|
||||||
|
CX=$((WIDTH / 2)); CY=15
|
||||||
|
|
||||||
|
click_center() {
|
||||||
|
swaymsg "seat $SEAT cursor set $CX $CY" >/dev/null
|
||||||
|
swaymsg "seat $SEAT cursor press button1" >/dev/null
|
||||||
|
swaymsg "seat $SEAT cursor release button1" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
# --- 1) on-click side effect ---------------------------------------------
|
||||||
|
echo "::group::on-click side effect"
|
||||||
|
SENTINEL="$(mktemp -u)"
|
||||||
|
cfg1="$(mktemp --suffix=.json)"
|
||||||
|
cat > "$cfg1" <<EOF
|
||||||
|
{
|
||||||
|
"layer": "top", "position": "top", "height": 30,
|
||||||
|
"modules-center": ["custom/btn"],
|
||||||
|
"custom/btn": { "exec": "echo CLICK ME", "interval": "once", "on-click": "touch $SENTINEL" }
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
smoke::launch_waybar "$cfg1" "$DIR/style.css" 4
|
||||||
|
smoke::assert_alive || fail=1
|
||||||
|
smoke::assert_clean || fail=1
|
||||||
|
click_center
|
||||||
|
sleep 2
|
||||||
|
if [ -f "$SENTINEL" ]; then
|
||||||
|
echo "✓ on-click ran (sentinel created)"
|
||||||
|
else
|
||||||
|
echo "::error::on-click did not fire — sentinel missing"; fail=1
|
||||||
|
fi
|
||||||
|
kill "$WAYBAR_PID" 2>/dev/null || true; sleep 1
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
# --- 2) format-alt toggle on click ---------------------------------------
|
||||||
|
echo "::group::format-alt toggle"
|
||||||
|
cfg2="$(mktemp --suffix=.json)"
|
||||||
|
cat > "$cfg2" <<EOF
|
||||||
|
{
|
||||||
|
"layer": "top", "position": "top", "height": 30,
|
||||||
|
"modules-center": ["custom/alt"],
|
||||||
|
"custom/alt": { "exec": "echo VALUE", "interval": "once", "format": "{}", "format-alt": "*** {} ***" }
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
smoke::launch_waybar "$cfg2" "$DIR/style.css" 4
|
||||||
|
smoke::assert_alive || fail=1
|
||||||
|
smoke::screenshot "$SHOTDIR/alt-before.png"
|
||||||
|
click_center
|
||||||
|
sleep 2
|
||||||
|
smoke::screenshot "$SHOTDIR/alt-after.png"
|
||||||
|
ae="$(compare -metric AE "$SHOTDIR/alt-before.png" "$SHOTDIR/alt-after.png" null: 2>&1 || true)"
|
||||||
|
ae="$(printf '%s' "$ae" | grep -oE '^[0-9]+' || echo 0)"
|
||||||
|
echo "pixels changed after click: $ae"
|
||||||
|
if [ "$ae" -gt 100 ]; then
|
||||||
|
echo "✓ format-alt toggled the label on click"
|
||||||
|
else
|
||||||
|
echo "::error::format-alt did not change the bar on click ($ae px)"; fail=1
|
||||||
|
fi
|
||||||
|
kill "$WAYBAR_PID" 2>/dev/null || true
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
[ "$fail" = 0 ] || exit 1
|
||||||
|
echo "✓ interaction tests passed"
|
||||||
Executable
+136
@@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shared helpers for the headless Waybar smoke/render/interaction tests.
|
||||||
|
#
|
||||||
|
# A test sources this file, then typically:
|
||||||
|
# smoke::setup
|
||||||
|
# smoke::start_compositor
|
||||||
|
# smoke::launch_waybar <config> [style]
|
||||||
|
# smoke::assert_alive
|
||||||
|
# smoke::assert_clean # ASan/UBSan/Gtk-CRITICAL/segfault gate
|
||||||
|
# smoke::screenshot out.png
|
||||||
|
# smoke::stop
|
||||||
|
#
|
||||||
|
# Environment knobs (optional): WIDTH HEIGHT SCALE COMPOSITOR WAYBAR_BIN
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
WIDTH="${WIDTH:-1920}"
|
||||||
|
HEIGHT="${HEIGHT:-1080}"
|
||||||
|
SCALE="${SCALE:-1}"
|
||||||
|
COMPOSITOR="${COMPOSITOR:-sway}"
|
||||||
|
WAYBAR_BIN="${WAYBAR_BIN:-waybar}"
|
||||||
|
|
||||||
|
SMOKE_LOG=""
|
||||||
|
COMP_PID=""
|
||||||
|
WAYBAR_PID=""
|
||||||
|
|
||||||
|
smoke::setup() {
|
||||||
|
export XDG_RUNTIME_DIR="$(mktemp -d)"
|
||||||
|
chmod 700 "$XDG_RUNTIME_DIR"
|
||||||
|
export WLR_BACKENDS=headless
|
||||||
|
export WLR_RENDERER=pixman
|
||||||
|
export WLR_LIBINPUT_NO_DEVICES=1
|
||||||
|
export LIBGL_ALWAYS_SOFTWARE=1
|
||||||
|
# Keep ASan/UBSan noise low but fatal on real errors (leaks in GTK/glib are
|
||||||
|
# too noisy to gate on, so only memory-corruption and UB abort the run).
|
||||||
|
export ASAN_OPTIONS="detect_leaks=0:halt_on_error=1:abort_on_error=1:detect_odr_violation=0"
|
||||||
|
export UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1"
|
||||||
|
}
|
||||||
|
|
||||||
|
smoke::start_compositor() {
|
||||||
|
case "$COMPOSITOR" in
|
||||||
|
sway)
|
||||||
|
local cfg; cfg="$(mktemp)"
|
||||||
|
cat > "$cfg" <<EOF
|
||||||
|
output HEADLESS-1 resolution ${WIDTH}x${HEIGHT} scale ${SCALE} background #000000 solid_color
|
||||||
|
EOF
|
||||||
|
sway -c "$cfg" >/tmp/compositor.log 2>&1 &
|
||||||
|
COMP_PID=$!
|
||||||
|
;;
|
||||||
|
labwc)
|
||||||
|
# labwc reads outputs from the wlroots headless backend directly.
|
||||||
|
labwc >/tmp/compositor.log 2>&1 &
|
||||||
|
COMP_PID=$!
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "::error::unknown compositor '$COMPOSITOR'"; return 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local sock=""
|
||||||
|
for _ in $(seq 1 40); do
|
||||||
|
sock="$(find "$XDG_RUNTIME_DIR" -maxdepth 1 -name 'wayland-*' ! -name '*.lock' 2>/dev/null | head -1 || true)"
|
||||||
|
[ -n "$sock" ] && break
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
|
if [ -z "$sock" ]; then
|
||||||
|
echo "::error::$COMPOSITOR did not create a Wayland socket"; cat /tmp/compositor.log || true; return 1
|
||||||
|
fi
|
||||||
|
export WAYLAND_DISPLAY="$(basename "$sock")"
|
||||||
|
|
||||||
|
# sway IPC socket, so swaymsg (cursor injection, clean exit) can reach it.
|
||||||
|
if [ "$COMPOSITOR" = sway ]; then
|
||||||
|
for _ in $(seq 1 10); do
|
||||||
|
SWAYSOCK="$(find "$XDG_RUNTIME_DIR" -maxdepth 1 -name 'sway-ipc.*.sock' 2>/dev/null | head -1 || true)"
|
||||||
|
[ -n "$SWAYSOCK" ] && break
|
||||||
|
sleep 0.3
|
||||||
|
done
|
||||||
|
export SWAYSOCK
|
||||||
|
fi
|
||||||
|
echo "compositor '$COMPOSITOR' up on WAYLAND_DISPLAY=$WAYLAND_DISPLAY (${WIDTH}x${HEIGHT} scale ${SCALE})"
|
||||||
|
}
|
||||||
|
|
||||||
|
# smoke::launch_waybar <config> [style] [settle_seconds]
|
||||||
|
smoke::launch_waybar() {
|
||||||
|
local config="$1" style="${2:-}" settle="${3:-6}"
|
||||||
|
SMOKE_LOG="$(mktemp)"
|
||||||
|
local args=(-c "$config" -l debug)
|
||||||
|
[ -n "$style" ] && args+=(-s "$style")
|
||||||
|
"$WAYBAR_BIN" "${args[@]}" >"$SMOKE_LOG" 2>&1 &
|
||||||
|
WAYBAR_PID=$!
|
||||||
|
sleep "$settle"
|
||||||
|
}
|
||||||
|
|
||||||
|
smoke::log() { cat "$SMOKE_LOG" 2>/dev/null || true; }
|
||||||
|
|
||||||
|
smoke::assert_alive() {
|
||||||
|
if ! kill -0 "$WAYBAR_PID" 2>/dev/null; then
|
||||||
|
echo "::error::waybar exited early (crashed on startup)"
|
||||||
|
echo "::group::waybar log"; smoke::log; echo "::endgroup::"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "✓ waybar is alive"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fail on sanitizer reports and GTK/GLib criticals in the log.
|
||||||
|
smoke::assert_clean() {
|
||||||
|
local bad
|
||||||
|
bad="$(grep -iE 'AddressSanitizer|runtime error:|LeakSanitizer|Gtk-CRITICAL|GLib-CRITICAL|GLib-GObject-CRITICAL|assertion .*failed|segfault|terminate called|SUMMARY: .*Sanitizer' "$SMOKE_LOG" || true)"
|
||||||
|
if [ -n "$bad" ]; then
|
||||||
|
echo "::error::waybar reported sanitizer/critical issues:"
|
||||||
|
echo "$bad"
|
||||||
|
echo "::group::full waybar log"; smoke::log; echo "::endgroup::"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "✓ no sanitizer/critical issues in log"
|
||||||
|
}
|
||||||
|
|
||||||
|
smoke::screenshot() {
|
||||||
|
local out="$1"
|
||||||
|
grim "$out"
|
||||||
|
echo "✓ screenshot -> $out"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Assert the top bar strip actually rendered something (not a uniform color).
|
||||||
|
smoke::assert_not_blank() {
|
||||||
|
local img="$1" h="${2:-30}"
|
||||||
|
local sd
|
||||||
|
sd="$(convert "$img" -crop "${WIDTH}x${h}+0+0" +repage -colorspace Gray -format '%[fx:standard_deviation]' info:)"
|
||||||
|
echo "bar strip stddev = $sd"
|
||||||
|
awk "BEGIN{ exit !($sd > 0.01) }" || { echo "::error::bar strip is blank in $img"; return 1; }
|
||||||
|
echo "✓ bar rendered content"
|
||||||
|
}
|
||||||
|
|
||||||
|
smoke::stop() {
|
||||||
|
kill "$WAYBAR_PID" 2>/dev/null || true
|
||||||
|
swaymsg -q exit 2>/dev/null || kill "$COMP_PID" 2>/dev/null || true
|
||||||
|
wait 2>/dev/null || true
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
// Waybar config for the CI smoke test: exercises real modules that work in a
|
|
||||||
// headless environment (no hardware/daemon dependency). Its content is dynamic
|
|
||||||
// (time, cpu, ...) so it is NOT screenshot-compared — it only checks that these
|
|
||||||
// modules load and render without crashing the bar.
|
|
||||||
{
|
|
||||||
"layer": "top",
|
|
||||||
"position": "top",
|
|
||||||
"height": 30,
|
|
||||||
"modules-center": ["clock", "cpu", "memory", "disk", "custom/hello"],
|
|
||||||
"clock": {},
|
|
||||||
"cpu": {},
|
|
||||||
"memory": {},
|
|
||||||
"disk": {},
|
|
||||||
"custom/hello": {
|
|
||||||
"exec": "echo ok",
|
|
||||||
"interval": "once"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Tier 1 — per-module matrix: render each headless-safe module in isolation and
|
||||||
|
# assert it loads, logs no sanitizer/critical error, and draws something.
|
||||||
|
#
|
||||||
|
# Usage: modules.sh <screenshot-dir>
|
||||||
|
set -euo pipefail
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "$DIR/lib.sh"
|
||||||
|
|
||||||
|
SHOTDIR="${1:-module-shots}"
|
||||||
|
mkdir -p "$SHOTDIR"
|
||||||
|
|
||||||
|
# module name -> its config options (JSON object). Only modules that work with
|
||||||
|
# no real hardware/daemon in a headless runner are included.
|
||||||
|
declare -A MODULES=(
|
||||||
|
[clock]='{}'
|
||||||
|
[cpu]='{}'
|
||||||
|
[memory]='{}'
|
||||||
|
[disk]='{}'
|
||||||
|
[idle_inhibitor]='{"format":"{status}"}'
|
||||||
|
[custom/echo]='{"exec":"echo hello","interval":"once"}'
|
||||||
|
)
|
||||||
|
|
||||||
|
smoke::setup
|
||||||
|
smoke::start_compositor
|
||||||
|
trap smoke::stop EXIT
|
||||||
|
|
||||||
|
fails=()
|
||||||
|
for mod in "${!MODULES[@]}"; do
|
||||||
|
safe="${mod//\//-}"
|
||||||
|
cfg="$(mktemp --suffix=.json)"
|
||||||
|
cat > "$cfg" <<EOF
|
||||||
|
{
|
||||||
|
"layer": "top", "position": "top", "height": 30,
|
||||||
|
"modules-center": ["$mod"],
|
||||||
|
"$mod": ${MODULES[$mod]}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
echo "::group::module: $mod"
|
||||||
|
smoke::launch_waybar "$cfg" "$DIR/style.css" 4
|
||||||
|
ok=1
|
||||||
|
smoke::assert_alive || ok=0
|
||||||
|
smoke::assert_clean || ok=0
|
||||||
|
if [ "$ok" = 1 ]; then
|
||||||
|
smoke::screenshot "$SHOTDIR/$safe.png"
|
||||||
|
smoke::assert_not_blank "$SHOTDIR/$safe.png" || ok=0
|
||||||
|
fi
|
||||||
|
[ "$ok" = 1 ] || fails+=("$mod")
|
||||||
|
kill "$WAYBAR_PID" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
echo "::endgroup::"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#fails[@]}" -gt 0 ]; then
|
||||||
|
echo "::error::modules failed to render cleanly: ${fails[*]}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ all ${#MODULES[@]} modules rendered cleanly"
|
||||||
Executable
+54
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Tier 3 — layout matrix: bar at top/bottom (horizontal) and left (vertical),
|
||||||
|
# plus a HiDPI (scale 2) run. Each must launch and render without a crash or
|
||||||
|
# sanitizer/critical error. Screenshots are uploaded for visual inspection.
|
||||||
|
#
|
||||||
|
# Usage: positions.sh [screenshot-dir]
|
||||||
|
set -euo pipefail
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "$DIR/lib.sh"
|
||||||
|
|
||||||
|
SHOTDIR="${1:-layout-shots}"
|
||||||
|
mkdir -p "$SHOTDIR"
|
||||||
|
|
||||||
|
smoke::setup
|
||||||
|
trap smoke::stop EXIT
|
||||||
|
|
||||||
|
fails=()
|
||||||
|
|
||||||
|
# run_case <name> <position> <scale>
|
||||||
|
run_case() {
|
||||||
|
local name="$1" pos="$2" scale="$3"
|
||||||
|
echo "::group::layout: $name (position=$pos scale=$scale)"
|
||||||
|
SCALE="$scale" smoke::start_compositor || { fails+=("$name"); echo "::endgroup::"; return; }
|
||||||
|
local cfg; cfg="$(mktemp --suffix=.json)"
|
||||||
|
cat > "$cfg" <<EOF
|
||||||
|
{
|
||||||
|
"layer": "top", "position": "$pos", "height": 30,
|
||||||
|
"modules-left": ["custom/l"], "modules-center": ["custom/c"], "modules-right": ["custom/r"],
|
||||||
|
"custom/l": { "exec": "echo LEFT", "interval": "once" },
|
||||||
|
"custom/c": { "exec": "echo CENTER", "interval": "once" },
|
||||||
|
"custom/r": { "exec": "echo RIGHT", "interval": "once" }
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
smoke::launch_waybar "$cfg" "$DIR/style.css" 4
|
||||||
|
local ok=1
|
||||||
|
smoke::assert_alive || ok=0
|
||||||
|
smoke::assert_clean || ok=0
|
||||||
|
smoke::screenshot "$SHOTDIR/$name.png" || true
|
||||||
|
[ "$ok" = 1 ] || fails+=("$name")
|
||||||
|
smoke::stop
|
||||||
|
echo "::endgroup::"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_case top-x1 top 1
|
||||||
|
run_case bottom-x1 bottom 1
|
||||||
|
run_case left-x1 left 1
|
||||||
|
run_case top-x2 top 2
|
||||||
|
|
||||||
|
if [ "${#fails[@]}" -gt 0 ]; then
|
||||||
|
echo "::error::layout cases failed: ${fails[*]}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ all layout cases launched and rendered cleanly"
|
||||||
+14
-69
@@ -1,77 +1,22 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Launch Waybar inside a headless sway compositor and verify it starts and
|
# Launch Waybar inside a headless compositor and verify it starts, stays alive,
|
||||||
# renders. Used by the `smoke` CI workflow.
|
# logs no sanitizer/critical error, and (optionally) renders a screenshot.
|
||||||
#
|
#
|
||||||
# Usage: run.sh <config> [style] [screenshot.png]
|
# Usage: run.sh <config> [style] [screenshot.png]
|
||||||
# - always: assert waybar launches, stays alive, logs no fatal error
|
|
||||||
# - if <screenshot.png> given: capture the bar with grim
|
|
||||||
#
|
|
||||||
# Requires: sway, grim, waybar (on PATH). Software-rendered, no GPU needed.
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=lib.sh
|
||||||
|
source "$DIR/lib.sh"
|
||||||
|
|
||||||
CONFIG="$1"
|
CONFIG="$1"; STYLE="${2:-}"; SHOT="${3:-}"
|
||||||
STYLE="${2:-}"
|
|
||||||
SHOT="${3:-}"
|
|
||||||
|
|
||||||
export XDG_RUNTIME_DIR="$(mktemp -d)"
|
smoke::setup
|
||||||
chmod 700 "$XDG_RUNTIME_DIR"
|
smoke::start_compositor
|
||||||
export WLR_BACKENDS=headless
|
trap smoke::stop EXIT
|
||||||
export WLR_RENDERER=pixman
|
smoke::launch_waybar "$CONFIG" "$STYLE"
|
||||||
export WLR_LIBINPUT_NO_DEVICES=1
|
|
||||||
export LIBGL_ALWAYS_SOFTWARE=1
|
|
||||||
|
|
||||||
LOG="$(mktemp)"
|
echo "::group::waybar log"; smoke::log; echo "::endgroup::"
|
||||||
SWAYCFG="$(mktemp)"
|
|
||||||
|
|
||||||
wb="waybar -c $CONFIG"
|
smoke::assert_alive
|
||||||
[ -n "$STYLE" ] && wb="$wb -s $STYLE"
|
smoke::assert_clean
|
||||||
|
[ -n "$SHOT" ] && smoke::screenshot "$SHOT"
|
||||||
cat > "$SWAYCFG" <<EOF
|
|
||||||
output HEADLESS-1 resolution 1920x1080 background #000000 solid_color
|
|
||||||
exec "$wb -l debug > $LOG 2>&1"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "::group::Starting sway (headless) + waybar"
|
|
||||||
sway -c "$SWAYCFG" &
|
|
||||||
SWAY_PID=$!
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
swaymsg -q exit 2>/dev/null || kill "$SWAY_PID" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
# Wait for the Wayland socket to appear.
|
|
||||||
sock=""
|
|
||||||
for _ in $(seq 1 40); do
|
|
||||||
sock="$(find "$XDG_RUNTIME_DIR" -maxdepth 1 -name 'wayland-*' ! -name '*.lock' 2>/dev/null | head -1 || true)"
|
|
||||||
[ -n "$sock" ] && break
|
|
||||||
sleep 0.5
|
|
||||||
done
|
|
||||||
[ -n "$sock" ] || { echo "::error::compositor did not create a Wayland socket"; exit 1; }
|
|
||||||
export WAYLAND_DISPLAY="$(basename "$sock")"
|
|
||||||
echo "compositor up on WAYLAND_DISPLAY=$WAYLAND_DISPLAY"
|
|
||||||
echo "::endgroup::"
|
|
||||||
|
|
||||||
# Give modules time to poll/settle.
|
|
||||||
sleep 6
|
|
||||||
|
|
||||||
echo "::group::Waybar log"
|
|
||||||
cat "$LOG" || true
|
|
||||||
echo "::endgroup::"
|
|
||||||
|
|
||||||
# --- Level 1: smoke ---
|
|
||||||
if ! pgrep -x waybar >/dev/null; then
|
|
||||||
echo "::error::waybar is not running — it crashed on startup (config: $CONFIG)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if grep -iqE 'critical|segfault|\bfatal\b|terminate called' "$LOG"; then
|
|
||||||
echo "::error::waybar logged a fatal error (config: $CONFIG)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "✓ waybar launched and stayed alive under headless sway"
|
|
||||||
|
|
||||||
# --- Level 2: screenshot ---
|
|
||||||
if [ -n "$SHOT" ]; then
|
|
||||||
grim "$SHOT"
|
|
||||||
echo "✓ captured screenshot -> $SHOT"
|
|
||||||
fi
|
|
||||||
|
|||||||
Reference in New Issue
Block a user