github.com/resourced/tail@v0.0.0-20160821000709-5c9f0ff76696/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/resourced/tail/ratelimiter" 18 "github.com/resourced/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.ReadLines(tail, content) 347 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 354 // rename must trigger reopen 355 <-time.After(delay) 356 tailTest.RenameFile("test.txt", "test.txt.rotated") 357 <-time.After(delay) 358 tailTest.CreateFile("test.txt", "endofworld\n") 359 360 // Delete after a reasonable delay, to give tail sufficient time 361 // to read all lines. 362 <-time.After(delay) 363 tailTest.RemoveFile("test.txt") 364 <-time.After(delay) 365 366 // Do not bother with stopping as it could kill the tomb during 367 // the reading of data written above. Timings can vary based on 368 // test environment. 369 tail.Cleanup() 370 } 371 372 func reSeek(t *testing.T, poll bool) { 373 var name string 374 if poll { 375 name = "reseek-polling" 376 } else { 377 name = "reseek-inotify" 378 } 379 tailTest := NewTailTest(name, t) 380 tailTest.CreateFile("test.txt", "a really long string goes here\nhello\nworld\n") 381 tail := tailTest.StartTail( 382 "test.txt", 383 Config{Follow: true, ReOpen: false, Poll: poll}) 384 385 go tailTest.VerifyTailOutput(tail, []string{ 386 "a really long string goes here", "hello", "world", "h311o", "w0r1d", "endofworld"}, false) 387 388 // truncate now 389 <-time.After(100 * time.Millisecond) 390 tailTest.TruncateFile("test.txt", "h311o\nw0r1d\nendofworld\n") 391 392 // Delete after a reasonable delay, to give tail sufficient time 393 // to read all lines. 394 <-time.After(100 * time.Millisecond) 395 tailTest.RemoveFile("test.txt") 396 397 // Do not bother with stopping as it could kill the tomb during 398 // the reading of data written above. Timings can vary based on 399 // test environment. 400 tailTest.Cleanup(tail, false) 401 } 402 403 // Test library 404 405 type TailTest struct { 406 Name string 407 path string 408 done chan struct{} 409 *testing.T 410 } 411 412 func NewTailTest(name string, t *testing.T) TailTest { 413 tt := TailTest{name, ".test/" + name, make(chan struct{}), t} 414 err := os.MkdirAll(tt.path, os.ModeTemporary|0700) 415 if err != nil { 416 tt.Fatal(err) 417 } 418 419 return tt 420 } 421 422 func (t TailTest) CreateFile(name string, contents string) { 423 err := ioutil.WriteFile(t.path+"/"+name, []byte(contents), 0600) 424 if err != nil { 425 t.Fatal(err) 426 } 427 } 428 429 func (t TailTest) RemoveFile(name string) { 430 err := os.Remove(t.path + "/" + name) 431 if err != nil { 432 t.Fatal(err) 433 } 434 } 435 436 func (t TailTest) RenameFile(oldname string, newname string) { 437 oldname = t.path + "/" + oldname 438 newname = t.path + "/" + newname 439 err := os.Rename(oldname, newname) 440 if err != nil { 441 t.Fatal(err) 442 } 443 } 444 445 func (t TailTest) AppendFile(name string, contents string) { 446 f, err := os.OpenFile(t.path+"/"+name, os.O_APPEND|os.O_WRONLY, 0600) 447 if err != nil { 448 t.Fatal(err) 449 } 450 defer f.Close() 451 _, err = f.WriteString(contents) 452 if err != nil { 453 t.Fatal(err) 454 } 455 } 456 457 func (t TailTest) TruncateFile(name string, contents string) { 458 f, err := os.OpenFile(t.path+"/"+name, os.O_TRUNC|os.O_WRONLY, 0600) 459 if err != nil { 460 t.Fatal(err) 461 } 462 defer f.Close() 463 _, err = f.WriteString(contents) 464 if err != nil { 465 t.Fatal(err) 466 } 467 } 468 469 func (t TailTest) StartTail(name string, config Config) *Tail { 470 tail, err := TailFile(t.path+"/"+name, config) 471 if err != nil { 472 t.Fatal(err) 473 } 474 return tail 475 } 476 477 func (t TailTest) VerifyTailOutput(tail *Tail, lines []string, expectEOF bool) { 478 defer close(t.done) 479 t.ReadLines(tail, lines) 480 // It is important to do this if only EOF is expected 481 // otherwise we could block on <-tail.Lines 482 if expectEOF { 483 line, ok := <-tail.Lines 484 if ok { 485 t.Fatalf("more content from tail: %+v", line) 486 } 487 } 488 } 489 490 func (t TailTest) ReadLines(tail *Tail, lines []string) { 491 for idx, line := range lines { 492 tailedLine, ok := <-tail.Lines 493 if !ok { 494 // tail.Lines is closed and empty. 495 err := tail.Err() 496 if err != nil { 497 t.Fatalf("tail ended with error: %v", err) 498 } 499 t.Fatalf("tail ended early; expecting more: %v", lines[idx:]) 500 } 501 if tailedLine == nil { 502 t.Fatalf("tail.Lines returned nil; not possible") 503 } 504 // Note: not checking .Err as the `lines` argument is designed 505 // to match error strings as well. 506 if tailedLine.Text != line { 507 t.Fatalf( 508 "unexpected line/err from tail: "+ 509 "expecting <<%s>>>, but got <<<%s>>>", 510 line, tailedLine.Text) 511 } 512 } 513 } 514 515 func (t TailTest) Cleanup(tail *Tail, stop bool) { 516 <-t.done 517 if stop { 518 tail.Stop() 519 } 520 tail.Cleanup() 521 }