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