Merge pull request #127667 from zylxjtu/dev

Add log file rotation
This commit is contained in:
Kubernetes Prow Robot
2025-05-15 01:05:13 -07:00
committed by GitHub
6 changed files with 780 additions and 9 deletions

View File

@@ -15,6 +15,66 @@ Why do we need this?
- Nowadays, the `--log-file` parameter is deprecated for Kubernetes components
and should not be used anymore. `kube-log-runner` is a direct replacement.
## Flags
- -flush-interval
The `-flush-interval` flag is a duration flag that specifies how frequently the log
file content is flushed to disk. The default value is 0, meaning no periodic flushing.
If set to a non-zero value, the log file is flushed at the specified interval.
Type: Duration
Default: 0 (flushing disabled)
Usage: When set to non-zero, the log file is flushed every specified interval.
While this may not be necessary on Linux, on Windows, it ensures that recent log
entries are written to disk in near real-time, which is particularly useful for
users who need to monitor logs as they are generated without delay.
- -log-file-size
The `-log-file-size` flag is an optional string flag that sets a size limit for
the log file, triggering automatic log rotation when the specified size is reached.
This is especially useful in production environments where the log file may become
too large to view effectively.
Beware that rotation can happen at arbitrary points in the byte stream emitted by the command.
This can lead to splitting a log entry into two parts, with the first part in one file
and the second part in the next file.
Type: String (expects a value in Resource.Quantity format, such as 10M or 500K)
Default: "0" (disabled, no automatic rotation of log files)
Usage: When set to a positive value, the log file will rotate upon reaching the specified
size limit. The current log files contents will be saved to a backup file, and a new log
file will be created at the path specified by the `-log-file` flag, ready for future log entries.
Backup File Naming Convention:
`<original-file-name>-<timestamp><file-extension>`.
* `<original-file-name>`: The name of the original log file, without the file extension.
* `<timestamp>`: A timestamp is added to each backup files name to uniquely identify it
based on the time it was created. The timestamp follows the format "20060102-150405".
For example, a backup created on June 2, 2006, at 3:04:05 PM would include this timestamp.
* `<file-extension>`: The original files extension (e.g., .log) remains unchanged.
This naming convention ensures easy organization and retrieval of rotated log files based on their creation time.
- -log-file-age
The `-log-file-age` flag is an optional time duration setting that defines how long
old backup log files are retained. This flag is used alongside log rotation (enabled
by setting a positive value for -log-file-size) to help manage storage by removing
outdated backup logs.
Type: Duration
Default: 0 (disabled, no automatic deletion of backup files)
Usage: When -log-file-age is set to a positive duration (e.g., 24h for 24 hours)
and log rotation is enabled, backup log files will be automatically deleted if
the time when they were created (as encoded in the file name) is older than the
specified duration from the current time.
This ensures that only recent backup logs are kept, preventing accumulation of old logs
and reducing storage usage.
For example instead of running kube-apiserver like this:
```bash
"/bin/sh",
@@ -49,6 +109,17 @@ kube-log-runner -log-file=/tmp/log echo "hello world"
# Copy into log file and print to stdout (same as 2>&1 | tee -a /tmp/log).
kube-log-runner -log-file=/tmp/log -also-stdout echo "hello world"
# Copy into log file and print to stdout (same as 2>&1 | tee -a /tmp/log),
# will flush the logging file in 5s,
# rotate the log file when its size exceedes 10 MB
kube-log-runner -flush-interval=5s -log-file=/tmp/log -log-file-size=10M -also-stdout echo "hello world"
# Copy into log file and print to stdout (same as 2>&1 | tee -a /tmp/log),
# will flush the logging file in 10s,
# rotate the log file when its size exceedes 10 MB,
# and clean up old rotated log files when their age are older than 168h (7 days)
kube-log-runner -flush-interval=10s -log-file=/tmp/log -log-file-size=10M -log-file-age=168h -also-stdout echo "hello world"
# Redirect only stdout into log file (same as 1>>/tmp/log).
kube-log-runner -log-file=/tmp/log -redirect-stderr=false echo "hello world"
```

