Skip to content

Commit e8c466f

Browse files
authored
[test] Add tests for logger.ServerFileLogger.Close (#3159)
## Test Coverage Improvement: `ServerFileLogger.Close` ### Function Analyzed - **Package**: `internal/logger` - **Function**: `ServerFileLogger.Close` - **File**: `internal/logger/server_file_logger.go:108` - **Previous Coverage**: 58.8% - **New Coverage**: 94.1% - **Complexity**: High (nil-receiver guard, loop over file map, sync+close error handling with first-error-wins tracking) ### Why This Function? `ServerFileLogger.Close` had the lowest real test coverage (58.8%) among non-env-gated functions in the testable subset of the codebase. Its complexity comes from: - Nil-receiver guard (callable on `nil *ServerFileLogger`) - Iterating a map of open log files, running two operations per file - First-error-wins tracking: `firstErr` captures the first Sync or Close failure without being overwritten by later errors - Unconditional map clearing even when errors occur All of these paths except the "Sync succeeds but Close fails" branch (which requires NFS-level failure to trigger) are now exercised. ### Tests Added | Test | Path Covered | |------|--------------| | `TestServerFileLoggerClose_NilReceiver` | Nil-receiver guard in `Close()` | | `TestServerFileLoggerClose_EmptyFiles` | Loop body never entered; maps are still reset | | `TestServerFileLoggerClose_SyncError` | `file.Sync()` error → `firstErr` set | | `TestServerFileLoggerClose_FirstErrorTracking` | Multiple files both failing; first-error-wins over multiple iterations | | `TestServerFileLoggerClose_ValidFiles` | Happy-path close (all files flush and close cleanly) | | `TestServerFileLoggerClose_CloseTwice` | Idempotent: second call is a no-op with empty maps | | `TestServerFileLoggerLog_NilReceiver` | Nil-receiver guard in `Log()` | | `TestServerFileLoggerLog_SyncError` | `file.Sync()` error inside `Log()` does not panic | | `TestServerFileLoggerGetOrCreate_FileCreationError` | `os.OpenFile` failure in `getOrCreateLogger()` gracefully falls back to `LogDebug` | ### Coverage Report ```` Before: Close 58.8% Log 84.6% getOrCreateLogger 90.9% After: Close 94.1% (+35.3 pp) Log 100.0% (+15.4 pp) getOrCreateLogger 100.0% (+9.1 pp) ``` Overall `internal/logger` package: **88.8% → 90.7%** ### Test Execution ``` === RUN TestServerFileLoggerClose_NilReceiver --- PASS: TestServerFileLoggerClose_NilReceiver (0.00s) === RUN TestServerFileLoggerClose_EmptyFiles --- PASS: TestServerFileLoggerClose_EmptyFiles (0.00s) === RUN TestServerFileLoggerClose_SyncError --- PASS: TestServerFileLoggerClose_SyncError (0.00s) === RUN TestServerFileLoggerClose_FirstErrorTracking --- PASS: TestServerFileLoggerClose_FirstErrorTracking (0.00s) === RUN TestServerFileLoggerClose_ValidFiles --- PASS: TestServerFileLoggerClose_ValidFiles (0.00s) === RUN TestServerFileLoggerClose_CloseTwice --- PASS: TestServerFileLoggerClose_CloseTwice (0.00s) === RUN TestServerFileLoggerLog_NilReceiver --- PASS: TestServerFileLoggerLog_NilReceiver (0.00s) === RUN TestServerFileLoggerLog_SyncError --- PASS: TestServerFileLoggerLog_SyncError (0.00s) === RUN TestServerFileLoggerGetOrCreate_FileCreationError --- PASS: TestServerFileLoggerGetOrCreate_FileCreationError (0.00s) PASS ok github.com/github/gh-aw-mcpg/internal/logger 0.006s ```` All existing tests continue to pass. --- *Generated by Test Coverage Improver* *Next run should target `writeToFile` (63.6%) or `logEntry` (66.7%)* > Generated by [Test Coverage Improver](https://github.com/github/gh-aw-mcpg/actions/runs/23978202716/agentic_workflow) · [◷](https://github.com/search?q=repo%3Agithub%2Fgh-aw-mcpg+%22gh-aw-workflow-id%3A+test-coverage-improver%22&type=pullrequests) <!-- gh-aw-agentic-workflow: Test Coverage Improver, engine: copilot, model: auto, id: 23978202716, workflow_id: test-coverage-improver, run: https://github.com/github/gh-aw-mcpg/actions/runs/23978202716 --> <!-- gh-aw-workflow-id: test-coverage-improver -->
2 parents f18739e + 20be3f8 commit e8c466f

File tree

1 file changed

+201
-0
lines changed

1 file changed

+201
-0
lines changed

internal/logger/server_file_logger_test.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package logger
22

33
import (
4+
"log"
45
"os"
56
"path/filepath"
67
"strings"
@@ -263,3 +264,203 @@ func TestServerFileLoggerPreservesUnifiedView(t *testing.T) {
263264
lines := strings.Split(strings.TrimSpace(string(unifiedContent)), "\n")
264265
assert.GreaterOrEqual(t, len(lines), 3, "unified log should have at least 3 messages")
265266
}
267+
268+
// TestServerFileLoggerClose_NilReceiver verifies that Close() handles nil receiver gracefully.
269+
func TestServerFileLoggerClose_NilReceiver(t *testing.T) {
270+
var sfl *ServerFileLogger
271+
err := sfl.Close()
272+
assert.NoError(t, err, "Close() on nil receiver should return nil without panicking")
273+
}
274+
275+
// TestServerFileLoggerClose_EmptyFiles verifies that Close() succeeds when no files are open.
276+
func TestServerFileLoggerClose_EmptyFiles(t *testing.T) {
277+
sfl := &ServerFileLogger{
278+
loggers: make(map[string]*log.Logger),
279+
files: make(map[string]*os.File),
280+
}
281+
282+
err := sfl.Close()
283+
284+
assert.NoError(t, err, "Close() with no open files should return nil")
285+
assert.Empty(t, sfl.loggers, "loggers map should be cleared after Close()")
286+
assert.Empty(t, sfl.files, "files map should be cleared after Close()")
287+
}
288+
289+
// TestServerFileLoggerClose_SyncError verifies that Close() returns the first error when file.Sync() fails.
290+
// This covers the sync error path and firstErr tracking within the Close() loop.
291+
func TestServerFileLoggerClose_SyncError(t *testing.T) {
292+
require := require.New(t)
293+
assert := assert.New(t)
294+
295+
tmpDir := t.TempDir()
296+
297+
// Create a real file, then close it to invalidate the descriptor.
298+
// Subsequent Sync() and Close() calls on the *os.File will return an error.
299+
f, err := os.CreateTemp(tmpDir, "test-server-*.log")
300+
require.NoError(err, "CreateTemp should succeed")
301+
f.Close() // Close to invalidate the file descriptor
302+
303+
sfl := &ServerFileLogger{
304+
loggers: map[string]*log.Logger{
305+
"server1": log.New(f, "", 0),
306+
},
307+
files: map[string]*os.File{
308+
"server1": f,
309+
},
310+
}
311+
312+
closeErr := sfl.Close()
313+
314+
// Sync() on a closed file descriptor should fail, so Close() must return an error.
315+
assert.Error(closeErr, "Close() should return an error when Sync() fails on an invalidated file")
316+
317+
// Maps must be cleared even when errors occur.
318+
assert.Empty(sfl.loggers, "loggers map should be cleared after Close() even on error")
319+
assert.Empty(sfl.files, "files map should be cleared after Close() even on error")
320+
}
321+
322+
// TestServerFileLoggerClose_FirstErrorTracking verifies that Close() returns the first error encountered
323+
// and does not overwrite it with subsequent errors when multiple files fail.
324+
func TestServerFileLoggerClose_FirstErrorTracking(t *testing.T) {
325+
require := require.New(t)
326+
assert := assert.New(t)
327+
328+
tmpDir := t.TempDir()
329+
330+
// Create two files and close both to make all operations fail.
331+
f1, err := os.CreateTemp(tmpDir, "test-server1-*.log")
332+
require.NoError(err)
333+
f1.Close()
334+
335+
f2, err := os.CreateTemp(tmpDir, "test-server2-*.log")
336+
require.NoError(err)
337+
f2.Close()
338+
339+
sfl := &ServerFileLogger{
340+
loggers: map[string]*log.Logger{
341+
"server1": log.New(f1, "", 0),
342+
"server2": log.New(f2, "", 0),
343+
},
344+
files: map[string]*os.File{
345+
"server1": f1,
346+
"server2": f2,
347+
},
348+
}
349+
350+
closeErr := sfl.Close()
351+
352+
// At least one error should be returned; the first one wins.
353+
assert.Error(closeErr, "Close() should return an error when files have already been closed")
354+
355+
// Maps must be cleared regardless.
356+
assert.Empty(sfl.loggers, "loggers map should be cleared after Close()")
357+
assert.Empty(sfl.files, "files map should be cleared after Close()")
358+
}
359+
360+
// TestServerFileLoggerClose_ValidFiles verifies that Close() returns nil when all files close successfully.
361+
func TestServerFileLoggerClose_ValidFiles(t *testing.T) {
362+
require := require.New(t)
363+
assert := assert.New(t)
364+
365+
tmpDir := t.TempDir()
366+
367+
// Create two valid open files.
368+
f1, err := os.OpenFile(filepath.Join(tmpDir, "server1.log"), os.O_CREATE|os.O_WRONLY, 0644)
369+
require.NoError(err)
370+
371+
f2, err := os.OpenFile(filepath.Join(tmpDir, "server2.log"), os.O_CREATE|os.O_WRONLY, 0644)
372+
require.NoError(err)
373+
374+
sfl := &ServerFileLogger{
375+
loggers: map[string]*log.Logger{
376+
"server1": log.New(f1, "", 0),
377+
"server2": log.New(f2, "", 0),
378+
},
379+
files: map[string]*os.File{
380+
"server1": f1,
381+
"server2": f2,
382+
},
383+
}
384+
385+
closeErr := sfl.Close()
386+
387+
assert.NoError(closeErr, "Close() should return nil when all files close successfully")
388+
assert.Empty(sfl.loggers, "loggers map should be cleared after Close()")
389+
assert.Empty(sfl.files, "files map should be cleared after Close()")
390+
}
391+
392+
// TestServerFileLoggerClose_CloseTwice verifies that calling Close() twice does not panic.
393+
// After the first Close(), maps are empty so the second Close() is a no-op.
394+
func TestServerFileLoggerClose_CloseTwice(t *testing.T) {
395+
assert := assert.New(t)
396+
397+
sfl := &ServerFileLogger{
398+
loggers: make(map[string]*log.Logger),
399+
files: make(map[string]*os.File),
400+
}
401+
402+
err1 := sfl.Close()
403+
err2 := sfl.Close()
404+
405+
assert.NoError(err1, "First Close() should return nil")
406+
assert.NoError(err2, "Second Close() should return nil (no-op)")
407+
}
408+
409+
// TestServerFileLoggerLog_NilReceiver verifies that Log() on a nil ServerFileLogger does not panic.
410+
func TestServerFileLoggerLog_NilReceiver(t *testing.T) {
411+
var sfl *ServerFileLogger
412+
assert.NotPanics(t, func() {
413+
sfl.Log("server1", LogLevelInfo, "test", "message")
414+
}, "Log() on nil receiver should not panic")
415+
}
416+
417+
// TestServerFileLoggerLog_SyncError verifies that Log() handles file sync failures gracefully.
418+
// This covers the file.Sync() error path inside the Log() method.
419+
func TestServerFileLoggerLog_SyncError(t *testing.T) {
420+
require := require.New(t)
421+
422+
tmpDir := t.TempDir()
423+
424+
// Create a file then close it to simulate a broken file descriptor.
425+
f, err := os.CreateTemp(tmpDir, "test-sync-*.log")
426+
require.NoError(err)
427+
f.Close()
428+
429+
sfl := &ServerFileLogger{
430+
logDir: tmpDir,
431+
loggers: map[string]*log.Logger{
432+
"server1": log.New(f, "", 0),
433+
},
434+
files: map[string]*os.File{
435+
"server1": f,
436+
},
437+
}
438+
439+
// Log() should not panic even when Sync() fails internally.
440+
assert.NotPanics(t, func() {
441+
sfl.Log("server1", LogLevelInfo, "test", "message after close")
442+
}, "Log() should not panic when file sync fails")
443+
}
444+
445+
// TestServerFileLoggerGetOrCreate_FileCreationError verifies that getOrCreateLogger handles
446+
// file creation failures gracefully, falling back to the debug logger.
447+
func TestServerFileLoggerGetOrCreate_FileCreationError(t *testing.T) {
448+
// Use a non-existent non-writable path to force os.OpenFile to fail.
449+
sfl := &ServerFileLogger{
450+
logDir: "/nonexistent/path/that/does/not/exist",
451+
loggers: make(map[string]*log.Logger),
452+
files: make(map[string]*os.File),
453+
useFallback: false, // not in fallback mode; will attempt to create files
454+
}
455+
456+
// Log() should not panic. It falls back to LogDebug when file creation fails.
457+
assert.NotPanics(t, func() {
458+
sfl.Log("server1", LogLevelInfo, "test", "message")
459+
}, "Log() should not panic when file creation fails")
460+
461+
// Since getOrCreateLogger returned an error, the file should not be in the map.
462+
sfl.mu.RLock()
463+
_, exists := sfl.files["server1"]
464+
sfl.mu.RUnlock()
465+
assert.False(t, exists, "files map should not contain server1 after creation failure")
466+
}

0 commit comments

Comments
 (0)