Files
awesome-quant/tests/test_audit_readme.py
T

770 lines
30 KiB
Python

import tempfile
import threading
import time
import unittest
from datetime import datetime, timedelta, timezone
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
from github import GithubException
from scripts.audit_readme import (
AuditTarget,
Candidate,
Finding,
FindingKind,
TRACKING_MARKER,
TRACKING_TITLE,
audit_readme,
collect_targets,
render_report,
sync_tracking_issue,
)
from scripts.readme_entries import ReadmeEntry
from scripts.url_probe import Outcome, UrlObservation
def entry(line_number, section, name, url, *, raw_line=None):
return ReadmeEntry(
line_number=line_number,
raw_line=raw_line or f"- [{name}]({url}) - `Python` - Entry.",
section=section,
name=name,
url=url,
tail="`Python` - Entry.",
languages=["Python"],
description="Entry.",
)
def observation(url, outcome=Outcome.OK, *, status=200, attempts=1, final_url=None, error=""):
return UrlObservation(
requested_url=url,
final_url=final_url or url,
status=status,
outcome=outcome,
attempts=attempts,
error=error,
)
class FakeRepository:
def __init__(
self,
full_name,
*,
name=None,
stars=0,
archived=False,
disabled=False,
fork=False,
parent=None,
):
self.full_name = full_name
self.name = name or full_name.rsplit("/", 1)[-1]
self.html_url = f"https://github.com/{full_name}"
self.stargazers_count = stars
self.archived = archived
self.disabled = disabled
self.fork = fork
self.parent = parent
class FakeGithubClient:
def __init__(self, repos=None, searches=None):
self.repos = repos or {}
self.searches = searches or {}
self.repo_calls = []
self.search_calls = []
def get_repo(self, full_name):
self.repo_calls.append(full_name)
result = self.repos[full_name]
if isinstance(result, BaseException):
raise result
return result
def search_repositories(self, **kwargs):
self.search_calls.append(kwargs)
return iter(self.searches.get(kwargs["query"], ()))
class FakeIssue:
def __init__(self, title, body, state, *, edit_error=None):
self.title = title
self.body = body
self.state = state
self.edit_error = edit_error
self.edits = []
def edit(self, **kwargs):
if self.edit_error is not None:
raise self.edit_error
self.edits.append(kwargs)
if "body" in kwargs:
self.body = kwargs["body"]
if "state" in kwargs:
self.state = kwargs["state"]
class FakeTrackingRepository:
def __init__(self, issues=(), *, get_issues_error=None, create_issue_error=None):
self.issues = list(issues)
self.get_issues_error = get_issues_error
self.create_issue_error = create_issue_error
self.get_issues_calls = []
self.create_issue_calls = []
def get_issues(self, **kwargs):
if self.get_issues_error is not None:
raise self.get_issues_error
self.get_issues_calls.append(kwargs)
return iter(self.issues)
def create_issue(self, **kwargs):
if self.create_issue_error is not None:
raise self.create_issue_error
self.create_issue_calls.append(kwargs)
issue = FakeIssue(kwargs["title"], kwargs["body"], "open")
self.issues.append(issue)
return issue
class AuditReadmeTests(unittest.TestCase):
def test_render_report_is_a_complete_deterministic_markdown_document(self):
findings = [
Finding(
FindingKind.CANDIDATES,
"Zeta [Section] #1 | Top",
"Replacement *Tool* ~~Gone~~ ©",
80,
"https://dead.example/tool",
"The original primary URL returned confirmed HTTP 404.",
"Manual verification of these candidate repositories is required.",
(
Candidate("zeta/tool", "https://github.com/zeta/tool", 2),
Candidate("beta/tool", "https://github.com/beta/tool", 9),
Candidate("alpha/tool", "https://github.com/alpha/tool", 9),
),
),
Finding(
FindingKind.PERMANENT_REDIRECT,
"Redirects",
"Moved",
50,
"https://old.example/path",
"The link permanently redirects to https://new.example/path.",
"Update the link to https://new.example/path.",
),
Finding(
FindingKind.HTTP_ERROR,
"HTTP",
"Broken",
40,
"https://error.example",
"HTTP probe failed; last error unsafe URL.",
"Perform manual diagnosis before changing the link.",
),
Finding(
FindingKind.DEAD,
"Alpha & Beta",
"A_B [tool]",
10,
"https://dead.example/a",
"Confirmed HTTP 404 response.",
"Manually verify, then remove or replace this link.",
),
Finding(
FindingKind.GITHUB_ARCHIVED,
"Repositories",
"Archive",
60,
"https://github.com/owner/archive",
"GitHub marks the repository as archived.",
"Manually verify whether to replace it or move it to the historical section.",
),
Finding(
FindingKind.RESTRICTED,
"Access",
"Restricted",
30,
"https://restricted.example",
"Automated access was restricted by HTTP 403.",
"Perform manual browser verification before changing the link.",
),
Finding(
FindingKind.TRANSIENT,
"Transient",
"Retry",
20,
"https://retry.example",
"Transient failure after 3 attempts; last status 503.",
"Retry this link on the next audit run.",
),
Finding(
FindingKind.DEAD,
"Alpha & Beta",
"Zed",
11,
"https://dead.example/z",
"Confirmed HTTP 410 response.",
"Manually verify, then remove or replace this link.",
),
]
checked_at = datetime(
2026, 9, 2, 9, 45, tzinfo=timezone(timedelta(hours=-3))
)
expected = r"""<!-- awesome-quant-readme-audit -->
# Weekly README audit report
Checked: 2026-09-02 12:45 UTC
Entries are reported for manual review; this automation did not modify README.md.
## Confirmed dead links
- **Entry:** A\_B \[tool\]; **README line:** 10; **Section:** Alpha \& Beta; **Checked URL:** <https://dead.example/a>; **Evidence:** Confirmed HTTP 404 response.; **Manual suggestion:** Manually verify, then remove or replace this link.
- **Entry:** Zed; **README line:** 11; **Section:** Alpha \& Beta; **Checked URL:** <https://dead.example/z>; **Evidence:** Confirmed HTTP 410 response.; **Manual suggestion:** Manually verify, then remove or replace this link.
## Repeated transient failures
- **Entry:** Retry; **README line:** 20; **Section:** Transient; **Checked URL:** <https://retry.example>; **Evidence:** Transient failure after 3 attempts; last status 503.; **Manual suggestion:** Retry this link on the next audit run.
## Access-restricted links
- **Entry:** Restricted; **README line:** 30; **Section:** Access; **Checked URL:** <https://restricted.example>; **Evidence:** Automated access was restricted by HTTP 403.; **Manual suggestion:** Perform manual browser verification before changing the link.
## Other HTTP failures
- **Entry:** Broken; **README line:** 40; **Section:** HTTP; **Checked URL:** <https://error.example>; **Evidence:** HTTP probe failed; last error unsafe URL.; **Manual suggestion:** Perform manual diagnosis before changing the link.
## Permanent redirects
- **Entry:** Moved; **README line:** 50; **Section:** Redirects; **Checked URL:** <https://old.example/path>; **Evidence:** The link permanently redirects to <https://new.example/path>.; **Manual suggestion:** Update the link to <https://new.example/path>.
## GitHub repository findings
- **Entry:** Archive; **README line:** 60; **Section:** Repositories; **Checked URL:** <https://github.com/owner/archive>; **Evidence:** GitHub marks the repository as archived.; **Manual suggestion:** Manually verify whether to replace it or move it to the historical section.
## Candidate replacements
- **Entry:** Replacement \*Tool\* \~\~Gone\~\~ \&copy\;; **README line:** 80; **Section:** Zeta \[Section\] \#1 \| Top; **Checked URL:** <https://dead.example/tool>; **Evidence:** The original primary URL returned confirmed HTTP 404.; **Manual suggestion:** Manual verification of these candidate repositories is required.
- **Candidate 1:** alpha/tool — <https://github.com/alpha/tool> — 9 stars
- **Candidate 2:** beta/tool — <https://github.com/beta/tool> — 9 stars
- **Candidate 3:** zeta/tool — <https://github.com/zeta/tool> — 2 stars
"""
self.assertEqual(render_report(findings, checked_at=checked_at), expected)
self.assertEqual(
render_report(reversed(findings), checked_at=checked_at),
expected,
)
def test_render_report_clean_body_has_no_finding_headings(self):
report = render_report(
[],
checked_at=datetime(2026, 9, 2, 12, 45, tzinfo=timezone.utc),
)
self.assertEqual(
report,
"""<!-- awesome-quant-readme-audit -->
# Weekly README audit report
Checked: 2026-09-02 12:45 UTC
Entries are reported for manual review; this automation did not modify README.md.
No findings.
""",
)
def test_sync_tracking_issue_follows_the_full_lifecycle_matrix(self):
findings = [SimpleNamespace()]
new_body = f"{TRACKING_MARKER}\nnew report"
old_body = f"{TRACKING_MARKER}\nold report"
cases = (
("none with findings", (), findings, "created", None),
(
"open with findings",
(FakeIssue(TRACKING_TITLE, old_body, "open"),),
findings,
"updated",
{"body": new_body},
),
(
"closed with findings",
(FakeIssue(TRACKING_TITLE, old_body, "closed"),),
findings,
"reopened",
{"body": new_body, "state": "open"},
),
(
"open clean",
(FakeIssue(TRACKING_TITLE, old_body, "open"),),
[],
"closed",
{"body": new_body, "state": "closed"},
),
(
"closed clean",
(FakeIssue(TRACKING_TITLE, old_body, "closed"),),
[],
"updated",
{"body": new_body},
),
("none clean", (), [], "clean", None),
)
for name, issues, case_findings, expected, edit in cases:
with self.subTest(name=name):
repository = FakeTrackingRepository(issues)
result = sync_tracking_issue(repository, case_findings, new_body)
self.assertEqual(result, expected)
self.assertEqual(repository.get_issues_calls, [{"state": "all"}])
if issues:
self.assertEqual(issues[0].edits, [edit] if edit else [])
self.assertEqual(repository.create_issue_calls, [])
elif case_findings:
self.assertEqual(
repository.create_issue_calls,
[{"title": TRACKING_TITLE, "body": new_body}],
)
else:
self.assertEqual(repository.create_issue_calls, [])
def test_sync_tracking_issue_does_nothing_when_body_and_state_are_current(self):
body = f"{TRACKING_MARKER}\ncurrent report"
for findings, state in (([SimpleNamespace()], "open"), ([], "closed")):
with self.subTest(state=state):
issue = FakeIssue(TRACKING_TITLE, body, state)
repository = FakeTrackingRepository((issue,))
result = sync_tracking_issue(repository, findings, body)
self.assertEqual(result, "unchanged")
self.assertEqual(issue.edits, [])
self.assertEqual(repository.create_issue_calls, [])
def test_sync_tracking_issue_ignores_unmarked_issues_and_rejects_duplicates(self):
body = f"{TRACKING_MARKER}\nreport"
unmarked = FakeIssue(TRACKING_TITLE, "human issue", "open")
wrong_title = FakeIssue("Another report", body, "open")
repository = FakeTrackingRepository((unmarked, wrong_title))
self.assertEqual(
sync_tracking_issue(repository, [SimpleNamespace()], body),
"created",
)
duplicate_repository = FakeTrackingRepository(
(
FakeIssue(TRACKING_TITLE, body, "open"),
FakeIssue(TRACKING_TITLE, f"prefix\n{body}", "closed"),
)
)
with self.assertRaisesRegex(RuntimeError, "multiple tracking issues"):
sync_tracking_issue(duplicate_repository, [], body)
def test_sync_tracking_issue_propagates_repository_and_issue_errors(self):
body = f"{TRACKING_MARKER}\nreport"
get_error = RuntimeError("get failed")
create_error = RuntimeError("create failed")
edit_error = RuntimeError("edit failed")
cases = (
(
"get issues",
FakeTrackingRepository(get_issues_error=get_error),
get_error,
),
(
"create issue",
FakeTrackingRepository(create_issue_error=create_error),
create_error,
),
(
"edit issue",
FakeTrackingRepository(
(
FakeIssue(
TRACKING_TITLE,
f"{TRACKING_MARKER}\nold report",
"open",
edit_error=edit_error,
),
)
),
edit_error,
),
)
for name, repository, expected_error in cases:
with self.subTest(name=name):
with self.assertRaises(RuntimeError) as caught:
sync_tracking_issue(repository, [SimpleNamespace()], body)
self.assertIs(caught.exception, expected_error)
def test_collect_targets_uses_one_parser_pass_and_preserves_entry_and_url_order(self):
first = entry(
3,
"Trading & Backtesting",
"First",
"https://first.example",
raw_line=(
"- [First](https://first.example) - `Python` - "
"[Docs](https://docs.example)."
),
)
second = entry(4, "Trading & Backtesting", "Second", "http://second.example")
with patch(
"scripts.audit_readme.iter_readme_entries",
return_value=iter((first, second)),
) as parser:
targets = collect_targets("README.fixture.md")
parser.assert_called_once_with("README.fixture.md")
self.assertEqual(
targets,
[
AuditTarget(first, "https://first.example", True),
AuditTarget(first, "https://docs.example", False),
AuditTarget(second, "http://second.example", True),
],
)
def test_historical_entries_keep_link_and_move_findings_but_suppress_status_and_parent(self):
url = "https://github.com/old/project.git"
parent = FakeRepository("upstream/project")
repository = FakeRepository(
"new/project",
archived=True,
disabled=True,
fork=True,
parent=parent,
)
client = FakeGithubClient({"old/project": repository})
findings = self.audit(
f"## Historical & Archived Projects\n- [Project]({url}) - `Historical` - Entry.\n",
client,
{url: observation(url, Outcome.DEAD, status=404)},
)
self.assertEqual(
[finding.kind for finding in findings],
[FindingKind.DEAD, FindingKind.GITHUB_MOVED],
)
self.assertIn("https://github.com/new/project", findings[1].suggestion)
self.assertEqual(client.repo_calls, ["old/project"])
def test_moved_repository_is_reported_for_each_reference_but_looked_up_once(self):
first_url = "http://github.com/Owner/Old.git"
second_url = "https://github.com/owner/old"
client = FakeGithubClient(
{"Owner/Old": FakeRepository("Canonical/New")}
)
readme = (
"## Trading & Backtesting\n"
f"- [First]({first_url}) - `Python` - Entry.\n"
f"- [Second]({second_url}) - `Python` - Entry.\n"
)
findings = self.audit(
readme,
client,
{
first_url: observation(first_url),
second_url: observation(second_url),
},
)
self.assertEqual(
[finding.kind for finding in findings],
[FindingKind.GITHUB_MOVED, FindingKind.GITHUB_MOVED],
)
self.assertEqual(client.repo_calls, ["Owner/Old"])
self.assertTrue(
all("https://github.com/Canonical/New" in finding.suggestion for finding in findings)
)
def test_active_fork_promotes_a_different_active_parent(self):
url = "https://github.com/fork/project"
parent = FakeRepository("upstream/project")
client = FakeGithubClient(
{"fork/project": FakeRepository("fork/project", fork=True, parent=parent)}
)
findings = self.audit(
f"## Trading & Backtesting\n- [Project]({url}) - `Python` - Entry.\n",
client,
{url: observation(url)},
)
self.assertEqual([finding.kind for finding in findings], [FindingKind.GITHUB_PARENT])
self.assertIn(parent.html_url, findings[0].suggestion)
def test_active_non_fork_has_no_github_metadata_finding(self):
url = "https://github.com/owner/project/"
client = FakeGithubClient(
{"owner/project": FakeRepository("OWNER/PROJECT", parent=FakeRepository("other/project"))}
)
findings = self.audit(
f"## Trading & Backtesting\n- [Project]({url}) - `Python` - Entry.\n",
client,
{url: observation(url)},
)
self.assertEqual(findings, [])
def test_empty_query_and_fragment_delimiters_are_not_github_repository_urls(self):
urls = [
"https://github.com/owner/query?",
"https://github.com/owner/fragment#",
"https://github.com/owner/slash-query/?",
"https://github.com/owner/slash-fragment/#",
]
readme = "## Trading & Backtesting\n" + "".join(
f"- [Project {index}]({url}) - `Python` - Entry.\n"
for index, url in enumerate(urls)
)
client = FakeGithubClient(
{
"owner/query": FakeRepository("owner/query"),
"owner/fragment": FakeRepository("owner/fragment"),
"owner/slash-query": FakeRepository("owner/slash-query"),
"owner/slash-fragment": FakeRepository("owner/slash-fragment"),
}
)
findings = self.audit(
readme,
client,
{url: observation(url) for url in urls},
)
self.assertEqual(findings, [])
self.assertEqual(client.repo_calls, [])
def test_github_404_adds_no_metadata_finding_and_other_api_errors_escape(self):
missing_url = "https://github.com/owner/missing"
broken_url = "https://github.com/owner/broken"
missing = GithubException(404, {"message": "Not Found"})
broken = GithubException(500, {"message": "Unavailable"})
findings = self.audit(
f"## Trading & Backtesting\n- [Missing]({missing_url}) - `Python` - Entry.\n",
FakeGithubClient({"owner/missing": missing}),
{missing_url: observation(missing_url, Outcome.DEAD, status=404)},
)
self.assertEqual([finding.kind for finding in findings], [FindingKind.DEAD])
with self.assertRaises(GithubException):
self.audit(
f"## Trading & Backtesting\n- [Broken]({broken_url}) - `Python` - Entry.\n",
FakeGithubClient({"owner/broken": broken}),
{broken_url: observation(broken_url)},
)
def test_candidates_are_bounded_filtered_deduplicated_ranked_and_strictly_eligible(self):
eligible_url = "https://dead.example/tool"
suffix_url = "https://dead.example/docs"
github_url = "https://github.com/owner/dead-tool"
linked_url = "https://dead.example/linked"
historical_url = "https://dead.example/historical"
readme = (
"## Trading & Backtesting\n"
f"- [Dead Tool]({eligible_url}) - `Python` - Entry.\n"
f"- [Suffix Owner](https://ok.example) - `Python` - [Docs]({suffix_url}).\n"
f"- [Dead Tool]({github_url}) - `Python` - Entry.\n"
f"- [Linked Tool]({linked_url}) - `Python` - Entry. [GitHub](https://github.com/linked/tool)\n"
"## Historical & Archived Projects\n"
f"- [Historical Tool]({historical_url}) - `Historical` - Entry.\n"
)
exact = [
FakeRepository("zeta/dead-tool", name="dead_tool", stars=10),
FakeRepository("beta/DeadTool", name="DeadTool", stars=80),
FakeRepository("BETA/deadtool", name="dead-tool", stars=70),
FakeRepository("alpha/dead-tool", name="DEAD TOOL", stars=80),
FakeRepository("gamma/dead-tool", name="dead.tool", stars=30),
FakeRepository("archived/dead-tool", name="dead-tool", stars=500, archived=True),
FakeRepository("disabled/dead-tool", name="dead-tool", stars=400, disabled=True),
FakeRepository("wrong/something", name="something", stars=900),
]
padding = [FakeRepository(f"wrong/pad-{index}", name=f"pad-{index}") for index in range(12)]
beyond_cap = FakeRepository("beyond/dead-tool", name="dead-tool", stars=1000)
client = FakeGithubClient(
repos={
"owner/dead-tool": FakeRepository("owner/dead-tool"),
"linked/tool": FakeRepository("linked/tool"),
},
searches={"Dead Tool in:name": [*exact, *padding, beyond_cap]},
)
outcomes = {
url: observation(
url,
Outcome.DEAD if url != "https://ok.example" else Outcome.OK,
status=410 if url != "https://ok.example" else 200,
)
for url in (
eligible_url,
"https://ok.example",
suffix_url,
github_url,
linked_url,
"https://github.com/linked/tool",
historical_url,
)
}
findings = self.audit(readme, client, outcomes)
candidate_findings = [finding for finding in findings if finding.kind == FindingKind.CANDIDATES]
self.assertEqual(len(candidate_findings), 1)
self.assertEqual(
candidate_findings[0].candidates,
(
Candidate("alpha/dead-tool", "https://github.com/alpha/dead-tool", 80),
Candidate("beta/DeadTool", "https://github.com/beta/DeadTool", 80),
Candidate("gamma/dead-tool", "https://github.com/gamma/dead-tool", 30),
),
)
self.assertIn("410", candidate_findings[0].evidence)
self.assertIn("manual verification", candidate_findings[0].suggestion.casefold())
self.assertEqual(
client.search_calls,
[{"query": "Dead Tool in:name", "sort": "stars", "order": "desc"}],
)
def test_equal_star_case_variant_candidates_are_independent_of_api_order(self):
url = "https://dead.example/tool"
readme = (
"## Trading & Backtesting\n"
f"- [Dead Tool]({url}) - `Python` - Entry.\n"
)
upper = FakeRepository("OWNER/Dead-Tool", name="dead-tool", stars=40)
lower = FakeRepository("owner/dead-tool", name="dead-tool", stars=40)
selections = []
for repositories in ((lower, upper), (upper, lower)):
client = FakeGithubClient(
searches={"Dead Tool in:name": repositories}
)
findings = self.audit(
readme,
client,
{url: observation(url, Outcome.DEAD, status=404)},
)
selections.append(
next(
finding.candidates
for finding in findings
if finding.kind == FindingKind.CANDIDATES
)
)
expected = (
Candidate("OWNER/Dead-Tool", "https://github.com/OWNER/Dead-Tool", 40),
)
self.assertEqual(selections, [expected, expected])
def test_all_finding_kinds_are_mapped_and_sorted_deterministically(self):
urls = {
"dead": "https://dead.example",
"transient": "https://transient.example",
"restricted": "https://restricted.example",
"error": "https://error.example",
"redirect": "https://redirect.example",
"moved": "https://github.com/old/moved",
"archived": "https://github.com/owner/archived",
"disabled": "https://github.com/owner/disabled",
"parent": "https://github.com/fork/project",
}
readme = "## Trading & Backtesting\n" + "".join(
f"- [{name.title()}]({url}) - `Python` - Entry.\n"
for name, url in reversed(list(urls.items()))
)
client = FakeGithubClient(
repos={
"old/moved": FakeRepository("new/moved"),
"owner/archived": FakeRepository("owner/archived", archived=True),
"owner/disabled": FakeRepository("owner/disabled", disabled=True),
"fork/project": FakeRepository(
"fork/project", fork=True, parent=FakeRepository("upstream/project")
),
},
searches={"Dead in:name": [FakeRepository("candidate/dead", name="dead", stars=4)]},
)
outcomes = {
urls["dead"]: observation(urls["dead"], Outcome.DEAD, status=404),
urls["transient"]: observation(
urls["transient"], Outcome.TRANSIENT, status=503, attempts=3,
error="retry limit exceeded: HTTP 503",
),
urls["restricted"]: observation(urls["restricted"], Outcome.RESTRICTED, status=403),
urls["error"]: observation(
urls["error"], Outcome.HTTP_ERROR, status=None, error="unsafe URL"
),
urls["redirect"]: observation(
urls["redirect"], Outcome.PERMANENT_REDIRECT, status=200,
final_url="https://redirect.example/new",
),
urls["moved"]: observation(urls["moved"]),
urls["archived"]: observation(urls["archived"]),
urls["disabled"]: observation(urls["disabled"]),
urls["parent"]: observation(urls["parent"]),
}
findings = self.audit(readme, client, outcomes)
self.assertEqual([finding.kind for finding in findings], list(FindingKind))
self.assertIn("3 attempts", findings[1].evidence)
self.assertIn("503", findings[1].evidence)
self.assertIn("next audit run", findings[1].suggestion)
self.assertIn("manual browser", findings[2].suggestion)
self.assertIn("manual diagnosis", findings[3].suggestion)
self.assertIn("https://redirect.example/new", findings[4].evidence)
self.assertIn("update", findings[4].suggestion.casefold())
def test_eight_worker_out_of_order_probes_have_stable_results(self):
urls = [f"https://project-{index}.example" for index in range(8)]
readme = "## Trading & Backtesting\n" + "".join(
f"- [Project {index}]({url}) - `Python` - Entry.\n"
for index, url in enumerate(urls)
)
def run_with_delays(reverse):
barrier = threading.Barrier(8)
def delayed_probe(url):
index = urls.index(url)
barrier.wait(timeout=2)
delay_index = index if reverse else 7 - index
time.sleep(delay_index / 1000)
return observation(url, Outcome.DEAD, status=404)
return self.audit(readme, FakeGithubClient(), delayed_probe)
forward = run_with_delays(False)
reverse = run_with_delays(True)
self.assertEqual(forward, reverse)
self.assertEqual([finding.entry_name for finding in forward], [f"Project {i}" for i in range(8)])
def audit(self, readme, client, observations):
if callable(observations):
prober = observations
else:
prober = observations.__getitem__
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "README.md"
path.write_text(readme, encoding="utf-8")
return audit_readme(path, client, prober=prober)
if __name__ == "__main__":
unittest.main()