Skip to content

Commit 4ab60ca

Browse files
committed
test(fixtures): refactor e2e infrastructure to be more configurable & resilient
1 parent 46d189f commit 4ab60ca

File tree

9 files changed

+658
-277
lines changed

9 files changed

+658
-277
lines changed

tests/conftest.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Note: fixtures are stored in the tests/fixtures directory for better organisation"""
1+
"""Note: fixtures are stored in the tests/fixtures directory for better organization"""
22

33
from __future__ import annotations
44

@@ -9,21 +9,23 @@
99
from hashlib import md5
1010
from pathlib import Path
1111
from tempfile import NamedTemporaryFile
12-
from typing import TYPE_CHECKING
12+
from typing import TYPE_CHECKING, cast
1313
from unittest import mock
1414

1515
import pytest
1616
from click.testing import CliRunner
1717
from filelock import FileLock
1818
from git import Commit, Repo
1919

20+
from semantic_release.version.version import Version
21+
2022
from tests.const import PROJ_DIR
2123
from tests.fixtures import *
2224
from tests.util import copy_dir_tree, remove_dir_tree
2325

2426
if TYPE_CHECKING:
2527
from tempfile import _TemporaryFileWrapper
26-
from typing import Any, Callable, Generator, Protocol, Sequence, TypedDict
28+
from typing import Any, Callable, Generator, Optional, Protocol, Sequence, TypedDict
2729

2830
from click.testing import Result
2931
from filelock import AcquireReturnProxy
@@ -325,7 +327,7 @@ def _get_authorization_to_build_repo_cache(
325327
def get_cached_repo_data(request: pytest.FixtureRequest) -> GetCachedRepoDataFn:
326328
def _get_cached_repo_data(proj_dirname: str) -> RepoData | None:
327329
cache_key = f"psr/repos/{proj_dirname}"
328-
return request.config.cache.get(cache_key, None)
330+
return cast("Optional[RepoData]", request.config.cache.get(cache_key, None))
329331

330332
return _get_cached_repo_data
331333

@@ -335,6 +337,10 @@ def set_cached_repo_data(request: pytest.FixtureRequest) -> SetCachedRepoDataFn:
335337
def magic_serializer(obj: Any) -> Any:
336338
if isinstance(obj, Path):
337339
return obj.__fspath__()
340+
341+
if isinstance(obj, Version):
342+
return obj.__dict__
343+
338344
return obj
339345

340346
def _set_cached_repo_data(proj_dirname: str, data: RepoData) -> None:
@@ -386,13 +392,30 @@ def _build_repo_w_cache_checking(
386392
with log_file_lock, log_file.open(mode="a") as afd:
387393
afd.write(f"{stable_now_date().isoformat()}: {build_msg}...\n")
388394

395+
try:
396+
# Try to build repository but catch any errors so that it doesn't cascade through all tests
397+
# do to an unreleased lock
398+
build_definition = build_repo_func(cached_repo_path)
399+
except Exception:
400+
remove_dir_tree(cached_repo_path, force=True)
401+
402+
if filelock:
403+
filelock.lock.release()
404+
405+
with log_file_lock, log_file.open(mode="a") as afd:
406+
afd.write(
407+
f"{stable_now_date().isoformat()}: {build_msg}...FAILED\n"
408+
)
409+
410+
raise
411+
389412
# Marks the date when the cached repo was created
390413
set_cached_repo_data(
391414
repo_name,
392415
{
393416
"build_date": today_date_str,
394417
"build_spec_hash": build_spec_hash,
395-
"build_definition": build_repo_func(cached_repo_path),
418+
"build_definition": build_definition,
396419
},
397420
)
398421

tests/e2e/cmd_changelog/test_changelog.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@
7777

7878
from requests_mock import Mocker
7979

80+
from semantic_release.commit_parser.conventional.parser import (
81+
ConventionalCommitParser,
82+
)
83+
from semantic_release.commit_parser.emoji import EmojiCommitParser
84+
from semantic_release.commit_parser.scipy import ScipyCommitParser
85+
8086
from tests.conftest import RunCliFn
8187
from tests.e2e.conftest import RetrieveRuntimeContextFn
8288
from tests.fixtures.example_project import (
@@ -867,9 +873,12 @@ def test_changelog_update_mode_unreleased_n_released(
867873
commit_n_rtn_changelog_entry: CommitNReturnChangelogEntryFn,
868874
changelog_file: Path,
869875
insertion_flag: str,
870-
get_commit_def_of_conventional_commit: GetCommitDefFn,
871-
get_commit_def_of_emoji_commit: GetCommitDefFn,
872-
get_commit_def_of_scipy_commit: GetCommitDefFn,
876+
get_commit_def_of_conventional_commit: GetCommitDefFn[ConventionalCommitParser],
877+
get_commit_def_of_emoji_commit: GetCommitDefFn[EmojiCommitParser],
878+
get_commit_def_of_scipy_commit: GetCommitDefFn[ScipyCommitParser],
879+
default_conventional_parser: ConventionalCommitParser,
880+
default_emoji_parser: EmojiCommitParser,
881+
default_scipy_parser: ScipyCommitParser,
873882
):
874883
"""
875884
Given there are unreleased changes and a previous release in the changelog,
@@ -890,18 +899,23 @@ def test_changelog_update_mode_unreleased_n_released(
890899
commit_n_section: Commit2Section = {
891900
"conventional": {
892901
"commit": get_commit_def_of_conventional_commit(
893-
"perf: improve the performance of the application"
902+
"perf: improve the performance of the application",
903+
parser=default_conventional_parser,
894904
),
895905
"section": "Performance Improvements",
896906
},
897907
"emoji": {
898908
"commit": get_commit_def_of_emoji_commit(
899-
":zap: improve the performance of the application"
909+
":zap: improve the performance of the application",
910+
parser=default_emoji_parser,
900911
),
901912
"section": ":zap:",
902913
},
903914
"scipy": {
904-
"commit": get_commit_def_of_scipy_commit("MAINT: fix an issue"),
915+
"commit": get_commit_def_of_scipy_commit(
916+
"MAINT: fix an issue",
917+
parser=default_scipy_parser,
918+
),
905919
"section": "Fix",
906920
},
907921
}

tests/e2e/cmd_version/bump_version/conftest.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
if TYPE_CHECKING:
1414
from pathlib import Path
15-
from typing import Protocol
15+
from typing import Protocol, Sequence
1616

1717
from click.testing import Result
1818

@@ -24,40 +24,51 @@ class InitMirrorRepo4RebuildFn(Protocol):
2424
def __call__(
2525
self,
2626
mirror_repo_dir: Path,
27-
configuration_step: RepoActionConfigure,
27+
configuration_steps: Sequence[RepoActionConfigure],
28+
files_to_remove: Sequence[Path],
2829
) -> Path: ...
2930

3031
class RunPSReleaseFn(Protocol):
3132
def __call__(
3233
self,
3334
next_version_str: str,
3435
git_repo: Repo,
36+
config_toml_path: Path = ...,
3537
) -> Result: ...
3638

3739

3840
@pytest.fixture(scope="session")
3941
def init_mirror_repo_for_rebuild(
4042
build_repo_from_definition: BuildRepoFromDefinitionFn,
41-
changelog_md_file: Path,
42-
changelog_rst_file: Path,
4343
) -> InitMirrorRepo4RebuildFn:
4444
def _init_mirror_repo_for_rebuild(
4545
mirror_repo_dir: Path,
46-
configuration_step: RepoActionConfigure,
46+
configuration_steps: Sequence[RepoActionConfigure],
47+
files_to_remove: Sequence[Path],
4748
) -> Path:
4849
# Create the mirror repo directory
4950
mirror_repo_dir.mkdir(exist_ok=True, parents=True)
5051

5152
# Initialize mirror repository
5253
build_repo_from_definition(
5354
dest_dir=mirror_repo_dir,
54-
repo_construction_steps=[configuration_step],
55+
repo_construction_steps=configuration_steps,
5556
)
5657

5758
with Repo(mirror_repo_dir) as mirror_git_repo:
58-
# remove the default changelog files to enable Update Mode (new default of v10)
59-
mirror_git_repo.git.rm(str(changelog_md_file), force=True)
60-
mirror_git_repo.git.rm(str(changelog_rst_file), force=True)
59+
for filepath in files_to_remove:
60+
file = (
61+
(mirror_git_repo.working_dir / filepath).resolve().absolute()
62+
if not filepath.is_absolute()
63+
else filepath
64+
)
65+
if (
66+
not file.is_relative_to(mirror_git_repo.working_dir)
67+
or not file.exists()
68+
):
69+
continue
70+
71+
mirror_git_repo.git.rm(str(file), force=True)
6172

6273
return mirror_repo_dir
6374

@@ -69,6 +80,7 @@ def run_psr_release(
6980
run_cli: RunCliFn,
7081
changelog_rst_file: Path,
7182
update_pyproject_toml: UpdatePyprojectTomlFn,
83+
pyproject_toml_file: Path,
7284
) -> RunPSReleaseFn:
7385
base_version_cmd = [MAIN_PROG_NAME, "--strict", VERSION_SUBCMD]
7486
write_changelog_only_cmd = [
@@ -82,6 +94,7 @@ def run_psr_release(
8294
def _run_psr_release(
8395
next_version_str: str,
8496
git_repo: Repo,
97+
config_toml_path: Path = pyproject_toml_file,
8598
) -> Result:
8699
version_n_buildmeta = next_version_str.split("+", maxsplit=1)
87100
version_n_prerelease = version_n_buildmeta[0].split("-", maxsplit=1)
@@ -107,6 +120,7 @@ def _run_psr_release(
107120
update_pyproject_toml(
108121
"tool.semantic_release.changelog.default_templates.changelog_file",
109122
str(changelog_rst_file),
123+
toml_file=config_toml_path,
110124
)
111125
cli_cmd = [*write_changelog_only_cmd, *prerelease_args, *build_metadata_args]
112126
result = run_cli(cli_cmd[1:], env={Github.DEFAULT_ENV_TOKEN_NAME: "1234"})
@@ -116,7 +130,7 @@ def _run_psr_release(
116130
git_repo.git.reset("--mixed", "HEAD")
117131

118132
# Add the changelog file to the git index but reset the working directory
119-
git_repo.git.add(str(changelog_rst_file))
133+
git_repo.git.add(str(changelog_rst_file.resolve()))
120134
git_repo.git.checkout("--", ".")
121135

122136
# Actual run to release & write the MD changelog

tests/e2e/conftest.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class GetSanitizedChangelogContentFn(Protocol):
3434
def __call__(
3535
self,
3636
repo_dir: Path,
37+
changelog_file: Path = ...,
3738
remove_insertion_flag: bool = True,
3839
) -> str: ...
3940

@@ -81,7 +82,7 @@ def config_path(example_project_dir: ExProjectDir) -> Path:
8182
return example_project_dir / DEFAULT_CONFIG_FILE
8283

8384

84-
@pytest.fixture
85+
@pytest.fixture(scope="session")
8586
def read_config_file() -> ReadConfigFileFn:
8687
def _read_config_file(file: Path | str) -> RawConfig:
8788
config_text = load_raw_config_file(file)
@@ -136,31 +137,35 @@ def _strip_logging_messages(log: str) -> str:
136137

137138

138139
@pytest.fixture(scope="session")
139-
def long_hash_pattern() -> Pattern:
140+
def long_hash_pattern() -> Pattern[str]:
140141
return regexp(r"\b([0-9a-f]{40})\b", IGNORECASE)
141142

142143

143144
@pytest.fixture(scope="session")
144-
def short_hash_pattern() -> Pattern:
145+
def short_hash_pattern() -> Pattern[str]:
145146
return regexp(r"\b([0-9a-f]{7})\b", IGNORECASE)
146147

147148

148149
@pytest.fixture(scope="session")
149150
def get_sanitized_rst_changelog_content(
150151
changelog_rst_file: Path,
151152
default_rst_changelog_insertion_flag: str,
152-
long_hash_pattern: Pattern,
153-
short_hash_pattern: Pattern,
153+
long_hash_pattern: Pattern[str],
154+
short_hash_pattern: Pattern[str],
154155
) -> GetSanitizedChangelogContentFn:
155156
rst_short_hash_link_pattern = regexp(r"(_[0-9a-f]{7})\b", IGNORECASE)
156157

157158
def _get_sanitized_rst_changelog_content(
158159
repo_dir: Path,
160+
changelog_file: Path = changelog_rst_file,
159161
remove_insertion_flag: bool = False,
160162
) -> str:
163+
if not (changelog_path := repo_dir / changelog_file).exists():
164+
return ""
165+
161166
# Note that our repo generation fixture includes the insertion flag automatically
162167
# toggle remove_insertion_flag to True to remove the insertion flag, applies to Init mode repos
163-
with (repo_dir / changelog_rst_file).open(newline=os.linesep) as rfd:
168+
with changelog_path.open(newline=os.linesep) as rfd:
164169
# use os.linesep here because the insertion flag is os-specific
165170
# but convert the content to universal newlines for comparison
166171
changelog_content = (
@@ -182,16 +187,20 @@ def _get_sanitized_rst_changelog_content(
182187
def get_sanitized_md_changelog_content(
183188
changelog_md_file: Path,
184189
default_md_changelog_insertion_flag: str,
185-
long_hash_pattern: Pattern,
186-
short_hash_pattern: Pattern,
190+
long_hash_pattern: Pattern[str],
191+
short_hash_pattern: Pattern[str],
187192
) -> GetSanitizedChangelogContentFn:
188193
def _get_sanitized_md_changelog_content(
189194
repo_dir: Path,
195+
changelog_file: Path = changelog_md_file,
190196
remove_insertion_flag: bool = False,
191197
) -> str:
198+
if not (changelog_path := repo_dir / changelog_file).exists():
199+
return ""
200+
192201
# Note that our repo generation fixture includes the insertion flag automatically
193202
# toggle remove_insertion_flag to True to remove the insertion flag, applies to Init mode repos
194-
with (repo_dir / changelog_md_file).open(newline=os.linesep) as rfd:
203+
with changelog_path.open(newline=os.linesep) as rfd:
195204
# use os.linesep here because the insertion flag is os-specific
196205
# but convert the content to universal newlines for comparison
197206
changelog_content = (

tests/e2e/test_main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import subprocess
55
from pathlib import Path
6+
from shutil import rmtree
67
from textwrap import dedent
78
from typing import TYPE_CHECKING
89

@@ -18,8 +19,6 @@
1819
from tests.util import assert_exit_code, assert_successful_exit_code
1920

2021
if TYPE_CHECKING:
21-
from pathlib import Path
22-
2322
from tests.conftest import RunCliFn
2423
from tests.e2e.conftest import StripLoggingMessagesFn
2524
from tests.fixtures.example_project import ExProjectDir, UpdatePyprojectTomlFn
@@ -245,7 +244,10 @@ def test_uses_default_config_when_no_config_file_found(
245244
# We have to initialise an empty git repository, as the example projects
246245
# all have pyproject.toml configs which would be used by default
247246
with git.Repo.init(example_project_dir) as repo:
247+
rmtree(str(Path(repo.git_dir, "hooks")))
248+
248249
repo.git.branch("-M", "main")
250+
249251
with repo.config_writer("repository") as config:
250252
config.set_value("user", "name", "semantic release testing")
251253
config.set_value("user", "email", "[email protected]")

0 commit comments

Comments
 (0)