rustraight 0.5.2

A simple 2D game library for Rust, inspired by DXLib
// 検証結果を 検証ログ/verify_all_<epoch秒>.log へ即時flushで記録するロガー。

use std::collections::HashSet;
use std::fs::{self, File, OpenOptions};
use std::io::Write;
use std::time::{Instant, SystemTime, UNIX_EPOCH};

use crate::checklist::{ChecklistItem, VerifyMethod, CHECKLIST};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
    Pass,
    Fail,
    Skip,
    NotRun,
}

impl Verdict {
    fn as_str(self) -> &'static str {
        match self {
            Verdict::Pass => "PASS",
            Verdict::Fail => "FAIL",
            Verdict::Skip => "SKIP",
            Verdict::NotRun => "NOTRUN",
        }
    }
}

pub struct VerifyLog {
    file: File,
    path: String,
    start: Instant,
    pass: u32,
    fail: u32,
    skip: u32,
    notrun: u32,
    logged_ids: HashSet<u32>,
    problems: Vec<(u32, &'static str, &'static str, String)>, // id, api, verdict文字列, reason
}

impl VerifyLog {
    pub fn new() -> Self {
        fs::create_dir_all("検証ログ").expect("検証ログディレクトリの作成に失敗しました");
        let epoch = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
        let path = format!("検証ログ/verify_all_{epoch}.log");
        let mut file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(&path)
            .expect("ログファイルを開けませんでした");
        writeln!(file, "=== rustraight verify_all run (epoch={epoch}) ===").ok();
        file.flush().ok();
        println!("検証ログ: {path}");
        Self {
            file,
            path,
            start: Instant::now(),
            pass: 0,
            fail: 0,
            skip: 0,
            notrun: 0,
            logged_ids: HashSet::new(),
            problems: Vec::new(),
        }
    }

    pub fn record(&mut self, item: &ChecklistItem, verdict: Verdict, reason: &str) {
        if !self.logged_ids.insert(item.id) {
            return; // 二重記録防止
        }
        let elapsed = self.start.elapsed().as_secs_f32();
        writeln!(
            self.file,
            "t+{elapsed:>8.2}s  cat={:?}  id={:>3}  api=\"{}\"  verdict={}  reason=\"{}\"",
            item.category,
            item.id,
            item.api,
            verdict.as_str(),
            reason
        )
        .ok();
        self.file.flush().ok();

        match verdict {
            Verdict::Pass => self.pass += 1,
            Verdict::Fail => {
                self.fail += 1;
                self.problems.push((item.id, item.api, "FAIL", reason.to_string()));
            }
            Verdict::Skip => {
                self.skip += 1;
                self.problems.push((item.id, item.api, "SKIP", reason.to_string()));
            }
            Verdict::NotRun => {
                self.notrun += 1;
                self.problems.push((item.id, item.api, "NOTRUN", reason.to_string()));
            }
        }
    }

    pub fn summary_counts(&self) -> (u32, u32, u32, u32) {
        (self.pass, self.fail, self.skip, self.notrun)
    }

    pub fn path(&self) -> &str {
        &self.path
    }

    /// まだ記録されていない人力判定項目(自動テスト可を除く)を NotRun として一括記録する。
    /// Esc中断・ウィンドウを閉じての終了のどちらでも、プログラム終了直前に1回呼べば良い。
    pub fn sweep_notrun_all(&mut self) {
        let remaining: Vec<&'static ChecklistItem> = CHECKLIST
            .iter()
            .filter(|i| i.method != VerifyMethod::Auto && !self.logged_ids.contains(&i.id))
            .collect();
        for item in remaining {
            self.record(item, Verdict::NotRun, "ツール終了のため未実施");
        }
    }

    pub fn write_summary(&mut self) {
        let elapsed = self.start.elapsed().as_secs_f32();
        let total = self.pass + self.fail + self.skip + self.notrun;
        writeln!(self.file, "=== summary at t+{elapsed:.2}s ===").ok();
        writeln!(
            self.file,
            "PASS={} FAIL={} SKIP={} NOTRUN={} TOTAL={}",
            self.pass, self.fail, self.skip, self.notrun, total
        )
        .ok();
        for (id, api, kind, reason) in &self.problems {
            writeln!(self.file, "  [{kind}] id={id} api=\"{api}\" reason=\"{reason}\"").ok();
        }
        self.file.flush().ok();
    }
}