// Integration test: verifies the compositor's Wayland protocol implementation // by starting a real compositor instance and connecting as a client. use std::collections::HashMap; use std::io::{Read, Write}; use std::os::unix::net::UnixStream; use std::process::{Child, Command}; use std::thread; use std::time::Duration; fn push_wayland_string(buf: &mut Vec, value: &str) { let bytes = value.as_bytes(); buf.extend_from_slice(&((bytes.len() + 1) as u32).to_le_bytes()); buf.extend_from_slice(bytes); buf.push(0); while buf.len() % 4 != 0 { buf.push(0); } } fn read_wayland_string(payload: &[u8], cursor: &mut usize) -> String { let length = u32::from_le_bytes([ payload[*cursor], payload[*cursor + 1], payload[*cursor + 2], payload[*cursor + 3], ]) as usize; *cursor += 4; let bytes = &payload[*cursor..*cursor + length]; let string_len = bytes .iter() .position(|byte| *byte == 0) .unwrap_or(bytes.len()); *cursor += length; while *cursor % 4 != 0 { *cursor += 1; } std::str::from_utf8(&bytes[..string_len]) .unwrap() .to_string() } fn collect_globals(client: &mut WaylandClient, registry: u32) -> HashMap { let mut globals = HashMap::new(); for _ in 0..9 { let (object_id, opcode, payload) = client.read_message().expect("read global failed"); assert_eq!(object_id, registry); assert_eq!(opcode, 0); // wl_registry.global let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); let mut cursor = 4; let iface = read_wayland_string(&payload, &mut cursor); let version = u32::from_le_bytes([ payload[cursor], payload[cursor + 1], payload[cursor + 2], payload[cursor + 3], ]); globals.insert(iface, (name, version)); } globals } struct WaylandClient { stream: UnixStream, next_id: u32, } impl WaylandClient { fn connect(socket_path: &str) -> std::io::Result { for _ in 0..20 { if std::path::Path::new(socket_path).exists() { let stream = UnixStream::connect(socket_path)?; stream.set_read_timeout(Some(Duration::from_secs(2)))?; return Ok(Self { stream, next_id: 2 }); } thread::sleep(Duration::from_millis(100)); } Err(std::io::Error::new( std::io::ErrorKind::NotFound, "compositor socket did not appear", )) } fn alloc_id(&mut self) -> u32 { let id = self.next_id; self.next_id += 1; id } fn send_message(&mut self, object_id: u32, opcode: u16, payload: &[u8]) -> std::io::Result<()> { let size = 8 + payload.len(); let mut msg = Vec::with_capacity(size); msg.extend_from_slice(&object_id.to_le_bytes()); let header = ((size as u32) << 16) | opcode as u32; msg.extend_from_slice(&header.to_le_bytes()); msg.extend_from_slice(payload); self.stream.write_all(&msg) } fn read_message(&mut self) -> std::io::Result<(u32, u16, Vec)> { let mut header = [0u8; 8]; self.stream.read_exact(&mut header)?; let object_id = u32::from_le_bytes([header[0], header[1], header[2], header[3]]); let size_opcode = u32::from_le_bytes([header[4], header[5], header[6], header[7]]); let size = ((size_opcode >> 16) & 0xFFFF) as usize; let opcode = (size_opcode & 0xFFFF) as u16; let mut payload = vec![0u8; size - 8]; if size > 8 { self.stream.read_exact(&mut payload)?; } Ok((object_id, opcode, payload)) } fn sync(&mut self) -> std::io::Result { let callback_id = self.alloc_id(); self.send_message(1, 0, &callback_id.to_le_bytes())?; // wl_display.sync Ok(callback_id) } fn get_registry(&mut self) -> std::io::Result { let registry_id = self.alloc_id(); self.send_message(1, 1, ®istry_id.to_le_bytes())?; // wl_display.get_registry Ok(registry_id) } fn bind( &mut self, registry_id: u32, name: u32, iface: &str, version: u32, ) -> std::io::Result { let new_id = self.alloc_id(); let mut payload = Vec::new(); payload.extend_from_slice(&name.to_le_bytes()); push_wayland_string(&mut payload, iface); payload.extend_from_slice(&version.to_le_bytes()); payload.extend_from_slice(&new_id.to_le_bytes()); self.send_message(registry_id, 0, &payload)?; // wl_registry.bind Ok(new_id) } } fn start_compositor(socket_path: &str) -> Child { let compositor_bin = std::env::var("COMPOSITOR_BIN") .unwrap_or_else(|_| "target/debug/redbear-compositor".into()); let runtime_dir = std::path::Path::new(socket_path).parent().unwrap(); std::fs::create_dir_all(runtime_dir).ok(); let mut cmd = Command::new(&compositor_bin); cmd.env( "WAYLAND_DISPLAY", socket_path.rsplit('/').next().unwrap_or("wayland-0"), ) .env("XDG_RUNTIME_DIR", runtime_dir) .env("FRAMEBUFFER_WIDTH", "1280") .env("FRAMEBUFFER_HEIGHT", "720") .env("FRAMEBUFFER_STRIDE", "5120") .env("FRAMEBUFFER_ADDR", "0x80000000"); cmd.spawn().expect("failed to start compositor") } #[test] fn test_compositor_globals() { let socket = "/tmp/test-redbear-compositor.sock"; let _ = std::fs::remove_file(socket); let mut compositor = start_compositor(socket); let mut client = WaylandClient::connect(socket).expect("failed to connect"); // Get registry let _registry = client.get_registry().expect("get_registry failed"); // Read global events let mut globals = Vec::new(); for _ in 0..9 { match client.read_message() { Ok((_obj_id, opcode, payload)) => { assert_eq!(opcode, 0); // wl_registry.global let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); let mut cursor = 4; let iface = read_wayland_string(&payload, &mut cursor); globals.push((name, iface)); } Err(e) => { eprintln!("read error: {}", e); break; } } } assert!( globals.iter().any(|(_, i)| i == "wl_compositor"), "wl_compositor missing" ); assert!(globals.iter().any(|(_, i)| i == "wl_shm"), "wl_shm missing"); assert!( globals.iter().any(|(_, i)| i == "wl_shell"), "wl_shell missing" ); assert!( globals.iter().any(|(_, i)| i == "wl_seat"), "wl_seat missing" ); assert!( globals.iter().any(|(_, i)| i == "wl_output"), "wl_output missing" ); assert!( globals.iter().any(|(_, i)| i == "xdg_wm_base"), "xdg_wm_base missing" ); assert!( globals.iter().any(|(_, i)| i == "wl_fixes"), "wl_fixes missing" ); compositor.kill().ok(); let _ = std::fs::remove_file(socket); } #[test] fn test_compositor_shm_formats() { let socket = "/tmp/test-redbear-compositor-shm.sock"; let _ = std::fs::remove_file(socket); let mut compositor = start_compositor(socket); let mut client = WaylandClient::connect(socket).expect("failed to connect"); let registry = client.get_registry().expect("get_registry failed"); // Read globals to find wl_shm name let mut shm_name = 0u32; for _ in 0..9 { let (_, _, payload) = client.read_message().expect("read failed"); let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); let mut cursor = 4; let iface = read_wayland_string(&payload, &mut cursor); if iface == "wl_shm" { shm_name = name; break; } } assert_ne!(shm_name, 0, "wl_shm global not found"); // Bind wl_shm let _shm = client .bind(registry, shm_name, "wl_shm", 1) .expect("bind shm failed"); // Should receive format events let mut formats = Vec::new(); for _ in 0..3 { match client.read_message() { Ok((_, opcode, payload)) => { if opcode == 0 && payload.len() >= 4 { let format = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); formats.push(format); } } Err(_) => break, } } assert!(!formats.is_empty(), "no wl_shm.format events received"); compositor.kill().ok(); let _ = std::fs::remove_file(socket); } #[test] fn test_compositor_wl_fixes_destroy_registry() { let socket = "/tmp/test-redbear-compositor-wl-fixes.sock"; let _ = std::fs::remove_file(socket); let mut compositor = start_compositor(socket); let mut client = WaylandClient::connect(socket).expect("failed to connect"); let registry = client.get_registry().expect("get_registry failed"); let mut fixes_name = 0u32; for _ in 0..9 { let (_, opcode, payload) = client.read_message().expect("read failed"); assert_eq!(opcode, 0); // wl_registry.global let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); let mut cursor = 4; let iface = read_wayland_string(&payload, &mut cursor); if iface == "wl_fixes" { fixes_name = name; } } assert_ne!(fixes_name, 0, "wl_fixes global not found"); let fixes = client .bind(registry, fixes_name, "wl_fixes", 2) .expect("bind wl_fixes failed"); let removed_global_name = 5u32; // wl_output in the compositor's fixed global table. client .send_message(fixes, 2, &removed_global_name.to_le_bytes()) .expect("wl_fixes.ack_global_remove failed"); client .send_message(fixes, 1, ®istry.to_le_bytes()) .expect("wl_fixes.destroy_registry failed"); let (object_id, opcode, payload) = client.read_message().expect("read delete_id failed"); assert_eq!(object_id, 1, "delete_id must be sent by wl_display"); assert_eq!(opcode, 2, "expected wl_display.delete_id"); assert_eq!(payload, registry.to_le_bytes()); compositor.kill().ok(); let _ = std::fs::remove_file(socket); } #[test] fn test_compositor_output_and_seat_metadata() { let socket = "/tmp/test-redbear-compositor-output-seat.sock"; let _ = std::fs::remove_file(socket); let mut compositor = start_compositor(socket); let mut client = WaylandClient::connect(socket).expect("failed to connect"); let registry = client.get_registry().expect("get_registry failed"); let globals = collect_globals(&mut client, registry); let (output_name, output_version) = *globals.get("wl_output").expect("wl_output missing"); assert!( output_version >= 4, "wl_output must advertise metadata-capable v4" ); let output_id = client .bind(registry, output_name, "wl_output", 4) .expect("bind wl_output failed"); let mut saw_geometry = false; let mut saw_mode = false; let mut saw_scale = false; let mut saw_name = false; let mut saw_description = false; let mut saw_done = false; for _ in 0..6 { let (object_id, opcode, payload) = client.read_message().expect("read output event failed"); assert_eq!(object_id, output_id); match opcode { 0 => saw_geometry = true, 1 => { assert_eq!(payload.len(), 16); saw_mode = true; } 2 => saw_done = true, 3 => { assert_eq!(payload, 1i32.to_le_bytes()); saw_scale = true; } 4 => { let mut cursor = 0; assert_eq!(read_wayland_string(&payload, &mut cursor), "RedBear-0"); saw_name = true; } 5 => { let mut cursor = 0; assert_eq!( read_wayland_string(&payload, &mut cursor), "Red Bear OS framebuffer output" ); saw_description = true; } _ => panic!("unexpected wl_output opcode {opcode}"), } } assert!(saw_geometry && saw_mode && saw_scale && saw_name && saw_description && saw_done); let (seat_name, seat_version) = *globals.get("wl_seat").expect("wl_seat missing"); assert!(seat_version >= 2, "wl_seat must advertise name-capable v2+"); let seat_id = client .bind(registry, seat_name, "wl_seat", 5) .expect("bind wl_seat failed"); let mut saw_capabilities = false; let mut saw_seat_name = false; for _ in 0..2 { let (object_id, opcode, payload) = client.read_message().expect("read seat event failed"); assert_eq!(object_id, seat_id); match opcode { 0 => { assert_eq!(payload, 1u32.to_le_bytes()); saw_capabilities = true; } 1 => { let mut cursor = 0; assert_eq!(read_wayland_string(&payload, &mut cursor), "seat0"); saw_seat_name = true; } _ => panic!("unexpected wl_seat opcode {opcode}"), } } assert!(saw_capabilities && saw_seat_name); client .send_message(output_id, 0, &[]) .expect("wl_output.release failed"); let (object_id, opcode, payload) = client.read_message().expect("read output delete_id failed"); assert_eq!(object_id, 1); assert_eq!(opcode, 2); assert_eq!(payload, output_id.to_le_bytes()); compositor.kill().ok(); let _ = std::fs::remove_file(socket); } #[test] fn test_compositor_real_surface_opcodes() { let socket = "/tmp/test-redbear-compositor-real-surface.sock"; let _ = std::fs::remove_file(socket); let mut compositor = start_compositor(socket); let mut client = WaylandClient::connect(socket).expect("failed to connect"); let registry = client.get_registry().expect("get_registry failed"); let mut globals = HashMap::new(); for _ in 0..9 { let (_, opcode, payload) = client.read_message().expect("read failed"); assert_eq!(opcode, 0); // wl_registry.global let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); let mut cursor = 4; let iface = read_wayland_string(&payload, &mut cursor); globals.insert(iface, name); } let compositor_name = *globals .get("wl_compositor") .expect("wl_compositor global missing"); let compositor_id = client .bind(registry, compositor_name, "wl_compositor", 4) .expect("bind wl_compositor failed"); let surface_id = client.alloc_id(); client .send_message(compositor_id, 0, &surface_id.to_le_bytes()) .expect("wl_compositor.create_surface failed"); let region_id = client.alloc_id(); client .send_message(compositor_id, 1, ®ion_id.to_le_bytes()) .expect("wl_compositor.create_region failed"); client .send_message(surface_id, 4, ®ion_id.to_le_bytes()) .expect("wl_surface.set_opaque_region failed"); client .send_message(region_id, 0, &[]) .expect("wl_region.destroy failed"); let (object_id, opcode, payload) = client.read_message().expect("read region delete_id failed"); assert_eq!(object_id, 1); assert_eq!(opcode, 2); assert_eq!(payload, region_id.to_le_bytes()); let callback_id = client.alloc_id(); client .send_message(surface_id, 3, &callback_id.to_le_bytes()) .expect("wl_surface.frame failed"); let (object_id, opcode, payload) = client.read_message().expect("read frame callback failed"); assert_eq!(object_id, callback_id); assert_eq!(opcode, 0); assert_eq!(payload.len(), 4); client .send_message(surface_id, 6, &[]) .expect("wl_surface.commit failed"); client .send_message(surface_id, 0, &[]) .expect("wl_surface.destroy failed"); let (object_id, opcode, payload) = client .read_message() .expect("read surface delete_id failed"); assert_eq!(object_id, 1); assert_eq!(opcode, 2); assert_eq!(payload, surface_id.to_le_bytes()); compositor.kill().ok(); let _ = std::fs::remove_file(socket); } #[test] fn test_compositor_xdg_popup_lifecycle() { let socket = "/tmp/test-redbear-compositor-xdg-popup.sock"; let _ = std::fs::remove_file(socket); let mut compositor = start_compositor(socket); let mut client = WaylandClient::connect(socket).expect("failed to connect"); let registry = client.get_registry().expect("get_registry failed"); let mut globals = HashMap::new(); for _ in 0..9 { let (_, opcode, payload) = client.read_message().expect("read failed"); assert_eq!(opcode, 0); // wl_registry.global let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); let mut cursor = 4; let iface = read_wayland_string(&payload, &mut cursor); globals.insert(iface, name); } let compositor_name = *globals .get("wl_compositor") .expect("wl_compositor global missing"); let xdg_name = *globals.get("xdg_wm_base").expect("xdg_wm_base missing"); let compositor_id = client .bind(registry, compositor_name, "wl_compositor", 4) .expect("bind wl_compositor failed"); let xdg_wm_base_id = client .bind(registry, xdg_name, "xdg_wm_base", 1) .expect("bind xdg_wm_base failed"); let parent_surface = client.alloc_id(); client .send_message(compositor_id, 0, &parent_surface.to_le_bytes()) .expect("create parent surface failed"); let parent_xdg = client.alloc_id(); let mut payload = Vec::new(); payload.extend_from_slice(&parent_xdg.to_le_bytes()); payload.extend_from_slice(&parent_surface.to_le_bytes()); client .send_message(xdg_wm_base_id, 2, &payload) .expect("get parent xdg_surface failed"); let positioner = client.alloc_id(); client .send_message(xdg_wm_base_id, 1, &positioner.to_le_bytes()) .expect("create_positioner failed"); let mut payload = Vec::new(); payload.extend_from_slice(&64i32.to_le_bytes()); payload.extend_from_slice(&32i32.to_le_bytes()); client .send_message(positioner, 1, &payload) .expect("positioner.set_size failed"); let popup_surface = client.alloc_id(); client .send_message(compositor_id, 0, &popup_surface.to_le_bytes()) .expect("create popup surface failed"); let popup_xdg = client.alloc_id(); let mut payload = Vec::new(); payload.extend_from_slice(&popup_xdg.to_le_bytes()); payload.extend_from_slice(&popup_surface.to_le_bytes()); client .send_message(xdg_wm_base_id, 2, &payload) .expect("get popup xdg_surface failed"); let popup = client.alloc_id(); let mut payload = Vec::new(); payload.extend_from_slice(&popup.to_le_bytes()); payload.extend_from_slice(&parent_xdg.to_le_bytes()); payload.extend_from_slice(&positioner.to_le_bytes()); client .send_message(popup_xdg, 2, &payload) .expect("xdg_surface.get_popup failed"); let (object_id, opcode, payload) = client.read_message().expect("read popup configure failed"); assert_eq!(object_id, popup); assert_eq!(opcode, 0); assert_eq!(payload.len(), 16); let (object_id, opcode, payload) = client .read_message() .expect("read popup surface configure failed"); assert_eq!(object_id, popup_xdg); assert_eq!(opcode, 0); assert_eq!(payload.len(), 4); client .send_message(popup, 0, &[]) .expect("xdg_popup.destroy failed"); let (object_id, opcode, payload) = client.read_message().expect("read popup delete_id failed"); assert_eq!(object_id, 1); assert_eq!(opcode, 2); assert_eq!(payload, popup.to_le_bytes()); client .send_message(positioner, 0, &[]) .expect("xdg_positioner.destroy failed"); let (object_id, opcode, payload) = client .read_message() .expect("read positioner delete_id failed"); assert_eq!(object_id, 1); assert_eq!(opcode, 2); assert_eq!(payload, positioner.to_le_bytes()); compositor.kill().ok(); let _ = std::fs::remove_file(socket); } #[test] fn test_compositor_sync_roundtrip() { let socket = "/tmp/test-redbear-compositor-sync.sock"; let _ = std::fs::remove_file(socket); let mut compositor = start_compositor(socket); let mut client = WaylandClient::connect(socket).expect("failed to connect"); let callback_id = client.sync().expect("sync failed"); // Should receive callback.done let (obj_id, opcode, payload) = client.read_message().expect("read failed"); assert_eq!(obj_id, callback_id, "callback id mismatch"); assert_eq!(opcode, 0, "expected callback.done (opcode 0)"); assert_eq!(payload.len(), 4, "callback.done payload should be 4 bytes"); compositor.kill().ok(); let _ = std::fs::remove_file(socket); }