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/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 new file mode 100644 index 0000000..9d800b4 --- /dev/null +++ b/Sources/ProjectDescription/GeneratedTestPlan.swift @@ -0,0 +1,548 @@ +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 `GeneratedTestPlanTestableTarget` is expected: +/// +/// ```swift +/// let targets: [GeneratedTestPlanTestableTarget] = [ +/// "MyFeatureTests", +/// .target("MyOtherTests", skippedTests: ["MyOtherTests/testFlaky"]), +/// ] +/// ``` +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 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 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 var 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 ``GeneratedTestPlanTestableTarget`` 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 ``GeneratedTestPlanTestableTarget``. + public static func target( + _ target: TestableTarget, + selectedTests: [String] = [], + skippedTests: [String] = [], + ) -> Self { + .init(target, selectedTests: selectedTests, skippedTests: skippedTests) + } +} + +/// 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, Hashable, Sendable { + public var argument: String + public var enabled: Bool? + + 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, Hashable, Sendable { + public var enabled: Bool? + public var key: String + public var value: String + + 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, Hashable, Sendable { + public var identifier: String + public var referenceType: String + + public init(identifier: String, referenceType: String) { + self.identifier = identifier + self.referenceType = referenceType + } + } + /// Automatic Screen Capture + public enum UITestingScreenshotsLifetime: String, Codable, Hashable, 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, Hashable, Sendable { + /// Video (default) + case video + /// Screenshot + case screenshot + } + + /// Distribution + public enum Distributor: String, Codable, Hashable, Sendable { + case appStore = "com.apple.AppStore" + case testFlight = "com.apple.TestFlight" + } + + /// Attachments + public enum UserAttachmentLifetime: String, Codable, Hashable, 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, Hashable, Sendable { + /// Never + case never = "Never" + /// When testing with xcodebuild (default) + case xcodebuild + /// Always + case always = "Always" + } + + /// XCTest Execution Order + public enum TestExecutionOrdering: String, Codable, Hashable, Sendable { + /// Alphabetical (default) + case alphabetical + /// Random + case random + } + + /// Test Repetition Mode + public enum TestRepetitionMode: String, Codable, Hashable, Sendable { + /// Until Failure + case untilFailure + /// Retry On Failure + case retryOnFailure + /// Up Until Maximum Repetitions + case fixedIterations + /// None (default) + 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 var detectStackUseAfterReturn: Bool? + public var 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, Hashable, Sendable { + public enum Severity: String, Codable, Sendable { + case warning + case failure + } + + public var severity: Severity? + public var 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, Hashable, Sendable { + public var enabled: Bool + public var 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, Hashable, Sendable { + /// On + case on + /// Off (default) + case off + } + + /// Arguments Passed On Launch + public var commandLineArgumentEntries: [CommandLineArgumentEntry]? + /// Environment Variables + public var environmentVariableEntries: [VariableEntity]? + /// Target for Variable Expansion + public var targetForVariableExpansion: TargetReference? + /// Application Language + public var language: String? + /// Application Region + public var region: String? + /// Simulated Location + public var locationScenario: LocationScenario? + /// Automatic Screen Capture + public var uiTestingScreenshotsLifetime: UITestingScreenshotsLifetime? + /// Preferred Capture Format + public var preferredScreenCaptureFormat: PreferredScreenCaptureFormat? + /// Localization Screenshots + public var areLocalizationScreenshotsEnabled: Bool? + /// Distribution + public var distributor: Distributor? + /// Attachments + public var userAttachmentLifetime: UserAttachmentLifetime? + /// Collect Test Diagnostics on Failure + public var diagnosticCollectionPolicy: DiagnosticCollectionPolicy? + /// XCTest Execution Order + public var testExecutionOrdering: TestExecutionOrdering? + /// Test Timeouts + public var testTimeoutsEnabled: Bool? + /// Default Test Execution Time Allowance (s) (default: 600) + public var defaultTestExecutionTimeAllowance: Int? + /// Maximum Test Execution Time Allowance (s) (default: None) + public var maximumTestExecutionTimeAllowance: Int? + /// Test Repetition Mode + public var testRepetitionMode: TestRepetitionMode? + /// Maximum Test Repetitions (default: 3) + public var maximumTestRepetitions: Int? + /// Relaunch Tests for Each Repetition + public var repeatInNewRunnerProcess: Bool? + /// Code Coverage + public var codeCoverage: Coverage? + /// Address Sanitizer + public var addressSanitizer: AddressSanitizer? + /// Thread Sanitizer + public var threadSanitizerEnabled: Bool? + /// Undefined Behavior Sanitizer + public var undefinedBehaviorSanitizerEnabled: Bool? + /// Main Thread Checker + public var mainThreadCheckerDetectionPolicy: RuntimeApiChecking? + /// Thread Performance Checker + public var threadPerformanceCheckerRuntimeIssueDetection: RuntimeApiChecking? + /// Other Runtime Issues + public var runtimeIssueDetection: RuntimeApiChecking? + /// Hardware Memory Tagging + public var checkedAllocations: CheckedAllocations? + /// Malloc Scribble + public var mallocScribbleEnabled: Bool? + /// Malloc Guard Edges + public var mallocGuardEdgesEnabled: Bool? + /// Zombie Objects + public var nsZombieEnabled: Bool? + /// Malloc Stack Logging + public var mallocStackLoggingOptions: MallocStackLoggingOptions? + + public init( + commandLineArgumentEntries: [CommandLineArgumentEntry]?, + environmentVariableEntries: [VariableEntity]?, + targetForVariableExpansion: TargetReference?, + 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: Coverage?, + 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 + 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 static func options( + commandLineArgumentEntries: [CommandLineArgumentEntry]? = nil, + environmentVariableEntries: [VariableEntity]? = nil, + targetForVariableExpansion: TargetReference? = 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: Coverage? = 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 + ) + } + } + + /// A build configuration of the generated test plan. + public struct Configuration: Codable, Hashable { + /// The name of the configuration. + public var name: String + /// The options applied to this configuration. + public var options: Options? + + public init(name: String, options: Options?) { + self.name = name + self.options = options + } + + public static func configuration(name: String, options: Options? = nil) -> Self { + Self(name: name, options: options) + } + } + + @frozen + public enum MissingTargetPolicy: Codable, Hashable, Sendable { + @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 + 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 + public var configurations: [Configuration] + /// Default options + 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] + /// 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, + directory: FilePath?, + configurations: [Configuration], + defaultOptions: Options?, + testTargets: [GeneratedTestPlanTestableTarget], + targetSelection: [TestableTargetSelectionScope], + missingTargetPolicy: MissingTargetPolicy + ) { + self.name = name.split(separator: ".").first.map(String.init) ?? name + self.directory = directory + self.configurations = configurations + self.defaultOptions = defaultOptions + self.testTargets = testTargets + self.targetSelection = targetSelection + self.missingTargetPolicy = missingTargetPolicy + + // Internal + self.path = "/" + self.isDefault = false + } +} 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..20e6fb4 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, @@ -149,6 +162,7 @@ public struct TestAction: Equatable, Codable { /// - 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, @@ -161,6 +175,41 @@ public struct TestAction: Equatable, Codable { 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. + /// - 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. + public static func testPlans( + list: [TestPlan], + configuration: ConfigurationName = .debug, + attachDebugger: Bool = true, + preActions: [ExecutionAction] = [], + postActions: [ExecutionAction] = [] + ) -> Self { + Self( + testPlans: list, + targets: [], + targetSelection: [], arguments: nil, configuration: configuration, attachDebugger: attachDebugger, diff --git a/Sources/ProjectDescription/TestPlan.swift b/Sources/ProjectDescription/TestPlan.swift index 3acb4f6..6dc5572 100644 --- a/Sources/ProjectDescription/TestPlan.swift +++ b/Sources/ProjectDescription/TestPlan.swift @@ -1,15 +1,119 @@ 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, ExpressibleByStringLiteral, ExpressibleByStringInterpolation { + /// Reference an existing `.xctestplan` file on disk. + case file(FileTestPlan) + /// 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(fileTestPlan): + fileTestPlan.name + case let .generated(generatedTestPlan): + generatedTestPlan.name + } + } + + /// The path of the test plan. + public var path: FilePath { + switch self { + case let .file(fileTestPlan): + fileTestPlan.path + case let .generated(plan): + plan.path + } + } + + /// The testable targets the test plan runs. + public var testTargets: [TestableTarget] { + switch self { + 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(fileTestPlan): + fileTestPlan.isDefault + case let .generated(generatedTestPlan): + generatedTestPlan.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( + FileTestPlan(path: path) + ) + } + + 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: + /// - path: Path to the existing `.xctestplan` file. + public static func file(path: FilePath) -> Self { + .file( + FileTestPlan(path: path) + ) + } + + /// 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. + /// - 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] = [], + missingTargetPolicy: GeneratedTestPlan.MissingTargetPolicy = .skipTestPlan() + ) -> Self { + .generated( + GeneratedTestPlan( + name: name, + directory: directory, + configurations: configurations, + defaultOptions: defaultOptions, + testTargets: testTargets, + targetSelection: targetSelection, + missingTargetPolicy: missingTargetPolicy + ) + ) } } diff --git a/Sources/ProjectDescription/TestableTarget.swift b/Sources/ProjectDescription/TestableTarget.swift index 06147ee..dda1d7c 100644 --- a/Sources/ProjectDescription/TestableTarget.swift +++ b/Sources/ProjectDescription/TestableTarget.swift @@ -23,6 +23,17 @@ public struct TestableTarget: Equatable, Hashable, Codable, ExpressibleByStringI isRandomExecutionOrdering = randomExecutionOrdering } + public init( + target: TargetReference, + skipped: Bool = false, + options: TestingOptions = TestingOptions() + ) { + self.target = target + isSkipped = skipped + isParallelizable = options.contains(.parallelizable) + isRandomExecutionOrdering = options.contains(.randomExecutionOrdering) + } + public init(stringLiteral value: String) { self.init(target: .init(projectPath: nil, target: value)) } 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/WorkspaceGenerationOptions.swift b/Sources/ProjectDescription/WorkspaceGenerationOptions.swift index 24ff9c6..3950fed 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) public var autogeneratedWorkspaceSchemes: AutogeneratedWorkspaceSchemes /// Allows to suppress warnings in Xcode about updates to recommended settings added in or below the specified Xcode @@ -134,7 +135,7 @@ extension Workspace { commonSettings: [CommonSettings], configurations: [String: BuildConfiguration.Variant], // TODO: Remove default value in next major release - suppressObjcDuplicateClassWarningsDuringPluginLoading: Bool = true + suppressObjcDuplicateClassWarningsDuringPluginLoading: Bool = true, ) { self.enableAutomaticXcodeSchemes = enableAutomaticXcodeSchemes self.autogeneratedWorkspaceSchemes = autogeneratedWorkspaceSchemes @@ -156,7 +157,7 @@ extension Workspace { autogenerateLocalPodsSchemes: AutogenerateLocalPodsSchemes = .disabled, commonSettings: [CommonSettings] = [], configurations: [String: BuildConfiguration.Variant] = ["Debug": .debug, "Release": .release], - suppressObjcDuplicateClassWarningsDuringPluginLoading: Bool = true + suppressObjcDuplicateClassWarningsDuringPluginLoading: Bool = true, ) -> Self { GenerationOptions( enableAutomaticXcodeSchemes: enableAutomaticXcodeSchemes, @@ -167,7 +168,7 @@ extension Workspace { autogenerateLocalPodsSchemes: autogenerateLocalPodsSchemes, commonSettings: commonSettings, configurations: configurations, - suppressObjcDuplicateClassWarningsDuringPluginLoading: suppressObjcDuplicateClassWarningsDuringPluginLoading + suppressObjcDuplicateClassWarningsDuringPluginLoading: suppressObjcDuplicateClassWarningsDuringPluginLoading, ) } }