github.com/mr-pmillz/tail@v1.5.0/tail_test.go (about) 1 // Copyright (c) 2019 FOSS contributors of https://github.com/nxadm/tail 2 // Copyright (c) 2015 HPE Software Inc. All rights reserved. 3 // Copyright (c) 2013 ActiveState Software Inc. All rights reserved. 4 5 // TODO: 6 // * repeat all the tests with Poll:true 7 8 package tail 9 10 import ( 11 "fmt" 12 _ "fmt" 13 "io" 14 "io/ioutil" 15 "os" 16 "strings" 17 "testing" 18 "time" 19 20 "github.com/mr-pmillz/tail/ratelimiter" 21 "github.com/mr-pmillz/tail/watch" 22 ) 23 24 func TestTailFile(t *testing.T) { 25 t.SkipNow() 26 } 27 28 func ExampleTailFile() { 29 // Keep tracking a file even when recreated. 30 // /var/log/messages is typically continuously written and rotated daily. 31 testFileName := "/var/log/messages" 32 // ReOpen when truncated, Follow to wait for new input when EOL is reached 33 tailedFile, err := TailFile(testFileName, Config{ReOpen: true, Follow: true}) 34 if err != nil { 35 panic(err) 36 } 37 38 for line := range tailedFile.Lines { 39 fmt.Println(line.Text) 40 } 41 // Prints all the lines in the logfile and keeps printing new input 42 } 43 44 func TestMain(m *testing.M) { 45 // Use a smaller poll duration for faster test runs. Keep it below 46 // 100ms (which value is used as common delays for tests) 47 watch.POLL_DURATION = 5 * time.Millisecond 48 os.Exit(m.Run()) 49 } 50 51 func TestMustExist(t *testing.T) { 52 tail, err := TailFile("/no/such/file", Config{Follow: true, MustExist: true}) 53 if err == nil { 54 t.Error("MustExist:true is violated") 55 tail.Stop() 56 } 57 tail, err = TailFile("/no/such/file", Config{Follow: true, MustExist: false}) 58 if err != nil { 59 t.Error("MustExist:false is violated") 60 } 61 tail.Stop() 62 _, err = TailFile("README.md", Config{Follow: true, MustExist: true}) 63 if err != nil { 64 t.Error("MustExist:true on an existing file is violated") 65 } 66 tail.Cleanup() 67 } 68 69 // This test is not very robust because its purpose is to test that a race 70 // condition doesn't exist. As such, the failure pattern of this test would 71 // be flaky failure, not necessarily easily reproduced failure. 72 // 73 // Running this same test *without* SyncOpen: true, alongside 74 // stress --hdd 8 (to make the filesystem very busy), will fail most 75 // of the time. (It fails significantly even without stress.) 76 func TestSeeksSynchronously(t *testing.T) { 77 tailTest, cleanup := NewTailTest("seeks-synchronously", t) 78 defer cleanup() 79 tailTest.CreateFile("test.txt", "hello\nworld\n") 80 tail := tailTest.StartTail("test.txt", Config{SyncOpen: true, Location: &SeekInfo{0, io.SeekEnd}}) 81 go tailTest.VerifyTailOutput(tail, []string{"more", "data"}, false) 82 tailTest.AppendToFile("test.txt", "more\ndata\n") 83 tailTest.Cleanup(tail, true) 84 } 85 86 func TestWaitsForFileToExist(t *testing.T) { 87 tailTest, cleanup := NewTailTest("waits-for-file-to-exist", t) 88 defer cleanup() 89 tail := tailTest.StartTail("test.txt", Config{}) 90 go tailTest.VerifyTailOutput(tail, []string{"hello", "world"}, false) 91 92 <-time.After(100 * time.Millisecond) 93 tailTest.CreateFile("test.txt", "hello\nworld\n") 94 tailTest.Cleanup(tail, true) 95 } 96 97 func TestWaitsForFileToExistRelativePath(t *testing.T) { 98 tailTest, cleanup := NewTailTest("waits-for-file-to-exist-relative", t) 99 defer cleanup() 100 101 oldWD, err := os.Getwd() 102 if err != nil { 103 tailTest.Fatal(err) 104 } 105 os.Chdir(tailTest.path) 106 defer os.Chdir(oldWD) 107 108 tail, err := TailFile("test.txt", Config{}) 109 if err != nil { 110 tailTest.Fatal(err) 111 } 112 113 go tailTest.VerifyTailOutput(tail, []string{"hello", "world"}, false) 114 115 <-time.After(100 * time.Millisecond) 116 if err := ioutil.WriteFile("test.txt", []byte("hello\nworld\n"), 0600); err != nil { 117 tailTest.Fatal(err) 118 } 119 tailTest.Cleanup(tail, true) 120 } 121 122 func TestStop(t *testing.T) { 123 tail, err := TailFile("_no_such_file", Config{Follow: true, MustExist: false}) 124 if err != nil { 125 t.Error("MustExist:false is violated") 126 } 127 if tail.Stop() != nil { 128 t.Error("Should be stoped successfully") 129 } 130 tail.Cleanup() 131 } 132 133 func TestStopNonEmptyFile(t *testing.T) { 134 tailTest, cleanup := NewTailTest("maxlinesize", t) 135 defer cleanup() 136 tailTest.CreateFile("test.txt", "hello\nthere\nworld\n") 137 tail := tailTest.StartTail("test.txt", Config{}) 138 tail.Stop() 139 tail.Cleanup() 140 // success here is if it doesn't panic. 141 } 142 143 func TestStopAtEOF(t *testing.T) { 144 tailTest, cleanup := NewTailTest("maxlinesize", t) 145 defer cleanup() 146 tailTest.CreateFile("test.txt", "hello\nthere\nworld\n") 147 tail := tailTest.StartTail("test.txt", Config{Follow: true, Location: nil}) 148 149 // read "hello" 150 line := <-tail.Lines 151 if line.Text != "hello" { 152 t.Errorf("Expected to get 'hello', got '%s' instead", line.Text) 153 } 154 155 if line.Num != 1 { 156 t.Errorf("Expected to get 1, got %d instead", line.Num) 157 } 158 159 tailTest.VerifyTailOutput(tail, []string{"there", "world"}, false) 160 tail.StopAtEOF() 161 tailTest.Cleanup(tail, true) 162 } 163 164 func TestMaxLineSizeFollow(t *testing.T) { 165 // As last file line does not end with newline, it will not be present in tail's output 166 maxLineSize(t, true, "hello\nworld\nfin\nhe", []string{"hel", "lo", "wor", "ld", "fin", "he"}) 167 } 168 169 func TestMaxLineSizeNoFollow(t *testing.T) { 170 maxLineSize(t, false, "hello\nworld\nfin\nhe", []string{"hel", "lo", "wor", "ld", "fin", "he"}) 171 } 172 173 func TestOver4096ByteLine(t *testing.T) { 174 tailTest, cleanup := NewTailTest("Over4096ByteLine", t) 175 defer cleanup() 176 testString := strings.Repeat("a", 4097) 177 tailTest.CreateFile("test.txt", "test\n"+testString+"\nhello\nworld\n") 178 tail := tailTest.StartTail("test.txt", Config{Follow: true, Location: nil}) 179 go tailTest.VerifyTailOutput(tail, []string{"test", testString, "hello", "world"}, false) 180 181 // Delete after a reasonable delay, to give tail sufficient time 182 // to read all lines. 183 <-time.After(100 * time.Millisecond) 184 tailTest.RemoveFile("test.txt") 185 tailTest.Cleanup(tail, true) 186 } 187 188 func TestOver4096ByteLineWithSetMaxLineSize(t *testing.T) { 189 tailTest, cleanup := NewTailTest("Over4096ByteLineMaxLineSize", t) 190 defer cleanup() 191 testString := strings.Repeat("a", 4097) 192 tailTest.CreateFile("test.txt", "test\n"+testString+"\nhello\nworld\n") 193 tail := tailTest.StartTail("test.txt", Config{Follow: true, Location: nil, MaxLineSize: 4097}) 194 go tailTest.VerifyTailOutput(tail, []string{"test", testString, "hello", "world"}, false) 195 196 // Delete after a reasonable delay, to give tail sufficient time 197 // to read all lines. 198 <-time.After(100 * time.Millisecond) 199 tailTest.RemoveFile("test.txt") 200 tailTest.Cleanup(tail, true) 201 } 202 203 func TestReOpenWithCursor(t *testing.T) { 204 delay := 300 * time.Millisecond // account for POLL_DURATION 205 tailTest, cleanup := NewTailTest("reopen-cursor", t) 206 defer cleanup() 207 tailTest.CreateFile("test.txt", "hello\nworld\n") 208 tail := tailTest.StartTail( 209 "test.txt", 210 Config{Follow: true, ReOpen: true, Poll: true}) 211 content := []string{"hello", "world", "more", "data", "endofworld"} 212 go tailTest.VerifyTailOutputUsingCursor(tail, content, false) 213 214 // deletion must trigger reopen 215 <-time.After(delay) 216 tailTest.RemoveFile("test.txt") 217 <-time.After(delay) 218 tailTest.CreateFile("test.txt", "hello\nworld\nmore\ndata\n") 219 220 // rename must trigger reopen 221 <-time.After(delay) 222 tailTest.RenameFile("test.txt", "test.txt.rotated") 223 <-time.After(delay) 224 tailTest.CreateFile("test.txt", "hello\nworld\nmore\ndata\nendofworld\n") 225 226 // Delete after a reasonable delay, to give tail sufficient time 227 // to read all lines. 228 <-time.After(delay) 229 tailTest.RemoveFile("test.txt") 230 <-time.After(delay) 231 232 // Do not bother with stopping as it could kill the tomb during 233 // the reading of data written above. Timings can vary based on 234 // test environment. 235 tailTest.Cleanup(tail, false) 236 } 237 238 func TestLocationFull(t *testing.T) { 239 tailTest, cleanup := NewTailTest("location-full", t) 240 defer cleanup() 241 tailTest.CreateFile("test.txt", "hello\nworld\n") 242 tail := tailTest.StartTail("test.txt", Config{Follow: true, Location: nil}) 243 go tailTest.VerifyTailOutput(tail, []string{"hello", "world"}, false) 244 245 // Delete after a reasonable delay, to give tail sufficient time 246 // to read all lines. 247 <-time.After(100 * time.Millisecond) 248 tailTest.RemoveFile("test.txt") 249 tailTest.Cleanup(tail, true) 250 } 251 252 func TestLocationFullDontFollow(t *testing.T) { 253 tailTest, cleanup := NewTailTest("location-full-dontfollow", t) 254 defer cleanup() 255 tailTest.CreateFile("test.txt", "hello\nworld\n") 256 tail := tailTest.StartTail("test.txt", Config{Follow: false, Location: nil}) 257 go tailTest.VerifyTailOutput(tail, []string{"hello", "world"}, false) 258 259 // Add more data only after reasonable delay. 260 <-time.After(100 * time.Millisecond) 261 tailTest.AppendFile("test.txt", "more\ndata\n") 262 <-time.After(100 * time.Millisecond) 263 264 tailTest.Cleanup(tail, true) 265 } 266 267 func TestLocationEnd(t *testing.T) { 268 tailTest, cleanup := NewTailTest("location-end", t) 269 defer cleanup() 270 tailTest.CreateFile("test.txt", "hello\nworld\n") 271 tail := tailTest.StartTail("test.txt", Config{Follow: true, Location: &SeekInfo{0, io.SeekEnd}}) 272 go tailTest.VerifyTailOutput(tail, []string{"more", "data"}, false) 273 274 <-time.After(100 * time.Millisecond) 275 tailTest.AppendFile("test.txt", "more\ndata\n") 276 277 // Delete after a reasonable delay, to give tail sufficient time 278 // to read all lines. 279 <-time.After(100 * time.Millisecond) 280 tailTest.RemoveFile("test.txt") 281 tailTest.Cleanup(tail, true) 282 } 283 284 func TestLocationMiddle(t *testing.T) { 285 // Test reading from middle. 286 tailTest, cleanup := NewTailTest("location-middle", t) 287 defer cleanup() 288 tailTest.CreateFile("test.txt", "hello\nworld\n") 289 tail := tailTest.StartTail("test.txt", Config{Follow: true, Location: &SeekInfo{-6, io.SeekEnd}}) 290 go tailTest.VerifyTailOutput(tail, []string{"world", "more", "data"}, false) 291 292 <-time.After(100 * time.Millisecond) 293 tailTest.AppendFile("test.txt", "more\ndata\n") 294 295 // Delete after a reasonable delay, to give tail sufficient time 296 // to read all lines. 297 <-time.After(100 * time.Millisecond) 298 tailTest.RemoveFile("test.txt") 299 tailTest.Cleanup(tail, true) 300 } 301 302 // The use of polling file watcher could affect file rotation 303 // (detected via renames), so test these explicitly. 304 305 func TestReOpenInotify(t *testing.T) { 306 reOpen(t, false) 307 } 308 309 func TestReOpenPolling(t *testing.T) { 310 reOpen(t, true) 311 } 312 313 // The use of polling file watcher could affect file rotation 314 // (detected via renames), so test these explicitly. 315 316 func TestReSeekInotify(t *testing.T) { 317 reSeek(t, false) 318 } 319 320 func TestReSeekPolling(t *testing.T) { 321 reSeek(t, true) 322 } 323 324 func TestReSeekWithCursor(t *testing.T) { 325 tailTest, cleanup := NewTailTest("reseek-cursor", t) 326 defer cleanup() 327 tailTest.CreateFile("test.txt", "a really long string goes here\nhello\nworld\n") 328 tail := tailTest.StartTail( 329 "test.txt", 330 Config{Follow: true, ReOpen: false, Poll: false}) 331 332 go tailTest.VerifyTailOutputUsingCursor(tail, []string{ 333 "a really long string goes here", "hello", "world", "but", "not", "me"}, false) 334 335 // truncate now 336 <-time.After(100 * time.Millisecond) 337 tailTest.TruncateFile("test.txt", "skip\nme\nplease\nbut\nnot\nme\n") 338 339 // Delete after a reasonable delay, to give tail sufficient time 340 // to read all lines. 341 <-time.After(100 * time.Millisecond) 342 tailTest.RemoveFile("test.txt") 343 344 // Do not bother with stopping as it could kill the tomb during 345 // the reading of data written above. Timings can vary based on 346 // test environment. 347 tailTest.Cleanup(tail, false) 348 } 349 350 func TestRateLimiting(t *testing.T) { 351 tailTest, cleanup := NewTailTest("rate-limiting", t) 352 defer cleanup() 353 tailTest.CreateFile("test.txt", "hello\nworld\nagain\nextra\n") 354 config := Config{ 355 Follow: true, 356 RateLimiter: ratelimiter.NewLeakyBucket(2, time.Second)} 357 leakybucketFull := "Too much log activity; waiting a second before resuming tailing" 358 tail := tailTest.StartTail("test.txt", config) 359 360 // TODO: also verify that tail resumes after the cooloff period. 361 go tailTest.VerifyTailOutput(tail, []string{ 362 "hello", "world", "again", 363 leakybucketFull, 364 "more", "data", 365 leakybucketFull}, false) 366 367 // Add more data only after reasonable delay. 368 <-time.After(1200 * time.Millisecond) 369 tailTest.AppendFile("test.txt", "more\ndata\n") 370 371 // Delete after a reasonable delay, to give tail sufficient time 372 // to read all lines. 373 <-time.After(100 * time.Millisecond) 374 tailTest.RemoveFile("test.txt") 375 376 tailTest.Cleanup(tail, true) 377 } 378 379 func TestTell(t *testing.T) { 380 tailTest, cleanup := NewTailTest("tell-position", t) 381 defer cleanup() 382 tailTest.CreateFile("test.txt", "hello\nworld\nagain\nmore\n") 383 config := Config{ 384 Follow: false, 385 Location: &SeekInfo{0, io.SeekStart}} 386 tail := tailTest.StartTail("test.txt", config) 387 // read one line 388 line := <-tail.Lines 389 if line.Num != 1 { 390 tailTest.Errorf("expected line to have number 1 but got %d", line.Num) 391 } 392 offset, err := tail.Tell() 393 if err != nil { 394 tailTest.Errorf("Tell return error: %s", err.Error()) 395 } 396 tail.Stop() 397 398 config = Config{ 399 Follow: false, 400 Location: &SeekInfo{offset, io.SeekStart}} 401 tail = tailTest.StartTail("test.txt", config) 402 for l := range tail.Lines { 403 // it may readed one line in the chan(tail.Lines), 404 // so it may lost one line. 405 if l.Text != "world" && l.Text != "again" { 406 tailTest.Fatalf("mismatch; expected world or again, but got %s", 407 l.Text) 408 if l.Num < 1 || l.Num > 2 { 409 tailTest.Errorf("expected line number to be between 1 and 2 but got %d", l.Num) 410 } 411 } 412 break 413 } 414 tailTest.RemoveFile("test.txt") 415 tail.Stop() 416 tail.Cleanup() 417 } 418 419 func TestBlockUntilExists(t *testing.T) { 420 tailTest, cleanup := NewTailTest("block-until-file-exists", t) 421 defer cleanup() 422 config := Config{ 423 Follow: true, 424 } 425 tail := tailTest.StartTail("test.txt", config) 426 go func() { 427 time.Sleep(100 * time.Millisecond) 428 tailTest.CreateFile("test.txt", "hello world\n") 429 }() 430 for l := range tail.Lines { 431 if l.Text != "hello world" { 432 tailTest.Fatalf("mismatch; expected hello world, but got %s", 433 l.Text) 434 } 435 break 436 } 437 tailTest.RemoveFile("test.txt") 438 tail.Stop() 439 tail.Cleanup() 440 } 441 442 func maxLineSize(t *testing.T, follow bool, fileContent string, expected []string) { 443 tailTest, cleanup := NewTailTest("maxlinesize", t) 444 defer cleanup() 445 tailTest.CreateFile("test.txt", fileContent) 446 tail := tailTest.StartTail("test.txt", Config{Follow: follow, Location: nil, MaxLineSize: 3}) 447 go tailTest.VerifyTailOutput(tail, expected, false) 448 449 // Delete after a reasonable delay, to give tail sufficient time 450 // to read all lines. 451 <-time.After(100 * time.Millisecond) 452 tailTest.RemoveFile("test.txt") 453 tailTest.Cleanup(tail, true) 454 } 455 456 func reOpen(t *testing.T, poll bool) { 457 var name string 458 var delay time.Duration 459 if poll { 460 name = "reopen-polling" 461 delay = 300 * time.Millisecond // account for POLL_DURATION 462 } else { 463 name = "reopen-inotify" 464 delay = 100 * time.Millisecond 465 } 466 tailTest, cleanup := NewTailTest(name, t) 467 defer cleanup() 468 tailTest.CreateFile("test.txt", "hello\nworld\n") 469 tail := tailTest.StartTail( 470 "test.txt", 471 Config{Follow: true, ReOpen: true, Poll: poll}) 472 content := []string{"hello", "world", "more", "data", "endofworld"} 473 go tailTest.VerifyTailOutput(tail, content, false) 474 475 if poll { 476 // deletion must trigger reopen 477 <-time.After(delay) 478 tailTest.RemoveFile("test.txt") 479 <-time.After(delay) 480 tailTest.CreateFile("test.txt", "more\ndata\n") 481 } else { 482 // In inotify mode, fsnotify is currently unable to deliver notifications 483 // about deletion of open files, so we are not testing file deletion. 484 // (see https://github.com/fsnotify/fsnotify/issues/194 for details). 485 <-time.After(delay) 486 tailTest.AppendToFile("test.txt", "more\ndata\n") 487 } 488 489 // rename must trigger reopen 490 <-time.After(delay) 491 tailTest.RenameFile("test.txt", "test.txt.rotated") 492 <-time.After(delay) 493 tailTest.CreateFile("test.txt", "endofworld\n") 494 495 // Delete after a reasonable delay, to give tail sufficient time 496 // to read all lines. 497 <-time.After(delay) 498 tailTest.RemoveFile("test.txt") 499 <-time.After(delay) 500 501 // Do not bother with stopping as it could kill the tomb during 502 // the reading of data written above. Timings can vary based on 503 // test environment. 504 tailTest.Cleanup(tail, false) 505 } 506 507 func TestInotify_WaitForCreateThenMove(t *testing.T) { 508 tailTest, cleanup := NewTailTest("wait-for-create-then-reopen", t) 509 defer cleanup() 510 os.Remove(tailTest.path + "/test.txt") // Make sure the file does NOT exist. 511 512 tail := tailTest.StartTail( 513 "test.txt", 514 Config{Follow: true, ReOpen: true, Poll: false}) 515 516 content := []string{"hello", "world", "endofworld"} 517 go tailTest.VerifyTailOutput(tail, content, false) 518 519 time.Sleep(50 * time.Millisecond) 520 tailTest.CreateFile("test.txt", "hello\nworld\n") 521 time.Sleep(50 * time.Millisecond) 522 tailTest.RenameFile("test.txt", "test.txt.rotated") 523 time.Sleep(50 * time.Millisecond) 524 tailTest.CreateFile("test.txt", "endofworld\n") 525 time.Sleep(50 * time.Millisecond) 526 tailTest.RemoveFile("test.txt.rotated") 527 tailTest.RemoveFile("test.txt") 528 529 // Do not bother with stopping as it could kill the tomb during 530 // the reading of data written above. Timings can vary based on 531 // test environment. 532 tailTest.Cleanup(tail, false) 533 } 534 535 func TestIncompleteLines(t *testing.T) { 536 tailTest, cleanup := NewTailTest("incomplete-lines", t) 537 defer cleanup() 538 filename := "test.txt" 539 config := Config{ 540 Follow: true, 541 CompleteLines: true, 542 } 543 tail := tailTest.StartTail(filename, config) 544 go func() { 545 time.Sleep(100 * time.Millisecond) 546 tailTest.CreateFile(filename, "hello world\n") 547 time.Sleep(100 * time.Millisecond) 548 // here we intentially write a partial line to see if `Tail` contains 549 // information that it's incomplete 550 tailTest.AppendFile(filename, "hello") 551 time.Sleep(100 * time.Millisecond) 552 tailTest.AppendFile(filename, " again\n") 553 }() 554 555 lines := []string{"hello world", "hello again"} 556 557 tailTest.ReadLines(tail, lines, false) 558 559 tailTest.RemoveFile(filename) 560 tail.Stop() 561 tail.Cleanup() 562 } 563 564 func TestIncompleteLongLines(t *testing.T) { 565 tailTest, cleanup := NewTailTest("incomplete-lines-long", t) 566 defer cleanup() 567 filename := "test.txt" 568 config := Config{ 569 Follow: true, 570 MaxLineSize: 3, 571 CompleteLines: true, 572 } 573 tail := tailTest.StartTail(filename, config) 574 go func() { 575 time.Sleep(100 * time.Millisecond) 576 tailTest.CreateFile(filename, "hello world\n") 577 time.Sleep(100 * time.Millisecond) 578 tailTest.AppendFile(filename, "hello") 579 time.Sleep(100 * time.Millisecond) 580 tailTest.AppendFile(filename, "again\n") 581 }() 582 583 lines := []string{"hel", "lo ", "wor", "ld", "hel", "loa", "gai", "n"} 584 585 tailTest.ReadLines(tail, lines, false) 586 587 tailTest.RemoveFile(filename) 588 tail.Stop() 589 tail.Cleanup() 590 } 591 592 func TestIncompleteLinesWithReopens(t *testing.T) { 593 tailTest, cleanup := NewTailTest("incomplete-lines-reopens", t) 594 defer cleanup() 595 filename := "test.txt" 596 config := Config{ 597 Follow: true, 598 CompleteLines: true, 599 } 600 tail := tailTest.StartTail(filename, config) 601 go func() { 602 time.Sleep(100 * time.Millisecond) 603 tailTest.CreateFile(filename, "hello world\nhi") 604 time.Sleep(100 * time.Millisecond) 605 tailTest.TruncateFile(filename, "rewriting\n") 606 }() 607 608 // not that the "hi" gets lost, because it was never a complete line 609 lines := []string{"hello world", "rewriting"} 610 611 tailTest.ReadLines(tail, lines, false) 612 613 tailTest.RemoveFile(filename) 614 tail.Stop() 615 tail.Cleanup() 616 } 617 618 func TestIncompleteLinesWithoutFollow(t *testing.T) { 619 tailTest, cleanup := NewTailTest("incomplete-lines-no-follow", t) 620 defer cleanup() 621 filename := "test.txt" 622 config := Config{ 623 Follow: false, 624 CompleteLines: true, 625 } 626 tail := tailTest.StartTail(filename, config) 627 go func() { 628 time.Sleep(100 * time.Millisecond) 629 // intentionally missing a newline at the end 630 tailTest.CreateFile(filename, "foo\nbar\nbaz") 631 }() 632 633 lines := []string{"foo", "bar", "baz"} 634 635 tailTest.VerifyTailOutput(tail, lines, true) 636 637 tailTest.RemoveFile(filename) 638 tail.Stop() 639 tail.Cleanup() 640 } 641 642 func reSeek(t *testing.T, poll bool) { 643 var name string 644 if poll { 645 name = "reseek-polling" 646 } else { 647 name = "reseek-inotify" 648 } 649 tailTest, cleanup := NewTailTest(name, t) 650 defer cleanup() 651 tailTest.CreateFile("test.txt", "a really long string goes here\nhello\nworld\n") 652 tail := tailTest.StartTail( 653 "test.txt", 654 Config{Follow: true, ReOpen: false, Poll: poll}) 655 656 go tailTest.VerifyTailOutput(tail, []string{ 657 "a really long string goes here", "hello", "world", "h311o", "w0r1d", "endofworld"}, false) 658 659 // truncate now 660 <-time.After(100 * time.Millisecond) 661 tailTest.TruncateFile("test.txt", "h311o\nw0r1d\nendofworld\n") 662 663 // Delete after a reasonable delay, to give tail sufficient time 664 // to read all lines. 665 <-time.After(100 * time.Millisecond) 666 tailTest.RemoveFile("test.txt") 667 668 // Do not bother with stopping as it could kill the tomb during 669 // the reading of data written above. Timings can vary based on 670 // test environment. 671 tailTest.Cleanup(tail, false) 672 } 673 674 // Test library 675 676 type TailTest struct { 677 Name string 678 path string 679 done chan struct{} 680 *testing.T 681 } 682 683 func NewTailTest(name string, t *testing.T) (TailTest, func()) { 684 testdir, err := ioutil.TempDir("", "tail-test-"+name) 685 if err != nil { 686 t.Fatal(err) 687 } 688 689 return TailTest{name, testdir, make(chan struct{}), t}, func() { 690 if err := os.RemoveAll(testdir); err != nil { 691 t.Logf("failed to remove test directory: %v", testdir) 692 } 693 } 694 } 695 696 func (t TailTest) CreateFile(name string, contents string) { 697 err := ioutil.WriteFile(t.path+"/"+name, []byte(contents), 0600) 698 if err != nil { 699 t.Fatal(err) 700 } 701 } 702 703 func (t TailTest) AppendToFile(name string, contents string) { 704 err := ioutil.WriteFile(t.path+"/"+name, []byte(contents), 0600|os.ModeAppend) 705 if err != nil { 706 t.Fatal(err) 707 } 708 } 709 710 func (t TailTest) RemoveFile(name string) { 711 err := os.Remove(t.path + "/" + name) 712 if err != nil { 713 t.Fatal(err) 714 } 715 } 716 717 func (t TailTest) RenameFile(oldname string, newname string) { 718 oldname = t.path + "/" + oldname 719 newname = t.path + "/" + newname 720 err := os.Rename(oldname, newname) 721 if err != nil { 722 t.Fatal(err) 723 } 724 } 725 726 func (t TailTest) AppendFile(name string, contents string) { 727 f, err := os.OpenFile(t.path+"/"+name, os.O_APPEND|os.O_WRONLY, 0600) 728 if err != nil { 729 t.Fatal(err) 730 } 731 defer f.Close() 732 _, err = f.WriteString(contents) 733 if err != nil { 734 t.Fatal(err) 735 } 736 } 737 738 func (t TailTest) TruncateFile(name string, contents string) { 739 f, err := os.OpenFile(t.path+"/"+name, os.O_TRUNC|os.O_WRONLY, 0600) 740 if err != nil { 741 t.Fatal(err) 742 } 743 defer f.Close() 744 _, err = f.WriteString(contents) 745 if err != nil { 746 t.Fatal(err) 747 } 748 } 749 750 func (t TailTest) StartTail(name string, config Config) *Tail { 751 tail, err := TailFile(t.path+"/"+name, config) 752 if err != nil { 753 t.Fatal(err) 754 } 755 return tail 756 } 757 758 func (t TailTest) VerifyTailOutput(tail *Tail, lines []string, expectEOF bool) { 759 defer close(t.done) 760 t.ReadLines(tail, lines, false) 761 // It is important to do this if only EOF is expected 762 // otherwise we could block on <-tail.Lines 763 if expectEOF { 764 line, ok := <-tail.Lines 765 if ok { 766 t.Fatalf("more content from tail: %+v", line) 767 } 768 } 769 } 770 771 func (t TailTest) VerifyTailOutputUsingCursor(tail *Tail, lines []string, expectEOF bool) { 772 defer close(t.done) 773 t.ReadLines(tail, lines, true) 774 // It is important to do this if only EOF is expected 775 // otherwise we could block on <-tail.Lines 776 if expectEOF { 777 line, ok := <-tail.Lines 778 if ok { 779 t.Fatalf("more content from tail: %+v", line) 780 } 781 } 782 } 783 784 func (t TailTest) ReadLines(tail *Tail, lines []string, useCursor bool) { 785 cursor := 1 786 787 for _, line := range lines { 788 for { 789 tailedLine, ok := <-tail.Lines 790 if !ok { 791 // tail.Lines is closed and empty. 792 err := tail.Err() 793 if err != nil { 794 t.Fatalf("tail ended with error: %v", err) 795 } 796 t.Fatalf("tail ended early; expecting more: %v", lines[cursor:]) 797 } 798 if tailedLine == nil { 799 t.Fatalf("tail.Lines returned nil; not possible") 800 } 801 802 if useCursor && tailedLine.Num < cursor { 803 // skip lines up until cursor 804 continue 805 } 806 807 // Note: not checking .Err as the `lines` argument is designed 808 // to match error strings as well. 809 if tailedLine.Text != line { 810 t.Fatalf( 811 "unexpected line/err from tail: "+ 812 "expecting <<%s>>>, but got <<<%s>>>", 813 line, tailedLine.Text) 814 } 815 816 cursor++ 817 break 818 } 819 } 820 } 821 822 func (t TailTest) Cleanup(tail *Tail, stop bool) { 823 <-t.done 824 if stop { 825 tail.Stop() 826 } 827 tail.Cleanup() 828 }