Dette protocolaire transversale comblée : wl_output n'était pas déclaré comme global, plusieurs toolkits client (sctk, GTK, Qt) le considèrent implicitement requis. Maintenant exposé à v3 avec gating conforme : - geometry, mode : envoyés à toutes les versions (>= 1) - scale : seulement si bind >= 3 (gating conservateur) - done : seulement si bind >= 2 Mode flags = CURRENT | PREFERRED, refresh 60 000 mHz, scale_factor=1, transform Normal, subpixel Unknown, make=Redox, model=redox-wl-output:0. Position (0,0), single output, taille pixel issue de screen_w/screen_h (setté par set_screen_size au boot compositor). Nouveau crate de validation : redox-wl-test-wl-output. Deux niveaux : 1. Client autonome pour test runtime dans QEMU (bind aux 3 versions, dump events, PASS/FAIL via exit code) 2. Test natif (cargo test, tourne sur l'hôte sans QEMU) qui simule serveur+client en-process via UnixStream::pair() et valide les 11 assertions du gating. Vérifié PASS le 2026-05-16 sur CachyOS. Le test natif sert de garde-fou anti-régression pour CI ultérieur. Plus besoin de relancer QEMU pour valider ce path. run-qemu.sh : copie maintenant aussi le test client si compilé (skip silencieux sinon). Leyoda 2026 – GPLv3
247 lines
7.5 KiB
Rust
247 lines
7.5 KiB
Rust
//! Test natif (CachyOS, hors QEMU) : version gating de wl_output.
|
|
//!
|
|
//! On simule en-process l'init du global wl_output v3 avec la MÊME logique
|
|
//! de gating que `redox-wl-wayland-frontend::lib.rs` :
|
|
//! - geometry, mode : toujours envoyés
|
|
//! - scale : seulement si version >= 3
|
|
//! - done : seulement si version >= 2
|
|
//!
|
|
//! Un seul process : serveur sur un thread + client sur le thread principal,
|
|
//! reliés par `UnixStream::pair()`. Pas besoin de Redox, pas besoin de QEMU.
|
|
|
|
use std::os::unix::net::UnixStream;
|
|
use std::sync::Arc;
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
|
|
use wayland_server::backend::{ClientData, ClientId, DisconnectReason};
|
|
use wayland_server::{
|
|
protocol::wl_output as srv_wl_output, Client as SrvClient, DataInit, Display, DisplayHandle,
|
|
GlobalDispatch, Resource,
|
|
};
|
|
|
|
use wayland_client::{
|
|
backend::Backend,
|
|
protocol::{wl_output as cli_wl_output, wl_registry},
|
|
Connection, Dispatch as CliDispatch, QueueHandle,
|
|
};
|
|
|
|
const OUTPUT_VERSION: u32 = 3;
|
|
const SCREEN_W: i32 = 1280;
|
|
const SCREEN_H: i32 = 800;
|
|
|
|
// ============= server side =============
|
|
|
|
struct ServerState;
|
|
|
|
struct StubClientData;
|
|
impl ClientData for StubClientData {
|
|
fn initialized(&self, _: ClientId) {}
|
|
fn disconnected(&self, _: ClientId, _: DisconnectReason) {}
|
|
}
|
|
|
|
impl GlobalDispatch<srv_wl_output::WlOutput, ()> for ServerState {
|
|
fn bind(
|
|
_state: &mut Self,
|
|
_handle: &DisplayHandle,
|
|
_client: &SrvClient,
|
|
resource: wayland_server::New<srv_wl_output::WlOutput>,
|
|
_data: &(),
|
|
data_init: &mut DataInit<'_, Self>,
|
|
) {
|
|
let output = data_init.init(resource, ());
|
|
let version = output.version();
|
|
|
|
// Logique identique à redox-wl-wayland-frontend/src/lib.rs
|
|
output.geometry(
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
srv_wl_output::Subpixel::Unknown,
|
|
"Redox".to_string(),
|
|
"redox-wl-output:0".to_string(),
|
|
srv_wl_output::Transform::Normal,
|
|
);
|
|
|
|
let mode_flags = srv_wl_output::Mode::Current | srv_wl_output::Mode::Preferred;
|
|
output.mode(mode_flags, SCREEN_W, SCREEN_H, 60_000);
|
|
|
|
if version >= 3 {
|
|
output.scale(1);
|
|
}
|
|
if version >= 2 {
|
|
output.done();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl wayland_server::Dispatch<srv_wl_output::WlOutput, ()> for ServerState {
|
|
fn request(
|
|
_state: &mut Self,
|
|
_client: &SrvClient,
|
|
_r: &srv_wl_output::WlOutput,
|
|
_request: srv_wl_output::Request,
|
|
_data: &(),
|
|
_dh: &DisplayHandle,
|
|
_data_init: &mut DataInit<'_, Self>,
|
|
) {
|
|
}
|
|
}
|
|
|
|
// ============= client side =============
|
|
|
|
#[derive(Default, Debug, Clone, Copy)]
|
|
struct OutputObs {
|
|
seen_geometry: bool,
|
|
seen_mode: bool,
|
|
seen_scale: bool,
|
|
seen_done: bool,
|
|
mode_flags: u32,
|
|
scale_factor: i32,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct ClientState {
|
|
global_name: Option<u32>,
|
|
obs: [OutputObs; 3],
|
|
cur: Option<usize>,
|
|
}
|
|
|
|
impl CliDispatch<wl_registry::WlRegistry, ()> for ClientState {
|
|
fn event(
|
|
state: &mut Self,
|
|
_: &wl_registry::WlRegistry,
|
|
event: wl_registry::Event,
|
|
_: &(),
|
|
_: &Connection,
|
|
_: &QueueHandle<Self>,
|
|
) {
|
|
if let wl_registry::Event::Global {
|
|
name, interface, ..
|
|
} = event
|
|
{
|
|
if interface == "wl_output" {
|
|
state.global_name = Some(name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CliDispatch<cli_wl_output::WlOutput, ()> for ClientState {
|
|
fn event(
|
|
state: &mut Self,
|
|
_: &cli_wl_output::WlOutput,
|
|
event: cli_wl_output::Event,
|
|
_: &(),
|
|
_: &Connection,
|
|
_: &QueueHandle<Self>,
|
|
) {
|
|
let Some(idx) = state.cur else { return };
|
|
let obs = &mut state.obs[idx];
|
|
match event {
|
|
cli_wl_output::Event::Geometry { .. } => obs.seen_geometry = true,
|
|
cli_wl_output::Event::Mode { flags, .. } => {
|
|
obs.seen_mode = true;
|
|
obs.mode_flags = flags.into();
|
|
}
|
|
cli_wl_output::Event::Scale { factor } => {
|
|
obs.seen_scale = true;
|
|
obs.scale_factor = factor;
|
|
}
|
|
cli_wl_output::Event::Done => obs.seen_done = true,
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============= test =============
|
|
|
|
#[test]
|
|
fn wl_output_v123_gating() {
|
|
// 1. Setup serveur dans un thread
|
|
let (s_stream, c_stream) = UnixStream::pair().expect("UnixStream::pair");
|
|
|
|
let server_thread = thread::spawn(move || {
|
|
let mut display: Display<ServerState> = Display::new().unwrap();
|
|
let dh = display.handle();
|
|
dh.create_global::<ServerState, srv_wl_output::WlOutput, _>(OUTPUT_VERSION, ());
|
|
|
|
s_stream.set_nonblocking(true).unwrap();
|
|
let _client = display
|
|
.handle()
|
|
.insert_client(s_stream, Arc::new(StubClientData))
|
|
.unwrap();
|
|
|
|
let mut state = ServerState;
|
|
// Dispatch loop : on tourne ~5 secondes pour absorber tous les
|
|
// round-trips du test client. Le test signale la fin en fermant
|
|
// le socket côté client, ce qui sort dispatch_clients sur error.
|
|
let start = std::time::Instant::now();
|
|
while start.elapsed() < Duration::from_secs(5) {
|
|
let _ = display.dispatch_clients(&mut state);
|
|
let _ = display.flush_clients();
|
|
thread::sleep(Duration::from_millis(10));
|
|
}
|
|
});
|
|
|
|
// 2. Client connecte via l'autre bout du socket
|
|
c_stream.set_nonblocking(false).unwrap();
|
|
let backend = Backend::connect(c_stream).unwrap();
|
|
let conn = Connection::from_backend(backend);
|
|
let mut event_queue = conn.new_event_queue::<ClientState>();
|
|
let qh = event_queue.handle();
|
|
let display = conn.display();
|
|
let _reg1 = display.get_registry(&qh, ());
|
|
|
|
let mut state = ClientState::default();
|
|
event_queue.roundtrip(&mut state).expect("roundtrip registry");
|
|
|
|
let global_name = state.global_name.expect("wl_output non advertised");
|
|
|
|
// 3. Bind aux 3 versions
|
|
let registry = display.get_registry(&qh, ());
|
|
for (idx, version) in [(0usize, 1u32), (1, 2), (2, 3)].iter() {
|
|
state.cur = Some(*idx);
|
|
let _o = registry.bind::<cli_wl_output::WlOutput, _, _>(global_name, *version, &qh, ());
|
|
for _ in 0..3 {
|
|
event_queue.roundtrip(&mut state).expect("roundtrip bind");
|
|
}
|
|
state.cur = None;
|
|
}
|
|
|
|
// 4. Drop client to make server exit
|
|
drop(conn);
|
|
server_thread.join().unwrap();
|
|
|
|
// 5. Asserts
|
|
let v1 = &state.obs[0];
|
|
assert!(v1.seen_geometry, "v1: geometry attendu");
|
|
assert!(v1.seen_mode, "v1: mode attendu");
|
|
assert!(!v1.seen_scale, "v1: scale ne devrait PAS être envoyé");
|
|
assert!(!v1.seen_done, "v1: done ne devrait PAS être envoyé");
|
|
|
|
let v2 = &state.obs[1];
|
|
assert!(v2.seen_geometry, "v2: geometry attendu");
|
|
assert!(v2.seen_mode, "v2: mode attendu");
|
|
assert!(
|
|
!v2.seen_scale,
|
|
"v2: scale ne devrait PAS être envoyé (gating conservateur v3+)"
|
|
);
|
|
assert!(v2.seen_done, "v2: done attendu");
|
|
|
|
let v3 = &state.obs[2];
|
|
assert!(v3.seen_geometry, "v3: geometry attendu");
|
|
assert!(v3.seen_mode, "v3: mode attendu");
|
|
assert!(v3.seen_scale, "v3: scale attendu");
|
|
assert!(v3.seen_done, "v3: done attendu");
|
|
assert_eq!(v3.scale_factor, 1, "v3: scale_factor doit être 1");
|
|
assert!(
|
|
v3.mode_flags & 0x1 != 0,
|
|
"v3: mode flags doit contenir CURRENT (0x1)"
|
|
);
|
|
assert!(
|
|
v3.mode_flags & 0x2 != 0,
|
|
"v3: mode flags doit contenir PREFERRED (0x2)"
|
|
);
|
|
}
|