Skip to content

Commit 005df36

Browse files
committed
Assert roundtrip for patch formats
Re-parse our string representation and make sure it is equal. This is important for binary patches due to inconsistencies in DEFLATE implementations, but we do it for text patches too.
1 parent 7aac9c2 commit 005df36

File tree

3 files changed

+96
-40
lines changed

3 files changed

+96
-40
lines changed

gitdiff/gitdiff.go

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,11 @@ func (f *File) String() string {
137137
} else {
138138
diff.WriteString("GIT binary patch\n")
139139
diff.WriteString(f.BinaryFragment.String())
140+
diff.WriteByte('\n')
141+
140142
if f.ReverseBinaryFragment != nil {
141-
diff.WriteByte('\n')
142143
diff.WriteString(f.ReverseBinaryFragment.String())
144+
diff.WriteByte('\n')
143145
}
144146
}
145147
}
@@ -402,35 +404,10 @@ func (f *BinaryFragment) String() string {
402404
return diff.String()
403405
}
404406

405-
// TODO(bkeyes): The 'compress/flate' package does not produce minimal output
406-
// streams. Instead of flagging that the last block of data represents the end
407-
// of the stream, it always writes a final empty block to mark the end. Git's
408-
// implementation using the 'zlib' C library does not do this, which means that
409-
// what we produce for binary patches does not match the input, even though it
410-
// is valid.
411-
//
412-
// This is mostly a problem for my tests, where I compare the input and output
413-
// bytes. This comparison isn't required, but is helpful to catch invalid
414-
// output that might otherwise still parse.
415-
//
416-
// Options for fixing this:
417-
//
418-
// 1. Fix the tests to compare parsed objects instead of raw patches, at least
419-
// for binary patches. This means writing something to do reasonable
420-
// comparisons of File structs.
421-
//
422-
// 2. Add my own deflate function. By default, Git appears to use no
423-
// compression on binary patch data, which means "delfate" is just adding
424-
// the appropriate headers and checksums around the data. This would fix my
425-
// tests but means we could never emit compressed data, so we'd differ from
426-
// Git in other situations.
427-
//
428-
// Either way, there will be situations in which re-formatted binary patches
429-
// differ from the original inputs.
430407
func deflateBinaryChunk(data []byte) []byte {
431408
var b bytes.Buffer
432409

433-
zw, _ := zlib.NewWriterLevel(&b, zlib.NoCompression)
410+
zw := zlib.NewWriter(&b)
434411
_, _ = zw.Write(data)
435412
_ = zw.Close()
436413

gitdiff/gitdiff_string_test.go

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package gitdiff
22

33
import (
44
"bytes"
5+
"fmt"
56
"os"
7+
"slices"
68
"testing"
79
)
810

9-
func TestFile_String(t *testing.T) {
11+
func TestParseRoundtrip(t *testing.T) {
1012
sources := []string{
1113
"testdata/string/binary_modify.patch",
1214
"testdata/string/binary_new.patch",
@@ -35,6 +37,11 @@ func TestFile_String(t *testing.T) {
3537
if string(b) != str {
3638
t.Errorf("%s: incorrect patch\nexpected: %q\n actual: %q\n", src, string(b), str)
3739
}
40+
41+
reparsed := assertParseSingleFile(t, fmt.Sprintf("Parse(%q).String()", src), []byte(str))
42+
43+
// TODO(bkeyes): include source in these messages (via subtest?)
44+
assertFilesEqual(t, original, reparsed)
3845
}
3946
}
4047

@@ -49,19 +56,91 @@ func assertParseSingleFile(t *testing.T, src string, b []byte) *File {
4956
return files[0]
5057
}
5158

52-
/*
53-
func TestDecode(t *testing.T) {
54-
actual := []byte("cmV-O")
55-
mine := []byte("cmV)N")
59+
func assertFilesEqual(t *testing.T, expected, actual *File) {
60+
assertEqual(t, expected.OldName, actual.OldName, "OldName")
61+
assertEqual(t, expected.NewName, actual.NewName, "NewName")
62+
63+
assertEqual(t, expected.IsNew, actual.IsNew, "IsNew")
64+
assertEqual(t, expected.IsDelete, actual.IsDelete, "IsDelete")
65+
assertEqual(t, expected.IsCopy, actual.IsCopy, "IsCopy")
66+
assertEqual(t, expected.IsRename, actual.IsRename, "IsRename")
67+
68+
assertEqual(t, expected.OldMode, actual.OldMode, "OldMode")
69+
assertEqual(t, expected.NewMode, actual.NewMode, "NewMode")
70+
71+
assertEqual(t, expected.OldOIDPrefix, actual.OldOIDPrefix, "OldOIDPrefix")
72+
assertEqual(t, expected.NewOIDPrefix, actual.NewOIDPrefix, "NewOIDPrefix")
73+
assertEqual(t, expected.Score, actual.Score, "Score")
74+
75+
if len(expected.TextFragments) == len(actual.TextFragments) {
76+
for i := range expected.TextFragments {
77+
prefix := fmt.Sprintf("TextFragments[%d].", i)
78+
ef := expected.TextFragments[i]
79+
af := actual.TextFragments[i]
80+
81+
assertEqual(t, ef.Comment, af.Comment, prefix+"Comment")
82+
83+
assertEqual(t, ef.OldPosition, af.OldPosition, prefix+"OldPosition")
84+
assertEqual(t, ef.OldLines, af.OldLines, prefix+"OldLines")
85+
86+
assertEqual(t, ef.NewPosition, af.NewPosition, prefix+"NewPosition")
87+
assertEqual(t, ef.NewLines, af.NewLines, prefix+"NewLines")
88+
89+
assertEqual(t, ef.LinesAdded, af.LinesAdded, prefix+"LinesAdded")
90+
assertEqual(t, ef.LinesDeleted, af.LinesDeleted, prefix+"LinesDeleted")
91+
92+
assertEqual(t, ef.LeadingContext, af.LeadingContext, prefix+"LeadingContext")
93+
assertEqual(t, ef.TrailingContext, af.TrailingContext, prefix+"TrailingContext")
94+
95+
if !slices.Equal(ef.Lines, af.Lines) {
96+
t.Errorf("%sLines: expected %#v, actual %#v", prefix, ef.Lines, af.Lines)
97+
}
98+
}
99+
} else {
100+
t.Errorf("TextFragments: expected length %d, actual length %d", len(expected.TextFragments), len(actual.TextFragments))
101+
}
102+
103+
assertEqual(t, expected.IsBinary, actual.IsBinary, "IsBinary")
104+
105+
if expected.BinaryFragment != nil {
106+
if actual.BinaryFragment == nil {
107+
t.Errorf("BinaryFragment: expected non-nil, actual is nil")
108+
} else {
109+
ef := expected.BinaryFragment
110+
af := expected.BinaryFragment
111+
112+
assertEqual(t, ef.Method, af.Method, "BinaryFragment.Method")
113+
assertEqual(t, ef.Size, af.Size, "BinaryFragment.Size")
114+
115+
if !slices.Equal(ef.Data, af.Data) {
116+
t.Errorf("BinaryFragment.Data: expected %#v, actual %#v", ef.Data, af.Data)
117+
}
118+
}
119+
} else if actual.BinaryFragment != nil {
120+
t.Errorf("BinaryFragment: expected nil, actual is non-nil")
121+
}
56122

57-
dst := make([]byte, 4)
123+
if expected.ReverseBinaryFragment != nil {
124+
if actual.ReverseBinaryFragment == nil {
125+
t.Errorf("ReverseBinaryFragment: expected non-nil, actual is nil")
126+
} else {
127+
ef := expected.ReverseBinaryFragment
128+
af := expected.ReverseBinaryFragment
58129

59-
base85Decode(dst, actual)
60-
t.Logf("actual: %x / %b", dst, dst)
130+
assertEqual(t, ef.Method, af.Method, "ReverseBinaryFragment.Method")
131+
assertEqual(t, ef.Size, af.Size, "ReverseBinaryFragment.Size")
61132

62-
base85Decode(dst, mine)
63-
t.Logf(" mine: %x / %b", dst, dst)
133+
if !slices.Equal(ef.Data, af.Data) {
134+
t.Errorf("ReverseBinaryFragment.Data: expected %#v, actual %#v", ef.Data, af.Data)
135+
}
136+
}
137+
} else if actual.ReverseBinaryFragment != nil {
138+
t.Errorf("ReverseBinaryFragment: expected nil, actual is non-nil")
139+
}
140+
}
64141

65-
t.FailNow()
142+
func assertEqual[T comparable](t *testing.T, expected, actual T, name string) {
143+
if expected != actual {
144+
t.Errorf("%s: expected %#v, actual %#v", name, expected, actual)
145+
}
66146
}
67-
*/

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module github.com/bluekeyes/go-gitdiff
22

3-
go 1.13
3+
go 1.20

0 commit comments

Comments
 (0)