pith-digest 0.1.1

pith foundation: digest (zero-dep Rust)
Documentation
"""Offline tests for stamp-release.py: every manifest a release publishes from must
carry the version better-semantic-release computed, or the cd.yml version guard
refuses the tag (a registry version cannot be replaced, so the guard is right).

Run: python -m unittest scripts/test_stamp_release.py
"""
from __future__ import annotations

import importlib.util
import json
import pathlib
import tempfile
import unittest

_spec = importlib.util.spec_from_file_location(
    "stamp_release", pathlib.Path(__file__).with_name("stamp-release.py")
)
sr = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(sr)

CARGO = """\
[workspace]
members = ["cli"]

[package]
name = "pith-x"
version = "0.1.0"
edition = "2024"

[dependencies]
pith-y = { version = "0.1", git = "https://example.invalid/pith-y.git" }
"""

CLI = """\
[package]
name = "pith-x-cli"
version = "0.1.0"

[dependencies]
pith-x = { path = "..", version = "0.1" }
"""

LOCK = """\
# This file is automatically @generated by Cargo.
version = 4

[[package]]
name = "pith-x"
version = "0.1.0"
dependencies = [
 "pith-y",
]

[[package]]
name = "pith-x-cli"
version = "0.1.0"
dependencies = [
 "pith-x",
]

[[package]]
name = "pith-y"
version = "0.1.0"
source = "git+https://example.invalid/pith-y.git?branch=main#abc"
"""

PYPROJECT = """\
[build-system]
requires = ["setuptools>=70.1"]

[project]
name = "pith-x"
version = "0.1.0"
requires-python = ">=3.9"
"""

PKG = {"name": "pith-x", "version": "0.1.0", "dependencies": {"koffi": "^2.9.0"}}
PKG_LOCK = {
    "name": "pith-x",
    "version": "0.1.0",
    "lockfileVersion": 3,
    "packages": {
        "": {"name": "pith-x", "version": "0.1.0", "dependencies": {"koffi": "^2.9.0"}},
        "node_modules/koffi": {"version": "2.16.3"},
    },
}

POM = """\
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.example</groupId>
    <artifactId>parent</artifactId>
    <version>9.9.9</version>
  </parent>
  <groupId>io.github.pith-hash</groupId>
  <artifactId>pith-x</artifactId>
  <version>0.1.0</version>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>5.10.2</version>
    </dependency>
  </dependencies>
</project>
"""


def dump_json(obj) -> str:
    return json.dumps(obj, indent=2, ensure_ascii=False) + "\n"


def make_repo(root: pathlib.Path) -> None:
    def w(rel: str, text: str) -> None:
        p = root / rel
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_text(text, encoding="utf-8", newline="")

    w("Cargo.toml", CARGO)
    w("cli/Cargo.toml", CLI)
    w("Cargo.lock", LOCK)
    w("sdk/python/pyproject.toml", PYPROJECT)
    w("sdk/node/package.json", dump_json(PKG))
    w("sdk/node/package-lock.json", dump_json(PKG_LOCK))
    w("sdk/java/pom.xml", POM)
    # Build output must never be stamped.
    w("target/package/Cargo.toml", CARGO)
    w("sdk/node/node_modules/koffi/package.json", dump_json({"name": "koffi", "version": "2.16.3"}))


class TomlTests(unittest.TestCase):
    def test_stamps_only_the_requested_table(self):
        new, old = sr.stamp_toml(CARGO, "package", "0.1.1")
        self.assertEqual(old, "0.1.0")
        self.assertIn('[package]\nname = "pith-x"\nversion = "0.1.1"', new)
        # the dependency pin `version = "0.1"` is a requirement, not our version
        self.assertIn('pith-y = { version = "0.1"', new)

    def test_missing_table_returns_none(self):
        self.assertIsNone(sr.stamp_toml(PYPROJECT, "package", "0.1.1"))

    def test_preserves_crlf(self):
        text = CARGO.replace("\n", "\r\n")
        new, _ = sr.stamp_toml(text, "package", "0.1.1")
        self.assertNotIn("\n\n", new.replace("\r\n", ""))
        self.assertEqual(new.count("\r\n"), text.count("\r\n"))

    def test_workspace_inherited_version_is_not_stamped(self):
        text = '[package]\nname = "a"\nversion.workspace = true\n'
        self.assertIsNone(sr.stamp_toml(text, "package", "0.1.1"))


