diff --git a/config/templates/report.html b/config/templates/report.html deleted file mode 100644 index 901b40d..0000000 --- a/config/templates/report.html +++ /dev/null @@ -1,218 +0,0 @@ - - - - - - MiniTIAS 照明均一性評価レポート - - - -
- - -

MiniTIAS 照明均一性評価レポート

-

生成日時: {{ generated_at }}

- - -

均一性サマリ

- - - - - - - - - - - - - - {% for row in summary_rows %} - - - - - - - - - - {% endfor %} - -
画像名平均輝度標準偏差CoV最大/最小比最大輝度最小輝度
{{ row.image_name }}{{ "%.2f"|format(row.mean|float) }}{{ "%.2f"|format(row.std|float) }}{{ "%.4f"|format(row.cov|float) }}{{ "%.4f"|format(row.max_min_ratio|float) }}{{ "%.2f"|format(row.max|float) }}{{ "%.2f"|format(row.min|float) }}
- - -

空間解析サマリ(中心-周辺勾配)

- - - - - - - - - - - - - {% for row in spatial_rows %} - - - - - - - - - {% endfor %} - -
画像名中心平均輝度中間平均輝度周辺平均輝度中心/周辺比勾配量 (%)
{{ row.image_name }}{{ "%.2f"|format(row.center_mean|float) }}{{ "%.2f"|format(row.middle_mean|float) }}{{ "%.2f"|format(row.periphery_mean|float) }}{{ "%.4f"|format(row.center_periphery_ratio|float) }}{{ "%.2f"|format(row.gradient_magnitude|float) }}
- - -

画像別詳細

- {% for item in image_details %} -
-

{{ item.name }}

-
- {% if item.luminance_map %} -
- 輝度マップ -

輝度マップ

-
- {% endif %} - {% if item.histogram %} -
- 輝度ヒストグラム -

輝度ヒストグラム

-
- {% endif %} - {% if item.radial_profile %} -
- 放射状プロファイル -

放射状輝度プロファイル

-
- {% endif %} - {% if item.zone_map %} -
- ゾーンマップ -

ゾーンマップ(中心 / 中間 / 周辺)

