Move common go-junit-report code into separate internal package

There was some code duplication between the go-junit-report binary, its
tests and the testdata/generate-golden script. This has been moved into
an internal package. The go-junit-report binary can now just focus on
flag parsing and validation, and it should be less likely that the
binary, tests and golden report generator behave differently.
This commit is contained in:
Joël Stemmer 2022-04-06 00:52:52 +01:00
parent d84b066208
commit 3260a9d2e0
4 changed files with 152 additions and 170 deletions

View File

@ -0,0 +1,98 @@
package gojunitreport
import (
"encoding/json"
"encoding/xml"
"fmt"
"io"
"os"
"time"
"github.com/jstemmer/go-junit-report/v2/gtr"
"github.com/jstemmer/go-junit-report/v2/junit"
"github.com/jstemmer/go-junit-report/v2/parser/gotest"
)
type parser interface {
Parse(r io.Reader) (gtr.Report, error)
Events() []gotest.Event
}
// Config contains the go-junit-report command configuration.
type Config struct {
Parser string
Hostname string
PackageName string
SkipXMLHeader bool
Properties map[string]string
TimestampFunc func() time.Time
// For debugging
PrintEvents bool
}
// Run runs the go-junit-report command and returns the generated report.
func (c Config) Run(input io.Reader, output io.Writer) (*gtr.Report, error) {
var p parser
switch c.Parser {
case "gotest":
p = gotest.NewParser(
gotest.PackageName(c.PackageName),
gotest.TimestampFunc(c.TimestampFunc),
)
case "gojson":
p = gotest.NewJSONParser(
gotest.PackageName(c.PackageName),
gotest.TimestampFunc(c.TimestampFunc),
)
default:
return nil, fmt.Errorf("invalid parser: %s", c.Parser)
}
report, err := p.Parse(input)
if err != nil {
return nil, fmt.Errorf("error parsing input: %w", err)
}
if c.PrintEvents {
enc := json.NewEncoder(os.Stderr)
for _, event := range p.Events() {
if err := enc.Encode(event); err != nil {
return nil, err
}
}
}
for i := range report.Packages {
for k, v := range c.Properties {
report.Packages[i].SetProperty(k, v)
}
}
if err = c.writeXML(output, report); err != nil {
return nil, err
}
return &report, nil
}
func (c Config) writeXML(w io.Writer, report gtr.Report) error {
testsuites := junit.CreateFromReport(report, c.Hostname)
if !c.SkipXMLHeader {
_, err := fmt.Fprintf(w, xml.Header)
if err != nil {
return err
}
}
enc := xml.NewEncoder(w)
enc.Indent("", "\t")
if err := enc.Encode(testsuites); err != nil {
return err
}
if err := enc.Flush(); err != nil {
return err
}
_, err := fmt.Fprintf(w, "\n")
return err
}

View File

