Skip to content

Commit 4a87521

Browse files
charlyxnkubala
andauthored
✨ Add new output format JUnit (#254)
* 🚧 Introduce new output format JUnit * deprecated `json` flag option * added new flag option `output` * started XML marshalling for JUnit format * 🐛 Add missing type in OutputValue String method * 🐛 Print Junit report * ✨ Generate report in Junit format * ✅ Test FinalResults for JSON and JUnit * Apply suggestions from code review Co-authored-by: Nick Kubala <[email protected]> * 🔥 Remove unused package errors * 📝 Add missing parenthesis * 📝 Add junit to output formats in test help * 📝 Add output formats section with samples Co-authored-by: Nick Kubala <[email protected]>
1 parent c7dfd1c commit 4a87521

File tree

7 files changed

+223
-24
lines changed

7 files changed

+223
-24
lines changed

README.md

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,12 +387,83 @@ container_test(
387387
-f, --force force run of host driver (without user prompt)
388388
-h, --help help for test
389389
-i, --image string path to test image
390-
-j, --json output test results in json format
391390
--metadata string path to image metadata file
391+
--no-color no color in the output
392+
-o, --output string output format for the test report (available format: text, json, junit) (default "text")
392393
--pull force a pull of the image before running tests
393394
-q, --quiet flag to suppress output
395+
--runtime string runtime to use with docker driver
394396
--save preserve created containers after test run
395-
--test-report string generate JSON test report and write it to specified file.
396-
397+
--test-report string generate test report and write it to specified file (supported format: json, junit; default: json)
397398
```
398399
See this [example repo](https://github.com/nkubala/structure-test-examples) for a full working example.
400+
401+
## Output formats
402+
403+
Reports are generated using one of the following output formats: `text`, `json` or `junit`.
404+
Formats like `json` and `junit` can also be used to write a report to a specified file using the `--test-report`.
405+
406+
### Output samples
407+
408+
#### Text
409+
410+
```text
411+
====================================
412+
====== Test file: config.yaml ======
413+
====================================
414+
=== RUN: File Existence Test: whoami
415+
--- PASS
416+
duration: 0s
417+
=== RUN: Metadata Test
418+
--- PASS
419+
duration: 0s
420+
421+
=====================================
422+
============== RESULTS ==============
423+
=====================================
424+
Passes: 2
425+
Failures: 0
426+
Duration: 0s
427+
Total tests: 2
428+
429+
PASS
430+
```
431+
432+
#### JSON
433+
434+
The following sample has been formatted.
435+
436+
```json
437+
{
438+
"Pass": 2,
439+
"Fail": 0,
440+
"Total": 2,
441+
"Duration": 0,
442+
"Results": [
443+
{
444+
"Name": "File Existence Test: whoami",
445+
"Pass": true,
446+
"Duration": 0
447+
},
448+
{
449+
"Name": "Metadata Test",
450+
"Pass": true,
451+
"Duration": 0
452+
}
453+
]
454+
}
455+
```
456+
457+
### JUnit
458+
459+
The following sample has been formatted.
460+
461+
```xml
462+
<?xml version="1.0"?>
463+
<testsuites failures="0" tests="2" time="0">
464+
<testsuite>
465+
<testcase name="File Existence Test: whoami" time="0"/>
466+
<testcase name="Metadata Test" time="0"/>
467+
</testsuite>
468+
</testsuites>
469+
```

cmd/container-structure-test/app/cmd/test.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,12 @@ func NewCmdTest(out io.Writer) *cobra.Command {
6464
RunE: func(cmd *cobra.Command, _ []string) error {
6565
if opts.TestReport != "" {
6666
// Force JsonOutput
67-
opts.JSON = true
67+
if opts.Output == unversioned.Text {
68+
opts.JSON = true
69+
opts.Output = unversioned.Json
70+
71+
logrus.Warn("raw text format unsupported for writing output file, defaulting to JSON")
72+
}
6873
testReportFile, err := os.Create(opts.TestReport)
6974
if err != nil {
7075
return err
@@ -79,6 +84,10 @@ func NewCmdTest(out io.Writer) *cobra.Command {
7984

8085
color.NoColor = opts.NoColor
8186

87+
if opts.JSON {
88+
opts.Output = unversioned.Json
89+
}
90+
8291
return run(out)
8392
},
8493
}
@@ -132,12 +141,12 @@ func run(out io.Writer) error {
132141
channel := make(chan interface{}, 1)
133142
go runTests(out, channel, args, driverImpl)
134143
// TODO(nkubala): put a sync.WaitGroup here
135-
return test.ProcessResults(out, opts.JSON, channel)
144+
return test.ProcessResults(out, opts.Output, channel)
136145
}
137146

138147
func runTests(out io.Writer, channel chan interface{}, args *drivers.DriverConfig, driverImpl func(drivers.DriverConfig) (drivers.Driver, error)) {
139148
for _, file := range opts.ConfigFiles {
140-
if !opts.JSON {
149+
if opts.Output == unversioned.Text {
141150
output.Banner(out, file)
142151
}
143152
tests, err := test.Parse(file, args, driverImpl)
@@ -190,9 +199,11 @@ func AddTestFlags(cmd *cobra.Command) {
190199
cmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "flag to suppress output")
191200
cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "force run of host driver (without user prompt)")
192201
cmd.Flags().BoolVarP(&opts.JSON, "json", "j", false, "output test results in json format")
202+
cmd.Flags().MarkDeprecated("json", "please use --output instead")
203+
cmd.Flags().VarP(&opts.Output, "output", "o", "output format for the test report (available format: text, json, junit)")
193204
cmd.Flags().BoolVar(&opts.NoColor, "no-color", false, "no color in the output")
194205

195206
cmd.Flags().StringArrayVarP(&opts.ConfigFiles, "config", "c", []string{}, "test config files")
196207
cmd.MarkFlagRequired("config")
197-
cmd.Flags().StringVar(&opts.TestReport, "test-report", "", "generate JSON test report and write it to specified file.")
208+
cmd.Flags().StringVar(&opts.TestReport, "test-report", "", "generate test report and write it to specified file (supported format: json, junit; default: json)")
198209
}

cmd/container-structure-test/app/cmd/test/util.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ func Parse(fp string, args *drivers.DriverConfig, driverImpl func(drivers.Driver
105105
return tests, nil
106106
}
107107

108-
func ProcessResults(out io.Writer, json bool, c chan interface{}) error {
108+
func ProcessResults(out io.Writer, format unversioned.OutputValue, c chan interface{}) error {
109109
totalPass := 0
110110
totalFail := 0
111111
totalDuration := time.Duration(0)
@@ -115,7 +115,7 @@ func ProcessResults(out io.Writer, json bool, c chan interface{}) error {
115115
return errors.Wrap(err, "reading results from channel")
116116
}
117117
for _, r := range results {
118-
if !json {
118+
if format == unversioned.Text {
119119
// output individual results if we're not in json mode
120120
output.OutputResult(out, r)
121121
}
@@ -139,11 +139,11 @@ func ProcessResults(out io.Writer, json bool, c chan interface{}) error {
139139
Fail: totalFail,
140140
Duration: totalDuration,
141141
}
142-
if json {
142+
if format == unversioned.Json || format == unversioned.Junit {
143143
// only output results here if we're in json mode
144144
summary.Results = results
145145
}
146-
output.FinalResults(out, json, summary)
146+
output.FinalResults(out, format, summary)
147147

148148
return err
149149
}

pkg/config/options.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
package config
1616

17+
import "github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned"
18+
1719
type StructureTestOptions struct {
1820
ImagePath string
1921
Driver string
@@ -23,6 +25,7 @@ type StructureTestOptions struct {
2325
ConfigFiles []string
2426

2527
JSON bool
28+
Output unversioned.OutputValue
2629
Pull bool
2730
Save bool
2831
Quiet bool

pkg/output/output.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package output
1616

1717
import (
1818
"encoding/json"
19+
"encoding/xml"
1920
"fmt"
2021
"io"
2122
"path/filepath"
@@ -56,8 +57,8 @@ func Banner(out io.Writer, filename string) {
5657
color.Purple.Fprintln(out, strings.Repeat("=", bannerLength))
5758
}
5859

59-
func FinalResults(out io.Writer, jsonOut bool, result types.SummaryObject) error {
60-
if jsonOut {
60+
func FinalResults(out io.Writer, format types.OutputValue, result types.SummaryObject) error {
61+
if format == types.Json {
6162
res, err := json.Marshal(result)
6263
if err != nil {
6364
return errors.Wrap(err, "marshalling json")
@@ -66,6 +67,17 @@ func FinalResults(out io.Writer, jsonOut bool, result types.SummaryObject) error
6667
_, err = out.Write(res)
6768
return err
6869
}
70+
71+
if format == types.Junit {
72+
res, err := xml.Marshal(result)
73+
if err != nil {
74+
return errors.Wrap(err, "marshalling xml")
75+
}
76+
res = append(res, []byte("\n")...)
77+
_, err = out.Write(res)
78+
return err
79+
}
80+
6981
if bannerLength%2 == 0 {
7082
bannerLength++
7183
}

pkg/output/output_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package output
2+
3+
import (
4+
"bytes"
5+
"strings"
6+
"testing"
7+
"time"
8+
9+
"github.com/GoogleContainerTools/container-structure-test/pkg/types/unversioned"
10+
)
11+
12+
func TestFinalResults(t *testing.T) {
13+
t.Parallel()
14+
15+
result := unversioned.SummaryObject{
16+
Pass: 1,
17+
Fail: 1,
18+
Total: 2,
19+
Duration: time.Duration(2),
20+
Results: []*unversioned.TestResult{
21+
{
22+
Name: "my first test",
23+
Pass: true,
24+
Stdout: "it works!",
25+
Stderr: "",
26+
Duration: time.Duration(1),
27+
},
28+
{
29+
Name: "my fail",
30+
Pass: false,
31+
Stdout: "",
32+
Stderr: "this failed",
33+
Errors: []string{"this failed because of that"},
34+
Duration: time.Duration(1),
35+
},
36+
},
37+
}
38+
39+
var finalResultsTests = []struct {
40+
actual *bytes.Buffer
41+
format unversioned.OutputValue
42+
expected string
43+
}{
44+
{
45+
actual: bytes.NewBuffer([]byte{}),
46+
format: unversioned.Junit,
47+
expected: `<testsuites failures="1" tests="2" time="2"><testsuite><testcase name="my first test" time="1"></testcase><testcase name="my fail" time="1"><failure>this failed because of that</failure></testcase></testsuite></testsuites>`,
48+
},
49+
{
50+
actual: bytes.NewBuffer([]byte{}),
51+
format: unversioned.Json,
52+
expected: `{"Pass":1,"Fail":1,"Total":2,"Duration":2,"Results":[{"Name":"my first test","Pass":true,"Stdout":"it works!","Duration":1},{"Name":"my fail","Pass":false,"Stderr":"this failed","Errors":["this failed because of that"],"Duration":1}]}`,
53+
},
54+
}
55+
56+
for _, test := range finalResultsTests {
57+
test := test
58+
59+
t.Run(test.format.String(), func(t *testing.T) {
60+
t.Parallel()
61+
62+
FinalResults(test.actual, test.format, result)
63+
64+
if strings.TrimSpace(test.actual.String()) != test.expected {
65+
t.Errorf("expected %s but got %s", test.expected, test.actual)
66+
}
67+
})
68+
}
69+
}

pkg/types/unversioned/types.go

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package unversioned
1616

1717
import (
18+
"encoding/xml"
1819
"fmt"
1920
"strings"
2021
"time"
@@ -43,12 +44,12 @@ type Config struct {
4344
}
4445

4546
type TestResult struct {
46-
Name string
47-
Pass bool
48-
Stdout string `json:",omitempty"`
49-
Stderr string `json:",omitempty"`
50-
Errors []string `json:",omitempty"`
51-
Duration time.Duration
47+
Name string `xml:"name,attr"`
48+
Pass bool `xml:"-"`
49+
Stdout string `json:",omitempty" xml:"-"`
50+
Stderr string `json:",omitempty" xml:"-"`
51+
Errors []string `json:",omitempty" xml:"failure"`
52+
Duration time.Duration `xml:"time,attr"`
5253
}
5354

5455
func (t *TestResult) String() string {
@@ -86,9 +87,41 @@ func (t *TestResult) IsPass() bool {
8687
}
8788

8889
type SummaryObject struct {
89-
Pass int
90-
Fail int
91-
Total int
92-
Duration time.Duration
93-
Results []*TestResult `json:",omitempty"`
90+
XMLName xml.Name `json:"-" xml:"testsuites"`
91+
Pass int `xml:"-"`
92+
Fail int `xml:"failures,attr"`
93+
Total int `xml:"tests,attr"`
94+
Duration time.Duration `xml:"time,attr"`
95+
Results []*TestResult `json:",omitempty" xml:"testsuite>testcase"`
96+
}
97+
98+
type OutputValue int
99+
100+
const (
101+
Text OutputValue = iota
102+
Json
103+
Junit
104+
)
105+
106+
func (o OutputValue) String() string {
107+
return [...]string{"text", "json", "junit"}[o]
108+
}
109+
110+
func (o OutputValue) Type() string {
111+
return "string"
112+
}
113+
114+
func (o *OutputValue) Set(value string) error {
115+
switch value {
116+
case "text":
117+
*o = Text
118+
case "json":
119+
*o = Json
120+
case "junit":
121+
*o = Junit
122+
default:
123+
return fmt.Errorf("unsupported format %s: please select from `text`, `json`, or `junit`", value)
124+
}
125+
126+
return nil
94127
}

0 commit comments

Comments
 (0)