File: test_chango.py

package info (click to toggle)
python-sphinx-chango 0.5.0-2
  • links: PTS
  • area: main
  • in suites: sid
  • size: 1,776 kB
  • sloc: python: 4,909; javascript: 74; makefile: 23
file content (299 lines) | stat: -rw-r--r-- 11,478 bytes parent folder | download
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({})