github.com/ethereumproject/go-ethereum@v5.5.2+incompatible/logger/glog/glog_test.go (about)

     1  // Go support for leveled logs, analogous to https://code.google.com/p/google-glog/
     2  //
     3  // Copyright 2013 Google Inc. All Rights Reserved.
     4  // Modifications copyright 2017 ETC Dev Team. All Rights Reserved.
     5  //
     6  // Licensed under the Apache License, Version 2.0 (the "License");
     7  // you may not use this file except in compliance with the License.
     8  // You may obtain a copy of the License at
     9  //
    10  //     http://www.apache.org/licenses/LICENSE-2.0
    11  //
    12  // Unless required by applicable law or agreed to in writing, software
    13  // distributed under the License is distributed on an "AS IS" BASIS,
    14  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15  // See the License for the specific language governing permissions and
    16  // limitations under the License.
    17  
    18  package glog
    19  
    20  import (
    21  	"bytes"
    22  	"compress/gzip"
    23  	"fmt"
    24  	"io/ioutil"
    25  	stdLog "log"
    26  	"math/rand"
    27  	"os"
    28  	"path/filepath"
    29  	"runtime"
    30  	"strconv"
    31  	"strings"
    32  	"sync"
    33  	"sync/atomic"
    34  	"testing"
    35  	"time"
    36  )
    37  
    38  // Test that shortHostname works as advertised.
    39  func TestShortHostname(t *testing.T) {
    40  	for hostname, expect := range map[string]string{
    41  		"":                "",
    42  		"host":            "host",
    43  		"host.google.com": "host",
    44  	} {
    45  		if got := shortHostname(hostname); expect != got {
    46  			t.Errorf("shortHostname(%q): expected %q, got %q", hostname, expect, got)
    47  		}
    48  	}
    49  }
    50  
    51  // flushBuffer wraps a bytes.Buffer to satisfy flushSyncWriter.
    52  type flushBuffer struct {
    53  	bytes.Buffer
    54  }
    55  
    56  func (f *flushBuffer) Flush() error {
    57  	return nil
    58  }
    59  
    60  func (f *flushBuffer) Sync() error {
    61  	return nil
    62  }
    63  
    64  // swapLogging sets the log writers and returns the old array.
    65  func (l *loggingT) swapLogging(writers [numSeverity]flushSyncWriter) (old [numSeverity]flushSyncWriter) {
    66  	l.mu.Lock()
    67  	defer l.mu.Unlock()
    68  	old = l.file
    69  	for i, w := range writers {
    70  		logging.file[i] = w
    71  	}
    72  	return
    73  }
    74  
    75  // swapLogging sets the log writers and returns the old array.
    76  func (l *loggingT) swapDisplay(writers [numSeverity]flushSyncWriter) (old [numSeverity]flushSyncWriter) {
    77  	l.mu.Lock()
    78  	defer l.mu.Unlock()
    79  	old = l.file
    80  	for i, w := range writers {
    81  		display.file[i] = w
    82  	}
    83  	return
    84  }
    85  
    86  // newLoggingBuffers sets the log writers to all new byte buffers and returns the old array.
    87  func (l *loggingT) newLoggingBuffers() [numSeverity]flushSyncWriter {
    88  	return l.swapLogging([numSeverity]flushSyncWriter{new(flushBuffer), new(flushBuffer), new(flushBuffer), new(flushBuffer)})
    89  }
    90  
    91  // newDisplayBuffers sets the log writers to all new byte buffers and returns the old array.
    92  func (l *loggingT) newDisplayBuffers() [numSeverity]flushSyncWriter {
    93  	return l.swapDisplay([numSeverity]flushSyncWriter{new(flushBuffer), new(flushBuffer), new(flushBuffer), new(flushBuffer)})
    94  }
    95  
    96  // loggingContents returns the specified log value as a string.
    97  func loggingContents(s severity) string {
    98  	return logging.file[s].(*flushBuffer).String()
    99  }
   100  
   101  // displayContents returns the specified log value as a string.
   102  func displayContents(s severity) string {
   103  	return display.file[s].(*flushBuffer).String()
   104  }
   105  
   106  // loggingContains reports whether the string is contained in the log.
   107  func loggingContains(s severity, str string, t *testing.T) bool {
   108  	return strings.Contains(loggingContents(s), str)
   109  }
   110  
   111  // displayContains reports whether the string is contained in the log.
   112  func displayContains(s severity, str string, t *testing.T) bool {
   113  	return strings.Contains(displayContents(s), str)
   114  }
   115  
   116  // setFlags configures the logging flags how the test expects them.
   117  func setFlags() {
   118  	logging.toStderr = false
   119  	display.toStderr = false
   120  }
   121  
   122  // Test that Info works as advertised.
   123  func TestInfo(t *testing.T) {
   124  	setFlags()
   125  	defer logging.swapLogging(logging.newLoggingBuffers())
   126  	Info("test")
   127  	if !loggingContains(infoLog, "I", t) {
   128  		t.Errorf("Info has wrong character: %q", loggingContents(infoLog))
   129  	}
   130  	if !loggingContains(infoLog, "test", t) {
   131  		t.Error("Info failed")
   132  	}
   133  }
   134  
   135  func TestInfoDepth(t *testing.T) {
   136  	setFlags()
   137  	defer logging.swapLogging(logging.newLoggingBuffers())
   138  
   139  	f := func() { InfoDepth(1, "depth-test1") }
   140  
   141  	// The next three lines must stay together
   142  	_, _, wantLine, _ := runtime.Caller(0)
   143  	InfoDepth(0, "depth-test0")
   144  	f()
   145  
   146  	msgs := strings.Split(strings.TrimSuffix(loggingContents(infoLog), "\n"), "\n")
   147  	if len(msgs) != 2 {
   148  		t.Fatalf("Got %d lines, expected 2", len(msgs))
   149  	}
   150  
   151  	for i, m := range msgs {
   152  		if !strings.HasPrefix(m, "I") {
   153  			t.Errorf("InfoDepth[%d] has wrong character: %q", i, m)
   154  		}
   155  		w := fmt.Sprintf("depth-test%d", i)
   156  		if !strings.Contains(m, w) {
   157  			t.Errorf("InfoDepth[%d] missing %q: %q", i, w, m)
   158  		}
   159  
   160  		// pull out the line number (between : and ])
   161  		msg := m[strings.LastIndex(m, ":")+1:]
   162  		x := strings.Index(msg, "]")
   163  		if x < 0 {
   164  			t.Errorf("InfoDepth[%d]: missing ']': %q", i, m)
   165  			continue
   166  		}
   167  		line, err := strconv.Atoi(msg[:x])
   168  		if err != nil {
   169  			t.Errorf("InfoDepth[%d]: bad line number: %q", i, m)
   170  			continue
   171  		}
   172  		wantLine++
   173  		if wantLine != line {
   174  			t.Errorf("InfoDepth[%d]: got line %d, want %d", i, line, wantLine)
   175  		}
   176  	}
   177  }
   178  
   179  func init() {
   180  	CopyStandardLogTo("INFO")
   181  }
   182  
   183  // Test that CopyStandardLogTo panics on bad input.
   184  func TestCopyStandardLogToPanic(t *testing.T) {
   185  	defer func() {
   186  		if s, ok := recover().(string); !ok || !strings.Contains(s, "LOG") {
   187  			t.Errorf(`CopyStandardLogTo("LOG") should have panicked: %v`, s)
   188  		}
   189  	}()
   190  	CopyStandardLogTo("LOG")
   191  }
   192  
   193  // Test that using the standard log package logs to INFO.
   194  func TestStandardLog(t *testing.T) {
   195  	setFlags()
   196  	defer logging.swapLogging(logging.newLoggingBuffers())
   197  	stdLog.Print("test")
   198  	if !loggingContains(infoLog, "I", t) {
   199  		t.Errorf("Info has wrong character: %q", loggingContents(infoLog))
   200  	}
   201  	if !loggingContains(infoLog, "test", t) {
   202  		t.Error("Info failed")
   203  	}
   204  }
   205  
   206  func lineNumber() int {
   207  	_, _, line, _ := runtime.Caller(1)
   208  	return line
   209  }
   210  
   211  // Test that the header has the correct format.
   212  func TestHeader1ErrorLog(t *testing.T) {
   213  	setFlags()
   214  	defer logging.swapLogging(logging.newLoggingBuffers())
   215  	defer func(previous func() time.Time) { timeNow = previous }(timeNow)
   216  	timeNow = func() time.Time {
   217  		return time.Date(2006, 1, 2, 15, 4, 5, .067890e9, time.Local)
   218  	}
   219  	pid = 1234
   220  	Error("test")
   221  	line := lineNumber() - 1 // the line directly above
   222  	format := "E" + "0102 15:04:05.067890 logger/glog/glog_test.go:%d] test\n"
   223  	n, err := fmt.Sscanf(loggingContents(errorLog), format, &line)
   224  	if n != 1 || err != nil {
   225  		t.Errorf("log format error: %d elements, error %s:\n%s", n, err, loggingContents(errorLog))
   226  	}
   227  	// Scanf treats multiple spaces as equivalent to a single space,
   228  	// so check for correct space-padding also.
   229  	want := fmt.Sprintf(format, line)
   230  	if loggingContents(errorLog) != want {
   231  		t.Errorf("log format error: got:\n\t%q\nwant:\t%q", loggingContents(errorLog), want)
   232  	}
   233  }
   234  
   235  // Test that the header has the correct format.
   236  func TestHeader2InfoLog(t *testing.T) {
   237  	setFlags()
   238  	defer logging.swapLogging(logging.newLoggingBuffers())
   239  	defer func(previous func() time.Time) { timeNow = previous }(timeNow)
   240  	timeNow = func() time.Time {
   241  		return time.Date(2006, 1, 2, 15, 4, 5, .067890e9, time.Local)
   242  	}
   243  	s := logging.verbosityTraceThreshold.get()
   244  	logging.verbosityTraceThreshold.set(5) // Use app flag defaults
   245  	defer logging.verbosityTraceThreshold.set(s)
   246  	pid = 1234
   247  	Info("test")
   248  	format := "I" + "0102 15:04:05.067890 logger/glog/glog_test.go:" + strconv.Itoa(lineNumber()-1) + "] test\n"
   249  	n, err := fmt.Sscanf(loggingContents(infoLog), format)
   250  	if err != nil {
   251  		t.Errorf("log format error: %d elements, error %s:\n%s", n, err, loggingContents(infoLog))
   252  	}
   253  	// Scanf treats multiple spaces as equivalent to a single space,
   254  	// so check for correct space-padding also.
   255  	want := fmt.Sprintf(format)
   256  	if loggingContents(infoLog) != want {
   257  		t.Errorf("log format error: got:\n\t%q\nwant:\n\t%q", loggingContents(infoLog), want)
   258  	}
   259  }
   260  
   261  // Test that an Error log goes to Warning and Info.
   262  // Even in the Info log, the source character will be E, so the data should
   263  // all be identical.
   264  func TestError(t *testing.T) {
   265  	setFlags()
   266  	defer logging.swapLogging(logging.newLoggingBuffers())
   267  	defer display.swapDisplay(display.newDisplayBuffers())
   268  	Error("test")
   269  	if !loggingContains(errorLog, "E", t) {
   270  		t.Errorf("Error has wrong character: %q", loggingContents(errorLog))
   271  	}
   272  	if !loggingContains(errorLog, "test", t) {
   273  		t.Error("Error failed")
   274  	}
   275  	str := loggingContents(errorLog)
   276  	if !loggingContains(warningLog, str, t) {
   277  		t.Error("Warning failed")
   278  	}
   279  	if !loggingContains(infoLog, str, t) {
   280  		t.Error("Info failed")
   281  	}
   282  }
   283  
   284  // Test that a Warning log goes to Info.
   285  // Even in the Info log, the source character will be W, so the data should
   286  // all be identical.
   287  func TestWarningLogging(t *testing.T) {
   288  	setFlags()
   289  	defer logging.swapLogging(logging.newLoggingBuffers())
   290  	Warning("test")
   291  	if !loggingContains(warningLog, "W", t) {
   292  		t.Errorf("Warning has wrong character: %q", loggingContents(warningLog))
   293  	}
   294  	if !loggingContains(warningLog, "test", t) {
   295  		t.Error("Warning failed")
   296  	}
   297  	str := loggingContents(warningLog)
   298  	if !loggingContains(infoLog, str, t) {
   299  		t.Error("Info failed")
   300  	}
   301  }
   302  
   303  func TestWarningDisplay(t *testing.T) {
   304  	setFlags()
   305  	defer display.swapDisplay(display.newDisplayBuffers())
   306  	display.verbosity.Set("3")
   307  	defer display.verbosity.Set("0")
   308  	D(2).Warnln("test")
   309  	if !displayContains(warningLog, "test", t) {
   310  		t.Error("Warning failed")
   311  	}
   312  	str := displayContents(warningLog)
   313  	if !displayContains(infoLog, str, t) {
   314  		t.Error("Info failed")
   315  	}
   316  }
   317  
   318  func TestErrorDisplay(t *testing.T) {
   319  	setFlags()
   320  	defer display.swapDisplay(display.newDisplayBuffers())
   321  	display.verbosity.Set("3")
   322  	defer display.verbosity.Set("0")
   323  	D(2).Errorln("test")
   324  	if !displayContains(errorLog, "ERR ", t) {
   325  		t.Errorf("Error has wrong character: %q", displayContents(errorLog))
   326  	}
   327  	if !displayContains(warningLog, "test", t) {
   328  		t.Error("Warning failed")
   329  	}
   330  	str := displayContents(warningLog)
   331  	if !displayContains(infoLog, str, t) {
   332  		t.Error("Info failed")
   333  	}
   334  }
   335  
   336  // Test that a V log goes to Info.
   337  func TestV(t *testing.T) {
   338  	setFlags()
   339  	defer logging.swapLogging(logging.newLoggingBuffers())
   340  	logging.verbosity.Set("2")
   341  	defer logging.verbosity.Set("0")
   342  	V(2).Info("test")
   343  	if !loggingContains(infoLog, "I", t) {
   344  		t.Errorf("Info has wrong character: %q", loggingContents(infoLog))
   345  	}
   346  	if !loggingContains(infoLog, "test", t) {
   347  		t.Error("Info failed")
   348  	}
   349  }
   350  
   351  // Test that a V log goes to Info.
   352  func TestD(t *testing.T) {
   353  	setFlags()
   354  	defer display.swapDisplay(display.newDisplayBuffers())
   355  	display.verbosity.Set("2")
   356  	defer display.verbosity.Set("0")
   357  	D(2).Infoln("test")
   358  	if !displayContains(infoLog, ":", t) {
   359  		t.Errorf("Info has wrong character: %q", displayContents(infoLog))
   360  	}
   361  	if displayContains(infoLog, "I", t) {
   362  		t.Errorf("Info has wrong character: %q", displayContents(infoLog))
   363  	}
   364  	if !displayContains(infoLog, "test", t) {
   365  		t.Error("Info failed")
   366  	}
   367  }
   368  
   369  // Test that a vmodule enables a log in this file.
   370  func TestVmoduleOn(t *testing.T) {
   371  	setFlags()
   372  	defer logging.swapLogging(logging.newLoggingBuffers())
   373  	logging.vmodule.Set("glog_test.go=2")
   374  	defer logging.vmodule.Set("")
   375  	if !V(1) {
   376  		t.Error("V not enabled for 1")
   377  	}
   378  	if !V(2) {
   379  		t.Error("V not enabled for 2")
   380  	}
   381  	if V(3) {
   382  		t.Error("V enabled for 3")
   383  	}
   384  	V(2).Info("test")
   385  	if !loggingContains(infoLog, "I", t) {
   386  		t.Errorf("Info has wrong character: %q", loggingContents(infoLog))
   387  	}
   388  	if !loggingContains(infoLog, "test", t) {
   389  		t.Error("Info failed")
   390  	}
   391  }
   392  
   393  // Test that a vmodule of another file does not enable a log in this file.
   394  func TestVmoduleOff(t *testing.T) {
   395  	setFlags()
   396  	defer logging.swapLogging(logging.newLoggingBuffers())
   397  	logging.vmodule.Set("notthisfile=2")
   398  	defer logging.vmodule.Set("")
   399  	for i := 1; i <= 3; i++ {
   400  		if V(Level(i)) {
   401  			t.Errorf("V enabled for %d", i)
   402  		}
   403  	}
   404  	V(2).Info("test")
   405  	if loggingContents(infoLog) != "" {
   406  		t.Error("V logged incorrectly")
   407  	}
   408  }
   409  
   410  var patternTests = []struct{ input, want string }{
   411  	{"foo/bar/x.go", ".*/foo/bar/x\\.go$"},
   412  	{"foo/*/x.go", ".*/foo(/.*)?/x\\.go$"},
   413  	{"foo/*", ".*/foo(/.*)?/[^/]+\\.go$"},
   414  }
   415  
   416  func TestCompileModulePattern(t *testing.T) {
   417  	for _, test := range patternTests {
   418  		re, err := compileModulePattern(test.input)
   419  		if err != nil {
   420  			t.Fatalf("%s: %v", test.input, err)
   421  		}
   422  		if re.String() != test.want {
   423  			t.Errorf("mismatch for %q: got %q, want %q", test.input, re.String(), test.want)
   424  		}
   425  	}
   426  }
   427  
   428  // vGlobs are patterns that match/don't match this file at V=2.
   429  var vGlobs = map[string]bool{
   430  	// Easy to test the numeric match here.
   431  	"glog_test.go=1": false, // If -vmodule sets V to 1, V(2) will fail.
   432  	"glog_test.go=2": true,
   433  	"glog_test.go=3": true, // If -vmodule sets V to 1, V(3) will succeed.
   434  
   435  	// Import path prefix matching
   436  	"logger/glog=1": false,
   437  	"logger/glog=2": true,
   438  	"logger/glog=3": true,
   439  
   440  	// Import path glob matching
   441  	"logger/*=1": false,
   442  	"logger/*=2": true,
   443  	"logger/*=3": true,
   444  
   445  	// These all use 2 and check the patterns.
   446  	"*=2": true,
   447  }
   448  
   449  // Test that vmodule globbing works as advertised.
   450  func testVmoduleGlob(pat string, match bool, t *testing.T) {
   451  	setFlags()
   452  	defer logging.swapLogging(logging.newLoggingBuffers())
   453  	defer logging.vmodule.Set("")
   454  	logging.vmodule.Set(pat)
   455  	if V(2) != Verbose(match) {
   456  		t.Errorf("incorrect match for %q: got %t expected %t", pat, V(2), match)
   457  	}
   458  }
   459  
   460  // Test that a vmodule globbing works as advertised.
   461  func TestVmoduleGlob(t *testing.T) {
   462  	for glob, match := range vGlobs {
   463  		testVmoduleGlob(glob, match, t)
   464  	}
   465  }
   466  
   467  func TestRollover(t *testing.T) {
   468  	setFlags()
   469  	var err error
   470  	defer func(previous func(error)) { logExitFunc = previous }(logExitFunc)
   471  	logExitFunc = func(e error) {
   472  		err = e
   473  	}
   474  	defer func(previous uint64) { MaxSize = previous }(MaxSize)
   475  	MaxSize = 512
   476  
   477  	Info("x") // Be sure we have a file.
   478  	info, ok := logging.file[infoLog].(*syncBuffer)
   479  	if !ok {
   480  		t.Fatal("info wasn't created")
   481  	}
   482  	if err != nil {
   483  		t.Fatalf("info has initial error: %v", err)
   484  	}
   485  	fname0 := info.file.Name()
   486  	Info(strings.Repeat("x", int(MaxSize))) // force a rollover
   487  	if err != nil {
   488  		t.Fatalf("info has error after big write: %v", err)
   489  	}
   490  
   491  	// Make sure the next log file gets a file name with a different
   492  	// time stamp.
   493  	//
   494  	// TODO: determine whether we need to support subsecond log
   495  	// rotation.  C++ does not appear to handle this case (nor does it
   496  	// handle Daylight Savings Time properly).
   497  	time.Sleep(1 * time.Second)
   498  
   499  	Info("x") // create a new file
   500  	if err != nil {
   501  		t.Fatalf("error after rotation: %v", err)
   502  	}
   503  	fname1 := info.file.Name()
   504  	if fname0 == fname1 {
   505  		t.Errorf("info.f.Name did not change: %v", fname0)
   506  	}
   507  	if info.nbytes >= MaxSize {
   508  		t.Errorf("file size was not reset: %d", info.nbytes)
   509  	}
   510  }
   511  
   512  func TestLogBacktraceAt(t *testing.T) {
   513  	setFlags()
   514  	defer logging.swapLogging(logging.newLoggingBuffers())
   515  	// The peculiar style of this code simplifies line counting and maintenance of the
   516  	// tracing block below.
   517  	var infoLine string
   518  	setTraceLocation := func(file string, line int, ok bool, delta int) {
   519  		if !ok {
   520  			t.Fatal("could not get file:line")
   521  		}
   522  		_, file = filepath.Split(file)
   523  		infoLine = fmt.Sprintf("%s:%d", file, line+delta)
   524  		err := logging.traceLocation.Set(infoLine)
   525  		if err != nil {
   526  			t.Fatal("error setting log_backtrace_at: ", err)
   527  		}
   528  	}
   529  	{
   530  		// Start of tracing block. These lines know about each other's relative position.
   531  		_, file, line, ok := runtime.Caller(0)
   532  		setTraceLocation(file, line, ok, +2) // Two lines between Caller and Info calls.
   533  		Info("we want a stack trace here")
   534  	}
   535  	numAppearances := strings.Count(loggingContents(infoLog), infoLine)
   536  	if numAppearances < 2 {
   537  		// Need 2 appearances, one in the log header and one in the trace:
   538  		//   log_test.go:281: I0511 16:36:06.952398 02238 log_test.go:280] we want a stack trace here
   539  		//   ...
   540  		//   github.com/glog/glog_test.go:280 (0x41ba91)
   541  		//   ...
   542  		// We could be more precise but that would require knowing the details
   543  		// of the traceback format, which may not be dependable.
   544  		t.Fatal("got no trace back; log is ", loggingContents(infoLog))
   545  	}
   546  }
   547  
   548  func TestExtractTimestamp(t *testing.T) {
   549  	preffix := fmt.Sprintf("%s.%s.%s.log.", "geth_test", "sampleHost", "sampleUser")
   550  	cases := []struct {
   551  		name     string
   552  		fileName string
   553  		expected string
   554  	}{
   555  		{"valid INFO", preffix + "INFO.20171202-132113.2841", "20171202-132113"},
   556  		{"valid WARNIG", preffix + "WARNING.20171202-210922.13848", "20171202-210922"},
   557  		{"valid gzipped", preffix + "WARNING.20171202-210922.13848.gz", "20171202-210922"},
   558  		{"extra long filename", preffix + "WARNING.20171202-210922.13848.gz.bak", "20171202-210922"},
   559  		{"too short filename", preffix + "WARNING.20171202-21092", ""},
   560  		{"no preffix", "WARNING.20171202-21092", ""},
   561  	}
   562  
   563  	for _, test := range cases {
   564  		t.Run(test.name, func(t *testing.T) {
   565  			actual := extractTimestamp(test.fileName, preffix)
   566  			if test.expected != actual {
   567  				t.Errorf("Expected: '%s', actual: '%s'", test.expected, actual)
   568  			}
   569  		})
   570  	}
   571  }
   572  
   573  func TestShouldRotate(t *testing.T) {
   574  	// fixed date, to make tests stable, 04.12.2017 is Monday
   575  	start := time.Date(2017, time.December, 4, 0, 0, 0, 0, time.UTC)
   576  
   577  	cases := []struct {
   578  		name     string
   579  		nbytes   uint64
   580  		len      int
   581  		now      time.Time
   582  		minSize  uint64
   583  		maxSize  uint64
   584  		interval Interval
   585  		expected bool
   586  	}{
   587  		{"empty log no rotation", 0, 123, start, 0, 0, Never, false},
   588  		{"empty log with hourly rotation", 0, 123, start, 0, 0, Hourly, false},
   589  		{"empty log with size rotation", 0, 123, start, 0, 1024 * 1024, Never, false},
   590  		{"log with hourly rotation after less than hour", 1234, 123, start.Add(45 * time.Minute), 0, 0, Hourly, false},
   591  		{"log with hourly rotation after more than hour", 1234, 123, start.Add(65 * time.Minute), 0, 0, Hourly, true},
   592  		{"log with size rotation below MinSize", 1024, 123, start, 512 * 1024, 1024 * 1024, Never, false},
   593  		{"log with size rotation between MinSize and MaxSize", 765 * 1024, 123, start, 512 * 1024, 1024 * 1024, Never, false},
   594  		{"log with size rotation above MaxSize", 1024*1024 - 100, 123, start, 512 * 1024, 1024 * 1024, Never, true},
   595  		{"log with daily rotation after less than day", 1234, 123, start.Add(23 * time.Hour), 0, 0, Daily, false},
   596  		{"log with daily rotation after more than day", 1234, 123, start.Add(25 * time.Hour), 0, 0, Daily, true},
   597  		{"log with weekly rotation after less than week", 1234, 123, start.Add((6*24 + 23) * time.Hour), 0, 0, Weekly, false},
   598  		{"log with weekly rotation after more than week", 1234, 123, start.Add((7*24 + 1) * time.Hour), 0, 0, Weekly, true},
   599  		{"log with monthly rotation after less than month", 1234, 123, start.Add(14 * 24 * time.Hour), 0, 0, Monthly, false},
   600  		{"log with monthly rotation after more than month", 1234, 123, start.Add(30 * 24 * time.Hour), 0, 0, Monthly, true},
   601  	}
   602  
   603  	for _, test := range cases {
   604  		t.Run(test.name, func(t *testing.T) {
   605  			sb := &syncBuffer{nbytes: test.nbytes, time: start}
   606  			MinSize = test.minSize
   607  			MaxSize = test.maxSize
   608  			RotationInterval = test.interval
   609  			actual := sb.shouldRotate(test.len, test.now)
   610  			if test.expected != actual {
   611  				t.Errorf("Expected: '%v', actual: '%v'", test.expected, actual)
   612  			}
   613  		})
   614  	}
   615  }
   616  
   617  func TestGzipFile(t *testing.T) {
   618  	dir, err := ioutil.TempDir("", "")
   619  	if err != nil {
   620  		t.Error(err)
   621  	}
   622  	defer os.RemoveAll(dir)
   623  
   624  	file := filepath.Join(dir, "sample.log")
   625  	data := strings.Repeat("lorem ipsum dolor sit amet", 4096)
   626  	ioutil.WriteFile(file, []byte(data), 0600)
   627  
   628  	err = gzipFile(file)
   629  	if err != nil {
   630  		t.Fatal(err)
   631  	}
   632  
   633  	gzipped := file + ".gz"
   634  
   635  	files, err := ioutil.ReadDir(dir)
   636  	if err != nil {
   637  		t.Fatal(err)
   638  	}
   639  
   640  	if len(files) != 1 {
   641  		t.Errorf("expected 1 file in directory, found %d", len(files))
   642  	}
   643  	if files[0].Size() == 0 || files[0].Size() >= int64(len(data)) {
   644  		t.Errorf("expected: 0 < file size < %d [bytes], actual: %d [bytes]", len(data), files[0].Size())
   645  	}
   646  	if filepath.Join(dir, files[0].Name()) != gzipped {
   647  		t.Errorf("expected filename: %s; actual filename: %s", gzipped, files[0].Name())
   648  	}
   649  
   650  	input, err := os.Open(filepath.Join(dir, files[0].Name()))
   651  	if err != nil {
   652  		t.Error(err)
   653  	}
   654  	defer input.Close()
   655  
   656  	reader, err := gzip.NewReader(input)
   657  	if err != nil {
   658  		t.Fatal(err)
   659  	}
   660  	defer reader.Close()
   661  
   662  	result, err := ioutil.ReadAll(reader)
   663  	if err != nil {
   664  		t.Error(err)
   665  	}
   666  	if string(result) != data {
   667  		t.Errorf("contents of gzip file are invalid")
   668  	}
   669  }
   670  
   671  // there are always 10 interesting files, 10*1024b each
   672  // some of them are gzipped
   673  // there are WARN and INFO logs
   674  // there are also 3 other files, that shoudn't be touched
   675  // there are 2 "current files" (pointed by symlinks)
   676  // global configuration options should be applied
   677  // after running rotation, assertions about resulting data are checked
   678  func testRotation(t *testing.T) {
   679  	dir, err := ioutil.TempDir("", "")
   680  	if err != nil {
   681  		t.Error(err)
   682  	}
   683  	defer os.RemoveAll(dir)
   684  
   685  	start := time.Date(2017, time.December, 06, 0, 0, 0, 0, time.UTC)
   686  
   687  	data := []byte(strings.Repeat(".", 10*1024))
   688  	logDate := start
   689  	// generate files
   690  	for i := 0; i < 5; i++ {
   691  		infoF, infoL := logName("INFO", logDate.Add(1*time.Second))
   692  		warnF, warnL := logName("WARNING", logDate.Add(10*time.Second).Add(1*time.Second))
   693  
   694  		ioutil.WriteFile(filepath.Join(dir, infoF), data, 0600)
   695  		ioutil.WriteFile(filepath.Join(dir, warnF), data, 0600)
   696  
   697  		infoSL := filepath.Join(dir, infoL)
   698  		os.Remove(infoSL)                             // ignore err
   699  		os.Symlink(filepath.Join(dir, infoF), infoSL) // ignore err
   700  
   701  		warnSL := filepath.Join(dir, warnL)
   702  		os.Remove(warnSL)                             // ignore err
   703  		os.Symlink(filepath.Join(dir, warnF), warnSL) // ignore err
   704  
   705  		logDate = logDate.Add(24 * time.Hour)
   706  	}
   707  	dummy1 := "and_now_for_something_completely_different.log"
   708  	ioutil.WriteFile(filepath.Join(dir, dummy1), data, 0600)
   709  	dummy2, _ := logName("ERROR", start.Add(-10*24*time.Hour))
   710  	dummy2 = "keep." + dummy2
   711  	ioutil.WriteFile(filepath.Join(dir, dummy2), data, 0600)
   712  	dummy3, _ := logName("INFO", start.Add(+10*24*time.Hour))
   713  	dummy3 = strings.Replace(dummy3, userName, "differentUser", 1)
   714  	ioutil.WriteFile(filepath.Join(dir, dummy3), data, 0600)
   715  
   716  	// prepare environment
   717  	logDirs = nil
   718  	SetLogDir(dir)
   719  	createLogDirs()
   720  
   721  	// execute rotation
   722  	sb := &syncBuffer{}
   723  	atomic.StoreInt64(&rotationTime, 0) // make sure, that rotation is executed
   724  	sb.rotateOld(logDate)
   725  
   726  	// make assertions
   727  	files, err := ioutil.ReadDir(dir)
   728  	if err != nil {
   729  		t.Fatal(err)
   730  	}
   731  
   732  	// ensure that 3rd party files are intact
   733  	dummy1ok := false
   734  	dummy2ok := false
   735  	dummy3ok := false
   736  	totalSize := uint64(0)
   737  	nGzipped := 0
   738  	preffix := fmt.Sprintf("%s.%s.%s.log.", program, host, userName)
   739  	maxTimestamp := ""
   740  	if MaxAge > 0 {
   741  		t := logDate.Add(-1 * MaxAge)
   742  		maxTimestamp = fmt.Sprintf("%04d%02d%02d-%02d%02d%02d",
   743  			t.Year(),
   744  			t.Month(),
   745  			t.Day(),
   746  			t.Hour(),
   747  			t.Minute(),
   748  			t.Second(),
   749  		)
   750  	}
   751  	for _, file := range files {
   752  		sameSize := file.Size() == int64(len(data))
   753  		switch file.Name() {
   754  		case dummy1:
   755  			dummy1ok = sameSize
   756  		case dummy2:
   757  			dummy2ok = sameSize
   758  		case dummy3:
   759  			dummy3ok = sameSize
   760  		default:
   761  			totalSize += uint64(file.Size())
   762  			if maxTimestamp != "" && file.Mode().IsRegular() {
   763  				timestamp := extractTimestamp(file.Name(), preffix)
   764  				if strings.Compare(timestamp, maxTimestamp) < 0 {
   765  					t.Errorf("Old file not removed properly: %s, %v\n", file.Name(), maxTimestamp)
   766  				}
   767  			}
   768  		}
   769  		if strings.HasSuffix(file.Name(), ".gz") {
   770  			nGzipped++
   771  		}
   772  
   773  	}
   774  
   775  	if !dummy1ok || !dummy2ok || !dummy3ok {
   776  		t.Error("Some 3rd party files removed or modified!")
   777  	}
   778  
   779  	if MaxTotalSize > 0 && totalSize >= MaxTotalSize {
   780  		t.Error("MaxTotalSize constraint violated!")
   781  	}
   782  
   783  	// 3 x 3rd-party files, 2 x symlink, 2 x current log files
   784  	if Compress && nGzipped == len(files)-3-2-2 {
   785  		t.Error("Some files not compressed!")
   786  	}
   787  }
   788  
   789  func TestRotateOldFiles(t *testing.T) {
   790  	MinSize = 0
   791  	MaxSize = 1024
   792  	RotationInterval = Never
   793  
   794  	cases := []struct {
   795  		name         string
   796  		maxAge       time.Duration
   797  		maxTotalSize uint64
   798  		compress     bool
   799  	}{
   800  		{"no limits", 0, 0, false},
   801  		{"no limits with compression", 0, 0, true},
   802  		{"with age limit", 2 * 24 * time.Hour, 0, false},
   803  		{"with size limit", 0, 5 * 1024, false},
   804  		{"with both limits", 4 * 24 * time.Hour, 5 * 1024, false},
   805  		{"with age limit and compression", 2 * 24 * time.Hour, 0, true},
   806  		{"with size limit and compression", 0, 5 * 1024, true},
   807  		{"with both limits and compression", 4 * 24 * time.Hour, 5 * 1024, true},
   808  	}
   809  
   810  	for _, test := range cases {
   811  		t.Run(test.name, func(t *testing.T) {
   812  			MaxAge = test.maxAge
   813  			MaxTotalSize = test.maxTotalSize
   814  			Compress = test.compress
   815  			testRotation(t)
   816  		})
   817  	}
   818  }
   819  
   820  func TestParseInterval(t *testing.T) {
   821  	cases := []struct {
   822  		value    string
   823  		expected Interval
   824  		err      bool
   825  	}{
   826  		{"never", Never, false},
   827  		{"NeVeR", Never, false},
   828  		{"daily", Daily, false},
   829  		{"Daily", Daily, false},
   830  		{"weekly", Weekly, false},
   831  		{"weekLY", Weekly, false},
   832  		{"monthly", Monthly, false},
   833  		{"mONThLy", Monthly, false},
   834  		{"invalid", Never, true},
   835  		{"daily weekly", Never, true},
   836  		{"none", Never, true},
   837  	}
   838  
   839  	for _, test := range cases {
   840  		t.Run(test.value, func(t *testing.T) {
   841  			interval, err := ParseInterval(test.value)
   842  			if test.expected != interval {
   843  				t.Error("Invalid interval value")
   844  			}
   845  			if test.err != (err != nil) {
   846  				t.Error("Invalid error value")
   847  			}
   848  		})
   849  	}
   850  }
   851  
   852  const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
   853  const (
   854  	letterIdxBits = 6                    // 6 bits to represent a letter index
   855  	letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
   856  	letterIdxMax  = 63 / letterIdxBits   // # of letter indices fitting in 63 bits
   857  )
   858  
   859  // See: https://stackoverflow.com/a/31832326/3474438
   860  func randStringBytesMaskImprSrc(n int, src rand.Source) []byte {
   861  	b := make([]byte, n)
   862  	// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
   863  	for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
   864  		if remain == 0 {
   865  			cache, remain = src.Int63(), letterIdxMax
   866  		}
   867  		if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
   868  			b[i] = letterBytes[idx]
   869  			i--
   870  		}
   871  		cache >>= letterIdxBits
   872  		remain--
   873  	}
   874  	return b
   875  }
   876  
   877  func TestLongRunningRotateOld(t *testing.T) {
   878  	dir, err := ioutil.TempDir("", "")
   879  	if err != nil {
   880  		t.Error(err)
   881  	}
   882  	defer os.RemoveAll(dir)
   883  
   884  	start := time.Date(2017, time.December, 06, 0, 0, 0, 0, time.UTC)
   885  
   886  	fileSize := 512 * 1024
   887  
   888  	logDate := start
   889  	// generate files
   890  	var src = rand.NewSource(time.Now().UnixNano())
   891  	for i := 0; i < 64; i++ {
   892  		infoF, infoL := logName("INFO", logDate.Add(1*time.Second))
   893  
   894  		ioutil.WriteFile(filepath.Join(dir, infoF), randStringBytesMaskImprSrc(fileSize, src), 0600)
   895  
   896  		infoSL := filepath.Join(dir, infoL)
   897  		os.Remove(infoSL)                             // ignore err
   898  		os.Symlink(filepath.Join(dir, infoF), infoSL) // ignore err
   899  
   900  		logDate = logDate.Add(24 * time.Hour)
   901  	}
   902  
   903  	// prepare environment
   904  	logDirs = nil
   905  	SetLogDir(dir)
   906  	createLogDirs()
   907  
   908  	// execute rotation
   909  	sb := &syncBuffer{}
   910  
   911  	wg := sync.WaitGroup{}
   912  
   913  	run := func() {
   914  		wg.Add(1)
   915  		defer wg.Done()
   916  		sb.rotateOld(time.Now())
   917  	}
   918  
   919  	go run()
   920  	for i := 0; i < 64; i++ {
   921  		time.Sleep(32 * time.Millisecond)
   922  		go run()
   923  	}
   924  
   925  	wg.Wait()
   926  }
   927  
   928  func BenchmarkHeaderLogging(b *testing.B) {
   929  	for i := 0; i < b.N; i++ {
   930  		buf, _, _ := logging.header(infoLog, 0)
   931  		logging.putBuffer(buf)
   932  	}
   933  }
   934  
   935  func BenchmarkHeaderDisplay(b *testing.B) {
   936  	for i := 0; i < b.N; i++ {
   937  		buf, _, _ := display.header(infoLog, 0)
   938  		display.putBuffer(buf)
   939  	}
   940  }