Skip to content

Commit f30f9a5

Browse files
committed
add tests for PR title validation and enhance CI workflow integration
1 parent e8e2757 commit f30f9a5

File tree

8 files changed

+168
-6
lines changed

8 files changed

+168
-6
lines changed

.github/PULL_REQUEST_TEMPLATE/task_submission_en.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
PR Title (CI enforced):
2-
- Tasks: `[TASK] <Task>-<Variant>. <Last Name> <First Name> <Middle Name>. <Group>. <Task name>.`
2+
- Tasks: `[TASK] <Task>-<Variant>. <Last Name> <First Name> <Middle Name>. <Group>. <Technology: SEQ|MPI|ALL|OMP|STL|TBB>. <Task name>.`
33
- Development: `[DEV] <any descriptive title>`
44

55
<!--

.github/PULL_REQUEST_TEMPLATE/task_submission_ru.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
Формат заголовка PR (CI):
2-
- Задачи: `[TASK] <Task>-<Variant>. <Фамилия> <Имя> <Отчество>. <Группа>. <Название задачи>.`
2+
- Задачи: `[TASK] <Task>-<Variant>. <Фамилия> <Имя> <Отчество>. <Группа>. <Технология: SEQ|MPI|ALL|OMP|STL|TBB>. <Название задачи>.`
33
- Разработка: `[DEV] <произвольное осмысленное название>`
44

55
<!--

.github/policy/pr_title.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"allow_dev": true,
3-
"task_regex": "^\\[TASK\\]\\s+(?P<task>\\d+)-(?P<variant>\\d+)\\.\\s+(?P<lastname>[А-ЯA-ZЁ][а-яa-zё]+)\\s+(?P<firstname>[А-ЯA-ZЁ][а-яa-zё]+)\\s+(?P<middlename>[А-ЯA-ZЁ][а-яa-zё]+)\\.\\s+(?P<group>.+?)\\.\\s+(?P<taskname>\\S.*)$",
3+
"task_regex": "^\\[TASK\\]\\s+(?P<task>[1-5])-(?P<variant>\\d+)\\.\\s+(?P<lastname>[А-ЯA-ZЁ][а-яa-zё]+)\\s+(?P<firstname>[А-ЯA-ZЁ][а-яa-zё]+)\\s+(?P<middlename>[А-ЯA-ZЁ][а-яa-zё]+)\\.\\s+(?P<group>.+?)\\.\\s+(?P<technology>SEQ|MPI|ALL|OMP|STL|TBB)\\.\\s+(?P<taskname>\\S.*)$",
44
"dev_regex": "^\\[DEV\\]\\s+\\S.*$",
55
"examples": {
6-
"task_ru": "[TASK] 2-12. Иванов Иван Иванович. 2341-а234. Вычисление суммы элементов вектора.",
7-
"task_en": "[TASK] 2-12. Ivanov Ivan Ivanovich. 2341-a234. Vector elements sum calculation.",
6+
"task_ru": "[TASK] 2-12. Иванов Иван Иванович. 2341-а234. OMP. Вычисление суммы элементов вектора.",
7+
"task_en": "[TASK] 2-12. Ivanov Ivan Ivanovich. 2341-a234. OMP. Vector elements sum calculation.",
88
"dev": "[DEV] Update docs for lab 2"
99
}
1010
}

.github/pull_request_template.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
PR Title (CI enforced):
2-
- Tasks: `[TASK] <Task>-<Variant>. <Last Name> <First Name> <Middle Name>. <Group>. <Task name>.`
2+
- Tasks: `[TASK] <Task>-<Variant>. <Last Name> <First Name> <Middle Name>. <Group>. <Technology: SEQ|MPI|ALL|OMP|STL|TBB>. <Task name>.`
33
- Development: `[DEV] <any descriptive title>`
44

55
<!-- Solution for PR template choice: https://stackoverflow.com/a/75030350/24543008 -->

.github/workflows/main.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,17 @@ jobs:
2222
pr_title:
2323
uses: ./.github/workflows/pr-title.yml
2424

