Skip to content

Commit

Permalink
if you retry flaky tests and the last attempt succeeded, the step sho…
Browse files Browse the repository at this point in the history
…uld succeed (#95)

* if you retry flaky tests and the last attempt succeeded, the step should succeed

* distinguish based on dimension

* fix npe?

* remove log line

* debug logging on dimension success

* more logging

* fix last step calc

* remove logging

* restructure for simplicity to understand

* missing completion time debugging

* more debugging

* stop caring about completion time

* move to package

* use testify

* rename package to not conflict with existing bitrise mores

* rename package

* well i guess i learned a little bout go

* bitrise syntax

* bitrise syntax
  • Loading branch information
benbitrise authored Jun 4, 2024
1 parent f6fabdd commit 4fdef16
Show file tree
Hide file tree
Showing 59 changed files with 23,951 additions and 627 deletions.
4 changes: 3 additions & 1 deletion bitrise.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ workflows:
# Ignoring maintenance test as it needs gcloud binary
- go-test:
inputs:
- packages: "."
- packages: |-
.
./resultprocessing
- path::./:
inputs:
- test_type: instrumentation
Expand Down
9 changes: 8 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ go 1.17
require (
github.com/bitrise-io/go-steputils v1.0.2
github.com/bitrise-io/go-utils v1.0.2
github.com/pkg/errors v0.8.1
github.com/pkg/errors v0.9.1
google.golang.org/api v0.114.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

require (
cloud.google.com/go/compute v1.19.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
Expand All @@ -17,6 +23,7 @@ require (
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.7.1 // indirect
github.com/stretchr/testify v1.9.0
go.opencensus.io v0.24.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/oauth2 v0.7.0 // indirect
Expand Down
556 changes: 8 additions & 548 deletions go.sum

Large diffs are not rendered by default.

111 changes: 60 additions & 51 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/bitrise-io/go-utils/log"
"github.com/bitrise-io/go-utils/pathutil"
"github.com/bitrise-io/go-utils/sliceutil"
"github.com/bitrise-steplib/steps-virtual-device-testing-for-android/resultprocessing"
toolresults "google.golang.org/api/toolresults/v1beta3"
)

Expand Down Expand Up @@ -136,68 +137,24 @@ func main() {
failf("Failed to write in tabwriter, error: %s", err)
}

successful, err = resultprocessing.GetSuccessOfExecution(responseModel.Steps)
if err != nil {
failf("Failed to process results, error: %s", err)
}

for _, step := range responseModel.Steps {
dimensions := map[string]string{}
for _, dimension := range step.DimensionValue {
dimensions[dimension.Key] = dimension.Value
}

outcome := step.Outcome.Summary

switch outcome {
case "success":
outcome = colorstring.Green(outcome)
case "failure":
successful = false
if step.Outcome.FailureDetail != nil {
if step.Outcome.FailureDetail.Crashed {
outcome += "(Crashed)"
}
if step.Outcome.FailureDetail.NotInstalled {
outcome += "(NotInstalled)"
}
if step.Outcome.FailureDetail.OtherNativeCrash {
outcome += "(OtherNativeCrash)"
}
if step.Outcome.FailureDetail.TimedOut {
outcome += "(TimedOut)"
}
if step.Outcome.FailureDetail.UnableToCrawl {
outcome += "(UnableToCrawl)"
}
}
outcome = colorstring.Red(outcome)
case "inconclusive":
successful = false
if step.Outcome.InconclusiveDetail != nil {
if step.Outcome.InconclusiveDetail.AbortedByUser {
outcome += "(AbortedByUser)"
}
if step.Outcome.InconclusiveDetail.InfrastructureFailure {
outcome += "(InfrastructureFailure)"
}
}
outcome = colorstring.Yellow(outcome)
case "skipped":
successful = false
if step.Outcome.SkippedDetail != nil {
if step.Outcome.SkippedDetail.IncompatibleAppVersion {
outcome += "(IncompatibleAppVersion)"
}
if step.Outcome.SkippedDetail.IncompatibleArchitecture {
outcome += "(IncompatibleArchitecture)"
}
if step.Outcome.SkippedDetail.IncompatibleDevice {
outcome += "(IncompatibleDevice)"
}
}
outcome = colorstring.Blue(outcome)
}
outcome := processStepResult(step)

if _, err := fmt.Fprintln(w, fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t", dimensions["Model"], dimensions["Version"], dimensions["Locale"], dimensions["Orientation"], outcome)); err != nil {
failf("Failed to write in tabwriter, error: %s", err)
}
}

if err := w.Flush(); err != nil {
log.Errorf("Failed to flush writer, error: %s", err)
}
Expand Down Expand Up @@ -351,3 +308,55 @@ func uploadFile(uploadURL string, archiveFilePath string) error {

return nil
}

func processStepResult(step *toolresults.Step) string {
outcome := step.Outcome.Summary

switch outcome {
case "success":
outcome = colorstring.Green(outcome)
case "failure":
if step.Outcome.FailureDetail != nil {
if step.Outcome.FailureDetail.Crashed {
outcome += "(Crashed)"
}
if step.Outcome.FailureDetail.NotInstalled {
outcome += "(NotInstalled)"
}
if step.Outcome.FailureDetail.OtherNativeCrash {
outcome += "(OtherNativeCrash)"
}
if step.Outcome.FailureDetail.TimedOut {
outcome += "(TimedOut)"
}
if step.Outcome.FailureDetail.UnableToCrawl {
outcome += "(UnableToCrawl)"
}
}
outcome = colorstring.Red(outcome)
case "inconclusive":
if step.Outcome.InconclusiveDetail != nil {
if step.Outcome.InconclusiveDetail.AbortedByUser {
outcome += "(AbortedByUser)"
}
if step.Outcome.InconclusiveDetail.InfrastructureFailure {
outcome += "(InfrastructureFailure)"
}
}
outcome = colorstring.Yellow(outcome)
case "skipped":
if step.Outcome.SkippedDetail != nil {
if step.Outcome.SkippedDetail.IncompatibleAppVersion {
outcome += "(IncompatibleAppVersion)"
}
if step.Outcome.SkippedDetail.IncompatibleArchitecture {
outcome += "(IncompatibleArchitecture)"
}
if step.Outcome.SkippedDetail.IncompatibleDevice {
outcome += "(IncompatibleDevice)"
}
}
outcome = colorstring.Blue(outcome)
}
return outcome
}
44 changes: 44 additions & 0 deletions resultprocessing/ftl_result_processor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package resultprocessing

import (
"encoding/json"

toolresults "google.golang.org/api/toolresults/v1beta3"
)

// GetSuccessOfExecution ...
// Given multiple steps, return whether we can consider the overall execution successful
// To do this, step executions are grouped by dimension (aka what device they ran on)
// and each dimension must have a successful execution
func GetSuccessOfExecution(steps []*toolresults.Step) (bool, error) {
outcomeByDimension, err := getOutcomeByDimension(steps)
if err != nil {
return false, err
}

for _, outcome := range outcomeByDimension {
if outcome.Summary != "success" {
return false, nil
}
}

return true, nil
}

func getOutcomeByDimension(steps []*toolresults.Step) (map[string]*toolresults.Outcome, error) {
groupedByDimension := make(map[string]*toolresults.Outcome)
for _, step := range steps {
key, err := json.Marshal(step.DimensionValue)
if err != nil {
return nil, err
}
if key != nil {
dimensionStr := string(key)
if groupedByDimension[dimensionStr] == nil || groupedByDimension[dimensionStr].Summary != "success" {
groupedByDimension[dimensionStr] = step.Outcome
}
}
}

return groupedByDimension, nil
}
103 changes: 103 additions & 0 deletions resultprocessing/ftl_result_processor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package resultprocessing

import (
"testing"

"github.com/stretchr/testify/require"
toolresults "google.golang.org/api/toolresults/v1beta3"
)

func TestGetSuccessOfExecution_AllSucceed(t *testing.T) {
steps := []*toolresults.Step{
{
DimensionValue: []*toolresults.StepDimensionValueEntry{{Key: "os", Value: "android"}},
Outcome: &toolresults.Outcome{Summary: "success"},
},
{
DimensionValue: []*toolresults.StepDimensionValueEntry{{Key: "os", Value: "ios"}},
Outcome: &toolresults.Outcome{Summary: "success"},
},
}

isSuccess, err := GetSuccessOfExecution(steps)
require.NoError(t, err)
require.True(t, isSuccess)
}

func TestGetSuccessOfExecution_FirstFailThenSucceed(t *testing.T) {
steps := []*toolresults.Step{
{
DimensionValue: []*toolresults.StepDimensionValueEntry{{Key: "os", Value: "android"}},
Outcome: &toolresults.Outcome{Summary: "failure"},
},
{
DimensionValue: []*toolresults.StepDimensionValueEntry{{Key: "os", Value: "android"}},
Outcome: &toolresults.Outcome{Summary: "success"},
},
{
DimensionValue: []*toolresults.StepDimensionValueEntry{{Key: "os", Value: "ios"}},
Outcome: &toolresults.Outcome{Summary: "success"},
},
}

isSuccess, err := GetSuccessOfExecution(steps)
require.NoError(t, err)
require.True(t, isSuccess)
}

func TestGetSuccessOfExecution_FailDifferentDimension(t *testing.T) {
steps := []*toolresults.Step{
{
DimensionValue: []*toolresults.StepDimensionValueEntry{{Key: "os", Value: "android"}},
Outcome: &toolresults.Outcome{Summary: "failure"},
},
{
DimensionValue: []*toolresults.StepDimensionValueEntry{{Key: "os", Value: "android"}},
Outcome: &toolresults.Outcome{Summary: "success"},
},
{
DimensionValue: []*toolresults.StepDimensionValueEntry{{Key: "os", Value: "ios"}},
Outcome: &toolresults.Outcome{Summary: "failure"},
},
}

isSuccess, err := GetSuccessOfExecution(steps)
require.NoError(t, err)
require.False(t, isSuccess)
}

func TestGetSuccessOfExecution_FailForDimension(t *testing.T) {
steps := []*toolresults.Step{
{
DimensionValue: []*toolresults.StepDimensionValueEntry{{Key: "os", Value: "android"}},
CompletionTime: &toolresults.Timestamp{Seconds: 1},
Outcome: &toolresults.Outcome{Summary: "failure"},
},
{
DimensionValue: []*toolresults.StepDimensionValueEntry{{Key: "os", Value: "ios"}},
CompletionTime: &toolresults.Timestamp{Seconds: 2},
Outcome: &toolresults.Outcome{Summary: "success"},
},
}

isSuccess, err := GetSuccessOfExecution(steps)
require.NoError(t, err)
require.False(t, isSuccess)
}

func TestGetSuccessOfExecution_FailBothDimensions(t *testing.T) {
steps := []*toolresults.Step{
{
DimensionValue: []*toolresults.StepDimensionValueEntry{{Key: "os", Value: "android"}},
Outcome: &toolresults.Outcome{Summary: "failure"},
},
{
DimensionValue: []*toolresults.StepDimensionValueEntry{{Key: "os", Value: "ios"}},
Outcome: &toolresults.Outcome{Summary: "failure"},
},
}

isSuccess, err := GetSuccessOfExecution(steps)
require.NoError(t, err)
require.False(t, isSuccess)
}
15 changes: 15 additions & 0 deletions vendor/github.com/davecgh/go-spew/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 4fdef16

Please sign in to comment.