use super::*;
fn dirty_now(world: &World) -> bool {
world
.get_resource::<ZOrderChainPlan>()
.expect("受け口が無い")
.dirty
}
fn run_passes(world: &mut World, script: Script, passes: usize) -> (String, Script) {
let mut schedule = chain_schedule();
run_on(&mut schedule, world, script, |schedule, world| {
for _ in 0..passes {
schedule.run(world);
}
})
}
fn run_on(
schedule: &mut Schedule,
world: &mut World,
script: Script,
body: impl FnOnce(&mut Schedule, &mut World),
) -> (String, Script) {
with_script(script, || {
capture_under_filter(SIGNOFF_DIRECTIVES, || {
body(schedule, world);
})
})
}
#[test]
fn waiting_for_window_handles_never_touches_the_runtime_and_records_only_once() {
const PASSES: usize = 5;
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
publish(
&mut world,
vec![a, b],
vec![edge(a, b, ChainSegment::Group(0))],
);
let (out, used) = run_passes(&mut world, Script::default(), PASSES);
assert!(
!touched_runtime(&used.calls),
"ハンドルが無いのに実行環境を呼んでいる({PASSES} 巡): {:?}",
used.calls
);
assert_line_count(
&out,
SKIPPED,
1,
"窓ハンドル待ちの持ち越し 5 巡ぶん(待ち始めの 1 回だけ出す)",
);
assert!(
dirty_now(&world),
"{PASSES} 巡回したあとに印が落ちている(あとからハンドルが現れても二度と立たない)"
);
}
fn publish_with_one_more_handleless_member(
world: &mut World,
members: &[Entity],
cross_edges: &[CrossEdge],
) {
let newcomer = world.spawn_empty().id();
let tail = *members.last().expect("鎖に窓が 1 枚も無い");
let mut members = members.to_vec();
members.push(newcomer);
let mut cross_edges = cross_edges.to_vec();
cross_edges.push(edge(tail, newcomer, ChainSegment::Group(0)));
publish(world, members, cross_edges);
}
#[test]
fn a_wait_resolved_by_writing_re_arms_the_record_for_the_next_wait() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
let members = vec![a, b];
let cross_edges = vec![edge(a, b, ChainSegment::Group(0))];
publish(&mut world, members.clone(), cross_edges.clone());
let mut schedule = chain_schedule();
let (out, used) = run_on(
&mut schedule,
&mut world,
Script::default(),
|schedule, world| {
for _ in 0..3 {
schedule.run(world);
}
world.entity_mut(a).insert(WindowHandle {
hwnd: fake_hwnd(0x10),
instance: HINSTANCE::default(),
});
world.entity_mut(b).insert(WindowHandle {
hwnd: fake_hwnd(0x20),
instance: HINSTANCE::default(),
});
schedule.run(world);
publish_with_one_more_handleless_member(world, &members, &cross_edges);
for _ in 0..3 {
schedule.run(world);
}
},
);
assert_line_count(&out, LINKED, 1, "ハンドルが揃った巡で繋ぐ");
assert!(
used.calls.contains(&Call::SetOwner(0x10, 0x20)),
"ハンドルが揃った巡で所有関係を書いていない: {:?}",
used.calls
);
assert_line_count(
&out,
SKIPPED,
2,
"待ちが 2 度あるのに記録が 2 本でない(1 本なら再武装が壊れて 2 度目の待ちが無記録・ 4 本以上なら抑止が壊れて記録が氾濫)",
);
assert!(
dirty_now(&world),
"2 度目の待ちで印が落ちている(あとからハンドルが現れても二度と立たない)"
);
}
#[test]
fn a_wait_ended_by_a_non_carrying_pass_also_re_arms_the_record() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
publish(
&mut world,
vec![a, b],
vec![edge(a, b, ChainSegment::Group(0))],
);
let mut script = Script::default();
script.owner_of.insert(0x10, 0x99);
let mut schedule = chain_schedule();
let (out, used) = run_on(&mut schedule, &mut world, script, |schedule, world| {
for _ in 0..2 {
schedule.run(world);
}
world.entity_mut(a).insert(WindowHandle {
hwnd: fake_hwnd(0x10),
instance: HINSTANCE::default(),
});
world.entity_mut(b).insert(WindowHandle {
hwnd: fake_hwnd(0x20),
instance: HINSTANCE::default(),
});
record_link(world, a, b, fake_hwnd(0x10), fake_hwnd(0x20));
world.insert_resource(ZOrderChainPlan {
chain: None,
dirty: true,
});
schedule.run(world);
let c = world.spawn_empty().id();
publish(world, vec![a, c], vec![edge(a, c, ChainSegment::Group(0))]);
for _ in 0..2 {
schedule.run(world);
}
});
assert!(
lines_with(&out, UNLINKED)
.iter()
.any(|line| field(line, "reason") == "Diverged"),
"食い違いの巡が組めていない: {out}"
);
assert!(
!used.calls.contains(&Call::ClearOwner(0x10)),
"食い違いなのに実行環境を呼んでいる: {:?}",
used.calls
);
assert_line_count(
&out,
SKIPPED,
2,
"持ち越さない巡を挟んだあとの待ちが無記録になっている(再武装が壊れている)",
);
assert!(dirty_now(&world), "2 度目の待ちで印が落ちている");
}
#[test]
fn a_wait_ended_by_a_no_change_pass_also_re_arms_the_record() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
publish(
&mut world,
vec![a, b],
vec![edge(a, b, ChainSegment::Group(0))],
);
let mut schedule = chain_schedule();
let (out, used) = run_on(
&mut schedule,
&mut world,
Script::default(),
|schedule, world| {
for _ in 0..2 {
schedule.run(world);
}
publish(world, vec![a, b], Vec::new());
schedule.run(world);
let c = world.spawn_empty().id();
publish(world, vec![a, c], vec![edge(a, c, ChainSegment::Group(0))]);
for _ in 0..2 {
schedule.run(world);
}
},
);
let reasons: Vec<&str> = lines_with(&out, SKIPPED)
.iter()
.map(|line| field(line, "reason"))
.collect();
assert!(
reasons.contains(&"NoChange"),
"差分ゼロの巡が組めていない(`NoChange` の記録が無い): {reasons:?}"
);
assert!(
!touched_runtime(&used.calls),
"どの巡も実行環境を呼ばないはずが呼んでいる: {:?}",
used.calls
);
let waits = reasons
.iter()
.filter(|reason| **reason == "HandleMissing")
.count();
assert_eq!(
waits, 2,
"差分ゼロの巡を挟んだあとの待ちが無記録になっている(再武装が壊れている): {reasons:?}"
);
assert!(dirty_now(&world), "2 度目の待ちで印が落ちている");
}
#[test]
fn a_pass_that_could_not_write_a_single_link_keeps_the_mark_raised() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
publish(
&mut world,
vec![a, b],
vec![edge(a, b, 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!(
dirty_now(&world),
"1 本も書けなかった巡で印が落ちている(あとからハンドルが現れても二度と立たない)"
);
}
#[test]
fn the_carried_mark_is_consumed_as_soon_as_the_handles_appear() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
publish(
&mut world,
vec![a, b],
vec![edge(a, b, ChainSegment::Group(0))],
);
let (_out, _used) = run_once(&mut world, Script::default());
assert!(dirty_now(&world), "1 巡目で印が落ちている");
world.entity_mut(a).insert(WindowHandle {
hwnd: fake_hwnd(0x10),
instance: HINSTANCE::default(),
});
world.entity_mut(b).insert(WindowHandle {
hwnd: fake_hwnd(0x20),
instance: HINSTANCE::default(),
});
let (out, used) = run_once(&mut world, Script::default());
assert_line_count(&out, LINKED, 1, "ハンドルが揃えば張る");
assert!(
used.calls.contains(&Call::SetOwner(0x10, 0x20)),
"ハンドルが揃った巡で所有関係を書いていない: {:?}",
used.calls
);
assert!(
!dirty_now(&world),
"書けた巡でも印が残っている(毎巡の空振りへ逆戻りしている)"
);
}
#[test]
fn a_pass_that_only_found_a_diverged_ledger_entry_does_not_carry_the_mark() {
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::new());
let mut script = Script::default();
script.owner_of.insert(0x10, 0x99);
let (out, used) = run_once(&mut world, script);
assert!(
!used.calls.contains(&Call::ClearOwner(0x10)),
"食い違いなのに実行環境を呼んでいる: {:?}",
used.calls
);
assert!(
lines_with(&out, UNLINKED)
.iter()
.any(|line| field(line, "reason") == "Diverged"),
"食い違いの記録が無い: {out}"
);
assert!(
!dirty_now(&world),
"食い違いだけの巡で印を持ち越している(毎巡の反復是正へ逆戻りしている)"
);
}
#[test]
fn the_nudge_runs_once_for_the_whole_chain_and_the_measurement_follows_it_immediately() {
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
.front_of
.insert(0x30, vec![0x20usize, 0x99usize, 0x10usize]);
let (out, used) = run_once(&mut world, script);
let nudges: Vec<&Call> = used
.calls
.iter()
.filter(|c| matches!(c, Call::Nudge(_, _)))
.collect();
assert_eq!(
nudges.len(),
1,
"後押しが鎖全体につき 1 回になっていない: {:?}",
used.calls
);
assert_eq!(
*nudges[0],
Call::Nudge(0x30, 0x20),
"後押しの形が「鎖の根を錨(1 つ手前の窓)の直後へ差し直す」になっていない"
);
let nudge_at = used
.calls
.iter()
.position(|c| matches!(c, Call::Nudge(_, _)))
.expect("後押しが無い");
assert_eq!(
used.calls.get(nudge_at - 1),
Some(&Call::RawNext(0x20)),
"後押しの直前に錨の現況を読んでいない: {:?}",
used.calls
);
assert_eq!(
used.calls
.iter()
.filter(|c| matches!(c, Call::RawNext(_)))
.count(),
1,
"現況の読みが 1 巡に 1 度でない(周期的な観測へ逆戻りしている): {:?}",
used.calls
);
assert_eq!(
used.calls.get(nudge_at + 1),
Some(&Call::MeasureFront(0x30)),
"後押しの直後に実測していない: {:?}",
used.calls
);
assert_line_count(&out, SETTLED, 1, "鎖全体につき 1 行");
let settled = lines_with(&out, SETTLED)[0];
assert_eq!(field(settled, "nudged_hwnd"), "0x30", "{settled}");
assert_eq!(field(settled, "insert_after"), "0x20", "{settled}");
assert_eq!(field(settled, "declared"), "0x10,0x20,0x30", "{settled}");
assert_eq!(field(settled, "measured"), "0x10,0x20,0x30", "{settled}");
assert_eq!(field(settled, "nudge_ok"), "true", "{settled}");
}
#[test]
fn a_pass_whose_plain_insert_position_is_redundant_nudges_against_the_head_instead() {
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.next_of.insert(0x20, 0x30);
script.front_of.insert(0x30, vec![0x20usize, 0x10usize]);
let (out, used) = run_once(&mut world, script);
let nudges: Vec<&Call> = used
.calls
.iter()
.filter(|c| matches!(c, Call::Nudge(_, _)))
.collect();
assert_eq!(nudges.len(), 1, "後押しが 1 回でない: {:?}", used.calls);
assert_eq!(
*nudges[0],
Call::Nudge(0x30, 0x10),
"空振りする巡で挿入位置が先頭へ切り替わっていない"
);
let settled = lines_with(&out, SETTLED)[0];
assert_eq!(field(settled, "nudged_hwnd"), "0x30", "{settled}");
assert_eq!(field(settled, "insert_after"), "0x10", "{settled}");
}
#[test]
fn the_same_pass_without_the_redundant_neighbour_keeps_the_plain_insert_position() {
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.next_of.insert(0x20, 0x99);
script.front_of.insert(0x30, vec![0x20usize, 0x10usize]);
let (_out, used) = run_once(&mut world, script);
assert!(
used.calls.contains(&Call::Nudge(0x30, 0x20)),
"錨の直後が根でない巡で素直な形が出ていない: {:?}",
used.calls
);
}
#[test]
fn a_failed_nudge_is_recorded_on_the_settled_line_rather_than_swallowed() {
let mut world = World::new();
let a = spawn_window(&mut world, fake_hwnd(0x10));
let b = spawn_window(&mut world, fake_hwnd(0x20));
publish(
&mut world,
vec![a, b],
vec![edge(a, b, ChainSegment::Group(0))],
);
let mut script = Script::default();
script.nudge_fails = true;
let (out, _used) = run_once(&mut world, script);
assert_line_count(&out, SETTLED, 1, "後押しの失敗");
assert_eq!(
field(lines_with(&out, SETTLED)[0], "nudge_ok"),
"false",
"後押しの失敗が行から読めない"
);
}
#[test]
fn the_measurement_reports_only_the_windows_the_chain_declared() {
let mut world = World::new();
let a = spawn_window(&mut world, fake_hwnd(0x10));
let b = spawn_window(&mut world, fake_hwnd(0x20));
publish(
&mut world,
vec![a, b],
vec![edge(a, b, ChainSegment::Group(0))],
);
let mut script = Script::default();
script
.front_of
.insert(0x20, vec![0xAAusize, 0x10usize, 0xBBusize]);
let (out, _used) = run_once(&mut world, script);
assert_eq!(
field(lines_with(&out, SETTLED)[0], "measured"),
"0x10,0x20",
"鎖の外の窓を実測へ混ぜている"
);
}