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