diff --git a/internal/app/app.go b/internal/app/app.go index 6e75242..93f7f82 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -10,6 +10,8 @@ import ( ) type Options struct { + // Output is used to write the final output, such as the tables, summary, etc. + Output io.Writer // DisableColor will disable all colors. DisableColor bool // Format will set the output format for tables. @@ -26,12 +28,16 @@ type Options struct { SummaryTableOptions SummaryTableOptions // FollowOutput will follow the raw output as go test is running. - FollowOutput bool + FollowOutput bool // Output to stdout + FollowOutputWriter io.WriteCloser // Output to a file, takes precedence over FollowOutput FollowOutputVerbose bool // Progress will print a single summary line for each package once the package has completed. // Useful for long running test suites. Maybe used with FollowOutput or on its own. - Progress bool + // + // This will output to stdout. + Progress bool + ProgressOutput io.Writer // DisableTableOutput will disable all table output. This is used for testing. DisableTableOutput bool @@ -44,7 +50,7 @@ type Options struct { Compare string } -func Run(w io.Writer, option Options) (int, error) { +func Run(option Options) (int, error) { var reader io.ReadCloser var err error if option.FileName != "" { @@ -58,12 +64,17 @@ func Run(w io.Writer, option Options) (int, error) { } defer reader.Close() + if option.FollowOutputWriter != nil { + defer option.FollowOutputWriter.Close() + } + summary, err := parse.Process( reader, parse.WithFollowOutput(option.FollowOutput), parse.WithFollowVersboseOutput(option.FollowOutputVerbose), - parse.WithWriter(w), + parse.WithWriter(option.FollowOutputWriter), parse.WithProgress(option.Progress), + parse.WithProgressOutput(option.ProgressOutput), ) if err != nil { return 1, err @@ -74,7 +85,7 @@ func Run(w io.Writer, option Options) (int, error) { // Useful for tests that don't need tparse table output. Very useful for testing output from // [parse.Process] if !option.DisableTableOutput { - display(w, summary, option) + display(option.Output, summary, option) } return summary.ExitCode(), nil } diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 881bd88..878e1d7 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "io" "sort" "strings" ) @@ -45,3 +46,14 @@ func minimum(a, b int) int { } return b } + +// DiscardCloser is an io.Writer that implements io.Closer by doing nothing. +// +// https://github.com/golang/go/issues/22823 +type WriteNopCloser struct { + io.Writer +} + +func (WriteNopCloser) Close() error { + return nil +} diff --git a/main.go b/main.go index bf10327..d10e79d 100644 --- a/main.go +++ b/main.go @@ -4,34 +4,37 @@ import ( "errors" "flag" "fmt" + "io" "log" "os" "github.com/mfridman/buildversion" "github.com/mfridman/tparse/internal/app" + "github.com/mfridman/tparse/internal/utils" "github.com/mfridman/tparse/parse" ) // Flags. var ( - vPtr = flag.Bool("v", false, "") - versionPtr = flag.Bool("version", false, "") - hPtr = flag.Bool("h", false, "") - helpPtr = flag.Bool("help", false, "") - allPtr = flag.Bool("all", false, "") - passPtr = flag.Bool("pass", false, "") - skipPtr = flag.Bool("skip", false, "") - showNoTestsPtr = flag.Bool("notests", false, "") - smallScreenPtr = flag.Bool("smallscreen", false, "") - noColorPtr = flag.Bool("nocolor", false, "") - slowPtr = flag.Int("slow", 0, "") - fileNamePtr = flag.String("file", "", "") - formatPtr = flag.String("format", "", "") - followPtr = flag.Bool("follow", false, "") - sortPtr = flag.String("sort", "name", "") - progressPtr = flag.Bool("progress", false, "") - comparePtr = flag.String("compare", "", "") - trimPathPtr = flag.String("trimpath", "", "") + vPtr = flag.Bool("v", false, "") + versionPtr = flag.Bool("version", false, "") + hPtr = flag.Bool("h", false, "") + helpPtr = flag.Bool("help", false, "") + allPtr = flag.Bool("all", false, "") + passPtr = flag.Bool("pass", false, "") + skipPtr = flag.Bool("skip", false, "") + showNoTestsPtr = flag.Bool("notests", false, "") + smallScreenPtr = flag.Bool("smallscreen", false, "") + noColorPtr = flag.Bool("nocolor", false, "") + slowPtr = flag.Int("slow", 0, "") + fileNamePtr = flag.String("file", "", "") + formatPtr = flag.String("format", "", "") + followPtr = flag.Bool("follow", false, "") + followOutputPtr = flag.String("follow-output", "", "") + sortPtr = flag.String("sort", "name", "") + progressPtr = flag.Bool("progress", false, "") + comparePtr = flag.String("compare", "", "") + trimPathPtr = flag.String("trimpath", "", "") // Undocumented flags followVerbosePtr = flag.Bool("follow-verbose", false, "") @@ -57,7 +60,8 @@ Options: -nocolor Disable all colors. (NO_COLOR also supported) -format The output format for tables [basic, plain, markdown]. Default is basic. -file Read test output from a file. - -follow Follow raw output as go test is running. + -follow Follow raw output from go test to stdout. + -follow-output Write raw output from go test to a file (takes precedence over -follow). -progress Print a single summary line for each package. Useful for long running test suites. -compare Compare against a previous test output file. (experimental) -trimpath Remove path prefix from package names in output, simplifying their display. @@ -95,7 +99,7 @@ func main() { format = app.OutputFormatPlain } default: - fmt.Fprintf(os.Stderr, "invalid option:%q. The -format flag must be one of: basic, plain or markdown", *formatPtr) + fmt.Fprintf(os.Stderr, "invalid option:%q. The -format flag must be one of: basic, plain or markdown\n", *formatPtr) return } var sorter parse.PackageSorter @@ -120,10 +124,28 @@ func main() { if _, ok := os.LookupEnv("NO_COLOR"); ok || *noColorPtr { disableColor = true } + + var followOutput io.WriteCloser + switch { + case *followOutputPtr != "": + var err error + followOutput, err = os.Create(*followOutputPtr) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return + } + case *followPtr: + followOutput = os.Stdout + default: + // If no follow flags are set, we should not write to followOutput. + followOutput = utils.WriteNopCloser{Writer: io.Discard} + } // TODO(mf): we should marry the options with the flags to avoid having to do this. options := app.Options{ + Output: os.Stdout, DisableColor: disableColor, FollowOutput: *followPtr, + FollowOutputWriter: followOutput, FollowOutputVerbose: *followVerbosePtr, FileName: *fileNamePtr, TestTableOptions: app.TestTableOptions{ @@ -137,16 +159,17 @@ func main() { Trim: *smallScreenPtr, TrimPath: *trimPathPtr, }, - Format: format, - Sorter: sorter, - ShowNoTests: *showNoTestsPtr, - Progress: *progressPtr, - Compare: *comparePtr, + Format: format, + Sorter: sorter, + ShowNoTests: *showNoTestsPtr, + Progress: *progressPtr, + ProgressOutput: os.Stdout, + Compare: *comparePtr, // Do not expose publicly. DisableTableOutput: false, } - exitCode, err := app.Run(os.Stdout, options) + exitCode, err := app.Run(options) if err != nil { msg := err.Error() if errors.Is(err, parse.ErrNotParsable) { diff --git a/parse/process.go b/parse/process.go index c0e33b5..62f1489 100644 --- a/parse/process.go +++ b/parse/process.go @@ -109,7 +109,7 @@ func Process(r io.Reader, optionsFunc ...OptionsFunc) (*GoTestSummary, error) { // Progress is a special case of follow, where we only print the // progress of the test suite, but not the output. if option.progress && option.w != nil { - printProgress(option.w, e, summary.Packages) + printProgress(option.progressOutput, e, summary.Packages) } summary.AddEvent(e) diff --git a/parse/process_options.go b/parse/process_options.go index f039cd9..8a63b5c 100644 --- a/parse/process_options.go +++ b/parse/process_options.go @@ -9,7 +9,9 @@ type options struct { follow bool followVerbose bool debug bool - progress bool + + progress bool + progressOutput io.Writer } type OptionsFunc func(o *options) @@ -33,3 +35,7 @@ func WithDebug() OptionsFunc { func WithProgress(b bool) OptionsFunc { return func(o *options) { o.progress = b } } + +func WithProgressOutput(w io.Writer) OptionsFunc { + return func(o *options) { o.progressOutput = w } +} diff --git a/tests/follow_test.go b/tests/follow_test.go index 66ba053..e4c9004 100644 --- a/tests/follow_test.go +++ b/tests/follow_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/mfridman/tparse/internal/app" + "github.com/mfridman/tparse/internal/utils" "github.com/mfridman/tparse/parse" ) @@ -35,15 +36,16 @@ func TestFollow(t *testing.T) { } for _, tc := range tt { t.Run(tc.fileName, func(t *testing.T) { + buf := bytes.NewBuffer(nil) inputFile := filepath.Join(base, tc.fileName+".jsonl") options := app.Options{ FileName: inputFile, FollowOutput: true, + FollowOutputWriter: utils.WriteNopCloser{Writer: buf}, FollowOutputVerbose: true, DisableTableOutput: true, } - var buf bytes.Buffer - gotExitCode, err := app.Run(&buf, options) + gotExitCode, err := app.Run(options) if err != nil && !errors.Is(err, tc.err) { t.Fatal(err) } @@ -70,14 +72,15 @@ func TestFollow(t *testing.T) { } for _, tc := range tt { t.Run(tc.fileName, func(t *testing.T) { + buf := bytes.NewBuffer(nil) inputFile := filepath.Join(base, tc.fileName+".jsonl") options := app.Options{ FileName: inputFile, FollowOutput: true, + FollowOutputWriter: utils.WriteNopCloser{Writer: buf}, DisableTableOutput: true, } - var buf bytes.Buffer - gotExitCode, err := app.Run(&buf, options) + gotExitCode, err := app.Run(options) if err != nil && !errors.Is(err, tc.err) { t.Fatal(err) }