f07220b0c6
- Add internal/node package: reads/writes package.json version (single or multi-path via node.package_json / node.package_jsons) - Add maven.pom_paths support: update multiple pom.xml files in one release commit; pom_paths overrides pom_path; --pom flag clears pom_paths - Add git.bump_rules config: per-type control of which version component bumps (breaking/feat/fix accept "patch" or "minor"); wired through version.Next() as a new sixth parameter - Extract injectable function vars (absPath, gitAllCommits, gitCommitsSince, gitCommitFiles) to enable error-path testing without interfaces - Achieve 100% per-package statement coverage across all 12 packages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
161 lines
4.1 KiB
Go
161 lines
4.1 KiB
Go
package commits
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// FuzzParse verifies that the parser never panics and always returns a valid Type.
|
|
func FuzzParse(f *testing.F) {
|
|
// Seed corpus: representative conventional commit messages
|
|
seeds := []string{
|
|
"feat: add OAuth2 login",
|
|
"fix: correct null pointer",
|
|
"feat!: remove legacy API",
|
|
"feat(auth): add login\n\nBREAKING CHANGE: old endpoint removed",
|
|
"chore: update deps",
|
|
"Feat:missing space",
|
|
"FIX(scope)!: mixed case breaking",
|
|
"",
|
|
"\x00",
|
|
"BREAKING CHANGE: bare footer",
|
|
"not a conventional commit at all",
|
|
"feat: \n\n body only",
|
|
strings.Repeat("a", 4096),
|
|
}
|
|
for _, s := range seeds {
|
|
f.Add(s)
|
|
}
|
|
|
|
f.Fuzz(func(t *testing.T, msg string) {
|
|
result := Parse(msg)
|
|
if result < TypeNone || result > TypeBreaking {
|
|
t.Errorf("Parse(%q) returned out-of-range type %d", msg, result)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestExtractSubject(t *testing.T) {
|
|
cases := []struct {
|
|
header string
|
|
want string
|
|
}{
|
|
{"feat: add login", "add login"},
|
|
{"feat(auth): add OAuth2", "add OAuth2"},
|
|
{"feat!: remove API", "remove API"},
|
|
{"FIX:typo", "typo"},
|
|
{"plain message", "plain message"},
|
|
}
|
|
for _, c := range cases {
|
|
got := ExtractSubject(c.header)
|
|
if got != c.want {
|
|
t.Errorf("ExtractSubject(%q) = %q, want %q", c.header, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGroup(t *testing.T) {
|
|
messages := []string{
|
|
"feat: add login",
|
|
"fix: patch null pointer",
|
|
"feat!: remove legacy API",
|
|
"chore: update deps",
|
|
"fix: handle empty response",
|
|
}
|
|
breaking, feats, fixes := Group(messages)
|
|
if len(breaking) != 1 || breaking[0] != "remove legacy API" {
|
|
t.Errorf("breaking = %v, want [remove legacy API]", breaking)
|
|
}
|
|
if len(feats) != 1 || feats[0] != "add login" {
|
|
t.Errorf("feats = %v, want [add login]", feats)
|
|
}
|
|
if len(fixes) != 2 {
|
|
t.Errorf("fixes = %v, want 2 items", fixes)
|
|
}
|
|
}
|
|
|
|
func TestReleasableSet(t *testing.T) {
|
|
all := ReleasableSet(nil)
|
|
if !all[TypeFix] || !all[TypeFeat] || !all[TypeBreaking] {
|
|
t.Error("nil input should return all three types")
|
|
}
|
|
only := ReleasableSet([]string{"fix"})
|
|
if !only[TypeFix] || only[TypeFeat] || only[TypeBreaking] {
|
|
t.Errorf("fix-only set: %v", only)
|
|
}
|
|
onlyFeat := ReleasableSet([]string{"feat"})
|
|
if onlyFeat[TypeFix] || !onlyFeat[TypeFeat] || onlyFeat[TypeBreaking] {
|
|
t.Errorf("feat-only set: %v", onlyFeat)
|
|
}
|
|
onlyBreaking := ReleasableSet([]string{"breaking"})
|
|
if onlyBreaking[TypeFix] || onlyBreaking[TypeFeat] || !onlyBreaking[TypeBreaking] {
|
|
t.Errorf("breaking-only set: %v", onlyBreaking)
|
|
}
|
|
}
|
|
|
|
func TestTypeString(t *testing.T) {
|
|
cases := []struct {
|
|
t Type
|
|
want string
|
|
}{
|
|
{TypeNone, "none"},
|
|
{TypeFix, "fix"},
|
|
{TypeFeat, "feat"},
|
|
{TypeBreaking, "breaking"},
|
|
{Type(99), "none"}, // unknown → default
|
|
}
|
|
for _, c := range cases {
|
|
if got := c.t.String(); got != c.want {
|
|
t.Errorf("Type(%d).String() = %q, want %q", c.t, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParse(t *testing.T) {
|
|
cases := []struct {
|
|
msg string
|
|
want Type
|
|
}{
|
|
// standard cases
|
|
{"feat: add login page", TypeFeat},
|
|
{"fix: correct null pointer", TypeFix},
|
|
{"chore: update deps", TypeNone},
|
|
{"docs: update README", TypeNone},
|
|
|
|
// non-strict: case-insensitive
|
|
{"Feat: add login page", TypeFeat},
|
|
{"FIX: correct null pointer", TypeFix},
|
|
{"FEAT: add login", TypeFeat},
|
|
|
|
// non-strict: missing space after colon
|
|
{"feat:add login", TypeFeat},
|
|
{"fix:correct null pointer", TypeFix},
|
|
|
|
// with scope
|
|
{"feat(auth): add OAuth2", TypeFeat},
|
|
{"fix(db): prevent deadlock", TypeFix},
|
|
|
|
// breaking change via !
|
|
{"feat!: remove legacy API", TypeBreaking},
|
|
{"fix!: change response format", TypeBreaking},
|
|
{"feat(api)!: rename endpoint", TypeBreaking},
|
|
|
|
// breaking change via footer
|
|
{"feat: new API\n\nBREAKING CHANGE: old API removed", TypeBreaking},
|
|
{"refactor: cleanup\n\nBREAKING CHANGE: interface changed", TypeBreaking},
|
|
|
|
// unparseable — silently ignored
|
|
{"WIP", TypeNone},
|
|
{"Merge branch 'main' into release/1.2", TypeNone},
|
|
{"", TypeNone},
|
|
{"fix a bug without colon", TypeNone},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
got := Parse(c.msg)
|
|
if got != c.want {
|
|
t.Errorf("Parse(%q) = %s, want %s", c.msg, got, c.want)
|
|
}
|
|
}
|
|
}
|