From f557afcaa1aac0becc35852bbbdc2185feb46fee Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 16 Apr 2024 07:04:34 -0500 Subject: [PATCH] Start on Rust API tests Does not currently work because every api struct is a static, yikes --- Cargo.lock | 1 + Cargo.toml | 1 + api/rust/src/lib.rs | 10 ++-- api/rust/src/output.rs | 45 ++++++++++++----- api/rust/src/signal.rs | 1 + tests/common/mod.rs | 76 ++++++++++++++++++++++++++++ tests/lua_api.rs | 112 +++++++---------------------------------- tests/rust_api.rs | 100 ++++++++++++++++++++++++++++++++++++ 8 files changed, 235 insertions(+), 111 deletions(-) create mode 100644 tests/common/mod.rs create mode 100644 tests/rust_api.rs diff --git a/Cargo.lock b/Cargo.lock index bd7a523..005bec7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1767,6 +1767,7 @@ dependencies = [ "image", "nix", "pinnacle", + "pinnacle-api", "pinnacle-api-defs", "prost", "serde", diff --git a/Cargo.toml b/Cargo.toml index 56dccfd..1b08a0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,6 +109,7 @@ temp-env = "0.3.6" tempfile = "3.10.1" test-log = { version = "0.2.15", default-features = false, features = ["trace"] } pinnacle = { path = ".", features = ["testing"] } +pinnacle-api = { path = "./api/rust" } [features] testing = [ diff --git a/api/rust/src/lib.rs b/api/rust/src/lib.rs index 7fe6175..4fc265a 100644 --- a/api/rust/src/lib.rs +++ b/api/rust/src/lib.rs @@ -173,12 +173,10 @@ pub async fn connect( let layout = LAYOUT.get_or_init(|| Layout::new(channel.clone())); let render = RENDER.get_or_init(|| Render::new(channel.clone())); - SIGNAL - .set(RwLock::new(SignalState::new( - channel.clone(), - fut_sender.clone(), - ))) - .map_err(|_| "failed to create SIGNAL")?; + let _ = SIGNAL.set(RwLock::new(SignalState::new( + channel.clone(), + fut_sender.clone(), + ))); let modules = ApiModules { pinnacle, diff --git a/api/rust/src/output.rs b/api/rust/src/output.rs index f170fac..a7b10b2 100644 --- a/api/rust/src/output.rs +++ b/api/rust/src/output.rs @@ -211,13 +211,17 @@ impl Output { if setup.output.matches(output) { if let Some(OutputSetupLoc::Point(x, y)) = setup.loc { output.set_location(x, y); + placed_outputs.insert(output.clone()); + let props = output.props(); + let x = props.x.unwrap(); + let width = props.logical_width.unwrap() as i32; if rightmost_output_and_x.is_none() || rightmost_output_and_x .as_ref() - .is_some_and(|(_, rm_x)| x > *rm_x) + .is_some_and(|(_, rm_x)| x + width > *rm_x) { - rightmost_output_and_x = Some((output.clone(), x)); + rightmost_output_and_x = Some((output.clone(), x + width)); } } } @@ -236,16 +240,19 @@ impl Output { output.set_loc_adj_to(rm_op, Alignment::RightAlignTop); } else { output.set_location(0, 0); + println!("SET LOC FOR {} TO (0, 0)", output.name()); } placed_outputs.insert(output.clone()); - let x = output.x().unwrap(); + let props = output.props(); + let x = props.x.unwrap(); + let width = props.logical_width.unwrap() as i32; if rightmost_output_and_x.is_none() || rightmost_output_and_x .as_ref() - .is_some_and(|(_, rm_x)| x > *rm_x) + .is_some_and(|(_, rm_x)| x + width > *rm_x) { - rightmost_output_and_x = Some((output.clone(), x)); + rightmost_output_and_x = Some((output.clone(), x + width)); } } } @@ -272,36 +279,49 @@ impl Output { output.set_loc_adj_to(relative_to, *alignment); placed_outputs.insert(output.clone()); - let x = output.x().unwrap(); + let props = output.props(); + let x = props.x.unwrap(); + let width = props.logical_width.unwrap() as i32; if rightmost_output_and_x.is_none() || rightmost_output_and_x .as_ref() - .is_some_and(|(_, rm_x)| x > *rm_x) + .is_some_and(|(_, rm_x)| x + width > *rm_x) { - rightmost_output_and_x = Some((output.clone(), x)); + rightmost_output_and_x = Some((output.clone(), x + width)); } } + // dbg!(&placed_outputs); + // dbg!(&outputs); + // Place all remaining outputs right of the rightmost one for output in outputs .iter() - .filter(|op| !placed_outputs.contains(op)) + .filter(|op| { + // println!("CHECKING {}", op.name()); + !placed_outputs.contains(op) + }) .collect::>() { + // println!("ATTEMPTING TO PLACE {}", output.name()); if let Some((rm_op, _)) = rightmost_output_and_x.as_ref() { output.set_loc_adj_to(rm_op, Alignment::RightAlignTop); + // println!("SET LOC FOR {} TO RIGHTMOST, REMAINING", output.name()); } else { output.set_location(0, 0); + // println!("SET LOC FOR {} TO (0, 0), REMAINING", output.name()); } placed_outputs.insert(output.clone()); - let x = output.x().unwrap(); + let props = output.props(); + let x = props.x.unwrap(); + let width = props.logical_width.unwrap() as i32; if rightmost_output_and_x.is_none() || rightmost_output_and_x .as_ref() - .is_some_and(|(_, rm_x)| x > *rm_x) + .is_some_and(|(_, rm_x)| x + width > *rm_x) { - rightmost_output_and_x = Some((output.clone(), x)); + rightmost_output_and_x = Some((output.clone(), x + width)); } } }; @@ -312,6 +332,7 @@ impl Output { let layout_outputs_clone2 = layout_outputs.clone(); self.connect_signal(OutputSignal::Connect(Box::new(move |output| { + println!("GOT CONNECTION FOR OUTPUT {}", output.name()); apply_all_but_loc(output); layout_outputs_clone2(); }))); diff --git a/api/rust/src/signal.rs b/api/rust/src/signal.rs index a479677..ea7f101 100644 --- a/api/rust/src/signal.rs +++ b/api/rust/src/signal.rs @@ -70,6 +70,7 @@ macro_rules! signals { callback_sender .send((self.current_id, callback)) + .map_err(|e| { println!("{e}"); e }) .expect("failed to send callback"); let handle = SignalHandle::new(self.current_id, remove_callback_sender); diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..2581b35 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,76 @@ +use std::{panic::UnwindSafe, time::Duration}; + +use pinnacle::{backend::dummy::setup_dummy, state::State}; +use smithay::reexports::calloop::{ + self, + channel::{Event, Sender}, +}; + +#[allow(clippy::type_complexity)] +pub fn with_state( + sender: &Sender>, + with_state: impl FnOnce(&mut State) + Send + 'static, +) { + sender.send(Box::new(with_state)).unwrap(); +} + +pub fn sleep_secs(secs: u64) { + std::thread::sleep(Duration::from_secs(secs)); +} + +pub fn test_api( + test: impl FnOnce(Sender>) + Send + UnwindSafe + 'static, +) -> anyhow::Result<()> { + let (mut state, mut event_loop) = setup_dummy(true, None)?; + + let (sender, recv) = calloop::channel::channel::>(); + + event_loop + .handle() + .insert_source(recv, |event, _, state| match event { + Event::Msg(f) => f(state), + Event::Closed => (), + }) + .map_err(|_| anyhow::anyhow!("failed to insert source"))?; + + let tempdir = tempfile::tempdir()?; + + state.start_grpc_server(tempdir.path())?; + + let loop_signal = event_loop.get_signal(); + + let join_handle = std::thread::spawn(move || { + let res = std::panic::catch_unwind(|| { + test(sender); + }); + loop_signal.stop(); + if let Err(err) = res { + std::panic::resume_unwind(err); + } + }); + + event_loop.run(None, &mut state, |state| { + state.fixup_z_layering(); + state.space.refresh(); + state.popup_manager.cleanup(); + + state + .display_handle + .flush_clients() + .expect("failed to flush client buffers"); + + // TODO: couple these or something, this is really error-prone + assert_eq!( + state.windows.len(), + state.z_index_stack.len(), + "Length of `windows` and `z_index_stack` are different. \ + If you see this, report it to the developer." + ); + })?; + + if let Err(err) = join_handle.join() { + panic!("{err:?}"); + } + + Ok(()) +} diff --git a/tests/lua_api.rs b/tests/lua_api.rs index b269b9b..08948e5 100644 --- a/tests/lua_api.rs +++ b/tests/lua_api.rs @@ -1,19 +1,13 @@ +mod common; + use std::{ io::Write, - panic::UnwindSafe, process::{Command, Stdio}, - time::Duration, }; -use pinnacle::{ - backend::dummy::setup_dummy, - state::{State, WithState}, -}; -use smithay::reexports::calloop::{ - self, - channel::{Event, Sender}, -}; +use crate::common::{sleep_secs, test_api, with_state}; +use pinnacle::state::WithState; use test_log::test; fn run_lua(ident: &str, code: &str) { @@ -93,18 +87,6 @@ fn setup_lua(ident: &str, code: &str) -> SetupLuaGuard { // } } -#[allow(clippy::type_complexity)] -fn with_state( - sender: &Sender>, - assert: impl FnOnce(&mut State) + Send + 'static, -) { - sender.send(Box::new(assert)).unwrap(); -} - -fn sleep_secs(secs: u64) { - std::thread::sleep(Duration::from_secs(secs)); -} - macro_rules! run_lua { { |$ident:ident| $($body:tt)* } => { run_lua(stringify!($ident), stringify!($($body)*)); @@ -117,63 +99,6 @@ macro_rules! setup_lua { }; } -fn test_lua_api( - test: impl FnOnce(Sender>) + Send + UnwindSafe + 'static, -) -> anyhow::Result<()> { - let (mut state, mut event_loop) = setup_dummy(true, None)?; - - let (sender, recv) = calloop::channel::channel::>(); - - event_loop - .handle() - .insert_source(recv, |event, _, state| match event { - Event::Msg(f) => f(state), - Event::Closed => (), - }) - .map_err(|_| anyhow::anyhow!("failed to insert source"))?; - - let tempdir = tempfile::tempdir()?; - - state.start_grpc_server(tempdir.path())?; - - let loop_signal = event_loop.get_signal(); - - let join_handle = std::thread::spawn(move || { - let res = std::panic::catch_unwind(|| { - test(sender); - }); - loop_signal.stop(); - if let Err(err) = res { - std::panic::resume_unwind(err); - } - }); - - event_loop.run(None, &mut state, |state| { - state.fixup_z_layering(); - state.space.refresh(); - state.popup_manager.cleanup(); - - state - .display_handle - .flush_clients() - .expect("failed to flush client buffers"); - - // TODO: couple these or something, this is really error-prone - assert_eq!( - state.windows.len(), - state.z_index_stack.len(), - "Length of `windows` and `z_index_stack` are different. \ - If you see this, report it to the developer." - ); - })?; - - if let Err(err) = join_handle.join() { - panic!("{err:?}"); - } - - Ok(()) -} - mod coverage { use pinnacle::{ tag::TagId, @@ -188,12 +113,13 @@ mod coverage { // Process mod process { + use super::*; #[tokio::main] #[self::test] async fn spawn() -> anyhow::Result<()> { - test_lua_api(|sender| { + test_api(|sender| { run_lua! { |Pinnacle| Pinnacle.process.spawn("foot") } @@ -210,7 +136,7 @@ mod coverage { #[tokio::main] #[self::test] async fn set_env() -> anyhow::Result<()> { - test_lua_api(|sender| { + test_api(|sender| { run_lua! { |Pinnacle| Pinnacle.process.set_env("PROCESS_SET_ENV", "env value") } @@ -235,7 +161,7 @@ mod coverage { #[tokio::main] #[self::test] async fn get_all() -> anyhow::Result<()> { - test_lua_api(|_sender| { + test_api(|_sender| { run_lua! { |Pinnacle| assert(#Pinnacle.window.get_all() == 0) @@ -255,7 +181,7 @@ mod coverage { #[tokio::main] #[self::test] async fn get_focused() -> anyhow::Result<()> { - test_lua_api(|_sender| { + test_api(|_sender| { run_lua! { |Pinnacle| assert(not Pinnacle.window.get_focused()) @@ -274,7 +200,7 @@ mod coverage { #[tokio::main] #[self::test] async fn add_window_rule() -> anyhow::Result<()> { - test_lua_api(|sender| { + test_api(|sender| { run_lua! { |Pinnacle| Pinnacle.tag.add(Pinnacle.output.get_focused(), "Tag Name") Pinnacle.window.add_window_rule({ @@ -356,7 +282,7 @@ mod coverage { #[tokio::main] #[self::test] async fn close() -> anyhow::Result<()> { - test_lua_api(|sender| { + test_api(|sender| { run_lua! { |Pinnacle| Pinnacle.process.spawn("foot") } @@ -382,7 +308,7 @@ mod coverage { #[tokio::main] #[self::test] async fn move_to_tag() -> anyhow::Result<()> { - test_lua_api(|sender| { + test_api(|sender| { run_lua! { |Pinnacle| local tags = Pinnacle.tag.add(Pinnacle.output.get_focused(), "1", "2", "3") tags[1]:set_active(true) @@ -452,7 +378,7 @@ mod coverage { #[tokio::main] #[self::test] async fn props() -> anyhow::Result<()> { - test_lua_api(|_sender| { + test_api(|_sender| { run_lua! { |Pinnacle| Pinnacle.output.connect_for_all(function(op) local tags = Pinnacle.tag.add(op, "First", "Mungus", "Potato") @@ -506,7 +432,7 @@ mod coverage { #[tokio::main] #[self::test] async fn setup() -> anyhow::Result<()> { - test_lua_api(|sender| { + test_api(|sender| { setup_lua! { |Pinnacle| Pinnacle.output.setup({ { @@ -626,7 +552,7 @@ mod coverage { #[tokio::main] #[test] async fn window_count_with_tag_is_correct() -> anyhow::Result<()> { - test_lua_api(|sender| { + test_api(|sender| { run_lua! { |Pinnacle| Pinnacle.tag.add(Pinnacle.output.get_focused(), "1") Pinnacle.process.spawn("foot") @@ -651,7 +577,7 @@ async fn window_count_with_tag_is_correct() -> anyhow::Result<()> { #[tokio::main] #[test] async fn window_count_without_tag_is_correct() -> anyhow::Result<()> { - test_lua_api(|sender| { + test_api(|sender| { run_lua! { |Pinnacle| Pinnacle.process.spawn("foot") } @@ -665,7 +591,7 @@ async fn window_count_without_tag_is_correct() -> anyhow::Result<()> { #[tokio::main] #[test] async fn spawned_window_on_active_tag_has_keyboard_focus() -> anyhow::Result<()> { - test_lua_api(|sender| { + test_api(|sender| { run_lua! { |Pinnacle| Pinnacle.tag.add(Pinnacle.output.get_focused(), "1")[1]:set_active(true) Pinnacle.process.spawn("foot") @@ -688,7 +614,7 @@ async fn spawned_window_on_active_tag_has_keyboard_focus() -> anyhow::Result<()> #[tokio::main] #[test] async fn spawned_window_on_inactive_tag_does_not_have_keyboard_focus() -> anyhow::Result<()> { - test_lua_api(|sender| { + test_api(|sender| { run_lua! { |Pinnacle| Pinnacle.tag.add(Pinnacle.output.get_focused(), "1") Pinnacle.process.spawn("foot") @@ -705,7 +631,7 @@ async fn spawned_window_on_inactive_tag_does_not_have_keyboard_focus() -> anyhow #[tokio::main] #[test] async fn spawned_window_has_correct_tags() -> anyhow::Result<()> { - test_lua_api(|sender| { + test_api(|sender| { run_lua! { |Pinnacle| Pinnacle.tag.add(Pinnacle.output.get_focused(), "1", "2", "3") Pinnacle.process.spawn("foot") diff --git a/tests/rust_api.rs b/tests/rust_api.rs new file mode 100644 index 0000000..d31713b --- /dev/null +++ b/tests/rust_api.rs @@ -0,0 +1,100 @@ +mod common; + +use std::thread::JoinHandle; + +use pinnacle_api::ApiModules; +use test_log::test; + +use crate::common::{sleep_secs, test_api, with_state}; + +#[tokio::main] +async fn setup_rust_inner(run: impl FnOnce(ApiModules) + Send + 'static) { + let (api, recv) = pinnacle_api::connect().await.unwrap(); + + run(api); + + pinnacle_api::listen(recv).await; +} + +fn setup_rust(run: impl FnOnce(ApiModules) + Send + 'static) -> JoinHandle<()> { + std::thread::spawn(|| { + setup_rust_inner(run); + }) +} + +mod output { + use pinnacle_api::output::{Alignment, OutputMatcher, OutputSetup}; + use smithay::utils::Rectangle; + + use super::*; + + #[tokio::main] + #[self::test] + async fn setup() -> anyhow::Result<()> { + test_api(|sender| { + setup_rust(|api| { + api.output + .setup([OutputSetup::new_with_matcher(|_| true).with_tags(["1", "2", "3"])]); + }); + + sleep_secs(1); + + with_state(&sender, |state| { + state.new_output("First", (300, 200).into()); + }); + }) + } + + #[tokio::main] + #[self::test] + async fn setup_with_cyclic_relative_tos_work() -> anyhow::Result<()> { + test_api(|sender| { + setup_rust(|api| { + api.output.setup([ + OutputSetup::new("Pinnacle Window"), + OutputSetup::new("First").with_relative_loc( + OutputMatcher::Name("Second".into()), + Alignment::RightAlignTop, + ), + OutputSetup::new("Second").with_relative_loc( + OutputMatcher::Name("First".into()), + Alignment::LeftAlignTop, + ), + ]); + }); + + sleep_secs(1); + + with_state(&sender, |state| { + state.new_output("First", (300, 200).into()); + }); + + sleep_secs(1); + + with_state(&sender, |state| { + let original_op = state + .space + .outputs() + .find(|op| op.name() == "Pinnacle Window") + .unwrap(); + let first_op = state + .space + .outputs() + .find(|op| op.name() == "First") + .unwrap(); + + let original_geo = state.space.output_geometry(original_op).unwrap(); + let first_geo = state.space.output_geometry(first_op).unwrap(); + + assert_eq!( + original_geo, + Rectangle::from_loc_and_size((0, 0), (1920, 1080)) + ); + assert_eq!( + first_geo, + Rectangle::from_loc_and_size((1920, 0), (300, 200)) + ); + }); + }) + } +}