mirror of
https://github.com/bluekeyes/go-gitdiff
synced 2026-06-08 13:18:30 +00:00
cbd3915448
This gets 100% test coverage for the functions involved in parsing Git-style file headers. Whether that actually makes it correct remains to be seen.
518 lines
11 KiB
Go
518 lines
11 KiB
Go
package gitdiff
|
|
|
|
import (
|
|
"bufio"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestLineOperations(t *testing.T) {
|
|
content := `the first line
|
|
the second line
|
|
the third line
|
|
`
|
|
|
|
newParser := func() *parser {
|
|
return &parser{r: bufio.NewReader(strings.NewReader(content))}
|
|
}
|
|
|
|
t.Run("readLine", func(t *testing.T) {
|
|
p := newParser()
|
|
|
|
line, err := p.Line()
|
|
if err != nil {
|
|
t.Fatalf("error reading first line: %v", err)
|
|
}
|
|
if line != "the first line\n" {
|
|
t.Fatalf("incorrect first line: %s", line)
|
|
}
|
|
|
|
line, err = p.Line()
|
|
if err != nil {
|
|
t.Fatalf("error reading second line: %v", err)
|
|
}
|
|
if line != "the second line\n" {
|
|
t.Fatalf("incorrect second line: %s", line)
|
|
}
|
|
})
|
|
|
|
t.Run("peekLine", func(t *testing.T) {
|
|
p := newParser()
|
|
|
|
line, err := p.PeekLine()
|
|
if err != nil {
|
|
t.Fatalf("error peeking line: %v", err)
|
|
}
|
|
if line != "the first line\n" {
|
|
t.Fatalf("incorrect peek line: %s", line)
|
|
}
|
|
|
|
// test that a second peek returns the same value
|
|
line, err = p.PeekLine()
|
|
if err != nil {
|
|
t.Fatalf("error peeking line: %v", err)
|
|
}
|
|
if line != "the first line\n" {
|
|
t.Fatalf("incorrect peek line: %s", line)
|
|
}
|
|
|
|
// test that reading the line returns the same value
|
|
line, err = p.Line()
|
|
if err != nil {
|
|
t.Fatalf("error reading line: %v", err)
|
|
}
|
|
if line != "the first line\n" {
|
|
t.Fatalf("incorrect line: %s", line)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestParseFragmentHeader(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Input string
|
|
Output *Fragment
|
|
Err bool
|
|
}{
|
|
"shortest": {
|
|
Input: "@@ -0,0 +1 @@\n",
|
|
Output: &Fragment{
|
|
OldPosition: 0,
|
|
OldLines: 0,
|
|
NewPosition: 1,
|
|
NewLines: 1,
|
|
},
|
|
},
|
|
"standard": {
|
|
Input: "@@ -21,5 +28,9 @@\n",
|
|
Output: &Fragment{
|
|
OldPosition: 21,
|
|
OldLines: 5,
|
|
NewPosition: 28,
|
|
NewLines: 9,
|
|
},
|
|
},
|
|
"trailingWhitespace": {
|
|
Input: "@@ -21,5 +28,9 @@ \r\n",
|
|
Output: &Fragment{
|
|
OldPosition: 21,
|
|
OldLines: 5,
|
|
NewPosition: 28,
|
|
NewLines: 9,
|
|
},
|
|
},
|
|
"incomplete": {
|
|
Input: "@@ -12,3 +2\n",
|
|
Err: true,
|
|
},
|
|
"badNumbers": {
|
|
Input: "@@ -1a,2b +3c,4d @@\n",
|
|
Err: true,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
var frag Fragment
|
|
err := parseFragmentHeader(&frag, test.Input)
|
|
if test.Err {
|
|
if err == nil {
|
|
t.Fatalf("expected error parsing header, but got nil")
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("error parsing header: %v", err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(*test.Output, frag) {
|
|
t.Fatalf("incorrect fragment\nexpected: %+v\nactual: %+v", *test.Output, frag)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCleanName(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Input string
|
|
Drop int
|
|
Output string
|
|
}{
|
|
"alreadyClean": {
|
|
Input: "a/b/c.txt", Output: "a/b/c.txt",
|
|
},
|
|
"doubleSlashes": {
|
|
Input: "a//b/c.txt", Output: "a/b/c.txt",
|
|
},
|
|
"tripleSlashes": {
|
|
Input: "a///b/c.txt", Output: "a/b/c.txt",
|
|
},
|
|
"dropPrefix": {
|
|
Input: "a/b/c.txt", Drop: 2, Output: "c.txt",
|
|
},
|
|
"removeDoublesBeforeDrop": {
|
|
Input: "a//b/c.txt", Drop: 1, Output: "b/c.txt",
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
output := cleanName(test.Input, test.Drop)
|
|
if output != test.Output {
|
|
t.Fatalf("incorrect output: expected %q, actual %q", test.Output, output)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseName(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Input string
|
|
Term rune
|
|
Drop int
|
|
Output string
|
|
N int
|
|
Err bool
|
|
}{
|
|
"singleUnquoted": {
|
|
Input: "dir/file.txt", Output: "dir/file.txt", N: 12,
|
|
},
|
|
"singleQuoted": {
|
|
Input: `"dir/file.txt"`, Output: "dir/file.txt", N: 14,
|
|
},
|
|
"quotedWithEscape": {
|
|
Input: `"dir/\"quotes\".txt"`, Output: `dir/"quotes".txt`, N: 20,
|
|
},
|
|
"quotedWithSpaces": {
|
|
Input: `"dir/space file.txt"`, Output: "dir/space file.txt", N: 20,
|
|
},
|
|
"tabTerminator": {
|
|
Input: "dir/space file.txt\tfile2.txt", Term: '\t', Output: "dir/space file.txt", N: 18,
|
|
},
|
|
"dropPrefix": {
|
|
Input: "a/dir/file.txt", Drop: 1, Output: "dir/file.txt", N: 14,
|
|
},
|
|
"multipleNames": {
|
|
Input: "dir/a.txt dir/b.txt", Term: -1, Output: "dir/a.txt", N: 9,
|
|
},
|
|
"devNull": {
|
|
Input: "/dev/null", Term: '\t', Drop: 1, Output: "/dev/null", N: 9,
|
|
},
|
|
"emptyString": {
|
|
Input: "", Err: true,
|
|
},
|
|
"emptyQuotedString": {
|
|
Input: `""`, Err: true,
|
|
},
|
|
"unterminatedQuotes": {
|
|
Input: `"dir/file.txt`, Err: true,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
output, n, err := parseName(test.Input, test.Term, test.Drop)
|
|
if test.Err {
|
|
if err == nil {
|
|
t.Fatalf("expected error parsing name, but got nil")
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error parsing name: %v", err)
|
|
}
|
|
|
|
if output != test.Output {
|
|
t.Errorf("incorect output: expected %q, actual: %q", test.Output, output)
|
|
}
|
|
if n != test.N {
|
|
t.Errorf("incorrect next position: expected %d, actual %d", test.N, n)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseGitHeaderData(t *testing.T) {
|
|
tests := map[string]struct {
|
|
InputFile *File
|
|
Line string
|
|
DefaultName string
|
|
|
|
OutputFile *File
|
|
End bool
|
|
Err bool
|
|
}{
|
|
"fragementEndsParsing": {
|
|
Line: "@@ -12,3 +12,2 @@\n",
|
|
End: true,
|
|
},
|
|
"unknownEndsParsing": {
|
|
Line: "GIT binary file\n",
|
|
End: true,
|
|
},
|
|
"oldFileName": {
|
|
Line: "--- a/dir/file.txt\n",
|
|
OutputFile: &File{
|
|
OldName: "dir/file.txt",
|
|
},
|
|
},
|
|
"oldFileNameDevNull": {
|
|
InputFile: &File{
|
|
IsNew: true,
|
|
},
|
|
Line: "--- /dev/null\n",
|
|
OutputFile: &File{
|
|
IsNew: true,
|
|
},
|
|
},
|
|
"oldFileNameInconsistent": {
|
|
InputFile: &File{
|
|
OldName: "dir/foo.txt",
|
|
},
|
|
Line: "--- a/dir/bar.txt\n",
|
|
Err: true,
|
|
},
|
|
"oldFileNameExistingCreateMismatch": {
|
|
InputFile: &File{
|
|
OldName: "dir/foo.txt",
|
|
IsNew: true,
|
|
},
|
|
Line: "--- /dev/null\n",
|
|
Err: true,
|
|
},
|
|
"oldFileNameParsedCreateMismatch": {
|
|
InputFile: &File{
|
|
IsNew: true,
|
|
},
|
|
Line: "--- a/dir/file.txt\n",
|
|
Err: true,
|
|
},
|
|
"oldFileNameMissing": {
|
|
Line: "--- \n",
|
|
Err: true,
|
|
},
|
|
"newFileName": {
|
|
Line: "+++ b/dir/file.txt\n",
|
|
OutputFile: &File{
|
|
NewName: "dir/file.txt",
|
|
},
|
|
},
|
|
"newFileNameDevNull": {
|
|
InputFile: &File{
|
|
IsDelete: true,
|
|
},
|
|
Line: "+++ /dev/null\n",
|
|
OutputFile: &File{
|
|
IsDelete: true,
|
|
},
|
|
},
|
|
"newFileNameInconsistent": {
|
|
InputFile: &File{
|
|
NewName: "dir/foo.txt",
|
|
},
|
|
Line: "+++ b/dir/bar.txt\n",
|
|
Err: true,
|
|
},
|
|
"newFileNameExistingDeleteMismatch": {
|
|
InputFile: &File{
|
|
NewName: "dir/foo.txt",
|
|
IsDelete: true,
|
|
},
|
|
Line: "+++ /dev/null\n",
|
|
Err: true,
|
|
},
|
|
"newFileNameParsedDeleteMismatch": {
|
|
InputFile: &File{
|
|
IsDelete: true,
|
|
},
|
|
Line: "+++ b/dir/file.txt\n",
|
|
Err: true,
|
|
},
|
|
"newFileNameMissing": {
|
|
Line: "+++ \n",
|
|
Err: true,
|
|
},
|
|
"oldMode": {
|
|
Line: "old mode 100644\n",
|
|
OutputFile: &File{
|
|
OldMode: os.FileMode(0100644),
|
|
},
|
|
},
|
|
"invalidOldMode": {
|
|
Line: "old mode rw\n",
|
|
Err: true,
|
|
},
|
|
"newMode": {
|
|
Line: "new mode 100755\n",
|
|
OutputFile: &File{
|
|
NewMode: os.FileMode(0100755),
|
|
},
|
|
},
|
|
"invalidNewMode": {
|
|
Line: "new mode rwx\n",
|
|
Err: true,
|
|
},
|
|
"deletedFileMode": {
|
|
Line: "deleted file mode 100644\n",
|
|
DefaultName: "dir/file.txt",
|
|
OutputFile: &File{
|
|
OldName: "dir/file.txt",
|
|
OldMode: os.FileMode(0100644),
|
|
IsDelete: true,
|
|
},
|
|
},
|
|
"newFileMode": {
|
|
Line: "new file mode 100755\n",
|
|
DefaultName: "dir/file.txt",
|
|
OutputFile: &File{
|
|
NewName: "dir/file.txt",
|
|
NewMode: os.FileMode(0100755),
|
|
IsNew: true,
|
|
},
|
|
},
|
|
"copyFrom": {
|
|
Line: "copy from dir/file.txt\n",
|
|
OutputFile: &File{
|
|
OldName: "dir/file.txt",
|
|
IsCopy: true,
|
|
},
|
|
},
|
|
"copyTo": {
|
|
Line: "copy to dir/file.txt\n",
|
|
OutputFile: &File{
|
|
NewName: "dir/file.txt",
|
|
IsCopy: true,
|
|
},
|
|
},
|
|
"renameFrom": {
|
|
Line: "rename from dir/file.txt\n",
|
|
OutputFile: &File{
|
|
OldName: "dir/file.txt",
|
|
IsRename: true,
|
|
},
|
|
},
|
|
"renameTo": {
|
|
Line: "rename to dir/file.txt\n",
|
|
OutputFile: &File{
|
|
NewName: "dir/file.txt",
|
|
IsRename: true,
|
|
},
|
|
},
|
|
"similarityIndex": {
|
|
Line: "similarity index 88\n",
|
|
OutputFile: &File{
|
|
Score: 88,
|
|
},
|
|
},
|
|
"similarityIndexTooBig": {
|
|
Line: "similarity index 9001\n",
|
|
OutputFile: &File{
|
|
Score: 0,
|
|
},
|
|
},
|
|
"similarityIndexInvalid": {
|
|
Line: "similarity index 12ab\n",
|
|
Err: true,
|
|
},
|
|
"indexFullSHA1AndMode": {
|
|
Line: "index 79c6d7f7b7e76c75b3d238f12fb1323f2333ba14..04fab916d8f938173cbb8b93469855f0e838f098 100644\n",
|
|
OutputFile: &File{
|
|
OldOID: "79c6d7f7b7e76c75b3d238f12fb1323f2333ba14",
|
|
NewOID: "04fab916d8f938173cbb8b93469855f0e838f098",
|
|
OldMode: os.FileMode(0100644),
|
|
},
|
|
},
|
|
"indexFullSHA1NoMode": {
|
|
Line: "index 79c6d7f7b7e76c75b3d238f12fb1323f2333ba14..04fab916d8f938173cbb8b93469855f0e838f098\n",
|
|
OutputFile: &File{
|
|
OldOID: "79c6d7f7b7e76c75b3d238f12fb1323f2333ba14",
|
|
NewOID: "04fab916d8f938173cbb8b93469855f0e838f098",
|
|
},
|
|
},
|
|
"indexAbbrevSHA1AndMode": {
|
|
Line: "index 79c6d7..04fab9 100644\n",
|
|
OutputFile: &File{
|
|
OldOID: "79c6d7",
|
|
NewOID: "04fab9",
|
|
OldMode: os.FileMode(0100644),
|
|
},
|
|
},
|
|
"indexInvalid": {
|
|
Line: "index 79c6d7f7b7e76c75b3d238f12fb1323f2333ba14\n",
|
|
Err: true,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
var f File
|
|
if test.InputFile != nil {
|
|
f = *test.InputFile
|
|
}
|
|
|
|
end, err := parseGitHeaderData(&f, test.Line, test.DefaultName)
|
|
if test.Err {
|
|
if err == nil {
|
|
t.Fatalf("expected error parsing header data, but got nil")
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error parsing header data: %v", err)
|
|
}
|
|
|
|
if test.OutputFile != nil && !reflect.DeepEqual(test.OutputFile, &f) {
|
|
t.Errorf("incorrect output:\nexpected: %+v\nactual: %+v", test.OutputFile, &f)
|
|
}
|
|
if end != test.End {
|
|
t.Errorf("incorrect end state, expected %t, actual %t", test.End, end)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseGitHeaderName(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Input string
|
|
Output string
|
|
Err bool
|
|
}{
|
|
"twoMatchingNames": {
|
|
Input: "a/dir/file.txt b/dir/file.txt",
|
|
Output: "dir/file.txt",
|
|
},
|
|
"twoDifferentNames": {
|
|
Input: "a/dir/foo.txt b/dir/bar.txt",
|
|
Output: "",
|
|
},
|
|
"missingSecondName": {
|
|
Input: "a/dir/foo.txt",
|
|
Err: true,
|
|
},
|
|
"invalidName": {
|
|
Input: `"a/dir/file.txt b/dir/file.txt`,
|
|
Err: true,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
output, err := parseGitHeaderName(test.Input)
|
|
if test.Err {
|
|
if err == nil {
|
|
t.Fatalf("expected error parsing header name, but got nil")
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error parsing header name: %v", err)
|
|
}
|
|
|
|
if output != test.Output {
|
|
t.Errorf("incorrect output: expected %q, actual %q", test.Output, output)
|
|
}
|
|
})
|
|
}
|
|
}
|