Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add JSON reporter for Sauce Orchestrate #849

Merged
merged 15 commits into from
Nov 3, 2023
Merged
31 changes: 30 additions & 1 deletion api/saucectl.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@
"filename": {
"description": "Filename for the generated JSON report.",
"type": "string",
"default": "saucectl-test-result.xml"
"default": "saucectl-report.json"
}
}
},
Expand Down Expand Up @@ -2845,6 +2845,32 @@
"required": [
"name"
]
},
"reporters": {
"description": "Supported saucectl reporters.",
"type": "object",
"properties": {
"json": {
"type": "object",
"description": "The JSON reporter creates single report of all executed saucectl suites.",
"properties": {
"enabled": {
"description": "Toggles the reporter on/off.",
"type": "boolean"
},
"webhookURL": {
"description": "Webhook URL to pass JSON report.",
"type": "string"
},
"filename": {
"description": "Filename for the generated JSON report.",
"type": "string",
"default": "saucectl-report.json"
}
}
},
"additionalProperties": false
}
}
},
"properties": {
Expand All @@ -2868,6 +2894,9 @@
"items": {
"$ref": "#/allOf/9/then/definitions/suite"
}
},
"reporters": {
"$ref": "#/allOf/9/then/definitions/reporters"
}
},
"required": [
Expand Down
29 changes: 29 additions & 0 deletions api/v1alpha/framework/imagerunner.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,32 @@
"required": [
"name"
]
},
"reporters": {
"description": "Supported saucectl reporters.",
"type": "object",
"properties": {
"json": {
"type": "object",
"description": "The JSON reporter creates single report of all executed saucectl suites.",
"properties": {
"enabled": {
"description": "Toggles the reporter on/off.",
"type": "boolean"
},
"webhookURL": {
"description": "Webhook URL to pass JSON report.",
"type": "string"
},
"filename": {
"description": "Filename for the generated JSON report.",
"type": "string",
"default": "saucectl-report.json"
}
}
},
"additionalProperties": false
}
}
},
"properties": {
Expand All @@ -220,6 +246,9 @@
"items": {
"$ref": "#/definitions/suite"
}
},
"reporters": {
"$ref": "#/definitions/reporters"
}
},
"required": [
Expand Down
2 changes: 1 addition & 1 deletion api/v1alpha/subschema/reporters.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"filename": {
"description": "Filename for the generated JSON report.",
"type": "string",
"default": "saucectl-test-result.xml"
"default": "saucectl-report.json"
}
}
},
Expand Down
22 changes: 18 additions & 4 deletions internal/cmd/run/imagerunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/saucelabs/saucectl/internal/imagerunner"
"github.com/saucelabs/saucectl/internal/region"
"github.com/saucelabs/saucectl/internal/report"
"github.com/saucelabs/saucectl/internal/report/json"
"github.com/saucelabs/saucectl/internal/report/table"
"github.com/saucelabs/saucectl/internal/saucecloud"
"github.com/saucelabs/saucectl/internal/segment"
Expand Down Expand Up @@ -50,14 +51,27 @@ func runImageRunner(cmd *cobra.Command) (int, error) {
_ = tracker.Close()
}()

reporters := []report.Reporter{
&table.Reporter{
Dst: os.Stdout,
},
}
if !gFlags.async {
if p.Reporters.JSON.Enabled {
reporters = append(reporters, &json.Reporter{
WebhookURL: p.Reporters.JSON.WebhookURL,
Filename: p.Reporters.JSON.Filename,
})
}
}

