fork of go-gitdiff with jj support
at v0.3.0 11 kB view raw
1package gitdiff 2 3import ( 4 "bytes" 5 "encoding/binary" 6 "encoding/json" 7 "io" 8 "os" 9 "reflect" 10 "testing" 11) 12 13func TestLineOperations(t *testing.T) { 14 const content = "the first line\nthe second line\nthe third line\n" 15 16 t.Run("read", func(t *testing.T) { 17 p := newTestParser(content, false) 18 19 for i, expected := range []string{ 20 "the first line\n", 21 "the second line\n", 22 "the third line\n", 23 } { 24 if err := p.Next(); err != nil { 25 t.Fatalf("error advancing parser after line %d: %v", i, err) 26 } 27 if p.lineno != int64(i+1) { 28 t.Fatalf("incorrect line number: expected %d, actual: %d", i+1, p.lineno) 29 } 30 31 line := p.Line(0) 32 if line != expected { 33 t.Fatalf("incorrect line %d: expected %q, was %q", i+1, expected, line) 34 } 35 } 36 37 // reading after the last line should return EOF 38 if err := p.Next(); err != io.EOF { 39 t.Fatalf("expected EOF after end, but got: %v", err) 40 } 41 if p.lineno != 4 { 42 t.Fatalf("incorrect line number: expected %d, actual: %d", 4, p.lineno) 43 } 44 45 // reading again returns EOF again and does not advance the line 46 if err := p.Next(); err != io.EOF { 47 t.Fatalf("expected EOF after end, but got: %v", err) 48 } 49 if p.lineno != 4 { 50 t.Fatalf("incorrect line number: expected %d, actual: %d", 4, p.lineno) 51 } 52 }) 53 54 t.Run("peek", func(t *testing.T) { 55 p := newTestParser(content, false) 56 if err := p.Next(); err != nil { 57 t.Fatalf("error advancing parser: %v", err) 58 } 59 60 line := p.Line(1) 61 if line != "the second line\n" { 62 t.Fatalf("incorrect peek line: %s", line) 63 } 64 65 if err := p.Next(); err != nil { 66 t.Fatalf("error advancing parser after peek: %v", err) 67 } 68 69 line = p.Line(0) 70 if line != "the second line\n" { 71 t.Fatalf("incorrect read line: %s", line) 72 } 73 }) 74 75 t.Run("emptyInput", func(t *testing.T) { 76 p := newTestParser("", false) 77 if err := p.Next(); err != io.EOF { 78 t.Fatalf("expected EOF on first Next(), but got: %v", err) 79 } 80 }) 81} 82 83func TestParserInvariant_Advancement(t *testing.T) { 84 tests := map[string]struct { 85 Input string 86 Parse func(p *parser) error 87 EndLine string 88 }{ 89 "ParseGitFileHeader": { 90 Input: `diff --git a/dir/file.txt b/dir/file.txt 91index 9540595..30e6333 100644 92--- a/dir/file.txt 93+++ b/dir/file.txt 94@@ -1,2 +1,3 @@ 95context line 96`, 97 Parse: func(p *parser) error { 98 _, err := p.ParseGitFileHeader() 99 return err 100 }, 101 EndLine: "@@ -1,2 +1,3 @@\n", 102 }, 103 "ParseTraditionalFileHeader": { 104 Input: `--- dir/file.txt 105+++ dir/file.txt 106@@ -1,2 +1,3 @@ 107context line 108`, 109 Parse: func(p *parser) error { 110 _, err := p.ParseTraditionalFileHeader() 111 return err 112 }, 113 EndLine: "@@ -1,2 +1,3 @@\n", 114 }, 115 "ParseTextFragmentHeader": { 116 Input: `@@ -1,2 +1,3 @@ 117context line 118`, 119 Parse: func(p *parser) error { 120 _, err := p.ParseTextFragmentHeader() 121 return err 122 }, 123 EndLine: "context line\n", 124 }, 125 "ParseTextChunk": { 126 Input: ` context line 127-old line 128+new line 129 context line 130@@ -1 +1 @@ 131`, 132 Parse: func(p *parser) error { 133 return p.ParseTextChunk(&TextFragment{OldLines: 3, NewLines: 3}) 134 }, 135 EndLine: "@@ -1 +1 @@\n", 136 }, 137 "ParseTextFragments": { 138 Input: `@@ -1,2 +1,2 @@ 139 context line 140-old line 141+new line 142@@ -1,2 +1,2 @@ 143-old line 144+new line 145 context line 146diff --git a/file.txt b/file.txt 147`, 148 Parse: func(p *parser) error { 149 _, err := p.ParseTextFragments(&File{}) 150 return err 151 }, 152 EndLine: "diff --git a/file.txt b/file.txt\n", 153 }, 154 "ParseNextFileHeader": { 155 Input: `not a header 156diff --git a/file.txt b/file.txt 157--- a/file.txt 158+++ b/file.txt 159@@ -1,2 +1,2 @@ 160`, 161 Parse: func(p *parser) error { 162 _, _, err := p.ParseNextFileHeader() 163 return err 164 }, 165 EndLine: "@@ -1,2 +1,2 @@\n", 166 }, 167 "ParseBinaryMarker": { 168 Input: `Binary files differ 169diff --git a/file.txt b/file.txt 170`, 171 Parse: func(p *parser) error { 172 _, _, err := p.ParseBinaryMarker() 173 return err 174 }, 175 EndLine: "diff --git a/file.txt b/file.txt\n", 176 }, 177 "ParseBinaryFragmentHeader": { 178 Input: `literal 0 179HcmV?d00001 180`, 181 Parse: func(p *parser) error { 182 _, err := p.ParseBinaryFragmentHeader() 183 return err 184 }, 185 EndLine: "HcmV?d00001\n", 186 }, 187 "ParseBinaryChunk": { 188 Input: "TcmZQzU|?i`" + `U?w2V48*Je09XJG 189 190literal 0 191`, 192 Parse: func(p *parser) error { 193 return p.ParseBinaryChunk(&BinaryFragment{Size: 20}) 194 }, 195 EndLine: "literal 0\n", 196 }, 197 "ParseBinaryFragments": { 198 Input: `GIT binary patch 199literal 40 200gcmZQzU|?i` + "`" + `U?w2V48*KJ%mKu_Kr9NxN<eH500b)lkN^Mx 201 202literal 0 203HcmV?d00001 204 205diff --git a/file.txt b/file.txt 206`, 207 Parse: func(p *parser) error { 208 _, err := p.ParseBinaryFragments(&File{}) 209 return err 210 }, 211 EndLine: "diff --git a/file.txt b/file.txt\n", 212 }, 213 } 214 215 for name, test := range tests { 216 t.Run(name, func(t *testing.T) { 217 p := newTestParser(test.Input, true) 218 219 if err := test.Parse(p); err != nil { 220 t.Fatalf("unexpected error while parsing: %v", err) 221 } 222 223 if test.EndLine != p.Line(0) { 224 t.Errorf("incorrect position after parsing\nexpected: %q\n actual: %q", test.EndLine, p.Line(0)) 225 } 226 }) 227 } 228} 229 230func TestParseNextFileHeader(t *testing.T) { 231 tests := map[string]struct { 232 Input string 233 Output *File 234 Preamble string 235 Err bool 236 }{ 237 "gitHeader": { 238 Input: `commit 1acbae563cd6ef5750a82ee64e116c6eb065cb94 239Author: Morton Haypenny <mhaypenny@example.com> 240Date: Tue Apr 2 22:30:00 2019 -0700 241 242 This is a sample commit message. 243 244diff --git a/file.txt b/file.txt 245index cc34da1..1acbae5 100644 246--- a/file.txt 247+++ b/file.txt 248@@ -1,3 +1,4 @@ 249`, 250 Output: &File{ 251 OldName: "file.txt", 252 NewName: "file.txt", 253 OldMode: os.FileMode(0100644), 254 OldOIDPrefix: "cc34da1", 255 NewOIDPrefix: "1acbae5", 256 }, 257 Preamble: `commit 1acbae563cd6ef5750a82ee64e116c6eb065cb94 258Author: Morton Haypenny <mhaypenny@example.com> 259Date: Tue Apr 2 22:30:00 2019 -0700 260 261 This is a sample commit message. 262 263`, 264 }, 265 "traditionalHeader": { 266 Input: ` 267--- file.txt 2019-04-01 22:58:14.833597918 -0700 268+++ file.txt 2019-04-01 22:58:14.833597918 -0700 269@@ -1,3 +1,4 @@ 270`, 271 Output: &File{ 272 OldName: "file.txt", 273 NewName: "file.txt", 274 }, 275 Preamble: "\n", 276 }, 277 "noHeaders": { 278 Input: ` 279this is a line 280this is another line 281--- could this be a header? 282nope, it's just some dashes 283`, 284 Output: nil, 285 Preamble: "", 286 }, 287 "detatchedFragmentLike": { 288 Input: ` 289a wild fragment appears? 290@@ -1,3 +1,4 ~1,5 @@ 291`, 292 Output: nil, 293 }, 294 "detatchedFragment": { 295 Input: ` 296a wild fragment appears? 297@@ -1,3 +1,4 @@ 298`, 299 Err: true, 300 }, 301 } 302 303 for name, test := range tests { 304 t.Run(name, func(t *testing.T) { 305 p := newTestParser(test.Input, true) 306 307 f, pre, err := p.ParseNextFileHeader() 308 if test.Err { 309 if err == nil || err == io.EOF { 310 t.Fatalf("expected error parsing next file header, but got %v", err) 311 } 312 return 313 } 314 if err != nil { 315 t.Fatalf("unexpected error parsing next file header: %v", err) 316 } 317 318 if test.Preamble != pre { 319 t.Errorf("incorrect preamble\nexpected: %q\n actual: %q", test.Preamble, pre) 320 } 321 if !reflect.DeepEqual(test.Output, f) { 322 t.Errorf("incorrect file\nexpected: %+v\n actual: %+v", test.Output, f) 323 } 324 }) 325 } 326} 327 328func TestParse(t *testing.T) { 329 textFragments := []*TextFragment{ 330 { 331 OldPosition: 3, 332 OldLines: 6, 333 NewPosition: 3, 334 NewLines: 8, 335 Comment: "fragment 1", 336 Lines: []Line{ 337 {OpContext, "context line\n"}, 338 {OpDelete, "old line 1\n"}, 339 {OpDelete, "old line 2\n"}, 340 {OpContext, "context line\n"}, 341 {OpAdd, "new line 1\n"}, 342 {OpAdd, "new line 2\n"}, 343 {OpAdd, "new line 3\n"}, 344 {OpContext, "context line\n"}, 345 {OpDelete, "old line 3\n"}, 346 {OpAdd, "new line 4\n"}, 347 {OpAdd, "new line 5\n"}, 348 }, 349 LinesAdded: 5, 350 LinesDeleted: 3, 351 LeadingContext: 1, 352 }, 353 { 354 OldPosition: 31, 355 OldLines: 2, 356 NewPosition: 33, 357 NewLines: 2, 358 Comment: "fragment 2", 359 Lines: []Line{ 360 {OpContext, "context line\n"}, 361 {OpDelete, "old line 4\n"}, 362 {OpAdd, "new line 6\n"}, 363 }, 364 LinesAdded: 1, 365 LinesDeleted: 1, 366 LeadingContext: 1, 367 }, 368 } 369 370 textPreamble := `commit 5d9790fec7d95aa223f3d20936340bf55ff3dcbe 371Author: Morton Haypenny <mhaypenny@example.com> 372Date: Tue Apr 2 22:55:40 2019 -0700 373 374 A file with multiple fragments. 375 376 The content is arbitrary. 377 378` 379 380 binaryPreamble := `commit 5d9790fec7d95aa223f3d20936340bf55ff3dcbe 381Author: Morton Haypenny <mhaypenny@example.com> 382Date: Tue Apr 2 22:55:40 2019 -0700 383 384 A binary file with the first 10 fibonacci numbers. 385 386` 387 tests := map[string]struct { 388 InputFile string 389 Output []*File 390 Preamble string 391 Err bool 392 }{ 393 "oneFile": { 394 InputFile: "testdata/one_file.patch", 395 Output: []*File{ 396 { 397 OldName: "dir/file1.txt", 398 NewName: "dir/file1.txt", 399 OldMode: os.FileMode(0100644), 400 OldOIDPrefix: "ebe9fa54", 401 NewOIDPrefix: "fe103e1d", 402 TextFragments: textFragments, 403 }, 404 }, 405 Preamble: textPreamble, 406 }, 407 "twoFiles": { 408 InputFile: "testdata/two_files.patch", 409 Output: []*File{ 410 { 411 OldName: "dir/file1.txt", 412 NewName: "dir/file1.txt", 413 OldMode: os.FileMode(0100644), 414 OldOIDPrefix: "ebe9fa54", 415 NewOIDPrefix: "fe103e1d", 416 TextFragments: textFragments, 417 }, 418 { 419 OldName: "dir/file2.txt", 420 NewName: "dir/file2.txt", 421 OldMode: os.FileMode(0100644), 422 OldOIDPrefix: "417ebc70", 423 NewOIDPrefix: "67514b7f", 424 TextFragments: textFragments, 425 }, 426 }, 427 Preamble: textPreamble, 428 }, 429 "newBinaryFile": { 430 InputFile: "testdata/new_binary_file.patch", 431 Output: []*File{ 432 { 433 OldName: "", 434 NewName: "dir/ten.bin", 435 NewMode: os.FileMode(0100644), 436 OldOIDPrefix: "0000000000000000000000000000000000000000", 437 NewOIDPrefix: "77b068ba48c356156944ea714740d0d5ca07bfec", 438 IsNew: true, 439 IsBinary: true, 440 BinaryFragment: &BinaryFragment{ 441 Method: BinaryPatchLiteral, 442 Size: 40, 443 Data: fib(10, binary.BigEndian), 444 }, 445 ReverseBinaryFragment: &BinaryFragment{ 446 Method: BinaryPatchLiteral, 447 Size: 0, 448 Data: []byte{}, 449 }, 450 }, 451 }, 452 Preamble: binaryPreamble, 453 }, 454 } 455 456 for name, test := range tests { 457 t.Run(name, func(t *testing.T) { 458 f, err := os.Open(test.InputFile) 459 if err != nil { 460 t.Fatalf("unexpected error opening input file: %v", err) 461 } 462 463 files, pre, err := Parse(f) 464 if test.Err { 465 if err == nil || err == io.EOF { 466 t.Fatalf("expected error parsing patch, but got %v", err) 467 } 468 return 469 } 470 if err != nil { 471 t.Fatalf("unexpected error parsing patch: %v", err) 472 } 473 474 if len(test.Output) != len(files) { 475 t.Fatalf("incorrect number of parsed files: expected %d, actual %d", len(test.Output), len(files)) 476 } 477 if test.Preamble != pre { 478 t.Errorf("incorrect preamble\nexpected: %q\n actual: %q", test.Preamble, pre) 479 } 480 for i := range test.Output { 481 if !reflect.DeepEqual(test.Output[i], files[i]) { 482 exp, _ := json.MarshalIndent(test.Output[i], "", " ") 483 act, _ := json.MarshalIndent(files[i], "", " ") 484 t.Errorf("incorrect file at position %d\nexpected: %s\n actual: %s", i, exp, act) 485 } 486 } 487 }) 488 } 489} 490 491func newTestParser(input string, init bool) *parser { 492 p := newParser(bytes.NewBufferString(input)) 493 if init { 494 _ = p.Next() 495 } 496 return p 497}