wintf 0.0.3

Windows Tategaki Framework - Rust UI library with Japanese vertical text support
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
//! `tick_gate` の決定論テスト。
//!
//! 見るのは純関数 [`should_run`] ただ 1 つで、時計も乱数も共有状態も使わない
//! (実時間の閾値は 1 つも登場しない=要件 6.5)。
//!
//! 主軸は 2 本立てである。
//!
//! ⒈ **全組合せ**——旗 2^10(1,024 通り)× 期限の有無 2 × 前回実行からのフレーム数 6 点
//!    (0・1・29・30・31・上限)× 起動からのフレーム数 6 点(0・1・599・600・601・上限)
//!    × 門の有効 2 = 147,456 通りを総なめし、**本番とは別に書き下ろした対照実装**
//!    ([`oracle`])と 1 件ずつ突き合わせる。対照実装は素直な if の連なりで、本番の
//!    書き方に引きずられないよう意図的に別の形にしてある。
//!
//! ⒉ **省略の必要十分条件**——同じ 147,456 通りに対して「省略になるのは、旗が 1 本も
//!    立たず・期限も来ておらず・心拍に届かず・起動直後でもなく・門が有効なとき、
//!    かつそのときだけ」を対照実装を経由せずに直にぶつける。⒈ が対照実装ごと間違って
//!    いても、この 1 本が独立に捕まえる。
//!
//! 残りは優先順位(5 つの理由の上下関係)・境界の値(599/600・29/30)・
//! `Wake` が運ぶ旗の中身・名前の綴り・[`TickGateInputs::from_snapshot`] の写しである。

use super::*;

use crate::ecs::world::tick_wake::{
    ANIM, DRAG, FORCE, GRAPHICS, POINTER, PRESENT, REARM, WINDOW_CMD, WM_GEOMETRY, WakeSnapshot,
    ZORDER,
};

// ------------------------------------------------------------ 総なめの目盛り

/// 前回実行からのフレーム数の検査点(心拍 30 の直前・ちょうど・直後を挟む)。
const SINCE_RUN_POINTS: [u32; 6] = [0, 1, 29, 30, 31, u32::MAX];

/// 起動からのフレーム数の検査点(起動直後 600 の直前・ちょうど・直後を挟む)。
const SINCE_BOOT_POINTS: [u32; 6] = [0, 1, 599, 600, 601, u32::MAX];

/// 旗の全組合せ(10 本ぶん)。
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,
    }
}

/// 総なめの本体(同じ 147,456 通りを 2 つの検査が使う)。
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,
                        ));
                    }
                }
            }
        }
    }
}

/// 本番とは別に書き下ろした対照実装。
///
/// 優先順位を上から素直に並べただけの形にしてあり、本番の実装を参照しない。数値も
/// 本番の定数を借りずに 30/600 を直に書く(定数を書き換えたら、ここも赤くなって
/// 気づけるようにするため)。
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() {
    // 起動直後・旗あり・期限あり・心拍超過を全部同時に立てても、門が無効なら Disabled。
    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));

    // 回す理由が 1 つも無くても、門が無効なら省略にはならない。
    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());
}

// ================================================== 生産者一覧の字面検査

// ここから下は「旗を立てる側が現に在るか」の字面検査である。判断の中身は見ない
// ——判断の正しさは上の全組合せが見ており、ここが守るのは配線の抜けだけである
// (旗を立て忘れると、門は正しく判断したまま反応しない画面更新を作ってしまう)。

/// 旗を立てる側(wintf 内)の一覧。(見出し・中身・期待する旗の名前)。
///
/// 並びは見出しの辞書順。areka 側の生産者は別クレートなのでここからは読めず、
/// areka 側の同種の検査が受け持つ。
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} を立てていない"
            );
        }
    }
}

/// `pointer/buffers.rs` は投入の入口が 6 つあり、旗を立てるのはその全部である。
///
/// 呼出は私設ヘルパー 1 本に束ねてあるので、`tick_wake::mark(` が在るだけでは
/// 「ヘルパーは残っているが誰も呼んでいない」形を見逃す。呼び元の数を直に数える。
#[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: 配送点がメッセージ→旗の写像を通っていない"
    );
}

