Range over slice copies each element by value; use index or pointer receiver for large structs to avoid copies
for i, snap := range commits {
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}
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.