class PathDependencyTests(unittest.TestCase):
    """A prerelease never satisfies a caret requirement ("0.1"), so a workspace
    member's `path + version` dependency has to move with the version stamped."""

    def test_member_path_dependency_requirement_is_stamped(self):
        out = sr.stamp_member_dependencies(CLI, {"pith-x", "pith-x-cli"}, "0.1.1-beta.1")
        self.assertIn('pith-x = { path = "..", version = "0.1.1-beta.1" }', out)

    def test_non_member_and_pathless_dependencies_are_left_alone(self):
        out = sr.stamp_member_dependencies(CARGO, {"pith-x", "pith-x-cli"}, "0.1.1-beta.1")
        self.assertEqual(out, CARGO)

    def test_dev_and_build_and_target_tables_are_covered(self):
        text = (
            '[dev-dependencies]\npith-x = { path = "..", version = "0.1" }\n'
            '[build-dependencies]\npith-x = { version = "0.1", path = ".." }\n'
            "[target.'cfg(unix)'.dependencies]\npith-x = { path = \"..\", version = \"0.1\" }\n"
        )
        out = sr.stamp_member_dependencies(text, {"pith-x"}, "0.1.2")
        self.assertEqual(out.count('version = "0.1.2"'), 3)
        self.assertNotIn('version = "0.1"', out)

    def test_unstampable_member_dependency_is_an_error(self):
        text = '[dependencies.pith-x]\npath = ".."\nversion = "0.1"\n'
        with self.assertRaises(sr.StampError):
            sr.stamp_member_dependencies(text, {"pith-x"}, "0.1.1")

    def test_crlf_is_preserved(self):
        out = sr.stamp_member_dependencies(CLI.replace("\n", "\r\n"), {"pith-x"}, "0.1.1")
        self.assertIn('version = "0.1.1" }\r\n', out)
        self.assertNotIn("\n\n", out.replace("\r\n", ""))


class LockTests(unittest.TestCase):
    def test_stamps_workspace_members_not_registry_or_git_packages(self):
        new = sr.stamp_cargo_lock(LOCK, {"pith-x", "pith-x-cli"}, "0.1.1")
        self.assertIn('name = "pith-x"\nversion = "0.1.1"', new)
        self.assertIn('name = "pith-x-cli"\nversion = "0.1.1"', new)
        self.assertIn('name = "pith-y"\nversion = "0.1.0"', new)

    def test_prefix_name_is_not_confused(self):
        # `pith-x` must not match inside `pith-x-cli`
        new = sr.stamp_cargo_lock(LOCK, {"pith-x"}, "0.1.1")
        self.assertIn('name = "pith-x-cli"\nversion = "0.1.0"', new)

    def test_member_missing_from_lock_is_an_error(self):
        with self.assertRaises(sr.StampError):
            sr.stamp_cargo_lock(LOCK, {"pith-absent"}, "0.1.1")


class JsonTests(unittest.TestCase):
    def test_package_json_only_version_changes(self):
        new = sr.stamp_package_json(dump_json(PKG), "0.1.1")
        self.assertEqual(json.loads(new), {**PKG, "version": "0.1.1"})

    def test_package_lock_root_and_workspace_entry_but_not_dependencies(self):
        new = json.loads(sr.stamp_package_lock(dump_json(PKG_LOCK), "0.1.1"))
        self.assertEqual(new["version"], "0.1.1")
        self.assertEqual(new["packages"][""]["version"], "0.1.1")
        self.assertEqual(new["packages"]["node_modules/koffi"]["version"], "2.16.3")

    def test_non_canonical_json_is_refused_rather_than_reformatted(self):
        with self.assertRaises(sr.StampError):
            sr.stamp_package_json('{"name":"x","version":"0.1.0"}', "0.1.1")


