Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions tests/test_diff_versions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# -*- coding: utf-8 -*-
"""Which changelog a new one gets compared against.

`write_changelog` asks `find_previous_version` what the previous release was,
and the answer comes from the file names in `changelogs/` alone. That made the
naming of those files load-bearing: the pattern used to be digits and dots, so
a version carrying a release level -- 3.14.7.1b1 -- matched nothing and the
comparison skipped straight past it. PEP 440 orders those versions correctly
already (3.14.7.0 < 3.14.7.1b1 < 3.14.7.1); only the file name pattern did not.

The layouts here are synthetic: the functions care about names, not contents,
so an empty file under the right name exercises the real code.
"""
from pathlib import Path

import pytest

from wppm.diff import changelog_versions, copy_changelogs, find_previous_version


def changelogs(searchdir: Path, *names: str) -> Path:
"""A changelogs directory holding exactly these files."""
searchdir.mkdir(parents=True, exist_ok=True)
for name in names:
(searchdir / name).write_text("", encoding="utf-8")
return searchdir


class TestFindPreviousVersion:
def test_picks_the_nearest_earlier_version(self, tmp_path):
searchdir = changelogs(
tmp_path,
"WinPythondot-64bit-3.13.15.0.md",
"WinPythondot-64bit-3.14.7.0.md",
"WinPythondot-64bit-3.14.7.1.md",
)
assert find_previous_version("3.14.7.1", searchdir, "dot") == "3.14.7.0"

def test_a_release_level_is_visible_and_ordered(self, tmp_path):
"""The b1 sits between .0 and .1, so it is what .1 compares against.

This is the case the old digits-and-dots pattern dropped: it would
have answered 3.14.7.0 and reported a beta's worth of changes twice.
"""
searchdir = changelogs(
tmp_path,
"WinPythondot-64bit-3.14.7.0.md",
"WinPythondot-64bit-3.14.7.1b1.md",
)
assert find_previous_version("3.14.7.1", searchdir, "dot") == "3.14.7.1b1"

def test_a_release_level_compares_against_the_release_before_it(self, tmp_path):
searchdir = changelogs(
tmp_path,
"WinPythondot-64bit-3.14.7.0.md",
"WinPythondot-64bit-3.14.7.1b1.md",
)
assert find_previous_version("3.14.7.1b1", searchdir, "dot") == "3.14.7.0"

@pytest.mark.parametrize("level", ["b1", "b2", "rc1", "a1", "post1"])
def test_every_level_winpython_has_used_parses(self, tmp_path, level):
searchdir = changelogs(
tmp_path,
"WinPythondot-64bit-3.14.7.0.md",
f"WinPythondot-64bit-3.14.7.1{level}.md",
)
assert find_previous_version("3.14.8.0", searchdir, "dot") == f"3.14.7.1{level}"

def test_history_companions_are_never_the_answer(self, tmp_path):
"""`_History.md` sits beside every changelog and is not one."""
searchdir = changelogs(
tmp_path,
"WinPythondot-64bit-3.14.7.0.md",
"WinPythondot-64bit-3.14.7.0_History.md",
)
assert find_previous_version("3.14.7.1", searchdir, "dot") == "3.14.7.0"

def test_unrelated_files_are_ignored(self, tmp_path):
searchdir = changelogs(
tmp_path,
"WinPythondot-64bit-3.14.7.0.md",
"pylock.64-3_14_7_0dot.toml",
"requir.64-3_14_7_0dot.txt",
"README.md",
)
assert find_previous_version("3.14.7.1", searchdir, "dot") == "3.14.7.0"

def test_the_old_txt_changelogs_still_count(self, tmp_path):
searchdir = changelogs(tmp_path, "WinPython-64bit-2.7.10.1.txt")
assert find_previous_version("2.7.10.2", searchdir, "") == "2.7.10.1"

def test_nothing_earlier_returns_the_target_itself(self, tmp_path):
"""The documented fallback: comparing a version against itself is empty."""
searchdir = changelogs(tmp_path, "WinPythondot-64bit-3.14.7.1.md")
assert find_previous_version("3.14.7.1", searchdir, "dot") == "3.14.7.1"

def test_flavors_do_not_see_each_other(self, tmp_path):
"""dot and slim ship different package sets; a cross-flavor diff is noise."""
searchdir = changelogs(
tmp_path,
"WinPythondot-64bit-3.14.7.0.md",
"WinPythonslim-64bit-3.14.7.1.md",
)
assert find_previous_version("3.14.7.2", searchdir, "dot") == "3.14.7.0"
assert find_previous_version("3.14.7.2", searchdir, "slim") == "3.14.7.1"

def test_a_flavor_is_not_a_prefix_of_another(self, tmp_path):
"""dot must not match dotf: the free-threaded build is a separate line."""
searchdir = changelogs(
tmp_path,
"WinPythondot-64bit-3.14.7.0.md",
"WinPythondotf-64bit-3.14.7.1.md",
)
assert find_previous_version("3.14.7.2", searchdir, "dot") == "3.14.7.0"
assert find_previous_version("3.14.7.2", searchdir, "dotf") == "3.14.7.1"

def test_the_unflavored_name_does_not_match_flavored_files(self, tmp_path):
searchdir = changelogs(
tmp_path,
"WinPython-64bit-3.9.8.0.md",
"WinPythondot-64bit-3.14.7.0.md",
)
assert find_previous_version("3.15.0.0", searchdir, "") == "3.9.8.0"

def test_architecture_separates_the_lines(self, tmp_path):
searchdir = changelogs(
tmp_path,
"WinPythondot-32bit-3.9.0.0.md",
"WinPythondot-64bit-3.8.0.0.md",
)
assert find_previous_version("3.14.0.0", searchdir, "dot", 64) == "3.8.0.0"
assert find_previous_version("3.14.0.0", searchdir, "dot", 32) == "3.9.0.0"


