Repository: gotestyourself/gotestsum
Pull request #288 (2022-12-02 18:20:48)

Hide empty packages on junit output
Followup to #282 and #283. I am leaving the failed golden test in here, because I think perhaps it might be a bug.  The original golden test output for junit.xml includes the following section, which my code hides:

```xml
<testsuite tests="0" failures="0" time="0.001000" name="gotest.tools/gotestsum/testjson/internal/badmain" timestamp="0001-01-01T00:00:00Z">
	<properties>
		<property name="go.version" value="go7.7.7"></property>
	</properties>
	<testcase classname="" name="TestMain" time="0.000000">
		<failure message="Failed" type="">sometimes main can exit 2&#xA;FAIL&#x9;gotest.tools/gotestsum/testjson/internal/badmain&#x9;0.001s&#xA;</failure>
	</testcase>
</testsuite>
```

This looks wrong to me, since `tests="0"` and `failures="0"`, yet there's clearly a failing testcase being simulated. Not sure if it has to do with the empty classname or if a failure like this is likely to occur or not, but it could conceivably get eaten by a test suite during display.  Not sure how the different libraries handle having both values set to zero and still having a non-skipped test case, but I definitely want to confirm before I tell remove it from the golden file.

Also worth noting: I also tried `len(pkg.Passed) ==0 && len(pkg.Failed)==0 && len(pkg.Skipped)==0` and no change.


Patch:
diff --git a/cmd/flags.go b/cmd/flags.go
index a5d811b..dc45a99 100644
--- a/cmd/flags.go
+++ b/cmd/flags.go
@@ -132,3 +132,11 @@ func (s *stringSlice) Set(raw string) error {
 func (s *stringSlice) Type() string {
 	return "list"
 }
+
+func truthyFlag(s string) bool {
+	switch strings.ToLower(s) {
+	case "true", "yes", "1":
+		return true
+	}
+	return false
+}
diff --git a/cmd/handler.go b/cmd/handler.go
index 135eade..440fd4d 100644
--- a/cmd/handler.go
+++ b/cmd/handler.go
@@ -96,6 +96,7 @@ func writeJUnitFile(opts *options, execution *testjson.Execution) error {
 		ProjectName:             opts.junitProjectName,
 		FormatTestSuiteName:     opts.junitTestSuiteNameFormat.Value(),
 		FormatTestCaseClassname: opts.junitTestCaseClassnameFormat.Value(),
+		HideEmptyPackages:       opts.junitHideEmptyPackages,
 	})
 }
 
