Merge pull request #5173 from Alexays/ci/smoke-plus

ci(smoke): sanitizer build + module/interaction/a11y/layout coverage
This commit is contained in:
Alexis Rouillard
2026-07-05 10:39:29 +02:00
committed by GitHub
10 changed files with 524 additions and 135 deletions
+69 -28
View File
@@ -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
View File
@@ -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
``` ```
+58
View File
@@ -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())
+18
View File
@@ -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"
+81
View File
@@ -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"
+136
View File
@@ -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
}
-18
View File
@@ -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"
}
}
+59
View File
@@ -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"
+54
View File
@@ -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
View File
@@ -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