use crate::ecs::test_support::capture_under_filter;
use crate::ecs::window::DPI;
use crate::ecs::world::EcsWorld;
use crate::executor::util::WindowMessage;
use bevy_ecs::prelude::Entity;
use std::cell::RefCell;
use std::rc::Rc;
use windows::Win32::Foundation::{HWND, LPARAM, RECT, WPARAM};
use windows::Win32::UI::WindowsAndMessaging::WM_DPICHANGED;
const PROCEDURE_DIRECTIVES: &str =
"info,wintf::ecs::window_proc=debug,wintf::ecs::drag=debug,areka::placement::diag=debug";
const WRITE_PATH_DIRECTIVES: &str = "info,wintf::ecs::window=debug";
const DEFAULT_DIRECTIVES: &str = "info";
fn dpichanged_message(new_dpi: u16, suggested: &RECT) -> WindowMessage {
WindowMessage {
hwnd: HWND(std::ptr::null_mut()),
msg: WM_DPICHANGED,
wparam: WPARAM(((new_dpi as usize) << 16) | new_dpi as usize),
lparam: LPARAM(suggested as *const RECT as isize),
}
}
fn dispatch_dpichanged(new_dpi: u16, suggested: RECT) -> Entity {
let world = Rc::new(RefCell::new(EcsWorld::new()));
let entity = world
.borrow_mut()
.world_mut()
.spawn(DPI::from_dpi(96, 96))
.id();
let m = dpichanged_message(new_dpi, &suggested);
let _ = crate::ecs::dispatch_window_message(&world, entity, &m);
let _ = crate::ecs::window::DpiChangeContext::take();
entity
}
fn suggested_rect() -> RECT {
RECT {
left: 3210,
top: 140,
right: 3810,
bottom: 620,
}
}
#[test]
fn suggested_position_decision_is_visible_under_procedure_directive() {
let out = capture_under_filter(PROCEDURE_DIRECTIVES, || {
dispatch_dpichanged(192, suggested_rect());
});
assert!(
out.contains("[WM_DPICHANGED] suggested position write decision"),
"提案位置の実施可否が診断手順の水準で観測できない(要件 1.3/1.5): {out}"
);
}
#[test]
fn suggested_position_decision_carries_applied_flag_and_suggested_origin() {
let out = capture_under_filter(PROCEDURE_DIRECTIVES, || {
dispatch_dpichanged(192, suggested_rect());
});
let line = out
.lines()
.find(|l| l.contains("[WM_DPICHANGED] suggested position write decision"))
.unwrap_or_else(|| panic!("実施可否行が無い: {out}"));
assert!(
line.contains("applied="),
"実施可否フィールドが無い: {line}"
);
assert!(
line.contains("suggested_left=3210") && line.contains("suggested_top=140"),
"提案 left/top が復元できない: {line}"
);
assert!(
line.contains("entity="),
"表示基盤ログと areka 側レコードの結合キー `entity` が無い: {line}"
);
}
#[test]
fn dpi_acceptance_line_reports_old_and_new_dpi_under_procedure_directive() {
let out = capture_under_filter(PROCEDURE_DIRECTIVES, || {
dispatch_dpichanged(192, suggested_rect());
});
let line = out
.lines()
.find(|l| l.contains("[WM_DPICHANGED] DPI component directly updated"))
.unwrap_or_else(|| panic!("DPI 受理行が無い: {out}"));
assert!(
line.contains("old_dpi_x=96") && line.contains("new_dpi_x=192"),
"新旧 DPI が同一行から復元できない(方向の機械判定が不能): {line}"
);
}
#[test]
fn suggested_position_decision_is_silent_under_default_info_filter() {
let out = capture_under_filter(DEFAULT_DIRECTIVES, || {
dispatch_dpichanged(192, suggested_rect());
});
assert!(
!out.contains("[WM_DPICHANGED] suggested position write decision"),
"既定 `info` で診断専用の実施可否行が漏れている: {out}"
);
}
#[test]
fn window_pos_write_path_is_visible_at_debug() {
let out = capture_under_filter(WRITE_PATH_DIRECTIVES, || {
dispatch_dpichanged(192, suggested_rect());
});
assert!(
out.contains("[guarded_set_window_pos] Calling SetWindowPos"),
"窓位置書込の共通経路が debug 水準で観測できない(要件 1.3): {out}"
);
}
#[test]
fn window_pos_write_path_is_silent_under_default_info_filter() {
let out = capture_under_filter(DEFAULT_DIRECTIVES, || {
dispatch_dpichanged(192, suggested_rect());
});
assert!(
!out.contains("[guarded_set_window_pos] Calling SetWindowPos"),
"既定 `info` で書込経路ログが漏れている: {out}"
);
}
use crate::ecs::window::DpiSuggestedRectPolicy;
const CURRENT_ORIGIN: (i32, i32) = (1200, 400);
const SUGGESTED_EXTENT: (i32, i32) = (400, 300);
fn suggested_rect_for(dpi: u16, current: (i32, i32)) -> RECT {
let ratio = dpi as f32 / 96.0;
let left = (current.0 as f32 * ratio).round() as i32;
let top = (current.1 as f32 * ratio).round() as i32;
RECT {
left,
top,
right: left + SUGGESTED_EXTENT.0,
bottom: top + SUGGESTED_EXTENT.1,
}
}
#[derive(Debug)]
struct DpiChangedOutcome {
dpi_x_after: u16,
context_established: bool,
written_origin: Option<(i32, i32)>,
handler_result: Option<isize>,
}
impl DpiChangedOutcome {
fn final_position(&self, current: (i32, i32)) -> (i32, i32) {
self.written_origin.unwrap_or(current)
}
}
fn parse_write_origin(out: &str) -> Option<(i32, i32)> {
let line = out
.lines()
.find(|l| l.contains("[guarded_set_window_pos] Calling SetWindowPos"))?;
let field = |name: &str| -> Option<i32> {
line.split_whitespace()
.find_map(|tok| tok.strip_prefix(name))
.and_then(|v| v.parse::<i32>().ok())
};
Some((field("x=")?, field("y=")?))
}
fn dispatch_dpichanged_logged(
new_dpi: u16,
suggested: RECT,
policy: Option<DpiSuggestedRectPolicy>,
) -> String {
let world = Rc::new(RefCell::new(EcsWorld::new()));
let entity = {
let mut w = world.borrow_mut();
let mut e = w.world_mut().spawn(DPI::from_dpi(96, 96));
if let Some(p) = policy {
e.insert(p);
}
e.id()
};
let m = dpichanged_message(new_dpi, &suggested);
let out = capture_under_filter(PROCEDURE_DIRECTIVES, || {
let _ = crate::ecs::dispatch_window_message(&world, entity, &m);
});
let _ = crate::ecs::window::DpiChangeContext::take();
out
}
fn decision_line(out: &str) -> String {
out.lines()
.find(|l| l.contains("[WM_DPICHANGED] suggested position write decision"))
.unwrap_or_else(|| panic!("実施可否行が無い: {out}"))
.to_string()
}
fn dispatch_dpichanged_observed(
new_dpi: u16,
suggested: RECT,
policy: Option<DpiSuggestedRectPolicy>,
) -> DpiChangedOutcome {
let world = Rc::new(RefCell::new(EcsWorld::new()));
let entity = {
let mut w = world.borrow_mut();
let mut e = w.world_mut().spawn(DPI::from_dpi(96, 96));
if let Some(p) = policy {
e.insert(p);
}
e.id()
};
let m = dpichanged_message(new_dpi, &suggested);
let mut handler_result = None;
let out = capture_under_filter(WRITE_PATH_DIRECTIVES, || {
handler_result =
crate::ecs::dispatch_window_message(&world, entity, &m).map(|lresult| lresult.0);
});
let context_established = crate::ecs::window::DpiChangeContext::take().is_some();
let dpi_x_after = {
let mut w = world.borrow_mut();
w.world_mut()
.get::<DPI>(entity)
.copied()
.expect("DPI component は spawn 時に付与済み")
.dpi_x
};
DpiChangedOutcome {
dpi_x_after,
context_established,
written_origin: parse_write_origin(&out),
handler_result,
}
}
fn assert_external_authority_preserves_anchor_at(dpi: u16) {
let suggested = suggested_rect_for(dpi, CURRENT_ORIGIN);
let outcome = dispatch_dpichanged_observed(
dpi,
suggested,
Some(DpiSuggestedRectPolicy::ExternalAuthority),
);
assert_eq!(
outcome.dpi_x_after, dpi,
"dpi={dpi}: DPI component が更新されていない: {outcome:?}"
);
if dpi == 96 {
assert_eq!(
(suggested.left, suggested.top),
CURRENT_ORIGIN,
"dpi=96 では提案原点が現位置と一致する前提(96 が欠陥を隠す性質)"
);
} else {
assert_ne!(
suggested.left, CURRENT_ORIGIN.0,
"dpi={dpi} では提案 X が現位置から動いている前提"
);
}
assert_eq!(
outcome.final_position(CURRENT_ORIGIN),
CURRENT_ORIGIN,
"dpi={dpi}: ExternalAuthority 窓の最終位置は現接地点のままであるべき\
(OS 提案位置が無条件に採用されている=S1・Req 4.3/4.2): {outcome:?}"
);
}
#[test]
fn s1_red_external_authority_preserves_anchor_at_dpi96() {
assert_external_authority_preserves_anchor_at(96);
}
#[test]
fn s1_red_external_authority_preserves_anchor_at_dpi120() {
assert_external_authority_preserves_anchor_at(120);
}
#[test]
fn s1_red_external_authority_preserves_anchor_at_dpi192() {
assert_external_authority_preserves_anchor_at(192);
}
#[test]
fn s1_red_external_authority_establishes_no_write_context() {
for dpi in [120_u16, 192] {
let suggested = suggested_rect_for(dpi, CURRENT_ORIGIN);
let outcome = dispatch_dpichanged_observed(
dpi,
suggested,
Some(DpiSuggestedRectPolicy::ExternalAuthority),
);
assert_eq!(
outcome.dpi_x_after, dpi,
"dpi={dpi}: DPI component が更新されていない: {outcome:?}"
);
assert!(
!outcome.context_established,
"dpi={dpi}: ExternalAuthority 窓で DpiChangeContext が確立されている\
(残置コンテキストが後続 WM_WINDOWPOSCHANGED を DPI echo と誤認させる・D3): {outcome:?}"
);
assert!(
outcome.written_origin.is_none(),
"dpi={dpi}: ExternalAuthority 窓へ OS 提案位置が書き込まれている(S1): {outcome:?}"
);
}
}
#[test]
fn s1_control_default_policy_windows_apply_suggested_origin() {
for dpi in [96_u16, 120, 192] {
for policy in [None, Some(DpiSuggestedRectPolicy::ApplyPosition)] {
let suggested = suggested_rect_for(dpi, CURRENT_ORIGIN);
let outcome = dispatch_dpichanged_observed(dpi, suggested, policy);
assert_eq!(
outcome.dpi_x_after, dpi,
"dpi={dpi} policy={policy:?}: DPI component が更新されていない: {outcome:?}"
);
assert!(
outcome.context_established,
"dpi={dpi} policy={policy:?}: 既定窓で DpiChangeContext が確立されない: {outcome:?}"
);
assert_eq!(
outcome.written_origin,
Some((suggested.left, suggested.top)),
"dpi={dpi} policy={policy:?}: 既定窓へ提案原点が書かれていない: {outcome:?}"
);
}
}
}
#[test]
fn s1_write_context_and_position_write_are_branched_together() {
for dpi in [96_u16, 120, 192] {
for policy in [
None,
Some(DpiSuggestedRectPolicy::ApplyPosition),
Some(DpiSuggestedRectPolicy::ExternalAuthority),
] {
let suggested = suggested_rect_for(dpi, CURRENT_ORIGIN);
let outcome = dispatch_dpichanged_observed(dpi, suggested, policy);
assert_eq!(
outcome.context_established,
outcome.written_origin.is_some(),
"dpi={dpi} policy={policy:?}: コンテキスト確立と位置書込が別々に分岐している(D3 違反): {outcome:?}"
);
}
}
}
#[test]
fn s1_decision_line_reports_external_authority_and_applied_false() {
let suggested = suggested_rect_for(192, CURRENT_ORIGIN);
let out = dispatch_dpichanged_logged(
192,
suggested,
Some(DpiSuggestedRectPolicy::ExternalAuthority),
);
let line = decision_line(&out);
assert!(
line.contains("applied=false"),
"外部権威窓なのに実施可否が false でない(`applied` が定数のまま): {line}"
);
assert!(
line.contains("policy=ExternalAuthority"),
"判断の根拠となった政策が同一行から復元できない(design.md:330 のフィールド): {line}"
);
assert!(
line.contains(&format!("suggested_left={}", suggested.left)),
"退けた提案原点が記録されていない: {line}"
);
}
#[test]
fn s1_decision_line_reports_unset_policy_and_applied_true() {
let out = dispatch_dpichanged_logged(192, suggested_rect_for(192, CURRENT_ORIGIN), None);
let line = decision_line(&out);
assert!(
line.contains("applied=true"),
"既定窓の実施可否が true でない(後方互換の非退行が壊れている): {line}"
);
assert!(
line.contains("policy=unset"),
"component 未付与が政策フィールドから読み取れない: {line}"
);
}
#[test]
fn s1_decision_line_reports_apply_position_as_its_own_label() {
let out = dispatch_dpichanged_logged(
192,
suggested_rect_for(192, CURRENT_ORIGIN),
Some(DpiSuggestedRectPolicy::ApplyPosition),
);
let line = decision_line(&out);
assert!(
line.contains("policy=ApplyPosition"),
"明示 ApplyPosition が専用の語で報告されていない: {line}"
);
assert!(
line.contains("applied=true"),
"ApplyPosition は書く判断のはず(純関数の契約と食い違っている): {line}"
);
assert!(
!line.contains("policy=unset") && !line.contains("policy=unreachable"),
"政策フィールドに複数の語が同時に現れている(書式の取り違え): {line}"
);
}
#[test]
fn s1_decision_line_reports_unreachable_when_policy_cannot_be_read() {
let suggested = suggested_rect_for(192, CURRENT_ORIGIN);
let world = Rc::new(RefCell::new(EcsWorld::new()));
let despawned = {
let mut w = world.borrow_mut();
let e = w
.world_mut()
.spawn((
DPI::from_dpi(96, 96),
DpiSuggestedRectPolicy::ExternalAuthority,
))
.id();
w.world_mut().despawn(e);
e
};
let m = dpichanged_message(192, &suggested);
let out = {
capture_under_filter(PROCEDURE_DIRECTIVES, || {
let _ = crate::ecs::dispatch_window_message(&world, despawned, &m);
})
};
let _ = crate::ecs::window::DpiChangeContext::take();
let line = decision_line(&out);
assert!(
line.contains("policy=unreachable"),
"破棄済み entity で政策が読めなかったのに `unreachable` を名乗っていない\
(`unset` と混同すると事後突合が偽の結論を作る・Req 1.5): {line}"
);
assert!(
line.contains("applied=true"),
"到達不能時のフォールバックは従来挙動(書く)であるべき: {line}"
);
let world2 = Rc::new(RefCell::new(EcsWorld::new()));
let entity2 = world2
.borrow_mut()
.world_mut()
.spawn((
DPI::from_dpi(96, 96),
DpiSuggestedRectPolicy::ExternalAuthority,
))
.id();
let m2 = dpichanged_message(192, &suggested);
let out2 = {
capture_under_filter(PROCEDURE_DIRECTIVES, || {
let _held = world2.borrow_mut(); let _ = crate::ecs::dispatch_window_message(&world2, entity2, &m2);
})
};
let _ = crate::ecs::window::DpiChangeContext::take();
let line2 = decision_line(&out2);
assert!(
line2.contains("policy=unreachable"),
"World 借用の再入で政策が読めなかったのに `unreachable` を名乗っていない: {line2}"
);
let readable = dispatch_dpichanged_logged(
192,
suggested,
Some(DpiSuggestedRectPolicy::ExternalAuthority),
);
assert!(
decision_line(&readable).contains("policy=ExternalAuthority"),
"同一構成の窓が到達可能なら ExternalAuthority を出すはず(探針が退化している)"
);
}
#[test]
fn s1_external_authority_handles_the_message_instead_of_delegating_to_defwindowproc() {
for dpi in [96_u16, 120, 192] {
let suggested = suggested_rect_for(dpi, CURRENT_ORIGIN);
let outcome = dispatch_dpichanged_observed(
dpi,
suggested,
Some(DpiSuggestedRectPolicy::ExternalAuthority),
);
assert!(
outcome.written_origin.is_none(),
"dpi={dpi}: 探針が「書かない」経路を通っていない: {outcome:?}"
);
assert_eq!(
outcome.handler_result,
Some(0),
"dpi={dpi}: 書かない判定でハンドラが未処理(None)を返している\
——`DefWindowProcW` が提案矩形を適用し、その中の `SetWindowPos` が
同期的に窓を動かすため源断ちが無効化する: {outcome:?}"
);
}
}
#[test]
fn s1_default_policy_windows_also_report_the_message_as_handled() {
for policy in [None, Some(DpiSuggestedRectPolicy::ApplyPosition)] {
let suggested = suggested_rect_for(192, CURRENT_ORIGIN);
let outcome = dispatch_dpichanged_observed(192, suggested, policy);
assert!(
outcome.written_origin.is_some(),
"policy={policy:?}: 探針が「書く」経路を通っていない: {outcome:?}"
);
assert_eq!(
outcome.handler_result,
Some(0),
"policy={policy:?}: 既定窓でハンドラの戻り値が変わっている: {outcome:?}"
);
}
}