"""エントリポイントのコマンドライン引数の検証(ENV_04「起動」「デバッグ用」).
対象は `tiasjudge.__main__` の引数解析と,`--preview-result` 用のダミー診断結果の
組み立て(デバッグ専用.診断・SVM 学習は行わない).
- `--check`: 画面を構築して即終了する(既存の起動確認は `test_package_skeleton.py`)
- `--preview-result`: 結果画面だけをダミー結果で開く
- `--failed`: `--preview-result` と併用し,診断失敗パターン(旧実装の失敗時スコア組)で開く
- `--english`: `--preview-result` と併用し,英語表示で開く(結果画面は表示専用になり
画面内で言語を切り替えられないため,プレビューの言語はコマンドラインで指定する)
※ testing.md の方針により GUI(QWidget)そのものの自動テストは書かない.ここで画面を
生成するのは `--check` による**起動可能性の確認**に限る(Qt は conftest.py で offscreen).
"""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
import pytest
from tiasjudge.__main__ import (
PREVIEW_FAILED_TOP_LABELS,
PREVIEW_IMAGE_RGB,
PREVIEW_IMAGE_SIZE,
PREVIEW_PERCENTAGES,
PREVIEW_TOP_LABELS,
_build_parser,
_preview_image,
_preview_result,
_run_preview,
main,
)
from tiasjudge.core.csv_io import EVALUATION_COLUMN_COUNT
from tiasjudge.core.svm import COLOR_KEYS, LEGACY_FAILURE_PERCENTAGES
from tiasjudge.gui.result_window import DISPLAY_ORDER, HIGHLIGHT_COUNT, Language, build_result_rows
# --- 引数解析 ---
OPTION_FLAGS = ("check", "preview_result", "failed", "english")
class TestArgumentParser:
"""`--check` / `--preview-result` / `--failed` / `--english` の 4 オプション(ENV_04)."""
def test_no_options_by_default(self) -> None:
arguments = _build_parser().parse_args([])
flags = tuple(getattr(arguments, name) for name in OPTION_FLAGS)
assert flags == (False, False, False, False)
@pytest.mark.parametrize(
("argv", "expected"),
[
(["--check"], ("check",)),
(["--preview-result"], ("preview_result",)),
(["--preview-result", "--failed"], ("preview_result", "failed")),
(["--failed", "--preview-result", "--check"], ("preview_result", "failed", "check")),
(["--preview-result", "--english"], ("preview_result", "english")),
(
["--preview-result", "--english", "--check"],
("preview_result", "english", "check"),
),
(
["--preview-result", "--english", "--failed"],
("preview_result", "english", "failed"),
),
],
)
def test_flags_are_parsed(self, argv: list[str], expected: tuple[str, ...]) -> None:
arguments = _build_parser().parse_args(argv)
for name in OPTION_FLAGS:
assert getattr(arguments, name) is (name in expected), name
def test_unknown_option_is_rejected(self) -> None:
"""未知のオプションはエラー終了する(黙って無視しない)."""
with pytest.raises(SystemExit) as error:
_build_parser().parse_args(["--unknown"])
assert error.value.code == 2
@pytest.mark.parametrize("option", ["--check", "--preview-result", "--failed", "--english"])
def test_help_lists_the_option(self, option: str) -> None:
assert option in _build_parser().format_help()
# --- プレビュー用のダミーデータ(デバッグ専用) ---
class TestPreviewImage:
"""ダミー画像(舌画像の代わり)."""
def test_shape_and_dtype(self) -> None:
image = _preview_image()
assert image.shape == (PREVIEW_IMAGE_SIZE, PREVIEW_IMAGE_SIZE, 3)
assert image.dtype.name == "uint8"
def test_is_stored_in_bgr_order(self) -> None:
"""結果画面は BGR 前提で受け取るため,BGR 並びで埋める."""
red, green, blue = PREVIEW_IMAGE_RGB
assert tuple(_preview_image()[0, 0]) == (blue, green, red)
class TestPreviewResult:
"""ダミー診断結果が `DiagnosisResult` として妥当であること."""
@pytest.mark.parametrize("failed", [False, True])
def test_scores_cover_all_color_keys(self, failed: bool) -> None:
result = _preview_result(failed=failed)
assert set(result.scores) == set(COLOR_KEYS)
assert set(result.percentages) == set(COLOR_KEYS)
@pytest.mark.parametrize("failed", [False, True])
def test_scores_are_percentages_divided_by_100(self, failed: bool) -> None:
result = _preview_result(failed=failed)
for key, percent in result.percentages.items():
assert result.scores[key] == pytest.approx(percent / 100)
assert 0.0 <= result.scores[key] <= 1.0
@pytest.mark.parametrize("failed", [False, True])
def test_top_labels_are_two_known_colors(self, failed: bool) -> None:
result = _preview_result(failed=failed)
assert len(result.top_labels) == HIGHLIGHT_COUNT
assert len(set(result.top_labels)) == HIGHLIGHT_COUNT
assert set(result.top_labels) <= set(COLOR_KEYS)
@pytest.mark.parametrize("failed", [False, True])
def test_top_labels_are_the_two_highest_scores(self, failed: bool) -> None:
"""上位 2 色はスコア降順の先頭 2 件(表示と矛盾しないダミーであること)."""
result = _preview_result(failed=failed)
ranked = sorted(result.percentages, key=lambda key: result.percentages[key], reverse=True)
assert set(result.top_labels) == set(ranked[:HIGHLIGHT_COUNT])
@pytest.mark.parametrize("failed", [False, True])
def test_predictions_have_the_full_column_count(self, failed: bool) -> None:
result = _preview_result(failed=failed)
assert result.predictions.shape == (EVALUATION_COLUMN_COUNT,)
def test_success_pattern(self) -> None:
"""成功パターンは `PREVIEW_PERCENTAGES`・失敗フラグなし(ENV_04「デバッグ用」)."""
result = _preview_result(failed=False)
assert result.percentages == PREVIEW_PERCENTAGES
assert result.top_labels == PREVIEW_TOP_LABELS
assert result.failed is False
assert result.failure_reason is None
def test_success_pattern_totals_100_percent(self) -> None:
assert sum(PREVIEW_PERCENTAGES.values()) == 100
def test_failed_pattern_uses_the_legacy_failure_scores(self) -> None:
"""失敗パターンは旧実装の失敗時スコア組(`LEGACY_FAILURE_PERCENTAGES`)."""
result = _preview_result(failed=True)
assert result.percentages == LEGACY_FAILURE_PERCENTAGES
assert result.top_labels == PREVIEW_FAILED_TOP_LABELS
assert result.failed is True
assert result.failure_reason is not None
def test_failed_top_labels_follow_the_legacy_scores(self) -> None:
"""失敗パターンの上位 2 色は旧スコア組を降順に並べた先頭 2 件(紫・淡紅)."""
ranked = sorted(
LEGACY_FAILURE_PERCENTAGES,
key=lambda key: LEGACY_FAILURE_PERCENTAGES[key],
reverse=True,
)
assert set(PREVIEW_FAILED_TOP_LABELS) == set(ranked[:HIGHLIGHT_COUNT])
def test_does_not_share_the_legacy_score_dictionary(self) -> None:
"""モジュール定数を書き換えない(コピーを持つ)."""
result = _preview_result(failed=True)
assert result.percentages is not LEGACY_FAILURE_PERCENTAGES
result = _preview_result(failed=False)
assert result.percentages is not PREVIEW_PERCENTAGES
@pytest.mark.parametrize("failed", [False, True])
@pytest.mark.parametrize("language", list(Language))
def test_rows_can_be_built_from_the_dummy_result(
self, failed: bool, language: Language
) -> None:
"""ダミー結果から結果画面の 5 行を組み立てられる(表示可能な形であること)."""
rows = build_result_rows(_preview_result(failed=failed), language)
assert tuple(row.key for row in rows) == DISPLAY_ORDER
assert len({row.key for row in rows if row.highlighted}) == HIGHLIGHT_COUNT
# --- 起動(ENV_04「デバッグ用」) ---
class TestPreviewLaunch:
"""`--preview-result` の起動確認(`--check` で画面を構築して即終了する)."""
@pytest.mark.parametrize(
"argv",
[
["--check"],
["--preview-result", "--check"],
["--preview-result", "--failed", "--check"],
["--preview-result", "--english", "--check"],
["--preview-result", "--english", "--failed", "--check"],
],
)
def test_check_exits_successfully(self, argv: list[str]) -> None:
assert main(argv) == 0
def test_preview_does_not_build_the_main_window(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""プレビューはメイン画面を作らない(結果画面だけを開く)."""
import tiasjudge.gui.main_window as main_window
def fail(*args: object, **kwargs: object) -> None:
raise AssertionError("プレビューではメイン画面を作ってはならない")
monkeypatch.setattr(main_window, "MainWindow", fail)
assert main(["--preview-result", "--check"]) == 0
@pytest.mark.parametrize(
("options", "expected_language"),
[([], Language.JAPANESE), (["--english"], Language.ENGLISH)],
ids=["default_japanese", "english_option"],
)
@pytest.mark.parametrize("failed", [False, True])
def test_preview_opens_the_result_window_with_the_requested_language(
self,
monkeypatch: pytest.MonkeyPatch,
failed: bool,
options: list[str],
expected_language: Language,
) -> None:
"""結果画面にダミー結果・ダミー画像・指定言語が渡される(ENV_04「デバッグ用」).
既定は日本語,`--english` を付けると英語で開く(`--failed` との併用も可).
"""
import tiasjudge.gui.result_window as result_window
captured: dict[str, object] = {}
class FakeResultWindow:
def __init__(self, result: object, image: object, language: object) -> None:
captured.update(result=result, image=image, language=language)
def close(self) -> None:
captured["closed"] = True
monkeypatch.setattr(result_window, "ResultWindow", FakeResultWindow)
argv = ["--preview-result", "--check", *options] + (["--failed"] if failed else [])
assert main(argv) == 0
assert captured["language"] is expected_language
assert captured["closed"] is True
result = captured["result"]
assert result.failed is failed # type: ignore[union-attr]
assert result.percentages == _preview_result(failed=failed).percentages # type: ignore[union-attr]
image = captured["image"]
assert image.shape == (PREVIEW_IMAGE_SIZE, PREVIEW_IMAGE_SIZE, 3) # type: ignore[union-attr]
def test_module_invocation_with_preview_exits_successfully(
self, repo_root: Path, headless_env: dict[str, str]
) -> None:
"""`python -m tiasjudge --preview-result --check` が正常終了する(ENV_04 の記載どおり)."""
result = subprocess.run(
[sys.executable, "-m", "tiasjudge", "--preview-result", "--check"],
cwd=repo_root,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
env=headless_env,
timeout=120,
)
assert result.returncode == 0, result.stderr
# --- 表示先モニタ(プレビュー) ---
class TestRunPreview:
"""`_run_preview`(`--preview-result` の本体)のモニタ選択・終了コード.
※ `QApplication` はスタブを渡す(イベントループを回さずに検証するため).
画面の見た目は人間が確認する(testing.md「GUI の自動テストは書かない」).
"""
class FakeApp:
"""`QApplication` の代わり(`screens` / `primaryScreen` / `exec` のみ)."""
def __init__(self, screens: list[object], exit_code: int = 0) -> None:
self._screens = screens
self._exit_code = exit_code
self.exec_called = False
def screens(self) -> list[object]:
return self._screens
def primaryScreen(self) -> object | None: # noqa: N802 - Qt の API 名に合わせる
return self._screens[0] if self._screens else None
def exec(self) -> int:
self.exec_called = True
return self._exit_code
@pytest.fixture
def fake_result_window(self, monkeypatch: pytest.MonkeyPatch) -> dict[str, object]:
"""`ResultWindow` をスタブに差し替え,生成・close を記録する."""
import tiasjudge.gui.result_window as result_window
captured: dict[str, object] = {"closed": False}
class FakeResultWindow:
def __init__(self, result: object, image: object, language: object) -> None:
captured.update(result=result, image=image, language=language, window=self)
def close(self) -> None:
captured["closed"] = True
monkeypatch.setattr(result_window, "ResultWindow", FakeResultWindow)
return captured
def test_check_returns_zero_without_selecting_a_screen(
self, monkeypatch: pytest.MonkeyPatch, fake_result_window: dict[str, object]
) -> None:
"""`--check` 併用時は画面を構築して閉じるだけ(最大化もイベントループも行わない)."""
import tiasjudge.gui.result_window as result_window
def fail(*args: object, **kwargs: object) -> bool:
raise AssertionError("--check では表示先モニタを選んではならない")
monkeypatch.setattr(result_window, "show_on_secondary_screen", fail)
app = TestRunPreview.FakeApp([object()])
assert _run_preview(app, failed=False, check=True) == 0 # type: ignore[arg-type]
assert fake_result_window["closed"] is True
assert app.exec_called is False
def test_uses_the_primary_screen_as_the_reference(
self, monkeypatch: pytest.MonkeyPatch, fake_result_window: dict[str, object]
) -> None:
"""プレビューにはメイン画面が無いため,基準はプライマリモニタ."""
import tiasjudge.gui.result_window as result_window
captured: dict[str, object] = {}
def record(window: object, screens: object, reference: object) -> bool:
captured.update(window=window, screens=screens, reference=reference)
return True
monkeypatch.setattr(result_window, "show_on_secondary_screen", record)
screens = [object(), object()]
app = TestRunPreview.FakeApp(screens, exit_code=0)
assert _run_preview(app, failed=False, check=False) == 0 # type: ignore[arg-type]
assert captured["window"] is fake_result_window["window"]
assert list(captured["screens"]) == screens # type: ignore[call-overload]
assert captured["reference"] is screens[0]
assert app.exec_called is True
def test_returns_the_event_loop_exit_code(
self, monkeypatch: pytest.MonkeyPatch, fake_result_window: dict[str, object]
) -> None:
"""イベントループの終了コードをそのまま返す."""
import tiasjudge.gui.result_window as result_window
monkeypatch.setattr(
result_window, "show_on_secondary_screen", lambda *args, **kwargs: False
)
app = TestRunPreview.FakeApp([object()], exit_code=3)
assert _run_preview(app, failed=True, check=False) == 3 # type: ignore[arg-type]
assert fake_result_window["language"] is Language.JAPANESE
assert fake_result_window["result"].failed is True # type: ignore[union-attr]
def test_language_defaults_to_japanese(self, fake_result_window: dict[str, object]) -> None:
"""`english` を省略した呼び出しは日本語(既定値)."""
app = TestRunPreview.FakeApp([object()])
assert _run_preview(app, failed=False, check=True) == 0 # type: ignore[arg-type]
assert fake_result_window["language"] is Language.JAPANESE
@pytest.mark.parametrize(
("english", "expected_language"),
[(False, Language.JAPANESE), (True, Language.ENGLISH)],
)
def test_english_option_selects_the_language(
self,
fake_result_window: dict[str, object],
english: bool,
expected_language: Language,
) -> None:
"""結果画面は表示専用のため,プレビューの言語は `--english` で決まる."""
app = TestRunPreview.FakeApp([object()])
assert _run_preview(app, failed=False, english=english, check=True) == 0 # type: ignore[arg-type]
assert fake_result_window["language"] is expected_language