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