-
- {% endif %} -
-
- {% endfor %} - -
- - diff --git "a/docs/04_SPEC/SPEC_02_\347\205\247\346\230\216\345\235\207\344\270\200\346\200\247\350\251\225\344\276\241\343\202\242\343\203\253\343\202\264\343\203\252\343\202\272\343\203\240.md" "b/docs/04_SPEC/SPEC_02_\347\205\247\346\230\216\345\235\207\344\270\200\346\200\247\350\251\225\344\276\241\343\202\242\343\203\253\343\202\264\343\203\252\343\202\272\343\203\240.md" index 933cefd..fed1221 100644 --- "a/docs/04_SPEC/SPEC_02_\347\205\247\346\230\216\345\235\207\344\270\200\346\200\247\350\251\225\344\276\241\343\202\242\343\203\253\343\202\264\343\203\252\343\202\272\343\203\240.md" +++ "b/docs/04_SPEC/SPEC_02_\347\205\247\346\230\216\345\235\207\344\270\200\346\200\247\350\251\225\344\276\241\343\202\242\343\203\253\343\202\264\343\203\252\343\202\272\343\203\240.md" @@ -180,33 +180,6 @@ - **全体比較タブ**: 全画像の均一性指標テーブル・統計サマリー・指標の比較グラフ(2×2 棒グラフ) - **個別画像タブ**: ドロップダウンまたは矢印ボタンで画像を切り替え,元画像(ROI オーバーレイ付き)・輝度マップ・ヒストグラムを横並びで確認 -## レポート生成 (Report Generation) - -バッチ解析の結果から自己完結型の HTML レポートを生成する. - -### 起動コマンド - -```bash -python scripts/generate_report.py --output output/ -``` - -### 前提条件 - -- `scripts/run_uniformity.py` でバッチ解析(空間分析を含む)が完了していること -- `output/results/summary_uniformity.csv` と `output/results/summary_spatial.csv` が存在すること - -### 出力 - -- `output/reports/uniformity_report.html` — 自己完結 HTML レポート - -### レポート内容 - -- 均一性指標サマリテーブル(CoV,std,max_min_ratio 等) -- 空間分析サマリテーブル(center_mean,periphery_mean,C/P ratio,gradient 等) -- 画像別の詳細セクション(輝度マップ,ヒストグラム,放射状プロファイル,ゾーンマップ) - -画像はすべて base64 でインライン埋め込みされるため,HTML ファイル単体で閲覧可能.PDF が必要な場合はブラウザの印刷機能を使用する. - ## キャリブレーション (Calibration) 照明均一性評価ではカラーキャリブレーションを行わない.輝度の相対的なばらつき(CoV 等)は画像内の比較であり,カメラの色特性のズレが全画素に等しく影響するため,指標の値に影響しない. diff --git a/scripts/generate_report.py b/scripts/generate_report.py deleted file mode 100644 index 1a72439..0000000 --- a/scripts/generate_report.py +++ /dev/null @@ -1,62 +0,0 @@ -"""MiniTIAS 照明均一性評価 HTML レポート生成スクリプト. - -使い方: - python scripts/generate_report.py - python scripts/generate_report.py --output output/ - python scripts/generate_report.py --output output/ \\ - --template config/templates/report.html -""" - -import argparse -import sys -from pathlib import Path - -# プロジェクトルートを sys.path に追加(スクリプトを任意の場所から実行できるよう) -PROJECT_ROOT = Path(__file__).resolve().parent.parent -sys.path.insert(0, str(PROJECT_ROOT)) - -from src.export.report import build_report_context, render_html_report # noqa: E402 - - -def main() -> None: - """エントリーポイント.""" - parser = argparse.ArgumentParser( - description="MiniTIAS 照明均一性評価の HTML レポートを生成する." - ) - parser.add_argument( - "--output", - default=str(PROJECT_ROOT / "output"), - help="出力ディレクトリのパス(デフォルト: output/)", - ) - parser.add_argument( - "--template", - default=str(PROJECT_ROOT / "config" / "templates" / "report.html"), - help="Jinja2 テンプレートのパス(デフォルト: config/templates/report.html)", - ) - args = parser.parse_args() - - output_base = Path(args.output) - summary_csv = str(output_base / "results" / "summary_uniformity.csv") - spatial_csv = str(output_base / "results" / "summary_spatial.csv") - figures_dir = str(output_base / "figures") - report_output = str(output_base / "reports" / "uniformity_report.html") - - print("HTML レポートを生成します...") - print(f" 均一性 CSV : {summary_csv}") - print(f" 空間解析 CSV: {spatial_csv}") - print(f" 図ディレクトリ: {figures_dir}") - print(f" テンプレート: {args.template}") - - try: - context = build_report_context(summary_csv, spatial_csv, figures_dir) - except FileNotFoundError as e: - print(f"エラー: {e}") - print("先に run_uniformity.py でバッチ解析を実行してください.") - sys.exit(1) - - render_html_report(context, args.template, report_output) - print(f"レポート生成完了: {report_output}") - - -if __name__ == "__main__": - main() diff --git a/src/export/exporter.py b/src/export/exporter.py index 3f771ca..51756f8 100644 --- a/src/export/exporter.py +++ b/src/export/exporter.py @@ -16,8 +16,17 @@ ] +def _ensure_parent_dir(output_path: str) -> None: + """出力ファイルの親ディレクトリを作成する. + + Args: + output_path: 出力先ファイルパス. + """ + Path(output_path).parent.mkdir(parents=True, exist_ok=True) + + def ensure_output_dirs(base_path: str) -> None: - """output/figures/, output/results/, output/reports/ ディレクトリを作成する. + """output/figures/, output/results/ ディレクトリを作成する. Args: base_path: 出力ルートディレクトリのパス. @@ -25,7 +34,6 @@ base = Path(base_path) (base / "figures").mkdir(parents=True, exist_ok=True) (base / "results").mkdir(parents=True, exist_ok=True) - (base / "reports").mkdir(parents=True, exist_ok=True) print(f"出力ディレクトリを確認しました: {base}") @@ -38,7 +46,7 @@ results: 均一性指標の辞書(calc_uniformity の戻り値). output_path: 出力先ファイルパス(CSV). """ - Path(output_path).parent.mkdir(parents=True, exist_ok=True) + _ensure_parent_dir(output_path) with Path(output_path).open("w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=list(results.keys())) @@ -59,7 +67,7 @@ "max_min_ratio": ..., "max": ..., "min": ...} の形式. output_path: 出力先ファイルパス(CSV). """ - Path(output_path).parent.mkdir(parents=True, exist_ok=True) + _ensure_parent_dir(output_path) with Path(output_path).open("w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=SUMMARY_FIELDNAMES, extrasaction="ignore") @@ -81,7 +89,7 @@ "gradient_magnitude": ...} を含む形式. output_path: 出力先ファイルパス(CSV). """ - Path(output_path).parent.mkdir(parents=True, exist_ok=True) + _ensure_parent_dir(output_path) with Path(output_path).open("w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=SPATIAL_FIELDNAMES, extrasaction="ignore") @@ -145,7 +153,7 @@ stats: calc_batch_statistics の戻り値. output_path: 出力先ファイルパス(CSV). """ - Path(output_path).parent.mkdir(parents=True, exist_ok=True) + _ensure_parent_dir(output_path) fieldnames = ["statistic", "mean", "std", "min", "max", "cv"] rows = [] diff --git a/src/export/report.py b/src/export/report.py deleted file mode 100644 index b4b8217..0000000 --- a/src/export/report.py +++ /dev/null @@ -1,120 +0,0 @@ -"""HTML レポート生成モジュール.""" - -import base64 -from datetime import datetime -from pathlib import Path - -import pandas -from jinja2 import Environment, FileSystemLoader - - -def encode_image_base64(image_path: str) -> str: - """PNG 画像ファイルを Base64 エンコードした data URI に変換する. - - HTML に画像をインライン埋め込みすることで、レポートを自己完結させる. - - Args: - image_path: PNG 画像ファイルのパス. - - Returns: - "data:image/png;base64,..." 形式の文字列. - ファイルが存在しない場合は空文字列を返す. - """ - path = Path(image_path) - if not path.exists(): - return "" - - with path.open("rb") as f: - encoded = base64.b64encode(f.read()).decode("utf-8") - - return f"data:image/png;base64,{encoded}" - - -def build_report_context( - summary_csv_path: str, - spatial_csv_path: str, - figures_dir: str, -) -> dict: - """CSV と図ファイルからテンプレート描画用のコンテキスト辞書を構築する. - - 画像は Base64 エンコードしてインライン埋め込みにするため、 - レポート HTML が単独で完結する形になる. - - Args: - summary_csv_path: 均一性サマリ CSV のパス(summary_uniformity.csv). - spatial_csv_path: 空間解析サマリ CSV のパス(summary_spatial.csv). - figures_dir: 各種図ファイルが格納されたディレクトリのパス. - - Returns: - Jinja2 テンプレートに渡すコンテキスト辞書.キーは: - "generated_at", "summary_rows", "spatial_rows", "image_details". - - Raises: - FileNotFoundError: CSV ファイルが存在しない場合. - """ - summary_path = Path(summary_csv_path) - if not summary_path.exists(): - raise FileNotFoundError( - f"均一性サマリ CSV が見つかりません: {summary_csv_path}" - ) - - spatial_path = Path(spatial_csv_path) - if not spatial_path.exists(): - raise FileNotFoundError( - f"空間解析サマリ CSV が見つかりません: {spatial_csv_path}" - ) - - summary_df = pandas.read_csv(summary_path) - spatial_df = pandas.read_csv(spatial_path) - - figures_path = Path(figures_dir) - - # 画像別詳細: サマリに登場する image_name ごとに図を収集する - image_details = [] - for image_name in summary_df["image_name"].tolist(): - detail = { - "name": image_name, - "luminance_map": encode_image_base64( - str(figures_path / f"{image_name}_luminance_map.png") - ), - "histogram": encode_image_base64( - str(figures_path / f"{image_name}_histogram.png") - ), - "radial_profile": encode_image_base64( - str(figures_path / f"{image_name}_radial_profile.png") - ), - "zone_map": encode_image_base64( - str(figures_path / f"{image_name}_zone_map.png") - ), - } - image_details.append(detail) - - return { - "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "summary_rows": summary_df.to_dict(orient="records"), - "spatial_rows": spatial_df.to_dict(orient="records"), - "image_details": image_details, - } - - -def render_html_report(context: dict, template_path: str, output_path: str) -> None: - """Jinja2 テンプレートにコンテキストを適用して HTML レポートを生成する. - - Args: - context: テンプレートに渡すコンテキスト辞書(build_report_context の戻り値). - template_path: Jinja2 テンプレートファイルのパス. - output_path: 出力先 HTML ファイルのパス. - """ - template_file = Path(template_path) - env = Environment( - loader=FileSystemLoader(str(template_file.parent)), - autoescape=True, - ) - template = env.get_template(template_file.name) - html = template.render(**context) - - out_path = Path(output_path) - out_path.parent.mkdir(parents=True, exist_ok=True) - out_path.write_text(html, encoding="utf-8") - - print(f"HTML レポートを保存しました: {output_path}") diff --git a/tests/export/test_exporter.py b/tests/export/test_exporter.py index 8b7afb8..50f0b5b 100644 --- a/tests/export/test_exporter.py +++ b/tests/export/test_exporter.py @@ -46,11 +46,6 @@ ensure_output_dirs(str(tmp_path)) assert (tmp_path / "results").is_dir() - def test_creates_reports_directory(self, tmp_path: Path) -> None: - """正常: reports/ サブディレクトリが作成されること.""" - ensure_output_dirs(str(tmp_path)) - assert (tmp_path / "reports").is_dir() - def test_idempotent_when_dirs_already_exist(self, tmp_path: Path) -> None: """正常: 既にディレクトリが存在する場合でもエラーにならないこと.""" ensure_output_dirs(str(tmp_path)) @@ -58,7 +53,6 @@ ensure_output_dirs(str(tmp_path)) assert (tmp_path / "figures").is_dir() assert (tmp_path / "results").is_dir() - assert (tmp_path / "reports").is_dir() def test_creates_nested_base_directory(self, tmp_path: Path) -> None: """正常: base_path が存在しない場合でも自動作成されること.""" @@ -66,7 +60,6 @@ ensure_output_dirs(str(nested_base)) assert (nested_base / "figures").is_dir() assert (nested_base / "results").is_dir() - assert (nested_base / "reports").is_dir() # --------------------------------------------------------------------------- diff --git a/tests/export/test_report.py b/tests/export/test_report.py deleted file mode 100644 index 929cd14..0000000 --- a/tests/export/test_report.py +++ /dev/null @@ -1,411 +0,0 @@ -"""report.py の単体テスト. - -仕様 (実装: src/export/report.py): - - encode_image_base64: PNG→data URI(不在時は空文字列) - - build_report_context: CSV からコンテキスト辞書を構築 - - render_html_report: Jinja2 テンプレートレンダリング - -テスト方針 (GUIDE_08_テスト方針.md): - - pytest を使用 - - tmp_path フィクスチャで一時ディレクトリを利用 -""" - -import base64 -import struct -import zlib -from pathlib import Path - -import pytest - -from src.export.report import ( - build_report_context, - encode_image_base64, - render_html_report, -) - - -# --------------------------------------------------------------------------- -# ヘルパー: 最小 PNG ファイルを生成する -# --------------------------------------------------------------------------- - - -def _create_minimal_png(path: Path) -> None: - """1x1 ピクセルの最小有効 PNG ファイルを tmp_path に作成する.""" - # 1x1 白ピクセル PNG の最小バイナリ - def png_chunk(chunk_type: bytes, data: bytes) -> bytes: - length = struct.pack(">I", len(data)) - crc = struct.pack(">I", zlib.crc32(chunk_type + data) & 0xFFFFFFFF) - return length + chunk_type + data + crc - - signature = b"\x89PNG\r\n\x1a\n" - ihdr_data = struct.pack(">IIBBBBB", 1, 1, 8, 2, 0, 0, 0) # 1x1 RGB - ihdr = png_chunk(b"IHDR", ihdr_data) - - # フィルタバイト (0) + RGB = 0xFF, 0xFF, 0xFF - raw_row = b"\x00\xff\xff\xff" - compressed = zlib.compress(raw_row) - idat = png_chunk(b"IDAT", compressed) - iend = png_chunk(b"IEND", b"") - - path.write_bytes(signature + ihdr + idat + iend) - - -# --------------------------------------------------------------------------- -# encode_image_base64 テスト -# --------------------------------------------------------------------------- - - -class TestEncodeImageBase64: - """encode_image_base64 関数のテスト群.""" - - def test_returns_data_uri_for_existing_png(self, tmp_path: Path) -> None: - """正常: 存在する PNG ファイルで data URI 文字列が返ること.""" - png_path = tmp_path / "test.png" - _create_minimal_png(png_path) - - result = encode_image_base64(str(png_path)) - assert result.startswith("data:image/png;base64,") - - def test_returns_empty_string_for_missing_file(self, tmp_path: Path) -> None: - """正常: 存在しないファイルパスを渡した場合に空文字列を返すこと.""" - missing_path = str(tmp_path / "nonexistent.png") - result = encode_image_base64(missing_path) - assert result == "" - - def test_base64_content_is_decodable(self, tmp_path: Path) -> None: - """正常: data URI の base64 部分がデコード可能であること.""" - png_path = tmp_path / "test.png" - _create_minimal_png(png_path) - - result = encode_image_base64(str(png_path)) - # "data:image/png;base64," プレフィックスを除去してデコードを試みる - prefix = "data:image/png;base64," - encoded_part = result[len(prefix):] - decoded = base64.b64decode(encoded_part) - # PNG シグネチャで始まることを確認 - assert decoded[:8] == b"\x89PNG\r\n\x1a\n" - - def test_base64_content_matches_file_content(self, tmp_path: Path) -> None: - """正常: エンコードされたデータがファイルの内容と一致すること.""" - png_path = tmp_path / "test.png" - _create_minimal_png(png_path) - - result = encode_image_base64(str(png_path)) - prefix = "data:image/png;base64," - encoded_part = result[len(prefix):] - decoded = base64.b64decode(encoded_part) - - original = png_path.read_bytes() - assert decoded == original - - def test_returns_string_type(self, tmp_path: Path) -> None: - """正常: 戻り値が str 型であること(存在するファイル).""" - png_path = tmp_path / "test.png" - _create_minimal_png(png_path) - result = encode_image_base64(str(png_path)) - assert isinstance(result, str) - - def test_returns_string_type_for_missing_file(self, tmp_path: Path) -> None: - """正常: 戻り値が str 型であること(存在しないファイル).""" - result = encode_image_base64(str(tmp_path / "missing.png")) - assert isinstance(result, str) - - -# --------------------------------------------------------------------------- -# build_report_context テスト -# --------------------------------------------------------------------------- - - -class TestBuildReportContext: - """build_report_context 関数のテスト群.""" - - @pytest.fixture - def setup_csv_files(self, tmp_path: Path) -> tuple[Path, Path, Path]: - """モック CSV ファイルと figures ディレクトリを作成して返す fixture.""" - summary_csv = tmp_path / "summary_uniformity.csv" - spatial_csv = tmp_path / "summary_spatial.csv" - figures_dir = tmp_path / "figures" - figures_dir.mkdir() - - summary_csv.write_text( - "image_name,mean,std,cov,max_min_ratio,max,min\n" - "image_001,180.0,5.0,0.028,1.15,190.0,165.0\n" - "image_002,170.0,8.0,0.047,1.20,185.0,155.0\n", - encoding="utf-8", - ) - spatial_csv.write_text( - "image_name,center_mean,middle_mean,periphery_mean,center_periphery_ratio,gradient_magnitude\n" - "image_001,185.0,178.0,165.0,1.12,10.8\n" - "image_002,175.0,168.0,152.0,1.15,13.1\n", - encoding="utf-8", - ) - return summary_csv, spatial_csv, figures_dir - - def test_return_value_has_generated_at_key( - self, setup_csv_files: tuple[Path, Path, Path] - ) -> None: - """正常: 戻り値に 'generated_at' キーが存在すること.""" - summary_csv, spatial_csv, figures_dir = setup_csv_files - context = build_report_context( - str(summary_csv), str(spatial_csv), str(figures_dir) - ) - assert "generated_at" in context - - def test_return_value_has_summary_rows_key( - self, setup_csv_files: tuple[Path, Path, Path] - ) -> None: - """正常: 戻り値に 'summary_rows' キーが存在すること.""" - summary_csv, spatial_csv, figures_dir = setup_csv_files - context = build_report_context( - str(summary_csv), str(spatial_csv), str(figures_dir) - ) - assert "summary_rows" in context - - def test_return_value_has_spatial_rows_key( - self, setup_csv_files: tuple[Path, Path, Path] - ) -> None: - """正常: 戻り値に 'spatial_rows' キーが存在すること.""" - summary_csv, spatial_csv, figures_dir = setup_csv_files - context = build_report_context( - str(summary_csv), str(spatial_csv), str(figures_dir) - ) - assert "spatial_rows" in context - - def test_return_value_has_image_details_key( - self, setup_csv_files: tuple[Path, Path, Path] - ) -> None: - """正常: 戻り値に 'image_details' キーが存在すること.""" - summary_csv, spatial_csv, figures_dir = setup_csv_files - context = build_report_context( - str(summary_csv), str(spatial_csv), str(figures_dir) - ) - assert "image_details" in context - - def test_return_value_has_exactly_4_keys( - self, setup_csv_files: tuple[Path, Path, Path] - ) -> None: - """正常: 戻り値のキー数が 4 であること.""" - summary_csv, spatial_csv, figures_dir = setup_csv_files - context = build_report_context( - str(summary_csv), str(spatial_csv), str(figures_dir) - ) - assert len(context) == 4 - - def test_summary_rows_count_matches_csv_rows( - self, setup_csv_files: tuple[Path, Path, Path] - ) -> None: - """正常: summary_rows の要素数が CSV のデータ行数と一致すること.""" - summary_csv, spatial_csv, figures_dir = setup_csv_files - context = build_report_context( - str(summary_csv), str(spatial_csv), str(figures_dir) - ) - assert len(context["summary_rows"]) == 2 - - def test_spatial_rows_count_matches_csv_rows( - self, setup_csv_files: tuple[Path, Path, Path] - ) -> None: - """正常: spatial_rows の要素数が CSV のデータ行数と一致すること.""" - summary_csv, spatial_csv, figures_dir = setup_csv_files - context = build_report_context( - str(summary_csv), str(spatial_csv), str(figures_dir) - ) - assert len(context["spatial_rows"]) == 2 - - def test_image_details_count_matches_summary_rows( - self, setup_csv_files: tuple[Path, Path, Path] - ) -> None: - """正常: image_details の要素数が summary_rows と一致すること.""" - summary_csv, spatial_csv, figures_dir = setup_csv_files - context = build_report_context( - str(summary_csv), str(spatial_csv), str(figures_dir) - ) - assert len(context["image_details"]) == len(context["summary_rows"]) - - def test_image_details_have_name_key( - self, setup_csv_files: tuple[Path, Path, Path] - ) -> None: - """正常: image_details の各要素が 'name' キーを持つこと.""" - summary_csv, spatial_csv, figures_dir = setup_csv_files - context = build_report_context( - str(summary_csv), str(spatial_csv), str(figures_dir) - ) - for detail in context["image_details"]: - assert "name" in detail - - def test_image_details_have_figure_keys( - self, setup_csv_files: tuple[Path, Path, Path] - ) -> None: - """正常: image_details の各要素が図ファイルのキーを持つこと.""" - summary_csv, spatial_csv, figures_dir = setup_csv_files - context = build_report_context( - str(summary_csv), str(spatial_csv), str(figures_dir) - ) - required_fig_keys = {"luminance_map", "histogram", "radial_profile", "zone_map"} - for detail in context["image_details"]: - assert required_fig_keys.issubset(set(detail.keys())) - - def test_missing_figure_files_return_empty_string( - self, setup_csv_files: tuple[Path, Path, Path] - ) -> None: - """正常: 対応する図ファイルが存在しない場合に空文字列が設定されること.""" - summary_csv, spatial_csv, figures_dir = setup_csv_files - context = build_report_context( - str(summary_csv), str(spatial_csv), str(figures_dir) - ) - # 図ファイルを作成していないため全て空文字列 - for detail in context["image_details"]: - assert detail["luminance_map"] == "" - assert detail["histogram"] == "" - assert detail["radial_profile"] == "" - assert detail["zone_map"] == "" - - def test_raises_file_not_found_for_missing_summary_csv( - self, tmp_path: Path - ) -> None: - """異常: 均一性サマリ CSV が存在しない場合に FileNotFoundError が送出されること.""" - missing_summary = str(tmp_path / "missing_summary.csv") - spatial_csv = tmp_path / "spatial.csv" - spatial_csv.write_text( - "image_name,center_mean,middle_mean,periphery_mean,center_periphery_ratio,gradient_magnitude\n", - encoding="utf-8", - ) - - with pytest.raises(FileNotFoundError): - build_report_context(missing_summary, str(spatial_csv), str(tmp_path)) - - def test_raises_file_not_found_for_missing_spatial_csv( - self, tmp_path: Path - ) -> None: - """異常: 空間解析サマリ CSV が存在しない場合に FileNotFoundError が送出されること.""" - summary_csv = tmp_path / "summary.csv" - summary_csv.write_text( - "image_name,mean,std,cov,max_min_ratio,max,min\n", - encoding="utf-8", - ) - missing_spatial = str(tmp_path / "missing_spatial.csv") - - with pytest.raises(FileNotFoundError): - build_report_context(str(summary_csv), missing_spatial, str(tmp_path)) - - def test_generated_at_is_string( - self, setup_csv_files: tuple[Path, Path, Path] - ) -> None: - """正常: generated_at が文字列であること.""" - summary_csv, spatial_csv, figures_dir = setup_csv_files - context = build_report_context( - str(summary_csv), str(spatial_csv), str(figures_dir) - ) - assert isinstance(context["generated_at"], str) - - def test_existing_figure_files_are_base64_encoded( - self, setup_csv_files: tuple[Path, Path, Path] - ) -> None: - """正常: 図ファイルが存在する場合に data URI として設定されること.""" - summary_csv, spatial_csv, figures_dir = setup_csv_files - # image_001 の luminance_map PNG を作成 - png_path = figures_dir / "image_001_luminance_map.png" - _create_minimal_png(png_path) - - context = build_report_context( - str(summary_csv), str(spatial_csv), str(figures_dir) - ) - # image_001 の detail を探す - detail_001 = next( - d for d in context["image_details"] if d["name"] == "image_001" - ) - assert detail_001["luminance_map"].startswith("data:image/png;base64,") - - -# --------------------------------------------------------------------------- -# render_html_report テスト -# --------------------------------------------------------------------------- - - -class TestRenderHtmlReport: - """render_html_report 関数のテスト群.""" - - @pytest.fixture - def simple_template(self, tmp_path: Path) -> Path: - """シンプルな Jinja2 テンプレートを作成して返す fixture.""" - template_path = tmp_path / "templates" / "report.html.j2" - template_path.parent.mkdir(parents=True, exist_ok=True) - template_path.write_text( - "" - "

