tee: add tests; linter fixes.
Additionally, disable reassign in testing files.
This commit is contained in:
@@ -472,4 +472,5 @@ linters:
|
||||
- goconst
|
||||
- gosec
|
||||
- noctx
|
||||
- reassign
|
||||
- wrapcheck
|
||||
|
||||
48
tee/tee.go
48
tee/tee.go
@@ -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
197
tee/tee_test.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user