From 25bee8cf2fa8310deebe6fe6ebbf16bc83236abe Mon Sep 17 00:00:00 2001 From: Billy Keyes Date: Sun, 5 Jan 2020 20:36:01 -0800 Subject: [PATCH] Add validation function for text fragment Applying a fragment requires the content to match the stored counts, so there must be a way to check this. Parsed fragments should always be valid, but manually created or modified fragments may be invalid. --- gitdiff/gitdiff.go | 66 ++++++++++++++++ gitdiff/gitdiff_test.go | 161 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 gitdiff/gitdiff_test.go diff --git a/gitdiff/gitdiff.go b/gitdiff/gitdiff.go index 8e886eb..a728276 100644 --- a/gitdiff/gitdiff.go +++ b/gitdiff/gitdiff.go @@ -61,6 +61,72 @@ func (f *TextFragment) Header() string { return fmt.Sprintf("@@ -%d,%d +%d,%d @@ %s", f.OldPosition, f.OldLines, f.NewPosition, f.NewLines, f.Comment) } +// Validate checks that the fragment is self-consistent and appliable. Validate +// returns an error if and only if the fragment is invalid. +func (f *TextFragment) Validate() error { + var ( + oldLines, newLines int64 + leadingContext, trailingContext int64 + contextLines, addedLines, deletedLines int64 + ) + + // count the types of lines in the fragment content + for i, line := range f.Lines { + switch line.Op { + case OpContext: + oldLines++ + newLines++ + contextLines++ + if addedLines == 0 && deletedLines == 0 { + leadingContext++ + } else { + trailingContext++ + } + case OpAdd: + newLines++ + addedLines++ + trailingContext = 0 + case OpDelete: + oldLines++ + deletedLines++ + trailingContext = 0 + default: + return fmt.Errorf("unknown operator %q on line %d", line.Op, i+1) + } + } + + // check the actual counts against the reported counts + if oldLines != f.OldLines { + return lineCountErr("old", oldLines, f.OldLines) + } + if newLines != f.NewLines { + return lineCountErr("new", newLines, f.NewLines) + } + if leadingContext != f.LeadingContext { + return lineCountErr("leading context", leadingContext, f.LeadingContext) + } + if trailingContext != f.TrailingContext { + return lineCountErr("trailing context", trailingContext, f.TrailingContext) + } + if addedLines != f.LinesAdded { + return lineCountErr("added", addedLines, f.LinesAdded) + } + if deletedLines != f.LinesDeleted { + return lineCountErr("deleted", deletedLines, f.LinesDeleted) + } + + // if a file is being created, it can only contain additions + if f.OldPosition == 0 && f.OldLines != 0 { + return fmt.Errorf("file creation fragment contains context or deletion lines") + } + + return nil +} + +func lineCountErr(kind string, actual, reported int64) error { + return fmt.Errorf("fragment contains %d %s lines but reports %d", actual, kind, reported) +} + // Line is a line in a text fragment. type Line struct { Op LineOp diff --git a/gitdiff/gitdiff_test.go b/gitdiff/gitdiff_test.go new file mode 100644 index 0000000..4f63d51 --- /dev/null +++ b/gitdiff/gitdiff_test.go @@ -0,0 +1,161 @@ +package gitdiff + +import ( + "strings" + "testing" +) + +func TestTextFragmentValidate(t *testing.T) { + tests := map[string]struct { + Fragment TextFragment + Err string + }{ + "oldLines": { + Fragment: TextFragment{ + OldPosition: 1, + OldLines: 3, + NewPosition: 1, + NewLines: 2, + LeadingContext: 1, + TrailingContext: 0, + LinesAdded: 1, + LinesDeleted: 1, + Lines: []Line{ + {Op: OpContext, Line: "line 1\n"}, + {Op: OpDelete, Line: "old line 2\n"}, + {Op: OpAdd, Line: "new line 2\n"}, + }, + }, + Err: "2 old lines", + }, + "newLines": { + Fragment: TextFragment{ + OldPosition: 1, + OldLines: 2, + NewPosition: 1, + NewLines: 3, + LeadingContext: 1, + TrailingContext: 0, + LinesAdded: 1, + LinesDeleted: 1, + Lines: []Line{ + {Op: OpContext, Line: "line 1\n"}, + {Op: OpDelete, Line: "old line 2\n"}, + {Op: OpAdd, Line: "new line 2\n"}, + }, + }, + Err: "2 new lines", + }, + "leadingContext": { + Fragment: TextFragment{ + OldPosition: 1, + OldLines: 2, + NewPosition: 1, + NewLines: 2, + LeadingContext: 0, + TrailingContext: 0, + LinesAdded: 1, + LinesDeleted: 1, + Lines: []Line{ + {Op: OpContext, Line: "line 1\n"}, + {Op: OpDelete, Line: "old line 2\n"}, + {Op: OpAdd, Line: "new line 2\n"}, + }, + }, + Err: "1 leading context lines", + }, + "trailingContext": { + Fragment: TextFragment{ + OldPosition: 1, + OldLines: 4, + NewPosition: 1, + NewLines: 3, + LeadingContext: 1, + TrailingContext: 1, + LinesAdded: 1, + LinesDeleted: 2, + Lines: []Line{ + {Op: OpContext, Line: "line 1\n"}, + {Op: OpDelete, Line: "old line 2\n"}, + {Op: OpAdd, Line: "new line 2\n"}, + {Op: OpContext, Line: "line 3\n"}, + {Op: OpDelete, Line: "old line 4\n"}, + }, + }, + Err: "0 trailing context lines", + }, + "linesAdded": { + Fragment: TextFragment{ + OldPosition: 1, + OldLines: 4, + NewPosition: 1, + NewLines: 3, + LeadingContext: 1, + TrailingContext: 0, + LinesAdded: 2, + LinesDeleted: 2, + Lines: []Line{ + {Op: OpContext, Line: "line 1\n"}, + {Op: OpDelete, Line: "old line 2\n"}, + {Op: OpAdd, Line: "new line 2\n"}, + {Op: OpContext, Line: "line 3\n"}, + {Op: OpDelete, Line: "old line 4\n"}, + }, + }, + Err: "1 added lines", + }, + "linesDeleted": { + Fragment: TextFragment{ + OldPosition: 1, + OldLines: 4, + NewPosition: 1, + NewLines: 3, + LeadingContext: 1, + TrailingContext: 0, + LinesAdded: 1, + LinesDeleted: 1, + Lines: []Line{ + {Op: OpContext, Line: "line 1\n"}, + {Op: OpDelete, Line: "old line 2\n"}, + {Op: OpAdd, Line: "new line 2\n"}, + {Op: OpContext, Line: "line 3\n"}, + {Op: OpDelete, Line: "old line 4\n"}, + }, + }, + Err: "2 deleted lines", + }, + "fileCreation": { + Fragment: TextFragment{ + OldPosition: 0, + OldLines: 2, + NewPosition: 1, + NewLines: 1, + LeadingContext: 0, + TrailingContext: 0, + LinesAdded: 1, + LinesDeleted: 2, + Lines: []Line{ + {Op: OpDelete, Line: "old line 1\n"}, + {Op: OpDelete, Line: "old line 2\n"}, + {Op: OpAdd, Line: "new line\n"}, + }, + }, + Err: "creation fragment", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + err := test.Fragment.Validate() + if test.Err == "" && err != nil { + t.Fatalf("unexpected validation error: %v", err) + } + if test.Err != "" && err == nil { + t.Fatal("expected validation error, but got nil") + } + if !strings.Contains(err.Error(), test.Err) { + t.Fatalf("incorrect validation error: %q is not in %q", test.Err, err.Error()) + } + }) + } +}