gitee.com/quant1x/pkg@v0.2.8/tools/tail/tail_test.go (about)

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