1// SPDX-License-Identifier: MIT23package processor45import (6 "strings"7 "testing"8)910func TestApplyDiffToBlameNewFile(t *testing.T) {11 got := applyDiffToBlame(nil, 3, []LineRange{{Start: 1, Count: 3}}, nil, 7)12 want := []authorID{7, 7, 7}13 if !equalIDs(got, want) {14 t.Errorf("new file blame = %v, want %v", got, want)15 }16}1718func TestApplyDiffToBlameAppend(t *testing.T) {19 prev := []authorID{1, 1}20 got := applyDiffToBlame(prev, 4, []LineRange{{Start: 3, Count: 2}}, nil, 9)21 want := []authorID{1, 1, 9, 9}22 if !equalIDs(got, want) {23 t.Errorf("append blame = %v, want %v", got, want)24 }25}2627func TestApplyDiffToBlamePureDelete(t *testing.T) {28 prev := []authorID{1, 1, 1, 1}29 got := applyDiffToBlame(prev, 2, nil, []LineRange{{Start: 2, Count: 2}}, 9)30 want := []authorID{1, 1}31 if !equalIDs(got, want) {32 t.Errorf("delete blame = %v, want %v", got, want)33 }34}3536func TestApplyDiffToBlameReplaceMiddle(t *testing.T) {37 // 5 lines from author 1; replace line 3 with two lines from author 2.38 prev := []authorID{1, 1, 1, 1, 1}39 got := applyDiffToBlame(prev, 6,40 []LineRange{{Start: 3, Count: 2}},41 []LineRange{{Start: 3, Count: 1}},42 2)43 want := []authorID{1, 1, 2, 2, 1, 1}44 if !equalIDs(got, want) {45 t.Errorf("replace blame = %v, want %v", got, want)46 }47}4849func TestApplyDiffToBlamePadsToNewLines(t *testing.T) {50 // Diff arithmetic disagrees with newLines — defensive pad with sentinel.51 got := applyDiffToBlame(nil, 4, nil, nil, 9)52 want := []authorID{0, 0, 0, 0}53 if !equalIDs(got, want) {54 t.Errorf("pad blame = %v, want %v", got, want)55 }56}5758func TestAuthorRegistryInternsCanonical(t *testing.T) {59 mm := parseMailmap([]byte("Alice <alice@example.com> <a@example.com>\n"))60 r := newAuthorRegistry(mm)61 id1 := r.intern("Alice", "a@example.com")62 id2 := r.intern("Alice", "alice@example.com")63 if id1 != id2 {64 t.Errorf("mailmap-folded identities should collapse: %d vs %d", id1, id2)65 }66 if r.record(id1).Email != "alice@example.com" {67 t.Errorf("canonical email = %q, want alice@example.com", r.record(id1).Email)68 }69}7071func TestAuthorRegistrySentinelReserved(t *testing.T) {72 r := newAuthorRegistry(nil)73 id := r.intern("Bob", "bob@example.com")74 if id == sentinelAuthorID {75 t.Errorf("real author should not be assigned sentinel ID")76 }77}7879func TestParseMailmapNameOnly(t *testing.T) {80 m := parseMailmap([]byte("Proper Name <commit@example.com>\n"))81 name, email := m.Resolve("Other Name", "commit@example.com")82 if name != "Proper Name" {83 t.Errorf("name = %q, want Proper Name", name)84 }85 if email != "commit@example.com" {86 t.Errorf("email = %q, want commit@example.com", email)87 }88}8990func TestParseMailmapEmailReplacement(t *testing.T) {91 m := parseMailmap([]byte("<proper@example.com> <commit@example.com>\n"))92 name, email := m.Resolve("Commit Name", "commit@example.com")93 if email != "proper@example.com" {94 t.Errorf("email = %q, want proper@example.com", email)95 }96 if name != "Commit Name" {97 t.Errorf("name = %q, want unchanged Commit Name", name)98 }99}100101func TestParseMailmapNameAndEmailReplacement(t *testing.T) {102 m := parseMailmap([]byte("Proper <proper@example.com> Commit <commit@example.com>\n"))103 // Should only match when commit name AND commit email both match.104 name, email := m.Resolve("Commit", "commit@example.com")105 if name != "Proper" || email != "proper@example.com" {106 t.Errorf("got (%q,%q), want (Proper, proper@example.com)", name, email)107 }108 // Different commit name → no match.109 name2, email2 := m.Resolve("Other", "commit@example.com")110 if name2 != "Other" || email2 != "commit@example.com" {111 t.Errorf("got (%q,%q), want unchanged", name2, email2)112 }113}114115func TestParseMailmapSkipsCommentsAndBlanks(t *testing.T) {116 body := "# comment\n\nAlice <a@x> # trailing comment\n"117 m := parseMailmap([]byte(body))118 if len(m.byEmail) != 1 {119 t.Errorf("byEmail entries = %d, want 1", len(m.byEmail))120 }121}122123func TestMailmapResolveNilSafe(t *testing.T) {124 var m *mailmap125 n, e := m.Resolve("Bob", "b@x")126 if n != "Bob" || e != "b@x" {127 t.Errorf("nil mailmap should be no-op, got (%q,%q)", n, e)128 }129}130131func TestParseMailmapLineForms(t *testing.T) {132 cases := []struct {133 in string134 properN string135 properE string136 commitN string137 commitE string138 ok bool139 }{140 {"Proper Name <c@x>", "Proper Name", "", "", "c@x", true},141 {"<p@x> <c@x>", "", "p@x", "", "c@x", true},142 {"Proper Name <p@x> <c@x>", "Proper Name", "p@x", "", "c@x", true},143 {"Proper Name <p@x> Commit Name <c@x>", "Proper Name", "p@x", "Commit Name", "c@x", true},144 {"no brackets here", "", "", "", "", false},145 }146 for _, c := range cases {147 got, ok := parseMailmapLine(c.in)148 if ok != c.ok {149 t.Errorf("parseMailmapLine(%q) ok = %v, want %v", c.in, ok, c.ok)150 continue151 }152 if !ok {153 continue154 }155 if got.properName != c.properN || got.properEmail != c.properE ||156 got.commitName != c.commitN || got.commitEmail != c.commitE {157 t.Errorf("parseMailmapLine(%q) = %+v, want (%q,%q,%q,%q)",158 c.in, got, c.properN, c.properE, c.commitN, c.commitE)159 }160 }161}162163func TestParseMailmapTrimsWhitespace(t *testing.T) {164 m := parseMailmap([]byte(" Proper Name <c@x> \n"))165 if e, ok := m.byEmail["c@x"]; !ok || e.Name != "Proper Name" {166 t.Errorf("byEmail[c@x] = %+v, want Proper Name", e)167 }168}169170// TestAuthorRegistryFoldsByNameAndDomain documents the canonical scc-repo171// case: the same human committing from a personal address and a172// github-noreply address. The two have different domains so under the173// strict (name, domain) rule they intentionally stay split — folding here174// would require the looser name-only mode which is not the default.175func TestAuthorRegistryFoldsByNameAndDomain(t *testing.T) {176 r := newAuthorRegistryWithFold(nil, true)177 id1 := r.intern("Ben Boyter", "ben@boyter.org")178 id2 := r.intern("Ben Boyter", "boyter@users.noreply.github.com")179 if id1 == id2 {180 t.Errorf("different domains should not fold under strict (name,domain): %d == %d", id1, id2)181 }182}183184func TestAuthorRegistryFoldsSameDomainDifferentEmail(t *testing.T) {185 r := newAuthorRegistryWithFold(nil, true)186 id1 := r.intern("Alice", "alice@x.com")187 id2 := r.intern("Alice", "alice.smith@x.com")188 if id1 != id2 {189 t.Errorf("same (name,domain) should fold: %d vs %d", id1, id2)190 }191}192193func TestAuthorRegistryDoesNotFoldDifferentDomain(t *testing.T) {194 r := newAuthorRegistryWithFold(nil, true)195 id1 := r.intern("Daniel", "d@a.com")196 id2 := r.intern("Daniel", "d@b.com")197 if id1 == id2 {198 t.Errorf("different domains should stay split: %d == %d", id1, id2)199 }200}201202func TestAuthorRegistrySkipsGenericNames(t *testing.T) {203 r := newAuthorRegistryWithFold(nil, true)204 id1 := r.intern("root", "root@host-a")205 id2 := r.intern("root", "root@host-b")206 if id1 == id2 {207 t.Errorf("generic name 'root' must not fold across hosts: %d == %d", id1, id2)208 }209 // Even within the same domain, the skip list keeps them split — the210 // name is too generic to assume identity.211 id3 := r.intern("root", "root@host-a")212 if id3 != id1 {213 t.Errorf("identical (name,email) for skipped names should still dedupe by primary key: %d vs %d", id3, id1)214 }215}216217func TestAuthorRegistryFoldsBots(t *testing.T) {218 r := newAuthorRegistryWithFold(nil, true)219 id1 := r.intern("dependabot[bot]", "x@y.com")220 id2 := r.intern("dependabot[bot]", "x@y.com")221 if id1 != id2 {222 t.Errorf("identical bot identities should collapse: %d vs %d", id1, id2)223 }224}225226func TestAuthorRegistryMailmapBeatsFold(t *testing.T) {227 // Mailmap maps A→C and B→D. Even though A and B share a name+domain228 // after the (unrelated) folding key would suggest, the mailmap routes229 // them to distinct canonical identities and they must stay split.230 mm := parseMailmap([]byte(231 "Carol <carol@example.com> Sam <a@example.com>\n" +232 "Dave <dave@example.com> Sam <b@example.com>\n"))233 r := newAuthorRegistryWithFold(mm, true)234 id1 := r.intern("Sam", "a@example.com")235 id2 := r.intern("Sam", "b@example.com")236 if id1 == id2 {237 t.Errorf("mailmap-distinct identities must not fold: %d == %d", id1, id2)238 }239}240241func TestAuthorRegistryFoldDisabled(t *testing.T) {242 r := newAuthorRegistryWithFold(nil, false)243 id1 := r.intern("Alice", "alice@x.com")244 id2 := r.intern("Alice", "alice.smith@x.com")245 if id1 == id2 {246 t.Errorf("with fold disabled, distinct emails must stay split: %d == %d", id1, id2)247 }248}249250func TestEmailDomain(t *testing.T) {251 cases := []struct{ in, want string }{252 {"a@b.com", "b.com"},253 {"A@B.COM", "b.com"},254 {"a@b@c.com", "c.com"},255 {"noatsign", ""},256 {"", ""},257 }258 for _, c := range cases {259 if got := emailDomain(c.in); got != c.want {260 t.Errorf("emailDomain(%q) = %q, want %q", c.in, got, c.want)261 }262 }263}264265func equalIDs(a, b []authorID) bool {266 if len(a) != len(b) {267 return false268 }269 for i := range a {270 if a[i] != b[i] {271 return false272 }273 }274 return true275}276277// TestParseMailmapHandlesCaseInsensitiveEmail confirms that mailmap email278// keys are folded to lowercase so commits using mixed-case emails still279// resolve. Real-world commits often have inconsistent casing.280func TestParseMailmapHandlesCaseInsensitiveEmail(t *testing.T) {281 m := parseMailmap([]byte("Alice <alice@example.com> <a@example.com>\n"))282 n, e := m.Resolve("Anyone", strings.ToUpper("a@example.com"))283 if n != "Alice" || e != "alice@example.com" {284 t.Errorf("case-insensitive lookup got (%q,%q), want (Alice, alice@example.com)", n, e)285 }286}
Findings
✓ No findings reported for this file.