1
0
Fork 0
forked from wry/wry

Merge pull request #591 from mahkoh/jorth/more-it-tests

Add more integration tests
This commit is contained in:
mahkoh 2025-09-03 17:55:21 +02:00 committed by GitHub
commit 47a09af8f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 508 additions and 7 deletions

View file

@ -27,7 +27,7 @@ use {
state::State,
udmabuf::Udmabuf,
utils::{
clonecell::CloneCell, copyhashmap::CopyHashMap, errorfmt::ErrorFmt,
clonecell::CloneCell, copyhashmap::CopyHashMap, errorfmt::ErrorFmt, numcell::NumCell,
on_change::OnChange, oserror::OsError, syncqueue::SyncQueue,
},
video::{
@ -84,6 +84,7 @@ impl TestBackend {
events: Default::default(),
feedback: Default::default(),
idle: Default::default(),
damage_calls: NumCell::new(0),
});
let default_mouse = Rc::new(TestBackendMouse {
common: TestInputDeviceCommon {
@ -318,6 +319,7 @@ pub struct TestConnector {
pub events: OnChange<ConnectorEvent>,
pub feedback: CloneCell<Option<Rc<DrmFeedback>>>,
pub idle: TEEH<bool>,
pub damage_calls: NumCell<u32>,
}
impl Connector for TestConnector {
@ -338,7 +340,7 @@ impl Connector for TestConnector {
}
fn damage(&self) {
// nothing
self.damage_calls.fetch_add(1);
}
fn drm_dev(&self) -> Option<DrmDeviceId> {

View file

@ -8,9 +8,10 @@ use {
test_ifs::{
test_compositor::TestCompositor, test_cursor_shape_manager::TestCursorShapeManager,
test_data_device_manager::TestDataDeviceManager,
test_jay_compositor::TestJayCompositor, test_keyboard::TestKeyboard,
test_pointer::TestPointer, test_registry::TestRegistry, test_seat::TestSeat,
test_shm::TestShm, test_single_pixel_buffer_manager::TestSinglePixelBufferManager,
test_fifo_manager::TestFifoManager, test_jay_compositor::TestJayCompositor,
test_keyboard::TestKeyboard, test_pointer::TestPointer,
test_registry::TestRegistry, test_seat::TestSeat, test_shm::TestShm,
test_single_pixel_buffer_manager::TestSinglePixelBufferManager,
test_subcompositor::TestSubcompositor, test_viewporter::TestViewporter,
test_xdg_activation::TestXdgActivation, test_xdg_base::TestXdgWmBase,
},
@ -38,6 +39,7 @@ pub struct TestClient {
pub activation: Rc<TestXdgActivation>,
pub data_device_manager: Rc<TestDataDeviceManager>,
pub cursor_shape_manager: Rc<TestCursorShapeManager>,
pub fifo_manager: Rc<TestFifoManager>,
}
pub struct DefaultSeat {

View file

@ -20,6 +20,7 @@ pub mod test_dmabuf;
pub mod test_dmabuf_feedback;
pub mod test_ext_foreign_toplevel_handle;
pub mod test_ext_foreign_toplevel_list;
pub mod test_fifo_manager;
pub mod test_input_method;
pub mod test_input_method_keyboard_grab;
pub mod test_input_method_manager;

View file

@ -0,0 +1,76 @@
use {
crate::{
it::{
test_error::TestError, test_ifs::test_surface::TestSurface, test_object::TestObject,
test_transport::TestTransport,
},
wire::{WpFifoManagerV1Id, WpFifoV1Id, wp_fifo_manager_v1::*, wp_fifo_v1},
},
std::rc::Rc,
};
pub struct TestFifoManager {
pub id: WpFifoManagerV1Id,
pub tran: Rc<TestTransport>,
}
pub struct TestFifo {
pub id: WpFifoV1Id,
pub tran: Rc<TestTransport>,
}
impl TestFifoManager {
pub fn new(tran: &Rc<TestTransport>) -> Self {
Self {
id: tran.id(),
tran: tran.clone(),
}
}
pub fn get_fifo(&self, surface: &TestSurface) -> Result<Rc<TestFifo>, TestError> {
let obj = Rc::new(TestFifo {
id: self.tran.id(),
tran: self.tran.clone(),
});
self.tran.send(GetFifo {
self_id: self.id,
id: obj.id,
surface: surface.id,
})?;
self.tran.add_obj(obj.clone())?;
Ok(obj)
}
#[expect(dead_code)]
pub fn destroy(&self) -> Result<(), TestError> {
self.tran.send(Destroy { self_id: self.id })?;
Ok(())
}
}
impl TestFifo {
pub fn set_barrier(&self) -> Result<(), TestError> {
self.tran.send(wp_fifo_v1::SetBarrier { self_id: self.id })
}
pub fn wait_barrier(&self) -> Result<(), TestError> {
self.tran.send(wp_fifo_v1::WaitBarrier { self_id: self.id })
}
#[expect(dead_code)]
pub fn destroy(&self) -> Result<(), TestError> {
self.tran.send(wp_fifo_v1::Destroy { self_id: self.id })?;
Ok(())
}
}
test_object! {
TestFifoManager, WpFifoManagerV1;
}
test_object! {
TestFifo, WpFifoV1;
}
impl TestObject for TestFifoManager {}
impl TestObject for TestFifo {}

View file

@ -11,6 +11,7 @@ use {
test_data_control_manager::TestDataControlManager,
test_data_device_manager::TestDataDeviceManager, test_dmabuf::TestDmabuf,
test_ext_foreign_toplevel_list::TestExtForeignToplevelList,
test_fifo_manager::TestFifoManager,
test_input_method_manager::TestInputMethodManager,
test_jay_compositor::TestJayCompositor, test_shm::TestShm,
test_single_pixel_buffer_manager::TestSinglePixelBufferManager,
@ -60,6 +61,7 @@ pub struct TestRegistrySingletons {
pub zwp_input_method_manager_v2: u32,
pub zwp_text_input_manager_v3: u32,
pub wl_fixes: u32,
pub wp_fifo_manager_v1: u32,
}
pub struct TestRegistry {
@ -88,6 +90,7 @@ pub struct TestRegistry {
pub input_method_manager: CloneCell<Option<Rc<TestInputMethodManager>>>,
pub text_input_manager: CloneCell<Option<Rc<TestTextInputManager>>>,
pub wl_fixes: CloneCell<Option<Rc<TestWlFixes>>>,
pub fifo_manager: CloneCell<Option<Rc<TestFifoManager>>>,
pub seats: CopyHashMap<GlobalName, Rc<WlSeatGlobal>>,
}
@ -160,6 +163,7 @@ impl TestRegistry {
zwp_input_method_manager_v2,
zwp_text_input_manager_v3,
wl_fixes,
wp_fifo_manager_v1,
};
self.singletons.set(Some(singletons.clone()));
Ok(singletons)
@ -276,6 +280,13 @@ impl TestRegistry {
TestTextInputManager
);
create_singleton!(get_wl_fixes, wl_fixes, wl_fixes, 1, TestWlFixes);
create_singleton!(
get_fifo_manager,
fifo_manager,
wp_fifo_manager_v1,
1,
TestFifoManager
);
pub fn bind<O: TestObject>(
&self,

View file

@ -3,14 +3,14 @@ use {
ifs::wl_surface::WlSurface,
it::{
test_error::{TestError, TestResult},
test_ifs::test_region::TestRegion,
test_ifs::{test_callback::TestCallback, test_region::TestRegion},
test_object::TestObject,
test_transport::TestTransport,
test_utils::test_expected_event::TEEH,
testrun::ParseFull,
},
utils::buffd::MsgParser,
wire::{WlBufferId, WlSurfaceId, wl_surface::*},
wire::{WlBufferId, WlCallbackId, WlSurfaceId, wl_surface::*},
},
std::{cell::Cell, rc::Rc},
};
@ -89,6 +89,22 @@ impl TestSurface {
Ok(())
}
pub fn frame(&self) -> Result<Rc<TestCallback>, TestError> {
let id: WlCallbackId = self.tran.id();
let callback = Rc::new(TestCallback {
id,
_tran: self.tran.clone(),
handler: Cell::new(None),
done: Cell::new(false),
});
self.tran.add_obj(callback.clone())?;
self.tran.send(Frame {
self_id: self.id,
callback: callback.id,
})?;
Ok(callback)
}
pub fn commit(&self) -> Result<(), TestError> {
self.tran.send(Commit { self_id: self.id })?;
Ok(())

View file

@ -74,6 +74,7 @@ impl TestTransport {
input_method_manager: Default::default(),
text_input_manager: Default::default(),
wl_fixes: Default::default(),
fifo_manager: Default::default(),
seats: Default::default(),
});
self.send(wl_display::GetRegistry {

View file

@ -91,6 +91,7 @@ impl TestRun {
activation: registry.get_activation().await?,
data_device_manager: registry.get_data_device_manager().await?,
cursor_shape_manager: registry.get_cursor_shape_manager().await?,
fifo_manager: registry.get_fifo_manager().await?,
registry,
}))
}

View file

@ -78,6 +78,9 @@ mod t0044_stacked_focus;
mod t0045_content_type;
mod t0046_buffer_release;
mod t0047_surface_damage;
mod t0048_frame_callback;
mod t0049_surface_damage_backend;
mod t0050_fifo;
pub trait TestCase: Sync {
fn name(&self) -> &'static str;
@ -144,5 +147,8 @@ pub fn tests() -> Vec<&'static dyn TestCase> {
t0045_content_type,
t0046_buffer_release,
t0047_surface_damage,
t0048_frame_callback,
t0049_surface_damage_backend,
t0050_fifo,
}
}

View file

@ -7,6 +7,7 @@ use {
format::XRGB8888,
ifs::wl_output::OutputId,
it::{test_backend::TestConnector, test_error::TestResult, testrun::TestRun},
utils::numcell::NumCell,
video::drm::ConnectorType,
},
std::rc::Rc,
@ -35,6 +36,7 @@ async fn test(run: Rc<TestRun>) -> TestResult {
events: Default::default(),
feedback: Default::default(),
idle: Default::default(),
damage_calls: NumCell::new(0),
});
let new_monitor_info = MonitorInfo {
modes: vec![],

View file

@ -0,0 +1,91 @@
use {
crate::it::{test_error::TestResult, testrun::TestRun},
std::rc::Rc,
};
testcase!();
/// Test wl_surface.frame callbacks
/// This test verifies that the compositor sends wl_callback.done events after the vblank event.
/// According to the Wayland protocol, frame callbacks should be fired to indicate when
/// it's a good time to start drawing the next frame, and they should be posted after
/// the compositor has finished presenting the previous frame (i.e., after vblank).
async fn test(run: Rc<TestRun>) -> TestResult {
run.backend.install_default()?;
let client = run.create_client().await?;
// Create a visible window so frame callbacks can be triggered
let window = client.create_window().await?;
window.map().await?;
client.sync().await;
// Test 1: Basic frame callback functionality
let surface = &window.surface.surface;
let callback1 = surface.frame()?;
surface.commit()?;
client.sync().await;
// Manually trigger vblank event to simulate frame completion
let connector_id = run.backend.default_connector.id;
// Trigger vblank manually - this processes frame callbacks
run.state.vblank(connector_id);
client.sync().await;
// The frame callback should have been fired after vblank
tassert!(callback1.done.get());
// Test 2: Multiple frame callbacks
let callback2 = surface.frame()?;
let callback3 = surface.frame()?;
surface.commit()?;
client.sync().await;
// Before triggering vblank, callbacks should not be done yet
tassert!(!callback2.done.get());
tassert!(!callback3.done.get());
// Trigger vblank manually - this processes frame callbacks
run.state.vblank(connector_id);
client.sync().await;
// Both callbacks should be done after vblank
tassert!(callback2.done.get());
tassert!(callback3.done.get());
// Test 3: Frame callbacks on invisible surface should not be processed
// Create a new surface but don't make it visible
let invisible_surface = client.comp.create_surface().await?;
let buffer = client
.spbm
.create_buffer(crate::theme::Color::from_srgb(255, 0, 0))?;
invisible_surface.attach(buffer.id)?;
let callback_invisible = invisible_surface.frame()?;
invisible_surface.commit()?;
client.sync().await;
// Trigger vblank manually - this processes frame callbacks
run.state.vblank(connector_id);
client.sync().await;
// Frame callback on invisible surface should not be processed
tassert!(!callback_invisible.done.get());
// Test 4: Frame callback timing - verify they happen after vblank
let callback_timing = surface.frame()?;
surface.commit()?;
client.sync().await;
// The callback should not be done immediately after commit
tassert!(!callback_timing.done.get());
// Trigger vblank manually - this processes frame callbacks
run.state.vblank(connector_id);
client.sync().await;
// Now it should be done
tassert!(callback_timing.done.get());
Ok(())
}

View file

@ -0,0 +1,125 @@
use {
crate::it::{test_error::TestResult, testrun::TestRun},
std::rc::Rc,
};
testcase!();
/// Test that committing damage on a visible surface causes the backend connector to be damaged.
/// This test verifies that surface damage triggers backend connector damage tracking AND
/// that the frontend actually calls into the backend connector's damage method,
/// ensuring the rendering pipeline knows when to update the display.
async fn test(run: Rc<TestRun>) -> TestResult {
run.backend.install_default()?;
let client = run.create_client().await?;
// Get connector for tracking backend damage state
let connector_id = run.backend.default_connector.id;
let connector_data = run.state.connectors.get(&connector_id).unwrap();
// Create a visible window with mapped surface
let window = client.create_window().await?;
let buffer = client
.spbm
.create_buffer(crate::theme::Color::from_srgb(0, 255, 0))?;
window.surface.attach(buffer.id)?;
window.map().await?;
client.sync().await;
// Test 1: Ensure initially the backend is not damaged
connector_data.damaged.set(false);
run.backend.default_connector.damage_calls.set(0);
tassert!(!connector_data.damaged.get());
tassert_eq!(run.backend.default_connector.damage_calls.get(), 0);
// Test 2: Add surface damage and commit - this should trigger backend connector damage
window.surface.damage(10, 10, 50, 50)?;
window.surface.commit()?;
client.sync().await;
// Critical test: Verify the backend connector is now damaged AND the backend method was called
tassert!(connector_data.damaged.get());
tassert!(run.backend.default_connector.damage_calls.get() > 0);
// Test 3: Reset damage state and test buffer damage
connector_data.damaged.set(false);
let previous_calls = run.backend.default_connector.damage_calls.get();
tassert!(!connector_data.damaged.get());
// Add buffer damage and commit - this should also trigger backend connector damage
window.surface.damage_buffer(0, 0, 1, 1)?; // Damage entire 1x1 buffer
window.surface.commit()?;
client.sync().await;
// Verify the backend connector is damaged again AND more backend calls were made
tassert!(connector_data.damaged.get());
tassert!(run.backend.default_connector.damage_calls.get() > previous_calls);
// Test 4: Test that invisible surfaces do not trigger backend damage
let invisible_surface = client.comp.create_surface().await?;
let invisible_buffer = client
.spbm
.create_buffer(crate::theme::Color::from_srgb(255, 255, 0))?;
invisible_surface.attach(invisible_buffer.id)?;
invisible_surface.commit()?; // Initial commit to attach buffer
client.sync().await;
// Reset damage state
connector_data.damaged.set(false);
let invisible_calls_before = run.backend.default_connector.damage_calls.get();
tassert!(!connector_data.damaged.get());
// Add damage to invisible surface and commit
invisible_surface.damage(20, 20, 30, 30)?;
invisible_surface.commit()?;
client.sync().await;
// Invisible surface damage should NOT trigger backend connector damage or backend calls
tassert!(!connector_data.damaged.get());
tassert_eq!(
run.backend.default_connector.damage_calls.get(),
invisible_calls_before
);
// Test 5: Test multiple damage areas on visible surface
connector_data.damaged.set(false);
let multi_calls_before = run.backend.default_connector.damage_calls.get();
tassert!(!connector_data.damaged.get());
// Add multiple damage rectangles to visible surface
window.surface.damage(5, 5, 10, 10)?;
window.surface.damage(25, 25, 15, 15)?;
window.surface.damage_buffer(0, 0, 1, 1)?;
window.surface.commit()?;
client.sync().await;
// Multiple damage areas on visible surface should trigger backend connector damage and calls
tassert!(connector_data.damaged.get());
tassert!(run.backend.default_connector.damage_calls.get() > multi_calls_before);
// Test 6: Test that damage without commit does not trigger backend damage
connector_data.damaged.set(false);
let no_commit_calls_before = run.backend.default_connector.damage_calls.get();
tassert!(!connector_data.damaged.get());
// Add damage but don't commit
window.surface.damage(40, 40, 20, 20)?;
client.sync().await;
// Damage without commit should NOT trigger backend connector damage or backend calls
tassert!(!connector_data.damaged.get());
tassert_eq!(
run.backend.default_connector.damage_calls.get(),
no_commit_calls_before
);
// Now commit the pending damage
window.surface.commit()?;
client.sync().await;
// After commit, backend connector should be damaged and backend called
tassert!(connector_data.damaged.get());
tassert!(run.backend.default_connector.damage_calls.get() > no_commit_calls_before);
Ok(())
}

153
src/it/tests/t0050_fifo.rs Normal file
View file

@ -0,0 +1,153 @@
use {
crate::{
it::{
test_error::{TestError, TestResult},
testrun::TestRun,
},
theme::Color,
},
std::rc::Rc,
};
testcase!();
/// Test wp_fifo_v1 protocol implementation
/// This test verifies that the compositor correctly implements the fifo-v1 protocol
/// for synchronizing surface updates with display refresh cycles.
async fn test(run: Rc<TestRun>) -> TestResult {
run.backend.install_default()?;
let client = run.create_client().await?;
// Create a visible window so fifo constraints can be tested
let window = client.create_window().await?;
window.map().await?;
client.sync().await;
let fifo_manager = &client.fifo_manager;
let surface = &window.surface.surface;
let connector_id = run.backend.default_connector.id;
// Get a fifo object for the surface
let fifo = fifo_manager.get_fifo(surface)?;
client.sync().await;
// Test 1: Basic fifo barrier functionality without wait_barrier
// This should not block the commit - the old buffer should be released immediately
fifo.set_barrier()?;
let buffer1 = client.spbm.create_buffer(Color::from_srgb(255, 0, 0))?;
surface.attach(buffer1.id)?;
surface.commit()?;
client.sync().await;
// Test 1a: Critical test - without wait_barrier, commit should be applied immediately
let buffer1a = client.spbm.create_buffer(Color::from_srgb(255, 128, 0))?;
// Reset buffer tracking to detect when buffer1 gets released
buffer1.released.set(false);
// Attach new buffer and commit WITHOUT wait_barrier
surface.attach(buffer1a.id)?;
surface.commit()?;
client.sync().await;
// WITHOUT wait_barrier, the commit should be applied immediately even if barrier is set
// So buffer1 should be released immediately, without needing a vblank
if !buffer1.released.get() {
return Err(TestError::new(
"Without wait_barrier, commit should be applied immediately - buffer1 was not released",
));
}
// Test 2: Critical test - wait_barrier SHOULD block commit until next after_latch
// This contrasts with Test 1a above where commits were applied immediately
// Set the barrier and immediately test wait_barrier (without intermediate commits that might clear it)
fifo.set_barrier()?;
fifo.wait_barrier()?;
let buffer2_current = client.spbm.create_buffer(Color::from_srgb(0, 255, 0))?;
let buffer2_next = client.spbm.create_buffer(Color::from_srgb(0, 0, 255))?;
// First attach current buffer
surface.attach(buffer2_current.id)?;
surface.commit()?;
client.sync().await;
// Reset tracking for the buffer we want to monitor
buffer2_current.released.set(false);
// Now attach the new buffer - this should trigger the wait_barrier blocking
surface.attach(buffer2_next.id)?;
surface.commit()?;
client.sync().await;
// CRITICAL: The commit should be blocked, so buffer2_current should NOT be released yet
if buffer2_current.released.get() {
return Err(TestError::new(
"wait_barrier did not block the commit - buffer2_current was released immediately",
));
}
// The commit was successfully blocked! This proves wait_barrier works.
// Now trigger after_latch to clear the barrier and apply the queued commit
run.state.latch(connector_id);
client.sync().await;
// After after_latch, the barrier should be cleared and the commit applied
if !buffer2_current.released.get() {
return Err(TestError::new(
"after_latch should have cleared barrier and applied commit - buffer2_current was not released",
));
}
// Test 3: Test tearing mode - barrier should be cleared on vblank instead of immediately
fifo.set_barrier()?;
fifo.wait_barrier()?;
let buffer3_current = client.spbm.create_buffer(Color::from_srgb(128, 128, 0))?;
let buffer3_next = client.spbm.create_buffer(Color::from_srgb(0, 128, 128))?;
// First attach current buffer
surface.attach(buffer3_current.id)?;
surface.commit()?;
client.sync().await;
// Reset tracking for the buffer we want to monitor
buffer3_current.released.set(false);
// Now attach the new buffer - this should trigger the wait_barrier blocking
surface.attach(buffer3_next.id)?;
surface.commit()?;
client.sync().await;
// Verify the commit is blocked
if buffer3_current.released.get() {
return Err(TestError::new(
"wait_barrier did not block the commit in tearing test - buffer3_current was released immediately",
));
}
// Trigger latch with tearing=true - this should defer clearing to vblank
run.state.latch_tearing(connector_id);
client.sync().await;
// With tearing=true, the barrier should NOT be cleared yet, commit should still be blocked
if buffer3_current.released.get() {
return Err(TestError::new(
"In tearing mode, latch should not clear barrier immediately - buffer3_current was released",
));
}
// Now trigger vblank - this should clear the barrier and apply the commit
run.state.vblank(connector_id);
client.sync().await;
// After vblank, the commit should be applied
if !buffer3_current.released.get() {
return Err(TestError::new(
"vblank should have cleared barrier and applied commit - buffer3_current was not released",
));
}
Ok(())
}

View file

@ -1445,6 +1445,20 @@ impl State {
}
}
#[cfg(feature = "it")]
pub fn latch(&self, connector: ConnectorId) {
if let Some(output) = self.root.outputs.get(&connector) {
output.latched(false);
}
}
#[cfg(feature = "it")]
pub fn latch_tearing(&self, connector: ConnectorId) {
if let Some(output) = self.root.outputs.get(&connector) {
output.latched(true);
}
}
#[cfg(feature = "it")]
pub async fn idle(&self) {
loop {