Files
k-os/crates/servers/display-server/src/tests/server.rs

301 lines
12 KiB
Rust

extern crate std;
use crate::server::{BAR_HEIGHT, DisplayServer, Host};
use driver_framebuffer_mock::GridFramebuffer;
use server_framework::{MALFORMED, Server};
use std::vec;
use std::vec::Vec;
use types::bounded::CapSlot;
use types::display::Color;
use types::display::window::{self as w, BADGE};
const W: usize = 32;
const H: usize = 16 + 8;
/// Buffers in memory; a capability is its slot number plus 40.
#[derive(Default)]
struct FakeHost {
/// Each slot's pixels, the bytes R, G, B, A.
buffers: Vec<Option<Vec<u8>>>,
minted: Vec<usize>,
revoked: Vec<CapSlot>,
reclaimed: Vec<usize>,
signalled: Vec<CapSlot>,
events: Vec<[usize; 4]>,
}
impl Host for FakeHost {
fn buffer(&mut self, index: usize, pixels: usize) -> Option<CapSlot> {
if self.buffers.len() <= index {
self.buffers.resize(index + 1, None);
}
self.buffers[index].get_or_insert_with(|| vec![0; pixels * 4]);
CapSlot::new(40 + index as u8)
}
fn blank(&mut self, index: usize) {
if let Some(b) = self.buffers[index].as_mut() {
b.fill(0);
}
}
fn pixels(&self, index: usize, from: usize, count: usize) -> &[u8] {
&self.buffers[index].as_ref().unwrap()[from * 4..(from + count) * 4]
}
fn reclaim(&mut self, index: usize) {
self.reclaimed.push(index);
}
fn mint(&mut self, badge: usize) -> Option<CapSlot> {
self.minted.push(badge);
CapSlot::new(20 + self.minted.len() as u8)
}
fn revoke(&mut self, cap: CapSlot) {
self.revoked.push(cap);
}
fn signal(&mut self, notify: CapSlot) {
self.signalled.push(notify);
}
fn input(&mut self) -> Option<[usize; 4]> {
(!self.events.is_empty()).then(|| self.events.remove(0))
}
}
/// A window buffer all one colour, as an app would draw it.
fn fill(buf: &mut [u8], c: Color) {
for px in buf.chunks_exact_mut(4) {
px.copy_from_slice(&c.to_rgba());
}
}
type Srv = DisplayServer<GridFramebuffer<W, H>, FakeHost>;
fn server() -> Srv {
DisplayServer::new(GridFramebuffer::new(), FakeHost::default(), crate::themes::Themes::new())
}
/// Opens a window through launcher `opener`: its badge.
fn open(s: &mut Srv, opener: usize, name: &[u8]) -> usize {
let n = w::pack_name(name);
let (reply, cap) = s.handle_call(opener, [w::OPEN, n[0], n[1], n[2]], None);
assert_eq!(reply, [w::OK, W, H - BAR_HEIGHT as usize, 0]);
assert!(cap.is_some_and(|g| g.keep_delegate), "the launcher may pass it on");
*s.host().minted.last().unwrap()
}
/// Paints window `slot`'s buffer one colour and presents all of it.
fn paint(s: &mut Srv, badge: usize, slot: usize, color: Color) -> [usize; 4] {
// The app writes its buffer directly; here, through the fake.
fill(s.host_mut_for_test().buffers[slot].as_mut().unwrap(), color);
let rect = w::pack_rect(
types::display::ScreenCoord::new(0),
types::display::ScreenCoord::new(0),
types::display::Dimension::new(W as u16),
types::display::Dimension::new(H as u16),
);
s.handle_call(badge, [w::PRESENT, rect[0], rect[1], 0], None).0
}
fn shown(s: &Srv) -> Option<Color> {
s.framebuffer().pixel(W / 2, H - 1)
}
const RED: Color = Color::rgb(200, 0, 0);
const BLUE: Color = Color::rgb(0, 0, 200);
#[test]
fn the_front_window_is_shown_below_the_bar() {
let mut s = server();
let t = open(&mut s, 1, b"Terminal");
assert_eq!(paint(&mut s, t, 0, RED)[0], w::OK);
assert_eq!(shown(&s), Some(RED));
assert_ne!(s.framebuffer().pixel(W - 1, 0), Some(RED), "the bar is the display's");
assert_eq!(s.framebuffer().pixel(0, BAR_HEIGHT as usize), Some(RED), "the window starts under it");
}
#[test]
fn a_window_behind_draws_nothing_until_it_is_in_front() {
let mut s = server();
let t = open(&mut s, 1, b"Terminal");
paint(&mut s, t, 0, RED);
let snake = open(&mut s, 2, b"Snake");
assert_eq!(shown(&s), Some(Color::from_raw(0)), "a new window starts blank");
paint(&mut s, t, 0, BLUE);
assert_eq!(shown(&s), Some(Color::from_raw(0)), "the terminal is behind");
paint(&mut s, snake, 1, RED);
assert_eq!(shown(&s), Some(RED));
// The switch key brings the terminal back, as it last drew.
s.host_mut_for_test().events = vec![[1, 10, 1, 0], [2, 10, 1, 0]];
s.signal(0);
assert_eq!(shown(&s), Some(BLUE));
}
#[test]
fn input_goes_to_the_front_window_and_the_switch_key_to_nobody() {
let mut s = server();
let t = open(&mut s, 1, b"Terminal");
let notify = CapSlot::new(9).unwrap();
assert_eq!(s.handle_call(t, [w::SUBSCRIBE, 0, 0, 0], Some(notify)).0[0], w::OK);
s.host_mut_for_test().events = vec![[1, 9, b'a' as usize, 0], [1, 10, 1, 0]];
s.signal(0);
assert_eq!(s.host().signalled, [notify]);
assert_eq!(s.handle_call(t, [w::POLL, 0, 0, 0], None).0, [1, 9, b'a' as usize, 0]);
assert_eq!(s.handle_call(t, [w::POLL, 0, 0, 0], None).0, [0; 4], "F1 was the display's");
}
#[test]
fn only_launchers_open_windows_and_only_init_sets_the_user() {
let mut s = server();
let t = open(&mut s, 1, b"Terminal");
let n = w::pack_name(b"Fake");
assert_eq!(s.handle_call(t, [w::OPEN, n[0], n[1], n[2]], None).0[0], MALFORMED, "not through a window");
assert_eq!(s.handle_call(1, [w::SET_USER, n[0], n[1], n[2]], None).0[0], MALFORMED);
assert_eq!(s.handle_call(0, [w::SET_USER, n[0], n[1], n[2]], None).0[0], w::OK);
assert_eq!(s.handle_call(BADGE | 12345, [w::POLL, 0, 0, 0], None).0[0], MALFORMED, "a made-up window");
}
#[test]
fn closing_takes_the_window_and_its_buffer_back() {
let mut s = server();
let t = open(&mut s, 1, b"Terminal");
let snake = open(&mut s, 2, b"Snake");
paint(&mut s, t, 0, BLUE);
assert_eq!(s.handle_call(snake, [w::CLOSE, 0, 0, 0], None).0[0], w::OK);
assert_eq!(s.host().revoked, [CapSlot::new(22).unwrap()]);
assert_eq!(s.host().reclaimed, [1]);
assert_eq!(shown(&s), Some(BLUE), "the terminal is in front again");
assert_eq!(s.handle_call(snake, [w::POLL, 0, 0, 0], None).0[0], MALFORMED, "closed");
// Its buffer is reused, blank, for the next window.
fill(s.host_mut_for_test().buffers[1].as_mut().unwrap(), RED);
open(&mut s, 2, b"Snake");
assert!(s.host().buffers[1].as_ref().unwrap().iter().all(|b| *b == 0));
}
#[test]
fn a_launcher_gone_takes_its_windows_with_it() {
let mut s = server();
open(&mut s, 1, b"Terminal");
open(&mut s, 2, b"Snake");
open(&mut s, 2, b"Counter");
assert_eq!(s.handle_call(3, [w::FORGET, 2, 0, 0], None).0[0], MALFORMED, "only init says so");
assert_eq!(s.handle_call(0, [w::FORGET, 2, 0, 0], None).0[0], w::OK);
assert_eq!(s.host().reclaimed, [1, 2]);
}
#[test]
fn a_present_outside_the_window_is_clipped() {
let mut s = server();
let t = open(&mut s, 1, b"Terminal");
fill(s.host_mut_for_test().buffers[0].as_mut().unwrap(), RED);
let rect = w::pack_rect(
types::display::ScreenCoord::new(30),
types::display::ScreenCoord::new(5),
types::display::Dimension::new(60000),
types::display::Dimension::new(60000),
);
assert_eq!(s.handle_call(t, [w::PRESENT, rect[0], rect[1], 0], None).0[0], w::OK);
assert_eq!(s.framebuffer().pixel(31, H - 1), Some(RED));
assert_ne!(s.framebuffer().pixel(29, H - 1), Some(RED), "left of the rectangle is untouched");
}
#[test]
fn init_switches_the_theme_and_every_window_hears_of_it() {
let light = include_str!("../../../../../disk/themes/whimsy-light.toml");
let mut themes = crate::themes::Themes::new();
themes.add(light).unwrap();
let mut s = DisplayServer::new(GridFramebuffer::new(), FakeHost::default(), themes);
let t = open(&mut s, 1, b"Terminal");
let snake = open(&mut s, 2, b"Snake");
let (n1, n2) = (CapSlot::new(9).unwrap(), CapSlot::new(10).unwrap());
s.handle_call(t, [w::SUBSCRIBE, 0, 0, 0], Some(n1));
s.handle_call(snake, [w::SUBSCRIBE, 0, 0, 0], Some(n2));
let names: Vec<_> = (0..)
.map_while(|i| match s.handle_call(t, [w::THEME_NAME, i, 0, 0], None).0 {
[w::OK, a, b, c] => {
let (n, len) = w::unpack_name([a, b, c]);
Some(n[..len].to_vec())
}
_ => None,
})
.collect();
assert_eq!(names, [b"Whimsy Dark".to_vec(), b"Whimsy Light".to_vec()]);
let read = |s: &mut Srv| -> (usize, theme::Theme) {
let mut words = [0; w::THEME_WORDS];
let mut head = 0;
for part in 0..w::THEME_WORDS / 2 {
let r = s.handle_call(snake, [w::THEME, part, 0, 0], None).0;
assert_eq!(r[0], w::OK);
head = r[1];
words[part * 2..part * 2 + 2].copy_from_slice(&r[2..]);
}
(head >> 32, theme::wire::decode(words))
};
assert_eq!(read(&mut s).0, 0);
let n = w::pack_name(b"Whimsy Light");
assert_eq!(s.handle_call(5, [w::SET_THEME, n[0], n[1], n[2]], None).0[0], MALFORMED, "init's alone");
assert_eq!(s.handle_call(0, [w::SET_THEME, n[0], n[1], n[2]], None).0[0], w::OK);
let (index, t2) = read(&mut s);
assert_eq!((index, t2.background), (1, theme::parse(light).unwrap().background));
assert!(s.host().signalled.contains(&n1) && s.host().signalled.contains(&n2), "behind or in front");
assert_eq!(s.handle_call(t, [w::POLL, 0, 0, 0], None).0[0], w::THEME_CHANGED);
let bogus = w::pack_name(b"Nope");
assert_eq!(s.handle_call(0, [w::SET_THEME, bogus[0], bogus[1], bogus[2]], None).0[0], MALFORMED);
}
#[test]
fn only_the_launcher_sets_an_accent_and_it_is_in_the_theme() {
let mut s = server();
let app = open(&mut s, 2, b"Tally");
let other = open(&mut s, 3, b"Snake");
let pink = 0xe91e63;
assert_eq!(s.handle_call(app, [w::ACCENT, pink, 0, 0], None).0[0], w::OK);
assert_eq!(s.handle_call(app, [w::ACCENT, 0x00ff00, 0, 0], None).0[0], MALFORMED, "once");
// The app starts: too late for it to choose one itself.
s.handle_call(other, [w::BUFFER, 0, 0, 0], None);
assert_eq!(s.handle_call(other, [w::ACCENT, pink, 0, 0], None).0[0], MALFORMED);
let accent = |s: &mut Srv, badge| {
let r = s.handle_call(badge, [w::THEME, 0, 0, 0], None).0;
// Colour 3, the top of the theme's second word.
theme::Rgba::rgb((r[3] >> 32) as u32 & 0xff_ffff)
};
assert_eq!(accent(&mut s, other), theme::Theme::WHIMSY_DARK.accent);
assert_eq!(accent(&mut s, app), theme::with_accent(theme::Theme::WHIMSY_DARK, theme::Rgba::rgb(pink as u32)).accent);
}
#[test]
fn window_keys_raise_home_and_close_only_apps() {
let key = |s: &mut Srv, k: usize| {
s.host_mut_for_test().events = vec![[1, 10, k, 0], [2, 10, k, 0]];
s.signal(0);
};
let mut s = server();
let terminal = open(&mut s, 1, b"Terminal");
let home = open(&mut s, 2, b"Home");
let app = open(&mut s, 2, b"Tally");
assert_eq!(s.handle_call(home, [w::ROLE, w::role::HOME, 0, 0], None).0[0], w::OK);
assert_eq!(s.handle_call(app, [w::ROLE, w::role::APP, 0, 0], None).0[0], w::OK);
assert_eq!(s.handle_call(app, [w::ROLE, w::role::HOME, 0, 0], None).0[0], MALFORMED, "once");
s.handle_call(terminal, [w::BUFFER, 0, 0, 0], None);
assert_eq!(s.handle_call(terminal, [w::ROLE, w::role::APP, 0, 0], None).0[0], MALFORMED, "not once it runs");
let notify = CapSlot::new(9).unwrap();
s.handle_call(app, [w::SUBSCRIBE, 0, 0, 0], Some(notify));
key(&mut s, w::HOME_KEY);
assert_eq!(s.windows_front_for_test(), s.slot_for_test(home));
key(&mut s, w::CLOSE_KEY);
assert!(s.slot_for_test(home).is_some(), "Home is not closed");
// The app in front: closed, and told.
s.handle_call(app, [w::POLL, 0, 0, 0], None);
key(&mut s, w::SWITCH_KEY);
assert_eq!(s.windows_front_for_test(), s.slot_for_test(app));
key(&mut s, w::CLOSE_KEY);
assert_eq!(s.slot_for_test(app), None);
assert!(s.host().signalled.contains(&notify));
assert_eq!(s.handle_call(app, [w::POLL, 0, 0, 0], None).0[0], MALFORMED, "its window is gone");
// The terminal in front: kept.
key(&mut s, w::SWITCH_KEY);
while s.windows_front_for_test() != s.slot_for_test(terminal) {
key(&mut s, w::SWITCH_KEY);
}
key(&mut s, w::CLOSE_KEY);
assert!(s.slot_for_test(terminal).is_some());
}