Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Configurable report granularity #581

Merged
merged 1 commit into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/testing/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ artifactsDir | string | The directory to output artifacts to (cur
commands | list of [Commands](#commands) | Commands to run prior to running the tests. | []
kindContainers | list of strings | List of Docker images to load into the KIND cluster once it is started. | []
reportFormat | string | Determines the report format. If empty, no report is generated. One of: JSON, XML. |
reportGranularity | string | What granularity to report failures at. One of: `step`, `test`. | `step`
reportName | string | The name of report to create. This field is not used unless reportFormat is set. | "kuttl-test"
namespace | string | The namespace to use for tests. This namespace will be created if it does not exist and removed if it was created (unless `skipDelete` is set). If no namespace is set, one will be auto-generated. |
suppress | list of strings | Suppresses log collection of the specified types. Currently only `events` is supported. |
Expand Down
4 changes: 4 additions & 0 deletions pkg/apis/testharness/v1beta1/test_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ type TestSuite struct {

// ReportName defines the name of report to create. It defaults to "kuttl-report" and is not used unless ReportFormat is defined.
ReportName string `json:"reportName"`

// ReportGranularity defines the granularity at which failures are reported. It defaults to "step".
ReportGranularity string `json:"reportGranularity"`

// Namespace defines the namespace to use for tests
// The value "" means to auto-generate tests namespaces, these namespaces will be created and removed for each test
// Any other value is the name of the namespace to use. This namespace will be created if it does not exist and will
Expand Down
9 changes: 9 additions & 0 deletions pkg/kuttlctl/cmd/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func newTestCmd() *cobra.Command { //nolint:gocyclo
timeout := 30
reportFormat := ""
reportName := "kuttl-report"
reportGranularity := "kuttl-report"
namespace := ""
suppress := []string{}
var runLabels labelSetValue
Expand Down Expand Up @@ -183,6 +184,13 @@ For more detailed documentation, visit: https://kuttl.dev`,
options.ReportName = reportName
}

if isSet(flags, "report-granularity") {
if reportGranularity != "step" && reportGranularity != "test" {
return fmt.Errorf("unrecognized report granularity %q", reportGranularity)
}
options.ReportGranularity = reportGranularity
}

if isSet(flags, "artifacts-dir") {
options.ArtifactsDir = artifactsDir
}
Expand Down Expand Up @@ -257,6 +265,7 @@ For more detailed documentation, visit: https://kuttl.dev`,
testCmd.Flags().IntVar(&timeout, "timeout", 30, "The timeout to use as default for TestSuite configuration.")
testCmd.Flags().StringVar(&reportFormat, "report", "", "Specify JSON|XML for report. Report location determined by --artifacts-dir.")
testCmd.Flags().StringVar(&reportName, "report-name", "kuttl-report", "Name for the report. Report location determined by --artifacts-dir and report file type determined by --report.")
testCmd.Flags().StringVar(&reportGranularity, "report-granularity", "step", "Report granularity. Can be 'step' (default) or 'test'.")
testCmd.Flags().StringVarP(&namespace, "namespace", "n", "", "Namespace to use for tests. Provided namespaces must exist prior to running tests.")
testCmd.Flags().StringSliceVar(&suppress, "suppress-log", []string{}, "Suppress logging for these kinds of logs (events).")
testCmd.Flags().Var(&runLabels, "test-run-labels", "Labels to use for this test run.")
Expand Down
52 changes: 33 additions & 19 deletions pkg/report/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ type Testsuite struct {
// Testcases is a collection of test cases.
Testcases []*Testcase `xml:"testcase" json:"testcase,omitempty"`
// SubSuites is a collection of child test suites.
SubSuites []*Testsuite `xml:"testsuite" json:"testsuite,omitempty"`
lock sync.Mutex
SubSuites []*Testsuite `xml:"testsuite" json:"testsuite,omitempty"`
lock sync.Mutex
reportGranularity string
}

// Testsuites is a collection of Testsuite and defines the rollup summary of all stats.
Expand Down Expand Up @@ -135,9 +136,9 @@ func NewSuiteCollection(name string) *Testsuites {
}

// NewSuite returns the address of a newly created TestSuite
func NewSuite(name string) *Testsuite {
func NewSuite(name string, reportGranularity string) *Testsuite {
start := time.Now()
return &Testsuite{Name: name, Timestamp: start}
return &Testsuite{Name: name, Timestamp: start, reportGranularity: reportGranularity}
}

// NewCase returns the address of a newly create Testcase
Expand Down Expand Up @@ -190,7 +191,7 @@ func (ts *Testsuite) AddProperty(property Property) {

// NewSubSuite creates a new child suite and returns it.
func (ts *Testsuite) NewSubSuite(name string) *Testsuite {
s := NewSuite(name)
s := NewSuite(name, "")
ts.lock.Lock()
defer ts.lock.Unlock()
ts.SubSuites = append(ts.SubSuites, s)
Expand Down Expand Up @@ -223,9 +224,15 @@ func (ts *Testsuite) summarize() time.Time {
return end
}

func (ts *Testsuite) NewTest(name string) TestReporter {
subSuite := ts.NewSubSuite(name)
return &testReporter{suite: subSuite}
func (ts *Testsuite) NewTestReporter(name string) TestReporter {
switch ts.reportGranularity {
case "test":
tc := NewCase(name)
return &testReporter{testCase: tc, suite: ts}
default:
subSuite := ts.NewSubSuite(name)
return &testReporter{suite: subSuite}
}
}

type stepReport struct {
Expand All @@ -246,9 +253,17 @@ func (s *stepReport) AddAssertions(i int) {
s.assertions += i
}

func (s *stepReport) populate(testCase *Testcase) {
if s.failed {
testCase.Failure = NewFailure(s.failureMsg, s.errors)
}
testCase.Assertions += s.assertions
}

type testReporter struct {
suite *Testsuite
stepReports []*stepReport
testCase *Testcase
}

func (r *testReporter) Step(stepName string) StepReporter {
Expand All @@ -258,12 +273,18 @@ func (r *testReporter) Step(stepName string) StepReporter {
}

func (r *testReporter) Done() {
if r.testCase != nil {
// Reporting with test granularity.
for _, report := range r.stepReports {
report.populate(r.testCase)
}
r.suite.AddTestcase(r.testCase)
return
}
// Reporting with step granularity.
for _, report := range r.stepReports {
testCase := NewCase(report.name)
if report.failed {
testCase.Failure = NewFailure(report.failureMsg, report.errors)
}
testCase.Assertions += report.assertions
report.populate(testCase)
r.suite.AddTestcase(testCase)
}
}
Expand Down Expand Up @@ -346,13 +367,6 @@ func ensureDir(dir string) error {
return err
}

// NewSuite creates and assigns a TestSuite to the TestSuites (then returns the suite)
func (ts *Testsuites) NewSuite(name string) *Testsuite {
suite := NewSuite(name)
ts.AddTestSuite(suite)
return suite
}

// SetFailure adds a failure to the TestSuites collection for startup failures in the test harness
func (ts *Testsuites) SetFailure(message string) {
ts.Failure = &Failure{
Expand Down
12 changes: 9 additions & 3 deletions pkg/test/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ func (h *Harness) RunTests() {

h.T.Run("harness", func(t *testing.T) {
for testDir, tests := range realTestSuite {
suiteReport := h.report.NewSuite(testDir)
suiteReport := h.NewSuiteReport(testDir)
for _, test := range tests {
test := test

Expand All @@ -397,8 +397,7 @@ func (h *Harness) RunTests() {
t.Fatal(err)
}

testReporter := suiteReport.NewTest(test.Name)
test.Run(t, testReporter)
test.Run(t, suiteReport.NewTestReporter(test.Name))
})
}
}
Expand Down Expand Up @@ -612,6 +611,13 @@ func (h *Harness) Report() {
}
}

// NewSuiteReport creates and assigns a TestSuite to the TestSuites (then returns the suite),
func (h *Harness) NewSuiteReport(name string) *report.Testsuite {
suite := report.NewSuite(name, h.TestSuite.ReportGranularity)
h.report.AddTestSuite(suite)
return suite
}

// reportName returns the configured ReportName.
func (h *Harness) reportName() string {
if h.TestSuite.ReportName != "" {
Expand Down
6 changes: 6 additions & 0 deletions test/junit/.gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
/kuttl-ouput-step-json.txt
/kuttl-ouput-step-xml.txt
/kuttl-ouput-test-json.txt
/kuttl-ouput-test-xml.txt
/kuttl-report-step.json
/kuttl-report-step.json.normalized
/kuttl-report-step.xml
/kuttl-report-step.xml.normalized
/kuttl-report-test.json
/kuttl-report-test.json.normalized
/kuttl-report-test.xml
/kuttl-report-test.xml.normalized
14 changes: 12 additions & 2 deletions test/junit/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,28 @@
test:
$(MAKE) -C ../../ cli
rm -f kuttl-report-step.xml.normalized kuttl-report-step.json.normalized
../../bin/kubectl-kuttl test --config /dev/null --start-control-plane --report-name kuttl-report-step --timeout 10 --report xml suite1 suite2 > kuttl-ouput-step-xml.txt 2>&1 || true # this is meant to fail
rm -f kuttl-report-test.xml.normalized kuttl-report-test.json.normalized
../../bin/kubectl-kuttl test --config /dev/null --start-control-plane --report-name kuttl-report-step --report-granularity step --timeout 10 --report xml suite1 suite2 > kuttl-ouput-step-xml.txt 2>&1 || true # this is meant to fail
if [ ! -e kuttl-report-step.xml ]; then cat kuttl-output-step-xml.txt; exit 1; fi
../../bin/kubectl-kuttl test --config /dev/null --start-control-plane --report-name kuttl-report-step --timeout 10 --report json suite1 suite2 > kuttl-ouput-step-json.txt 2>&1 || true # this is meant to fail
../../bin/kubectl-kuttl test --config /dev/null --start-control-plane --report-name kuttl-report-step --report-granularity step --timeout 10 --report json suite1 suite2 > kuttl-ouput-step-json.txt 2>&1 || true # this is meant to fail
if [ ! -e kuttl-report-step.json ]; then cat kuttl-output-step-json.txt; exit 1; fi
../../bin/kubectl-kuttl test --config /dev/null --start-control-plane --report-name kuttl-report-test --report-granularity test --timeout 10 --report xml suite1 suite2 > kuttl-ouput-test-xml.txt 2>&1 || true # this is meant to fail
if [ ! -e kuttl-report-test.xml ]; then cat kuttl-output-test-xml.txt; exit 1; fi
../../bin/kubectl-kuttl test --config /dev/null --start-control-plane --report-name kuttl-report-test --report-granularity test --timeout 10 --report json suite1 suite2 > kuttl-ouput-test-json.txt 2>&1 || true # this is meant to fail
if [ ! -e kuttl-report-test.json ]; then cat kuttl-output-test-json.txt; exit 1; fi
$(MAKE) kuttl-report-step.xml.normalized kuttl-report-step.json.normalized
$(MAKE) kuttl-report-test.xml.normalized kuttl-report-test.json.normalized
diff -u kuttl-report-step.xml.golden kuttl-report-step.xml.normalized
diff -u kuttl-report-step.json.golden kuttl-report-step.json.normalized
diff -u kuttl-report-test.xml.golden kuttl-report-test.xml.normalized
diff -u kuttl-report-test.json.golden kuttl-report-test.json.normalized

.PHONY: update-golden
update-golden:
cp kuttl-report-step.json.normalized kuttl-report-step.json.golden
cp kuttl-report-step.xml.normalized kuttl-report-step.xml.golden
cp kuttl-report-test.json.normalized kuttl-report-test.json.golden
cp kuttl-report-test.xml.normalized kuttl-report-test.xml.golden

# The following targets replace all timestamps and durations with dummy values to make comparisons easy.

Expand Down
78 changes: 78 additions & 0 deletions test/junit/kuttl-report-test.json.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"name": "",
"tests": 6,
"failures": 4,
"time": "1.0",
"testsuite": [
{
"tests": 3,
"failures": 2,
"timestamp": "2000-01-01T00:00:00.00000000+00:00",
"time": "1.0",
"name": "suite1",
"testcase": [
{
"classname": "suite1",
"name": "test0",
"timestamp": "2000-01-01T00:00:00.00000000+00:00",
"time": "1.0"
},
{
"classname": "suite1",
"name": "test1",
"timestamp": "2000-01-01T00:00:00.00000000+00:00",
"time": "1.0",
"failure": {
"text": "command \"echo step stdout\\\\n echo \u003e\u00262 step stderr\\\\n false\" failed, exit status 1",
"message": "failed in step 2-run"
}
},
{
"classname": "suite1",
"name": "test2",
"timestamp": "2000-01-01T00:00:00.00000000+00:00",
"time": "1.0",
"failure": {
"text": "command \"echo assert stdout\\\\n echo \u003e\u00262 assert stderr\\\\n false\" failed, exit status 1",
"message": "failed in step 1-run"
}
}
]
},
{
"tests": 3,
"failures": 2,
"timestamp": "2000-01-01T00:00:00.00000000+00:00",
"time": "1.0",
"name": "suite2",
"testcase": [
{
"classname": "suite2",
"name": "test0",
"timestamp": "2000-01-01T00:00:00.00000000+00:00",
"time": "1.0"
},
{
"classname": "suite2",
"name": "test1",
"timestamp": "2000-01-01T00:00:00.00000000+00:00",
"time": "1.0",
"failure": {
"text": "command \"echo step stdout\\\\n echo \u003e\u00262 step stderr\\\\n false\" failed, exit status 1",
"message": "failed in step 2-run"
}
},
{
"classname": "suite2",
"name": "test2",
"timestamp": "2000-01-01T00:00:00.00000000+00:00",
"time": "1.0",
"failure": {
"text": "command \"echo assert stdout\\\\n echo \u003e\u00262 assert stderr\\\\n false\" failed, exit status 1",
"message": "failed in step 1-run"
}
}
]
}
]
}
20 changes: 20 additions & 0 deletions test/junit/kuttl-report-test.xml.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<testsuites name="" tests="6" failures="4" time="1.0">
<testsuite tests="3" failures="2" timestamp="2000-01-01T00:00:00.00000000+00:00" time="1.0" name="suite1">
<testcase classname="suite1" name="test0" timestamp="2000-01-01T00:00:00.00000000+00:00" time="1.0" assertions="0"></testcase>
<testcase classname="suite1" name="test1" timestamp="2000-01-01T00:00:00.00000000+00:00" time="1.0" assertions="0">
<failure message="failed in step 2-run" type="">command &#34;echo step stdout\\n echo &gt;&amp;2 step stderr\\n false&#34; failed, exit status 1</failure>
</testcase>
<testcase classname="suite1" name="test2" timestamp="2000-01-01T00:00:00.00000000+00:00" time="1.0" assertions="0">
<failure message="failed in step 1-run" type="">command &#34;echo assert stdout\\n echo &gt;&amp;2 assert stderr\\n false&#34; failed, exit status 1</failure>
</testcase>
</testsuite>
<testsuite tests="3" failures="2" timestamp="2000-01-01T00:00:00.00000000+00:00" time="1.0" name="suite2">
<testcase classname="suite2" name="test0" timestamp="2000-01-01T00:00:00.00000000+00:00" time="1.0" assertions="0"></testcase>
<testcase classname="suite2" name="test1" timestamp="2000-01-01T00:00:00.00000000+00:00" time="1.0" assertions="0">
<failure message="failed in step 2-run" type="">command &#34;echo step stdout\\n echo &gt;&amp;2 step stderr\\n false&#34; failed, exit status 1</failure>
</testcase>
<testcase classname="suite2" name="test2" timestamp="2000-01-01T00:00:00.00000000+00:00" time="1.0" assertions="0">
<failure message="failed in step 1-run" type="">command &#34;echo assert stdout\\n echo &gt;&amp;2 assert stderr\\n false&#34; failed, exit status 1</failure>
</testcase>
</testsuite>
</testsuites>
Loading