diff --git a/README.md b/README.md index 40ae783c..376350d6 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ Run tests with custom xcconfig file path: | `xcpretty_options` | Additional options to be added to the executed xcpretty command. | | `--color --report html --output "${BITRISE_DEPLOY_DIR}/xcode-test-results-${BITRISE_SCHEME}.html"` | | `cache_level` | Defines what cache content should be automatically collected. Use key-based caching instead for better performance. Available options: - `none`: Disable collecting cache content. - `swift_packages`: Collect Swift PM packages added to the Xcode project. With key-based caching, you only need the Restore SPM cache and the Save SPM cache Steps to cache your Swift packages. [See devcenter for more information.](https://devcenter.bitrise.io/en/dependencies-and-caching/managing-dependencies-for-ios-apps/managing-dependencies-with-spm.html#caching-swift-packages) | | `none` | | `verbose_log` | If this input is set, the Step will print additional logs for debugging. | | `no` | -| `collect_simulator_diagnostics` | If this input is set, the simulator verbose logging will be enabled and the simulator diagnostics log will be exported. | | `never` | +| `collect_simulator_diagnostics` | Enables Simulator verbose logging and controls whether Simulator diagnostics are collected after the test run. xcodebuild collects the Simulator diagnostics (a `simctl diagnose` bundle) itself after a failing test run and stores them in the `.xcresult`; extract them with `xcrun xcresulttool export diagnostics`. Simulator verbose logging is enabled before the tests run unless this Input is `never`. The default value is `never`, which prevents intermittent hangs in a CI environment. To let xcodebuild follow the test plan (collect on failure unless the plan says otherwise), set this Input to `project_setting`. A `-collect-test-diagnostics` option passed in `xcodebuild_options` takes precedence over this Input. On Xcode 16 and earlier, xcodebuild has no such option and the Step collects the diagnostics itself after the tests and exports them as a zip artifact. Available options: - `project_setting`: use the option set in the Xcode project test plan. - `always`: same as `on_failure` on Xcode 26 and later, where xcodebuild collects the diagnostics itself. On Xcode 16 and earlier the Step also collects after passing runs. - `on_failure`: collect diagnostics when the tests fail. - `never`: overrides any test plan setting and prevents the collection of diagnostics. | | `never` | | `headless_mode` | In headless mode the simulator is not launched in the foreground. If this input is set, the simulator will not be visible but tests (even the screenshots) will run just like if you run a simulator in foreground. | | `yes` | | `quarantined_tests` | JSON list of tests added to quarantine on Bitrise.io, quarantined tests are excluded from test runs. | | `$BITRISE_QUARANTINED_TESTS_JSON` | diff --git a/bitrise.yml b/bitrise.yml index 89b04e10..091a41da 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -1,14 +1,17 @@ format_version: "11" default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git +include: +- repository: steps-check + branch: master + path: steps.bitrise.yml + app: envs: - ORIG_BITRISE_SOURCE_DIR: $BITRISE_SOURCE_DIR workflows: - check: - steps: - - git::https://github.com/bitrise-steplib/steps-check.git: { } + # check: included from steps-check/steps.bitrise.yml e2e: steps: diff --git a/e2e/bitrise.yml b/e2e/bitrise.yml index 7ca5447c..5c884106 100644 --- a/e2e/bitrise.yml +++ b/e2e/bitrise.yml @@ -209,7 +209,7 @@ workflows: - repository_url: $TEST_APP_URL - clone_into_dir: ./_tmp - branch: $TEST_APP_BRANCH - - certificate-and-profile-installer: { } + - certificate-and-profile-installer: {} - script: title: Set 'collect_simulator_diagnostics' input inputs: diff --git a/main.go b/main.go index 37d83201..78178f65 100644 --- a/main.go +++ b/main.go @@ -38,13 +38,13 @@ func run() int { configParser := createConfigParser(logger) config, err := configParser.ProcessConfig() if err != nil { - logger.Errorf(errorutil.FormattedError(fmt.Errorf("Failed to process Step inputs: %w", err))) + logger.Errorf("%s", errorutil.FormattedError(fmt.Errorf("Failed to process Step inputs: %w", err))) //nolint:staticcheck // top-level messages are user facing return 1 } xcodeTestRunner, err := createStep(logger, config.LogFormatter) if err != nil { - logger.Errorf(errorutil.FormattedError(fmt.Errorf("Failed to process Step inputs: %w", err))) + logger.Errorf("%s", errorutil.FormattedError(fmt.Errorf("Failed to process Step inputs: %w", err))) //nolint:staticcheck // top-level messages are user facing return 1 } @@ -54,12 +54,12 @@ func run() int { exportErr := xcodeTestRunner.Export(res, runErr != nil) if runErr != nil { - logger.Errorf(errorutil.FormattedError(fmt.Errorf("Failed to execute Step: %w", runErr))) + logger.Errorf("%s", errorutil.FormattedError(fmt.Errorf("Failed to execute Step: %w", runErr))) //nolint:staticcheck // top-level messages are user facing return 1 } if exportErr != nil { - logger.Errorf(errorutil.FormattedError(fmt.Errorf("Failed to export Step outputs: %w", exportErr))) + logger.Errorf("%s", errorutil.FormattedError(fmt.Errorf("Failed to export Step outputs: %w", exportErr))) //nolint:staticcheck // top-level messages are user facing return 1 } @@ -73,13 +73,13 @@ func createConfigParser(logger log.Logger) step.XcodeTestConfigParser { xcodeVersionProvider := xcodeversion.NewXcodeVersionProvider(commandFactory) xcodeVersion, err := xcodeVersionProvider.GetVersion() if err != nil { // Not a fatal error, continuing with empty version - logger.Errorf("failed to read Xcode version: %w", err) + logger.Errorf("Failed to read Xcode version: %s", err) } pathModifier := pathutil.NewPathModifier() deviceFinder := destination.NewDeviceFinder(logger, commandFactory, xcodeVersion) utils := step.NewUtils(logger) - return step.NewXcodeTestConfigParser(inputParser, logger, deviceFinder, pathModifier, utils) + return step.NewXcodeTestConfigParser(inputParser, logger, deviceFinder, pathModifier, utils, xcodeVersion) } func createStep(logger log.Logger, logFormatter string) (step.XcodeTestRunner, error) { diff --git a/step.yml b/step.yml index 7dd64d70..76e5a254 100644 --- a/step.yml +++ b/step.yml @@ -258,8 +258,28 @@ inputs: opts: category: Debugging title: Collect Simulator diagnostics - summary: If this input is set, the simulator verbose logging will be enabled and the simulator diagnostics log will be exported. + summary: Enables Simulator verbose logging and controls whether Simulator diagnostics are collected after the test run. + description: |- + Enables Simulator verbose logging and controls whether Simulator diagnostics are collected after the test run. + + xcodebuild collects the Simulator diagnostics (a `simctl diagnose` bundle) itself after a failing test run + and stores them in the `.xcresult`; extract them with `xcrun xcresulttool export diagnostics`. + Simulator verbose logging is enabled before the tests run unless this Input is `never`. + + The default value is `never`, which prevents intermittent hangs in a CI environment. + To let xcodebuild follow the test plan (collect on failure unless the plan says otherwise), set this Input to `project_setting`. + A `-collect-test-diagnostics` option passed in `xcodebuild_options` takes precedence over this Input. + + On Xcode 16 and earlier, xcodebuild has no such option and the Step collects the diagnostics itself after the + tests and exports them as a zip artifact. + + Available options: + - `project_setting`: use the option set in the Xcode project test plan. + - `always`: same as `on_failure` on Xcode 26 and later, where xcodebuild collects the diagnostics itself. On Xcode 16 and earlier the Step also collects after passing runs. + - `on_failure`: collect diagnostics when the tests fail. + - `never`: overrides any test plan setting and prevents the collection of diagnostics. value_options: + - project_setting - always - on_failure - never diff --git a/step/diagnostics_test.go b/step/diagnostics_test.go new file mode 100644 index 00000000..831e5dff --- /dev/null +++ b/step/diagnostics_test.go @@ -0,0 +1,157 @@ +package step + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_xcodebuildDiagnosticsOverride(t *testing.T) { + tests := []struct { + name string + condition exportCondition + xcodeMajorVersion int64 + additionalOptions []string + want string + }{ + { + name: "Xcode 26, never - suppresses xcodebuild's own collection", + condition: never, + xcodeMajorVersion: 26, + want: "never", + }, + { + name: "Xcode 27, never - suppresses xcodebuild's own collection", + condition: never, + xcodeMajorVersion: 27, + want: "never", + }, + { + name: "Xcode 27, on_failure - keeps xcodebuild's collection", + condition: onFailure, + xcodeMajorVersion: 27, + want: "on-failure", + }, + { + // xcodebuild has no "always"; its collection is failure-triggered regardless. + name: "Xcode 27, always - maps onto on-failure", + condition: always, + xcodeMajorVersion: 27, + want: "on-failure", + }, + { + // project_setting leaves the decision to the test plan, so nothing is passed. + name: "Xcode 27, project_setting - option not passed", + condition: projectSetting, + xcodeMajorVersion: 27, + want: "", + }, + { + // The option does not exist before Xcode 26; passing it would be a usage error. + name: "Xcode 25, never - option not passed", + condition: never, + xcodeMajorVersion: 25, + want: "", + }, + { + name: "Xcode 15, always - option not passed", + condition: always, + xcodeMajorVersion: 15, + want: "", + }, + { + // main.go treats an unreadable Xcode version as non-fatal and leaves Major at 0. + name: "unknown Xcode version - option not passed", + condition: never, + xcodeMajorVersion: 0, + want: "", + }, + { + name: "user set the option explicitly - theirs wins", + condition: never, + xcodeMajorVersion: 27, + additionalOptions: []string{"-collect-test-diagnostics", "on-failure"}, + want: "", + }, + { + // xcodebuild silently drops the -option=value form, so it must not count as an override. + name: "user wrote the option with = syntax - Step still passes its own", + condition: never, + xcodeMajorVersion: 27, + additionalOptions: []string{"-collect-test-diagnostics=on-failure"}, + want: "never", + }, + { + name: "unrelated additional options are ignored", + condition: never, + xcodeMajorVersion: 27, + additionalOptions: []string{"-quiet", "-parallel-testing-enabled", "NO"}, + want: "never", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := xcodebuildDiagnosticsOverride(tt.condition, tt.xcodeMajorVersion, tt.additionalOptions) + require.Equal(t, tt.want, got) + }) + } +} + +func Test_shouldStepCollectDiagnostics(t *testing.T) { + tests := []struct { + name string + condition exportCondition + testFailed bool + xcodeMajorVersion int64 + want bool + }{ + {name: "Xcode 16, always, passed", condition: always, testFailed: false, xcodeMajorVersion: 16, want: true}, + {name: "Xcode 16, always, failed", condition: always, testFailed: true, xcodeMajorVersion: 16, want: true}, + {name: "Xcode 16, on_failure, passed", condition: onFailure, testFailed: false, xcodeMajorVersion: 16, want: false}, + {name: "Xcode 16, on_failure, failed", condition: onFailure, testFailed: true, xcodeMajorVersion: 16, want: true}, + {name: "Xcode 16, never, failed", condition: never, testFailed: true, xcodeMajorVersion: 16, want: false}, + // project_setting has nothing to follow before Xcode 26, the Step does not collect on its own. + {name: "Xcode 16, project_setting, failed", condition: projectSetting, testFailed: true, xcodeMajorVersion: 16, want: false}, + {name: "Xcode 27, project_setting, failed", condition: projectSetting, testFailed: true, xcodeMajorVersion: 27, want: false}, + // main.go leaves the major at 0 when the Xcode version cannot be read; keep collecting there. + {name: "unknown Xcode, on_failure, failed", condition: onFailure, testFailed: true, xcodeMajorVersion: 0, want: true}, + // Since Xcode 26 xcodebuild collects into the xcresult itself; the Step must not collect a second copy. + {name: "Xcode 26, on_failure, failed", condition: onFailure, testFailed: true, xcodeMajorVersion: 26, want: false}, + {name: "Xcode 27, always, failed", condition: always, testFailed: true, xcodeMajorVersion: 27, want: false}, + {name: "Xcode 27, always, passed", condition: always, testFailed: false, xcodeMajorVersion: 27, want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := shouldStepCollectDiagnostics(tt.condition, tt.testFailed, tt.xcodeMajorVersion) + require.Equal(t, tt.want, got) + }) + } +} + +func Test_xcodebuildCollectsDiagnostics(t *testing.T) { + tests := []struct { + name string + condition exportCondition + testFailed bool + xcodeMajorVersion int64 + want bool + }{ + {name: "Xcode 27, on_failure, failed", condition: onFailure, testFailed: true, xcodeMajorVersion: 27, want: true}, + {name: "Xcode 27, always, failed", condition: always, testFailed: true, xcodeMajorVersion: 27, want: true}, + {name: "Xcode 27, project_setting, failed", condition: projectSetting, testFailed: true, xcodeMajorVersion: 27, want: true}, + {name: "Xcode 27, never, failed", condition: never, testFailed: true, xcodeMajorVersion: 27, want: false}, + {name: "Xcode 27, on_failure, passed", condition: onFailure, testFailed: false, xcodeMajorVersion: 27, want: false}, + // xcodebuild has no collection of its own before Xcode 26. + {name: "Xcode 16, on_failure, failed", condition: onFailure, testFailed: true, xcodeMajorVersion: 16, want: false}, + {name: "Xcode 16, project_setting, failed", condition: projectSetting, testFailed: true, xcodeMajorVersion: 16, want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := xcodebuildCollectsDiagnostics(tt.condition, tt.testFailed, tt.xcodeMajorVersion) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/step/step.go b/step/step.go index 2924cb60..af0dd1cd 100644 --- a/step/step.go +++ b/step/step.go @@ -20,6 +20,7 @@ import ( "github.com/bitrise-io/go-xcode/v2/simulator" cache "github.com/bitrise-io/go-xcode/v2/xcodecache" "github.com/bitrise-io/go-xcode/v2/xcodecommand" + "github.com/bitrise-io/go-xcode/v2/xcodeversion" "github.com/bitrise-steplib/steps-xcode-test/output" "github.com/bitrise-steplib/steps-xcode-test/xcodebuild" "github.com/kballard/go-shellquote" @@ -54,7 +55,7 @@ type Input struct { // Debugging VerboseLog bool `env:"verbose_log,opt[yes,no]"` QuarantinedTests string `env:"quarantined_tests"` - CollectSimulatorDiagnostics string `env:"collect_simulator_diagnostics,opt[always,on_failure,never]"` + CollectSimulatorDiagnostics string `env:"collect_simulator_diagnostics,opt[project_setting,always,on_failure,never]"` HeadlessMode bool `env:"headless_mode,opt[yes,no]"` // Output export @@ -64,9 +65,10 @@ type Input struct { type exportCondition string const ( - always = "always" - never = "never" - onFailure = "on_failure" + always = "always" + never = "never" + onFailure = "on_failure" + projectSetting = "project_setting" ) // Output tools @@ -97,9 +99,11 @@ type Config struct { CacheLevel string - SkipTesting []string - CollectSimulatorDiagnostics exportCondition - HeadlessMode bool + SkipTesting []string + CollectSimulatorDiagnostics exportCondition + XcodebuildDiagnosticsOverride string + HeadlessMode bool + XcodeMajorVersion int64 DeployDir string } @@ -110,15 +114,17 @@ type XcodeTestConfigParser struct { deviceFinder destination.DeviceFinder pathModifier pathutil.PathModifier utils Utils + xcodeVersion xcodeversion.Version } -func NewXcodeTestConfigParser(inputParser stepconf.InputParser, logger log.Logger, deviceFinder destination.DeviceFinder, pathModifier pathutil.PathModifier, utils Utils) XcodeTestConfigParser { +func NewXcodeTestConfigParser(inputParser stepconf.InputParser, logger log.Logger, deviceFinder destination.DeviceFinder, pathModifier pathutil.PathModifier, utils Utils, xcodeVersion xcodeversion.Version) XcodeTestConfigParser { return XcodeTestConfigParser{ logger: logger, inputParser: inputParser, deviceFinder: deviceFinder, pathModifier: pathModifier, utils: utils, + xcodeVersion: xcodeVersion, } } @@ -205,7 +211,7 @@ func (s XcodeTestConfigParser) ProcessConfig() (Config, error) { return Config{}, fmt.Errorf("failed to process quarentined tests: %w", err) } - return s.utils.CreateConfig(input, projectPath, sim, additionalOptions, additionalLogFormatterOptions, skipTesting), nil + return s.utils.CreateConfig(input, projectPath, sim, additionalOptions, additionalLogFormatterOptions, skipTesting, s.xcodeVersion.Major), nil } /* @@ -297,7 +303,7 @@ func (s XcodeTestRunner) Run(cfg Config) (Result, error) { testExitCode = code } - result.SimulatorDiagnosticsPath = s.teardownSimulator(cfg.Simulator.UDID, cfg.CollectSimulatorDiagnostics, cfg.IsSimulatorBooted, testErr) + result.SimulatorDiagnosticsPath = s.teardownSimulator(cfg.Simulator.UDID, cfg.CollectSimulatorDiagnostics, cfg.IsSimulatorBooted, testErr, cfg.XcodeMajorVersion) if testErr != nil { s.logger.Println() @@ -485,10 +491,10 @@ func (s XcodeTestRunner) runTests(cfg Config) (Result, int, error) { return result, exitCode, testErr } -func (s XcodeTestRunner) teardownSimulator(simulatorID string, simulatorDebug exportCondition, isSimulatorBooted bool, testErr error) string { +func (s XcodeTestRunner) teardownSimulator(simulatorID string, simulatorDebug exportCondition, isSimulatorBooted bool, testErr error, xcodeMajorVersion int64) string { var simulatorDiagnosticsPath string - if simulatorDebug == always || (simulatorDebug == onFailure && testErr != nil) { + if shouldStepCollectDiagnostics(simulatorDebug, testErr != nil, xcodeMajorVersion) { s.logger.Println() s.logger.Infof("Collecting Simulator diagnostics") @@ -499,6 +505,9 @@ func (s XcodeTestRunner) teardownSimulator(simulatorID string, simulatorDebug ex s.logger.Donef("Simulator diagnostics are available as an artifact (%s)", diagnosticsPath) simulatorDiagnosticsPath = diagnosticsPath } + } else if xcodebuildCollectsDiagnostics(simulatorDebug, testErr != nil, xcodeMajorVersion) { + s.logger.Println() + s.logger.Infof("xcodebuild collects the Simulator diagnostics into the xcresult (xcrun xcresulttool export diagnostics), the Step does not collect them separately") } // Shut down the simulator if it was started by the step for diagnostic logs. diff --git a/step/step_test.go b/step/step_test.go index 83190820..cc432124 100644 --- a/step/step_test.go +++ b/step/step_test.go @@ -9,6 +9,7 @@ import ( "github.com/bitrise-io/go-steputils/v2/stepconf" "github.com/bitrise-io/go-utils/v2/log" "github.com/bitrise-io/go-xcode/v2/destination" + "github.com/bitrise-io/go-xcode/v2/xcodeversion" commonMocks "github.com/bitrise-steplib/steps-xcode-test/mocks" "github.com/bitrise-steplib/steps-xcode-test/step/mocks" "github.com/hashicorp/go-version" @@ -156,6 +157,21 @@ func Test_GivenConfigParser_WhenParsesConfig(t *testing.T) { return config }, }, + { + name: "collect_simulator_diagnostics_project_setting", + envsFunc: func() map[string]string { + envValues := defaultEnvValues() + envValues["collect_simulator_diagnostics"] = "project_setting" + return envValues + }, + expectedConfig: func() Config { + config := defaultConfigs() + config.CollectSimulatorDiagnostics = projectSetting + // Nothing is passed to xcodebuild, the test plan decides. + config.XcodebuildDiagnosticsOverride = "" + return config + }, + }, { name: "skip_tests", envsFunc: func() map[string]string { @@ -381,8 +397,10 @@ func defaultConfigs() Config { CacheLevel: "swift_packages", - CollectSimulatorDiagnostics: never, - HeadlessMode: true, + CollectSimulatorDiagnostics: never, + XcodebuildDiagnosticsOverride: "never", + HeadlessMode: true, + XcodeMajorVersion: 27, } } func defaultSimulator() destination.Device { @@ -422,7 +440,10 @@ func createConfigParser(t *testing.T, envValues map[string]string) (XcodeTestCon pathModifier := mocks.NewPathModifier(t) utils := NewUtils(logger) - configParser := NewXcodeTestConfigParser(inputParser, logger, deviceFinder, pathModifier, utils) + // Xcode 27 so that the -collect-test-diagnostics mapping is exercised by the existing cases. + xcodeVersion := xcodeversion.Version{Version: "27.0", BuildVersion: "27A266a", Major: 27, Minor: 0} + + configParser := NewXcodeTestConfigParser(inputParser, logger, deviceFinder, pathModifier, utils, xcodeVersion) mocks := configParserMocks{ deviceFinder: deviceFinder, pathModifier: pathModifier, diff --git a/step/utils.go b/step/utils.go index e8023e37..8cf81748 100644 --- a/step/utils.go +++ b/step/utils.go @@ -12,7 +12,7 @@ import ( type Utils interface { PrintLastLinesOfXcodebuildTestLog(rawXcodebuildOutput string, isRunSuccess bool) - CreateConfig(input Input, projectPath string, sim destination.Device, additionalOptions, additionalLogFormatterOptions []string, skipTesting []string) Config + CreateConfig(input Input, projectPath string, sim destination.Device, additionalOptions, additionalLogFormatterOptions []string, skipTesting []string, xcodeMajorVersion int64) Config CreateTestParams(cfg Config, xcresultPath, swiftPackagesPath string) xcodebuild.TestRunParams } @@ -52,7 +52,8 @@ that will attach the file to your build as an artifact!`)) func (u utils) CreateConfig(input Input, projectPath string, sim destination.Device, - additionalOptions, additionalLogFormatterOptions []string, skipTesting []string) Config { + additionalOptions, additionalLogFormatterOptions []string, skipTesting []string, + xcodeMajorVersion int64) Config { return Config{ ProjectPath: projectPath, Scheme: input.Scheme, @@ -76,7 +77,10 @@ func (u utils) CreateConfig(input Input, SkipTesting: skipTesting, CollectSimulatorDiagnostics: exportCondition(input.CollectSimulatorDiagnostics), - HeadlessMode: input.HeadlessMode, + XcodebuildDiagnosticsOverride: xcodebuildDiagnosticsOverride( + exportCondition(input.CollectSimulatorDiagnostics), xcodeMajorVersion, additionalOptions), + HeadlessMode: input.HeadlessMode, + XcodeMajorVersion: xcodeMajorVersion, DeployDir: input.DeployDir, } @@ -95,6 +99,7 @@ func (u utils) CreateTestParams(cfg Config, xcresultPath, swiftPackagesPath stri XCConfigContent: cfg.XCConfigContent, PerformCleanAction: cfg.PerformCleanAction, SkipTesting: cfg.SkipTesting, + XcodebuildDiagnosticsOverride: cfg.XcodebuildDiagnosticsOverride, AdditionalOptions: cfg.XcodebuildOptions, } @@ -106,3 +111,40 @@ func (u utils) CreateTestParams(cfg Config, xcresultPath, swiftPackagesPath stri SwiftPackagesPath: swiftPackagesPath, } } + +// minimumXcodeMajorWithDiagnosticsOption is the first Xcode version that understands +// -collect-test-diagnostics and collects Simulator diagnostics itself after a failing test run. +const minimumXcodeMajorWithDiagnosticsOption = 26 + +func shouldStepCollectDiagnostics(condition exportCondition, testFailed bool, xcodeMajorVersion int64) bool { + if xcodeMajorVersion >= minimumXcodeMajorWithDiagnosticsOption { // xcodebuild collects + return false + } + + return condition == always || (condition == onFailure && testFailed) +} + +func xcodebuildCollectsDiagnostics(condition exportCondition, testFailed bool, xcodeMajorVersion int64) bool { + return xcodeMajorVersion >= minimumXcodeMajorWithDiagnosticsOption && condition != never && testFailed +} + +func xcodebuildDiagnosticsOverride(condition exportCondition, xcodeMajorVersion int64, additionalOptions []string) string { + if xcodeMajorVersion < minimumXcodeMajorWithDiagnosticsOption { // no such option yet + return "" + } + + for _, option := range additionalOptions { + if option == "-collect-test-diagnostics" { // user's option wins + return "" + } + } + + if condition == projectSetting { // leave it to the test plan + return "" + } + + if condition == never { + return "never" + } + return "on-failure" +} diff --git a/testaddon/utils.go b/testaddon/utils.go index 9dd24b0b..4e7d349f 100644 --- a/testaddon/utils.go +++ b/testaddon/utils.go @@ -30,8 +30,8 @@ func NewTestAddon(logger log.Logger) TestAddon { // ReplaceUnsupportedFilenameCharacters Replaces characters '/' and ':', which are unsupported in filnenames on macOS func (t testAddon) ReplaceUnsupportedFilenameCharacters(s string) string { - s = strings.Replace(s, "/", "-", -1) - s = strings.Replace(s, ":", "-", -1) + s = strings.ReplaceAll(s, "/", "-") + s = strings.ReplaceAll(s, ":", "-") return s } diff --git a/xcodebuild/utils.go b/xcodebuild/utils.go index 86998b32..dc30af15 100644 --- a/xcodebuild/utils.go +++ b/xcodebuild/utils.go @@ -53,6 +53,7 @@ type TestParams struct { XCConfigContent string PerformCleanAction bool SkipTesting []string + XcodebuildDiagnosticsOverride string AdditionalOptions []string } @@ -104,6 +105,11 @@ func (b *xcodebuild) createXcodebuildTestArgs(params TestParams) ([]string, erro xcodebuildArgs = append(xcodebuildArgs, fmt.Sprintf("-skip-testing:%s", test)) } + if params.XcodebuildDiagnosticsOverride != "" { + xcodebuildArgs = append(xcodebuildArgs, "-collect-test-diagnostics", params.XcodebuildDiagnosticsOverride) + } + + // Appended last so that anything the user passes in xcodebuild_options takes precedence. xcodebuildArgs = append(xcodebuildArgs, params.AdditionalOptions...) return xcodebuildArgs, nil diff --git a/xcodebuild/xcodebuild_test.go b/xcodebuild/xcodebuild_test.go index 6441dad8..150b588e 100644 --- a/xcodebuild/xcodebuild_test.go +++ b/xcodebuild/xcodebuild_test.go @@ -260,6 +260,7 @@ func runParameters() TestRunParams { RelaunchTestsForEachRepetition: true, XCConfigContent: "XCConfigContent", PerformCleanAction: false, + XcodebuildDiagnosticsOverride: "never", AdditionalOptions: []string{"AdditionalOptions"}, } @@ -316,6 +317,10 @@ func argumentsFromRunParameters(parameters TestRunParams) []string { arguments = append(arguments, fmt.Sprintf("-skip-testing:%s", test)) } + if parameters.TestParams.XcodebuildDiagnosticsOverride != "" { + arguments = append(arguments, "-collect-test-diagnostics", parameters.TestParams.XcodebuildDiagnosticsOverride) + } + arguments = append(arguments, parameters.TestParams.AdditionalOptions...) return arguments