mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2025-01-30 20:34:49 +01:00
Start on Rust API tests
Does not currently work because every api struct is a static, yikes
This commit is contained in:
parent
ce8b56eee8
commit
f557afcaa1
8 changed files with 235 additions and 111 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1767,6 +1767,7 @@ dependencies = [
|
|||
"image",
|
||||
"nix",
|
||||
"pinnacle",
|
||||
"pinnacle-api",
|
||||
"pinnacle-api-defs",
|
||||
"prost",
|
||||
"serde",
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
})));
|
||||
|
|
|
@ -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
76
tests/common/mod.rs
Normal 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(())
|
||||
}
|
112
tests/lua_api.rs
112
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<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
100
tests/rust_api.rs
Normal 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))
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue