From 8f3bd70cc78b00986aaf7fc8e9fbd77df05e6b26 Mon Sep 17 00:00:00 2001 From: Onsi Fakhouri Date: Wed, 17 Jan 2024 13:17:54 -0700 Subject: [PATCH] JUnit reports now interpret Label(owner:X) and set owner to X. --- docs/index.md | 2 +- reporters/junit_report.go | 12 ++++++++++++ reporters/junit_report_test.go | 16 ++++++++++------ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/docs/index.md b/docs/index.md index 15ef9187d..c68cc6e17 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3389,7 +3389,7 @@ Ginkgo also supports generating JUnit reports with ginkgo --junit-report=report.xml ``` -The JUnit report is compatible with the JUnit specification, however Ginkgo specs carry much more metadata than can be easily mapped onto the JUnit spec so some information is lost and/or a bit harder to decode than using Ginkgo's native JSON format. +The JUnit report is compatible with the JUnit specification, however Ginkgo specs carry much more metadata than can be easily mapped onto the JUnit spec so some information is lost and/or a bit harder to decode than using Ginkgo's native JSON format. Nonetheless, Ginkgo does its best to populate as much of the JUnit report as possible. This includes adding additional metadata using [labels](#spec-labels) - in particular if you provide a label of the form `Label("owner:XYZ")`, the generating JUnit spec will set the `Owner` attribute to `XYZ`. Ginkgo also supports Teamcity reports with `ginkgo --teamcity-report=report.teamcity` though, again, the Teamcity spec makes it difficult to capture all the spec metadata. diff --git a/reporters/junit_report.go b/reporters/junit_report.go index 816042208..314aeaba0 100644 --- a/reporters/junit_report.go +++ b/reporters/junit_report.go @@ -15,6 +15,7 @@ import ( "fmt" "os" "path" + "regexp" "strings" "github.com/onsi/ginkgo/v2/config" @@ -104,6 +105,8 @@ type JUnitProperty struct { Value string `xml:"value,attr"` } +var ownerRE = regexp.MustCompile(`(?i)^owner:(.*)$`) + type JUnitTestCase struct { // Name maps onto the full text of the spec - equivalent to "[SpecReport.LeafNodeType] SpecReport.FullText()" Name string `xml:"name,attr"` @@ -113,6 +116,8 @@ type JUnitTestCase struct { Status string `xml:"status,attr"` // Time is the time in seconds to execute the spec - maps onto SpecReport.RunTime Time float64 `xml:"time,attr"` + // Owner is the owner the spec - is set if a label matching Label("owner:X") is provided + Owner string `xml:"owner,attr,omitempty"` //Skipped is populated with a message if the test was skipped or pending Skipped *JUnitSkipped `xml:"skipped,omitempty"` //Error is populated if the test panicked or was interrupted @@ -195,6 +200,12 @@ func GenerateJUnitReportWithConfig(report types.Report, dst string, config Junit if len(labels) > 0 && !config.OmitSpecLabels { name = name + " [" + strings.Join(labels, ", ") + "]" } + owner := "" + for _, label := range labels { + if matches := ownerRE.FindStringSubmatch(label); len(matches) == 2 { + owner = matches[1] + } + } name = strings.TrimSpace(name) test := JUnitTestCase{ @@ -202,6 +213,7 @@ func GenerateJUnitReportWithConfig(report types.Report, dst string, config Junit Classname: report.SuiteDescription, Status: spec.State.String(), Time: spec.RunTime.Seconds(), + Owner: owner, } if !spec.State.Is(config.OmitTimelinesForSpecState) { test.SystemErr = systemErrForUnstructuredReporters(spec) diff --git a/reporters/junit_report_test.go b/reporters/junit_report_test.go index 78c92edc0..9c71df483 100644 --- a/reporters/junit_report_test.go +++ b/reporters/junit_report_test.go @@ -33,7 +33,7 @@ var _ = Describe("JunitReport", func() { RE("a hidden report entry", cl1, TL("ginkgowriter\noutput\n"), types.ReportEntryVisibilityNever), AF(types.SpecStateFailed, "a subsequent failure", types.FailureNodeInContainer, FailureNodeLocation(cl3), types.NodeTypeAfterEach, 0, TL("ginkgowriter\noutput\ncleanup!")), ), - S(types.NodeTypeIt, "A", cl0, STD("some captured stdout\n"), GW("some GinkgoWriter\noutput is interspersed\nhere and there\n"), + S(types.NodeTypeIt, "A", cl0, STD("some captured stdout\n"), GW("some GinkgoWriter\noutput is interspersed\nhere and there\n"), Label("cat", "owner:frank", "OWNer:bob"), SE(types.SpecEventNodeStart, types.NodeTypeIt, "A", cl0), PR("my progress report", LeafNodeText("A"), TL("some GinkgoWriter\n")), SE(types.SpecEventByStart, "My Step", cl1, TL("some GinkgoWriter\n")), @@ -42,8 +42,8 @@ var _ = Describe("JunitReport", func() { SE(types.SpecEventByEnd, "My Step", cl1, time.Millisecond*200, TL("some GinkgoWriter\noutput is interspersed\n")), SE(types.SpecEventNodeEnd, types.NodeTypeIt, "A", cl0, time.Millisecond*300, TL("some GinkgoWriter\noutput is interspersed\nhere and there\n")), ), - S(types.NodeTypeIt, "A", cl0, types.SpecStatePending), - S(types.NodeTypeIt, "A", cl0, types.SpecStatePanicked, STD("some captured stdout\n"), + S(types.NodeTypeIt, "A", cl0, types.SpecStatePending, CLabels(Label("owner:org")), Label("owner:team")), + S(types.NodeTypeIt, "A", cl0, types.SpecStatePanicked, CLabels(Label("owner:org")), STD("some captured stdout\n"), SE(types.SpecEventNodeStart, types.NodeTypeIt, "A", cl0), F("failure\nmessage", cl1, types.FailureNodeIsLeafNode, FailureNodeLocation(cl0), types.NodeTypeIt, ForwardedPanic("the panic")), SE(types.SpecEventNodeEnd, types.NodeTypeIt, "A", cl0, time.Millisecond*300, TL("some GinkgoWriter\noutput is interspersed\nhere and there\n")), @@ -95,6 +95,7 @@ var _ = Describe("JunitReport", func() { Ω(failingSpec.Status).Should(Equal("timedout")) Ω(failingSpec.Skipped).Should(BeNil()) Ω(failingSpec.Error).Should(BeNil()) + Ω(failingSpec.Owner).Should(Equal("")) Ω(failingSpec.Failure.Message).Should(Equal("failure\nmessage")) Ω(failingSpec.Failure.Type).Should(Equal("timedout")) Ω(failingSpec.Failure.Description).Should(MatchLines( @@ -141,12 +142,13 @@ var _ = Describe("JunitReport", func() { )) passingSpec := suite.TestCases[1] - Ω(passingSpec.Name).Should(Equal("[It] A")) + Ω(passingSpec.Name).Should(Equal("[It] A [cat, owner:frank, OWNer:bob]")) Ω(passingSpec.Classname).Should(Equal("My Suite")) Ω(passingSpec.Status).Should(Equal("passed")) Ω(passingSpec.Skipped).Should(BeNil()) Ω(passingSpec.Error).Should(BeNil()) Ω(passingSpec.Failure).Should(BeNil()) + Ω(passingSpec.Owner).Should(Equal("bob")) Ω(passingSpec.SystemOut).Should(Equal("some captured stdout\n")) Ω(passingSpec.SystemErr).Should(MatchLines( spr("> Enter [It] A - cl0.go:12 @ %s", FORMATTED_TIME), @@ -165,20 +167,22 @@ var _ = Describe("JunitReport", func() { )) pendingSpec := suite.TestCases[2] - Ω(pendingSpec.Name).Should(Equal("[It] A")) + Ω(pendingSpec.Name).Should(Equal("[It] A [owner:org, owner:team]")) Ω(pendingSpec.Classname).Should(Equal("My Suite")) Ω(pendingSpec.Status).Should(Equal("pending")) Ω(pendingSpec.Skipped.Message).Should(Equal("pending")) Ω(pendingSpec.Error).Should(BeNil()) + Ω(pendingSpec.Owner).Should(Equal("team")) Ω(pendingSpec.Failure).Should(BeNil()) Ω(pendingSpec.SystemOut).Should(BeEmpty()) Ω(pendingSpec.SystemErr).Should(BeEmpty()) panickedSpec := suite.TestCases[3] - Ω(panickedSpec.Name).Should(Equal("[It] A")) + Ω(panickedSpec.Name).Should(Equal("[It] A [owner:org]")) Ω(panickedSpec.Classname).Should(Equal("My Suite")) Ω(panickedSpec.Status).Should(Equal("panicked")) Ω(panickedSpec.Skipped).Should(BeNil()) + Ω(panickedSpec.Owner).Should(Equal("org")) Ω(panickedSpec.Error.Message).Should(Equal("the panic")) Ω(panickedSpec.Error.Type).Should(Equal("panicked")) Ω(panickedSpec.Error.Description).Should(MatchLines(