use std::cell::RefCell;
use windows::Win32::Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DefWindowProcW, DestroyWindow, GW_HWNDNEXT, GetTopWindow, GetWindow, IsWindow,
RegisterClassExW, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, SWP_SHOWWINDOW,
WINDOW_EX_STYLE, WM_WINDOWPOSCHANGED, WNDCLASSEXW, WS_EX_TOOLWINDOW, WS_POPUP,
};
use windows::core::{PCWSTR, w};
use super::super::transition_diag::{self, KIND_FLUSH, KIND_WRITE, RECORD_PREFIX_TAG};
use super::{
BatchDegrade, SetWindowPosCommand, WindowPosArgs, drain_window_pos_commands,
flush_window_pos_commands, is_self_initiated, window_pos_args, with_forced_batch_begin_failure,
};
use crate::ecs::test_support::capture_under_filter;
const SIGNOFF_DIRECTIVES: &str = "info,wintf::transition=debug";
const CONTROL_LINE: &str = "[transition-probe] alive";
fn emit_control() {
tracing::info!(target: transition_diag::TRANSITION_TARGET, "{CONTROL_LINE}");
}
fn clean_slate() {
let _residue = drain_window_pos_commands();
transition_diag::reset_for_test();
}
fn transition_lines(captured: &str) -> Vec<&str> {
captured
.lines()
.filter(|line| line.contains(RECORD_PREFIX_TAG))
.collect()
}
fn lines_of_kind<'a>(captured: &'a str, kind: &str) -> Vec<&'a str> {
let needle = format!("kind={kind} ");
transition_lines(captured)
.into_iter()
.filter(|line| line.contains(&needle))
.collect()
}
fn fake_hwnd(tag: u8) -> HWND {
let value = 0xFFFF_FE00_usize | (usize::from(tag) << 1) | 1;
let hwnd = HWND(value as *mut core::ffi::c_void);
assert!(
!unsafe { IsWindow(Some(hwnd)) }.as_bool(),
"偽ハンドル 0x{value:X} が実窓を掴んでいる"
);
hwnd
}
thread_local! {
static SELF_INITIATED_ON_POSCHANGED: RefCell<Vec<bool>> = const { RefCell::new(Vec::new()) };
static RECORDING: RefCell<bool> = const { RefCell::new(false) };
}
unsafe extern "system" fn test_wndproc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
if msg == WM_WINDOWPOSCHANGED && RECORDING.with(|cell| *cell.borrow()) {
let observed = is_self_initiated();
SELF_INITIATED_ON_POSCHANGED.with(|cell| cell.borrow_mut().push(observed));
}
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
}
const TEST_CLASS: PCWSTR = w!("wintf-command-batch-test");
fn register_test_class() {
use std::sync::Once;
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
static ONCE: Once = Once::new();
ONCE.call_once(|| {
unsafe {
let hinstance: HINSTANCE = GetModuleHandleW(None).expect("GetModuleHandleW").into();
let class = WNDCLASSEXW {
cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
lpfnWndProc: Some(test_wndproc),
hInstance: hinstance,
lpszClassName: TEST_CLASS,
..Default::default()
};
assert_ne!(
RegisterClassExW(&class),
0,
"テスト窓クラスの登録に失敗した"
);
}
});
}
fn create_test_window() -> HWND {
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
register_test_class();
unsafe {
let hinstance: HINSTANCE = GetModuleHandleW(None).expect("GetModuleHandleW").into();
CreateWindowExW(
WINDOW_EX_STYLE(WS_EX_TOOLWINDOW.0),
TEST_CLASS,
w!("command-batch"),
WS_POPUP,
10,
10,
0,
0,
None,
None,
Some(hinstance),
None,
)
.expect("CreateWindowExW should create a test window")
}
}
fn destroy(windows: &[HWND]) {
for hwnd in windows {
unsafe {
let _ = DestroyWindow(*hwnd);
}
}
}
fn geometry_command(hwnd: HWND, x: i32, y: i32, w: i32, h: i32) -> SetWindowPosCommand {
SetWindowPosCommand::new(hwnd, x, y, w, h, SWP_NOZORDER | SWP_NOACTIVATE, None)
}
fn zorder_command(hwnd: HWND, after: HWND) -> SetWindowPosCommand {
SetWindowPosCommand::new(
hwnd,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
Some(after),
)
}
#[test]
fn window_pos_args_carries_all_seven_arguments_verbatim() {
let hwnd = fake_hwnd(0x10);
let after = fake_hwnd(0x11);
let geometry = geometry_command(hwnd, 11, 22, 33, 44);
assert_eq!(
window_pos_args(&geometry),
WindowPosArgs {
hwnd,
hwnd_insert_after: None,
x: 11,
y: 22,
cx: 33,
cy: 44,
flags: SWP_NOZORDER | SWP_NOACTIVATE,
}
);
let zorder = zorder_command(hwnd, after);
assert_eq!(
window_pos_args(&zorder),
WindowPosArgs {
hwnd,
hwnd_insert_after: Some(after),
x: 0,
y: 0,
cx: 0,
cy: 0,
flags: SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
}
);
let show = SetWindowPosCommand::new(hwnd, 1, 2, 3, 4, SWP_SHOWWINDOW, None);
assert_eq!(window_pos_args(&show).flags, SWP_SHOWWINDOW);
let tagged = geometry_command(hwnd, 11, 22, 33, 44).with_tag(transition_diag::WriteTag {
origin: transition_diag::ORIGIN_ZORDER_PAIR,
scope: Some(3),
kind: "shell",
});
assert_eq!(
window_pos_args(&tagged),
window_pos_args(&geometry),
"要求語彙タグが引数を動かしている"
);
}
#[test]
fn the_queue_maps_to_the_same_arguments_in_the_same_order() {
let a = fake_hwnd(0x12);
let b = fake_hwnd(0x13);
let queue = [
geometry_command(a, 1, 2, 3, 4),
zorder_command(a, b),
geometry_command(b, 5, 6, 7, 8),
geometry_command(a, 9, 10, 11, 12),
];
let applied: Vec<WindowPosArgs> = queue.iter().map(window_pos_args).collect();
assert_eq!(applied.len(), queue.len(), "件数が変わっている");
for (index, (args, cmd)) in applied.iter().zip(queue.iter()).enumerate() {
assert_eq!(args.hwnd, cmd.hwnd, "{index} 番目の対象窓");
assert_eq!(
args.hwnd_insert_after, cmd.hwnd_insert_after,
"{index} 番目の挿入位置"
);
assert_eq!(args.flags, cmd.flags, "{index} 番目のフラグ");
assert_eq!(
(args.x, args.y, args.cx, args.cy),
(cmd.x, cmd.y, cmd.width, cmd.height),
"{index} 番目の矩形"
);
}
assert_eq!(
applied
.iter()
.position(|args| args.hwnd_insert_after.is_some()),
Some(1),
"Z 専用指令が並びの中で動いている: {applied:?}"
);
}
#[test]
fn each_degrade_reason_has_its_own_word() {
let words = [
BatchDegrade::BeginFailed.as_str(),
BatchDegrade::DeferFailed { seq: 2 }.as_str(),
BatchDegrade::EndFailed.as_str(),
];
let mut unique = words.to_vec();
unique.sort_unstable();
unique.dedup();
assert_eq!(
unique.len(),
words.len(),
"縮退の理由語が重複している: {words:?}"
);
}
#[test]
fn a_flush_of_real_windows_is_submitted_as_one_batch_and_says_so() {
clean_slate();
let first = create_test_window();
let second = create_test_window();
let captured = capture_under_filter(SIGNOFF_DIRECTIVES, || {
emit_control();
SetWindowPosCommand::enqueue(geometry_command(first, 100, 110, 40, 30));
SetWindowPosCommand::enqueue(geometry_command(second, 200, 210, 50, 60));
SetWindowPosCommand::enqueue(zorder_command(second, first));
flush_window_pos_commands();
});
let degraded = capture_under_filter(SIGNOFF_DIRECTIVES, || {
emit_control();
SetWindowPosCommand::enqueue(geometry_command(first, 100, 110, 40, 30));
SetWindowPosCommand::enqueue(geometry_command(second, 200, 210, 50, 60));
SetWindowPosCommand::enqueue(zorder_command(second, first));
with_forced_batch_begin_failure(flush_window_pos_commands);
});
destroy(&[first, second]);
clean_slate();
assert!(captured.contains(CONTROL_LINE), "捕捉が死んでいる");
let flushes = lines_of_kind(&captured, KIND_FLUSH);
assert_eq!(flushes.len(), 2, "区間は開始と終了の 2 行だけ: {captured}");
assert!(
flushes[0].contains("stage=begin") && flushes[0].contains("count=3"),
"開始行の指令数: {}",
flushes[0]
);
assert!(
flushes[1].contains("stage=end") && flushes[1].contains("count=3"),
"終了行の指令数: {}",
flushes[1]
);
let writes = lines_of_kind(&captured, KIND_WRITE);
assert_eq!(writes.len(), 3, "指令 1 件につき書込 1 行: {captured}");
for (index, line) in writes.iter().enumerate() {
assert!(
line.contains(&format!("seq={index} ")),
"{index} 番目の通し番号が並びと違う: {line}"
);
assert!(
line.contains(" in_batch=true"),
"1 バッチで投入した書込が in_batch=true を名乗っていない: {line}"
);
assert!(
line.contains("ok=true"),
"実窓への書込が失敗している: {line}"
);
}
let degraded_writes = lines_of_kind(°raded, KIND_WRITE);
assert_eq!(degraded_writes.len(), 3, "縮退でも書込は 3 行: {degraded}");
for line in °raded_writes {
assert!(
line.contains(" in_batch=false"),
"縮退した書込が in_batch=false を名乗っていない: {line}"
);
assert!(
line.contains("ok=true"),
"縮退でも実窓への書込は成功する: {line}"
);
}
assert!(
degraded.contains(BatchDegrade::BeginFailed.as_str()),
"縮退が無音である(理由語が残っていない): {degraded}"
);
}
#[test]
fn an_invalid_handle_discards_the_batch_and_degrades_to_one_write_per_command() {
clean_slate();
let first = fake_hwnd(0x20);
let second = fake_hwnd(0x21);
let captured = capture_under_filter(SIGNOFF_DIRECTIVES, || {
emit_control();
SetWindowPosCommand::enqueue(geometry_command(first, 1, 1, 1, 1));
SetWindowPosCommand::enqueue(geometry_command(second, 2, 2, 2, 2));
flush_window_pos_commands();
});
clean_slate();
assert!(captured.contains(CONTROL_LINE), "捕捉が死んでいる");
assert!(
captured.contains(BatchDegrade::DeferFailed { seq: 0 }.as_str()),
"`DeferWindowPos` の失敗が記録されていない: {captured}"
);
assert!(
!captured.contains(BatchDegrade::BeginFailed.as_str()),
"確保は成功しているのに確保失敗の理由が出ている: {captured}"
);
let writes = lines_of_kind(&captured, KIND_WRITE);
assert_eq!(
writes.len(),
2,
"縮退しても指令の件数だけ書込が出る: {captured}"
);
for line in &writes {
assert!(line.contains(" in_batch=false"), "縮退した書込の札: {line}");
assert!(
line.contains("ok=false"),
"偽ハンドルへの書込は失敗する: {line}"
);
}
}
const WRITE_LOG_DIRECTIVES: &str = "info,wintf::ecs::window=debug";
fn write_log_lines<'a>(captured: &'a str, via: Option<&str>) -> Vec<&'a str> {
captured
.lines()
.filter(|line| line.contains(super::WINDOW_WRITE_LOG_MESSAGE))
.filter(|line| via.is_none_or(|via| line.contains(&format!("via=\"{via}\""))))
.collect()
}
#[test]
fn the_write_log_line_is_emitted_once_per_command_on_both_paths() {
clean_slate();
let first = create_test_window();
let second = create_test_window();
let batched = capture_under_filter(WRITE_LOG_DIRECTIVES, || {
SetWindowPosCommand::enqueue(geometry_command(first, 120, 130, 40, 30));
SetWindowPosCommand::enqueue(geometry_command(second, 220, 230, 50, 60));
flush_window_pos_commands();
});
let degraded = capture_under_filter(WRITE_LOG_DIRECTIVES, || {
SetWindowPosCommand::enqueue(geometry_command(first, 121, 131, 40, 30));
SetWindowPosCommand::enqueue(geometry_command(second, 221, 231, 50, 60));
with_forced_batch_begin_failure(flush_window_pos_commands);
});
destroy(&[first, second]);
let discarded = capture_under_filter(WRITE_LOG_DIRECTIVES, || {
SetWindowPosCommand::enqueue(geometry_command(fake_hwnd(0x30), 1, 1, 1, 1));
SetWindowPosCommand::enqueue(geometry_command(fake_hwnd(0x31), 2, 2, 2, 2));
flush_window_pos_commands();
});
clean_slate();
assert_eq!(
write_log_lines(&batched, None).len(),
2,
"バッチ経路の実施ログが指令数と違う: {batched}"
);
assert_eq!(
write_log_lines(&batched, Some("DeferWindowPos")).len(),
2,
"バッチ経路の行が呼んだ API を名乗っていない: {batched}"
);
assert_eq!(
write_log_lines(°raded, None).len(),
2,
"縮退経路の実施ログが指令数と違う: {degraded}"
);
assert_eq!(
write_log_lines(°raded, Some("SetWindowPos")).len(),
2,
"縮退経路の行が呼んだ API を名乗っていない: {degraded}"
);
assert_eq!(
write_log_lines(&discarded, None).len(),
2,
"破棄されたバッチの指令が「書いた」と名乗っている(O9 の本数が二重になる): {discarded}"
);
assert_eq!(
write_log_lines(&discarded, Some("DeferWindowPos")).len(),
0,
"適用されなかった積み上げが実施ログを残している: {discarded}"
);
assert_eq!(
write_log_lines(&batched, Some("SetWindowPos")).len(),
0,
"`via=` の絞り込みが効いていない(部分一致で両方拾っている): {batched}"
);
}
#[test]
fn the_batch_keeps_the_self_initiated_guard_over_the_messages_it_sends() {
clean_slate();
let hwnd = create_test_window();
SELF_INITIATED_ON_POSCHANGED.with(|cell| cell.borrow_mut().clear());
RECORDING.with(|cell| *cell.borrow_mut() = true);
SetWindowPosCommand::enqueue(geometry_command(hwnd, 60, 70, 20, 25));
flush_window_pos_commands();
let batched = SELF_INITIATED_ON_POSCHANGED.with(|cell| cell.borrow().clone());
SELF_INITIATED_ON_POSCHANGED.with(|cell| cell.borrow_mut().clear());
SetWindowPosCommand::enqueue(geometry_command(hwnd, 80, 90, 20, 25));
with_forced_batch_begin_failure(flush_window_pos_commands);
let sequential = SELF_INITIATED_ON_POSCHANGED.with(|cell| cell.borrow().clone());
RECORDING.with(|cell| *cell.borrow_mut() = false);
destroy(&[hwnd]);
clean_slate();
assert!(
!batched.is_empty(),
"`EndDeferWindowPos` が `WM_WINDOWPOSCHANGED` を送っていない(探針が空虚)"
);
assert!(
batched.iter().all(|observed| *observed),
"バッチの送るメッセージが外部由来と判定されている(位置権威が壊れる): {batched:?}"
);
assert!(!sequential.is_empty(), "縮退側でも受理はある");
assert!(
sequential.iter().all(|observed| *observed),
"1 本ずつの経路で自発判定が偽になった: {sequential:?}"
);
assert!(
!is_self_initiated(),
"一括書込の外側なのに自発判定が真のまま(錠が効いていない)"
);
}
const Z_SCAN_ATTEMPTS: usize = 8;
fn relative_z_order(windows: &[HWND]) -> Vec<HWND> {
let mut best: Vec<HWND> = Vec::new();
for _ in 0..Z_SCAN_ATTEMPTS {
let seen = scan_z_once(windows);
if seen.len() == windows.len() {
return seen;
}
if seen.len() > best.len() {
best = seen;
}
}
best
}
fn scan_z_once(windows: &[HWND]) -> Vec<HWND> {
let mut result = Vec::new();
let mut cursor = unsafe { GetTopWindow(None) }.ok();
let mut steps = 0usize;
while let Some(hwnd) = cursor {
if hwnd.is_invalid() {
break;
}
if windows.contains(&hwnd) && !result.contains(&hwnd) {
result.push(hwnd);
if result.len() == windows.len() {
break;
}
}
steps += 1;
if steps > 100_000 {
break;
}
cursor = unsafe { GetWindow(hwnd, GW_HWNDNEXT) }.ok();
}
result
}
fn z_shape(set: &[HWND]) -> Vec<usize> {
relative_z_order(set)
.iter()
.filter_map(|hwnd| set.iter().position(|w| w == hwnd))
.collect()
}
fn normalize_z(set: &[HWND]) {
for index in 1..set.len() {
SetWindowPosCommand::enqueue(zorder_command(set[index], set[index - 1]));
}
with_forced_batch_begin_failure(flush_window_pos_commands);
}
#[test]
fn a_batched_zorder_list_lands_in_the_same_order_as_one_write_per_command() {
clean_slate();
let batch_set: Vec<HWND> = (0..3).map(|_| create_test_window()).collect();
let sequential_set: Vec<HWND> = (0..3).map(|_| create_test_window()).collect();
normalize_z(&batch_set);
normalize_z(&sequential_set);
let batch_start = z_shape(&batch_set);
let sequential_start = z_shape(&sequential_set);
const EXPECTED: [usize; 3] = [2, 0, 1];
let program = |set: &[HWND]| {
SetWindowPosCommand::enqueue(zorder_command(set[0], set[2]));
SetWindowPosCommand::enqueue(zorder_command(set[1], set[0]));
};
program(&batch_set);
flush_window_pos_commands();
let batched = z_shape(&batch_set);
program(&sequential_set);
with_forced_batch_begin_failure(flush_window_pos_commands);
let sequential = z_shape(&sequential_set);
destroy(&batch_set);
destroy(&sequential_set);
clean_slate();
assert_eq!(
batch_start,
vec![0, 1, 2],
"バッチ側の始点が揃っていない: {batch_start:?}"
);
assert_eq!(
sequential_start,
vec![0, 1, 2],
"1 本ずつ側の始点が揃っていない: {sequential_start:?}"
);
assert_eq!(
batched,
EXPECTED.to_vec(),
"バッチ投入の最終 Z 順序が逐次適用の結果と違う(要件 10.3)"
);
assert_eq!(
sequential,
EXPECTED.to_vec(),
"1 本ずつの書込の最終 Z 順序が期待と違う(対照が壊れている)"
);
assert_eq!(
batched, sequential,
"バッチ投入と 1 本ずつの書込で Z の最終順序が食い違う(要件 10.3)"
);
}