tests/github/test_compose_notes.py
Rama main · réplica de lectura del repositorio original.
from __future__ import annotations
import json
import re
import runpy
import subprocess
import sys
from pathlib import Path
import pytest
SCRIPT = Path(__file__).resolve().parents[2] / ".github/scripts/changelog/compose_notes.py"
compose_notes = runpy.run_path(str(SCRIPT))["compose_notes"]
REPO = "o/o"
CREDITS = [
{"pr": pr, "author": author, "author_url": f"https://github.com/{author}"}
for pr, author in [(1, "alice"), (2, "bob"), (3, "carol"), (4, "dave"), (5, "bob")]
]
def _raw(highlights: str) -> str:
return f"<!-- RELEASE_NOTES -->\n{highlights}\n<!-- /RELEASE_NOTES -->"
@pytest.mark.parametrize(
"highlights",
[
"## Major new features\n\n- New feature (#3, @carol)\n\n"
+ "## Breaking changes\n\n- Renamed flag (#4, @dave)\n\n"
+ "## Bug fixes\n\n- Fixed crashes (#1, @alice)",
"## Major new features\n\n- New feature (#3, @carol)\n\n"
+ "## Bug fixes\n\n- Fixed crashes (#1, @alice)",
"## Bug fixes\n\n- Fixed crashes (#1, @alice)",
],
)
def test_curated_sections_omit_other_contributions(highlights: str) -> None:
notes = compose_notes(_raw(highlights), CREDITS, REPO)
highlighted = set(re.findall(r"#(\d+)", highlights))
summary, thanks = notes.split("### 💜 Thanks to our community")
assert re.findall(r"(?m)^## .+$", summary) == re.findall(r"(?m)^## .+$", highlights)
assert summary.count("- ") == highlights.count("- ")
for credit in CREDITS:
link = f"[#{credit['pr']}](https://github.com/o/o/pull/{credit['pr']})"
assert notes.count(link) == int(str(credit["pr"]) in highlighted)
assert (link in summary) == (str(credit["pr"]) in highlighted)
assert f"[@{credit['author']}]({credit['author_url']})" in thanks
assert "/pull/" not in thanks
assert "## Other contributions" not in notes
assert "## All contributions" not in notes
assert notes.endswith("Full Changelog: https://github.com/o/o/blob/main/CHANGELOG.md\n")
@pytest.mark.parametrize(
"raw",
[
"",
"Agent failed",
_raw(""),
_raw("## Highlights\n- Wrong heading (#1)"),
_raw("## Bug fixes\n## Major new features\n- Wrong order (#1)"),
_raw("## Bug fixes\n## Bug fixes\n- Duplicate headings (#1)"),
_raw("## Bug fixes\n- Unknown PR (#999)"),
_raw("## Bug fixes\n- Missing PR citation"),
_raw("## Bug fixes\n- Fixed (#1)\n\n## Other contributions\n- Other (#2)"),
],
)
def test_unavailable_or_invalid_highlights_keep_every_credit(raw: str) -> None:
assert compose_notes(raw, CREDITS, REPO) == compose_notes("", CREDITS, REPO)
notes = compose_notes(raw, CREDITS, REPO)
assert notes.startswith("### 💜 Thanks to our community\n")
assert "Other contributions" not in notes
assert "/pull/" not in notes
for credit in CREDITS:
assert f"[@{credit['author']}]({credit['author_url']})" in notes
def test_highlight_credits_use_github_authors_and_deduplicate() -> None:
notes = compose_notes(_raw("## Bug fixes\n- Fixed issues (#2, #5, #2, @wrong)"), CREDITS, REPO)
summary, thanks = notes.split("### 💜 Thanks to our community")
assert "@wrong" not in notes
assert summary.count("[@bob]") == 1
assert summary.count("/pull/2)") == 1
assert summary.count("/pull/5)") == 1
assert "/pull/" not in thanks
def test_all_highlighted_omits_empty_other_section() -> None:
notes = compose_notes(_raw("## Bug fixes\n- Fixed (#1)"), CREDITS[:1], REPO)
assert "Other contributions" not in notes
assert "Full Changelog:" in notes
@pytest.mark.parametrize("raw", ["", _raw("## Bug fixes\n- Fixed (#3)")])
def test_community_handles_bots_and_deleted_authors(raw: str) -> None:
credits = [
{"pr": 3, "author": "", "author_url": ""},
{"pr": 2, "author": "bot[bot]", "author_url": "https://github.com/apps/bot"},
{"pr": 1, "author": "bot[bot]", "author_url": "https://github.com/apps/bot"},
]
notes = compose_notes(raw, credits, REPO)
assert "Author unavailable" not in notes
assert "Other contributions" not in notes
assert "/pull/1)" not in notes and "/pull/2)" not in notes
assert ("/pull/3)" in notes) == bool(raw)
assert notes.count("[@bot[bot]](https://github.com/apps/bot)") == 1
@pytest.mark.parametrize("raw", ["", _raw("## Bug fixes\n- Fixed (#1, #2, #3, #4, #5)")])
def test_community_credits_all_authors_once_in_alphabetical_order(raw: str) -> None:
credits = [
{"pr": pr, "author": author, "author_url": url}
for pr, author, url in [
(1, "zed", "https://github.com/zed"),
(2, "alice", "https://github.com/alice"),
(3, "Bob", "https://github.com/Bob"),
(4, "ALICE", "https://github.com/ALICE"),
(5, "", ""),
(6, "bot[bot]", "https://github.com/apps/bot"),
]
]
notes = compose_notes(raw, credits, REPO)
assert notes.count("### 💜 Thanks to our community") == 1
thanks, footer = notes.split("### 💜 Thanks to our community\n\n")[1].split(
"\n\nFull Changelog:"
)
assert thanks.split("\n\n")[-1] == (
"[@ALICE](https://github.com/ALICE), [@Bob](https://github.com/Bob), "
"[@bot[bot]](https://github.com/apps/bot), [@zed](https://github.com/zed)"
)
assert "Author unavailable" not in thanks
assert footer.strip() == "https://github.com/o/o/blob/main/CHANGELOG.md"
def test_community_note_without_known_authors() -> None:
notes = compose_notes("", [{"pr": 1, "author": "", "author_url": ""}], REPO)
thanks = notes.split("### 💜 Thanks to our community\n\n")[1]
assert "Thank you for building omnigent with us" in thanks
assert "[@" not in thanks
assert "Author unavailable" not in thanks
def test_empty_release_keeps_community_note_and_changelog_link() -> None:
notes = compose_notes("", [], REPO)
assert notes.startswith("### 💜 Thanks to our community\n")
assert "Other contributions" not in notes
assert "[@" not in notes
assert notes.endswith("Full Changelog: https://github.com/o/o/blob/main/CHANGELOG.md\n")
def test_cli_missing_highlights_uses_community_fallback(tmp_path: Path) -> None:
credits = tmp_path / "credits.json"
credits.write_text(json.dumps(CREDITS))
out = tmp_path / "notes.md"
result = subprocess.run(
[
sys.executable,
str(SCRIPT),
"--highlights",
str(tmp_path / "missing.txt"),
"--credits",
str(credits),
"--repo",
REPO,
"--out",
str(out),
],
check=True,
capture_output=True,
text=True,
)
assert out.read_text() == compose_notes("", CREDITS, REPO)
assert (
"::warning::Release highlights omitted: drafter output is empty or unavailable"
in result.stderr
)
@pytest.mark.parametrize(
("raw", "reason"),
[
("Agent failed", "RELEASE_NOTES markers are missing"),
(_raw(""), "the RELEASE_NOTES block is empty"),
(_raw("## Highlights\n- Fix (#1)"), "highlight headings"),
(_raw("## Bug fixes\n- Fixed (#1)\n wrapped text"), "highlight line 3"),
(
_raw("## Bug fixes\n- Fixed (#1) with more text"),
"must be a bullet ending in PR citations",
),
(_raw("## Bug fixes\n- Fixed (#999)"), "cites a PR outside the release"),
(_raw("## Bug fixes"), "contains no highlight bullets"),
],
)
def test_fallback_warning_explains_reason_and_preserves_credits(raw, reason, capsys) -> None:
notes = compose_notes(raw, CREDITS, REPO, warn_on_fallback=True)
assert notes == compose_notes("", CREDITS, REPO)
warning = capsys.readouterr().err
assert "::warning::Release highlights omitted:" in warning
assert reason in warning
assert "Keeping community thanks and the full changelog link" in warning
def test_valid_highlights_and_intentional_fallback_do_not_warn(capsys) -> None:
compose_notes(_raw("## Bug fixes\n- Fixed (#1)"), CREDITS, REPO, warn_on_fallback=True)
compose_notes("", CREDITS, REPO)
assert capsys.readouterr().err == ""
def test_repeated_pr_across_highlights_falls_back_with_reason(capsys) -> None:
raw = _raw("## Major new features\n- Feature (#1)\n## Bug fixes\n- Fix (#1)")
assert compose_notes(raw, CREDITS, REPO, warn_on_fallback=True) == compose_notes(
"", CREDITS, REPO
)
assert "repeats an already-cited PR" in capsys.readouterr().err
def test_cli_invalid_utf8_preserves_community_fallback(tmp_path: Path) -> None:
credits = tmp_path / "credits.json"
credits.write_text(json.dumps(CREDITS))
highlights = tmp_path / "highlights.txt"
highlights.write_bytes(b"\xff")
out = tmp_path / "notes.md"
result = subprocess.run(
[
sys.executable,
str(SCRIPT),
"--highlights",
str(highlights),
"--credits",
str(credits),
"--repo",
REPO,
"--out",
str(out),
],
capture_output=True,
text=True,
check=True,
)
assert "RELEASE_NOTES markers are missing" in result.stderr
assert out.read_text() == compose_notes("", CREDITS, REPO)