diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-28 10:06:29 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-28 12:50:12 +0200 |
| commit | 72b5a6f7b2100d228a0f16171c6c08b8f311f0d4 (patch) | |
| tree | 9a1d0424d2b0943b54323cb04445178488370024 | |
| parent | dbf8980f1bc5428d1eb463505968469617dbf3fc (diff) | |
refactor(task): move debug state into debugConfig struct
DIP/MEDIUM task (UUID 2f64277a): the two bare package-level vars
(debugWriter, debugFile) were mutable shared state that prevented
concurrent test isolation.
Introduce private debugConfig struct and a single package-level
instance (dbg). SetDebugLog and run() are updated to use dbg.writer /
dbg.file. The public API is unchanged so main.go needs no modification.
Add TestSetDebugLog covering: invalid-path error (negative), enable,
log write reaching the file, reconfiguration, and disable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | internal/task/task.go | 34 | ||||
| -rw-r--r-- | internal/task/task_test.go | 58 |
2 files changed, 80 insertions, 12 deletions
diff --git a/internal/task/task.go b/internal/task/task.go index 8eadd18..d8e2e72 100644 --- a/internal/task/task.go +++ b/internal/task/task.go @@ -43,17 +43,27 @@ type Task struct { Annotations []Annotation `json:"annotations"` } -var debugWriter io.Writer -var debugFile *os.File // Track the file handle to close it properly +// debugConfig groups the optional debug-logging state for the task package. +// Collecting related vars into a struct makes the mutable state explicit and +// allows the logger to be swapped or reset cleanly without touching unrelated +// package globals. +type debugConfig struct { + writer io.Writer + file *os.File // tracked separately so it can be closed on reconfiguration +} + +// dbg holds the active debug-logging configuration for this package. +// It is written only via SetDebugLog and read only in run(). +var dbg debugConfig // SetDebugLog enables logging of executed commands to the given file. -// Passing an empty path disables logging. +// Passing an empty path disables logging and closes any previously opened file. func SetDebugLog(path string) error { - // Close existing debug file if open - if debugFile != nil { - debugFile.Close() - debugFile = nil - debugWriter = nil + // Close existing debug file if open before re-configuring. + if dbg.file != nil { + dbg.file.Close() + dbg.file = nil + dbg.writer = nil } if path == "" { @@ -64,8 +74,8 @@ func SetDebugLog(path string) error { if err != nil { return err } - debugFile = f - debugWriter = f + dbg.file = f + dbg.writer = f return nil } @@ -142,8 +152,8 @@ func Export(filters ...string) ([]Task, error) { } func run(args ...string) error { - if debugWriter != nil { - fmt.Fprintln(debugWriter, "task "+strings.Join(args, " ")) + if dbg.writer != nil { + fmt.Fprintln(dbg.writer, "task "+strings.Join(args, " ")) } cmd := exec.Command("task", args...) diff --git a/internal/task/task_test.go b/internal/task/task_test.go index 22233db..2869260 100644 --- a/internal/task/task_test.go +++ b/internal/task/task_test.go @@ -1,11 +1,69 @@ package task import ( + "fmt" "os" "os/exec" + "path/filepath" + "strings" "testing" ) +// TestSetDebugLog exercises the lifecycle of the debug logger: enable, +// disable, and re-enable. It also covers the negative case where the +// target directory does not exist. +func TestSetDebugLog(t *testing.T) { + // Ensure the package-level state is clean before and after. + t.Cleanup(func() { SetDebugLog("") }) //nolint:errcheck + + // Negative: directory does not exist — must return an error. + if err := SetDebugLog("/nonexistent-dir-xyz/debug.log"); err == nil { + t.Error("expected error for non-existent directory, got nil") + } + // After a failed open the package state must remain empty. + if dbg.writer != nil || dbg.file != nil { + t.Error("dbg fields must be nil after a failed SetDebugLog call") + } + + // Positive: enable logging to a temp file. + tmp := t.TempDir() + logPath := filepath.Join(tmp, "debug.log") + if err := SetDebugLog(logPath); err != nil { + t.Fatalf("SetDebugLog failed: %v", err) + } + if dbg.writer == nil || dbg.file == nil { + t.Fatal("dbg fields must be set after a successful SetDebugLog call") + } + + // The run helper uses dbg.writer — verify that a write actually reaches + // the log file. We fake a task invocation by writing directly. + fmt.Fprintln(dbg.writer, "test-entry") + content, err := os.ReadFile(logPath) + if err != nil { + t.Fatalf("read log file: %v", err) + } + if !strings.Contains(string(content), "test-entry") { + t.Errorf("expected log entry in file, got: %q", string(content)) + } + + // Re-enable with a different path — old file must be closed, new one opened. + logPath2 := filepath.Join(tmp, "debug2.log") + if err := SetDebugLog(logPath2); err != nil { + t.Fatalf("second SetDebugLog failed: %v", err) + } + if dbg.file == nil { + t.Error("dbg.file must be non-nil after re-enabling debug log") + } + + // Disable — both fields must return to nil, no error expected. + if err := SetDebugLog(""); err != nil { + t.Fatalf("disable SetDebugLog failed: %v", err) + } + if dbg.writer != nil || dbg.file != nil { + t.Error("dbg fields must be nil after disabling SetDebugLog") + } +} + func TestAddAndExport(t *testing.T) { if _, err := exec.LookPath("task"); err != nil { t.Skip("task command not available") |
