From 8753644cc03ce300c6f83fbad75fd8b924570d8f Mon Sep 17 00:00:00 2001 From: Billy Keyes Date: Sun, 31 Mar 2019 21:16:34 -0700 Subject: [PATCH] Add tests for single fragment parsing To make output better, also add some (temporary?) String() functions and fix an assumption about the smallest fragment header I discovered was wrong while looking at sample patches. --- gitdiff/file_header.go | 2 +- gitdiff/file_header_test.go | 8 +- gitdiff/gitdiff.go | 16 +++ gitdiff/parser.go | 17 ++- gitdiff/parser_test.go | 277 +++++++++++++++++++++++++++++++++--- 5 files changed, 289 insertions(+), 31 deletions(-) diff --git a/gitdiff/file_header.go b/gitdiff/file_header.go index 4ff5ff5..781a448 100644 --- a/gitdiff/file_header.go +++ b/gitdiff/file_header.go @@ -61,7 +61,7 @@ func (p *parser) ParseGitFileHeader() (*File, error) { } func (p *parser) ParseTraditionalFileHeader() (*File, error) { - const shortestValidFragHeader = "@@ -0,0 +1 @@\n" + const shortestValidFragHeader = "@@ -1 +1 @@\n" const ( oldPrefix = "--- " newPrefix = "+++ " diff --git a/gitdiff/file_header_test.go b/gitdiff/file_header_test.go index 46581c3..5ea3e79 100644 --- a/gitdiff/file_header_test.go +++ b/gitdiff/file_header_test.go @@ -1,10 +1,8 @@ package gitdiff import ( - "bufio" "os" "reflect" - "strings" "testing" ) @@ -150,8 +148,7 @@ index deadbeef for name, test := range tests { t.Run(name, func(t *testing.T) { - p := &parser{r: bufio.NewReader(strings.NewReader(test.Input))} - p.Next() + p := newTestParser(test.Input, true) f, err := p.ParseGitFileHeader() if test.Err { @@ -256,8 +253,7 @@ context line for name, test := range tests { t.Run(name, func(t *testing.T) { - p := &parser{r: bufio.NewReader(strings.NewReader(test.Input))} - p.Next() + p := newTestParser(test.Input, true) f, err := p.ParseTraditionalFileHeader() if test.Err { diff --git a/gitdiff/gitdiff.go b/gitdiff/gitdiff.go index bba48a9..cd6f51d 100644 --- a/gitdiff/gitdiff.go +++ b/gitdiff/gitdiff.go @@ -51,6 +51,10 @@ type FragmentLine struct { Line string } +func (fl FragmentLine) String() string { + return fl.Op.String() + fl.Line +} + // LineOp describes the type of a fragment line: context, added, or removed. type LineOp int @@ -63,6 +67,18 @@ const ( OpAdd ) +func (op LineOp) String() string { + switch op { + case OpContext: + return " " + case OpDelete: + return "-" + case OpAdd: + return "+" + } + return "?" +} + // Header returns the cannonical header of this fragment. func (f *Fragment) Header() string { return fmt.Sprintf("@@ -%d,%d +%d,%d @@ %s", f.OldPosition, f.OldLines, f.NewPosition, f.NewLines, f.Comment) diff --git a/gitdiff/parser.go b/gitdiff/parser.go index 1e134e3..6ce0960 100644 --- a/gitdiff/parser.go +++ b/gitdiff/parser.go @@ -245,8 +245,11 @@ func (p *parser) ParseTextChunk(frag *Fragment) error { oldLines, newLines := frag.OldLines, frag.NewLines for { line := p.Line(0) - switch line[0] { + op, data := line[0], line[1:] + + switch op { case '\n': + data = "\n" fallthrough // newer GNU diff versions create empty context lines case ' ': oldLines-- @@ -256,25 +259,25 @@ func (p *parser) ParseTextChunk(frag *Fragment) error { } else { frag.TrailingContext++ } - frag.Lines = append(frag.Lines, FragmentLine{OpContext, line[1:]}) + frag.Lines = append(frag.Lines, FragmentLine{OpContext, data}) case '-': oldLines-- frag.LinesDeleted++ frag.TrailingContext = 0 - frag.Lines = append(frag.Lines, FragmentLine{OpDelete, line[1:]}) + frag.Lines = append(frag.Lines, FragmentLine{OpDelete, data}) case '+': newLines-- frag.LinesAdded++ frag.TrailingContext = 0 - frag.Lines = append(frag.Lines, FragmentLine{OpAdd, line[1:]}) + frag.Lines = append(frag.Lines, FragmentLine{OpAdd, data}) default: // this may appear in middle of fragment if it's for a deleted line if isNoNewlineLine(line) { - last := len(frag.Lines) - 1 - frag.Lines[last].Line = strings.TrimSuffix(frag.Lines[last].Line, "\n") + last := &frag.Lines[len(frag.Lines)-1] + last.Line = strings.TrimSuffix(last.Line, "\n") break } - return p.Errorf(0, "invalid line operation: %q", line[0]) + return p.Errorf(0, "invalid line operation: %q", op) } next := p.Line(1) diff --git a/gitdiff/parser_test.go b/gitdiff/parser_test.go index 3a7b8fb..ff66712 100644 --- a/gitdiff/parser_test.go +++ b/gitdiff/parser_test.go @@ -11,13 +11,8 @@ import ( func TestLineOperations(t *testing.T) { const content = "the first line\nthe second line\nthe third line\n" - newParser := func() *parser { - return &parser{r: bufio.NewReader(strings.NewReader(content))} - } - t.Run("read", func(t *testing.T) { - p := newParser() - + p := newTestParser(content, false) if err := p.Next(); err != nil { t.Fatalf("error advancing parser: %v", err) } @@ -72,8 +67,7 @@ func TestLineOperations(t *testing.T) { }) t.Run("peek", func(t *testing.T) { - p := newParser() - + p := newTestParser(content, false) if err := p.Next(); err != nil { t.Fatalf("error advancing parser: %v", err) } @@ -94,7 +88,7 @@ func TestLineOperations(t *testing.T) { }) t.Run("emptyInput", func(t *testing.T) { - p := &parser{r: bufio.NewReader(strings.NewReader(""))} + p := newTestParser("", false) if err := p.Next(); err != io.EOF { t.Fatalf("expected EOF, but got: %v", err) } @@ -147,8 +141,7 @@ context line for name, test := range tests { t.Run(name, func(t *testing.T) { - p := &parser{r: bufio.NewReader(strings.NewReader(test.Input))} - p.Next() + p := newTestParser(test.Input, true) if err := test.Parse(p); err != nil { t.Fatalf("unexpected error while parsing: %v", err) @@ -168,10 +161,10 @@ func TestParseTextFragmentHeader(t *testing.T) { Err bool }{ "shortest": { - Input: "@@ -0,0 +1 @@\n", + Input: "@@ -1 +1 @@\n", Output: &Fragment{ - OldPosition: 0, - OldLines: 0, + OldPosition: 1, + OldLines: 1, NewPosition: 1, NewLines: 1, }, @@ -207,8 +200,7 @@ func TestParseTextFragmentHeader(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - p := &parser{r: bufio.NewReader(strings.NewReader(test.Input))} - p.Next() + p := newTestParser(test.Input, true) frag, err := p.ParseTextFragmentHeader() if test.Err { @@ -222,8 +214,259 @@ func TestParseTextFragmentHeader(t *testing.T) { } if !reflect.DeepEqual(test.Output, frag) { - t.Fatalf("incorrect fragment\nexpected: %+v\nactual: %+v", test.Output, frag) + t.Errorf("incorrect fragment\nexpected: %+v\nactual: %+v", test.Output, frag) } }) } } + +func TestParseTextChunk(t *testing.T) { + tests := map[string]struct { + Input string + Fragment Fragment + + Output *Fragment + Err bool + }{ + "addWithContext": { + Input: ` context line ++new line 1 ++new line 2 + context line +`, + Fragment: Fragment{ + OldLines: 2, + NewLines: 4, + }, + Output: &Fragment{ + OldLines: 2, + NewLines: 4, + Lines: []FragmentLine{ + {OpContext, "context line\n"}, + {OpAdd, "new line 1\n"}, + {OpAdd, "new line 2\n"}, + {OpContext, "context line\n"}, + }, + LinesAdded: 2, + LeadingContext: 1, + TrailingContext: 1, + }, + }, + "deleteWithContext": { + Input: ` context line +-old line 1 +-old line 2 + context line +`, + Fragment: Fragment{ + OldLines: 4, + NewLines: 2, + }, + Output: &Fragment{ + OldLines: 4, + NewLines: 2, + Lines: []FragmentLine{ + {OpContext, "context line\n"}, + {OpDelete, "old line 1\n"}, + {OpDelete, "old line 2\n"}, + {OpContext, "context line\n"}, + }, + LinesDeleted: 2, + LeadingContext: 1, + TrailingContext: 1, + }, + }, + "replaceWithContext": { + Input: ` context line +-old line 1 ++new line 1 + context line +`, + Fragment: Fragment{ + OldLines: 3, + NewLines: 3, + }, + Output: &Fragment{ + OldLines: 3, + NewLines: 3, + Lines: []FragmentLine{ + {OpContext, "context line\n"}, + {OpDelete, "old line 1\n"}, + {OpAdd, "new line 1\n"}, + {OpContext, "context line\n"}, + }, + LinesDeleted: 1, + LinesAdded: 1, + LeadingContext: 1, + TrailingContext: 1, + }, + }, + "deleteFinalNewline": { + Input: ` context line +-old line 1 ++new line 1 +\ No newline at end of file +`, + Fragment: Fragment{ + OldLines: 2, + NewLines: 2, + }, + Output: &Fragment{ + OldLines: 2, + NewLines: 2, + Lines: []FragmentLine{ + {OpContext, "context line\n"}, + {OpDelete, "old line 1\n"}, + {OpAdd, "new line 1"}, + }, + LinesDeleted: 1, + LinesAdded: 1, + LeadingContext: 1, + }, + }, + "addFinalNewline": { + Input: ` context line +-old line 1 +\ No newline at end of file ++new line 1 +`, + Fragment: Fragment{ + OldLines: 2, + NewLines: 2, + }, + Output: &Fragment{ + OldLines: 2, + NewLines: 2, + Lines: []FragmentLine{ + {OpContext, "context line\n"}, + {OpDelete, "old line 1"}, + {OpAdd, "new line 1\n"}, + }, + LinesDeleted: 1, + LinesAdded: 1, + LeadingContext: 1, + }, + }, + "addAll": { + Input: `+new line 1 ++new line 2 ++new line 3 +`, + Fragment: Fragment{ + OldLines: 0, + NewLines: 3, + }, + Output: &Fragment{ + OldLines: 0, + NewLines: 3, + Lines: []FragmentLine{ + {OpAdd, "new line 1\n"}, + {OpAdd, "new line 2\n"}, + {OpAdd, "new line 3\n"}, + }, + LinesAdded: 3, + }, + }, + "deleteAll": { + Input: `-old line 1 +-old line 2 +-old line 3 +`, + Fragment: Fragment{ + OldLines: 3, + NewLines: 0, + }, + Output: &Fragment{ + OldLines: 3, + NewLines: 0, + Lines: []FragmentLine{ + {OpDelete, "old line 1\n"}, + {OpDelete, "old line 2\n"}, + {OpDelete, "old line 3\n"}, + }, + LinesDeleted: 3, + }, + }, + "emptyContextLine": { + Input: ` context line + ++new line + context line +`, + Fragment: Fragment{ + OldLines: 3, + NewLines: 4, + }, + Output: &Fragment{ + OldLines: 3, + NewLines: 4, + Lines: []FragmentLine{ + {OpContext, "context line\n"}, + {OpContext, "\n"}, + {OpAdd, "new line\n"}, + {OpContext, "context line\n"}, + }, + LinesAdded: 1, + LeadingContext: 2, + TrailingContext: 1, + }, + }, + "emptyChunk": { + Input: "", + Err: true, + }, + "invalidOperation": { + Input: ` context line +?wat line + context line +`, + Fragment: Fragment{ + OldLines: 3, + NewLines: 3, + }, + Err: true, + }, + "unbalancedHeader": { + Input: ` context line +-old line 1 ++new line 1 + context line +`, + Fragment: Fragment{ + OldLines: 2, + NewLines: 5, + }, + Err: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + p := newTestParser(test.Input, true) + + frag := test.Fragment + err := p.ParseTextChunk(&frag) + if test.Err { + if err == nil { + t.Fatalf("expected error parsing text chunk, but got nil") + } + return + } + if err != nil { + t.Fatalf("error parsing text chunk: %v", err) + } + + if !reflect.DeepEqual(test.Output, &frag) { + t.Errorf("incorrect fragment\nexpected: %+v\nactual: %+v", test.Output, &frag) + } + }) + } +} + +func newTestParser(input string, init bool) *parser { + p := &parser{r: bufio.NewReader(strings.NewReader(input))} + if init { + _ = p.Next() + } + return p +}