this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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.

+289 -31
+1 -1
gitdiff/file_header.go
··· 61 61 } 62 62 63 63 func (p *parser) ParseTraditionalFileHeader() (*File, error) { 64 - const shortestValidFragHeader = "@@ -0,0 +1 @@\n" 64 + const shortestValidFragHeader = "@@ -1 +1 @@\n" 65 65 const ( 66 66 oldPrefix = "--- " 67 67 newPrefix = "+++ "
+2 -6
gitdiff/file_header_test.go
··· 1 1 package gitdiff 2 2 3 3 import ( 4 - "bufio" 5 4 "os" 6 5 "reflect" 7 - "strings" 8 6 "testing" 9 7 ) 10 8 ··· 150 148 151 149 for name, test := range tests { 152 150 t.Run(name, func(t *testing.T) { 153 - p := &parser{r: bufio.NewReader(strings.NewReader(test.Input))} 154 - p.Next() 151 + p := newTestParser(test.Input, true) 155 152 156 153 f, err := p.ParseGitFileHeader() 157 154 if test.Err { ··· 256 253 257 254 for name, test := range tests { 258 255 t.Run(name, func(t *testing.T) { 259 - p := &parser{r: bufio.NewReader(strings.NewReader(test.Input))} 260 - p.Next() 256 + p := newTestParser(test.Input, true) 261 257 262 258 f, err := p.ParseTraditionalFileHeader() 263 259 if test.Err {
+16
gitdiff/gitdiff.go
··· 51 51 Line string 52 52 } 53 53 54 + func (fl FragmentLine) String() string { 55 + return fl.Op.String() + fl.Line 56 + } 57 + 54 58 // LineOp describes the type of a fragment line: context, added, or removed. 55 59 type LineOp int 56 60 ··· 62 66 // OpAdd indicates an added line 63 67 OpAdd 64 68 ) 69 + 70 + func (op LineOp) String() string { 71 + switch op { 72 + case OpContext: 73 + return " " 74 + case OpDelete: 75 + return "-" 76 + case OpAdd: 77 + return "+" 78 + } 79 + return "?" 80 + } 65 81 66 82 // Header returns the cannonical header of this fragment. 67 83 func (f *Fragment) Header() string {
+10 -7
gitdiff/parser.go
··· 245 245 oldLines, newLines := frag.OldLines, frag.NewLines 246 246 for { 247 247 line := p.Line(0) 248 - switch line[0] { 248 + op, data := line[0], line[1:] 249 + 250 + switch op { 249 251 case '\n': 252 + data = "\n" 250 253 fallthrough // newer GNU diff versions create empty context lines 251 254 case ' ': 252 255 oldLines-- ··· 256 259 } else { 257 260 frag.TrailingContext++ 258 261 } 259 - frag.Lines = append(frag.Lines, FragmentLine{OpContext, line[1:]}) 262 + frag.Lines = append(frag.Lines, FragmentLine{OpContext, data}) 260 263 case '-': 261 264 oldLines-- 262 265 frag.LinesDeleted++ 263 266 frag.TrailingContext = 0 264 - frag.Lines = append(frag.Lines, FragmentLine{OpDelete, line[1:]}) 267 + frag.Lines = append(frag.Lines, FragmentLine{OpDelete, data}) 265 268 case '+': 266 269 newLines-- 267 270 frag.LinesAdded++ 268 271 frag.TrailingContext = 0 269 - frag.Lines = append(frag.Lines, FragmentLine{OpAdd, line[1:]}) 272 + frag.Lines = append(frag.Lines, FragmentLine{OpAdd, data}) 270 273 default: 271 274 // this may appear in middle of fragment if it's for a deleted line 272 275 if isNoNewlineLine(line) { 273 - last := len(frag.Lines) - 1 274 - frag.Lines[last].Line = strings.TrimSuffix(frag.Lines[last].Line, "\n") 276 + last := &frag.Lines[len(frag.Lines)-1] 277 + last.Line = strings.TrimSuffix(last.Line, "\n") 275 278 break 276 279 } 277 - return p.Errorf(0, "invalid line operation: %q", line[0]) 280 + return p.Errorf(0, "invalid line operation: %q", op) 278 281 } 279 282 280 283 next := p.Line(1)
+260 -17
gitdiff/parser_test.go
··· 11 11 func TestLineOperations(t *testing.T) { 12 12 const content = "the first line\nthe second line\nthe third line\n" 13 13 14 - newParser := func() *parser { 15 - return &parser{r: bufio.NewReader(strings.NewReader(content))} 16 - } 17 - 18 14 t.Run("read", func(t *testing.T) { 19 - p := newParser() 20 - 15 + p := newTestParser(content, false) 21 16 if err := p.Next(); err != nil { 22 17 t.Fatalf("error advancing parser: %v", err) 23 18 } ··· 72 67 }) 73 68 74 69 t.Run("peek", func(t *testing.T) { 75 - p := newParser() 76 - 70 + p := newTestParser(content, false) 77 71 if err := p.Next(); err != nil { 78 72 t.Fatalf("error advancing parser: %v", err) 79 73 } ··· 94 88 }) 95 89 96 90 t.Run("emptyInput", func(t *testing.T) { 97 - p := &parser{r: bufio.NewReader(strings.NewReader(""))} 91 + p := newTestParser("", false) 98 92 if err := p.Next(); err != io.EOF { 99 93 t.Fatalf("expected EOF, but got: %v", err) 100 94 } ··· 147 141 148 142 for name, test := range tests { 149 143 t.Run(name, func(t *testing.T) { 150 - p := &parser{r: bufio.NewReader(strings.NewReader(test.Input))} 151 - p.Next() 144 + p := newTestParser(test.Input, true) 152 145 153 146 if err := test.Parse(p); err != nil { 154 147 t.Fatalf("unexpected error while parsing: %v", err) ··· 168 161 Err bool 169 162 }{ 170 163 "shortest": { 171 - Input: "@@ -0,0 +1 @@\n", 164 + Input: "@@ -1 +1 @@\n", 172 165 Output: &Fragment{ 173 - OldPosition: 0, 174 - OldLines: 0, 166 + OldPosition: 1, 167 + OldLines: 1, 175 168 NewPosition: 1, 176 169 NewLines: 1, 177 170 }, ··· 207 200 208 201 for name, test := range tests { 209 202 t.Run(name, func(t *testing.T) { 210 - p := &parser{r: bufio.NewReader(strings.NewReader(test.Input))} 211 - p.Next() 203 + p := newTestParser(test.Input, true) 212 204 213 205 frag, err := p.ParseTextFragmentHeader() 214 206 if test.Err { ··· 222 214 } 223 215 224 216 if !reflect.DeepEqual(test.Output, frag) { 225 - t.Fatalf("incorrect fragment\nexpected: %+v\nactual: %+v", test.Output, frag) 217 + t.Errorf("incorrect fragment\nexpected: %+v\nactual: %+v", test.Output, frag) 218 + } 219 + }) 220 + } 221 + } 222 + 223 + func TestParseTextChunk(t *testing.T) { 224 + tests := map[string]struct { 225 + Input string 226 + Fragment Fragment 227 + 228 + Output *Fragment 229 + Err bool 230 + }{ 231 + "addWithContext": { 232 + Input: ` context line 233 + +new line 1 234 + +new line 2 235 + context line 236 + `, 237 + Fragment: Fragment{ 238 + OldLines: 2, 239 + NewLines: 4, 240 + }, 241 + Output: &Fragment{ 242 + OldLines: 2, 243 + NewLines: 4, 244 + Lines: []FragmentLine{ 245 + {OpContext, "context line\n"}, 246 + {OpAdd, "new line 1\n"}, 247 + {OpAdd, "new line 2\n"}, 248 + {OpContext, "context line\n"}, 249 + }, 250 + LinesAdded: 2, 251 + LeadingContext: 1, 252 + TrailingContext: 1, 253 + }, 254 + }, 255 + "deleteWithContext": { 256 + Input: ` context line 257 + -old line 1 258 + -old line 2 259 + context line 260 + `, 261 + Fragment: Fragment{ 262 + OldLines: 4, 263 + NewLines: 2, 264 + }, 265 + Output: &Fragment{ 266 + OldLines: 4, 267 + NewLines: 2, 268 + Lines: []FragmentLine{ 269 + {OpContext, "context line\n"}, 270 + {OpDelete, "old line 1\n"}, 271 + {OpDelete, "old line 2\n"}, 272 + {OpContext, "context line\n"}, 273 + }, 274 + LinesDeleted: 2, 275 + LeadingContext: 1, 276 + TrailingContext: 1, 277 + }, 278 + }, 279 + "replaceWithContext": { 280 + Input: ` context line 281 + -old line 1 282 + +new line 1 283 + context line 284 + `, 285 + Fragment: Fragment{ 286 + OldLines: 3, 287 + NewLines: 3, 288 + }, 289 + Output: &Fragment{ 290 + OldLines: 3, 291 + NewLines: 3, 292 + Lines: []FragmentLine{ 293 + {OpContext, "context line\n"}, 294 + {OpDelete, "old line 1\n"}, 295 + {OpAdd, "new line 1\n"}, 296 + {OpContext, "context line\n"}, 297 + }, 298 + LinesDeleted: 1, 299 + LinesAdded: 1, 300 + LeadingContext: 1, 301 + TrailingContext: 1, 302 + }, 303 + }, 304 + "deleteFinalNewline": { 305 + Input: ` context line 306 + -old line 1 307 + +new line 1 308 + \ No newline at end of file 309 + `, 310 + Fragment: Fragment{ 311 + OldLines: 2, 312 + NewLines: 2, 313 + }, 314 + Output: &Fragment{ 315 + OldLines: 2, 316 + NewLines: 2, 317 + Lines: []FragmentLine{ 318 + {OpContext, "context line\n"}, 319 + {OpDelete, "old line 1\n"}, 320 + {OpAdd, "new line 1"}, 321 + }, 322 + LinesDeleted: 1, 323 + LinesAdded: 1, 324 + LeadingContext: 1, 325 + }, 326 + }, 327 + "addFinalNewline": { 328 + Input: ` context line 329 + -old line 1 330 + \ No newline at end of file 331 + +new line 1 332 + `, 333 + Fragment: Fragment{ 334 + OldLines: 2, 335 + NewLines: 2, 336 + }, 337 + Output: &Fragment{ 338 + OldLines: 2, 339 + NewLines: 2, 340 + Lines: []FragmentLine{ 341 + {OpContext, "context line\n"}, 342 + {OpDelete, "old line 1"}, 343 + {OpAdd, "new line 1\n"}, 344 + }, 345 + LinesDeleted: 1, 346 + LinesAdded: 1, 347 + LeadingContext: 1, 348 + }, 349 + }, 350 + "addAll": { 351 + Input: `+new line 1 352 + +new line 2 353 + +new line 3 354 + `, 355 + Fragment: Fragment{ 356 + OldLines: 0, 357 + NewLines: 3, 358 + }, 359 + Output: &Fragment{ 360 + OldLines: 0, 361 + NewLines: 3, 362 + Lines: []FragmentLine{ 363 + {OpAdd, "new line 1\n"}, 364 + {OpAdd, "new line 2\n"}, 365 + {OpAdd, "new line 3\n"}, 366 + }, 367 + LinesAdded: 3, 368 + }, 369 + }, 370 + "deleteAll": { 371 + Input: `-old line 1 372 + -old line 2 373 + -old line 3 374 + `, 375 + Fragment: Fragment{ 376 + OldLines: 3, 377 + NewLines: 0, 378 + }, 379 + Output: &Fragment{ 380 + OldLines: 3, 381 + NewLines: 0, 382 + Lines: []FragmentLine{ 383 + {OpDelete, "old line 1\n"}, 384 + {OpDelete, "old line 2\n"}, 385 + {OpDelete, "old line 3\n"}, 386 + }, 387 + LinesDeleted: 3, 388 + }, 389 + }, 390 + "emptyContextLine": { 391 + Input: ` context line 392 + 393 + +new line 394 + context line 395 + `, 396 + Fragment: Fragment{ 397 + OldLines: 3, 398 + NewLines: 4, 399 + }, 400 + Output: &Fragment{ 401 + OldLines: 3, 402 + NewLines: 4, 403 + Lines: []FragmentLine{ 404 + {OpContext, "context line\n"}, 405 + {OpContext, "\n"}, 406 + {OpAdd, "new line\n"}, 407 + {OpContext, "context line\n"}, 408 + }, 409 + LinesAdded: 1, 410 + LeadingContext: 2, 411 + TrailingContext: 1, 412 + }, 413 + }, 414 + "emptyChunk": { 415 + Input: "", 416 + Err: true, 417 + }, 418 + "invalidOperation": { 419 + Input: ` context line 420 + ?wat line 421 + context line 422 + `, 423 + Fragment: Fragment{ 424 + OldLines: 3, 425 + NewLines: 3, 426 + }, 427 + Err: true, 428 + }, 429 + "unbalancedHeader": { 430 + Input: ` context line 431 + -old line 1 432 + +new line 1 433 + context line 434 + `, 435 + Fragment: Fragment{ 436 + OldLines: 2, 437 + NewLines: 5, 438 + }, 439 + Err: true, 440 + }, 441 + } 442 + 443 + for name, test := range tests { 444 + t.Run(name, func(t *testing.T) { 445 + p := newTestParser(test.Input, true) 446 + 447 + frag := test.Fragment 448 + err := p.ParseTextChunk(&frag) 449 + if test.Err { 450 + if err == nil { 451 + t.Fatalf("expected error parsing text chunk, but got nil") 452 + } 453 + return 454 + } 455 + if err != nil { 456 + t.Fatalf("error parsing text chunk: %v", err) 457 + } 458 + 459 + if !reflect.DeepEqual(test.Output, &frag) { 460 + t.Errorf("incorrect fragment\nexpected: %+v\nactual: %+v", test.Output, &frag) 226 461 } 227 462 }) 228 463 } 229 464 } 465 + 466 + func newTestParser(input string, init bool) *parser { 467 + p := &parser{r: bufio.NewReader(strings.NewReader(input))} 468 + if init { 469 + _ = p.Next() 470 + } 471 + return p 472 + }