Repository: gotestyourself/gotestsum
Pull request #261 (2022-06-27 13:44:25)

Add project name to junit.xml output
Add attributes `name`, `timestamp`, `tests`, `failures`, `errors`, `skipepd`, `time`, `hostname` to the `testsuites` xml node, according to the schema: https://github.com/windyroad/JUnit-Schema/blob/master/JUnit.xsd.

nikolaigut: > Thank you for the PR! Sorry it took me some time to read over it.
> 
> One of the challenges of Junit.xml is that there is no official spec (as far as I've seen). Some people have published their version of a spec, but I don't think the tools and systems that read the file all agree.
> 
> So far the `junit.xml` output of `gotestsum` has mostly been limited to what common CI systems will use. It doesn't make any effort to add other fields unless they will be used by something.
> 
> Can you elaborate on your use case? What tool or system is receiving the `junit.xml`, and how does it make use of these fields? Understanding that will help ensure we are building the right thing.
> 
> I think it would be fine to add some more fields. I've left some suggestions an questions below as well.

Thank you very much for your review.

I need these changes to generate a more user-friendly report with the tool https://lotterfriends.github.io/online-junit-parser/, which uses some of these properties in the title.
nikolaigut: > Thanks! The test is looking good. Just a few more questions to make sure I understand the purpose of some of these additions.

Thanks for this input. You are totally right. I removed the unused properties.

Patch:
diff --git a/cmd/handler.go b/cmd/handler.go
index 773ceac..d67284f 100644
--- a/cmd/handler.go
+++ b/cmd/handler.go
@@ -93,6 +93,7 @@ func writeJUnitFile(opts *options, execution *testjson.Execution) error {
 	}()
 
 	return junitxml.Write(junitFile, execution, junitxml.Config{
+		ProjectName:             opts.junitProjectName,
 		FormatTestSuiteName:     opts.junitTestSuiteNameFormat.Value(),
 		FormatTestCaseClassname: opts.junitTestCaseClassnameFormat.Value(),
 	})
diff --git a/cmd/main.go b/cmd/main.go
index 020b1ce..553cad5 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -88,6 +88,9 @@ func setupFlags(name string) (*pflag.FlagSet, *options) {
 		"format the testsuite name field as: "+junitFieldFormatValues)
 	flags.Var(opts.junitTestCaseClassnameFormat, "junitfile-testcase-classname",
 		"format the testcase classname field as: "+junitFieldFormatValues)
+	flags.StringVar(&opts.junitProjectName, "junitfile-project-name",
+		lookEnvWithDefault("GOTESTSUM_JUNITFILE_PROJECT_NAME", ""),
+		"name of the project used in 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.")
@@ -152,6 +155,7 @@ type options struct {
 	hideSummary                  *hideSummaryValue
 	junitTestSuiteNameFormat     *junitFieldFormatValue
 	junitTestCaseClassnameFormat *junitFieldFormatValue
+	junitProjectName             string
 	rerunFailsMaxAttempts        int
 	rerunFailsMaxInitialFailures int
 	rerunFailsReportFile         string
diff --git a/internal/junitxml/report.go b/internal/junitxml/report.go
index 8520969..cd23b1f 100644
--- a/internal/junitxml/report.go
+++ b/internal/junitxml/report.go
@@ -17,8 +17,13 @@ import (
 
 // JUnitTestSuites is a collection of JUnit test suites.
 type JUnitTestSuites struct {
-	XMLName xml.Name `xml:"testsuites"`
-	Suites  []JUnitTestSuite
+	XMLName  xml.Name `xml:"testsuites"`
+	Name     string   `xml:"name,attr,omitempty"`
+	Tests    int      `xml:"tests,attr"`
+	Failures int      `xml:"failures,attr"`
+	Errors   int      `xml:"errors,attr"`
+	Time     string   `xml:"time,attr"`
+	Suites   []JUnitTestSuite
 }
 
 // JUnitTestSuite is a single JUnit test suite which may contain many
@@ -64,10 +69,12 @@ type JUnitFailure struct {
 
 // Config used to write a junit XML document.
 type Config struct {
+	ProjectName             string
 	FormatTestSuiteName     FormatFunc
 	FormatTestCaseClassname FormatFunc
 	// This is used for tests to have a consistent timestamp
 	customTimestamp string
+	customElapsed   string
 }
 
 // FormatFunc converts a string from one format into another.
@@ -84,8 +91,17 @@ func Write(out io.Writer, exec *testjson.Execution, cfg Config) error {
 func generate(exec *testjson.Execution, cfg Config) JUnitTestSuites {
 	cfg = configWithDefaults(cfg)
 	version := goVersion()
-	suites := JUnitTestSuites{}
+	suites := JUnitTestSuites{
+		Name:     cfg.ProjectName,
+		Tests:    exec.Total(),
+		Failures: len(exec.Failed()),
+		Errors:   len(exec.Errors()),
+		Time:     formatDurationAsSeconds(time.Since(exec.Started())),
+	}
 
+	if cfg.customElapsed != "" {
+		suites.Time = cfg.customElapsed
+	}
 	for _, pkgname := range exec.Packages() {
 		pkg := exec.Package(pkgname)
 		junitpkg := JUnitTestSuite{


Test patch:
diff --git a/cmd/testdata/gotestsum-help-text b/cmd/testdata/gotestsum-help-text
index 430f975..9e46882 100644
--- a/cmd/testdata/gotestsum-help-text
+++ b/cmd/testdata/gotestsum-help-text
@@ -8,6 +8,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-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)
       --max-fails int                               end the test run after this number of failures
diff --git a/internal/junitxml/report_test.go b/internal/junitxml/report_test.go
index 295a247..14e8afb 100644
--- a/internal/junitxml/report_test.go
+++ b/internal/junitxml/report_test.go
@@ -20,7 +20,11 @@ func TestWrite(t *testing.T) {
 	exec := createExecution(t)
 
 	env.Patch(t, "GOVERSION", "go7.7.7")
-	err := Write(out, exec, Config{customTimestamp: new(time.Time).Format(time.RFC3339)})
+	err := Write(out, exec, Config{
+		ProjectName:     "test",
+		customTimestamp: new(time.Time).Format(time.RFC3339),
+		customElapsed:   "2.1",
+	})
 	assert.NilError(t, err)
 	golden.Assert(t, out.String(), "junitxml-report.golden")
 }
diff --git a/internal/junitxml/testdata/junitxml-report.golden b/internal/junitxml/testdata/junitxml-report.golden
index 0ca2d0e..9988d8b 100644
--- a/internal/junitxml/testdata/junitxml-report.golden
+++ b/internal/junitxml/testdata/junitxml-report.golden
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<testsuites>
+<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>