rlumod 1.0.0

Safe, no_std matrix updates and linear solves using the LUmod algorithm
Documentation
import contextlib
import io
import tempfile
import unittest
from pathlib import Path

from check_changelog import main, validate_changelog


VALID_CHANGELOG = """# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- An upcoming feature.

## [1.1.0] - 2024-02-02

### Fixed

- A released fix.

[Unreleased]: https://example.com/compare/v1.1.0...HEAD
[1.1.0]: https://example.com/releases/tag/v1.1.0
"""


class ValidateChangelogTests(unittest.TestCase):
    def validate(self, content: str) -> list[str]:
        with tempfile.TemporaryDirectory() as directory:
            path = Path(directory, "CHANGELOG.md")
            path.write_text(content, encoding="utf-8")
            return validate_changelog(path)

    def assert_invalid(self, content: str, expected: str) -> None:
        self.assertTrue(
            any(expected in error for error in self.validate(content)),
            f"expected an error containing {expected!r}",
        )

    def test_accepts_valid_changelog(self) -> None:
        self.assertEqual(self.validate(VALID_CHANGELOG), [])

    def test_rejects_missing_unreleased_section(self) -> None:
        content = VALID_CHANGELOG.replace("## [Unreleased]\n\n### Added\n\n- An upcoming feature.\n\n", "")
        content = content.replace(
            "[Unreleased]: https://example.com/compare/v1.1.0...HEAD\n", ""
        )
        self.assert_invalid(content, "Unreleased")

    def test_rejects_unknown_category(self) -> None:
        self.assert_invalid(
            VALID_CHANGELOG.replace("### Added", "### Improved"),
            "unsupported category",
        )

    def test_rejects_uncategorized_content(self) -> None:
        self.assert_invalid(
            VALID_CHANGELOG.replace(
                "## [Unreleased]\n", "## [Unreleased]\n\nUncategorized change."
            ),
            "uncategorized",
        )

    def test_rejects_non_semantic_version(self) -> None:
        self.assert_invalid(
            VALID_CHANGELOG.replace("1.1.0", "version-one"),
            "valid semantic version",
        )

    def test_rejects_invalid_release_date(self) -> None:
        self.assert_invalid(
            VALID_CHANGELOG.replace("2024-02-02", "02/02/2024"),
            "ISO 8601 date",
        )

    def test_rejects_missing_release_link(self) -> None:
        self.assert_invalid(
            VALID_CHANGELOG.replace(
                "[1.1.0]: https://example.com/releases/tag/v1.1.0\n", ""
            ),
            "reference link",
        )

    def test_rejects_duplicate_release(self) -> None:
        duplicate = "\n## [1.1.0] - 2024-02-02\n\n### Fixed\n\n- Duplicate.\n"
        self.assert_invalid(
            VALID_CHANGELOG.replace("\n[Unreleased]:", duplicate + "\n[Unreleased]:"),
            "duplicate release",
        )

    def test_rejects_release_dates_out_of_order(self) -> None:
        older_release = """
## [1.0.0] - 2024-03-03

### Added

- An older release.
"""
        content = VALID_CHANGELOG.replace(
            "\n[Unreleased]:",
            older_release + "\n[Unreleased]:",
        ).replace(
            "[1.1.0]: https://example.com/releases/tag/v1.1.0\n",
            "[1.1.0]: https://example.com/releases/tag/v1.1.0\n"
            "[1.0.0]: https://example.com/releases/tag/v1.0.0\n",
        )
        self.assert_invalid(content, "newest to oldest")

    def test_cli_reports_errors_and_returns_failure(self) -> None:
        with tempfile.TemporaryDirectory() as directory:
            path = Path(directory, "CHANGELOG.md")
            path.write_text("not a changelog\n", encoding="utf-8")
            stderr = io.StringIO()
            with contextlib.redirect_stderr(stderr):
                result = main([str(path)])

        self.assertEqual(result, 1)
        self.assertIn(f"{path}:", stderr.getvalue())


if __name__ == "__main__":
    unittest.main()