Skip to content

Commit

Permalink
Refactor how xctestrun files are parsed (#553)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
si-net authored Jan 27, 2025
1 parent 376dba1 commit bbbcfeb
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 83 deletions.
25 changes: 10 additions & 15 deletions ios/testmanagerd/xctestrunnerutils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

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

func TestTestHostBundleIdentifier(t *testing.T) {
xcTestRunData := createAndParseXCTestRunFile(t)
assert.Equal(t, "com.example.myApp", xcTestRunData.TestConfig.TestHostBundleIdentifier, "TestHostBundleIdentifier mismatch")
assert.Equal(t, "com.example.myApp", xcTestRunData.TestHostBundleIdentifier, "TestHostBundleIdentifier mismatch")
}

func TestTestBundlePath(t *testing.T) {
xcTestRunData := createAndParseXCTestRunFile(t)
assert.Equal(t, "__TESTHOST__/PlugIns/RunnerTests.xctest", xcTestRunData.TestConfig.TestBundlePath, "TestBundlePath mismatch")
assert.Equal(t, "__TESTHOST__/PlugIns/RunnerTests.xctest", xcTestRunData.TestBundlePath, "TestBundlePath mismatch")
}

func TestEnvironmentVariables(t *testing.T) {
Expand All @@ -151,7 +151,7 @@ func TestEnvironmentVariables(t *testing.T) {
"OS_ACTIVITY_DT_MODE": "YES",
"SQLITE_ENABLE_THREAD_ASSERTIONS": "1",
"TERM": "dumb",
}, xcTestRunData.TestConfig.EnvironmentVariables, "EnvironmentVariables mismatch")
}, xcTestRunData.EnvironmentVariables, "EnvironmentVariables mismatch")
}

func TestTestingEnvironmentVariables(t *testing.T) {
Expand All @@ -160,38 +160,33 @@ func TestTestingEnvironmentVariables(t *testing.T) {
"DYLD_INSERT_LIBRARIES": "__TESTHOST__/Frameworks/libXCTestBundleInject.dylib",
"XCInjectBundleInto": "unused",
"Test": "xyz",
}, xcTestRunData.TestConfig.TestingEnvironmentVariables, "TestingEnvironmentVariables mismatch")
}, xcTestRunData.TestingEnvironmentVariables, "TestingEnvironmentVariables mismatch")
}

func TestCommandLineArguments(t *testing.T) {
xcTestRunData := createAndParseXCTestRunFile(t)
assert.Equal(t, []string{}, xcTestRunData.TestConfig.CommandLineArguments, "CommandLineArguments mismatch")
assert.Equal(t, []string{}, xcTestRunData.CommandLineArguments, "CommandLineArguments mismatch")
}

func TestOnlyTestIdentifiers(t *testing.T) {
xcTestRunData := createAndParseXCTestRunFile(t)
assert.Equal(t, []string{
"TestClass1/testMethod1",
"TestClass2/testMethod1",
}, xcTestRunData.TestConfig.OnlyTestIdentifiers, "OnlyTestIdentifiers mismatch")
}, xcTestRunData.OnlyTestIdentifiers, "OnlyTestIdentifiers mismatch")
}

func TestSkipTestIdentifiers(t *testing.T) {
xcTestRunData := createAndParseXCTestRunFile(t)
assert.Equal(t, []string{
"TestClass1/testMethod2",
"TestClass2/testMethod2",
}, xcTestRunData.TestConfig.SkipTestIdentifiers, "SkipTestIdentifiers mismatch")
}

func TestFormatVersion(t *testing.T) {
xcTestRunData := createAndParseXCTestRunFile(t)
assert.Equal(t, 1, xcTestRunData.XCTestRunMetadata.FormatVersion, "FormatVersion mismatch")
}, xcTestRunData.SkipTestIdentifiers, "SkipTestIdentifiers mismatch")
}

func TestIsUITestBundle(t *testing.T) {
xcTestRunData := createAndParseXCTestRunFile(t)
assert.Equal(t, true, xcTestRunData.TestConfig.IsUITestBundle, "IsUITestBundle mismatch")
assert.Equal(t, true, xcTestRunData.IsUITestBundle, "IsUITestBundle mismatch")
}

