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
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ require (
cloud.google.com/go/storage v1.50.0
github.com/bitrise-io/go-plist v0.0.0-20210301100253-4b1a112ccd10
github.com/bitrise-io/go-steputils v1.0.5
github.com/bitrise-io/go-steputils/v2 v2.0.0-alpha.43
github.com/bitrise-io/go-steputils/v2 v2.0.0-alpha.48.0.20260312091018-7447bc60506b
github.com/bitrise-io/go-utils v1.0.13
github.com/bitrise-io/go-utils/v2 v2.0.0-alpha.31
github.com/bitrise-io/go-xcode v1.3.3-0.20260309170849-9b6f618441f7
github.com/bitrise-io/go-utils/v2 v2.0.0-alpha.33
github.com/bitrise-io/go-xcode v1.3.3
github.com/globocom/go-buffer/v2 v2.0.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/go-querystring v1.1.0
Expand All @@ -23,6 +23,7 @@ require (
golang.org/x/oauth2 v0.24.0
golang.org/x/text v0.21.0
google.golang.org/api v0.214.0
howett.net/plist v1.0.0
)

require (
Expand Down Expand Up @@ -79,5 +80,4 @@ require (
google.golang.org/grpc v1.67.3 // indirect
google.golang.org/protobuf v1.35.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.0 // indirect
)
14 changes: 8 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,17 @@ github.com/bitrise-io/go-plist v0.0.0-20210301100253-4b1a112ccd10 h1:/2OyBFI7GjY
github.com/bitrise-io/go-plist v0.0.0-20210301100253-4b1a112ccd10/go.mod h1:pARutiL3kEuRLV3JvswidvfCj+9Y3qMZtji2BDqLFsA=
github.com/bitrise-io/go-steputils v1.0.5 h1:OBH7CPXeqIWFWJw6BOUMQnUb8guspwKr2RhYBhM9tfc=
github.com/bitrise-io/go-steputils v1.0.5/go.mod h1:YIUaQnIAyK4pCvQG0hYHVkSzKNT9uL2FWmkFNW4mfNI=
github.com/bitrise-io/go-steputils/v2 v2.0.0-alpha.43 h1:oahoCL46PPywHRBin54zrwDOhXlMPIXx6zdo1Hgkz1g=
github.com/bitrise-io/go-steputils/v2 v2.0.0-alpha.43/go.mod h1:SjWTgoD5wDyyIa+xPrA+U2UgL9K8Lx/xuLaK5LBCjXE=
github.com/bitrise-io/go-steputils/v2 v2.0.0-alpha.48.0.20260311110650-2026f65db4da h1:d05p0A4po0o5nytc1GaRNsjq1ktqjmNa7FeGp58DvmY=
github.com/bitrise-io/go-steputils/v2 v2.0.0-alpha.48.0.20260311110650-2026f65db4da/go.mod h1:CL1sOqz4+q4XK/OCjB8YNV27Xmz/Fo7v/QKxobmGIx4=
github.com/bitrise-io/go-steputils/v2 v2.0.0-alpha.48.0.20260312091018-7447bc60506b h1:GIRdLWgdpa+qImAJlfuHAQppI7EFieCyNceVcUwNbo4=
github.com/bitrise-io/go-steputils/v2 v2.0.0-alpha.48.0.20260312091018-7447bc60506b/go.mod h1:CL1sOqz4+q4XK/OCjB8YNV27Xmz/Fo7v/QKxobmGIx4=
github.com/bitrise-io/go-utils v1.0.1/go.mod h1:ZY1DI+fEpZuFpO9szgDeICM4QbqoWVt0RSY3tRI1heY=
github.com/bitrise-io/go-utils v1.0.13 h1:1QENhTS/JlKH9F7+/nB+TtbTcor6jGrE6cQ4CJWfp5U=
github.com/bitrise-io/go-utils v1.0.13/go.mod h1:ZY1DI+fEpZuFpO9szgDeICM4QbqoWVt0RSY3tRI1heY=
github.com/bitrise-io/go-utils/v2 v2.0.0-alpha.31 h1:Lay9mco4/T88oYY+kqZlpdWeU9aj32/qWMRwcTg812o=
github.com/bitrise-io/go-utils/v2 v2.0.0-alpha.31/go.mod h1:3XUplo0dOWc3DqT2XA2SeHToDSg7+j1y1HTHibT2H68=
github.com/bitrise-io/go-xcode v1.3.3-0.20260309170849-9b6f618441f7 h1:hTsfgezmMkCGeHV/+KPXxT8A4R3o4UGzrooJnfmNPtA=
github.com/bitrise-io/go-xcode v1.3.3-0.20260309170849-9b6f618441f7/go.mod h1:9OwsvrhZ4A2JxHVoEY7CPcABAKA+OE7FQqFfBfvbFuY=
github.com/bitrise-io/go-utils/v2 v2.0.0-alpha.33 h1:2Skyp4yg8aNKLr5GB5amM9UK9n1yzIMT88Rb/ZBz8m4=
github.com/bitrise-io/go-utils/v2 v2.0.0-alpha.33/go.mod h1:3XUplo0dOWc3DqT2XA2SeHToDSg7+j1y1HTHibT2H68=
github.com/bitrise-io/go-xcode v1.3.3 h1:aYkSMWP+1/n2ZabRy3OMfeaWmE4l1gAPq63azx06LIw=
github.com/bitrise-io/go-xcode v1.3.3/go.mod h1:9OwsvrhZ4A2JxHVoEY7CPcABAKA+OE7FQqFfBfvbFuY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
Expand Down
112 changes: 112 additions & 0 deletions testresult/xcresult/testsummariesplist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package xcresult

import (
"fmt"
"strings"
)

type testSummaryPlist struct {
FormatVersion string
TestableSummaries []testableSummary
}

func collapsesubtestTree(data subtests) (tests subtests) {
for _, test := range data {
if len(test.Subtests) > 0 {
tests = append(tests, collapsesubtestTree(test.Subtests)...)
}
if test.TestStatus != "" {
tests = append(tests, test)
}
}
return
}

func (summaryPlist testSummaryPlist) tests() ([]string, map[string]subtests) {
var keyOrder []string
tests := map[string]subtests{}
var subTests subtests
for _, testableSummary := range summaryPlist.TestableSummaries {
for _, test := range testableSummary.Tests {
subTests = append(subTests, collapsesubtestTree(test.Subtests)...)
}
}
for _, test := range subTests {
// TestIdentifier is in a format of testID/testCase
testID := strings.Split(test.TestIdentifier, "/")[0]
if _, found := tests[testID]; !found {
keyOrder = append(keyOrder, testID)
}
tests[testID] = append(tests[testID], test)
}
return keyOrder, tests
}

type test struct {
Subtests subtests
}

type testableSummary struct {
TargetName string
TestKind string
TestName string
TestObjectClass string
Tests []test
}

type failureSummary struct {
FileName string
LineNumber int
Message string
PerformanceFailure bool
}

type subtest struct {
Duration float64
TestStatus string
TestIdentifier string
TestName string
TestObjectClass string
Subtests subtests
FailureSummaries []failureSummary
}

func (st subtest) failure() (message string) {
prefix := ""
for _, failure := range st.FailureSummaries {
message += fmt.Sprintf("%s%s:%d - %s", prefix, failure.FileName, failure.LineNumber, failure.Message)
prefix = "\n"
}
return
}

func (st subtest) skipped() bool {
return st.TestStatus == "Skipped"
}

type subtests []subtest

func (sts subtests) failuresCount() (count int) {
for _, test := range sts {
if len(test.FailureSummaries) > 0 {
count++
}
}
return count
}

func (sts subtests) skippedCount() (count int) {
for _, test := range sts {
if test.skipped() {
count++
}
}
return count
}

func (sts subtests) totalTime() (time float64) {
for _, test := range sts {
time += test.Duration
}
return time
}
119 changes: 119 additions & 0 deletions testresult/xcresult/xcresult.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package xcresult

import (
"path/filepath"
"strings"
"unicode"

"howett.net/plist"

"github.com/bitrise-io/go-utils/fileutil"
"github.com/bitrise-io/go-utils/pathutil"
"github.com/bitrise-io/go-steputils/v2/testreport"
)

// Converter ...
type Converter struct {
files []string
testSummariesPlistPath string
}

// Detect ...
func (c *Converter) Detect(files []string) bool {
c.files = files
for _, file := range c.files {
if filepath.Ext(file) == ".xcresult" {
testSummariesPlistPath := filepath.Join(file, "TestSummaries.plist")
if exist, err := pathutil.IsPathExists(testSummariesPlistPath); err != nil || !exist {
continue
}

c.testSummariesPlistPath = testSummariesPlistPath
return true
}
}
return false
}

// by one of our issue reports, need to replace backspace char (U+0008) as it is an invalid character for xml unmarshaller
// the legal character ranges are here: https://www.w3.org/TR/REC-xml/#charsets
// so the exclusion will be:
/*
\u0000 - \u0008
\u000B
\u000C
\u000E - \u001F
\u007F - \u0084
\u0086 - \u009F
\uD800 - \uDFFF

Unicode range D800–DFFF is used as surrogate pair. Unicode and ISO/IEC 10646 do not assign characters to any of the code points in the D800–DFFF range, so an individual code value from a surrogate pair does not represent a character. (A couple of code points — the first from the high surrogate area (D800–DBFF), and the second from the low surrogate area (DC00–DFFF) — are used in UTF-16 to represent a character in supplementary planes)
\uFDD0 - \uFDEF; \uFFFE; \uFFFF
*/
// These are non-characters in the standard, not assigned to anything; and have no meaning.
func filterIllegalChars(data []byte) (filtered []byte) {
illegalCharFilter := func(r rune) rune {
if unicode.IsPrint(r) {
return r
}
return -1
}
filtered = []byte(strings.Map(illegalCharFilter, string(data)))
return
}

// Convert returns the test report parsed from the xcresult file.
func (c *Converter) Convert() (testreport.TestReport, error) {
data, err := fileutil.ReadBytesFromFile(c.testSummariesPlistPath)
if err != nil {
return testreport.TestReport{}, err
}

data = filterIllegalChars(data)

var plistData testSummaryPlist
if _, err := plist.Unmarshal(data, &plistData); err != nil {
return testreport.TestReport{}, err
}

var xmlData testreport.TestReport
keyOrder, tests := plistData.tests()
for _, testID := range keyOrder {
tests := tests[testID]
testSuite := testreport.TestSuite{
Name: testID,
Tests: len(tests),
Failures: tests.failuresCount(),
Skipped: tests.skippedCount(),
Time: tests.totalTime(),
}

for _, test := range tests {
failureMessage := test.failure()

var failure *testreport.Failure
if len(failureMessage) > 0 {
failure = &testreport.Failure{
Value: failureMessage,
}
}

var skipped *testreport.Skipped
if test.skipped() {
skipped = &testreport.Skipped{}
}

testSuite.TestCases = append(testSuite.TestCases, testreport.TestCase{
Name: test.TestName,
ClassName: testID,
Failure: failure,
Skipped: skipped,
Time: test.Duration,
})
}

xmlData.TestSuites = append(xmlData.TestSuites, testSuite)
}

return xmlData, nil
}
15 changes: 15 additions & 0 deletions testresult/xcresult/xcresult_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package xcresult

import (
"reflect"
"testing"
)

func Test_filterIllegalChars(t *testing.T) {
// \b == /u0008 -> backspace
content := []byte("test\b text")

if !reflect.DeepEqual(filterIllegalChars(content), []byte("test text")) {
t.Fatal("illegal character is not removed")
}
}
89 changes: 89 additions & 0 deletions testresult/xcresult3/action_invocation_record.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package xcresult3

import (
"fmt"
"strings"

"github.com/bitrise-io/go-steputils/v2/testreport"
)

type actionsInvocationRecord struct {
Actions struct {
Values []struct {
ActionResult struct {
TestsRef struct {
ID struct {
Value string `json:"_value"`
} `json:"id"`
} `json:"testsRef"`
} `json:"actionResult"`
} `json:"_values"`
} `json:"actions"`

Issues issues `json:"issues"`
}

type issues struct {
TestFailureSummaries testFailureSummaries `json:"testFailureSummaries"`
}

type testFailureSummaries struct {
Values []testFailureSummary `json:"_values"`
}

type testFailureSummary struct {
DocumentLocationInCreatingWorkspace documentLocationInCreatingWorkspace `json:"documentLocationInCreatingWorkspace"`
Message message `json:"message"`
ProducingTarget producingTarget `json:"producingTarget"`
TestCaseName testCaseName `json:"testCaseName"`
}

type url struct {
Value string `json:"_value"`
}

type documentLocationInCreatingWorkspace struct {
URL url `json:"url"`
}

type producingTarget struct {
Value string `json:"_value"`
}

type testCaseName struct {
Value string `json:"_value"`
}

type message struct {
Value string `json:"_value"`
}

func testCaseMatching(test actionTestSummaryGroup, tcName string) bool {
class, method := test.references()

return tcName == class+"."+method ||
tcName == fmt.Sprintf("-[%s %s]", class, method)
}

// failure returns the failure reason for the given test from the invocation record.
func (r actionsInvocationRecord) failure(test actionTestSummaryGroup, testSuite testreport.TestSuite) string {
for _, failureSummary := range r.Issues.TestFailureSummaries.Values {
if failureSummary.ProducingTarget.Value == testSuite.Name && testCaseMatching(test, failureSummary.TestCaseName.Value) {
file, line := failureSummary.fileAndLineNumber()
return fmt.Sprintf("%s:%s - %s", file, line, failureSummary.Message.Value)
}
}
return ""
}

// fileAndLineNumber unwraps the file path and line number from the document location URL.
func (s testFailureSummary) fileAndLineNumber() (file string, line string) {
// file:\/\/\/Users\/bitrisedeveloper\/Develop\/ios\/Xcode11Test\/Xcode11TestUITests\/Xcode11TestUITests.swift#CharacterRangeLen=0&EndingLineNumber=42&StartingLineNumber=42
if s.DocumentLocationInCreatingWorkspace.URL.Value != "" {
i := strings.LastIndex(s.DocumentLocationInCreatingWorkspace.URL.Value, "#")
if i > -1 && i+1 < len(s.DocumentLocationInCreatingWorkspace.URL.Value) {
return s.DocumentLocationInCreatingWorkspace.URL.Value[:i], s.DocumentLocationInCreatingWorkspace.URL.Value[i+1:]
}
}
return
}
Loading
Loading