diff --git a/.github/workflows/linux-native-cairo-build.yml b/.github/workflows/linux-native-cairo-build.yml index aaf6f59..034bafd 100644 --- a/.github/workflows/linux-native-cairo-build.yml +++ b/.github/workflows/linux-native-cairo-build.yml @@ -27,7 +27,7 @@ jobs: sudo apt-get update sudo apt-get install --no-install-recommends \ build-essential cmake ninja-build pkg-config \ - libcairo2-dev libx11-dev xvfb + libcairo2-dev libx11-dev xvfb xfwm4 x11-utils xauth dbus-daemon - name: Configure run: >- @@ -71,6 +71,38 @@ jobs: unset LD_PRELOAD xvfb-run -a ctest --test-dir build --output-on-failure + - name: Repeat bare and managed X11 window smoke + run: | + xvfb-run -a ctest --test-dir build --output-on-failure \ + -R '^native\.linux_window_smoke$' --repeat until-fail:20 + xvfb-run -a dbus-run-session -- sh <<'EOF' + set -eu + xfwm4 --compositor=off --sm-client-disable >/tmp/xege-xfwm4.log 2>&1 & + wm_pid=$! + trap 'kill "$wm_pid" 2>/dev/null || true' EXIT + ready=false + for attempt in $(seq 1 100); do + if kill -0 "$wm_pid" 2>/dev/null && + xprop -root _NET_SUPPORTING_WM_CHECK | grep -Eq 'window id # 0x[0-9a-fA-F]*[1-9a-fA-F][0-9a-fA-F]*$'; then + ready=true + break + fi + if ! kill -0 "$wm_pid" 2>/dev/null; then + cat /tmp/xege-xfwm4.log + exit 1 + fi + sleep 0.05 + done + if [ "$ready" != true ]; then + echo 'Window manager did not become ready within 5 seconds' >&2 + cat /tmp/xege-xfwm4.log + exit 1 + fi + ctest --test-dir build --output-on-failure \ + -R '^native\.linux_window_smoke$' --repeat until-fail:20 + kill -0 "$wm_pid" + EOF + - name: Enforce lightweight GUI dependency boundary shell: bash run: | diff --git a/tests/native/linux_window_smoke.cpp b/tests/native/linux_window_smoke.cpp index e97401b..602cf38 100644 --- a/tests/native/linux_window_smoke.cpp +++ b/tests/native/linux_window_smoke.cpp @@ -4,8 +4,10 @@ #include #include +#include #include #include +#include #include namespace @@ -37,6 +39,22 @@ int fail(const char* message) return 1; } +// Mapping and resizing a managed X11 window require another client's event +// loop. XSync on our connection does not wait for the window manager to act. +// Keep pumping until the expected result arrives, with a bounded failure path. +template +bool waitForEvents(ege::backend::LinuxWindow& window, Predicate ready) +{ + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + do { + window.processEvents(); + if (ready()) return true; + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } while (std::chrono::steady_clock::now() < deadline); + window.processEvents(); + return ready(); +} + void send(Display* display, ::Window window, XEvent* event, long mask) { event->xany.display = display; @@ -48,6 +66,12 @@ void send(Display* display, ::Window window, XEvent* event, long mask) int main() { + // Keep a test connection alive throughout setup. Otherwise a bare Xvfb + // can reset when primaryScreenSize() closes its temporary last client, + // racing with the connection opened by window.create(). + Display* display = XOpenDisplay(nullptr); + if (!display) return fail("test X connection failed"); + int screenWidth = 0, screenHeight = 0; if (!ege::backend::LinuxWindow::primaryScreenSize(&screenWidth, &screenHeight) || screenWidth <= 0 || screenHeight <= 0) return fail("screen size unavailable"); @@ -58,15 +82,25 @@ int main() if (window.isClosed() || window.getWidth() != 96 || window.getHeight() != 64) return fail("initial size invalid"); window.show(); window.setTitle("EGE native Xlib smoke"); - window.setPosition(12, 12); - window.setSize(80, 48); - Display* display = XOpenDisplay(nullptr); - if (!display) return fail("second X connection failed"); const ::Window native = static_cast<::Window>(reinterpret_cast(window.getNativeHandle())); XWindowAttributes attributes{}; - XGetWindowAttributes(display, native, &attributes); - if (attributes.map_state == IsUnmapped) return fail("window was not mapped"); + if (!waitForEvents(window, [&] { + XGetWindowAttributes(display, native, &attributes); + return attributes.map_state == IsViewable; + })) return fail("window was not mapped within 2 seconds"); + + window.setPosition(12, 12); + window.setSize(80, 48); + if (!waitForEvents(window, [&] { + return window.getWidth() == 80 && window.getHeight() == 48 + && sink.resizes > 0 && sink.lastWidth == 80 && sink.lastHeight == 48; + })) { + std::cerr << "Expected 80x48; window is " << window.getWidth() << 'x' + << window.getHeight() << ", last resize is " << sink.lastWidth + << 'x' << sink.lastHeight << " (" << sink.resizes << " events)\n"; + return fail("resize event missing after 2 seconds"); + } XEvent motion{}; motion.type = MotionNotify; @@ -96,8 +130,10 @@ int main() std::vector pixels(80 * 48, 0xFF336699U); window.present(pixels.data(), 80, 48, 80 * sizeof(std::uint32_t)); - window.processEvents(); - if (window.getWidth() != 80 || window.getHeight() != 48 || sink.resizes == 0) return fail("resize event missing"); + if (!waitForEvents(window, [&] { + return sink.moves > 0 && sink.maxClicks == 2 && sink.wheel > 0 + && sink.keys.size() >= 2; + })) return fail("input events missing after 2 seconds"); if (sink.moves == 0 || sink.lastButton != 0 || sink.maxClicks != 2 || sink.wheel <= 0) return fail("mouse mapping invalid"); if (sink.keys.size() < 2 || sink.keys.front() != 'A' || sink.keyPressed.front() != true) return fail("key mapping invalid"); @@ -108,11 +144,13 @@ int main() close.xclient.format = 32; close.xclient.data.l[0] = static_cast(wmDelete); send(display, native, &close, NoEventMask); - window.processEvents(); + if (!waitForEvents(window, [&] { return sink.closes >= 1; })) + return fail("rejected close event missing after 2 seconds"); if (window.isClosed() || sink.closes != 1) return fail("rejected close was not preserved"); sink.allowClose = true; send(display, native, &close, NoEventMask); - window.processEvents(); + if (!waitForEvents(window, [&] { return window.isClosed(); })) + return fail("accepted close event missing after 2 seconds"); if (!window.isClosed() || sink.closes != 2) return fail("accepted close did not close"); XCloseDisplay(display); std::cout << "LinuxWindow smoke passed\n";