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