Monorepo for Tangled
tangled.org
1package patchutil
2
3import (
4 "errors"
5 "reflect"
6 "testing"
7
8 "tangled.org/core/types"
9)
10
11func TestIsPatchValid(t *testing.T) {
12 tests := []struct {
13 name string
14 patch string
15 expected error
16 }{
17 {
18 name: `empty patch`,
19 patch: ``,
20 expected: EmptyPatchError,
21 },
22 {
23 name: `single line patch`,
24 patch: `single line`,
25 expected: EmptyPatchError,
26 },
27 {
28 name: `valid diff patch`,
29 patch: `diff --git a/file.txt b/file.txt
30index abc..def 100644
31--- a/file.txt
32+++ b/file.txt
33@@ -1,3 +1,3 @@
34-old line
35+new line
36 context`,
37 expected: nil,
38 },
39 {
40 name: `valid patch starting with ---`,
41 patch: `--- a/file.txt
42+++ b/file.txt
43@@ -1,3 +1,3 @@
44-old line
45+new line
46 context`,
47 expected: nil,
48 },
49 {
50 name: `valid patch starting with Index`,
51 patch: `Index: file.txt
52==========
53--- a/file.txt
54+++ b/file.txt
55@@ -1,3 +1,3 @@
56-old line
57+new line
58 context`,
59 expected: nil,
60 },
61 {
62 name: `valid patch starting with +++`,
63 patch: `+++ b/file.txt
64--- a/file.txt
65@@ -1,3 +1,3 @@
66-old line
67+new line
68 context`,
69 expected: nil,
70 },
71 {
72 name: `valid patch starting with @@`,
73 patch: `@@ -1,3 +1,3 @@
74-old line
75+new line
76 context
77`,
78 expected: nil,
79 },
80 {
81 name: `valid format patch`,
82 patch: `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
83From: Author <author@example.com>
84Date: Wed, 16 Apr 2025 11:01:00 +0300
85Subject: [PATCH] Example patch
86
87diff --git a/file.txt b/file.txt
88index 123456..789012 100644
89--- a/file.txt
90+++ b/file.txt
91@@ -1 +1 @@
92-old content
93+new content
94--
952.48.1`,
96 expected: nil,
97 },
98 {
99 name: `invalid format patch`,
100 patch: `From 1234567890123456789012345678901234567890 Mon Sep 17 00:00:00 2001
101From: Author <author@example.com>
102This is not a valid patch format`,
103 expected: FormatPatchError,
104 },
105 {
106 name: `not a patch at all`,
107 patch: `This is
108just some
109random text
110that isn't a patch`,
111 expected: GenericPatchError,
112 },
113 }
114
115 for _, tt := range tests {
116 t.Run(tt.name, func(t *testing.T) {
117 result := IsPatchValid(tt.patch)
118 if !errors.Is(result, tt.expected) {
119 t.Errorf("IsPatchValid() = %v, want %v", result, tt.expected)
120 }
121 })
122 }
123}
124
125func TestSplitPatches(t *testing.T) {
126 tests := []struct {
127 name string
128 input string
129 expected []string
130 }{
131 {
132 name: "Empty input",
133 input: "",
134 expected: []string{},
135 },
136 {
137 name: "No valid patches",
138 input: "This is not a \nJust some random text",
139 expected: []string{},
140 },
141 {
142 name: "Single patch",
143 input: `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
144From: Author <author@example.com>
145Date: Wed, 16 Apr 2025 11:01:00 +0300
146Subject: [PATCH] Example patch
147
148diff --git a/file.txt b/file.txt
149index 123456..789012 100644
150--- a/file.txt
151+++ b/file.txt
152@@ -1 +1 @@
153-old content
154+new content
155--
1562.48.1`,
157 expected: []string{
158 `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
159From: Author <author@example.com>
160Date: Wed, 16 Apr 2025 11:01:00 +0300
161Subject: [PATCH] Example patch
162
163diff --git a/file.txt b/file.txt
164index 123456..789012 100644
165--- a/file.txt
166+++ b/file.txt
167@@ -1 +1 @@
168-old content
169+new content
170--
1712.48.1`,
172 },
173 },
174 {
175 name: "Two patches",
176 input: `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
177From: Author <author@example.com>
178Date: Wed, 16 Apr 2025 11:01:00 +0300
179Subject: [PATCH 1/2] First patch
180
181diff --git a/file1.txt b/file1.txt
182index 123456..789012 100644
183--- a/file1.txt
184+++ b/file1.txt
185@@ -1 +1 @@
186-old content
187+new content
188--
1892.48.1
190From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001
191From: Author <author@example.com>
192Date: Wed, 16 Apr 2025 11:03:11 +0300
193Subject: [PATCH 2/2] Second patch
194
195diff --git a/file2.txt b/file2.txt
196index abcdef..ghijkl 100644
197--- a/file2.txt
198+++ b/file2.txt
199@@ -1 +1 @@
200-foo bar
201+baz qux
202--
2032.48.1`,
204 expected: []string{
205 `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
206From: Author <author@example.com>
207Date: Wed, 16 Apr 2025 11:01:00 +0300
208Subject: [PATCH 1/2] First patch
209
210diff --git a/file1.txt b/file1.txt
211index 123456..789012 100644
212--- a/file1.txt
213+++ b/file1.txt
214@@ -1 +1 @@
215-old content
216+new content
217--
2182.48.1`,
219 `From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001
220From: Author <author@example.com>
221Date: Wed, 16 Apr 2025 11:03:11 +0300
222Subject: [PATCH 2/2] Second patch
223
224diff --git a/file2.txt b/file2.txt
225index abcdef..ghijkl 100644
226--- a/file2.txt
227+++ b/file2.txt
228@@ -1 +1 @@
229-foo bar
230+baz qux
231--
2322.48.1`,
233 },
234 },
235 {
236 name: "Patches with additional text between them",
237 input: `Some text before the patches
238
239From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
240From: Author <author@example.com>
241Subject: [PATCH] First patch
242
243diff content here
244--
2452.48.1
246
247Some text between patches
248
249From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001
250From: Author <author@example.com>
251Subject: [PATCH] Second patch
252
253more diff content
254--
2552.48.1
256
257Text after patches`,
258 expected: []string{
259 `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
260From: Author <author@example.com>
261Subject: [PATCH] First patch
262
263diff content here
264--
2652.48.1
266
267Some text between patches`,
268 `From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001
269From: Author <author@example.com>
270Subject: [PATCH] Second patch
271
272more diff content
273--
2742.48.1
275
276Text after patches`,
277 },
278 },
279 {
280 name: "Patches with whitespace padding",
281 input: `
282
283From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
284From: Author <author@example.com>
285Subject: Patch
286
287content
288--
2892.48.1
290
291
292From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001
293From: Author <author@example.com>
294Subject: Another patch
295
296content
297--
2982.48.1
299 `,
300 expected: []string{
301 `From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001
302From: Author <author@example.com>
303Subject: Patch
304
305content
306--
3072.48.1`,
308 `From a9529f3b3a653329a5268f0f4067225480207e3c Mon Sep 17 00:00:00 2001
309From: Author <author@example.com>
310Subject: Another patch
311
312content
313--
3142.48.1`,
315 },
316 },
317 }
318
319 for _, tt := range tests {
320 t.Run(tt.name, func(t *testing.T) {
321 result := splitFormatPatch(tt.input)
322 if !reflect.DeepEqual(result, tt.expected) {
323 t.Errorf("splitPatches() = %v, want %v", result, tt.expected)
324 }
325 })
326 }
327}
328
329func TestIsFormatPatch(t *testing.T) {
330 tests := []struct {
331 name string
332 patch string
333 want bool
334 }{
335 // fast path: sentinel timestamp
336 {
337 name: "sentinel timestamp",
338 patch: "From 3c5035488318164b81f60fe3adcd6c9199d76331 Mon Sep 17 00:00:00 2001\nFrom: Author <a@example.com>\n",
339 want: true,
340 },
341 // header-count path: various two-header combinations
342 {
343 name: "From and Date headers",
344 patch: "From: Author <a@example.com>\nDate: Mon, 1 Jan 2024 00:00:00 +0000\n",
345 want: true,
346 },
347 {
348 name: "From and Subject headers",
349 patch: "From: Author <a@example.com>\nSubject: [PATCH] fix thing\n",
350 want: true,
351 },
352 {
353 name: "Subject and Date headers",
354 patch: "Subject: [PATCH] fix thing\nDate: Mon, 1 Jan 2024 00:00:00 +0000\n",
355 want: true,
356 },
357 {
358 name: "commit and From headers",
359 patch: "commit abc123\nFrom: Author <a@example.com>\n",
360 want: true,
361 },
362 // boundary: headers at lines 9 and 10 (0-indexed 8 and 9, last scanned)
363 {
364 name: "headers at lines 9 and 10",
365 patch: "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nFrom: Author <a@example.com>\nSubject: [PATCH] fix\n",
366 want: true,
367 },
368 // false cases
369 {
370 name: "empty string",
371 patch: "",
372 want: false,
373 },
374 {
375 name: "single line",
376 patch: "From: Author <a@example.com>",
377 want: false,
378 },
379 {
380 name: "plain diff",
381 patch: "diff --git a/f.txt b/f.txt\n--- a/f.txt\n+++ b/f.txt\n",
382 want: false,
383 },
384 {
385 name: "From prefix but wrong timestamp falls through to header count of 1",
386 patch: "From 3c5035488318164b81f60fe3adcd6c9199d76331 Tue Oct 10 12:00:00 2023\nFrom: Author <a@example.com>\n",
387 want: false,
388 },
389 {
390 name: "only one recognized header",
391 patch: "Subject: [PATCH] fix thing\nsome other line\n",
392 want: false,
393 },
394 {
395 name: "headers pushed past line 10 are not counted",
396 patch: "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nFrom: Author <a@example.com>\nSubject: [PATCH] fix\n",
397 want: false,
398 },
399 }
400
401 for _, tt := range tests {
402 t.Run(tt.name, func(t *testing.T) {
403 if got := IsFormatPatch(tt.patch); got != tt.want {
404 t.Errorf("IsFormatPatch() = %v, want %v", got, tt.want)
405 }
406 })
407 }
408}
409
410func TestImplsInterfaces(t *testing.T) {
411 id := &InterdiffResult{}
412 _ = isDiffsRenderer(id)
413}
414
415func isDiffsRenderer[S types.DiffRenderer](S) bool { return true }