"""run_selftest.py のテスト(python -m unittest scripts/verify/test_run_selftest.py).
仕様(ENV_04「セルフテスト」・.claude/verify-profile.md):
- ランナーがクリーンするのは Debug 出力(TIASshot/ と TIASshot.Tests/ の bin/x64/Debug・obj/x64/Debug)のみ.
bin/x64/Release 等ほかの構成の出力は消さない
- --full で packages の退避(改名)自体に失敗した場合は packages を変更せずに終了コード 2 で止まる
実ビルド・実行は行わず,一時ディレクトリ上で検証する.
"""
from __future__ import annotations
import importlib.util
import shutil
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
_HERE = Path(__file__).resolve().parent
_spec = importlib.util.spec_from_file_location("run_selftest", _HERE / "run_selftest.py")
rs = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(rs)
class CleanDirsTest(unittest.TestCase):
def test_clean_dirs_are_exactly_debug_x64_outputs_of_both_projects(self):
rel = sorted(str(p.relative_to(rs.REPO)).replace("\\", "/") for p in rs.CLEAN_DIRS)
self.assertEqual(rel, sorted([
"TIASshot/bin/x64/Debug", "TIASshot/obj/x64/Debug",
"TIASshot.Tests/bin/x64/Debug", "TIASshot.Tests/obj/x64/Debug",
]))
def test_clean_removes_only_debug_outputs(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
keep = [
root / "TIASshot" / "bin" / "x64" / "Release" / "TIASshot.exe",
root / "TIASshot" / "obj" / "x64" / "Release" / "a.cache",
root / "TIASshot.Tests" / "bin" / "x64" / "Release" / "t.dll",
root / "TIASshot" / "bin" / "Debug" / "anycpu.exe",
root / "TIASshot" / "Program.cs",
root / "packages" / "p.nupkg",
]
remove = [root / p.relative_to(rs.REPO) / "x.dll" for p in rs.CLEAN_DIRS]
for f in keep + remove:
f.parent.mkdir(parents=True, exist_ok=True)
f.write_text("x")
with mock.patch.object(rs, "CLEAN_DIRS", [root / p.relative_to(rs.REPO) for p in rs.CLEAN_DIRS]):
rs.clean_build_outputs()
for f in keep:
self.assertTrue(f.exists(), f"消してはいけない: {f}")
for f in remove:
self.assertFalse(f.parent.exists(), f"消すべき: {f.parent}")
def test_clean_with_missing_dirs_is_noop(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
with mock.patch.object(rs, "CLEAN_DIRS", [root / p.relative_to(rs.REPO) for p in rs.CLEAN_DIRS]):
rs.clean_build_outputs() # 例外にならない
def test_clean_failure_is_precondition_error(self):
with tempfile.TemporaryDirectory() as tmp:
d = Path(tmp) / "TIASshot" / "bin" / "x64" / "Debug"
d.mkdir(parents=True)
with mock.patch.object(rs, "CLEAN_DIRS", [d]), \
mock.patch.object(rs.shutil, "rmtree", side_effect=PermissionError("in use")):
with self.assertRaises(rs.PreconditionError):
rs.clean_build_outputs()
class PackagesRenameFailureTest(unittest.TestCase):
def setUp(self):
self._tmp = tempfile.mkdtemp()
self.root = Path(self._tmp)
self.packages = self.root / "packages"
(self.packages / "OpenCvSharp4").mkdir(parents=True)
(self.packages / "OpenCvSharp4" / "x.nupkg").write_text("x")
vs = self.root / "vs"
msbuild = vs / "MSBuild" / "Current" / "Bin" / "MSBuild.exe"
msbuild.parent.mkdir(parents=True)
msbuild.write_text("")
self.vs = vs
def tearDown(self):
shutil.rmtree(self._tmp, ignore_errors=True)
def _run_main(self, argv):
real_rename = Path.rename
def failing_rename(self_path, target):
if Path(self_path).name == "packages":
raise PermissionError("使用中")
return real_rename(self_path, target)
run_logged = mock.Mock(return_value=0)
with mock.patch.object(rs, "REPO", self.root), \
mock.patch.object(rs, "SLN", self.root / "TIASshot.sln"), \
mock.patch.object(rs, "find_vs", return_value=self.vs), \
mock.patch.object(rs, "find_nuget", return_value="nuget"), \
mock.patch.object(rs, "run_logged", run_logged), \
mock.patch.object(rs.subprocess, "run", side_effect=AssertionError("実行してはいけない")), \
mock.patch.object(Path, "rename", failing_rename), \
mock.patch.object(sys, "argv", ["run_selftest.py"] + argv), \
mock.patch("sys.stdout", new_callable=_Sink):
code = rs.main()
return code, run_logged
def test_rename_failure_exits_2_and_keeps_packages(self):
code, run_logged = self._run_main(["--full", "--no-clean"])
self.assertEqual(code, rs.EXIT_PRECONDITION)
self.assertEqual(code, 2)
self.assertTrue((self.packages / "OpenCvSharp4" / "x.nupkg").exists(), "packages は変更しない")
self.assertEqual(list(self.root.glob("packages.bak-*")), [], "退避先を作らない")
run_logged.assert_not_called() # restore・ビルドに進まない
def test_rename_failure_summary_is_precondition(self):
import json
self._run_main(["--full", "--no-clean"])
summaries = list((self.root / ".verify" / "selftest").glob("run-*/summary.json"))
self.assertEqual(len(summaries), 1)
data = json.loads(summaries[0].read_text(encoding="utf-8"))
self.assertEqual(data["result"], "PRECONDITION")
self.assertEqual(data["steps"][-1]["name"], "precondition")
class _Sink:
"""print の出力を捨てる(reconfigure を持たない stdout の代わり)."""
def write(self, s):
return len(s)
def flush(self):
pass
if __name__ == "__main__":
unittest.main()