diff --git a/Makefile b/Makefile index 6591b24f..118b1ba4 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ default: test test: *.go - go test -v -race ./... + go test -v -race -timeout 30s ./... fmt: gofmt -w . diff --git a/tail.go b/tail.go index c99cdaa2..694ae099 100644 --- a/tail.go +++ b/tail.go @@ -18,7 +18,7 @@ import ( "github.com/hpcloud/tail/ratelimiter" "github.com/hpcloud/tail/util" "github.com/hpcloud/tail/watch" - "gopkg.in/tomb.v1" + tomb "gopkg.in/tomb.v1" ) var ( @@ -27,13 +27,14 @@ var ( type Line struct { Text string + Num int Time time.Time Err error // Error from tail } // NewLine returns a Line with present time. -func NewLine(text string) *Line { - return &Line{text, time.Now(), nil} +func NewLine(text string, lineNum int) *Line { + return &Line{text, lineNum, time.Now(), nil} } // SeekInfo represents arguments to `os.Seek` @@ -78,8 +79,9 @@ type Tail struct { Lines chan *Line Config - file *os.File - reader *bufio.Reader + file *os.File + reader *bufio.Reader + lineNum int watcher watch.FileWatcher changes *watch.FileChanges @@ -186,6 +188,8 @@ func (tail *Tail) closeFile() { func (tail *Tail) reopen() error { tail.closeFile() + // reset line number + tail.lineNum = 0 for { var err error tail.file, err = OpenFile(tail.Filename) @@ -275,7 +279,7 @@ func (tail *Tail) tailFileSync() { // file when rate limit is reached. msg := ("Too much log activity; waiting a second " + "before resuming tailing") - tail.Lines <- &Line{msg, time.Now(), errors.New(msg)} + tail.Lines <- &Line{msg, tail.lineNum, time.Now(), errors.New(msg)} select { case <-time.After(time.Second): case <-tail.Dying(): @@ -414,7 +418,8 @@ func (tail *Tail) sendLine(line string) bool { } for _, line := range lines { - tail.Lines <- &Line{line, now, nil} + tail.lineNum++ + tail.Lines <- &Line{line, tail.lineNum, now, nil} } if tail.Config.RateLimiter != nil { diff --git a/tail_test.go b/tail_test.go index 38d6b84b..7bcc479c 100644 --- a/tail_test.go +++ b/tail_test.go @@ -106,6 +106,9 @@ func TestStopAtEOF(t *testing.T) { if line.Text != "hello" { t.Errorf("Expected to get 'hello', got '%s' instead", line.Text) } + if line.Num != 1 { + t.Errorf("Expected to get 1, got %d instead", line.Num) + } tailTest.VerifyTailOutput(tail, []string{"there", "world"}, false) tail.StopAtEOF() @@ -134,6 +137,7 @@ func TestOver4096ByteLine(t *testing.T) { tailTest.RemoveFile("test.txt") tailTest.Cleanup(tail, true) } + func TestOver4096ByteLineWithSetMaxLineSize(t *testing.T) { tailTest := NewTailTest("Over4096ByteLineMaxLineSize", t) testString := strings.Repeat("a", 4097) @@ -219,6 +223,40 @@ func TestReOpenPolling(t *testing.T) { reOpen(t, true) } +func TestReOpenWithCursor(t *testing.T) { + delay := 300 * time.Millisecond // account for POLL_DURATION + tailTest := NewTailTest("reopen-cursor", t) + tailTest.CreateFile("test.txt", "hello\nworld\n") + tail := tailTest.StartTail( + "test.txt", + Config{Follow: true, ReOpen: true, Poll: true}) + content := []string{"hello", "world", "more", "data", "endofworld"} + go tailTest.VerifyTailOutputUsingCursor(tail, content, false) + + // deletion must trigger reopen + <-time.After(delay) + tailTest.RemoveFile("test.txt") + <-time.After(delay) + tailTest.CreateFile("test.txt", "hello\nworld\nmore\ndata\n") + + // rename must trigger reopen + <-time.After(delay) + tailTest.RenameFile("test.txt", "test.txt.rotated") + <-time.After(delay) + tailTest.CreateFile("test.txt", "hello\nworld\nmore\ndata\nendofworld\n") + + // Delete after a reasonable delay, to give tail sufficient time + // to read all lines. + <-time.After(delay) + tailTest.RemoveFile("test.txt") + <-time.After(delay) + + // Do not bother with stopping as it could kill the tomb during + // the reading of data written above. Timings can vary based on + // test environment. + tailTest.Cleanup(tail, false) +} + // The use of polling file watcher could affect file rotation // (detected via renames), so test these explicitly. @@ -230,6 +268,31 @@ func TestReSeekPolling(t *testing.T) { reSeek(t, true) } +func TestReSeekWithCursor(t *testing.T) { + tailTest := NewTailTest("reseek-cursor", t) + tailTest.CreateFile("test.txt", "a really long string goes here\nhello\nworld\n") + tail := tailTest.StartTail( + "test.txt", + Config{Follow: true, ReOpen: false, Poll: false}) + + go tailTest.VerifyTailOutputUsingCursor(tail, []string{ + "a really long string goes here", "hello", "world", "but", "not", "me"}, false) + + // truncate now + <-time.After(100 * time.Millisecond) + tailTest.TruncateFile("test.txt", "skip\nme\nplease\nbut\nnot\nme\n") + + // Delete after a reasonable delay, to give tail sufficient time + // to read all lines. + <-time.After(100 * time.Millisecond) + tailTest.RemoveFile("test.txt") + + // Do not bother with stopping as it could kill the tomb during + // the reading of data written above. Timings can vary based on + // test environment. + tailTest.Cleanup(tail, false) +} + func TestRateLimiting(t *testing.T) { tailTest := NewTailTest("rate-limiting", t) tailTest.CreateFile("test.txt", "hello\nworld\nagain\nextra\n") @@ -266,7 +329,10 @@ func TestTell(t *testing.T) { Location: &SeekInfo{0, os.SEEK_SET}} tail := tailTest.StartTail("test.txt", config) // read noe line - <-tail.Lines + line := <-tail.Lines + if line.Num != 1 { + tailTest.Errorf("expected line to have number 1 but got %d", line.Num) + } offset, err := tail.Tell() if err != nil { tailTest.Errorf("Tell return error: %s", err.Error()) @@ -285,6 +351,9 @@ func TestTell(t *testing.T) { tailTest.Fatalf("mismatch; expected world or again, but got %s", l.Text) } + if l.Num < 1 || l.Num > 2 { + tailTest.Errorf("expected line number to be between 1 and 2 but got %d", l.Num) + } break } tailTest.RemoveFile("test.txt") @@ -518,7 +587,7 @@ func (t TailTest) StartTail(name string, config Config) *Tail { func (t TailTest) VerifyTailOutput(tail *Tail, lines []string, expectEOF bool) { defer close(t.done) - t.ReadLines(tail, lines) + t.ReadLines(tail, lines, false) // It is important to do this if only EOF is expected // otherwise we could block on <-tail.Lines if expectEOF { @@ -529,27 +598,53 @@ func (t TailTest) VerifyTailOutput(tail *Tail, lines []string, expectEOF bool) { } } -func (t TailTest) ReadLines(tail *Tail, lines []string) { - for idx, line := range lines { - tailedLine, ok := <-tail.Lines - if !ok { - // tail.Lines is closed and empty. - err := tail.Err() - if err != nil { - t.Fatalf("tail ended with error: %v", err) - } - t.Fatalf("tail ended early; expecting more: %v", lines[idx:]) - } - if tailedLine == nil { - t.Fatalf("tail.Lines returned nil; not possible") +func (t TailTest) VerifyTailOutputUsingCursor(tail *Tail, lines []string, expectEOF bool) { + defer close(t.done) + t.ReadLines(tail, lines, true) + // It is important to do this if only EOF is expected + // otherwise we could block on <-tail.Lines + if expectEOF { + line, ok := <-tail.Lines + if ok { + t.Fatalf("more content from tail: %+v", line) } - // Note: not checking .Err as the `lines` argument is designed - // to match error strings as well. - if tailedLine.Text != line { - t.Fatalf( - "unexpected line/err from tail: "+ - "expecting <<%s>>>, but got <<<%s>>>", - line, tailedLine.Text) + } +} + +func (t TailTest) ReadLines(tail *Tail, lines []string, useCursor bool) { + cursor := 1 + + for _, line := range lines { + for { + tailedLine, ok := <-tail.Lines + if !ok { + // tail.Lines is closed and empty. + err := tail.Err() + if err != nil { + t.Fatalf("tail ended with error: %v", err) + } + t.Fatalf("tail ended early; expecting more: %v", lines[cursor:]) + } + if tailedLine == nil { + t.Fatalf("tail.Lines returned nil; not possible") + } + + if useCursor && tailedLine.Num < cursor { + // skip lines up until cursor + continue + } + + // Note: not checking .Err as the `lines` argument is designed + // to match error strings as well. + if tailedLine.Text != line { + t.Fatalf( + "unexpected line/err from tail: "+ + "expecting <<%s>>>, but got <<<%s>>>", + line, tailedLine.Text) + } + + cursor++ + break } } }