Files
magnus919_agent-skills/release-engineering/tests/test_semver_check.py
T
usernameandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> 6730015312 feat(release-engineering): add release engineering skill bundle
Dense release engineering methodology bundle: 15 references (role/career, process models,
CD pipelines, progressive delivery, change governance/compliance, readiness gates, rollback
by system type, versioning/artifacts, feature flags, monorepo/polyrepo, toolchain,
supply-chain security, DORA metrics, release operations), 6 templates, 4 assets, 5 stdlib-only
Python CLIs (version_bump, semver_check, changelog_check, dora_metrics, release_plan_scaffold)
with colocated tests, schema-v1 evals (8 cases), README, and catalog regeneration.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-08-01 22:54:13 -04:00

234 lines
8.3 KiB
Python

"""Tests for semver_check.py.
Covers: strict SemVer validation (--check), precedence comparison
(--compare), precedence-aware sorting (--sort), --json output, and exit
codes.
Discoverable by both pytest and unittest (unittest.TestCase classes).
"""
import json
import os
import subprocess
import sys
import unittest
SCRIPTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "scripts")
SEMVER_CHECK = os.path.join(SCRIPTS_DIR, "semver_check.py")
def run_semver(args):
"""Run semver_check.py with given args, return (returncode, stdout, stderr)."""
cmd = [sys.executable, SEMVER_CHECK] + args
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
return proc.returncode, proc.stdout, proc.stderr
class TestSemverCheckValidation(unittest.TestCase):
"""--check validates strict SemVer 2.0.0."""
def test_valid_version_exit_0(self):
"""A conforming version exits 0."""
rc, out, _ = run_semver(["--check", "1.2.3"])
self.assertEqual(rc, 0)
self.assertIn("valid", out)
def test_valid_with_prerelease(self):
"""Pre-release versions are valid."""
rc, out, _ = run_semver(["--check", "1.2.3-alpha.1"])
self.assertEqual(rc, 0)
self.assertIn("valid", out)
def test_valid_with_build_metadata(self):
"""Build metadata is valid."""
rc, _, _ = run_semver(["--check", "1.2.3+build.5"])
self.assertEqual(rc, 0)
def test_zero_version_valid(self):
"""0.x.y versions are valid."""
rc, _, _ = run_semver(["--check", "0.0.0"])
self.assertEqual(rc, 0)
def test_leading_zero_invalid_exit_1(self):
"""Leading zeros are rejected."""
rc, out, _ = run_semver(["--check", "01.2.3"])
self.assertEqual(rc, 1)
self.assertIn("invalid", out)
self.assertIn("leading zeros", out)
def test_missing_patch_invalid_exit_1(self):
"""A two-part version is invalid."""
rc, out, _ = run_semver(["--check", "1.2"])
self.assertEqual(rc, 1)
self.assertIn("invalid", out)
def test_non_numeric_component_invalid(self):
"""Non-numeric components are invalid."""
rc, out, _ = run_semver(["--check", "1.x.3"])
self.assertEqual(rc, 1)
self.assertIn("invalid", out)
def test_empty_version_invalid(self):
"""An empty version is invalid."""
rc, out, _ = run_semver(["--check", ""])
self.assertEqual(rc, 1)
self.assertIn("invalid", out)
def test_v_prefix_invalid(self):
"""A 'v' prefix is not part of SemVer."""
rc, out, _ = run_semver(["--check", "v1.2.3"])
self.assertEqual(rc, 1)
self.assertIn("invalid", out)
def test_leading_zero_prerelease_invalid(self):
"""Numeric pre-release identifiers must not have leading zeros."""
rc, _, _ = run_semver(["--check", "1.2.3-alpha.01"])
self.assertEqual(rc, 1)
class TestSemverCheckCompare(unittest.TestCase):
"""--compare applies SemVer precedence."""
def test_compare_lt(self):
"""Lower precedence prints lt."""
rc, out, _ = run_semver(["--compare", "1.2.3", "1.2.4"])
self.assertEqual(rc, 0)
self.assertEqual(out.strip(), "lt")
def test_compare_gt(self):
"""Higher precedence prints gt."""
rc, out, _ = run_semver(["--compare", "2.0.0", "1.9.9"])
self.assertEqual(rc, 0)
self.assertEqual(out.strip(), "gt")
def test_compare_eq(self):
"""Equal precedence prints eq."""
rc, out, _ = run_semver(["--compare", "1.2.3", "1.2.3"])
self.assertEqual(rc, 0)
self.assertEqual(out.strip(), "eq")
def test_prerelease_sorts_below_release(self):
"""1.0.0-alpha < 1.0.0."""
rc, out, _ = run_semver(["--compare", "1.0.0-alpha", "1.0.0"])
self.assertEqual(rc, 0)
self.assertEqual(out.strip(), "lt")
def test_prerelease_numeric_below_alphanumeric(self):
"""Numeric pre-release identifiers sort below alphanumeric ones."""
rc, out, _ = run_semver(["--compare", "1.0.0-1", "1.0.0-alpha"])
self.assertEqual(rc, 0)
self.assertEqual(out.strip(), "lt")
def test_build_metadata_ignored(self):
"""Build metadata does not affect precedence."""
rc, out, _ = run_semver(["--compare", "1.0.0+a", "1.0.0+b"])
self.assertEqual(rc, 0)
self.assertEqual(out.strip(), "eq")
def test_invalid_compare_exit_1(self):
"""An invalid version in --compare is an input error."""
rc, _, err = run_semver(["--compare", "1.2.3", "nope"])
self.assertEqual(rc, 1)
self.assertIn("invalid", err.lower())
class TestSemverCheckSort(unittest.TestCase):
"""--sort orders versions ascending by precedence."""
def test_sort_basic(self):
"""Simple numeric ordering."""
rc, out, _ = run_semver(["--sort", "1.2.3", "1.0.0", "1.2.0"])
self.assertEqual(rc, 0)
lines = out.splitlines()
self.assertEqual(lines, ["1.0.0", "1.2.0", "1.2.3"])
def test_sort_prerelease_before_release(self):
"""Pre-releases sort before the same core without one."""
rc, out, _ = run_semver(
["--sort", "1.0.0", "1.0.0-beta.2", "1.0.0-alpha.1"]
)
self.assertEqual(rc, 0)
lines = out.splitlines()
self.assertEqual(lines, ["1.0.0-alpha.1", "1.0.0-beta.2", "1.0.0"])
def test_sort_build_metadata_keeps_input_order(self):
"""Equal-precedence versions (build only differs) keep input order."""
rc, out, _ = run_semver(["--sort", "1.0.0+b", "1.0.0+a", "1.0.0+c"])
self.assertEqual(rc, 0)
lines = out.splitlines()
self.assertEqual(lines, ["1.0.0+b", "1.0.0+a", "1.0.0+c"])
def test_sort_invalid_exit_1(self):
"""An invalid version in --sort is an input error."""
rc, _, err = run_semver(["--sort", "1.0.0", "2.x.0"])
self.assertEqual(rc, 1)
self.assertIn("invalid", err.lower())
class TestSemverCheckJsonOutput(unittest.TestCase):
"""--json output is machine-parseable."""
def test_check_json_valid(self):
"""Valid --check --json parses with fields."""
rc, out, _ = run_semver(["--check", "1.2.3", "--json"])
self.assertEqual(rc, 0)
data = json.loads(out)
self.assertTrue(data["valid"])
self.assertEqual(data["version"], "1.2.3")
self.assertEqual(data["parsed"]["major"], 1)
def test_check_json_invalid(self):
"""Invalid --check --json carries a reason."""
rc, out, _ = run_semver(["--check", "01.2.3", "--json"])
self.assertEqual(rc, 1)
data = json.loads(out)
self.assertFalse(data["valid"])
self.assertIsNotNone(data["reason"])
def test_compare_json(self):
"""--compare --json parses with a relation."""
rc, out, _ = run_semver(["--compare", "1.0.0", "1.0.1", "--json"])
self.assertEqual(rc, 0)
data = json.loads(out)
self.assertEqual(data["relation"], "lt")
def test_sort_json(self):
"""--sort --json parses with a sorted list."""
rc, out, _ = run_semver(["--sort", "2.0.0", "1.0.0", "--json"])
self.assertEqual(rc, 0)
data = json.loads(out)
self.assertEqual(data["sorted"], ["1.0.0", "2.0.0"])
class TestSemverCheckExitCodes(unittest.TestCase):
"""Exit codes and usage errors."""
def test_no_mode_exit_2(self):
"""No mode given is a usage error (exit 2)."""
rc, _, err = run_semver([])
self.assertEqual(rc, 2)
self.assertIn("usage", err.lower())
def test_conflicting_modes_exit_2(self):
"""--check and --compare together is a usage error."""
rc, _, _ = run_semver(["--check", "1.0.0", "--compare", "1.0.0", "2.0.0"])
self.assertEqual(rc, 2)
def test_no_traceback_on_invalid(self):
"""Invalid input never produces a traceback."""
rc, out, err = run_semver(["--check", "1.2"])
self.assertEqual(rc, 1)
self.assertNotIn("Traceback", err)
self.assertNotIn("Traceback", out)
def test_help_exits_0(self):
"""--help exits 0 and describes usage."""
rc, out, _ = run_semver(["--help"])
self.assertEqual(rc, 0)
self.assertIn("usage", out.lower())
self.assertIn("semver_check", out.lower())
if __name__ == "__main__":
unittest.main()