Skip to content

Commit 5e19d33

Browse files
CodeRabbit Generated Unit Tests: Add pytest tests for README validation in tests/test_readme.py
1 parent d5fffc4 commit 5e19d33

File tree

1 file changed

+257
-0
lines changed

1 file changed

+257
-0
lines changed

tests/test_readme.py

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
"""
2+
README quality and consistency tests.
3+
4+
Testing library/framework: pytest
5+
- These tests use only Python stdlib to avoid introducing new dependencies.
6+
- They validate structure, key sections, badge/link syntax, and critical snippets
7+
from the README to catch regressions in documentation that users depend on.
8+
"""
9+
from __future__ import annotations
10+
11+
import re
12+
from pathlib import Path
13+
from urllib.parse import urlparse
14+
15+
import sys
16+
17+
README_CANDIDATES = [
18+
Path("README.md"),
19+
Path("Readme.md"),
20+
Path("README.rst"),
21+
]
22+
README_PATH = next((p for p in README_CANDIDATES if p.exists()), None)
23+
24+
25+
def _require_readme() -> str:
26+
assert README_PATH is not None, f"README file not found. Looked for: {', '.join(map(str, README_CANDIDATES))}"
27+
return README_PATH.read_text(encoding="utf-8", errors="replace")
28+
29+
30+
def test_readme_has_expected_title_and_intro():
31+
readme = _require_readme()
32+
assert "Commit-Check GitHub Action" in readme
33+
assert "A GitHub Action for checking commit message formatting" in readme
34+
35+
36+
def test_table_of_contents_contains_expected_sections_ordered():
37+
readme = _require_readme()
38+
# Basic presence checks
39+
for section in [
40+
"Usage",
41+
"Optional Inputs",
42+
"GitHub Action Job Summary",
43+
"GitHub Pull Request Comments",
44+
"Badging Your Repository",
45+
"Versioning",
46+
]:
47+
assert f"[{section}]" in readme or f"## {section}" in readme, f"Missing section: {section}"
48+
49+
# Check that TOC links match headers (anchor-style)
50+
toc_block_match = re.search(r"## Table of Contents\s+([\s\S]+?)\n## ", readme)
51+
assert toc_block_match, "Table of Contents block not found"
52+
toc_block = toc_block_match.group(1)
53+
anchors = re.findall(r"\*\s+\[(.+?)\]\(#([a-z0-9\-]+)\)", toc_block, flags=re.I)
54+
assert anchors, "No TOC anchors found"
55+
# Ensure each anchor has a corresponding header
56+
for label, anchor in anchors:
57+
header_pattern = rf"^##\s+{re.escape(label)}\s*$"
58+
assert re.search(header_pattern, readme, flags=re.M), f"Header for TOC entry '{label}' not found"
59+
60+
61+
def test_badges_and_links_are_well_formed_urls():
62+
readme = _require_readme()
63+
# Collect all markdown image and link URLs
64+
urls = []
65+
urls += re.findall(r"\!\[[^\]]*\]\((https?://[^)]+)\)", readme) # images
66+
urls += re.findall(r"\[[^\]]*\]\((https?://[^)]+)\)", readme) # links
67+
68+
assert urls, "No URLs found in README; expected badges/links to be present"
69+
70+
for u in urls:
71+
parsed = urlparse(u)
72+
assert parsed.scheme in ("http", "https"), f"Unexpected URL scheme in {u}"
73+
assert parsed.netloc, f"URL missing host: {u}"
74+
# Basic sanity: disallow spaces
75+
assert " " not in u, f"URL contains spaces: {u}"
76+
77+
# Spot-check expected badge providers/domains
78+
assert any("img.shields.io" in u for u in urls), "Shields.io badges should be present"
79+
assert any("github.com/commit-check/commit-check-action" in u for u in urls), "Repository links should be present"
80+
81+
82+
def test_usage_yaml_snippet_contains_expected_github_actions_fields():
83+
readme = _require_readme()
84+
# Extract the fenced yaml code block under Usage
85+
usage_match = re.search(r"## Usage[\s\S]+?```yaml([\s\S]+?)```", readme, flags=re.I)
86+
assert usage_match, "Usage YAML block not found"
87+
yaml_text = usage_match.group(1)
88+
89+
# Validate presence of critical keys/values by regex (no external YAML dependency)
90+
expected_lines = [
91+
r"^name:\s*Commit Check\s*$",
92+
r"^on:\s*$",
93+
r"^\s+push:\s*$",
94+
r"^\s+pull_request:\s*$",
95+
r"^\s+branches:\s*'main'\s*$",
96+
r"^jobs:\s*$",
97+
r"^\s+commit-check:\s*$",
98+
r"^\s+runs-on:\s*ubuntu-latest\s*$",
99+
r"^\s+permissions:\s*#? ?use permissions because use of pr-comments\s*$",
100+
r"^\s+contents:\s*read\s*$",
101+
r"^\s+pull-requests:\s*write\s*$",
102+
r"^\s+steps:\s*$",
103+
r"^\s+- uses:\s*actions/checkout@v5\s*$",
104+
r"^\s+ref:\s*\$\{\{\s*github\.event\.pull_request\.head\.sha\s*\}\}\s*# checkout PR HEAD commit\s*$",
105+
r"^\s+fetch-depth:\s*0\s*# required for merge-base check\s*$",
106+
r"^\s+- uses:\s*commit-check/commit-check-action@v1\s*$",
107+
r"^\s+env:\s*$",
108+
r"^\s+GITHUB_TOKEN:\s*\$\{\{\s*secrets\.GITHUB_TOKEN\s*\}\}\s*# use GITHUB_TOKEN because use of pr-comments\s*$",
109+
r"^\s+with:\s*$",
110+
r"^\s+message:\s*true\s*$",
111+
r"^\s+branch:\s*true\s*$",
112+
r"^\s+author-name:\s*true\s*$",
113+
r"^\s+author-email:\s*true\s*$",
114+
r"^\s+commit-signoff:\s*true\s*$",
115+
r"^\s+merge-base:\s*false\s*$",
116+
r"^\s+imperative:\s*false\s*$",
117+
r"^\s+job-summary:\s*true\s*$",
118+
r"^\s+pr-comments:\s*\$\{\{\s*github\.event_name\s*==\s*'pull_request'\s*\}\}\s*$",
119+
]
120+
for pattern in expected_lines:
121+
assert re.search(pattern, yaml_text, flags=re.M), f"Missing or malformed line in Usage YAML matching: {pattern}"
122+
123+
124+
def test_optional_inputs_section_lists_all_expected_inputs_with_defaults():
125+
readme = _require_readme()
126+
# Build a map of input -> default as shown
127+
items = {
128+
"message": "true",
129+
"branch": "true",
130+
"author-name": "true",
131+
"author-email": "true",
132+
"commit-signoff": "true",
133+
"merge-base": "false",
134+
"imperative": "false",
135+
"dry-run": "false",
136+
"job-summary": "true",
137+
"pr-comments": "false",
138+
}
139+
140+
# Ensure each input has a dedicated subsection header and default line
141+
for key, default in items.items():
142+
header_pat = rf"^###\s*`{re.escape(key)}`\s*$"
143+
assert re.search(header_pat, readme, flags=re.M), f"Missing Optional Inputs header for `{key}`"
144+
default_pat = rf"^-+\s*Default:\s*`{re.escape(default)}`"
145+
# Search within a bounded region (from header to either next ### or end)
146+
section_match = re.search(header_pat + r"([\s\S]+?)(^###\s*`|\Z)", readme, flags=re.M)
147+
assert section_match, f"Section body for `{key}` not found"
148+
section_body = section_match.group(1)
149+
assert re.search(default_pat, section_body, flags=re.M), f"Default for `{key}` should be `{default}`"
150+
151+
152+
def test_merge_base_important_note_present_and_mentions_fetch_depth_zero():
153+
readme = _require_readme()
154+
note_match = re.search(r"###\s*`merge-base`[\s\S]+?>\s*\[\!IMPORTANT\][\s\S]+?fetch-depth:\s*0", readme, flags=re.I)
155+
assert note_match, "IMPORTANT note for `merge-base` with fetch-depth: 0 is missing"
156+
157+
158+
def test_pr_comments_important_note_mentions_github_token_and_issue_77():
159+
readme = _require_readme()
160+
assert "### `pr-comments`" in readme
161+
assert re.search(r">\s*\[\!IMPORTANT\][\s\S]+GITHUB_TOKEN[\s\S]+#77", readme), \
162+
"IMPORTANT note for `pr-comments` should mention GITHUB_TOKEN and issue #77"
163+
164+
165+
def test_used_by_section_contains_expected_orgs_and_structure():
166+
readme = _require_readme()
167+
# Simple checks for known org names and <img ... alt="...">
168+
expected_orgs = [
169+
("Apache", "https://github.com/apache"),
170+
("Texas Instruments", "https://github.com/TexasInstruments"),
171+
("OpenCADC", "https://github.com/opencadc"),
172+
("Extrawest", "https://github.com/extrawest"),
173+
("Mila", "https://github.com/mila-iqia"),
174+
("Chainlift", "https://github.com/Chainlift"),
175+
]
176+
for alt, href in expected_orgs:
177+
assert alt in readme, f"Expected org '{alt}' not mentioned"
178+
assert href in readme, f"Expected org link '{href}' not present"
179+
# Check that avatars come from GitHub's avatars CDN
180+
assert re.search(r'src="https://avatars\.githubusercontent\.com/u/\d+\?s=200&v=4"', readme), \
181+
"Org avatar images should use githubusercontent avatars"
182+
183+
184+
def test_badging_section_contains_markdown_and_rst_snippets():
185+
readme = _require_readme()
186+
# Markdown fenced snippet
187+
assert re.search(
188+
r"\[\!\[Commit Check\]\(https://github\.com/commit-check/commit-check-action/actions/workflows/commit-check\.yml/badge\.svg\)\]"
189+
r"\(https://github\.com/commit-check/commit-check-action/actions/workflows/commit-check\.yml\)",
190+
readme,
191+
), "Markdown badge snippet missing or malformed"
192+
193+
# reStructuredText snippet
194+
assert re.search(
195+
r"\.\. image:: https://github\.com/commit-check/commit-check-action/actions/workflows/commit-check\.yml/badge\.svg\s+"
196+
r":target: https://github\.com/commit-check/commit-check-action/actions/workflows/commit-check\.yml\s+"
197+
r":alt: Commit Check",
198+
readme,
199+
), "reStructuredText badge snippet missing or malformed"
200+
201+
202+
def test_versioning_and_feedback_sections_present_with_expected_links():
203+
readme = _require_readme()
204+
assert "Versioning follows" in readme and "Semantic Versioning" in readme
205+
# Feedback/issues link
206+
assert re.search(r"\[issues\]\(https://github\.com/commit-check/commit-check/issues\)", readme), \
207+
"Issues link in feedback section is missing"
208+
209+
210+
def test_all_markdown_links_and_images_have_alt_text_or_label():
211+
readme = _require_readme()
212+
# Images must have alt text inside \![...]
213+
for m in re.finditer(r"\!\[(?P<alt>[^\]]*)\]\((?P<url>https?://[^)]+)\)", readme):
214+
alt = (m.group("alt") or "").strip()
215+
assert alt != "", f"Image missing alt text for URL {m.group('url')}"
216+
217+
# Links should have non-empty labels
218+
for m in re.finditer(r"\[(?P<label>[^\]]+)\]\((?P<url>https?://[^)]+)\)", readme):
219+
label = (m.group("label") or "").strip()
220+
assert label != "", f"Link missing label for URL {m.group('url')}"
221+
222+
223+
def test_no_http_links_only_https():
224+
readme = _require_readme()
225+
http_links = re.findall(r"\((http://[^)]+)\)", readme)
226+
assert not http_links, f"Insecure http links found: {http_links}"
227+
228+
229+
def test_job_summary_and_pr_comments_screenshots_referenced():
230+
readme = _require_readme()
231+
assert "screenshot/success-job-summary.png" in readme
232+
assert "screenshot/failure-job-summary.png" in readme
233+
assert "screenshot/success-pr-comments.png" in readme
234+
assert "screenshot/failure-pr-comments.png" in readme
235+
236+
237+
# Edge cases / failure scenarios
238+
239+
240+
def test_readme_is_not_empty_and_has_min_length():
241+
readme = _require_readme()
242+
assert len(readme.strip()) > 400, "README seems too short; expected a substantive document"
243+
244+
245+
def test_usage_block_contains_both_checkout_and_action_steps_once_each():
246+
readme = _require_readme()
247+
usage_match = re.search(r"## Usage[\s\S]+?```yaml([\s\S]+?)```", readme, flags=re.I)
248+
assert usage_match, "Usage YAML block not found"
249+
yaml_text = usage_match.group(1)
250+
assert yaml_text.count("actions/checkout@v5") == 1, "Expected one checkout step"
251+
assert yaml_text.count("commit-check/commit-check-action@v1") == 1, "Expected one commit-check-action step"
252+
253+
254+
def test_references_to_experimental_features_present():
255+
readme = _require_readme()
256+
assert re.search(r"`merge-base`.*experimental feature", readme, flags=re.I | re.S)
257+
assert re.search(r"`pr-comments`.*experimental feature", readme, flags=re.I | re.S)

0 commit comments

Comments
 (0)