diff --git a/cmd/main.go b/cmd/main.go
index bfc1d01..8eacaa0 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -93,6 +93,9 @@ func setupFlags(name string) (*pflag.FlagSet, *options) {
 	flags.StringVar(&opts.junitProjectName, "junitfile-project-name",
 		lookEnvWithDefault("GOTESTSUM_JUNITFILE_PROJECT_NAME", ""),
 		"name of the project used in the junit.xml file")
+	flags.BoolVar(&opts.junitHideEmptyPackages, "junitfile-hide-empty-pkg",
+		truthyFlag(lookEnvWithDefault("GOTESTSUM_JUNIT_HIDE_EMPTY_PKG", "")),
+		"omit packages with no tests from the junit.xml file")
 
 	flags.IntVar(&opts.rerunFailsMaxAttempts, "rerun-fails", 0,
 		"rerun failed tests until they all pass, or attempts exceeds maximum. Defaults to max 2 reruns when enabled.")
@@ -160,6 +163,7 @@ type options struct {
 	junitTestSuiteNameFormat     *junitFieldFormatValue
 	junitTestCaseClassnameFormat *junitFieldFormatValue
 	junitProjectName             string
+	junitHideEmptyPackages       bool
 	rerunFailsMaxAttempts        int
 	rerunFailsMaxInitialFailures int
 	rerunFailsReportFile         string
diff --git a/internal/junitxml/report.go b/internal/junitxml/report.go
index cd23b1f..f821e2e 100644
--- a/internal/junitxml/report.go
+++ b/internal/junitxml/report.go
@@ -72,6 +72,7 @@ type Config struct {
 	ProjectName             string
 	FormatTestSuiteName     FormatFunc
 	FormatTestCaseClassname FormatFunc
+	HideEmptyPackages       bool
 	// This is used for tests to have a consistent timestamp
 	customTimestamp string
 	customElapsed   string
@@ -104,6 +105,9 @@ func generate(exec *testjson.Execution, cfg Config) JUnitTestSuites {
 	}
 	for _, pkgname := range exec.Packages() {
 		pkg := exec.Package(pkgname)
+		if cfg.HideEmptyPackages && pkg.IsEmpty() {
+			continue
+		}
 		junitpkg := JUnitTestSuite{
 			Name:       cfg.FormatTestSuiteName(pkgname),
 			Tests:      pkg.Total,


Test patch:
diff --git a/cmd/testdata/gotestsum-help-text b/cmd/testdata/gotestsum-help-text
index e220b94..83c9823 100644
--- a/cmd/testdata/gotestsum-help-text
+++ b/cmd/testdata/gotestsum-help-text
@@ -11,6 +11,7 @@ Flags:
       --hide-summary summary                        hide sections of the summary: skipped,failed,errors,output (default none)
       --jsonfile string                             write all TestEvents to file
       --junitfile string                            write a JUnit XML file
+      --junitfile-hide-empty-pkg                    omit packages with no tests from the junit.xml file
       --junitfile-project-name string               name of the project used in the junit.xml file
       --junitfile-testcase-classname field-format   format the testcase classname field as: full, relative, short (default full)
       --junitfile-testsuite-name field-format       format the testsuite name field as: full, relative, short (default full)
diff --git a/internal/junitxml/report_test.go b/internal/junitxml/report_test.go
index 14e8afb..38d309c 100644
--- a/internal/junitxml/report_test.go
+++ b/internal/junitxml/report_test.go
@@ -29,6 +29,21 @@ func TestWrite(t *testing.T) {
 	golden.Assert(t, out.String(), "junitxml-report.golden")
 }
 
+func TestWrite_HideEmptyPackages(t *testing.T) {
+	out := new(bytes.Buffer)
+	exec := createExecution(t)
+
+	env.Patch(t, "GOVERSION", "go7.7.7")
+	err := Write(out, exec, Config{
+		ProjectName:       "test",
+		HideEmptyPackages: true,
+		customTimestamp:   new(time.Time).Format(time.RFC3339),
+		customElapsed:     "2.1",
+	})
+	assert.NilError(t, err)
+	golden.Assert(t, out.String(), "junitxml-report-skip-empty.golden")
+}
+
 func createExecution(t *testing.T) *testjson.Execution {
 	exec, err := testjson.ScanTestOutput(testjson.ScanConfig{
 		Stdout: readTestData(t, "out"),
diff --git a/internal/junitxml/testdata/junitxml-report-skip-empty.golden b/internal/junitxml/testdata/junitxml-report-skip-empty.golden
new file mode 100644
index 0000000..432accd
--- /dev/null
+++ b/internal/junitxml/testdata/junitxml-report-skip-empty.golden
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<testsuites name="test" tests="59" failures="13" errors="1" time="2.1">
+	<testsuite tests="0" failures="0" time="0.001000" name="gotest.tools/gotestsum/testjson/internal/badmain" timestamp="0001-01-01T00:00:00Z">
+		<properties>
+			<property name="go.version" value="go7.7.7"></property>
+		</properties>
+		<testcase classname="" name="TestMain" time="0.000000">
+			<failure message="Failed" type="">sometimes main can exit 2&#xA;FAIL&#x9;gotest.tools/gotestsum/testjson/internal/badmain&#x9;0.001s&#xA;</failure>
+		</testcase>
+	</testsuite>
+	<testsuite tests="18" failures="0" time="0.000000" name="gotest.tools/gotestsum/testjson/internal/good" timestamp="0001-01-01T00:00:00Z">
+		<properties>
+			<property name="go.version" value="go7.7.7"></property>
+		</properties>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestSkipped" time="0.000000">
+			<skipped message="=== RUN   TestSkipped&#xA;    good_test.go:23: &#xA;--- SKIP: TestSkipped (0.00s)&#xA;"></skipped>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestSkippedWitLog" time="0.000000">
+			<skipped message="=== RUN   TestSkippedWitLog&#xA;    good_test.go:27: the skip message&#xA;--- SKIP: TestSkippedWitLog (0.00s)&#xA;"></skipped>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestPassed" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestPassedWithLog" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestPassedWithStdout" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestWithStderr" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/a/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/a" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/b/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/b" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/c/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/c" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/d/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess/d" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestNestedSuccess" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestParallelTheFirst" time="0.010000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestParallelTheThird" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/good" name="TestParallelTheSecond" time="0.010000"></testcase>
+	</testsuite>
+	<testsuite tests="12" failures="8" time="0.020000" name="gotest.tools/gotestsum/testjson/internal/parallelfails" timestamp="0001-01-01T00:00:00Z">
+		<properties>
+			<property name="go.version" value="go7.7.7"></property>
+		</properties>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures/a" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedParallelFailures/a&#xA;=== PAUSE TestNestedParallelFailures/a&#xA;=== CONT  TestNestedParallelFailures/a&#xA;    fails_test.go:50: failed sub a&#xA;    --- FAIL: TestNestedParallelFailures/a (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures/d" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedParallelFailures/d&#xA;=== PAUSE TestNestedParallelFailures/d&#xA;=== CONT  TestNestedParallelFailures/d&#xA;    fails_test.go:50: failed sub d&#xA;    --- FAIL: TestNestedParallelFailures/d (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures/c" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedParallelFailures/c&#xA;=== PAUSE TestNestedParallelFailures/c&#xA;=== CONT  TestNestedParallelFailures/c&#xA;    fails_test.go:50: failed sub c&#xA;    --- FAIL: TestNestedParallelFailures/c (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures/b" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedParallelFailures/b&#xA;=== PAUSE TestNestedParallelFailures/b&#xA;=== CONT  TestNestedParallelFailures/b&#xA;    fails_test.go:50: failed sub b&#xA;    --- FAIL: TestNestedParallelFailures/b (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestNestedParallelFailures" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedParallelFailures&#xA;--- FAIL: TestNestedParallelFailures (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestParallelTheFirst" time="0.010000">
+			<failure message="Failed" type="">=== RUN   TestParallelTheFirst&#xA;=== PAUSE TestParallelTheFirst&#xA;=== CONT  TestParallelTheFirst&#xA;    fails_test.go:29: failed the first&#xA;--- FAIL: TestParallelTheFirst (0.01s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestParallelTheThird" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestParallelTheThird&#xA;=== PAUSE TestParallelTheThird&#xA;=== CONT  TestParallelTheThird&#xA;    fails_test.go:41: failed the third&#xA;--- FAIL: TestParallelTheThird (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestParallelTheSecond" time="0.010000">
+			<failure message="Failed" type="">=== RUN   TestParallelTheSecond&#xA;=== PAUSE TestParallelTheSecond&#xA;=== CONT  TestParallelTheSecond&#xA;    fails_test.go:35: failed the second&#xA;--- FAIL: TestParallelTheSecond (0.01s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestPassed" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestPassedWithLog" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestPassedWithStdout" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/parallelfails" name="TestWithStderr" time="0.000000"></testcase>
+	</testsuite>
+	<testsuite tests="29" failures="4" time="0.020000" name="gotest.tools/gotestsum/testjson/internal/withfails" timestamp="0001-01-01T00:00:00Z">
+		<properties>
+			<property name="go.version" value="go7.7.7"></property>
+		</properties>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestFailed" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestFailed&#xA;    fails_test.go:34: this failed&#xA;--- FAIL: TestFailed (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestFailedWithStderr" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestFailedWithStderr&#xA;this is stderr&#xA;    fails_test.go:43: also failed&#xA;--- FAIL: TestFailedWithStderr (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/c" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedWithFailure/c&#xA;    fails_test.go:65: failed&#xA;    --- FAIL: TestNestedWithFailure/c (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure" time="0.000000">
+			<failure message="Failed" type="">=== RUN   TestNestedWithFailure&#xA;--- FAIL: TestNestedWithFailure (0.00s)&#xA;</failure>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestSkipped" time="0.000000">
+			<skipped message="=== RUN   TestSkipped&#xA;    fails_test.go:26: &#xA;--- SKIP: TestSkipped (0.00s)&#xA;"></skipped>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestSkippedWitLog" time="0.000000">
+			<skipped message="=== RUN   TestSkippedWitLog&#xA;    fails_test.go:30: the skip message&#xA;--- SKIP: TestSkippedWitLog (0.00s)&#xA;"></skipped>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestTimeout" time="0.000000">
+			<skipped message="=== RUN   TestTimeout&#xA;    timeout_test.go:13: skipping slow test&#xA;--- SKIP: TestTimeout (0.00s)&#xA;"></skipped>
+		</testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestPassed" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestPassedWithLog" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestPassedWithStdout" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestWithStderr" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/a/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/a" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/b/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/b" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/d/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedWithFailure/d" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/a/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/a" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/b/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/b" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/c/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/c" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/d/sub" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess/d" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestNestedSuccess" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestParallelTheFirst" time="0.010000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestParallelTheThird" time="0.000000"></testcase>
+		<testcase classname="gotest.tools/gotestsum/testjson/internal/withfails" name="TestParallelTheSecond" time="0.010000"></testcase>
+	</testsuite>
+</testsuites>
\ No newline at end of file
diff --git a/testjson/dotformat.go b/testjson/dotformat.go
index bb0897e..fd8a293 100644
--- a/testjson/dotformat.go
+++ b/testjson/dotformat.go
@@ -103,7 +103,7 @@ func (d *dotFormatter) Format(event TestEvent, exec *Execution) error {
 
 	sort.Slice(d.order, d.orderByLastUpdated)
 	for _, pkg := range d.order {
-		if d.opts.HideEmptyPackages && exec.Package(pkg).Total == 0 {
+		if d.opts.HideEmptyPackages && exec.Package(pkg).IsEmpty() {
 			continue
 		}
 
diff --git a/testjson/execution.go b/testjson/execution.go
index dd6e507..0204d07 100644
--- a/testjson/execution.go
+++ b/testjson/execution.go
@@ -232,6 +232,11 @@ func (p *Package) TestMainFailed() bool {
 	return p.action == ActionFail && len(p.Failed) == 0
 }
 
+// IsEmpty returns true if this package contains no tests.
+func (p *Package) IsEmpty() bool {
+	return p.Total == 0 && !p.TestMainFailed()
+}
+
 const neverFinished time.Duration = -1
 
 // end adds any tests that were missing an ActionFail TestEvent to the list of