Files
releaser/internal/commits/commits_test.go
T

153 lines
3.8 KiB
Go
Raw Normal View History

2026-07-07 00:07:53 +02:00
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)
}
}
2026-07-07 00:07:53 +02:00
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)
}
}
}