class TestChangelogVersions:
def test_reports_the_file_each_version_came_from(self, tmp_path):
searchdir = changelogs(
tmp_path,
"WinPythondot-64bit-3.14.7.1b1.md",
"WinPythondot-64bit-3.14.7.0_History.md",
)
found = changelog_versions(searchdir, "dot")
assert [(raw, name) for _, raw, name in found] == [
("3.14.7.1b1", "WinPythondot-64bit-3.14.7.1b1.md")
]


class TestCopyChangelogs:
@pytest.fixture
def dest(self, tmp_path):
target = tmp_path / "dest"
target.mkdir()
return target

def test_takes_the_whole_major_minor_including_levels(self, tmp_path, dest):
src = changelogs(
tmp_path / "src",
"WinPythondot-64bit-3.14.7.0.md",
"WinPythondot-64bit-3.14.7.1b1.md",
"WinPythondot-64bit-3.13.15.0.md",
)
copy_changelogs("3.14.7.1", src, "dot", 64, dest)
assert sorted(p.name for p in dest.iterdir()) == [
"WinPythondot-64bit-3.14.7.0.md",
"WinPythondot-64bit-3.14.7.1b1.md",
]

def test_a_minor_is_not_a_string_prefix(self, tmp_path, dest):
"""Selecting on the parsed version stops 3.1 from dragging in 3.15."""
src = changelogs(
tmp_path / "src",
"WinPythondot-64bit-3.1.0.0.md",
"WinPythondot-64bit-3.15.0.5.md",
)
copy_changelogs("3.1.0.0", src, "dot", 64, dest)
assert [p.name for p in dest.iterdir()] == ["WinPythondot-64bit-3.1.0.0.md"]

def test_history_companions_are_not_copied(self, tmp_path, dest):
src = changelogs(
tmp_path / "src",
"WinPythondot-64bit-3.14.7.0.md",
"WinPythondot-64bit-3.14.7.0_History.md",
)
copy_changelogs("3.14.7.0", src, "dot", 64, dest)
assert [p.name for p in dest.iterdir()] == ["WinPythondot-64bit-3.14.7.0.md"]
49 changes: 39 additions & 10 deletions wppm/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,38 @@ def compare_files(file1, file2, mode="full", header1=None, header2=None, header_

# --- ORIGINAL/HISTORICAL VERSION-TO-VERSION COMPARISON ---

def changelog_versions(searchdir, flavor="", architecture=64):
"""The changelogs on disk, as (parsed version, version string, file name).

The file name is the only source of the version, so the pattern has to
accept everything a WinPython version can be. It used to be digits and
dots, which silently dropped any release level: 3.14.7.1b1 is a perfectly
ordinary PEP 440 version -- it sorts after 3.14.7.0 and before 3.14.7.1 --
but no b1 changelog was ever visible to the comparison. Now the name is
handed to the version parser, which also drops the _History companions and
anything else in the directory that is not a changelog, since they do not
parse as versions.
"""
pattern = re.compile(rf"WinPython{re.escape(flavor)}-{architecture}bit-(.+)\.(?:txt|md)$")
found = []
for name in os.listdir(searchdir):
match = pattern.match(name)
if not match:
continue
try:
found.append((version.parse(match.group(1)), match.group(1), name))
except version.InvalidVersion:
continue
return found

def find_previous_version(target_version, searchdir=None, flavor="", architecture=64):
search_dir = Path(searchdir) if searchdir else CHANGELOGS_DIR
pattern = re.compile(rf"WinPython{flavor}-{architecture}bit-([0-9\.]+)\.(txt|md)")
versions = [pattern.match(f).group(1) for f in os.listdir(search_dir) if pattern.match(f)]
versions = [v for v in versions if version.parse(v) < version.parse(target_version)]
return max(versions, key=version.parse, default=target_version)
target = version.parse(target_version)
earlier = [
(parsed, raw) for parsed, raw, _ in changelog_versions(search_dir, flavor, architecture)
if parsed < target
]
return max(earlier, default=(None, target_version))[1]

def load_version_markdown(version, searchdir, flavor="", architecture=64):
filename = Path(searchdir) / f"WinPython{flavor}-{architecture}bit-{version}.md"
Expand All @@ -142,13 +168,16 @@ def compare_package_indexes(version2, version1=None, searchdir=None, flavor="",
result += compare_markdown_sections(md1, md2, k, k, version1, version2) + "\n"
return result+ "\n\n* * *\n"

def copy_changelogs(version, searchdir, flavor="", architecture=64, basedir=None):
"""Copy all changelogs for a major.minor version into basedir."""
basever = ".".join(str(version).split(".")[:2])
pattern = re.compile(rf"WinPython{flavor}-{architecture}bit-{basever}[0-9\.]*\.(txt|md)")
def copy_changelogs(target_version, searchdir, flavor="", architecture=64, basedir=None):
"""Copy all changelogs for a major.minor version into basedir.

Selected on the parsed version rather than a string prefix, so a release
level comes along and 3.1 cannot pick up 3.15.
"""
basever = version.parse(str(target_version)).release[:2]
dest = Path(basedir)
for fname in os.listdir(searchdir):
if pattern.match(fname):
for parsed, _, fname in changelog_versions(searchdir, flavor, architecture):
if parsed.release[:2] == basever:
shutil.copyfile(Path(searchdir) / fname, dest / fname)

def write_changelog(version2, version1=None, searchdir=None, flavor="", architecture=64, basedir=None):
Expand Down
Loading