use std::sync::{Arc, Mutex};
use std::thread::ThreadId;
use bevy_ecs::schedule::SingleThreadedExecutor;
const WORLD_CONSTRUCTION_SRC: &str = include_str!("../../world/mod.rs");
use windows::Win32::Foundation::RECT;
use super::*;
use crate::ecs::test_support::capture_under_filter;
use crate::ecs::types::{Point, SizeI};
use crate::ecs::window::transition_diag::{
KIND_MONITOR, KIND_WINDPI, RECORD_PREFIX_TAG, TRANSITION_TARGET,
};
use crate::ecs::world::{EcsWorld, Update};
use crate::ecs::{DPI, Monitor, WindowPos};
const SIGNOFF_DIRECTIVES: &str = "info,wintf::transition=debug";
const SIGNOFF_WITH_EXISTING_DIRECTIVES: &str =
"info,wintf::transition=debug,wintf::ecs::layout=debug";
const DEFAULT_DIRECTIVES: &str = "info";
const CONTROL_LINE: &str = "[transition-probe] alive";
const PROBE_STAMP: Stamp = Stamp {
frame: 7,
t_us: 1234,
};
fn emit_control() {
tracing::info!(target: TRANSITION_TARGET, "{CONTROL_LINE}");
}
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 monitor_before() -> Monitor {
Monitor {
handle: 0x0000_ABCD,
bounds: RECT {
left: 0,
top: 0,
right: 3840,
bottom: 2160,
},
work_area: RECT {
left: 0,
top: 0,
right: 3840,
bottom: 2100,
},
dpi: 120,
is_primary: true,
}
}
fn monitor_after_work_area_only() -> Monitor {
Monitor {
work_area: RECT {
left: 0,
top: 0,
right: 3840,
bottom: 2064,
},
..monitor_before()
}
}
fn monitor_after() -> Monitor {
Monitor {
work_area: RECT {
left: 0,
top: 0,
right: 3840,
bottom: 2064,
},
dpi: 192,
..monitor_before()
}
}
#[derive(Resource, Clone)]
struct InjectedMonitorTable(Vec<Monitor>);
#[derive(Resource, Clone, Copy)]
struct ProbeStamp(Stamp);
#[derive(Resource, Default)]
struct RedriveCount(usize);
#[allow(clippy::too_many_arguments)]
fn apply_injected_with_stamp(
mut commands: Commands,
layout_root: Query<Entity, With<LayoutRoot>>,
mut existing_monitors: Query<(Entity, &mut Monitor), With<Monitor>>,
mut windows: Query<(Entity, &WindowPos, &mut DPI)>,
mut taffy_res: ResMut<TaffyLayoutResource>,
injected: Res<InjectedMonitorTable>,
stamp: Res<ProbeStamp>,
mut redriven: ResMut<RedriveCount>,
) {
let root_entity = layout_root
.single()
.expect("檻の LayoutRoot が単一で存在する");
redriven.0 = apply_monitor_snapshot(
&mut commands,
root_entity,
&mut existing_monitors,
&mut windows,
&mut taffy_res,
injected.0.clone(),
stamp.0,
);
}
fn probe_world(injected: Vec<Monitor>) -> (World, Entity) {
let mut world = World::new();
world.insert_resource(TaffyLayoutResource::default());
world.insert_resource(InjectedMonitorTable(injected));
world.insert_resource(ProbeStamp(PROBE_STAMP));
world.insert_resource(RedriveCount::default());
world.spawn(LayoutRoot);
let monitor_entity = world.spawn(monitor_before()).id();
(world, monitor_entity)
}
fn spawn_probe_window(world: &mut World, dpi: u16) -> Entity {
world
.spawn((
WindowPos {
position: Some(Point { x: 100, y: 100 }),
size: Some(SizeI {
width: 400,
height: 600,
}),
..Default::default()
},
DPI::from_dpi(dpi, dpi),
))
.id()
}
fn run_apply(world: &mut World) {
let mut schedule = Schedule::default();
schedule.set_executor(SingleThreadedExecutor::new());
schedule.add_systems(apply_injected_with_stamp);
schedule.run(world);
}
#[test]
fn a_value_change_update_is_recorded_with_the_supplied_stamp() {
let (mut world, monitor_entity) = probe_world(vec![monitor_after()]);
let captured = capture_under_filter(SIGNOFF_WITH_EXISTING_DIRECTIVES, || {
emit_control();
run_apply(&mut world);
});
assert!(
captured.contains(CONTROL_LINE),
"対照が拾えていない=捕捉が死んでいる: {captured}"
);
let records = lines_of_kind(&captured, KIND_MONITOR);
assert_eq!(records.len(), 1, "値変化 1 件につき 1 行: {captured}");
let line = records[0];
assert!(
line.contains("[transition] frame=7 t_us=1234 kind=monitor"),
"接頭語の刻印が渡した値と違う: {line}"
);
assert!(
line.contains(&format!("entity={monitor_entity:?}")),
"更新したモニタの entity が載る(areka 側レコードとの結合キー): {line}"
);
assert!(
line.contains("old_dpi=120 new_dpi=192"),
"新旧の DPI が同一行から復元できない: {line}"
);
assert!(
line.contains("old_wa=0,0,3840,2100 new_wa=0,0,3840,2064"),
"新旧の作業領域が同一行から復元できない: {line}"
);
assert!(
captured.contains("[detect_display_change_system] Updating Monitor entity"),
"既存の更新ログが失われている: {captured}"
);
let stored = world
.get::<Monitor>(monitor_entity)
.expect("Monitor エンティティが生存している")
.clone();
assert_eq!(
(stored.dpi, stored.work_area.bottom),
(192, 2064),
"モニタ表の更新そのものが壊れている: {stored:?}"
);
}
#[test]
fn an_unchanged_table_records_nothing() {
let (mut world, _monitor_entity) = probe_world(vec![monitor_before()]);
let captured = capture_under_filter(SIGNOFF_DIRECTIVES, || {
emit_control();
run_apply(&mut world);
});
assert!(captured.contains(CONTROL_LINE), "捕捉が死んでいる");
assert!(
lines_of_kind(&captured, KIND_MONITOR).is_empty(),
"値が動いていないのに記録が出ている(定常フレームで行が増える): {captured}"
);
}
#[test]
fn the_monitor_record_is_silent_under_the_default_directives() {
let (mut world, _monitor_entity) = probe_world(vec![monitor_after()]);
let captured = capture_under_filter(DEFAULT_DIRECTIVES, || {
emit_control();
run_apply(&mut world);
});
assert!(
captured.contains(CONTROL_LINE),
"対照が拾えていない=捕捉が死んでいる: {captured}"
);
assert!(
transition_lines(&captured).is_empty(),
"観測 target を点けない運転では観測行は 1 行も出ない: {captured}"
);
}
#[test]
fn redriving_a_window_dpi_records_the_window_origin() {
let (mut world, _monitor_entity) = probe_world(vec![monitor_after()]);
let window = spawn_probe_window(&mut world, 120);
let captured = capture_under_filter(SIGNOFF_WITH_EXISTING_DIRECTIVES, || {
emit_control();
run_apply(&mut world);
});
assert!(
captured.contains(CONTROL_LINE),
"対照が拾えていない=捕捉が死んでいる: {captured}"
);
let records = lines_of_kind(&captured, KIND_WINDPI);
assert_eq!(records.len(), 1, "書き換えた窓 1 つにつき 1 行: {captured}");
let line = records[0];
assert!(
line.contains("[transition] frame=7 t_us=1234 kind=windpi"),
"接頭語の刻印が渡した値と違う(本関数は時刻を読まない): {line}"
);
assert!(
line.contains(&format!("entity={window:?}")),
"書き換えた**窓**の entity が載る(モニタの entity ではない): {line}"
);
assert!(
line.contains("old_dpi=120 new_dpi=192"),
"新旧の表示 DPI が同一行から復元できない: {line}"
);
assert!(
captured.contains("[detect_display_change_system] Redriving window DPI"),
"既存の再導出ログが失われている: {captured}"
);
assert_eq!(
*world.get::<DPI>(window).expect("窓の DPI"),
DPI::from_dpi(192, 192),
"再導出そのものが壊れている"
);
}
#[test]
fn a_window_whose_dpi_does_not_change_records_nothing() {
let (mut world, _monitor_entity) = probe_world(vec![monitor_after_work_area_only()]);
let window = spawn_probe_window(&mut world, 120);
let captured = capture_under_filter(SIGNOFF_DIRECTIVES, || {
emit_control();
run_apply(&mut world);
});
assert!(captured.contains(CONTROL_LINE), "捕捉が死んでいる");
assert_eq!(
lines_of_kind(&captured, KIND_MONITOR).len(),
1,
"対照の起点行(モニタ表の値変化)が出ていない=檻の前提が崩れている: {captured}"
);
assert!(
lines_of_kind(&captured, KIND_WINDPI).is_empty(),
"窓の DPI が動いていないのに起点が出ている(遷移が水増しされる): {captured}"
);
assert_eq!(
*world.get::<DPI>(window).expect("窓の DPI"),
DPI::from_dpi(120, 120),
"同値の腕で書き換えが起きている"
);
}
#[test]
fn the_window_origin_is_silent_under_the_default_directives() {
let (mut world, _monitor_entity) = probe_world(vec![monitor_after()]);
let _window = spawn_probe_window(&mut world, 120);
let captured = capture_under_filter(DEFAULT_DIRECTIVES, || {
emit_control();
run_apply(&mut world);
});
assert!(
captured.contains(CONTROL_LINE),
"対照が拾えていない=捕捉が死んでいる: {captured}"
);
assert!(
transition_lines(&captured).is_empty(),
"観測 target を点けない運転では観測行は 1 行も出ない: {captured}"
);
}
#[derive(Clone, Copy, Debug)]
struct StampObservation {
from_world: Stamp,
from_mirror: u32,
thread: ThreadId,
}
#[derive(Resource, Clone, Default)]
struct ObservedMonitorStamps(Arc<Mutex<Vec<StampObservation>>>);
#[allow(clippy::too_many_arguments)]
fn observe_monitor_stamp(
mut commands: Commands,
frame: Res<FrameCount>,
tick_start: Res<TickStart>,
layout_root: Query<Entity, With<LayoutRoot>>,
mut existing_monitors: Query<(Entity, &mut Monitor), With<Monitor>>,
mut windows: Query<(Entity, &WindowPos, &mut DPI)>,
mut taffy_res: ResMut<TaffyLayoutResource>,
injected: Res<InjectedMonitorTable>,
out: Res<ObservedMonitorStamps>,
) {
let stamp = transition_diag::stamp_from_world(&frame, &tick_start);
out.0
.lock()
.expect("観測バッファの毒化なし")
.push(StampObservation {
from_world: stamp,
from_mirror: transition_diag::current_frame(),
thread: std::thread::current().id(),
});
let Ok(root_entity) = layout_root.single() else {
return;
};
apply_monitor_snapshot(
&mut commands,
root_entity,
&mut existing_monitors,
&mut windows,
&mut taffy_res,
injected.0.clone(),
stamp,
);
}
#[test]
fn monitor_stamps_match_frame_count_under_the_default_multi_threaded_executor() {
let mut ecs = EcsWorld::new();
let mut query = ecs.world_mut().query::<&Monitor>();
let mut injected: Vec<Monitor> = query.iter(ecs.world()).cloned().collect();
let probe_entity = ecs.world_mut().spawn(monitor_before()).id();
injected.push(monitor_after());
let sink = ObservedMonitorStamps::default();
ecs.world_mut().insert_resource(sink.clone());
ecs.world_mut()
.insert_resource(InjectedMonitorTable(injected));
ecs.add_systems(Update, observe_monitor_stamp);
assert!(
WORLD_CONSTRUCTION_SRC.contains("schedules.insert(Schedule::new(Update));"),
"本番の World 構築が `Update` の実行器を差し替えている。既定の多スレッド実行器を前提とする本テストは空振りする"
);
assert!(
ecs.try_tick_world(),
"システム登録済みの World は tick する"
);
let expected = ecs.world().resource::<FrameCount>().0;
assert_eq!(expected, 1, "1 周で FrameCount は 1 になる");
let observations = sink.0.lock().expect("観測バッファの毒化なし").clone();
assert!(
!observations.is_empty(),
"観測系システムが 1 度も走っていない(空虚に緑になっている)"
);
for observation in &observations {
assert_eq!(
observation.from_world.frame, expected,
"多スレッド実行でもモニタ表更新点の刻印は FrameCount とずれない: {observation:?}"
);
}
let ui_thread = std::thread::current().id();
for observation in observations.iter().filter(|o| o.thread != ui_thread) {
assert_eq!(
observation.from_mirror, 0,
"ワーカースレッドからは写しが読めない(World を持つ点が stamp() を呼ぶ退行の検出): {observation:?}"
);
}
let stored = ecs
.world()
.get::<Monitor>(probe_entity)
.expect("探針のモニタが生存している")
.clone();
assert_eq!(
(stored.dpi, stored.work_area.bottom),
(192, 2064),
"探針が値変化更新の分岐(=記録点)を通っていない: {stored:?}"
);
transition_diag::reset_for_test();
}
fn code_only(source: &str) -> String {
source
.lines()
.filter(|line| !line.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn the_monitor_record_builds_its_stamp_from_world_resources_only() {
let code = code_only(include_str!("monitor_systems.rs"));
assert!(
code.contains("transition_diag::stamp_from_world(&frame, &tick_start)"),
"モニタ表更新点の刻印が World 資源から組まれていない(D1 違反=ワーカースレッドで frame=0 になる)"
);
assert_eq!(
code.matches("transition_diag::stamp()").count(),
0,
"World を借りられる観測点がスレッド局所ミラーを読んでいる(D1 違反)"
);
assert_eq!(
code.matches("transition_diag::is_enabled()").count(),
2,
"記録点の前置ガードが 2 つでない(既定運転で行の組立の費用を払う形になる)"
);
assert!(
code.contains("pub fn detect_display_change_system("),
"説明文を落とす処理が本文まで落としている"
);
}