Conventional-Commit-driven version bumps (scripts/bump.py), a commit-msg validation hook, and a /release-notes workflow that produces a human-reviewed CHANGELOG.md. Adds an in-app version badge + 'What's new' dialog on the launcher. The version is single-sourced from pyproject.toml (cim_suite.__version__), and the deterministic bump backbone lives in scripts/release/ with tests in tests/release/. Marks the 1.0.0 baseline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
174 lines
5.5 KiB
Python
174 lines
5.5 KiB
Python
"""Tests for commit parsing, bump detection, range resolution, and classification."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from release.commits import (
|
|
ParsedCommit,
|
|
build_digest,
|
|
commit_range,
|
|
determine_bump,
|
|
last_version_tag,
|
|
parse_commit,
|
|
validate_commit_message,
|
|
)
|
|
|
|
|
|
# --- parse_commit ----------------------------------------------------------
|
|
def test_parse_plain_type():
|
|
c = parse_commit("abc", "feat: add launcher")
|
|
assert c.type == "feat"
|
|
assert c.scope is None
|
|
assert c.breaking is False
|
|
assert c.description == "add launcher"
|
|
|
|
|
|
def test_parse_with_scope():
|
|
c = parse_commit("abc", "fix(repository): stretch Value column")
|
|
assert c.type == "fix"
|
|
assert c.scope == "repository"
|
|
assert c.description == "stretch Value column"
|
|
|
|
|
|
def test_parse_bang_marks_breaking():
|
|
c = parse_commit("abc", "feat(api)!: drop wireless sensors")
|
|
assert c.type == "feat"
|
|
assert c.scope == "api"
|
|
assert c.breaking is True
|
|
|
|
|
|
def test_parse_breaking_change_in_body():
|
|
c = parse_commit("abc", "refactor: rework transport", body="BREAKING CHANGE: ports moved")
|
|
assert c.breaking is True
|
|
|
|
|
|
def test_parse_breaking_change_hyphen_variant():
|
|
c = parse_commit("abc", "feat: x", body="BREAKING-CHANGE: y")
|
|
assert c.breaking is True
|
|
|
|
|
|
def test_parse_non_conventional_subject():
|
|
c = parse_commit("abc", "Merge branch 'feat/foo' into main")
|
|
assert c.type is None
|
|
assert c.breaking is False
|
|
assert c.description == "Merge branch 'feat/foo' into main"
|
|
|
|
|
|
# --- determine_bump --------------------------------------------------------
|
|
def test_bump_breaking_wins():
|
|
commits = [
|
|
parse_commit("1", "feat: a"),
|
|
parse_commit("2", "fix: b"),
|
|
parse_commit("3", "feat!: c"),
|
|
]
|
|
assert determine_bump(commits) == "major"
|
|
|
|
|
|
def test_bump_feat_over_fix():
|
|
commits = [parse_commit("1", "fix: a"), parse_commit("2", "feat: b")]
|
|
assert determine_bump(commits) == "minor"
|
|
|
|
|
|
def test_bump_fix_only():
|
|
commits = [parse_commit("1", "fix: a"), parse_commit("2", "docs: b")]
|
|
assert determine_bump(commits) == "patch"
|
|
|
|
|
|
def test_bump_none_when_no_releasable_commits():
|
|
commits = [parse_commit("1", "docs: a"), parse_commit("2", "chore: b")]
|
|
assert determine_bump(commits) is None
|
|
|
|
|
|
def test_bump_none_for_empty():
|
|
assert determine_bump([]) is None
|
|
|
|
|
|
# --- last_version_tag ------------------------------------------------------
|
|
def test_last_version_tag_picks_highest_semver():
|
|
tags = ["v1.0.0", "v1.2.0", "v1.10.0", "v1.9.0"]
|
|
assert last_version_tag(tags) == "v1.10.0"
|
|
|
|
|
|
def test_last_version_tag_ignores_non_version_tags():
|
|
tags = ["baseline", "v1.0.0", "release-candidate", "not-a-version"]
|
|
assert last_version_tag(tags) == "v1.0.0"
|
|
|
|
|
|
def test_last_version_tag_none_when_empty():
|
|
assert last_version_tag([]) is None
|
|
assert last_version_tag(["nightly", "stable"]) is None
|
|
|
|
|
|
# --- commit_range ----------------------------------------------------------
|
|
def test_range_prefers_last_tag():
|
|
assert commit_range(last_tag="v1.2.0", baseline="abc123") == "v1.2.0..HEAD"
|
|
|
|
|
|
def test_range_falls_back_to_baseline():
|
|
assert commit_range(last_tag=None, baseline="abc123") == "abc123..HEAD"
|
|
|
|
|
|
def test_range_all_commits_when_neither():
|
|
assert commit_range(last_tag=None, baseline=None) is None
|
|
|
|
|
|
# --- build_digest ----------------------------------------------------------
|
|
def _commits() -> list[ParsedCommit]:
|
|
return [
|
|
parse_commit("1", "feat: new screen"),
|
|
parse_commit("2", "fix: crash on open"),
|
|
parse_commit("3", "feat!: remove old mode"),
|
|
parse_commit("4", "refactor: tidy"),
|
|
parse_commit("5", "test: add coverage"),
|
|
parse_commit("6", "Merge branch 'x'"),
|
|
]
|
|
|
|
|
|
def test_digest_buckets():
|
|
d = build_digest(_commits())
|
|
assert [c.sha for c in d.features] == ["1", "3"]
|
|
assert [c.sha for c in d.fixes] == ["2"]
|
|
assert [c.sha for c in d.breaking] == ["3"]
|
|
# refactor, test, and the merge all collapse into internal.
|
|
assert [c.sha for c in d.internal] == ["4", "5", "6"]
|
|
|
|
|
|
# --- validate_commit_message ----------------------------------------------
|
|
def test_validate_accepts_conventional():
|
|
assert validate_commit_message("feat: add thing") is None
|
|
assert validate_commit_message("fix(scope): bug") is None
|
|
assert validate_commit_message("feat(api)!: breaking") is None
|
|
|
|
|
|
def test_validate_accepts_merge_and_revert():
|
|
assert validate_commit_message("Merge branch 'main' into feat/x") is None
|
|
assert validate_commit_message('Revert "feat: add thing"') is None
|
|
|
|
|
|
def test_validate_accepts_fixup_and_squash():
|
|
# git commit --fixup/--squash generate these; rejecting them breaks autosquash.
|
|
assert validate_commit_message("fixup! feat: add thing") is None
|
|
assert validate_commit_message("squash! fix: the bug") is None
|
|
assert validate_commit_message("amend! feat: add thing") is None
|
|
|
|
|
|
def test_validate_rejects_missing_type():
|
|
err = validate_commit_message("add a thing")
|
|
assert err is not None
|
|
assert "type" in err.lower()
|
|
|
|
|
|
def test_validate_rejects_unknown_type():
|
|
err = validate_commit_message("feet: typo in type")
|
|
assert err is not None
|
|
|
|
|
|
def test_validate_rejects_empty_description():
|
|
err = validate_commit_message("feat: ")
|
|
assert err is not None
|
|
|
|
|
|
def test_validate_ignores_comment_lines():
|
|
# git passes the whole COMMIT_EDITMSG; comment lines must not be validated.
|
|
msg = "feat: real subject\n\n# Please enter the commit message\n# comment"
|
|
assert validate_commit_message(msg) is None
|