Merge pull request #174 from pinnacle-comp/integration_testing

Add some Lua API integration/unit tests

Ok they're more like unit tests tbh
This commit is contained in:
Ottatop 2024-03-11 19:45:09 -05:00 committed by GitHub
commit eea10c60a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 752 additions and 78 deletions

View file

@ -22,7 +22,7 @@ jobs:
- name: Cache stuff
uses: Swatinem/rust-cache@v2
- name: Get dependencies
run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler
run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev
- name: Setup Lua
uses: leafo/gh-actions-lua@v10
with:
@ -44,7 +44,7 @@ jobs:
- name: Cache stuff
uses: Swatinem/rust-cache@v2
- name: Get dependencies
run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler
run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev foot
- name: Setup Lua
uses: leafo/gh-actions-lua@v10
with:
@ -52,7 +52,11 @@ jobs:
- name: Setup LuaRocks
uses: leafo/gh-actions-luarocks@v4
- name: Build
run: cargo test
if: ${{ runner.debug != '1' }}
run: cargo test -- --test-threads=1
- name: Build (debug)
if: ${{ runner.debug == '1' }}
run: RUST_LOG=debug cargo test -- --nocapture --test-threads=1
check-format:
runs-on: ubuntu-latest
name: Check formatting
@ -75,8 +79,10 @@ jobs:
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Cache stuff
uses: Swatinem/rust-cache@v2
- name: Get dependencies
run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler
run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev
- name: Setup Lua
uses: leafo/gh-actions-lua@v10
with:

23
Cargo.lock generated
View file

