Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion .github/workflows/linux-native-cairo-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: >-
Expand Down Expand Up @@ -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: |
Expand Down
58 changes: 48 additions & 10 deletions tests/native/linux_window_smoke.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
#include <X11/Xlib.h>
#include <X11/keysym.h>

#include <chrono>
#include <cstdint>
#include <iostream>
#include <thread>
#include <vector>

namespace
Expand Down Expand Up @@ -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 <typename Predicate>
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;
Expand All @@ -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");
Expand All @@ -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<std::uintptr_t>(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;
Expand Down Expand Up @@ -96,8 +130,10 @@ int main()

std::vector<std::uint32_t> 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");

Expand All @@ -108,11 +144,13 @@ int main()
close.xclient.format = 32;
close.xclient.data.l[0] = static_cast<long>(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";
Expand Down
Loading