Go 200 lines
package workflow
import (
"os/exec"
"strings"
"testing"
)
// stubDocDriftTags overrides docDriftTagSource for the test so the
// tag side of the comparison is fixed without touching real git.
func stubDocDriftTags(t *testing.T, tags []string) {
t.Helper()
old := docDriftTagSource
docDriftTagSource = func(string) ([]string, error) { return tags, nil }
t.Cleanup(func() { docDriftTagSource = old })
}
func TestDocDrift_NoChangelog(t *testing.T) {
cfg := newCfg(t)
stubDocDriftTags(t, []string{"v1.0.0"})
res, err := docDrift{}.Run(Env{Config: cfg})
if err != nil {
t.Fatal(err)
}
if res.Code != CodeClean {
t.Errorf("Code = %d, want %d (%q)", res.Code, CodeClean, res.Summary)
}
if res.Summary != "no CHANGELOG.md to check" {
t.Errorf("Summary = %q", res.Summary)
}
}
func TestDocDrift_NoTags(t *testing.T) {
cfg := newCfg(t)
writeRepoFile(t, cfg.RepoRoot, "CHANGELOG.md", "# Changelog\n\n## [v1.0.0]\n")
stubDocDriftTags(t, nil)
res, err := docDrift{}.Run(Env{Config: cfg})
if err != nil {
t.Fatal(err)
}
if res.Code != CodeClean {
t.Errorf("Code = %d, want %d (%q)", res.Code, CodeClean, res.Summary)
}
if res.Summary != "no git tags to check against" {
t.Errorf("Summary = %q", res.Summary)
}
}
func TestDocDrift_AllDocumented(t *testing.T) {
cfg := newCfg(t)
writeRepoFile(t, cfg.RepoRoot, "CHANGELOG.md",
"# Changelog\n\n## [Unreleased]\n\n## [v1.1.0]\n\n## [v1.0.0]\n")
stubDocDriftTags(t, []string{"v1.1.0", "v1.0.0"})
res, err := docDrift{}.Run(Env{Config: cfg})
if err != nil {
t.Fatal(err)
}
if res.Code != CodeClean {
t.Fatalf("Code = %d, want %d (%q)", res.Code, CodeClean, res.Summary)
}
if q := queueBody(t, cfg); q != "" {
t.Errorf("queue should be empty, got:\n%s", q)
}
}
func TestDocDrift_TagWithoutSection(t *testing.T) {
cfg := newCfg(t)
writeRepoFile(t, cfg.RepoRoot, "CHANGELOG.md", "# Changelog\n\n## [v1.0.0]\n")
stubDocDriftTags(t, []string{"v1.1.0", "v1.0.0"})
res, err := docDrift{}.Run(Env{Config: cfg})
if err != nil {
t.Fatal(err)
}
if res.Code != CodeFinding {
t.Fatalf("Code = %d, want %d (%q)", res.Code, CodeFinding, res.Summary)
}
if len(res.Findings) != 1 {
t.Fatalf("Findings = %d, want 1: %+v", len(res.Findings), res.Findings)
}
if !strings.Contains(res.Findings[0].Msg, "v1.1.0") {
t.Errorf("Finding.Msg = %q, want it to name v1.1.0", res.Findings[0].Msg)
}
q := queueBody(t, cfg)
if strings.Count(q, "**doc-drift**") != 1 || !strings.Contains(q, "v1.1.0") {
t.Errorf("queue missing doc-drift item for v1.1.0:\n%s", q)
}
}
func TestDocDrift_SectionWithoutTagBelowLatest(t *testing.T) {
cfg := newCfg(t)
writeRepoFile(t, cfg.RepoRoot, "CHANGELOG.md",
"# Changelog\n\n## [v1.2.0]\n\n## [v1.1.0]\n\n## [v1.0.0]\n")
// v1.1.0 is documented but never tagged; it sits below the latest
// tag v1.2.0, so it is a genuine gap, not a release-in-progress.
stubDocDriftTags(t, []string{"v1.2.0", "v1.0.0"})
res, err := docDrift{}.Run(Env{Config: cfg})
if err != nil {
t.Fatal(err)
}
if res.Code != CodeFinding {
t.Fatalf("Code = %d, want %d (%q)", res.Code, CodeFinding, res.Summary)
}
if len(res.Findings) != 1 || !strings.Contains(res.Findings[0].Msg, "v1.1.0") {
t.Fatalf("Findings = %+v, want one naming v1.1.0", res.Findings)
}
if strings.Count(queueBody(t, cfg), "**doc-drift**") != 1 {
t.Errorf("want exactly one queued doc-drift item")
}
}
func TestDocDrift_SectionAheadOfLatestTagIsClean(t *testing.T) {
cfg := newCfg(t)
// v1.1.0 is documented ahead of the latest tag v1.0.0 — the expected
// release-in-progress state, not drift. `## [Unreleased]` is ignored.
writeRepoFile(t, cfg.RepoRoot, "CHANGELOG.md",
"# Changelog\n\n## [Unreleased]\n\n## [v1.1.0]\n\n## [v1.0.0]\n")
stubDocDriftTags(t, []string{"v1.0.0"})
res, err := docDrift{}.Run(Env{Config: cfg})
if err != nil {
t.Fatal(err)
}
if res.Code != CodeClean {
t.Fatalf("Code = %d, want %d (%q)", res.Code, CodeClean, res.Summary)
}
if q := queueBody(t, cfg); q != "" {
t.Errorf("queue should be empty, got:\n%s", q)
}
}
func TestDocDrift_Mixed(t *testing.T) {
cfg := newCfg(t)
// Tags v1.0.0 + v1.2.0 are both undocumented (class 1); section
// v1.1.0 has no tag and sits below latest v1.2.0 (class 2); section
// v1.3.0 is ahead of latest, exempt.
writeRepoFile(t, cfg.RepoRoot, "CHANGELOG.md",
"# Changelog\n\n## [v1.3.0]\n\n## [v1.1.0]\n")
stubDocDriftTags(t, []string{"v1.2.0", "v1.0.0"})
res, err := docDrift{}.Run(Env{Config: cfg})
if err != nil {
t.Fatal(err)
}
if res.Code != CodeFinding {
t.Fatalf("Code = %d, want %d (%q)", res.Code, CodeFinding, res.Summary)
}
if len(res.Findings) != 3 {
t.Fatalf("Findings = %d, want 3: %+v", len(res.Findings), res.Findings)
}
if n := strings.Count(queueBody(t, cfg), "**doc-drift**"); n != 3 {
t.Errorf("queued doc-drift items = %d, want 3", n)
}
}
func TestDocDrift_GitMissing(t *testing.T) {
cfg := newCfg(t)
writeRepoFile(t, cfg.RepoRoot, "CHANGELOG.md", "# Changelog\n\n## [v1.0.0]\n")
// Emptying PATH hides the git binary; the workflow must report
// blocked rather than pass a check it never ran.
t.Setenv("PATH", "")
res, err := docDrift{}.Run(Env{Config: cfg})
if err != nil {
t.Fatal(err)
}
if res.Code != CodeBlocked {
t.Errorf("Code = %d, want %d (%q)", res.Code, CodeBlocked, res.Summary)
}
}
// TestDocDrift_RealGit exercises the real gitx.SemverTags wiring (no
// stub) against an actual tagged repo, covering the integration end to
// end alongside the stubbed table cases above.
func TestDocDrift_RealGit(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not available")
}
cfg := newCfg(t)
writeRepoFile(t, cfg.RepoRoot, "CHANGELOG.md", "# Changelog\n\n## [Unreleased]\n")
gitInit(t, cfg.RepoRoot)
runGit(t, cfg.RepoRoot, "commit", "-q", "-m", "init")
runGit(t, cfg.RepoRoot, "tag", "v0.1.0")
res, err := docDrift{}.Run(Env{Config: cfg})
if err != nil {
t.Fatal(err)
}
if res.Code != CodeFinding {
t.Fatalf("Code = %d, want %d (%q)", res.Code, CodeFinding, res.Summary)
}
if len(res.Findings) != 1 || !strings.Contains(res.Findings[0].Msg, "v0.1.0") {
t.Errorf("Findings = %+v, want one naming v0.1.0", res.Findings)
}
}