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