mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-13 04:26:28 +03:00
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>
234 lines
8.3 KiB
Python
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()
|