// ============================================ 生産者一覧の完全性(現物の木から照合する)

// 上の 2 本は「表に載っている行が現に旗を立てるか」を見る。それだけでは**表から落ちた
// 生産者**を捕まえられない——行を回すだけの検査は、載っていないファイルについては何も
// 言わないからである(実際 `zorder_group_maintain.rs` は一度落ちたまま緑で通っていた)。
// ここから下は逆向き、すなわち「木の中で旗を立てているファイルが全部表に載っているか」を
// 見る。両向きが揃って初めて名簿は名簿として働く。
//
// 見るのは 2 つの書き方である——`tick_wake::mark` とパスで書いた参照と、
// `tick_wake::` を含む `use` 行が `mark` を名指している形(`use …tick_wake::{ZORDER, mark};`)である。
// 呼出の字面 `tick_wake::mark(` だけを見ていたときは、**表に載せていない新設の生産者が
// `use` 形だと両向とも緑で抜けた**(表の側の検査は表に在る行しか見ないので助けに
// ならない)。なおモジュールごと別名にする形(`use …tick_wake as tw;`)までは見ていない
// ——その形が要るときは [`uses_tick_wake_name`] を広げること。

use std::path::{Path, PathBuf};

/// クレートの `src/` 配下の本番 `.rs` を集める(テストのファイルは除く)。
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);
    }
}

/// この本文が `tick_wake` の名前 `name` を使っているか。
///
/// 見るのは 2 つ——`tick_wake::name` と書いた参照と、`use …tick_wake::…name…` で
/// 引き込んでから素の `name` で書く形の `use` 行である。
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)
    })
}

/// 走査の判定子は 2 つの書き方をどちらも見る(`use` 形が抜けないことの自己検査)。
///
/// この 3 行が無いと、広げたはずの走査が実は広がっていなくても誰も気づかない
/// (現物の木に `use` 形の生産者が 1 つも無いため)。
#[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` 行を生産者と見なしている"
    );
}

/// `src/` からの相対パス(区切りは `/`)で、旗を立てている本番ファイルを列挙する。
///
/// `flag` が空文字なら「どれかの旗を立てている」、それ以外なら「その旗を立てている」。
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
}

/// 木の中で旗を立てている本番ファイルは、1 つ残らず `WINTF_PRODUCERS` に載っている。
///
/// 逆向きも同時に見る——表の行が現物のどのファイルにも当たらなければ、走査が壊れて
/// 0 件を返している(そのときは何を消しても緑になる)。両側から挟んでおく。
#[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}"
        );
    }
}

/// `tick_wake.rs` 冒頭の散文名簿の `ZORDER` の行は、旗を立てている wintf のファイルを
/// 1 つ残らずファイル名で名指ししている。
///
/// この行は一度実際にずれた(`zorder_group_maintain.rs` と `window_pos.rs` が落ちたまま)。
/// 見つけたのは人の目であってテストではない。以後は機械が見る。
#[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} を名指ししていない"
        );
    }
}

/// `ZORDER` 定数の doc は生産者を 1 人も名指ししない(名簿はモジュール冒頭のただ 1 つ)。
///
/// 写しが 2 つあると片方だけが古くなり、しかも古くなったことが誰にも見えない
/// (`PRESENT` 定数の doc が生産者を挙げていないのと同じ流儀)。
#[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 つ目の写しになる)"
        );
    }
}

/// `tick_wake.rs` の中身(散文名簿と定数 doc を切り出す元)。
const TICK_WAKE_SRC: &str = include_str!("tick_wake.rs");

/// 相対パスからファイル名の幹(`window/zorder_pair_maintain.rs` → `zorder_pair_maintain`)。
fn file_stem_of(rel: &str) -> &str {
    rel.rsplit('/')
        .next()
        .and_then(|name| name.strip_suffix(".rs"))
        .expect("相対パスが .rs で終わっていない")
}

/// 冒頭の散文名簿から `ZORDER` の項だけを切り出す(次の項の手前まで)。
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"))
}

/// 指定した旗の定数の直前に付いている doc コメントを切り出す。
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")
}