Start on Rust API tests

Does not currently work because every api struct is a static, yikes
This commit is contained in:
Ottatop 2024-04-16 07:04:34 -05:00
parent ce8b56eee8
commit f557afcaa1
8 changed files with 235 additions and 111 deletions

1
Cargo.lock generated
View file

@ -1767,6 +1767,7 @@ dependencies = [
"image",
"nix",
"pinnacle",
"pinnacle-api",
"pinnacle-api-defs",
"prost",
"serde",

View file

@ -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 = [

View file

@ -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,

View file

@ -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::<Vec<_>>()
{
// 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();
})));

View file

@ -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);

76
tests/common/mod.rs Normal file
View file

@ -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<Box<dyn FnOnce(&mut State) + Send>>,
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<Box<dyn FnOnce(&mut State) + Send>>) + Send + UnwindSafe + 'static,
) -> anyhow::Result<()> {
let (mut state, mut event_loop) = setup_dummy(true, None)?;
let (sender, recv) = calloop::channel::channel::<Box<dyn FnOnce(&mut State) + Send>>();
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(())
}

View file

@ -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<Box<dyn FnOnce(&mut State) + Send>>,
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<Box<dyn FnOnce(&mut State) + Send>>) + Send + UnwindSafe + 'static,
) -> anyhow::Result<()> {
let (mut state, mut event_loop) = setup_dummy(true, None)?;
let (sender, recv) = calloop::channel::channel::<Box<dyn FnOnce(&mut State) + Send>>();
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")

100
tests/rust_api.rs Normal file
View file

@ -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))
);
});
})
}
}