tee: add tests; linter fixes.
Additionally, disable reassign in testing files.
This commit is contained in:
@@ -472,4 +472,5 @@ linters:
|
|||||||
- goconst
|
- goconst
|
||||||
- gosec
|
- gosec
|
||||||
- noctx
|
- noctx
|
||||||
|
- reassign
|
||||||
- wrapcheck
|
- wrapcheck
|
||||||
|
|||||||
48
tee/tee.go
48
tee/tee.go
@@ -17,23 +17,6 @@ type Tee struct {
|
|||||||
Verbose bool
|
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
|
// NewOut writes to standard output only. The file is created, not
|
||||||
// appended to.
|
// appended to.
|
||||||
func NewOut(logFile string) (*Tee, error) {
|
func NewOut(logFile string) (*Tee, error) {
|
||||||
@@ -48,9 +31,32 @@ func NewOut(logFile string) (*Tee, error) {
|
|||||||
return &Tee{f: f}, nil
|
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
|
// Printf formats according to a format specifier and writes to the
|
||||||
// tee instance.
|
// 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...)
|
s := fmt.Sprintf(format, args...)
|
||||||
n, err := os.Stdout.WriteString(s)
|
n, err := os.Stdout.WriteString(s)
|
||||||
if err != nil {
|
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
|
// VPrintf is a variant of Printf that only prints if the Tee's
|
||||||
// Verbose flag is set.
|
// 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 {
|
if t.Verbose {
|
||||||
return t.Printf(format, args...)
|
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
|
// Printf formats according to a format specifier and writes to the
|
||||||
// global tee.
|
// global tee.
|
||||||
func Printf(format string, args ...interface{}) (int, error) {
|
func Printf(format string, args ...any) (int, error) {
|
||||||
return globalTee.Printf(format, args...)
|
return globalTee.Printf(format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// VPrintf calls VPrintf on the global tee instance.
|
// 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...)
|
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