@ -1,4 +1,4 @@
package main package gojunitreport
import ( import (
"bytes" "bytes"
@ -12,25 +12,17 @@ import (
"testing" "testing"
"time" "time"
"github.com/jstemmer/go-junit-report/v2/junit"
"github.com/jstemmer/go-junit-report/v2/parser/gotest"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
) )
const testDataDir = "testdata/" const testDataDir = "../../testdata/"
var matchTest = flag.String("match", "", "only test testdata matching this pattern") var matchTest = flag.String("match", "", "only test testdata matching this pattern")
type TestConfig struct { var testConfigs = map[int]Config{
noXMLHeader bool 5: {SkipXMLHeader: true},
packageName string 6: {SkipXMLHeader: true},
} 7: {PackageName: "test/package"},
var testConfigs = map[int]TestConfig{
5: {noXMLHeader: true},
6: {noXMLHeader: true},
7: {packageName: "test/package"},
} }
func TestRun(t *testing.T) { func TestRun(t *testing.T) {
@ -58,7 +50,7 @@ func TestRun(t *testing.T) {
} }
} }
func testRun(inputFile, reportFile string, config TestConfig, t *testing.T) { func testRun(inputFile, reportFile string, config Config, t *testing.T) {
input, err := os.Open(inputFile) input, err := os.Open(inputFile)
if err != nil { if err != nil {
t.Fatalf("error opening input file: %v", err) t.Fatalf("error opening input file: %v", err)
@ -72,33 +64,19 @@ func testRun(inputFile, reportFile string, config TestConfig, t *testing.T) {
t.Fatalf("error loading report file: %v", err) t.Fatalf("error loading report file: %v", err)
} }
options := []gotest.Option{ config.Parser = "gotest"
gotest.PackageName(config.packageName),
gotest.TimestampFunc(func() time.Time {
return time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
}),
}
var parser Parser
if strings.HasSuffix(inputFile, ".gojson.txt") { if strings.HasSuffix(inputFile, ".gojson.txt") {
parser = gotest.NewJSONParser(options...) config.Parser = "gojson"
} else {
parser = gotest.NewParser(options...)
} }
config.Hostname = "hostname"
report, err := parser.Parse(input) config.Properties = map[string]string{"go.version": "1.0"}
if err != nil { config.TimestampFunc = func() time.Time {
t.Fatal(err) return time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
} }
for i := range report.Packages {
report.Packages[i].SetProperty("go.version", "1.0")
}
testsuites := junit.CreateFromReport(report, "hostname")
var output bytes.Buffer var output bytes.Buffer
if err := writeXML(&output, testsuites, config.noXMLHeader); err != nil { if _, err := config.Run(input, &output); err != nil {
t.Fatalf("error writing XML: %v", err) t.Fatal(err)
} }
if diff := cmp.Diff(output.String(), string(wantReport)); diff != "" { if diff := cmp.Diff(output.String(), string(wantReport)); diff != "" {
@ -106,16 +84,16 @@ func testRun(inputFile, reportFile string, config TestConfig, t *testing.T) {
} }
} }
func testFileConfig(filename string) (conf TestConfig, reportFile string, err error) { func testFileConfig(filename string) (config Config, reportFile string, err error) {
var prefix string var prefix string
if idx := strings.IndexByte(filename, '-'); idx < 0 { if idx := strings.IndexByte(filename, '-'); idx < 0 {
return conf, "", fmt.Errorf("testdata file does not contain a dash (-); expected name `{id}-{name}.txt` got `%s`", filename) return config, "", fmt.Errorf("testdata file does not contain a dash (-); expected name `{id}-{name}.txt` got `%s`", filename)
} else { } else {
prefix = filename[:idx] prefix = filename[:idx]
} }
id, err := strconv.Atoi(prefix) id, err := strconv.Atoi(prefix)
if err != nil { if err != nil {
return conf, "", fmt.Errorf("testdata file did not start with a valid number: %w", err) return config, "", fmt.Errorf("testdata file did not start with a valid number: %w", err)
} }
return testConfigs[id], fmt.Sprintf("%s-report.xml", prefix), nil return testConfigs[id], fmt.Sprintf("%s-report.xml", prefix), nil
} }

View File

@ -1,17 +1,13 @@
package main package main
import ( import (
"encoding/json"
"encoding/xml"
"flag" "flag"
"fmt" "fmt"
"io" "io"
"os" "os"
"strings" "strings"
"github.com/jstemmer/go-junit-report/v2/gtr" "github.com/jstemmer/go-junit-report/v2/internal/gojunitreport"
"github.com/jstemmer/go-junit-report/v2/junit"
"github.com/jstemmer/go-junit-report/v2/parser/gotest"
) )
var ( var (
@ -29,7 +25,7 @@ var (
output = flag.String("out", "", "write XML report to `file`") output = flag.String("out", "", "write XML report to `file`")
iocopy = flag.Bool("iocopy", false, "copy input to stdout; can only be used in conjunction with -out") iocopy = flag.Bool("iocopy", false, "copy input to stdout; can only be used in conjunction with -out")
properties = make(keyValueFlag) properties = make(keyValueFlag)
inputParser = flag.String("parser", "gotest", "set input parser: gotest, gojson") parser = flag.String("parser", "gotest", "set input parser: gotest, gojson")
// debug flags // debug flags
printEvents = flag.Bool("debug.print-events", false, "print events generated by the go test parser") printEvents = flag.Bool("debug.print-events", false, "print events generated by the go test parser")
@ -39,8 +35,9 @@ var (
) )
func main() { func main() {
flag.Var(&properties, "prop", "add property to generated report; properties should be specified as `key=value`") flag.Var(&properties, "p", "add `key=value` property to generated report; repeat this flag to add multiple properties.")
flag.Parse() flag.Parse()
if *iocopy && *output == "" { if *iocopy && *output == "" {
exitf("you must specify an output file with -out when using -iocopy") exitf("you must specify an output file with -out when using -iocopy")
} }
@ -51,7 +48,7 @@ func main() {
} }
if *goVersionFlag != "" { if *goVersionFlag != "" {
fmt.Fprintf(os.Stderr, "the -go-version flag is deprecated and will be removed in the future.\n") fmt.Fprintf(os.Stderr, "the -go-version flag is deprecated and will be removed in the future, use the -p flag instead.\n")
properties["go.version"] = *goVersionFlag properties["go.version"] = *goVersionFlag
} }
@ -62,7 +59,6 @@ func main() {
exitf("") exitf("")
} }
// Read input
var in io.Reader = os.Stdin var in io.Reader = os.Stdin
if *input != "" { if *input != "" {
f, err := os.Open(*input) f, err := os.Open(*input)
@ -73,44 +69,6 @@ func main() {
in = f in = f
} }
if *iocopy {
in = io.TeeReader(in, os.Stdout)
}
var parser Parser
switch *inputParser {
case "gotest":
parser = gotest.NewParser(gotest.PackageName(*packageName))
case "gojson":
parser = gotest.NewJSONParser(gotest.PackageName(*packageName))
default:
fmt.Fprintf(os.Stderr, "invalid parser: %s\n", *inputParser)
flag.Usage()
os.Exit(1)
}
report, err := parser.Parse(in)
if err != nil {
exitf("error parsing input: %s\n", err)
}
if *printEvents {
enc := json.NewEncoder(os.Stderr)
for _, event := range parser.Events() {
if err := enc.Encode(event); err != nil {
exitf("error printing events: %v\n", err)
}
}
}
for i := range report.Packages {
for k, v := range properties {
report.Packages[i].SetProperty(k, v)
}
}
hostname, _ := os.Hostname() // ignore error
testsuites := junit.CreateFromReport(report, hostname)
var out io.Writer = os.Stdout var out io.Writer = os.Stdout
if *output != "" { if *output != "" {
f, err := os.Create(*output) f, err := os.Create(*output)
@ -121,8 +79,23 @@ func main() {
out = f out = f
} }
if err := writeXML(out, testsuites, *noXMLHeader); err != nil { if *iocopy {
exitf("error writing XML: %v", err) in = io.TeeReader(in, os.Stdout)
}
hostname, _ := os.Hostname() // ignore error
config := gojunitreport.Config{
Parser: *parser,
Hostname: hostname,
PackageName: *packageName,
SkipXMLHeader: *noXMLHeader,
Properties: properties,
PrintEvents: *printEvents,
}
report, err := config.Run(in, out)
if err != nil {
exitf("error: %v\n", err)
} }
if *setExitCode && !report.IsSuccessful() { if *setExitCode && !report.IsSuccessful() {
@ -130,25 +103,6 @@ func main() {
} }
} }
func writeXML(w io.Writer, testsuites junit.Testsuites, skipHeader bool) error {
if !skipHeader {
_, err := fmt.Fprintf(w, xml.Header)
if err != nil {
return err
}
}
enc := xml.NewEncoder(w)
enc.Indent("", "\t")
if err := enc.Encode(testsuites); err != nil {
return err
}
if err := enc.Flush(); err != nil {
return err
}
_, err := fmt.Fprintf(w, "\n")
return err
}
func exitf(msg string, args ...interface{}) { func exitf(msg string, args ...interface{}) {
if msg != "" { if msg != "" {
fmt.Fprintf(os.Stderr, msg+"\n", args...) fmt.Fprintf(os.Stderr, msg+"\n", args...)
@ -178,8 +132,3 @@ func (f *keyValueFlag) Set(value string) error {
(*f)[k] = v (*f)[k] = v
return nil return nil
} }
type Parser interface {
Parse(r io.Reader) (gtr.Report, error)
Events() []gotest.Event
}

View File

@ -1,9 +1,8 @@
//go:generate go run generate-golden.go -w //go:generate go run generate-golden-reports.go -w
package main package main
import ( import (
"encoding/xml"
"flag" "flag"
"fmt" "fmt"
"io" "io"
@ -12,22 +11,15 @@ import (
"strings" "strings"
"time" "time"
"github.com/jstemmer/go-junit-report/v2/gtr" "github.com/jstemmer/go-junit-report/v2/internal/gojunitreport"
"github.com/jstemmer/go-junit-report/v2/junit"
"github.com/jstemmer/go-junit-report/v2/parser/gotest"
) )
var verbose bool var verbose bool
type Settings struct { var configs = map[string]gojunitreport.Config{
skipXMLHeader bool "005-no_xml_header.txt": {SkipXMLHeader: true},
packageName string "006-mixed.txt": {SkipXMLHeader: true},
} "007-compiled_test.txt": {PackageName: "test/package"},
var fileSettings = map[string]Settings{
"005-no_xml_header.txt": {skipXMLHeader: true},
"006-mixed.txt": {skipXMLHeader: true},
"007-compiled_test.txt": {packageName: "test/package"},
} }
func main() { func main() {
@ -102,53 +94,18 @@ func createReportFromInput(inputFile, outputFile string, write bool) error {
out = f out = f
} }
settings := fileSettings[inputFile] config := configs[inputFile]
config.Parser = "gotest"
options := []gotest.Option{
gotest.PackageName(settings.packageName),
gotest.TimestampFunc(func() time.Time {
return time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
}),
}
var parser Parser
if strings.HasSuffix(inputFile, ".gojson.txt") { if strings.HasSuffix(inputFile, ".gojson.txt") {
parser = gotest.NewJSONParser(options...) config.Parser = "gojson"
} else {
parser = gotest.NewParser(options...)
} }
report, err := parser.Parse(in) config.Hostname = "hostname"
if err != nil { config.TimestampFunc = func() time.Time {
return err return time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
} }
return writeReport(report, out, settings) config.Properties = map[string]string{"go.version": "1.0"}
}
type Parser interface { _, err = config.Run(in, out)
Parse(r io.Reader) (gtr.Report, error)
}
func writeReport(report gtr.Report, out io.Writer, settings Settings) error {
for i := range report.Packages {
report.Packages[i].SetProperty("go.version", "1.0")
}
testsuites := junit.CreateFromReport(report, "hostname")
if !settings.skipXMLHeader {
if _, err := fmt.Fprintf(out, xml.Header); err != nil {
return err
}
}
enc := xml.NewEncoder(out)
enc.Indent("", "\t")
if err := enc.Encode(testsuites); err != nil {
return err
}
if err := enc.Flush(); err != nil {
return err
}
_, err := fmt.Fprintf(out, "\n")
return err return err
} }