Newer
Older
SmTIAS-Evaluation / tests / scripts / test_script_entrypoints.py
"""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()