Skip to content

Commit bbbcfeb

Browse files
authored
Refactor how xctestrun files are parsed (#553)
By introducing new methods and introducing a new metadata object to extract the version, the parsing becomes easier to read and extend in the future.
1 parent 376dba1 commit bbbcfeb

File tree

2 files changed

+61
-83
lines changed

2 files changed

+61
-83
lines changed

ios/testmanagerd/xctestrunnerutils_test.go

+10-15
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
)
1010

1111
// Helper function to create mock data and parse the .xctestrun file
12-
func createAndParseXCTestRunFile(t *testing.T) xCTestRunData {
12+
func createAndParseXCTestRunFile(t *testing.T) schemeData {
1313
// Arrange: Create a temporary .xctestrun file with mock data
1414
tempFile, err := os.CreateTemp("", "testfile*.xctestrun")
1515
assert.NoError(t, err, "Failed to create temp file")
@@ -136,12 +136,12 @@ func createAndParseXCTestRunFile(t *testing.T) xCTestRunData {
136136

137137
func TestTestHostBundleIdentifier(t *testing.T) {
138138
xcTestRunData := createAndParseXCTestRunFile(t)
139-
assert.Equal(t, "com.example.myApp", xcTestRunData.TestConfig.TestHostBundleIdentifier, "TestHostBundleIdentifier mismatch")
139+
assert.Equal(t, "com.example.myApp", xcTestRunData.TestHostBundleIdentifier, "TestHostBundleIdentifier mismatch")
140140
}
141141

142142
func TestTestBundlePath(t *testing.T) {
143143
xcTestRunData := createAndParseXCTestRunFile(t)
144-
assert.Equal(t, "__TESTHOST__/PlugIns/RunnerTests.xctest", xcTestRunData.TestConfig.TestBundlePath, "TestBundlePath mismatch")
144+
assert.Equal(t, "__TESTHOST__/PlugIns/RunnerTests.xctest", xcTestRunData.TestBundlePath, "TestBundlePath mismatch")
145145
}
146146

147147
func TestEnvironmentVariables(t *testing.T) {
@@ -151,7 +151,7 @@ func TestEnvironmentVariables(t *testing.T) {
151151
"OS_ACTIVITY_DT_MODE": "YES",
152152
"SQLITE_ENABLE_THREAD_ASSERTIONS": "1",
153153
"TERM": "dumb",
154-
}, xcTestRunData.TestConfig.EnvironmentVariables, "EnvironmentVariables mismatch")
154+
}, xcTestRunData.EnvironmentVariables, "EnvironmentVariables mismatch")
155155
}
156156

157157
func TestTestingEnvironmentVariables(t *testing.T) {
@@ -160,38 +160,33 @@ func TestTestingEnvironmentVariables(t *testing.T) {
160160
"DYLD_INSERT_LIBRARIES": "__TESTHOST__/Frameworks/libXCTestBundleInject.dylib",
161161
"XCInjectBundleInto": "unused",
162162
"Test": "xyz",
163-
}, xcTestRunData.TestConfig.TestingEnvironmentVariables, "TestingEnvironmentVariables mismatch")
163+
}, xcTestRunData.TestingEnvironmentVariables, "TestingEnvironmentVariables mismatch")
164164
}
165165

166166
func TestCommandLineArguments(t *testing.T) {
167167
xcTestRunData := createAndParseXCTestRunFile(t)
168-
assert.Equal(t, []string{}, xcTestRunData.TestConfig.CommandLineArguments, "CommandLineArguments mismatch")
168+
assert.Equal(t, []string{}, xcTestRunData.CommandLineArguments, "CommandLineArguments mismatch")
169169
}
170170

