use std::collections::{HashMap, HashSet};
use bevy_ecs::prelude::*;
use bevy_ecs::schedule::{Schedule, SingleThreadedExecutor};
use windows::Win32::Foundation::{HINSTANCE, HWND};
use super::apply_zorder_chain;
use super::double::{self, Call, Script, with_script};
use crate::ecs::test_support::capture_under_filter;
use crate::ecs::window::{
ChainPlan, ChainSegment, CrossEdge, CrossOwnerLink, WindowHandle, ZOrderChainPlan,
};
const SIGNOFF_DIRECTIVES: &str = "info,wintf::ecs::window::zorder_chain=debug";
const LINKED: &str = "[zorder-chain] linked";
const UNLINKED: &str = "[zorder-chain] unlinked";
const SETTLED: &str = "[zorder-chain] settled";
const SKIPPED: &str = "[zorder-chain] skipped";
const LINK_FAILED: &str = "[zorder-chain] link-failed";
fn fake_hwnd(v: usize) -> HWND {
HWND(v as *mut _)
}
fn chain_schedule() -> Schedule {
let mut schedule = Schedule::default();
schedule.set_executor(SingleThreadedExecutor::new());
schedule.add_systems(apply_zorder_chain);
schedule
}
fn spawn_window(world: &mut World, hwnd: HWND) -> Entity {
world
.spawn(WindowHandle {
hwnd,
instance: HINSTANCE::default(),
})
.id()
}
fn edge(owned: Entity, owner: Entity, segment: ChainSegment) -> CrossEdge {
CrossEdge {
owned,
owner,
segment,
}
}
fn record_link(
world: &mut World,
owned: Entity,
owner: Entity,
owned_hwnd: HWND,
owner_hwnd: HWND,
) {
record_link_in(
world,
owned,
owner,
owned_hwnd,
owner_hwnd,
ChainSegment::Group(0),
);
}
fn record_link_in(
world: &mut World,
owned: Entity,
owner: Entity,
owned_hwnd: HWND,
owner_hwnd: HWND,
segment: ChainSegment,
) {
world.entity_mut(owned).insert(CrossOwnerLink {
owner,
owned_hwnd,
owner_hwnd,
segment,
});
}
fn publish(world: &mut World, members: Vec<Entity>, cross_edges: Vec<CrossEdge>) {
world.insert_resource(ZOrderChainPlan {
chain: Some(ChainPlan {
members,
cross_edges,
absent: Vec::new(),
}),
dirty: true,
});
}
fn run_once(world: &mut World, script: Script) -> (String, Script) {
let mut schedule = chain_schedule();
let (out, used) = with_script(script, || {
capture_under_filter(SIGNOFF_DIRECTIVES, || {
schedule.run(world);
})
});
(out, used)
}
fn lines_with<'a>(out: &'a str, tag: &str) -> Vec<&'a str> {
out.lines().filter(|l| l.contains(tag)).collect()
}
fn assert_line_count(out: &str, tag: &str, expected: usize, what: &str) {
let found = lines_with(out, tag);
assert_eq!(
found.len(),
expected,
"{what}: `{tag}` は {expected} 本のはずが {} 本\n---捕捉全文---\n{out}",
found.len()
);
}
fn field<'a>(line: &'a str, key: &str) -> &'a str {
let needle = format!("{key}=");
let start = line
.find(&needle)
.unwrap_or_else(|| panic!("`{key}=` が行に無い: {line}"))
+ needle.len();
line[start..].split_whitespace().next().unwrap_or("")
}
fn code_only(source: &str) -> String {
source
.lines()
.filter(|line| !line.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n")
}
fn touched_runtime(calls: &[Call]) -> bool {
!calls.is_empty()
}
#[test]
fn an_unchanged_plan_issues_nothing_at_all() {
let mut world = World::new();
let front = spawn_window(&mut world, fake_hwnd(0x10));
let back = spawn_window(&mut world, fake_hwnd(0x20));
record_link(&mut world, front, back, fake_hwnd(0x10), fake_hwnd(0x20));
world.insert_resource(ZOrderChainPlan {
chain: Some(ChainPlan {
members: vec![front, back],
cross_edges: vec![edge(front, back, ChainSegment::Group(0))],
absent: Vec::new(),
}),
dirty: false,
});
let mut schedule = chain_schedule();
let (out, used) = with_script(Script::default(), || {
capture_under_filter(SIGNOFF_DIRECTIVES, || {
tracing::debug!("{}", "[zorder-chain] canary from the same capture window");
schedule.run(&mut world);
})
});
assert!(
out.contains("canary from the same capture window"),
"捕捉窓が死んでいる(対照の 1 行すら拾えていない): {out}"
);
assert!(
!touched_runtime(&used.calls),
"印が立っていない巡で実行環境を呼んでいる: {:?}",
used.calls
);
for tag in [LINKED, UNLINKED, SETTLED, SKIPPED, LINK_FAILED] {
assert_line_count(&out, tag, 0, "印が立っていない巡");
}
}
#[test]
fn a_republished_but_identical_plan_records_a_reason_and_touches_nothing() {
let mut world = World::new();
let front = spawn_window(&mut world, fake_hwnd(0x10));
let back = spawn_window(&mut world, fake_hwnd(0x20));
record_link(&mut world, front, back, fake_hwnd(0x10), fake_hwnd(0x20));
publish(
&mut world,
vec![front, back],
vec![edge(front, back, ChainSegment::Group(0))],
);
let (out, used) = run_once(&mut world, Script::default());
assert!(
!touched_runtime(&used.calls),
"出す操作が無いのに実行環境を呼んでいる: {:?}",
used.calls
);
assert_line_count(&out, SKIPPED, 1, "同じ内容の再公開");
assert_eq!(
field(lines_with(&out, SKIPPED)[0], "reason"),
"NoChange",
"見送りの理由が読めない"
);
assert_line_count(&out, SETTLED, 0, "操作が走っていない巡に後押しは出ない");
}
#[test]
fn every_detach_reaches_the_runtime_before_every_attach() {
let mut world = World::new();
let a = spawn_window(&mut world, fake_hwnd(0x10));
let b = spawn_window(&mut world, fake_hwnd(0x20));
let c = spawn_window(&mut world, fake_hwnd(0x30));
record_link(&mut world, a, b, fake_hwnd(0x10), fake_hwnd(0x20));
publish(
&mut world,
vec![a, c, b],
vec![
edge(a, c, ChainSegment::Group(0)),
edge(c, b, ChainSegment::Group(0)),
],
);
let mut script = Script::default();
script.owner_of.insert(0x10, 0x20);
let (out, used) = run_once(&mut world, script);
let last_detach = used
.calls
.iter()
.rposition(|c| matches!(c, Call::ClearOwner(_)))
.expect("撤去が 1 度も走っていない");
let first_attach = used
.calls
.iter()
.position(|c| matches!(c, Call::SetOwner(_, _)))
.expect("付与が 1 度も走っていない");
assert!(
last_detach < first_attach,
"付与のあとに撤去が並んでいる(途中状態が壊れる手順): {:?}",
used.calls
);
let read = used
.calls
.iter()
.position(|c| matches!(c, Call::ReadOwner(0x10)))
.expect("外す前に現況を読んでいない");
assert!(read < last_detach, "読む前に外している: {:?}", used.calls);
assert_line_count(&out, UNLINKED, 1, "張り替え");
assert_line_count(&out, LINKED, 2, "張り替え");
}
#[test]
fn a_diverged_ledger_entry_is_dropped_without_calling_the_runtime() {
let mut world = World::new();
let a = spawn_window(&mut world, fake_hwnd(0x10));
let b = spawn_window(&mut world, fake_hwnd(0x20));
record_link(&mut world, a, b, fake_hwnd(0x10), fake_hwnd(0x20));
world.insert_resource(ZOrderChainPlan {
chain: None,
dirty: true,
});
let mut script = Script::default();
script.owner_of.insert(0x10, 0x99);
let (out, used) = run_once(&mut world, script);
assert!(
used.calls.contains(&Call::ReadOwner(0x10)),
"外す前に現況を読んでいない: {:?}",
used.calls
);
assert!(
!used.calls.iter().any(|c| matches!(c, Call::ClearOwner(_))),
"食い違っているのに実行環境へ撤去を頼んでいる: {:?}",
used.calls
);
assert_line_count(&out, UNLINKED, 1, "食い違い");
assert_eq!(
field(lines_with(&out, UNLINKED)[0], "reason"),
"Diverged",
"食い違いの理由が読めない"
);
world.flush();
assert!(
world.entity(a).get::<CrossOwnerLink>().is_none(),
"食い違った帳簿が残っている"
);
assert_line_count(&out, SETTLED, 0, "書いていない巡");
}
#[test]
fn a_failed_attach_skips_only_that_edge_and_the_rest_are_linked() {
let mut world = World::new();
let a = spawn_window(&mut world, fake_hwnd(0x10));
let b = spawn_window(&mut world, fake_hwnd(0x20));
let c = spawn_window(&mut world, fake_hwnd(0x30));
publish(
&mut world,
vec![a, b, c],
vec![
edge(a, b, ChainSegment::Group(0)),
edge(b, c, ChainSegment::Group(0)),
],
);
let mut script = Script::default();
script.set_fails = HashSet::from([0x10usize]);
let (out, used) = run_once(&mut world, script);
let attempts: Vec<&Call> = used
.calls
.iter()
.filter(|c| matches!(c, Call::SetOwner(_, _)))
.collect();
assert_eq!(
attempts.len(),
2,
"同じ巡で再試行している、ないし残りを張っていない: {:?}",
used.calls
);
assert_line_count(&out, LINK_FAILED, 1, "1 本だけ失敗");
assert_line_count(&out, LINKED, 1, "残りは張る");
assert_eq!(
field(lines_with(&out, LINKED)[0], "owned_hwnd"),
"0x20",
"失敗した側を成功として記録している"
);
world.flush();
assert!(
world.entity(a).get::<CrossOwnerLink>().is_none(),
"張れなかった繋ぎが帳簿に載っている"
);
assert!(
world.entity(b).get::<CrossOwnerLink>().is_some(),
"張れた繋ぎが帳簿に載っていない"
);
}
#[test]
fn an_edge_without_handles_is_skipped_with_a_reason_and_the_rest_are_linked() {
let mut world = World::new();
let a = spawn_window(&mut world, fake_hwnd(0x10));
let b = spawn_window(&mut world, fake_hwnd(0x20));
let pending = world.spawn_empty().id();
publish(
&mut world,
vec![a, b, pending],
vec![
edge(a, b, ChainSegment::Group(0)),
edge(b, pending, ChainSegment::Group(0)),
],
);
let (out, used) = run_once(&mut world, Script::default());
assert_line_count(&out, SKIPPED, 1, "ハンドル未取得");
assert_eq!(
field(lines_with(&out, SKIPPED)[0], "reason"),
"HandleMissing",
"見送りの理由が読めない"
);
assert_line_count(&out, LINKED, 1, "残りは張る");
assert_eq!(
used.calls
.iter()
.filter(|c| matches!(c, Call::SetOwner(_, _)))
.count(),
1,
"ハンドルの無い窓へ書き込もうとしている: {:?}",
used.calls
);
}
#[test]
fn a_link_whose_owner_is_leaving_is_detached_before_the_window_is_destroyed() {
let mut world = World::new();
let owned = spawn_window(&mut world, fake_hwnd(0x10));
let owner = spawn_window(&mut world, fake_hwnd(0x20));
record_link(&mut world, owned, owner, fake_hwnd(0x10), fake_hwnd(0x20));
world.insert_resource(ZOrderChainPlan {
chain: Some(ChainPlan {
members: vec![owned, owner],
cross_edges: vec![edge(owned, owner, ChainSegment::Group(0))],
absent: Vec::new(),
}),
dirty: false,
});
world.despawn(owner);
let mut script = Script::default();
script.owner_of.insert(0x10, 0x20);
let (out, used) = run_once(&mut world, script);
assert!(
used.calls.contains(&Call::ClearOwner(0x10)),
"去る窓の繋ぎを外していない: {:?}",
used.calls
);
assert_line_count(&out, UNLINKED, 1, "去る窓");
assert_eq!(
field(lines_with(&out, UNLINKED)[0], "reason"),
"Departing",
"去る窓の理由が読めない"
);
world.flush();
assert!(
world.entity(owned).get::<CrossOwnerLink>().is_none(),
"去った相手の帳簿が残っている"
);
}
#[test]
fn a_link_whose_owner_is_alive_is_left_untouched() {
let mut world = World::new();
let owned = spawn_window(&mut world, fake_hwnd(0x10));
let owner = spawn_window(&mut world, fake_hwnd(0x20));
record_link(&mut world, owned, owner, fake_hwnd(0x10), fake_hwnd(0x20));
world.insert_resource(ZOrderChainPlan {
chain: None,
dirty: false,
});
let (_out, used) = run_once(&mut world, Script::default());
assert!(
!touched_runtime(&used.calls),
"健在な相手の繋ぎに触っている: {:?}",
used.calls
);
}
#[test]
fn one_pass_records_a_group_edge_and_a_tail_edge_under_different_segments() {
let mut world = World::new();
let a = spawn_window(&mut world, fake_hwnd(0x10));
let b = spawn_window(&mut world, fake_hwnd(0x20));
let c = spawn_window(&mut world, fake_hwnd(0x30));
let d = spawn_window(&mut world, fake_hwnd(0x40));
publish(
&mut world,
vec![a, b, c, d],
vec![
edge(a, b, ChainSegment::Group(0)),
edge(b, c, ChainSegment::Group(0)),
edge(c, d, ChainSegment::Tail),
],
);
let (out, _used) = run_once(&mut world, Script::default());
assert_line_count(&out, LINKED, 3, "区間の混在した 1 巡");
let by_owned = |hwnd: &str| -> &str {
let found: Vec<&str> = lines_with(&out, LINKED)
.into_iter()
.filter(|l| field(l, "owned_hwnd") == hwnd)
.collect();
assert_eq!(
found.len(),
1,
"owned_hwnd={hwnd} の繋いだ行が 1 本でない: {out}"
);
found[0]
};
assert_eq!(field(by_owned("0x10"), "segment"), "g0", "{out}");
assert_eq!(field(by_owned("0x20"), "segment"), "g0", "{out}");
assert_eq!(field(by_owned("0x30"), "segment"), "tail", "{out}");
assert!(
!lines_with(&out, LINKED)
.iter()
.any(|l| field(l, "segment") == "-"),
"区間が番兵へ潰れている: {out}"
);
let mut kinds: Vec<&str> = lines_with(&out, LINKED)
.iter()
.map(|l| field(l, "segment"))
.collect();
kinds.sort_unstable();
kinds.dedup();
assert_eq!(
kinds,
vec!["g0", "tail"],
"区間が 1 種類へ潰れている: {out}"
);
assert!(
by_owned("0x10").contains(&format!("segment=g0 owned={a:?}")),
"{out}"
);
assert!(
by_owned("0x30").contains(&format!("segment=tail owned={c:?}")),
"{out}"
);
}
#[test]
fn the_unlinked_line_names_the_segment_the_ledger_recorded_when_it_was_linked() {
let mut world = World::new();
let a = spawn_window(&mut world, fake_hwnd(0x10));
let b = spawn_window(&mut world, fake_hwnd(0x20));
let c = spawn_window(&mut world, fake_hwnd(0x30));
let d = spawn_window(&mut world, fake_hwnd(0x40));
record_link_in(
&mut world,
a,
b,
fake_hwnd(0x10),
fake_hwnd(0x20),
ChainSegment::Group(2),
);
record_link_in(
&mut world,
c,
d,
fake_hwnd(0x30),
fake_hwnd(0x40),
ChainSegment::Tail,
);
world.insert_resource(ZOrderChainPlan {
chain: None,
dirty: true,
});
let mut script = Script::default();
script.owner_of.insert(0x10, 0x20);
script.owner_of.insert(0x30, 0x40);
let (out, _used) = run_once(&mut world, script);
assert_line_count(&out, UNLINKED, 2, "全解除");
let segments: Vec<(&str, &str)> = lines_with(&out, UNLINKED)
.into_iter()
.map(|l| (field(l, "owned_hwnd"), field(l, "segment")))
.collect();
assert!(
segments.contains(&("0x10", "g2")) && segments.contains(&("0x30", "tail")),
"撤去の区間が帳簿の控えを名乗っていない: {segments:?}
{out}"
);
}
#[test]
fn a_reattributed_edge_updates_the_ledger_without_calling_the_runtime() {
let mut world = World::new();
let a = spawn_window(&mut world, fake_hwnd(0x10));
let b = spawn_window(&mut world, fake_hwnd(0x20));
record_link_in(
&mut world,
a,
b,
fake_hwnd(0x10),
fake_hwnd(0x20),
ChainSegment::Tail,
);
publish(
&mut world,
vec![a, b],
vec![edge(a, b, ChainSegment::Group(1))],
);
let (out, used) = run_once(&mut world, Script::default());
assert!(
!touched_runtime(&used.calls),
"帰属が変わっただけで実行環境を呼んでいる: {:?}",
used.calls
);
assert_line_count(&out, SKIPPED, 1, "帰属だけの変化");
assert_eq!(field(lines_with(&out, SKIPPED)[0], "reason"), "NoChange");
world.flush();
assert_eq!(
world
.entity(a)
.get::<CrossOwnerLink>()
.expect("帳簿は残る")
.segment,
ChainSegment::Group(1),
"帳簿の区間が古いまま残っている"
);
}
#[test]
fn the_apply_system_never_asks_the_runtime_to_keep_ticking() {
let here = code_only(include_str!("zorder_chain_apply.rs"));
assert!(
!here.contains("tick_wake"),
"適用の側から起床を促している(要件 14.2 が禁じた形)"
);
assert!(
!here.contains("SetWindowPosCommand::enqueue"),
"後押しが遅延キューへ積まれている(同じ巡で実測できない・要件 9.2)"
);
assert!(
code_only(include_str!("command.rs")).contains("tick_wake"),
"走査そのものが壊れている(起床を促す既存経路を見つけられない)"
);
assert!(
code_only(include_str!("zorder_pair_maintain.rs")).contains("SetWindowPosCommand::enqueue"),
"走査そのものが壊れている(積む既存経路を見つけられない)"
);
let swapped = here.replace(
"unsafe {\n guarded_set_window_pos(",
"SetWindowPosCommand::enqueue(cmd.clone());\n unsafe {\n guarded_set_window_pos(",
);
assert_ne!(swapped, here, "差し替えの当て先が本文から消えている");
assert!(
swapped.contains("SetWindowPosCommand::enqueue"),
"呼ぶ側の実装へ差し替えても走査が当たらない(この検査は空虚に緑である)"
);
}
#[test]
fn the_runtime_double_is_only_in_effect_while_its_script_is_installed() {
let (seen, used) = with_script(Script::default(), || {
double::read_owner(fake_hwnd(0x10)).is_some()
});
assert!(seen, "台本を据えても替え玉が効いていない");
assert_eq!(
used.calls,
vec![Call::ReadOwner(0x10)],
"呼び出しが記録されていない"
);
assert!(
double::read_owner(fake_hwnd(0x10)).is_none(),
"台本を外しても替え玉が居座っている(本番の窓口へ戻っていない)"
);
}
#[test]
fn the_scripted_failures_actually_fail() {
let script = Script {
read_fails: HashSet::from([0x10usize]),
clear_fails: HashSet::from([0x20usize]),
set_fails: HashSet::from([0x30usize]),
owner_of: HashMap::new(),
..Script::default()
};
let (results, _used) = with_script(script, || {
(
double::read_owner(fake_hwnd(0x10))
.expect("据わっている")
.is_err(),
double::clear_owner(fake_hwnd(0x20))
.expect("据わっている")
.is_err(),
double::set_owner(fake_hwnd(0x30), fake_hwnd(0x40))
.expect("据わっている")
.is_err(),
double::set_owner(fake_hwnd(0x50), fake_hwnd(0x60))
.expect("据わっている")
.is_ok(),
)
});
assert_eq!(
results,
(true, true, true, true),
"台本の失敗指定が窓口へ届いていない"
);
}
#[path = "zorder_chain_apply_nudge_tests.rs"]
mod zorder_chain_apply_nudge_tests;