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