171171
func TestOnlyTestIdentifiers(t *testing.T) {
172172
xcTestRunData := createAndParseXCTestRunFile(t)
173173
assert.Equal(t, []string{
174174
"TestClass1/testMethod1",
175175
"TestClass2/testMethod1",
176-
}, xcTestRunData.TestConfig.OnlyTestIdentifiers, "OnlyTestIdentifiers mismatch")
176+
}, xcTestRunData.OnlyTestIdentifiers, "OnlyTestIdentifiers mismatch")
177177
}
178178

179179
func TestSkipTestIdentifiers(t *testing.T) {
180180
xcTestRunData := createAndParseXCTestRunFile(t)
181181
assert.Equal(t, []string{
182182
"TestClass1/testMethod2",
183183
"TestClass2/testMethod2",
184-
}, xcTestRunData.TestConfig.SkipTestIdentifiers, "SkipTestIdentifiers mismatch")
185-
}
186-
187-
func TestFormatVersion(t *testing.T) {
188-
xcTestRunData := createAndParseXCTestRunFile(t)
189-
assert.Equal(t, 1, xcTestRunData.XCTestRunMetadata.FormatVersion, "FormatVersion mismatch")
184+
}, xcTestRunData.SkipTestIdentifiers, "SkipTestIdentifiers mismatch")
190185
}
191186

192187
func TestIsUITestBundle(t *testing.T) {
193188
xcTestRunData := createAndParseXCTestRunFile(t)
194-
assert.Equal(t, true, xcTestRunData.TestConfig.IsUITestBundle, "IsUITestBundle mismatch")
189+
assert.Equal(t, true, xcTestRunData.IsUITestBundle, "IsUITestBundle mismatch")
195190
}
196191

