Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(changelog): refactor changelog validation #809

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2bdd085
test(fixtures): add common fixture to write default changelog from re…
codejedi365 Jan 16, 2024
4f30dab
test(cli-changelog): refactor changelog re-gen test to show debuggabl…
codejedi365 Jan 1, 2024
9349a4f
test(changelog): add assertion to check for changelog file exist befo…
codejedi365 Jan 7, 2024
2e0ba55
test: improve reliability & error readability of assertions
codejedi365 Jan 2, 2024
6cc0704
test(cli-version): ensure CHANGELOG is included in changed files
codejedi365 Jan 2, 2024
b262d3b
test(fixtures): remove changelog generation prevention
codejedi365 Jan 17, 2024
34ddf60
test(fixtures): trigger changelog generation in repo fixtures
codejedi365 Jan 17, 2024
615c4b0
test(repo-commits): fix angular syntax for scopes in commits
codejedi365 Jan 2, 2024
4b42df3
test(unit-changelog): drop context test & duplicate/incorrect template
codejedi365 Feb 4, 2024
ca8b086
test(unit-release-notes): refactor template testing
codejedi365 Feb 4, 2024
475a3e0
test(unit-changelog): refactor template testing to be fast & simple
codejedi365 Feb 5, 2024
6dfaa03
test(fixtures): refactor for better chronological ordering for test s…
codejedi365 Feb 10, 2024
ab955eb
test(changelog): enforce common single newline after generated docs
codejedi365 Feb 10, 2024
637d159
fix(changelog): make sure default templates render ending in 1 newline
codejedi365 Feb 10, 2024
d6f7d48
style(tests): add additional typing to test args
codejedi365 Jan 7, 2024
d6e15fe
fix(changelog-generation): fix incorrect release timezone determination
codejedi365 Jan 7, 2024
db4eb8e
test(changelog): increase changelog rigor to all commit types
codejedi365 Mar 2, 2024
195f260
style(test-changelog): change to direct fixture reference & improve c…
codejedi365 Mar 2, 2024
5b1c96b
style: resolve linter & formatting across codebase
codejedi365 Mar 2, 2024
230e72b
test: add bitbucket to hvcs parameter list & bitbucket to configs
codejedi365 Mar 2, 2024
46d3c35
test(fixtures): adjust scipy changelog expectations related to parse …
codejedi365 Mar 2, 2024
a183229
test(fixtures): correct the ordering of commits in changelog expectat…
codejedi365 Mar 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 6 additions & 2 deletions semantic_release/changelog/release_history.py
Expand Up @@ -81,15 +81,19 @@ 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
)
else:
# 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
)
Expand Down
4 changes: 2 additions & 2 deletions semantic_release/cli/commands/changelog.py
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion semantic_release/cli/commands/version.py
Expand Up @@ -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))]

Expand Down
8 changes: 5 additions & 3 deletions semantic_release/cli/common.py
Expand Up @@ -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(
Expand All @@ -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()
)
2 changes: 1 addition & 1 deletion semantic_release/commit_parser/util.py
Expand Up @@ -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.

Expand Down
165 changes: 119 additions & 46 deletions tests/command_line/test_changelog.py
Expand Up @@ -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

Expand All @@ -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",
),
],
Expand Down Expand Up @@ -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)

Expand All @@ -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")]
)
Expand All @@ -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],
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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"]