diff --git a/internal/output/csv.go b/internal/output/csv.go index 22be2b26..ee271828 100644 --- a/internal/output/csv.go +++ b/internal/output/csv.go @@ -7,7 +7,6 @@ import ( "encoding/csv" "fmt" "io" - "os" ) // FormatAsCSV formats data as CSV (with header) and writes it to w. @@ -67,10 +66,10 @@ func writeCSVRows(w io.Writer, rows []map[string]string, cols []string, writeHea flushCSV(cw) } -// flushCSV flushes the csv.Writer and reports any write error to stderr. +// flushCSV flushes the csv.Writer and reports any write error to ErrorSink. func flushCSV(cw *csv.Writer) { cw.Flush() if err := cw.Error(); err != nil { - fmt.Fprintf(os.Stderr, "csv write error: %v\n", err) + writeInternalError("csv write error: %v\n", err) } } diff --git a/internal/output/csv_test.go b/internal/output/csv_test.go index d22248a4..5fd9b879 100644 --- a/internal/output/csv_test.go +++ b/internal/output/csv_test.go @@ -5,6 +5,7 @@ package output import ( "bytes" + "errors" "strings" "testing" ) @@ -150,3 +151,26 @@ func TestFormatAsCSV_SingleObject(t *testing.T) { t.Errorf("output should contain 'Alice', got:\n%s", out) } } + +type failingCSVWriter struct{} + +func (failingCSVWriter) Write(p []byte) (int, error) { + return 0, errors.New("boom") +} + +func TestFormatAsCSV_WriteError_UsesErrorSink(t *testing.T) { + origSink := ErrorSink + var errBuf bytes.Buffer + ErrorSink = &errBuf + defer func() { ErrorSink = origSink }() + + data := []interface{}{ + map[string]interface{}{"name": "Alice"}, + } + + FormatAsCSV(failingCSVWriter{}, data) + + if got := errBuf.String(); got == "" || !strings.Contains(got, "csv write error: boom") { + t.Fatalf("expected csv write error in ErrorSink, got: %q", got) + } +} diff --git a/internal/output/print.go b/internal/output/print.go index c26c2edb..19fa24a1 100644 --- a/internal/output/print.go +++ b/internal/output/print.go @@ -12,12 +12,20 @@ import ( "github.com/larksuite/cli/internal/validate" ) +// ErrorSink receives best-effort internal output formatting errors. +// Tests and embedders may override it to capture these messages. +var ErrorSink io.Writer = os.Stderr + +func writeInternalError(format string, args ...interface{}) { + fmt.Fprintf(ErrorSink, format, args...) +} + // PrintJson prints data as formatted JSON to w. func PrintJson(w io.Writer, data interface{}) { injectNotice(data) b, err := json.MarshalIndent(data, "", " ") if err != nil { - fmt.Fprintf(os.Stderr, "json marshal error: %v\n", err) + writeInternalError("json marshal error: %v\n", err) return } fmt.Fprintln(w, string(b)) @@ -53,7 +61,7 @@ func PrintNdjson(w io.Writer, data interface{}) { emit := func(item interface{}) { b, err := json.Marshal(item) if err != nil { - fmt.Fprintf(os.Stderr, "ndjson marshal error: %v\n", err) + writeInternalError("ndjson marshal error: %v\n", err) return } fmt.Fprintln(w, string(b)) diff --git a/internal/output/print_test.go b/internal/output/print_test.go index 46c13f93..632b1331 100644 --- a/internal/output/print_test.go +++ b/internal/output/print_test.go @@ -99,3 +99,37 @@ func TestPrintJson_NoNotice(t *testing.T) { t.Error("expected no _notice when PendingNotice is nil") } } + +func TestPrintJson_MarshalError_UsesErrorSink(t *testing.T) { + origSink := ErrorSink + var errBuf bytes.Buffer + ErrorSink = &errBuf + defer func() { ErrorSink = origSink }() + + var out bytes.Buffer + PrintJson(&out, map[string]interface{}{"bad": make(chan int)}) + + if out.Len() != 0 { + t.Fatalf("expected no stdout output on marshal error, got: %q", out.String()) + } + if got := errBuf.String(); got == "" || !bytes.Contains(errBuf.Bytes(), []byte("json marshal error:")) { + t.Fatalf("expected json marshal error in ErrorSink, got: %q", got) + } +} + +func TestPrintNdjson_MarshalError_UsesErrorSink(t *testing.T) { + origSink := ErrorSink + var errBuf bytes.Buffer + ErrorSink = &errBuf + defer func() { ErrorSink = origSink }() + + var out bytes.Buffer + PrintNdjson(&out, map[string]interface{}{"bad": make(chan int)}) + + if out.Len() != 0 { + t.Fatalf("expected no stdout output on marshal error, got: %q", out.String()) + } + if got := errBuf.String(); got == "" || !bytes.Contains(errBuf.Bytes(), []byte("ndjson marshal error:")) { + t.Fatalf("expected ndjson marshal error in ErrorSink, got: %q", got) + } +}