197192
func TestParseXCTestRunNotSupportedForFormatVersionOtherThanOne(t *testing.T) {
@@ -221,7 +216,7 @@ func TestParseXCTestRunNotSupportedForFormatVersionOtherThanOne(t *testing.T) {
221216
_, err = parseFile(tempFile.Name())
222217

223218
// Assert the Error Message
224-
assert.Equal(t, "go-ios currently only supports .xctestrun files in formatVersion 1: The formatVersion of your xctestrun file is 2, feel free to open an issue in https://github.com/danielpaulus/go-ios/issues to add support", err.Error(), "Error Message mismatch")
219+
assert.Equal(t, "the provided .xctestrun file used format version 2, which is not yet supported", err.Error(), "Error Message mismatch")
225220
}
226221

227222
// Helper function to create testConfig from parsed mock data

ios/testmanagerd/xctestrunutils.go

+51-68
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package testmanagerd
22

33
import (
44
"bytes"
5-
"errors"
65
"fmt"
76
"github.com/danielpaulus/go-ios/ios"
87
"howett.net/plist"
@@ -24,10 +23,6 @@ import (
2423
// contributions or requests for support can be made in the relevant GitHub repository.
2524

2625
// xCTestRunData represents the structure of an .xctestrun file
27-
type xCTestRunData struct {
28-
TestConfig schemeData `plist:"-"`
29-
XCTestRunMetadata xCTestRunMetadata `plist:"__xctestrun_metadata__"`
30-
}
3126

3227
// schemeData represents the structure of a scheme-specific test configuration
3328
type schemeData struct {
@@ -41,33 +36,28 @@ type schemeData struct {
4136
TestingEnvironmentVariables map[string]any
4237
}
4338

44-
// XCTestRunMetadata contains metadata about the .xctestrun file
45-
type xCTestRunMetadata struct {
46-
FormatVersion int `plist:"FormatVersion"`
47-
}
48-
49-
func (data xCTestRunData) buildTestConfig(device ios.DeviceEntry, listener *TestListener) (TestConfig, error) {
50-
testsToRun := data.TestConfig.OnlyTestIdentifiers
51-
testsToSkip := data.TestConfig.SkipTestIdentifiers
39+
func (data schemeData) buildTestConfig(device ios.DeviceEntry, listener *TestListener) (TestConfig, error) {
40+
testsToRun := data.OnlyTestIdentifiers
41+
testsToSkip := data.SkipTestIdentifiers
5242

5343
testEnv := make(map[string]any)
54-
if data.TestConfig.IsUITestBundle {
55-
maps.Copy(testEnv, data.TestConfig.EnvironmentVariables)
56-
maps.Copy(testEnv, data.TestConfig.TestingEnvironmentVariables)
44+
if data.IsUITestBundle {
45+
maps.Copy(testEnv, data.EnvironmentVariables)
46+
maps.Copy(testEnv, data.TestingEnvironmentVariables)
5747
}
5848

5949
// Extract only the file name
60-
var testBundlePath = filepath.Base(data.TestConfig.TestBundlePath)
50+
var testBundlePath = filepath.Base(data.TestBundlePath)
6151

6252
// Build the TestConfig object from parsed data
6353
testConfig := TestConfig{
64-
TestRunnerBundleId: data.TestConfig.TestHostBundleIdentifier,
54+
TestRunnerBundleId: data.TestHostBundleIdentifier,
6555
XctestConfigName: testBundlePath,
66-
Args: data.TestConfig.CommandLineArguments,
56+
Args: data.CommandLineArguments,
6757
Env: testEnv,
6858
TestsToRun: testsToRun,
6959
TestsToSkip: testsToSkip,
70-
XcTest: !data.TestConfig.IsUITestBundle,
60+
XcTest: !data.IsUITestBundle,
7161
Device: device,
7262
Listener: listener,
7363
}
@@ -76,69 +66,66 @@ func (data xCTestRunData) buildTestConfig(device ios.DeviceEntry, listener *Test
7666
}
7767

7868
// parseFile reads the .xctestrun file and decodes it into a map
79-
func parseFile(filePath string) (xCTestRunData, error) {
69+
func parseFile(filePath string) (schemeData, error) {
8070
file, err := os.Open(filePath)
8171
if err != nil {
82-
return xCTestRunData{}, fmt.Errorf("failed to open xctestrun file: %w", err)
72+
return schemeData{}, fmt.Errorf("failed to open xctestrun file: %w", err)
8373
}
8474
defer file.Close()
8575
return decode(file)
8676
}
8777

8878
// decode decodes the binary xctestrun content into the xCTestRunData struct
89-
func decode(r io.Reader) (xCTestRunData, error) {
79+
func decode(r io.Reader) (schemeData, error) {
9080
// Read the entire content once
91-
content, err := io.ReadAll(r)
81+
xctestrunFileContent, err := io.ReadAll(r)
9282
if err != nil {
93-
return xCTestRunData{}, fmt.Errorf("failed to read content: %w", err)
94-
}
95-
96-
// Use a single map for initial parsing
97-
var rawData map[string]interface{}
98-
if _, err := plist.Unmarshal(content, &rawData); err != nil {
99-
return xCTestRunData{}, fmt.Errorf("failed to unmarshal plist: %w", err)
100-
}
101-
102-
result := xCTestRunData{
103-
TestConfig: schemeData{}, // Initialize TestConfig
83+
return schemeData{}, fmt.Errorf("unable to read xctestrun content: %w", err)
10484
}
10585

106-
// Parse metadata
107-
metadataMap, ok := rawData["__xctestrun_metadata__"].(map[string]interface{})
108-
if !ok {
109-
return xCTestRunData{}, errors.New("invalid or missing __xctestrun_metadata__")
86+
// First, we only parse the version property of the xctestrun file. The rest of the parsing depends on this version.
87+
version, err := getFormatVersion(xctestrunFileContent)
88+
if err != nil {
89+
return schemeData{}, err
11090
}
11191

112-
// Direct decoding of metadata to avoid additional conversion
113-
switch v := metadataMap["FormatVersion"].(type) {
114-
case int:
115-
result.XCTestRunMetadata.FormatVersion = v
116-
case uint64:
117-
result.XCTestRunMetadata.FormatVersion = int(v)
92+
switch version {
93+
case 1:
94+
return parseVersion1(xctestrunFileContent)
95+
case 2:
96+
return schemeData{}, fmt.Errorf("the provided .xctestrun file used format version 2, which is not yet supported")
11897
default:
119-
return xCTestRunData{}, fmt.Errorf("unexpected FormatVersion type: %T", metadataMap["FormatVersion"])
98+
return schemeData{}, fmt.Errorf("the provided .xctestrun format version %d is not supported", version)
12099
}
100+
}
121101

122-
// Verify FormatVersion
123-
if result.XCTestRunMetadata.FormatVersion != 1 {
124-
return result, fmt.Errorf("go-ios currently only supports .xctestrun files in formatVersion 1: "+
125-
"The formatVersion of your xctestrun file is %d, feel free to open an issue in https://github.com/danielpaulus/go-ios/issues to "+
126-
"add support", result.XCTestRunMetadata.FormatVersion)
102+
// Helper method to get the format version of the xctestrun file
103+
func getFormatVersion(xctestrunFileContent []byte) (int, error) {
104+
105+
type xCTestRunMetadata struct {
106+
Metadata struct {
107+
Version int `plist:"FormatVersion"`
108+
} `plist:"__xctestrun_metadata__"`
127109
}
128110

129-
// Parse test schemes
130-
if err := parseTestSchemes(rawData, &result.TestConfig); err != nil {
131-
return xCTestRunData{}, err
111+
var metadata xCTestRunMetadata
112+
if _, err := plist.Unmarshal(xctestrunFileContent, &metadata); err != nil {
113+
return 0, fmt.Errorf("failed to parse format version: %w", err)
132114
}
133115

134-
return result, nil
116+
return metadata.Metadata.Version, nil
135117
}
136118

137-
// parseTestSchemes extracts and parses test schemes from the raw data
138-
func parseTestSchemes(rawData map[string]interface{}, scheme *schemeData) error {
139-
// Dynamically find and parse test schemes
140-
for key, value := range rawData {
141-
// Skip metadata key
119+
func parseVersion1(content []byte) (schemeData, error) {
120+
// xctestrun files in version 1 use a dynamic key for the pListRoot of the TestConfig. As in the 'key' for the TestConfig is the name
121+
// of the app. This forces us to iterate over the root of the plist, instead of using a static struct to decode the xctestrun file.
122+
var pListRoot map[string]interface{}
123+
if _, err := plist.Unmarshal(content, &pListRoot); err != nil {
124+
return schemeData{}, fmt.Errorf("failed to unmarshal plist: %w", err)
125+
}
126+
127+
for key, value := range pListRoot {
128+
// Skip the metadata object
142129
if key == "__xctestrun_metadata__" {
143130
continue
144131
}
@@ -154,19 +141,15 @@ func parseTestSchemes(rawData map[string]interface{}, scheme *schemeData) error
154141
schemeBuf := new(bytes.Buffer)
155142
encoder := plist.NewEncoder(schemeBuf)
156143
if err := encoder.Encode(schemeMap); err != nil {
157-
return fmt.Errorf("failed to encode scheme %s: %w", key, err)
144+
return schemeData{}, fmt.Errorf("failed to encode scheme %s: %w", key, err)
158145
}
159146

160147
// Decode the plist buffer into schemeData
161148
decoder := plist.NewDecoder(bytes.NewReader(schemeBuf.Bytes()))
162149
if err := decoder.Decode(&schemeParsed); err != nil {
163-
return fmt.Errorf("failed to decode scheme %s: %w", key, err)
150+
return schemeData{}, fmt.Errorf("failed to decode scheme %s: %w", key, err)
164151
}
165-
166-
// Store the scheme in the result TestConfig
167-
*scheme = schemeParsed
168-
break // Only one scheme expected, break after the first valid scheme
152+
return schemeParsed, nil
169153
}
170-
171-
return nil
154+
return schemeData{}, nil
172155
}

0 commit comments

Comments
 (0)