25+
pr_title_tests:
26+
needs:
27+
- pr_title
28+
if: ${{ github.event_name != 'pull_request' || github.event.action != 'edited' || github.event.changes.title }}
29+
uses: ./.github/workflows/pr-title-tests.yml
30+
2531
pre-commit:
2632
if: ${{ github.event_name != 'pull_request' || github.event.action != 'edited' || github.event.changes.title }}
2733
needs:
2834
- pr_title
35+
- pr_title_tests
2936
uses: ./.github/workflows/pre-commit.yml
3037
ubuntu:
3138
if: ${{ github.event_name != 'pull_request' || github.event.action != 'edited' || github.event.changes.title }}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: PR Title Tests
2+
on:
3+
workflow_call:
4+
jobs:
5+
unit:
6+
name: Validator Unit Tests
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v4
10+
- uses: actions/setup-python@v5
11+
with:
12+
python-version: '3.11'
13+
- name: Run unit tests
14+
run: |
15+
python -m unittest -v scripts/tests/pr_title/test_validate_title.py scripts/tests/pr_title/test_main_integration.py
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import json
2+
import os
3+
import tempfile
4+
from importlib.util import module_from_spec, spec_from_file_location
5+
from pathlib import Path
6+
from unittest import TestCase
7+
8+
9+
REPO_ROOT = Path.cwd()
10+
VALIDATOR_PATH = REPO_ROOT / ".github/scripts/validate_pr.py"
11+
12+
13+
def load_validator():
14+
spec = spec_from_file_location("validate_pr", str(VALIDATOR_PATH))
15+
assert spec and spec.loader
16+
mod = module_from_spec(spec)
17+
spec.loader.exec_module(mod) # type: ignore[attr-defined]
18+
return mod
19+
20+
21+
class TestMainIntegration(TestCase):
22+
def setUp(self) -> None:
23+
self.validator = load_validator()
24+
self._old_event_path = os.environ.get("GITHUB_EVENT_PATH")
25+
26+
def tearDown(self) -> None:
27+
if self._old_event_path is None:
28+
os.environ.pop("GITHUB_EVENT_PATH", None)
29+
else:
30+
os.environ["GITHUB_EVENT_PATH"] = self._old_event_path
31+
32+
def _with_event(self, title: str) -> str:
33+
payload = {"pull_request": {"title": title}}
34+
fd, path = tempfile.mkstemp(prefix="gh-event-", suffix=".json")
35+
with os.fdopen(fd, "w", encoding="utf-8") as f:
36+
json.dump(payload, f)
37+
os.environ["GITHUB_EVENT_PATH"] = path
38+
return path
39+
40+
def test_main_ok(self) -> None:
41+
self._with_event(
42+
"[TASK] 2-12. Иванов Иван Иванович. 2341-а234. OMP. Вычисление суммы элементов вектора."
43+
)
44+
rc = self.validator.main()
45+
self.assertEqual(rc, 0)
46+
47+
def test_main_fail(self) -> None:
48+
self._with_event("Bad title format")
49+
rc = self.validator.main()
50+
self.assertEqual(rc, 1)
51+
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import json
2+
from importlib.util import module_from_spec, spec_from_file_location
3+
from pathlib import Path
4+
from typing import Tuple
5+
from unittest import TestCase, mock
6+
7+
8+
REPO_ROOT = Path.cwd()
9+
VALIDATOR_PATH = REPO_ROOT / ".github/scripts/validate_pr.py"
10+
POLICY_PATH = REPO_ROOT / ".github/policy/pr_title.json"
11+
12+
13+
def load_validator():
14+
spec = spec_from_file_location("validate_pr", str(VALIDATOR_PATH))
15+
assert spec and spec.loader
16+
mod = module_from_spec(spec)
17+
spec.loader.exec_module(mod) # type: ignore[attr-defined]
18+
return mod
19+
20+
21+
class TestValidateTitle(TestCase):
22+
@classmethod
23+
def setUpClass(cls) -> None:
24+
assert VALIDATOR_PATH.exists(), "Validator script not found"
25+
assert POLICY_PATH.exists(), "Policy file not found"
26+
cls.validator = load_validator()
27+
with open(POLICY_PATH, "r", encoding="utf-8") as f:
28+
cls.policy = json.load(f)
29+
30+
def test_valid_task_ru(self) -> None:
31+
title = (
32+
"[TASK] 2-12. Иванов Иван Иванович. 2341-а234. OMP. Вычисление суммы элементов вектора."
33+
)
34+
errs = self.validator.validate_title(title)
35+
self.assertEqual(errs, [])
36+
37+
def test_valid_task_en(self) -> None:
38+
title = (
39+
"[TASK] 3-4. Ivanov Ivan Ivanovich. 2341-a234. MPI. Vector elements sum calculation."
40+
)
41+
errs = self.validator.validate_title(title)
42+
self.assertEqual(errs, [])
43+
44+
def test_invalid_task_number_out_of_range(self) -> None:
45+
title = (
46+
"[TASK] 6-1. Иванов Иван Иванович. 2341-а234. SEQ. Вычисление суммы элементов вектора."
47+
)
48+
errs = self.validator.validate_title(title)
49+
self.assertTrue(errs, "Expected errors for out-of-range task number")
50+
51+
def test_valid_dev_when_allowed(self) -> None:
52+
title = "[DEV] Update docs for lab 2"
53+
errs = self.validator.validate_title(title)
54+
self.assertEqual(errs, [])
55+
56+
def test_dev_disallowed_by_policy(self) -> None:
57+
cfg = dict(self.policy)
58+
cfg["allow_dev"] = False
59+
60+
def fake_load_title_config() -> Tuple[dict, list]:
61+
return cfg, [str(POLICY_PATH)]
62+
63+
with mock.patch.object(self.validator, "_load_title_config", fake_load_title_config):
64+
errs = self.validator.validate_title("[DEV] Working WIP")
65+
self.assertTrue(errs, "Expected errors when allow_dev is False")
66+
67+
def test_missing_policy_file(self) -> None:
68+
def fake_load_title_config_missing():
69+
return None, [str(POLICY_PATH)]
70+
71+
with mock.patch.object(self.validator, "_load_title_config", fake_load_title_config_missing):
72+
errs = self.validator.validate_title("[TASK] 2-1. X Y Z. G. OMP. Title.")
73+
self.assertTrue(errs, "Expected error for missing policy config")
74+
self.assertIn("policy config not found", " ".join(errs).lower())
75+
76+
def test_missing_technology_block(self) -> None:
77+
title = (
78+
"[TASK] 2-12. Иванов Иван Иванович. 2341-а234. Вычисление суммы элементов вектора."
79+
)
80+
errs = self.validator.validate_title(title)
81+
self.assertTrue(errs, "Expected error when technology block is missing")
82+
83+
def test_invalid_technology_token(self) -> None:
84+
title = (
85+
"[TASK] 2-12. Иванов Иван Иванович. 2341-а234. CUDA. Вычисление суммы элементов вектора."
86+
)
87+
errs = self.validator.validate_title(title)
88+
self.assertTrue(errs, "Expected error for unsupported technology token")
89+

0 commit comments

Comments
 (0)