mirror of
https://github.com/pinnacle-comp/pinnacle.git
synced 2024-12-26 21:58:10 +01:00
512 lines
14 KiB
Rust
512 lines
14 KiB
Rust
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 test_log::test;
|
|
|
|
fn run_lua(ident: &str, code: &str) {
|
|
#[rustfmt::skip]
|
|
let code = format!(r#"
|
|
require("pinnacle").run(function({ident})
|
|
local run = function({ident})
|
|
{code}
|
|
end
|
|
|
|
local success, err = pcall(run, {ident})
|
|
|
|
if not success then
|
|
print(err)
|
|
os.exit(1)
|
|
end
|
|
end)
|
|
"#);
|
|
|
|
let mut child = Command::new("lua").stdin(Stdio::piped()).spawn().unwrap();
|
|
|
|
let mut stdin = child.stdin.take().unwrap();
|
|
|
|
stdin.write_all(code.as_bytes()).unwrap();
|
|
|
|
drop(stdin);
|
|
|
|
let exit_status = child.wait().unwrap();
|
|
|
|
if exit_status.code().is_some_and(|code| code != 0) {
|
|
panic!("lua code panicked");
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::type_complexity)]
|
|
fn assert(
|
|
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)*));
|
|
};
|
|
}
|
|
|
|
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,
|
|
window::{
|
|
rules::{WindowRule, WindowRuleCondition},
|
|
window_state::FullscreenOrMaximized,
|
|
},
|
|
};
|
|
|
|
use super::*;
|
|
|
|
// Process
|
|
|
|
mod process {
|
|
use super::*;
|
|
|
|
#[tokio::main]
|
|
#[self::test]
|
|
async fn spawn() -> anyhow::Result<()> {
|
|
test_lua_api(|sender| {
|
|
run_lua! { |Pinnacle|
|
|
Pinnacle.process.spawn("foot")
|
|
}
|
|
|
|
sleep_secs(1);
|
|
|
|
assert(&sender, |state| {
|
|
assert_eq!(state.windows.len(), 1);
|
|
assert_eq!(state.windows[0].class(), Some("foot".to_string()));
|
|
});
|
|
})
|
|
}
|
|
|
|
#[tokio::main]
|
|
#[self::test]
|
|
async fn set_env() -> anyhow::Result<()> {
|
|
test_lua_api(|sender| {
|
|
run_lua! { |Pinnacle|
|
|
Pinnacle.process.set_env("PROCESS_SET_ENV", "env value")
|
|
}
|
|
|
|
sleep_secs(1);
|
|
|
|
assert(&sender, |_state| {
|
|
assert_eq!(
|
|
std::env::var("PROCESS_SET_ENV"),
|
|
Ok("env value".to_string())
|
|
);
|
|
});
|
|
})
|
|
}
|
|
}
|
|
|
|
// Window
|
|
|
|
mod window {
|
|
use super::*;
|
|
|
|
#[tokio::main]
|
|
#[self::test]
|
|
async fn get_all() -> anyhow::Result<()> {
|
|
test_lua_api(|_sender| {
|
|
run_lua! { |Pinnacle|
|
|
assert(#Pinnacle.window.get_all() == 0)
|
|
|
|
for i = 1, 5 do
|
|
Pinnacle.process.spawn("foot")
|
|
end
|
|
}
|
|
|
|
sleep_secs(1);
|
|
|
|
run_lua! { |Pinnacle|
|
|
assert(#Pinnacle.window.get_all() == 5)
|
|
}
|
|
})
|
|
}
|
|
|
|
#[tokio::main]
|
|
#[self::test]
|
|
async fn get_focused() -> anyhow::Result<()> {
|
|
test_lua_api(|_sender| {
|
|
run_lua! { |Pinnacle|
|
|
assert(not Pinnacle.window.get_focused())
|
|
|
|
Pinnacle.tag.add(Pinnacle.output.get_focused(), "1")[1]:set_active(true)
|
|
Pinnacle.process.spawn("foot")
|
|
}
|
|
|
|
sleep_secs(1);
|
|
|
|
run_lua! { |Pinnacle|
|
|
assert(Pinnacle.window.get_focused())
|
|
}
|
|
})
|
|
}
|
|
|
|
#[tokio::main]
|
|
#[self::test]
|
|
async fn add_window_rule() -> anyhow::Result<()> {
|
|
test_lua_api(|sender| {
|
|
run_lua! { |Pinnacle|
|
|
Pinnacle.tag.add(Pinnacle.output.get_focused(), "Tag Name")
|
|
Pinnacle.window.add_window_rule({
|
|
cond = { classes = { "firefox" } },
|
|
rule = { tags = { Pinnacle.tag.get("Tag Name") } },
|
|
})
|
|
}
|
|
|
|
sleep_secs(1);
|
|
|
|
assert(&sender, |state| {
|
|
assert_eq!(state.config.window_rules.len(), 1);
|
|
assert_eq!(
|
|
state.config.window_rules[0],
|
|
(
|
|
WindowRuleCondition {
|
|
class: Some(vec!["firefox".to_string()]),
|
|
..Default::default()
|
|
},
|
|
WindowRule {
|
|
tags: Some(vec![TagId(0)]),
|
|
..Default::default()
|
|
}
|
|
)
|
|
);
|
|
});
|
|
|
|
run_lua! { |Pinnacle|
|
|
Pinnacle.tag.add(Pinnacle.output.get_focused(), "Tag Name 2")
|
|
Pinnacle.window.add_window_rule({
|
|
cond = {
|
|
all = {
|
|
{
|
|
classes = { "steam" },
|
|
tags = {
|
|
Pinnacle.tag.get("Tag Name"),
|
|
Pinnacle.tag.get("Tag Name 2"),
|
|
},
|
|
}
|
|
}
|
|
},
|
|
rule = { fullscreen_or_maximized = "fullscreen" },
|
|
})
|
|
}
|
|
|
|
sleep_secs(1);
|
|
|
|
assert(&sender, |state| {
|
|
assert_eq!(state.config.window_rules.len(), 2);
|
|
assert_eq!(
|
|
state.config.window_rules[1],
|
|
(
|
|
WindowRuleCondition {
|
|
cond_all: Some(vec![WindowRuleCondition {
|
|
class: Some(vec!["steam".to_string()]),
|
|
tag: Some(vec![TagId(0), TagId(1)]),
|
|
..Default::default()
|
|
}]),
|
|
..Default::default()
|
|
},
|
|
WindowRule {
|
|
fullscreen_or_maximized: Some(FullscreenOrMaximized::Fullscreen),
|
|
..Default::default()
|
|
}
|
|
)
|
|
);
|
|
});
|
|
})
|
|
}
|
|
|
|
// TODO: window_begin_move
|
|
// TODO: window_begin_resize
|
|
|
|
mod handle {
|
|
use super::*;
|
|
|
|
// WindowHandle
|
|
|
|
#[tokio::main]
|
|
#[self::test]
|
|
async fn close() -> anyhow::Result<()> {
|
|
test_lua_api(|sender| {
|
|
run_lua! { |Pinnacle|
|
|
Pinnacle.process.spawn("foot")
|
|
}
|
|
|
|
sleep_secs(1);
|
|
|
|
assert(&sender, |state| {
|
|
assert_eq!(state.windows.len(), 1);
|
|
});
|
|
|
|
run_lua! { |Pinnacle|
|
|
Pinnacle.window.get_all()[1]:close()
|
|
}
|
|
|
|
sleep_secs(1);
|
|
|
|
assert(&sender, |state| {
|
|
assert_eq!(state.windows.len(), 0);
|
|
});
|
|
})
|
|
}
|
|
|
|
#[tokio::main]
|
|
#[self::test]
|
|
async fn move_to_tag() -> anyhow::Result<()> {
|
|
test_lua_api(|sender| {
|
|
run_lua! { |Pinnacle|
|
|
local tags = Pinnacle.tag.add(Pinnacle.output.get_focused(), "1", "2", "3")
|
|
tags[1]:set_active(true)
|
|
tags[2]:set_active(true)
|
|
Pinnacle.process.spawn("foot")
|
|
}
|
|
|
|
sleep_secs(1);
|
|
|
|
assert(&sender, |state| {
|
|
assert_eq!(
|
|
state.windows[0].with_state(|st| st
|
|
.tags
|
|
.iter()
|
|
.map(|tag| tag.name())
|
|
.collect::<Vec<_>>()),
|
|
vec!["1", "2"]
|
|
);
|
|
});
|
|
|
|
// Correct usage
|
|
run_lua! { |Pinnacle|
|
|
Pinnacle.window.get_all()[1]:move_to_tag(Pinnacle.tag.get("3"))
|
|
}
|
|
|
|
sleep_secs(1);
|
|
|
|
assert(&sender, |state| {
|
|
assert_eq!(
|
|
state.windows[0].with_state(|st| st
|
|
.tags
|
|
.iter()
|
|
.map(|tag| tag.name())
|
|
.collect::<Vec<_>>()),
|
|
vec!["3"]
|
|
);
|
|
});
|
|
|
|
// Move to the same tag
|
|
run_lua! { |Pinnacle|
|
|
Pinnacle.window.get_all()[1]:move_to_tag(Pinnacle.tag.get("3"))
|
|
}
|
|
|
|
sleep_secs(1);
|
|
|
|
assert(&sender, |state| {
|
|
assert_eq!(
|
|
state.windows[0].with_state(|st| st
|
|
.tags
|
|
.iter()
|
|
.map(|tag| tag.name())
|
|
.collect::<Vec<_>>()),
|
|
vec!["3"]
|
|
);
|
|
});
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::main]
|
|
#[test]
|
|
async fn window_count_with_tag_is_correct() -> anyhow::Result<()> {
|
|
test_lua_api(|sender| {
|
|
run_lua! { |Pinnacle|
|
|
Pinnacle.tag.add(Pinnacle.output.get_focused(), "1")
|
|
Pinnacle.process.spawn("foot")
|
|
}
|
|
|
|
sleep_secs(1);
|
|
|
|
assert(&sender, |state| assert_eq!(state.windows.len(), 1));
|
|
|
|
run_lua! { |Pinnacle|
|
|
for i = 1, 20 do
|
|
Pinnacle.process.spawn("foot")
|
|
end
|
|
}
|
|
|
|
sleep_secs(1);
|
|
|
|
assert(&sender, |state| assert_eq!(state.windows.len(), 21));
|
|
})
|
|
}
|
|
|
|
#[tokio::main]
|
|
#[test]
|
|
async fn window_count_without_tag_is_correct() -> anyhow::Result<()> {
|
|
test_lua_api(|sender| {
|
|
run_lua! { |Pinnacle|
|
|
Pinnacle.process.spawn("foot")
|
|
}
|
|
|
|
sleep_secs(1);
|
|
|
|
assert(&sender, |state| assert_eq!(state.windows.len(), 1));
|
|
})
|
|
}
|
|
|
|
#[tokio::main]
|
|
#[test]
|
|
async fn spawned_window_on_active_tag_has_keyboard_focus() -> anyhow::Result<()> {
|
|
test_lua_api(|sender| {
|
|
run_lua! { |Pinnacle|
|
|
Pinnacle.tag.add(Pinnacle.output.get_focused(), "1")[1]:set_active(true)
|
|
Pinnacle.process.spawn("foot")
|
|
}
|
|
|
|
sleep_secs(1);
|
|
|
|
assert(&sender, |state| {
|
|
assert_eq!(
|
|
state
|
|
.focused_window(state.focused_output().unwrap())
|
|
.unwrap()
|
|
.class(),
|
|
Some("foot".to_string())
|
|
);
|
|
});
|
|
})
|
|
}
|
|
|
|
#[tokio::main]
|
|
#[test]
|
|
async fn spawned_window_on_inactive_tag_does_not_have_keyboard_focus() -> anyhow::Result<()> {
|
|
test_lua_api(|sender| {
|
|
run_lua! { |Pinnacle|
|
|
Pinnacle.tag.add(Pinnacle.output.get_focused(), "1")
|
|
Pinnacle.process.spawn("foot")
|
|
}
|
|
|
|
sleep_secs(1);
|
|
|
|
assert(&sender, |state| {
|
|
assert_eq!(state.focused_window(state.focused_output().unwrap()), None);
|
|
});
|
|
})
|
|
}
|
|
|
|
#[tokio::main]
|
|
#[test]
|
|
async fn spawned_window_has_correct_tags() -> anyhow::Result<()> {
|
|
test_lua_api(|sender| {
|
|
run_lua! { |Pinnacle|
|
|
Pinnacle.tag.add(Pinnacle.output.get_focused(), "1", "2", "3")
|
|
Pinnacle.process.spawn("foot")
|
|
}
|
|
|
|
sleep_secs(1);
|
|
|
|
assert(&sender, |state| {
|
|
assert_eq!(state.windows.len(), 1);
|
|
assert_eq!(state.windows[0].with_state(|st| st.tags.len()), 1);
|
|
});
|
|
|
|
run_lua! { |Pinnacle|
|
|
Pinnacle.tag.get("1"):set_active(true)
|
|
Pinnacle.tag.get("3"):set_active(true)
|
|
Pinnacle.process.spawn("foot")
|
|
}
|
|
|
|
sleep_secs(1);
|
|
|
|
assert(&sender, |state| {
|
|
assert_eq!(state.windows.len(), 2);
|
|
assert_eq!(state.windows[1].with_state(|st| st.tags.len()), 2);
|
|
assert_eq!(
|
|
state.windows[1].with_state(|st| st
|
|
.tags
|
|
.iter()
|
|
.map(|tag| tag.name())
|
|
.collect::<Vec<_>>()),
|
|
vec!["1", "3"]
|
|
);
|
|
});
|
|
})
|
|
}
|