1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain
4
5import (
6 "errors"
7 "testing"
8)
9
10func budgetID(t *testing.T, s string) ObjectID {
11 t.Helper()
12 id, err := ParseObjectID(s)
13 if err != nil {
14 t.Fatal(err)
15 }
16 return id
17}
18
19func TestValidateCommitObjects(t *testing.T) {
20 id := budgetID(t, "1111111111111111111111111111111111111111")
21 for _, tt := range []struct {
22 name string
23 ids []ObjectID
24 max int
25 want error
26 }{
27 {"empty", nil, 1, nil}, {"boundary", []ObjectID{id}, 1, nil},
28 {"too many", []ObjectID{id, id}, 1, ErrInvalid}, {"zero", []ObjectID{{}}, 1, ErrInvalidObjectID},
29 } {
30 t.Run(tt.name, func(t *testing.T) {
31 if err := ValidateCommitObjects(tt.ids, tt.max); !errors.Is(err, tt.want) {
32 t.Fatalf("got %v, want %v", err, tt.want)
33 }
34 })
35 }
36}
37
38func TestCommitObjectBudget(t *testing.T) {
39 id := budgetID(t, "1111111111111111111111111111111111111111")
40 for _, tt := range []struct {
41 name string
42 commit bool
43 size, total int64
44 seen, take, stop bool
45 }{
46 {"non commit", false, 1, 0, false, false, false},
47 {"oversize", true, MaxCommitObjectBytes + 1, 0, false, false, false},
48 {"exact object", true, MaxCommitObjectBytes, 0, false, true, false},
49 {"exact total", true, 1, MaxCommitObjectsBytes - 1, false, true, false},
50 {"over total", true, 2, MaxCommitObjectsBytes - 1, false, false, true},
51 {"duplicate", true, 1, MaxCommitObjectsBytes, true, false, false},
52 {"negative", true, -1, 0, false, false, false},
53 } {
54 t.Run(tt.name, func(t *testing.T) {
55 b := CommitObjectBudget{total: tt.total}
56 if tt.seen {
57 b.seen = map[ObjectID]bool{id: true}
58 }
59 take, stop := b.Admit(id, tt.commit, tt.size)
60 if take != tt.take || stop != tt.stop {
61 t.Fatalf("got %v,%v", take, stop)
62 }
63 if take {
64 b.Charge(id, tt.size)
65 if !b.Seen(id) || b.total != tt.total+tt.size {
66 t.Fatal("charge not recorded")
67 }
68 }
69 })
70 }
71}
72
73func TestDiffBudgetWindow(t *testing.T) {
74 for _, tt := range []struct {
75 name string
76 limits DiffLimits
77 kept, from, to int
78 truncated bool
79 }{
80 {"unlimited", DiffLimits{}, 5, 0, 5, false},
81 {"cap", DiffLimits{MaxFiles: 3}, 3, 0, 3, true},
82 {"window", DiffLimits{From: 1, To: 4}, 5, 1, 4, false},
83 {"clamped", DiffLimits{From: -1, To: 9, MaxFiles: 3}, 3, 0, 3, true},
84 {"past end", DiffLimits{From: 8}, 5, 8, 5, false},
85 {"reversed", DiffLimits{From: 4, To: 2}, 5, 4, 2, false},
86 } {
87 t.Run(tt.name, func(t *testing.T) {
88 b := NewDiffBudget(tt.limits)
89 k, f, to, tr := b.Window(5)
90 if k != tt.kept || f != tt.from || to != tt.to || tr != tt.truncated {
91 t.Fatalf("got %d,%d,%d,%v", k, f, to, tr)
92 }
93 })
94 }
95}
96
97func TestDiffBudgetAdmit(t *testing.T) {
98 for _, tt := range []struct {
99 name string
100 limits DiffLimits
101 binary bool
102 path string
103 n int
104 wantRead bool
105 wantWhy FileOmission
106 }{
107 {"fits", DiffLimits{MaxFileLines: 3, MaxTotalLines: 3, MaxTotalBytes: 1}, false, "a.go", 3, true, OmitNone},
108 {"binary", DiffLimits{MaxTotalBytes: 1}, true, "a.bin", 3, false, OmitNone},
109 {"no changes", DiffLimits{MaxTotalBytes: 1}, false, "a.go", 0, false, OmitNone},
110 {"file limit", DiffLimits{MaxFileLines: 3, MaxTotalLines: 5, MaxTotalBytes: 1}, false, "a.go", 4, false, OmitLarge},
111 {"total lines", DiffLimits{MaxFileLines: 3, MaxTotalLines: 2, MaxTotalBytes: 1}, false, "a.go", 3, false, OmitBudget},
112 {"no bytes", DiffLimits{MaxFileLines: 3, MaxTotalLines: 3}, false, "a.go", 1, false, OmitBudget},
113 {"generated", DiffLimits{SkipGenerated: true, MaxTotalBytes: 1}, false, "go.sum", 1, false, OmitGenerated},
114 {"generated not skipped", DiffLimits{MaxTotalBytes: 1}, false, "go.sum", 1, true, OmitNone},
115 {"generated before large", DiffLimits{SkipGenerated: true, MaxFileLines: 1, MaxTotalBytes: 1}, false, "go.sum", 9, false, OmitGenerated},
116 {"unlimited lines", DiffLimits{MaxTotalBytes: 1}, false, "a.go", 1 << 20, true, OmitNone},
117 } {
118 t.Run(tt.name, func(t *testing.T) {
119 b := NewDiffBudget(tt.limits)
120 read, why := b.Admit(tt.binary, tt.path, tt.n)
121 if read != tt.wantRead || why != tt.wantWhy {
122 t.Fatalf("got %v,%v", read, why)
123 }
124 })
125 }
126}
127
128func TestDiffBudgetCharge(t *testing.T) {
129 for _, tt := range []struct {
130 name string
131 bytes int
132 want FileOmission
133 }{{"exact", 3, OmitNone}, {"over", 4, OmitUnavailable}} {
134 t.Run(tt.name, func(t *testing.T) {
135 b := NewDiffBudget(DiffLimits{MaxFileLines: 2, MaxTotalLines: 1, MaxTotalBytes: 3})
136 before := b
137 if b.FileLines() != 1 {
138 t.Fatal("wrong file limit")
139 }
140 if got := b.ChargeLine(tt.bytes, true); got != tt.want {
141 t.Fatalf("wrong charge result %v", got)
142 }
143 if b.ChargeLine(0, true) == OmitNone {
144 t.Fatal("line budget exceeded")
145 }
146 b = before
147 if read, _ := b.Admit(false, "a", 1); !read {
148 t.Fatal("discarded file consumed budget")
149 }
150 })
151 }
152}
153
154func TestDiffBudgetChargeReasons(t *testing.T) {
155 b := NewDiffBudget(DiffLimits{MaxTotalLines: 1, MaxTotalBytes: 3})
156 if b.ChargeLine(2, false) != OmitNone || b.FileLines() != 1 {
157 t.Fatal("context consumes bytes only")
158 }
159 if b.ChargeLine(1, true) != OmitNone || b.FileLines() != 0 {
160 t.Fatal("first changed line fits")
161 }
162 if got := b.ChargeLine(0, true); got != OmitBudget {
163 t.Fatalf("lines ran out: %v", got)
164 }
165 b = NewDiffBudget(DiffLimits{MaxTotalLines: 9, MaxTotalBytes: 1})
166 if got := b.ChargeLine(2, false); got != OmitUnavailable {
167 t.Fatalf("bytes ran out: %v", got)
168 }
169 free := NewDiffBudget(DiffLimits{MaxTotalBytes: 5})
170 for range 100 {
171 if free.ChargeLine(0, true) != OmitNone {
172 t.Fatal("0 means no line limit")
173 }
174 }
175 if free.FileLines() < 1<<30 {
176 t.Fatal("unlimited FileLines")
177 }
178}
179
180func TestFileOmissionCollapsed(t *testing.T) {
181 for o, want := range map[FileOmission]bool{OmitNone: false, OmitGenerated: true, OmitLarge: true, OmitBudget: true, OmitUnavailable: false} {
182 if o.Collapsed() != want {
183 t.Errorf("%d: got %v", o, !want)
184 }
185 }
186}
187
188func TestOneFileDiffLimits(t *testing.T) {
189 l := OneFileDiffLimits(3)
190 if l.From != 3 || l.To != 4 || l.MaxFileLines != 0 || l.MaxAloneLines != MaxOneFileLines || l.MaxTotalLines != 0 || l.MaxFiles != 0 || l.MaxTotalBytes != DefaultDiffLimits().MaxTotalBytes {
191 t.Fatalf("got %+v", l)
192 }
193 // A negative index (an unchecked IndexFunc) selects no file, not the whole diff.
194 if _, from, to, _ := NewDiffBudget(OneFileDiffLimits(-1)).Window(10); from < to {
195 t.Fatalf("OneFileDiffLimits(-1) selects files [%d, %d)", from, to)
196 }
197}
198
199func TestDiffBudgetBytesOfEarlierFiles(t *testing.T) {
200 b := NewDiffBudget(DiffLimits{MaxTotalBytes: 10})
201 b.StartFile()
202 if b.ChargeLine(4, true) != OmitNone {
203 t.Fatal("the first file fits")
204 }
205 b.StartFile()
206 if got := b.ChargeLine(7, true); got != OmitBudget {
207 t.Fatalf("a file meeting the bytes of earlier files: got %v, want OmitBudget", got)
208 }
209 alone := NewDiffBudget(DiffLimits{MaxTotalBytes: 10})
210 alone.StartFile()
211 if got := alone.ChargeLine(11, true); got != OmitUnavailable {
212 t.Fatalf("a file over the bytes on its own: got %v, want OmitUnavailable", got)
213 }
214}
215
216func TestDiffBudgetContext(t *testing.T) {
217 b := NewDiffBudget(DiffLimits{MaxFileLines: 2, MaxTotalLines: 2, MaxTotalBytes: 6})
218 if b.ChargeLine(2, false) != OmitNone || b.FileLines() != 2 {
219 t.Fatal("context consumed changed-line budget")
220 }
221 first := b.ChargeLine(1, true)
222 second := b.ChargeLine(1, true)
223 if first != OmitNone || second != OmitNone || b.FileLines() != 0 {
224 t.Fatal("additions and deletions must consume changed-line budget")
225 }
226 if b.ChargeLine(2, false) != OmitNone {
227 t.Fatal("context at the byte boundary must fit even without changed lines left")
228 }
229 if b.ChargeLine(1, false) != OmitUnavailable {
230 t.Fatal("context must not exceed the byte budget")
231 }
232}
233
234func TestDiffBudgetAdmitAloneLines(t *testing.T) {
235 b := NewDiffBudget(OneFileDiffLimits(0))
236 if read, why := b.Admit(false, "a.go", MaxOneFileLines); !read || why != OmitNone {
237 t.Fatalf("at the cap: read=%v why=%v", read, why)
238 }
239 if read, why := b.Admit(false, "a.go", MaxOneFileLines+1); read || why != OmitUnavailable {
240 t.Fatalf("over the cap: read=%v why=%v, want OmitUnavailable", read, why)
241 }
242}