diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 8503faf71d..b6b9a25b67 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -401,8 +401,12 @@ class Check: body = re.sub(r"", "", body or "", flags=re.S) sections = dict(zip(SECTION.findall(body), SECTION.split(body)[2::2])) for name in ("What", "Tested", "Checklist"): - if not sections.get(name, "").strip(): - self.report("pr-template", "", 0, f"section '{name}' is missing or empty") + if name not in sections: + found = re.search(rf"^[ \t]*#+[ \t]*{name}[ \t]*$", body, re.M | re.I) + hint = f"; found '{found.group(0).strip()}'" if found else "" + self.report("pr-template", "", 0, f"add the '### {name}' heading of the template{hint}") + elif not sections[name].strip(): + self.report("pr-template", "", 0, f"fill the '### {name}' section") checklist = sections.get("Checklist", "") for item in re.findall(r"^\s*[-*+]\s*\[ \]\s*(.+)$", checklist, re.M): self.report("pr-template", "", 0, f"unchecked: {item.strip()}") diff --git a/tools/tests/test_check_conventions_pr_template.py b/tools/tests/test_check_conventions_pr_template.py index 51412f21dd..35ff674f59 100644 --- a/tools/tests/test_check_conventions_pr_template.py +++ b/tools/tests/test_check_conventions_pr_template.py @@ -71,8 +71,8 @@ class PullRequestTemplateTests(unittest.TestCase): self.assertTrue(items) result, findings = self.run_checker(template) self.assertEqual(result.returncode, 1, result.stdout) - self.assertIn("section 'What' is missing or empty", findings) - self.assertIn("section 'Tested' is missing or empty", findings) + self.assertIn("fill the '### What' section", findings) + self.assertIn("fill the '### Tested' section", findings) for item in items: self.assertIn(f"unchecked: {item.strip()}", findings) self.assertIn("answer AI-assisted with yes or no", findings) @@ -109,9 +109,28 @@ class PullRequestTemplateTests(unittest.TestCase): def test_sections_must_have_content(self): checklist = "- [x] Depends on: none\n- [x] AI-assisted: yes\n" - self.assert_findings(body(checklist, what=""), ["section 'What' is missing or empty"]) - self.assert_findings(body(checklist, tested=" \n"), ["section 'Tested' is missing or empty"]) - self.assert_findings(body(checklist).replace("### What", "#### What"), ["section 'What' is missing or empty"]) + self.assert_findings(body(checklist, what=""), ["fill the '### What' section"]) + self.assert_findings(body(checklist, tested=" \n"), ["fill the '### Tested' section"]) + self.assert_findings(body(checklist).replace("### What", "#### What"), + ["add the '### What' heading of the template; found '#### What'"]) + + def test_a_heading_at_another_level_or_case_is_named(self): + checklist = "- [x] Depends on: none\n- [x] AI-assisted: yes\n" + cases = { + "## What": "add the '### What' heading of the template; found '## What'", + " ### what ": "add the '### What' heading of the template; found '### what'", + "#WHAT": "add the '### What' heading of the template; found '#WHAT'", + } + for heading, finding in cases.items(): + with self.subTest(heading=heading): + self.assert_findings(body(checklist).replace("### What", heading), [finding]) + self.assert_findings(body(checklist).replace("### What", "### Whatever"), ["add the '### What' heading of the template"]) + + def test_a_body_without_template_headings_names_each_heading(self): + text = "What\nFix x.\n\nTested\nNot tested.\n\nChecklist\n- [x] Depends on: none\n- [x] AI-assisted: yes\n" + self.assert_findings(text, ["add the '### What' heading of the template", + "add the '### Tested' heading of the template", + "add the '### Checklist' heading of the template"]) def test_title_must_be_conventional(self): text = body("- [x] Depends on: none\n- [x] AI-assisted: yes\n")