blob: 938c4ae6d6989c1b19a0ea0bb680d2d9597e00c8 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
import pytest
from github_rollup.rollup import (
ROLLUP_MARKER_PREFIX,
build_entry,
build_new_rollup_body,
iter_entries,
parse_summary_line,
rebuild_with_appended_entry,
)
MARKER_LINE = (
"<!-- airflow-s status rollup v1 — all bot-authored status updates fold into this single comment. -->"
)
# ---------------------------------------------------------------------------
# parse_summary_line
# ---------------------------------------------------------------------------
def test_parse_canonical_summary():
e = parse_summary_line("2026-05-30 · @potiuk · CVE allocated (CVE-2026-12345)")
assert e is not None
assert e.date == "2026-05-30"
assert e.user == "@potiuk"
assert e.action == "CVE allocated (CVE-2026-12345)"
def test_parse_with_extra_whitespace():
e = parse_summary_line(" 2026-05-30 · @potiuk · Sync pass ")
assert e is not None
assert e.date == "2026-05-30"
assert e.user == "@potiuk"
assert e.action == "Sync pass"
@pytest.mark.parametrize(
"summary",
[
"not a summary",
"2026-05-30 · @potiuk", # missing action
"26-05-30 · @potiuk · Sync", # bad date
"2026-13-99 · @potiuk · Sync", # invalid month/day but format matches
"2026-05-30 · potiuk · Sync", # missing @
"2026-05-30 · @ · Sync", # bare @
"2026-05-30 · @potiuk · ", # empty action
],
)
def test_parse_summary_rejects_invalid(summary):
# Some of the above (e.g. 2026-13-99) only fail format-wise, but
# the parser still returns None for the missing-action and bad-
# handle cases. The 2026-13-99 case passes the regex (4-2-2 digit
# groups) and is returned with raw fields — we don't validate
# calendar correctness here.
result = parse_summary_line(summary)
if "13-99" in summary:
assert result is not None # regex-permissive
return
assert result is None
# ---------------------------------------------------------------------------
# iter_entries
# ---------------------------------------------------------------------------
def test_iter_entries_single():
body = (
f"{MARKER_LINE}\n"
"<details><summary>2026-05-30 · @potiuk · CVE allocated</summary>\n"
"\n"
"Allocated CVE-2026-12345 via Vulnogram.\n"
"\n"
"</details>"
)
entries = iter_entries(body)
assert len(entries) == 1
e = entries[0]
assert e.date == "2026-05-30"
assert e.user == "@potiuk"
assert e.action == "CVE allocated"
assert "Allocated CVE-2026-12345" in e.body
def test_iter_entries_multiple():
body = (
f"{MARKER_LINE}\n"
"<details><summary>2026-05-28 · @potiuk · Import</summary>\n\n"
"First.\n\n"
"</details>\n"
"\n---\n\n"
"<details><summary>2026-05-30 · @potiuk · Sync</summary>\n\n"
"Second.\n\n"
"</details>"
)
entries = iter_entries(body)
assert len(entries) == 2
assert entries[0].action == "Import"
assert entries[1].action == "Sync"
assert "First." in entries[0].body
assert "Second." in entries[1].body
def test_iter_entries_no_marker_line():
"""If a caller passes a body without the marker (e.g. they pre-
stripped it), still parse what's there."""
body = "<details><summary>2026-05-30 · @potiuk · CVE allocated</summary>\n\nBody.\n\n</details>"
entries = iter_entries(body)
assert len(entries) == 1
def test_iter_entries_empty_body():
assert iter_entries("") == []
def test_iter_entries_missing_close_tag_tolerated():
body = (
f"{MARKER_LINE}\n"
"<details><summary>2026-05-30 · @potiuk · Sync</summary>\n\n"
"Body that was never closed."
)
entries = iter_entries(body)
assert len(entries) == 1
assert "Body that was never closed." in entries[0].body
def test_iter_entries_non_canonical_summary_preserved():
body = f"{MARKER_LINE}\n<details><summary>2026-05-30 · @potiuk · </summary>\n\nBody.\n\n</details>"
entries = iter_entries(body)
assert len(entries) == 1
# The summary was non-canonical (empty action), so the parsed
# fields are blank but the body is still returned for round-trip.
e = entries[0]
assert e.date == ""
assert e.user == ""
assert e.action == ""
assert "Body." in e.body
# ---------------------------------------------------------------------------
# build_entry
# ---------------------------------------------------------------------------
def test_build_entry_canonical():
e = build_entry(
date="2026-05-30",
user="@potiuk",
action="CVE allocated",
body="Allocated CVE-2026-12345.",
)
assert e == (
"<details><summary>2026-05-30 · @potiuk · CVE allocated</summary>\n"
"\n"
"Allocated CVE-2026-12345.\n"
"\n"
"</details>"
)
def test_build_entry_adds_at_prefix():
e = build_entry(date="2026-05-30", user="potiuk", action="Sync", body="x")
assert "· @potiuk ·" in e
def test_build_entry_strips_body_whitespace():
e = build_entry(date="2026-05-30", user="@a", action="b", body="\n\n body\n\n")
assert "\n\nbody\n\n</details>" in e
# ---------------------------------------------------------------------------
# build_new_rollup_body
# ---------------------------------------------------------------------------
def test_build_new_rollup_body_includes_marker_first():
entry = build_entry(date="2026-05-30", user="@a", action="b", body="c")
body = build_new_rollup_body(entry)
assert body.startswith(ROLLUP_MARKER_PREFIX)
assert "<details>" in body
# Marker line + immediately the entry — no blank line gap.
first_nl = body.find("\n")
assert body[first_nl + 1 :].startswith("<details>")
# ---------------------------------------------------------------------------
# rebuild_with_appended_entry
# ---------------------------------------------------------------------------
def test_rebuild_with_appended_entry_inserts_canonical_ruler():
existing = f"{MARKER_LINE}\n<details><summary>2026-05-28 · @a · Import</summary>\n\nA.\n\n</details>"
new = build_entry(date="2026-05-30", user="@b", action="Sync", body="B.")
out = rebuild_with_appended_entry(existing, new)
assert "</details>\n\n---\n\n<details>" in out
# Both entries are findable by iter_entries.
entries = iter_entries(out)
assert len(entries) == 2
assert entries[0].action == "Import"
assert entries[1].action == "Sync"
def test_rebuild_strips_trailing_whitespace_before_ruler():
existing = (
f"{MARKER_LINE}\n<details><summary>2026-05-28 · @a · Import</summary>\n\nA.\n\n</details>\n\n\n \n"
)
new = build_entry(date="2026-05-30", user="@b", action="Sync", body="B.")
out = rebuild_with_appended_entry(existing, new)
# Exactly the canonical ruler — no double blanks.
assert "</details>\n\n---\n\n<details>" in out
assert "</details>\n\n\n---" not in out
def test_round_trip_three_appends_keeps_count():
existing = build_new_rollup_body(build_entry(date="2026-05-01", user="@a", action="x1", body="b1"))
for i, action in enumerate(["x2", "x3"], start=2):
existing = rebuild_with_appended_entry(
existing,
build_entry(date=f"2026-05-0{i}", user="@a", action=action, body=f"b{i}"),
)
assert [e.action for e in iter_entries(existing)] == ["x1", "x2", "x3"]