diff --git a/semantic_release/changelog/release_history.py b/semantic_release/changelog/release_history.py index 483d2117e..f34fc3e51 100644 --- a/semantic_release/changelog/release_history.py +++ b/semantic_release/changelog/release_history.py @@ -81,7 +81,9 @@ def from_git_history( if isinstance(tag.object, TagObject): tagger = tag.object.tagger committer = tag.object.tagger.committer() - _tz = timezone(timedelta(seconds=tag.object.tagger_tz_offset)) + _tz = timezone( + timedelta(seconds=-1 * tag.object.tagger_tz_offset) + ) tagged_date = datetime.fromtimestamp( tag.object.tagged_date, tz=_tz ) @@ -89,7 +91,9 @@ def from_git_history( # For some reason, sometimes tag.object is a Commit tagger = tag.object.author committer = tag.object.author - _tz = timezone(timedelta(seconds=tag.object.author_tz_offset)) + _tz = timezone( + timedelta(seconds=-1 * tag.object.author_tz_offset) + ) tagged_date = datetime.fromtimestamp( tag.object.committed_date, tz=_tz ) diff --git a/semantic_release/cli/commands/changelog.py b/semantic_release/cli/commands/changelog.py index 8623219dd..785f0a149 100644 --- a/semantic_release/cli/commands/changelog.py +++ b/semantic_release/cli/commands/changelog.py @@ -67,7 +67,7 @@ def changelog(ctx: click.Context, release_tag: str | None = None) -> None: ) else: changelog_text = render_default_changelog_file(env) - changelog_file.write_text(changelog_text, encoding="utf-8") + changelog_file.write_text(f"{changelog_text}\n", encoding="utf-8") else: if runtime.global_cli_options.noop: @@ -112,7 +112,7 @@ def changelog(ctx: click.Context, release_tag: str | None = None) -> None: else: try: hvcs_client.create_or_update_release( - release_tag, release_notes, prerelease=version.is_prerelease + release_tag, f"{release_notes}\n", prerelease=version.is_prerelease ) except Exception as e: log.exception(e) diff --git a/semantic_release/cli/commands/version.py b/semantic_release/cli/commands/version.py index 8f422b120..278a4d880 100644 --- a/semantic_release/cli/commands/version.py +++ b/semantic_release/cli/commands/version.py @@ -429,7 +429,7 @@ def version( # noqa: C901 ) else: changelog_text = render_default_changelog_file(env) - changelog_file.write_text(changelog_text, encoding="utf-8") + changelog_file.write_text(f"{changelog_text}\n", encoding="utf-8") updated_paths = [str(changelog_file.relative_to(repo.working_dir))] diff --git a/semantic_release/cli/common.py b/semantic_release/cli/common.py index 10b285ba9..d6dbc98ee 100644 --- a/semantic_release/cli/common.py +++ b/semantic_release/cli/common.py @@ -34,7 +34,7 @@ def render_default_changelog_file(template_environment: Environment) -> str: .read_text(encoding="utf-8") ) tmpl = template_environment.from_string(changelog_text) - return tmpl.render() + return tmpl.render().rstrip() def render_release_notes( @@ -43,6 +43,8 @@ def render_release_notes( version: Version, release: Release, ) -> str: - return template_environment.from_string(release_notes_template).render( - version=version, release=release + return ( + template_environment.from_string(release_notes_template) + .render(version=version, release=release) + .rstrip() ) diff --git a/semantic_release/commit_parser/util.py b/semantic_release/commit_parser/util.py index 8bffae7fe..6ad7660aa 100644 --- a/semantic_release/commit_parser/util.py +++ b/semantic_release/commit_parser/util.py @@ -6,7 +6,7 @@ def parse_paragraphs(text: str) -> list[str]: - """ + r""" This will take a text block and return a list containing each paragraph with single line breaks collapsed into spaces. diff --git a/tests/command_line/test_changelog.py b/tests/command_line/test_changelog.py index 0f7a4de05..35acd3cd2 100644 --- a/tests/command_line/test_changelog.py +++ b/tests/command_line/test_changelog.py @@ -18,6 +18,34 @@ EXAMPLE_RELEASE_NOTES_TEMPLATE, EXAMPLE_REPO_NAME, EXAMPLE_REPO_OWNER, + SUCCESS_EXIT_CODE, +) +from tests.fixtures.repos import ( + repo_w_github_flow_w_feature_release_channel_angular_commits, + repo_w_github_flow_w_feature_release_channel_emoji_commits, + repo_w_github_flow_w_feature_release_channel_scipy_commits, + repo_w_github_flow_w_feature_release_channel_tag_commits, + repo_with_git_flow_and_release_channels_angular_commits, + repo_with_git_flow_and_release_channels_angular_commits_using_tag_format, + repo_with_git_flow_and_release_channels_emoji_commits, + repo_with_git_flow_and_release_channels_scipy_commits, + repo_with_git_flow_and_release_channels_tag_commits, + repo_with_git_flow_angular_commits, + repo_with_git_flow_emoji_commits, + repo_with_git_flow_scipy_commits, + repo_with_git_flow_tag_commits, + repo_with_no_tags_angular_commits, + repo_with_no_tags_emoji_commits, + repo_with_no_tags_scipy_commits, + repo_with_no_tags_tag_commits, + repo_with_single_branch_and_prereleases_angular_commits, + repo_with_single_branch_and_prereleases_emoji_commits, + repo_with_single_branch_and_prereleases_scipy_commits, + repo_with_single_branch_and_prereleases_tag_commits, + repo_with_single_branch_angular_commits, + repo_with_single_branch_emoji_commits, + repo_with_single_branch_scipy_commits, + repo_with_single_branch_tag_commits, ) from tests.util import flatten_dircmp, get_release_history_from_context, remove_dir_tree @@ -32,24 +60,31 @@ from tests.fixtures.example_project import ExProjectDir, UseReleaseNotesTemplateFn +changelog_subcmd = changelog.name or changelog.__name__ # type: ignore + + @pytest.mark.parametrize( "repo,tag", [ - (lazy_fixture("repo_with_no_tags_angular_commits"), None), - (lazy_fixture("repo_with_single_branch_angular_commits"), "v0.1.1"), + (lazy_fixture(repo_with_no_tags_angular_commits.__name__), None), + (lazy_fixture(repo_with_single_branch_angular_commits.__name__), "v0.1.1"), ( - lazy_fixture("repo_with_single_branch_and_prereleases_angular_commits"), + lazy_fixture( + repo_with_single_branch_and_prereleases_angular_commits.__name__ + ), "v0.2.0", ), ( lazy_fixture( - "repo_w_github_flow_w_feature_release_channel_angular_commits" + repo_w_github_flow_w_feature_release_channel_angular_commits.__name__ ), "v0.2.0", ), - (lazy_fixture("repo_with_git_flow_angular_commits"), "v1.0.0"), + (lazy_fixture(repo_with_git_flow_angular_commits.__name__), "v1.0.0"), ( - lazy_fixture("repo_with_git_flow_and_release_channels_angular_commits"), + lazy_fixture( + repo_with_git_flow_and_release_channels_angular_commits.__name__ + ), "v1.1.0-alpha.3", ), ], @@ -85,11 +120,9 @@ def test_changelog_noop_is_noop( "semantic_release.hvcs.github.build_requests_session", return_value=session, ), requests_mock.Mocker(session=session) as mocker: - result = cli_runner.invoke( - main, ["--noop", changelog.name or "changelog", *args] - ) + result = cli_runner.invoke(main, ["--noop", changelog_subcmd, *args]) - assert result.exit_code == 0 + assert SUCCESS_EXIT_CODE == result.exit_code # noqa: SIM300 dcmp = filecmp.dircmp(str(example_project_dir.resolve()), tempdir) @@ -104,39 +137,62 @@ def test_changelog_noop_is_noop( @pytest.mark.parametrize( "repo", [ - lazy_fixture("repo_with_no_tags_angular_commits"), - lazy_fixture("repo_with_single_branch_angular_commits"), - lazy_fixture("repo_with_single_branch_and_prereleases_angular_commits"), - lazy_fixture("repo_w_github_flow_w_feature_release_channel_angular_commits"), - lazy_fixture("repo_with_git_flow_angular_commits"), - lazy_fixture("repo_with_git_flow_and_release_channels_angular_commits"), + lazy_fixture(repo_fixture) + for repo_fixture in [ + repo_with_no_tags_angular_commits.__name__, + repo_with_no_tags_emoji_commits.__name__, + repo_with_no_tags_scipy_commits.__name__, + repo_with_no_tags_tag_commits.__name__, + repo_with_single_branch_angular_commits.__name__, + repo_with_single_branch_emoji_commits.__name__, + repo_with_single_branch_scipy_commits.__name__, + repo_with_single_branch_tag_commits.__name__, + repo_with_single_branch_and_prereleases_angular_commits.__name__, + repo_with_single_branch_and_prereleases_emoji_commits.__name__, + repo_with_single_branch_and_prereleases_scipy_commits.__name__, + repo_with_single_branch_and_prereleases_tag_commits.__name__, + repo_w_github_flow_w_feature_release_channel_angular_commits.__name__, + repo_w_github_flow_w_feature_release_channel_emoji_commits.__name__, + repo_w_github_flow_w_feature_release_channel_scipy_commits.__name__, + repo_w_github_flow_w_feature_release_channel_tag_commits.__name__, + repo_with_git_flow_angular_commits.__name__, + repo_with_git_flow_emoji_commits.__name__, + repo_with_git_flow_scipy_commits.__name__, + repo_with_git_flow_tag_commits.__name__, + repo_with_git_flow_and_release_channels_angular_commits.__name__, + repo_with_git_flow_and_release_channels_emoji_commits.__name__, + repo_with_git_flow_and_release_channels_scipy_commits.__name__, + repo_with_git_flow_and_release_channels_tag_commits.__name__, + repo_with_git_flow_and_release_channels_angular_commits_using_tag_format.__name__, + ] ], ) def test_changelog_content_regenerated( repo: Repo, - tmp_path_factory: pytest.TempPathFactory, - example_project_dir: ExProjectDir, example_changelog_md: Path, cli_runner: CliRunner, ): - tempdir = tmp_path_factory.mktemp("test_changelog") - remove_dir_tree(tempdir.resolve(), force=True) - shutil.copytree(src=str(example_project_dir.resolve()), dst=tempdir) + expected_changelog_content = example_changelog_md.read_text() # Remove the changelog and then check that we can regenerate it os.remove(str(example_changelog_md.resolve())) - result = cli_runner.invoke(main, [changelog.name or "changelog"]) - assert result.exit_code == 0 + result = cli_runner.invoke(main, [changelog_subcmd]) + assert SUCCESS_EXIT_CODE == result.exit_code # noqa: SIM300 - dcmp = filecmp.dircmp(str(example_project_dir.resolve()), tempdir) + # Check that the changelog file was re-created + assert example_changelog_md.exists() - differing_files = flatten_dircmp(dcmp) - assert not differing_files + actual_content = example_changelog_md.read_text() + + # Check that the changelog content is the same as before + assert expected_changelog_content == actual_content # Just need to test that it works for "a" project, not all -@pytest.mark.usefixtures("repo_with_single_branch_and_prereleases_angular_commits") +@pytest.mark.usefixtures( + repo_with_single_branch_and_prereleases_angular_commits.__name__ +) @pytest.mark.parametrize( "args", [("--post-to-release-tag", "v1.99.91910000000000000000000000000")] ) @@ -146,16 +202,20 @@ def test_changelog_release_tag_not_in_history( example_project_dir: ExProjectDir, cli_runner: CliRunner, ): + expected_err_code = 2 tempdir = tmp_path_factory.mktemp("test_changelog") remove_dir_tree(tempdir.resolve(), force=True) shutil.copytree(src=str(example_project_dir.resolve()), dst=tempdir) - result = cli_runner.invoke(main, [changelog.name or "changelog", *args]) - assert result.exit_code == 2 + result = cli_runner.invoke(main, [changelog_subcmd, *args]) + + assert expected_err_code == result.exit_code assert "not in release history" in result.stderr.lower() -@pytest.mark.usefixtures("repo_with_single_branch_and_prereleases_angular_commits") +@pytest.mark.usefixtures( + repo_with_single_branch_and_prereleases_angular_commits.__name__ +) @pytest.mark.parametrize("args", [("--post-to-release-tag", "v0.1.0")]) def test_changelog_post_to_release( args: list[str], @@ -181,6 +241,14 @@ def test_changelog_post_to_release( session.mount("http://", mock_adapter) session.mount("https://", mock_adapter) + expected_request_url = ( + "https://{api_url}/repos/{owner}/{repo_name}/releases".format( + api_url=Github.DEFAULT_API_DOMAIN, + owner=EXAMPLE_REPO_OWNER, + repo_name=EXAMPLE_REPO_NAME, + ) + ) + # Patch out env vars that affect changelog URLs but only get set in e.g. # Github actions with mock.patch( @@ -189,19 +257,14 @@ def test_changelog_post_to_release( ) as mocker, monkeypatch.context() as m: m.delenv("GITHUB_REPOSITORY", raising=False) m.delenv("CI_PROJECT_NAMESPACE", raising=False) - result = cli_runner.invoke(main, [changelog.name, *args]) + result = cli_runner.invoke(main, [changelog_subcmd, *args]) - assert result.exit_code == 0 + assert SUCCESS_EXIT_CODE == result.exit_code # noqa: SIM300 assert mocker.called assert mock_adapter.called - assert mock_adapter.last_request.url == ( - "https://{api_url}/repos/{owner}/{repo_name}/releases".format( - api_url=Github.DEFAULT_API_DOMAIN, - owner=EXAMPLE_REPO_OWNER, - repo_name=EXAMPLE_REPO_NAME, - ) - ) + assert mock_adapter.last_request is not None + assert expected_request_url == mock_adapter.last_request.url def test_custom_release_notes_template( @@ -217,23 +280,33 @@ def test_custom_release_notes_template( runtime_context_with_tags = retrieve_runtime_context( repo_with_single_branch_and_prereleases_angular_commits ) + expected_call_count = 1 + # Arrange release_history = get_release_history_from_context(runtime_context_with_tags) tag = runtime_context_with_tags.repo.tags[-1].name + version = runtime_context_with_tags.version_translator.from_tag(tag) + if version is None: + raise ValueError(f"Tag {tag} not in release history") + release = release_history.released[version] # Act - resp = cli_runner.invoke(main, [changelog.name, "--post-to-release-tag", tag]) - expected_release_notes = runtime_context_with_tags.template_environment.from_string( - EXAMPLE_RELEASE_NOTES_TEMPLATE - ).render(version=version, release=release) + resp = cli_runner.invoke(main, [changelog_subcmd, "--post-to-release-tag", tag]) + expected_release_notes = ( + runtime_context_with_tags.template_environment.from_string( + EXAMPLE_RELEASE_NOTES_TEMPLATE + ).render(version=version, release=release) + + "\n" + ) # Assert - assert resp.exit_code == 0, ( + assert SUCCESS_EXIT_CODE == resp.exit_code, ( # noqa: SIM300 "Unexpected failure in command " - f"'semantic-release {changelog.name} --post-to-release-tag {tag}': " + f"'semantic-release {changelog_subcmd} --post-to-release-tag {tag}': " + resp.stderr ) - assert post_mocker.call_count == 1 + assert expected_call_count == post_mocker.call_count + assert post_mocker.last_request is not None assert expected_release_notes == post_mocker.last_request.json()["body"] diff --git a/tests/command_line/test_main.py b/tests/command_line/test_main.py index 625fd4b7d..4b47facd8 100644 --- a/tests/command_line/test_main.py +++ b/tests/command_line/test_main.py @@ -1,5 +1,8 @@ +from __future__ import annotations + import json import os +from pathlib import Path from textwrap import dedent from typing import TYPE_CHECKING @@ -10,24 +13,29 @@ from semantic_release.cli import main if TYPE_CHECKING: + from pathlib import Path + from click.testing import CliRunner + from git import Repo from tests.fixtures.example_project import UpdatePyprojectTomlFn -def test_main_prints_version_and_exits(cli_runner): +def test_main_prints_version_and_exits(cli_runner: CliRunner): result = cli_runner.invoke(main, ["--version"]) assert result.exit_code == 0 assert result.output == f"semantic-release, version {__version__}\n" @pytest.mark.parametrize("args", [[], ["--help"]]) -def test_main_prints_help_text(cli_runner, args): +def test_main_prints_help_text(cli_runner: CliRunner, args: list[str]): result = cli_runner.invoke(main, args) assert result.exit_code == 0 -def test_not_a_release_branch_exit_code(repo_with_git_flow_angular_commits, cli_runner): +def test_not_a_release_branch_exit_code( + repo_with_git_flow_angular_commits: Repo, cli_runner: CliRunner +): # Run anything that doesn't trigger the help text repo_with_git_flow_angular_commits.git.checkout("-b", "branch-does-not-exist") result = cli_runner.invoke(main, ["version", "--no-commit"]) @@ -35,7 +43,7 @@ def test_not_a_release_branch_exit_code(repo_with_git_flow_angular_commits, cli_ def test_not_a_release_branch_exit_code_with_strict( - repo_with_git_flow_angular_commits, cli_runner + repo_with_git_flow_angular_commits: Repo, cli_runner: CliRunner ): # Run anything that doesn't trigger the help text repo_with_git_flow_angular_commits.git.checkout("-b", "branch-does-not-exist") @@ -44,7 +52,7 @@ def test_not_a_release_branch_exit_code_with_strict( def test_not_a_release_branch_detached_head_exit_code( - repo_with_git_flow_angular_commits, cli_runner + repo_with_git_flow_angular_commits: Repo, cli_runner: CliRunner ): expected_err_msg = ( "Detached HEAD state cannot match any release groups; no release will be made" @@ -60,7 +68,7 @@ def test_not_a_release_branch_detached_head_exit_code( @pytest.fixture -def toml_file_with_no_configuration_for_psr(tmp_path): +def toml_file_with_no_configuration_for_psr(tmp_path: Path) -> Path: path = tmp_path / "config.toml" path.write_text( dedent( @@ -76,7 +84,7 @@ def toml_file_with_no_configuration_for_psr(tmp_path): @pytest.fixture -def json_file_with_no_configuration_for_psr(tmp_path): +def json_file_with_no_configuration_for_psr(tmp_path: Path) -> Path: path = tmp_path / "config.json" path.write_text(json.dumps({"foo": [1, 2, 3]})) @@ -85,8 +93,8 @@ def json_file_with_no_configuration_for_psr(tmp_path): @pytest.mark.usefixtures("repo_with_git_flow_angular_commits") def test_default_config_is_used_when_none_in_toml_config_file( - cli_runner, - toml_file_with_no_configuration_for_psr, + cli_runner: CliRunner, + toml_file_with_no_configuration_for_psr: Path, ): result = cli_runner.invoke( main, @@ -98,8 +106,8 @@ def test_default_config_is_used_when_none_in_toml_config_file( @pytest.mark.usefixtures("repo_with_git_flow_angular_commits") def test_default_config_is_used_when_none_in_json_config_file( - cli_runner, - json_file_with_no_configuration_for_psr, + cli_runner: CliRunner, + json_file_with_no_configuration_for_psr: Path, ): result = cli_runner.invoke( main, @@ -111,7 +119,7 @@ def test_default_config_is_used_when_none_in_json_config_file( @pytest.mark.usefixtures("repo_with_git_flow_angular_commits") def test_errors_when_config_file_does_not_exist_and_passed_explicitly( - cli_runner, + cli_runner: CliRunner, ): result = cli_runner.invoke( main, @@ -124,7 +132,7 @@ def test_errors_when_config_file_does_not_exist_and_passed_explicitly( @pytest.mark.usefixtures("repo_with_no_tags_angular_commits") def test_errors_when_config_file_invalid_configuration( - cli_runner: "CliRunner", update_pyproject_toml: "UpdatePyprojectTomlFn" + cli_runner: CliRunner, update_pyproject_toml: UpdatePyprojectTomlFn ): update_pyproject_toml("tool.semantic_release.remote.type", "invalidType") result = cli_runner.invoke(main, ["--config", "pyproject.toml", "version"]) @@ -136,8 +144,8 @@ def test_errors_when_config_file_invalid_configuration( def test_uses_default_config_when_no_config_file_found( - tmp_path, - cli_runner, + tmp_path: Path, + cli_runner: CliRunner, ): # We have to initialise an empty git repository, as the example projects # all have pyproject.toml configs which would be used by default diff --git a/tests/command_line/test_version.py b/tests/command_line/test_version.py index ba2fbeaed..8aeb739c7 100644 --- a/tests/command_line/test_version.py +++ b/tests/command_line/test_version.py @@ -38,6 +38,9 @@ ) +version_subcmd = version.name or version.__name__ + + @pytest.mark.parametrize( "repo", [ @@ -49,7 +52,12 @@ lazy_fixture("repo_with_git_flow_and_release_channels_angular_commits"), ], ) -def test_version_noop_is_noop(tmp_path_factory, example_project_dir, repo, cli_runner): +def test_version_noop_is_noop( + tmp_path_factory: pytest.TempPathFactory, + example_project_dir: ExProjectDir, + repo: Repo, + cli_runner: CliRunner, +): # Make a commit to ensure we have something to release # otherwise the "no release will be made" logic will kick in first new_file = example_project_dir / "temp.txt" @@ -65,7 +73,7 @@ def test_version_noop_is_noop(tmp_path_factory, example_project_dir, repo, cli_r head_before = repo.head.commit tags_before = sorted(repo.tags, key=lambda tag: tag.name) - result = cli_runner.invoke(main, ["--noop", version.name]) + result = cli_runner.invoke(main, ["--noop", version_subcmd]) tags_after = sorted(repo.tags, key=lambda tag: tag.name) head_after = repo.head.commit @@ -216,7 +224,12 @@ def test_version_noop_is_noop(tmp_path_factory, example_project_dir, repo, cli_r ], ) def test_version_print( - repo, cli_args, expected_stdout, example_project_dir, tmp_path_factory, cli_runner + repo: Repo, + cli_args: list[str], + expected_stdout: str, + example_project_dir: ExProjectDir, + tmp_path_factory: pytest.TempPathFactory, + cli_runner: CliRunner, ): # Make a commit to ensure we have something to release # otherwise the "no release will be made" logic will kick in first @@ -232,7 +245,7 @@ def test_version_print( head_before = repo.head.commit tags_before = sorted(repo.tags, key=lambda tag: tag.name) - result = cli_runner.invoke(main, [version.name, *cli_args, "--print"]) + result = cli_runner.invoke(main, [version_subcmd, *cli_args, "--print"]) tags_after = sorted(repo.tags, key=lambda tag: tag.name) head_after = repo.head.commit @@ -258,11 +271,11 @@ def test_version_print( lazy_fixture("repo_with_git_flow_and_release_channels_angular_commits"), ], ) -def test_version_already_released_no_push(repo, cli_runner): +def test_version_already_released_no_push(repo: Repo, cli_runner: CliRunner): # In these tests, unless arguments are supplied then the latest version # has already been released, so we expect an exit code of 2 with the message # to indicate that no release will be made - result = cli_runner.invoke(main, ["--strict", version.name, "--no-push"]) + result = cli_runner.invoke(main, ["--strict", version_subcmd, "--no-push"]) assert result.exit_code == 2 assert "no release will be made" in result.stderr.lower() @@ -410,7 +423,7 @@ def test_version_no_push_force_level( tags_before = sorted(repo.tags, key=lambda tag: tag.name) result = cli_runner.invoke( - main, [version.name or "version", *cli_args, "--no-push"] + main, [version_subcmd or "version", *cli_args, "--no-push"] ) tags_after = sorted(repo.tags, key=lambda tag: tag.name) @@ -423,13 +436,16 @@ def test_version_no_push_force_level( assert head_before in repo.head.commit.parents dcmp = filecmp.dircmp(str(example_project_dir.resolve()), tempdir) - differing_files = flatten_dircmp(dcmp) + differing_files = sorted(flatten_dircmp(dcmp)) # Changelog already reflects changes this should introduce - assert differing_files == [ - "pyproject.toml", - f"src/{EXAMPLE_PROJECT_NAME}/_version.py", - ] + assert differing_files == sorted( + [ + "CHANGELOG.md", + "pyproject.toml", + f"src/{EXAMPLE_PROJECT_NAME}/_version.py", + ] + ) # Compare pyproject.toml new_pyproject_toml = tomlkit.loads( @@ -480,17 +496,16 @@ def test_version_no_push_force_level( ], ) def test_version_build_metadata_triggers_new_version(repo: Repo, cli_runner: CliRunner): - version_cmd_name = version.name or "version" # Verify we get "no version to release" without build metadata no_metadata_result = cli_runner.invoke( - main, ["--strict", version_cmd_name, "--no-push"] + main, ["--strict", version_subcmd, "--no-push"] ) assert no_metadata_result.exit_code == 2 assert "no release will be made" in no_metadata_result.stderr.lower() metadata_suffix = "build.abc-12345" result = cli_runner.invoke( - main, [version_cmd_name, "--no-push", "--build-metadata", metadata_suffix] + main, [version_subcmd, "--no-push", "--build-metadata", metadata_suffix] ) assert result.exit_code == 0 assert repo.git.tag(l=f"*{metadata_suffix}") @@ -499,7 +514,7 @@ def test_version_build_metadata_triggers_new_version(repo: Repo, cli_runner: Cli def test_version_prints_current_version_if_no_new_version( repo_with_git_flow_angular_commits: Repo, cli_runner: CliRunner ): - result = cli_runner.invoke(main, [version.name or "version", "--no-push"]) + result = cli_runner.invoke(main, [version_subcmd or "version", "--no-push"]) assert result.exit_code == 0 assert "no release will be made" in result.stderr.lower() assert result.stdout == "1.2.0-alpha.2\n" @@ -526,7 +541,7 @@ def test_version_runs_build_command( ): # ACT: run & force a new version that will trigger the build command result = cli_runner.invoke( - main, [version.name or "version", "--patch", "--no-push"] + main, [version_subcmd or "version", "--patch", "--no-push"] ) assert result.exit_code == 0 @@ -542,7 +557,7 @@ def test_version_skips_build_command_with_skip_build( "subprocess.run", return_value=CompletedProcess(args=(), returncode=0) ) as patched_subprocess_run: result = cli_runner.invoke( - main, [version.name, "--patch", "--no-push", "--skip-build"] + main, [version_subcmd, "--patch", "--no-push", "--skip-build"] ) # force a new version assert result.exit_code == 0 @@ -554,7 +569,7 @@ def test_version_writes_github_actions_output( ): mock_output_file = tmp_path / "action.out" monkeypatch.setenv("GITHUB_OUTPUT", str(mock_output_file.resolve())) - result = cli_runner.invoke(main, [version.name, "--patch", "--no-push"]) + result = cli_runner.invoke(main, [version_subcmd, "--patch", "--no-push"]) assert result.exit_code == 0 action_outputs = actions_output_to_dict( @@ -569,7 +584,7 @@ def test_version_writes_github_actions_output( def test_version_exit_code_when_strict(repo_with_git_flow_angular_commits, cli_runner): - result = cli_runner.invoke(main, ["--strict", version.name, "--no-push"]) + result = cli_runner.invoke(main, ["--strict", version_subcmd, "--no-push"]) assert result.exit_code != 0 @@ -577,7 +592,7 @@ def test_version_exit_code_when_not_strict( repo_with_git_flow_angular_commits, cli_runner ): # Testing "no release will be made" - result = cli_runner.invoke(main, [version.name, "--no-push"]) + result = cli_runner.invoke(main, [version_subcmd, "--no-push"]) assert result.exit_code == 0 @@ -597,9 +612,7 @@ def test_custom_release_notes_template( ) # Act - resp = cli_runner.invoke( - main, [version.name or "version", "--skip-build", "--vcs-release"] - ) + resp = cli_runner.invoke(main, [version_subcmd, "--skip-build", "--vcs-release"]) release_history = get_release_history_from_context(runtime_context_with_no_tags) tag = runtime_context_with_no_tags.repo.tags[-1].name @@ -619,7 +632,8 @@ def test_custom_release_notes_template( assert mocked_git_push.call_count == 2 # 1 for commit, 1 for tag assert resp.exit_code == 0, ( "Unexpected failure in command " - f"'semantic-release {version.name} --skip-build --vcs-release': " + resp.stderr + f"'semantic-release {version_subcmd} --skip-build --vcs-release': " + + resp.stderr ) assert post_mocker.call_count == 1 assert post_mocker.last_request is not None @@ -639,7 +653,7 @@ def test_version_tag_only_push( head_before = runtime_context_with_no_tags.repo.head.commit # Act - args = [version.name, "--tag", "--no-commit", "--skip-build", "--no-vcs-release"] + args = [version_subcmd, "--tag", "--no-commit", "--skip-build", "--no-vcs-release"] resp = cli_runner.invoke(main, args) tag_after = runtime_context_with_no_tags.repo.tags[-1].name @@ -681,7 +695,7 @@ def test_version_only_update_files_no_git_actions( tags_before = runtime_context_with_tags.repo.tags # Act - args = [version.name, "--minor", "--no-tag", "--no-commit", "--skip-build"] + args = [version_subcmd, "--minor", "--no-tag", "--no-commit", "--skip-build"] resp = cli_runner.invoke(main, args) tags_after = runtime_context_with_tags.repo.tags @@ -699,13 +713,18 @@ def test_version_only_update_files_no_git_actions( ) dcmp = filecmp.dircmp(str(example_project_dir.resolve()), tempdir) - differing_files = flatten_dircmp(dcmp) + differing_files = sorted(flatten_dircmp(dcmp)) # Files that should receive version change - assert differing_files == [ - "pyproject.toml", - f"src/{EXAMPLE_PROJECT_NAME}/_version.py", - ] + expected_changed_files = sorted( + [ + # CHANGELOG.md is not included as no modification to Git History + # (no commit or tag) has been made + "pyproject.toml", + f"src/{EXAMPLE_PROJECT_NAME}/_version.py", + ] + ) + assert expected_changed_files == differing_files # Compare pyproject.toml new_pyproject_toml = tomlkit.loads( diff --git a/tests/const.py b/tests/const.py index d53ddd967..9801b13c7 100644 --- a/tests/const.py +++ b/tests/const.py @@ -1,3 +1,5 @@ +from datetime import datetime + A_FULL_VERSION_STRING = "1.11.567" A_PRERELEASE_VERSION_STRING = "2.3.4-dev.23" A_FULL_VERSION_STRING_WITH_BUILD_METADATA = "4.2.3+build.12345" @@ -6,6 +8,11 @@ EXAMPLE_REPO_NAME = "example_repo" EXAMPLE_HVCS_DOMAIN = "example.com" +SUCCESS_EXIT_CODE = 0 + +TODAY_DATE_STR = datetime.now().strftime("%Y-%m-%d") +"""Date formatted as how it would appear in the changelog (Must match local timezone)""" + COMMIT_MESSAGE = "{version}\n\nAutomatically generated by python-semantic-release\n" # Different in-scope commits that produce a certain release type diff --git a/tests/fixtures/example_project.py b/tests/fixtures/example_project.py index 6d79ba402..eabd66c6f 100644 --- a/tests/fixtures/example_project.py +++ b/tests/fixtures/example_project.py @@ -14,7 +14,7 @@ ScipyCommitParser, TagCommitParser, ) -from semantic_release.hvcs import Gitea, Github, Gitlab +from semantic_release.hvcs import Bitbucket, Gitea, Github, Gitlab from tests.const import ( EXAMPLE_CHANGELOG_MD_CONTENT, @@ -107,7 +107,6 @@ def cached_example_project( setup_py_file: Path, changelog_md_file: Path, cached_files_dir: Path, - changelog_template_dir: Path, teardown_cached_dir: TeardownCachedDirFn, ) -> Path: """ @@ -158,11 +157,6 @@ def hello_world() -> None: # write file contents abs_filepath.write_text(contents) - # create the changelog template directory - cached_project_path.joinpath(changelog_template_dir).mkdir( - parents=True, exist_ok=True - ) - # trigger automatic cleanup of cache directory during teardown return teardown_cached_dir(cached_project_path) @@ -358,3 +352,16 @@ def _use_gitea_hvcs(domain: str | None = None) -> type[HvcsBase]: return Gitea return _use_gitea_hvcs + + +@pytest.fixture(scope="session") +def use_bitbucket_hvcs(update_pyproject_toml: UpdatePyprojectTomlFn) -> UseHvcsFn: + """Modify the configuration file to use BitBucket as the HVCS.""" + + def _use_bitbucket_hvcs(domain: str | None = None) -> type[HvcsBase]: + update_pyproject_toml("tool.semantic_release.remote.type", "bitbucket") + if domain is not None: + update_pyproject_toml("tool.semantic_release.remote.domain", domain) + return Bitbucket + + return _use_bitbucket_hvcs diff --git a/tests/fixtures/git_repo.py b/tests/fixtures/git_repo.py index 7c55b5a2c..ae39a57f1 100644 --- a/tests/fixtures/git_repo.py +++ b/tests/fixtures/git_repo.py @@ -11,6 +11,7 @@ EXAMPLE_HVCS_DOMAIN, EXAMPLE_REPO_NAME, EXAMPLE_REPO_OWNER, + TODAY_DATE_STR, ) from tests.util import ( add_text_to_file, @@ -103,6 +104,14 @@ class GetRepoDefinitionFn(Protocol): def __call__(self, commit_type: CommitConvention = "angular") -> RepoDefinition: ... + class SimulateDefaultChangelogCreationFn(Protocol): + def __call__( + self, + repo_definition: RepoDefinition, + dest_file: Path | None = None, + ) -> str: + ... + @pytest.fixture(scope="session") def commit_author(): @@ -243,6 +252,7 @@ def build_configured_base_repo( use_github_hvcs: UseHvcsFn, use_gitlab_hvcs: UseHvcsFn, use_gitea_hvcs: UseHvcsFn, + use_bitbucket_hvcs: UseHvcsFn, use_angular_parser: UseParserFn, use_emoji_parser: UseParserFn, use_scipy_parser: UseParserFn, @@ -292,6 +302,8 @@ def _build_configured_base_repo( hvcs_class = use_gitlab_hvcs(hvcs_domain) elif hvcs_client_name == "gitea": hvcs_class = use_gitea_hvcs(hvcs_domain) + elif hvcs_client_name == "bitbucket": + hvcs_class = use_bitbucket_hvcs(hvcs_domain) else: raise ValueError(f"Unknown HVCS client name: {hvcs_client_name}") @@ -314,6 +326,49 @@ def _build_configured_base_repo( return _build_configured_base_repo +@pytest.fixture(scope="session") +def simulate_default_changelog_creation() -> SimulateDefaultChangelogCreationFn: + def build_version_entry(version: VersionStr, version_def: RepoVersionDef) -> str: + version_entry = [] + if version == "Unreleased": + version_entry.append(f"## {version}\n") + else: + version_entry.append( + # TODO: artificial newline in front due to template when no Unreleased changes exist + f"\n## v{version} ({TODAY_DATE_STR})\n" + ) + + for section_def in version_def["changelog_sections"]: + version_entry.append(f"### {section_def['section']}\n") + for i in section_def["i_commits"]: + version_entry.append(f"* {version_def['commits'][i]}\n") + + return str.join("\n", version_entry) + + def _mimic_semantic_release_default_changelog( + repo_definition: RepoDefinition, + dest_file: Path | None = None, + ) -> str: + header = "# CHANGELOG" + version_entries = [] + + for version, version_def in repo_definition.items(): + # prepend entries to force reverse ordering + version_entries.insert(0, build_version_entry(version, version_def)) + + changelog_content = ( + str.join("\n" * 3, [header, str.join("\n", list(version_entries))]).rstrip() + + "\n" + ) + + if dest_file is not None: + dest_file.write_text(changelog_content) + + return changelog_content + + return _mimic_semantic_release_default_changelog + + @pytest.fixture def example_project_git_repo( example_project_dir: ExProjectDir, diff --git a/tests/fixtures/repos/git_flow/repo_w_2_release_channels.py b/tests/fixtures/repos/git_flow/repo_w_2_release_channels.py index 29acb7c9e..7e47a6955 100644 --- a/tests/fixtures/repos/git_flow/repo_w_2_release_channels.py +++ b/tests/fixtures/repos/git_flow/repo_w_2_release_channels.py @@ -26,6 +26,7 @@ GetVersionStringsFn, RepoDefinition, SimulateChangeCommitsNReturnChangelogEntryFn, + SimulateDefaultChangelogCreationFn, TomlSerializableTypes, VersionStr, ) @@ -38,7 +39,7 @@ def get_commits_for_git_flow_repo_with_2_release_channels() -> GetRepoDefinition "changelog_sections": { "angular": [{"section": "Unknown", "i_commits": [0]}], "emoji": [{"section": "Other", "i_commits": [0]}], - "scipy": [{"section": "None", "i_commits": [0]}], + "scipy": [{"section": "Unknown", "i_commits": [0]}], "tag": [{"section": "Unknown", "i_commits": [0]}], }, "commits": [ @@ -107,7 +108,7 @@ def get_commits_for_git_flow_repo_with_2_release_channels() -> GetRepoDefinition }, "commits": [ { - "angular": "feat: (dev) add some more text", + "angular": "feat(dev): add some more text", "emoji": ":sparkles: (dev) add some more text", "scipy": "ENH: (dev) add some more text", "tag": ":sparkles: (dev) add some more text", @@ -123,7 +124,7 @@ def get_commits_for_git_flow_repo_with_2_release_channels() -> GetRepoDefinition }, "commits": [ { - "angular": "fix: (dev) add some more text", + "angular": "fix(dev): add some more text", "emoji": ":bug: (dev) add some more text", "scipy": "MAINT: (dev) add some more text", "tag": ":nut_and_bolt: (dev) add some more text", @@ -139,7 +140,7 @@ def get_commits_for_git_flow_repo_with_2_release_channels() -> GetRepoDefinition }, "commits": [ { - "angular": "feat: (feature) add some more text", + "angular": "feat(feature): add some more text", "emoji": ":sparkles: (feature) add some more text", "scipy": "ENH: (feature) add some more text", "tag": ":sparkles: (feature) add some more text", @@ -170,13 +171,13 @@ def get_commits_for_git_flow_repo_with_2_release_channels() -> GetRepoDefinition }, "commits": [ { - "angular": "feat: (feature) add some more text", + "angular": "feat(feature): add some more text", "emoji": ":sparkles: (feature) add some more text", "scipy": "ENH: (feature) add some more text", "tag": ":sparkles: (feature) add some more text", }, { - "angular": "fix: (feature) add some missing text", + "angular": "fix(feature): add some missing text", "emoji": ":bug: (feature) add some missing text", "scipy": "MAINT: (feature) add some missing text", "tag": ":nut_and_bolt: (feature) add some missing text", @@ -223,7 +224,9 @@ def build_git_flow_repo_with_2_release_channels( get_commits_for_git_flow_repo_with_2_release_channels: GetRepoDefinitionFn, build_configured_base_repo: BuildRepoFn, default_tag_format_str: str, + changelog_md_file: Path, simulate_change_commits_n_rtn_changelog_entry: SimulateChangeCommitsNReturnChangelogEntryFn, + simulate_default_changelog_creation: SimulateDefaultChangelogCreationFn, create_release_tagged_commit: CreateReleaseFn, ) -> BuildRepoFn: """ @@ -378,6 +381,12 @@ def _build_git_flow_repo_with_2_release_channels( git_repo, next_version_def["commits"], hvcs ) + # write expected changelog (should match template changelog) + simulate_default_changelog_creation( + repo_def, + repo_dir.joinpath(changelog_md_file), + ) + # Make a 2nd alpha prerelease (v1.2.0-alpha.2) on the feature branch create_release_tagged_commit(git_repo, next_version, tag_format) diff --git a/tests/fixtures/repos/git_flow/repo_w_3_release_channels.py b/tests/fixtures/repos/git_flow/repo_w_3_release_channels.py index e333b0615..6077450bd 100644 --- a/tests/fixtures/repos/git_flow/repo_w_3_release_channels.py +++ b/tests/fixtures/repos/git_flow/repo_w_3_release_channels.py @@ -26,6 +26,7 @@ GetVersionStringsFn, RepoDefinition, SimulateChangeCommitsNReturnChangelogEntryFn, + SimulateDefaultChangelogCreationFn, TomlSerializableTypes, VersionStr, ) @@ -38,7 +39,7 @@ def get_commits_for_git_flow_repo_w_3_release_channels() -> GetRepoDefinitionFn: "changelog_sections": { "angular": [{"section": "Unknown", "i_commits": [0]}], "emoji": [{"section": "Other", "i_commits": [0]}], - "scipy": [{"section": "None", "i_commits": [0]}], + "scipy": [{"section": "Unknown", "i_commits": [0]}], "tag": [{"section": "Unknown", "i_commits": [0]}], }, "commits": [ @@ -107,7 +108,7 @@ def get_commits_for_git_flow_repo_w_3_release_channels() -> GetRepoDefinitionFn: }, "commits": [ { - "angular": "feat: (dev) add some more text", + "angular": "feat(dev): add some more text", "emoji": ":sparkles: (dev) add some more text", "scipy": "ENH: (dev) add some more text", "tag": ":sparkles: (dev) add some more text", @@ -123,7 +124,7 @@ def get_commits_for_git_flow_repo_w_3_release_channels() -> GetRepoDefinitionFn: }, "commits": [ { - "angular": "fix: (dev) add some more text", + "angular": "fix(dev): add some more text", "emoji": ":bug: (dev) add some more text", "scipy": "MAINT: (dev) add some more text", "tag": ":nut_and_bolt: (dev) add some more text", @@ -139,7 +140,7 @@ def get_commits_for_git_flow_repo_w_3_release_channels() -> GetRepoDefinitionFn: }, "commits": [ { - "angular": "feat: (feature) add some more text", + "angular": "feat(feature): add some more text", "emoji": ":sparkles: (feature) add some more text", "scipy": "ENH: (feature) add some more text", "tag": ":sparkles: (feature) add some more text", @@ -155,7 +156,7 @@ def get_commits_for_git_flow_repo_w_3_release_channels() -> GetRepoDefinitionFn: }, "commits": [ { - "angular": "feat: (feature) add some more text", + "angular": "feat(feature): add some more text", "emoji": ":sparkles: (feature) add some more text", "scipy": "ENH: (feature) add some more text", "tag": ":sparkles: (feature) add some more text", @@ -171,10 +172,10 @@ def get_commits_for_git_flow_repo_w_3_release_channels() -> GetRepoDefinitionFn: }, "commits": [ { - "angular": "fix: (feature) add some more text", - "emoji": ":bug: (feature) add some more text", - "scipy": "MAINT: (feature) add some more text", - "tag": ":nut_and_bolt: (feature) add some more text", + "angular": "fix(feature): add some missing text", + "emoji": ":bug: (feature) add some missing text", + "scipy": "MAINT: (feature) add some missing text", + "tag": ":nut_and_bolt: (feature) add some missing text", }, ], }, @@ -218,7 +219,9 @@ def build_git_flow_repo_w_3_release_channels( get_commits_for_git_flow_repo_w_3_release_channels: GetRepoDefinitionFn, build_configured_base_repo: BuildRepoFn, default_tag_format_str: str, + changelog_md_file: Path, simulate_change_commits_n_rtn_changelog_entry: SimulateChangeCommitsNReturnChangelogEntryFn, + simulate_default_changelog_creation: SimulateDefaultChangelogCreationFn, create_release_tagged_commit: CreateReleaseFn, ) -> BuildRepoFn: def _build_git_flow_repo_w_3_release_channels( @@ -373,6 +376,12 @@ def _build_git_flow_repo_w_3_release_channels( git_repo, next_version_def["commits"], hvcs ) + # write expected changelog (should match template changelog) + simulate_default_changelog_creation( + repo_def, + repo_dir.joinpath(changelog_md_file), + ) + # Make a 3rd alpha prerelease (v1.1.0-alpha.3) create_release_tagged_commit(git_repo, next_version, tag_format) diff --git a/tests/fixtures/repos/github_flow/repo_w_release_channels.py b/tests/fixtures/repos/github_flow/repo_w_release_channels.py index 0b2c2cd97..5e5e7c38e 100644 --- a/tests/fixtures/repos/github_flow/repo_w_release_channels.py +++ b/tests/fixtures/repos/github_flow/repo_w_release_channels.py @@ -26,6 +26,7 @@ GetVersionStringsFn, RepoDefinition, SimulateChangeCommitsNReturnChangelogEntryFn, + SimulateDefaultChangelogCreationFn, TomlSerializableTypes, VersionStr, ) @@ -38,7 +39,7 @@ def get_commits_for_github_flow_repo_w_feature_release_channel() -> GetRepoDefin "changelog_sections": { "angular": [{"section": "Unknown", "i_commits": [0]}], "emoji": [{"section": "Other", "i_commits": [0]}], - "scipy": [{"section": "None", "i_commits": [0]}], + "scipy": [{"section": "Unknown", "i_commits": [0]}], "tag": [{"section": "Unknown", "i_commits": [0]}], }, "commits": [ @@ -107,7 +108,7 @@ def get_commits_for_github_flow_repo_w_feature_release_channel() -> GetRepoDefin }, "commits": [ { - "angular": "feat: (feature) add some more text", + "angular": "feat(feature): add some more text", "emoji": ":sparkles: (feature) add some more text", "scipy": "ENH: (feature) add some more text", "tag": ":sparkles: (feature) add some more text", @@ -156,7 +157,9 @@ def build_github_flow_repo_w_feature_release_channel( get_commits_for_github_flow_repo_w_feature_release_channel: GetRepoDefinitionFn, build_configured_base_repo: BuildRepoFn, default_tag_format_str: str, + changelog_md_file: Path, simulate_change_commits_n_rtn_changelog_entry: SimulateChangeCommitsNReturnChangelogEntryFn, + simulate_default_changelog_creation: SimulateDefaultChangelogCreationFn, create_release_tagged_commit: CreateReleaseFn, ) -> BuildRepoFn: def _build_github_flow_repo_w_feature_release_channel( @@ -255,6 +258,12 @@ def _build_github_flow_repo_w_feature_release_channel( git_repo, next_version_def["commits"], hvcs ) + # Write the expected changelog (should match template changelog) + simulate_default_changelog_creation( + repo_def, + repo_dir.joinpath(changelog_md_file), + ) + # Make a feature level beta release (v0.3.0-beta.1) create_release_tagged_commit(git_repo, next_version, tag_format) diff --git a/tests/fixtures/repos/trunk_based_dev/repo_w_no_tags.py b/tests/fixtures/repos/trunk_based_dev/repo_w_no_tags.py index 56782f616..06ff258ae 100644 --- a/tests/fixtures/repos/trunk_based_dev/repo_w_no_tags.py +++ b/tests/fixtures/repos/trunk_based_dev/repo_w_no_tags.py @@ -25,6 +25,7 @@ GetVersionStringsFn, RepoDefinition, SimulateChangeCommitsNReturnChangelogEntryFn, + SimulateDefaultChangelogCreationFn, TomlSerializableTypes, VersionStr, ) @@ -44,18 +45,18 @@ def get_commits_for_trunk_only_repo_w_no_tags() -> GetRepoDefinitionFn: {"section": "Unknown", "i_commits": [0]}, ], "emoji": [ - {"section": ":bug:", "i_commits": [1, 3]}, + {"section": ":bug:", "i_commits": [3, 1]}, {"section": ":sparkles:", "i_commits": [2]}, {"section": "Other", "i_commits": [0]}, ], "scipy": [ {"section": "Feature", "i_commits": [2]}, - {"section": "Fix", "i_commits": [1, 3]}, - {"section": "None", "i_commits": [0]}, + {"section": "Fix", "i_commits": [3, 1]}, + {"section": "Unknown", "i_commits": [0]}, ], "tag": [ {"section": "Feature", "i_commits": [2]}, - {"section": "Fix", "i_commits": [1, 3]}, + {"section": "Fix", "i_commits": [3, 1]}, {"section": "Unknown", "i_commits": [0]}, ], }, @@ -125,7 +126,9 @@ def _get_versions_for_trunk_only_repo_w_no_tags() -> list[VersionStr]: def build_trunk_only_repo_w_no_tags( get_commits_for_trunk_only_repo_w_no_tags: GetRepoDefinitionFn, build_configured_base_repo: BuildRepoFn, + changelog_md_file: Path, simulate_change_commits_n_rtn_changelog_entry: SimulateChangeCommitsNReturnChangelogEntryFn, + simulate_default_changelog_creation: SimulateDefaultChangelogCreationFn, ) -> BuildRepoFn: def _build_trunk_only_repo_w_no_tags( dest_dir: Path | str, @@ -155,6 +158,12 @@ def _build_trunk_only_repo_w_no_tags( git_repo, next_version_def["commits"], hvcs ) + # write expected changelog (should match template changelog) + simulate_default_changelog_creation( + repo_def, + repo_dir.joinpath(changelog_md_file), + ) + return repo_dir, hvcs return _build_trunk_only_repo_w_no_tags diff --git a/tests/fixtures/repos/trunk_based_dev/repo_w_prereleases.py b/tests/fixtures/repos/trunk_based_dev/repo_w_prereleases.py index ef000809f..b013dcee5 100644 --- a/tests/fixtures/repos/trunk_based_dev/repo_w_prereleases.py +++ b/tests/fixtures/repos/trunk_based_dev/repo_w_prereleases.py @@ -26,6 +26,7 @@ GetVersionStringsFn, RepoDefinition, SimulateChangeCommitsNReturnChangelogEntryFn, + SimulateDefaultChangelogCreationFn, TomlSerializableTypes, VersionStr, ) @@ -38,7 +39,7 @@ def get_commits_for_trunk_only_repo_w_prerelease_tags() -> GetRepoDefinitionFn: "changelog_sections": { "angular": [{"section": "Unknown", "i_commits": [0]}], "emoji": [{"section": "Other", "i_commits": [0]}], - "scipy": [{"section": "None", "i_commits": [0]}], + "scipy": [{"section": "Unknown", "i_commits": [0]}], "tag": [{"section": "Unknown", "i_commits": [0]}], }, "commits": [ @@ -138,7 +139,9 @@ def build_trunk_only_repo_w_prerelease_tags( get_commits_for_trunk_only_repo_w_prerelease_tags: GetRepoDefinitionFn, build_configured_base_repo: BuildRepoFn, default_tag_format_str: str, + changelog_md_file: Path, simulate_change_commits_n_rtn_changelog_entry: SimulateChangeCommitsNReturnChangelogEntryFn, + simulate_default_changelog_creation: SimulateDefaultChangelogCreationFn, create_release_tagged_commit: CreateReleaseFn, ) -> BuildRepoFn: def _build_trunk_only_repo_w_prerelease_tags( @@ -210,6 +213,12 @@ def _build_trunk_only_repo_w_prerelease_tags( git_repo, next_version_def["commits"], hvcs ) + # write expected changelog (should match template changelog) + simulate_default_changelog_creation( + repo_def, + repo_dir.joinpath(changelog_md_file), + ) + # Make a full release create_release_tagged_commit(git_repo, next_version, tag_format) diff --git a/tests/fixtures/repos/trunk_based_dev/repo_w_tags.py b/tests/fixtures/repos/trunk_based_dev/repo_w_tags.py index 6d4ba14ee..0669e684d 100644 --- a/tests/fixtures/repos/trunk_based_dev/repo_w_tags.py +++ b/tests/fixtures/repos/trunk_based_dev/repo_w_tags.py @@ -26,6 +26,7 @@ GetVersionStringsFn, RepoDefinition, SimulateChangeCommitsNReturnChangelogEntryFn, + SimulateDefaultChangelogCreationFn, TomlSerializableTypes, VersionStr, ) @@ -38,7 +39,7 @@ def get_commits_for_trunk_only_repo_w_tags() -> GetRepoDefinitionFn: "changelog_sections": { "angular": [{"section": "Unknown", "i_commits": [0]}], "emoji": [{"section": "Other", "i_commits": [0]}], - "scipy": [{"section": "None", "i_commits": [0]}], + "scipy": [{"section": "Unknown", "i_commits": [0]}], "tag": [{"section": "Unknown", "i_commits": [0]}], }, "commits": [ @@ -106,7 +107,9 @@ def build_trunk_only_repo_w_tags( get_commits_for_trunk_only_repo_w_tags: GetRepoDefinitionFn, build_configured_base_repo: BuildRepoFn, default_tag_format_str: str, + changelog_md_file: Path, simulate_change_commits_n_rtn_changelog_entry: SimulateChangeCommitsNReturnChangelogEntryFn, + simulate_default_changelog_creation: SimulateDefaultChangelogCreationFn, create_release_tagged_commit: CreateReleaseFn, ) -> BuildRepoFn: def _build_trunk_only_repo_w_tags( @@ -155,6 +158,12 @@ def _build_trunk_only_repo_w_tags( git_repo, next_version_def["commits"], hvcs ) + # write expected changelog (should match template changelog) + simulate_default_changelog_creation( + repo_def, + repo_dir.joinpath(changelog_md_file), + ) + # Make a patch level release (v0.1.1) create_release_tagged_commit(git_repo, next_version, tag_format) diff --git a/tests/scenario/test_release_history.py b/tests/scenario/test_release_history.py index f71cb7396..0c51ced8f 100644 --- a/tests/scenario/test_release_history.py +++ b/tests/scenario/test_release_history.py @@ -93,7 +93,7 @@ class FakeReleaseHistoryElements(NamedTuple): "unknown": [COMMIT_MESSAGE.format(version="0.2.0")], }, Version.parse("0.3.0-beta.1"): { - "feature": ["feat: (feature) add some more text\n"], + "feature": ["feat(feature): add some more text\n"], "unknown": [COMMIT_MESSAGE.format(version="0.3.0-beta.1")], }, }, @@ -119,20 +119,20 @@ class FakeReleaseHistoryElements(NamedTuple): "unknown": [COMMIT_MESSAGE.format(version="1.0.0")], }, Version.parse("1.1.0"): { - "feature": ["feat: (dev) add some more text\n"], + "feature": ["feat(dev): add some more text\n"], "unknown": [COMMIT_MESSAGE.format(version="1.1.0")], }, Version.parse("1.1.1"): { - "fix": ["fix: (dev) add some more text\n"], + "fix": ["fix(dev): add some more text\n"], "unknown": [COMMIT_MESSAGE.format(version="1.1.1")], }, Version.parse("1.2.0-alpha.1"): { - "feature": ["feat: (feature) add some more text\n"], + "feature": ["feat(feature): add some more text\n"], "unknown": [COMMIT_MESSAGE.format(version="1.2.0-alpha.1")], }, Version.parse("1.2.0-alpha.2"): { - "feature": ["feat: (feature) add some more text\n"], - "fix": ["fix: (feature) add some missing text\n"], + "feature": ["feat(feature): add some more text\n"], + "fix": ["fix(feature): add some missing text\n"], "unknown": [COMMIT_MESSAGE.format(version="1.2.0-alpha.2")], }, }, @@ -158,23 +158,23 @@ class FakeReleaseHistoryElements(NamedTuple): "unknown": [COMMIT_MESSAGE.format(version="1.0.0")], }, Version.parse("1.1.0-rc.1"): { - "feature": ["feat: (dev) add some more text\n"], + "feature": ["feat(dev): add some more text\n"], "unknown": [COMMIT_MESSAGE.format(version="1.1.0-rc.1")], }, Version.parse("1.1.0-rc.2"): { - "fix": ["fix: (dev) add some more text\n"], + "fix": ["fix(dev): add some more text\n"], "unknown": [COMMIT_MESSAGE.format(version="1.1.0-rc.2")], }, Version.parse("1.1.0-alpha.1"): { - "feature": ["feat: (feature) add some more text\n"], + "feature": ["feat(feature): add some more text\n"], "unknown": [COMMIT_MESSAGE.format(version="1.1.0-alpha.1")], }, Version.parse("1.1.0-alpha.2"): { - "feature": ["feat: (feature) add some more text\n"], + "feature": ["feat(feature): add some more text\n"], "unknown": [COMMIT_MESSAGE.format(version="1.1.0-alpha.2")], }, Version.parse("1.1.0-alpha.3"): { - "fix": ["fix: (feature) add some more text\n"], + "fix": ["fix(feature): add some missing text\n"], "unknown": [COMMIT_MESSAGE.format(version="1.1.0-alpha.3")], }, }, @@ -233,15 +233,22 @@ def test_release_history( ), ) for k in expected_release_history.released: - expected, actual = expected_release_history.released[k], released[k]["elements"] - actual_released_messages = [ - res.commit.message for results in actual.values() for res in results - ] - assert all( - msg in actual_released_messages - for bucket in expected.values() - for msg in bucket + expected = expected_release_history.released[k] + actual = released[k]["elements"] + actual_released_messages = str.join( + "\n\n", + sorted( + [ + str(res.commit.message) + for results in actual.values() + for res in results + ] + ), + ) + expected_released_messages = str.join( + "\n\n", sorted([msg for bucket in expected.values() for msg in bucket]) ) + assert expected_released_messages == actual_released_messages for commit_message in ANGULAR_COMMITS_MINOR: add_text_to_file(repo, file_in_repo) @@ -251,17 +258,33 @@ def test_release_history( new_unreleased, new_released = ReleaseHistory.from_git_history( repo, translator, default_angular_parser ) - actual_unreleased_messages = [ - res.commit.message for results in new_unreleased.values() for res in results - ] - assert all( - msg in actual_unreleased_messages - for bucket in [ - *expected_release_history.unreleased.values(), - ANGULAR_COMMITS_MINOR, - ] - for msg in bucket + + actual_unreleased_messages = str.join( + "\n\n", + sorted( + [ + str(res.commit.message) + for results in new_unreleased.values() + for res in results + ] + ), + ) + + expected_unreleased_messages = str.join( + "\n\n", + sorted( + [ + msg + for bucket in [ + ANGULAR_COMMITS_MINOR[::-1], + *expected_release_history.unreleased.values(), + ] + for msg in bucket + ] + ), ) + + assert expected_unreleased_messages == actual_unreleased_messages assert ( new_released == released ), "something that shouldn't be considered release has been released" diff --git a/tests/scenario/test_template_render.py b/tests/scenario/test_template_render.py index af116a74f..b3d1f1598 100644 --- a/tests/scenario/test_template_render.py +++ b/tests/scenario/test_template_render.py @@ -1,11 +1,19 @@ +from __future__ import annotations + import itertools import os -from pathlib import Path +from typing import TYPE_CHECKING import pytest from semantic_release.changelog.template import environment, recursive_render +if TYPE_CHECKING: + from pathlib import Path + + from tests.fixtures.example_project import ExProjectDir + + NORMAL_TEMPLATE_SRC = """--- content: - a string @@ -38,45 +46,45 @@ def _strip_trailing_j2(path: Path) -> Path: @pytest.fixture -def normal_template(example_project_template_dir): +def normal_template(example_project_template_dir: Path) -> Path: template = example_project_template_dir / "normal.yaml.j2" + template.parent.mkdir(parents=True, exist_ok=True) template.write_text(NORMAL_TEMPLATE_SRC) return template @pytest.fixture -def long_directory_path(example_project_template_dir): +def long_directory_path(example_project_template_dir: Path) -> Path: # NOTE: fixture enables using Path rather than # constant string, so no issue with / vs \ on Windows - d = example_project_template_dir / "long" / "dir" / "path" - os.makedirs(str(d.resolve()), exist_ok=True) - return d + return example_project_template_dir / "long" / "dir" / "path" @pytest.fixture -def deeply_nested_file(long_directory_path): +def deeply_nested_file(long_directory_path: Path) -> Path: file = long_directory_path / "buried.txt" + file.parent.mkdir(parents=True, exist_ok=True) file.write_text(PLAINTEXT_FILE_CONTENT) return file @pytest.fixture -def hidden_file(example_project_template_dir): +def hidden_file(example_project_template_dir: Path) -> Path: file = example_project_template_dir / ".hidden" + file.parent.mkdir(parents=True, exist_ok=True) file.write_text("I shouldn't be present") return file @pytest.fixture -def directory_path_with_hidden_subfolder(example_project_template_dir): - d = example_project_template_dir / "path" / ".subfolder" / "hidden" - os.makedirs(str(d.resolve()), exist_ok=True) - return d +def directory_path_with_hidden_subfolder(example_project_template_dir: Path) -> Path: + return example_project_template_dir / "path" / ".subfolder" / "hidden" @pytest.fixture -def excluded_file(directory_path_with_hidden_subfolder): +def excluded_file(directory_path_with_hidden_subfolder: Path) -> Path: file = directory_path_with_hidden_subfolder / "excluded.txt" + file.parent.mkdir(parents=True, exist_ok=True) file.write_text("I shouldn't be present") return file @@ -132,21 +140,25 @@ def test_recursive_render( @pytest.fixture -def dotfolder_template_dir(example_project_dir: Path): - newpath = example_project_dir / ".templates/.psr-templates" - newpath.mkdir(parents=True, exist_ok=True) - return newpath +def dotfolder_template_dir(example_project_dir: ExProjectDir) -> Path: + return example_project_dir / ".templates/.psr-templates" @pytest.fixture -def dotfolder_template(dotfolder_template_dir: Path): +def dotfolder_template( + init_example_project: None, dotfolder_template_dir: Path +) -> Path: tmpl = dotfolder_template_dir / "template.txt" + tmpl.parent.mkdir(parents=True, exist_ok=True) tmpl.write_text("I am a template") return tmpl def test_recursive_render_with_top_level_dotfolder( - example_project_dir, dotfolder_template, dotfolder_template_dir + init_example_project: None, + example_project_dir: ExProjectDir, + dotfolder_template: Path, + dotfolder_template_dir: Path, ): preexisting_paths = set(example_project_dir.rglob("**/*")) env = environment(template_dir=dotfolder_template_dir.resolve()) diff --git a/tests/unit/semantic_release/changelog/TEST_CHANGELOG.md.j2 b/tests/unit/semantic_release/changelog/TEST_CHANGELOG.md.j2 deleted file mode 100644 index e28faed21..000000000 --- a/tests/unit/semantic_release/changelog/TEST_CHANGELOG.md.j2 +++ /dev/null @@ -1,24 +0,0 @@ -{# - NOTE: this changelog test doesn't include commit hashes from the default template as - they always change - which makes it notoriously difficult to check exact content -#}# CHANGELOG -{% if context.history.unreleased | length > 0 %} -## Unreleased -{% for type_, commits in context.history.unreleased | dictsort %} -### {{ type_ | capitalize }} -{% for commit in commits %}{% if type_ != "unknown" %} -* {{ commit.message.rstrip() }} -{% else %} -* {{ commit.message.rstrip() }} -{% endif %}{% endfor %} -{% endfor %}{% endif %} -{% for version, release in context.history.released.items() %} -## {{ version.as_semver_tag() }} ({{ release.tagged_date.strftime("%Y-%m-%d") }}) -{% for type_, commits in release["elements"] | dictsort %} -### {{ type_ | capitalize }} -{% for commit in commits %}{% if type_ != "unknown" %} -* {{ commit.message.rstrip() }} -{% else %} -* {{ commit.message.rstrip() }} -{% endif %}{% endfor %} -{% endfor %}{% endfor %} diff --git a/tests/unit/semantic_release/changelog/test_changelog_context.py b/tests/unit/semantic_release/changelog/test_changelog_context.py deleted file mode 100644 index dcb62124c..000000000 --- a/tests/unit/semantic_release/changelog/test_changelog_context.py +++ /dev/null @@ -1,144 +0,0 @@ -import pytest -from git.objects.base import Object -from pytest_lazyfixture import lazy_fixture - -from semantic_release.changelog import environment, make_changelog_context -from semantic_release.changelog.release_history import ReleaseHistory -from semantic_release.hvcs import Bitbucket, Gitea, Github, Gitlab -from semantic_release.version.translator import VersionTranslator - -NULL_HEX_SHA = Object.NULL_HEX_SHA -SHORT_SHA = NULL_HEX_SHA[:7] - - -# Test with just one project for the moment - can be expanded to all -# example projects later - -CHANGELOG_TEMPLATE = r""" -# CHANGELOG -{% if context.history.unreleased | length > 0 %} -## Unreleased -{% for type_, commits in context.history.unreleased.items() %} -### {{ type_ | capitalize }} -{% for commit in commits %}{% if type_ != "unknown" %} -* {{ commit.message.rstrip() }} ([`{{ commit.short_hash }}`]({{ commit.hexsha | commit_hash_url }})) -{% else %} -* {{ commit.message.rstrip() }} ([`{{ commit.short_hash }}`]({{ commit.hexsha | commit_hash_url }})) -{% endif %}{% endfor %}{% endfor %}{% endif %} -{% for version, release in context.history.released.items() %} -## {{ version.as_tag() }} ({{ release.tagged_date.strftime("%Y-%m-%d") }}) -{% for type_, commits in release["elements"].items() %} -### {{ type_ | capitalize }} -{% for commit in commits %}{% if type_ != "unknown" %} -* {{ commit.message.rstrip() }} ([`{{ commit.short_hash }}`]({{ commit.hexsha | commit_hash_url }})) -{% else %} -* {{ commit.message.rstrip() }} ([`{{ commit.short_hash }}`]({{ commit.hexsha | commit_hash_url }})) -{% endif %}{% endfor %}{% endfor %}{% endfor %} -""" # noqa: E501 - -EXPECTED_CHANGELOG_CONTENT_ANGULAR = r""" -# CHANGELOG -## v0.2.0 -### Feature -* feat: add some more text ([`{SHORT_SHA}`]({commit_url})) -## v0.2.0-rc.1 -### Feature -* feat: add some more text ([`{SHORT_SHA}`]({commit_url})) -## v0.1.1-rc.1 -### Fix -* fix: add some more text ([`{SHORT_SHA}`]({commit_url})) -## v0.1.0 -### Unknown -* Initial commit ([`{SHORT_SHA}`]({commit_url})) -""" - - -EXPECTED_CHANGELOG_CONTENT_EMOJI = r""" -# CHANGELOG -## v0.2.0 -### :sparkles: -* :sparkles: add some more text ([`{SHORT_SHA}`]({commit_url})) -## v0.2.0-rc.1 -### :sparkles: -* :sparkles: add some more text ([`{SHORT_SHA}`]({commit_url})) -## v0.1.1-rc.1 -### :bug: -* :bug: add some more text ([`{SHORT_SHA}`]({commit_url})) -## v0.1.0 -### Unknown -* Initial commit ([`{SHORT_SHA}`]({commit_url})) -""" - -EXPECTED_CHANGELOG_CONTENT_SCIPY = r""" -# CHANGELOG -## v0.2.0 -### ENH: -* ENH: add some more text ([`{SHORT_SHA}`]({commit_url})) -## v0.2.0-rc.1 -### ENH: -* ENH: add some more text ([`{SHORT_SHA}`]({commit_url})) -## v0.1.1-rc.1 -### MAINT: -* MAINT: add some more text ([`{SHORT_SHA}`]({commit_url})) -## v0.1.0 -### Unknown -* Initial commit ([`{SHORT_SHA}`]({commit_url})) -""" - -EXPECTED_CHANGELOG_CONTENT_TAG = r""" -# CHANGELOG -## v0.2.0 -### :sparkles: -* :sparkles: add some more text ([`{SHORT_SHA}`]({commit_url})) -## v0.2.0-rc.1 -### :sparkles: -* :sparkles: add some more text ([`{SHORT_SHA}`]({commit_url})) -## v0.1.1-rc.1 -### :nut_and_bolt: -* :nut_and_bolt: add some more text ([`{SHORT_SHA}`]({commit_url})) -## v0.1.0 -### Unknown -* Initial commit ([`{SHORT_SHA}`]({commit_url})) -""" - - -@pytest.mark.parametrize("changelog_template", (CHANGELOG_TEMPLATE,)) -@pytest.mark.parametrize( - "repo, commit_parser, expected_changelog", - [ - ( - lazy_fixture("repo_with_single_branch_and_prereleases_angular_commits"), - lazy_fixture("default_angular_parser"), - EXPECTED_CHANGELOG_CONTENT_ANGULAR, - ), - ( - lazy_fixture("repo_with_single_branch_and_prereleases_emoji_commits"), - lazy_fixture("default_emoji_parser"), - EXPECTED_CHANGELOG_CONTENT_EMOJI, - ), - ( - lazy_fixture("repo_with_single_branch_and_prereleases_scipy_commits"), - lazy_fixture("default_scipy_parser"), - EXPECTED_CHANGELOG_CONTENT_SCIPY, - ), - ( - lazy_fixture("repo_with_single_branch_and_prereleases_tag_commits"), - lazy_fixture("default_tag_parser"), - EXPECTED_CHANGELOG_CONTENT_TAG, - ), - ], -) -@pytest.mark.parametrize("hvcs_client_class", (Github, Gitlab, Gitea, Bitbucket)) -@pytest.mark.usefixtures("expected_changelog") -def test_changelog_context(repo, changelog_template, commit_parser, hvcs_client_class): - # NOTE: this test only checks that the changelog can be rendered with the - # contextual information we claim to offer. Testing that templates render - # appropriately is the responsibility of the template engine's authors, - # so we shouldn't be re-testing that here. - hvcs_client = hvcs_client_class(remote_url=repo.remote().url) - env = environment(lstrip_blocks=True, keep_trailing_newline=True, trim_blocks=True) - rh = ReleaseHistory.from_git_history(repo, VersionTranslator(), commit_parser) - context = make_changelog_context(hvcs_client=hvcs_client, release_history=rh) - context.bind_to_environment(env) - actual_content = env.from_string(changelog_template).render() - assert actual_content diff --git a/tests/unit/semantic_release/changelog/test_default_changelog.py b/tests/unit/semantic_release/changelog/test_default_changelog.py index e79fe323b..f5dc9fbc3 100644 --- a/tests/unit/semantic_release/changelog/test_default_changelog.py +++ b/tests/unit/semantic_release/changelog/test_default_changelog.py @@ -1,109 +1,189 @@ -# NOTE: use backport with newer API +from __future__ import annotations + from datetime import datetime +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest +from git import Commit, Object, Repo +# NOTE: use backport with newer API from importlib_resources import files +import semantic_release from semantic_release.changelog.context import make_changelog_context -from semantic_release.changelog.release_history import ReleaseHistory +from semantic_release.changelog.release_history import Release, ReleaseHistory from semantic_release.changelog.template import environment -from semantic_release.hvcs import Github -from semantic_release.version.translator import VersionTranslator - -from tests.const import COMMIT_MESSAGE - -default_changelog_template = ( - files("tests") - .joinpath("unit/semantic_release/changelog/TEST_CHANGELOG.md.j2") - .read_text(encoding="utf-8") -) - -today_as_str = datetime.now().strftime("%Y-%m-%d") - - -def _cm_rstripped(version: str) -> str: - return COMMIT_MESSAGE.format(version=version).rstrip() - - -EXPECTED_CONTENT = f"""\ -# CHANGELOG -## v1.1.0-alpha.3 ({today_as_str}) -### Fix -* fix: (feature) add some more text -### Unknown -* {_cm_rstripped("1.1.0-alpha.3")} -## v1.1.0-alpha.2 ({today_as_str}) -### Feature -* feat: (feature) add some more text -### Unknown -* {_cm_rstripped("1.1.0-alpha.2")} -## v1.1.0-alpha.1 ({today_as_str}) -### Feature -* feat: (feature) add some more text -### Unknown -* {_cm_rstripped("1.1.0-alpha.1")} -## v1.1.0-rc.2 ({today_as_str}) -### Fix -* fix: (dev) add some more text -### Unknown -* {_cm_rstripped("1.1.0-rc.2")} -## v1.1.0-rc.1 ({today_as_str}) -### Feature -* feat: (dev) add some more text -### Unknown -* {_cm_rstripped("1.1.0-rc.1")} -## v1.0.0 ({today_as_str}) -### Feature -* feat: add some more text -### Unknown -* {_cm_rstripped("1.0.0")} -## v1.0.0-rc.1 ({today_as_str}) -### Breaking -* feat!: add some more text -### Unknown -* {_cm_rstripped("1.0.0-rc.1")} -## v0.1.1-rc.1 ({today_as_str}) -### Fix -* fix: add some more text -### Unknown -* {_cm_rstripped("0.1.1-rc.1")} -## v0.1.0 ({today_as_str}) -### Unknown -* {_cm_rstripped("0.1.0")} -* Initial commit -""" +from semantic_release.commit_parser import ParsedCommit +from semantic_release.enums import LevelBump +from semantic_release.hvcs import Bitbucket, Gitea, Github, Gitlab +from semantic_release.version.translator import Version + +from tests.const import TODAY_DATE_STR + +if TYPE_CHECKING: + from git import Actor + + from semantic_release.hvcs import HvcsBase + + +@pytest.fixture +def default_changelog_template() -> str: + """Retrieve the semantic-release default changelog template.""" + version_notes_template = files(semantic_release.__name__).joinpath( + Path("data", "templates", "CHANGELOG.md.j2") + ) + return version_notes_template.read_text(encoding="utf-8") +@pytest.fixture +def artificial_release_history(commit_author: Actor): + version = Version.parse("1.0.0") + + commit_subject = "fix(cli): fix a problem" + + fix_commit = Commit( + Repo("."), + Object.NULL_HEX_SHA[:20].encode("utf-8"), + message=commit_subject, + ) + + fix_commit_parsed = ParsedCommit( + bump=LevelBump.PATCH, + type="fix", + scope="cli", + descriptions=[commit_subject], + breaking_descriptions=[], + commit=fix_commit, + ) + + commit_subject = "feat(cli): add a new feature" + + feat_commit = Commit( + Repo("."), + Object.NULL_HEX_SHA[:20].encode("utf-8"), + message=commit_subject, + ) + + feat_commit_parsed = ParsedCommit( + bump=LevelBump.MINOR, + type="feat", + scope="cli", + descriptions=[commit_subject], + breaking_descriptions=[], + commit=feat_commit, + ) + + return ReleaseHistory( + unreleased={ + "feature": [feat_commit_parsed], + }, + released={ + version: Release( + tagger=commit_author, + committer=commit_author, + tagged_date=datetime.utcnow(), + elements={ + "feature": [feat_commit_parsed], + "fix": [fix_commit_parsed], + }, + ) + }, + ) + + +@pytest.mark.parametrize("hvcs_client", [Github, Gitlab, Gitea, Bitbucket]) def test_default_changelog_template( - repo_with_git_flow_and_release_channels_angular_commits, default_angular_parser + default_changelog_template: str, + hvcs_client: type[HvcsBase], + example_git_https_url: str, + artificial_release_history: ReleaseHistory, ): - repo = repo_with_git_flow_and_release_channels_angular_commits - env = environment(trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True) - rh = ReleaseHistory.from_git_history( - repo=repo, translator=VersionTranslator(), commit_parser=default_angular_parser + version_str = "1.0.0" + version = Version.parse(version_str) + rh = artificial_release_history + rh.unreleased = {} # Wipe out unreleased + + feat_commit_obj = artificial_release_history.released[version]["elements"][ + "feature" + ][0] + feat_commit_url = hvcs_client(example_git_https_url).commit_hash_url( + feat_commit_obj.commit.hexsha + ) + feat_description = str.join("\n", feat_commit_obj.descriptions) + + fix_commit_obj = artificial_release_history.released[version]["elements"]["fix"][0] + fix_commit_url = hvcs_client(example_git_https_url).commit_hash_url( + fix_commit_obj.commit.hexsha + ) + fix_description = str.join("\n", fix_commit_obj.descriptions) + + expected_changelog = str.join( + "\n", + [ + "# CHANGELOG", + f"## v{version_str} ({TODAY_DATE_STR})", + "### Feature", + f"* {feat_description} ([`{feat_commit_obj.commit.hexsha[:7]}`]({feat_commit_url}))", + "### Fix", + f"* {fix_description} ([`{fix_commit_obj.commit.hexsha[:7]}`]({fix_commit_url}))", + "", + ], ) + env = environment(trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True) context = make_changelog_context( - hvcs_client=Github(remote_url=repo.remote().url), release_history=rh + hvcs_client=hvcs_client(remote_url=example_git_https_url), + release_history=rh, ) context.bind_to_environment(env) - actual_content = env.from_string(default_changelog_template).render() - assert actual_content == EXPECTED_CONTENT + actual_changelog = env.from_string(default_changelog_template).render() + assert expected_changelog == actual_changelog -def test_default_changelog_template_using_tag_format( - repo_with_git_flow_and_release_channels_angular_commits_using_tag_format, - default_angular_parser, +@pytest.mark.parametrize("hvcs_client", [Github, Gitlab, Gitea, Bitbucket]) +def test_default_changelog_template_w_unreleased_changes( + default_changelog_template: str, + hvcs_client: type[HvcsBase], + example_git_https_url: str, + artificial_release_history: ReleaseHistory, ): - repo = repo_with_git_flow_and_release_channels_angular_commits_using_tag_format - env = environment(trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True) - rh = ReleaseHistory.from_git_history( - repo=repo, - translator=VersionTranslator(tag_format="vpy{version}"), - commit_parser=default_angular_parser, + version_str = "1.0.0" + version = Version.parse(version_str) + + feat_commit_obj = artificial_release_history.released[version]["elements"][ + "feature" + ][0] + feat_commit_url = hvcs_client(example_git_https_url).commit_hash_url( + feat_commit_obj.commit.hexsha + ) + feat_description = str.join("\n", feat_commit_obj.descriptions) + + fix_commit_obj = artificial_release_history.released[version]["elements"]["fix"][0] + fix_commit_url = hvcs_client(example_git_https_url).commit_hash_url( + fix_commit_obj.commit.hexsha ) + fix_description = str.join("\n", fix_commit_obj.descriptions) + + expected_changelog = str.join( + "\n", + [ + "# CHANGELOG", + "## Unreleased", + "### Feature", + f"* {feat_description} ([`{feat_commit_obj.commit.hexsha[:7]}`]({feat_commit_url}))", + f"## v{version_str} ({TODAY_DATE_STR})", + "### Feature", + f"* {feat_description} ([`{feat_commit_obj.commit.hexsha[:7]}`]({feat_commit_url}))", + "### Fix", + f"* {fix_description} ([`{fix_commit_obj.commit.hexsha[:7]}`]({fix_commit_url}))", + "", + ], + ) + env = environment(trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True) context = make_changelog_context( - hvcs_client=Github(remote_url=repo.remote().url), release_history=rh + hvcs_client=hvcs_client(remote_url=example_git_https_url), + release_history=artificial_release_history, ) context.bind_to_environment(env) - - actual_content = env.from_string(default_changelog_template).render() - assert actual_content == EXPECTED_CONTENT + actual_changelog = env.from_string(default_changelog_template).render() + assert expected_changelog == actual_changelog diff --git a/tests/unit/semantic_release/changelog/test_release_notes.md.j2 b/tests/unit/semantic_release/changelog/test_release_notes.md.j2 deleted file mode 100644 index 088337c84..000000000 --- a/tests/unit/semantic_release/changelog/test_release_notes.md.j2 +++ /dev/null @@ -1,8 +0,0 @@ -# {{ version.as_tag() }} ({{ release.tagged_date.strftime("%Y-%m-%d") }}) -{% for type_, commits in release["elements"] | dictsort %} -## {{ type_ | capitalize }} -{% for commit in commits %}{% if type_ != "unknown" %} -* {{ commit.message.rstrip() }} -{% else %} -* {{ commit.message.rstrip() }} -{% endif %}{% endfor %}{% endfor %} diff --git a/tests/unit/semantic_release/changelog/test_release_notes.py b/tests/unit/semantic_release/changelog/test_release_notes.py index 441522612..285eea1b1 100644 --- a/tests/unit/semantic_release/changelog/test_release_notes.py +++ b/tests/unit/semantic_release/changelog/test_release_notes.py @@ -1,53 +1,112 @@ -# NOTE: use backport with newer API +from __future__ import annotations + from datetime import datetime +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest +from git import Commit, Repo +from git.objects import Object +# NOTE: use backport with newer API to support 3.7 from importlib_resources import files +import semantic_release from semantic_release.changelog.context import make_changelog_context -from semantic_release.changelog.release_history import ReleaseHistory +from semantic_release.changelog.release_history import Release, ReleaseHistory from semantic_release.changelog.template import environment -from semantic_release.hvcs import Github -from semantic_release.version import Version, VersionTranslator +from semantic_release.commit_parser import ParsedCommit +from semantic_release.enums import LevelBump +from semantic_release.hvcs import Bitbucket, Gitea, Github, Gitlab +from semantic_release.version import Version -from tests.const import COMMIT_MESSAGE +from tests.const import TODAY_DATE_STR -default_release_notes_template = ( - files("tests") - .joinpath("unit/semantic_release/changelog/test_release_notes.md.j2") - .read_text(encoding="utf-8") -) +if TYPE_CHECKING: + from git import Actor -today_as_str = datetime.now().strftime("%Y-%m-%d") + from semantic_release.hvcs import HvcsBase -def _cm_rstripped(version: str) -> str: - return COMMIT_MESSAGE.format(version=version).rstrip() +@pytest.fixture +def artificial_release_history(commit_author: Actor): + version = Version.parse("1.1.0-alpha.3") + commit_subject = "fix(cli): fix a problem" + + fix_commit = Commit( + Repo("."), + Object.NULL_HEX_SHA[:20].encode("utf-8"), + message=commit_subject, + ) + fix_commit_parsed = ParsedCommit( + bump=LevelBump.PATCH, + type="fix", + scope="cli", + descriptions=[commit_subject], + breaking_descriptions=[], + commit=fix_commit, + ) -EXPECTED_CONTENT = f"""\ -# v1.1.0-alpha.3 ({today_as_str}) -## Fix -* fix: (feature) add some more text -## Unknown -* {_cm_rstripped("1.1.0-alpha.3")} -""" + return ReleaseHistory( + unreleased={}, + released={ + version: Release( + tagger=commit_author, + committer=commit_author, + tagged_date=datetime.utcnow(), + elements={ + "fix": [fix_commit_parsed], + }, + ) + }, + ) -def test_default_changelog_template( - repo_with_git_flow_and_release_channels_angular_commits, default_angular_parser +@pytest.fixture +def release_notes_template() -> str: + """Retrieve the semantic-release default release notes template.""" + version_notes_template = files(semantic_release.__name__).joinpath( + Path("data", "templates", "release_notes.md.j2") + ) + return version_notes_template.read_text(encoding="utf-8") + + +@pytest.mark.parametrize("hvcs_client", [Github, Gitlab, Gitea, Bitbucket]) +def test_default_release_notes_template( + example_git_https_url: str, + hvcs_client: type[HvcsBase], + release_notes_template: str, + artificial_release_history: ReleaseHistory, ): - version = Version.parse("1.1.0-alpha.3") - repo = repo_with_git_flow_and_release_channels_angular_commits - env = environment(trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True) - rh = ReleaseHistory.from_git_history( - repo=repo, translator=VersionTranslator(), commit_parser=default_angular_parser + """ + Unit test goal: just make sure it renders the release notes template without error. + + Scenarios are better suited for all the variations (commit types). + """ + version_str = "1.1.0-alpha.3" + version = Version.parse(version_str) + commit_obj = artificial_release_history.released[version]["elements"]["fix"][0] + commit_url = hvcs_client(example_git_https_url).commit_hash_url( + commit_obj.commit.hexsha + ) + commit_description = str.join("\n", commit_obj.descriptions) + expected_content = str.join( + "\n", + [ + f"# v{version_str} ({TODAY_DATE_STR})", + "## Fix", + f"* {commit_description} ([`{commit_obj.commit.hexsha[:7]}`]({commit_url}))", + "", + ], ) + env = environment(trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True) context = make_changelog_context( - hvcs_client=Github(remote_url=repo.remote().url), release_history=rh + hvcs_client=hvcs_client(remote_url=example_git_https_url), + release_history=artificial_release_history, ) context.bind_to_environment(env) - release = rh.released[version] - actual_content = env.from_string(default_release_notes_template).render( - version=version, release=release + actual_content = env.from_string(release_notes_template).render( + version=version, release=context.history.released[version] ) - assert actual_content == EXPECTED_CONTENT + assert expected_content == actual_content diff --git a/tests/unit/semantic_release/cli/test_config.py b/tests/unit/semantic_release/cli/test_config.py index 8956c259c..72447c6d5 100644 --- a/tests/unit/semantic_release/cli/test_config.py +++ b/tests/unit/semantic_release/cli/test_config.py @@ -27,6 +27,10 @@ from pathlib import Path from typing import Any + from git import Repo + + from tests.fixtures.example_project import ExProjectDir + @pytest.mark.parametrize( "remote_config, expected_token", @@ -115,7 +119,7 @@ def test_invalid_commit_parser_value(commit_parser: str): assert "commit_parser" in str(excinfo.value) -def test_default_toml_config_valid(example_project_dir): +def test_default_toml_config_valid(example_project_dir: ExProjectDir): default_config_file = example_project_dir / "default.toml" default_config_file.write_text( @@ -142,9 +146,9 @@ def test_default_toml_config_valid(example_project_dir): ) def test_commit_author_configurable( example_pyproject_toml: Path, - repo_with_no_tags_angular_commits, - mock_env, - expected_author, + repo_with_no_tags_angular_commits: Repo, + mock_env: dict[str, str], + expected_author: str, ): content = tomlkit.loads(example_pyproject_toml.read_text(encoding="utf-8")).unwrap()