View File

@@ -0,0 +1,205 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logrotation
import (
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
const timeLayout string = "20060102-150405"
type rotationFile struct {
// required, the max size of the log file in bytes, 0 means no rotation
maxSize int64
// required, the max age of the log file, 0 means no cleanup
maxAge time.Duration
filePath string
mut sync.Mutex
file *os.File
currentSize int64
lasSyncTime time.Time
flushInterval time.Duration
}
func Open(filePath string, flushInterval time.Duration, maxSize int64, maxAge time.Duration) (io.WriteCloser, error) {
w := &rotationFile{
filePath: filePath,
maxSize: maxSize,
maxAge: maxAge,
flushInterval: flushInterval,
}
logFile, err := os.OpenFile(w.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil, err
}
w.file = logFile
w.lasSyncTime = time.Now()
if w.maxSize > 0 {
info, err := os.Stat(w.filePath)
if err != nil {
return nil, err
}
w.currentSize = info.Size()
}
return w, nil
}
// Write implements the io.Writer interface.
func (w *rotationFile) Write(p []byte) (n int, err error) {
w.mut.Lock()
defer w.mut.Unlock()
n, err = w.file.Write(p)
if err != nil {
return 0, err
}
if w.flushInterval > 0 && time.Since(w.lasSyncTime) >= w.flushInterval {
err = w.file.Sync()
if err != nil {
return 0, err
}
w.lasSyncTime = time.Now()
}
if w.maxSize > 0 {
w.currentSize += int64(len(p))
// if file size over maxsize rotate the log file
if w.currentSize >= w.maxSize {
err = w.rotate()
if err != nil {
return 0, err
}
}
}
return n, nil
}
func (w *rotationFile) rotate() error {
// Get the file extension
ext := filepath.Ext(w.filePath)
// Remove the extension from the filename
pathWithoutExt := strings.TrimSuffix(w.filePath, ext)
rotateFilePath := pathWithoutExt + "-" + time.Now().Format(timeLayout) + ext
if w.filePath == rotateFilePath {
return nil
}
err := w.file.Close()
if err != nil {
return err
}
err = os.Rename(w.filePath, rotateFilePath)
if err != nil {
return err
}
w.file, err = os.OpenFile(w.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
w.currentSize = 0
if w.maxAge > 0 {
go func() {
err = w.clean(pathWithoutExt, ext)
}()
}
return nil
}
// Clean up the old log files in the format of
// <basename>-<timestamp><ext>.
// This should be safe enough to avoid false deletion.
// This will work for multiple restarts of the same program.
func (w *rotationFile) clean(pathWithoutExt string, ext string) error {
ageTime := time.Now().Add(-w.maxAge)
directory := filepath.Dir(pathWithoutExt)
basename := filepath.Base(pathWithoutExt) + "-"
dir, err := os.ReadDir(directory)
if err != nil {
return err
}
err = nil
for _, v := range dir {
if strings.HasPrefix(v.Name(), basename) && strings.HasSuffix(v.Name(), ext) {
// Remove the prefix and suffix
trimmed := strings.TrimPrefix(v.Name(), basename)
trimmed = strings.TrimSuffix(trimmed, ext)
_, err = time.Parse(timeLayout, trimmed)
if err == nil {
info, errInfo := v.Info()
if errInfo != nil {
err = errInfo
// Ignore the error while continue with the next clenup
continue
}
if ageTime.After(info.ModTime()) {
err = os.Remove(filepath.Join(directory, v.Name()))
if err != nil {
// Ignore the error while continue with the next clenup
continue
}
}
}
}
}
return err
}
func (w *rotationFile) Close() error {
w.mut.Lock()
defer w.mut.Unlock()
// Explicitly call file.Sync() to ensure data is written to disk
err := w.file.Sync()
if err != nil {
return err
}
err = w.file.Close()
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,238 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logrotation
import (
"os"
"path/filepath"
"testing"
"time"
)
func TestLogrotationWrite(t *testing.T) {
tests := []struct {
name string
flushInterval time.Duration
maxSize int64
maxAge time.Duration
rotationExpected bool
cleanupExpected bool
}{
{
name: "no rotation",
flushInterval: 0,
maxSize: 0,
maxAge: 10 * time.Hour,
rotationExpected: false,
cleanupExpected: false,
},
{
name: "Rotation with flushInterval, maxSize, no cleanup",
flushInterval: 5 * time.Second,
maxSize: int64(1024 * 1024),
maxAge: time.Duration(0),
rotationExpected: true,
cleanupExpected: false,
},
{
name: "Rotation with maxSize and cleanup with maxAge",
flushInterval: 0,
maxSize: int64(1024 * 1024),
maxAge: 24 * time.Hour,
rotationExpected: true,
cleanupExpected: true,
},
{
name: "Rotation with flushInterval, maxSize and cleanup with maxAge",
flushInterval: 10 * time.Second,
maxSize: int64(1024 * 1024),
maxAge: 24 * time.Hour,
rotationExpected: true,
cleanupExpected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "logrotation_test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
t.Cleanup(func() {
if err = os.RemoveAll(tmpDir); err != nil {
t.Errorf("Failed to remove temp directory: %v", err)
}
})
logFilePath := filepath.Join(tmpDir, "test.log")
flushInterval := tt.flushInterval
maxSize := tt.maxSize
maxAge := tt.maxAge
rotationFile, err := Open(logFilePath, flushInterval, maxSize, maxAge)
if err != nil {
t.Fatalf("Failed to open RotationFile: %v", err)
}
t.Cleanup(func() {
if err := rotationFile.Close(); err != nil {
t.Errorf("Failed to close rotationFile: %v", err)
}
})
testData := []byte("This is a test log entry.")
n, err := rotationFile.Write(testData)
if err != nil {
t.Fatalf("Failed to write to RotationFile: %v", err)
}
if n != len(testData) {
t.Errorf("Expected to write %d bytes, but wrote %d bytes", len(testData), n)
}
// Check if data is written to the file
content, err := os.ReadFile(logFilePath)
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
if string(content) != string(testData) {
t.Errorf("Expected log file content to be %q, but got %q", string(testData), string(content))
}
// Write more data to trigger rotation
largeData := make([]byte, 1024*1024) // 1 MB
n, err = rotationFile.Write(largeData)
if err != nil {
t.Fatalf("Failed to write large data to RotationFile: %v", err)
}
if n != len(largeData) {
t.Errorf("Expected to write %d bytes, but wrote %d bytes", len(largeData), n)
}
// Check if rotation happened
rotatedFiles, err := filepath.Glob(filepath.Join(tmpDir, "test-*.log"))
if err != nil {
t.Fatalf("Failed to list rotated files: %v", err)
}
if tt.rotationExpected {
numberFiles := 1
if len(rotatedFiles) != numberFiles {
t.Errorf("Expected %d rotated log files, but found %d", numberFiles, len(rotatedFiles))
}
// Check if new log file is created
newContent, err := os.ReadFile(logFilePath)
if err != nil {
t.Fatalf("Failed to read new log file: %v", err)
}
// Rotation after the write, new log file should be empty
if len(newContent) != 0 {
t.Errorf("Expected new log file content to be 0 bytes, but got %d bytes", len(newContent))
}
// Check if content is append to the rotated log file
content, err := os.ReadFile(rotatedFiles[0])
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
if len(content) != len(testData)+len(largeData) {
t.Errorf("Expected log file content to be %d bytes, but got %d bytes", len(testData)+len(largeData), len(content))
}
} else {
numberFiles := 0
if len(rotatedFiles) != numberFiles {
t.Errorf("Expected %d rotated log files, but found %d", numberFiles, len(rotatedFiles))
}
// Check if content is append to the existing log file
content, err := os.ReadFile(logFilePath)
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
if len(content) != len(testData)+len(largeData) {
t.Errorf("Expected log file content to be %d bytes, but got %d bytes", len(testData)+len(largeData), len(content))
}
}
// Check if rotated file was able to be cleaned up
if len(rotatedFiles) != 0 {
err = os.Chtimes(rotatedFiles[0], time.Now(), time.Now().AddDate(0, 0, -10))
if err != nil {
t.Fatalf("Failed to change access time of rotated file: %v", err)
}
}
// Trigger another rotation and check if the old rotated file has been cleaned up
time.Sleep(1 * time.Second)
largeData2 := make([]byte, 1024*1025)
_, err = rotationFile.Write(largeData2)
if err != nil {
t.Fatalf("Failed to write to RotationFile: %v", err)
}
rotatedFiles2, err := filepath.Glob(filepath.Join(tmpDir, "test-*.log"))
if err != nil {
t.Fatalf("Failed to list rotated files: %v", err)
}
// Read the content of the log file
content, err = os.ReadFile(logFilePath)
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
if !tt.rotationExpected {
if len(rotatedFiles2) != 0 {
t.Errorf("Expected rotated log files to be 0, but found %d", len(rotatedFiles))
}
if len(content) != len(testData)+len(largeData)+len(largeData2) {
t.Errorf("Expected log file content to be %d bytes, but got %d bytes", len(testData)+len(largeData)+len(largeData2), len(content))
}
} else {
if tt.cleanupExpected {
// clean up is performed in a async goroutine, wait for it to finish
end := time.Now().Add(5 * time.Second)
for time.Now().Before(end) {
rotatedFiles2, err = filepath.Glob(filepath.Join(tmpDir, "test-*.log"))
if err != nil {
t.Fatalf("Failed to list rotated files: %v", err)
}
if len(rotatedFiles2) == 1 {
break
}
time.Sleep(10 * time.Millisecond)
}
rotatedFiles2, err = filepath.Glob(filepath.Join(tmpDir, "test-*.log"))
if err != nil {
t.Fatalf("Failed to list rotated files: %v", err)
}
if len(rotatedFiles2) != 1 {
t.Errorf("Expected rotated log files to be 1, but found %d", len(rotatedFiles2))
}
if rotatedFiles[0] == rotatedFiles2[0] {
t.Errorf("Expected rotated log files to be different, but found same")
}
} else if len(rotatedFiles2) != 2 {
t.Errorf("Expected rotated log files to be 2, but found %d", len(rotatedFiles2))
}
// Rotation after the write, new log file should be empty
if len(content) != 0 {
t.Errorf("Expected new log file content to be 0 bytes, but got %d bytes", len(content))
}
}
})
}
}

View File

@@ -0,0 +1,40 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package logrotation
import (
"fmt"
"io"
"time"
)
type RotationStub struct{}
func OpenStub(filePath string, flushInterval time.Duration, maxSize int64, maxAge time.Duration) (io.WriteCloser, error) {
w := &RotationStub{}
fmt.Printf("filePath: %s, flushInterval: %s, maxSize: %d, maxAge: %s", filePath, flushInterval, maxSize, maxAge)
return w, nil
}
// write func to satisfy io.writer interface
func (w *RotationStub) Write(p []byte) (int, error) {
return len(p), nil
}
func (w *RotationStub) Close() error {
return nil
}

View File

@@ -26,23 +26,42 @@ import (
"os/signal"
"strings"
"syscall"
"time"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/component-base/logs/kube-log-runner/internal/logrotation"
)
type OpenFunc func(filePath string, flushInterval time.Duration, maxSize int64, maxAge time.Duration) (io.WriteCloser, error)
var (
logFilePath = flag.String("log-file", "", "If non-empty, save stdout to this file")
alsoToStdOut = flag.Bool("also-stdout", false, "useful with log-file, log to standard output as well as the log file")
redirectStderr = flag.Bool("redirect-stderr", true, "treat stderr same as stdout")
flushInterval *time.Duration
logFilePath *string
logFileSize *resource.QuantityValue
logFileAge *time.Duration
alsoToStdOut *bool
redirectStderr *bool
)
func initFlags() {
logFileSize = &resource.QuantityValue{}
flushInterval = flag.Duration("flush-interval", 0, "interval to flush log file to disk, default value 0, if non-zero, flush log file every interval")
logFilePath = flag.String("log-file", "", "If non-empty, save stdout to this file")
flag.Var(logFileSize, "log-file-size", "useful with log-file, in format of resource.quantity, default value 0, if non-zero, rotate log file when it reaches this size")
logFileAge = flag.Duration("log-file-age", 0, "useful with log-file-size, in format of timeDuration, if non-zero, remove log files older than this duration")
alsoToStdOut = flag.Bool("also-stdout", false, "useful with log-file, log to standard output as well as the log file")
redirectStderr = flag.Bool("redirect-stderr", true, "treat stderr same as stdout")
}
func main() {
initFlags()
flag.Parse()
if err := configureAndRun(); err != nil {
if err := configureAndRun(logrotation.Open); err != nil {
log.Fatal(err)
}
}
func configureAndRun() error {
func configureAndRun(open OpenFunc) error {
var (
outputStream io.Writer = os.Stdout
errStream io.Writer = os.Stderr
@@ -53,8 +72,19 @@ func configureAndRun() error {
return fmt.Errorf("not enough arguments to run")
}
maxSize := logFileSize.Value()
if maxSize < 0 {
return fmt.Errorf("log-file-size must be non-negative quantity")
}
// Check the time.duration is not negative
if *logFileAge < 0 {
return fmt.Errorf("log-file-age must be non-negative")
}
if logFilePath != nil && *logFilePath != "" {
logFile, err := os.OpenFile(*logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
logFile, err := open(*logFilePath, *flushInterval, maxSize, *logFileAge)
if err != nil {
return fmt.Errorf("failed to create log file %v: %w", *logFilePath, err)
}
@@ -95,10 +125,10 @@ func configureAndRun() error {
// cmdInfo generates a useful look at what the command is for printing/debug.
func cmdInfo(cmd *exec.Cmd) string {
return fmt.Sprintf(
`Command env: (log-file=%v, also-stdout=%v, redirect-stderr=%v)
`Command env: (enable-flush=%v, log-file=%v, log-file-size=%v, log-file-age=%v, also-stdout=%v, redirect-stderr=%v)
Run from directory: %v
Executable path: %v
Args (comma-delimited): %v`, *logFilePath, *alsoToStdOut, *redirectStderr,
Args (comma-delimited): %v`, *flushInterval, *logFilePath, logFileSize.Value(), *logFileAge, *alsoToStdOut, *redirectStderr,
cmd.Dir, cmd.Path, strings.Join(cmd.Args, ","),
)
}

View File

@@ -0,0 +1,187 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"bytes"
"flag"
"os"
"testing"
"k8s.io/component-base/logs/kube-log-runner/internal/logrotation"
)
// TestConfigureAndRun tests configureAndRun with various flag inputs.
func TestConfigureAndRun(t *testing.T) {
tests := []struct {
name string
commandLine []string
outputWanted string
}{
{
name: "No_log-file_flag",
commandLine: []string{"echo", "hello"},
outputWanted: "hello\n",
},
{
name: "No_log-file_flag_with_extra_other_flags",
commandLine: []string{"-flush-interval=5s", "-log-file-size=1Gi", "-log-file-age=24h30m", "echo", "hello"},
outputWanted: "hello\n",
},
{
name: "log-file_flag_only",
commandLine: []string{"-log-file=test.log", "echo", "hello"},
outputWanted: "filePath: test.log, flushInterval: 0s, maxSize: 0, maxAge: 0s",
},
{
name: "log-file_flag_with_enable-flush_flag",
commandLine: []string{"-log-file=test.log", "-flush-interval=10s", "echo", "hello"},
outputWanted: "filePath: test.log, flushInterval: 10s, maxSize: 0, maxAge: 0s",
},
{
name: "log-file_flag_with_enable-flush_flag_and_log-file-size_flag",
commandLine: []string{"-log-file=test.log", "-flush-interval=1m", "-log-file-size=15M", "echo", "hello"},
outputWanted: "filePath: test.log, flushInterval: 1m0s, maxSize: 15000000, maxAge: 0s",
},
{
name: "Invalid_CPU_format_log-file-size_flag",
commandLine: []string{"-log-file=test.log", "-flush-interval=5s", "-log-file-size=125m", "echo", "hello"},
outputWanted: "filePath: test.log, flushInterval: 5s, maxSize: 1, maxAge: 0s",
},
{
name: "log-file_flag_with_enable-flush_flag_and_log-file-size_flag_and_log-file-age_flag",
commandLine: []string{"-log-file=test.log", "-flush-interval=5s", "-log-file-size=1Gi", "-log-file-age=24h30m", "echo", "hello"},
outputWanted: "filePath: test.log, flushInterval: 5s, maxSize: 1073741824, maxAge: 24h30m0s",
},
{
name: "log-file_flag_with_enable-flush_flag_and_log-file-size_flag_and_log-file-age_flag_and_also-stdout_flag",
commandLine: []string{"-log-file=test.log", "-flush-interval=5s", "-log-file-size=1500", "-log-file-age=24h30m", "-also-stdout", "echo", "hello"},
outputWanted: "filePath: test.log, flushInterval: 5s, maxSize: 1500, maxAge: 24h30m0shello\n",
},
{
name: "log-file_flag_with_enable-flush_flag_and_log-file-size_flag_and_log-file-age_flag_and_also-stdout_flag_and_redirect-stderr",
commandLine: []string{"-log-file=test.log", "-flush-interval=5s", "-log-file-size=1500", "-log-file-age=24h30m", "-also-stdout", "-redirect-stderr=false", "echo", "hello"},
outputWanted: "filePath: test.log, flushInterval: 5s, maxSize: 1500, maxAge: 24h30m0shello\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resetFlags()
err := flag.CommandLine.Parse(tt.commandLine)
if err != nil {
t.Fatalf("Failed to parse commandline: %v", err)
return
}
// Capture the log output and compare
originalStdout := os.Stdout
reader, writer, err := os.Pipe()
if err != nil {
t.Fatalf("Failed to create pipe: %v", err)
return
}
defer func() {
err = reader.Close()
if err != nil {
t.Fatalf("Failed to close pipe reader: %v", err)
return
}
}()
os.Stdout = writer
var buf bytes.Buffer
err = configureAndRun(logrotation.OpenStub)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
err = writer.Close()
if err != nil {
t.Fatalf("Failed to close pipe writer: %v", err)
return
}
// <-done
_, err = buf.ReadFrom(reader)
if err != nil {
t.Fatalf("Failed to read from pipe: %v", err)
return
}
output := buf.String()
if output != tt.outputWanted {
t.Errorf("Expected '%s' but got '%s'", tt.outputWanted, output)
}
// Restore original os.Stdout
os.Stdout = originalStdout
})
}
}
func TestNegativeConfigureAndRun(t *testing.T) {
tests := []struct {
name string
commandLine []string
errorWanted string
}{
{
name: "Empty_commandline",
commandLine: []string{},
errorWanted: "not enough arguments to run",
},
{
name: "Invalid_log-file-size_flag",
commandLine: []string{"-log-file-size=10MM", "test"},
errorWanted: "invalid value \"10MM\" for flag -log-file-size: unable to parse quantity's suffix",
},
{
name: "Negative_log-file-size_flag",
commandLine: []string{"-log-file-size=-10M", "test"},
errorWanted: "log-file-size must be non-negative quantity",
},
{
name: "Invalid_log-file-age_flag",
commandLine: []string{"-log-file-age=-10h", "test"},
errorWanted: "log-file-age must be non-negative",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resetFlags()
err := flag.CommandLine.Parse(tt.commandLine)
if err != nil {
if err.Error() != tt.errorWanted {
t.Errorf("Expected error: %v, got: %v", tt.errorWanted, err)
}
return
}
err = configureAndRun(logrotation.OpenStub)
if err == nil || err.Error() != tt.errorWanted {
t.Errorf("Expected error: %v, got: %v", tt.errorWanted, err)
}
})
}
}
func resetFlags() {
// Reinitialize the flag to the default value
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
initFlags()
}