Skip to content

Commit

Permalink
adapt esreporter to ginkgo v2 report type (gardener#5504)
Browse files Browse the repository at this point in the history
* adapt esreporter to ginkgo v2 report type

* add tests

* report errors and failures

* add license header
  • Loading branch information
hendrikKahl authored Mar 8, 2022
1 parent 7f3f29b commit 708d86e
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 64 deletions.
123 changes: 63 additions & 60 deletions test/framework/reporter/esreporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import (
"regexp"
"strings"

"github.com/onsi/ginkgo/v2/config"
"github.com/onsi/ginkgo/v2/reporters"
"github.com/onsi/ginkgo/v2"

"github.com/onsi/ginkgo/v2/types"
)

Expand All @@ -38,7 +38,7 @@ type TestSuiteMetadata struct {
Duration float64 `json:"duration"`
}

// TestCase is one instanace of a test execution
// TestCase is one instance of a test execution
type TestCase struct {
Metadata *TestSuiteMetadata `json:"suite"`
Name string `json:"name"`
Expand All @@ -50,7 +50,7 @@ type TestCase struct {
SystemOut string `json:"system-out,omitempty"`
}

// FailureMessage describes the error and the log output if a error occurred.
// FailureMessage describes the error and the log output if an error occurred.
type FailureMessage struct {
Type ESSpecPhase `json:"type"`
Message string `json:"message"`
Expand All @@ -72,7 +72,7 @@ const (
SpecPhaseInterrupted ESSpecPhase = "Interrupted"
)

// GardenerESReporter is a custom ginkgo exporter for gardener integration tests that write a summary of the tests in a
// GardenerESReporter is a custom ginkgo exporter for gardener integration tests that write a summary of the tests in an
// elastic search json report.
type GardenerESReporter struct {
suite *TestSuiteMetadata
Expand All @@ -85,14 +85,9 @@ type GardenerESReporter struct {

var matchLabel, _ = regexp.Compile(`\\[(.*?)\\]`)

// NewDeprecatedGardenerESReporter creates a new Gardener elasticsearch reporter.
// The json bulk will be stored in the passed filename in the given es index.
// It can be used with reporters.ReportViaDeprecatedReporter in ginkgo v2 to get the same reporting as in ginkgo v1.
// However, it must be invoked in ReportAfterSuite instead of passed to RunSpecsWithDefaultAndCustomReporters, hence
// it was renamed to force dependent repositories to adapt.
// Deprecated: this needs to be reworked to ginkgo's new reporting infrastructure, ReportViaDeprecatedReporter will be
// removed in a future version of ginkgo v2, see https://onsi.github.io/ginkgo/MIGRATING_TO_V2#removed-custom-reporters
func NewDeprecatedGardenerESReporter(filename, index string) reporters.DeprecatedReporter {
// newGardenerESReporter creates a new Gardener elasticsearch reporter.
// Any report will be encoded to json and stored to the passed filename in the given es index.
func newGardenerESReporter(filename, index string) *GardenerESReporter {
reporter := &GardenerESReporter{
filename: filename,
testCases: []TestCase{},
Expand All @@ -104,50 +99,67 @@ func NewDeprecatedGardenerESReporter(filename, index string) reporters.Deprecate
return reporter
}

// SuiteWillBegin is the first function that is invoked by ginkgo when a test suites starts.
// It is used to setup metadata information about the suite
func (reporter *GardenerESReporter) SuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) {
// ReportResults implements reporting based on the ginkgo v2 Report type
// while maintaining the existing structure of the elastic index.
// ReportsResults is intended to be called once in an ReportAfterSuite node.
func ReportResults(filename, index string, report ginkgo.Report) {
reporter := newGardenerESReporter(filename, index)
reporter.processReport(report)
reporter.storeResults()
}

func (reporter *GardenerESReporter) processReport(report ginkgo.Report) {
reporter.suite = &TestSuiteMetadata{
Name: summary.SuiteDescription,
Name: report.SuiteDescription,
Phase: SpecPhaseSucceeded,
}
reporter.testSuiteName = summary.SuiteDescription
}
reporter.testSuiteName = report.SuiteDescription

// SpecDidComplete analysis the completed test and creates new es entry
func (reporter *GardenerESReporter) SpecDidComplete(specSummary *types.SpecSummary) {
// do not report skipped tests
if specSummary.State == types.SpecStateSkipped || specSummary.State == types.SpecStatePending {
return
}
for _, spec := range report.SpecReports {
// do not report skipped tests
if spec.State == types.SpecStateSkipped || spec.State == types.SpecStatePending {
continue
}

testCase := TestCase{
Metadata: reporter.suite,
Name: strings.Join(specSummary.ComponentTexts[1:], " "),
ShortName: getShortName(specSummary.ComponentTexts[len(specSummary.ComponentTexts)-1]),
Phase: PhaseForState(specSummary.State),
Labels: parseLabels(strings.Join(specSummary.ComponentTexts[1:], " ")),
}
if specSummary.State == types.SpecStateFailed || specSummary.State == types.SpecStateInterrupted || specSummary.State == types.SpecStatePanicked {
testCase.FailureMessage = &FailureMessage{
Type: PhaseForState(specSummary.State),
Message: failureMessage(specSummary.Failure),
var componentTexts []string
componentTexts = append(componentTexts, spec.ContainerHierarchyTexts...)
componentTexts = append(componentTexts, spec.LeafNodeText)
testCaseName := strings.Join(componentTexts[1:], " ")
testCase := TestCase{
Metadata: reporter.suite,
Name: testCaseName,
ShortName: getShortName(componentTexts[len(componentTexts)-1]),
Phase: PhaseForState(spec.State),
Labels: parseLabels(testCaseName),
}

if spec.State == types.SpecStateFailed || spec.State == types.SpecStateInterrupted || spec.State == types.SpecStatePanicked {
if spec.State == types.SpecStateFailed {
reporter.suite.Failures++
} else {
reporter.suite.Errors++
}

testCase.FailureMessage = &FailureMessage{
Type: PhaseForState(spec.State),
Message: failureMessage(spec.Failure),
}
testCase.SystemOut = spec.CombinedOutput()
}
testCase.SystemOut = specSummary.CapturedOutput

testCase.Duration = spec.RunTime.Seconds()
reporter.testCases = append(reporter.testCases, testCase)

}

if reporter.suite.Failures != 0 || reporter.suite.Errors != 0 {
reporter.suite.Phase = SpecPhaseFailed
}
testCase.Duration = specSummary.RunTime.Seconds()
reporter.testCases = append(reporter.testCases, testCase)
reporter.suite.Tests = report.PreRunStats.SpecsThatWillRun
reporter.suite.Duration = math.Trunc(report.RunTime.Seconds()*1000) / 1000
}

// SuiteDidEnd collects the metadata for the whole test suite and writes the results
// as elasticsearch json bulk to the specified location.
func (reporter *GardenerESReporter) SuiteDidEnd(summary *types.SuiteSummary) {
reporter.suite.Tests = summary.NumberOfSpecsThatWillBeRun
reporter.suite.Duration = math.Trunc(summary.RunTime.Seconds()*1000) / 1000
reporter.suite.Failures = summary.NumberOfFailedSpecs
reporter.suite.Errors = 0

func (reporter *GardenerESReporter) storeResults() {
dir := filepath.Dir(reporter.filename)
if _, err := os.Stat(dir); err != nil {
if !os.IsNotExist(err) {
Expand Down Expand Up @@ -187,20 +199,11 @@ func (reporter *GardenerESReporter) SuiteDidEnd(summary *types.SuiteSummary) {
}
}

// SpecWillRun is implemented as a noop to satisfy the reporter interface for ginkgo.
func (reporter *GardenerESReporter) SpecWillRun(specSummary *types.SpecSummary) {}

// BeforeSuiteDidRun is implemented as a noop to satisfy the reporter interface for ginkgo.
func (reporter *GardenerESReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) {}

// AfterSuiteDidRun is implemented as a noop to satisfy the reporter interface for ginkgo.
func (reporter *GardenerESReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) {}

func failureMessage(failure types.SpecFailure) string {
return fmt.Sprintf("%s\n%s\n%s", failure.ComponentCodeLocation.String(), failure.Message, failure.Location.String())
func failureMessage(failure types.Failure) string {
return fmt.Sprintf("%s\n%s\n%s", failure.FailureNodeLocation.String(), failure.Message, failure.Location.String())
}

// parseLabels returns all labels of a test that have teh format [<label>]
// parseLabels returns all labels of a test that have the format [<label>]
func parseLabels(name string) []string {
labels := matchLabel.FindAllString(name, -1)
for i, label := range labels {
Expand All @@ -215,7 +218,7 @@ func getShortName(name string) string {
return strings.TrimSpace(short)
}

// getESIndexString returns a bulk index configuration string for a index.
// getESIndexString returns a bulk index configuration string for an index.
func getESIndexString(index string) string {
format := `{ "index": { "_index": "%s", "_type": "_doc" } }`
return fmt.Sprintf(format, index)
Expand Down
197 changes: 197 additions & 0 deletions test/framework/reporter/esreporter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// Copyright 2022 Copyright (c) 2022 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package reporter

import (
"fmt"
"math"
"testing"
"time"

. "github.com/onsi/ginkgo/v2"
"github.com/onsi/ginkgo/v2/types"
. "github.com/onsi/gomega"
)

func TestGardenerESReporter(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Gardener ES Reporter Test Suite")
}

const (
reportFileName = "/tmp/report_test.json"
indexName = "test-index"
mockReportSuiteDescription = "mock report suite"
testCaseName = "[DEFAULT] [REPORT] Should complete successfully"
)

var _ = Describe("processReport tests", func() {

var (
reporter *GardenerESReporter
mockReport Report
mockContainerHierarchyTexts []string
suiteDuration float64
testCaseDuration float64
)

BeforeEach(func() {
reporter = newGardenerESReporter(reportFileName, indexName)
mockReport.SuiteDescription = mockReportSuiteDescription
mockContainerHierarchyTexts = []string{"DESCRIBE"}
mockReport.RunTime = time.Duration(2000000000)
mockReport.SpecReports = []SpecReport{
{
ContainerHierarchyTexts: mockContainerHierarchyTexts,
LeafNodeText: testCaseName,
RunTime: time.Duration(1000000000),
Failure: types.Failure{},
CapturedGinkgoWriterOutput: "",
CapturedStdOutErr: "",
},
}
suiteDuration = math.Trunc(mockReport.RunTime.Seconds()*1000) / 1000
testCaseDuration = time.Duration(1000000000).Seconds()
})

It("should setup test suite metadata correctly", func() {
expectedIndex := append([]byte(fmt.Sprintf(`{ "index": { "_index": "%s", "_type": "_doc" } }`, indexName)), []byte("\n")...)
mockReport.PreRunStats.SpecsThatWillRun = 0

reporter.processReport(mockReport)

Expect(reporter.filename).To(Equal(reportFileName))
Expect(reporter.index).To(Equal(expectedIndex))
Expect(reporter.testSuiteName).To(Equal(mockReportSuiteDescription))
Expect(reporter.suite.Name).To(Equal(mockReportSuiteDescription))
Expect(reporter.suite.Failures).To(Equal(0))
Expect(reporter.suite.Phase).To(Equal(SpecPhaseSucceeded))
Expect(reporter.suite.Tests).To(Equal(0))
Expect(reporter.suite.Duration).To(Equal(suiteDuration))
Expect(reporter.suite.Errors).To(Equal(0))
})

It("should process one successful test correctly", func() {
mockReport.PreRunStats.SpecsThatWillRun = 1
mockReport.SpecReports[0].State = types.SpecStatePassed

reporter.processReport(mockReport)

Expect(reporter.suite.Tests).To(Equal(1))
Expect(reporter.suite.Failures).To(Equal(0))
Expect(reporter.suite.Phase).To(Equal(SpecPhaseSucceeded))

Expect(len(reporter.testCases)).To(Equal(1))
Expect(reporter.testCases[0].Metadata.Name).To(Equal(mockReportSuiteDescription))
Expect(reporter.testCases[0].Name).To(Equal(testCaseName))
Expect(reporter.testCases[0].ShortName).To(Equal(testCaseName))
Expect(reporter.testCases[0].Phase).To(Equal(SpecPhaseSucceeded))
Expect(reporter.testCases[0].Duration).To(Equal(testCaseDuration))
})

It("should process one failed test correctly", func() {
stderr := "stderr - something failed"
failureMessage := "something went wrong"
location := types.CodeLocation{
FileName: "test.go",
LineNumber: 10,
FullStackTrace: "some text",
}
failureLocation := types.CodeLocation{
FileName: "error.go",
LineNumber: 20,
FullStackTrace: "some error",
}
mockReport.PreRunStats.SpecsThatWillRun = 1
mockReport.SpecReports[0].State = types.SpecStateFailed
mockReport.SpecReports[0].Failure = types.Failure{
Message: failureMessage,
Location: location,
FailureNodeLocation: failureLocation,
}
mockReport.SpecReports[0].CapturedStdOutErr = stderr

reporter.processReport(mockReport)

Expect(reporter.suite.Tests).To(Equal(1))
Expect(reporter.suite.Failures).To(Equal(1))
Expect(reporter.suite.Errors).To(Equal(0))
Expect(reporter.suite.Phase).To(Equal(SpecPhaseFailed))

Expect(len(reporter.testCases)).To(Equal(1))
Expect(reporter.testCases[0].Metadata.Name).To(Equal(mockReportSuiteDescription))
Expect(reporter.testCases[0].Name).To(Equal(testCaseName))
Expect(reporter.testCases[0].ShortName).To(Equal(testCaseName))
Expect(reporter.testCases[0].Phase).To(Equal(SpecPhaseFailed))
Expect(reporter.testCases[0].Duration).To(Equal(testCaseDuration))
Expect(reporter.testCases[0].FailureMessage).NotTo(BeNil())
Expect(reporter.testCases[0].FailureMessage.Type).To(Equal(SpecPhaseFailed))
Expect(reporter.testCases[0].FailureMessage.Message).To(Equal(fmt.Sprintf("%s\n%s\n%s", failureLocation.String(), failureMessage, location.String())))
Expect(reporter.testCases[0].SystemOut).To(Equal(stderr))
})

It("should process one panicked test correctly", func() {
stderr := "stderr - something panicked"
failureMessage := "something went utterly wrong"
location := types.CodeLocation{
FileName: "test.go",
LineNumber: 10,
FullStackTrace: "some text",
}
failureLocation := types.CodeLocation{
FileName: "error.go",
LineNumber: 20,
FullStackTrace: "some error",
}
mockReport.PreRunStats.SpecsThatWillRun = 1
mockReport.SpecReports[0].State = types.SpecStatePanicked
mockReport.SpecReports[0].Failure = types.Failure{
Message: failureMessage,
Location: location,
FailureNodeLocation: failureLocation,
}
mockReport.SpecReports[0].CapturedStdOutErr = stderr

reporter.processReport(mockReport)

Expect(reporter.suite.Tests).To(Equal(1))
Expect(reporter.suite.Failures).To(Equal(0))
Expect(reporter.suite.Errors).To(Equal(1))
Expect(reporter.suite.Phase).To(Equal(SpecPhaseFailed))

Expect(len(reporter.testCases)).To(Equal(1))
Expect(reporter.testCases[0].Metadata.Name).To(Equal(mockReportSuiteDescription))
Expect(reporter.testCases[0].Name).To(Equal(testCaseName))
Expect(reporter.testCases[0].ShortName).To(Equal(testCaseName))
Expect(reporter.testCases[0].Phase).To(Equal(SpecPhaseFailed))
Expect(reporter.testCases[0].Duration).To(Equal(testCaseDuration))
Expect(reporter.testCases[0].FailureMessage).NotTo(BeNil())
Expect(reporter.testCases[0].FailureMessage.Type).To(Equal(SpecPhaseFailed))
Expect(reporter.testCases[0].FailureMessage.Message).To(Equal(fmt.Sprintf("%s\n%s\n%s", failureLocation.String(), failureMessage, location.String())))
Expect(reporter.testCases[0].SystemOut).To(Equal(stderr))
})

It("should process one skipped test correctly", func() {
mockReport.PreRunStats.SpecsThatWillRun = 0
mockReport.SpecReports[0].State = types.SpecStateSkipped

reporter.processReport(mockReport)

Expect(reporter.suite.Tests).To(Equal(0))
Expect(reporter.suite.Failures).To(Equal(0))
Expect(reporter.suite.Phase).To(Equal(SpecPhaseSucceeded))
Expect(len(reporter.testCases)).To(Equal(0))
})
})
Loading

0 comments on commit 708d86e

Please sign in to comment.