diff --git a/logger.go b/logger.go index a89a6ef..52d72b8 100644 --- a/logger.go +++ b/logger.go @@ -94,13 +94,13 @@ func (lf *LogFile) truncate() error { func prettyByteSize(b int64) string { bf := float64(b) - for _, unit := range []string{"", "K", "M", "G", "T", "P", "E", "Z"} { + for _, unit := range []string{"", "K", "M", "G", "T", "P"} { if math.Abs(bf) < 1024.0 { return fmt.Sprintf("%3.1f%sB", bf, unit) } bf /= 1024.0 } - return fmt.Sprintf("%.1fYB", bf) + return fmt.Sprintf("%.1fEB", bf) } func compressOldFile(fname string) error { diff --git a/logger_test.go b/logger_test.go index 222ac0b..08073c4 100644 --- a/logger_test.go +++ b/logger_test.go @@ -2,7 +2,10 @@ package main import ( "fmt" + "os" "testing" + + "github.com/stretchr/testify/assert" ) func TestTruncate(t *testing.T) { @@ -11,14 +14,123 @@ func TestTruncate(t *testing.T) { mkDirForTest(t, fmt.Sprintf("%s/tmp", tmpDir)) rulesJsonFp := "testData/app_over_size.log" - expected := fmt.Sprintf("%s/tmp/app.log", tmpDir) + tmpLf := fmt.Sprintf("%s/tmp/app.log", tmpDir) - cpFileForTest(t, rulesJsonFp, expected) + cpFileForTest(t, rulesJsonFp, tmpLf) - cfg := &LogFile{ - path: expected, + lf := &LogFile{ + path: tmpLf, } + lf.path = tmpLf // Continue from here + isEmpty := isFileEmpty(t, tmpLf) + assert.Equal(t, isEmpty, false) + err := lf.truncate() + assert.Equal(t, err, nil) + isEmpty = isFileEmpty(t, tmpLf) + assert.Equal(t, isEmpty, true) +} + +func TestMakeCopyTo(t *testing.T) { + tmpDir := t.TempDir() + + mkDirForTest(t, fmt.Sprintf("%s/tmp", tmpDir)) + rulesJsonFp := "testData/app_over_size.log" + + tmpLf := fmt.Sprintf("%s/tmp/app.log", tmpDir) + + cpFileForTest(t, rulesJsonFp, tmpLf) + + newLocation := fmt.Sprintf("%s/tmp/app.1.log", tmpDir) + + lf := &LogFile{ + path: tmpLf, + } + + lf.path = tmpLf + + isEmpty := isFileEmpty(t, tmpLf) + assert.Equal(t, isEmpty, false) + + err := lf.makeCopyTo(newLocation) + assert.Equal(t, err, nil) + isEmpty = isFileEmpty(t, tmpLf) + assert.Equal(t, isEmpty, false) + ok := areFilesTheSame(t, lf.path, newLocation) + assert.Equal(t, ok, true) +} + +func TestPrettyByteSize(t *testing.T) { + tests := map[string]struct { + input int + want string + }{ + "KB": {input: 5675, want: "5.5KB"}, + "MB": {input: 7060600, want: "6.7MB"}, + "GB": {input: 8000000000, want: "7.5GB"}, + "TB": {input: 1300007000000, want: "1.2TB"}, + "EB": {input: 1300007000000000000, want: "1.1EB"}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + got := prettyByteSize(int64(tc.input)) + assert.Equal(t, got, tc.want) + }) + } +} + +func TestCompressFile(t *testing.T) { + tmpDir := t.TempDir() + + mkDirForTest(t, fmt.Sprintf("%s/tmp", tmpDir)) + rulesJsonFp := "testData/app_over_size.log" + + tmpLf := fmt.Sprintf("%s/tmp/app.log", tmpDir) + + cpFileForTest(t, rulesJsonFp, tmpLf) + + expected := fmt.Sprintf("%s/tmp/app.log.gz", tmpDir) + + err := compressOldFile(tmpLf) + assert.Equal(t, err, nil) + exists := doesFileExist(tmpLf) + assert.Equal(t, exists, false) + exists = doesFileExist(expected) + assert.Equal(t, exists, true) +} + +func TestRotate(t *testing.T) { + tmpDir := t.TempDir() + + mkDirForTest(t, fmt.Sprintf("%s/tmp", tmpDir)) + rulesJsonFp := "testData/app_over_size.log" + + tmpLf := fmt.Sprintf("%s/tmp/app.log", tmpDir) + + cpFileForTest(t, rulesJsonFp, tmpLf) + + lf := &LogFile{ + path: tmpLf, + } + + fd, _ := os.Open(tmpLf) + lf.handle = fd + + isEmpty := isFileEmpty(t, tmpLf) + assert.Equal(t, isEmpty, false) + err := lf.rotate(true) + assert.Equal(t, err, nil) + exists := doesFileExist(tmpLf) + assert.Equal(t, exists, true) + isEmpty = isFileEmpty(t, tmpLf) + assert.Equal(t, isEmpty, true) + + expected := walkMatch(t, tmpDir, "*.gz") + assert.NotEmpty(t, expected) + t.Logf("%+v", expected) + + assert.NotEqual(t, lf.handle, fd) } diff --git a/testUtils_test.go b/testUtils_test.go index 71ab79b..9051323 100644 --- a/testUtils_test.go +++ b/testUtils_test.go @@ -1,11 +1,13 @@ package main import ( + "bytes" "encoding/json" "errors" "io" "io/fs" "os" + "path/filepath" "testing" ) @@ -81,6 +83,29 @@ func doesFileExist(name string) bool { return err == nil } +// Derived from here (https://stackoverflow.com/a/55300382) +func walkMatch(t *testing.T, root, pattern string) []string { + var matches []string + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + if matched, err := filepath.Match(pattern, filepath.Base(path)); err != nil { + return err + } else if matched { + matches = append(matches, path) + } + return nil + }) + if err != nil { + t.Fatal(err) + } + return matches +} + // func doesFileExist(name string) bool { // _, err := os.Stat(name) // return !errors.Is(err, fs.ErrNotExist) @@ -113,3 +138,72 @@ func isFileEmpty(t *testing.T, name string) bool { } return finfo.Size() < 1 } + +// Derived from here (https://stackoverflow.com/a/73411967) +func areFilesTheSame(t *testing.T, fp_1 string, fp_2 string) bool { + chunkSize := 4 * 1024 + + // shortcuts: check file metadata + finfo_1, err := os.Stat(fp_1) + if err != nil { + t.Fatal(err) + } + + finfo_2, err := os.Stat(fp_2) + if err != nil { + t.Fatal(err) + } + + // are inputs are literally the same file? + if os.SameFile(finfo_1, finfo_2) { + return true + } + + // do inputs at least have the same size? + if finfo_1.Size() != finfo_2.Size() { + return false + } + + // long way: compare contents + fd_1, err := os.Open(fp_1) + if err != nil { + t.Fatal(err) + } + + defer fd_1.Close() + + fd_2, err := os.Open(fp_2) + if err != nil { + t.Fatal(err) + } + defer fd_2.Close() + + bfd_1 := make([]byte, chunkSize) + bfd_2 := make([]byte, chunkSize) + for { + n1, err1 := io.ReadFull(fd_1, bfd_1) + n2, err2 := io.ReadFull(fd_2, bfd_2) + + // https://pkg.go.dev/io#Reader + // > Callers should always process the n > 0 bytes returned + // > before considering the error err. Doing so correctly + // > handles I/O errors that happen after reading some bytes + // > and also both of the allowed EOF behaviors. + + if !bytes.Equal(bfd_1[:n1], bfd_2[:n2]) { + return false + } + + if (err1 == io.EOF && err2 == io.EOF) || (err1 == io.ErrUnexpectedEOF && err2 == io.ErrUnexpectedEOF) { + return true + } + + // some other error, like a dropped network connection or a bad transfer + if err1 != nil { + t.Fatal(err1) + } + if err2 != nil { + t.Fatal(err2) + } + } +}