diff --git a/bazel/container_structure_test.bzl b/bazel/container_structure_test.bzl index cd5d0088..7c8264b7 100644 --- a/bazel/container_structure_test.bzl +++ b/bazel/container_structure_test.bzl @@ -44,10 +44,13 @@ fi """ def _structure_test_impl(ctx): + # --test-report writes to the file specified by $XML_OUTPUT_FILE + # --test-report-format junit ensures bazel-compatible JUnit XML output + # stdout uses default text format for human-readable output (captured as test.log) fixed_args = [ "--test-report $XML_OUTPUT_FILE", - "--output junit", - "--junit-suite-name $TEST_TARGET" + "--test-report-format junit", + "--junit-suite-name $TEST_TARGET", ] test_bin = ctx.toolchains["@container_structure_test//bazel:structure_test_toolchain_type"].st_info.binary jq_bin = ctx.toolchains["@aspect_bazel_lib//lib:jq_toolchain_type"].jqinfo.bin diff --git a/cmd/container-structure-test/app/cmd/test.go b/cmd/container-structure-test/app/cmd/test.go index 069b1464..b64043b7 100644 --- a/cmd/container-structure-test/app/cmd/test.go +++ b/cmd/container-structure-test/app/cmd/test.go @@ -60,20 +60,30 @@ func NewCmdTest(out io.Writer) *cobra.Command { return test.ValidateArgs(opts) }, RunE: func(cmd *cobra.Command, _ []string) error { - if opts.TestReport != "" { - // Force JsonOutput - if opts.Output == unversioned.Text { - opts.JSON = true - opts.Output = unversioned.Json + // Open test report file if specified + var reportOut io.Writer + reportFormatFlag := cmd.Flags().Lookup("test-report-format") - logrus.Warn("raw text format unsupported for writing output file, defaulting to JSON") + if opts.TestReport != "" { + // Validate report format - only json and junit are supported + if opts.TestReportFormat == unversioned.Text { + if reportFormatFlag.Changed { + // User explicitly set --test-report-format text, which is not supported + return fmt.Errorf("--test-report-format does not support 'text'; use 'json' or 'junit'") + } + // Default to JSON for backward compatibility (Text is zero value when flag not set) + opts.TestReportFormat = unversioned.Json } + testReportFile, err := os.Create(opts.TestReport) if err != nil { return err } - rootCmd.SetOutput(testReportFile) - out = testReportFile // override writer + defer testReportFile.Close() + reportOut = testReportFile + } else if reportFormatFlag.Changed { + // User specified --test-report-format without --test-report + return fmt.Errorf("--test-report-format requires --test-report to be specified") } if opts.Quiet { @@ -86,7 +96,7 @@ func NewCmdTest(out io.Writer) *cobra.Command { opts.Output = unversioned.Json } - return run(out) + return run(out, reportOut) }, } @@ -94,7 +104,7 @@ func NewCmdTest(out io.Writer) *cobra.Command { return testCmd } -func run(out io.Writer) error { +func run(out, reportOut io.Writer) error { args = &drivers.DriverConfig{ Image: opts.ImagePath, Save: opts.Save, @@ -153,7 +163,7 @@ func run(out io.Writer) error { } var r string if r, err = daemon.Write(tag, img); err != nil { - logrus.Fatalf("error loading oci layout into daemon: %v, %s", err) + logrus.Fatalf("error loading oci layout into daemon: %v", err) } // For some reason, daemon.Write doesn't return errors for some edge cases. // We should always print what the daemon sent back so that errors are transparent. @@ -205,7 +215,7 @@ func run(out io.Writer) error { channel := make(chan interface{}, 1) go runTests(out, channel, args, driverImpl) // TODO(nkubala): put a sync.WaitGroup here - return test.ProcessResults(out, opts.Output, opts.JunitSuiteName, channel) + return test.ProcessResults(out, reportOut, opts.Output, opts.TestReportFormat, opts.JunitSuiteName, channel) } func runTests(out io.Writer, channel chan interface{}, args *drivers.DriverConfig, driverImpl func(drivers.DriverConfig) (drivers.Driver, error)) { @@ -250,5 +260,7 @@ func AddTestFlags(cmd *cobra.Command) { cmd.Flags().StringArrayVarP(&opts.ConfigFiles, "config", "c", []string{}, "test config files") cmd.MarkFlagRequired("config") - cmd.Flags().StringVar(&opts.TestReport, "test-report", "", "generate test report and write it to specified file (supported format: json, junit; default: json)") + cmd.Flags().StringVar(&opts.TestReport, "test-report", "", "generate test report and write it to specified file") + cmd.Flags().VarP(&opts.TestReportFormat, "test-report-format", "", "format for the test report file (json, junit)") + cmd.Flags().Lookup("test-report-format").DefValue = "json" } diff --git a/cmd/container-structure-test/app/cmd/test/util.go b/cmd/container-structure-test/app/cmd/test/util.go index ba912e34..6216bfba 100644 --- a/cmd/container-structure-test/app/cmd/test/util.go +++ b/cmd/container-structure-test/app/cmd/test/util.go @@ -105,7 +105,14 @@ func Parse(fp string, args *drivers.DriverConfig, driverImpl func(drivers.Driver return tests, nil } -func ProcessResults(out io.Writer, format unversioned.OutputValue, junitSuiteName string, c chan interface{}) error { +// ProcessResults processes test results and writes output to the appropriate destinations. +// - out: primary output writer (stdout) - format controlled by the format parameter +// - reportOut: optional test report writer - format controlled by reportFormat parameter +// - format: output format for the primary output (text, json, or junit) +// - reportFormat: output format for the report file (json or junit) +// - junitSuiteName: name for the JUnit test suite +// - c: channel of test results +func ProcessResults(out, reportOut io.Writer, format, reportFormat unversioned.OutputValue, junitSuiteName string, c chan interface{}) error { totalPass := 0 totalFail := 0 totalDuration := time.Duration(0) @@ -116,7 +123,7 @@ func ProcessResults(out io.Writer, format unversioned.OutputValue, junitSuiteNam } for _, r := range results { if format == unversioned.Text { - // output individual results if we're not in json mode + // output individual results if we're in text mode output.OutputResult(out, r) } if r.IsPass() { @@ -139,11 +146,20 @@ func ProcessResults(out io.Writer, format unversioned.OutputValue, junitSuiteNam Fail: totalFail, Duration: totalDuration, } - if format == unversioned.Json || format == unversioned.Junit { - // only output results here if we're in json mode - summary.Results = results + + summary.Results = results + + // Write final summary to primary output (stdout) + if outputErr := output.FinalResults(out, format, junitSuiteName, summary); outputErr != nil { + return outputErr + } + + // If a test report file is specified, write to it using the report format + if reportOut != nil { + if reportErr := output.FinalResults(reportOut, reportFormat, junitSuiteName, summary); reportErr != nil { + return reportErr + } } - output.FinalResults(out, format, junitSuiteName, summary) return err } diff --git a/pkg/config/options.go b/pkg/config/options.go index ae24c3c3..23cc81a6 100644 --- a/pkg/config/options.go +++ b/pkg/config/options.go @@ -26,6 +26,7 @@ type StructureTestOptions struct { Platform string Metadata string TestReport string + TestReportFormat unversioned.OutputValue ConfigFiles []string JSON bool