use std::path::PathBuf;
const PRODUCTION_FILES: [&str; 8] = [
"src/ecs/window/zorder_pair.rs",
"src/ecs/window/zorder_pair_diag.rs",
"src/ecs/window/zorder_pair_establish.rs",
"src/ecs/window/zorder_pair_maintain.rs",
"src/ecs/window/zorder_pair_sink.rs",
"src/ecs/window/zorder_chain.rs",
"src/ecs/window/zorder_chain_apply.rs",
"src/ecs/window/zorder_chain_diag.rs",
];
const DEFERRED_NEEDLES: [(&str, &str); 9] = [
(
"topmost",
"常時最前面(`\\v`/`\\![set,windowstate,stayontop]` の実装面・要件 8.1/8.3)",
),
("stayontop", "`\\![set,windowstate,stayontop]`(要件 8.3)"),
("windowstate", "`\\![set,windowstate,...]`(要件 8.3)"),
(
"minimize",
"最小化(`\\![set,windowstate,minimize]`/`OnWindowStateMinimize`・要件 8.3/8.4/8.5)",
),
("iconic", "最小化状態の読み取り(要件 8.5)"),
(
"onwindowstate",
"`OnWindowStateMinimize`/`OnWindowStateRestore`(要件 8.4)",
),
(
"onfullscreenapp",
"`OnFullScreenAppMinimize`/`OnFullScreenAppRestore`(要件 8.4)",
),
("appwindow", "タスクバー露出の切り替え(要件 8.5)"),
(
"taskbar",
"タスクバー・アプリ切り替え一覧の扱い(要件 8.5)",
),
];
fn source_path(relative: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(relative)
}
fn read_source(relative: &str) -> String {
let path = source_path(relative);
std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("本番ファイルを読めなかった({}): {e}", path.display()))
}
fn code_only(source: &str) -> String {
source
.lines()
.filter(|line| !line.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n")
}
fn deferred_hits(code: &str) -> Vec<(&'static str, &'static str)> {
let lowered = code.to_lowercase();
DEFERRED_NEEDLES
.iter()
.filter(|(needle, _)| lowered.contains(*needle))
.copied()
.collect()
}
#[test]
fn deferred_window_state_vocabulary_is_absent_from_this_features_wintf_sources() {
let mut scanned_files = 0usize;
let mut scanned_code_bytes = 0usize;
for relative in PRODUCTION_FILES {
let raw = read_source(relative);
assert!(
raw.len() > 1_000,
"{relative}: 本番ファイルにしては短すぎる(読み違えの疑い): {} バイト",
raw.len()
);
let code = code_only(&raw);
assert!(
!code.trim().is_empty(),
"{relative}: コメントを落としたら本文が空になった(走査が空振りしている)"
);
let hits = deferred_hits(&code);
assert!(
hits.is_empty(),
"{relative}: 先送りした語彙が本番コードに現れている(要件 8.3〜8.5): {hits:?}"
);
scanned_files += 1;
scanned_code_bytes += code.len();
}
assert_eq!(
scanned_files,
PRODUCTION_FILES.len(),
"対象ファイルを全部読めていない"
);
assert!(
scanned_code_bytes > 20_000,
"読んだコードが少なすぎる(走査が実体に届いていない): {scanned_code_bytes} バイト"
);
}
#[test]
fn the_deferred_vocabulary_scan_can_actually_find_what_it_looks_for() {
for (needle, why) in DEFERRED_NEEDLES {
let sample = format!("let handle = SomeApi::{needle}(hwnd);");
let hits = deferred_hits(&sample);
assert!(
hits.iter().any(|(n, _)| *n == needle),
"語 `{needle}`({why})を検出できていない: {hits:?}"
);
}
assert_eq!(
deferred_hits("SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, flags);")
.iter()
.map(|(n, _)| *n)
.collect::<Vec<_>>(),
vec!["topmost"],
"大文字の綴りを取り逃がしている"
);
assert!(
deferred_hits("let decision = decide_pair_fix(&observation);").is_empty(),
"無関係な本文で語を検出している"
);
let raw = read_source("src/ecs/window/zorder_pair.rs");
assert!(
raw.to_lowercase().contains("topmost"),
"前提が崩れている: 素の全文には常時最前面の語(否定の説明)があるはず"
);
let code = code_only(&raw);
assert!(
!code.to_lowercase().contains("topmost"),
"常時最前面の語が説明文以外に現れている(要件 8.1/8.3)"
);
let maintain = code_only(&read_source("src/ecs/window/zorder_pair_maintain.rs"));
assert!(
maintain.contains("SetWindowPosCommand::enqueue"),
"コードだけの本文から実装が消えている(走査が中身を見ていない)"
);
assert!(
maintain.contains("pub fn apply_zorder_pair_maintenance"),
"コードだけの本文から維持系の定義が消えている"
);
}
fn all_source_files() -> Vec<String> {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut stack = vec![manifest.join("src")];
let mut found = Vec::new();
while let Some(dir) = stack.pop() {
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() {
stack.push(path);
} else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
let relative = path
.strip_prefix(&manifest)
.expect("マニフェスト配下のはず")
.to_string_lossy()
.replace(std::path::MAIN_SEPARATOR, "/");
found.push(relative);
}
}
}
found.sort();
found
}
fn zorder_production_sources() -> Vec<String> {
all_source_files()
.into_iter()
.filter(|relative| {
let name = relative.rsplit('/').next().unwrap_or(relative);
name.starts_with("zorder_")
&& !name.ends_with("_tests.rs")
&& !name.ends_with("_test_support.rs")
})
.collect()
}
#[test]
fn the_scanned_roster_covers_every_zorder_production_source_in_this_crate() {
let actual = zorder_production_sources();
assert!(
actual.contains(&"src/ecs/window/zorder_pair.rs".to_string()),
"実物の走査が効いていない(既知の本番ファイルを見つけられない): {actual:?}"
);
assert!(
!actual.iter().any(|p| p.ends_with("_tests.rs")),
"テストファイルを本番ファイルとして数えている: {actual:?}"
);
let missing: Vec<&String> = actual
.iter()
.filter(|relative| !PRODUCTION_FILES.contains(&relative.as_str()))
.collect();
assert!(
missing.is_empty(),
"本番ファイルが先送り語彙の走査対象に載っていない(名簿へ足すこと): {missing:?}"
);
for relative in PRODUCTION_FILES {
assert!(
source_path(relative).is_file(),
"走査対象の一覧に実在しないファイルが載っている: {relative}"
);
}
}