use super::*;
use crate::ecs::world::tick_wake::{
ANIM, DRAG, FORCE, GRAPHICS, POINTER, PRESENT, REARM, WINDOW_CMD, WM_GEOMETRY, WakeSnapshot,
ZORDER,
};
const SINCE_RUN_POINTS: [u32; 6] = [0, 1, 29, 30, 31, u32::MAX];
const SINCE_BOOT_POINTS: [u32; 6] = [0, 1, 599, 600, 601, u32::MAX];
const BITS_COUNT: u32 = 1 << 10;
fn inputs(
bits: u32,
deadline_due: bool,
frames_since_run: u32,
frames_since_boot: u32,
gate_enabled: bool,
) -> TickGateInputs {
TickGateInputs {
bits,
deadline_due,
frames_since_run,
frames_since_boot,
gate_enabled,
}
}
fn for_each_case(mut visit: impl FnMut(TickGateInputs)) {
for bits in 0..BITS_COUNT {
for deadline_due in [false, true] {
for frames_since_run in SINCE_RUN_POINTS {
for frames_since_boot in SINCE_BOOT_POINTS {
for gate_enabled in [false, true] {
visit(inputs(
bits,
deadline_due,
frames_since_run,
frames_since_boot,
gate_enabled,
));
}
}
}
}
}
}
fn oracle(i: &TickGateInputs) -> TickDecision {
if !i.gate_enabled {
return TickDecision::Run(RunReason::Disabled);
}
if i.frames_since_boot < 600 {
return TickDecision::Run(RunReason::Warmup);
}
if i.bits != 0 {
return TickDecision::Run(RunReason::Wake(i.bits));
}
if i.deadline_due {
return TickDecision::Run(RunReason::Deadline);
}
if i.frames_since_run >= 30 {
return TickDecision::Run(RunReason::Heartbeat);
}
TickDecision::Skip
}
#[test]
fn exhaustive_matches_independent_oracle() {
let mut checked = 0u32;
for_each_case(|i| {
assert_eq!(
should_run(&i),
oracle(&i),
"対照実装と食い違った: bits={:#x} deadline_due={} since_run={} since_boot={} enabled={}",
i.bits,
i.deadline_due,
i.frames_since_run,
i.frames_since_boot,
i.gate_enabled
);
checked += 1;
});
assert_eq!(checked, 1024 * 2 * 6 * 6 * 2, "総なめの件数");
assert_eq!(checked, 147_456, "総なめの件数(実数)");
}
#[test]
fn exhaustive_skip_holds_exactly_when_nothing_asks_for_a_run() {
for_each_case(|i| {
let should_skip = i.bits == 0
&& !i.deadline_due
&& i.frames_since_run < TICK_HEARTBEAT_FRAMES
&& i.frames_since_boot >= TICK_GATE_WARMUP_FRAMES
&& i.gate_enabled;
let decision = should_run(&i);
assert_eq!(
decision == TickDecision::Skip,
should_skip,
"省略の必要十分条件を外れた: bits={:#x} deadline_due={} since_run={} since_boot={} enabled={} decision={decision:?}",
i.bits,
i.deadline_due,
i.frames_since_run,
i.frames_since_boot,
i.gate_enabled
);
assert_eq!(decision.is_run(), !should_skip, "is_run は Skip の裏返し");
});
}
#[test]
fn disabled_outranks_every_other_reason() {
let i = inputs(FORCE.bits(), true, u32::MAX, 0, false);
assert_eq!(should_run(&i), TickDecision::Run(RunReason::Disabled));
let i = inputs(POINTER.bits(), true, u32::MAX, u32::MAX, false);
assert_eq!(should_run(&i), TickDecision::Run(RunReason::Disabled));
let i = inputs(0, false, 0, u32::MAX, false);
assert_eq!(should_run(&i), TickDecision::Run(RunReason::Disabled));
}
#[test]
fn warmup_outranks_wake_deadline_and_heartbeat() {
let i = inputs(PRESENT.bits(), true, u32::MAX, 0, true);
assert_eq!(should_run(&i), TickDecision::Run(RunReason::Warmup));
let i = inputs(0, false, 0, 599, true);
assert_eq!(should_run(&i), TickDecision::Run(RunReason::Warmup));
}
#[test]
fn wake_outranks_deadline_and_heartbeat() {
let i = inputs(ANIM.bits(), true, u32::MAX, 600, true);
assert_eq!(
should_run(&i),
TickDecision::Run(RunReason::Wake(ANIM.bits()))
);
}
#[test]
fn deadline_outranks_heartbeat() {
let i = inputs(0, true, u32::MAX, 600, true);
assert_eq!(should_run(&i), TickDecision::Run(RunReason::Deadline));
}
#[test]
fn heartbeat_is_the_last_resort() {
let i = inputs(0, false, TICK_HEARTBEAT_FRAMES, 600, true);
assert_eq!(should_run(&i), TickDecision::Run(RunReason::Heartbeat));
}
#[test]
fn warmup_boundary_is_599_in_and_600_out() {
let just_inside = inputs(0, false, 0, TICK_GATE_WARMUP_FRAMES - 1, true);
assert_eq!(
should_run(&just_inside),
TickDecision::Run(RunReason::Warmup),
"599 フレーム目はまだ起動直後"
);
let just_outside = inputs(0, false, 0, TICK_GATE_WARMUP_FRAMES, true);
assert_eq!(
should_run(&just_outside),
TickDecision::Skip,
"600 フレーム目で起動直後は終わる"
);
}
#[test]
fn heartbeat_boundary_is_29_skip_and_30_run() {
let below = inputs(
0,
false,
TICK_HEARTBEAT_FRAMES - 1,
TICK_GATE_WARMUP_FRAMES,
true,
);
assert_eq!(
should_run(&below),
TickDecision::Skip,
"29 はまだ心拍に届かない"
);
let at = inputs(
0,
false,
TICK_HEARTBEAT_FRAMES,
TICK_GATE_WARMUP_FRAMES,
true,
);
assert_eq!(
should_run(&at),
TickDecision::Run(RunReason::Heartbeat),
"30 でちょうど心拍"
);
}
#[test]
fn constants_are_fixed() {
assert_eq!(TICK_HEARTBEAT_FRAMES, 30);
assert_eq!(TICK_GATE_WARMUP_FRAMES, 600);
}
#[test]
fn wake_carries_the_exact_bits() {
let bits = (POINTER | PRESENT | ZORDER).bits();
let i = inputs(bits, false, 0, TICK_GATE_WARMUP_FRAMES, true);
match should_run(&i) {
TickDecision::Run(RunReason::Wake(carried)) => assert_eq!(carried, bits),
other => panic!("Wake を期待した: {other:?}"),
}
}
#[test]
fn wake_names_joins_flag_names_in_bit_order() {
assert_eq!(wake_names((POINTER | PRESENT).bits()), "POINTER|PRESENT");
assert_eq!(wake_names((PRESENT | POINTER).bits()), "POINTER|PRESENT");
assert_eq!(wake_names(FORCE.bits()), "FORCE");
assert_eq!(
wake_names((DRAG | WINDOW_CMD | WM_GEOMETRY | ANIM | REARM | GRAPHICS).bits()),
"DRAG|WINDOW_CMD|WM_GEOMETRY|ANIM|REARM|GRAPHICS"
);
}
#[test]
fn wake_names_covers_all_ten_flags_and_the_empty_case() {
assert_eq!(wake_names(0), "NONE");
assert_eq!(
wake_names(BITS_COUNT - 1),
"POINTER|DRAG|WINDOW_CMD|ZORDER|WM_GEOMETRY|PRESENT|ANIM|REARM|GRAPHICS|FORCE"
);
}
#[test]
fn wake_names_shows_unknown_bits_instead_of_dropping_them() {
assert_eq!(wake_names(1 << 10), "0x400");
assert_eq!(wake_names(POINTER.bits() | (1 << 11)), "POINTER|0x800");
}
#[test]
fn from_snapshot_copies_fields_verbatim() {
let snapshot = WakeSnapshot {
bits: (POINTER | ANIM).bits(),
deadline_due: true,
};
let i = TickGateInputs::from_snapshot(&snapshot, 7, 1234, true);
assert_eq!(i.bits, snapshot.bits);
assert_eq!(i.deadline_due, snapshot.deadline_due);
assert_eq!(i.frames_since_run, 7);
assert_eq!(i.frames_since_boot, 1234);
assert!(i.gate_enabled);
let empty = WakeSnapshot {
bits: 0,
deadline_due: false,
};
let i = TickGateInputs::from_snapshot(&empty, 0, u32::MAX, false);
assert_eq!(i.bits, 0);
assert!(!i.deadline_due);
assert_eq!(i.frames_since_run, 0);
assert_eq!(i.frames_since_boot, u32::MAX);
assert!(!i.gate_enabled);
}
#[test]
fn reason_names_are_fixed() {
assert_eq!(RunReason::Disabled.as_str(), "disabled");
assert_eq!(RunReason::Warmup.as_str(), "warmup");
assert_eq!(RunReason::Wake(POINTER.bits()).as_str(), "wake");
assert_eq!(RunReason::Wake(0).as_str(), "wake");
assert_eq!(RunReason::Deadline.as_str(), "deadline");
assert_eq!(RunReason::Heartbeat.as_str(), "heartbeat");
}
#[test]
fn is_run_distinguishes_run_from_skip() {
assert!(TickDecision::Run(RunReason::Disabled).is_run());
assert!(TickDecision::Run(RunReason::Warmup).is_run());
assert!(TickDecision::Run(RunReason::Wake(0)).is_run());
assert!(TickDecision::Run(RunReason::Deadline).is_run());
assert!(TickDecision::Run(RunReason::Heartbeat).is_run());
assert!(!TickDecision::Skip.is_run());
}
const WINTF_PRODUCERS: [(&str, &str, &str); 9] = [
("app.rs", include_str!("../app.rs"), "WM_GEOMETRY"),
("dola/mod.rs", include_str!("../dola/mod.rs"), "ANIM"),
("drag/reentry.rs", include_str!("../drag/reentry.rs"), ""),
(
"drag/systems.rs",
include_str!("../drag/systems.rs"),
"DRAG",
),
(
"graphics/systems/init.rs",
include_str!("../graphics/systems/init.rs"),
"GRAPHICS",
),
(
"pointer/buffers.rs",
include_str!("../pointer/buffers.rs"),
"POINTER",
),
(
"window/command.rs",
include_str!("../window/command.rs"),
"WINDOW_CMD",
),
(
"window/zorder_pair_maintain.rs",
include_str!("../window/zorder_pair_maintain.rs"),
"ZORDER",
),
(
"window_proc/mod.rs",
include_str!("../window_proc/mod.rs"),
"",
),
];
fn code_only(src: &str) -> String {
src.lines()
.filter(|line| !line.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn every_wintf_producer_marks_the_tick_wake() {
for (label, src, flag) in WINTF_PRODUCERS {
let code = code_only(src);
assert!(
code.contains("tick_wake::mark("),
"{label}: 旗を立てる呼出(tick_wake::mark()が無い"
);
if !flag.is_empty() {
assert!(
code.contains(&format!("tick_wake::{flag}")),
"{label}: 期待する旗 {flag} を立てていない"
);
}
}
}
#[test]
fn every_pointer_entry_point_calls_the_wake_helper() {
let code = code_only(include_str!("../pointer/buffers.rs"));
let call_sites = code.matches("wake_pointer();").count();
assert!(
call_sites >= 6,
"pointer/buffers.rs: 投入の入口 6 つから旗を立てるはずが {call_sites} 箇所しかない"
);
}
#[test]
fn window_proc_dispatch_maps_the_message_to_bits() {
let code = code_only(include_str!("../window_proc/mod.rs"));
assert!(
code.contains("tick_wake::wake_bits_for_message("),
"window_proc/mod.rs: 配送点がメッセージ→旗の写像を通っていない"
);
}
use std::path::{Path, PathBuf};
fn production_rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
let entries = std::fs::read_dir(dir)
.unwrap_or_else(|e| panic!("木を歩けない(検査が空振りする): {} — {e}", dir.display()));
for entry in entries {
let path = entry.expect("ディレクトリ項目が読めない").path();
if path.is_dir() {
production_rs_files(&path, out);
continue;
}
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
if !name.ends_with(".rs")
|| name.ends_with("_tests.rs")
|| name.ends_with("test_support.rs")
{
continue;
}
out.push(path);
}
}
fn uses_tick_wake_name(code: &str, name: &str) -> bool {
if code.contains(&format!("tick_wake::{name}")) {
return true;
}
code.lines().any(|line| {
let line = line.trim_start();
line.starts_with("use ") && line.contains("tick_wake::") && line.contains(name)
})
}
#[test]
fn the_scan_sees_both_the_path_form_and_the_use_form() {
assert!(
uses_tick_wake_name("tick_wake::mark(tick_wake::ZORDER);", "mark"),
"パス形の呼出を見落としている"
);
assert!(
uses_tick_wake_name(
"use crate::ecs::world::tick_wake::{ZORDER, mark};
mark(ZORDER);",
"mark"
),
"`use` 形で引き込んだ生産者を見落としている(この形は両向とも緑で抜ける)"
);
assert!(
!uses_tick_wake_name("use crate::ecs::world::tick_wake;", "mark"),
"モジュールを引き込んだだけの `use` 行を生産者と見なしている"
);
}
fn files_marking(flag: &str) -> Vec<String> {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut files = Vec::new();
production_rs_files(&root, &mut files);
let mut found: Vec<String> = files
.iter()
.filter(|path| {
let src = std::fs::read_to_string(path).expect("本番ファイルが読めない");
let code = code_only(&src);
uses_tick_wake_name(&code, "mark")
&& (flag.is_empty() || uses_tick_wake_name(&code, flag))
})
.map(|path| {
path.strip_prefix(&root)
.expect("走査の根の下に無い")
.to_string_lossy()
.replace('\\', "/")
})
.collect();
found.sort();
found
}
#[test]
fn the_wintf_table_lists_every_file_that_marks_the_wake() {
let found = files_marking("");
assert!(
!found.is_empty(),
"走査が 1 件も見つけていない(検査そのものが壊れている)"
);
for rel in &found {
assert!(
WINTF_PRODUCERS
.iter()
.any(|(label, _, _)| rel.ends_with(label)),
"旗を立てているのに WINTF_PRODUCERS に載っていない: {rel}"
);
}
for (label, _, _) in WINTF_PRODUCERS {
assert!(
found.iter().any(|rel| rel.ends_with(label)),
"WINTF_PRODUCERS の行が現物のどのファイルにも当たらない: {label}"
);
}
}
#[test]
fn the_module_roster_names_every_wintf_zorder_producer() {
let bullet = zorder_bullet();
let producers = files_marking("ZORDER");
assert!(
!producers.is_empty(),
"ZORDER の生産者が 1 件も見つからない(検査そのものが壊れている)"
);
for rel in &producers {
let stem = file_stem_of(rel);
assert!(
bullet.contains(stem),
"tick_wake.rs 冒頭の ZORDER の名簿が {rel} を名指ししていない"
);
}
}
#[test]
fn the_zorder_constant_doc_keeps_no_second_roster() {
let doc = const_doc_of("ZORDER");
let producers = files_marking("ZORDER");
assert!(
!producers.is_empty(),
"ZORDER の生産者が 1 件も見つからない(検査そのものが壊れている——走査が空なら何を書いても緑になる)"
);
assert!(
!doc.is_empty(),
"ZORDER 定数の doc が空(切り出しが壊れている——何を書いても緑になる)"
);
for rel in producers {
let stem = file_stem_of(&rel);
assert!(
!doc.contains(stem),
"ZORDER 定数の doc が生産者 {rel} を名指ししている(名簿の 2 つ目の写しになる)"
);
}
}
const TICK_WAKE_SRC: &str = include_str!("tick_wake.rs");
fn file_stem_of(rel: &str) -> &str {
rel.rsplit('/')
.next()
.and_then(|name| name.strip_suffix(".rs"))
.expect("相対パスが .rs で終わっていない")
}
fn zorder_bullet() -> String {
let normalized = TICK_WAKE_SRC.replace("\r\n", "\n");
let mut lines = normalized
.lines()
.skip_while(|l| !l.contains("- [`ZORDER`]"));
let head = lines.next().expect("散文名簿に ZORDER の項が無い");
let rest: Vec<&str> = lines
.take_while(|l| l.starts_with("//!") && !l.contains("- ["))
.collect();
format!("{head}\n{}", rest.join("\n"))
}
fn const_doc_of(name: &str) -> String {
let normalized = TICK_WAKE_SRC.replace("\r\n", "\n");
let lines: Vec<&str> = normalized.lines().collect();
let at = lines
.iter()
.position(|l| l.starts_with(&format!("pub const {name}:")))
.unwrap_or_else(|| panic!("定数 {name} の宣言が見つからない"));
let mut doc: Vec<&str> = Vec::new();
for line in lines[..at].iter().rev() {
if !line.starts_with("///") {
break;
}
doc.push(line);
}
assert!(!doc.is_empty(), "定数 {name} に doc が付いていない");
doc.join("\n")
}