"""종류가 요구하는 내용이 채워졌는지 보는 검사기. 고정 사례는 다섯 종류마다 둘이다 — 채운 것과 하나를 뺀 것. 「무조건 통과」도 「무조건 거절」도 아닌 것을 이 짝이 확인한다. """ import importlib.util import os import unittest ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) FIXTURES = os.path.join(ROOT, "scripts", "tests", "fixtures", "required-content") _spec = importlib.util.spec_from_file_location( "check_required_content", os.path.join(ROOT, "scripts", "check-required-content.py")) crc = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(crc) KINDS = ("case", "concept", "reference", "question", "decision") def _report(sub, name): rep = crc.techlog.Report("fixture") crc.check_record(os.path.join(FIXTURES, sub, f"{name}.md"), rep) return rep class RequiredContentTest(unittest.TestCase): def test_every_kind_passes_when_filled(self): for kind in KINDS: with self.subTest(kind=kind): rep = _report("ok", kind) self.assertEqual(0, rep.error_count, f"{kind}: {dict(rep.errors)}") def test_every_kind_fails_when_a_required_part_is_missing(self): for kind in KINDS: with self.subTest(kind=kind): rep = _report("missing", kind) self.assertGreater(rep.error_count, 0, f"{kind} 의 누락 사례가 통과했다") def test_the_missing_part_is_named(self): """무엇이 빠졌는지 말한다. 「어딘가 잘못됐다」로 끝나지 않는다.""" expected = { "case": "결론", "concept": "basisVersion", "reference": "적용 조건", "question": "다음 검증", "decision": "판단 이유", } for kind, part in expected.items(): with self.subTest(kind=kind): rules = " / ".join(_report("missing", kind).errors) self.assertIn(part, rules) def test_decision_kind_is_project_decision_in_frontmatter(self): """decision/ 폴더의 기록은 kind: PROJECT_DECISION 이다 (templates/decision.md).""" self.assertEqual("decision", crc.KIND_ALIASES["PROJECT_DECISION"]) rep = _report("ok", "decision") self.assertNotIn("kind 를 모르겠다", rep.errors) def test_an_unanswered_question_is_not_an_error(self): """답이 없는 QUESTION 자체는 결함이 아니다. 답을 구할 방법이 없는 것이 결함이다.""" rep = _report("ok", "question") self.assertEqual(0, rep.error_count) def test_body_markers_belong_only_to_case_and_concept(self): self.assertEqual({"case", "concept"}, crc.BODY_KINDS) def _cli(self, *args): import subprocess return subprocess.run( ["python3", os.path.join(ROOT, "scripts", "check-required-content.py"), *args], cwd=ROOT, capture_output=True, text=True) def test_a_missing_project_is_not_reported_as_clean(self): """대상이 없으면 통과가 아니다. 오타 하나로 관문이 무효가 되면 안 된다.""" p = self._cli("nonexistent-project") self.assertEqual(2, p.returncode) self.assertIn("대상이 성립하지 않는다", p.stderr) def test_a_project_without_a_contract_does_not_come_back_green(self): """계약이 없으면 「볼 것이 없어서 통과」다. 그것을 초록으로 내지 않는다.""" p = self._cli("ca-tmpl") self.assertEqual(2, p.returncode) self.assertIn("tech-log-tree.json 이 없다", p.stderr) def test_a_contract_with_no_records_yet_is_not_an_error(self): """아직 안 쓴 것은 결함이 아니다. 다만 초록으로 보이면 안 된다.""" p = self._cli("keycloak-session-store") self.assertEqual(0, p.returncode) self.assertIn("아직 안 쓰였다", p.stdout) def test_one_ungrounded_target_stops_the_whole_run(self): """성립하는 것과 안 하는 것을 함께 주면 통과로 뭉개지 않는다. 성립하는 쪽으로 `keycloak` 을 쓴다. `verify-pipeline.py` 의 계약이 scripts/ 안에 저장소 체크아웃 이름을 적는 것을 금지해서(`FORBIDDEN_LITERAL`), 그 이름과 같은 프로젝트를 테스트에 적으면 계약 검사가 깨진다. """ p = self._cli("keycloak", "ca-tmpl") self.assertEqual(2, p.returncode) if __name__ == "__main__": unittest.main()