Generated: {{ generated_at }}

" - "{% for row in summary_rows %}" - "

{{ row.image_name }}

" - "{% endfor %}" - "", - encoding="utf-8", - ) - return template_path - - @pytest.fixture - def simple_context(self) -> dict: - """シンプルなレンダリングコンテキストを返す fixture.""" - return { - "generated_at": "2026-04-08 12:00:00", - "summary_rows": [{"image_name": "image_001"}], - "spatial_rows": [], - "image_details": [], - } - - def test_output_html_file_is_created( - self, - simple_template: Path, - simple_context: dict, - tmp_path: Path, - ) -> None: - """正常: HTML ファイルが生成されること.""" - output_path = str(tmp_path / "report.html") - render_html_report(simple_context, str(simple_template), output_path) - assert Path(output_path).exists() - - def test_output_html_file_is_not_empty( - self, - simple_template: Path, - simple_context: dict, - tmp_path: Path, - ) -> None: - """正常: 出力ファイルが空でないこと.""" - output_path = str(tmp_path / "report.html") - render_html_report(simple_context, str(simple_template), output_path) - assert Path(output_path).stat().st_size > 0 - - def test_output_html_contains_rendered_content( - self, - simple_template: Path, - simple_context: dict, - tmp_path: Path, - ) -> None: - """正常: テンプレートがコンテキストで正しくレンダリングされること.""" - output_path = str(tmp_path / "report.html") - render_html_report(simple_context, str(simple_template), output_path) - - html_content = Path(output_path).read_text(encoding="utf-8") - assert "2026-04-08 12:00:00" in html_content - assert "image_001" in html_content - - def test_creates_parent_directory_automatically( - self, - simple_template: Path, - simple_context: dict, - tmp_path: Path, - ) -> None: - """正常: 出力ディレクトリが存在しない場合に自動作成されること.""" - nested_output = str(tmp_path / "output" / "reports" / "report.html") - render_html_report(simple_context, str(simple_template), nested_output) - assert Path(nested_output).exists() - - def test_output_is_utf8_encoded( - self, - simple_template: Path, - simple_context: dict, - tmp_path: Path, - ) -> None: - """正常: 出力ファイルが UTF-8 エンコードで書き込まれること.""" - output_path = str(tmp_path / "report.html") - render_html_report(simple_context, str(simple_template), output_path) - # UTF-8 として読み込めること(例外が発生しないこと) - content = Path(output_path).read_text(encoding="utf-8") - assert isinstance(content, str)