processor/history_authors_test.go GO 522 lines View on github.com → Search inside
1// SPDX-License-Identifier: MIT23package processor45import (6	"encoding/csv"7	"os"8	"path/filepath"9	"strings"10	"testing"11	"time"1213	"github.com/go-git/go-git/v5"14	"github.com/go-git/go-git/v5/plumbing/object"15	jsoniter "github.com/json-iterator/go"16)1718// authoredCommit is one step in a fixture history. Files maps relative path19// to the full file content at this commit. Author/Email override the default20// per-commit identity.21type authoredCommit struct {22	Files  map[string]string23	Author string24	Email  string25}2627// makeAuthoredRepo builds a temp on-disk repo with a sequence of commits each28// using the caller's named author. Used to exercise per-author attribution29// in the authors observer.30func makeAuthoredRepo(t *testing.T, commits []authoredCommit) string {31	t.Helper()32	ProcessConstants()33	dir := t.TempDir()3435	repo, err := git.PlainInit(dir, false)36	if err != nil {37		t.Fatalf("init repo: %v", err)38	}39	wt, err := repo.Worktree()40	if err != nil {41		t.Fatalf("worktree: %v", err)42	}4344	when := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)45	for i, snap := range commits {46		for path, content := range snap.Files {47			full := filepath.Join(dir, path)48			if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {49				t.Fatalf("mkdir %s: %v", full, err)50			}51			if err := os.WriteFile(full, []byte(content), 0o644); err != nil {52				t.Fatalf("write %s: %v", full, err)53			}54			if _, err := wt.Add(path); err != nil {55				t.Fatalf("add %s: %v", path, err)56			}57		}58		_, err := wt.Commit("commit "+itoa(i), &git.CommitOptions{59			Author: &object.Signature{60				Name:  snap.Author,61				Email: snap.Email,62				When:  when.Add(time.Duration(i) * time.Hour),63			},64		})65		if err != nil {66			t.Fatalf("commit %d: %v", i, err)67		}68	}69	return dir70}7172// findAuthorRow returns the row whose canonical name matches `name`, or73// fails the test.74func findAuthorRow(t *testing.T, rows []authorRow, name string) authorRow {75	t.Helper()76	for _, r := range rows {77		if r.Name == name {78			return r79		}80	}81	t.Fatalf("no row found for %q in %+v", name, rows)82	return authorRow{}83}8485func TestAuthorsLastToucherAttribution(t *testing.T) {86	saveDepth := HistoryDepth87	HistoryDepth = 10088	t.Cleanup(func() { HistoryDepth = saveDepth })8990	// Alice introduces 7 lines; Bob rewrites lines 4–7 (4 lines).91	// Net: Alice owns 3, Bob owns 4. No (before window) — full history walked.92	first := "package x\nfunc A() {}\nfunc B() {}\nfunc C() {}\nfunc D() {}\nfunc E() {}\nfunc F() {}\n"93	// 7 lines: line1=package, line2=A, line3=B, line4=C, line5=D, line6=E, line7=F.94	// We want Bob to rewrite lines 4-7 (4 lines).95	second := "package x\nfunc A() {}\nfunc B() {}\nfunc CC() {}\nfunc DD() {}\nfunc EE() {}\nfunc FF() {}\n"9697	dir := makeAuthoredRepo(t, []authoredCommit{98		{Files: map[string]string{"main.go": first}, Author: "Alice", Email: "alice@x"},99		{Files: map[string]string{"main.go": second}, Author: "Bob", Email: "bob@x"},100	})101102	obs := newHistoryAuthorsObserver()103	if _, err := runHistory(dir, obs); err != nil {104		t.Fatalf("runHistory: %v", err)105	}106107	alice := findAuthorRow(t, obs.rows, "Alice")108	bob := findAuthorRow(t, obs.rows, "Bob")109110	if alice.Code != 3 {111		t.Errorf("Alice Code = %d, want 3", alice.Code)112	}113	if bob.Code != 4 {114		t.Errorf("Bob Code = %d, want 4", bob.Code)115	}116}117118func TestAuthorsPercentagesSumTo100(t *testing.T) {119	saveDepth := HistoryDepth120	HistoryDepth = 100121	t.Cleanup(func() { HistoryDepth = saveDepth })122123	dir := makeAuthoredRepo(t, []authoredCommit{124		{Files: map[string]string{"a.go": "package a\nfunc A() {}\nfunc B() {}\n"}, Author: "Alice", Email: "alice@x"},125		{Files: map[string]string{"a.go": "package a\nfunc A() {}\nfunc B() {}\nfunc C() {}\n"}, Author: "Bob", Email: "bob@x"},126	})127128	obs := newHistoryAuthorsObserver()129	if _, err := runHistory(dir, obs); err != nil {130		t.Fatalf("runHistory: %v", err)131	}132133	sum := 0.0134	for _, r := range obs.rows {135		sum += r.OwnsPercent136	}137	if sum < 99.99 || sum > 100.01 {138		t.Errorf("OwnsPercent sum = %f, want ~100", sum)139	}140}141142func TestAuthorsBaselineSentinel(t *testing.T) {143	saveDepth := HistoryDepth144	HistoryDepth = 1145	t.Cleanup(func() { HistoryDepth = saveDepth })146147	// Two commits, depth=1 keeps only the second; the first becomes baseline.148	first := "package a\nfunc A() {}\nfunc B() {}\nfunc C() {}\n"               // 4 lines, all "before window"149	second := "package a\nfunc A() {}\nfunc B() {}\nfunc C() {}\nfunc D() {}\n" // adds 1150151	dir := makeAuthoredRepo(t, []authoredCommit{152		{Files: map[string]string{"a.go": first}, Author: "Alice", Email: "alice@x"},153		{Files: map[string]string{"a.go": second}, Author: "Bob", Email: "bob@x"},154	})155156	obs := newHistoryAuthorsObserver()157	if _, err := runHistory(dir, obs); err != nil {158		t.Fatalf("runHistory: %v", err)159	}160161	// Bob is the only real author in the window; expect (before window) sentinel162	// to hold Alice's surviving lines.163	var sentinel *authorRow164	for i := range obs.rows {165		if obs.rows[i].Sentinel {166			sentinel = &obs.rows[i]167		}168	}169	if sentinel == nil {170		t.Fatalf("no sentinel row; got rows %+v", obs.rows)171	}172	if sentinel.Code == 0 {173		t.Errorf("sentinel Code = 0, want surviving baseline lines")174	}175	if sentinel.OwnsPercent < 50 {176		t.Errorf("sentinel OwnsPercent = %f, want > 50 (only 1 line added in window)", sentinel.OwnsPercent)177	}178}179180func TestAuthorsMailmapFolding(t *testing.T) {181	saveDepth := HistoryDepth182	HistoryDepth = 100183	t.Cleanup(func() { HistoryDepth = saveDepth })184185	// Same person under two emails — mailmap folds them.186	dir := makeAuthoredRepo(t, []authoredCommit{187		{Files: map[string]string{188			".mailmap": "Alice <alice@example.com> <alt@example.com>\n",189			"a.go":     "package a\nfunc A() {}\nfunc B() {}\n",190		}, Author: "Alice", Email: "alice@example.com"},191		{Files: map[string]string{192			"a.go": "package a\nfunc A() {}\nfunc B() {}\nfunc C() {}\n",193		}, Author: "Alice", Email: "alt@example.com"},194	})195196	obs := newHistoryAuthorsObserver()197	if _, err := runHistory(dir, obs); err != nil {198		t.Fatalf("runHistory: %v", err)199	}200201	// Should collapse to a single Alice row.202	aliceCount := 0203	for _, r := range obs.rows {204		if r.Name == "Alice" {205			aliceCount++206		}207	}208	if aliceCount != 1 {209		t.Errorf("Alice rows after mailmap fold = %d, want 1; rows = %+v", aliceCount, obs.rows)210	}211}212213func TestAuthorsBusFactorDominant(t *testing.T) {214	saveDepth := HistoryDepth215	HistoryDepth = 100216	t.Cleanup(func() { HistoryDepth = saveDepth })217218	// Alice writes most code; Bob adds a tiny bit. Bus factor should be 1.219	bigAlice := "package x\n" + strings.Repeat("func F() {}\n", 20)220	dir := makeAuthoredRepo(t, []authoredCommit{221		{Files: map[string]string{"a.go": bigAlice}, Author: "Alice", Email: "alice@x"},222		{Files: map[string]string{"b.go": "package y\nfunc B() {}\n"}, Author: "Bob", Email: "bob@x"},223	})224225	obs := newHistoryAuthorsObserver()226	if _, err := runHistory(dir, obs); err != nil {227		t.Fatalf("runHistory: %v", err)228	}229230	if obs.busFactor != 1 {231		t.Errorf("busFactor = %d, want 1", obs.busFactor)232	}233}234235func TestAuthorsBusFactorBalanced(t *testing.T) {236	saveDepth := HistoryDepth237	HistoryDepth = 100238	t.Cleanup(func() { HistoryDepth = saveDepth })239240	// Three roughly-equal contributors; bus factor should be >= 2.241	chunk := func(name string) string {242		return "package " + name + "\n" + strings.Repeat("func F() {}\n", 5)243	}244	dir := makeAuthoredRepo(t, []authoredCommit{245		{Files: map[string]string{"a.go": chunk("a")}, Author: "Alice", Email: "alice@x"},246		{Files: map[string]string{"b.go": chunk("b")}, Author: "Bob", Email: "bob@x"},247		{Files: map[string]string{"c.go": chunk("c")}, Author: "Carol", Email: "carol@x"},248	})249250	obs := newHistoryAuthorsObserver()251	if _, err := runHistory(dir, obs); err != nil {252		t.Fatalf("runHistory: %v", err)253	}254255	if obs.busFactor < 2 {256		t.Errorf("busFactor = %d, want >= 2", obs.busFactor)257	}258}259260func TestAuthorsBusFactorIgnoresSentinel(t *testing.T) {261	saveDepth := HistoryDepth262	HistoryDepth = 2263	t.Cleanup(func() { HistoryDepth = saveDepth })264265	// Big baseline commit predates the window (depth=2 keeps only the last266	// two commits). Alice and Bob each add a small amount in-window. The267	// surviving HEAD is dominated by the baseline (sentinel), but bus factor268	// must reflect Alice/Bob, not be diluted by the sentinel.269	baseline := "package x\n" + strings.Repeat("func B() {}\n", 50)270	aliceAdd := baseline + "func A1() {}\nfunc A2() {}\nfunc A3() {}\n"271	bobAdd := aliceAdd + "func Bo1() {}\nfunc Bo2() {}\n"272273	dir := makeAuthoredRepo(t, []authoredCommit{274		{Files: map[string]string{"a.go": baseline}, Author: "Zed", Email: "zed@x"},275		{Files: map[string]string{"a.go": aliceAdd}, Author: "Alice", Email: "alice@x"},276		{Files: map[string]string{"a.go": bobAdd}, Author: "Bob", Email: "bob@x"},277	})278279	obs := newHistoryAuthorsObserver()280	if _, err := runHistory(dir, obs); err != nil {281		t.Fatalf("runHistory: %v", err)282	}283284	if obs.busFactor < 1 || obs.busFactor > 2 {285		t.Errorf("busFactor = %d, want 1 or 2", obs.busFactor)286	}287288	// Sentinel must exist and dominate share-of-all, but must not appear in289	// the bus-factor walk.290	var sentinelFound bool291	for _, r := range obs.rows {292		if r.Sentinel {293			sentinelFound = true294			if r.OwnsPercent < 50 {295				t.Errorf("sentinel OwnsPercent = %.1f, want > 50 (most code is pre-window)", r.OwnsPercent)296			}297			if r.InWindowPercent != 0 {298				t.Errorf("sentinel InWindowPercent = %.1f, want 0", r.InWindowPercent)299			}300		}301	}302	if !sentinelFound {303		t.Fatalf("no sentinel row; got rows %+v", obs.rows)304	}305306	// busCovered is over in-window code; with only Alice+Bob in-window it307	// must reach > 50 within the (1 or 2) walked authors. The old behavior308	// would have left busCovered ~ a few percent, diluted by the sentinel.309	if obs.busCovered <= 50 {310		t.Errorf("busCovered = %.1f, want > 50 (in-window denominator)", obs.busCovered)311	}312}313314func TestAuthorsBusFactorAllPreWindow(t *testing.T) {315	saveDepth := HistoryDepth316	HistoryDepth = 1317	t.Cleanup(func() { HistoryDepth = saveDepth })318319	// Two commits: depth=1 keeps only the second. The second commit just320	// deletes a file, so every surviving line at HEAD came from the baseline321	// — there is no in-window code at all.322	first := "package a\nfunc A() {}\nfunc B() {}\nfunc C() {}\n"323	dir := makeAuthoredRepo(t, []authoredCommit{324		{Files: map[string]string{325			"a.go": first,326			"b.go": "package b\nfunc B() {}\n",327		}, Author: "Alice", Email: "alice@x"},328		// Second commit: rewrite b.go to be empty — does not modify a.go,329		// so the only HEAD code is Alice's a.go, all pre-window.330		{Files: map[string]string{"b.go": ""}, Author: "Bob", Email: "bob@x"},331	})332333	obs := newHistoryAuthorsObserver()334	if _, err := runHistory(dir, obs); err != nil {335		t.Fatalf("runHistory: %v", err)336	}337338	if obs.inWindowCode != 0 {339		t.Errorf("inWindowCode = %d, want 0", obs.inWindowCode)340	}341	if obs.busFactor != 0 {342		t.Errorf("busFactor = %d, want 0", obs.busFactor)343	}344345	footer := formatAuthorsFooter(obs, 79)346	if !strings.Contains(footer, "no code touched in window") {347		t.Errorf("footer = %q, want 'no code touched in window'", footer)348	}349}350351func TestAuthorsCSVIncludesEveryAuthorAndSentinel(t *testing.T) {352	saveDepth, saveFormat := HistoryDepth, Format353	HistoryDepth, Format = 1, "csv"354	t.Cleanup(func() { HistoryDepth, Format = saveDepth, saveFormat })355356	first := "package a\nfunc A() {}\nfunc B() {}\nfunc C() {}\n"357	second := "package a\nfunc A() {}\nfunc B() {}\nfunc C() {}\nfunc D() {}\n"358	dir := makeAuthoredRepo(t, []authoredCommit{359		{Files: map[string]string{"a.go": first}, Author: "Alice", Email: "alice@x"},360		{Files: map[string]string{"a.go": second}, Author: "Bob", Email: "bob@x"},361	})362363	obs := newHistoryAuthorsObserver()364	if _, err := runHistory(dir, obs); err != nil {365		t.Fatalf("runHistory: %v", err)366	}367	out, err := renderAuthors(obs)368	if err != nil {369		t.Fatalf("render: %v", err)370	}371	if !strings.HasPrefix(out, "# window:") {372		t.Fatalf("CSV should start with '# window:' comment, got:\n%s", out)373	}374375	body := strings.SplitN(out, "\n", 2)[1]376	r := csv.NewReader(strings.NewReader(body))377	rows, err := r.ReadAll()378	if err != nil {379		t.Fatalf("csv parse: %v", err)380	}381	wantHeader := []string{"Author", "Email", "Code", "Complexity", "Comment", "Files", "OwnsPercent", "LastCommit", "BeforeWindow"}382	for i, h := range wantHeader {383		if rows[0][i] != h {384			t.Errorf("header col %d = %q, want %q", i, rows[0][i], h)385		}386	}387388	// Find Bob and sentinel row.389	var sawBob, sawSentinel bool390	for _, row := range rows[1:] {391		if row[0] == "Bob" {392			sawBob = true393		}394		if row[len(row)-1] == "true" {395			sawSentinel = true396		}397	}398	if !sawBob {399		t.Errorf("CSV missing Bob row")400	}401	if !sawSentinel {402		t.Errorf("CSV missing (before window) sentinel row")403	}404}405406func TestAuthorsJSONShape(t *testing.T) {407	saveDepth, saveFormat := HistoryDepth, Format408	HistoryDepth, Format = 100, "json"409	t.Cleanup(func() { HistoryDepth, Format = saveDepth, saveFormat })410411	dir := makeAuthoredRepo(t, []authoredCommit{412		{Files: map[string]string{"a.go": "package a\nfunc A() {}\n"}, Author: "Alice", Email: "alice@x"},413		{Files: map[string]string{"a.go": "package a\nfunc A() {}\nfunc B() {}\n"}, Author: "Bob", Email: "bob@x"},414	})415416	obs := newHistoryAuthorsObserver()417	if _, err := runHistory(dir, obs); err != nil {418		t.Fatalf("runHistory: %v", err)419	}420	out, err := renderAuthors(obs)421	if err != nil {422		t.Fatalf("render: %v", err)423	}424425	var doc authorsJSONDoc426	if err := jsoniter.Unmarshal([]byte(out), &doc); err != nil {427		t.Fatalf("json parse: %v, body:\n%s", err, out)428	}429	if doc.Report != "authors" {430		t.Errorf("report = %q, want authors", doc.Report)431	}432	if doc.Window.Commits != 2 {433		t.Errorf("window.commits = %d, want 2", doc.Window.Commits)434	}435	if len(doc.Authors) == 0 {436		t.Fatalf("no authors in JSON output")437	}438	// A real author has Name/Email set.439	var foundReal bool440	for _, a := range doc.Authors {441		if a.Name != nil && *a.Name != "" {442			foundReal = true443		}444	}445	if !foundReal {446		t.Errorf("no real author with name field in JSON: %+v", doc.Authors)447	}448}449450func TestRenderAuthorsRejectsUnsupportedFormat(t *testing.T) {451	saveFormat := Format452	Format = "xml"453	t.Cleanup(func() { Format = saveFormat })454	obs := newHistoryAuthorsObserver()455	if _, err := renderAuthors(obs); err == nil {456		t.Fatal("expected error for --format xml")457	}458}459460func TestWrapBusFactorFooterFitsSingleLine(t *testing.T) {461	got := wrapBusFactorFooter("Bus factor 2 · ", []string{"Alice", "Bob"},462		" last-touched 80% of code", 79)463	if strings.Contains(got, "\n") {464		t.Errorf("short footer should not wrap: %q", got)465	}466}467468func TestWrapBusFactorFooterBreaksOnTokenBoundary(t *testing.T) {469	names := []string{}470	for i := 0; i < 20; i++ {471		names = append(names, "Author"+itoa(i))472	}473	got := wrapBusFactorFooter("Bus factor 20 · ", names,474		" last-touched 60% of code", 79)475	if !strings.Contains(got, "\n") {476		t.Errorf("long footer should wrap: %q", got)477	}478	for _, line := range strings.Split(got, "\n") {479		if runewidthStringWidthForTest(line) > 79 {480			t.Errorf("wrapped line exceeds 79 cols (%d): %q",481				runewidthStringWidthForTest(line), line)482		}483	}484}485486func runewidthStringWidthForTest(s string) int {487	w := 0488	for _, r := range s {489		if r == '\t' {490			w++491			continue492		}493		w++494	}495	return w496}497498func TestAuthorsTabularContainsBusFactorFooter(t *testing.T) {499	saveDepth, saveFormat := HistoryDepth, Format500	HistoryDepth, Format = 100, "tabular"501	t.Cleanup(func() { HistoryDepth, Format = saveDepth, saveFormat })502503	dir := makeAuthoredRepo(t, []authoredCommit{504		{Files: map[string]string{"a.go": "package a\nfunc A() {}\nfunc B() {}\n"}, Author: "Alice", Email: "alice@x"},505	})506507	obs := newHistoryAuthorsObserver()508	if _, err := runHistory(dir, obs); err != nil {509		t.Fatalf("runHistory: %v", err)510	}511	out, err := renderAuthors(obs)512	if err != nil {513		t.Fatalf("render: %v", err)514	}515	if !strings.Contains(out, "Bus factor") {516		t.Errorf("tabular output missing 'Bus factor' footer:\n%s", out)517	}518	if !strings.Contains(out, "Authors") {519		t.Errorf("tabular output missing 'Authors' header:\n%s", out)520	}521}

Code quality findings 7

Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
info performance copy-large-struct
for i, snap := range commits {
Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
info performance copy-large-struct
for path, content := range snap.Files {
Can cause issues on Windows consider filepath.Join instead
info correctness path-join-windows
full := filepath.Join(dir, path)
String to byte slice conversion inside loop allocates a new slice each iteration; convert once before the loop
info correctness string-to-byte-in-loop
if err := os.WriteFile(full, []byte(content), 0o644); err != nil {
Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
info performance copy-large-struct
for i, h := range wantHeader {
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
names = append(names, "Author"+itoa(i))
Deeply nested control structures reduce readability; consider extracting to functions or using early returns
info maintainability deep-nesting
if _, err := runHistory(dir, obs); err != nil {

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.