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