summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-28 10:06:29 +0200
committerPaul Buetow <paul@buetow.org>2026-02-28 12:50:12 +0200
commit72b5a6f7b2100d228a0f16171c6c08b8f311f0d4 (patch)
tree9a1d0424d2b0943b54323cb04445178488370024
parentdbf8980f1bc5428d1eb463505968469617dbf3fc (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.go34
-rw-r--r--internal/task/task_test.go58
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")