-
Notifications
You must be signed in to change notification settings - Fork 21
[test-improver] Improve tests for logger/FileLogger #2760
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||
| package logger | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import ( | ||||||||||||||||||||||
| "io" | ||||||||||||||||||||||
| "os" | ||||||||||||||||||||||
| "path/filepath" | ||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||
|
|
@@ -11,47 +12,37 @@ import ( | |||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| func TestInitFileLogger(t *testing.T) { | ||||||||||||||||||||||
| // Create a temporary directory for testing | ||||||||||||||||||||||
| tmpDir := t.TempDir() | ||||||||||||||||||||||
| logDir := filepath.Join(tmpDir, "logs") | ||||||||||||||||||||||
| fileName := "test.log" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Initialize the logger | ||||||||||||||||||||||
| err := InitFileLogger(logDir, fileName) | ||||||||||||||||||||||
| require.NoError(t, err, "InitFileLogger failed") | ||||||||||||||||||||||
| defer CloseGlobalLogger() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Check that the log directory was created | ||||||||||||||||||||||
| if _, err := os.Stat(logDir); os.IsNotExist(err) { | ||||||||||||||||||||||
| t.Errorf("Log directory was not created: %s", logDir) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| _, err = os.Stat(logDir) | ||||||||||||||||||||||
| require.NoError(t, err, "Log directory was not created: %s", logDir) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Check that the log file was created | ||||||||||||||||||||||
| logPath := filepath.Join(logDir, fileName) | ||||||||||||||||||||||
| if _, err := os.Stat(logPath); os.IsNotExist(err) { | ||||||||||||||||||||||
| t.Errorf("Log file was not created: %s", logPath) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| _, err = os.Stat(logPath) | ||||||||||||||||||||||
| require.NoError(t, err, "Log file was not created: %s", logPath) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| func TestFileLoggerFallback(t *testing.T) { | ||||||||||||||||||||||
| // Use a non-writable directory (e.g., root on Unix) | ||||||||||||||||||||||
| // This will trigger fallback to stdout | ||||||||||||||||||||||
| logDir := "/root/nonexistent/directory" | ||||||||||||||||||||||
| fileName := "test.log" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Initialize the logger - should not fail, but use fallback | ||||||||||||||||||||||
| err := InitFileLogger(logDir, fileName) | ||||||||||||||||||||||
| require.NoError(t, err, "InitFileLogger should not fail on fallback") | ||||||||||||||||||||||
| defer CloseGlobalLogger() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| globalLoggerMu.RLock() | ||||||||||||||||||||||
| useFallback := globalFileLogger.useFallback | ||||||||||||||||||||||
| logger := globalFileLogger | ||||||||||||||||||||||
| globalLoggerMu.RUnlock() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if !useFallback { | ||||||||||||||||||||||
| // Note: This might not fail on systems where we have root access | ||||||||||||||||||||||
| // In that case, we just verify the logger was initialized | ||||||||||||||||||||||
| t.Logf("Logger initialized without fallback (may have permissions)") | ||||||||||||||||||||||
| require.NotNil(t, logger, "Logger should be initialized even in fallback mode") | ||||||||||||||||||||||
| if !logger.useFallback { | ||||||||||||||||||||||
| t.Logf("Logger initialized without fallback (system may have root access)") | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -64,23 +55,20 @@ func TestFileLoggerLogging(t *testing.T) { | |||||||||||||||||||||
| require.NoError(t, err, "InitFileLogger failed") | ||||||||||||||||||||||
| defer CloseGlobalLogger() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Write some log messages | ||||||||||||||||||||||
| LogInfo("test", "This is an info message") | ||||||||||||||||||||||
| LogWarn("test", "This is a warning message with value: %d", 42) | ||||||||||||||||||||||
| LogError("test", "This is an error message") | ||||||||||||||||||||||
| LogDebug("test", "This is a debug message") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Close the logger to ensure all data is flushed | ||||||||||||||||||||||
| // Close to flush before reading. | ||||||||||||||||||||||
| CloseGlobalLogger() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Read the log file | ||||||||||||||||||||||
| logPath := filepath.Join(logDir, fileName) | ||||||||||||||||||||||
| content, err := os.ReadFile(logPath) | ||||||||||||||||||||||
| require.NoError(t, err, "Failed to read log file") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| logContent := string(content) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Verify log messages are present | ||||||||||||||||||||||
| expectedMessages := []struct { | ||||||||||||||||||||||
| level string | ||||||||||||||||||||||
| message string | ||||||||||||||||||||||
|
|
@@ -92,28 +80,17 @@ func TestFileLoggerLogging(t *testing.T) { | |||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| for _, expected := range expectedMessages { | ||||||||||||||||||||||
| if !strings.Contains(logContent, expected.level) { | ||||||||||||||||||||||
| t.Errorf("Log file does not contain level: %s", expected.level) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if !strings.Contains(logContent, expected.message) { | ||||||||||||||||||||||
| t.Errorf("Log file does not contain message: %s", expected.message) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| assert.True(t, strings.Contains(logContent, "[test]"), "Log file does not contain category: [test]") | ||||||||||||||||||||||
| assert.Contains(t, logContent, expected.level, "Log file missing level %q", expected.level) | ||||||||||||||||||||||
| assert.Contains(t, logContent, expected.message, "Log file missing message %q", expected.message) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| assert.Contains(t, logContent, "[test]", "Log file missing category [test]") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Verify timestamp format (RFC3339) | ||||||||||||||||||||||
| // Each non-empty line should start with a timestamp in brackets. | ||||||||||||||||||||||
| lines := strings.Split(strings.TrimSpace(logContent), "\n") | ||||||||||||||||||||||
| if len(lines) < 4 { | ||||||||||||||||||||||
| t.Errorf("Expected at least 4 log lines, got %d", len(lines)) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| require.GreaterOrEqual(t, len(lines), 4, "Expected at least 4 log lines") | ||||||||||||||||||||||
| for _, line := range lines { | ||||||||||||||||||||||
| if line == "" { | ||||||||||||||||||||||
| continue | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| // Each line should start with a timestamp in brackets | ||||||||||||||||||||||
| if !strings.HasPrefix(line, "[") { | ||||||||||||||||||||||
| t.Errorf("Log line does not start with timestamp: %s", line) | ||||||||||||||||||||||
| if line != "" { | ||||||||||||||||||||||
| assert.True(t, strings.HasPrefix(line, "["), "Log line should start with timestamp bracket: %s", line) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
@@ -123,28 +100,24 @@ func TestFileLoggerAppend(t *testing.T) { | |||||||||||||||||||||
| logDir := filepath.Join(tmpDir, "logs") | ||||||||||||||||||||||
| fileName := "append-test.log" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // First logger session | ||||||||||||||||||||||
| err := InitFileLogger(logDir, fileName) | ||||||||||||||||||||||
| require.NoError(t, err, "InitFileLogger failed") | ||||||||||||||||||||||
| LogInfo("test", "First message") | ||||||||||||||||||||||
| CloseGlobalLogger() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Second logger session - should append | ||||||||||||||||||||||
| // Second session should append to the existing file. | ||||||||||||||||||||||
| err = InitFileLogger(logDir, fileName) | ||||||||||||||||||||||
| require.NoError(t, err, "InitFileLogger failed") | ||||||||||||||||||||||
| require.NoError(t, err, "InitFileLogger failed on second init") | ||||||||||||||||||||||
| LogInfo("test", "Second message") | ||||||||||||||||||||||
| CloseGlobalLogger() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Read the log file | ||||||||||||||||||||||
| logPath := filepath.Join(logDir, fileName) | ||||||||||||||||||||||
| content, err := os.ReadFile(logPath) | ||||||||||||||||||||||
| require.NoError(t, err, "Failed to read log file") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| logContent := string(content) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Verify both messages are present | ||||||||||||||||||||||
| assert.True(t, strings.Contains(logContent, "First message"), "Log file does not contain first message") | ||||||||||||||||||||||
| assert.True(t, strings.Contains(logContent, "Second message"), "Log file does not contain second message") | ||||||||||||||||||||||
| assert.Contains(t, logContent, "First message", "Log file missing first message") | ||||||||||||||||||||||
| assert.Contains(t, logContent, "Second message", "Log file missing second message") | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| func TestFileLoggerConcurrency(t *testing.T) { | ||||||||||||||||||||||
|
|
@@ -156,7 +129,6 @@ func TestFileLoggerConcurrency(t *testing.T) { | |||||||||||||||||||||
| require.NoError(t, err, "InitFileLogger failed") | ||||||||||||||||||||||
| defer CloseGlobalLogger() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Write from multiple goroutines | ||||||||||||||||||||||
| done := make(chan bool, 10) | ||||||||||||||||||||||
| for i := 0; i < 10; i++ { | ||||||||||||||||||||||
| go func(id int) { | ||||||||||||||||||||||
|
|
@@ -167,25 +139,18 @@ func TestFileLoggerConcurrency(t *testing.T) { | |||||||||||||||||||||
| }(i) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Wait for all goroutines to complete | ||||||||||||||||||||||
| for i := 0; i < 10; i++ { | ||||||||||||||||||||||
| <-done | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| CloseGlobalLogger() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Read the log file | ||||||||||||||||||||||
| logPath := filepath.Join(logDir, fileName) | ||||||||||||||||||||||
| content, err := os.ReadFile(logPath) | ||||||||||||||||||||||
| require.NoError(t, err, "Failed to read log file") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Count the number of log lines | ||||||||||||||||||||||
| lines := strings.Split(strings.TrimSpace(string(content)), "\n") | ||||||||||||||||||||||
| // Should have 100 lines (10 goroutines * 10 messages each) | ||||||||||||||||||||||
| expectedLines := 100 | ||||||||||||||||||||||
| if len(lines) != expectedLines { | ||||||||||||||||||||||
| t.Errorf("Expected %d log lines, got %d", expectedLines, len(lines)) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| assert.Len(t, lines, 100, "Expected 100 log lines (10 goroutines × 10 messages)") | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| func TestFileLoggerReadableByOtherProcesses(t *testing.T) { | ||||||||||||||||||||||
|
|
@@ -194,33 +159,27 @@ func TestFileLoggerReadableByOtherProcesses(t *testing.T) { | |||||||||||||||||||||
| fileName := "readable-test.log" | ||||||||||||||||||||||
| logPath := filepath.Join(logDir, fileName) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Initialize the logger | ||||||||||||||||||||||
| err := InitFileLogger(logDir, fileName) | ||||||||||||||||||||||
| require.NoError(t, err, "InitFileLogger failed") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Write some data | ||||||||||||||||||||||
| LogInfo("test", "Testing file readability") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Verify another process can open and read the file without any locks | ||||||||||||||||||||||
| // This simulates another process trying to read the log file | ||||||||||||||||||||||
| // Another goroutine (simulating another process) can open and read the file | ||||||||||||||||||||||
| // while the logger still holds the file descriptor open. | ||||||||||||||||||||||
| readFile, err := os.Open(logPath) | ||||||||||||||||||||||
| require.NoError(t, err, "Failed to open log file for reading") | ||||||||||||||||||||||
| defer readFile.Close() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Read the content to verify it's readable | ||||||||||||||||||||||
| content, err := os.ReadFile(logPath) | ||||||||||||||||||||||
| require.NoError(t, err, "Failed to read log file content") | ||||||||||||||||||||||
| assert.Contains(t, string(content), "Testing file readability", "Log file missing expected content") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| assert.True(t, strings.Contains(string(content), "Testing file readability"), "Log file does not contain expected content") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Close the logger | ||||||||||||||||||||||
| CloseGlobalLogger() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Verify file is still readable after close | ||||||||||||||||||||||
| // File should still be readable after the logger is closed. | ||||||||||||||||||||||
| content, err = os.ReadFile(logPath) | ||||||||||||||||||||||
| require.NoError(t, err, "Failed to read log file after close") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| assert.True(t, strings.Contains(string(content), "Testing file readability"), "Log file does not contain expected content after close") | ||||||||||||||||||||||
| assert.Contains(t, string(content), "Testing file readability", "Log file missing content after close") | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| func TestFileLoggerFlushes(t *testing.T) { | ||||||||||||||||||||||
|
|
@@ -229,18 +188,151 @@ func TestFileLoggerFlushes(t *testing.T) { | |||||||||||||||||||||
| fileName := "flush-test.log" | ||||||||||||||||||||||
| logPath := filepath.Join(logDir, fileName) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Initialize the logger | ||||||||||||||||||||||
| err := InitFileLogger(logDir, fileName) | ||||||||||||||||||||||
| require.NoError(t, err, "InitFileLogger failed") | ||||||||||||||||||||||
| defer CloseGlobalLogger() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Write a message | ||||||||||||||||||||||
| LogInfo("test", "Immediate flush test") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Immediately read the file without closing the logger | ||||||||||||||||||||||
| // This tests that Sync() is being called after each write | ||||||||||||||||||||||
| // Read without closing - Sync() after each write means data is on disk immediately. | ||||||||||||||||||||||
| content, err := os.ReadFile(logPath) | ||||||||||||||||||||||
| require.NoError(t, err, "Failed to read log file") | ||||||||||||||||||||||
| assert.Contains(t, string(content), "Immediate flush test", "Data not flushed to disk immediately after write") | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // TestFileLogger_GetWriter verifies GetWriter returns the underlying file for a real | ||||||||||||||||||||||
| // logger and os.Stdout for the fallback logger. | ||||||||||||||||||||||
| func TestFileLogger_GetWriter(t *testing.T) { | ||||||||||||||||||||||
| t.Run("real logger returns file writer", func(t *testing.T) { | ||||||||||||||||||||||
| tmpDir := t.TempDir() | ||||||||||||||||||||||
| logDir := filepath.Join(tmpDir, "logs") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| err := InitFileLogger(logDir, "test.log") | ||||||||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||||||||
| defer CloseGlobalLogger() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| globalLoggerMu.RLock() | ||||||||||||||||||||||
| logger := globalFileLogger | ||||||||||||||||||||||
| globalLoggerMu.RUnlock() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| require.NotNil(t, logger) | ||||||||||||||||||||||
| w := logger.GetWriter() | ||||||||||||||||||||||
| require.NotNil(t, w, "GetWriter should return non-nil writer") | ||||||||||||||||||||||
| _, isFile := w.(*os.File) | ||||||||||||||||||||||
| assert.True(t, isFile, "GetWriter should return *os.File for real logger") | ||||||||||||||||||||||
|
Comment on lines
+219
to
+222
|
||||||||||||||||||||||
| w := logger.GetWriter() | |
| require.NotNil(t, w, "GetWriter should return non-nil writer") | |
| _, isFile := w.(*os.File) | |
| assert.True(t, isFile, "GetWriter should return *os.File for real logger") | |
| assert.False(t, logger.useFallback, "real logger should not be using fallback") | |
| w := logger.GetWriter() | |
| require.NotNil(t, w, "GetWriter should return non-nil writer") | |
| assert.Same(t, logger.logFile, w, "GetWriter should return the underlying log file for real logger") | |
| assert.NotEqual(t, os.Stdout, w, "real logger writer should not be os.Stdout") |
Copilot
AI
Mar 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "GetWriter implements io.Writer interface" subtest is effectively a no-op because GetWriter() already has return type io.Writer, so the assignment will always compile and doesn’t validate behavior. If the intent is to verify the returned writer is usable, consider asserting it is non-nil and performing a small Write() (and optionally checking the log file contents).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests use a hard-coded
/root/nonexistent/directoryto try to force fallback. This makes behavior environment-dependent (e.g., running tests as root can create/write there, and other platforms may not have/root), and it can also leave artifacts outside the temp test directory. A more deterministic approach is to create a temp dir and chmod it to non-writable (or create a non-writable parent) to reliably trigger the fallback without touching system paths.This issue also appears on line 225 of the same file.