Files
TheRock/rocm-systems/tools/systems_pr_bot/test_policy_check_ut.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

476 lines
18 KiB
Python

"""
Unit + integration tests for policy_check.py.
These let us iterate on policies WITHOUT pushing branches or running workflows:
• Unit tests — exercise individual validators / regex patterns.
• Integration — feed blobs of [title, description, files] through
the higher-level ensure_* functions.
"""
import re
import sys
import unittest
from pathlib import Path
from typing import Any, Dict, List, Optional
# Make `policy_check` importable regardless of the working directory.
THIS_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(THIS_DIR))
import policy_check as pc # noqa: E402
# ----------------------------- helpers ---------------------------------------
_ISSUE_PATTERNS = [
r"(?im)^\s*JIRA\s*ID\s*[:\-]?\s*(#?\d+|[A-Z][A-Z0-9]+-\d+|https?:\/\/\S+)",
r"(?im)^\s*ISSUE\s*ID\s*[:\-]?\s*(#?\d+|[A-Z][A-Z0-9]+-\d+|https?:\/\/\S+)",
# JIRA/ISSUE ID on a separate line (blank lines + trailing spaces allowed).
r"(?im)^[ \t]*JIRA[ \t]+ID[ \t]*\r?\n[ \t\r\n]*([A-Z][A-Z0-9]+-\d+)",
r"(?im)^[ \t]*ISSUE[ \t]+ID[ \t]*\r?\n[ \t\r\n]*([A-Z][A-Z0-9]+-\d+|\d+)",
r"(?im)\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\b\s*:?\s*"
r"(?:[A-Za-z0-9._\-]+\/[A-Za-z0-9._\-]+)?#\d+",
# Bare GitHub issue reference, e.g. #123
r"(?m)(?:^|\s)#\d+\b",
# GitHub issue URL
r"(?i)https?:\/\/github\.com\/[^\/\s]+\/[^\/\s]+\/issues\/\d+",
]
_CHECKLIST_PATTERNS = [
r"(?im)^\s*-\s*\[[xX]\]\s*.*contributing guidelines",
]
def make_policy(**overrides: Any) -> pc.Policy:
"""Build a Policy with sensible defaults; override any field per-test.
Independent of policy.yml so regex/validator behaviour can be pinned even
if the shipped config changes.
"""
defaults: Dict[str, Any] = dict(
title_min_length=10,
title_max_length=80,
description_min_length=30,
description_issue_patterns=[re.compile(p) for p in _ISSUE_PATTERNS],
description_checklist_patterns=[re.compile(p) for p in _CHECKLIST_PATTERNS],
block_draft=True,
forbidden_title_patterns=[re.compile(r"(?i)\bWIP\b")],
forbidden_paths=["**/*.pem", "**/.env", "**/id_rsa"],
unit_test_code_extensions=[".py", ".cpp"],
unit_test_patterns=[
"test_*",
"testing_*",
"*_test.*",
"*_tests.*",
"**/test/gtest/**",
],
unit_test_exempt_paths=[],
bump_bot_authors=["assistant-librarian", "systems-assistant"],
required_checks=["pre-commit"],
precommit_failure_comment=None,
)
defaults.update(overrides)
return pc.Policy(**defaults)
def make_file(
filename: str,
status: str = "modified",
additions: int = 0,
deletions: int = 0,
changes: Optional[int] = None,
) -> Dict[str, Any]:
return {
"filename": filename,
"status": status,
"additions": additions,
"deletions": deletions,
"changes": changes if changes is not None else additions + deletions,
}
# ----------------------------- PR title --------------------------------------
class TitleTests(unittest.TestCase):
def setUp(self) -> None:
self.policy = make_policy()
def _errs(self, title: str) -> List[str]:
e: List[str] = []
pc.ensure_pr_title(self.policy, title, e)
return e
def test_valid_title(self) -> None:
self.assertEqual(self._errs("feat(auth): add token refresh support"), [])
def test_too_short(self) -> None:
self.assertTrue(any("too short" in x for x in self._errs("fix: a")))
def test_too_long(self) -> None:
long_title = "feat: " + ("x" * 90)
self.assertTrue(any("too long" in x for x in self._errs(long_title)))
def test_forbidden_word(self) -> None:
self.assertTrue(
any("forbidden" in x.lower() for x in self._errs("feat: WIP add things"))
)
# ----------------------------- PR description --------------------------------
class DescriptionTests(unittest.TestCase):
def test_too_short(self) -> None:
policy = make_policy()
e: List[str] = []
pc.ensure_pr_description(policy, "short", e)
self.assertTrue(any("too short" in x for x in e))
def test_missing_issue_reference(self) -> None:
# No checklist patterns so only the reference check fires.
policy = make_policy(description_checklist_patterns=[])
e: List[str] = []
pc.ensure_pr_description(policy, "A long enough description with no ref.", e)
self.assertTrue(any("must reference a JIRA ID" in x for x in e))
def test_issue_reference_in_comment_does_not_pass(self) -> None:
# Isolate reference detection (skip min-length and checklist).
policy = make_policy(
description_min_length=0, description_checklist_patterns=[]
)
multiline_comment = """<!--
Fixes #1234
-->"""
multiple_comments = """This description has no visible issue reference.
<!-- Related to #1234 -->
Some visible text between the comments.
<!-- https://github.com/ROCm/TheRock/issues/5678 -->"""
for body in [
"<!-- GitHub issue: https://github.com/ROCm/TheRock/issues/1234 -->",
multiline_comment,
multiple_comments,
]:
with self.subTest(body=body):
e: List[str] = []
pc.ensure_pr_description(policy, body, e)
self.assertTrue(any("must reference a JIRA ID" in x for x in e))
def test_issue_reference_variants_pass(self) -> None:
# Isolate reference detection (skip min-length and checklist).
policy = make_policy(
description_min_length=0, description_checklist_patterns=[]
)
for body in [
"JIRA ID : TESTAUTO-6039",
"JIRA ID - #330",
"JIRA ID #330",
"ISSUE ID : TESTUTO-3334",
"ISSUE ID - TESTAUTO-3433",
"ISSUE ID : https://github.com/org/repo/issues/1234",
# Multiline format with JIRA ID
"JIRA ID\nROCM-25757",
"JIRA ID\n\nROCM-25757",
"jira id\nROCM-25757", # case-insensitive
# Trailing spaces after label + blank line before key
"JIRA ID \n\nAIRUNTIME-2352",
"JIRA ID\t\n\n\nROCM-25757",
"JIRA ID \r\n\r\nROCM-25757", # CRLF line endings
# Multiline format with ISSUE ID
"ISSUE ID\nAIRUNTIME-2352",
"ISSUE ID\n\nAIRUNTIME-2352",
"issue id\nAIRUNTIME-2352", # case-insensitive
"ISSUE ID \n\nAIRUNTIME-2352", # trailing spaces + blank line
]:
with self.subTest(body=body):
e: List[str] = []
pc.ensure_pr_description(policy, body, e)
self.assertEqual(e, [])
def test_closing_keyword_variants_pass(self) -> None:
# GitHub closing keywords are also accepted as a tracking ref.
policy = make_policy(
description_min_length=0, description_checklist_patterns=[]
)
for body in [
"Closes #10",
"Fixes octo-org/octo-repo#100",
"Resolves #10",
"resolves #123",
"resolves octo-org/octo-repo#100",
"Closes: #10",
"CLOSES #10",
"CLOSES: #10",
"This change fixes the bug.\nFixes #4321\n",
]:
with self.subTest(body=body):
e: List[str] = []
pc.ensure_pr_description(policy, body, e)
self.assertEqual(e, [])
def test_plain_github_issue_refs_pass(self) -> None:
# Bare '#<number>' and GitHub issue URLs are accepted without a keyword.
policy = make_policy(
description_min_length=0, description_checklist_patterns=[]
)
for body in [
"Related to #123",
"#4321",
"See https://github.com/ROCm/TheRock/issues/6043",
]:
with self.subTest(body=body):
e: List[str] = []
pc.ensure_pr_description(policy, body, e)
self.assertEqual(e, [])
def test_reference_inside_larger_body(self) -> None:
policy = make_policy(description_checklist_patterns=[])
body = "This change fixes the parser.\n\nISSUE ID : TESTUTO-3334\n"
e: List[str] = []
pc.ensure_pr_description(policy, body, e)
self.assertEqual(e, [])
def test_checklist_ticked_passes(self) -> None:
policy = make_policy(description_min_length=0, description_issue_patterns=[])
body = "- [x] Look over the contributing guidelines at https://..."
e: List[str] = []
pc.ensure_pr_description(policy, body, e)
self.assertEqual(e, [])
def test_checklist_unticked_fails(self) -> None:
policy = make_policy(description_min_length=0, description_issue_patterns=[])
body = "- [ ] Look over the contributing guidelines at https://..."
e: List[str] = []
pc.ensure_pr_description(policy, body, e)
self.assertTrue(any("Checklist" in x or "checklist" in x for x in e))
# ----------------------------- forbidden files -------------------------------
class ForbiddenFileTests(unittest.TestCase):
def setUp(self) -> None:
self.policy = make_policy()
def _errs(self, files: List[Dict[str, Any]]) -> List[str]:
e: List[str] = []
pc.ensure_no_forbidden_files(self.policy, files, e)
return e
def test_flags_secret_files(self) -> None:
for name in ["secret.pem", "config/.env", "deploy/id_rsa"]:
with self.subTest(name=name):
self.assertTrue(self._errs([make_file(name)]))
def test_allows_normal_files(self) -> None:
files = [make_file("src/app.py"), make_file("README.md")]
self.assertEqual(self._errs(files), [])
def test_removed_forbidden_file_is_ignored(self) -> None:
self.assertEqual(self._errs([make_file("secret.pem", status="removed")]), [])
# ----------------------------- unit tests check ------------------------------
class UnitTestRuleTests(unittest.TestCase):
def setUp(self) -> None:
self.policy = make_policy()
def _errs(self, files: List[Dict[str, Any]]) -> List[str]:
e: List[str] = []
pc.ensure_unit_tests(self.policy, files, e)
return e
def test_code_without_test_fails(self) -> None:
self.assertTrue(self._errs([make_file("src/module.py")]))
def test_code_with_test_passes(self) -> None:
files = [make_file("src/module.py"), make_file("tests/test_module.py")]
self.assertEqual(self._errs(files), [])
def test_docs_only_passes(self) -> None:
files = [make_file("README.md"), make_file("config/settings.yml")]
self.assertEqual(self._errs(files), [])
def test_source_anywhere_requires_test(self) -> None:
# Unit tests are required for source code placed ANYWHERE in the repo —
# no folder is special. Each non-test source file, on its own, fails.
for src in [
"policy_check.py",
"src/app.py",
"deep/nested/dir/module.py",
".github/therock_pr_bot/policy_check.py",
"lib/foo.cpp",
# 'test.py' is NOT a test file — 'test_*' needs the 'test_' prefix.
"test.py",
]:
with self.subTest(src=src):
self.assertTrue(self._errs([make_file(src)]))
def test_test_file_anywhere_satisfies_requirement(self) -> None:
# A real test_* file in ANY folder satisfies the requirement.
for test_path in [
"tests/test_module.py",
"deep/nested/test_module.py",
"any/where/module_test.py",
]:
with self.subTest(test_path=test_path):
files = [make_file("src/module.py"), make_file(test_path)]
self.assertEqual(self._errs(files), [])
# ----------------------------- draft + bump ----------------------------------
class DraftAndBumpTests(unittest.TestCase):
def test_draft_blocked_when_enabled(self) -> None:
policy = make_policy(block_draft=True)
e: List[str] = []
pc.ensure_pr_not_draft(policy, True, e)
self.assertTrue(e)
def test_draft_allowed_when_not_draft(self) -> None:
policy = make_policy(block_draft=True)
e: List[str] = []
pc.ensure_pr_not_draft(policy, False, e)
self.assertEqual(e, [])
def test_bump_author_detection(self) -> None:
policy = make_policy()
self.assertTrue(pc.is_bump_pr(policy, "assistant-librarian"))
self.assertTrue(pc.is_bump_pr(policy, "assistant-librarian[bot]"))
self.assertTrue(pc.is_bump_pr(policy, "SYSTEMS-ASSISTANT"))
self.assertFalse(pc.is_bump_pr(policy, "some-human"))
self.assertFalse(pc.is_bump_pr(policy, ""))
# ----------------------------- integration -----------------------------------
class IntegrationBlobTests(unittest.TestCase):
"""Feed full [title, description, files] blobs through validators."""
def setUp(self) -> None:
self.policy = make_policy()
def _evaluate(
self, *, title: str, body: str, files: List[Dict[str, Any]]
) -> Dict[str, List[str]]:
out: Dict[str, List[str]] = {}
e: List[str] = []
pc.ensure_pr_title(self.policy, title, e)
pc.ensure_pr_description(self.policy, body, e)
out["title_desc"] = e
e = []
pc.ensure_no_forbidden_files(self.policy, files, e)
out["forbidden"] = e
e = []
pc.ensure_unit_tests(self.policy, files, e)
out["unit"] = e
return out
def test_fully_compliant_pr(self) -> None:
result = self._evaluate(
title="feat(ci): add policy unit tests",
body=(
"Adds unit tests for the policy checker.\n"
"ISSUE ID : TESTUTO-3334\n"
"- [x] Look over the contributing guidelines at https://..."
),
files=[make_file("src/feature.py"), make_file("tests/test_feature.py")],
)
for key, errs in result.items():
with self.subTest(check=key):
self.assertEqual(errs, [])
def test_fully_noncompliant_pr(self) -> None:
result = self._evaluate(
title="wip",
body="too short",
files=[make_file("secret.pem"), make_file("src/module.py")],
)
self.assertTrue(result["title_desc"])
self.assertTrue(result["forbidden"])
self.assertTrue(result["unit"])
def test_docs_only_pr_is_compliant(self) -> None:
result = self._evaluate(
title="docs: clarify contributing guide",
body=(
"Improves the contributing docs.\n"
"JIRA ID : DOCS-42\n"
"- [x] Look over the contributing guidelines at https://..."
),
files=[make_file("docs/CONTRIBUTING.md"), make_file("README.md")],
)
for key, errs in result.items():
with self.subTest(check=key):
self.assertEqual(errs, [])
# ----------------------------- load_policy -----------------------------------
class LoadPolicyTests(unittest.TestCase):
"""Smoke-test the shipped policy.yml so config drift is caught."""
def test_load_shipped_policy(self) -> None:
policy_path = THIS_DIR / "policy.yml"
if not policy_path.exists():
self.skipTest("policy.yml not present next to tests")
policy = pc.load_policy(policy_path)
self.assertIn("pre-commit", policy.required_checks)
self.assertGreaterEqual(policy.title_max_length, policy.title_min_length)
def test_multiline_jira_issue_patterns_loaded(self) -> None:
"""Verify multiline JIRA/ISSUE ID patterns are in the loaded policy."""
policy_path = THIS_DIR / "policy.yml"
if not policy_path.exists():
self.skipTest("policy.yml not present next to tests")
policy = pc.load_policy(policy_path)
# Should have at least 5 issue reference patterns (inline + multiline + closing keywords + bare refs + urls)
self.assertGreaterEqual(len(policy.description_issue_patterns), 5)
# Verify multiline patterns work by testing them directly
multiline_jira_pattern = None
multiline_issue_pattern = None
for pat in policy.description_issue_patterns:
if pat.search("JIRA ID\nROCM-25757"):
multiline_jira_pattern = pat
if pat.search("ISSUE ID\nAIRUNTIME-2352"):
multiline_issue_pattern = pat
self.assertIsNotNone(
multiline_jira_pattern, "Multiline JIRA ID pattern not found in policy"
)
self.assertIsNotNone(
multiline_issue_pattern, "Multiline ISSUE ID pattern not found in policy"
)
def test_unit_test_patterns_exclude_unit_glob(self) -> None:
"""Verify 'unit/**' pattern is NOT in the loaded unit_test_patterns."""
policy_path = THIS_DIR / "policy.yml"
if not policy_path.exists():
self.skipTest("policy.yml not present next to tests")
policy = pc.load_policy(policy_path)
# Per team lead request, 'unit/**' was removed from unit_test_patterns.
# Test files are now recognized ONLY by basename (test_*, *_test.*, Test*).
self.assertNotIn("unit/**", policy.unit_test_patterns)
# Verify the three allowed patterns ARE present.
self.assertIn("test_*", policy.unit_test_patterns)
self.assertIn("*_test.*", policy.unit_test_patterns)
self.assertIn("*_tests.*", policy.unit_test_patterns)
self.assertIn("Test*", policy.unit_test_patterns)
self.assertIn("**/test/gtest/**", policy.unit_test_patterns)
if __name__ == "__main__":
unittest.main()