use bevy_ecs::prelude::*;
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::WindowsAndMessaging::{
SET_WINDOW_POS_FLAGS, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE,
};
use windows::core::HRESULT;
use super::{
ChainOp, CrossEdge, CrossOwnerLink, log_chain_absent, log_chain_link_failed, log_chain_linked,
log_chain_settled, log_chain_skipped, log_chain_unlink_failed, log_chain_unlinked,
nudge_command, plan_chain_ops,
};
use crate::ecs::test_support::capture_under_filter;
use crate::ecs::window::SetWindowPosCommand;
use crate::ecs::window::zorder_chain_diag::{
ChainSegment, ChainSkipReason, DetachReason, chain_record_tags, log_group_rejected,
};
const SIGNOFF_DIRECTIVES: &str = "info,wintf::ecs::window::zorder_chain=debug";
const CHAIN_TARGET: &str = "wintf::ecs::window::zorder_chain";
const DIAG_TARGET: &str = "wintf::ecs::window::zorder_chain_diag";
fn fake_hwnd(v: usize) -> HWND {
HWND(v as *mut _)
}
fn entities(n: usize) -> Vec<Entity> {
let mut world = World::new();
(0..n).map(|_| world.spawn_empty().id()).collect()
}
fn edge(owned: Entity, owner: Entity) -> CrossEdge {
CrossEdge {
owned,
owner,
segment: ChainSegment::Group(0),
}
}
fn edge_in_tail(owned: Entity, owner: Entity) -> CrossEdge {
CrossEdge {
owned,
owner,
segment: ChainSegment::Tail,
}
}
fn ledger(owned: Entity, owner: Entity, seq: usize) -> (Entity, CrossOwnerLink) {
(
owned,
CrossOwnerLink {
owner,
owned_hwnd: fake_hwnd(0x1000 + seq),
owner_hwnd: fake_hwnd(0x2000 + seq),
segment: ChainSegment::Group(0),
},
)
}
fn fake_error() -> windows::core::Error {
windows::core::Error::from(HRESULT(0x8007_0005u32 as i32))
}
fn only_line_with<'a>(out: &'a str, tag: &str) -> &'a str {
let found: Vec<&str> = out.lines().filter(|l| l.contains(tag)).collect();
assert_eq!(found.len(), 1, "`{tag}` の行がちょうど 1 本ではない: {out}");
found[0]
}
fn code_only(source: &str) -> String {
source
.lines()
.filter(|line| !line.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n")
}
fn hwnds_named_by(cmd: &SetWindowPosCommand) -> Vec<HWND> {
let mut named = vec![cmd.hwnd];
named.extend(cmd.hwnd_insert_after);
named
}
fn assert_every_detach_precedes_every_attach(ops: &[ChainOp]) {
let last_detach = ops
.iter()
.rposition(|op| matches!(op, ChainOp::Detach { .. }));
let first_attach = ops
.iter()
.position(|op| matches!(op, ChainOp::Attach { .. }));
if let (Some(last), Some(first)) = (last_detach, first_attach) {
assert!(
last < first,
"付与のあとに撤去が並んでいる(途中状態が壊れる手順): {ops:?}"
);
}
}
#[test]
fn adding_edges_to_an_empty_ledger_yields_attaches_in_the_declared_order() {
let e = entities(3);
let desired = [edge(e[0], e[1]), edge(e[1], e[2])];
let ops = plan_chain_ops(&desired, &[]);
assert_eq!(
ops,
vec![
ChainOp::Attach {
owned: e[0],
owner: e[1]
},
ChainOp::Attach {
owned: e[1],
owner: e[2]
},
]
);
assert_every_detach_precedes_every_attach(&ops);
}
#[test]
fn dropping_the_whole_chain_tears_down_every_recorded_edge() {
let e = entities(3);
let current = [ledger(e[0], e[1], 0), ledger(e[1], e[2], 1)];
let ops = plan_chain_ops(&[], ¤t);
assert_eq!(
ops,
vec![
ChainOp::Detach {
owned: e[0],
reason: DetachReason::Teardown
},
ChainOp::Detach {
owned: e[1],
reason: DetachReason::Teardown
},
]
);
assert_every_detach_precedes_every_attach(&ops);
}
#[test]
fn rechaining_one_window_detaches_before_it_attaches() {
let e = entities(3);
let desired = [edge(e[0], e[2])];
let current = [ledger(e[0], e[1], 0)];
let ops = plan_chain_ops(&desired, ¤t);
assert_eq!(
ops,
vec![
ChainOp::Detach {
owned: e[0],
reason: DetachReason::Rechain
},
ChainOp::Attach {
owned: e[0],
owner: e[2]
},
]
);
assert_every_detach_precedes_every_attach(&ops);
}
#[test]
fn a_chain_that_already_matches_yields_no_operation_at_all() {
let e = entities(3);
let desired = [edge(e[0], e[1]), edge(e[1], e[2])];
let current = [ledger(e[0], e[1], 0), ledger(e[1], e[2], 1)];
let ops = plan_chain_ops(&desired, ¤t);
assert!(ops.is_empty(), "変化が無いのに操作が出ている: {ops:?}");
}
#[test]
fn a_segment_only_change_yields_no_operation_at_all() {
let e = entities(3);
let desired = [edge_in_tail(e[0], e[1]), edge_in_tail(e[1], e[2])];
let current = [ledger(e[0], e[1], 0), ledger(e[1], e[2], 1)];
assert_ne!(
desired[0],
edge(e[0], e[1]),
"対照が成立していない(区間が同じなら何も主張できない)"
);
let ops = plan_chain_ops(&desired, ¤t);
assert!(ops.is_empty(), "区間の差で操作を出している: {ops:?}");
}
#[test]
fn a_splice_keeps_all_detaches_ahead_of_all_attaches() {
let e = entities(4);
let current = [ledger(e[0], e[1], 0), ledger(e[2], e[3], 1)];
let desired = [edge(e[0], e[2]), edge(e[1], e[3])];
let ops = plan_chain_ops(&desired, ¤t);
assert_eq!(
ops,
vec![
ChainOp::Detach {
owned: e[0],
reason: DetachReason::Rechain
},
ChainOp::Detach {
owned: e[2],
reason: DetachReason::Teardown
},
ChainOp::Attach {
owned: e[0],
owner: e[2]
},
ChainOp::Attach {
owned: e[1],
owner: e[3]
},
]
);
assert_every_detach_precedes_every_attach(&ops);
}
#[test]
fn an_unchanged_edge_is_left_alone_while_its_neighbour_is_rechained() {
let e = entities(4);
let current = [ledger(e[0], e[1], 0), ledger(e[1], e[2], 1)];
let desired = [edge(e[0], e[1]), edge(e[1], e[3])];
let ops = plan_chain_ops(&desired, ¤t);
assert_eq!(
ops,
vec![
ChainOp::Detach {
owned: e[1],
reason: DetachReason::Rechain
},
ChainOp::Attach {
owned: e[1],
owner: e[3]
},
]
);
}
#[test]
fn no_window_is_ever_attached_twice_in_one_plan() {
let e = entities(3);
let desired = [edge(e[0], e[1]), edge(e[0], e[2])];
let ops = plan_chain_ops(&desired, &[]);
assert_eq!(
ops,
vec![ChainOp::Attach {
owned: e[0],
owner: e[1]
}]
);
let mut attached: Vec<Entity> = Vec::new();
for op in &ops {
if let ChainOp::Attach { owned, .. } = op {
assert!(
!attached.contains(owned),
"同じ窓へ 2 本の付与が出ている: {ops:?}"
);
attached.push(*owned);
}
}
}
#[test]
fn the_nudge_reinserts_the_root_right_behind_its_anchor() {
let members = [fake_hwnd(0x10), fake_hwnd(0x20), fake_hwnd(0x30)];
let cmd =
nudge_command(&members, Some(fake_hwnd(0xAA))).expect("窓が 2 枚以上あれば後押しが出る");
assert_eq!(cmd.hwnd, members[2], "動かす窓が鎖の根でない");
assert_eq!(
cmd.hwnd_insert_after,
Some(members[1]),
"挿入位置が根の 1 つ手前の窓(錨)でない"
);
}
#[test]
fn a_missing_raw_neighbour_still_takes_the_plain_shape() {
let members = [fake_hwnd(0x10), fake_hwnd(0x20), fake_hwnd(0x30)];
let cmd = nudge_command(&members, None).expect("窓が 2 枚以上あれば後押しが出る");
assert_eq!(cmd.hwnd, members[2], "動かす窓が鎖の根でない");
assert_eq!(
cmd.hwnd_insert_after,
Some(members[1]),
"隣が無い巡で挿入位置が錨から外れている"
);
}
#[test]
fn a_redundant_insert_position_switches_to_the_head_of_the_chain() {
let members = [fake_hwnd(0x10), fake_hwnd(0x20), fake_hwnd(0x30)];
let cmd = nudge_command(&members, Some(members[2])).expect("窓が 2 枚以上あれば後押しが出る");
assert_eq!(cmd.hwnd, members[2], "動かす窓が鎖の根でない");
assert_eq!(
cmd.hwnd_insert_after,
Some(members[0]),
"空振りする巡で挿入位置が先頭へ切り替わっていない"
);
}
#[test]
fn the_two_insert_positions_can_never_both_be_redundant() {
let members = [
fake_hwnd(0x10),
fake_hwnd(0x20),
fake_hwnd(0x30),
fake_hwnd(0x40),
];
let root = members[3];
let anchor = members[2];
let head = members[0];
let redundant = nudge_command(&members, Some(root)).expect("後押しが出る");
assert_eq!(redundant.hwnd_insert_after, Some(head));
assert_ne!(
redundant.hwnd_insert_after,
Some(anchor),
"空振りする挿入位置をそのまま採っている"
);
let plain = nudge_command(&members, Some(head)).expect("後押しが出る");
assert_eq!(plain.hwnd_insert_after, Some(anchor));
assert_ne!(
redundant.hwnd_insert_after, plain.hwnd_insert_after,
"2 つの形が同じ挿入位置に畳まれている(空振りを塞げない)"
);
}
#[test]
fn a_two_window_chain_collapses_both_insert_positions_into_one() {
let members = [fake_hwnd(0x10), fake_hwnd(0x20)];
let plain = nudge_command(&members, Some(fake_hwnd(0xAA))).expect("後押しが出る");
let redundant = nudge_command(&members, Some(members[1])).expect("後押しが出る");
assert_eq!(plain.hwnd, members[1], "動かす窓が根でない");
assert_eq!(redundant.hwnd, members[1], "動かす窓が根でない");
assert_eq!(plain.hwnd_insert_after, Some(members[0]));
assert_eq!(
redundant.hwnd_insert_after,
Some(members[0]),
"2 枚のときに存在しない 3 枚目を挿入位置にしている"
);
}
#[test]
fn fewer_than_two_windows_produce_no_nudge_at_all() {
assert!(
nudge_command(&[], None).is_none(),
"窓 0 枚で後押しが出ている"
);
assert!(
nudge_command(&[fake_hwnd(0x10)], None).is_none(),
"窓 1 枚で後押しが出ている"
);
assert!(
nudge_command(&[fake_hwnd(0x10), fake_hwnd(0x20)], None).is_some(),
"境界の反対側(窓 2 枚)で後押しが消えている"
);
}
#[test]
fn the_nudge_command_only_ever_names_the_chains_own_windows() {
let members = [
fake_hwnd(0x10),
fake_hwnd(0x20),
fake_hwnd(0x30),
fake_hwnd(0x40),
];
for (label, anchor_next) in [
("素直な枝", Some(fake_hwnd(0xAA))),
("切り替えた枝", Some(members[3])),
] {
let cmd = nudge_command(&members, anchor_next).expect("窓が 2 枚以上あれば後押しが出る");
let named = hwnds_named_by(&cmd);
assert!(
!named.is_empty(),
"{label}: 指令が窓を 1 つも名指ししていない(検査が空振りしている)"
);
for hwnd in &named {
assert!(
members.contains(hwnd),
"{label}: 鎖の外の窓ハンドルが後押しの指令に現れている: {hwnd:?} / members={members:?}"
);
}
assert_eq!(
named.len(),
2,
"{label}: 指令が名指しする窓が 2 枚でない: {named:?}"
);
assert!(
!named.contains(&members[1]),
"{label}: 根と挿入位置以外の窓を参照している: {named:?}"
);
}
}
#[test]
fn the_same_check_actually_catches_the_forbidden_nudge_shapes() {
let members = [fake_hwnd(0x10), fake_hwnd(0x20)];
let cmd = nudge_command(&members, None).expect("窓が 2 枚以上あれば後押しが出る");
let foreign = SetWindowPosCommand::new(
members[0],
0,
0,
0,
0,
cmd.flags,
Some(fake_hwnd(0xDEAD_BEEF)),
);
assert!(
!hwnds_named_by(&foreign).iter().all(|h| members.contains(h)),
"他プロセスの窓を渡した指令を検査が素通りさせている"
);
let absolute_band = SetWindowPosCommand::new(
members[0],
0,
0,
0,
0,
cmd.flags,
Some(HWND(std::ptr::null_mut())),
);
assert!(
!hwnds_named_by(&absolute_band)
.iter()
.all(|h| members.contains(h)),
"絶対帯の指定を検査が素通りさせている"
);
}
#[test]
fn the_nudge_moves_only_the_zorder() {
let members = [fake_hwnd(0x10), fake_hwnd(0x20), fake_hwnd(0x30)];
for (label, anchor_next) in [("素直な枝", None), ("切り替えた枝", Some(members[2]))] {
let cmd = nudge_command(&members, anchor_next).expect("窓が 2 枚以上あれば後押しが出る");
assert_eq!(
cmd.flags,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
"{label}: 重なり以外を動かすフラグが立っている: {:?}",
cmd.flags
);
assert_eq!(
(cmd.x, cmd.y, cmd.width, cmd.height),
(0, 0, 0, 0),
"{label}: 位置・寸法の欄に値が入っている"
);
assert_eq!(
cmd.flags & SET_WINDOW_POS_FLAGS(0x0004),
SET_WINDOW_POS_FLAGS(0),
"{label}: Z を触らない指令になっている(後押しとして効かない形)"
);
}
}
#[test]
fn the_nudge_is_built_without_the_deferred_queue_or_a_wake_request() {
let here = code_only(include_str!("zorder_chain.rs"));
assert!(
!here.contains("SetWindowPosCommand::enqueue"),
"後押しが遅延キューへ積まれている(同じ巡で実測できない・要件 9.2)"
);
assert!(
!here.contains("tick_wake"),
"適用の側から起床を促している(要件 14.2 が禁じた形)"
);
assert!(
code_only(include_str!("zorder_pair_maintain.rs")).contains("SetWindowPosCommand::enqueue"),
"走査そのものが壊れている(積む既存経路を見つけられない)"
);
assert!(
code_only(include_str!("command.rs")).contains("tick_wake"),
"走査そのものが壊れている(起床を促す既存経路を見つけられない)"
);
}
#[test]
fn every_chain_record_leaves_from_this_module_not_from_the_line_builders() {
let e = entities(2);
let out = capture_under_filter(SIGNOFF_DIRECTIVES, || {
log_chain_linked(
Some(ChainSegment::Group(1)),
e[0],
e[1],
Some(fake_hwnd(0x10)),
Some(fake_hwnd(0x20)),
1,
2,
);
log_chain_unlinked(
Some(ChainSegment::Tail),
e[0],
Some(fake_hwnd(0x10)),
Some(fake_hwnd(0x20)),
DetachReason::Teardown,
);
log_chain_settled(
Some(fake_hwnd(0x10)),
Some(fake_hwnd(0x20)),
&[fake_hwnd(0x10), fake_hwnd(0x20)],
&[fake_hwnd(0x10), fake_hwnd(0x20)],
Some(true),
);
log_chain_absent(3, "b0");
log_chain_skipped(ChainSkipReason::NoChange);
log_chain_link_failed(
Some(ChainSegment::Group(0)),
Some(fake_hwnd(0x10)),
Some(fake_hwnd(0x20)),
&fake_error(),
);
log_chain_unlink_failed(Some(fake_hwnd(0x10)), &fake_error());
});
for tag in chain_record_tags() {
let line = only_line_with(&out, tag);
assert!(
line.contains(CHAIN_TARGET),
"`{tag}` の出力先が鎖のモジュールでない: {line}"
);
assert!(
!line.contains(DIAG_TARGET),
"`{tag}` が行組立の層から出ている(記録の出口が割れている): {line}"
);
}
}
#[test]
fn the_preserved_group_records_still_leave_from_the_line_builder_module() {
let out = capture_under_filter(SIGNOFF_DIRECTIVES, || {
log_group_rejected("ModeMixed", "b1,0");
log_chain_skipped(ChainSkipReason::NoChange);
});
let preserved = only_line_with(&out, "[zorder-group] rejected");
assert!(
preserved.contains(DIAG_TARGET),
"保全語彙の住処が行組立の層から動いている: {preserved}"
);
let chain = only_line_with(&out, "[zorder-chain] skipped");
assert!(
chain.contains(CHAIN_TARGET) && !chain.contains(DIAG_TARGET),
"鎖の記録が行組立の層から出ている: {chain}"
);
}
#[test]
fn the_tracing_macros_for_the_chain_have_exactly_one_home() {
let here = code_only(include_str!("zorder_chain.rs"));
let diag = code_only(include_str!("zorder_chain_diag.rs"));
assert_eq!(
here.matches("debug!(").count(),
5,
"鎖の debug 記録の本数が 5 本でない"
);
assert_eq!(
here.matches("error!(").count(),
2,
"鎖の error 記録の本数が 2 本でない"
);
assert_eq!(
diag.matches("debug!(").count(),
1,
"行組立の層の debug 記録が保全語彙 1 語ぶんでない"
);
assert_eq!(
diag.matches("warn!(").count(),
1,
"行組立の層の warn 記録が保全語彙 1 語ぶんでない"
);
assert_eq!(
diag.matches("error!(").count(),
0,
"行組立の層が error 記録を出している(鎖の失敗語が二重の住処を持つ)"
);
assert!(
here.contains("debug!(") && here.contains("error!("),
"走査そのものが空振りしている"
);
}
#[test]
fn the_chain_tag_spellings_stay_in_the_line_builder_module() {
let here = code_only(include_str!("zorder_chain.rs"));
assert!(
!here.contains("[zorder-chain]"),
"鎖のタグの字面が記録の出口にも書かれている(住処が 2 つに割れる)"
);
assert!(
!here.contains("[zorder-group]"),
"保全語彙のタグの字面が鎖のモジュールに書かれている"
);
let diag = code_only(include_str!("zorder_chain_diag.rs"));
for tag in chain_record_tags() {
assert!(
diag.contains(&format!("\"{tag}\"")),
"行組立の層に `{tag}` の字面が無い(走査が壊れている)"
);
}
}