tee: add tests; linter fixes.

Additionally, disable reassign in testing files.
This commit is contained in:
2025-11-15 20:17:53 -08:00
parent 66d16acebc
commit e3a6355edb
3 changed files with 225 additions and 21 deletions

View File

@@ -472,4 +472,5 @@ linters:
- goconst
- gosec
- noctx
- reassign
- wrapcheck

View File

@@ -17,23 +17,6 @@ type Tee struct {
Verbose bool
}
func (t *Tee) Write(p []byte) (int, error) {
n, err := os.Stdout.Write(p)
if err != nil {
return n, err
}
if t.f != nil {
return t.f.Write(p)
}
return n, nil
}
// Close calls Close on the underlying file.
func (t *Tee) Close() error {
return t.f.Close()
}
// NewOut writes to standard output only. The file is created, not
// appended to.
func NewOut(logFile string) (*Tee, error) {
@@ -48,9 +31,32 @@ func NewOut(logFile string) (*Tee, error) {
return &Tee{f: f}, nil
}
func (t *Tee) Write(p []byte) (int, error) {
n, err := os.Stdout.Write(p)
if err != nil {
return n, err
}
if t.f != nil {
return t.f.Write(p)
}
return n, nil
}
// Close calls Close on the underlying file if present.
// It is safe to call Close on a Tee with no file; in that case, it returns nil.
func (t *Tee) Close() error {
if t == nil || t.f == nil {
return nil
}
err := t.f.Close()
t.f = nil
return err
}
// Printf formats according to a format specifier and writes to the
// tee instance.
func (t *Tee) Printf(format string, args ...interface{}) (int, error) {
func (t *Tee) Printf(format string, args ...any) (int, error) {
s := fmt.Sprintf(format, args...)
n, err := os.Stdout.WriteString(s)
if err != nil {
@@ -66,7 +72,7 @@ func (t *Tee) Printf(format string, args ...interface{}) (int, error) {
// VPrintf is a variant of Printf that only prints if the Tee's
// Verbose flag is set.
func (t *Tee) VPrintf(format string, args ...interface{}) (int, error) {
func (t *Tee) VPrintf(format string, args ...any) (int, error) {
if t.Verbose {
return t.Printf(format, args...)
}
@@ -87,12 +93,12 @@ func Open(logFile string) error {
// Printf formats according to a format specifier and writes to the
// global tee.
func Printf(format string, args ...interface{}) (int, error) {
func Printf(format string, args ...any) (int, error) {
return globalTee.Printf(format, args...)
}
// VPrintf calls VPrintf on the global tee instance.
func VPrintf(format string, args ...interface{}) (int, error) {
func VPrintf(format string, args ...any) (int, error) {
return globalTee.VPrintf(format, args...)
}

197
tee/tee_test.go Normal file
View File

@@ -0,0 +1,197 @@
package tee_test
import (
"io"
"os"
"path/filepath"
"testing"
tee "git.wntrmute.dev/kyle/goutils/tee"
)
// captureStdout redirects os.Stdout for the duration of fn and returns what was written.
func captureStdout(t *testing.T, fn func()) string {
t.Helper()
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("pipe: %v", err)
}
old := os.Stdout
os.Stdout = w
defer func() { os.Stdout = old }()
fn()
// Close writer to unblock reader and restore stdout
_ = w.Close()
b, _ := io.ReadAll(r)
_ = r.Close()
return string(b)
}
func TestNewOutEmpty_WritesToStdoutOnly(t *testing.T) {
teeInst, err := tee.NewOut("")
if err != nil {
t.Fatalf("NewOut: %v", err)
}
out := captureStdout(t, func() {
var n int
if n, err = teeInst.Write([]byte("abc")); err != nil || n != 3 {
t.Fatalf("Write got n=%d err=%v", n, err)
}
if n, err = teeInst.Printf("-%d-", 7); err != nil || n != len("-7-") {
t.Fatalf("Printf got n=%d err=%v", n, err)
}
})
if out != "abc-7-" {
t.Fatalf("stdout = %q, want %q", out, "abc-7-")
}
}
func TestNewOutWithFile_WritesToBoth(t *testing.T) {
dir := t.TempDir()
logPath := filepath.Join(dir, "log.txt")
teeInst, err := tee.NewOut(logPath)
if err != nil {
t.Fatalf("NewOut: %v", err)
}
defer func() { _ = teeInst.Close() }()
out := captureStdout(t, func() {
if _, err = teeInst.Write([]byte("x")); err != nil {
t.Fatalf("Write: %v", err)
}
if _, err = teeInst.Printf("%s", "y"); err != nil {
t.Fatalf("Printf: %v", err)
}
})
if out != "xy" {
t.Fatalf("stdout = %q, want %q", out, "xy")
}
// Close to flush and release the file before reading
if err = teeInst.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
data, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if string(data) != "xy" {
t.Fatalf("file content = %q, want %q", string(data), "xy")
}
}
func TestVPrintf_VerboseToggle(t *testing.T) {
teeInst := &tee.Tee{} // stdout only
out := captureStdout(t, func() {
if n, err := teeInst.VPrintf("hello"); err != nil || n != 0 {
t.Fatalf("VPrintf (quiet) got n=%d err=%v", n, err)
}
})
if out != "" {
t.Fatalf("stdout = %q, want empty when not verbose", out)
}
teeInst.Verbose = true
out = captureStdout(t, func() {
if n, err := teeInst.VPrintf("%s", "hello"); err != nil || n != len("hello") {
t.Fatalf("VPrintf (verbose) got n=%d err=%v", n, err)
}
})
if out != "hello" {
t.Fatalf("stdout = %q, want %q", out, "hello")
}
}
func TestWrite_StdoutErrorDoesNotWriteToFile(t *testing.T) {
dir := t.TempDir()
logPath := filepath.Join(dir, "log.txt")
teeInst, err := tee.NewOut(logPath)
if err != nil {
t.Fatalf("NewOut: %v", err)
}
defer func() { _ = teeInst.Close() }()
// Replace stdout with a closed pipe writer to force write error.
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("pipe: %v", err)
}
old := os.Stdout
os.Stdout = w
_ = w.Close() // immediately close to cause EPIPE on write
defer func() {
os.Stdout = old
_ = r.Close()
}()
var n int
if n, err = teeInst.Write([]byte("abc")); err == nil {
t.Fatalf("expected error writing to closed stdout, got n=%d err=nil", n)
}
// Ensure file remained empty because stdout write failed first.
_ = teeInst.Close()
data, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if len(data) != 0 {
t.Fatalf("file content = %q, want empty due to stdout failure", string(data))
}
}
func TestGlobal_OpenPrintfVPrintfClose(t *testing.T) {
// Ensure a clean slate for global tee
_ = tee.Close()
tee.SetVerbose(false)
dir := t.TempDir()
logPath := filepath.Join(dir, "glog.txt")
if err := tee.Open(logPath); err != nil {
t.Fatalf("Open: %v", err)
}
out := captureStdout(t, func() {
if _, err := tee.Printf("A"); err != nil {
t.Fatalf("Printf: %v", err)
}
// Not verbose yet, should not print
if n, err := tee.VPrintf("B"); err != nil || n != 0 {
t.Fatalf("VPrintf (quiet) n=%d err=%v", n, err)
}
tee.SetVerbose(true)
if _, err := tee.VPrintf("C%d", 1); err != nil {
t.Fatalf("VPrintf (verbose): %v", err)
}
})
if out != "AC1" {
t.Fatalf("stdout = %q, want %q", out, "AC1")
}
if err := tee.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
data, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if string(data) != "AC1" {
t.Fatalf("file content = %q, want %q", string(data), "AC1")
}
// Reset global tee for other tests/packages
_ = tee.Close()
tee.SetVerbose(false)
}