class PomTests(unittest.TestCase):
    def test_stamps_project_version_not_parent_or_dependencies(self):
        new = sr.stamp_pom(POM, "0.1.1")
        self.assertIn("<artifactId>pith-x</artifactId>\n  <version>0.1.1</version>", new)
        self.assertIn("<version>9.9.9</version>", new)
        self.assertIn("<version>5.10.2</version>", new)

    def test_pom_without_parent(self):
        text = POM.replace(POM[POM.index("  <parent>") : POM.index("</parent>\n") + len("</parent>\n")], "")
        self.assertIn("<version>0.1.1</version>", sr.stamp_pom(text, "0.1.1"))


class MainTests(unittest.TestCase):
    def run_main(self, root, version="0.1.1", extra=()):
        return sr.main([version, "--root", str(root), "--no-git-add", *extra])

    def test_end_to_end_stamps_every_manifest_and_skips_build_output(self):
        root = pathlib.Path(tempfile.mkdtemp())
        make_repo(root)
        self.assertEqual(self.run_main(root, "0.1.1-beta.1"), 0)
        read = lambda rel: (root / rel).read_text(encoding="utf-8")
        self.assertIn('version = "0.1.1-beta.1"', read("Cargo.toml"))
        self.assertIn('version = "0.1.1-beta.1"', read("cli/Cargo.toml"))
        self.assertIn('pith-x = { path = "..", version = "0.1.1-beta.1" }', read("cli/Cargo.toml"))
        self.assertIn('pith-y = { version = "0.1", git =', read("Cargo.toml"))
        self.assertIn('name = "pith-x"\nversion = "0.1.1-beta.1"', read("Cargo.lock"))
        self.assertIn('version = "0.1.1-beta.1"', read("sdk/python/pyproject.toml"))
        self.assertEqual(json.loads(read("sdk/node/package.json"))["version"], "0.1.1-beta.1")
        self.assertEqual(json.loads(read("sdk/node/package-lock.json"))["version"], "0.1.1-beta.1")
        self.assertIn("<version>0.1.1-beta.1</version>", read("sdk/java/pom.xml"))
        # untouched: build output and vendored dependency manifests
        self.assertEqual(read("target/package/Cargo.toml"), CARGO)
        self.assertEqual(json.loads(read("sdk/node/node_modules/koffi/package.json"))["version"], "2.16.3")

    def test_idempotent(self):
        root = pathlib.Path(tempfile.mkdtemp())
        make_repo(root)
        self.run_main(root)
        before = {p: p.read_bytes() for p in root.rglob("*") if p.is_file()}
        self.run_main(root)
        self.assertEqual(before, {p: p.read_bytes() for p in root.rglob("*") if p.is_file()})

    def test_repo_without_sdks_still_stamps_the_crate(self):
        root = pathlib.Path(tempfile.mkdtemp())
        (root / "Cargo.toml").write_text(CARGO.replace('members = ["cli"]', "members = []"), encoding="utf-8")
        (root / "Cargo.lock").write_text(LOCK, encoding="utf-8")
        self.assertEqual(self.run_main(root), 0)
        self.assertIn('version = "0.1.1"', (root / "Cargo.toml").read_text(encoding="utf-8"))

    def test_bad_version_is_rejected(self):
        root = pathlib.Path(tempfile.mkdtemp())
        make_repo(root)
        with self.assertRaises(SystemExit) as cm:
            self.run_main(root, "v0.1.1")
        self.assertEqual(cm.exception.code, 2)

    def test_nothing_to_stamp_is_a_failure_not_a_pass(self):
        root = pathlib.Path(tempfile.mkdtemp())
        self.assertEqual(self.run_main(root), 1)


if __name__ == "__main__":
    unittest.main()