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)
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)
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):
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):
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"))
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()