use std::time::Duration;
use super::common::unique_pipe_name;
use super::test_desktop::{
DaemonGuard, TestDesktop, TestWindow, query_windows, start_test_daemon, stop_test_daemon,
unique_title,
};
use flow_wm::common::Rect;
use flow_wm::ipc::message::SocketMessage;
use flow_wm::ipc::transport;
use flow_wm::registry::win32::get_window_rect;
fn find_window_by_title_base<'a>(
json: &'a serde_json::Value,
base: &str,
) -> Option<&'a serde_json::Value> {
let prefix = format!("FlowTest-{base}-");
json["windows"].as_array().and_then(|arr| {
arr.iter().find(|w| {
w["title"]
.as_str()
.map(|t| t.starts_with(&prefix))
.unwrap_or(false)
})
})
}
fn is_tiling_active(state: &serde_json::Value) -> bool {
state.get("Tiling").and_then(|t| t.get("Active")).is_some()
}
#[test]
#[allow(clippy::zombie_processes)] fn daemon_registers_existing_windows_as_tiling() {
let _ = env_logger::builder().is_test(true).try_init();
let td = TestDesktop::create().expect("create test desktop");
let pipe = unique_pipe_name();
let mut _daemon = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
std::thread::sleep(Duration::from_millis(500));
let w1 = TestWindow::create(&unique_title("snap-app1")).expect("create w1");
let w2 = TestWindow::create(&unique_title("snap-app2")).expect("create w2");
let w3 = TestWindow::create(&unique_title("snap-app3")).expect("create w3");
std::thread::sleep(Duration::from_secs(1));
let result = query_windows(&pipe).expect("query windows");
let windows = result["windows"]
.as_array()
.expect("windows should be a JSON array");
assert!(
windows.len() >= 3,
"expected at least 3 windows in registry, got {}. Windows: {:?}",
windows.len(),
windows
.iter()
.filter_map(|w| w["title"].as_str())
.collect::<Vec<_>>()
);
for base in &["snap-app1", "snap-app2", "snap-app3"] {
let entry = find_window_by_title_base(&result, base).unwrap_or_else(|| {
let titles: Vec<_> = windows.iter().filter_map(|w| w["title"].as_str()).collect();
panic!(
"window with base '{base}' not found. Registry titles: {:?}",
titles
)
});
let state = &entry["state"];
assert!(
!state.is_null(),
"window '{base}' should have a non-null state (daemon should classify it), got: {state}"
);
println!(" window '{base}' → state: {state}");
}
drop(w1);
drop(w2);
drop(w3);
drop(td);
}
#[test]
#[allow(clippy::zombie_processes)] fn daemon_arranges_windows_in_tiling_positions() {
let _ = env_logger::builder().is_test(true).try_init();
let td = TestDesktop::create().expect("create test desktop");
let pipe = unique_pipe_name();
let mut _daemon = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
std::thread::sleep(Duration::from_millis(500));
let title1 = unique_title("pos-app1");
let title2 = unique_title("pos-app2");
let w1 = TestWindow::create(&title1).expect("create w1");
let w2 = TestWindow::create(&title2).expect("create w2");
std::thread::sleep(Duration::from_secs(2));
let rect1 = get_window_rect(w1.hwnd).expect("get_window_rect w1");
let rect2 = get_window_rect(w2.hwnd).expect("get_window_rect w2");
println!(" w1 rect: {:?}", rect1);
println!(" w2 rect: {:?}", rect2);
assert!(
rect1.width > 0 && rect1.height > 0,
"w1 should have positive size, got {}x{}",
rect1.width,
rect1.height
);
assert!(
rect2.width > 0 && rect2.height > 0,
"w2 should have positive size, got {}x{}",
rect2.width,
rect2.height
);
let max_dim = 4000;
for (label, rect) in [("w1", rect1), ("w2", rect2)] {
assert!(
rect.x >= 0,
"{} x coordinate should be >= 0, got {}",
label,
rect.x
);
assert!(
rect.y >= 0,
"{} y coordinate should be >= 0, got {}",
label,
rect.y
);
assert!(
rect.width <= max_dim,
"{} width should be <= {}, got {}",
label,
max_dim,
rect.width
);
assert!(
rect.height <= max_dim,
"{} height should be <= {}, got {}",
label,
max_dim,
rect.height
);
}
let result = query_windows(&pipe).expect("query windows");
let s1 = find_window_by_title_base(&result, "pos-app1").map(|e| &e["state"]);
let s2 = find_window_by_title_base(&result, "pos-app2").map(|e| &e["state"]);
let both_tiling = s1.is_some_and(is_tiling_active) && s2.is_some_and(is_tiling_active);
if both_tiling {
let overlaps = rect1.overlaps(rect2);
println!(" both Tiling::Active, overlap={overlaps}");
if overlaps {
println!(" ⚠ tiling windows overlap — daemon may not have moved them on test desktop");
}
} else {
println!(
" windows not both tiling-active (s1={:?}, s2={:?})",
s1, s2
);
}
drop(w1);
drop(w2);
drop(td);
}
#[test]
#[allow(clippy::zombie_processes)] fn daemon_tiles_three_windows_without_overlap() {
let _ = env_logger::builder().is_test(true).try_init();
let td = TestDesktop::create().expect("create test desktop");
let pipe = unique_pipe_name();
let mut _daemon = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
std::thread::sleep(Duration::from_millis(500));
let w1 = TestWindow::create(&unique_title("tri-app1")).expect("create w1");
let w2 = TestWindow::create(&unique_title("tri-app2")).expect("create w2");
let w3 = TestWindow::create(&unique_title("tri-app3")).expect("create w3");
std::thread::sleep(Duration::from_secs(2));
let rects: Vec<(Rect, String)> = [&w1, &w2, &w3]
.iter()
.map(|w| {
let rect = get_window_rect(w.hwnd).expect("get_window_rect");
(rect, w.title.clone())
})
.collect();
for (rect, title) in &rects {
println!(" {} rect: {:?}", title, rect);
}
let max_dim = 4000;
for (rect, title) in &rects {
assert!(
rect.width > 0 && rect.height > 0,
"{} should have positive size, got {}x{}",
title,
rect.width,
rect.height
);
assert!(
rect.x >= 0 && rect.y >= 0,
"{} should have non-negative position, got ({}, {})",
title,
rect.x,
rect.y
);
assert!(
rect.width <= max_dim && rect.height <= max_dim,
"{} dimensions should be <= {max_dim}, got {}x{}",
title,
rect.width,
rect.height
);
}
let result = query_windows(&pipe).expect("query windows");
let all_tiling = ["tri-app1", "tri-app2", "tri-app3"].iter().all(|base| {
find_window_by_title_base(&result, base)
.map(|e| is_tiling_active(&e["state"]))
.unwrap_or(false)
});
if all_tiling {
let any_overlap =
(0..rects.len()).any(|i| (i + 1..rects.len()).any(|j| rects[i].0.overlaps(rects[j].0)));
println!(" all 3 Tiling::Active, any_overlap={any_overlap}");
if any_overlap {
println!(" ⚠ tiling windows overlap — daemon may not have moved them on test desktop");
}
} else {
println!(" not all windows are tiling-active");
}
drop(w1);
drop(w2);
drop(w3);
drop(td);
}
#[test]
fn daemon_shuts_down_cleanly() {
let _ = env_logger::builder().is_test(true).try_init();
let td = TestDesktop::create().expect("create test desktop");
let pipe = unique_pipe_name();
let mut child = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
std::thread::sleep(Duration::from_millis(500));
let query_result = query_windows(&pipe);
assert!(
query_result.is_ok(),
"daemon should be responsive before stop, got: {:?}",
query_result.err()
);
drop(_guard);
stop_test_daemon(&pipe);
let deadline = std::time::Instant::now() + Duration::from_secs(5);
loop {
match child.try_wait().expect("try_wait") {
Some(status) => {
println!(" daemon exited with status: {status}");
break;
}
None => {
assert!(
std::time::Instant::now() < deadline,
"daemon did not exit within 5s of Stop command"
);
std::thread::sleep(Duration::from_millis(100));
}
}
}
std::thread::sleep(Duration::from_millis(300));
let post_stop_result = query_windows(&pipe);
assert!(
post_stop_result.is_err(),
"query should fail after daemon is stopped, got: {:?}",
post_stop_result
);
drop(td);
}
#[test]
#[allow(clippy::zombie_processes)] fn double_stop_does_not_hang() {
let _ = env_logger::builder().is_test(true).try_init();
let td = TestDesktop::create().expect("create test desktop");
let pipe = unique_pipe_name();
let mut _daemon = start_test_daemon(&pipe, &td.name).expect("start daemon");
std::thread::sleep(Duration::from_millis(500));
stop_test_daemon(&pipe);
std::thread::sleep(Duration::from_millis(500));
let result = transport::send_message_to(&pipe, &SocketMessage::Stop);
match result {
Ok(resp) => {
println!(" double stop got response: {:?}", resp);
}
Err(e) => {
println!(" double stop got error (expected): {e}");
}
}
let deadline = std::time::Instant::now() + Duration::from_secs(3);
loop {
match _daemon.try_wait().expect("try_wait") {
Some(_) => break,
None => {
assert!(
std::time::Instant::now() < deadline,
"daemon did not exit after double stop"
);
std::thread::sleep(Duration::from_millis(100));
}
}
}
drop(td);
}
#[test]
#[allow(clippy::zombie_processes)] fn daemon_initializes_windows_with_sorted_positions() {
let _ = env_logger::builder().is_test(true).try_init();
let td = TestDesktop::create().expect("create test desktop");
let w1 = TestWindow::create(&unique_title("pre-app1")).expect("create w1");
let w2 = TestWindow::create(&unique_title("pre-app2")).expect("create w2");
let w3 = TestWindow::create(&unique_title("pre-app3")).expect("create w3");
std::thread::sleep(Duration::from_millis(300));
let pipe = unique_pipe_name();
let mut _daemon = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
std::thread::sleep(Duration::from_secs(2));
let result = query_windows(&pipe).expect("query windows");
let windows = result["windows"]
.as_array()
.expect("windows should be a JSON array");
assert!(
windows.len() >= 3,
"expected at least 3 windows, got {}. Titles: {:?}",
windows.len(),
windows
.iter()
.filter_map(|w| w["title"].as_str())
.collect::<Vec<_>>()
);
for base in &["pre-app1", "pre-app2", "pre-app3"] {
let entry = find_window_by_title_base(&result, base).unwrap_or_else(|| {
let titles: Vec<_> = windows.iter().filter_map(|w| w["title"].as_str()).collect();
panic!(
"window with base '{base}' not found after daemon init. Registry titles: {:?}",
titles
)
});
let state = &entry["state"];
assert!(
!state.is_null(),
"window '{base}' should have a classified state after init, got: {state}"
);
println!(" pre-init window '{base}' → state: {state}");
}
let max_dim = 4000i32;
let handles = [w1.hwnd, w2.hwnd, w3.hwnd];
for hwnd in &handles {
let rect = get_window_rect(*hwnd).expect("get_window_rect");
assert!(
rect.width > 0 && rect.height > 0,
"window {:?} should have positive size, got {}x{}",
hwnd,
rect.width,
rect.height
);
assert!(
rect.x >= 0 && rect.y >= 0,
"window {:?} should have non-negative position, got ({}, {})",
hwnd,
rect.x,
rect.y
);
assert!(
rect.width <= max_dim && rect.height <= max_dim,
"window {:?} dimensions should be <= {}, got {}x{}",
hwnd,
max_dim,
rect.width,
rect.height
);
}
drop(w1);
drop(w2);
drop(w3);
drop(td);
}
#[test]
#[allow(clippy::zombie_processes)] fn daemon_init_excludes_background_windows_from_real_desktop() {
let _ = env_logger::builder().is_test(true).try_init();
let td = TestDesktop::create().expect("create test desktop");
let pipe = unique_pipe_name();
let mut _daemon = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
std::thread::sleep(Duration::from_millis(500));
let w1 = TestWindow::create(&unique_title("bg-app1")).expect("create w1");
let w2 = TestWindow::create(&unique_title("bg-app2")).expect("create w2");
std::thread::sleep(Duration::from_secs(1));
let result = query_windows(&pipe).expect("query windows");
let windows = result["windows"]
.as_array()
.expect("windows should be a JSON array");
for w in windows {
let title = w["title"].as_str().unwrap_or("");
assert!(
!title.is_empty(),
"registry should not contain windows with empty titles, got window with hwnd={:?}",
w["hwnd"]
);
}
for w in windows {
let rect = w["window_rect"]
.as_object()
.or_else(|| w["pre_manage_rect"].as_object())
.expect("window entry should have window_rect or pre_manage_rect");
let x = rect["x"].as_i64().unwrap_or(0);
let y = rect["y"].as_i64().unwrap_or(0);
let width = rect["width"].as_i64().unwrap_or(0);
let height = rect["height"].as_i64().unwrap_or(0);
assert!(
x > -1000 && y > -1000,
"registry should not contain offscreen windows, got hwnd={:?} at ({}, {})",
w["hwnd"],
x,
y,
);
assert!(
width >= 2 && height >= 2,
"registry should not contain degenerate windows, got hwnd={:?} with {}x{}",
w["hwnd"],
width,
height,
);
}
for base in &["bg-app1", "bg-app2"] {
let entry = find_window_by_title_base(&result, base);
assert!(
entry.is_some(),
"test window '{base}' should be in registry. Registry titles: {:?}",
windows
.iter()
.filter_map(|w| w["title"].as_str())
.collect::<Vec<_>>()
);
}
println!(
" registry contains {} windows (all have non-empty titles and on-screen positions)",
windows.len()
);
drop(w1);
drop(w2);
drop(td);
}
#[test]
#[allow(clippy::zombie_processes)] fn daemon_init_tiling_windows_have_unique_columns() {
let _ = env_logger::builder().is_test(true).try_init();
let td = TestDesktop::create().expect("create test desktop");
let pipe = unique_pipe_name();
let mut _daemon = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
std::thread::sleep(Duration::from_millis(500));
let _w1 = TestWindow::create(&unique_title("col-app1")).expect("create w1");
let _w2 = TestWindow::create(&unique_title("col-app2")).expect("create w2");
let _w3 = TestWindow::create(&unique_title("col-app3")).expect("create w3");
std::thread::sleep(Duration::from_secs(2));
let result = query_windows(&pipe).expect("query windows");
let windows = result["windows"]
.as_array()
.expect("windows should be a JSON array");
assert!(
windows.len() >= 3,
"expected at least 3 windows in registry, got {}. Titles: {:?}",
windows.len(),
windows
.iter()
.filter_map(|w| w["title"].as_str())
.collect::<Vec<_>>()
);
let mut cols: Vec<i64> = Vec::new();
for w in windows {
let state = &w["state"];
if is_tiling_active(state) {
let col = state["Tiling"]["Active"]["col"].as_i64().unwrap_or(-1);
cols.push(col);
println!(
" tiling window '{}' → col={}",
w["title"].as_str().unwrap_or("?"),
col
);
}
}
println!(" tiling-active windows: {}", cols.len());
println!(" columns: {:?}", cols);
assert!(
!cols.is_empty(),
"expected at least 1 tiling-active window, got none"
);
let all_zero = cols.iter().all(|&c| c == 0);
if all_zero && cols.len() > 1 {
println!(
" ⚠ REGRESSION INDICATOR: all {} tiling windows have col=0 \
(expected distinct columns 0, 1, 2). \
The layout engine may not have written back projected positions.",
cols.len()
);
} else if cols.len() > 1 {
let unique_cols: std::collections::HashSet<i64> = cols.iter().copied().collect();
assert_eq!(
unique_cols.len(),
cols.len(),
"tiling-active windows should have unique columns, got {:?}",
cols
);
println!(
" ✓ {} tiling windows have unique columns: {:?}",
cols.len(),
cols
);
}
drop(_w1);
drop(_w2);
drop(_w3);
drop(td);
}
#[test]
#[allow(clippy::zombie_processes)] fn daemon_init_tiling_windows_have_reasonable_positions() {
let _ = env_logger::builder().is_test(true).try_init();
let td = TestDesktop::create().expect("create test desktop");
let pipe = unique_pipe_name();
let mut _daemon = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
std::thread::sleep(Duration::from_millis(500));
let title1 = unique_title("pos2-app1");
let title2 = unique_title("pos2-app2");
let w1 = TestWindow::create(&title1).expect("create w1");
let w2 = TestWindow::create(&title2).expect("create w2");
std::thread::sleep(Duration::from_secs(2));
let rect1 = get_window_rect(w1.hwnd).expect("get_window_rect w1");
let rect2 = get_window_rect(w2.hwnd).expect("get_window_rect w2");
println!(" w1 rect: {:?}", rect1);
println!(" w2 rect: {:?}", rect2);
assert!(
rect1.width > 0 && rect1.height > 0,
"w1 should have positive size, got {}x{}",
rect1.width,
rect1.height
);
assert!(
rect2.width > 0 && rect2.height > 0,
"w2 should have positive size, got {}x{}",
rect2.width,
rect2.height
);
assert!(
rect1.x >= 0 && rect1.y >= 0,
"w1 should have non-negative position, got ({}, {})",
rect1.x,
rect1.y
);
assert!(
rect2.x >= 0 && rect2.y >= 0,
"w2 should have non-negative position, got ({}, {})",
rect2.x,
rect2.y
);
let result = query_windows(&pipe).expect("query windows");
let s1 = find_window_by_title_base(&result, "pos2-app1").map(|e| &e["state"]);
let s2 = find_window_by_title_base(&result, "pos2-app2").map(|e| &e["state"]);
let both_tiling = s1.is_some_and(is_tiling_active) && s2.is_some_and(is_tiling_active);
if both_tiling {
let overlaps = rect1.overlaps(rect2);
println!(" both Tiling::Active, overlap={overlaps}");
if overlaps {
println!(
" ⚠ tiling windows overlap on test desktop — SetWindowPos may not work \
correctly on isolated desktops"
);
} else {
println!(" ✓ tiling windows do not overlap");
}
} else {
println!(
" windows not both tiling-active (s1={:?}, s2={:?})",
s1, s2
);
}
let max_dim = 4000i32;
for (label, rect) in [("w1", rect1), ("w2", rect2)] {
assert!(
rect.width <= max_dim,
"{} width should be <= {}, got {}",
label,
max_dim,
rect.width
);
assert!(
rect.height <= max_dim,
"{} height should be <= {}, got {}",
label,
max_dim,
rect.height
);
}
drop(w1);
drop(w2);
drop(td);
}