cleanupArtifacts(p.Artifacts)
r := saucecloud.ImgRunner{
Project: p,
RunnerService: &imageRunnerClient,
TunnelService: &restoClient,
Reporters: []report.Reporter{&table.Reporter{
Dst: os.Stdout,
}},
Async: gFlags.async,
Reporters: reporters,
Async: gFlags.async,
}
return r.RunProject()
}
Expand Down
3 changes: 2 additions & 1 deletion internal/http/resto.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,10 @@ func (c *Resto) DownloadArtifact(jobID, suiteName string, realDevice bool) []str
for _, f := range files {
for _, pattern := range c.ArtifactConfig.Match {
if glob.Glob(pattern, f) {
artifacts = append(artifacts, filepath.Join(targetDir, f))
if err := c.downloadArtifact(targetDir, jobID, f); err != nil {
log.Error().Err(err).Msgf("Failed to download file: %s", f)
} else {
artifacts = append(artifacts, filepath.Join(targetDir, f))
}
break
}
Expand Down
1 change: 1 addition & 0 deletions internal/imagerunner/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Project struct {
DryRun bool `yaml:"-" json:"-"`
Env map[string]string `yaml:"env,omitempty" json:"env"`
EnvFlag map[string]string `yaml:"-" json:"-"`
Reporters config.Reporters `yaml:"reporters,omitempty" json:"-"`
}

type Defaults struct {
Expand Down
26 changes: 5 additions & 21 deletions internal/report/json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,51 +25,35 @@ func (r *Reporter) Add(t report.TestResult) {

// Render sends the result to specified webhook WebhookURL and log the result to the specified json file
func (r *Reporter) Render() {
r.cleanup()
body, err := json.Marshal(r.Results)
if err != nil {
log.Error().Msgf("failed to generate test result (%v)", err)
log.Err(err).Msg("failed to generate test result.")
return
}

if r.WebhookURL != "" {
resp, err := http.Post(r.WebhookURL, "application/json", bytes.NewBuffer(body))
if err != nil {
log.Error().Err(err).Str("webhook", r.WebhookURL).Msg("failed to send test result to webhook.")
log.Err(err).Str("webhook", r.WebhookURL).Msg("failed to send test result to webhook.")
} else {
webhookBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= http.StatusBadRequest {
log.Error().Str("webhook", r.WebhookURL).Msgf("failed to send test result to webhook, status: '%d', msg:'%v'", resp.StatusCode, string(webhookBody))
log.Error().Str("webhook", r.WebhookURL).Msgf("failed to send test result to webhook, status: %d, msg: %q.", resp.StatusCode, string(webhookBody))
}
if resp.StatusCode%100 == 2 {
log.Info().Str("webhook", r.WebhookURL).Msgf("test result has been sent successfully to webhook, msg: '%v'.", string(webhookBody))
log.Info().Str("webhook", r.WebhookURL).Msgf("test result has been sent successfully to webhook, msg: %q.", string(webhookBody))
}
}
}

if r.Filename != "" {
err = os.WriteFile(r.Filename, body, 0666)
if err != nil {
log.Error().Err(err).Msgf("failed to write test result to %s", r.Filename)
log.Err(err).Msgf("failed to write test result to %q.", r.Filename)
}
}
}

// cleanup removes any information that isn't relevant in the rendered report. Particularly when it comes to
// artifacts, this reporter is only interested in those that have been persisted to the file system.
func (r *Reporter) cleanup() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this behavior not needed anymore?

Copy link
Contributor Author

@tianfeng92 tianfeng92 Nov 3, 2023

Choose a reason for hiding this comment

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

It's not needed anymore do to the changes in https://github.com/saucelabs/saucectl/pull/849/files#diff-ea42af98511d2bfb326add8027a845ef0e7cfe15c81221258fdf22fd7367df6fR367 I don't think there is empty FilePath anymore.

RDC doesn't have this issue because it appends artifacts after the error handling, which means there is no empty FilePath. https://github.com/saucelabs/saucectl/blob/main/internal/http/rdcservice.go#L396

for i, result := range r.Results {
var artifacts []report.Artifact
for _, a := range result.Artifacts {
if a.FilePath == "" {
continue
}
artifacts = append(artifacts, a)
}
r.Results[i].Artifacts = artifacts
}
}

// Reset resets the reporter to its initial state. This action will delete all test results.
func (r *Reporter) Reset() {
r.Results = make([]report.TestResult, 0)
Expand Down
5 changes: 3 additions & 2 deletions internal/report/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ type TestResult struct {
Browser string `json:"browser,omitempty"`
Platform string `json:"platform"`
DeviceName string `json:"deviceName,omitempty"`
URL string `json:"url"`
URL string `json:"url,omitempty"`
Artifacts []Artifact `json:"artifacts,omitempty"`
Origin string `json:"origin"`
Origin string `json:"origin,omitempty"`
BuildURL string `json:"buildURL,omitempty"`
RunID string `json:"runID,omitempty"`
RDC bool `json:"-"`
TimedOut bool `json:"-"`
PassThreshold bool `json:"-"`
Expand Down
31 changes: 22 additions & 9 deletions internal/saucecloud/imagerunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io"
"os"
"os/signal"
"path/filepath"
"reflect"
"time"

Expand Down Expand Up @@ -318,7 +319,11 @@ func (r *ImgRunner) collectResults(results chan execResult, expected int) bool {

r.PrintResult(res)
r.PrintLogs(res.runID, res.name)
r.DownloadArtifacts(res.runID, res.name, res.status, res.err != nil)
files := r.DownloadArtifacts(res.runID, res.name, res.status, res.err != nil)
var artifacts []report.Artifact
for _, f := range files {
artifacts = append(artifacts, report.Artifact{FilePath: f})
}

for _, r := range r.Reporters {
r.Add(report.TestResult{
Expand All @@ -327,6 +332,9 @@ func (r *ImgRunner) collectResults(results chan execResult, expected int) bool {
StartTime: res.startTime,
EndTime: res.endTime,
Status: res.status,
Artifacts: artifacts,
Platform: "Linux",
RunID: res.runID,
Attempts: []report.Attempt{{
ID: res.runID,
Duration: res.duration,
Expand Down Expand Up @@ -391,51 +399,56 @@ func (r *ImgRunner) PollRun(ctx context.Context, id string, lastStatus string) (
}
}

func (r *ImgRunner) DownloadArtifacts(runnerID, suiteName, status string, passed bool) {
// DownloadArtifact downloads a zipped archive of artifacts and extracts the required files.
func (r *ImgRunner) DownloadArtifacts(runnerID, suiteName, status string, passed bool) []string {
if r.Async ||
runnerID == "" ||
status == imagerunner.StateCancelled ||
!r.Project.Artifacts.Download.When.IsNow(passed) {
return
return nil
}

dir, err := config.GetSuiteArtifactFolder(suiteName, r.Project.Artifacts.Download)
if err != nil {
log.Err(err).Msg("Unable to create artifacts folder.")
return
return nil
}

log.Info().Msg("Downloading artifacts archive")
reader, err := r.RunnerService.DownloadArtifacts(r.ctx, runnerID)
if err != nil {
log.Err(err).Str("suite", suiteName).Msg("Failed to fetch artifacts.")
return
return nil
}
defer reader.Close()

fileName, err := fileio.CreateTemp(reader)
if err != nil {
log.Err(err).Str("suite", suiteName).Msg("Failed to download artifacts content.")
return
return nil
}
defer os.Remove(fileName)

zf, err := zip.OpenReader(fileName)
if err != nil {
log.Error().Msgf("Unable to open zip file %s: %s", fileName, err)
return
log.Err(err).Msgf("Unable to open zip file %q", fileName)
return nil
}
defer zf.Close()
var artifacts []string
for _, f := range zf.File {
for _, pattern := range r.Project.Artifacts.Download.Match {
if glob.Glob(pattern, f.Name) {
if err = szip.Extract(dir, f); err != nil {
log.Error().Msgf("Unable to extract file '%s': %s", f.Name, err)
log.Err(err).Msgf("Unable to extract file %q", f.Name)
} else {
artifacts = append(artifacts, filepath.Join(dir, f.Name))
}
break
}
}
}
return artifacts
}

func (r *ImgRunner) PrintResult(res execResult) {
Expand Down
Loading