Files
feresaria/compositor/src/session_lock/tests.rs
T

629 lines
22 KiB
Rust

use super::*;
#[test]
fn dim_fade_respects_speed_without_changing_idle_deadlines() {
let policy = IdleSettings::default();
for (duration, half_at) in [(250, 30_125), (1000, 30_500)] {
let fade = Duration::from_millis(duration);
assert_eq!(policy.appearance(Duration::from_secs(29), fade), (0.0, false));
let halfway = policy.appearance(Duration::from_millis(half_at), fade);
assert!((halfway.0 - 0.325).abs() < 0.001 && !halfway.1);
assert_eq!(
policy.next_deadline(Duration::from_millis(half_at), fade),
Some(Duration::from_millis(16))
);
assert_eq!(policy.appearance(Duration::from_secs(120), fade), (1.0, true));
}
assert_eq!(
policy.appearance(Duration::from_secs(30), Duration::ZERO),
(0.65, false)
);
assert_eq!(policy.appearance(Duration::from_secs(120), Duration::ZERO), (1.0, true));
}
#[test]
fn idle_policy_fades_then_sleeps_and_activity_resets_it() {
let policy = IdleSettings::default();
assert_eq!(
policy.appearance(Duration::from_secs(29), Duration::from_millis(500)),
(0.0, false)
);
let halfway = policy.appearance(Duration::from_millis(30_250), Duration::from_millis(500));
assert!((halfway.0 - 0.325).abs() < 0.001 && !halfway.1);
assert_eq!(
policy.appearance(Duration::from_secs(31), Duration::from_millis(500)),
(0.65, false)
);
assert_eq!(
policy.appearance(Duration::from_secs(120), Duration::from_millis(500)),
(1.0, true)
);
assert_eq!(
policy.appearance(Duration::ZERO, Duration::from_millis(500)),
(0.0, false)
);
assert_eq!(
policy.next_deadline(Duration::from_millis(29_998), Duration::from_millis(500)),
Some(Duration::from_millis(2))
);
assert_eq!(
policy.next_deadline(Duration::from_secs(30), Duration::from_millis(500)),
Some(Duration::from_millis(16))
);
assert_eq!(
policy.next_deadline(Duration::from_secs(30), Duration::ZERO),
Some(Duration::from_secs(90))
);
assert_eq!(policy.next_deadline(Duration::from_secs(120), Duration::ZERO), None);
assert_eq!(
IdleSettings {
dim_after_seconds: 0,
sleep_after_seconds: 0
}
.next_deadline(Duration::ZERO, Duration::ZERO),
None
);
}
#[test]
fn mirror_must_present_protected_frame_before_lock_is_ready() {
let make_output = |name: &str| {
Output::new(
name.into(),
smithay::output::PhysicalProperties {
size: (0, 0).into(),
subpixel: smithay::output::Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
)
};
let source = make_output("source");
let mirror = make_output("mirror");
let mut lock = Lock {
lifecycle: Lifecycle::Orphaned,
..Lock::default()
};
lock.presented.insert(source.clone());
let displays = [&source, &mirror];
assert!(
!lock.ready_for_idle(displays.into_iter()),
"failed mirror rendering must block confirmation"
);
lock.presented.insert(mirror.clone());
assert!(lock.ready_for_idle(displays.into_iter()));
lock.output_added(&mirror);
assert!(
!lock.ready_for_idle(displays.into_iter()),
"reconfiguration invalidates physical protection"
);
lock.output_removed(&mirror);
assert!(lock.ready_for_idle(std::iter::once(&source)));
}
#[test]
fn protected_fallback_frames_allow_idle_after_early_locker_crash() {
let output = Output::new(
"test".into(),
smithay::output::PhysicalProperties {
size: (0, 0).into(),
subpixel: smithay::output::Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
let mut lock = Lock {
lifecycle: Lifecycle::Orphaned,
..Lock::default()
};
assert!(!lock.ready_for_idle(std::iter::once(&output)));
lock.presented.insert(output.clone());
assert!(lock.ready_for_idle(std::iter::once(&output)));
assert!(matches!(lock.lifecycle, Lifecycle::Orphaned));
lock.output_added(&output);
assert!(!lock.ready_for_idle(std::iter::once(&output)));
assert!(matches!(lock.lifecycle, Lifecycle::Orphaned));
}
#[test]
fn input_wakes_a_sleeping_orphan_without_unlocking() {
let mut lock = Lock {
lifecycle: Lifecycle::Orphaned,
sleeping: true,
idle_opacity: 1.0,
..Lock::default()
};
let now = Instant::now();
assert!(lock.activity(now));
assert!(lock.active() && matches!(lock.lifecycle, Lifecycle::Orphaned));
assert!(!lock.sleeping);
assert_eq!(lock.idle_opacity, 0.0);
assert_eq!(lock.idle_since, Some(now));
assert!(!lock.activity(now));
let mut unlocked = Lock::default();
assert!(!unlocked.activity(now));
assert!(unlocked.idle_since.is_none());
}
#[test]
fn idle_disabled_options_are_independent() {
assert_eq!(
IdleSettings {
dim_after_seconds: 0,
sleep_after_seconds: 0
}
.appearance(Duration::from_secs(9999), Duration::from_millis(500)),
(0.0, false)
);
assert_eq!(
IdleSettings {
dim_after_seconds: 0,
sleep_after_seconds: 10
}
.appearance(Duration::from_secs(10), Duration::from_millis(500)),
(1.0, true)
);
assert_eq!(
IdleSettings {
dim_after_seconds: 1,
sleep_after_seconds: 0
}
.appearance(Duration::from_secs(9999), Duration::from_millis(500)),
(0.65, false)
);
assert!(
IdleSettings {
dim_after_seconds: 86401,
sleep_after_seconds: 0
}
.validate()
.is_err()
);
assert!(crate::config::Config::parse_source("lock-screen { dim-after-seconds -1; }").is_err());
assert!(
crate::config::Config::parse_source("lock-screen { sleep-after-seconds 90000; }")
.unwrap()
.runtime_config()
.is_err()
);
}
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::sync::Arc;
use calloop::EventLoop;
use smithay::reexports::wayland_server::Client;
use smithay::reexports::wayland_server::protocol::wl_compositor::WlCompositor;
struct Fixture {
events: EventLoop<'static, Ferese>,
state: Ferese,
}
impl Fixture {
fn new(test: &str) -> Option<Self> {
if !crate::startup_tests::private_runtime(test) {
return None;
}
let mut events = EventLoop::try_new().unwrap();
let state = crate::startup_tests::state(&mut events);
Some(Self { events, state })
}
fn dispatch(&mut self) {
self.events.dispatch(Duration::from_millis(5), &mut self.state).unwrap();
crate::after_dispatch(&mut self.state);
}
fn output(&mut self, name: &str) -> Output {
let output = Output::new(
name.into(),
smithay::output::PhysicalProperties {
size: (0, 0).into(),
subpixel: smithay::output::Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
output.change_current_state(
Some(smithay::output::Mode {
size: (800, 600).into(),
refresh: 60_000,
}),
None,
None,
None,
);
self.state.space.map_output(&output, (0, 0));
self.state.register_output(&output, name.into());
output
}
fn acquire(&mut self) -> Locker {
let (server, mut wire) = UnixStream::pair().unwrap();
let client = self
.state
.display_handle
.insert_client(server, Arc::new(crate::state::ClientState::default()))
.unwrap();
let manager = client
.create_resource::<ExtSessionLockManagerV1, (), Ferese>(&self.state.display_handle, 1, ())
.unwrap();
request(&mut wire, manager.id().protocol_id(), 1, &[2]);
self.dispatch();
Locker {
wire,
client,
next_id: 3,
}
}
}
struct Locker {
wire: UnixStream,
client: Client,
next_id: u32,
}
impl Locker {
fn id(&mut self) -> u32 {
let id = self.next_id;
self.next_id += 1;
id
}
fn expect_locked(&mut self) {
self.wire.set_read_timeout(Some(Duration::from_secs(1))).unwrap();
let mut event = [0u8; 8];
self.wire.read_exact(&mut event).unwrap();
assert_eq!(u32::from_ne_bytes(event[..4].try_into().unwrap()), 2);
assert_eq!(u32::from_ne_bytes(event[4..].try_into().unwrap()), 8u32 << 16);
}
fn surface(&mut self, fixture: &mut Fixture, output: &Output) -> LockSurface {
output.create_global::<Ferese>(&fixture.state.display_handle);
let registry = self.id();
request(&mut self.wire, 1, 1, &[registry]);
fixture.dispatch();
self.wire.set_nonblocking(true).unwrap();
let mut events = Vec::new();
let _ = self.wire.read_to_end(&mut events);
self.wire.set_nonblocking(false).unwrap();
let mut offset = 0;
let mut output_name = None;
while offset < events.len() {
let object = u32::from_ne_bytes(events[offset..offset + 4].try_into().unwrap());
let header = u32::from_ne_bytes(events[offset + 4..offset + 8].try_into().unwrap());
if object == registry && header & 0xffff == 0 {
let len = u32::from_ne_bytes(events[offset + 12..offset + 16].try_into().unwrap()) as usize;
if &events[offset + 16..offset + 16 + len] == b"wl_output\0" {
let name = u32::from_ne_bytes(events[offset + 8..offset + 12].try_into().unwrap());
output_name = Some(output_name.map_or(name, |previous: u32| previous.max(name)));
}
}
offset += (header >> 16) as usize;
}
let output_id = self.id();
let mut args = vec![output_name.expect("output global"), 10];
args.extend(
b"wl_output\0\0\0"
.chunks_exact(4)
.map(|bytes| u32::from_ne_bytes(bytes.try_into().unwrap())),
);
args.extend([4, output_id]);
request(&mut self.wire, registry, 0, &args);
let compositor = self
.client
.create_resource::<WlCompositor, (), Ferese>(&fixture.state.display_handle, 6, ())
.unwrap();
let surface_id = self.id();
request(&mut self.wire, compositor.id().protocol_id(), 0, &[surface_id]);
let lock_surface = self.id();
request(&mut self.wire, 2, 1, &[lock_surface, surface_id, output_id]);
fixture.dispatch();
fixture
.state
.session_lock
.surfaces
.get(output)
.expect("lock surface on requested output")
.clone()
}
}
fn request(wire: &mut UnixStream, object: u32, opcode: u32, args: &[u32]) {
for word in [object, (((args.len() + 2) * 4) as u32) << 16 | opcode]
.into_iter()
.chain(args.iter().copied())
{
wire.write_all(&word.to_ne_bytes()).unwrap();
}
}
#[test]
fn lock_confirms_without_outputs() {
let Some(mut fixture) = Fixture::new("session_lock::tests::lock_confirms_without_outputs") else {
return;
};
let epoch = fixture.state.capture_epoch;
let mut locker = fixture.acquire();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_)));
assert_ne!(fixture.state.capture_epoch, epoch, "lock revokes deferred snapshots");
locker.expect_locked();
}
#[test]
fn lock_waits_for_each_output_before_confirmation() {
let Some(mut fixture) = Fixture::new("session_lock::tests::lock_waits_for_each_output_before_confirmation") else {
return;
};
let first = fixture.output("first");
let second = fixture.output("second");
let mut locker = fixture.acquire();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_)));
fixture.state.lock_frame_presented(&first);
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_)));
assert!(fixture.state.session_lock.idle_timer.is_none());
fixture.state.lock_frame_presented(&second);
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_)));
fixture.dispatch();
locker.expect_locked();
}
#[test]
fn live_owner_rejects_competitor_unlock() {
let Some(mut fixture) = Fixture::new("session_lock::tests::live_owner_rejects_competitor_unlock") else {
return;
};
let _owner_wire = fixture.acquire();
let owner = fixture.state.session_lock.lifecycle.owner().cloned();
let mut competitor = fixture.acquire();
assert_eq!(fixture.state.session_lock.lifecycle.owner(), owner.as_ref());
request(&mut competitor.wire, 2, 2, &[]);
fixture.dispatch();
assert_eq!(fixture.state.session_lock.lifecycle.owner(), owner.as_ref());
assert!(fixture.state.session_lock.active());
}
#[test]
fn pending_owner_cannot_unlock_before_confirmation() {
let Some(mut fixture) = Fixture::new("session_lock::tests::pending_owner_cannot_unlock_before_confirmation") else {
return;
};
fixture.output("pending");
let mut pending = fixture.acquire();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_)));
request(&mut pending.wire, 2, 2, &[]);
fixture.dispatch();
assert!(fixture.state.session_lock.active());
fixture.dispatch();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Orphaned));
}
#[test]
fn owner_disconnect_stays_locked_and_allows_replacement() {
let Some(mut fixture) = Fixture::new("session_lock::tests::owner_disconnect_stays_locked_and_allows_replacement")
else {
return;
};
let owner_wire = fixture.acquire();
let owner = fixture.state.session_lock.lifecycle.owner().cloned();
drop(owner_wire);
fixture.dispatch();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Orphaned));
assert!(fixture.state.session_lock.active());
let mut replacement = fixture.acquire();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_)));
assert_ne!(fixture.state.session_lock.lifecycle.owner(), owner.as_ref());
replacement.expect_locked();
}
#[test]
fn removing_last_output_confirms_pending_lock() {
let Some(mut fixture) = Fixture::new("session_lock::tests::removing_last_output_confirms_pending_lock") else {
return;
};
let output = fixture.output("last");
let mut pending = fixture.acquire();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_)));
fixture.state.unregister_output(&output);
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_)));
fixture.dispatch();
pending.expect_locked();
}
#[test]
fn refresh_after_removal_confirms_and_starts_idle_timer() {
let Some(mut fixture) = Fixture::new("session_lock::tests::refresh_after_removal_confirms_and_starts_idle_timer")
else {
return;
};
let output = fixture.output("last");
let mut pending = fixture.acquire();
fixture.state.space.unmap_output(&output);
fixture.state.session_lock.output_removed(&output);
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_)));
fixture.state.refresh_lock_outputs();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_)));
assert!(fixture.state.session_lock.idle_timer.is_some());
fixture.dispatch();
pending.expect_locked();
}
#[test]
fn activity_reuses_timer_and_expired_timer_rechecks_latest_activity() {
let Some(mut fixture) =
Fixture::new("session_lock::tests::activity_reuses_timer_and_expired_timer_rechecks_latest_activity")
else {
return;
};
fixture.state.session_lock.lifecycle = Lifecycle::Orphaned;
fixture.state.lock_idle = IdleSettings {
dim_after_seconds: 1,
sleep_after_seconds: 2,
};
fixture.state.session_lock.idle_since = Some(Instant::now() - Duration::from_millis(999));
fixture.state.refresh_lock_idle_policy();
let timer = fixture.state.session_lock.idle_timer.unwrap();
for _ in 0..1000 {
fixture.state.lock_input_activity();
assert_eq!(
fixture.state.session_lock.idle_timer,
Some(timer),
"input replaced the source"
);
}
let limit = Instant::now() + Duration::from_secs(1);
while fixture.state.session_lock.idle_timer.unwrap().1 == timer.1 {
assert!(Instant::now() < limit, "idle timer did not recheck");
fixture
.events
.dispatch(Duration::from_millis(10), &mut fixture.state)
.unwrap();
}
let rechecked = fixture.state.session_lock.idle_timer.unwrap();
assert_eq!(timer.0, rechecked.0, "timer callback replaced the source");
assert!(rechecked.1 > timer.1);
assert_eq!(fixture.state.session_lock.idle_opacity, 0.0);
assert!(!fixture.state.session_lock.sleeping);
}
#[test]
fn earlier_policy_replaces_timer_and_sleeping_activity_arms_one() {
let Some(mut fixture) =
Fixture::new("session_lock::tests::earlier_policy_replaces_timer_and_sleeping_activity_arms_one")
else {
return;
};
fixture.state.session_lock.lifecycle = Lifecycle::Orphaned;
fixture.state.lock_input_activity();
let first = fixture.state.session_lock.idle_timer.unwrap();
fixture.state.lock_idle.dim_after_seconds = 60;
fixture.state.refresh_lock_idle_policy();
assert_eq!(fixture.state.session_lock.idle_timer, Some(first));
fixture.state.lock_idle.dim_after_seconds = 1;
fixture.state.refresh_lock_idle_policy();
let earlier = fixture.state.session_lock.idle_timer.unwrap();
assert_ne!(first.0, earlier.0);
assert!(earlier.1 < first.1);
fixture.state.session_lock.idle_since = Some(Instant::now() - Duration::from_secs(121));
fixture.state.refresh_lock_idle_policy();
assert!(fixture.state.session_lock.sleeping);
assert!(fixture.state.session_lock.idle_timer.is_none());
fixture.state.lock_input_activity();
assert!(fixture.state.session_lock.idle_timer.is_some());
assert!(!fixture.state.session_lock.sleeping);
assert_eq!(fixture.state.session_lock.idle_opacity, 0.0);
assert!(fixture.state.session_lock.active());
}
#[test]
fn dead_preferred_surface_uses_live_fallback_in_stable_output_order() {
let Some(mut fixture) =
Fixture::new("session_lock::tests::dead_preferred_surface_uses_live_fallback_in_stable_output_order")
else {
return;
};
let preferred = fixture.output("preferred");
let second = fixture.output("second");
let third = fixture.output("third");
let mut locker = fixture.acquire();
let dead = locker.surface(&mut fixture, &preferred);
let fallback = locker.surface(&mut fixture, &second);
let later = locker.surface(&mut fixture, &third);
let preferred_id = fixture.state.output_id(&preferred).unwrap();
fixture.state.output_workspaces.focus_output(preferred_id).unwrap();
request(&mut locker.wire, dead.wl_surface().id().protocol_id(), 0, &[]);
fixture.dispatch();
assert!(!dead.alive());
// Exercise the defensive path even though destruction normally removes it.
fixture.state.session_lock.surfaces.insert(preferred.clone(), dead);
for _ in 0..5 {
fixture.state.session_lock.surfaces.remove(&second);
fixture
.state
.session_lock
.surfaces
.insert(second.clone(), fallback.clone());
fixture.state.session_lock.surfaces.insert(third.clone(), later.clone());
fixture.state.focus_lock_surface();
assert_eq!(
fixture.state.seat.get_keyboard().unwrap().current_focus(),
Some(fallback.wl_surface().clone())
);
}
}
#[test]
fn output_removed_clears_all_lock_resources() {
let Some(mut fixture) = Fixture::new("session_lock::tests::output_removed_clears_all_lock_resources") else {
return;
};
let output = fixture.output("removed");
let mut locker = fixture.acquire();
locker.surface(&mut fixture, &output);
let background =
smithay::backend::renderer::element::solid::SolidColorBuffer::new((800, 600), [0.0, 0.0, 0.0, 1.0]);
fixture
.state
.session_lock
.backgrounds
.insert(output.clone(), background.clone());
fixture
.state
.session_lock
.idle_overlays
.insert(output.clone(), background);
fixture.state.session_lock.presented.insert(output.clone());
fixture.state.session_lock.output_removed(&output);
let lock = &fixture.state.session_lock;
assert!(!lock.surfaces.contains_key(&output));
assert!(!lock.backgrounds.contains_key(&output));
assert!(!lock.idle_overlays.contains_key(&output));
assert!(!lock.presented.contains(&output));
assert!(lock.active(), "output cleanup must not unlock");
}
#[test]
fn confirmed_owner_can_unlock_and_cancel_idle_timer() {
let Some(mut fixture) = Fixture::new("session_lock::tests::confirmed_owner_can_unlock_and_cancel_idle_timer")
else {
return;
};
let mut locker = fixture.acquire();
locker.expect_locked();
assert!(fixture.state.session_lock.idle_timer.is_some());
request(&mut locker.wire, 2, 2, &[]);
fixture.dispatch();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Unlocked));
assert!(fixture.state.session_lock.idle_timer.is_none());
}
#[test]
fn acquiring_owner_disconnect_keeps_lock_and_replacement_waits_for_frame() {
let Some(mut fixture) =
Fixture::new("session_lock::tests::acquiring_owner_disconnect_keeps_lock_and_replacement_waits_for_frame")
else {
return;
};
let output = fixture.output("pending");
let pending = fixture.acquire();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_)));
drop(pending);
fixture.dispatch();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Orphaned));
assert!(fixture.state.session_lock.active());
let mut replacement = fixture.acquire();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_)));
fixture.state.lock_frame_presented(&output);
fixture.dispatch();
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_)));
replacement.expect_locked();
}