diff --git a/.claude/rules/coding-style.md b/.claude/rules/coding-style.md index 42ed3a7..a4b314e 100644 --- a/.claude/rules/coding-style.md +++ b/.claude/rules/coding-style.md @@ -11,7 +11,8 @@ ## フォーマット (Formatting) - フォーマッター・リンター: Ruff(`ruff.toml` で設定済み) -- 行長: 88 文字(`ruff.toml` で設定済み) +- 行長: 100 文字(`ruff.toml` で設定済み).日本語の docstring・コメントも意味の切れ目で折り返す +- `ruff check` の対象外: `.claude/`(テンプレート管理のフック・テスト.プロジェクト側で整形するとテンプレート同期で競合するため) - クォート: ダブルクォート `"` ## 命名規則 (Naming) diff --git "a/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" "b/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" index 1fb7132..ca6d068 100644 --- "a/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" +++ "b/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" @@ -21,15 +21,14 @@ ## 依存更新の検証スイート (Verification Suite) -`/deps-update` のローカル検証など,依存を更新した後に「壊れていないか」を確かめるときはこの 2 つを実行する. +`/deps-update` のローカル検証など,依存を更新した後に「壊れていないか」を確かめるときはこの 3 つを実行する(すべて成功で「検証緑」). ```bash uv sync uv run pytest +uv run ruff check . ``` -※ `ruff check` は既存コードに違反が残っている(主に E501 行長)ため,現時点では検証スイートに含めない. - ## テスト (Test) ```bash diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index da348df..a0402af 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -55,3 +55,7 @@ - **動機**: 従来は `rawpy>=0.21.0` 以外が無指定で,環境を作るたびに numpy・opencv 等の版が変わり,報告書の数値の再現性を担保できなかった.また無指定では Dependabot が更新 PR をほぼ作れない(ecosystem を `pip` → `uv` に変更し,lock 更新 PR が届くようにした). - **検証**: `uv run pytest` で 357 passed / 24 skipped(skip はすべてサンプル DNG が `data/` に無いことによる). - **未着手**: `ruff check` に既存違反 299 件(E501 行長 268・I001 24 等)が残るため,依存更新の検証スイート(ENV_04)は `uv sync` + `uv run pytest` のみとした. + +- [x] **ruff check の既存違反を解消(2026-09-29,#15)**: 違反 299 件 → 0 件.`ruff check .` を依存更新の検証スイート(ENV_04)に組み込んだ. + - **設計判断**: 行長を 88 → 100 に緩和(E501 268 件の大半は 89〜100 文字の日本語 docstring で,全件折り返すと差分が大きい割に可読性の利得が小さい.100 超の 26 件のみ手で折り返し).`.claude/` はテンプレート管理のため ruff 対象外に(整形すると `/sync-template` で競合する).`ruff format` は差分最小化のため未適用. + - **追加**: scripts をリポジトリ外 cwd から起動するスモークテスト `tests/scripts/test_script_entrypoints.py`(import 順の自動修正で `from src...` が `sys.path.insert` より前へ移る回帰を既存テストでは検出できなかったため). diff --git a/ruff.toml b/ruff.toml index 1da7bbf..3b7b04f 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,5 +1,6 @@ -line-length = 88 +line-length = 100 target-version = "py311" +extend-exclude = [".claude"] [format] quote-style = "double" diff --git a/scripts/compare_png_dng_slide.py b/scripts/compare_png_dng_slide.py index 1a41e7c..e1154a0 100644 --- a/scripts/compare_png_dng_slide.py +++ b/scripts/compare_png_dng_slide.py @@ -30,6 +30,7 @@ calc_distance_map, calc_radial_min_max_ratio, calc_radial_profile, + calc_spatial_uniformity, ) from src.analysis.uniformity import ( # noqa: E402 REC709_COEFF_B, @@ -37,7 +38,6 @@ REC709_COEFF_R, calc_uniformity, ) -from src.analysis.spatial import calc_spatial_uniformity # noqa: E402 from src.config import load_roi_config # noqa: E402 from src.io.dng_pipeline import dng_luminance # noqa: E402 from src.io.loader import extract_roi, load_image # noqa: E402 @@ -177,8 +177,14 @@ fig.savefig(str(OUTPUT_PATH), dpi=150, bbox_inches="tight") plt.close(fig) print(f"比較スライドを保存しました: {OUTPUT_PATH}") - print(f" PNG radial min/max = {png_radial['radial_min_max_ratio']:.4f}, CoV = {png_metrics['cov']:.4f}") - print(f" DNG radial min/max = {dng_radial['radial_min_max_ratio']:.4f}, CoV = {dng_metrics['cov']:.4f}") + print( + f" PNG radial min/max = {png_radial['radial_min_max_ratio']:.4f}, " + f"CoV = {png_metrics['cov']:.4f}" + ) + print( + f" DNG radial min/max = {dng_radial['radial_min_max_ratio']:.4f}, " + f"CoV = {dng_metrics['cov']:.4f}" + ) if __name__ == "__main__": diff --git a/scripts/run_uniformity.py b/scripts/run_uniformity.py index 1671960..52cf5f4 100644 --- a/scripts/run_uniformity.py +++ b/scripts/run_uniformity.py @@ -20,9 +20,9 @@ sys.path.insert(0, str(PROJECT_ROOT)) from src.analysis.reproducibility import calc_pairwise_ssim_registered # noqa: E402 -from src.config import load_roi_config # noqa: E402 from src.analysis.spatial import calc_spatial_uniformity # noqa: E402 from src.analysis.uniformity import calc_uniformity, to_grayscale # noqa: E402 +from src.config import load_roi_config # noqa: E402 from src.export.exporter import ( # noqa: E402 calc_batch_statistics, ensure_output_dirs, diff --git a/scripts/run_uniformity_dng.py b/scripts/run_uniformity_dng.py index 851f708..57125d4 100644 --- a/scripts/run_uniformity_dng.py +++ b/scripts/run_uniformity_dng.py @@ -5,7 +5,8 @@ PNG 版との違い: - 読み込みを load_image_dng(linear, output_color=raw)に置換 -- DNG は RAW_SENSOR で未補正のため,meta.json の lscMap でレンズシェーディングを順適用(--no-lsc で無効化可) +- DNG は RAW_SENSOR で未補正のため,meta.json の lscMap でレンズシェーディングを順適用 + (--no-lsc で無効化可) - DNG はセンサ native が landscape のため,PNG と同じ portrait へ rot90(-1) で向き合わせ 使い方: diff --git a/scripts/viewer.py b/scripts/viewer.py index fefb434..f6f9307 100644 --- a/scripts/viewer.py +++ b/scripts/viewer.py @@ -280,14 +280,18 @@ { "統計量": "平均シフト量 (px)", "SSIM(位置合わせなし)": "—", - "SSIM(位置合わせ後)": f"{float(ssim_stats['mean_shift_magnitude'].iloc[0]):.4f}", + "SSIM(位置合わせ後)": ( + f"{float(ssim_stats['mean_shift_magnitude'].iloc[0]):.4f}" + ), } ) rows.append( { "統計量": "最大シフト量 (px)", "SSIM(位置合わせなし)": "—", - "SSIM(位置合わせ後)": f"{float(ssim_stats['max_shift_magnitude'].iloc[0]):.4f}", + "SSIM(位置合わせ後)": ( + f"{float(ssim_stats['max_shift_magnitude'].iloc[0]):.4f}" + ), } ) diff --git a/src/io/dng_loader.py b/src/io/dng_loader.py index cd5b6d4..ebb6f57 100644 --- a/src/io/dng_loader.py +++ b/src/io/dng_loader.py @@ -5,7 +5,6 @@ import numpy import rawpy - # Rec.709 グレースケール変換係数(uniformity.py の REC709_COEFF_* と一致させる) # 参照: ITU-R BT.709 (https://www.itu.int/rec/R-REC-BT.709/en) _REC709_R = 0.2126 @@ -15,7 +14,8 @@ # demosaic 引数 → rawpy.DemosaicAlgorithm のマッピング # 参照: https://letmaik.github.io/rawpy/api/rawpy.DemosaicAlgorithm.html _DEMOSAIC_MAP: dict[str, rawpy.DemosaicAlgorithm] = { - "nearest": rawpy.DemosaicAlgorithm.LINEAR, # 最近傍は rawpy に専用 enum がないため LINEAR で代替 + # 最近傍は rawpy に専用 enum がないため LINEAR で代替 + "nearest": rawpy.DemosaicAlgorithm.LINEAR, "bilinear": rawpy.DemosaicAlgorithm.LINEAR, "ahd": rawpy.DemosaicAlgorithm.AHD, } @@ -133,7 +133,8 @@ **_postprocess_common, output_color=rawpy.ColorSpace.raw, ) - # rgb16: (H, W, 3) uint16,値域 0〜65535(black_level 補正済み linear,white_level で正規化) + # rgb16: (H, W, 3) uint16,値域 0〜65535 + # (black_level 補正済み linear,white_level で正規化) rgb_float = rgb16.astype(numpy.float32) / _UINT16_MAX # rgb_float: (H, W, 3) float32, linear, 値域 [0, 1] diff --git a/tests/analysis/test_radial_min_max.py b/tests/analysis/test_radial_min_max.py index 396f961..e6c7ba0 100644 --- a/tests/analysis/test_radial_min_max.py +++ b/tests/analysis/test_radial_min_max.py @@ -35,7 +35,6 @@ ) from src.export.exporter import SPATIAL_FIELDNAMES, export_spatial_summary - # --------------------------------------------------------------------------- # calc_radial_min_max_ratio テスト # --------------------------------------------------------------------------- @@ -276,7 +275,11 @@ result = calc_spatial_uniformity(sample_luminance) profile = result["radial_profile"] luminances = profile[:, 1] - expected_ratio = float(numpy.min(luminances)) / float(numpy.max(luminances)) if float(numpy.max(luminances)) != 0.0 else float("inf") + expected_ratio = ( + float(numpy.min(luminances)) / float(numpy.max(luminances)) + if float(numpy.max(luminances)) != 0.0 + else float("inf") + ) assert result["radial_min_max_ratio"] == pytest.approx(expected_ratio, abs=1e-6) def test_new_keys_do_not_overwrite_zone_stats_content( @@ -565,7 +568,8 @@ class TestPlotRadialProfileMinMaxMarkers: """plot_radial_profile に最大/最小マーカーと注記が追加された仕様検証テスト群. - テスト方針: 可視化テストは「出力ファイルが生成されること程度でよい」(.claude/rules/testing.md). + テスト方針: 可視化テストは「出力ファイルが生成されること程度でよい」 + (.claude/rules/testing.md). ここではファイル生成確認と、既知のプロファイルで例外が発生しないことを確認する. """ @@ -634,8 +638,11 @@ assert Path(output_path).exists() def test_signature_is_unchanged(self) -> None: - """仕様: plot_radial_profile のシグネチャが不変であること(引数 radial_profile, output_path).""" + """仕様: plot_radial_profile のシグネチャが不変であること + (引数 radial_profile, output_path). + """ import inspect + from src.visualization.plotter import plot_radial_profile sig = inspect.signature(plot_radial_profile) diff --git a/tests/analysis/test_registration.py b/tests/analysis/test_registration.py index c69c469..023b3df 100644 --- a/tests/analysis/test_registration.py +++ b/tests/analysis/test_registration.py @@ -27,7 +27,6 @@ from src.analysis.registration import apply_shift, compute_valid_mask, phase_correlate - # --------------------------------------------------------------------------- # ヘルパー # --------------------------------------------------------------------------- @@ -235,7 +234,9 @@ assert mask.shape == shape def test_positive_dy_makes_top_rows_false(self) -> None: - """正常: 正の dy シフトでは上端 ceil(dy) 行が False になること (仕様: 正=下方向シフト→上端無効).""" + """正常: 正の dy シフトでは上端 ceil(dy) 行が False になること + (仕様: 正=下方向シフト→上端無効). + """ dy = 3.2 shape = (32, 32) mask = compute_valid_mask(shape, dy, 0.0) @@ -246,7 +247,9 @@ assert np.all(mask[invalid_rows:, :]) def test_negative_dy_makes_bottom_rows_false(self) -> None: - """正常: 負の dy シフトでは下端 ceil(|dy|) 行が False になること (仕様: 負=上方向シフト→下端無効).""" + """正常: 負の dy シフトでは下端 ceil(|dy|) 行が False になること + (仕様: 負=上方向シフト→下端無効). + """ dy = -4.0 shape = (32, 32) mask = compute_valid_mask(shape, dy, 0.0) @@ -257,7 +260,9 @@ assert np.all(mask[: shape[0] - invalid_rows, :]) def test_positive_dx_makes_left_cols_false(self) -> None: - """正常: 正の dx シフトでは左端 ceil(dx) 列が False になること (仕様: 正=右方向シフト→左端無効).""" + """正常: 正の dx シフトでは左端 ceil(dx) 列が False になること + (仕様: 正=右方向シフト→左端無効). + """ dx = 5.0 shape = (32, 32) mask = compute_valid_mask(shape, 0.0, dx) @@ -266,7 +271,9 @@ assert np.all(mask[:, invalid_cols:]) def test_negative_dx_makes_right_cols_false(self) -> None: - """正常: 負の dx シフトでは右端 ceil(|dx|) 列が False になること (仕様: 負=左方向シフト→右端無効).""" + """正常: 負の dx シフトでは右端 ceil(|dx|) 列が False になること + (仕様: 負=左方向シフト→右端無効). + """ dx = -6.0 shape = (32, 32) mask = compute_valid_mask(shape, 0.0, dx) diff --git a/tests/analysis/test_reproducibility.py b/tests/analysis/test_reproducibility.py index 810fc09..800b6f4 100644 --- a/tests/analysis/test_reproducibility.py +++ b/tests/analysis/test_reproducibility.py @@ -28,7 +28,6 @@ from src.analysis.reproducibility import calc_pairwise_ssim, calc_pairwise_ssim_registered - # --------------------------------------------------------------------------- # ヘルパー # --------------------------------------------------------------------------- diff --git a/tests/analysis/test_spatial.py b/tests/analysis/test_spatial.py index 48696b6..0f5af30 100644 --- a/tests/analysis/test_spatial.py +++ b/tests/analysis/test_spatial.py @@ -24,7 +24,6 @@ classify_zones, ) - # --------------------------------------------------------------------------- # calc_distance_map テスト # --------------------------------------------------------------------------- @@ -126,7 +125,9 @@ assert zone_map[0, 0] == 0 # 0.0 < 0.33 → center def test_middle_zone_has_value_1(self) -> None: - """正常: center_threshold <= 距離 < periphery_threshold のピクセルがゾーン 1(middle)であること.""" + """正常: center_threshold <= 距離 < periphery_threshold のピクセルが + ゾーン 1(middle)であること. + """ distance_map = numpy.array([[0.0, 0.5, 0.9]], dtype=numpy.float64) zone_map = classify_zones(distance_map, center_threshold=0.33, periphery_threshold=0.66) assert zone_map[0, 1] == 1 # 0.5 → middle @@ -263,7 +264,9 @@ assert result["gradient_magnitude"] == float("inf") def test_gradient_magnitude_formula(self) -> None: - """正常: gradient_magnitude が (center_mean - periphery_mean) / center_mean * 100 の公式に従うこと.""" + """正常: gradient_magnitude が + (center_mean - periphery_mean) / center_mean * 100 の公式に従うこと. + """ luminance = numpy.full((9, 9), 50.0, dtype=numpy.float64) zone_map = numpy.full((9, 9), 2, dtype=numpy.uint8) zone_map[3:6, 3:6] = 0 diff --git a/tests/analysis/test_uniformity.py b/tests/analysis/test_uniformity.py index 51f8781..77b7026 100644 --- a/tests/analysis/test_uniformity.py +++ b/tests/analysis/test_uniformity.py @@ -21,7 +21,6 @@ to_grayscale, ) - # --------------------------------------------------------------------------- # to_grayscale テスト # --------------------------------------------------------------------------- diff --git a/tests/export/test_exporter.py b/tests/export/test_exporter.py index ea841a2..a8b8174 100644 --- a/tests/export/test_exporter.py +++ b/tests/export/test_exporter.py @@ -28,7 +28,6 @@ export_summary, ) - # --------------------------------------------------------------------------- # ensure_output_dirs テスト # --------------------------------------------------------------------------- @@ -354,7 +353,9 @@ "gradient_magnitude", ] for field in base_fields: - assert field in SPATIAL_FIELDNAMES, f"必須フィールド '{field}' が SPATIAL_FIELDNAMES に含まれていない" + assert field in SPATIAL_FIELDNAMES, ( + f"必須フィールド '{field}' が SPATIAL_FIELDNAMES に含まれていない" + ) assert len(SPATIAL_FIELDNAMES) >= len(base_fields) def test_creates_parent_directory_automatically( @@ -420,7 +421,8 @@ def two_image_results(self) -> list[dict]: """テスト用の 2 画像分の解析結果リストを返す fixture. - 仕様変更(動径 min/max 比の追加)に対応し radial_min_max_ratio / radial_min_distance を追加. + 仕様変更(動径 min/max 比の追加)に対応し + radial_min_max_ratio / radial_min_distance を追加. """ return [ { @@ -771,13 +773,21 @@ "max_min_ratio": {"mean": 1.3, "std": 0.1, "min": 1.2, "max": 1.4, "cv": 0.077}, }, "spatial": { - "center_mean": {"mean": 190.0, "std": 10.0, "min": 180.0, "max": 200.0, "cv": 0.053}, - "middle_mean": {"mean": 175.0, "std": 10.0, "min": 165.0, "max": 185.0, "cv": 0.057}, - "periphery_mean": {"mean": 150.0, "std": 10.0, "min": 140.0, "max": 160.0, "cv": 0.067}, + "center_mean": { + "mean": 190.0, "std": 10.0, "min": 180.0, "max": 200.0, "cv": 0.053 + }, + "middle_mean": { + "mean": 175.0, "std": 10.0, "min": 165.0, "max": 185.0, "cv": 0.057 + }, + "periphery_mean": { + "mean": 150.0, "std": 10.0, "min": 140.0, "max": 160.0, "cv": 0.067 + }, "center_periphery_ratio": { "mean": 1.268, "std": 0.018, "min": 1.25, "max": 1.286, "cv": 0.014 }, - "gradient_magnitude": {"mean": 21.1, "std": 1.1, "min": 20.0, "max": 22.2, "cv": 0.052}, + "gradient_magnitude": { + "mean": 21.1, "std": 1.1, "min": 20.0, "max": 22.2, "cv": 0.052 + }, }, } @@ -903,7 +913,8 @@ uniformity_names = set(sample_stats["uniformity"].keys()) for row in rows[:uniformity_count]: assert row["statistic"] in uniformity_names, ( - f"先頭 {uniformity_count} 行に uniformity 以外の指標が含まれている: {row['statistic']}" + f"先頭 {uniformity_count} 行に uniformity 以外の指標が含まれている: " + f"{row['statistic']}" ) spatial_names = set(sample_stats["spatial"].keys()) @@ -1180,7 +1191,9 @@ image_names: list[str], tmp_path: Path, ) -> None: - """正常: ペアワイズスコア行の image_a, image_b が画像名(インデックスではなく)になること.""" + """正常: ペアワイズスコア行の image_a, image_b が + 画像名(インデックスではなく)になること. + """ output_path = str(tmp_path / "ssim_summary.csv") export_ssim_summary(registered_ssim_results, image_names, output_path) diff --git a/tests/io/test_dng_loader.py b/tests/io/test_dng_loader.py index 094c4f1..11ba755 100644 --- a/tests/io/test_dng_loader.py +++ b/tests/io/test_dng_loader.py @@ -18,7 +18,6 @@ from pathlib import Path import numpy -import numpy as np import pytest # サンプル DNG パス(.gitignore 対象のため不在環境では実 DNG テストをスキップ) @@ -426,7 +425,6 @@ from src.io.dng_loader import load_image_dng rgb = load_image_dng(str(_SAMPLE_DNG_PATH), output_format="rgb_float32") - mean_r = float(numpy.mean(rgb[:, :, 0])) mean_g = float(numpy.mean(rgb[:, :, 1])) mean_b = float(numpy.mean(rgb[:, :, 2])) diff --git a/tests/io/test_loader.py b/tests/io/test_loader.py index cd44e80..be590d0 100644 --- a/tests/io/test_loader.py +++ b/tests/io/test_loader.py @@ -12,7 +12,6 @@ """ import re -import tempfile from pathlib import Path import cv2 @@ -21,7 +20,6 @@ from src.io.loader import extract_roi, load_image - # --------------------------------------------------------------------------- # ヘルパー: テスト用 PNG を一時ファイルとして生成する # --------------------------------------------------------------------------- diff --git a/tests/scripts/test_run_uniformity.py b/tests/scripts/test_run_uniformity.py index 9bf3e05..c0550cb 100644 --- a/tests/scripts/test_run_uniformity.py +++ b/tests/scripts/test_run_uniformity.py @@ -12,16 +12,15 @@ """ import json +import sys from pathlib import Path import pytest -import sys sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent)) from src.config import load_roi_config - # --------------------------------------------------------------------------- # load_roi_config テスト # --------------------------------------------------------------------------- diff --git a/tests/scripts/test_script_entrypoints.py b/tests/scripts/test_script_entrypoints.py new file mode 100644 index 0000000..0b53ebb --- /dev/null +++ b/tests/scripts/test_script_entrypoints.py @@ -0,0 +1,79 @@ +"""scripts/ 配下スクリプトの起動スモークテスト. + +目的: + - 各スクリプトはプロジェクトルートを自前で sys.path に追加し, + リポジトリ外のカレントディレクトリからでも実行できること(SPEC_01 の scripts/ 実行形態) + - import 整理(ruff の I001 自動修正等)で `from src...` が sys.path 追加より前へ + 移動するような回帰を検出する + +方針: + - argparse を持つスクリプトは `--help` で起動し終了コード 0 を確認する + - argparse を持たないスクリプトはモジュールとして読み込めること(import 成功)のみ確認する + - viewer.py は streamlit アプリのため対象外 + - いずれもサブプロセスで実行し,cwd はリポジトリ外(tmp_path),PYTHONPATH は除去する +""" + +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent +SCRIPTS_DIR = PROJECT_ROOT / "scripts" +EXCLUDED_SCRIPTS = {"viewer.py", "__init__.py"} +SCRIPT_PATHS = sorted( + path for path in SCRIPTS_DIR.glob("*.py") if path.name not in EXCLUDED_SCRIPTS +) +TIMEOUT_SEC = 120 + +IMPORT_SNIPPET = ( + "import importlib.util, sys\n" + "spec = importlib.util.spec_from_file_location('smoke_target', sys.argv[1])\n" + "module = importlib.util.module_from_spec(spec)\n" + "spec.loader.exec_module(module)\n" +) + + +def _clean_env() -> dict[str, str]: + """リポジトリ由来の import 経路を持ち込まないよう PYTHONPATH を除いた環境変数を返す.""" + env = {key: value for key, value in os.environ.items() if key != "PYTHONPATH"} + env["MPLBACKEND"] = "Agg" + return env + + +def _uses_argparse(script_path: Path) -> bool: + return "argparse" in script_path.read_text(encoding="utf-8") + + +def test_scripts_are_discovered() -> None: + """対象スクリプトが 1 件以上見つかること(glob の設定ミスで全件スキップされるのを防ぐ).""" + assert len(SCRIPT_PATHS) > 0 + + +@pytest.mark.parametrize("script_path", SCRIPT_PATHS, ids=lambda path: path.name) +def test_script_starts_from_outside_repository(script_path: Path, tmp_path: Path) -> None: + """リポジトリ外の cwd から起動(--help)または import できること.""" + if _uses_argparse(script_path): + command = [sys.executable, str(script_path), "--help"] + else: + command = [sys.executable, "-c", IMPORT_SNIPPET, str(script_path)] + + result = subprocess.run( + command, + cwd=tmp_path, + env=_clean_env(), + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=TIMEOUT_SEC, + ) + + assert result.returncode == 0, ( + f"{script_path.name} failed (returncode={result.returncode})\n" + f"stderr:\n{result.stderr}" + ) + if _uses_argparse(script_path): + assert "usage" in result.stdout.lower() diff --git a/tests/scripts/test_select_roi.py b/tests/scripts/test_select_roi.py index 98cc2d6..463609a 100644 --- a/tests/scripts/test_select_roi.py +++ b/tests/scripts/test_select_roi.py @@ -12,13 +12,12 @@ """ import json +import sys from pathlib import Path import pytest # スクリプトを直接インポートするため sys.path を調整 -import sys - sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent)) from scripts.select_roi import ( @@ -30,7 +29,6 @@ set_nested, ) - # --------------------------------------------------------------------------- # calc_display_scale テスト # --------------------------------------------------------------------------- diff --git a/tests/scripts/test_viewer.py b/tests/scripts/test_viewer.py index b6c5bb8..727bd25 100644 --- a/tests/scripts/test_viewer.py +++ b/tests/scripts/test_viewer.py @@ -22,10 +22,8 @@ - 合成画像(PIL.Image)でテストデータを生成する """ -import importlib import json import sys -import types from pathlib import Path import numpy @@ -65,7 +63,6 @@ """異常: CLAUDE.md が存在しないディレクトリ構造では FileNotFoundError を送出すること.""" # viewer モジュールを tmp_path 配下に疑似的に配置させるため、 # _find_project_root の __file__ を tmp_path 配下に差し替える - from scripts import viewer as viewer_module # 一時ディレクトリ構造(CLAUDE.md なし)で疑似モジュールを作成 fake_script_path = tmp_path / "scripts" / "viewer.py" @@ -432,7 +429,9 @@ def test_error_message_contains_run_uniformity_hint( self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """異常: FileNotFoundError のメッセージに run_uniformity.py の実行を促す案内が含まれること.""" + """異常: FileNotFoundError のメッセージに + run_uniformity.py の実行を促す案内が含まれること. + """ import scripts.viewer as viewer_module monkeypatch.setattr( @@ -521,9 +520,10 @@ self, sample_df: "pandas.DataFrame", sample_spatial_df: "pandas.DataFrame" ) -> None: """正常: render_tab_overview が df と spatial_df の 2 引数を受け付けること.""" - from scripts.viewer import render_tab_overview import inspect + from scripts.viewer import render_tab_overview + sig = inspect.signature(render_tab_overview) params = list(sig.parameters.keys()) assert "df" in params @@ -531,9 +531,10 @@ def test_render_tab_overview_spatial_df_has_default_or_nullable(self) -> None: """正常: render_tab_overview の spatial_df 引数が None を受け付けること(型ヒント確認).""" - from scripts.viewer import render_tab_overview import inspect + from scripts.viewer import render_tab_overview + sig = inspect.signature(render_tab_overview) # spatial_df パラメータのアノテーションに None が含まれることを確認 spatial_df_param = sig.parameters.get("spatial_df") @@ -560,9 +561,10 @@ def test_render_tab_individual_accepts_two_arguments(self) -> None: """正常: render_tab_individual が df と spatial_df の 2 引数を受け付けること.""" - from scripts.viewer import render_tab_individual import inspect + from scripts.viewer import render_tab_individual + sig = inspect.signature(render_tab_individual) params = list(sig.parameters.keys()) assert "df" in params, "df パラメータが存在しない" @@ -570,9 +572,10 @@ def test_render_tab_individual_has_exactly_two_parameters(self) -> None: """正常: render_tab_individual のパラメータがちょうど 2 個であること.""" - from scripts.viewer import render_tab_individual import inspect + from scripts.viewer import render_tab_individual + sig = inspect.signature(render_tab_individual) params = list(sig.parameters.keys()) assert len(params) == 2, ( @@ -580,10 +583,13 @@ ) def test_render_tab_individual_spatial_df_is_nullable(self) -> None: - """正常: render_tab_individual の spatial_df 引数が None を受け付けること(型ヒント確認).""" - from scripts.viewer import render_tab_individual + """正常: render_tab_individual の spatial_df 引数が None を受け付けること + (型ヒント確認). + """ import inspect + from scripts.viewer import render_tab_individual + sig = inspect.signature(render_tab_individual) spatial_df_param = sig.parameters.get("spatial_df") assert spatial_df_param is not None, "spatial_df パラメータが存在しない" @@ -595,27 +601,30 @@ def test_render_tab_individual_first_param_is_df(self) -> None: """正常: render_tab_individual の第1引数が df であること.""" - from scripts.viewer import render_tab_individual import inspect + from scripts.viewer import render_tab_individual + sig = inspect.signature(render_tab_individual) params = list(sig.parameters.keys()) assert params[0] == "df", f"第1引数が df でない: {params[0]}" def test_render_tab_individual_second_param_is_spatial_df(self) -> None: """正常: render_tab_individual の第2引数が spatial_df であること.""" - from scripts.viewer import render_tab_individual import inspect + from scripts.viewer import render_tab_individual + sig = inspect.signature(render_tab_individual) params = list(sig.parameters.keys()) assert params[1] == "spatial_df", f"第2引数が spatial_df でない: {params[1]}" def test_render_tab_individual_df_annotation_is_dataframe(self) -> None: """正常: render_tab_individual の df 引数が DataFrame の型ヒントを持つこと.""" - from scripts.viewer import render_tab_individual import inspect + from scripts.viewer import render_tab_individual + sig = inspect.signature(render_tab_individual) df_param = sig.parameters.get("df") assert df_param is not None, "df パラメータが存在しない" diff --git a/tests/visualization/test_plotter.py b/tests/visualization/test_plotter.py index 0ff5630..b5d1a6f 100644 --- a/tests/visualization/test_plotter.py +++ b/tests/visualization/test_plotter.py @@ -12,6 +12,7 @@ """ import matplotlib + matplotlib.use("Agg") # GUI なし環境でも動作するバックエンドを設定 from pathlib import Path