@ -1768,6 +1768,7 @@ dependencies = [
"dircpy",
"image",
"nix",
"pinnacle",
"pinnacle-api-defs",
"prost",
"serde",
@ -1777,6 +1778,7 @@ dependencies = [
"sysinfo",
"temp-env",
"tempfile",
"test-log",
"thiserror",
"tokio",
"tokio-stream",
@ -2405,6 +2407,27 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "test-log"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b319995299c65d522680decf80f2c108d85b861d81dfe340a10d16cee29d9e6"
dependencies = [
"test-log-macros",
"tracing-subscriber",
]
[[package]]
name = "test-log-macros"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8f546451eaa38373f549093fe9fd05e7d2bade739e2ddf834b9968621d60107"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "textwrap"
version = "0.16.1"

View file

@ -36,7 +36,7 @@ keywords = ["wayland", "compositor", "smithay", "lua"]
[dependencies]
# Smithay
smithay = { git = "https://github.com/Smithay/smithay", rev = "418190e", default-features = false, features = ["desktop", "wayland_frontend"] }
# smithay is down there somewhere
smithay-drm-extras = { git = "https://github.com/Smithay/smithay", rev = "418190e" }
# Tracing
tracing = "0.1.40"
@ -72,32 +72,44 @@ pinnacle-api-defs = { workspace = true }
dircpy = "0.3.16"
chrono = "0.4.34"
[dependencies.smithay]
git = "https://github.com/Smithay/smithay"
rev = "418190e"
default-features = false
features = [
"desktop",
"wayland_frontend",
# udev
"backend_libinput",
"backend_udev",
"backend_drm",
"backend_gbm",
"backend_vulkan",
"backend_egl",
"backend_session_libseat",
"renderer_gl",
"renderer_multi",
# egl
"use_system_lib",
"backend_egl",
# winit
"backend_winit",
"backend_drm",
# xwayland
"xwayland",
"x11rb_event_source",
]
[build-dependencies]
xdg = { workspace = true }
[dev-dependencies]
temp-env = "0.3.6"
tempfile = "3.10.1"
test-log = { version = "0.2.15", default-features = false, features = ["trace"] }
pinnacle = { path = ".", features = ["testing"] }
[features]
default = [
# udev
"smithay/backend_libinput",
"smithay/backend_udev",
"smithay/backend_drm",
"smithay/backend_gbm",
"smithay/backend_vulkan",
"smithay/backend_egl",
"smithay/backend_session_libseat",
"smithay/renderer_gl",
"smithay/renderer_multi",
# egl
"smithay/use_system_lib",
"smithay/backend_egl",
# winit
"smithay/backend_winit",
"smithay/backend_drm",
# xwayland
"smithay/xwayland",
"smithay/x11rb_event_source"
testing = [
"smithay/renderer_test",
]

View file

@ -224,7 +224,7 @@ local _fullscreen_or_maximized_keys = {
--- -- A simple window rule. This one will cause Firefox to open on tag "Browser".
---Window.add_window_rule({
--- cond = { classes = { "firefox" } },
--- rule = { tags = { "Browser" } },
--- rule = { tags = { Tag.get("Browser") } },
---})
---
--- -- To apply rules when *all* provided conditions are true, use `all`.
@ -234,8 +234,8 @@ local _fullscreen_or_maximized_keys = {
--- cond = {
--- all = {
--- {
--- class = "steam",
--- tag = Tag:get("5"),
--- classes = { "steam" },
--- tags = { Tag.get("5") },
--- }
--- }
--- },
@ -246,8 +246,8 @@ local _fullscreen_or_maximized_keys = {
--- -- Thus, the above can be shortened to:
---Window.add_window_rule({
--- cond = {
--- class = "steam",
--- tag = Tag:get("5"),
--- classes = { "steam" },
--- tags = { Tag.get("5") },
--- },
--- rule = { fullscreen_or_maximized = "fullscreen" },
---})
@ -268,17 +268,17 @@ local _fullscreen_or_maximized_keys = {
--- cond = {
--- all = { -- This `all` block is needed because the outermost block cannot be an array.
--- { any = {
--- { class = { "firefox", "thunderbird", "discord" } }
--- { classes = { "firefox", "thunderbird", "discord" } }
--- } },
--- { any = {
--- -- Because `tag` is inside an `all` block,
--- -- the window must have all these tags for this to be true.
--- -- If it was in an `any` block, only one tag would need to match.
--- { all = {
--- { tag = { "A", "B", "C" } }
--- { tags = { Tag.get("A"), Tag.get("B"), Tag.get("C") } }
--- } },
--- { all = {
--- { tag = { "1", "2" } }
--- { tags = { Tag.get("1"), Tag.get("2") } }
--- } },
--- } }
--- }

View file

@ -32,8 +32,12 @@ use crate::{
window::WindowElement,
};
#[cfg(feature = "testing")]
use self::dummy::Dummy;
use self::{udev::Udev, winit::Winit};
#[cfg(feature = "testing")]
pub mod dummy;
pub mod udev;
pub mod winit;
@ -42,6 +46,8 @@ pub enum Backend {
Winit(Winit),
/// The compositor is running in a tty
Udev(Udev),
#[cfg(feature = "testing")]
Dummy(Dummy),
}
impl Backend {
@ -49,6 +55,8 @@ impl Backend {
match self {
Backend::Winit(winit) => winit.seat_name(),
Backend::Udev(udev) => udev.seat_name(),
#[cfg(feature = "testing")]
Backend::Dummy(dummy) => dummy.seat_name(),
}
}
@ -56,6 +64,8 @@ impl Backend {
match self {
Backend::Winit(winit) => winit.early_import(surface),
Backend::Udev(udev) => udev.early_import(surface),
#[cfg(feature = "testing")]
Backend::Dummy(dummy) => dummy.early_import(surface),
}
}
@ -180,6 +190,8 @@ impl DmabufHandler for State {
.expect("udev had no dmabuf state")
.0
}
#[cfg(feature = "testing")]
Backend::Dummy(_) => unreachable!(),
}
}
@ -202,6 +214,12 @@ impl DmabufHandler for State {
.and_then(|mut renderer| renderer.import_dmabuf(&dmabuf, None))
.map(|_| ())
.map_err(|_| ()),
#[cfg(feature = "testing")]
Backend::Dummy(dummy) => dummy
.renderer
.import_dmabuf(&dmabuf, None)
.map(|_| ())
.map_err(|_| ()),
};
if res.is_ok() {

118
src/backend/dummy.rs Normal file
View file

@ -0,0 +1,118 @@
use smithay::backend::renderer::test::DummyRenderer;
use smithay::backend::renderer::ImportMemWl;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use std::ffi::OsString;
use std::path::PathBuf;
use smithay::{
output::{Output, Subpixel},
reexports::{calloop::EventLoop, wayland_server::Display},
utils::Transform,
};
use crate::state::State;
use super::Backend;
use super::BackendData;
pub struct Dummy {
pub renderer: DummyRenderer,
// pub dmabuf_state: (DmabufState, DmabufGlobal, Option<DmabufFeedback>),
}
impl Backend {
fn dummy_mut(&mut self) -> &Dummy {
let Backend::Dummy(dummy) = self else { unreachable!() };
dummy
}
}
impl BackendData for Dummy {
fn seat_name(&self) -> String {
"Dummy".to_string()
}
fn reset_buffers(&mut self, _output: &Output) {}
fn early_import(&mut self, _surface: &WlSurface) {}
}
pub fn setup_dummy(
no_config: bool,
config_dir: Option<PathBuf>,
) -> anyhow::Result<(State, EventLoop<'static, State>)> {
let event_loop: EventLoop<State> = EventLoop::try_new()?;
let display: Display<State> = Display::new()?;
let display_handle = display.handle();
let loop_handle = event_loop.handle();
let mode = smithay::output::Mode {
size: (1920, 1080).into(),
refresh: 60_000,
};
let physical_properties = smithay::output::PhysicalProperties {
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: "Pinnacle".to_string(),
model: "Winit Window".to_string(),
};
let output = Output::new("Pinnacle Window".to_string(), physical_properties);
output.create_global::<State>(&display_handle);
output.change_current_state(
Some(mode),
Some(Transform::Flipped180),
None,
Some((0, 0).into()),
);
output.set_preferred(mode);
let renderer = DummyRenderer::new();
// let dmabuf_state = {
// let dmabuf_formats = renderer.dmabuf_formats().collect::<Vec<_>>();
// let mut dmabuf_state = DmabufState::new();
// let dmabuf_global = dmabuf_state.create_global::<State>(&display_handle, dmabuf_formats);
// (dmabuf_state, dmabuf_global, None)
// };
let backend = Dummy {
renderer,
// dmabuf_state,
};
let mut state = State::init(
super::Backend::Dummy(backend),
display,
event_loop.get_signal(),
loop_handle,
no_config,
config_dir,
)?;
state.output_focus_stack.set_focus(output.clone());
let dummy = state.backend.dummy_mut();
state.shm_state.update_formats(dummy.renderer.shm_formats());
state.space.map_output(&output, (0, 0));
if let Err(err) = state.xwayland.start(
state.loop_handle.clone(),
None,
std::iter::empty::<(OsString, OsString)>(),
true,
|_| {},
) {
tracing::error!("Failed to start XWayland: {err}");
}
Ok((state, event_loop))
}

View file

@ -167,49 +167,46 @@ impl State {
///
/// Does nothing when called on the winit backend.
pub fn switch_vt(&mut self, vt: i32) {
match &mut self.backend {
Backend::Winit(_) => (),
Backend::Udev(udev) => {
for backend in udev.backends.values_mut() {
for surface in backend.surfaces.values_mut() {
// Clear the overlay planes on tty switch.
//
// On my machine, switching a tty would leave the topmost window on the
// screen. Smithay will render the topmost window on the overlay plane,
// so we clear it here.
let planes = surface.compositor.surface().planes().clone();
tracing::debug!("Clearing overlay planes");
for overlay_plane in planes.overlay {
if let Err(err) = surface
.compositor
.surface()
.clear_plane(overlay_plane.handle)
{
tracing::warn!("Failed to clear overlay planes: {err}");
}
if let Backend::Udev(udev) = &mut self.backend {
for backend in udev.backends.values_mut() {
for surface in backend.surfaces.values_mut() {
// Clear the overlay planes on tty switch.
//
// On my machine, switching a tty would leave the topmost window on the
// screen. Smithay will render the topmost window on the overlay plane,
// so we clear it here.
let planes = surface.compositor.surface().planes().clone();
tracing::debug!("Clearing overlay planes");
for overlay_plane in planes.overlay {
if let Err(err) = surface
.compositor
.surface()
.clear_plane(overlay_plane.handle)
{
tracing::warn!("Failed to clear overlay planes: {err}");
}
}
}
// Wait for the clear to commit before switching
self.schedule(
|state| {
let udev = state.backend.udev();
!udev
.backends
.values()
.flat_map(|backend| backend.surfaces.values())
.map(|surface| surface.compositor.surface())
.any(|drm_surf| drm_surf.commit_pending())
},
move |state| {
let udev = state.backend.udev_mut();
if let Err(err) = udev.session.change_vt(vt) {
tracing::error!("Failed to switch to vt {vt}: {err}");
}
},
);
}
// Wait for the clear to commit before switching
self.schedule(
|state| {
let udev = state.backend.udev();
!udev
.backends
.values()
.flat_map(|backend| backend.surfaces.values())
.map(|surface| surface.compositor.surface())
.any(|drm_surf| drm_surf.commit_pending())
},
move |state| {
let udev = state.backend.udev_mut();
if let Err(err) = udev.session.change_vt(vt) {
tracing::error!("Failed to switch to vt {vt}: {err}");
}
},
);
}
}
}

View file

@ -412,6 +412,8 @@ fn generate_config(args: ConfigGen) -> anyhow::Result<()> {
#[cfg(test)]
mod tests {
use anyhow::Context;
use super::*;
// TODO: find a way to test the interactive bits programmatically
@ -428,9 +430,7 @@ mod tests {
"--lang",
"rust",
"--dir",
temp_dir
.to_str()
.ok_or(anyhow::anyhow!("not valid unicode"))?,
temp_dir.to_str().context("not valid unicode")?,
"--non-interactive",
]);

View file

@ -404,7 +404,7 @@ impl State {
Ok(())
}
fn start_grpc_server(&mut self, socket_dir: &Path) -> anyhow::Result<()> {
pub fn start_grpc_server(&mut self, socket_dir: &Path) -> anyhow::Result<()> {
self.system_processes
.refresh_processes_specifics(ProcessRefreshKind::new());

View file

@ -14,7 +14,7 @@ use std::num::NonZeroU32;
use crate::{output::OutputName, tag::TagId, window::window_state::FullscreenOrMaximized};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
pub struct WindowRuleCondition {
/// This condition is met when any of the conditions provided is met.
#[serde(default)]
@ -137,7 +137,7 @@ impl WindowRuleCondition {
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
pub struct WindowRule {
/// Set the output the window will open on.
#[serde(default)]

500
tests/lua_api.rs Normal file
View file

@ -0,0 +1,500 @@
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").setup(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
#[tokio::main]
#[self::test]
async fn process_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 process_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
#[tokio::main]
#[self::test]
async fn window_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 window_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 window_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::Some(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::Some(0), TagId::Some(1)]),
..Default::default()
}]),
..Default::default()
},
WindowRule {
fullscreen_or_maximized: Some(FullscreenOrMaximized::Fullscreen),
..Default::default()
}
)
);
});
})
}
// TODO: window_begin_move
// TODO: window_begin_resize
// WindowHandle
#[tokio::main]
#[self::test]
async fn window_handle_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 window_handle_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"]
);
});
})
}