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
|
# SPDX-FileCopyrightText: 2024-present Hinrich Mahler <chango@mahlerhome.de>
#
# SPDX-License-Identifier: MIT
import datetime as dtm
import functools
import shutil
import subprocess
from pathlib import Path
import pytest
import chango as chango_module
from chango import Version
from chango.abc import ChanGo
from chango.concrete import (
CommentChangeNote,
CommentVersionNote,
DirectoryChanGo,
DirectoryVersionScanner,
HeaderVersionHistory,
)
from chango.error import ChanGoError
from chango.helpers import ensure_uid
from tests.auxil.files import data_path
@pytest.fixture
def scanner() -> DirectoryVersionScanner:
return DirectoryVersionScanner(TestChanGo.DATA_ROOT, "unreleased")
@pytest.fixture
def chango(scanner) -> DirectoryChanGo:
return DirectoryChanGo(
change_note_type=CommentChangeNote,
version_note_type=CommentVersionNote,
version_history_type=HeaderVersionHistory,
scanner=scanner,
)
@pytest.fixture
def chango_no_unreleased() -> DirectoryChanGo:
return DirectoryChanGo(
change_note_type=CommentChangeNote,
version_note_type=CommentVersionNote,
version_history_type=HeaderVersionHistory,
scanner=DirectoryVersionScanner(TestChanGo.DATA_ROOT, "no-unreleased"),
)
@pytest.fixture
def cache_invalidation_tracker():
class Tracker:
def __init__(self):
self.invalidate_caches = False
self.super_call = None
def __call__(self, *args, **kwargs):
self.invalidate_caches = True
if self.super_call:
self.super_call(*args, **kwargs)
@property
def was_called(self):
return self.invalidate_caches
def set_super(self, super_call):
self.super_call = super_call
return self
return Tracker()
class TestChanGo:
"""Since Chango is an abstract base class, we are testing with DirectoryChanGo as a simple
implementation.
Note that we do *not* test abstract methods, as that is the responsibility of the concrete
implementations.
"""
DATA_ROOT = data_path("directoryversionscanner")
@pytest.mark.parametrize(
"version",
[
None,
"1.1",
Version("1.1", dtm.date(2024, 1, 1)),
"1.2",
Version("1.2", dtm.date(2024, 1, 2)),
Version("new-version", dtm.date(2024, 1, 17)),
],
)
@pytest.mark.parametrize("encoding", ["utf-8", "utf-16"])
@pytest.mark.parametrize(
"has_git", [pytest.param(True, id="with-git"), pytest.param(False, id="without-git")]
)
def test_write_change_note(
self, chango, version, monkeypatch, encoding, cache_invalidation_tracker, has_git
):
# Unfortunately, testing the git-available part is not easily possible without using
# some of the internal utils and also not with directly running git. This is because
# a) the availability of git is cached and there is no public interface to reset it
# b) mocking subprocess before the module is imported is not easily possible
# c) actually running `git add` is hard to reset
# Since `chango._utils.files` is not part of the public API, we settle for testing
# with the private interfaces.
chango_module._utils.files._GIT_HELPER.git_available = None
def check_call(args, *_, **__):
assert args[:2] == ["git", "add"]
if not has_git:
raise subprocess.CalledProcessError(1, "git add")
monkeypatch.setattr("chango._utils.files.subprocess.check_call", check_call)
if version is None:
expected_path = chango.scanner.unreleased_directory
else:
version_uid = ensure_uid(version)
if version_uid == "new-version":
expected_path = chango.scanner.base_directory / "new-version_2024-01-17"
else:
day = int(version_uid.split(".")[-1])
expected_path = chango.scanner.base_directory / f"{version_uid}_2024-01-0{day}"
existed = expected_path.is_dir()
def to_file(*_, **kwargs):
assert kwargs.get("encoding") == encoding
assert kwargs.get("directory") == expected_path
note = chango.build_template_change_note("this-is-a-new-slug")
monkeypatch.setattr(note, "to_file", to_file)
monkeypatch.setattr(chango.scanner, "invalidate_caches", cache_invalidation_tracker)
for _ in range(3):
# run multiple times to cover all paths in _GIT_HELPER
try:
chango.write_change_note(note, version, encoding=encoding)
assert cache_invalidation_tracker.was_called
finally:
if not existed and expected_path.is_dir():
shutil.rmtree(expected_path)
def test_write_change_note_new_string_version(self, chango):
note = chango.build_template_change_note("this-is-a-new-slug")
with pytest.raises(ChanGoError, match="'new-version-uid' not available"):
chango.write_change_note(note, "new-version-uid")
def test_load_version_note_unavailable(self, chango):
with pytest.raises(ChanGoError, match="Version '1.4' not available."):
chango.load_version_note("1.4")
@pytest.mark.parametrize(
"version",
[
None,
"1.1",
Version("1.1", dtm.date(2024, 1, 1)),
"1.2",
Version("1.2", dtm.date(2024, 1, 2)),
],
)
def test_load_version_note(self, chango, version):
version_note = chango.load_version_note(version)
version_uid = ensure_uid(version)
expected_uids = {
f"uid_{(version_uid or 'ur').replace('.', '-')}_{idx}" for idx in range(3)
}
assert version_note.uid == version_uid
assert version_note.date == (
dtm.date(2024, 1, int(version_uid.split(".")[-1])) if version else None
)
assert set(version_note) == expected_uids
@pytest.mark.parametrize(
("start_from", "end_at"),
[(None, None), (None, "1.2"), ("1.3", None), ("1.2", "1.3"), ("1.3", "1.3")],
)
def test_load_version_history(self, chango, start_from, end_at):
lower_idx = int(start_from.split(".")[-1]) if start_from else 1
upper_idx = int(end_at.split(".")[-1]) + 1 if end_at else 4
versions = {
Version(f"1.{idx}", dtm.date(2024, 1, idx)) for idx in range(lower_idx, upper_idx)
}
if not end_at:
versions |= {Version("1.3.1", dtm.date(2024, 1, 3)), None}
version_history = chango.load_version_history(start_from, end_at)
assert set(version_history) == set(map(ensure_uid, versions))
for version in versions:
assert version_history[ensure_uid(version)].date == (version.date if version else None)
assert version_history[ensure_uid(version)].version == version
def test_release_no_unreleased_changes(
self, chango_no_unreleased: ChanGo, monkeypatch, cache_invalidation_tracker
):
monkeypatch.setattr(
chango_no_unreleased.scanner, "invalidate_caches", cache_invalidation_tracker
)
version = Version("1.4", dtm.date(2024, 1, 4))
assert not chango_no_unreleased.release(version)
assert not chango_no_unreleased.scanner.is_available(version)
assert not cache_invalidation_tracker.was_called
@pytest.mark.parametrize(
"has_git", [pytest.param(True, id="with-git"), pytest.param(False, id="without-git")]
)
def test_release(self, chango, cache_invalidation_tracker, monkeypatch, has_git):
# Unfortunately, testing the git-available part is not easily possible without using
# some of the internal utils and also not with directly running git. This is because
# a) the availability of git is cached and there is no public interface to reset it
# b) mocking subprocess before the module is imported is not easily possible
# c) actually running `git mv` is harder to reset than just using the pathlib move
# Since `chango._utils.files` is not part of the public API, we settle for testing
# with the private interfaces.
chango_module._utils.files._GIT_HELPER.git_available = None
def check_call(args, *_, **__):
assert args[:2] == ["git", "mv"]
if not has_git:
raise subprocess.CalledProcessError(1, "git mv")
source, destination = args[2], args[3]
Path(source).rename(destination)
monkeypatch.setattr("chango._utils.files.subprocess.check_call", check_call)
version = Version("1.4", dtm.date(2024, 1, 4))
expected_path = chango.scanner.base_directory / "1.4_2024-01-04"
expected_files = {
(file.name, file.read_bytes())
for file in (self.DATA_ROOT / "unreleased").iterdir()
if file.name != "not-a-change-note.txt"
}
monkeypatch.setattr(
chango.scanner,
"invalidate_caches",
cache_invalidation_tracker.set_super(chango.scanner.invalidate_caches),
)
try:
assert chango.release(version)
assert cache_invalidation_tracker.was_called
assert chango.scanner.is_available(version)
assert chango.scanner.get_version(version.uid) == version
assert expected_path.is_dir()
assert {
(file.name, file.read_bytes()) for file in expected_path.iterdir()
} == expected_files
finally:
for file_name, file_content in expected_files:
(self.DATA_ROOT / "unreleased" / file_name).write_bytes(file_content)
if expected_path.is_dir():
shutil.rmtree(expected_path)
def test_release_same_directory(self, chango, monkeypatch, cache_invalidation_tracker):
def get_write_directory(*_, **__):
return chango.scanner.unreleased_directory
monkeypatch.setattr(chango, "get_write_directory", get_write_directory)
monkeypatch.setattr(
chango.scanner,
"invalidate_caches",
cache_invalidation_tracker.set_super(chango.scanner.invalidate_caches),
)
version = Version("1.4", dtm.date(2024, 1, 4))
expected_files = {
(file.name, file.read_bytes())
for file in (self.DATA_ROOT / "unreleased").iterdir()
if file.name != "not-a-change-note.txt"
}
try:
assert chango.release(version)
assert cache_invalidation_tracker.was_called
for file_name, file_content in expected_files:
assert (self.DATA_ROOT / "unreleased" / file_name).read_bytes() == file_content
except Exception:
for file_name, file_content in expected_files:
(self.DATA_ROOT / "unreleased" / file_name).write_bytes(file_content)
def test_build_github_event_change_note(self, chango, monkeypatch):
monkeypatch.setattr(
chango,
"build_github_event_change_note",
functools.partial(ChanGo.build_github_event_change_note, chango),
)
with pytest.raises(NotImplementedError):
chango.build_github_event_change_note({})
|