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