func TestParseXCTestRunNotSupportedForFormatVersionOtherThanOne(t *testing.T) {
Expand Down Expand Up @@ -221,7 +216,7 @@ func TestParseXCTestRunNotSupportedForFormatVersionOtherThanOne(t *testing.T) {
_, err = parseFile(tempFile.Name())

// Assert the Error Message
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")
assert.Equal(t, "the provided .xctestrun file used format version 2, which is not yet supported", err.Error(), "Error Message mismatch")
}

// Helper function to create testConfig from parsed mock data
Expand Down
119 changes: 51 additions & 68 deletions ios/testmanagerd/xctestrunutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package testmanagerd

import (
"bytes"
"errors"
"fmt"
"github.com/danielpaulus/go-ios/ios"
"howett.net/plist"
Expand All @@ -24,10 +23,6 @@ import (
// contributions or requests for support can be made in the relevant GitHub repository.

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

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

// XCTestRunMetadata contains metadata about the .xctestrun file
type xCTestRunMetadata struct {
FormatVersion int `plist:"FormatVersion"`
}

func (data xCTestRunData) buildTestConfig(device ios.DeviceEntry, listener *TestListener) (TestConfig, error) {
testsToRun := data.TestConfig.OnlyTestIdentifiers
testsToSkip := data.TestConfig.SkipTestIdentifiers
func (data schemeData) buildTestConfig(device ios.DeviceEntry, listener *TestListener) (TestConfig, error) {
testsToRun := data.OnlyTestIdentifiers
testsToSkip := data.SkipTestIdentifiers

testEnv := make(map[string]any)
if data.TestConfig.IsUITestBundle {
maps.Copy(testEnv, data.TestConfig.EnvironmentVariables)
maps.Copy(testEnv, data.TestConfig.TestingEnvironmentVariables)
if data.IsUITestBundle {
maps.Copy(testEnv, data.EnvironmentVariables)
maps.Copy(testEnv, data.TestingEnvironmentVariables)
}

// Extract only the file name
var testBundlePath = filepath.Base(data.TestConfig.TestBundlePath)
var testBundlePath = filepath.Base(data.TestBundlePath)

// Build the TestConfig object from parsed data
testConfig := TestConfig{
TestRunnerBundleId: data.TestConfig.TestHostBundleIdentifier,
TestRunnerBundleId: data.TestHostBundleIdentifier,
XctestConfigName: testBundlePath,
Args: data.TestConfig.CommandLineArguments,
Args: data.CommandLineArguments,
Env: testEnv,
TestsToRun: testsToRun,
TestsToSkip: testsToSkip,
XcTest: !data.TestConfig.IsUITestBundle,
XcTest: !data.IsUITestBundle,
Device: device,
Listener: listener,
}
Expand All @@ -76,69 +66,66 @@ func (data xCTestRunData) buildTestConfig(device ios.DeviceEntry, listener *Test
}

// parseFile reads the .xctestrun file and decodes it into a map
func parseFile(filePath string) (xCTestRunData, error) {
func parseFile(filePath string) (schemeData, error) {
file, err := os.Open(filePath)
if err != nil {
return xCTestRunData{}, fmt.Errorf("failed to open xctestrun file: %w", err)
return schemeData{}, fmt.Errorf("failed to open xctestrun file: %w", err)
}
defer file.Close()
return decode(file)
}

// decode decodes the binary xctestrun content into the xCTestRunData struct
func decode(r io.Reader) (xCTestRunData, error) {
func decode(r io.Reader) (schemeData, error) {
// Read the entire content once
content, err := io.ReadAll(r)
xctestrunFileContent, err := io.ReadAll(r)
if err != nil {
return xCTestRunData{}, fmt.Errorf("failed to read content: %w", err)
}

// Use a single map for initial parsing
var rawData map[string]interface{}
if _, err := plist.Unmarshal(content, &rawData); err != nil {
return xCTestRunData{}, fmt.Errorf("failed to unmarshal plist: %w", err)
}

result := xCTestRunData{
TestConfig: schemeData{}, // Initialize TestConfig
return schemeData{}, fmt.Errorf("unable to read xctestrun content: %w", err)
}

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

// Direct decoding of metadata to avoid additional conversion
switch v := metadataMap["FormatVersion"].(type) {
case int:
result.XCTestRunMetadata.FormatVersion = v
case uint64:
result.XCTestRunMetadata.FormatVersion = int(v)
switch version {
case 1:
return parseVersion1(xctestrunFileContent)
case 2:
return schemeData{}, fmt.Errorf("the provided .xctestrun file used format version 2, which is not yet supported")
default:
return xCTestRunData{}, fmt.Errorf("unexpected FormatVersion type: %T", metadataMap["FormatVersion"])
return schemeData{}, fmt.Errorf("the provided .xctestrun format version %d is not supported", version)
}
}

// Verify FormatVersion
if result.XCTestRunMetadata.FormatVersion != 1 {
return result, fmt.Errorf("go-ios currently only supports .xctestrun files in formatVersion 1: "+
"The formatVersion of your xctestrun file is %d, feel free to open an issue in https://github.com/danielpaulus/go-ios/issues to "+
"add support", result.XCTestRunMetadata.FormatVersion)
// Helper method to get the format version of the xctestrun file
func getFormatVersion(xctestrunFileContent []byte) (int, error) {

type xCTestRunMetadata struct {
Metadata struct {
Version int `plist:"FormatVersion"`
} `plist:"__xctestrun_metadata__"`
}

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

return result, nil
return metadata.Metadata.Version, nil
}

// parseTestSchemes extracts and parses test schemes from the raw data
func parseTestSchemes(rawData map[string]interface{}, scheme *schemeData) error {
// Dynamically find and parse test schemes
for key, value := range rawData {
// Skip metadata key
func parseVersion1(content []byte) (schemeData, error) {
// 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
// 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.
var pListRoot map[string]interface{}
if _, err := plist.Unmarshal(content, &pListRoot); err != nil {
return schemeData{}, fmt.Errorf("failed to unmarshal plist: %w", err)
}

for key, value := range pListRoot {
// Skip the metadata object
if key == "__xctestrun_metadata__" {
continue
}
Expand All @@ -154,19 +141,15 @@ func parseTestSchemes(rawData map[string]interface{}, scheme *schemeData) error
schemeBuf := new(bytes.Buffer)
encoder := plist.NewEncoder(schemeBuf)
if err := encoder.Encode(schemeMap); err != nil {
return fmt.Errorf("failed to encode scheme %s: %w", key, err)
return schemeData{}, fmt.Errorf("failed to encode scheme %s: %w", key, err)
}

// Decode the plist buffer into schemeData
decoder := plist.NewDecoder(bytes.NewReader(schemeBuf.Bytes()))
if err := decoder.Decode(&schemeParsed); err != nil {
return fmt.Errorf("failed to decode scheme %s: %w", key, err)
return schemeData{}, fmt.Errorf("failed to decode scheme %s: %w", key, err)
}

// Store the scheme in the result TestConfig
*scheme = schemeParsed
break // Only one scheme expected, break after the first valid scheme
return schemeParsed, nil
}

return nil
return schemeData{}, nil
}

0 comments on commit bbbcfeb

Please sign in to comment.