diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index edf96ce01f..f2976d5dc1 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -177,6 +177,8 @@ jobs: steps: - name: SCM Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup Python & Poetry Environment uses: ./.github/actions/python-environment diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index 79e701b844..1c76e60609 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -1 +1,5 @@ # Unreleased + +## Refactoring + +* #498: Centralized changelog code relevant for `release:trigger` & robustly tested diff --git a/exasol/toolbox/git.py b/exasol/toolbox/git.py deleted file mode 100644 index 6b0021cd45..0000000000 --- a/exasol/toolbox/git.py +++ /dev/null @@ -1,14 +0,0 @@ -import subprocess -from collections.abc import Iterable - - -def tags() -> Iterable[str]: - """ - Returns a list of all tags, sorted from [0] oldest to [-1] newest. - PreConditions: - - the git cli tool is installed and can be found via `$PATH` - - the code is executed where the working directory is within a git repository - """ - command = ["git", "tag", "--sort=committerdate"] - result = subprocess.run(command, capture_output=True, check=True) - return [tag.strip() for tag in result.stdout.decode("utf-8").splitlines()] diff --git a/exasol/toolbox/nox/_dependencies.py b/exasol/toolbox/nox/_dependencies.py index 95f827f0ae..57ac718916 100644 --- a/exasol/toolbox/nox/_dependencies.py +++ b/exasol/toolbox/nox/_dependencies.py @@ -12,10 +12,7 @@ licenses, packages_to_markdown, ) -from exasol.toolbox.util.dependencies.poetry_dependencies import ( - PoetryDependencies, - PoetryToml, -) +from exasol.toolbox.util.dependencies.poetry_dependencies import get_dependencies class Audit: @@ -86,12 +83,8 @@ def run(self, session: Session) -> None: @nox.session(name="dependency:licenses", python=False) def dependency_licenses(session: Session) -> None: - """returns the packages and their licenses""" - working_directory = Path() - poetry_dep = PoetryToml.load_from_toml(working_directory=working_directory) - dependencies = PoetryDependencies( - groups=poetry_dep.groups, working_directory=working_directory - ).direct_dependencies + """Return the packages with their licenses""" + dependencies = get_dependencies(working_directory=Path()) package_infos = licenses() print(packages_to_markdown(dependencies=dependencies, packages=package_infos)) diff --git a/exasol/toolbox/nox/_release.py b/exasol/toolbox/nox/_release.py index 63f3c372d9..42482914e8 100644 --- a/exasol/toolbox/nox/_release.py +++ b/exasol/toolbox/nox/_release.py @@ -13,12 +13,8 @@ _version, ) from exasol.toolbox.nox.plugin import NoxTasks -from exasol.toolbox.release import ( - extract_release_notes, - new_changelog, - new_changes, - new_unreleased, -) +from exasol.toolbox.util.git import Git +from exasol.toolbox.util.release.changelog import Changelogs from exasol.toolbox.util.version import ( ReleaseTypes, Version, @@ -61,32 +57,12 @@ def _create_parser() -> argparse.ArgumentParser: return parser -def _is_valid_version(old: Version, new: Version) -> bool: - return new >= old - - def _update_project_version(session: Session, version: Version) -> Version: session.run("poetry", "version", f"{version}") _version(session, Mode.Fix) return version -def _update_changelog(version: Version) -> tuple[Path, Path, Path]: - unreleased = Path(PROJECT_CONFIG.root) / "doc" / "changes" / "unreleased.md" - changelog = Path(PROJECT_CONFIG.root) / "doc" / "changes" / f"changes_{version}.md" - changes = Path(PROJECT_CONFIG.root) / "doc" / "changes" / f"changelog.md" - - changelog_content = extract_release_notes(unreleased) - changelog.write_text(new_changelog(version, changelog_content)) - - unreleased.write_text(new_unreleased()) - - changes_content = new_changes(changes, version) - changes.write_text(changes_content) - - return changelog, changes, unreleased - - def _add_files_to_index(session: Session, files: list[Path]) -> None: for file in files: session.run("git", "add", f"{file}") @@ -131,21 +107,24 @@ def run(*args: str): @nox.session(name="release:prepare", python=False) def prepare_release(session: Session) -> None: """ - Prepares the project for a new release. + Prepare the project for a new release. """ parser = _create_parser() args = parser.parse_args(session.posargs) - new_version = Version.upgrade_version_from_poetry(args.type) if not args.no_branch and not args.no_add: - session.run("git", "switch", "-c", f"release/prepare-{new_version}") - - pm = NoxTasks.plugin_manager(PROJECT_CONFIG) + Git.create_and_switch_to_branch(f"release/prepare-{new_version}") _ = _update_project_version(session, new_version) - changelog, changes, unreleased = _update_changelog(new_version) + changelogs = Changelogs( + changes_path=PROJECT_CONFIG.doc / "changes", version=new_version + ) + changelogs.update_changelogs_for_release() + changed_files = changelogs.get_changed_files() + + pm = NoxTasks.plugin_manager(PROJECT_CONFIG) pm.hook.prepare_release_update_version( session=session, config=PROJECT_CONFIG, version=new_version ) @@ -153,18 +132,15 @@ def prepare_release(session: Session) -> None: if args.no_add: return - files = [ - changelog, - unreleased, - changes, + changed_files += [ PROJECT_CONFIG.root / "pyproject.toml", PROJECT_CONFIG.version_file, ] results = pm.hook.prepare_release_add_files(session=session, config=PROJECT_CONFIG) - files += [f for plugin_response in results for f in plugin_response] + changed_files += [f for plugin_response in results for f in plugin_response] _add_files_to_index( session, - files, + changed_files, ) session.run("git", "commit", "-m", f"Prepare release {new_version}") diff --git a/exasol/toolbox/release/__init__.py b/exasol/toolbox/release/__init__.py deleted file mode 100644 index a20e24d0df..0000000000 --- a/exasol/toolbox/release/__init__.py +++ /dev/null @@ -1,85 +0,0 @@ -from __future__ import annotations - -from datetime import datetime -from inspect import cleandoc -from pathlib import Path - -from exasol.toolbox.util.version import Version - - -def extract_release_notes(file: str | Path) -> str: - """ - Extract release notes from a given file. - - Args: - file: from which the release notes shall be extracted - - Returns: - A string containing the cleaned release notes extracted from the file. - """ - with open(file) as f: - lines = f.readlines()[1:] - content = "".join(lines) - content = cleandoc(content) - content += "\n" - return content - - -def new_changes(file: str | Path, version: Version) -> str: - """ - Create a new changelog list, adding the provided version to it. - - Args: - file: containing the old changelog list. - version: of the new entry to add to the list. - - Returns: - Content for the new changelog list. - """ - file = Path(file) - content = [] - - with open(file) as f: - for line in f: - content.append(line) - if line.startswith("* [unreleased]"): - content.append(f"* [{version}](changes_{version}.md)\n") - if line.startswith("unreleased"): - content.append(f"changes_{version}\n") - - return "".join(content) - - -def new_unreleased() -> str: - """ - Generates the content for a new Unreleased changelog file. - - Returns: - A string representing the content for an empty Unreleased changelog file. - """ - return "# Unreleased\n" - - -def new_changelog(version: Version, content: str, date: datetime | None = None) -> str: - """ - Create a changelog entry for a specific version. - - Args: - version: An instance of the Version class representing the release version. - content: The content of the changelog entry. - date: Optional. The release date. If not provided, the current date will be used. - - Returns: - The generated changelog. - """ - date = datetime.today() if date is None else date - template = cleandoc( - """ - # {version} - {date} - - {content} - """ - ) - return template.format( - version=version, date=date.strftime("%Y-%m-%d"), content=content - ) diff --git a/exasol/toolbox/util/dependencies/poetry_dependencies.py b/exasol/toolbox/util/dependencies/poetry_dependencies.py index 3a72f68b69..768f1ffa4c 100644 --- a/exasol/toolbox/util/dependencies/poetry_dependencies.py +++ b/exasol/toolbox/util/dependencies/poetry_dependencies.py @@ -1,6 +1,7 @@ from __future__ import annotations import subprocess +import tempfile from pathlib import Path from typing import Optional @@ -12,6 +13,7 @@ from tomlkit import TOMLDocument from exasol.toolbox.util.dependencies.shared_models import Package +from exasol.toolbox.util.git import Git class PoetryGroup(BaseModel): @@ -21,6 +23,7 @@ class PoetryGroup(BaseModel): toml_section: Optional[str] +PYPROJECT_TOML = "pyproject.toml" TRANSITIVE_GROUP = PoetryGroup(name="transitive", toml_section=None) @@ -31,7 +34,7 @@ class PoetryToml(BaseModel): @classmethod def load_from_toml(cls, working_directory: Path) -> PoetryToml: - file_path = working_directory / "pyproject.toml" + file_path = working_directory / PYPROJECT_TOML if not file_path.exists(): raise ValueError(f"File not found: {file_path}") @@ -142,3 +145,21 @@ def all_dependencies(self) -> dict[str, list[Package]]: transitive_dependencies.append(dep) return direct_dependencies | {TRANSITIVE_GROUP.name: transitive_dependencies} + + +def get_dependencies(working_directory: Path) -> dict[str, list[Package]]: + poetry_dep = PoetryToml.load_from_toml(working_directory=working_directory) + return PoetryDependencies( + groups=poetry_dep.groups, working_directory=working_directory + ).direct_dependencies + + +def get_dependencies_from_latest_tag() -> dict[str, list[Package]]: + latest_tag = Git.get_latest_tag() + with tempfile.TemporaryDirectory() as path: + tmpdir = Path(path) + + Git.copy_remote_file_locally(latest_tag, "poetry.lock", tmpdir) + Git.copy_remote_file_locally(latest_tag, PYPROJECT_TOML, tmpdir) + + return get_dependencies(working_directory=tmpdir) diff --git a/exasol/toolbox/util/dependencies/shared_models.py b/exasol/toolbox/util/dependencies/shared_models.py index c129ff4db5..f7e7514582 100644 --- a/exasol/toolbox/util/dependencies/shared_models.py +++ b/exasol/toolbox/util/dependencies/shared_models.py @@ -1,22 +1,22 @@ from __future__ import annotations +from typing import Annotated + from packaging.version import Version from pydantic import ( + AfterValidator, BaseModel, ConfigDict, - field_validator, ) +VERSION_TYPE = Annotated[str, AfterValidator(lambda v: Version(v))] + class Package(BaseModel): model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) name: str - version: Version - - @field_validator("version", mode="before") - def convert_version(cls, v: str) -> Version: - return Version(v) + version: VERSION_TYPE @property def normalized_name(self) -> str: diff --git a/exasol/toolbox/util/git.py b/exasol/toolbox/util/git.py new file mode 100644 index 0000000000..744ed3155a --- /dev/null +++ b/exasol/toolbox/util/git.py @@ -0,0 +1,50 @@ +import subprocess # nosec +from functools import wraps +from pathlib import Path + + +def run_command(func): + @wraps(func) + def wrapper(*args, **kwargs) -> str: + command_list = func(*args, **kwargs) + output = subprocess.run( + command_list, capture_output=True, text=True, check=True + ) # nosec + return output.stdout.strip() + + return wrapper + + +class Git: + @staticmethod + @run_command + def get_latest_tag(): + """ + Get the latest tag from the git repository. + """ + return ["git", "describe", "--tags", "--abbrev=0"] + + @staticmethod + @run_command + def read_file_from_tag(tag: str, remote_file: str): + """ + Read the contents of the specified file `remote_file` at the point in time + specified by git tag `tag`. + """ + return ["git", "cat-file", "blob", f"{tag}:{remote_file}"] + + @staticmethod + def copy_remote_file_locally( + tag: str, remote_file: str, destination_directory: Path + ) -> None: + """ + Copy the contents of the specified file `remote_file` at the point in time + specified by git tag `tag` and copy it into the local `destination_directory/remote_file`. + """ + contents = Git.read_file_from_tag(tag=tag, remote_file=remote_file) + (destination_directory / remote_file).write_text(contents) + + @staticmethod + @run_command + def create_and_switch_to_branch(branch_name: str): + return ["git", "switch", "-c", branch_name] diff --git a/exasol/toolbox/util/release/__init__.py b/exasol/toolbox/util/release/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exasol/toolbox/util/release/changelog.py b/exasol/toolbox/util/release/changelog.py new file mode 100644 index 0000000000..b63e928e07 --- /dev/null +++ b/exasol/toolbox/util/release/changelog.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +from datetime import datetime +from inspect import cleandoc +from pathlib import Path + +from exasol.toolbox.util.version import Version + +UNRELEASED_INITIAL_CONTENT = "# Unreleased\n" + + +class Changelogs: + def __init__(self, changes_path: Path, version: Version) -> None: + self.version = version + self.unreleased_md: Path = changes_path / "unreleased.md" + self.versioned_changelog_md: Path = changes_path / f"changes_{version}.md" + self.changelog_md: Path = changes_path / "changelog.md" + + def _create_new_unreleased(self): + """ + Write a new unreleased changelog file. + """ + self.unreleased_md.write_text(UNRELEASED_INITIAL_CONTENT) + + def _create_versioned_changelog(self, content: str) -> None: + """ + Create a changelog entry for a specific version. + + Args: + content: The content of the changelog entry. + + """ + template = cleandoc( + f""" + # {self.version} - {datetime.today().strftime("%Y-%m-%d")} + + {content} + """ + ) + self.versioned_changelog_md.write_text(template) + + def _extract_unreleased_notes(self) -> str: + """ + Extract release notes from `unreleased.md`. + """ + with self.unreleased_md.open(mode="r", encoding="utf-8") as f: + # skip header when reading in file, as contains # Unreleased + lines = f.readlines()[1:] + unreleased_content = cleandoc("".join(lines)) + unreleased_content += "\n" + return unreleased_content + + def _update_changelog_table_of_contents(self) -> None: + """ + Read in existing `changelog.md` and append to appropriate sections + before writing out to again. + """ + updated_content = [] + with self.changelog_md.open(mode="r", encoding="utf-8") as f: + for line in f: + updated_content.append(line) + if line.startswith("* [unreleased]"): + updated_content.append( + f"* [{self.version}](changes_{self.version}.md)\n" + ) + if line.startswith("unreleased"): + updated_content.append(f"changes_{self.version}\n") + updated_content_str = "".join(updated_content) + + self.changelog_md.write_text(updated_content_str) + + def get_changed_files(self) -> list[Path]: + return [self.unreleased_md, self.versioned_changelog_md, self.changelog_md] + + def update_changelogs_for_release(self) -> None: + """ + Rotates the changelogs as is needed for a release. + + 1. Moves the contents of the `unreleased.md` to the `changes_.md` + 2. Create a new file `unreleased.md` + 3. Updates the table of contents in the `changelog.md` with the new `changes_.md` + """ + + # create versioned changelog + unreleased_content = self._extract_unreleased_notes() + self._create_versioned_changelog(unreleased_content) + + # update other changelogs now that versioned changelog exists + self._create_new_unreleased() + self._update_changelog_table_of_contents() diff --git a/poetry.lock b/poetry.lock index 6ce853ce44..29e60b766a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,24 @@ # This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +[[package]] +name = "accessible-pygments" +version = "0.0.5" +description = "A collection of accessible pygments styles" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7"}, + {file = "accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872"}, +] + +[package.dependencies] +pygments = ">=1.5" + +[package.extras] +dev = ["pillow", "pkginfo (>=1.10)", "playwright", "pre-commit", "setuptools", "twine (>=5.0)"] +tests = ["hypothesis", "pytest"] + [[package]] name = "alabaster" version = "0.7.16" @@ -61,14 +80,14 @@ test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock [[package]] name = "astroid" -version = "3.3.10" +version = "3.3.11" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.9.0" groups = ["main"] files = [ - {file = "astroid-3.3.10-py3-none-any.whl", hash = "sha256:104fb9cb9b27ea95e847a94c003be03a9e039334a8ebca5ee27dafaf5c5711eb"}, - {file = "astroid-3.3.10.tar.gz", hash = "sha256:c332157953060c6deb9caa57303ae0d20b0fbdb2e59b4a4f2a6ba49d0a7961ce"}, + {file = "astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec"}, + {file = "astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce"}, ] [package.dependencies] @@ -298,14 +317,14 @@ redis = ["redis (>=2.10.5)"] [[package]] name = "certifi" -version = "2025.7.9" +version = "2025.7.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39"}, - {file = "certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079"}, + {file = "certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2"}, + {file = "certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995"}, ] [[package]] @@ -671,14 +690,14 @@ profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "distlib" -version = "0.3.9" +version = "0.4.0" description = "Distribution utilities" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, - {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, + {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, + {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, ] [[package]] @@ -731,17 +750,18 @@ typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "furo" -version = "2024.8.6" +version = "2025.7.19" description = "A clean customisable Sphinx documentation theme." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c"}, - {file = "furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01"}, + {file = "furo-2025.7.19-py3-none-any.whl", hash = "sha256:bdea869822dfd2b494ea84c0973937e35d1575af088b6721a29c7f7878adc9e3"}, + {file = "furo-2025.7.19.tar.gz", hash = "sha256:4164b2cafcf4023a59bb3c594e935e2516f6b9d35e9a5ea83d8f6b43808fe91f"}, ] [package.dependencies] +accessible-pygments = ">=0.0.5" beautifulsoup4 = "*" pygments = ">=2.7" sphinx = ">=6.0,<9.0" @@ -1295,44 +1315,44 @@ files = [ [[package]] name = "mypy" -version = "1.16.1" +version = "1.17.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a"}, - {file = "mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72"}, - {file = "mypy-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7469ee5902c95542bea7ee545f7006508c65c8c54b06dc2c92676ce526f3ea"}, - {file = "mypy-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352025753ef6a83cb9e7f2427319bb7875d1fdda8439d1e23de12ab164179574"}, - {file = "mypy-1.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff9fa5b16e4c1364eb89a4d16bcda9987f05d39604e1e6c35378a2987c1aac2d"}, - {file = "mypy-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:1256688e284632382f8f3b9e2123df7d279f603c561f099758e66dd6ed4e8bd6"}, - {file = "mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc"}, - {file = "mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782"}, - {file = "mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507"}, - {file = "mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca"}, - {file = "mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4"}, - {file = "mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6"}, - {file = "mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d"}, - {file = "mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9"}, - {file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79"}, - {file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15"}, - {file = "mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd"}, - {file = "mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b"}, - {file = "mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438"}, - {file = "mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536"}, - {file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f"}, - {file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359"}, - {file = "mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be"}, - {file = "mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee"}, - {file = "mypy-1.16.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7fc688329af6a287567f45cc1cefb9db662defeb14625213a5b7da6e692e2069"}, - {file = "mypy-1.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e198ab3f55924c03ead626ff424cad1732d0d391478dfbf7bb97b34602395da"}, - {file = "mypy-1.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09aa4f91ada245f0a45dbc47e548fd94e0dd5a8433e0114917dc3b526912a30c"}, - {file = "mypy-1.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13c7cd5b1cb2909aa318a90fd1b7e31f17c50b242953e7dd58345b2a814f6383"}, - {file = "mypy-1.16.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:58e07fb958bc5d752a280da0e890c538f1515b79a65757bbdc54252ba82e0b40"}, - {file = "mypy-1.16.1-cp39-cp39-win_amd64.whl", hash = "sha256:f895078594d918f93337a505f8add9bd654d1a24962b4c6ed9390e12531eb31b"}, - {file = "mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37"}, - {file = "mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab"}, + {file = "mypy-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8e08de6138043108b3b18f09d3f817a4783912e48828ab397ecf183135d84d6"}, + {file = "mypy-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce4a17920ec144647d448fc43725b5873548b1aae6c603225626747ededf582d"}, + {file = "mypy-1.17.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ff25d151cc057fdddb1cb1881ef36e9c41fa2a5e78d8dd71bee6e4dcd2bc05b"}, + {file = "mypy-1.17.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93468cf29aa9a132bceb103bd8475f78cacde2b1b9a94fd978d50d4bdf616c9a"}, + {file = "mypy-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:98189382b310f16343151f65dd7e6867386d3e35f7878c45cfa11383d175d91f"}, + {file = "mypy-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:c004135a300ab06a045c1c0d8e3f10215e71d7b4f5bb9a42ab80236364429937"}, + {file = "mypy-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d4fe5c72fd262d9c2c91c1117d16aac555e05f5beb2bae6a755274c6eec42be"}, + {file = "mypy-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96b196e5c16f41b4f7736840e8455958e832871990c7ba26bf58175e357ed61"}, + {file = "mypy-1.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73a0ff2dd10337ceb521c080d4147755ee302dcde6e1a913babd59473904615f"}, + {file = "mypy-1.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cfcc1179c4447854e9e406d3af0f77736d631ec87d31c6281ecd5025df625d"}, + {file = "mypy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56f180ff6430e6373db7a1d569317675b0a451caf5fef6ce4ab365f5f2f6c3"}, + {file = "mypy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:eafaf8b9252734400f9b77df98b4eee3d2eecab16104680d51341c75702cad70"}, + {file = "mypy-1.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f986f1cab8dbec39ba6e0eaa42d4d3ac6686516a5d3dccd64be095db05ebc6bb"}, + {file = "mypy-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51e455a54d199dd6e931cd7ea987d061c2afbaf0960f7f66deef47c90d1b304d"}, + {file = "mypy-1.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3204d773bab5ff4ebbd1f8efa11b498027cd57017c003ae970f310e5b96be8d8"}, + {file = "mypy-1.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1051df7ec0886fa246a530ae917c473491e9a0ba6938cfd0ec2abc1076495c3e"}, + {file = "mypy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f773c6d14dcc108a5b141b4456b0871df638eb411a89cd1c0c001fc4a9d08fc8"}, + {file = "mypy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:1619a485fd0e9c959b943c7b519ed26b712de3002d7de43154a489a2d0fd817d"}, + {file = "mypy-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c41aa59211e49d717d92b3bb1238c06d387c9325d3122085113c79118bebb06"}, + {file = "mypy-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e69db1fb65b3114f98c753e3930a00514f5b68794ba80590eb02090d54a5d4a"}, + {file = "mypy-1.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03ba330b76710f83d6ac500053f7727270b6b8553b0423348ffb3af6f2f7b889"}, + {file = "mypy-1.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037bc0f0b124ce46bfde955c647f3e395c6174476a968c0f22c95a8d2f589bba"}, + {file = "mypy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c38876106cb6132259683632b287238858bd58de267d80defb6f418e9ee50658"}, + {file = "mypy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:d30ba01c0f151998f367506fab31c2ac4527e6a7b2690107c7a7f9e3cb419a9c"}, + {file = "mypy-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:63e751f1b5ab51d6f3d219fe3a2fe4523eaa387d854ad06906c63883fde5b1ab"}, + {file = "mypy-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f7fb09d05e0f1c329a36dcd30e27564a3555717cde87301fae4fb542402ddfad"}, + {file = "mypy-1.17.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b72c34ce05ac3a1361ae2ebb50757fb6e3624032d91488d93544e9f82db0ed6c"}, + {file = "mypy-1.17.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434ad499ad8dde8b2f6391ddfa982f41cb07ccda8e3c67781b1bfd4e5f9450a8"}, + {file = "mypy-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f105f61a5eff52e137fd73bee32958b2add9d9f0a856f17314018646af838e97"}, + {file = "mypy-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:ba06254a5a22729853209550d80f94e28690d5530c661f9416a68ac097b13fc4"}, + {file = "mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496"}, + {file = "mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03"}, ] [package.dependencies] @@ -1667,14 +1687,14 @@ pytest-plugin = ["pytest-prysk (>=0.2.0,<0.3.0)"] [[package]] name = "py-serializable" -version = "2.0.0" +version = "2.1.0" description = "Library for serializing and deserializing Python Objects to and from JSON and XML." optional = false python-versions = "<4.0,>=3.8" groups = ["main"] files = [ - {file = "py_serializable-2.0.0-py3-none-any.whl", hash = "sha256:1721e4c0368adeec965c183168da4b912024702f19e15e13f8577098b9a4f8fe"}, - {file = "py_serializable-2.0.0.tar.gz", hash = "sha256:e9e6491dd7d29c31daf1050232b57f9657f9e8a43b867cca1ff204752cf420a5"}, + {file = "py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304"}, + {file = "py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103"}, ] [package.dependencies] @@ -2192,14 +2212,14 @@ files = [ [[package]] name = "shibuya" -version = "2025.5.30" +version = "2025.7.14" description = "A clean, responsive, and customizable Sphinx documentation theme with light/dark mode." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "shibuya-2025.5.30-py3-none-any.whl", hash = "sha256:9b57bb87196a5fb358aa75c6c1a1119e6a76fde2e3c961f88db5647b042075d4"}, - {file = "shibuya-2025.5.30.tar.gz", hash = "sha256:6ee0ca3af30d61ba240abe212b0ded21dc7542cf050c11ace7a28df9251171ae"}, + {file = "shibuya-2025.7.14-py3-none-any.whl", hash = "sha256:8103f4cf4149de39b745b3a4adda072beba06aeb4445ff44df401a2c6b7da5b7"}, + {file = "shibuya-2025.7.14.tar.gz", hash = "sha256:1a6309cb5f4d41af912d50d22429b473e33042cb089bf956ab9e1f97eec37acc"}, ] [package.dependencies] @@ -2655,14 +2675,14 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.31.2" +version = "20.32.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11"}, - {file = "virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af"}, + {file = "virtualenv-20.32.0-py3-none-any.whl", hash = "sha256:2c310aecb62e5aa1b06103ed7c2977b81e042695de2697d01017ff0f1034af56"}, + {file = "virtualenv-20.32.0.tar.gz", hash = "sha256:886bf75cadfdc964674e6e33eb74d787dff31ca314ceace03ca5810620f4ecf0"}, ] [package.dependencies] diff --git a/test/unit/git_test.py b/test/unit/git_test.py deleted file mode 100644 index 46dfd07d9f..0000000000 --- a/test/unit/git_test.py +++ /dev/null @@ -1,54 +0,0 @@ -import subprocess -from inspect import cleandoc -from unittest.mock import patch - -import pytest - -from exasol.toolbox import git - - -@pytest.fixture -def git_tag(): - result = subprocess.CompletedProcess( - args=["git", "tag", "--sort=comitterdate"], - returncode=0, - stdout=cleandoc( - """ - 0.0.1 - 0.6.2 - 0.1.0 - 0.2.0 - 0.5.0 - 0.3.0 - 0.4.0 - 0.6.0 - 0.6.1 - 0.8.0 - 0.7.0 - """ - ).encode("utf8"), - stderr="", - ) - yield result - - -def test_git_tags(git_tag): - expected = sorted( - [ - "0.0.1", - "0.1.0", - "0.2.0", - "0.3.0", - "0.4.0", - "0.5.0", - "0.6.0", - "0.6.1", - "0.6.2", - "0.7.0", - "0.8.0", - ] - ) - with patch("subprocess.run", return_value=git_tag): - actual = sorted(git.tags()) - - assert expected == actual diff --git a/test/unit/release_test.py b/test/unit/release_test.py index 32bb74ca18..3fe66c38f5 100644 --- a/test/unit/release_test.py +++ b/test/unit/release_test.py @@ -1,5 +1,3 @@ -from datetime import datetime -from inspect import cleandoc from subprocess import CalledProcessError from unittest.mock import ( MagicMock, @@ -12,87 +10,6 @@ ReleaseError, _trigger_release, ) -from exasol.toolbox.release import ( - extract_release_notes, - new_changelog, -) -from exasol.toolbox.util.version import Version - - -@pytest.mark.parametrize( - "version,content,date,expected", - [ - ( - Version(0, 1, 0), - cleandoc( - """ - ## Added - * Some great feature - - ## Refactored - * Replaced xyz - """ - ), - datetime(2024, 2, 7), - cleandoc( - """ - # 0.1.0 - 2024-02-07 - - ## Added - * Some great feature - - ## Refactored - * Replaced xyz - """ - ), - ), - ], -) -def test_changelog(version, content, date, expected): - actual = new_changelog(version, content, date) - assert expected == actual - - -@pytest.fixture -def unreleased_md(tmp_path): - file = tmp_path / "unreleased.md" - file.write_text( - cleandoc( - """ - # Unreleased - - ## ✨ Added - * Added Awesome feature - - ## 🔧 Changed - * Some behaviour - - ## 🐞 Fixed - * Fixed nasty bug - """ - ) - ) - yield file - - -def test_extract_release_notes(unreleased_md): - expected = ( - cleandoc( - """ - ## ✨ Added - * Added Awesome feature - - ## 🔧 Changed - * Some behaviour - - ## 🐞 Fixed - * Fixed nasty bug - """ - ) - + "\n" - ) - actual = extract_release_notes(unreleased_md) - assert expected == actual @pytest.fixture(scope="class") diff --git a/test/unit/util/dependencies/poetry_dependencies_test.py b/test/unit/util/dependencies/poetry_dependencies_test.py index e5b8212bf9..b44be56cdc 100644 --- a/test/unit/util/dependencies/poetry_dependencies_test.py +++ b/test/unit/util/dependencies/poetry_dependencies_test.py @@ -6,8 +6,11 @@ PoetryDependencies, PoetryGroup, PoetryToml, + get_dependencies, + get_dependencies_from_latest_tag, ) from exasol.toolbox.util.dependencies.shared_models import Package +from noxconfig import PROJECT_CONFIG MAIN_GROUP = PoetryGroup(name="main", toml_section="project.dependencies") DEV_GROUP = PoetryGroup(name="dev", toml_section="tool.poetry.group.dev.dependencies") @@ -128,3 +131,19 @@ def test_all_dependencies(create_poetry_project, project_path): transitive = result.pop("transitive") assert len(transitive) > 0 assert result == DIRECT_DEPENDENCIES + + +def test_get_dependencies(): + result = get_dependencies(PROJECT_CONFIG.root) + + # if successful, no errors & should be non-empty dictionary + assert isinstance(result, dict) + assert result.keys() + + +def test_get_dependencies_from_latest_tag(): + result = get_dependencies_from_latest_tag() + + # if successful, no errors & should be non-empty dictionary + assert isinstance(result, dict) + assert result.keys() diff --git a/test/unit/util/dependencies/shared_models_test.py b/test/unit/util/dependencies/shared_models_test.py index 72e0cfa4d2..e16b78fc96 100644 --- a/test/unit/util/dependencies/shared_models_test.py +++ b/test/unit/util/dependencies/shared_models_test.py @@ -1,6 +1,34 @@ import pytest +from packaging.version import Version +from pydantic import BaseModel +from pydantic_core._pydantic_core import ValidationError -from exasol.toolbox.util.dependencies.shared_models import Package +from exasol.toolbox.util.dependencies.shared_models import ( + VERSION_TYPE, + Package, +) + + +class Dummy(BaseModel): + version: VERSION_TYPE + + +class TestVersionType: + @staticmethod + @pytest.mark.parametrize("input", [1.0, {}]) + def test_wrong_input_raises_error(input): + with pytest.raises(ValidationError, match="Input should be a valid string"): + Dummy(version=input) + + @staticmethod + def test_string_not_version_raises_error(): + with pytest.raises(ValidationError, match="Value error, Invalid version:"): + Dummy(version="string") + + @staticmethod + def test_works_as_expected(): + result = Dummy(version="1.0.0") + assert result.version == Version("1.0.0") class TestPackage: diff --git a/test/unit/util/git_test.py b/test/unit/util/git_test.py new file mode 100644 index 0000000000..b92dcf4e80 --- /dev/null +++ b/test/unit/util/git_test.py @@ -0,0 +1,39 @@ +import pytest +from packaging.version import Version + +from exasol.toolbox.util.git import Git + +POETRY_LOCK = "poetry.lock" + + +@pytest.fixture(scope="module") +def latest_tag() -> str: + return Git.get_latest_tag() + + +@pytest.fixture(scope="module") +def read_file_from_tag(latest_tag) -> str: + return Git.read_file_from_tag(tag=latest_tag, remote_file=POETRY_LOCK) + + +class TestGit: + @staticmethod + def test_latest_tag(latest_tag): + assert isinstance(latest_tag, str) + assert Version(latest_tag) + + @staticmethod + def test_read_file_from_tag(read_file_from_tag): + assert isinstance(read_file_from_tag, str) + assert read_file_from_tag != "" + + @staticmethod + def test_copy_remote_file_locally(tmp_path, read_file_from_tag): + latest_tag = Git.get_latest_tag() + + Git.copy_remote_file_locally( + tag=latest_tag, remote_file=POETRY_LOCK, destination_directory=tmp_path + ) + + result = (tmp_path / POETRY_LOCK).read_text() + assert result == read_file_from_tag diff --git a/test/unit/util/release/changelog_test.py b/test/unit/util/release/changelog_test.py new file mode 100644 index 0000000000..5237d86cfa --- /dev/null +++ b/test/unit/util/release/changelog_test.py @@ -0,0 +1,123 @@ +from inspect import cleandoc + +import pytest + +from exasol.toolbox.util.release.changelog import ( + UNRELEASED_INITIAL_CONTENT, + Changelogs, +) +from exasol.toolbox.util.version import Version + + +class SampleContent: + changelog = cleandoc( + """ + ## Added + * Added Awesome feature + + ## Changed + * Some behaviour + + ## Fixed + * Fixed nasty bug + """ + ) + changes = cleandoc( + """ + # Changelog + + * [unreleased](unreleased.md) + * [0.1.0](changes_0.1.0.md) + + ```{toctree} + --- + hidden: + --- + unreleased + changes_0.1.0 + ``` + """ + ) + altered_changes = cleandoc( + """ + # Changelog + + * [unreleased](unreleased.md) + * [1.0.0](changes_1.0.0.md) + * [0.1.0](changes_0.1.0.md) + + ```{toctree} + --- + hidden: + --- + unreleased + changes_1.0.0 + changes_0.1.0 + ``` + """ + ) + + +@pytest.fixture(scope="function") +def changes_md(changelogs): + changelogs.changelog_md.write_text(SampleContent.changes) + + +@pytest.fixture(scope="function") +def unreleased_md(changelogs): + changelogs.unreleased_md.write_text( + UNRELEASED_INITIAL_CONTENT + SampleContent.changelog + ) + + +@pytest.fixture(scope="function") +def changelogs(tmp_path) -> Changelogs: + return Changelogs(changes_path=tmp_path, version=Version(major=1, minor=0, patch=0)) + + +class TestChangelogs: + """ + As some methods in the class `Changelogs` modify files, it is required that the + fixtures which create the sample files (changelog.md, unreleased.md, & changes_1.0.0.md) + reset per function and use `tmp_path`. By doing this, we ensure that the sample + are in their expected state for each test. + """ + + @staticmethod + def test_create_new_unreleased(changelogs): + changelogs._create_new_unreleased() + + assert changelogs.unreleased_md.read_text() == UNRELEASED_INITIAL_CONTENT + + @staticmethod + def test_create_versioned_changelog(changelogs): + changelogs._create_versioned_changelog(SampleContent.changelog) + saved_text = changelogs.versioned_changelog_md.read_text() + + assert "1.0.0" in saved_text + assert SampleContent.changelog in saved_text + + @staticmethod + def test_extract_unreleased_notes(changelogs, unreleased_md): + result = changelogs._extract_unreleased_notes() + + assert result == SampleContent.changelog + "\n" + + @staticmethod + def test_update_changelog_table_of_contents(changelogs, changes_md): + changelogs._update_changelog_table_of_contents() + + assert changelogs.changelog_md.read_text() == SampleContent.altered_changes + + @staticmethod + def test_update_changelogs_for_release(changelogs, unreleased_md, changes_md): + changelogs.update_changelogs_for_release() + + # changes.md + assert changelogs.changelog_md.read_text() == SampleContent.altered_changes + # unreleased.md + assert changelogs.unreleased_md.read_text() == UNRELEASED_INITIAL_CONTENT + # versioned.md + saved_text = changelogs.versioned_changelog_md.read_text() + assert "1.0.0" in saved_text + assert SampleContent.changelog in saved_text