Skip to content
Draft
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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,25 @@ kubetest2 \
exec \
go test ./my/test/package
```

---

### `gotest-junit` tester

This tester executes a Go test binary and generates JUnit XML output.

It wraps the test execution, captures the output, and uses `go-junit-report` to generate a JUnit XML file in the artifacts directory.

```
kubetest2 \
eksapi \
--kubernetes-version=X.XX \
--up \
--down \
--test=gotest-junit \
-- ./my/gotest/binary.test
```

The JUnit XML file is written to `$ARTIFACTS/junit_<test-case>.xml` (e.g., `_artifacts/junit_testcase.xml`).

The artifacts directory defaults to `./_artifacts` or can be set via the `ARTIFACTS` environment variable.
9 changes: 9 additions & 0 deletions cmd/kubetest2-tester-gotest-junit/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package main

import (
"github.com/aws/aws-k8s-tester/internal/testers/gotestjunit"
)

func main() {
gotestjunit.Main()
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ require (
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/in-toto/attestation v1.1.0 // indirect
github.com/jstemmer/go-junit-report/v2 v2.1.0 // indirect
github.com/kris-nova/logger v0.2.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
Expand Down Expand Up @@ -607,6 +608,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report/v2 v2.1.0 h1:X3+hPYlSczH9IMIpSC9CQSZA0L+BipYafciZUWHEmsc=
github.com/jstemmer/go-junit-report/v2 v2.1.0/go.mod h1:mgHVr7VUo5Tn8OLVr1cKnLuEy0M92wdRntM99h7RkgQ=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
Expand Down
168 changes: 168 additions & 0 deletions internal/testers/gotestjunit/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package gotestjunit

import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"

"github.com/aws/aws-k8s-tester/internal"
"github.com/jstemmer/go-junit-report/v2/junit"
"github.com/jstemmer/go-junit-report/v2/parser/gotest"
"github.com/octago/sflags/gen/gpflag"
"k8s.io/klog/v2"
"sigs.k8s.io/kubetest2/pkg/artifacts"
"sigs.k8s.io/kubetest2/pkg/testers"
)

type Tester struct {
argv []string
}

const usage = `kubetest2 --test=gotest-junit -- [TestCommand] [TestArgs]
TestCommand: the Go test binary to invoke
TestArgs: arguments passed to test command

This tester executes a Go test binary and generates JUnit XML output.
`

func (t *Tester) Execute() error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the intention to export these? Do we expect things outside this command to use them/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, but it's consistent with the ginkgov1 code.

fs, err := gpflag.Parse(t)
if err != nil {
return fmt.Errorf("failed to initialize tester: %v", err)
}

fs.Usage = func() {
fmt.Print(usage)
}

if len(os.Args) < 2 {
fs.Usage()
return nil
}

help := fs.BoolP("help", "h", false, "")
_ = fs.Parse(os.Args[1:2])

if *help {
fs.Usage()
return nil
}

t.argv = os.Args[1:]
if err := testers.WriteVersionToMetadata(internal.Version, ""); err != nil {
return err
}
return t.Test()
}

func expandEnv(args []string) []string {
expanded := make([]string, len(args))
for i, arg := range args {
if strings.Contains(arg, `\$`) {
expanded[i] = strings.ReplaceAll(arg, `\$`, `$`)
} else {
expanded[i] = os.ExpandEnv(arg)
}
}
return expanded
}

func (t *Tester) Test() error {
args := expandEnv(t.argv)
return run(args[0], args[1:])
}

func run(binary string, args []string) error {
var buf bytes.Buffer

cmd := exec.Command(binary, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = io.MultiWriter(os.Stdout, &buf)
cmd.Stderr = io.MultiWriter(os.Stderr, &buf)
Comment on lines +85 to +86
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cmd.Stdout = io.MultiWriter(os.Stdout, &buf)
cmd.Stderr = io.MultiWriter(os.Stderr, &buf)
// ensure we also capture output
var systemout bytes.Buffer
syncSystemOut := &mutexWriter{
writer: &systemout,
}
cmd.Stdout = io.MultiWriter(syncSystemOut, os.Stdout)
cmd.Stderr = io.MultiWriter(syncSystemOut, os.Stderr)

I wonder if we should add this to be more consistent with upstream kubetest2's junitexec.go.


signals := make(chan os.Signal, 5)
signal.Notify(signals)
defer signal.Stop(signals)
Comment on lines +88 to +90
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we are doing this with channels and having to manage signal handling vs just waiting on the binary in the main thread?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably just simplify it to testErr := cmd.Run(), but the current implementation is more consistent with the upstream kubetest2 exec.go.


if err := cmd.Start(); err != nil {
return err
}

wait := make(chan error, 1)
go func() {
wait <- cmd.Wait()
close(wait)
}()

var testErr error
for {
select {
case sig := <-signals:
_ = cmd.Process.Signal(sig)
case testErr = <-wait:
goto done
}
}
done:

if err := writeJUnit(binary, &buf); err != nil {
klog.Errorf("failed to write junit: %v", err)
}

return testErr
}

func writeJUnit(binary string, output *bytes.Buffer) error {
parser := gotest.NewParser()
report, err := parser.Parse(output)
if err != nil {
return err
}

name := filepath.Base(binary)
name = strings.TrimSuffix(name, ".test")

if err := os.MkdirAll(artifacts.BaseDir(), 0755); err != nil {
return err
}

hostname, _ := os.Hostname()
testsuites := junit.CreateFromReport(report, hostname)

for i, suite := range testsuites.Suites {
if suite.Name == "" {
testsuites.Suites[i].Name = name
}
filename := fmt.Sprintf("junit_%s.xml", name)
if len(testsuites.Suites) > 1 {
filename = fmt.Sprintf("junit_%s_%02d.xml", name, i)
}
f, err := os.Create(filepath.Join(artifacts.BaseDir(), filename))
if err != nil {
return err
}
single := junit.Testsuites{Suites: []junit.Testsuite{testsuites.Suites[i]}}
err = single.WriteXML(f)
f.Close()
if err != nil {
return err
}
}
return nil
}

func NewDefaultTester() *Tester {
return &Tester{}
}

func Main() {
t := NewDefaultTester()
if err := t.Execute(); err != nil {
klog.Fatalf("failed to run gotest-junit tester: %v", err)
}
}