Skip to content

Commit

Permalink
Caputre hive test results in CircleCI (#76)
Browse files Browse the repository at this point in the history
Adds junitformatter that converts hive test output JSON to JUnit xml that can be parsed by CircleCI.
  • Loading branch information
ajsutton authored Mar 14, 2023
1 parent b6d86ac commit 6b11a85
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 3 deletions.
17 changes: 14 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ jobs:
- go/mod-download
- go/save-cache
- run: {command: "go build ."}
- run: {command: "go build junit/junitformatter.go"}
# Store the executable.
- persist_to_workspace:
root: .
paths: ["hive"]
paths: ["hive", "junitformatter"]

# The below job runs the optimism test simulations. This requires a virtual
# machine instead of the container-based build environment because hive needs
Expand All @@ -41,16 +42,26 @@ jobs:
-sim=<<parameters.sim>> \
-sim.loglevel=5 \
-docker.pull=true \
-client=go-ethereum,op-geth_optimism,op-proposer_develop,op-batcher_develop,op-node_develop |& tee /tmp/build/hive.log || echo "failed."
-client=go-ethereum,op-geth_optimism,op-proposer_develop,op-batcher_develop,op-node_develop |& tee /tmp/build/hive.log
- run:
command: |
tar -cvf /tmp/workspace.tgz -C /home/circleci/project /home/circleci/project/workspace
name: "Archive workspace"
when: always
- store_artifacts:
path: /tmp/workspace.tgz
destination: hive-workspace.tgz
when: always
- run:
command: "! grep 'pass.*=false' /tmp/build/hive.log"
command: |
/tmp/build/junitformatter /home/circleci/project/workspace/logs/*.json > /home/circleci/project/workspace/logs/junit.xml
when: always
- store_test_results:
path: /home/circleci/project/workspace/logs/junit.xml
when: always
- store_artifacts:
path: /home/circleci/project/workspace/logs/junit.xml
when: always
- slack/notify:
channel: C03N11M0BBN
branch_pattern: optimism
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ workspace
.idea/
# build output
/hive
/junitformatter
155 changes: 155 additions & 0 deletions junit/junitformatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package main

import (
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"os"
"strconv"

"github.com/ethereum/hive/internal/libhive"
)

func main() {
if len(os.Args) <= 1 {
fail(errors.New("no input files specified"))
}

result := TestSuites{
Failures: 0,
Name: "Hive Results",
Tests: 0,
}
var suites []TestSuite

for i := 1; i < len(os.Args); i++ {
suite, err := readInput(os.Args[i])
if err != nil {
fail(err)
}
junitSuite := mapTestSuite(suite)
result.Failures = result.Failures + junitSuite.Failures
result.Tests = result.Tests + junitSuite.Tests
suites = append(suites, junitSuite)
}
result.Suites = suites

junit, err := xml.MarshalIndent(result, "", " ")
if err != nil {
fail(err)
}
fmt.Println(string(junit))
}

func readInput(file string) (libhive.TestSuite, error) {
inData, err := os.ReadFile(file)
if err != nil {
return libhive.TestSuite{}, fmt.Errorf("failed to read file '%v': %w", file, err)
}

var suite libhive.TestSuite
err = json.Unmarshal(inData, &suite)
if err != nil {
return libhive.TestSuite{}, fmt.Errorf("failed to parse file '%v': %w", file, err)
}
return suite, nil
}

func mapTestSuite(suite libhive.TestSuite) TestSuite {
junitSuite := TestSuite{
Name: suite.Name,
Failures: 0,
Tests: len(suite.TestCases),
Properties: Properties{},
}
for clientName, clientVersion := range suite.ClientVersions {
junitSuite.Properties.Properties = append(junitSuite.Properties.Properties, Property{
Name: clientName,
Value: clientVersion,
})
}
for _, testCase := range suite.TestCases {
if !testCase.SummaryResult.Pass {
junitSuite.Failures = junitSuite.Failures + 1
}
junitSuite.TestCases = append(junitSuite.TestCases, mapTestCase(testCase))
}
return junitSuite
}

func mapTestCase(source *libhive.TestCase) TestCase {
result := TestCase{
Name: source.Name,
}
if source.SummaryResult.Pass {
result.SystemOut = source.SummaryResult.Details
} else {
result.Failure = &Failure{Message: source.SummaryResult.Details}
}
duration := source.End.Sub(source.Start)
result.Time = strconv.FormatFloat(duration.Seconds(), 'f', 6, 64)
return result
}

func fail(reason error) {
fmt.Println(reason)
os.Exit(1)
}

/*
Target XML format (lots of it being optional):
<testsuites disabled="" errors="" failures="" name="" tests="" time="">
<testsuite disabled="" errors="" failures="" hostname="" id=""
name="" package="" skipped="" tests="" time="" timestamp="">
<properties>
<property name="" value=""/>
</properties>
<testcase assertions="" classname="" name="" status="" time="">
<skipped/>
<error message="" type=""/>
<failure message="" type=""/>
<system-out/>
<system-err/>
</testcase>
<system-out/>
<system-err/>
</testsuite>
</testsuites>
*/

type TestSuites struct {
XMLName string `xml:"testsuites,omitempty"`
Failures int `xml:"failures,attr"`
Name string `xml:"name,attr"`
Tests int `xml:"tests,attr"`
Suites []TestSuite `xml:"testsuite"`
}

type TestSuite struct {
Name string `xml:"name,attr"`
Failures int `xml:"failures,attr"`
Tests int `xml:"tests,attr"`
Properties Properties `xml:"properties,omitempty"`
TestCases []TestCase `xml:"testcase"`
}

type TestCase struct {
Name string `xml:"name,attr"`
Time string `xml:"time,attr"`
Failure *Failure `xml:"failure,omitempty"`
SystemOut string `xml:"system-out,omitempty"`
}

type Failure struct {
Message string `xml:"message,attr"`
}

type Properties struct {
Properties []Property `xml:"property,omitempty"`
}

type Property struct {
Name string `xml:"name,attr"`
Value string `xml:"value,attr"`
}

0 comments on commit 6b11a85

Please sign in to comment.