1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
|
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING, NamedTuple
import pytest
from git import Actor
from pytest_lazy_fixtures.lazy_fixture import lf as lazy_fixture
from semantic_release.changelog.release_history import ReleaseHistory
from semantic_release.version.translator import VersionTranslator
from semantic_release.version.version import Version
from tests.const import COMMIT_MESSAGE, CONVENTIONAL_COMMITS_MINOR
from tests.fixtures import (
repo_w_git_flow_w_alpha_prereleases_n_conventional_commits,
repo_w_git_flow_w_rc_n_alpha_prereleases_n_conventional_commits,
repo_w_github_flow_w_feature_release_channel_conventional_commits,
repo_w_no_tags_conventional_commits,
repo_w_trunk_only_conventional_commits,
repo_w_trunk_only_n_prereleases_conventional_commits,
)
from tests.util import add_text_to_file
if TYPE_CHECKING:
from typing import Protocol
from semantic_release.commit_parser.conventional import ConventionalCommitParser
from tests.fixtures.git_repo import (
BuiltRepoResult,
GetCommitsFromRepoBuildDefFn,
RepoDefinition,
)
class CreateReleaseHistoryFromRepoDefFn(Protocol):
def __call__(self, repo_def: RepoDefinition) -> FakeReleaseHistoryElements: ...
# NOTE: not testing parser correctness here, just that the right commits end up
# in the right places. So we only compare that the commits with the messages
# we anticipate are in the right place, rather than by hash
# So we are only using the conventional parser
# We are also currently only testing that the "elements" key of the releases
# is correct, i.e. the commits are in the right place - the other fields
# will need special attention of their own later
class FakeReleaseHistoryElements(NamedTuple):
"""
A fake release history structure that abstracts away the Parser-specific
logic and only focuses that the commit messages are in the correct order and place.
Where generally a ParsedCommit object exists, here we just use the actual `commit.message`.
"""
unreleased: dict[str, list[str]]
released: dict[Version, dict[str, list[str]]]
@pytest.fixture(scope="session")
def create_release_history_from_repo_def() -> CreateReleaseHistoryFromRepoDefFn:
def _create_release_history_from_repo_def(
repo_def: RepoDefinition,
) -> FakeReleaseHistoryElements:
# Organize the commits into the expected structure
unreleased_history = {}
released_history = {}
for version_str, version_def in repo_def.items():
commits_per_group: dict[str, list] = {
"Unknown": [],
}
for commit in version_def["commits"]:
if commit["category"] not in commits_per_group:
commits_per_group[commit["category"]] = []
commits_per_group[commit["category"]].append(commit["msg"].strip())
if version_str == "Unreleased":
unreleased_history = commits_per_group
continue
# handle released versions
version = Version.parse(version_str)
# add the PSR version commit message
commits_per_group["Unknown"].append(
COMMIT_MESSAGE.format(version=version).strip()
)
# store the organized commits for this version
released_history[version] = commits_per_group
return FakeReleaseHistoryElements(
unreleased=unreleased_history,
released=released_history,
)
return _create_release_history_from_repo_def
@pytest.mark.parametrize(
"repo_result",
[
# CONVENTIONAL parser
lazy_fixture(repo_w_no_tags_conventional_commits.__name__),
*[
pytest.param(
lazy_fixture(repo_fixture_name),
marks=pytest.mark.comprehensive,
)
for repo_fixture_name in [
repo_w_trunk_only_conventional_commits.__name__,
repo_w_trunk_only_n_prereleases_conventional_commits.__name__,
# This is not tested because currently unable to disern the commits that were squashed or not
# repo_w_github_flow_w_default_release_channel_conventional_commits.__name__,
repo_w_github_flow_w_feature_release_channel_conventional_commits.__name__,
repo_w_git_flow_w_alpha_prereleases_n_conventional_commits.__name__,
repo_w_git_flow_w_rc_n_alpha_prereleases_n_conventional_commits.__name__,
]
],
],
)
@pytest.mark.order("last")
def test_release_history(
repo_result: BuiltRepoResult,
default_conventional_parser: ConventionalCommitParser,
file_in_repo: str,
create_release_history_from_repo_def: CreateReleaseHistoryFromRepoDefFn,
get_commits_from_repo_build_def: GetCommitsFromRepoBuildDefFn,
):
repo = repo_result["repo"]
expected_release_history = create_release_history_from_repo_def(
get_commits_from_repo_build_def(
repo_result["definition"],
ignore_merge_commits=default_conventional_parser.options.ignore_merge_commits,
)
)
expected_released_versions = sorted(
map(str, expected_release_history.released.keys())
)
translator = VersionTranslator()
# Nothing has unreleased commits currently
history = ReleaseHistory.from_git_history(
repo,
translator,
default_conventional_parser, # type: ignore[arg-type]
)
released = history.released
actual_released_versions = sorted(map(str, released.keys()))
assert expected_released_versions == actual_released_versions
for k in expected_release_history.released:
expected = expected_release_history.released[k]
expected_released_messages = str.join(
"\n---\n", sorted([msg for bucket in expected.values() for msg in bucket])
)
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
]
),
)
assert expected_released_messages == actual_released_messages
# PART 2: add some commits to the repo and check that they are in the right place
for commit_message in CONVENTIONAL_COMMITS_MINOR:
add_text_to_file(repo, file_in_repo)
repo.git.commit(m=commit_message)
expected_unreleased_messages = str.join(
"\n---\n",
sorted(
[
str(msg).strip()
for bucket in [
CONVENTIONAL_COMMITS_MINOR[::-1],
*expected_release_history.unreleased.values(),
]
for msg in bucket
]
),
)
# Now we should have some unreleased commits, and nothing new released
new_history = ReleaseHistory.from_git_history(
repo,
translator,
default_conventional_parser, # type: ignore[arg-type]
)
new_unreleased = new_history.unreleased
new_released = new_history.released
actual_unreleased_messages = str.join(
"\n---\n",
sorted(
[
str(res.commit.message)
for results in new_unreleased.values()
for res in results
]
),
)
assert expected_unreleased_messages == actual_unreleased_messages
assert (
new_released == released
), "something that shouldn't be considered release has been released"
@pytest.mark.parametrize(
"repo_result",
[
lazy_fixture(repo_w_no_tags_conventional_commits.__name__),
*[
pytest.param(
lazy_fixture(repo_fixture_name),
marks=pytest.mark.comprehensive,
)
for repo_fixture_name in [
repo_w_trunk_only_conventional_commits.__name__,
repo_w_trunk_only_n_prereleases_conventional_commits.__name__,
repo_w_github_flow_w_feature_release_channel_conventional_commits.__name__,
repo_w_git_flow_w_alpha_prereleases_n_conventional_commits.__name__,
repo_w_git_flow_w_rc_n_alpha_prereleases_n_conventional_commits.__name__,
]
],
],
)
@pytest.mark.order("last")
def test_release_history_releases(
repo_result: BuiltRepoResult, default_conventional_parser: ConventionalCommitParser
):
new_version = Version.parse("100.10.1")
actor = Actor("semantic-release", "semantic-release")
release_history = ReleaseHistory.from_git_history(
repo=repo_result["repo"],
translator=VersionTranslator(),
commit_parser=default_conventional_parser, # type: ignore[arg-type]
)
tagged_date = datetime.now()
new_rh = release_history.release(
new_version,
committer=actor,
tagger=actor,
tagged_date=tagged_date,
)
assert new_rh is not release_history
assert new_rh.unreleased == {}
assert new_rh.released == {
new_version: {
"tagger": actor,
"committer": actor,
"tagged_date": tagged_date,
"elements": release_history.unreleased,
"version": new_version,
},
**release_history.released,
}
@pytest.mark.parametrize(
"repo_result",
[
lazy_fixture(repo_w_no_tags_conventional_commits.__name__),
*[
pytest.param(
lazy_fixture(repo_fixture_name),
marks=pytest.mark.comprehensive,
)
for repo_fixture_name in [
repo_w_trunk_only_conventional_commits.__name__,
repo_w_trunk_only_n_prereleases_conventional_commits.__name__,
repo_w_github_flow_w_feature_release_channel_conventional_commits.__name__,
repo_w_git_flow_w_alpha_prereleases_n_conventional_commits.__name__,
repo_w_git_flow_w_rc_n_alpha_prereleases_n_conventional_commits.__name__,
]
],
],
)
@pytest.mark.order("last")
def test_all_matching_repo_tags_are_released(
repo_result: BuiltRepoResult, default_conventional_parser: ConventionalCommitParser
):
repo = repo_result["repo"]
translator = VersionTranslator()
release_history = ReleaseHistory.from_git_history(
repo=repo,
translator=translator,
commit_parser=default_conventional_parser, # type: ignore[arg-type]
)
for tag in repo.tags:
assert translator.from_tag(tag.name) in release_history.released
|