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