From c2cbbbf1ea8c280074428b4c3b91ea8a2c99c337 Mon Sep 17 00:00:00 2001 From: dmnre Date: Wed, 2 Sep 2026 11:17:43 +0300 Subject: [PATCH 1/6] [patch] Added AutogeneratedSchemes and AutogeneratedTestPlan --- .../AutogeneratedSchemes.swift | 232 ++++++++++++ .../AutogeneratedTestPlan.swift | 346 ++++++++++++++++++ .../ProjectDescription/ProjectOptions.swift | 1 + .../WorkspaceGenerationOptions.swift | 21 +- 4 files changed, 597 insertions(+), 3 deletions(-) create mode 100644 Sources/ProjectDescription/AutogeneratedSchemes.swift create mode 100644 Sources/ProjectDescription/AutogeneratedTestPlan.swift diff --git a/Sources/ProjectDescription/AutogeneratedSchemes.swift b/Sources/ProjectDescription/AutogeneratedSchemes.swift new file mode 100644 index 0000000..3e20b03 --- /dev/null +++ b/Sources/ProjectDescription/AutogeneratedSchemes.swift @@ -0,0 +1,232 @@ +import Foundation + +@frozen +public enum AutogeneratedSchemes: Codable, Equatable { + public struct AutogeneratedSchemeWorkspace: Codable, Equatable { + /// Contains options for code coverage + @frozen + public enum CodeCoverageMode: Codable, Equatable { + /// Gather code coverage data for all targets in workspace. + case all + /// Enable code coverage for targets that have enabled code coverage in any of schemes in workspace. + case relevant + /// Gather code coverage for specified target references. + case targets([TargetReference]) + /// Do not gather code coverage data. + case disabled + } + + public let platform: Platform + public let productFilters: [Product] + public let onlyFocusedTargets: Bool + public let suffix: String? + public let testActionArguments: Arguments? + public let codeCoverageMode: CodeCoverageMode + public let testingOptions: TestingOptions + public let testLanguage: SchemeLanguage? + public let testRegion: String? + public let testScreenCaptureFormat: ScreenCaptureFormat? + + public init( + platform: Platform, + productFilters: [Product], + onlyFocusedTargets: Bool, + suffix: String?, + testActionArguments: Arguments?, + codeCoverageMode: CodeCoverageMode, + testingOptions: TestingOptions, + testLanguage: SchemeLanguage?, + testRegion: String?, + testScreenCaptureFormat: ScreenCaptureFormat? + ) { + self.platform = platform + self.productFilters = productFilters + self.onlyFocusedTargets = onlyFocusedTargets + self.testActionArguments = testActionArguments + self.suffix = suffix + self.codeCoverageMode = codeCoverageMode + self.testingOptions = testingOptions + self.testLanguage = testLanguage + self.testRegion = testRegion + self.testScreenCaptureFormat = testScreenCaptureFormat + } + } + + public struct AutogeneratedSchemeProject: Codable, Equatable { + @frozen + public enum TargetSchemesGrouping: Codable, Equatable { + /// Generate a single scheme for each project. + case singleScheme + + /// Group schemes according to the suffix of their names. + case byNameSuffix(build: Set, test: Set, run: Set) + + /// Group the targets by local pods. 1 build+test (required), n app (optional) + case pods + + /// Generate a scheme for each target. + case notGrouped + } + + public let onlyFocusedTargets: Bool + public let suffix: String? + public let exceptRegexps: [String] + public let testActionArguments: Arguments? + public let runActionArguments: Arguments? + public let targetSchemesGrouping: TargetSchemesGrouping + public let codeCoverageEnabled: Bool + public let testingOptions: TestingOptions + public let testLanguage: SchemeLanguage? + public let testRegion: String? + public let testScreenCaptureFormat: ScreenCaptureFormat? + public let runLanguage: SchemeLanguage? + public let runRegion: String? + public let testPlans: [String] + + public init( + onlyFocusedTargets: Bool, + suffix: String?, + exceptRegexps: [String], + testActionArguments: Arguments?, + runActionArguments: Arguments?, + targetSchemesGrouping: TargetSchemesGrouping, + codeCoverageEnabled: Bool, + testingOptions: TestingOptions, + testLanguage: SchemeLanguage?, + testRegion: String?, + testScreenCaptureFormat: ScreenCaptureFormat?, + runLanguage: SchemeLanguage?, + runRegion: String?, + testPlans: [String] + ) { + self.onlyFocusedTargets = onlyFocusedTargets + self.suffix = suffix + self.exceptRegexps = exceptRegexps + self.testActionArguments = testActionArguments + self.runActionArguments = runActionArguments + self.targetSchemesGrouping = targetSchemesGrouping + self.codeCoverageEnabled = codeCoverageEnabled + self.testingOptions = testingOptions + self.testLanguage = testLanguage + self.testRegion = testRegion + self.testScreenCaptureFormat = testScreenCaptureFormat + self.runLanguage = runLanguage + self.runRegion = runRegion + self.testPlans = testPlans + } + } + + case workspace(AutogeneratedSchemeWorkspace) + case allProjects(AutogeneratedSchemeProject) + case project(projectName: String, AutogeneratedSchemeProject) + + public static func workspace( + platform: Platform, + productFilters: [Product] = [], + onlyFocusedTargets: Bool = true, + suffix: String? = nil, + testActionArguments: Arguments? = nil, + codeCoverageMode: AutogeneratedSchemeWorkspace.CodeCoverageMode = .disabled, + testingOptions: TestingOptions = [], + testLanguage: SchemeLanguage? = nil, + testRegion: String? = nil, + testScreenCaptureFormat: ScreenCaptureFormat? = nil + ) -> AutogeneratedSchemes { + .workspace( + AutogeneratedSchemeWorkspace( + platform: platform, + productFilters: productFilters, + onlyFocusedTargets: onlyFocusedTargets, + suffix: suffix, + testActionArguments: testActionArguments, + codeCoverageMode: codeCoverageMode, + testingOptions: testingOptions, + testLanguage: testLanguage, + testRegion: testRegion, + testScreenCaptureFormat: testScreenCaptureFormat + ) + ) + } + + public static func allProjects( + onlyFocusedTargets: Bool = true, + suffix: String? = nil, + exceptRegexps: [String] = [], + testActionArguments: Arguments? = nil, + runActionArguments: Arguments? = nil, + targetSchemesGrouping: AutogeneratedSchemeProject.TargetSchemesGrouping = .byNameSuffix( + build: ["Implementation", "Interface", "Mocks", "Testing"], + test: ["Tests", "IntegrationTests", "UITests", "SnapshotTests"], + run: ["App", "Demo", "Example"] + ), + codeCoverageEnabled: Bool = false, + testingOptions: TestingOptions = [], + testLanguage: SchemeLanguage? = nil, + testRegion: String? = nil, + testScreenCaptureFormat: ScreenCaptureFormat? = nil, + runLanguage: SchemeLanguage? = nil, + runRegion: String? = nil, + testPlans: [String] = [] + ) -> AutogeneratedSchemes { + .allProjects( + AutogeneratedSchemeProject( + onlyFocusedTargets: onlyFocusedTargets, + suffix: suffix, + exceptRegexps: exceptRegexps, + testActionArguments: testActionArguments, + runActionArguments: runActionArguments, + targetSchemesGrouping: targetSchemesGrouping, + codeCoverageEnabled: codeCoverageEnabled, + testingOptions: testingOptions, + testLanguage: testLanguage, + testRegion: testRegion, + testScreenCaptureFormat: testScreenCaptureFormat, + runLanguage: runLanguage, + runRegion: runRegion, + testPlans: testPlans + ) + ) + } + + public static func project( + name: String, + onlyFocusedTargets: Bool = true, + suffix: String? = nil, + exceptRegexps: [String] = [], + testActionArguments: Arguments? = nil, + runActionArguments: Arguments? = nil, + targetSchemesGrouping: AutogeneratedSchemeProject.TargetSchemesGrouping = .byNameSuffix( + build: ["Implementation", "Interface", "Mocks", "Testing"], + test: ["Tests", "IntegrationTests", "UITests", "SnapshotTests"], + run: ["App", "Demo", "Example"] + ), + codeCoverageEnabled: Bool = false, + testingOptions: TestingOptions = [], + testLanguage: SchemeLanguage? = nil, + testRegion: String? = nil, + testScreenCaptureFormat: ScreenCaptureFormat? = nil, + runLanguage: SchemeLanguage? = nil, + runRegion: String? = nil, + testPlans: [String] = [] + ) -> AutogeneratedSchemes { + .project( + projectName: name, + AutogeneratedSchemeProject( + onlyFocusedTargets: onlyFocusedTargets, + suffix: suffix, + exceptRegexps: exceptRegexps, + testActionArguments: testActionArguments, + runActionArguments: runActionArguments, + targetSchemesGrouping: targetSchemesGrouping, + codeCoverageEnabled: codeCoverageEnabled, + testingOptions: testingOptions, + testLanguage: testLanguage, + testRegion: testRegion, + testScreenCaptureFormat: testScreenCaptureFormat, + runLanguage: runLanguage, + runRegion: runRegion, + testPlans: testPlans + ) + ) + } +} diff --git a/Sources/ProjectDescription/AutogeneratedTestPlan.swift b/Sources/ProjectDescription/AutogeneratedTestPlan.swift new file mode 100644 index 0000000..5cf0c15 --- /dev/null +++ b/Sources/ProjectDescription/AutogeneratedTestPlan.swift @@ -0,0 +1,346 @@ +import Foundation + +public struct AutogeneratedTestPlan: Codable, Equatable, Sendable { + public struct Options: Codable, Equatable, Sendable { + /// Arguments Passed On Launch + public struct CommandLineArgumentEntry: Codable, Equatable, Sendable { + public let argument: String + public let enabled: Bool? + + public init(argument: String, enabled: Bool? = nil) { + self.argument = argument + self.enabled = enabled + } + } + + /// Environment Variables + public struct VariableEntity: Codable, Equatable, Sendable { + public let enabled: Bool? + public let key: String + public let value: String + + public init(key: String, value: String, enabled: Bool? = nil) { + self.key = key + self.value = value + self.enabled = enabled + } + } + + /// Simulated Location + public struct LocationScenario: Codable, Equatable, Sendable { + public let identifier: String + public let referenceType: String + + public init(identifier: String, referenceType: String) { + self.identifier = identifier + self.referenceType = referenceType + } + } + + /// Automatic Screen Capture + public enum UITestingScreenshotsLifetime: String, Codable, Equatable, Sendable { + /// On, and keep all + case keepAlways + /// On, and delete if test succeeds (default) + case deleteOnSuccess + /// Off + case keepNever + } + + /// Preferred Capture Format + public enum PreferredScreenCaptureFormat: String, Codable, Equatable, Sendable { + /// Video (default) + case video + /// Screenshot + case screenshot + } + + /// Distribution + public enum Distributor: String, Codable, Equatable, Sendable { + case appStore = "com.apple.AppStore" + case testFlight = "com.apple.TestFlight" + } + + /// Attachments + public enum UserAttachmentLifetime: String, Codable, Equatable, Sendable { + /// On, and keep all + case keepAlways + /// On, and delete if test succeeds (default) + case deleteOnSuccess + /// Off + case keepNever + } + + /// Collect Test Diagnostics on Failure + public enum DiagnosticCollectionPolicy: String, Codable, Equatable, Sendable { + /// Never + case never = "Never" + /// When testing with xcodebuild (default) + case xcodebuild + /// Always + case always = "Always" + } + + /// XCTest Execution Order + public enum TestExecutionOrdering: String, Codable, Equatable, Sendable { + /// Alphabetical (default) + case alphabetical + /// Random + case random + } + + /// Test Repetition Mode + public enum TestRepetitionMode: String, Codable, Equatable, Sendable { + /// Until Failure + case untilFailure + /// Retry On Failure + case retryOnFailure + /// Up Until Maximum Repetitions + case fixedIterations + /// None (default) + case none + } + + /// Address Sanitizer + public struct AddressSanitizer: Codable, Equatable, Sendable { + public let detectStackUseAfterReturn: Bool? + public let enabled: Bool + + public init(detectStackUseAfterReturn: Bool? = nil, enabled: Bool = true) { + self.detectStackUseAfterReturn = detectStackUseAfterReturn + self.enabled = enabled + } + + /// On + public static func on() -> Self { Self(enabled: true) } + /// On, and detect stack use after return + public static func onAndDetectStackUseAfterReturn() -> Self { Self(detectStackUseAfterReturn: true, enabled: true) } + } + + public struct RuntimeApiChecking: Codable, Equatable, Sendable { + public enum Severity: String, Codable, Sendable { + case warning + case failure + } + + public let severity: Severity? + public let enabled: Bool? + + public init(severity: Severity?, enabled: Bool?) { + self.severity = severity + self.enabled = enabled + } + + /// On (as Warning) (default) + public static func onAsWarning() -> Self { Self(severity: .warning, enabled: nil) } + /// On (as Failure) + public static func onAsFailure() -> Self { Self(severity: .failure, enabled: nil) } + /// Off + public static func off() -> Self { Self(severity: nil, enabled: false) } + } + + /// Hardware Memory Tagging + public struct CheckedAllocations: Codable, Equatable, Sendable { + public let enabled: Bool + public let requiresHardwareAcceleration: Bool + + public init(enabled: Bool = true, requiresHardwareAcceleration: Bool) { + self.enabled = enabled + self.requiresHardwareAcceleration = requiresHardwareAcceleration + } + + /// Off + public static func off() -> Self { Self(enabled: false, requiresHardwareAcceleration: true) } + /// On (when supported) + public static func onWhenSupported() -> Self { Self(enabled: true, requiresHardwareAcceleration: true) } + /// On (when supported), fall back to Guard Malloc otherwise + public static func onWhenSupportedFallBackToGuardMalloc() -> Self { Self(enabled: true, requiresHardwareAcceleration: false) } + } + + /// Malloc Stack Logging + @frozen + public enum MallocStackLoggingOptions: Codable, Equatable, Sendable { + /// On + case on + /// Off (default) + case off + } + + /// Arguments Passed On Launch + public let commandLineArgumentEntries: [CommandLineArgumentEntry]? + /// Environment Variables + public let environmentVariableEntries: [VariableEntity]? + /// Target for Variable Expansion + public let targetForVariableExpansion: String? + /// Application Language + public let language: String? + /// Application Region + public let region: String? + /// Simulated Location + public let locationScenario: LocationScenario? + /// Automatic Screen Capture + public let uiTestingScreenshotsLifetime: UITestingScreenshotsLifetime? + /// Preferred Capture Format + public let preferredScreenCaptureFormat: PreferredScreenCaptureFormat? + /// Localization Screenshots + public let areLocalizationScreenshotsEnabled: Bool? + /// Distribution + public let distributor: Distributor? + /// Attachments + public let userAttachmentLifetime: UserAttachmentLifetime? + /// Collect Test Diagnostics on Failure + public let diagnosticCollectionPolicy: DiagnosticCollectionPolicy? + /// XCTest Execution Order + public let testExecutionOrdering: TestExecutionOrdering? + /// Test Timeouts + public let testTimeoutsEnabled: Bool? + /// Default Test Execution Time Allowance (s) (default: 600) + public let defaultTestExecutionTimeAllowance: Int? + /// Maximum Test Execution Time Allowance (s) (default: None) + public let maximumTestExecutionTimeAllowance: Int? + /// Test Repetition Mode + public let testRepetitionMode: TestRepetitionMode? + /// Maximum Test Repetitions (default: 3) + public let maximumTestRepetitions: Int? + /// Relaunch Tests for Each Repetition + public let repeatInNewRunnerProcess: Bool? + /// Code Coverage + public let codeCoverage: Bool? + /// Address Sanitizer + public let addressSanitizer: AddressSanitizer? + /// Thread Sanitizer + public let threadSanitizerEnabled: Bool? + /// Undefined Behavior Sanitizer + public let undefinedBehaviorSanitizerEnabled: Bool? + /// Main Thread Checker + public let mainThreadCheckerDetectionPolicy: RuntimeApiChecking? + /// Thread Performance Checker + public let threadPerformanceCheckerRuntimeIssueDetection: RuntimeApiChecking? + /// Other Runtime Issues + public let runtimeIssueDetection: RuntimeApiChecking? + /// Hardware Memory Tagging + public let checkedAllocations: CheckedAllocations? + /// Malloc Scribble + public let mallocScribbleEnabled: Bool? + /// Malloc Guard Edges + public let mallocGuardEdgesEnabled: Bool? + /// Zombie Objects + public let nsZombieEnabled: Bool? + /// Malloc Stack Logging + public let mallocStackLoggingOptions: MallocStackLoggingOptions? + + public init( + commandLineArgumentEntries: [CommandLineArgumentEntry]? = nil, + environmentVariableEntries: [VariableEntity]? = nil, + targetForVariableExpansion: String? = nil, + language: String? = nil, + region: String? = nil, + locationScenario: LocationScenario? = nil, + uiTestingScreenshotsLifetime: UITestingScreenshotsLifetime? = nil, + preferredScreenCaptureFormat: PreferredScreenCaptureFormat? = nil, + areLocalizationScreenshotsEnabled: Bool? = nil, + distributor: Distributor? = nil, + userAttachmentLifetime: UserAttachmentLifetime? = nil, + diagnosticCollectionPolicy: DiagnosticCollectionPolicy? = nil, + testExecutionOrdering: TestExecutionOrdering? = nil, + testTimeoutsEnabled: Bool? = nil, + defaultTestExecutionTimeAllowance: Int? = nil, + maximumTestExecutionTimeAllowance: Int? = nil, + testRepetitionMode: TestRepetitionMode? = nil, + maximumTestRepetitions: Int? = nil, + repeatInNewRunnerProcess: Bool? = nil, + codeCoverage: Bool? = nil, + addressSanitizer: AddressSanitizer? = nil, + threadSanitizerEnabled: Bool? = nil, + undefinedBehaviorSanitizerEnabled: Bool? = nil, + mainThreadCheckerDetectionPolicy: RuntimeApiChecking? = nil, + threadPerformanceCheckerRuntimeIssueDetection: RuntimeApiChecking? = nil, + runtimeIssueDetection: RuntimeApiChecking? = nil, + checkedAllocations: CheckedAllocations? = nil, + mallocScribbleEnabled: Bool? = nil, + mallocGuardEdgesEnabled: Bool? = nil, + nsZombieEnabled: Bool? = nil, + mallocStackLoggingOptions: MallocStackLoggingOptions? = nil, + ) { + self.commandLineArgumentEntries = commandLineArgumentEntries + self.environmentVariableEntries = environmentVariableEntries + self.targetForVariableExpansion = targetForVariableExpansion + self.language = language + self.region = region + self.locationScenario = locationScenario + self.uiTestingScreenshotsLifetime = uiTestingScreenshotsLifetime + self.preferredScreenCaptureFormat = preferredScreenCaptureFormat + self.areLocalizationScreenshotsEnabled = areLocalizationScreenshotsEnabled + self.distributor = distributor + self.userAttachmentLifetime = userAttachmentLifetime + self.diagnosticCollectionPolicy = diagnosticCollectionPolicy + self.testExecutionOrdering = testExecutionOrdering + self.testTimeoutsEnabled = testTimeoutsEnabled + self.defaultTestExecutionTimeAllowance = defaultTestExecutionTimeAllowance + self.maximumTestExecutionTimeAllowance = maximumTestExecutionTimeAllowance + self.testRepetitionMode = testRepetitionMode + self.maximumTestRepetitions = maximumTestRepetitions + self.repeatInNewRunnerProcess = repeatInNewRunnerProcess + self.codeCoverage = codeCoverage + self.addressSanitizer = addressSanitizer + self.threadSanitizerEnabled = threadSanitizerEnabled + self.undefinedBehaviorSanitizerEnabled = undefinedBehaviorSanitizerEnabled + self.mainThreadCheckerDetectionPolicy = mainThreadCheckerDetectionPolicy + self.threadPerformanceCheckerRuntimeIssueDetection = threadPerformanceCheckerRuntimeIssueDetection + self.runtimeIssueDetection = runtimeIssueDetection + self.checkedAllocations = checkedAllocations + self.mallocScribbleEnabled = mallocScribbleEnabled + self.mallocGuardEdgesEnabled = mallocGuardEdgesEnabled + self.nsZombieEnabled = nsZombieEnabled + self.mallocStackLoggingOptions = mallocStackLoggingOptions + } + } + + public struct SchemeReference: Codable, Equatable, Sendable { + /// Project name + public let project: String + /// Scheme name + public let scheme: String + + public init(project: String, scheme: String) { + self.project = project + self.scheme = scheme + } + } + + public enum TestTargets: Codable, Equatable, Sendable { + /// Add all available targets. When using the --cache parameter when generating a project, only the currently focused targets will be added. + case all + /// Add only the listed targets. + case testableTargets([TestableTarget]) + } + + /// Name of the test plan + public let name: String + /// Scheme to which the generated test plan should be added + public let scheme: SchemeReference? + /// The path to the folder where the test plan will be generated. If the path is `nil`, it will be written to `Derived/TestPlans/.xctestplan` + public let path: AbsolutePath? + /// Configuration options + public let configurationOptions: Options? + /// Default options + public let defaultOptions: Options? + /// Test targets to add to the generated plan. + public let testTargets: TestTargets + + public init( + name: String, + scheme: SchemeReference? = nil, + path: AbsolutePath? = nil, + configurationOptions: Options? = nil, + defaultOptions: Options? = nil, + testTargets: TestTargets = .all + ) { + self.name = name + self.scheme = scheme + self.path = path + self.configurationOptions = configurationOptions + self.defaultOptions = defaultOptions + self.testTargets = testTargets + } +} diff --git a/Sources/ProjectDescription/ProjectOptions.swift b/Sources/ProjectDescription/ProjectOptions.swift index 2f6fbe0..1807ee3 100644 --- a/Sources/ProjectDescription/ProjectOptions.swift +++ b/Sources/ProjectDescription/ProjectOptions.swift @@ -4,6 +4,7 @@ extension Project { /// Options to configure a project. public struct Options: Codable, Equatable { /// Configures automatic target schemes generation. + @available(*, deprecated, message: "Use Workspace.autogeneratedSchemes instead.") public var automaticSchemesOptions: AutomaticSchemesOptions /// Configures the default known regions diff --git a/Sources/ProjectDescription/WorkspaceGenerationOptions.swift b/Sources/ProjectDescription/WorkspaceGenerationOptions.swift index 24ff9c6..d645889 100644 --- a/Sources/ProjectDescription/WorkspaceGenerationOptions.swift +++ b/Sources/ProjectDescription/WorkspaceGenerationOptions.swift @@ -98,6 +98,7 @@ extension Workspace { /// Enable or disable automatic generation of `Workspace` schemes. If enabled, options to configure code coverage and test /// targets can be passed in via associated values. + @available(*, deprecated, message: "Use Workspace.autogeneratedSchemes instead.") public var autogeneratedWorkspaceSchemes: AutogeneratedWorkspaceSchemes /// Allows to suppress warnings in Xcode about updates to recommended settings added in or below the specified Xcode @@ -124,6 +125,10 @@ extension Workspace { /// Suppress objc class duplication warnings when loading plugins. public var suppressObjcDuplicateClassWarningsDuringPluginLoading: Bool + public var autogeneratedSchemes: [AutogeneratedSchemes] + + public var autogeneratedTestPlans: [AutogeneratedTestPlan] + public init( enableAutomaticXcodeSchemes: Bool?, autogeneratedWorkspaceSchemes: AutogeneratedWorkspaceSchemes, @@ -134,7 +139,11 @@ extension Workspace { commonSettings: [CommonSettings], configurations: [String: BuildConfiguration.Variant], // TODO: Remove default value in next major release - suppressObjcDuplicateClassWarningsDuringPluginLoading: Bool = true + suppressObjcDuplicateClassWarningsDuringPluginLoading: Bool = true, + // TODO: Remove default value in next major release + autogeneratedSchemes: [AutogeneratedSchemes] = [], + // TODO: Remove default value in next major release + autogeneratedTestPlans: [AutogeneratedTestPlan] = [], ) { self.enableAutomaticXcodeSchemes = enableAutomaticXcodeSchemes self.autogeneratedWorkspaceSchemes = autogeneratedWorkspaceSchemes @@ -145,6 +154,8 @@ extension Workspace { self.commonSettings = commonSettings self.configurations = configurations self.suppressObjcDuplicateClassWarningsDuringPluginLoading = suppressObjcDuplicateClassWarningsDuringPluginLoading + self.autogeneratedSchemes = autogeneratedSchemes + self.autogeneratedTestPlans = autogeneratedTestPlans } public static func options( @@ -156,7 +167,9 @@ extension Workspace { autogenerateLocalPodsSchemes: AutogenerateLocalPodsSchemes = .disabled, commonSettings: [CommonSettings] = [], configurations: [String: BuildConfiguration.Variant] = ["Debug": .debug, "Release": .release], - suppressObjcDuplicateClassWarningsDuringPluginLoading: Bool = true + suppressObjcDuplicateClassWarningsDuringPluginLoading: Bool = true, + autogeneratedSchemes: [AutogeneratedSchemes] = [], + autogeneratedTestPlans: [AutogeneratedTestPlan] = [], ) -> Self { GenerationOptions( enableAutomaticXcodeSchemes: enableAutomaticXcodeSchemes, @@ -167,7 +180,9 @@ extension Workspace { autogenerateLocalPodsSchemes: autogenerateLocalPodsSchemes, commonSettings: commonSettings, configurations: configurations, - suppressObjcDuplicateClassWarningsDuringPluginLoading: suppressObjcDuplicateClassWarningsDuringPluginLoading + suppressObjcDuplicateClassWarningsDuringPluginLoading: suppressObjcDuplicateClassWarningsDuringPluginLoading, + autogeneratedSchemes: autogeneratedSchemes, + autogeneratedTestPlans: autogeneratedTestPlans ) } } From ba08392291bc3bfcda85cad780c8f4434978066d Mon Sep 17 00:00:00 2001 From: dmnre Date: Thu, 10 Sep 2026 11:04:33 +0300 Subject: [PATCH 2/6] pull request fixes --- .../AutogeneratedTestPlan.swift | 152 ++++++++++++++++-- 1 file changed, 141 insertions(+), 11 deletions(-) diff --git a/Sources/ProjectDescription/AutogeneratedTestPlan.swift b/Sources/ProjectDescription/AutogeneratedTestPlan.swift index 5cf0c15..71ae5ce 100644 --- a/Sources/ProjectDescription/AutogeneratedTestPlan.swift +++ b/Sources/ProjectDescription/AutogeneratedTestPlan.swift @@ -1,5 +1,71 @@ import Foundation +/// A testable target entry used for auto-generated test plans in Geko. +/// +/// Wraps a ``TestableTarget`` with optional test filtering (selected/skipped tests). +/// Conforms to `ExpressibleByStringInterpolation`, so you can use a plain string +/// wherever an `AutogeneratedTestableTarget` is expected: +/// +/// ```swift +/// let targets: [AutogeneratedTestableTarget] = [ +/// "MyFeatureTests", +/// .target("MyOtherTests", skippedTests: ["MyOtherTests/testFlaky"]), +/// ] +/// ``` +public struct AutogeneratedTestableTarget: Codable, Equatable, Sendable, ExpressibleByStringInterpolation { + /// The underlying testable target reference that identifies + /// which target should be included in the generated test plan. + public let target: TestableTarget + /// A list of specific tests to include in the plan. + /// + /// Each entry uses the Xcode test identifier format, e.g. + /// `"MyTests/testExample"` or `"MyTests"` for an entire suite. + /// When non-empty, only these tests will be executed for this target. + /// An empty array means all tests are included (subject to `skippedTests`). + public let selectedTests: [String] + /// A list of specific tests to exclude from the plan. + /// + /// Each entry uses the Xcode test identifier format, e.g. + /// `"MyTests/testExample"` or `"MyTests"` for an entire suite. + /// These tests will be skipped even if they would otherwise run. + /// An empty array means no tests are skipped. + public let skippedTests: [String] + + public init( + _ target: TestableTarget, + selectedTests: [String] = [], + skippedTests: [String] = [], + ) { + self.target = target + self.selectedTests = selectedTests + self.skippedTests = skippedTests + } + + public init(stringLiteral value: String) { + self.init(TestableTarget(stringLiteral: value)) + } + + /// Creates an ``AutogeneratedTestableTarget`` with optional test filtering. + /// + /// This factory method provides a more readable call-site syntax: + /// ```swift + /// .target("MyTests", selectedTests: ["MyTests/testImportant"]) + /// ``` + /// + /// - Parameters: + /// - target: The testable target to include. + /// - selectedTests: Tests to run exclusively. Empty means run all. + /// - skippedTests: Tests to skip. Empty means skip none. + /// - Returns: A configured ``AutogeneratedTestableTarget``. + public static func target( + _ target: TestableTarget, + selectedTests: [String] = [], + skippedTests: [String] = [], + ) -> Self { + .init(target, selectedTests: selectedTests, skippedTests: skippedTests) + } +} + public struct AutogeneratedTestPlan: Codable, Equatable, Sendable { public struct Options: Codable, Equatable, Sendable { /// Arguments Passed On Launch @@ -308,11 +374,51 @@ public struct AutogeneratedTestPlan: Codable, Equatable, Sendable { } } - public enum TestTargets: Codable, Equatable, Sendable { - /// Add all available targets. When using the --cache parameter when generating a project, only the currently focused targets will be added. + /// Defines which targets should be included for testing. + @frozen + public enum TestTargetSelection: Codable, Equatable, Sendable { + /// Include all available targets. + /// + /// When the `--cache` parameter is used during project generation, only the currently focused targets will be added. If the list of focused targets is empty, all test targets will be added. case all - /// Add only the listed targets. - case testableTargets([TestableTarget]) + + /// Include only the explicitly listed targets. + /// + /// - Parameters: + /// - targets: The specific targets to include. + case specific(_ targets: [AutogeneratedTestableTarget]) + } + + @frozen + public enum MissingTargetPolicy: Codable, Equatable, Sendable { + + /// Determines whether to silently ignore or emit a warning when a target is missing. + @frozen + public enum Notification: Codable, Equatable, Sendable { + /// Silently ignore — no output, no warnings. + case silent + /// Emit a warning to inform the user. + case warning + } + + /// Do not generate the test plan at all. + /// + /// - Parameter notification: Whether to silently skip or emit a warning. + /// + /// Use when a missing target makes the whole test plan meaningless. + case skipTestPlan(Notification = .warning) + + /// Skip only the missing target from the test plan. + /// + /// - Parameter notification: Whether to silently skip or emit a warning. + /// + /// Other valid targets in the plan will still be included. + case skipTarget(Notification = .warning) + + /// Fail the entire generation with an error. + /// + /// Use when all specified targets are strictly required. + case fail } /// Name of the test plan @@ -326,21 +432,45 @@ public struct AutogeneratedTestPlan: Codable, Equatable, Sendable { /// Default options public let defaultOptions: Options? /// Test targets to add to the generated plan. - public let testTargets: TestTargets + public let testTargetSelection: TestTargetSelection + /// Policy for handling missing targets. + public let missingTargetPolicy: MissingTargetPolicy public init( name: String, - scheme: SchemeReference? = nil, - path: AbsolutePath? = nil, - configurationOptions: Options? = nil, - defaultOptions: Options? = nil, - testTargets: TestTargets = .all + scheme: SchemeReference?, + path: AbsolutePath?, + configurationOptions: Options?, + defaultOptions: Options?, + testTargetSelection: TestTargetSelection, + missingTargetPolicy: MissingTargetPolicy ) { self.name = name self.scheme = scheme self.path = path self.configurationOptions = configurationOptions self.defaultOptions = defaultOptions - self.testTargets = testTargets + self.testTargetSelection = testTargetSelection + self.missingTargetPolicy = missingTargetPolicy + } + + public static func testPlan( + name: String, + scheme: SchemeReference? = nil, + path: AbsolutePath? = nil, + configurationOptions: Options? = nil, + defaultOptions: Options? = nil, + testTargetSelection: TestTargetSelection = .all, + missingTargetPolicy: MissingTargetPolicy = .skipTestPlan() + ) -> Self { + Self( + name: name, + scheme: scheme, + path: path, + configurationOptions: configurationOptions, + defaultOptions: defaultOptions, + testTargetSelection: testTargetSelection, + missingTargetPolicy: missingTargetPolicy + ) } } From fadd6e5878e6c02a8a0781778e1a727e93d9874c Mon Sep 17 00:00:00 2001 From: dmnre Date: Wed, 7 Oct 2026 15:50:25 +0300 Subject: [PATCH 3/6] The public interface has been redesigned and TargetSelectionScope has been added --- .../AutogeneratedSchemes.swift | 232 ------------ Sources/ProjectDescription/BuildAction.swift | 10 + ...TestPlan.swift => GeneratedTestPlan.swift} | 329 ++++++++++-------- .../ProjectDescription/ProjectOptions.swift | 1 - .../TargetSelectionScope.swift | 25 ++ Sources/ProjectDescription/TestAction.swift | 35 +- .../TestActionOptions.swift | 24 ++ .../TestActionTestPlan.swift | 41 +++ Sources/ProjectDescription/TestPlan.swift | 140 +++++++- .../ProjectDescription/TestableTarget.swift | 18 +- .../TestableTargetSelectionScope.swift | 29 ++ .../ProjectDescription/TestingOptions.swift | 2 + .../WorkspaceGenerationOptions.swift | 16 +- 13 files changed, 506 insertions(+), 396 deletions(-) delete mode 100644 Sources/ProjectDescription/AutogeneratedSchemes.swift rename Sources/ProjectDescription/{AutogeneratedTestPlan.swift => GeneratedTestPlan.swift} (66%) create mode 100644 Sources/ProjectDescription/TargetSelectionScope.swift create mode 100644 Sources/ProjectDescription/TestActionTestPlan.swift create mode 100644 Sources/ProjectDescription/TestableTargetSelectionScope.swift diff --git a/Sources/ProjectDescription/AutogeneratedSchemes.swift b/Sources/ProjectDescription/AutogeneratedSchemes.swift deleted file mode 100644 index 3e20b03..0000000 --- a/Sources/ProjectDescription/AutogeneratedSchemes.swift +++ /dev/null @@ -1,232 +0,0 @@ -import Foundation - -@frozen -public enum AutogeneratedSchemes: Codable, Equatable { - public struct AutogeneratedSchemeWorkspace: Codable, Equatable { - /// Contains options for code coverage - @frozen - public enum CodeCoverageMode: Codable, Equatable { - /// Gather code coverage data for all targets in workspace. - case all - /// Enable code coverage for targets that have enabled code coverage in any of schemes in workspace. - case relevant - /// Gather code coverage for specified target references. - case targets([TargetReference]) - /// Do not gather code coverage data. - case disabled - } - - public let platform: Platform - public let productFilters: [Product] - public let onlyFocusedTargets: Bool - public let suffix: String? - public let testActionArguments: Arguments? - public let codeCoverageMode: CodeCoverageMode - public let testingOptions: TestingOptions - public let testLanguage: SchemeLanguage? - public let testRegion: String? - public let testScreenCaptureFormat: ScreenCaptureFormat? - - public init( - platform: Platform, - productFilters: [Product], - onlyFocusedTargets: Bool, - suffix: String?, - testActionArguments: Arguments?, - codeCoverageMode: CodeCoverageMode, - testingOptions: TestingOptions, - testLanguage: SchemeLanguage?, - testRegion: String?, - testScreenCaptureFormat: ScreenCaptureFormat? - ) { - self.platform = platform - self.productFilters = productFilters - self.onlyFocusedTargets = onlyFocusedTargets - self.testActionArguments = testActionArguments - self.suffix = suffix - self.codeCoverageMode = codeCoverageMode - self.testingOptions = testingOptions - self.testLanguage = testLanguage - self.testRegion = testRegion - self.testScreenCaptureFormat = testScreenCaptureFormat - } - } - - public struct AutogeneratedSchemeProject: Codable, Equatable { - @frozen - public enum TargetSchemesGrouping: Codable, Equatable { - /// Generate a single scheme for each project. - case singleScheme - - /// Group schemes according to the suffix of their names. - case byNameSuffix(build: Set, test: Set, run: Set) - - /// Group the targets by local pods. 1 build+test (required), n app (optional) - case pods - - /// Generate a scheme for each target. - case notGrouped - } - - public let onlyFocusedTargets: Bool - public let suffix: String? - public let exceptRegexps: [String] - public let testActionArguments: Arguments? - public let runActionArguments: Arguments? - public let targetSchemesGrouping: TargetSchemesGrouping - public let codeCoverageEnabled: Bool - public let testingOptions: TestingOptions - public let testLanguage: SchemeLanguage? - public let testRegion: String? - public let testScreenCaptureFormat: ScreenCaptureFormat? - public let runLanguage: SchemeLanguage? - public let runRegion: String? - public let testPlans: [String] - - public init( - onlyFocusedTargets: Bool, - suffix: String?, - exceptRegexps: [String], - testActionArguments: Arguments?, - runActionArguments: Arguments?, - targetSchemesGrouping: TargetSchemesGrouping, - codeCoverageEnabled: Bool, - testingOptions: TestingOptions, - testLanguage: SchemeLanguage?, - testRegion: String?, - testScreenCaptureFormat: ScreenCaptureFormat?, - runLanguage: SchemeLanguage?, - runRegion: String?, - testPlans: [String] - ) { - self.onlyFocusedTargets = onlyFocusedTargets - self.suffix = suffix - self.exceptRegexps = exceptRegexps - self.testActionArguments = testActionArguments - self.runActionArguments = runActionArguments - self.targetSchemesGrouping = targetSchemesGrouping - self.codeCoverageEnabled = codeCoverageEnabled - self.testingOptions = testingOptions - self.testLanguage = testLanguage - self.testRegion = testRegion - self.testScreenCaptureFormat = testScreenCaptureFormat - self.runLanguage = runLanguage - self.runRegion = runRegion - self.testPlans = testPlans - } - } - - case workspace(AutogeneratedSchemeWorkspace) - case allProjects(AutogeneratedSchemeProject) - case project(projectName: String, AutogeneratedSchemeProject) - - public static func workspace( - platform: Platform, - productFilters: [Product] = [], - onlyFocusedTargets: Bool = true, - suffix: String? = nil, - testActionArguments: Arguments? = nil, - codeCoverageMode: AutogeneratedSchemeWorkspace.CodeCoverageMode = .disabled, - testingOptions: TestingOptions = [], - testLanguage: SchemeLanguage? = nil, - testRegion: String? = nil, - testScreenCaptureFormat: ScreenCaptureFormat? = nil - ) -> AutogeneratedSchemes { - .workspace( - AutogeneratedSchemeWorkspace( - platform: platform, - productFilters: productFilters, - onlyFocusedTargets: onlyFocusedTargets, - suffix: suffix, - testActionArguments: testActionArguments, - codeCoverageMode: codeCoverageMode, - testingOptions: testingOptions, - testLanguage: testLanguage, - testRegion: testRegion, - testScreenCaptureFormat: testScreenCaptureFormat - ) - ) - } - - public static func allProjects( - onlyFocusedTargets: Bool = true, - suffix: String? = nil, - exceptRegexps: [String] = [], - testActionArguments: Arguments? = nil, - runActionArguments: Arguments? = nil, - targetSchemesGrouping: AutogeneratedSchemeProject.TargetSchemesGrouping = .byNameSuffix( - build: ["Implementation", "Interface", "Mocks", "Testing"], - test: ["Tests", "IntegrationTests", "UITests", "SnapshotTests"], - run: ["App", "Demo", "Example"] - ), - codeCoverageEnabled: Bool = false, - testingOptions: TestingOptions = [], - testLanguage: SchemeLanguage? = nil, - testRegion: String? = nil, - testScreenCaptureFormat: ScreenCaptureFormat? = nil, - runLanguage: SchemeLanguage? = nil, - runRegion: String? = nil, - testPlans: [String] = [] - ) -> AutogeneratedSchemes { - .allProjects( - AutogeneratedSchemeProject( - onlyFocusedTargets: onlyFocusedTargets, - suffix: suffix, - exceptRegexps: exceptRegexps, - testActionArguments: testActionArguments, - runActionArguments: runActionArguments, - targetSchemesGrouping: targetSchemesGrouping, - codeCoverageEnabled: codeCoverageEnabled, - testingOptions: testingOptions, - testLanguage: testLanguage, - testRegion: testRegion, - testScreenCaptureFormat: testScreenCaptureFormat, - runLanguage: runLanguage, - runRegion: runRegion, - testPlans: testPlans - ) - ) - } - - public static func project( - name: String, - onlyFocusedTargets: Bool = true, - suffix: String? = nil, - exceptRegexps: [String] = [], - testActionArguments: Arguments? = nil, - runActionArguments: Arguments? = nil, - targetSchemesGrouping: AutogeneratedSchemeProject.TargetSchemesGrouping = .byNameSuffix( - build: ["Implementation", "Interface", "Mocks", "Testing"], - test: ["Tests", "IntegrationTests", "UITests", "SnapshotTests"], - run: ["App", "Demo", "Example"] - ), - codeCoverageEnabled: Bool = false, - testingOptions: TestingOptions = [], - testLanguage: SchemeLanguage? = nil, - testRegion: String? = nil, - testScreenCaptureFormat: ScreenCaptureFormat? = nil, - runLanguage: SchemeLanguage? = nil, - runRegion: String? = nil, - testPlans: [String] = [] - ) -> AutogeneratedSchemes { - .project( - projectName: name, - AutogeneratedSchemeProject( - onlyFocusedTargets: onlyFocusedTargets, - suffix: suffix, - exceptRegexps: exceptRegexps, - testActionArguments: testActionArguments, - runActionArguments: runActionArguments, - targetSchemesGrouping: targetSchemesGrouping, - codeCoverageEnabled: codeCoverageEnabled, - testingOptions: testingOptions, - testLanguage: testLanguage, - testRegion: testRegion, - testScreenCaptureFormat: testScreenCaptureFormat, - runLanguage: runLanguage, - runRegion: runRegion, - testPlans: testPlans - ) - ) - } -} diff --git a/Sources/ProjectDescription/BuildAction.swift b/Sources/ProjectDescription/BuildAction.swift index 6348ab6..1ca8e69 100644 --- a/Sources/ProjectDescription/BuildAction.swift +++ b/Sources/ProjectDescription/BuildAction.swift @@ -6,6 +6,11 @@ import Foundation public struct BuildAction: Equatable, Codable { /// A list of targets to build, which are defined in the project. public var targets: [TargetReference] + /// A list of target selection scopes that determine which targets to build. + /// + /// Each scope describes a group of targets (e.g. all targets or a filtered subset) that + /// should be resolved into the `targets` list during generation. + public var targetSelection: [TargetSelectionScope] /// A list of actions that are executed before starting the build process. public var preActions: [ExecutionAction] /// A list of actions that are executed after the build process. @@ -17,12 +22,14 @@ public struct BuildAction: Equatable, Codable { public init( targets: [TargetReference] = [], + targetSelection: [TargetSelectionScope] = [], preActions: [ExecutionAction] = [], postActions: [ExecutionAction] = [], buildImplicitDependencies: Bool = true, runPostActionsOnFailure: Bool = false ) { self.targets = targets + self.targetSelection = targetSelection self.preActions = preActions self.postActions = postActions self.buildImplicitDependencies = buildImplicitDependencies @@ -32,12 +39,14 @@ public struct BuildAction: Equatable, Codable { /// Returns a build action. /// - Parameters: /// - targets: A list of targets to build, which are defined in the project. + /// - targetSelection: A list of target selection scopes that determine which targets to build. /// - preActions: A list of actions that are executed before starting the build process. /// - postActions: A list of actions that are executed after the build process. /// - runPostActionsOnFailure: Whether the post actions should be run in the case of a failure /// - Returns: Initialized build action. public static func buildAction( targets: [TargetReference], + targetSelection: [TargetSelectionScope] = [], preActions: [ExecutionAction] = [], postActions: [ExecutionAction] = [], buildImplicitDependencies: Bool = true, @@ -45,6 +54,7 @@ public struct BuildAction: Equatable, Codable { ) -> BuildAction { BuildAction( targets: targets, + targetSelection: targetSelection, preActions: preActions, postActions: postActions, buildImplicitDependencies: buildImplicitDependencies, diff --git a/Sources/ProjectDescription/AutogeneratedTestPlan.swift b/Sources/ProjectDescription/GeneratedTestPlan.swift similarity index 66% rename from Sources/ProjectDescription/AutogeneratedTestPlan.swift rename to Sources/ProjectDescription/GeneratedTestPlan.swift index 71ae5ce..542b94d 100644 --- a/Sources/ProjectDescription/AutogeneratedTestPlan.swift +++ b/Sources/ProjectDescription/GeneratedTestPlan.swift @@ -4,15 +4,15 @@ import Foundation /// /// Wraps a ``TestableTarget`` with optional test filtering (selected/skipped tests). /// Conforms to `ExpressibleByStringInterpolation`, so you can use a plain string -/// wherever an `AutogeneratedTestableTarget` is expected: +/// wherever an `GeneratedTestPlanTestableTarget` is expected: /// /// ```swift -/// let targets: [AutogeneratedTestableTarget] = [ +/// let targets: [GeneratedTestPlanTestableTarget] = [ /// "MyFeatureTests", /// .target("MyOtherTests", skippedTests: ["MyOtherTests/testFlaky"]), /// ] /// ``` -public struct AutogeneratedTestableTarget: Codable, Equatable, Sendable, ExpressibleByStringInterpolation { +public struct GeneratedTestPlanTestableTarget: Codable, Hashable, Sendable, ExpressibleByStringInterpolation { /// The underlying testable target reference that identifies /// which target should be included in the generated test plan. public let target: TestableTarget @@ -30,7 +30,7 @@ public struct AutogeneratedTestableTarget: Codable, Equatable, Sendable, Express /// These tests will be skipped even if they would otherwise run. /// An empty array means no tests are skipped. public let skippedTests: [String] - + public init( _ target: TestableTarget, selectedTests: [String] = [], @@ -40,12 +40,12 @@ public struct AutogeneratedTestableTarget: Codable, Equatable, Sendable, Express self.selectedTests = selectedTests self.skippedTests = skippedTests } - + public init(stringLiteral value: String) { self.init(TestableTarget(stringLiteral: value)) } - - /// Creates an ``AutogeneratedTestableTarget`` with optional test filtering. + + /// Creates an ``GeneratedTestPlanTestableTarget`` with optional test filtering. /// /// This factory method provides a more readable call-site syntax: /// ```swift @@ -56,7 +56,7 @@ public struct AutogeneratedTestableTarget: Codable, Equatable, Sendable, Express /// - target: The testable target to include. /// - selectedTests: Tests to run exclusively. Empty means run all. /// - skippedTests: Tests to skip. Empty means skip none. - /// - Returns: A configured ``AutogeneratedTestableTarget``. + /// - Returns: A configured ``GeneratedTestPlanTestableTarget``. public static func target( _ target: TestableTarget, selectedTests: [String] = [], @@ -66,34 +66,48 @@ public struct AutogeneratedTestableTarget: Codable, Equatable, Sendable, Express } } -public struct AutogeneratedTestPlan: Codable, Equatable, Sendable { - public struct Options: Codable, Equatable, Sendable { +/// A test plan that Geko generates during project generation. +/// +/// Unlike a ``TestPlan`` that references an existing `.xctestplan` file on disk, a generated test +/// plan is described declaratively and its `.xctestplan` file is produced by Geko. Test targets can +/// be listed explicitly via ``testTargets`` or resolved automatically from ``targetSelection`` scopes. +public struct GeneratedTestPlan: Codable, Hashable, Sendable { + /// Options applied to the generated test plan. + public struct Options: Codable, Hashable, Sendable { /// Arguments Passed On Launch - public struct CommandLineArgumentEntry: Codable, Equatable, Sendable { + public struct CommandLineArgumentEntry: Codable, Hashable, Sendable { public let argument: String public let enabled: Bool? - public init(argument: String, enabled: Bool? = nil) { + public init(argument: String, enabled: Bool?) { self.argument = argument self.enabled = enabled } + + public static func argument(argument: String, enabled: Bool? = nil) -> Self { + Self(argument: argument, enabled: enabled) + } } /// Environment Variables - public struct VariableEntity: Codable, Equatable, Sendable { + public struct VariableEntity: Codable, Hashable, Sendable { public let enabled: Bool? public let key: String public let value: String - public init(key: String, value: String, enabled: Bool? = nil) { + public init(key: String, value: String, enabled: Bool?) { self.key = key self.value = value self.enabled = enabled } + + public static func variable(key: String, value: String, enabled: Bool? = nil) -> Self { + Self(key: key, value: value, enabled: enabled) + } } /// Simulated Location - public struct LocationScenario: Codable, Equatable, Sendable { + public struct LocationScenario: Codable, Hashable, Sendable { public let identifier: String public let referenceType: String @@ -101,10 +115,9 @@ public struct AutogeneratedTestPlan: Codable, Equatable, Sendable { self.identifier = identifier self.referenceType = referenceType } - } - + } /// Automatic Screen Capture - public enum UITestingScreenshotsLifetime: String, Codable, Equatable, Sendable { + public enum UITestingScreenshotsLifetime: String, Codable, Hashable, Sendable { /// On, and keep all case keepAlways /// On, and delete if test succeeds (default) @@ -112,23 +125,23 @@ public struct AutogeneratedTestPlan: Codable, Equatable, Sendable { /// Off case keepNever } - + /// Preferred Capture Format - public enum PreferredScreenCaptureFormat: String, Codable, Equatable, Sendable { + public enum PreferredScreenCaptureFormat: String, Codable, Hashable, Sendable { /// Video (default) case video /// Screenshot case screenshot } - + /// Distribution - public enum Distributor: String, Codable, Equatable, Sendable { + public enum Distributor: String, Codable, Hashable, Sendable { case appStore = "com.apple.AppStore" case testFlight = "com.apple.TestFlight" } - + /// Attachments - public enum UserAttachmentLifetime: String, Codable, Equatable, Sendable { + public enum UserAttachmentLifetime: String, Codable, Hashable, Sendable { /// On, and keep all case keepAlways /// On, and delete if test succeeds (default) @@ -136,9 +149,9 @@ public struct AutogeneratedTestPlan: Codable, Equatable, Sendable { /// Off case keepNever } - + /// Collect Test Diagnostics on Failure - public enum DiagnosticCollectionPolicy: String, Codable, Equatable, Sendable { + public enum DiagnosticCollectionPolicy: String, Codable, Hashable, Sendable { /// Never case never = "Never" /// When testing with xcodebuild (default) @@ -146,17 +159,17 @@ public struct AutogeneratedTestPlan: Codable, Equatable, Sendable { /// Always case always = "Always" } - + /// XCTest Execution Order - public enum TestExecutionOrdering: String, Codable, Equatable, Sendable { + public enum TestExecutionOrdering: String, Codable, Hashable, Sendable { /// Alphabetical (default) case alphabetical /// Random case random } - + /// Test Repetition Mode - public enum TestRepetitionMode: String, Codable, Equatable, Sendable { + public enum TestRepetitionMode: String, Codable, Hashable, Sendable { /// Until Failure case untilFailure /// Retry On Failure @@ -168,7 +181,7 @@ public struct AutogeneratedTestPlan: Codable, Equatable, Sendable { } /// Address Sanitizer - public struct AddressSanitizer: Codable, Equatable, Sendable { + public struct AddressSanitizer: Codable, Hashable, Sendable { public let detectStackUseAfterReturn: Bool? public let enabled: Bool @@ -183,7 +196,7 @@ public struct AutogeneratedTestPlan: Codable, Equatable, Sendable { public static func onAndDetectStackUseAfterReturn() -> Self { Self(detectStackUseAfterReturn: true, enabled: true) } } - public struct RuntimeApiChecking: Codable, Equatable, Sendable { + public struct RuntimeApiChecking: Codable, Hashable, Sendable { public enum Severity: String, Codable, Sendable { case warning case failure @@ -206,7 +219,7 @@ public struct AutogeneratedTestPlan: Codable, Equatable, Sendable { } /// Hardware Memory Tagging - public struct CheckedAllocations: Codable, Equatable, Sendable { + public struct CheckedAllocations: Codable, Hashable, Sendable { public let enabled: Bool public let requiresHardwareAcceleration: Bool @@ -221,17 +234,16 @@ public struct AutogeneratedTestPlan: Codable, Equatable, Sendable { public static func onWhenSupported() -> Self { Self(enabled: true, requiresHardwareAcceleration: true) } /// On (when supported), fall back to Guard Malloc otherwise public static func onWhenSupportedFallBackToGuardMalloc() -> Self { Self(enabled: true, requiresHardwareAcceleration: false) } - } - + } /// Malloc Stack Logging @frozen - public enum MallocStackLoggingOptions: Codable, Equatable, Sendable { + public enum MallocStackLoggingOptions: Codable, Hashable, Sendable { /// On case on /// Off (default) case off } - + /// Arguments Passed On Launch public let commandLineArgumentEntries: [CommandLineArgumentEntry]? /// Environment Variables @@ -294,39 +306,39 @@ public struct AutogeneratedTestPlan: Codable, Equatable, Sendable { public let nsZombieEnabled: Bool? /// Malloc Stack Logging public let mallocStackLoggingOptions: MallocStackLoggingOptions? - + public init( - commandLineArgumentEntries: [CommandLineArgumentEntry]? = nil, - environmentVariableEntries: [VariableEntity]? = nil, - targetForVariableExpansion: String? = nil, - language: String? = nil, - region: String? = nil, - locationScenario: LocationScenario? = nil, - uiTestingScreenshotsLifetime: UITestingScreenshotsLifetime? = nil, - preferredScreenCaptureFormat: PreferredScreenCaptureFormat? = nil, - areLocalizationScreenshotsEnabled: Bool? = nil, - distributor: Distributor? = nil, - userAttachmentLifetime: UserAttachmentLifetime? = nil, - diagnosticCollectionPolicy: DiagnosticCollectionPolicy? = nil, - testExecutionOrdering: TestExecutionOrdering? = nil, - testTimeoutsEnabled: Bool? = nil, - defaultTestExecutionTimeAllowance: Int? = nil, - maximumTestExecutionTimeAllowance: Int? = nil, - testRepetitionMode: TestRepetitionMode? = nil, - maximumTestRepetitions: Int? = nil, - repeatInNewRunnerProcess: Bool? = nil, - codeCoverage: Bool? = nil, - addressSanitizer: AddressSanitizer? = nil, - threadSanitizerEnabled: Bool? = nil, - undefinedBehaviorSanitizerEnabled: Bool? = nil, - mainThreadCheckerDetectionPolicy: RuntimeApiChecking? = nil, - threadPerformanceCheckerRuntimeIssueDetection: RuntimeApiChecking? = nil, - runtimeIssueDetection: RuntimeApiChecking? = nil, - checkedAllocations: CheckedAllocations? = nil, - mallocScribbleEnabled: Bool? = nil, - mallocGuardEdgesEnabled: Bool? = nil, - nsZombieEnabled: Bool? = nil, - mallocStackLoggingOptions: MallocStackLoggingOptions? = nil, + commandLineArgumentEntries: [CommandLineArgumentEntry]?, + environmentVariableEntries: [VariableEntity]?, + targetForVariableExpansion: String?, + language: String?, + region: String?, + locationScenario: LocationScenario?, + uiTestingScreenshotsLifetime: UITestingScreenshotsLifetime?, + preferredScreenCaptureFormat: PreferredScreenCaptureFormat?, + areLocalizationScreenshotsEnabled: Bool?, + distributor: Distributor?, + userAttachmentLifetime: UserAttachmentLifetime?, + diagnosticCollectionPolicy: DiagnosticCollectionPolicy?, + testExecutionOrdering: TestExecutionOrdering?, + testTimeoutsEnabled: Bool?, + defaultTestExecutionTimeAllowance: Int?, + maximumTestExecutionTimeAllowance: Int?, + testRepetitionMode: TestRepetitionMode?, + maximumTestRepetitions: Int?, + repeatInNewRunnerProcess: Bool?, + codeCoverage: Bool?, + addressSanitizer: AddressSanitizer?, + threadSanitizerEnabled: Bool?, + undefinedBehaviorSanitizerEnabled: Bool?, + mainThreadCheckerDetectionPolicy: RuntimeApiChecking?, + threadPerformanceCheckerRuntimeIssueDetection: RuntimeApiChecking?, + runtimeIssueDetection: RuntimeApiChecking?, + checkedAllocations: CheckedAllocations?, + mallocScribbleEnabled: Bool?, + mallocGuardEdgesEnabled: Bool?, + nsZombieEnabled: Bool?, + mallocStackLoggingOptions: MallocStackLoggingOptions?, ) { self.commandLineArgumentEntries = commandLineArgumentEntries self.environmentVariableEntries = environmentVariableEntries @@ -360,39 +372,95 @@ public struct AutogeneratedTestPlan: Codable, Equatable, Sendable { self.nsZombieEnabled = nsZombieEnabled self.mallocStackLoggingOptions = mallocStackLoggingOptions } + + public static func options( + commandLineArgumentEntries: [CommandLineArgumentEntry]? = nil, + environmentVariableEntries: [VariableEntity]? = nil, + targetForVariableExpansion: String? = nil, + language: String? = nil, + region: String? = nil, + locationScenario: LocationScenario? = nil, + uiTestingScreenshotsLifetime: UITestingScreenshotsLifetime? = nil, + preferredScreenCaptureFormat: PreferredScreenCaptureFormat? = nil, + areLocalizationScreenshotsEnabled: Bool? = nil, + distributor: Distributor? = nil, + userAttachmentLifetime: UserAttachmentLifetime? = nil, + diagnosticCollectionPolicy: DiagnosticCollectionPolicy? = nil, + testExecutionOrdering: TestExecutionOrdering? = nil, + testTimeoutsEnabled: Bool? = nil, + defaultTestExecutionTimeAllowance: Int? = nil, + maximumTestExecutionTimeAllowance: Int? = nil, + testRepetitionMode: TestRepetitionMode? = nil, + maximumTestRepetitions: Int? = nil, + repeatInNewRunnerProcess: Bool? = nil, + codeCoverage: Bool? = nil, + addressSanitizer: AddressSanitizer? = nil, + threadSanitizerEnabled: Bool? = nil, + undefinedBehaviorSanitizerEnabled: Bool? = nil, + mainThreadCheckerDetectionPolicy: RuntimeApiChecking? = nil, + threadPerformanceCheckerRuntimeIssueDetection: RuntimeApiChecking? = nil, + runtimeIssueDetection: RuntimeApiChecking? = nil, + checkedAllocations: CheckedAllocations? = nil, + mallocScribbleEnabled: Bool? = nil, + mallocGuardEdgesEnabled: Bool? = nil, + nsZombieEnabled: Bool? = nil, + mallocStackLoggingOptions: MallocStackLoggingOptions? = nil, + ) -> Self { + Self( + commandLineArgumentEntries: commandLineArgumentEntries, + environmentVariableEntries: environmentVariableEntries, + targetForVariableExpansion: targetForVariableExpansion, + language: language, + region: region, + locationScenario: locationScenario, + uiTestingScreenshotsLifetime: uiTestingScreenshotsLifetime, + preferredScreenCaptureFormat: preferredScreenCaptureFormat, + areLocalizationScreenshotsEnabled: areLocalizationScreenshotsEnabled, + distributor: distributor, + userAttachmentLifetime: userAttachmentLifetime, + diagnosticCollectionPolicy: diagnosticCollectionPolicy, + testExecutionOrdering: testExecutionOrdering, + testTimeoutsEnabled: testTimeoutsEnabled, + defaultTestExecutionTimeAllowance: defaultTestExecutionTimeAllowance, + maximumTestExecutionTimeAllowance: maximumTestRepetitions, + testRepetitionMode: testRepetitionMode, + maximumTestRepetitions: maximumTestRepetitions, + repeatInNewRunnerProcess: repeatInNewRunnerProcess, + codeCoverage: codeCoverage, + addressSanitizer: addressSanitizer, + threadSanitizerEnabled: threadSanitizerEnabled, + undefinedBehaviorSanitizerEnabled: undefinedBehaviorSanitizerEnabled, + mainThreadCheckerDetectionPolicy: mainThreadCheckerDetectionPolicy, + threadPerformanceCheckerRuntimeIssueDetection: threadPerformanceCheckerRuntimeIssueDetection, + runtimeIssueDetection: runtimeIssueDetection, + checkedAllocations: checkedAllocations, + mallocScribbleEnabled: mallocScribbleEnabled, + mallocGuardEdgesEnabled: mallocGuardEdgesEnabled, + nsZombieEnabled: nsZombieEnabled, + mallocStackLoggingOptions: mallocStackLoggingOptions + ) + } } - public struct SchemeReference: Codable, Equatable, Sendable { - /// Project name - public let project: String - /// Scheme name - public let scheme: String + /// A build configuration of the generated test plan. + public struct Configuration: Codable, Hashable { + /// The name of the configuration. + public let name: String + /// The options applied to this configuration. + public let options: Options? - public init(project: String, scheme: String) { - self.project = project - self.scheme = scheme + public init(name: String, options: Options?) { + self.name = name + self.options = options } - } - - /// Defines which targets should be included for testing. - @frozen - public enum TestTargetSelection: Codable, Equatable, Sendable { - /// Include all available targets. - /// - /// When the `--cache` parameter is used during project generation, only the currently focused targets will be added. If the list of focused targets is empty, all test targets will be added. - case all - /// Include only the explicitly listed targets. - /// - /// - Parameters: - /// - targets: The specific targets to include. - case specific(_ targets: [AutogeneratedTestableTarget]) + public static func configuration(name: String, options: Options? = nil) -> Self { + Self(name: name, options: options) + } } @frozen - public enum MissingTargetPolicy: Codable, Equatable, Sendable { - - /// Determines whether to silently ignore or emit a warning when a target is missing. + public enum MissingTargetPolicy: Codable, Hashable, Sendable { @frozen public enum Notification: Codable, Equatable, Sendable { /// Silently ignore — no output, no warnings. @@ -400,77 +468,66 @@ public struct AutogeneratedTestPlan: Codable, Equatable, Sendable { /// Emit a warning to inform the user. case warning } - + /// Do not generate the test plan at all. /// /// - Parameter notification: Whether to silently skip or emit a warning. /// /// Use when a missing target makes the whole test plan meaningless. case skipTestPlan(Notification = .warning) - + /// Skip only the missing target from the test plan. /// /// - Parameter notification: Whether to silently skip or emit a warning. /// /// Other valid targets in the plan will still be included. case skipTarget(Notification = .warning) - + /// Fail the entire generation with an error. /// /// Use when all specified targets are strictly required. case fail } - + /// Name of the test plan public let name: String - /// Scheme to which the generated test plan should be added - public let scheme: SchemeReference? /// The path to the folder where the test plan will be generated. If the path is `nil`, it will be written to `Derived/TestPlans/.xctestplan` - public let path: AbsolutePath? - /// Configuration options - public let configurationOptions: Options? + public var directory: FilePath? + /// Configurations + public var configurations: [Configuration] /// Default options - public let defaultOptions: Options? - /// Test targets to add to the generated plan. - public let testTargetSelection: TestTargetSelection + public var defaultOptions: Options? + /// Explicit test targets to include in the generated test plan. + public var testTargets: [GeneratedTestPlanTestableTarget] + /// Scopes that resolve which test targets to include in the generated test plan. + public var targetSelection: [TestableTargetSelectionScope] + /// Whether this test plan is the default plan. + public var isDefault: Bool /// Policy for handling missing targets. - public let missingTargetPolicy: MissingTargetPolicy + public var missingTargetPolicy: MissingTargetPolicy + /// The path where the generated test plan is written. + public var path: FilePath public init( name: String, - scheme: SchemeReference?, - path: AbsolutePath?, - configurationOptions: Options?, + directory: FilePath?, + configurations: [Configuration], defaultOptions: Options?, - testTargetSelection: TestTargetSelection, + testTargets: [GeneratedTestPlanTestableTarget], + targetSelection: [TestableTargetSelectionScope], + isDefault: Bool, missingTargetPolicy: MissingTargetPolicy ) { - self.name = name - self.scheme = scheme - self.path = path - self.configurationOptions = configurationOptions + self.name = name.split(separator: ".").first.map(String.init) ?? name + self.directory = directory + self.configurations = configurations self.defaultOptions = defaultOptions - self.testTargetSelection = testTargetSelection + self.testTargets = testTargets + self.targetSelection = targetSelection + self.isDefault = isDefault self.missingTargetPolicy = missingTargetPolicy - } - - public static func testPlan( - name: String, - scheme: SchemeReference? = nil, - path: AbsolutePath? = nil, - configurationOptions: Options? = nil, - defaultOptions: Options? = nil, - testTargetSelection: TestTargetSelection = .all, - missingTargetPolicy: MissingTargetPolicy = .skipTestPlan() - ) -> Self { - Self( - name: name, - scheme: scheme, - path: path, - configurationOptions: configurationOptions, - defaultOptions: defaultOptions, - testTargetSelection: testTargetSelection, - missingTargetPolicy: missingTargetPolicy - ) + + // Internal + self.path = "/" } } diff --git a/Sources/ProjectDescription/ProjectOptions.swift b/Sources/ProjectDescription/ProjectOptions.swift index 1807ee3..2f6fbe0 100644 --- a/Sources/ProjectDescription/ProjectOptions.swift +++ b/Sources/ProjectDescription/ProjectOptions.swift @@ -4,7 +4,6 @@ extension Project { /// Options to configure a project. public struct Options: Codable, Equatable { /// Configures automatic target schemes generation. - @available(*, deprecated, message: "Use Workspace.autogeneratedSchemes instead.") public var automaticSchemesOptions: AutomaticSchemesOptions /// Configures the default known regions diff --git a/Sources/ProjectDescription/TargetSelectionScope.swift b/Sources/ProjectDescription/TargetSelectionScope.swift new file mode 100644 index 0000000..f3f05ad --- /dev/null +++ b/Sources/ProjectDescription/TargetSelectionScope.swift @@ -0,0 +1,25 @@ +import Foundation + +/// A scope that describes a group of targets to include in a build action. +/// +/// Used to express which targets of a project (or workspace) should be built when the +/// manifest cannot list them explicitly (for example, when targets are added or removed over time). +@frozen +public enum TargetSelectionScope: Equatable, Codable { + /// Include all buildable targets. + case all + + /// Include only targets matching the given filters. + /// + /// - Parameters: + /// - platform: When set, only targets of this platform are included. + /// - products: When set, only targets whose product type is in this list are included. + /// - regexp: When set, only targets whose name matches any of these regular expressions are included. + /// - exclude: When set, targets whose name matches any of these regular expressions are excluded. + case scope( + platform: Platform? = nil, + products: [Product]? = nil, + regexp: [String]? = nil, + exclude: [String]? = nil + ) +} \ No newline at end of file diff --git a/Sources/ProjectDescription/TestAction.swift b/Sources/ProjectDescription/TestAction.swift index c620b22..00f201a 100644 --- a/Sources/ProjectDescription/TestAction.swift +++ b/Sources/ProjectDescription/TestAction.swift @@ -11,6 +11,12 @@ public struct TestAction: Equatable, Codable { /// A list of testable targets, that are targets which are defined in the project with testable information. public var targets: [TestableTarget] + /// A list of target selection scopes that determine which test targets to test. + /// + /// Each scope describes a group of test targets (e.g. all targets or a filtered subset) that + /// should be resolved into the `targets` list during generation. + public var targetSelection: [TestableTargetSelectionScope] + /// Command line arguments passed on launch and environment variables. public var arguments: Arguments? @@ -40,6 +46,7 @@ public struct TestAction: Equatable, Codable { public init( targets: [TestableTarget], + targetSelection: [TestableTargetSelectionScope] = [], arguments: Arguments?, configurationName: String, attachDebugger: Bool, @@ -57,6 +64,7 @@ public struct TestAction: Equatable, Codable { ) { self.testPlans = testPlans self.targets = targets + self.targetSelection = targetSelection self.arguments = arguments self.configuration = .configuration(configurationName) self.attachDebugger = attachDebugger @@ -78,6 +86,7 @@ public struct TestAction: Equatable, Codable { private init( testPlans: [TestPlan]?, targets: [TestableTarget], + targetSelection: [TestableTargetSelectionScope], arguments: Arguments?, configuration: ConfigurationName, attachDebugger: Bool, @@ -90,6 +99,7 @@ public struct TestAction: Equatable, Codable { ) { self.testPlans = testPlans self.targets = targets + self.targetSelection = targetSelection self.arguments = arguments self.configuration = configuration self.attachDebugger = attachDebugger @@ -104,6 +114,7 @@ public struct TestAction: Equatable, Codable { /// Returns a test action from a list of targets to be tested. /// - Parameters: /// - targets: List of targets to be tested. + /// - targetSelection: List of target selection scopes that determine which test targets to test. /// - arguments: Arguments passed when running the tests. /// - configuration: Configuration to be used. /// - attachDebugger: A boolean controlling whether a debugger is attached to the process running the tests. @@ -116,6 +127,7 @@ public struct TestAction: Equatable, Codable { /// - Returns: An initialized test action. public static func targets( _ targets: [TestableTarget], + targetSelection: [TestableTargetSelectionScope] = [], arguments: Arguments? = nil, configuration: ConfigurationName = .debug, attachDebugger: Bool = true, @@ -129,6 +141,7 @@ public struct TestAction: Equatable, Codable { Self( testPlans: nil, targets: targets, + targetSelection: targetSelection, arguments: arguments, configuration: configuration, attachDebugger: attachDebugger, @@ -150,17 +163,34 @@ public struct TestAction: Equatable, Codable { /// - postActions: Actions to execute after running the tests. /// - Returns: A test action. public static func testPlans( - _ testPlans: [FilePath], + _ testPlans: [TestActionTestPlan], configuration: ConfigurationName = .debug, attachDebugger: Bool = true, preActions: [ExecutionAction] = [], postActions: [ExecutionAction] = [] ) -> Self { - let testPlans = testPlans.map { TestPlan(path: $0, testTargets: [], isDefault: $0 == testPlans.first) } + let testPlans: [TestPlan] = testPlans.enumerated().map { (index, testPlan) in + switch testPlan { + case let .file(path): + .file(name: path.basenameWithoutExt, path: path, testTargets: [], isDefault: index == 0) + case let .generated(name, directory, configurations, defaultOptions, testTargets, targetSelection, missingTargetPolicy): + .generated( + name: name, + directory: directory, + configurations: configurations, + defaultOptions: defaultOptions, + testTargets: testTargets, + targetSelection: targetSelection, + isDefault: index == 0, + missingTargetPolicy: missingTargetPolicy + ) + } + } return Self( testPlans: testPlans, targets: [], + targetSelection: [], arguments: nil, configuration: configuration, attachDebugger: attachDebugger, @@ -187,6 +217,7 @@ extension TestAction { set { options.coverage = newValue } } + @available(*, deprecated) public var codeCoverageTargets: [TargetReference] { get { options.codeCoverageTargets } set { options.codeCoverageTargets = newValue } diff --git a/Sources/ProjectDescription/TestActionOptions.swift b/Sources/ProjectDescription/TestActionOptions.swift index 91a2b0f..3a4ecb6 100644 --- a/Sources/ProjectDescription/TestActionOptions.swift +++ b/Sources/ProjectDescription/TestActionOptions.swift @@ -15,6 +15,7 @@ public struct TestActionOptions: Equatable, Codable { public var coverage: Bool /// A list of targets you want to gather the test coverage data for them, which are defined in the project. + @available(*, deprecated) public var codeCoverageTargets: [TargetReference] /// Returns a set of options for a test action. @@ -25,6 +26,7 @@ public struct TestActionOptions: Equatable, Codable { /// - coverage: Whether test coverage should be collected. /// - codeCoverageTargets: List of test targets whose code coverage information should be collected. /// - Returns: A set of options. + @available(*, deprecated) public static func options( language: SchemeLanguage? = nil, region: String? = nil, @@ -40,4 +42,26 @@ public struct TestActionOptions: Equatable, Codable { codeCoverageTargets: codeCoverageTargets ) } + + /// Returns a set of options for a test action. + /// - Parameters: + /// - language: Language used for running the tests. + /// - region: Region used for running the tests. + /// - preferredScreenCaptureFormat: Preferred screen capture format for UI tests results in Xcode 15+ + /// - coverage: Whether test coverage should be collected. + /// - Returns: A set of options. + public static func options( + language: SchemeLanguage? = nil, + region: String? = nil, + preferredScreenCaptureFormat: ScreenCaptureFormat? = nil, + coverage: Bool = false, + ) -> TestActionOptions { + TestActionOptions( + language: language, + region: region, + preferredScreenCaptureFormat: preferredScreenCaptureFormat, + coverage: coverage, + codeCoverageTargets: [] + ) + } } diff --git a/Sources/ProjectDescription/TestActionTestPlan.swift b/Sources/ProjectDescription/TestActionTestPlan.swift new file mode 100644 index 0000000..0fe9a21 --- /dev/null +++ b/Sources/ProjectDescription/TestActionTestPlan.swift @@ -0,0 +1,41 @@ +import Foundation + +/// A test plan passed to a test action. +/// +/// A test plan can either reference an existing `.xctestplan` file on disk or describe a +/// generated test plan that Geko creates during generation. +@frozen +public enum TestActionTestPlan: ExpressibleByStringLiteral, ExpressibleByStringInterpolation { + /// Reference an existing `.xctestplan` file on disk. + /// + /// - Parameter path: Path to the existing `.xctestplan` file. + case file(FilePath) + + /// Describe a test plan that Geko generates during generation. + /// + /// - Parameters: + /// - name: Name of the generated test plan. + /// - directory: Directory the test plan is written to. If `nil`, it's written to the derived directory. + /// - configurations: Configurations of the generated test plan. + /// - defaultOptions: Default options applied to the generated test plan. + /// - testTargets: Explicit test targets to include in the generated test plan. + /// - targetSelection: Scopes that resolve which test targets to include in the generated test plan. + /// - missingTargetPolicy: Policy applied when a target referenced by the generated test plan is missing. + case generated( + name: String, + directory: FilePath? = nil, + configurations: [GeneratedTestPlan.Configuration] = [], + defaultOptions: GeneratedTestPlan.Options? = nil, + testTargets: [GeneratedTestPlanTestableTarget] = [], + targetSelection: [TestableTargetSelectionScope] = [], + missingTargetPolicy: GeneratedTestPlan.MissingTargetPolicy = .skipTestPlan() + ) + + public init(_ value: StringLiteralType) { + self = .file(FilePath(value)) + } + + public init(stringLiteral value: String) { + self = .file(FilePath(stringLiteral: value)) + } +} \ No newline at end of file diff --git a/Sources/ProjectDescription/TestPlan.swift b/Sources/ProjectDescription/TestPlan.swift index 3acb4f6..910525a 100644 --- a/Sources/ProjectDescription/TestPlan.swift +++ b/Sources/ProjectDescription/TestPlan.swift @@ -1,15 +1,137 @@ import Foundation -public struct TestPlan: Hashable, Codable { - public let name: String - public let path: FilePath - public var testTargets: [TestableTarget] - public var isDefault: Bool +/// A test plan to run in a test action. +/// +/// A test plan can reference an existing `.xctestplan` file on disk (``file``) or be declared +/// declaratively and generated by Geko (``generated``). +@frozen +public enum TestPlan: Hashable, Codable { + /// Reference an existing `.xctestplan` file on disk. + /// + /// - Parameters: + /// - name: Name of the test plan. + /// - path: Path to the existing `.xctestplan` file. + /// - testTargets: Testable targets the plan runs. + /// - isDefault: Whether this is the default plan. + case file( + name: String, + path: FilePath, + testTargets: [TestableTarget], + isDefault: Bool, + ) + /// Describe a test plan that Geko generates during generation. + case generated(GeneratedTestPlan) + + /// The name of the test plan. + public var name: String { + switch self { + case let .file(name, _, _, _): + name + case let .generated(plan): + plan.name + } + } + + /// The path of the test plan. + public var path: FilePath { + switch self { + case let .file(_, path, _, _): + path + case let .generated(plan): + plan.path + } + } + + /// The testable targets the test plan runs. + public var testTargets: [TestableTarget] { + switch self { + case let .file(_, _, testTargets, _): + testTargets + case let .generated(plan): + plan.testTargets.map { $0.target } + } + } + + /// Whether this is the default test plan. + public var isDefault: Bool { + switch self { + case let .file(_, _, _, isDefault): + isDefault + case let .generated(plan): + plan.isDefault + } + } + + /// Whether this test plan is generated by Geko rather than referencing an existing file. + public var isGenerated: Bool { + switch self { + case .file: false + case .generated: true + } + } + + @available(*, deprecated) public init(path: FilePath, testTargets: [TestableTarget], isDefault: Bool) { - name = path.basenameWithoutExt - self.path = path - self.testTargets = testTargets - self.isDefault = isDefault + self = .file( + name: path.basenameWithoutExt, + path: path, + testTargets: testTargets, + isDefault: isDefault + ) + } + + /// Creates a test plan that references an existing `.xctestplan` file on disk. + /// + /// - Parameters: + /// - path: Path to the existing `.xctestplan` file. + /// - testTargets: Testable targets the plan runs. + /// - isDefault: Whether this is the default plan. + public static func file( + path: FilePath, + testTargets: [TestableTarget], + isDefault: Bool + ) -> Self { + .file( + name: path.basenameWithoutExt, + path: path, + testTargets: testTargets, + isDefault: isDefault + ) + } + + /// Creates a test plan that Geko generates during generation. + /// + /// - Parameters: + /// - name: Name of the generated test plan. + /// - directory: Directory the test plan is written to. If `nil`, it's written to the derived directory. + /// - configurations: Configurations of the generated test plan. + /// - defaultOptions: Default options applied to the generated test plan. + /// - testTargets: Explicit test targets to include in the generated test plan. + /// - targetSelection: Scopes that resolve which test targets to include in the generated test plan. + /// - isDefault: Whether this is the default plan. + /// - missingTargetPolicy: Policy applied when a target referenced by the generated test plan is missing. + public static func generated( + name: String, + directory: FilePath? = nil, + configurations: [GeneratedTestPlan.Configuration] = [], + defaultOptions: GeneratedTestPlan.Options? = nil, + testTargets: [GeneratedTestPlanTestableTarget] = [], + targetSelection: [TestableTargetSelectionScope] = [], + isDefault: Bool = false, + missingTargetPolicy: GeneratedTestPlan.MissingTargetPolicy = .skipTestPlan() + ) -> Self { + .generated( + GeneratedTestPlan( + name: name, + directory: directory, + configurations: configurations, + defaultOptions: defaultOptions, + testTargets: testTargets, + targetSelection: targetSelection, + isDefault: isDefault, + missingTargetPolicy: missingTargetPolicy + ) + ) } } diff --git a/Sources/ProjectDescription/TestableTarget.swift b/Sources/ProjectDescription/TestableTarget.swift index 06147ee..2ab3e22 100644 --- a/Sources/ProjectDescription/TestableTarget.swift +++ b/Sources/ProjectDescription/TestableTarget.swift @@ -10,17 +10,33 @@ public struct TestableTarget: Equatable, Hashable, Codable, ExpressibleByStringI public var isParallelizable: Bool /// Execute tests in random order. public var isRandomExecutionOrdering: Bool + /// Enables test coverage + public var isCoverable: Bool public init( target: TargetReference, skipped: Bool = false, parallelizable: Bool = false, - randomExecutionOrdering: Bool = false + randomExecutionOrdering: Bool = false, + coverage: Bool = false ) { self.target = target isSkipped = skipped isParallelizable = parallelizable isRandomExecutionOrdering = randomExecutionOrdering + isCoverable = coverage + } + + public init( + target: TargetReference, + skipped: Bool = false, + options: TestingOptions = TestingOptions() + ) { + self.target = target + isSkipped = skipped + isParallelizable = options.contains(.parallelizable) + isRandomExecutionOrdering = options.contains(.randomExecutionOrdering) + isCoverable = options.contains(.coverage) } public init(stringLiteral value: String) { diff --git a/Sources/ProjectDescription/TestableTargetSelectionScope.swift b/Sources/ProjectDescription/TestableTargetSelectionScope.swift new file mode 100644 index 0000000..a30c55a --- /dev/null +++ b/Sources/ProjectDescription/TestableTargetSelectionScope.swift @@ -0,0 +1,29 @@ +import Foundation + +/// A scope that describes a group of test targets to include in a test action. +/// +/// Used to express which test targets of a project (or workspace) should be tested when the +/// manifest cannot list them explicitly (for example, when targets are added or removed over time). +@frozen +public enum TestableTargetSelectionScope: Hashable, Codable { + /// Include all testable targets. + /// + /// - Parameter options: Options applied to every selected testable target. + case all(options: TestingOptions? = nil) + + /// Include only testable targets matching the given filters. + /// + /// - Parameters: + /// - platform: When set, only targets of this platform are included. + /// - products: When set, only targets whose product type is in this list are included. + /// - regexp: When set, only targets whose name matches any of these regular expressions are included. + /// - exclude: When set, targets whose name matches any of these regular expressions are excluded. + /// - options: Options applied to every selected testable target. + case scope( + platform: Platform? = nil, + products: [Product]? = nil, + regexp: [String]? = nil, + exclude: [String]? = nil, + options: TestingOptions? = nil + ) +} \ No newline at end of file diff --git a/Sources/ProjectDescription/TestingOptions.swift b/Sources/ProjectDescription/TestingOptions.swift index 11c10aa..bb4a279 100644 --- a/Sources/ProjectDescription/TestingOptions.swift +++ b/Sources/ProjectDescription/TestingOptions.swift @@ -10,4 +10,6 @@ public struct TestingOptions: OptionSet, Codable, Equatable, Hashable { public static let parallelizable = TestingOptions(rawValue: 1 << 0) /// Execute tests in random order public static let randomExecutionOrdering = TestingOptions(rawValue: 1 << 1) + /// Enable code coverage + public static let coverage = TestingOptions(rawValue: 1 << 2) } diff --git a/Sources/ProjectDescription/WorkspaceGenerationOptions.swift b/Sources/ProjectDescription/WorkspaceGenerationOptions.swift index d645889..3950fed 100644 --- a/Sources/ProjectDescription/WorkspaceGenerationOptions.swift +++ b/Sources/ProjectDescription/WorkspaceGenerationOptions.swift @@ -98,7 +98,7 @@ extension Workspace { /// Enable or disable automatic generation of `Workspace` schemes. If enabled, options to configure code coverage and test /// targets can be passed in via associated values. - @available(*, deprecated, message: "Use Workspace.autogeneratedSchemes instead.") + @available(*, deprecated) public var autogeneratedWorkspaceSchemes: AutogeneratedWorkspaceSchemes /// Allows to suppress warnings in Xcode about updates to recommended settings added in or below the specified Xcode @@ -125,10 +125,6 @@ extension Workspace { /// Suppress objc class duplication warnings when loading plugins. public var suppressObjcDuplicateClassWarningsDuringPluginLoading: Bool - public var autogeneratedSchemes: [AutogeneratedSchemes] - - public var autogeneratedTestPlans: [AutogeneratedTestPlan] - public init( enableAutomaticXcodeSchemes: Bool?, autogeneratedWorkspaceSchemes: AutogeneratedWorkspaceSchemes, @@ -140,10 +136,6 @@ extension Workspace { configurations: [String: BuildConfiguration.Variant], // TODO: Remove default value in next major release suppressObjcDuplicateClassWarningsDuringPluginLoading: Bool = true, - // TODO: Remove default value in next major release - autogeneratedSchemes: [AutogeneratedSchemes] = [], - // TODO: Remove default value in next major release - autogeneratedTestPlans: [AutogeneratedTestPlan] = [], ) { self.enableAutomaticXcodeSchemes = enableAutomaticXcodeSchemes self.autogeneratedWorkspaceSchemes = autogeneratedWorkspaceSchemes @@ -154,8 +146,6 @@ extension Workspace { self.commonSettings = commonSettings self.configurations = configurations self.suppressObjcDuplicateClassWarningsDuringPluginLoading = suppressObjcDuplicateClassWarningsDuringPluginLoading - self.autogeneratedSchemes = autogeneratedSchemes - self.autogeneratedTestPlans = autogeneratedTestPlans } public static func options( @@ -168,8 +158,6 @@ extension Workspace { commonSettings: [CommonSettings] = [], configurations: [String: BuildConfiguration.Variant] = ["Debug": .debug, "Release": .release], suppressObjcDuplicateClassWarningsDuringPluginLoading: Bool = true, - autogeneratedSchemes: [AutogeneratedSchemes] = [], - autogeneratedTestPlans: [AutogeneratedTestPlan] = [], ) -> Self { GenerationOptions( enableAutomaticXcodeSchemes: enableAutomaticXcodeSchemes, @@ -181,8 +169,6 @@ extension Workspace { commonSettings: commonSettings, configurations: configurations, suppressObjcDuplicateClassWarningsDuringPluginLoading: suppressObjcDuplicateClassWarningsDuringPluginLoading, - autogeneratedSchemes: autogeneratedSchemes, - autogeneratedTestPlans: autogeneratedTestPlans ) } } From 9c0ef232dd339795fb82f8c6c789656e11bd3797 Mon Sep 17 00:00:00 2001 From: dmnre Date: Fri, 9 Oct 2026 18:26:19 +0300 Subject: [PATCH 4/6] Redesign test plan API with code coverage targets and FileTestPlan --- Sources/ProjectDescription/FileTestPlan.swift | 25 ++++++++ .../GeneratedTestPlan.swift | 35 ++++++++--- Sources/ProjectDescription/TestAction.swift | 44 +++++++++++-- Sources/ProjectDescription/TestPlan.swift | 62 ++++++------------- .../ProjectDescription/TestableTarget.swift | 6 +- 5 files changed, 111 insertions(+), 61 deletions(-) create mode 100644 Sources/ProjectDescription/FileTestPlan.swift diff --git a/Sources/ProjectDescription/FileTestPlan.swift b/Sources/ProjectDescription/FileTestPlan.swift new file mode 100644 index 0000000..76904d8 --- /dev/null +++ b/Sources/ProjectDescription/FileTestPlan.swift @@ -0,0 +1,25 @@ +import Foundation + +/// Reference an existing `.xctestplan` file on disk. +public struct FileTestPlan: Hashable, Codable { + /// Path to the existing `.xctestplan` file. + public var path: FilePath + + // Internal + + /// Name of the test plan. + public var name: String + /// Parameter testTargets is not used in Project.swift or Workspace.swift. It is filled after loading testplan and can be used in plugins. + public var testTargets: [TestableTarget] + /// Whether this test plan is the default plan. + public var isDefault: Bool + + public init(path: FilePath) { + self.name = path.basenameWithoutExt + self.path = path + + // Internal + self.testTargets = [] + self.isDefault = false + } +} diff --git a/Sources/ProjectDescription/GeneratedTestPlan.swift b/Sources/ProjectDescription/GeneratedTestPlan.swift index 542b94d..a3a6f56 100644 --- a/Sources/ProjectDescription/GeneratedTestPlan.swift +++ b/Sources/ProjectDescription/GeneratedTestPlan.swift @@ -180,6 +180,17 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { case none } + /// Code coverage + @frozen + public enum Coverage: Codable, Hashable, Sendable { + /// Off + case disabled + /// On (default) + case all + /// List of targets with code coverage enabled + case selected([TargetReference]) + } + /// Address Sanitizer public struct AddressSanitizer: Codable, Hashable, Sendable { public let detectStackUseAfterReturn: Bool? @@ -249,7 +260,7 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { /// Environment Variables public let environmentVariableEntries: [VariableEntity]? /// Target for Variable Expansion - public let targetForVariableExpansion: String? + public let targetForVariableExpansion: TargetReference? /// Application Language public let language: String? /// Application Region @@ -283,7 +294,7 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { /// Relaunch Tests for Each Repetition public let repeatInNewRunnerProcess: Bool? /// Code Coverage - public let codeCoverage: Bool? + public let codeCoverage: Coverage? /// Address Sanitizer public let addressSanitizer: AddressSanitizer? /// Thread Sanitizer @@ -310,7 +321,7 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { public init( commandLineArgumentEntries: [CommandLineArgumentEntry]?, environmentVariableEntries: [VariableEntity]?, - targetForVariableExpansion: String?, + targetForVariableExpansion: TargetReference?, language: String?, region: String?, locationScenario: LocationScenario?, @@ -327,7 +338,7 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { testRepetitionMode: TestRepetitionMode?, maximumTestRepetitions: Int?, repeatInNewRunnerProcess: Bool?, - codeCoverage: Bool?, + codeCoverage: Coverage?, addressSanitizer: AddressSanitizer?, threadSanitizerEnabled: Bool?, undefinedBehaviorSanitizerEnabled: Bool?, @@ -376,7 +387,7 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { public static func options( commandLineArgumentEntries: [CommandLineArgumentEntry]? = nil, environmentVariableEntries: [VariableEntity]? = nil, - targetForVariableExpansion: String? = nil, + targetForVariableExpansion: TargetReference? = nil, language: String? = nil, region: String? = nil, locationScenario: LocationScenario? = nil, @@ -393,7 +404,7 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { testRepetitionMode: TestRepetitionMode? = nil, maximumTestRepetitions: Int? = nil, repeatInNewRunnerProcess: Bool? = nil, - codeCoverage: Bool? = nil, + codeCoverage: Coverage? = nil, addressSanitizer: AddressSanitizer? = nil, threadSanitizerEnabled: Bool? = nil, undefinedBehaviorSanitizerEnabled: Bool? = nil, @@ -501,12 +512,17 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { public var testTargets: [GeneratedTestPlanTestableTarget] /// Scopes that resolve which test targets to include in the generated test plan. public var targetSelection: [TestableTargetSelectionScope] - /// Whether this test plan is the default plan. - public var isDefault: Bool /// Policy for handling missing targets. public var missingTargetPolicy: MissingTargetPolicy + + // Internal + /// The path where the generated test plan is written. public var path: FilePath + /// Whether this test plan is the default plan. + public var isDefault: Bool + + // MARK: - Init public init( name: String, @@ -515,7 +531,6 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { defaultOptions: Options?, testTargets: [GeneratedTestPlanTestableTarget], targetSelection: [TestableTargetSelectionScope], - isDefault: Bool, missingTargetPolicy: MissingTargetPolicy ) { self.name = name.split(separator: ".").first.map(String.init) ?? name @@ -524,10 +539,10 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { self.defaultOptions = defaultOptions self.testTargets = testTargets self.targetSelection = targetSelection - self.isDefault = isDefault self.missingTargetPolicy = missingTargetPolicy // Internal self.path = "/" + self.isDefault = false } } diff --git a/Sources/ProjectDescription/TestAction.swift b/Sources/ProjectDescription/TestAction.swift index 00f201a..b968ca3 100644 --- a/Sources/ProjectDescription/TestAction.swift +++ b/Sources/ProjectDescription/TestAction.swift @@ -154,6 +154,43 @@ public struct TestAction: Equatable, Codable { ) } + /// Returns a test action from a list of test plans. + /// - Parameters: + /// - testPlans: List of test plans to run. + /// - configuration: Configuration to be used. + /// - attachDebugger: A boolean controlling whether a debugger is attached to the process running the tests. + /// - preActions: Actions to execute before running the tests. + /// - postActions: Actions to execute after running the tests. + /// - Returns: A test action. + @available(*, deprecated) + public static func testPlans( + _ testPlans: [FilePath], + configuration: ConfigurationName = .debug, + attachDebugger: Bool = true, + preActions: [ExecutionAction] = [], + postActions: [ExecutionAction] = [] + ) -> Self { + let testPlans = testPlans.map { TestPlan(path: $0, testTargets: [], isDefault: $0 == testPlans.first) } + + return Self( + testPlans: testPlans, + targets: [], + targetSelection: [], + arguments: nil, + configuration: configuration, + attachDebugger: attachDebugger, + expandVariableFromTarget: nil, + preActions: preActions, + postActions: postActions, + options: .options(), + diagnosticsOptions: .options( + mainThreadCheckerEnabled: false, + performanceAntipatternCheckerEnabled: false + ), + skippedTests: nil + ) + } + /// Returns a test action from a list of test plans. /// - Parameters: /// - testPlans: List of test plans to run. @@ -163,16 +200,16 @@ public struct TestAction: Equatable, Codable { /// - postActions: Actions to execute after running the tests. /// - Returns: A test action. public static func testPlans( - _ testPlans: [TestActionTestPlan], + list: [TestActionTestPlan], configuration: ConfigurationName = .debug, attachDebugger: Bool = true, preActions: [ExecutionAction] = [], postActions: [ExecutionAction] = [] ) -> Self { - let testPlans: [TestPlan] = testPlans.enumerated().map { (index, testPlan) in + let testPlans: [TestPlan] = list.map { testPlan in switch testPlan { case let .file(path): - .file(name: path.basenameWithoutExt, path: path, testTargets: [], isDefault: index == 0) + .file(path: path) case let .generated(name, directory, configurations, defaultOptions, testTargets, targetSelection, missingTargetPolicy): .generated( name: name, @@ -181,7 +218,6 @@ public struct TestAction: Equatable, Codable { defaultOptions: defaultOptions, testTargets: testTargets, targetSelection: targetSelection, - isDefault: index == 0, missingTargetPolicy: missingTargetPolicy ) } diff --git a/Sources/ProjectDescription/TestPlan.swift b/Sources/ProjectDescription/TestPlan.swift index 910525a..aac28d4 100644 --- a/Sources/ProjectDescription/TestPlan.swift +++ b/Sources/ProjectDescription/TestPlan.swift @@ -7,18 +7,7 @@ import Foundation @frozen public enum TestPlan: Hashable, Codable { /// Reference an existing `.xctestplan` file on disk. - /// - /// - Parameters: - /// - name: Name of the test plan. - /// - path: Path to the existing `.xctestplan` file. - /// - testTargets: Testable targets the plan runs. - /// - isDefault: Whether this is the default plan. - case file( - name: String, - path: FilePath, - testTargets: [TestableTarget], - isDefault: Bool, - ) + case file(FileTestPlan) /// Describe a test plan that Geko generates during generation. case generated(GeneratedTestPlan) @@ -26,18 +15,18 @@ public enum TestPlan: Hashable, Codable { /// The name of the test plan. public var name: String { switch self { - case let .file(name, _, _, _): - name - case let .generated(plan): - plan.name + case let .file(fileTestPlan): + fileTestPlan.name + case let .generated(generatedTestPlan): + generatedTestPlan.name } } /// The path of the test plan. public var path: FilePath { switch self { - case let .file(_, path, _, _): - path + case let .file(fileTestPlan): + fileTestPlan.path case let .generated(plan): plan.path } @@ -46,20 +35,20 @@ public enum TestPlan: Hashable, Codable { /// The testable targets the test plan runs. public var testTargets: [TestableTarget] { switch self { - case let .file(_, _, testTargets, _): - testTargets - case let .generated(plan): - plan.testTargets.map { $0.target } + case let .file(fileTestPlan): + fileTestPlan.testTargets + case let .generated(generatedTestPlan): + generatedTestPlan.testTargets.map { $0.target } } } /// Whether this is the default test plan. public var isDefault: Bool { switch self { - case let .file(_, _, _, isDefault): - isDefault - case let .generated(plan): - plan.isDefault + case let .file(fileTestPlan): + fileTestPlan.isDefault + case let .generated(generatedTestPlan): + generatedTestPlan.isDefault } } @@ -74,10 +63,7 @@ public enum TestPlan: Hashable, Codable { @available(*, deprecated) public init(path: FilePath, testTargets: [TestableTarget], isDefault: Bool) { self = .file( - name: path.basenameWithoutExt, - path: path, - testTargets: testTargets, - isDefault: isDefault + FileTestPlan(path: path) ) } @@ -85,18 +71,9 @@ public enum TestPlan: Hashable, Codable { /// /// - Parameters: /// - path: Path to the existing `.xctestplan` file. - /// - testTargets: Testable targets the plan runs. - /// - isDefault: Whether this is the default plan. - public static func file( - path: FilePath, - testTargets: [TestableTarget], - isDefault: Bool - ) -> Self { + public static func file(path: FilePath) -> Self { .file( - name: path.basenameWithoutExt, - path: path, - testTargets: testTargets, - isDefault: isDefault + FileTestPlan(path: path) ) } @@ -109,7 +86,6 @@ public enum TestPlan: Hashable, Codable { /// - defaultOptions: Default options applied to the generated test plan. /// - testTargets: Explicit test targets to include in the generated test plan. /// - targetSelection: Scopes that resolve which test targets to include in the generated test plan. - /// - isDefault: Whether this is the default plan. /// - missingTargetPolicy: Policy applied when a target referenced by the generated test plan is missing. public static func generated( name: String, @@ -118,7 +94,6 @@ public enum TestPlan: Hashable, Codable { defaultOptions: GeneratedTestPlan.Options? = nil, testTargets: [GeneratedTestPlanTestableTarget] = [], targetSelection: [TestableTargetSelectionScope] = [], - isDefault: Bool = false, missingTargetPolicy: GeneratedTestPlan.MissingTargetPolicy = .skipTestPlan() ) -> Self { .generated( @@ -129,7 +104,6 @@ public enum TestPlan: Hashable, Codable { defaultOptions: defaultOptions, testTargets: testTargets, targetSelection: targetSelection, - isDefault: isDefault, missingTargetPolicy: missingTargetPolicy ) ) diff --git a/Sources/ProjectDescription/TestableTarget.swift b/Sources/ProjectDescription/TestableTarget.swift index 2ab3e22..a20478a 100644 --- a/Sources/ProjectDescription/TestableTarget.swift +++ b/Sources/ProjectDescription/TestableTarget.swift @@ -11,7 +11,7 @@ public struct TestableTarget: Equatable, Hashable, Codable, ExpressibleByStringI /// Execute tests in random order. public var isRandomExecutionOrdering: Bool /// Enables test coverage - public var isCoverable: Bool + public var isCoverageEnabled: Bool public init( target: TargetReference, @@ -24,7 +24,7 @@ public struct TestableTarget: Equatable, Hashable, Codable, ExpressibleByStringI isSkipped = skipped isParallelizable = parallelizable isRandomExecutionOrdering = randomExecutionOrdering - isCoverable = coverage + isCoverageEnabled = coverage } public init( @@ -36,7 +36,7 @@ public struct TestableTarget: Equatable, Hashable, Codable, ExpressibleByStringI isSkipped = skipped isParallelizable = options.contains(.parallelizable) isRandomExecutionOrdering = options.contains(.randomExecutionOrdering) - isCoverable = options.contains(.coverage) + isCoverageEnabled = options.contains(.coverage) } public init(stringLiteral value: String) { From 609a792c3fad32e8480dad579f02bb8d7bcb3253 Mon Sep 17 00:00:00 2001 From: dmnre Date: Wed, 14 Oct 2026 16:40:44 +0300 Subject: [PATCH 5/6] GeneratedTestPlan refactoring --- .../GeneratedTestPlan.swift | 102 +++++++++--------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/Sources/ProjectDescription/GeneratedTestPlan.swift b/Sources/ProjectDescription/GeneratedTestPlan.swift index a3a6f56..9d800b4 100644 --- a/Sources/ProjectDescription/GeneratedTestPlan.swift +++ b/Sources/ProjectDescription/GeneratedTestPlan.swift @@ -15,22 +15,22 @@ import Foundation public struct GeneratedTestPlanTestableTarget: Codable, Hashable, Sendable, ExpressibleByStringInterpolation { /// The underlying testable target reference that identifies /// which target should be included in the generated test plan. - public let target: TestableTarget + public var target: TestableTarget /// A list of specific tests to include in the plan. /// /// Each entry uses the Xcode test identifier format, e.g. /// `"MyTests/testExample"` or `"MyTests"` for an entire suite. /// When non-empty, only these tests will be executed for this target. /// An empty array means all tests are included (subject to `skippedTests`). - public let selectedTests: [String] + public var selectedTests: [String] /// A list of specific tests to exclude from the plan. /// /// Each entry uses the Xcode test identifier format, e.g. /// `"MyTests/testExample"` or `"MyTests"` for an entire suite. /// These tests will be skipped even if they would otherwise run. /// An empty array means no tests are skipped. - public let skippedTests: [String] - + public var skippedTests: [String] + public init( _ target: TestableTarget, selectedTests: [String] = [], @@ -76,8 +76,8 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { public struct Options: Codable, Hashable, Sendable { /// Arguments Passed On Launch public struct CommandLineArgumentEntry: Codable, Hashable, Sendable { - public let argument: String - public let enabled: Bool? + public var argument: String + public var enabled: Bool? public init(argument: String, enabled: Bool?) { self.argument = argument @@ -91,9 +91,9 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { /// Environment Variables public struct VariableEntity: Codable, Hashable, Sendable { - public let enabled: Bool? - public let key: String - public let value: String + public var enabled: Bool? + public var key: String + public var value: String public init(key: String, value: String, enabled: Bool?) { self.key = key @@ -108,8 +108,8 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { /// Simulated Location public struct LocationScenario: Codable, Hashable, Sendable { - public let identifier: String - public let referenceType: String + public var identifier: String + public var referenceType: String public init(identifier: String, referenceType: String) { self.identifier = identifier @@ -193,8 +193,8 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { /// Address Sanitizer public struct AddressSanitizer: Codable, Hashable, Sendable { - public let detectStackUseAfterReturn: Bool? - public let enabled: Bool + public var detectStackUseAfterReturn: Bool? + public var enabled: Bool public init(detectStackUseAfterReturn: Bool? = nil, enabled: Bool = true) { self.detectStackUseAfterReturn = detectStackUseAfterReturn @@ -213,8 +213,8 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { case failure } - public let severity: Severity? - public let enabled: Bool? + public var severity: Severity? + public var enabled: Bool? public init(severity: Severity?, enabled: Bool?) { self.severity = severity @@ -231,8 +231,8 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { /// Hardware Memory Tagging public struct CheckedAllocations: Codable, Hashable, Sendable { - public let enabled: Bool - public let requiresHardwareAcceleration: Bool + public var enabled: Bool + public var requiresHardwareAcceleration: Bool public init(enabled: Bool = true, requiresHardwareAcceleration: Bool) { self.enabled = enabled @@ -256,67 +256,67 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { } /// Arguments Passed On Launch - public let commandLineArgumentEntries: [CommandLineArgumentEntry]? + public var commandLineArgumentEntries: [CommandLineArgumentEntry]? /// Environment Variables - public let environmentVariableEntries: [VariableEntity]? + public var environmentVariableEntries: [VariableEntity]? /// Target for Variable Expansion - public let targetForVariableExpansion: TargetReference? + public var targetForVariableExpansion: TargetReference? /// Application Language - public let language: String? + public var language: String? /// Application Region - public let region: String? + public var region: String? /// Simulated Location - public let locationScenario: LocationScenario? + public var locationScenario: LocationScenario? /// Automatic Screen Capture - public let uiTestingScreenshotsLifetime: UITestingScreenshotsLifetime? + public var uiTestingScreenshotsLifetime: UITestingScreenshotsLifetime? /// Preferred Capture Format - public let preferredScreenCaptureFormat: PreferredScreenCaptureFormat? + public var preferredScreenCaptureFormat: PreferredScreenCaptureFormat? /// Localization Screenshots - public let areLocalizationScreenshotsEnabled: Bool? + public var areLocalizationScreenshotsEnabled: Bool? /// Distribution - public let distributor: Distributor? + public var distributor: Distributor? /// Attachments - public let userAttachmentLifetime: UserAttachmentLifetime? + public var userAttachmentLifetime: UserAttachmentLifetime? /// Collect Test Diagnostics on Failure - public let diagnosticCollectionPolicy: DiagnosticCollectionPolicy? + public var diagnosticCollectionPolicy: DiagnosticCollectionPolicy? /// XCTest Execution Order - public let testExecutionOrdering: TestExecutionOrdering? + public var testExecutionOrdering: TestExecutionOrdering? /// Test Timeouts - public let testTimeoutsEnabled: Bool? + public var testTimeoutsEnabled: Bool? /// Default Test Execution Time Allowance (s) (default: 600) - public let defaultTestExecutionTimeAllowance: Int? + public var defaultTestExecutionTimeAllowance: Int? /// Maximum Test Execution Time Allowance (s) (default: None) - public let maximumTestExecutionTimeAllowance: Int? + public var maximumTestExecutionTimeAllowance: Int? /// Test Repetition Mode - public let testRepetitionMode: TestRepetitionMode? + public var testRepetitionMode: TestRepetitionMode? /// Maximum Test Repetitions (default: 3) - public let maximumTestRepetitions: Int? + public var maximumTestRepetitions: Int? /// Relaunch Tests for Each Repetition - public let repeatInNewRunnerProcess: Bool? + public var repeatInNewRunnerProcess: Bool? /// Code Coverage - public let codeCoverage: Coverage? + public var codeCoverage: Coverage? /// Address Sanitizer - public let addressSanitizer: AddressSanitizer? + public var addressSanitizer: AddressSanitizer? /// Thread Sanitizer - public let threadSanitizerEnabled: Bool? + public var threadSanitizerEnabled: Bool? /// Undefined Behavior Sanitizer - public let undefinedBehaviorSanitizerEnabled: Bool? + public var undefinedBehaviorSanitizerEnabled: Bool? /// Main Thread Checker - public let mainThreadCheckerDetectionPolicy: RuntimeApiChecking? + public var mainThreadCheckerDetectionPolicy: RuntimeApiChecking? /// Thread Performance Checker - public let threadPerformanceCheckerRuntimeIssueDetection: RuntimeApiChecking? + public var threadPerformanceCheckerRuntimeIssueDetection: RuntimeApiChecking? /// Other Runtime Issues - public let runtimeIssueDetection: RuntimeApiChecking? + public var runtimeIssueDetection: RuntimeApiChecking? /// Hardware Memory Tagging - public let checkedAllocations: CheckedAllocations? + public var checkedAllocations: CheckedAllocations? /// Malloc Scribble - public let mallocScribbleEnabled: Bool? + public var mallocScribbleEnabled: Bool? /// Malloc Guard Edges - public let mallocGuardEdgesEnabled: Bool? + public var mallocGuardEdgesEnabled: Bool? /// Zombie Objects - public let nsZombieEnabled: Bool? + public var nsZombieEnabled: Bool? /// Malloc Stack Logging - public let mallocStackLoggingOptions: MallocStackLoggingOptions? + public var mallocStackLoggingOptions: MallocStackLoggingOptions? public init( commandLineArgumentEntries: [CommandLineArgumentEntry]?, @@ -456,9 +456,9 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { /// A build configuration of the generated test plan. public struct Configuration: Codable, Hashable { /// The name of the configuration. - public let name: String + public var name: String /// The options applied to this configuration. - public let options: Options? + public var options: Options? public init(name: String, options: Options?) { self.name = name @@ -501,7 +501,7 @@ public struct GeneratedTestPlan: Codable, Hashable, Sendable { } /// Name of the test plan - public let name: String + public var name: String /// The path to the folder where the test plan will be generated. If the path is `nil`, it will be written to `Derived/TestPlans/.xctestplan` public var directory: FilePath? /// Configurations From b61639d7e1d2c3b1456f2b00e938049e16c7403f Mon Sep 17 00:00:00 2001 From: dmnre Date: Thu, 15 Oct 2026 11:55:34 +0300 Subject: [PATCH 6/6] remove TestActionTestPlan and TestableTarget.isCoverageEnabled --- Sources/ProjectDescription/TestAction.swift | 24 ++--------- .../TestActionOptions.swift | 24 ----------- .../TestActionTestPlan.swift | 41 ------------------- Sources/ProjectDescription/TestPlan.swift | 10 ++++- .../ProjectDescription/TestableTarget.swift | 7 +--- .../ProjectDescription/TestingOptions.swift | 2 - 6 files changed, 13 insertions(+), 95 deletions(-) delete mode 100644 Sources/ProjectDescription/TestActionTestPlan.swift diff --git a/Sources/ProjectDescription/TestAction.swift b/Sources/ProjectDescription/TestAction.swift index b968ca3..20e6fb4 100644 --- a/Sources/ProjectDescription/TestAction.swift +++ b/Sources/ProjectDescription/TestAction.swift @@ -200,31 +200,14 @@ public struct TestAction: Equatable, Codable { /// - postActions: Actions to execute after running the tests. /// - Returns: A test action. public static func testPlans( - list: [TestActionTestPlan], + list: [TestPlan], configuration: ConfigurationName = .debug, attachDebugger: Bool = true, preActions: [ExecutionAction] = [], postActions: [ExecutionAction] = [] ) -> Self { - let testPlans: [TestPlan] = list.map { testPlan in - switch testPlan { - case let .file(path): - .file(path: path) - case let .generated(name, directory, configurations, defaultOptions, testTargets, targetSelection, missingTargetPolicy): - .generated( - name: name, - directory: directory, - configurations: configurations, - defaultOptions: defaultOptions, - testTargets: testTargets, - targetSelection: targetSelection, - missingTargetPolicy: missingTargetPolicy - ) - } - } - - return Self( - testPlans: testPlans, + Self( + testPlans: list, targets: [], targetSelection: [], arguments: nil, @@ -253,7 +236,6 @@ extension TestAction { set { options.coverage = newValue } } - @available(*, deprecated) public var codeCoverageTargets: [TargetReference] { get { options.codeCoverageTargets } set { options.codeCoverageTargets = newValue } diff --git a/Sources/ProjectDescription/TestActionOptions.swift b/Sources/ProjectDescription/TestActionOptions.swift index 3a4ecb6..91a2b0f 100644 --- a/Sources/ProjectDescription/TestActionOptions.swift +++ b/Sources/ProjectDescription/TestActionOptions.swift @@ -15,7 +15,6 @@ public struct TestActionOptions: Equatable, Codable { public var coverage: Bool /// A list of targets you want to gather the test coverage data for them, which are defined in the project. - @available(*, deprecated) public var codeCoverageTargets: [TargetReference] /// Returns a set of options for a test action. @@ -26,7 +25,6 @@ public struct TestActionOptions: Equatable, Codable { /// - coverage: Whether test coverage should be collected. /// - codeCoverageTargets: List of test targets whose code coverage information should be collected. /// - Returns: A set of options. - @available(*, deprecated) public static func options( language: SchemeLanguage? = nil, region: String? = nil, @@ -42,26 +40,4 @@ public struct TestActionOptions: Equatable, Codable { codeCoverageTargets: codeCoverageTargets ) } - - /// Returns a set of options for a test action. - /// - Parameters: - /// - language: Language used for running the tests. - /// - region: Region used for running the tests. - /// - preferredScreenCaptureFormat: Preferred screen capture format for UI tests results in Xcode 15+ - /// - coverage: Whether test coverage should be collected. - /// - Returns: A set of options. - public static func options( - language: SchemeLanguage? = nil, - region: String? = nil, - preferredScreenCaptureFormat: ScreenCaptureFormat? = nil, - coverage: Bool = false, - ) -> TestActionOptions { - TestActionOptions( - language: language, - region: region, - preferredScreenCaptureFormat: preferredScreenCaptureFormat, - coverage: coverage, - codeCoverageTargets: [] - ) - } } diff --git a/Sources/ProjectDescription/TestActionTestPlan.swift b/Sources/ProjectDescription/TestActionTestPlan.swift deleted file mode 100644 index 0fe9a21..0000000 --- a/Sources/ProjectDescription/TestActionTestPlan.swift +++ /dev/null @@ -1,41 +0,0 @@ -import Foundation - -/// A test plan passed to a test action. -/// -/// A test plan can either reference an existing `.xctestplan` file on disk or describe a -/// generated test plan that Geko creates during generation. -@frozen -public enum TestActionTestPlan: ExpressibleByStringLiteral, ExpressibleByStringInterpolation { - /// Reference an existing `.xctestplan` file on disk. - /// - /// - Parameter path: Path to the existing `.xctestplan` file. - case file(FilePath) - - /// Describe a test plan that Geko generates during generation. - /// - /// - Parameters: - /// - name: Name of the generated test plan. - /// - directory: Directory the test plan is written to. If `nil`, it's written to the derived directory. - /// - configurations: Configurations of the generated test plan. - /// - defaultOptions: Default options applied to the generated test plan. - /// - testTargets: Explicit test targets to include in the generated test plan. - /// - targetSelection: Scopes that resolve which test targets to include in the generated test plan. - /// - missingTargetPolicy: Policy applied when a target referenced by the generated test plan is missing. - case generated( - name: String, - directory: FilePath? = nil, - configurations: [GeneratedTestPlan.Configuration] = [], - defaultOptions: GeneratedTestPlan.Options? = nil, - testTargets: [GeneratedTestPlanTestableTarget] = [], - targetSelection: [TestableTargetSelectionScope] = [], - missingTargetPolicy: GeneratedTestPlan.MissingTargetPolicy = .skipTestPlan() - ) - - public init(_ value: StringLiteralType) { - self = .file(FilePath(value)) - } - - public init(stringLiteral value: String) { - self = .file(FilePath(stringLiteral: value)) - } -} \ No newline at end of file diff --git a/Sources/ProjectDescription/TestPlan.swift b/Sources/ProjectDescription/TestPlan.swift index aac28d4..6dc5572 100644 --- a/Sources/ProjectDescription/TestPlan.swift +++ b/Sources/ProjectDescription/TestPlan.swift @@ -5,7 +5,7 @@ import Foundation /// A test plan can reference an existing `.xctestplan` file on disk (``file``) or be declared /// declaratively and generated by Geko (``generated``). @frozen -public enum TestPlan: Hashable, Codable { +public enum TestPlan: Hashable, Codable, ExpressibleByStringLiteral, ExpressibleByStringInterpolation { /// Reference an existing `.xctestplan` file on disk. case file(FileTestPlan) @@ -67,6 +67,14 @@ public enum TestPlan: Hashable, Codable { ) } + public init(_ value: StringLiteralType) { + self = .file(path: FilePath(value)) + } + + public init(stringLiteral value: String) { + self = .file(path: FilePath(stringLiteral: value)) + } + /// Creates a test plan that references an existing `.xctestplan` file on disk. /// /// - Parameters: diff --git a/Sources/ProjectDescription/TestableTarget.swift b/Sources/ProjectDescription/TestableTarget.swift index a20478a..dda1d7c 100644 --- a/Sources/ProjectDescription/TestableTarget.swift +++ b/Sources/ProjectDescription/TestableTarget.swift @@ -10,21 +10,17 @@ public struct TestableTarget: Equatable, Hashable, Codable, ExpressibleByStringI public var isParallelizable: Bool /// Execute tests in random order. public var isRandomExecutionOrdering: Bool - /// Enables test coverage - public var isCoverageEnabled: Bool public init( target: TargetReference, skipped: Bool = false, parallelizable: Bool = false, - randomExecutionOrdering: Bool = false, - coverage: Bool = false + randomExecutionOrdering: Bool = false ) { self.target = target isSkipped = skipped isParallelizable = parallelizable isRandomExecutionOrdering = randomExecutionOrdering - isCoverageEnabled = coverage } public init( @@ -36,7 +32,6 @@ public struct TestableTarget: Equatable, Hashable, Codable, ExpressibleByStringI isSkipped = skipped isParallelizable = options.contains(.parallelizable) isRandomExecutionOrdering = options.contains(.randomExecutionOrdering) - isCoverageEnabled = options.contains(.coverage) } public init(stringLiteral value: String) { diff --git a/Sources/ProjectDescription/TestingOptions.swift b/Sources/ProjectDescription/TestingOptions.swift index bb4a279..11c10aa 100644 --- a/Sources/ProjectDescription/TestingOptions.swift +++ b/Sources/ProjectDescription/TestingOptions.swift @@ -10,6 +10,4 @@ public struct TestingOptions: OptionSet, Codable, Equatable, Hashable { public static let parallelizable = TestingOptions(rawValue: 1 << 0) /// Execute tests in random order public static let randomExecutionOrdering = TestingOptions(rawValue: 1 << 1) - /// Enable code coverage - public static let coverage = TestingOptions(rawValue: 1 << 2) }