From a656573b64cb20d5aaa24be21b9d49cfa88f1545 Mon Sep 17 00:00:00 2001 From: Yann D'Isanto Date: Fri, 17 May 2024 17:12:37 +0200 Subject: [PATCH 1/3] feat: ctrf for custom reporter --- .gitignore | 3 + README.md | 28 +++- cmd/go-ctrf-json-reporter/main.go | 111 +++++++-------- ctrf/ctrf.go | 218 ++++++++++++++++++++++++++++++ ctrf/ctrf_test.go | 206 ++++++++++++++++++++++++++++ ctrf/report.json | 0 ctrf/validation-test-cases.yaml | 109 +++++++++++++++ go.mod | 7 + go.sum | 9 ++ reporter/reporter.go | 86 ++---------- 10 files changed, 649 insertions(+), 128 deletions(-) create mode 100644 ctrf/ctrf.go create mode 100644 ctrf/ctrf_test.go create mode 100644 ctrf/report.json create mode 100644 ctrf/validation-test-cases.yaml create mode 100644 go.sum diff --git a/.gitignore b/.gitignore index e69de29..65bdf8d 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,3 @@ + +.idea +*.iml diff --git a/README.md b/README.md index 18c8b1d..b13c80e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -# Go JSON Reporter +# Go CTRF JSON format support + +## Go JSON Reporter + A Go JSON test reporter to create test reports that follow the CTRF standard. @@ -115,3 +118,26 @@ When running go-ctrf-json-reporter results in a "command not found" error this u ## Support Us If you find this project useful, consider giving it a GitHub star ⭐ It means a lot to us. + + +## Generate a CTRF JSON report in your own testing tool written in go + +If you are writting your own testing tool and wish to generate a CTRF JSON report, you can use the `ctrf` package. + +```go +import ( + "github.com/ctrf-io/go-ctrf-json-reporter/ctrf" +) + +func runTests(destinationReportFile string) error { + env := ctrf.Environment{ + // add your environment details here + } + report := ctrf.NewReport("my-awesome-testing-tool", &env) + + // run your tests and populate the report object here + + return report.WriteFile(destinationReportFile) +} + +``` \ No newline at end of file diff --git a/cmd/go-ctrf-json-reporter/main.go b/cmd/go-ctrf-json-reporter/main.go index 73f7b7b..d8c9d13 100644 --- a/cmd/go-ctrf-json-reporter/main.go +++ b/cmd/go-ctrf-json-reporter/main.go @@ -1,70 +1,71 @@ package main import ( - "flag" - "fmt" - "github.com/ctrf-io/go-ctrf-json-reporter/reporter" - "os" + "flag" + "fmt" + "github.com/ctrf-io/go-ctrf-json-reporter/ctrf" + "github.com/ctrf-io/go-ctrf-json-reporter/reporter" + "os" ) func main() { - var outputFile string - var verbose bool - flag.BoolVar(&verbose, "verbose", false, "Enable verbose output") - flag.BoolVar(&verbose, "v", false, "Enable verbose output (shorthand)") - flag.StringVar(&outputFile, "output", "ctrf-report.json", "The output file for the test results") - flag.StringVar(&outputFile, "o", "ctrf-report.json", "The output file for the test results (shorthand)") + var outputFile string + var verbose bool + flag.BoolVar(&verbose, "verbose", false, "Enable verbose output") + flag.BoolVar(&verbose, "v", false, "Enable verbose output (shorthand)") + flag.StringVar(&outputFile, "output", "ctrf-report.json", "The output file for the test results") + flag.StringVar(&outputFile, "o", "ctrf-report.json", "The output file for the test results (shorthand)") - var tempAppName, tempAppVersion, tempOSPlatform, tempOSRelease, tempOSVersion, tempBuildName, tempBuildNumber string + var tempAppName, tempAppVersion, tempOSPlatform, tempOSRelease, tempOSVersion, tempBuildName, tempBuildNumber string - flag.StringVar(&tempAppName, "appName", "", "The name of the application being tested.") - flag.StringVar(&tempAppVersion, "appVersion", "", "The version of the application being tested.") - flag.StringVar(&tempOSPlatform, "osPlatform", "", "The operating system platform (e.g., Windows, Linux).") - flag.StringVar(&tempOSRelease, "osRelease", "", "The release version of the operating system.") - flag.StringVar(&tempOSVersion, "osVersion", "", "The version number of the operating system.") - flag.StringVar(&tempBuildName, "buildName", "", "The name of the build (e.g., feature branch name).") - flag.StringVar(&tempBuildNumber, "buildNumber", "", "The build number or identifier.") + flag.StringVar(&tempAppName, "appName", "", "The name of the application being tested.") + flag.StringVar(&tempAppVersion, "appVersion", "", "The version of the application being tested.") + flag.StringVar(&tempOSPlatform, "osPlatform", "", "The operating system platform (e.g., Windows, Linux).") + flag.StringVar(&tempOSRelease, "osRelease", "", "The release version of the operating system.") + flag.StringVar(&tempOSVersion, "osVersion", "", "The version number of the operating system.") + flag.StringVar(&tempBuildName, "buildName", "", "The name of the build (e.g., feature branch name).") + flag.StringVar(&tempBuildNumber, "buildNumber", "", "The build number or identifier.") - flag.Parse() + flag.Parse() - var env *reporter.Environment + var env *ctrf.Environment - if tempAppName != "" || tempAppVersion != "" || tempOSPlatform != "" || - tempOSRelease != "" || tempOSVersion != "" || tempBuildName != "" || tempBuildNumber != "" { - env = &reporter.Environment{} + if tempAppName != "" || tempAppVersion != "" || tempOSPlatform != "" || + tempOSRelease != "" || tempOSVersion != "" || tempBuildName != "" || tempBuildNumber != "" { + env = &ctrf.Environment{} - if tempAppName != "" { - env.AppName = &tempAppName - } - if tempAppVersion != "" { - env.AppVersion = &tempAppVersion - } - if tempOSPlatform != "" { - env.OSPlatform = &tempOSPlatform - } - if tempOSRelease != "" { - env.OSRelease = &tempOSRelease - } - if tempOSVersion != "" { - env.OSVersion = &tempOSVersion - } - if tempBuildName != "" { - env.BuildName = &tempBuildName - } - if tempBuildNumber != "" { - env.BuildNumber = &tempBuildNumber - } - } + if tempAppName != "" { + env.AppName = tempAppName + } + if tempAppVersion != "" { + env.AppVersion = tempAppVersion + } + if tempOSPlatform != "" { + env.OSPlatform = tempOSPlatform + } + if tempOSRelease != "" { + env.OSRelease = tempOSRelease + } + if tempOSVersion != "" { + env.OSVersion = tempOSVersion + } + if tempBuildName != "" { + env.BuildName = tempBuildName + } + if tempBuildNumber != "" { + env.BuildNumber = tempBuildNumber + } + } - report, err := reporter.ParseTestResults(os.Stdin, verbose, env) - if err != nil { - fmt.Fprintf(os.Stderr, "Error parsing test results: %v\n", err) - os.Exit(1) - } + report, err := reporter.ParseTestResults(os.Stdin, verbose, env) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Error parsing test results: %v\n", err) + os.Exit(1) + } - err = reporter.WriteReportToFile(outputFile, report) - if err != nil { - fmt.Fprintf(os.Stderr, "Error writing the report to file: %v\n", err) - os.Exit(1) - } + err = reporter.WriteReportToFile(outputFile, report) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Error writing the report to file: %v\n", err) + os.Exit(1) + } } diff --git a/ctrf/ctrf.go b/ctrf/ctrf.go new file mode 100644 index 0000000..4a9177b --- /dev/null +++ b/ctrf/ctrf.go @@ -0,0 +1,218 @@ +package ctrf + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "os" + "strings" +) + +type Report struct { + Results *Results `json:"results"` +} + +func NewReport(toolName string, env *Environment) *Report { + return &Report{ + Results: &Results{ + Tool: &Tool{Name: toolName}, + Environment: env, + Summary: &Summary{}, + }, + } +} + +func (report *Report) ToJson() (string, error) { + stringBuilder := &strings.Builder{} + + err := report.Write(stringBuilder, false) + if err != nil { + return "", err + } + + return stringBuilder.String(), nil +} + +func (report *Report) ToJsonPretty() (string, error) { + stringBuilder := &strings.Builder{} + + err := report.Write(stringBuilder, true) + if err != nil { + return "", err + } + + return stringBuilder.String(), nil +} + +func (report *Report) Write(w io.Writer, pretty bool) error { + if len(report.Validate()) > 0 { + return errors.New("report is invalid") + } + encoder := json.NewEncoder(w) + if pretty { + encoder.SetIndent("", " ") + } + err := encoder.Encode(report) + if err != nil { + return fmt.Errorf("error writing ctrf json report: %v", err) + } + return nil +} + +func (report *Report) WriteFile(filePath string) error { + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("error writing ctrf json report: %v", err) + } + defer func(file *os.File) { + _ = file.Close() + }(file) + + return report.Write(file, true) +} + +func (report *Report) Validate() []error { + results := report.Results + if results == nil { + return singleError("missing property 'results'") + } + + var errs []error + if results.Tool == nil { + errs = append(errs, errors.New("missing property 'results.tool'")) + } else { + errs = append(errs, results.Tool.Validate()...) + } + if results.Summary == nil { + errs = append(errs, errors.New("missing property 'results.summary'")) + } else { + errs = append(errs, results.Summary.Validate()...) + } + if results.Tests == nil { + errs = append(errs, errors.New("missing property 'results.tests'")) + } + return errs +} + +func singleError(errorMessage string) []error { + return []error{errors.New(errorMessage)} +} + +type Results struct { + Tool *Tool `json:"tool"` + Summary *Summary `json:"summary"` + Tests []*TestResult `json:"tests"` + Environment *Environment `json:"environment,omitempty"` + Extra interface{} `json:"extra,omitempty"` +} + +type Tool struct { + Name string `json:"name"` + Version string `json:"version,omitempty"` + Extra interface{} `json:"extra,omitempty"` +} + +func (tool *Tool) Validate() []error { + if tool.Name == "" { + return singleError("missing property 'results.tool.name'") + } + return nil +} + +type Summary struct { + Tests int `json:"tests"` + Passed int `json:"passed"` + Failed int `json:"failed"` + Pending int `json:"pending"` + Skipped int `json:"skipped"` + Other int `json:"other"` + Suites int `json:"suites,omitempty"` + Start int64 `json:"start"` + Stop int64 `json:"stop"` + Extra interface{} `json:"extra,omitempty"` +} + +func (summary *Summary) Validate() []error { + var errs []error + if summary.Tests < 0 { + errs = append(errs, errors.New("invalid property 'results.summary.tests'")) + } + if summary.Passed < 0 { + errs = append(errs, errors.New("invalid property 'results.summary.passed'")) + } + if summary.Failed < 0 { + errs = append(errs, errors.New("invalid property 'results.summary.failed'")) + } + if summary.Pending < 0 { + errs = append(errs, errors.New("invalid property 'results.summary.pending'")) + } + if summary.Skipped < 0 { + errs = append(errs, errors.New("invalid property 'results.summary.skipped'")) + } + if summary.Other < 0 { + errs = append(errs, errors.New("invalid property 'results.summary.other'")) + } + if summary.Start < 0 { + errs = append(errs, errors.New("invalid property 'results.summary.start'")) + } + if summary.Stop < 0 { + errs = append(errs, errors.New("invalid property 'results.summary.stop'")) + } + if summary.Suites < 0 { + errs = append(errs, errors.New("invalid property 'results.summary.suites'")) + } + if summary.Start > summary.Stop { + errs = append(errs, errors.New("invalid summary timestamps: start can't be greater than stop")) + } + testsSum := summary.Passed + summary.Failed + summary.Pending + summary.Skipped + summary.Other + if summary.Tests != testsSum { + errs = append(errs, fmt.Errorf("invalid summary counts: tests (%d) must be the sum of passed, failed, pending, skipped, and other (%d)", summary.Tests, testsSum)) + + } + return errs +} + +type TestStatus string + +const ( + TestPassed TestStatus = "passed" + TestFailed TestStatus = "failed" + TestSkipped TestStatus = "skipped" + TestPending TestStatus = "pending" + TestOther TestStatus = "other" +) + +type TestResult struct { + Name string `json:"name"` + Status TestStatus `json:"status"` + Duration float64 `json:"duration"` + Start int64 `json:"start,omitempty"` + Stop int64 `json:"stop,omitempty"` + Suite string `json:"suite,omitempty"` + Message string `json:"message,omitempty"` + Trace string `json:"trace,omitempty"` + RawStatus string `json:"rawStatus,omitempty"` + Tags []string `json:"tags,omitempty"` + Type string `json:"type,omitempty"` + Filepath string `json:"filepath,omitempty"` + Retry int `json:"retry,omitempty"` + Flake bool `json:"flake,omitempty"` + Browser string `json:"browser,omitempty"` + Device string `json:"device,omitempty"` + Screenshot string `json:"screenshot,omitempty"` + Parameters interface{} `json:"parameters,omitempty"` + Steps []interface{} `json:"steps,omitempty"` + Extra interface{} `json:"extra,omitempty"` +} + +type Environment struct { + AppName string `json:"appName,omitempty"` + AppVersion string `json:"appVersion,omitempty"` + OSPlatform string `json:"osPlatform,omitempty"` + OSRelease string `json:"osRelease,omitempty"` + OSVersion string `json:"osVersion,omitempty"` + BuildName string `json:"buildName,omitempty"` + BuildNumber string `json:"buildNumber,omitempty"` + Extra interface{} `json:"extra,omitempty"` +} diff --git a/ctrf/ctrf_test.go b/ctrf/ctrf_test.go new file mode 100644 index 0000000..bb4d538 --- /dev/null +++ b/ctrf/ctrf_test.go @@ -0,0 +1,206 @@ +package ctrf + +import ( + "encoding/json" + "gopkg.in/yaml.v3" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRequiredProperties(t *testing.T) { + // Arrange + report := Report{ + Results: &Results{ + Tool: &Tool{ + Name: "my tool", + }, + Summary: &Summary{ + Tests: 15, + Passed: 5, + Failed: 4, + Pending: 3, + Skipped: 2, + Other: 1, + Start: 42, + Stop: 1337, + }, + Tests: []*TestResult{ + { + Name: "test 1", + Status: TestPassed, + Duration: 10, + }, + { + Name: "test 2", + Status: TestFailed, + Duration: 11, + }, + { + Name: "test 3", + Status: TestPending, + Duration: 12, + }, + { + Name: "test 4", + Status: TestSkipped, + Duration: 13, + }, + { + Name: "test 5", + Status: TestOther, + Duration: 14, + }, + }, + }, + } + + expectedJson := `{ + "results": { + "tool": { + "name": "my tool" + }, + "summary": { + "tests": 15, + "passed": 5, + "failed": 4, + "pending": 3, + "skipped": 2, + "other": 1, + "start": 42, + "stop": 1337 + }, + "tests": [ + { + "name": "test 1", + "status": "passed", + "duration": 10 + }, + { + "name": "test 2", + "status": "failed", + "duration": 11 + }, + { + "name": "test 3", + "status": "pending", + "duration": 12 + }, + { + "name": "test 4", + "status": "skipped", + "duration": 13 + }, + { + "name": "test 5", + "status": "other", + "duration": 14 + } + ] + } +} +` + + // Act + actualJson, err := report.ToJsonPretty() + if err != nil { + t.Fatal(err) + } + + // Assert + assert.Equal(t, expectedJson, actualJson) +} + +func TestValidation(t *testing.T) { + forEachValidationTestCase(t, allTestCase, func(t *testing.T, testCase validationTestCase, report *Report) { + errs := report.Validate() + if len(testCase.ExpectedErrors) == 0 { + assert.Nil(t, errs) + } else { + assert.NotNil(t, errs) + for _, expectedError := range testCase.ExpectedErrors { + if errorNotPresent(errs, expectedError) { + t.Error("Expected error not found:", expectedError) + } + } + } + }) +} + +func TestWriteFailsForInvalidReport(t *testing.T) { + forEachValidationTestCase(t, failingTestCase, func(t *testing.T, testCase validationTestCase, report *Report) { + err := report.Write(os.Stdout, true) + assert.Error(t, err, "report is invalid") + }) +} + +func TestWriteFileFailsForInvalidReport(t *testing.T) { + forEachValidationTestCase(t, failingTestCase, func(t *testing.T, testCase validationTestCase, report *Report) { + err := report.WriteFile("report.json") + assert.Error(t, err, "report is invalid") + }) +} + +func TestToJsonFailsForInvalidReport(t *testing.T) { + forEachValidationTestCase(t, failingTestCase, func(t *testing.T, testCase validationTestCase, report *Report) { + _, err := report.ToJson() + assert.Error(t, err, "report is invalid") + }) +} + +func TestToJsonPrettyFailsForInvalidReport(t *testing.T) { + forEachValidationTestCase(t, failingTestCase, func(t *testing.T, testCase validationTestCase, report *Report) { + _, err := report.ToJsonPretty() + assert.Error(t, err, "report is invalid") + }) +} + +func forEachValidationTestCase(t *testing.T, testCaseFilter func(validationTestCase) bool, test func(*testing.T, validationTestCase, *Report)) { + data, err := os.ReadFile("validation-test-cases.yaml") + if err != nil { + t.Fatal(err) + } + var testCases []validationTestCase + err = yaml.Unmarshal(data, &testCases) + if err != nil { + t.Fatal(err) + } + + for _, testCase := range testCases { + if !testCaseFilter(testCase) { + continue + } + t.Run(testCase.Name, func(t *testing.T) { + report := &Report{} + err = json.Unmarshal([]byte(testCase.Report), report) + if err != nil { + t.Fatal(err) + } + test(t, testCase, report) + }) + } +} + +func allTestCase(validationTestCase) bool { + return true +} + +func failingTestCase(testCase validationTestCase) bool { + return len(testCase.ExpectedErrors) > 0 +} + +func errorNotPresent(errs []error, theErrorYouAreLookingFor string) bool { + for _, err := range errs { + if err.Error() == theErrorYouAreLookingFor { + return false + } + } + return true +} + +type validationTestCase struct { + Name string `json:"name" yaml:"name"` + Report string `json:"report" yaml:"report"` + ExpectedErrors []string `json:"expected_errors" yaml:"expected_errors"` +} diff --git a/ctrf/report.json b/ctrf/report.json new file mode 100644 index 0000000..e69de29 diff --git a/ctrf/validation-test-cases.yaml b/ctrf/validation-test-cases.yaml new file mode 100644 index 0000000..c594c0e --- /dev/null +++ b/ctrf/validation-test-cases.yaml @@ -0,0 +1,109 @@ + +- name: Minimal valid report + report: | + { + "results": { + "tool": { + "name": "tool-name" + }, + "summary": { + "tests": 15, + "passed": 5, + "failed": 4, + "pending": 3, + "skipped": 2, + "other": 1 + }, + "tests": [ + { + "name": "test-name", + "status": "pass", + "duration": 1 + } + ] + } + } + +- name: Missing results property + expected_errors: + - "missing property 'results'" + report: | + {} + +- name: Incomplete results property + expected_errors: + - "missing property 'results.tool'" + - "missing property 'results.summary'" + - "missing property 'results.tests'" + report: | + { + "results": {} + } + +- name: Missing tool name property + expected_errors: + - "missing property 'results.tool.name'" + report: | + { + "results": { + "tool": {} + } + } + +- name: Invalid summary properties + expected_errors: + - "invalid property 'results.summary.tests'" + - "invalid property 'results.summary.passed'" + - "invalid property 'results.summary.failed'" + - "invalid property 'results.summary.pending'" + - "invalid property 'results.summary.skipped'" + - "invalid property 'results.summary.other'" + - "invalid property 'results.summary.start'" + - "invalid property 'results.summary.stop'" + - "invalid property 'results.summary.suites'" + report: | + { + "results": { + "summary": { + "tests": -1, + "passed": -1, + "failed": -1, + "pending": -1, + "skipped": -1, + "other": -1, + "start": -1, + "stop": -1, + "suites": -1 + } + } + } + +- name: Invalid summary timestamps + expected_errors: + - "invalid summary timestamps: start can't be greater than stop" + report: | + { + "results": { + "summary": { + "start": 5, + "stop": 4 + } + } + } + +- name: Invalid summary counts + expected_errors: + - "invalid summary counts: tests (5) must be the sum of passed, failed, pending, skipped, and other (15)" + report: | + { + "results": { + "summary": { + "tests": 5, + "passed": 5, + "failed": 4, + "pending": 3, + "skipped": 2, + "other": 1 + } + } + } diff --git a/go.mod b/go.mod index 749e489..eeaafcc 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module github.com/ctrf-io/go-ctrf-json-reporter go 1.19 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e20fa14 --- /dev/null +++ b/go.sum @@ -0,0 +1,9 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/reporter/reporter.go b/reporter/reporter.go index f13fc04..76adf41 100644 --- a/reporter/reporter.go +++ b/reporter/reporter.go @@ -5,6 +5,8 @@ import ( "fmt" "io" "os" + + "github.com/ctrf-io/go-ctrf-json-reporter/ctrf" ) type TestEvent struct { @@ -16,54 +18,7 @@ type TestEvent struct { Output string } -type Summary struct { - Tests int `json:"tests"` - Passed int `json:"passed"` - Failed int `json:"failed"` - Pending int `json:"pending"` - Skipped int `json:"skipped"` - Other int `json:"other"` - Start int `json:"start"` - Stop int `json:"stop"` -} - -type TestResult struct { - Name string `json:"name"` - Status string `json:"status"` - Duration float64 `json:"duration"` -} - -type Environment struct { - AppName *string `json:"appName,omitempty"` - AppVersion *string `json:"appVersion,omitempty"` - OSPlatform *string `json:"osPlatform,omitempty"` - OSRelease *string `json:"osRelease,omitempty"` - OSVersion *string `json:"osVersion,omitempty"` - BuildName *string `json:"buildName,omitempty"` - BuildNumber *string `json:"buildNumber,omitempty"` -} - -func (e Environment) MarshalJSON() ([]byte, error) { - type Alias Environment - return json.Marshal(&struct { - *Alias - }{ - Alias: (*Alias)(&e), - }) -} - -type FinalReport struct { - Results struct { - Tool struct { - Name string `json:"name"` - } `json:"tool"` - Summary Summary `json:"summary"` - Tests []TestResult `json:"tests"` - Environment *Environment `json:"environment,omitempty"` - } `json:"results"` -} - -func ParseTestResults(r io.Reader, verbose bool, env *Environment) (*FinalReport, error) { +func ParseTestResults(r io.Reader, verbose bool, env *ctrf.Environment) (*ctrf.Report, error) { var testEvents []TestEvent decoder := json.NewDecoder(r) @@ -77,18 +32,13 @@ func ParseTestResults(r io.Reader, verbose bool, env *Environment) (*FinalReport testEvents = append(testEvents, event) } - report := &FinalReport{} - report.Results.Tool.Name = "gotest" - report.Results.Summary = Summary{} - report.Results.Tests = make([]TestResult, 0) - - report.Results.Environment = env + report := ctrf.NewReport("gotest", env) for _, event := range testEvents { if verbose { jsonEvent, err := json.Marshal(event) if err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "error: %v\n", err) } fmt.Println(string(jsonEvent)) } @@ -96,25 +46,25 @@ func ParseTestResults(r io.Reader, verbose bool, env *Environment) (*FinalReport if event.Action == "pass" { report.Results.Summary.Tests++ report.Results.Summary.Passed++ - report.Results.Tests = append(report.Results.Tests, TestResult{ + report.Results.Tests = append(report.Results.Tests, &ctrf.TestResult{ Name: event.Test, - Status: "passed", + Status: ctrf.TestPassed, Duration: event.Elapsed, }) } else if event.Action == "fail" { report.Results.Summary.Tests++ report.Results.Summary.Failed++ - report.Results.Tests = append(report.Results.Tests, TestResult{ + report.Results.Tests = append(report.Results.Tests, &ctrf.TestResult{ Name: event.Test, - Status: "failed", + Status: ctrf.TestFailed, Duration: event.Elapsed, }) } else if event.Action == "skip" { report.Results.Summary.Tests++ report.Results.Summary.Skipped++ - report.Results.Tests = append(report.Results.Tests, TestResult{ + report.Results.Tests = append(report.Results.Tests, &ctrf.TestResult{ Name: event.Test, - Status: "skipped", + Status: ctrf.TestSkipped, Duration: event.Elapsed, }) } @@ -124,18 +74,10 @@ func ParseTestResults(r io.Reader, verbose bool, env *Environment) (*FinalReport return report, nil } -func WriteReportToFile(filename string, report *FinalReport) error { - file, err := os.Create(filename) - if err != nil { - return fmt.Errorf("error writing ctrf json report: %v", err) - } - defer file.Close() - - encoder := json.NewEncoder(file) - encoder.SetIndent("", " ") - err = encoder.Encode(report) +func WriteReportToFile(filename string, report *ctrf.Report) error { + err := report.WriteFile(filename) if err != nil { - return fmt.Errorf("error writing ctrf json report: %v", err) + return err } fmt.Println("go-ctrf-json-reporter: successfully written ctrf json to", filename) From 2665e933d80841617e2a4840ff124f7aa63b9ff5 Mon Sep 17 00:00:00 2001 From: Yann D'Isanto Date: Fri, 17 May 2024 17:48:15 +0200 Subject: [PATCH 2/3] fix: reporter elapsed parsing gotest elapsed uses seconds but ctrf duration uses milliseconds --- ctrf/ctrf.go | 2 +- reporter/reporter.go | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ctrf/ctrf.go b/ctrf/ctrf.go index 4a9177b..39424f7 100644 --- a/ctrf/ctrf.go +++ b/ctrf/ctrf.go @@ -186,7 +186,7 @@ const ( type TestResult struct { Name string `json:"name"` Status TestStatus `json:"status"` - Duration float64 `json:"duration"` + Duration int64 `json:"duration"` Start int64 `json:"start,omitempty"` Stop int64 `json:"stop,omitempty"` Suite string `json:"suite,omitempty"` diff --git a/reporter/reporter.go b/reporter/reporter.go index 76adf41..fcb580d 100644 --- a/reporter/reporter.go +++ b/reporter/reporter.go @@ -49,7 +49,7 @@ func ParseTestResults(r io.Reader, verbose bool, env *ctrf.Environment) (*ctrf.R report.Results.Tests = append(report.Results.Tests, &ctrf.TestResult{ Name: event.Test, Status: ctrf.TestPassed, - Duration: event.Elapsed, + Duration: secondsToMillis(event.Elapsed), }) } else if event.Action == "fail" { report.Results.Summary.Tests++ @@ -57,7 +57,7 @@ func ParseTestResults(r io.Reader, verbose bool, env *ctrf.Environment) (*ctrf.R report.Results.Tests = append(report.Results.Tests, &ctrf.TestResult{ Name: event.Test, Status: ctrf.TestFailed, - Duration: event.Elapsed, + Duration: secondsToMillis(event.Elapsed), }) } else if event.Action == "skip" { report.Results.Summary.Tests++ @@ -65,7 +65,7 @@ func ParseTestResults(r io.Reader, verbose bool, env *ctrf.Environment) (*ctrf.R report.Results.Tests = append(report.Results.Tests, &ctrf.TestResult{ Name: event.Test, Status: ctrf.TestSkipped, - Duration: event.Elapsed, + Duration: secondsToMillis(event.Elapsed), }) } } @@ -83,3 +83,7 @@ func WriteReportToFile(filename string, report *ctrf.Report) error { fmt.Println("go-ctrf-json-reporter: successfully written ctrf json to", filename) return nil } + +func secondsToMillis(seconds float64) int64 { + return int64(seconds * 1000) +} From f03a890d0e6e3a3123b360539a943463d96f8f6f Mon Sep 17 00:00:00 2001 From: Yann D'Isanto Date: Fri, 17 May 2024 17:02:22 +0200 Subject: [PATCH 3/3] ci: add build workflow --- .github/workflows/build.yaml | 51 ++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/build.yaml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..927559b --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,51 @@ +name: Build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + + +jobs: + test: + strategy: + matrix: + go-version: [ 1.19.x, 1.20.x, 1.21.x ] + lint-and-coverage: [ false ] + include: + - go-version: 1.22.x + lint-and-coverage: true + + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go ${{ matrix.go-version }} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + + - name: Install dependencies + run: go get ./... + + - name: Build + run: go build -v ./... + + - name: Test with the Go CLI + run: | + go version + if [ ${{ matrix.lint-and-coverage }} = true ]; then + GO_TEST_OPTS="-covermode=atomic -coverprofile=coverage.out" + fi + export GORACE="halt_on_error=1" + go test -race $GO_TEST_OPTS ./... + + - name: Reporting coverage + if: matrix.lint-and-coverage + uses: shogo82148/actions-goveralls@v1 + with: + path-to-profile: coverage.out