diff --git a/Sources/Basics/Collections/IdentifiableSet.swift b/Sources/Basics/Collections/IdentifiableSet.swift index 5a7f1ed3249..70b80355823 100644 --- a/Sources/Basics/Collections/IdentifiableSet.swift +++ b/Sources/Basics/Collections/IdentifiableSet.swift @@ -83,7 +83,7 @@ public struct IdentifiableSet: Collection { public func intersection(_ otherSequence: some Sequence) -> Self { let keysToRemove = Set(self.storage.keys).subtracting(otherSequence.map(\.id)) - var result = Self() + var result = self for key in keysToRemove { result.storage.removeValue(forKey: key) } @@ -101,6 +101,14 @@ public struct IdentifiableSet: Collection { public func contains(id: Element.ID) -> Bool { self.storage.keys.contains(id) } + + public mutating func remove(id: Element.ID) -> Element? { + self.storage.removeValue(forKey: id) + } + + public mutating func remove(_ member: Element) -> Element? { + self.storage.removeValue(forKey: member.id) + } } extension OrderedDictionary where Value: Identifiable, Key == Value.ID { diff --git a/Sources/PackageGraph/GraphLoadingNode.swift b/Sources/PackageGraph/GraphLoadingNode.swift index e0c4bb7a173..ec286e0f2dd 100644 --- a/Sources/PackageGraph/GraphLoadingNode.swift +++ b/Sources/PackageGraph/GraphLoadingNode.swift @@ -30,13 +30,13 @@ public struct GraphLoadingNode: Equatable, Hashable { public let productFilter: ProductFilter /// The enabled traits for this package. - package var enabledTraits: Set + package var enabledTraits: EnabledTraits public init( identity: PackageIdentity, manifest: Manifest, productFilter: ProductFilter, - enabledTraits: Set + enabledTraits: EnabledTraits ) throws { self.identity = identity self.manifest = manifest diff --git a/Sources/PackageGraph/ModulesGraph+Loading.swift b/Sources/PackageGraph/ModulesGraph+Loading.swift index 2ea14951129..1e06fef53c7 100644 --- a/Sources/PackageGraph/ModulesGraph+Loading.swift +++ b/Sources/PackageGraph/ModulesGraph+Loading.swift @@ -399,7 +399,7 @@ private func createResolvedPackages( return ResolvedPackageBuilder( package, productFilter: node.productFilter, - enabledTraits: node.enabledTraits /*?? []*/, + enabledTraits: node.enabledTraits, isAllowedToVendUnsafeProducts: isAllowedToVendUnsafeProducts, allowedToOverride: allowedToOverride, platformVersionProvider: platformVersionProvider @@ -1438,7 +1438,7 @@ private final class ResolvedPackageBuilder: ResolvedBuilder { var products: [ResolvedProductBuilder] = [] /// The enabled traits of this package. - var enabledTraits: Set + var enabledTraits: EnabledTraits /// The dependencies of this package. var dependencies: [ResolvedPackageBuilder] = [] @@ -1462,7 +1462,7 @@ private final class ResolvedPackageBuilder: ResolvedBuilder { init( _ package: Package, productFilter: ProductFilter, - enabledTraits: Set, + enabledTraits: EnabledTraits, isAllowedToVendUnsafeProducts: Bool, allowedToOverride: Bool, platformVersionProvider: PlatformVersionProvider @@ -1485,7 +1485,7 @@ private final class ResolvedPackageBuilder: ResolvedBuilder { defaultLocalization: self.defaultLocalization, supportedPlatforms: self.supportedPlatforms, dependencies: self.dependencies.map(\.package.identity), - enabledTraits: self.enabledTraits, + enabledTraits: self.enabledTraits.names, modules: modules, products: products, registryMetadata: self.registryMetadata, diff --git a/Sources/PackageGraph/ModulesGraph.swift b/Sources/PackageGraph/ModulesGraph.swift index a5df0fb46e4..5df55390e6f 100644 --- a/Sources/PackageGraph/ModulesGraph.swift +++ b/Sources/PackageGraph/ModulesGraph.swift @@ -466,7 +466,8 @@ public func loadModulesGraph( useXCBuildFileRules: Bool = false, customXCTestMinimumDeploymentTargets: [PackageModel.Platform: PlatformVersion]? = .none, observabilityScope: ObservabilityScope, - traitConfiguration: TraitConfiguration = .default + traitConfiguration: TraitConfiguration = .default, + enabledTraitsMap: EnabledTraitsMap = .init() ) throws -> ModulesGraph { let rootManifests = manifests.filter(\.packageKind.isRoot).spm_createDictionary { ($0.path, $0) } let externalManifests = try manifests.filter { !$0.packageKind.isRoot } @@ -479,70 +480,6 @@ public func loadModulesGraph( let packages = Array(rootManifests.keys) - let manifestMap = manifests.reduce(into: [PackageIdentity: Manifest]()) { manifestMap, manifest in - manifestMap[manifest.packageIdentity] = manifest - } - - // Note: The following is a copy of the existing `Workspace.precomputeTraits` method - func precomputeTraits( - _ enabledTraitsMap: EnabledTraitsMap, - _ topLevelManifests: [Manifest], - _ manifestMap: [PackageIdentity: Manifest] - ) throws -> [PackageIdentity: Set] { - var visited: Set = [] - var enabledTraitsMap = enabledTraitsMap - - func dependencies(of parent: Manifest, _ productFilter: ProductFilter = .everything) throws { - let parentTraits = enabledTraitsMap[parent.packageIdentity] - let requiredDependencies = try parent.dependenciesRequired(for: productFilter, parentTraits) - let guardedDependencies = parent.dependenciesTraitGuarded(withEnabledTraits: parentTraits) - - _ = try (requiredDependencies + guardedDependencies).compactMap({ dependency in - return try manifestMap[dependency.identity].flatMap({ manifest in - - let explicitlyEnabledTraits = dependency.traits?.filter { - guard let condition = $0.condition else { return true } - return condition.isSatisfied(by: parentTraits) - }.map(\.name) - - if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { - let calculatedTraits = try manifest.enabledTraits( - using: enabledTraitsSet, - .init(parent) - ) - enabledTraitsMap[dependency.identity] = calculatedTraits - } - - let result = visited.insert(dependency.identity) - if result.inserted { - try dependencies(of: manifest, dependency.productFilter) - } - - return manifest - }) - }) - } - - for manifest in topLevelManifests { - // Track already-visited manifests to avoid cycles - let result = visited.insert(manifest.packageIdentity) - if result.inserted { - try dependencies(of: manifest) - } - } - - return enabledTraitsMap.dictionaryLiteral - } - - - // Precompute enabled traits for roots. - var enabledTraitsMap: EnabledTraitsMap = [:] - for root in rootManifests.values { - let enabledTraits = try root.enabledTraits(using: traitConfiguration) - enabledTraitsMap[root.packageIdentity] = enabledTraits - } - enabledTraitsMap = .init(try precomputeTraits(enabledTraitsMap, manifests, manifestMap)) - let input = PackageGraphRootInput(packages: packages, traitConfiguration: traitConfiguration) let graphRoot = try PackageGraphRoot( input: input, diff --git a/Sources/PackageGraph/PackageContainer.swift b/Sources/PackageGraph/PackageContainer.swift index 031799a22da..f1029ad3d5f 100644 --- a/Sources/PackageGraph/PackageContainer.swift +++ b/Sources/PackageGraph/PackageContainer.swift @@ -75,7 +75,7 @@ public protocol PackageContainer { /// - Precondition: `versions.contains(version)` /// - Throws: If the version could not be resolved; this will abort /// dependency resolution completely. - func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: Set) async throws -> [PackageContainerConstraint] + func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: EnabledTraits) async throws -> [PackageContainerConstraint] /// Fetch the declared dependencies for a particular revision. /// @@ -84,12 +84,12 @@ public protocol PackageContainer { /// /// - Throws: If the revision could not be resolved; this will abort /// dependency resolution completely. - func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: Set) async throws -> [PackageContainerConstraint] + func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: EnabledTraits) async throws -> [PackageContainerConstraint] /// Fetch the dependencies of an unversioned package container. /// /// NOTE: This method should not be called on a versioned container. - func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: Set) async throws -> [PackageContainerConstraint] + func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: EnabledTraits) async throws -> [PackageContainerConstraint] /// Get the updated identifier at a bound version. /// @@ -150,11 +150,11 @@ public struct PackageContainerConstraint: Equatable, Hashable { public let products: ProductFilter /// The traits that have been enabled for the package. - public let enabledTraits: Set + public let enabledTraits: EnabledTraits /// Create a constraint requiring the given `container` satisfying the /// `requirement`. - public init(package: PackageReference, requirement: PackageRequirement, products: ProductFilter, enabledTraits: Set = ["default"]) { + public init(package: PackageReference, requirement: PackageRequirement, products: ProductFilter, enabledTraits: EnabledTraits = ["default"]) { self.package = package self.requirement = requirement self.products = products @@ -163,7 +163,7 @@ public struct PackageContainerConstraint: Equatable, Hashable { /// Create a constraint requiring the given `container` satisfying the /// `versionRequirement`. - public init(package: PackageReference, versionRequirement: VersionSetSpecifier, products: ProductFilter, enabledTraits: Set = ["default"]) { + public init(package: PackageReference, versionRequirement: VersionSetSpecifier, products: ProductFilter, enabledTraits: EnabledTraits = ["default"]) { self.init(package: package, requirement: .versionSet(versionRequirement), products: products, enabledTraits: enabledTraits) } diff --git a/Sources/PackageGraph/PackageGraphRoot.swift b/Sources/PackageGraph/PackageGraphRoot.swift index 2bdcff664a0..e99ccd9d332 100644 --- a/Sources/PackageGraph/PackageGraphRoot.swift +++ b/Sources/PackageGraph/PackageGraphRoot.swift @@ -115,7 +115,7 @@ public struct PackageGraphRoot { // If not, then we can omit this dependency if pruning unused dependencies // is enabled. return manifests.values.reduce(false) { result, manifest in - let enabledTraits: Set = enabledTraitsMap[manifest.packageIdentity] + let enabledTraits = enabledTraitsMap[manifest.packageIdentity] if let isUsed = try? manifest.isPackageDependencyUsed(dep, enabledTraits: enabledTraits) { return result || isUsed } @@ -128,7 +128,7 @@ public struct PackageGraphRoot { // FIXME: `dependenciesRequired` modifies manifests and prevents conversion of `Manifest` to a value type let deps = try? manifests.values.lazy .map({ manifest -> [PackageDependency] in - let enabledTraits: Set = enabledTraitsMap[manifest.packageIdentity] + let enabledTraits = enabledTraitsMap[manifest.packageIdentity] return try manifest.dependenciesRequired(for: .everything, enabledTraits) }) .flatMap({ $0 }) @@ -145,7 +145,7 @@ public struct PackageGraphRoot { /// Returns the constraints imposed by root manifests + dependencies. public func constraints(_ enabledTraitsMap: EnabledTraitsMap) throws -> [PackageContainerConstraint] { - var rootEnabledTraits: Set = [] + var rootEnabledTraits: EnabledTraits = [] let constraints = self.packages.map { (identity, package) in let enabledTraits = enabledTraitsMap[identity] rootEnabledTraits.formUnion(enabledTraits) @@ -161,11 +161,11 @@ public struct PackageGraphRoot { .map { dep in let enabledTraits = dep.traits?.filter { guard let condition = $0.condition else { return true } - return condition.isSatisfied(by: rootEnabledTraits) - }.map(\.name) + return condition.isSatisfied(by: rootEnabledTraits.names) + }.map({ EnabledTrait(name: $0.name, setBy: .package(.init(identity: "root"))) }) var enabledTraitsSet = enabledTraitsMap[dep.identity] - enabledTraitsSet.formUnion(enabledTraits.flatMap({ Set($0) }) ?? []) + enabledTraitsSet.formUnion(enabledTraits ?? []) return PackageContainerConstraint( package: dep.packageRef, diff --git a/Sources/PackageGraph/PackageModel+Extensions.swift b/Sources/PackageGraph/PackageModel+Extensions.swift index 19f94f929b0..6c4084e49e0 100644 --- a/Sources/PackageGraph/PackageModel+Extensions.swift +++ b/Sources/PackageGraph/PackageModel+Extensions.swift @@ -35,14 +35,14 @@ extension PackageDependency { extension Manifest { /// Constructs constraints of the dependencies in the raw package. - public func dependencyConstraints(productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + public func dependencyConstraints(productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { return try self.dependenciesRequired(for: productFilter, enabledTraits).map({ let explicitlyEnabledTraits = $0.traits?.filter { guard let condition = $0.condition else { return true } - return condition.isSatisfied(by: enabledTraits) + return condition.isSatisfied(by: enabledTraits.names) }.map(\.name) - let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) ?? ["default"] + let enabledTraitsSet = EnabledTraits(explicitlyEnabledTraits ?? [], setBy: .package(.init(identity: self.packageIdentity, name: self.displayName))) return PackageContainerConstraint( package: $0.packageRef, @@ -60,7 +60,7 @@ extension PackageContainerConstraint { internal func nodes() -> [DependencyResolutionNode] { switch products { case .everything: - return [.root(package: self.package)] + return [.root(package: self.package, enabledTraits: self.enabledTraits)] case .specific: switch products { case .everything: @@ -70,7 +70,7 @@ extension PackageContainerConstraint { if set.isEmpty { // Pointing at the package without a particular product. return [.empty(package: self.package)] } else { - return set.sorted().map { .product($0, package: self.package) } + return set.sorted().map { .product($0, package: self.package, enabledTraits: self.enabledTraits) } } } } diff --git a/Sources/PackageGraph/Resolution/DependencyResolutionNode.swift b/Sources/PackageGraph/Resolution/DependencyResolutionNode.swift index 11693a109c7..5c71a8abd30 100644 --- a/Sources/PackageGraph/Resolution/DependencyResolutionNode.swift +++ b/Sources/PackageGraph/Resolution/DependencyResolutionNode.swift @@ -44,7 +44,7 @@ public enum DependencyResolutionNode { /// Since a non‐existent product ends up with only its implicit dependency on its own package, /// only whichever package contains the product will end up adding additional constraints. /// See `ProductFilter` and `Manifest.register(...)`. - case product(String, package: PackageReference, enabledTraits: Set = ["default"]) + case product(String, package: PackageReference, enabledTraits: EnabledTraits = ["default"]) /// A root node. /// @@ -58,7 +58,7 @@ public enum DependencyResolutionNode { /// It is a warning condition, and builds do not actually need these dependencies. /// However, forcing the graph to resolve and fetch them anyway allows the diagnostics passes access /// to the information needed in order to provide actionable suggestions to help the user stitch up the dependency declarations properly. - case root(package: PackageReference, enabledTraits: Set = ["default"]) + case root(package: PackageReference, enabledTraits: EnabledTraits = ["default"]) /// The package. public var package: PackageReference { @@ -91,7 +91,7 @@ public enum DependencyResolutionNode { } /// Returns the enabled traits for this node's manifest. - public var enabledTraits: Set { + public var enabledTraits: EnabledTraits { switch self { case .root(_, let enabledTraits), .product(_, _, let enabledTraits): return enabledTraits diff --git a/Sources/PackageGraph/Resolution/PubGrub/PubGrubDependencyResolver.swift b/Sources/PackageGraph/Resolution/PubGrub/PubGrubDependencyResolver.swift index 801162636d8..a8ae0399409 100644 --- a/Sources/PackageGraph/Resolution/PubGrub/PubGrubDependencyResolver.swift +++ b/Sources/PackageGraph/Resolution/PubGrub/PubGrubDependencyResolver.swift @@ -357,7 +357,7 @@ public struct PubGrubDependencyResolver { } for dependency in try await container.underlying - .getUnversionedDependencies(productFilter: node.productFilter, constraint.enabledTraits) + .getUnversionedDependencies(productFilter: node.productFilter, node.enabledTraits) { if let versionedBasedConstraints = VersionBasedConstraint.constraints(dependency) { for constraint in versionedBasedConstraints { @@ -431,7 +431,7 @@ public struct PubGrubDependencyResolver { var unprocessedDependencies = try await container.underlying.getDependencies( at: revisionForDependencies, productFilter: constraint.products, - constraint.enabledTraits + node.enabledTraits ) if let sharedRevision = node.revisionLock(revision: revision) { unprocessedDependencies.append(sharedRevision) diff --git a/Sources/PackageLoading/PackageBuilder.swift b/Sources/PackageLoading/PackageBuilder.swift index 08535c82807..77169042aa0 100644 --- a/Sources/PackageLoading/PackageBuilder.swift +++ b/Sources/PackageLoading/PackageBuilder.swift @@ -388,7 +388,7 @@ public final class PackageBuilder { private var swiftVersionCache: SwiftLanguageVersion? = nil /// The enabled traits of this package. - private let enabledTraits: Set + private let enabledTraits: EnabledTraits /// Create a builder for the given manifest and package `path`. /// @@ -414,7 +414,7 @@ public final class PackageBuilder { createREPLProduct: Bool = false, fileSystem: FileSystem, observabilityScope: ObservabilityScope, - enabledTraits: Set + enabledTraits: EnabledTraits ) { self.identity = identity self.manifest = manifest @@ -1151,7 +1151,7 @@ public final class PackageBuilder { // Process each setting. for setting in target.settings { - if let traits = setting.condition?.traits, traits.intersection(self.enabledTraits).isEmpty { + if let traits = setting.condition?.traits, traits.intersection(self.enabledTraits.names).isEmpty { // The setting is currently not enabled so we should skip it continue } diff --git a/Sources/PackageModel/CMakeLists.txt b/Sources/PackageModel/CMakeLists.txt index aa8848b4155..cd9a4b9f8cb 100644 --- a/Sources/PackageModel/CMakeLists.txt +++ b/Sources/PackageModel/CMakeLists.txt @@ -14,7 +14,7 @@ add_library(PackageModel BuildSettings.swift DependencyMapper.swift Diagnostics.swift - EnabledTraitsMap.swift + EnabledTrait.swift IdentityResolver.swift InstalledSwiftPMConfiguration.swift Manifest/Manifest.swift diff --git a/Sources/PackageModel/EnabledTrait.swift b/Sources/PackageModel/EnabledTrait.swift new file mode 100644 index 00000000000..f5692528f98 --- /dev/null +++ b/Sources/PackageModel/EnabledTrait.swift @@ -0,0 +1,606 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Basics + +// MARK: - EnabledTraitsMap + +/// A wrapper struct for a dictionary that stores the transitively enabled traits for each package. +/// This struct implicitly omits adding `default` traits to its storage, and returns `nil` if it +/// there is no existing entry for a given package, since if there are no explicitly enabled traits +/// set by anything else a package will then default to its `default` traits, if they exist. +/// +/// ## Union Behavior +/// When setting traits via the subscript setter (e.g., `map[packageId] = ["trait1", "trait2"]`), +/// the new traits are **unified** with any existing traits for that package, rather than +/// replacing them. This means multiple assignments to the same package will accumulate +/// all traits into a union. If the same trait name is set multiple times with different setters, the +/// setters are merged together. +/// +/// Example: +/// ```swift +/// var traits = EnabledTraitsMap() +/// traits[packageId] = ["Apple", "Banana"] +/// traits[packageId] = ["Coffee", "Chocolate"] +/// +/// // traits[packageId] now contains all four traits: +/// print(traits[packageId]) +/// // Output: ["Apple", "Banana", "Coffee", "Chocolate"] +/// ``` +/// +/// ## Disablers +/// When a package or trait configuration explicitly sets an empty trait set (`[]`) for another package, +/// this is tracked as a "disabler" to record the intent to disable default traits. Disablers coexist +/// with the unified trait system—a package can have both recorded disablers AND explicitly enabled +/// traits. This allows the system to distinguish between "no traits specified" versus "default traits +/// explicitly disabled but other traits may be enabled by different parents." +/// +/// Only packages (via `Setter.package`) and trait configurations (via `Setter.traitConfiguration`) +/// can disable default traits. Traits themselves cannot disable other packages' default traits. +/// +/// Example: +/// ```swift +/// var traits = EnabledTraitsMap() +/// let dependencyId = PackageIdentity(stringLiteral: "MyDependency") +/// let parent1 = PackageIdentity(stringLiteral: "Parent1") +/// let parent2 = PackageIdentity(stringLiteral: "Parent2") +/// +/// // Parent1 explicitly disables default traits +/// traits[dependencyId] = EnabledTraits([], setBy: .package(.init(identity: parent1))) +/// +/// // Parent2 enables specific traits for the same dependency +/// traits[dependencyId] = EnabledTraits(["MyTrait"], setBy: .package(.init(identity: parent2))) +/// +/// // Query disablers to see who disabled defaults +/// print(traits[disablersFor: dependencyId]) // Contains .package(Parent1) +/// +/// // The dependency has "MyTrait" trait enabled (unified from Parent2) +/// print(traits[dependencyId]) // Output: ["MyTrait"] +/// ``` +/// +/// ## Default Setters +/// When a parent package or trait configuration explicitly requests the`default` trait (or leaves the set of +/// traits unspecified), those setters are tracked separately. Query these using the `defaultSettersFor` subscript. +public struct EnabledTraitsMap { + public typealias Key = PackageIdentity + public typealias Value = EnabledTraits + + private struct Storage { + /// Storage for explicitly enabled traits per package. Omits packages with only the "default" trait. + var traits: [PackageIdentity: EnabledTraits] = [:] + + /// Tracks setters that explicitly disabled default traits (via []) for each package. + var _disablers: [PackageIdentity: Set] = [:] + + /// Tracks setters that requested default traits for each package. + /// This is used when a parent doesn't specify traits, meaning it wants the dependency to use its defaults, + /// or when the `default` trait is explicitly requested. + var _defaultSetters: [PackageIdentity: Set] = [:] + + init() { } + + init(_ traits: [PackageIdentity: EnabledTraits]) { + self.traits = traits + } + } + + private var storage = ThreadSafeBox(Storage()) + + public init() { } + + public subscript(key: String) -> EnabledTraits { + get { self[PackageIdentity(key)] } + set { self[PackageIdentity(key)] = newValue } + } + + public subscript(key: PackageIdentity) -> EnabledTraits { + get { storage.get()?.traits[key] ?? ["default"] } + set { + storage.mutate { state -> Storage? in + guard var state = state else { + return Storage() + } + + // Omit adding "default" explicitly, since the map returns "default" + // if there are no explicit traits enabled. This will allow us to check + // for nil entries in the stored dictionary, which tells us whether + // traits have been explicitly enabled or not. + // + // However, if "default" is explicitly set by a parent (has setters), + // track it in the `defaultSetters` property. + guard !(newValue == .defaults && !newValue.isExplicitlySetDefault) else { + return state + } + + // Track default setters + if newValue.isExplicitlySetDefault { + if let defaultSetter = newValue.first?.setters.first { + state._defaultSetters[key, default: []].insert(defaultSetter) + } + if state.traits[key] == [] { + state.traits[key] = nil + } + return state + } + + // Track disablers + if newValue.isEmpty, let disabler = newValue.disabledBy { + state._disablers[key, default: []].insert(disabler) + } + + // Union or create; the set of enabled traits is strictly additive. + if state.traits[key] == nil { + state.traits[key] = newValue + } else { + state.traits[key]?.formUnion(newValue) + } + + return state + } + } + } + + /// Returns the set of setters that explicitly disabled default traits for a package. + /// + /// When a parent package or trait configuration sets an empty trait array (`[]`) for a package, + /// that setter is tracked as a "disabler" to record the intent to disable default traits. + /// + /// - Parameter key: The package identity to query. + /// - Returns: The set of setters that disabled default traits, or `nil` if no disablers exist. + public subscript(disablersFor key: PackageIdentity) -> Set? { + storage.get()?._disablers[key] + } + + /// Returns the set of setters that explicitly disabled default traits for a package identified by a string. + /// + /// This is a convenience subscript that converts the string key to a `PackageIdentity`. + /// + /// - Parameter key: The package identity string to query. + /// - Returns: The set of setters that disabled default traits, or `nil` if no disablers exist. + public subscript(disablersFor key: String) -> Set? { + self[disablersFor: .init(key)] + } + + /// Returns the set of setters that requested default traits for a package. + /// + /// When a parent package or trait configuration sets default traits or leaves + /// traits unspecified, those setters are tracked. + /// + /// - Parameter key: The package identity to query. + /// - Returns: The set of setters that requested default traits, or `nil` if no default setters exist. + public subscript(defaultSettersFor key: PackageIdentity) -> Set? { + storage.get()?._defaultSetters[key] + } + + /// Returns the set of setters that requested default traits for a package identified by a string. + /// + /// This is a convenience subscript that converts the string key to a `PackageIdentity`. + /// + /// - Parameter key: The package identity string to query. + /// - Returns: The set of setters that requested default traits, or `nil` if no default setters exist. + public subscript(defaultSettersFor key: String) -> Set? { + self[defaultSettersFor: .init(key)] + } + + /// Returns a list of traits that were explicitly enabled for a given package. + /// + /// - Parameter key: The package identity to query. + /// - Returns: The explicitly enabled traits, or `nil` if no traits were explicitly set (meaning the package uses defaults). + public subscript(explicitlyEnabledTraitsFor key: PackageIdentity) -> EnabledTraits? { + storage.get()?.traits[key] + } + + /// Returns a list of traits that were explicitly enabled for a given package. + /// + /// This is a convenience subscript that converts the string key to a `PackageIdentity`. + /// + /// - Parameter key: The package identity string to query. + /// - Returns: The explicitly enabled traits, or `nil` if no traits were explicitly set (meaning the package uses defaults). + public subscript(explicitlyEnabledTraitsFor key: String) -> EnabledTraits? { + self[explicitlyEnabledTraitsFor: .init(key)] + } + + /// Returns a dictionary literal representation of the enabled traits map. + public var dictionaryLiteral: [PackageIdentity: EnabledTraits] { + return storage.get()?.traits ?? [:] + } +} + +// MARK: EnabledTraitsMap + ExpressibleByDictionaryLiteral +extension EnabledTraitsMap: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (Key, Value)...) { + for (key, value) in elements { + self[key] = value + } + } + + public init(_ dictionary: [String: Value]) { + let mappedDictionary = dictionary.reduce(into: [Key: Value]()) { result, element in + result[PackageIdentity(element.key)] = element.value + } + + self.storage = .init(.init(mappedDictionary)) + } + + public init(_ dictionary: [Key: Value]) { + self.storage = .init(.init(dictionary)) + } +} + +// MARK: - EnabledTrait + +/// A structure representing a trait that is enabled. The package in which this is enabled on is identified in +/// the EnabledTraitsMap. +/// +/// An enabled trait is a trait that is either explicitly enabled by a user-passed trait configuration from the command line, +/// a parent package that has defined enabled traits for its dependency package, or transitively by another trait (including the default case). +/// +/// An `EnabledTrait` is differentiated by its `name`, and all other data stored in this struct is treated as metadata for +/// convenience. When unifying two `EnabledTrait`s, it will combine the list of setters if the `name`s match. +/// +public struct EnabledTrait: Identifiable { + /// Convenience typealias for a list of `Setter` + public typealias Setters = Set + + /// The identifier for the trait. + public var id: String { name } + + /// The name of the trait. + public let name: String + + /// The list of setters who have enabled this trait. + public var setters: Setters = [] + + public init(name: String, setBy: Setter) { + self.name = name + self.setters = [setBy] + } + + public init(name: String, setBy: [Setter]) { + self.name = name + self.setters = Set(setBy) + } + + /// The packages that have enabled this trait. + public var parentPackages: [Manifest.PackageIdentifier] { + setters.compactMap(\.parentPackage) + } + + /// Returns true if this trait is the "default" trait. + public var isDefault: Bool { + name == "default" + } + + /// Returns true if this trait was enabled by the "default" trait (via `Setter.trait("default")`). + /// This is distinct from `isDefault`, which checks if this trait's name is "default". + public var isSetByDefault: Bool { + self.setters.contains(where: { $0 == .default }) + } + + /// Returns a new `EnabledTrait` that contains a merged list of `Setters` from + /// `self` and the `otherTrait`, only if the traits are equal. Otherwise, returns nil. + /// - Parameter otherTrait: The trait to merge in. + public func unify(_ otherTrait: EnabledTrait) -> EnabledTrait? { + guard self.name == otherTrait.name else { + return nil + } + + var updatedTrait = self + updatedTrait.setters = setters.union(otherTrait.setters) + return updatedTrait + } +} + +// MARK: EnabledTrait.Setter + +extension EnabledTrait { + /// An enumeration that describes how a given trait was set as enabled. + public enum Setter: Hashable, CustomStringConvertible { + case traitConfiguration + case package(Manifest.PackageIdentifier) + case trait(String) + + public var description: String { + switch self { + case .traitConfiguration: + "command-line trait configuration" + case .package(let parent): + "package \(parent.description)" + case .trait(let trait): + "trait \(trait)" + } + } + + /// The identifier of the parent package that defined this trait, if any. + public var parentPackage: Manifest.PackageIdentifier? { + switch self { + case .package(let id): + return id + case .traitConfiguration, .trait: + return nil + } + } + + public var parentTrait: String? { + switch self { + case .trait(let trait): + return trait + case .traitConfiguration, .package: + return nil + } + } + + public static var `default`: Self { + .trait("default") + } + } +} + +// MARK: EnabledTrait + Equatable + +extension EnabledTrait: Equatable { + // When comparing two `EnabledTraits`, if the names are the same then + // we know that these two objects are referring to the same trait of a package. + // In this case, the two objects should be combined into one. + public static func ==(lhs: EnabledTrait, rhs: EnabledTrait) -> Bool { + return lhs.name == rhs.name + } + + public static func ==(lhs: EnabledTrait, rhs: String) -> Bool { + return lhs.name == rhs + } + + public static func ==(lhs: String, rhs: EnabledTrait) -> Bool { + return lhs == rhs.name + } +} + +// MARK: EnabledTrait + Comparable + +extension EnabledTrait: Comparable { + public static func <(lhs: EnabledTrait, rhs: EnabledTrait) -> Bool { + return lhs.name < rhs.name + } +} + +// MARK: EnabledTrait + CustomStringConvertible + +extension EnabledTrait: CustomStringConvertible { + public var description: String { + name + } +} + +// MARK: EnabledTrait + ExpressibleByStringLiteral + +extension EnabledTrait: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.name = value + } +} + +// MARK: - EnabledTraits + +/// A collection wrapper around a set of `EnabledTrait` instances that provides specialized behavior +/// for trait management. This struct ensures that traits with the same name are automatically unified +/// by merging their setters when inserted, maintaining a single entry per unique trait name. It provides +/// convenient set operations like union and intersection, along with collection protocol conformance for +/// easy iteration and manipulation of enabled traits. +/// +/// ## Disabling All Traits +/// An `EnabledTraits` instance can represent a "disabled" state when created with an empty collection +/// and a `Setter`. In this case, the `disabledBy` property returns the setter that disabled default traits, +/// allowing callers to track which parent package or configuration explicitly disabled default traits for a package. +public struct EnabledTraits: Hashable { + public typealias Element = EnabledTrait + public typealias Index = IdentifiableSet.Index + + /// Storage of enabled traits. + private var _traits: IdentifiableSet = [] + + /// This should only ever be set in the case where a parent + /// disables all traits, and an empty set of traits is passed. + private var _disableAllTraitsSetter: EnabledTrait.Setter? = nil + + /// Returns the setter that disabled all traits for a package, if any. + /// This value is set when `EnabledTraits` is initialized with an empty collection, + /// indicating that a parent explicitly disabled all traits rather than leaving them + /// unset. + public var disabledBy: EnabledTrait.Setter? { + _disableAllTraitsSetter + } + + public var areDefaultsEnabled: Bool { + return !_traits.filter(\.isDefault).isEmpty || !_traits.filter(\.isSetByDefault).isEmpty + } + + /// Returns true if this represents an explicitly-set "default" trait (with setters), + /// as opposed to the sentinel `.defaults` value (no setters). + /// This is used to distinguish when a parent package enables default traits + /// either explicitly or when no traits have been specified for a package dependency + /// at all. + public var isExplicitlySetDefault: Bool { + // Check if this equals .defaults (only contains "default" trait) AND has explicit setters + return self == .defaults && _traits.contains(where: { !$0.setters.isEmpty }) + } + + public static var defaults: EnabledTraits { + ["default"] + } + + public init(_ enabledTraits: EnabledTraits) { + self._traits = enabledTraits._traits + } + + private init(_ disabler: EnabledTrait.Setter) { + self._disableAllTraitsSetter = disabler + } + + public init(_ traits: C, setBy setter: EnabledTrait.Setter) where C.Element == String { + guard !traits.isEmpty else { + self.init(setter) + return + } + let enabledTraits = traits.map({ EnabledTrait(name: $0, setBy: setter) }) + self.init(enabledTraits) + } + + private init(_ traits: C) where C.Element == EnabledTrait { + self._traits = IdentifiableSet(traits) + } + + public static func ==(_ lhs: EnabledTraits, _ rhs: EnabledTraits) -> Bool { + lhs._traits.names == rhs._traits.names + } +} + +// MARK: EnabledTraits + ExpressibleByArrayLiteral + +extension EnabledTraits: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: Element...) { + for element in elements { + _traits.insert(element) + } + } +} + +// MARK: EnabledTraits + Collection + +extension EnabledTraits: Collection { + public var startIndex: Index { + return _traits.startIndex + } + + public var endIndex: Index { + return _traits.endIndex + } + + public func index(after i: Index) -> Index { + return _traits.index(after: i) + } + + public subscript(position: Index) -> Element { + return _traits[position] + } + + public mutating func insert(_ newMember: Element) { + _traits.insert(newMember) + } + + public mutating func remove(_ member: Element) -> Element? { + return _traits.remove(member) + } + + public func contains(_ member: Element) -> Bool { + return _traits.contains(member) + } + + public func intersection(_ other: C) -> EnabledTraits where C.Element == String { + self.intersection(other.map(\.asEnabledTrait)) + } + + public func intersection(_ other: C) -> EnabledTraits where C.Element == Self.Element { + let otherSet = IdentifiableSet(other.map({ $0 })) + let intersection = self._traits.intersection(otherSet) + return EnabledTraits(intersection) + } + + public func union(_ other: C) -> EnabledTraits where C.Element == Self.Element { + let unionedTraits = _traits.union(other) + return EnabledTraits(unionedTraits) + } + + public mutating func formUnion(_ other: EnabledTraits) { + self._traits = self.union(other)._traits + } + + public mutating func formUnion(_ other: C) where C.Element == Self.Element { + self.formUnion(.init(other)) + } + + public func map(_ transform: (Self.Element) throws -> Self.Element) rethrows -> EnabledTraits { + let transformedTraits = try _traits.map(transform) + return EnabledTraits(transformedTraits) + } + + public func flatMap(_ transform: (Self.Element) throws -> EnabledTraits) rethrows -> EnabledTraits { + let transformedTraits = try _traits.flatMap(transform) + return EnabledTraits(transformedTraits) + } + + public static func ==(_ lhs: EnabledTraits, _ rhs: C) -> Bool where C.Element == Element { + lhs._traits.names == rhs.names + } + + public static func ==(_ lhs: C, _ rhs: EnabledTraits) -> Bool where C.Element == Element { + lhs.names == rhs._traits.names + } +} + +// MARK: - EnabledTraitConvertible + +/// Represents a type that can be converted into an `EnabledTrait`. +/// This protocol enables conversion between string-like types and `EnabledTrait` instances, +/// allowing for more flexible APIs that can accept either strings or traits interchangeably. +package protocol EnabledTraitConvertible: Equatable { + var asEnabledTrait: EnabledTrait { get } +} + +// MARK: String + EnabledTraitConvertible + +extension String: EnabledTraitConvertible { + package var asEnabledTrait: EnabledTrait { + .init(stringLiteral: self) + } +} + +// MARK: - Collection + EnabledTrait + +extension Collection where Element == EnabledTrait { + public var names: Set { + Set(self.map(\.name)) + } + + public func contains(_ trait: String) -> Bool { + return self.map(\.name).contains(trait) + } + + public func contains(_ trait: Element) -> Bool { + return self.contains(trait.description) + } + + public func joined(separator: String = "") -> String { + names.joined(separator: separator) + } +} + + +// MARK: - IdentifiableSet + EnabledTrait + +extension IdentifiableSet where Element == EnabledTrait { + private mutating func insertTrait(_ member: Element) { + if let oldElement = self.remove(member), let newElement = oldElement.unify(member) { + insert(newElement) + } else { + insert(member) + } + } + + package func union(_ other: C) -> IdentifiableSet where C.Element == Element { + var updatedContents = self + for element in other { + updatedContents.insertTrait(element) + } + return updatedContents + } +} + diff --git a/Sources/PackageModel/EnabledTraitsMap.swift b/Sources/PackageModel/EnabledTraitsMap.swift deleted file mode 100644 index a7338095511..00000000000 --- a/Sources/PackageModel/EnabledTraitsMap.swift +++ /dev/null @@ -1,55 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -/// A wrapper for a dictionary that stores the transitively enabled traits for each package. -public struct EnabledTraitsMap: ExpressibleByDictionaryLiteral { - public typealias Key = PackageIdentity - public typealias Value = Set - - var storage: [PackageIdentity: Set] = [:] - - public init() { } - - public init(dictionaryLiteral elements: (Key, Value)...) { - for (key, value) in elements { - storage[key] = value - } - } - - public init(_ dictionary: [Key: Value]) { - self.storage = dictionary - } - - public subscript(key: PackageIdentity) -> Set { - get { storage[key] ?? ["default"] } - set { - // Omit adding "default" explicitly, since the map returns "default" - // if there is no explicit traits declared. This will allow us to check - // for nil entries in the stored dictionary, which tells us whether - // traits have been explicitly declared. - guard newValue != ["default"] else { return } - if storage[key] == nil { - storage[key] = newValue - } else { - storage[key]?.formUnion(newValue) - } - } - } - - public subscript(explicitlyEnabledTraitsFor key: PackageIdentity) -> Set? { - get { storage[key] } - } - - public var dictionaryLiteral: [PackageIdentity: Set] { - return storage - } -} diff --git a/Sources/PackageModel/Manifest/Manifest+Traits.swift b/Sources/PackageModel/Manifest/Manifest+Traits.swift index a5535e828d4..061d89a3477 100644 --- a/Sources/PackageModel/Manifest/Manifest+Traits.swift +++ b/Sources/PackageModel/Manifest/Manifest+Traits.swift @@ -17,10 +17,15 @@ import Foundation /// Validator methods that check the correctness of traits and their support as defined in the manifest. extension Manifest { - public struct PackageIdentifier: Hashable, CustomStringConvertible { + /// Struct that contains information about a package's identity, as well as its name. + public struct PackageIdentifier: Hashable, CustomStringConvertible, Comparable, ExpressibleByStringLiteral { public var identity: String public var name: String? + public init(identity: PackageIdentity, name: String? = nil) { + self.init(identity: identity.description, name: name) + } + public init(identity: String, name: String? = nil) { self.identity = identity self.name = name @@ -31,6 +36,10 @@ extension Manifest { self.name = parent.displayName } + public init(stringLiteral string: String) { + self.identity = string + } + public var description: String { var result = "'\(identity)'" if let name { @@ -38,6 +47,10 @@ extension Manifest { } return result } + + public static func < (lhs: Manifest.PackageIdentifier, rhs: Manifest.PackageIdentifier) -> Bool { + lhs.identity < rhs.identity + } } /// Determines whether traits are supported for this Manifest. @@ -51,7 +64,7 @@ extension Manifest { if !supportsTraits { throw TraitError.invalidTrait( package: .init(self), - trait: trait.name, + trait: .init(stringLiteral: trait.name), availableTraits: traits.map({ $0.name }) ) } @@ -59,12 +72,12 @@ extension Manifest { return } - try self.validateTrait(trait.name) + try self.validateTrait(EnabledTrait(stringLiteral: trait.name)) } /// Validates a trait by checking that it is defined in the manifest; if not, an error is thrown. - private func validateTrait(_ trait: String, parentPackage: PackageIdentifier? = nil) throws { - guard trait != "default" else { + private func validateTrait(_ trait: EnabledTrait) throws { + guard !trait.isDefault else { if !supportsTraits { throw TraitError.invalidTrait( package: .init(self), @@ -77,12 +90,11 @@ extension Manifest { } // Check if the passed trait is a valid trait. - if self.traits.first(where: { $0.name == trait }) == nil { + if self.traits.first(where: { $0.name == trait.name }) == nil { throw TraitError.invalidTrait( package: .init(self), trait: trait, - availableTraits: self.traits.map({ $0.name }), - parent: parentPackage + availableTraits: self.traits.map({ $0.name }) ) } } @@ -90,16 +102,12 @@ extension Manifest { /// Validates a set of traits that is intended to be enabled for the manifest; if there are any discrepencies in the /// set of enabled traits and whether the manifest defines these traits (or if it defines any traits at all), then an /// error indicating the issue will be thrown. - private func validateEnabledTraits( - _ explicitlyEnabledTraits: Set, - _ parentPackage: PackageIdentifier? = nil - ) throws { + private func validateEnabledTraits(_ explicitlyEnabledTraits: EnabledTraits) throws { guard supportsTraits else { if explicitlyEnabledTraits != ["default"] { throw TraitError.traitsNotSupported( - parent: parentPackage, package: .init(self), - explicitlyEnabledTraits: explicitlyEnabledTraits.map({ $0 }) + explicitlyEnabledTraits: explicitlyEnabledTraits ) } @@ -110,7 +118,7 @@ extension Manifest { // Validate each trait to assure it's defined in the current package. for trait in enabledTraits { - try validateTrait(trait, parentPackage: parentPackage) + try validateTrait(trait) } let areDefaultsEnabled = enabledTraits.contains("default") @@ -120,9 +128,8 @@ extension Manifest { // We throw an error when default traits are disabled for a package without any traits // This allows packages to initially move new API behind traits once. throw TraitError.traitsNotSupported( - parent: parentPackage, package: .init(self), - explicitlyEnabledTraits: enabledTraits.map({ $0 }) + explicitlyEnabledTraits: enabledTraits ) } } @@ -132,15 +139,13 @@ extension Manifest { switch traitConfiguration { case .disableAllTraits: throw TraitError.traitsNotSupported( - parent: nil, package: .init(self), - explicitlyEnabledTraits: [] + explicitlyEnabledTraits: .init([], setBy: .traitConfiguration) ) case .enabledTraits(let traits): throw TraitError.traitsNotSupported( - parent: nil, package: .init(self), - explicitlyEnabledTraits: traits.map({ $0 }) + explicitlyEnabledTraits: EnabledTraits(traits, setBy: .traitConfiguration) ) case .enableAllTraits, .default: return @@ -150,7 +155,7 @@ extension Manifest { // Get the enabled traits; if the trait configuration's `.enabledTraits` returns nil, // we know that it's the `.enableAllTraits` case, since the config does not store // all the defined traits of the manifest itself. - let enabledTraits = traitConfiguration.enabledTraits ?? Set(self.traits.map({ $0.name })) + let enabledTraits: EnabledTraits = traitConfiguration.enabledTraits ?? EnabledTraits(self.traits.map(\.name), setBy: .traitConfiguration) try validateEnabledTraits(enabledTraits) } @@ -162,10 +167,10 @@ extension Manifest { /// Helper methods to calculate states of the manifest and its dependencies when given a set of enabled traits. extension Manifest { /// The default traits as defined in this package as the root. - public var defaultTraits: Set? { + public var defaultTraits: Set? { // First, guard against whether this package actually has traits. guard self.supportsTraits else { return nil } - return self.traits.filter(\.isDefault) + return Set(self.traits.filter(\.isDefault).flatMap(\.enabledTraits)) } /// A map of trait names to the trait description. @@ -177,7 +182,7 @@ extension Manifest { /// Calculates the set of all transitive traits that are enabled for this manifest using the passed trait configuration. /// Since a trait configuration is only used for root packages, this method is intended for use with root packages only. - public func enabledTraits(using traitConfiguration: TraitConfiguration) throws -> Set { + public func enabledTraits(using traitConfiguration: TraitConfiguration) throws -> EnabledTraits { // If this manifest does not support traits, but the passed configuration either // disables default traits or enables non-default traits (i.e. traits that would // not exist for this manifest) then we must throw an error. @@ -186,22 +191,22 @@ extension Manifest { return ["default"] } - var enabledTraits: Set = [] + var enabledTraits: EnabledTraits = [] switch traitConfiguration { case .enableAllTraits: - enabledTraits = Set(traits.map(\.name)) + enabledTraits = EnabledTraits(traits.map(\.name), setBy: .traitConfiguration) case .default: - if let defaultTraits = defaultTraits?.map(\.name) { - enabledTraits = Set(defaultTraits) + if let defaultTraits = defaultTraits { + enabledTraits = EnabledTraits(defaultTraits, setBy: .default) } case .disableAllTraits: return [] case .enabledTraits(let explicitlyEnabledTraits): - enabledTraits = explicitlyEnabledTraits + enabledTraits = EnabledTraits(explicitlyEnabledTraits, setBy: .traitConfiguration) } - if let allEnabledTraits = try? self.enabledTraits(using: enabledTraits, nil) { + if let allEnabledTraits = try? self.enabledTraits(using: enabledTraits) { enabledTraits = allEnabledTraits } @@ -211,18 +216,18 @@ extension Manifest { /// Calculates the set of all transitive traits that are enabled for this manifest using the passed set of /// explicitly enabled traits, and the parent package that defines the enabled traits for this package. /// This method is intended for use with non-root packages. - public func enabledTraits(using explicitlyEnabledTraits: Set = ["default"], _ parentPackage: PackageIdentifier?) throws -> Set { + public func enabledTraits(using explicitlyEnabledTraits: EnabledTraits = ["default"]) throws -> EnabledTraits { // If this manifest does not support traits, but the passed configuration either // disables default traits or enables non-default traits (i.e. traits that would // not exist for this manifest) then we must throw an error. - try validateEnabledTraits(explicitlyEnabledTraits, parentPackage) + try validateEnabledTraits(explicitlyEnabledTraits) guard supportsTraits else { return ["default"] } - var enabledTraits: Set = [] + var enabledTraits: EnabledTraits = [] - if let allEnabledTraits = try? calculateAllEnabledTraits(explictlyEnabledTraits: explicitlyEnabledTraits, parentPackage) { + if let allEnabledTraits = try? calculateAllEnabledTraits(explicitlyEnabledTraits: explicitlyEnabledTraits) { enabledTraits = allEnabledTraits } @@ -230,7 +235,7 @@ extension Manifest { } /// Determines if a trait is enabled with a given set of enabled traits. - public func isTraitEnabled(_ trait: TraitDescription, _ enabledTraits: Set) throws -> Bool { + public func isTraitEnabled(_ trait: TraitDescription, _ enabledTraits: EnabledTraits) throws -> Bool { // First, check that the queried trait is valid. try validateTrait(trait) // Then, check that the list of enabled traits is valid. @@ -238,27 +243,16 @@ extension Manifest { // Special case for dealing with whether a default trait is enabled. guard !trait.isDefault else { - // Check that the manifest defines default traits. + // Check that the manifest defines default traits; if so, + // determine whether the default traits are enabled. if self.traits.contains(where: \.isDefault) { - // If the trait is a default trait, then we must do the following checks: - // - If there exists a list of enabled traits, ensure that the default trait - // is declared in the set. - // - If there is no existing list of enabled traits (nil), and we know that the - // manifest has defined default traits, then just return true. - // - If none of these conditions are met, then defaults aren't enabled and we return false. - if enabledTraits.contains(trait.name) { - return true - } else if enabledTraits.isEmpty { - return true - } else { - return false - } + return enabledTraits.areDefaultsEnabled } // If manifest does not define default traits, then throw an invalid trait error. throw TraitError.invalidTrait( package: .init(self), - trait: trait.name, + trait: EnabledTrait(stringLiteral: trait.name), availableTraits: self.traits.map(\.name) ) } @@ -269,73 +263,50 @@ extension Manifest { return false } - // Special case for dealing with whether a default trait is enabled. - guard !trait.isDefault else { - // Check that the manifest defines default traits. - if self.traits.contains(where: \.isDefault) { - // If the trait is a default trait, then we must do the following checks: - // - If there exists a list of enabled traits, ensure that the default trait - // is declared in the set. - // - If there is no existing list of enabled traits (nil), and we know that the - // manifest has defined default traits, then just return true. - // - If none of these conditions are met, then defaults aren't enabled and we return false. - if enabledTraits.contains(trait.name) { - return true - } else if enabledTraits.isEmpty { - return true - } else { - return false - } - } - - // If manifest does not define default traits, then throw an invalid trait error. - throw TraitError.invalidTrait( - package: .init(self), - trait: trait.name, - availableTraits: self.traits.map(\.name) - ) - } - // Compute all transitively enabled traits. - let allEnabledTraits = try calculateAllEnabledTraits(explictlyEnabledTraits: enabledTraits) + let allEnabledTraits = try calculateAllEnabledTraits(explicitlyEnabledTraits: enabledTraits) return allEnabledTraits.contains(trait.name) } /// Calculates and returns a set of all enabled traits, beginning with a set of explicitly enabled traits (which can either be the default traits of a manifest, or a configuration of enabled traits determined from a user-generated trait configuration) and determines which traits are transitively enabled. - private func calculateAllEnabledTraits( - explictlyEnabledTraits: Set, - _ parentPackage: PackageIdentifier? = nil - ) throws -> Set { - try validateEnabledTraits(explictlyEnabledTraits, parentPackage) + private func calculateAllEnabledTraits(explicitlyEnabledTraits: EnabledTraits) throws -> EnabledTraits { + try validateEnabledTraits(explicitlyEnabledTraits) // This the point where we flatten the enabled traits and resolve the recursive traits - var enabledTraits = explictlyEnabledTraits + var enabledTraits = explicitlyEnabledTraits let areDefaultsEnabled = enabledTraits.remove("default") != nil // We have to enable all default traits if no traits are enabled or the defaults are explicitly enabled - if explictlyEnabledTraits == ["default"] || areDefaultsEnabled { + if explicitlyEnabledTraits == ["default"] || areDefaultsEnabled { if let defaultTraits { - enabledTraits.formUnion(defaultTraits.flatMap(\.enabledTraits)) + let transitiveDefaultTraits = EnabledTraits( + defaultTraits, + setBy: .default + ) + enabledTraits.formUnion(transitiveDefaultTraits) } } + // Initialize before loop to avoid recalculations of computed property + let traitsMap = traitsMap + // Iteratively flatten transitively enabled traits; stop when all transitive traits have been found. while true { - let transitivelyEnabledTraits = try Set( - // We are going to calculate which traits are actually enabled for a node here. To do this - // we have to check if default traits should be used and then flatten all the enabled traits. - enabledTraits - .flatMap { trait in - guard let traitDescription = traitsMap[trait] else { - throw TraitError.invalidTrait( - package: .init(self), - trait: trait, - parent: parentPackage - ) - } - return traitDescription.enabledTraits - } - ) + // We are going to calculate which traits are actually enabled for a node here. To do this + // we have to check if default traits should be used and then flatten all the enabled traits. + let transitivelyEnabledTraits = try enabledTraits.flatMap { trait in + guard let traitDescription = traitsMap[trait.name] else { + throw TraitError.invalidTrait( + package: .init(self), + trait: trait + ) + } + return EnabledTraits( + traitDescription.enabledTraits, + setBy: .trait(traitDescription.name) + ) + } + let appendedList = enabledTraits.union(transitivelyEnabledTraits) if appendedList.count == enabledTraits.count { @@ -349,22 +320,19 @@ extension Manifest { } /// Computes the dependencies that are in use per target in this manifest. - public func usedTargetDependencies(withTraits enabledTraits: Set) throws -> [String: Set] { - try self.targets.reduce(into: [String: Set]()) { depMap, target in + private func usedTargetDependencies(withTraits enabledTraits: EnabledTraits) throws -> [String: Set] { + let enabledTraits = try calculateAllEnabledTraits(explicitlyEnabledTraits: enabledTraits) + return self.targets.reduce(into: [String: Set]()) { depMap, target in let nonTraitDeps = target.dependencies.filter { $0.condition?.traits?.isEmpty ?? true } - let traitGuardedDeps = try target.dependencies.filter { dep in + let traitGuardedDeps = target.dependencies.filter { dep in let traits = dep.condition?.traits ?? [] - // If traits is empty, then we must manually validate the explicitly enabled traits. - if traits.isEmpty { - try validateEnabledTraits(enabledTraits) - } // For each trait that is a condition on this target dependency, assure that - // each one is enabled in the manifest. - return try traits.allSatisfy({ try isTraitEnabled(.init(stringLiteral: $0), enabledTraits) }) + // at least one is enabled in the manifest. + return !traits.intersection(enabledTraits.names).isEmpty } let deps = nonTraitDeps + traitGuardedDeps @@ -373,7 +341,7 @@ extension Manifest { } /// Computes the set of package dependencies that are used by targets of this manifest. - public func usedDependencies(withTraits enabledTraits: Set) throws -> (knownPackage: Set, unknownPackage: Set) { + public func usedDependencies(withTraits enabledTraits: EnabledTraits) throws -> (knownPackage: Set, unknownPackage: Set) { let deps = try self.usedTargetDependencies(withTraits: enabledTraits) .values .flatMap { $0 } @@ -437,7 +405,7 @@ extension Manifest { public func isTargetDependencyEnabled( target: String, _ dependency: TargetDescription.Dependency, - enabledTraits: Set, + enabledTraits: EnabledTraits, ) throws -> Bool { guard self.supportsTraits else { return true } guard let target = self.targetMap[target] else { return false } @@ -459,8 +427,9 @@ extension Manifest { return traitsToEnable.isEmpty || isEnabled } + /// Determines whether a given package dependency is used by this manifest given a set of enabled traits. - public func isPackageDependencyUsed(_ dependency: PackageDependency, enabledTraits: Set) throws -> Bool { + public func isPackageDependencyUsed(_ dependency: PackageDependency, enabledTraits: EnabledTraits) throws -> Bool { if self.pruneDependencies { let usedDependencies = try self.usedDependencies(withTraits: enabledTraits) let foundKnownPackage = usedDependencies.knownPackage.contains(where: { @@ -471,24 +440,39 @@ extension Manifest { // tentatively marking the package dependency as used. to be resolved later on. return foundKnownPackage || (!foundKnownPackage && !usedDependencies.unknownPackage.isEmpty) } else { - // alternate path to compute trait-guarded package dependencies if the prune deps feature is not enabled - try validateEnabledTraits(enabledTraits) + return try !isPackageDependencyTraitGuarded(dependency, enabledTraits: enabledTraits) + } + } - let targetDependenciesForPackageDependency = self.targets.flatMap({ $0.dependencies }) - .filter({ - $0.package?.caseInsensitiveCompare(dependency.identity.description) == .orderedSame - }) + /// Given a set of enabled traits, determine whether a package dependecy of this manifest is + /// guarded by traits. + private func isPackageDependencyTraitGuarded(_ dependency: PackageDependency, enabledTraits: EnabledTraits) throws -> Bool { + try validateEnabledTraits(enabledTraits) - // if target deps is empty, default to returning true here. - let isTraitGuarded = targetDependenciesForPackageDependency.isEmpty ? false : targetDependenciesForPackageDependency.compactMap({ $0.condition?.traits }).allSatisfy({ - let isGuarded = $0.intersection(enabledTraits).isEmpty - return isGuarded - }) + let targetDependenciesForPackageDependency = self.targets.flatMap({ $0.dependencies }) + .filter({ + $0.package?.caseInsensitiveCompare(dependency.identity.description) == .orderedSame + }) - let isUsedWithoutTraitGuarding = !targetDependenciesForPackageDependency.filter({ $0.condition?.traits == nil }).isEmpty + // Determine whether the current set of enabled traits still gate the package dependency + // across targets. + let isTraitGuarded = targetDependenciesForPackageDependency.isEmpty ? false : targetDependenciesForPackageDependency.filter({ $0.condition?.traits != nil }).allSatisfy({ self.isTargetDependencyTraitGuarded($0, enabledTraits: enabledTraits) + }) - return isUsedWithoutTraitGuarding || !isTraitGuarded - } + // Since we only omit a package dependency that is only guarded by traits, determine + // whether this dependency is used elsewhere without traits. + let isUsedWithoutTraitGuarding = !targetDependenciesForPackageDependency.filter({ $0.condition?.traits == nil }).isEmpty + + return !isUsedWithoutTraitGuarding && isTraitGuarded + } + + private func isTargetDependencyTraitGuarded( + _ dependency: TargetDescription.Dependency, + enabledTraits: EnabledTraits + ) -> Bool { + guard let condition = dependency.condition, let traits = condition.traits else { return false } + + return enabledTraits.intersection(traits).isEmpty } } @@ -498,29 +482,58 @@ public enum TraitError: Swift.Error { /// Indicates that an invalid trait was enabled. case invalidTrait( package: Manifest.PackageIdentifier, - trait: String, - availableTraits: [String] = [], - parent: Manifest.PackageIdentifier? = nil + trait: EnabledTrait, + availableTraits: [String] = [] ) /// Indicates that the manifest does not support traits, yet a method was called with a configuration of enabled /// traits. case traitsNotSupported( - parent: Manifest.PackageIdentifier? = nil, package: Manifest.PackageIdentifier, - explicitlyEnabledTraits: [String] + explicitlyEnabledTraits: EnabledTraits ) } extension TraitError: CustomStringConvertible { + private func generateSetterDescription(_ setters: EnabledTrait.Setters) -> String { + guard !setters.isEmpty else { + return "" + } + + var result: String = " enabled by" + if setters.count == 1, let setter = setters.first { + result += " \(setter.description)" + } else { + let parentPackages = setters.compactMap(\.parentPackage).sorted() + let parentTraits = setters.compactMap(\.parentTrait).sorted() + let traitConfiguration = setters.filter({ $0 == .traitConfiguration }).map(\.description) + + if !parentPackages.isEmpty { + result += " parent package" + result += parentPackages.count == 1 ? " " : "s " + result += parentPackages.map(\.description).joined(separator: ", ") + result += !parentTraits.isEmpty || !traitConfiguration.isEmpty ? ";" : "" + } + if !parentTraits.isEmpty { + result += " trait" + result += parentTraits.count == 1 ? " " : "s " + result += parentTraits.map(\.description).joined(separator: ", ") + result += !traitConfiguration.isEmpty ? ";" : "" + } + if !traitConfiguration.isEmpty { + result += " a custom trait configuration declared for the root" + } + } + + return result + } + public var description: String { switch self { - case .invalidTrait(let package, let trait, var availableTraits, let parentPackage): + case .invalidTrait(let package, let trait, var availableTraits): availableTraits = availableTraits.sorted() var errorMsg = "Trait '\(trait)'" - if let parentPackage { - errorMsg += " enabled by parent package \(parentPackage)" - } + errorMsg += generateSetterDescription(trait.setters) errorMsg += " is not declared by package \(package)." if availableTraits.isEmpty { errorMsg += " There are no available traits declared by this package." @@ -529,12 +542,13 @@ extension TraitError: CustomStringConvertible { " The available traits declared by this package are: \(availableTraits.joined(separator: ", "))." } return errorMsg - case .traitsNotSupported(let parentPackage, let package, var explicitlyEnabledTraits): - explicitlyEnabledTraits = explicitlyEnabledTraits.sorted() + case .traitsNotSupported(let package, let explicitlyEnabledTraits): + let parentPackages = Set(explicitlyEnabledTraits.compactMap(\.parentPackages).flatMap({ $0 })) + let parent: Manifest.PackageIdentifier? = parentPackages.count == 1 ? parentPackages.first : nil if explicitlyEnabledTraits.isEmpty { - if let parentPackage { + if let parent = explicitlyEnabledTraits.disabledBy { return """ - Disabled default traits by package \(parentPackage) on package \(package) that declares no traits. This is prohibited to allow packages to adopt traits initially without causing an API break. + Disabled default traits by \(parent.description) on package \(package) that declares no traits. This is prohibited to allow packages to adopt traits initially without causing an API break. """ } else { return """ @@ -542,9 +556,9 @@ extension TraitError: CustomStringConvertible { """ } } else { - if let parentPackage { + if let parent { return """ - Package \(parentPackage) enables traits [\(explicitlyEnabledTraits.joined(separator: ", "))] on package \(package) that declares no traits. + Package \(parent) enables traits [\(explicitlyEnabledTraits.joined(separator: ", "))] on package \(package) that declares no traits. """ } else { return """ diff --git a/Sources/PackageModel/Manifest/Manifest.swift b/Sources/PackageModel/Manifest/Manifest.swift index 396961e11d7..5d4220e314f 100644 --- a/Sources/PackageModel/Manifest/Manifest.swift +++ b/Sources/PackageModel/Manifest/Manifest.swift @@ -199,13 +199,13 @@ public final class Manifest: Sendable { /// /// If we set the `enabledTraits` to be `["Trait1"]`, then the list of dependencies guarded by traits would be `[]`. /// Otherwise, if `enabledTraits` were `nil`, then the dependencies guarded by traits would be `["Bar"]`. - public func dependenciesTraitGuarded(withEnabledTraits enabledTraits: Set) -> [PackageDependency] { + public func dependenciesTraitGuarded(withEnabledTraits enabledTraits: EnabledTraits) -> [PackageDependency] { guard supportsTraits else { return [] } let traitGuardedDeps = self.traitGuardedTargetDependencies(lowercasedKeys: true) - let explicitlyEnabledTraits = try? self.enabledTraits(using: enabledTraits, nil) + let explicitlyEnabledTraits = try? self.enabledTraits(using: enabledTraits) guard self.toolsVersion >= .v5_2 && !self.packageKind.isRoot else { let deps = self.dependencies.filter { @@ -249,7 +249,7 @@ public final class Manifest: Sendable { continue } - if guardingTraits.intersection(enabledTraits) != guardingTraits + if guardingTraits.intersection(enabledTraits.names) != guardingTraits { guardedDependencies.insert(dependency.identity) } @@ -266,7 +266,7 @@ public final class Manifest: Sendable { /// Returns the package dependencies required for a particular products filter and trait configuration. public func dependenciesRequired( for productFilter: ProductFilter, - _ enabledTraits: Set = ["default"] + _ enabledTraits: EnabledTraits = ["default"] ) throws -> [PackageDependency] { #if ENABLE_TARGET_BASED_DEPENDENCY_RESOLUTION // If we have already calculated it, returned the cached value. diff --git a/Sources/PackageModel/Manifest/PackageDependencyDescription.swift b/Sources/PackageModel/Manifest/PackageDependencyDescription.swift index b4bfffcaa96..d979d90e79b 100644 --- a/Sources/PackageModel/Manifest/PackageDependencyDescription.swift +++ b/Sources/PackageModel/Manifest/PackageDependencyDescription.swift @@ -31,6 +31,7 @@ public enum PackageDependency: Equatable, Hashable, Sendable { } public func isSatisfied(by enabledTraits: Set) -> Bool { + // If there are no traits in this condition, default to true. guard let traits else { return true } return !traits.intersection(enabledTraits).isEmpty } @@ -78,6 +79,18 @@ public enum PackageDependency: Equatable, Hashable, Sendable { public var isDefaultsCase: Bool { name == "default" && condition == nil } + + /// Determines whether this trait's condition would be met by a set of enabled traits, + /// therefore enabling this trait. + /// Defaults to true if there is no condition to be satisfied. + /// + /// - Parameters: + /// - traits: A list of enabled traits. + public func isEnabled(by traits: EnabledTraits) -> Bool { + guard let condition else { return true } + + return condition.isSatisfied(by: traits.names) + } } case fileSystem(FileSystem) diff --git a/Sources/PackageModel/Manifest/TraitConfiguration.swift b/Sources/PackageModel/Manifest/TraitConfiguration.swift index 65ebe5acc51..10614ea2073 100644 --- a/Sources/PackageModel/Manifest/TraitConfiguration.swift +++ b/Sources/PackageModel/Manifest/TraitConfiguration.swift @@ -44,12 +44,12 @@ public enum TraitConfiguration: Codable, Hashable { } /// The set of enabled traits, if available. - public var enabledTraits: Set? { + public var enabledTraits: EnabledTraits? { switch self { case .default: - ["default"] + EnabledTraits.defaults case .enabledTraits(let traits): - traits + EnabledTraits(traits, setBy: .traitConfiguration) case .disableAllTraits: [] case .enableAllTraits: diff --git a/Sources/Workspace/CMakeLists.txt b/Sources/Workspace/CMakeLists.txt index d0b033eb1cf..cdc939112f2 100644 --- a/Sources/Workspace/CMakeLists.txt +++ b/Sources/Workspace/CMakeLists.txt @@ -40,7 +40,8 @@ add_library(Workspace Workspace+ResolvedPackages.swift Workspace+Signing.swift Workspace+SourceControl.swift - Workspace+State.swift) + Workspace+State.swift + Workspace+Traits.swift) target_link_libraries(Workspace PUBLIC TSCBasic TSCUtility diff --git a/Sources/Workspace/PackageContainer/FileSystemPackageContainer.swift b/Sources/Workspace/PackageContainer/FileSystemPackageContainer.swift index 47a19c0bb73..2517c3d7f9c 100644 --- a/Sources/Workspace/PackageContainer/FileSystemPackageContainer.swift +++ b/Sources/Workspace/PackageContainer/FileSystemPackageContainer.swift @@ -94,7 +94,7 @@ public struct FileSystemPackageContainer: PackageContainer { } } - public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) async throws -> [PackageContainerConstraint] { + public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) async throws -> [PackageContainerConstraint] { let manifest = try await self.loadManifest() return try manifest.dependencyConstraints(productFilter: productFilter, enabledTraits) } @@ -121,11 +121,11 @@ public struct FileSystemPackageContainer: PackageContainer { fatalError("This should never be called") } - public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { fatalError("This should never be called") } - public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { fatalError("This should never be called") } } diff --git a/Sources/Workspace/PackageContainer/RegistryPackageContainer.swift b/Sources/Workspace/PackageContainer/RegistryPackageContainer.swift index 630ba49a280..1279e0d7074 100644 --- a/Sources/Workspace/PackageContainer/RegistryPackageContainer.swift +++ b/Sources/Workspace/PackageContainer/RegistryPackageContainer.swift @@ -104,16 +104,16 @@ public class RegistryPackageContainer: PackageContainer { return results } - public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) async throws -> [PackageContainerConstraint] { + public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) async throws -> [PackageContainerConstraint] { let manifest = try await self.loadManifest(version: version) return try manifest.dependencyConstraints(productFilter: productFilter, enabledTraits) } - public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { throw InternalError("getDependencies for revision not supported by RegistryPackageContainer") } - public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { throw InternalError("getUnversionedDependencies not supported by RegistryPackageContainer") } diff --git a/Sources/Workspace/PackageContainer/SourceControlPackageContainer.swift b/Sources/Workspace/PackageContainer/SourceControlPackageContainer.swift index 2444ec5bfae..227a9d8ac4f 100644 --- a/Sources/Workspace/PackageContainer/SourceControlPackageContainer.swift +++ b/Sources/Workspace/PackageContainer/SourceControlPackageContainer.swift @@ -241,7 +241,7 @@ internal final class SourceControlPackageContainer: PackageContainer, CustomStri } } - public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) async throws -> [Constraint] { + public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) async throws -> [Constraint] { do { return try await self.getCachedDependencies(forIdentifier: version.description, productFilter: productFilter) { guard let tag = try self.knownVersions()[version] else { @@ -259,7 +259,7 @@ internal final class SourceControlPackageContainer: PackageContainer, CustomStri } } - public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) async throws -> [Constraint] { + public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) async throws -> [Constraint] { do { return try await self.getCachedDependencies(forIdentifier: revision, productFilter: productFilter) { // resolve the revision identifier and return its dependencies. @@ -323,7 +323,7 @@ internal final class SourceControlPackageContainer: PackageContainer, CustomStri tag: String, version: Version? = nil, productFilter: ProductFilter, - enabledTraits: Set + enabledTraits: EnabledTraits ) async throws -> (Manifest, [Constraint]) { let manifest = try await self.loadManifest(tag: tag, version: version) return (manifest, try manifest.dependencyConstraints(productFilter: productFilter, enabledTraits)) @@ -334,13 +334,13 @@ internal final class SourceControlPackageContainer: PackageContainer, CustomStri at revision: Revision, version: Version? = nil, productFilter: ProductFilter, - enabledTraits: Set + enabledTraits: EnabledTraits ) async throws -> (Manifest, [Constraint]) { let manifest = try await self.loadManifest(at: revision, version: version) return (manifest, try manifest.dependencyConstraints(productFilter: productFilter, enabledTraits)) } - public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [Constraint] { + public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [Constraint] { // We just return an empty array if requested for unversioned dependencies. return [] } diff --git a/Sources/Workspace/ResolverPrecomputationProvider.swift b/Sources/Workspace/ResolverPrecomputationProvider.swift index 4ae40119dce..b6ad1ec31ed 100644 --- a/Sources/Workspace/ResolverPrecomputationProvider.swift +++ b/Sources/Workspace/ResolverPrecomputationProvider.swift @@ -122,7 +122,7 @@ private struct LocalPackageContainer: PackageContainer { try await self.versionsDescending() } - func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { // Because of the implementation of `reversedVersions`, we should only get the exact same version. switch dependency?.state { case .sourceControlCheckout(.version(version, revision: _)): @@ -134,7 +134,7 @@ private struct LocalPackageContainer: PackageContainer { } } - func getDependencies(at revisionString: String, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + func getDependencies(at revisionString: String, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { let revision = Revision(identifier: revisionString) switch dependency?.state { case .sourceControlCheckout(.branch(_, revision: revision)), .sourceControlCheckout(.revision(revision)): @@ -150,7 +150,7 @@ private struct LocalPackageContainer: PackageContainer { } } - func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { switch dependency?.state { case .none, .fileSystem, .edited: return try manifest.dependencyConstraints(productFilter: productFilter, enabledTraits) diff --git a/Sources/Workspace/Workspace+Dependencies.swift b/Sources/Workspace/Workspace+Dependencies.swift index 64eb37227b8..743e6ac926e 100644 --- a/Sources/Workspace/Workspace+Dependencies.swift +++ b/Sources/Workspace/Workspace+Dependencies.swift @@ -525,7 +525,6 @@ extension Workspace { enabledTraitsMap: self.enabledTraitsMap ) - // Of the enabled dependencies of targets, only consider these for dependency resolution let currentManifests = try await self.loadDependencyManifests( root: graphRoot, observabilityScope: observabilityScope @@ -598,6 +597,7 @@ extension Workspace { computedConstraints += currentManifests.editedPackagesConstraints computedConstraints += try graphRoot.constraints(self.enabledTraitsMap) + constraints + // Perform dependency resolution. let resolver = try self.createResolver(resolvedPackages: resolvedPackagesStore.resolvedPackages, observabilityScope: observabilityScope) self.activeResolver = resolver diff --git a/Sources/Workspace/Workspace+Manifests.swift b/Sources/Workspace/Workspace+Manifests.swift index 5151b895336..5c6093268e0 100644 --- a/Sources/Workspace/Workspace+Manifests.swift +++ b/Sources/Workspace/Workspace+Manifests.swift @@ -30,6 +30,9 @@ import struct PackageGraph.PackageGraphRoot import class PackageLoading.ManifestLoader import struct PackageLoading.ManifestValidator import struct PackageLoading.ToolsVersionParser +import struct PackageModel.EnabledTrait +import struct PackageModel.EnabledTraits +import enum PackageModel.TraitError import class PackageModel.Manifest import struct PackageModel.PackageIdentity import struct PackageModel.PackageReference @@ -542,7 +545,7 @@ extension Workspace { // Load root dependencies manifests (in parallel) let rootDependencies = root.dependencies.map(\.packageRef) try await prepopulateManagedDependencies(rootDependencies) - let rootDependenciesManifests = await self.loadManagedManifests( + let rootDependenciesManifests = try await self.loadManagedManifests( for: rootDependencies, observabilityScope: observabilityScope ) @@ -550,15 +553,6 @@ extension Workspace { let rootManifests = try root.manifests.mapValues { manifest in let parentEnabledTraits = self.enabledTraitsMap[manifest.packageIdentity] let deps = try manifest.dependencies.filter { dep in - let explicitlyEnabledTraits = dep.traits?.filter({ - guard let condition = $0.condition else { return true } - return condition.isSatisfied(by: parentEnabledTraits) - }).map(\.name) - - if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { - self.enabledTraitsMap[dep.identity] = enabledTraitsSet - } - let isDepUsed = try manifest.isPackageDependencyUsed(dep, enabledTraits: parentEnabledTraits) return isDepUsed } @@ -599,27 +593,18 @@ extension Workspace { // the case where a package is being loaded in a wrapper project (not package), // where there are no root packages but there are dependencies. if root.packages.isEmpty { - let topLevelManifestTraits = try manifest.enabledTraits(using: parentEnabledTraits, nil) + let topLevelManifestTraits = try manifest.enabledTraits(using: parentEnabledTraits) self.enabledTraitsMap[manifest.packageIdentity] = topLevelManifestTraits } return try manifest.dependencies.filter { dep in - let explicitlyEnabledTraits = dep.traits?.filter({ - guard let condition = $0.condition else { return true } - return condition.isSatisfied(by: parentEnabledTraits) - }).map(\.name) - - if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { - self.enabledTraitsMap[dep.identity] = enabledTraitsSet - } - let isDepUsed = try manifest.isPackageDependencyUsed(dep, enabledTraits: parentEnabledTraits) return isDepUsed }.map(\.packageRef) }.flatMap(\.self) - let firstLevelManifests = await self.loadManagedManifests( + let firstLevelManifests = try await self.loadManagedManifests( for: firstLevelDependencies, observabilityScope: observabilityScope ) @@ -640,27 +625,13 @@ extension Workspace { let dependenciesToLoad = dependenciesRequired.map(\.packageRef) .filter { !loadedManifests.keys.contains($0.identity) } try await prepopulateManagedDependencies(dependenciesToLoad) - let dependenciesManifests = await self.loadManagedManifests( + let dependenciesManifests = try await self.loadManagedManifests( for: dependenciesToLoad, observabilityScope: observabilityScope ) dependenciesManifests.forEach { loadedManifests[$0.key] = $0.value } return try dependenciesRequired.compactMap { dependency in return try loadedManifests[dependency.identity].flatMap { manifest in - - let explicitlyEnabledTraits = dependency.traits?.filter { - guard let condition = $0.condition else { return true } - return condition.isSatisfied(by: node.item.enabledTraits) - }.map(\.name) - - if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { - let calculatedTraits = try manifest.enabledTraits( - using: enabledTraitsSet, - .init(node.item.manifest) - ) - self.enabledTraitsMap[dependency.identity] = calculatedTraits - } - // we also compare the location as this function may attempt to load // dependencies that have the same identity but from a different location // which is an error case we diagnose an report about in the GraphLoading part which @@ -705,8 +676,13 @@ extension Workspace { } } - // Update enabled traits map - self.enabledTraitsMap = .init(try precomputeTraits( topLevelManifests.values.map({ $0 }), loadedManifests)) + // Second pass: Update enabled traits for dependencies now that we have all manifests loaded + // This resolves the race condition where parents might set traits for dependencies + // before the dependency manifest is loaded and its default traits are known. + let allManifests = allNodes.mapValues(\.manifest) + for (_, manifest) in allManifests { + try updateEnabledTraits(for: manifest) + } let dependencyManifests = allNodes.filter { !$0.value.manifest.packageKind.isRoot } @@ -747,71 +723,23 @@ extension Workspace { ) } - public func precomputeTraits( - _ topLevelManifests: [Manifest], - _ manifestMap: [PackageIdentity: Manifest] - ) throws -> [PackageIdentity: Set] { - var visited: Set = [] - - func dependencies(of parent: Manifest, _ productFilter: ProductFilter = .everything) throws { - let parentTraits = self.enabledTraitsMap[parent.packageIdentity] - let requiredDependencies = try parent.dependenciesRequired(for: productFilter, parentTraits) - let guardedDependencies = parent.dependenciesTraitGuarded(withEnabledTraits: parentTraits) - - _ = try (requiredDependencies + guardedDependencies).compactMap({ dependency in - return try manifestMap[dependency.identity].flatMap({ manifest in - - let explicitlyEnabledTraits = dependency.traits?.filter { - guard let condition = $0.condition else { return true } - return condition.isSatisfied(by: parentTraits) - }.map(\.name) - - if let enabledTraitsSet = explicitlyEnabledTraits.flatMap({ Set($0) }) { - let calculatedTraits = try manifest.enabledTraits( - using: enabledTraitsSet, - .init(parent) - ) - self.enabledTraitsMap[dependency.identity] = calculatedTraits - } - - let result = visited.insert(dependency.identity) - if result.inserted { - try dependencies(of: manifest, dependency.productFilter) - } - - return manifest - }) - }) - } - - for manifest in topLevelManifests { - // Track already-visited manifests to avoid cycles - let result = visited.insert(manifest.packageIdentity) - if result.inserted { - try dependencies(of: manifest) - } - } - - return self.enabledTraitsMap.dictionaryLiteral - } - /// Loads the given manifests, if it is present in the managed dependencies. /// private func loadManagedManifests( for packages: [PackageReference], observabilityScope: ObservabilityScope - ) async -> [PackageIdentity: Manifest] { - await withTaskGroup(of: (PackageIdentity, Manifest?).self) { group in + ) async throws -> [PackageIdentity: Manifest] { + try await withThrowingTaskGroup(of: (PackageIdentity, Manifest?).self) { group in for package in Set(packages) { group.addTask { await ( package.identity, - self.loadManagedManifest(for: package, observabilityScope: observabilityScope) + try self.loadManagedManifest(for: package, observabilityScope: observabilityScope) ) } } - return await group.compactMap { + return try await group.compactMap { $0 as? (PackageIdentity, Manifest) }.reduce(into: [PackageIdentity: Manifest]()) { partialResult, loadedManifest in partialResult[loadedManifest.0] = loadedManifest.1 @@ -823,7 +751,7 @@ extension Workspace { private func loadManagedManifest( for package: PackageReference, observabilityScope: ObservabilityScope - ) async -> Manifest? { + ) async throws -> Manifest? { // Check if this dependency is available. // we also compare the location as this function may attempt to load // dependencies that have the same identity but from a different location @@ -876,7 +804,7 @@ extension Workspace { } // Load and return the manifest. - return try? await self.loadManifest( + return try await self.loadManifest( packageIdentity: managedDependency.packageRef.identity, packageKind: packageKind, packagePath: packagePath, @@ -961,6 +889,10 @@ extension Workspace { manifestLoadingDiagnostics.append(contentsOf: validationIssues) throw Diagnostics.fatalError } + + // Upon loading a new manifest, update enabled traits. + try self.updateEnabledTraits(for: manifest) + self.delegate?.didLoadManifest( packageIdentity: packageIdentity, packagePath: packagePath, @@ -971,6 +903,7 @@ extension Workspace { diagnostics: manifestLoadingDiagnostics, duration: duration ) + return manifest } diff --git a/Sources/Workspace/Workspace+Traits.swift b/Sources/Workspace/Workspace+Traits.swift new file mode 100644 index 00000000000..856627f4fcb --- /dev/null +++ b/Sources/Workspace/Workspace+Traits.swift @@ -0,0 +1,94 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import class PackageModel.Manifest +import struct PackageModel.PackageIdentity +import enum PackageModel.ProductFilter +import enum PackageModel.PackageDependency +import struct PackageModel.EnabledTrait +import struct PackageModel.EnabledTraits + +extension Workspace { + /// Given a loaded `Manifest`, determine the traits that are enabled for it and + /// calculate whichever traits are enabled transitively from this, if possible, and update the + /// map of enabled traits on `Workspace` (`Workspace.enabledTraitsMap`). + /// + /// If the package defines a dependency with an explicit set of enabled traits, it will also + /// add them to the enabled traits map. + public func updateEnabledTraits(for manifest: Manifest) throws { + // If the `Manifest` is a root, then we should default to using + // the trait configuration set in the `Workspace`. Otherwise, + // check the enabled traits map to see if there are traits + // that have already been recorded as enabled. + let explicitlyEnabledTraits = manifest.packageKind.isRoot ? + try manifest.enabledTraits(using: self.traitConfiguration) : + self.enabledTraitsMap[manifest.packageIdentity] + + var enabledTraits = try manifest.enabledTraits(using: explicitlyEnabledTraits) + + // Check if any parents requested default traits for this package + // If so, expand the default traits and union them with existing traits + if let defaultSetters = self.enabledTraitsMap[defaultSettersFor: manifest.packageIdentity], + !defaultSetters.isEmpty { + // Calculate what the default traits are for this manifest + let defaultTraits = try manifest.enabledTraits(using: .defaults) + + // Create enabled traits for each setter that requested defaults + for setter in defaultSetters { + let traitsFromSetter = EnabledTraits( + defaultTraits.map(\.name), + setBy: setter + ) + enabledTraits.formUnion(traitsFromSetter) + } + } + + self.enabledTraitsMap[manifest.packageIdentity] = enabledTraits + + // Check enabled traits for the dependencies + for dep in manifest.dependencies { + updateEnabledTraits(forDependency: dep, manifest) + } + } + + /// Update the enabled traits for a `PackageDependency` of a given parent `Manifest`. + /// + /// This is called when a manifest is loaded to register the parent's trait requirements for its dependencies. + /// When a parent doesn't specify traits, this explicitly registers that the parent wants the dependency + /// to use its default traits, with the parent as the setter. + private func updateEnabledTraits(forDependency dependency: PackageDependency, _ parent: Manifest) { + let parentEnabledTraits = self.enabledTraitsMap[parent.packageIdentity] + + if let dependencyTraits = dependency.traits { + // Parent explicitly specified traits (could be [] to disable, or a list of specific traits) + let explicitlyEnabledTraits = dependencyTraits + .filter { $0.isEnabled(by: parentEnabledTraits) } + .map(\.name) + + let enabledTraits = EnabledTraits( + explicitlyEnabledTraits, + setBy: .package(.init(parent)) + ) + self.enabledTraitsMap[dependency.identity] = enabledTraits + } else { + // Parent didn't specify traits - it wants the dependency to use its defaults. + // Explicitly register "default" with this parent as the setter. + // This ensures the union system properly tracks that this parent wants defaults enabled, + // even if other parents have disabled traits. + let defaultTraits = EnabledTraits( + ["default"], + setBy: .package(.init(parent)) + ) + self.enabledTraitsMap[dependency.identity] = defaultTraits + } + } +} diff --git a/Sources/Workspace/Workspace.swift b/Sources/Workspace/Workspace.swift index b5243e1a170..76054b6988d 100644 --- a/Sources/Workspace/Workspace.swift +++ b/Sources/Workspace/Workspace.swift @@ -1097,6 +1097,10 @@ extension Workspace { ) return (package, manifest) } catch { + // Propagate the TraitError if it exists. + if let error = error as? TraitError { + throw error + } return nil } } @@ -1107,11 +1111,6 @@ extension Workspace { if let (package, manifest) = result { // Store the manifest. rootManifests[package] = manifest - - // Compute the enabled traits for roots. - let traitConfiguration = self.configuration.traitConfiguration - let enabledTraits = try manifest.enabledTraits(using: traitConfiguration) - self.enabledTraitsMap[manifest.packageIdentity] = enabledTraits } } @@ -1251,7 +1250,7 @@ extension Workspace { prebuilts: [:], fileSystem: self.fileSystem, observabilityScope: observabilityScope, - enabledTraits: try manifest.enabledTraits(using: .default) + enabledTraits: try manifest.enabledTraits(using: self.traitConfiguration) ) return try builder.construct() } @@ -1318,7 +1317,7 @@ extension Workspace { createREPLProduct: self.configuration.createREPLProduct, fileSystem: self.fileSystem, observabilityScope: observabilityScope, - enabledTraits: try manifest.enabledTraits(using: .default) + enabledTraits: try manifest.enabledTraits(using: self.traitConfiguration) ) return try builder.construct() } diff --git a/Sources/_InternalTestSupport/MockManifestLoader.swift b/Sources/_InternalTestSupport/MockManifestLoader.swift index 322681abde7..3ccfb3618cd 100644 --- a/Sources/_InternalTestSupport/MockManifestLoader.swift +++ b/Sources/_InternalTestSupport/MockManifestLoader.swift @@ -130,7 +130,7 @@ extension ManifestLoader { dependencyMapper: DependencyMapper? = .none, fileSystem: FileSystem, observabilityScope: ObservabilityScope - ) async throws -> Manifest{ + ) async throws -> Manifest { let packageIdentity: PackageIdentity let packageLocation: String switch packageKind { diff --git a/Sources/_InternalTestSupport/MockPackageContainer.swift b/Sources/_InternalTestSupport/MockPackageContainer.swift index 92a243f98c0..e0c87d3abb5 100644 --- a/Sources/_InternalTestSupport/MockPackageContainer.swift +++ b/Sources/_InternalTestSupport/MockPackageContainer.swift @@ -45,12 +45,12 @@ public class MockPackageContainer: CustomPackageContainer { return _versions } - public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) -> [MockPackageContainer.Constraint] { + public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) -> [MockPackageContainer.Constraint] { requestedVersions.insert(version) return getDependencies(at: version.description, productFilter: productFilter, enabledTraits) } - public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) -> [MockPackageContainer.Constraint] { + public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) -> [MockPackageContainer.Constraint] { let dependencies: [Dependency] if filteredMode { dependencies = filteredDependencies[productFilter]! @@ -63,7 +63,7 @@ public class MockPackageContainer: CustomPackageContainer { } } - public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) -> [MockPackageContainer.Constraint] { + public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) -> [MockPackageContainer.Constraint] { return unversionedDeps } diff --git a/Sources/_InternalTestSupport/MockPackageGraphs.swift b/Sources/_InternalTestSupport/MockPackageGraphs.swift index 7e60fa7c41d..1516d517a1a 100644 --- a/Sources/_InternalTestSupport/MockPackageGraphs.swift +++ b/Sources/_InternalTestSupport/MockPackageGraphs.swift @@ -126,7 +126,8 @@ package func macrosPackageGraph() throws -> MockPackageGraph { ), ], observabilityScope: observability.topScope, - traitConfiguration: .default + traitConfiguration: .default, + enabledTraitsMap: .init() ) XCTAssertNoDiagnostics(observability.diagnostics) @@ -284,7 +285,8 @@ package func macrosTestsPackageGraph() throws -> MockPackageGraph { ), ], observabilityScope: observability.topScope, - traitConfiguration: .default + traitConfiguration: .default, + enabledTraitsMap: .init() ) XCTAssertNoDiagnostics(observability.diagnostics) @@ -317,7 +319,8 @@ package func trivialPackageGraph() throws -> MockPackageGraph { ), ], observabilityScope: observability.topScope, - traitConfiguration: .default + traitConfiguration: .default, + enabledTraitsMap: .init() ) XCTAssertNoDiagnostics(observability.diagnostics) @@ -361,7 +364,8 @@ package func embeddedCxxInteropPackageGraph() throws -> MockPackageGraph { ), ], observabilityScope: observability.topScope, - traitConfiguration: .default + traitConfiguration: .default, + enabledTraitsMap: .init() ) XCTAssertNoDiagnostics(observability.diagnostics) @@ -426,7 +430,8 @@ package func toolsExplicitLibrariesGraph(linkage: ProductType.LibraryType) throw ), ], observabilityScope: observability.topScope, - traitConfiguration: .default + traitConfiguration: .default, + enabledTraitsMap: .init() ) XCTAssertNoDiagnostics(observability.diagnostics) diff --git a/Sources/_InternalTestSupport/MockWorkspace.swift b/Sources/_InternalTestSupport/MockWorkspace.swift index 4e8b202654f..355dd8845c1 100644 --- a/Sources/_InternalTestSupport/MockWorkspace.swift +++ b/Sources/_InternalTestSupport/MockWorkspace.swift @@ -596,7 +596,7 @@ public final class MockWorkspace { public func checkPackageGraph( roots: [String] = [], deps: [MockDependency], - _ result: (ModulesGraph, [Basics.Diagnostic]) -> Void + _ result: (ModulesGraph, [Basics.Diagnostic]) throws -> Void ) async throws { let dependencies = try deps.map { try $0.convert( baseURL: self.packagesDir, diff --git a/Tests/PackageGraphTests/ModulesGraphTests+Traits.swift b/Tests/PackageGraphTests/ModulesGraphTests+Traits.swift new file mode 100644 index 00000000000..4212ae76612 --- /dev/null +++ b/Tests/PackageGraphTests/ModulesGraphTests+Traits.swift @@ -0,0 +1,1053 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Basics +import PackageModel +import TSCUtility +import Testing +import _InternalTestSupport + +@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly) +@testable import PackageGraph + +extension ModulesGraphTests { + @Test + func traits_whenSingleManifest_andDefaultTrait() throws { + let fs = InMemoryFileSystem( + emptyFiles: + "/Foo/Sources/Foo/source.swift" + ) + + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: [ + Manifest.createRootManifest( + displayName: "Foo", + path: "/Foo", + toolsVersion: .v5_9, + targets: [ + TargetDescription( + name: "Foo" + ), + ], + traits: [ + .init(name: "default", enabledTraits: ["Trait1"]), + "Trait1", + ] + ), + ], + observabilityScope: observability.topScope, + enabledTraitsMap: [ + "Foo": ["Trait1"] + ] + ) + + #expect(observability.diagnostics.count == 0) + + try PackageGraphTester(graph) { result in + try result.checkPackage("Foo") { package in + #expect(package.enabledTraits == ["Trait1"]) + } + } + } + + @Test + func traits_whenTraitEnablesOtherTraits() throws { + let fs = InMemoryFileSystem( + emptyFiles: + "/Foo/Sources/Foo/source.swift" + ) + + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: [ + Manifest.createRootManifest( + displayName: "Foo", + path: "/Foo", + toolsVersion: .v5_9, + targets: [ + TargetDescription( + name: "Foo" + ), + ], + traits: [ + .init(name: "default", enabledTraits: ["Trait1"]), + .init(name: "Trait1", enabledTraits: ["Trait2"]), + .init(name: "Trait2", enabledTraits: ["Trait3", "Trait4"]), + "Trait3", + .init(name: "Trait4", enabledTraits: ["Trait5"]), + "Trait5", + ] + ), + ], + observabilityScope: observability.topScope, + enabledTraitsMap: [ + "Foo": ["Trait1", "Trait2", "Trait3", "Trait4", "Trait5"] + ] + ) + + #expect(observability.diagnostics.count == 0) + + try PackageGraphTester(graph) { result in + try result.checkPackage("Foo") { package in + #expect(package.enabledTraits == ["Trait1", "Trait2", "Trait3", "Trait4", "Trait5"]) + } + } + } + + @Test + func traits_whenDependencyTraitEnabled() throws { + let fs = InMemoryFileSystem( + emptyFiles: + "/Package1/Sources/Package1Target1/source.swift", + "/Package2/Sources/Package2Target1/source.swift" + ) + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: [ + Manifest.createRootManifest( + displayName: "Package1", + path: "/Package1", + toolsVersion: .v5_9, + dependencies: [ + .localSourceControl( + path: "/Package2", + requirement: .upToNextMajor(from: "1.0.0"), + traits: ["Package2Trait1"] + ), + ], + targets: [ + TargetDescription( + name: "Package1Target1", + dependencies: [ + .product(name: "Package2Target1", package: "Package2"), + ] + ), + ], + traits: [ + .init(name: "default", enabledTraits: ["Package1Trait1"]), + "Package1Trait1", + ] + ), + Manifest.createFileSystemManifest( + displayName: "Package2", + path: "/Package2", + toolsVersion: .v5_9, + products: [ + .init( + name: "Package2Target1", + type: .library(.automatic), + targets: ["Package2Target1"] + ), + ], + targets: [ + TargetDescription( + name: "Package2Target1" + ), + ], + traits: [ + "Package2Trait1", + ] + ), + ], + observabilityScope: observability.topScope, + enabledTraitsMap: [ + "Package1": ["Package1Trait1"], + "Package2": ["Package2Trait1"] + ] + ) + + #expect(observability.diagnostics.count == 0) + + try PackageGraphTester(graph) { result in + try result.checkPackage("Package1") { package in + #expect(package.enabledTraits == ["Package1Trait1"]) + #expect(package.dependencies.count == 1) + } + try result.checkPackage("Package2") { package in + #expect(package.enabledTraits == ["Package2Trait1"]) + } + } + } + + @Test + func traits_whenTraitEnablesDependencyTrait() throws { + let fs = InMemoryFileSystem( + emptyFiles: + "/Package1/Sources/Package1Target1/source.swift", + "/Package2/Sources/Package2Target1/source.swift" + ) + + let manifests = try [ + Manifest.createRootManifest( + displayName: "Package1", + path: "/Package1", + toolsVersion: .v5_9, + dependencies: [ + .localSourceControl( + path: "/Package2", + requirement: .upToNextMajor(from: "1.0.0"), + traits: .init([.init(name: "Package2Trait1", condition: .init(traits: ["Package1Trait1"]))]) + ), + ], + targets: [ + TargetDescription( + name: "Package1Target1", + dependencies: [ + .product(name: "Package2Target1", package: "Package2"), + ] + ), + ], + traits: [ + .init(name: "default", enabledTraits: ["Package1Trait1"]), + .init(name: "Package1Trait1"), + ] + ), + Manifest.createFileSystemManifest( + displayName: "Package2", + path: "/Package2", + toolsVersion: .v5_9, + products: [ + .init( + name: "Package2Target1", + type: .library(.automatic), + targets: ["Package2Target1"] + ), + ], + targets: [ + TargetDescription( + name: "Package2Target1" + ), + ], + traits: [ + "Package2Trait1", + ] + ), + ] + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: manifests, + observabilityScope: observability.topScope, + enabledTraitsMap: [ + "Package1": ["Package1Trait1"], + "Package2": ["Package2Trait1"] + ] + ) + + #expect(observability.diagnostics.count == 0) + + try PackageGraphTester(graph) { result in + try result.checkPackage("Package1") { package in + #expect(package.enabledTraits == ["Package1Trait1"]) + #expect(package.dependencies.count == 1) + } + try result.checkPackage("Package2") { package in + #expect(package.enabledTraits == ["Package2Trait1"]) + } + } + } + + @Test + func traits_whenComplex() throws { + let fs = InMemoryFileSystem( + emptyFiles: + "/Package1/Sources/Package1Target1/source.swift", + "/Package2/Sources/Package2Target1/source.swift", + "/Package3/Sources/Package3Target1/source.swift", + "/Package4/Sources/Package4Target1/source.swift", + "/Package5/Sources/Package5Target1/source.swift" + ) + + let manifests = try [ + Manifest.createRootManifest( + displayName: "Package1", + path: "/Package1", + toolsVersion: .v5_9, + dependencies: [ + .localSourceControl( + path: "/Package2", + requirement: .upToNextMajor(from: "1.0.0"), + traits: .init([.init(name: "Package2Trait1", condition: .init(traits: ["Package1Trait1"]))]) + ), + .localSourceControl( + path: "/Package4", + requirement: .upToNextMajor(from: "1.0.0"), + traits: .init(["Package4Trait2"]) + ), + .localSourceControl( + path: "/Package5", + requirement: .upToNextMajor(from: "1.0.0") + ), + ], + targets: [ + TargetDescription( + name: "Package1Target1", + dependencies: [ + .product(name: "Package2Target1", package: "Package2"), + .product(name: "Package4Target1", package: "Package4"), + .product( + name: "Package5Target1", + package: "Package5", + condition: .init(traits: ["Package1Trait2"]) + ), + ], + settings: [ + .init( + tool: .swift, + kind: .define("TEST_DEFINE"), + condition: .init(traits: ["Package1Trait1"]) + ), + ] + ), + ], + traits: [ + .init(name: "default", enabledTraits: ["Package1Trait1", "Package1Trait2"]), + .init(name: "Package1Trait1"), + .init(name: "Package1Trait2"), + ] + ), + Manifest.createFileSystemManifest( + displayName: "Package2", + path: "/Package2", + toolsVersion: .v5_9, + dependencies: [ + .localSourceControl( + path: "/Package3", + requirement: .upToNextMajor(from: "1.0.0"), + traits: .init([.init(name: "Package3Trait1", condition: .init(traits: ["Package2Trait1"]))]) + ), + ], + products: [ + .init( + name: "Package2Target1", + type: .library(.automatic), + targets: ["Package2Target1"] + ), + ], + targets: [ + TargetDescription( + name: "Package2Target1", + dependencies: [ + .product(name: "Package3Target1", package: "Package3"), + ] + ), + ], + traits: [ + "Package2Trait1", + ] + ), + Manifest.createFileSystemManifest( + displayName: "Package3", + path: "/Package3", + toolsVersion: .v5_9, + dependencies: [ + .localSourceControl( + path: "/Package4", + requirement: .upToNextMajor(from: "1.0.0"), + traits: .init([.init(name: "Package4Trait1", condition: .init(traits: ["Package3Trait1"]))]) + ), + ], + products: [ + .init( + name: "Package3Target1", + type: .library(.automatic), + targets: ["Package3Target1"] + ), + ], + targets: [ + TargetDescription( + name: "Package3Target1", + dependencies: [ + .product(name: "Package4Target1", package: "Package4"), + ] + ), + ], + traits: [ + "Package3Trait1", + ] + ), + Manifest.createFileSystemManifest( + displayName: "Package4", + path: "/Package4", + toolsVersion: .v5_9, + products: [ + .init( + name: "Package4Target1", + type: .library(.automatic), + targets: ["Package4Target1"] + ), + ], + targets: [ + TargetDescription( + name: "Package4Target1" + ), + ], + traits: [ + "Package4Trait1", + "Package4Trait2", + ] + ), + Manifest.createFileSystemManifest( + displayName: "Package5", + path: "/Package5", + toolsVersion: .v5_9, + products: [ + .init( + name: "Package5Target1", + type: .library(.automatic), + targets: ["Package5Target1"] + ), + ], + targets: [ + TargetDescription( + name: "Package5Target1" + ), + ] + ), + ] + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: manifests, + observabilityScope: observability.topScope, + enabledTraitsMap: [ + "Package1": ["Package1Trait1", "Package1Trait2"], + "Package2": ["Package2Trait1"], + "Package3": ["Package3Trait1"], + "Package4": ["Package4Trait1", "Package4Trait2"] + ] + ) + + #expect(observability.diagnostics.count == 0) + + try PackageGraphTester(graph) { result in + try result.checkPackage("Package1") { package in + #expect(package.enabledTraits == ["Package1Trait1", "Package1Trait2"]) + #expect(package.dependencies.count == 3) + } + try result.checkTarget("Package1Target1") { target in + target.check(dependencies: "Package2Target1", "Package4Target1", "Package5Target1") + target.checkBuildSetting( + declaration: .SWIFT_ACTIVE_COMPILATION_CONDITIONS, + assignments: [ + .init(values: ["TEST_DEFINE"], conditions: [.traits(.init(traits: ["Package1Trait1"]))]), + .init(values: ["Package1Trait2"]), + .init(values: ["Package1Trait1"]), + ] + ) + } + try result.checkPackage("Package2") { package in + #expect(package.enabledTraits == ["Package2Trait1"]) + } + try result.checkPackage("Package3") { package in + #expect(package.enabledTraits == ["Package3Trait1"]) + } + try result.checkPackage("Package4") { package in + #expect(package.enabledTraits == ["Package4Trait1", "Package4Trait2"]) + } + } + } + + @Test + func traits_whenPruneDependenciesEnabled() throws { + let fs = InMemoryFileSystem( + emptyFiles: + "/Package1/Sources/Package1Target1/source.swift", + "/Package2/Sources/Package2Target1/source.swift", + "/Package3/Sources/Package3Target1/source.swift", + "/Package4/Sources/Package4Target1/source.swift", + "/Package5/Sources/Package5Target1/source.swift" + ) + + let manifests = try [ + Manifest.createRootManifest( + displayName: "Package1", + path: "/Package1", + toolsVersion: .v5_9, + dependencies: [ + .localSourceControl( + path: "/Package2", + requirement: .upToNextMajor(from: "1.0.0"), + traits: .init([.init(name: "Package2Trait1", condition: .init(traits: ["Package1Trait1"]))]) + ), + .localSourceControl( + path: "/Package4", + requirement: .upToNextMajor(from: "1.0.0"), + traits: .init(["Package4Trait2"]) + ), + .localSourceControl( + path: "/Package5", + requirement: .upToNextMajor(from: "1.0.0") + ), + ], + targets: [ + TargetDescription( + name: "Package1Target1", + dependencies: [ + .product(name: "Package2Target1", package: "Package2"), + .product(name: "Package4Target1", package: "Package4"), + .product( + name: "Package5Target1", + package: "Package5", + condition: .init(traits: ["Package1Trait2"]) + ), + ], + settings: [ + .init( + tool: .swift, + kind: .define("TEST_DEFINE"), + condition: .init(traits: ["Package1Trait1"]) + ), + .init( + tool: .swift, + kind: .define("TEST_DEFINE_2"), + condition: .init(traits: ["Package1Trait3"]) + ), + ] + ), + ], + traits: [ + .init(name: "default", enabledTraits: ["Package1Trait3"]), + .init(name: "Package1Trait1"), + .init(name: "Package1Trait2"), + .init(name: "Package1Trait3"), + ], + pruneDependencies: true + ), + Manifest.createFileSystemManifest( + displayName: "Package2", + path: "/Package2", + toolsVersion: .v5_9, + dependencies: [ + .localSourceControl( + path: "/Package3", + requirement: .upToNextMajor(from: "1.0.0"), + traits: .init([.init(name: "Package3Trait1", condition: .init(traits: ["Package2Trait1"]))]) + ), + ], + products: [ + .init( + name: "Package2Target1", + type: .library(.automatic), + targets: ["Package2Target1"] + ), + ], + targets: [ + TargetDescription( + name: "Package2Target1", + dependencies: [ + .product(name: "Package3Target1", package: "Package3"), + ] + ), + ], + traits: [ + "Package2Trait1", + ], + pruneDependencies: true + ), + Manifest.createFileSystemManifest( + displayName: "Package3", + path: "/Package3", + toolsVersion: .v5_9, + dependencies: [ + .localSourceControl( + path: "/Package4", + requirement: .upToNextMajor(from: "1.0.0"), + traits: .init([.init(name: "Package4Trait1", condition: .init(traits: ["Package3Trait1"]))]) + ), + ], + products: [ + .init( + name: "Package3Target1", + type: .library(.automatic), + targets: ["Package3Target1"] + ), + ], + targets: [ + TargetDescription( + name: "Package3Target1", + dependencies: [ + .product(name: "Package4Target1", package: "Package4"), + ] + ), + ], + traits: [ + "Package3Trait1", + ], + pruneDependencies: true + ), + Manifest.createFileSystemManifest( + displayName: "Package4", + path: "/Package4", + toolsVersion: .v5_9, + products: [ + .init( + name: "Package4Target1", + type: .library(.automatic), + targets: ["Package4Target1"] + ), + ], + targets: [ + TargetDescription( + name: "Package4Target1" + ), + ], + traits: [ + "Package4Trait1", + "Package4Trait2", + ], + pruneDependencies: true + ), + Manifest.createFileSystemManifest( + displayName: "Package5", + path: "/Package5", + toolsVersion: .v5_9, + products: [ + .init( + name: "Package5Target1", + type: .library(.automatic), + targets: ["Package5Target1"] + ), + ], + targets: [ + TargetDescription( + name: "Package5Target1" + ), + ], + pruneDependencies: true + ), + ] + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: manifests, + observabilityScope: observability.topScope, + enabledTraitsMap: [ + "Package1": ["Package1Trait3"], + "Package2": [], + "Package3": [], + "Package4": ["Package4Trait2"] + ] + ) + + #expect(observability.diagnostics.count == 0) + + try PackageGraphTester(graph) { result in + try result.checkPackage("Package1") { package in + #expect(package.enabledTraits == ["Package1Trait3"]) + #expect(package.dependencies.count == 2) + } + try result.checkTarget("Package1Target1") { target in + target.check(dependencies: "Package2Target1", "Package4Target1") + target.checkBuildSetting( + declaration: .SWIFT_ACTIVE_COMPILATION_CONDITIONS, + assignments: [ + .init(values: ["TEST_DEFINE_2"], conditions: [.traits(.init(traits: ["Package1Trait3"]))]), + .init(values: ["Package1Trait3"]), + ] + ) + } + try result.checkPackage("Package2") { package in + #expect(package.enabledTraits == []) + } + try result.checkPackage("Package3") { package in + #expect(package.enabledTraits == []) + } + try result.checkPackage("Package4") { package in + #expect(package.enabledTraits == ["Package4Trait2"]) + } + } + } + + @Test + func traits_whenPruneDependenciesEnabledForSomeManifests() throws { + let fs = InMemoryFileSystem( + emptyFiles: + "/Package1/Sources/Package1Target1/source.swift", + "/Package2/Sources/Package2Target1/source.swift", + "/Package3/Sources/Package3Target1/source.swift", + "/Package4/Sources/Package4Target1/source.swift", + "/Package5/Sources/Package5Target1/source.swift" + ) + + let manifests = try [ + Manifest.createRootManifest( + displayName: "Package1", + path: "/Package1", + toolsVersion: .v5_9, + dependencies: [ + .localSourceControl( + path: "/Package2", + requirement: .upToNextMajor(from: "1.0.0"), + traits: .init([.init(name: "Package2Trait1", condition: .init(traits: ["Package1Trait1"]))]) + ), + .localSourceControl( + path: "/Package4", + requirement: .upToNextMajor(from: "1.0.0"), + traits: .init(["Package4Trait2"]) + ), + .localSourceControl( + path: "/Package5", + requirement: .upToNextMajor(from: "1.0.0") + ), + ], + targets: [ + TargetDescription( + name: "Package1Target1", + dependencies: [ + .product(name: "Package2Target1", package: "Package2"), + .product(name: "Package4Target1", package: "Package4"), + .product( + name: "Package5Target1", + package: "Package5", + condition: .init(traits: ["Package1Trait2"]) + ), + ], + settings: [ + .init( + tool: .swift, + kind: .define("TEST_DEFINE"), + condition: .init(traits: ["Package1Trait1"]) + ), + .init( + tool: .swift, + kind: .define("TEST_DEFINE_2"), + condition: .init(traits: ["Package1Trait3"]) + ), + ] + ), + ], + traits: [ + .init(name: "default", enabledTraits: ["Package1Trait3"]), + .init(name: "Package1Trait1"), + .init(name: "Package1Trait2"), + .init(name: "Package1Trait3"), + ], + pruneDependencies: false + ), + Manifest.createFileSystemManifest( + displayName: "Package2", + path: "/Package2", + toolsVersion: .v5_9, + dependencies: [ + .localSourceControl( + path: "/Package3", + requirement: .upToNextMajor(from: "1.0.0"), + traits: .init([.init(name: "Package3Trait1", condition: .init(traits: ["Package2Trait1"]))]) + ), + ], + products: [ + .init( + name: "Package2Target1", + type: .library(.automatic), + targets: ["Package2Target1"] + ), + ], + targets: [ + TargetDescription( + name: "Package2Target1", + dependencies: [ + .product(name: "Package3Target1", package: "Package3"), + ] + ), + ], + traits: [ + "Package2Trait1", + ], + pruneDependencies: true + ), + Manifest.createFileSystemManifest( + displayName: "Package3", + path: "/Package3", + toolsVersion: .v5_9, + dependencies: [ + .localSourceControl( + path: "/Package4", + requirement: .upToNextMajor(from: "1.0.0"), + traits: .init([.init(name: "Package4Trait1", condition: .init(traits: ["Package3Trait1"]))]) + ), + ], + products: [ + .init( + name: "Package3Target1", + type: .library(.automatic), + targets: ["Package3Target1"] + ), + ], + targets: [ + TargetDescription( + name: "Package3Target1", + dependencies: [ + .product(name: "Package4Target1", package: "Package4"), + ] + ), + ], + traits: [ + "Package3Trait1", + ], + pruneDependencies: true + ), + Manifest.createFileSystemManifest( + displayName: "Package4", + path: "/Package4", + toolsVersion: .v5_9, + products: [ + .init( + name: "Package4Target1", + type: .library(.automatic), + targets: ["Package4Target1"] + ), + ], + targets: [ + TargetDescription( + name: "Package4Target1" + ), + ], + traits: [ + "Package4Trait1", + "Package4Trait2", + ], + pruneDependencies: true + ), + Manifest.createFileSystemManifest( + displayName: "Package5", + path: "/Package5", + toolsVersion: .v5_9, + products: [ + .init( + name: "Package5Target1", + type: .library(.automatic), + targets: ["Package5Target1"] + ), + ], + targets: [ + TargetDescription( + name: "Package5Target1" + ), + ], + pruneDependencies: true + ), + ] + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: manifests, + observabilityScope: observability.topScope, + enabledTraitsMap: [ + "Package1": ["Package1Trait3"], + "Package2": [], + "Package3": [], + "Package4": ["Package4Trait2"] + ] + ) + + #expect(observability.diagnostics.count == 0) + try PackageGraphTester(graph) { result in + try result.checkPackage("Package1") { package in + #expect(package.enabledTraits == ["Package1Trait3"]) + #expect(package.dependencies.count == 2) + } + try result.checkTarget("Package1Target1") { target in + target.check(dependencies: "Package2Target1", "Package4Target1") + target.checkBuildSetting( + declaration: .SWIFT_ACTIVE_COMPILATION_CONDITIONS, + assignments: [ + .init(values: ["TEST_DEFINE_2"], conditions: [.traits(.init(traits: ["Package1Trait3"]))]), + .init(values: ["Package1Trait3"]), + ] + ) + } + try result.checkPackage("Package2") { package in + #expect(package.enabledTraits == []) + } + try result.checkPackage("Package3") { package in + #expect(package.enabledTraits == []) + } + try result.checkPackage("Package4") { package in + #expect(package.enabledTraits == ["Package4Trait2"]) + } + } + } + + @Test + func traits_whenConditionalDependencies() throws { + let fs = InMemoryFileSystem( + emptyFiles: + "/Lunch/Sources/Drink/source.swift", + "/Caffeine/Sources/CoffeeTarget/source.swift", + "/Juice/Sources/AppleJuiceTarget/source.swift", + ) + + let manifests = try [ + Manifest.createRootManifest( + displayName: "Lunch", + path: "/Lunch", + toolsVersion: .v5_9, + dependencies: [ + .localSourceControl( + path: "/Caffeine", + requirement: .upToNextMajor(from: "1.0.0"), + ), + .localSourceControl( + path: "/Juice", + requirement: .upToNextMajor(from: "1.0.0") + ) + ], + targets: [ + TargetDescription( + name: "Drink", + dependencies: [ + .product( + name: "Coffee", + package: "Caffeine", + condition: .init(traits: ["EnableCoffeeDep"]) + ), + .product( + name: "AppleJuice", + package: "Juice", + condition: .init(traits: ["EnableAppleJuiceDep"]) + ) + ], + ), + ], + traits: [ + .init(name: "default", enabledTraits: ["EnableCoffeeDep"]), + .init(name: "EnableCoffeeDep"), + .init(name: "EnableAppleJuiceDep"), + ], + ), + Manifest.createFileSystemManifest( + displayName: "Caffeine", + path: "/Caffeine", + toolsVersion: .v5_9, + products: [ + .init( + name: "Coffee", + type: .library(.automatic), + targets: ["CoffeeTarget"] + ), + ], + targets: [ + TargetDescription( + name: "CoffeeTarget", + ), + ], + ), + Manifest.createFileSystemManifest( + displayName: "Juice", + path: "/Juice", + toolsVersion: .v5_9, + products: [ + .init( + name: "AppleJuice", + type: .library(.automatic), + targets: ["AppleJuiceTarget"] + ), + ], + targets: [ + TargetDescription( + name: "AppleJuiceTarget", + ), + ], + ) + ] + + // Test graph with default trait configuration + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: manifests, + observabilityScope: observability.topScope, + enabledTraitsMap: [ + "Lunch": ["EnableCoffeeDep"] + ] + ) + + #expect(observability.diagnostics.count == 0) + try PackageGraphTester(graph) { result in + try result.checkPackage("Lunch") { package in + #expect(package.enabledTraits == ["EnableCoffeeDep"]) + #expect(package.dependencies.count == 1) + } + try result.checkTarget("Drink") { target in + target.check(dependencies: "Coffee") + } + try result.checkPackage("Caffeine") { package in + #expect(package.enabledTraits == ["default"]) + } + try result.checkPackage("Juice") { package in + #expect(package.enabledTraits == ["default"]) + } + } + + // Test graph when disabling all traits + let graphWithTraitsDisabled = try loadModulesGraph( + fileSystem: fs, + manifests: manifests, + observabilityScope: observability.topScope, + traitConfiguration: .disableAllTraits, + enabledTraitsMap: [ + "Lunch": [], + ] + ) + #expect(observability.diagnostics.count == 0) + + try PackageGraphTester(graphWithTraitsDisabled) { result in + try result.checkPackage("Lunch") { package in + #expect(package.enabledTraits == []) + #expect(package.dependencies.count == 0) + } + try result.checkTarget("Drink") { target in + #expect(target.target.dependencies.isEmpty) + } + try result.checkPackage("Caffeine") { package in + #expect(package.enabledTraits == ["default"]) + } + try result.checkPackage("Juice") { package in + #expect(package.enabledTraits == ["default"]) + } + } + + // Test graph when we set a trait configuration that enables different traits than the defaults + let graphWithDifferentEnabledTraits = try loadModulesGraph( + fileSystem: fs, + manifests: manifests, + observabilityScope: observability.topScope, + traitConfiguration: .enabledTraits(["EnableAppleJuiceDep"]), + enabledTraitsMap: [ + "Lunch": ["EnableAppleJuiceDep"], + ] + ) + #expect(observability.diagnostics.count == 0) + + try PackageGraphTester(graphWithDifferentEnabledTraits) { result in + try result.checkPackage("Lunch") { package in + #expect(package.enabledTraits == ["EnableAppleJuiceDep"]) + #expect(package.dependencies.count == 1) + } + try result.checkTarget("Drink") { target in + target.check(dependencies: "AppleJuice") + } + try result.checkPackage("Caffeine") { package in + #expect(package.enabledTraits == ["default"]) + } + try result.checkPackage("Juice") { package in + #expect(package.enabledTraits == ["default"]) + } + } + } + +} diff --git a/Tests/PackageGraphTests/ModulesGraphTests.swift b/Tests/PackageGraphTests/ModulesGraphTests.swift index 7145a61ea3d..1089a3ab30b 100644 --- a/Tests/PackageGraphTests/ModulesGraphTests.swift +++ b/Tests/PackageGraphTests/ModulesGraphTests.swift @@ -3792,995 +3792,6 @@ struct ModulesGraphTests { ) } } - - @Test - func traits_whenSingleManifest_andDefaultTrait() throws { - let fs = InMemoryFileSystem( - emptyFiles: - "/Foo/Sources/Foo/source.swift" - ) - - let observability = ObservabilitySystem.makeForTesting() - let graph = try loadModulesGraph( - fileSystem: fs, - manifests: [ - Manifest.createRootManifest( - displayName: "Foo", - path: "/Foo", - toolsVersion: .v5_9, - targets: [ - TargetDescription( - name: "Foo" - ), - ], - traits: [ - .init(name: "default", enabledTraits: ["Trait1"]), - "Trait1", - ] - ), - ], - observabilityScope: observability.topScope - ) - - #expect(observability.diagnostics.count == 0) - - try PackageGraphTester(graph) { result in - try result.checkPackage("Foo") { package in - #expect(package.enabledTraits == ["Trait1"]) - } - } - } - - @Test - func traits_whenTraitEnablesOtherTraits() throws { - let fs = InMemoryFileSystem( - emptyFiles: - "/Foo/Sources/Foo/source.swift" - ) - - let observability = ObservabilitySystem.makeForTesting() - let graph = try loadModulesGraph( - fileSystem: fs, - manifests: [ - Manifest.createRootManifest( - displayName: "Foo", - path: "/Foo", - toolsVersion: .v5_9, - targets: [ - TargetDescription( - name: "Foo" - ), - ], - traits: [ - .init(name: "default", enabledTraits: ["Trait1"]), - .init(name: "Trait1", enabledTraits: ["Trait2"]), - .init(name: "Trait2", enabledTraits: ["Trait3", "Trait4"]), - "Trait3", - .init(name: "Trait4", enabledTraits: ["Trait5"]), - "Trait5", - ] - ), - ], - observabilityScope: observability.topScope - ) - - #expect(observability.diagnostics.count == 0) - - try PackageGraphTester(graph) { result in - try result.checkPackage("Foo") { package in - #expect(package.enabledTraits == ["Trait1", "Trait2", "Trait3", "Trait4", "Trait5"]) - } - } - } - - @Test - func traits_whenDependencyTraitEnabled() throws { - let fs = InMemoryFileSystem( - emptyFiles: - "/Package1/Sources/Package1Target1/source.swift", - "/Package2/Sources/Package2Target1/source.swift" - ) - let observability = ObservabilitySystem.makeForTesting() - let graph = try loadModulesGraph( - fileSystem: fs, - manifests: [ - Manifest.createRootManifest( - displayName: "Package1", - path: "/Package1", - toolsVersion: .v5_9, - dependencies: [ - .localSourceControl( - path: "/Package2", - requirement: .upToNextMajor(from: "1.0.0"), - traits: ["Package2Trait1"] - ), - ], - targets: [ - TargetDescription( - name: "Package1Target1", - dependencies: [ - .product(name: "Package2Target1", package: "Package2"), - ] - ), - ], - traits: [ - .init(name: "default", enabledTraits: ["Package1Trait1"]), - "Package1Trait1", - ] - ), - Manifest.createFileSystemManifest( - displayName: "Package2", - path: "/Package2", - toolsVersion: .v5_9, - products: [ - .init( - name: "Package2Target1", - type: .library(.automatic), - targets: ["Package2Target1"] - ), - ], - targets: [ - TargetDescription( - name: "Package2Target1" - ), - ], - traits: [ - "Package2Trait1", - ] - ), - ], - observabilityScope: observability.topScope - ) - - #expect(observability.diagnostics.count == 0) - - try PackageGraphTester(graph) { result in - try result.checkPackage("Package1") { package in - #expect(package.enabledTraits == ["Package1Trait1"]) - #expect(package.dependencies.count == 1) - } - try result.checkPackage("Package2") { package in - #expect(package.enabledTraits == ["Package2Trait1"]) - } - } - } - - @Test - func traits_whenTraitEnablesDependencyTrait() throws { - let fs = InMemoryFileSystem( - emptyFiles: - "/Package1/Sources/Package1Target1/source.swift", - "/Package2/Sources/Package2Target1/source.swift" - ) - - let manifests = try [ - Manifest.createRootManifest( - displayName: "Package1", - path: "/Package1", - toolsVersion: .v5_9, - dependencies: [ - .localSourceControl( - path: "/Package2", - requirement: .upToNextMajor(from: "1.0.0"), - traits: .init([.init(name: "Package2Trait1", condition: .init(traits: ["Package1Trait1"]))]) - ), - ], - targets: [ - TargetDescription( - name: "Package1Target1", - dependencies: [ - .product(name: "Package2Target1", package: "Package2"), - ] - ), - ], - traits: [ - .init(name: "default", enabledTraits: ["Package1Trait1"]), - .init(name: "Package1Trait1"), - ] - ), - Manifest.createFileSystemManifest( - displayName: "Package2", - path: "/Package2", - toolsVersion: .v5_9, - products: [ - .init( - name: "Package2Target1", - type: .library(.automatic), - targets: ["Package2Target1"] - ), - ], - targets: [ - TargetDescription( - name: "Package2Target1" - ), - ], - traits: [ - "Package2Trait1", - ] - ), - ] - let observability = ObservabilitySystem.makeForTesting() - let graph = try loadModulesGraph( - fileSystem: fs, - manifests: manifests, - observabilityScope: observability.topScope - ) - - #expect(observability.diagnostics.count == 0) - - try PackageGraphTester(graph) { result in - try result.checkPackage("Package1") { package in - #expect(package.enabledTraits == ["Package1Trait1"]) - #expect(package.dependencies.count == 1) - } - try result.checkPackage("Package2") { package in - #expect(package.enabledTraits == ["Package2Trait1"]) - } - } - } - - @Test - func traits_whenComplex() throws { - let fs = InMemoryFileSystem( - emptyFiles: - "/Package1/Sources/Package1Target1/source.swift", - "/Package2/Sources/Package2Target1/source.swift", - "/Package3/Sources/Package3Target1/source.swift", - "/Package4/Sources/Package4Target1/source.swift", - "/Package5/Sources/Package5Target1/source.swift" - ) - - let manifests = try [ - Manifest.createRootManifest( - displayName: "Package1", - path: "/Package1", - toolsVersion: .v5_9, - dependencies: [ - .localSourceControl( - path: "/Package2", - requirement: .upToNextMajor(from: "1.0.0"), - traits: .init([.init(name: "Package2Trait1", condition: .init(traits: ["Package1Trait1"]))]) - ), - .localSourceControl( - path: "/Package4", - requirement: .upToNextMajor(from: "1.0.0"), - traits: .init(["Package4Trait2"]) - ), - .localSourceControl( - path: "/Package5", - requirement: .upToNextMajor(from: "1.0.0") - ), - ], - targets: [ - TargetDescription( - name: "Package1Target1", - dependencies: [ - .product(name: "Package2Target1", package: "Package2"), - .product(name: "Package4Target1", package: "Package4"), - .product( - name: "Package5Target1", - package: "Package5", - condition: .init(traits: ["Package1Trait2"]) - ), - ], - settings: [ - .init( - tool: .swift, - kind: .define("TEST_DEFINE"), - condition: .init(traits: ["Package1Trait1"]) - ), - ] - ), - ], - traits: [ - .init(name: "default", enabledTraits: ["Package1Trait1", "Package1Trait2"]), - .init(name: "Package1Trait1"), - .init(name: "Package1Trait2"), - ] - ), - Manifest.createFileSystemManifest( - displayName: "Package2", - path: "/Package2", - toolsVersion: .v5_9, - dependencies: [ - .localSourceControl( - path: "/Package3", - requirement: .upToNextMajor(from: "1.0.0"), - traits: .init([.init(name: "Package3Trait1", condition: .init(traits: ["Package2Trait1"]))]) - ), - ], - products: [ - .init( - name: "Package2Target1", - type: .library(.automatic), - targets: ["Package2Target1"] - ), - ], - targets: [ - TargetDescription( - name: "Package2Target1", - dependencies: [ - .product(name: "Package3Target1", package: "Package3"), - ] - ), - ], - traits: [ - "Package2Trait1", - ] - ), - Manifest.createFileSystemManifest( - displayName: "Package3", - path: "/Package3", - toolsVersion: .v5_9, - dependencies: [ - .localSourceControl( - path: "/Package4", - requirement: .upToNextMajor(from: "1.0.0"), - traits: .init([.init(name: "Package4Trait1", condition: .init(traits: ["Package3Trait1"]))]) - ), - ], - products: [ - .init( - name: "Package3Target1", - type: .library(.automatic), - targets: ["Package3Target1"] - ), - ], - targets: [ - TargetDescription( - name: "Package3Target1", - dependencies: [ - .product(name: "Package4Target1", package: "Package4"), - ] - ), - ], - traits: [ - "Package3Trait1", - ] - ), - Manifest.createFileSystemManifest( - displayName: "Package4", - path: "/Package4", - toolsVersion: .v5_9, - products: [ - .init( - name: "Package4Target1", - type: .library(.automatic), - targets: ["Package4Target1"] - ), - ], - targets: [ - TargetDescription( - name: "Package4Target1" - ), - ], - traits: [ - "Package4Trait1", - "Package4Trait2", - ] - ), - Manifest.createFileSystemManifest( - displayName: "Package5", - path: "/Package5", - toolsVersion: .v5_9, - products: [ - .init( - name: "Package5Target1", - type: .library(.automatic), - targets: ["Package5Target1"] - ), - ], - targets: [ - TargetDescription( - name: "Package5Target1" - ), - ] - ), - ] - let observability = ObservabilitySystem.makeForTesting() - let graph = try loadModulesGraph( - fileSystem: fs, - manifests: manifests, - observabilityScope: observability.topScope - ) - - #expect(observability.diagnostics.count == 0) - - try PackageGraphTester(graph) { result in - try result.checkPackage("Package1") { package in - #expect(package.enabledTraits == ["Package1Trait1", "Package1Trait2"]) - #expect(package.dependencies.count == 3) - } - try result.checkTarget("Package1Target1") { target in - target.check(dependencies: "Package2Target1", "Package4Target1", "Package5Target1") - target.checkBuildSetting( - declaration: .SWIFT_ACTIVE_COMPILATION_CONDITIONS, - assignments: [ - .init(values: ["TEST_DEFINE"], conditions: [.traits(.init(traits: ["Package1Trait1"]))]), - .init(values: ["Package1Trait2"]), - .init(values: ["Package1Trait1"]), - ] - ) - } - try result.checkPackage("Package2") { package in - #expect(package.enabledTraits == ["Package2Trait1"]) - } - try result.checkPackage("Package3") { package in - #expect(package.enabledTraits == ["Package3Trait1"]) - } - try result.checkPackage("Package4") { package in - #expect(package.enabledTraits == ["Package4Trait1", "Package4Trait2"]) - } - } - } - - @Test - func traits_whenPruneDependenciesEnabled() throws { - let fs = InMemoryFileSystem( - emptyFiles: - "/Package1/Sources/Package1Target1/source.swift", - "/Package2/Sources/Package2Target1/source.swift", - "/Package3/Sources/Package3Target1/source.swift", - "/Package4/Sources/Package4Target1/source.swift", - "/Package5/Sources/Package5Target1/source.swift" - ) - - let manifests = try [ - Manifest.createRootManifest( - displayName: "Package1", - path: "/Package1", - toolsVersion: .v5_9, - dependencies: [ - .localSourceControl( - path: "/Package2", - requirement: .upToNextMajor(from: "1.0.0"), - traits: .init([.init(name: "Package2Trait1", condition: .init(traits: ["Package1Trait1"]))]) - ), - .localSourceControl( - path: "/Package4", - requirement: .upToNextMajor(from: "1.0.0"), - traits: .init(["Package4Trait2"]) - ), - .localSourceControl( - path: "/Package5", - requirement: .upToNextMajor(from: "1.0.0") - ), - ], - targets: [ - TargetDescription( - name: "Package1Target1", - dependencies: [ - .product(name: "Package2Target1", package: "Package2"), - .product(name: "Package4Target1", package: "Package4"), - .product( - name: "Package5Target1", - package: "Package5", - condition: .init(traits: ["Package1Trait2"]) - ), - ], - settings: [ - .init( - tool: .swift, - kind: .define("TEST_DEFINE"), - condition: .init(traits: ["Package1Trait1"]) - ), - .init( - tool: .swift, - kind: .define("TEST_DEFINE_2"), - condition: .init(traits: ["Package1Trait3"]) - ), - ] - ), - ], - traits: [ - .init(name: "default", enabledTraits: ["Package1Trait3"]), - .init(name: "Package1Trait1"), - .init(name: "Package1Trait2"), - .init(name: "Package1Trait3"), - ], - pruneDependencies: true - ), - Manifest.createFileSystemManifest( - displayName: "Package2", - path: "/Package2", - toolsVersion: .v5_9, - dependencies: [ - .localSourceControl( - path: "/Package3", - requirement: .upToNextMajor(from: "1.0.0"), - traits: .init([.init(name: "Package3Trait1", condition: .init(traits: ["Package2Trait1"]))]) - ), - ], - products: [ - .init( - name: "Package2Target1", - type: .library(.automatic), - targets: ["Package2Target1"] - ), - ], - targets: [ - TargetDescription( - name: "Package2Target1", - dependencies: [ - .product(name: "Package3Target1", package: "Package3"), - ] - ), - ], - traits: [ - "Package2Trait1", - ], - pruneDependencies: true - ), - Manifest.createFileSystemManifest( - displayName: "Package3", - path: "/Package3", - toolsVersion: .v5_9, - dependencies: [ - .localSourceControl( - path: "/Package4", - requirement: .upToNextMajor(from: "1.0.0"), - traits: .init([.init(name: "Package4Trait1", condition: .init(traits: ["Package3Trait1"]))]) - ), - ], - products: [ - .init( - name: "Package3Target1", - type: .library(.automatic), - targets: ["Package3Target1"] - ), - ], - targets: [ - TargetDescription( - name: "Package3Target1", - dependencies: [ - .product(name: "Package4Target1", package: "Package4"), - ] - ), - ], - traits: [ - "Package3Trait1", - ], - pruneDependencies: true - ), - Manifest.createFileSystemManifest( - displayName: "Package4", - path: "/Package4", - toolsVersion: .v5_9, - products: [ - .init( - name: "Package4Target1", - type: .library(.automatic), - targets: ["Package4Target1"] - ), - ], - targets: [ - TargetDescription( - name: "Package4Target1" - ), - ], - traits: [ - "Package4Trait1", - "Package4Trait2", - ], - pruneDependencies: true - ), - Manifest.createFileSystemManifest( - displayName: "Package5", - path: "/Package5", - toolsVersion: .v5_9, - products: [ - .init( - name: "Package5Target1", - type: .library(.automatic), - targets: ["Package5Target1"] - ), - ], - targets: [ - TargetDescription( - name: "Package5Target1" - ), - ], - pruneDependencies: true - ), - ] - let observability = ObservabilitySystem.makeForTesting() - let graph = try loadModulesGraph( - fileSystem: fs, - manifests: manifests, - observabilityScope: observability.topScope - ) - - #expect(observability.diagnostics.count == 0) - - try PackageGraphTester(graph) { result in - try result.checkPackage("Package1") { package in - #expect(package.enabledTraits == ["Package1Trait3"]) - #expect(package.dependencies.count == 2) - } - try result.checkTarget("Package1Target1") { target in - target.check(dependencies: "Package2Target1", "Package4Target1") - target.checkBuildSetting( - declaration: .SWIFT_ACTIVE_COMPILATION_CONDITIONS, - assignments: [ - .init(values: ["TEST_DEFINE_2"], conditions: [.traits(.init(traits: ["Package1Trait3"]))]), - .init(values: ["Package1Trait3"]), - ] - ) - } - try result.checkPackage("Package2") { package in - #expect(package.enabledTraits == []) - } - try result.checkPackage("Package3") { package in - #expect(package.enabledTraits == []) - } - try result.checkPackage("Package4") { package in - #expect(package.enabledTraits == ["Package4Trait2"]) - } - } - } - - @Test - func traits_whenPruneDependenciesEnabledForSomeManifests() throws { - let fs = InMemoryFileSystem( - emptyFiles: - "/Package1/Sources/Package1Target1/source.swift", - "/Package2/Sources/Package2Target1/source.swift", - "/Package3/Sources/Package3Target1/source.swift", - "/Package4/Sources/Package4Target1/source.swift", - "/Package5/Sources/Package5Target1/source.swift" - ) - - let manifests = try [ - Manifest.createRootManifest( - displayName: "Package1", - path: "/Package1", - toolsVersion: .v5_9, - dependencies: [ - .localSourceControl( - path: "/Package2", - requirement: .upToNextMajor(from: "1.0.0"), - traits: .init([.init(name: "Package2Trait1", condition: .init(traits: ["Package1Trait1"]))]) - ), - .localSourceControl( - path: "/Package4", - requirement: .upToNextMajor(from: "1.0.0"), - traits: .init(["Package4Trait2"]) - ), - .localSourceControl( - path: "/Package5", - requirement: .upToNextMajor(from: "1.0.0") - ), - ], - targets: [ - TargetDescription( - name: "Package1Target1", - dependencies: [ - .product(name: "Package2Target1", package: "Package2"), - .product(name: "Package4Target1", package: "Package4"), - .product( - name: "Package5Target1", - package: "Package5", - condition: .init(traits: ["Package1Trait2"]) - ), - ], - settings: [ - .init( - tool: .swift, - kind: .define("TEST_DEFINE"), - condition: .init(traits: ["Package1Trait1"]) - ), - .init( - tool: .swift, - kind: .define("TEST_DEFINE_2"), - condition: .init(traits: ["Package1Trait3"]) - ), - ] - ), - ], - traits: [ - .init(name: "default", enabledTraits: ["Package1Trait3"]), - .init(name: "Package1Trait1"), - .init(name: "Package1Trait2"), - .init(name: "Package1Trait3"), - ], - pruneDependencies: false - ), - Manifest.createFileSystemManifest( - displayName: "Package2", - path: "/Package2", - toolsVersion: .v5_9, - dependencies: [ - .localSourceControl( - path: "/Package3", - requirement: .upToNextMajor(from: "1.0.0"), - traits: .init([.init(name: "Package3Trait1", condition: .init(traits: ["Package2Trait1"]))]) - ), - ], - products: [ - .init( - name: "Package2Target1", - type: .library(.automatic), - targets: ["Package2Target1"] - ), - ], - targets: [ - TargetDescription( - name: "Package2Target1", - dependencies: [ - .product(name: "Package3Target1", package: "Package3"), - ] - ), - ], - traits: [ - "Package2Trait1", - ], - pruneDependencies: true - ), - Manifest.createFileSystemManifest( - displayName: "Package3", - path: "/Package3", - toolsVersion: .v5_9, - dependencies: [ - .localSourceControl( - path: "/Package4", - requirement: .upToNextMajor(from: "1.0.0"), - traits: .init([.init(name: "Package4Trait1", condition: .init(traits: ["Package3Trait1"]))]) - ), - ], - products: [ - .init( - name: "Package3Target1", - type: .library(.automatic), - targets: ["Package3Target1"] - ), - ], - targets: [ - TargetDescription( - name: "Package3Target1", - dependencies: [ - .product(name: "Package4Target1", package: "Package4"), - ] - ), - ], - traits: [ - "Package3Trait1", - ], - pruneDependencies: true - ), - Manifest.createFileSystemManifest( - displayName: "Package4", - path: "/Package4", - toolsVersion: .v5_9, - products: [ - .init( - name: "Package4Target1", - type: .library(.automatic), - targets: ["Package4Target1"] - ), - ], - targets: [ - TargetDescription( - name: "Package4Target1" - ), - ], - traits: [ - "Package4Trait1", - "Package4Trait2", - ], - pruneDependencies: true - ), - Manifest.createFileSystemManifest( - displayName: "Package5", - path: "/Package5", - toolsVersion: .v5_9, - products: [ - .init( - name: "Package5Target1", - type: .library(.automatic), - targets: ["Package5Target1"] - ), - ], - targets: [ - TargetDescription( - name: "Package5Target1" - ), - ], - pruneDependencies: true - ), - ] - let observability = ObservabilitySystem.makeForTesting() - let graph = try loadModulesGraph( - fileSystem: fs, - manifests: manifests, - observabilityScope: observability.topScope - ) - - #expect(observability.diagnostics.count == 0) - try PackageGraphTester(graph) { result in - try result.checkPackage("Package1") { package in - #expect(package.enabledTraits == ["Package1Trait3"]) - #expect(package.dependencies.count == 2) - } - try result.checkTarget("Package1Target1") { target in - target.check(dependencies: "Package2Target1", "Package4Target1") - target.checkBuildSetting( - declaration: .SWIFT_ACTIVE_COMPILATION_CONDITIONS, - assignments: [ - .init(values: ["TEST_DEFINE_2"], conditions: [.traits(.init(traits: ["Package1Trait3"]))]), - .init(values: ["Package1Trait3"]), - ] - ) - } - try result.checkPackage("Package2") { package in - #expect(package.enabledTraits == []) - } - try result.checkPackage("Package3") { package in - #expect(package.enabledTraits == []) - } - try result.checkPackage("Package4") { package in - #expect(package.enabledTraits == ["Package4Trait2"]) - } - } - } - - @Test - func traits_whenConditionalDependencies() throws { - let fs = InMemoryFileSystem( - emptyFiles: - "/Lunch/Sources/Drink/source.swift", - "/Caffeine/Sources/CoffeeTarget/source.swift", - "/Juice/Sources/AppleJuiceTarget/source.swift", - ) - - let manifests = try [ - Manifest.createRootManifest( - displayName: "Lunch", - path: "/Lunch", - toolsVersion: .v5_9, - dependencies: [ - .localSourceControl( - path: "/Caffeine", - requirement: .upToNextMajor(from: "1.0.0"), - ), - .localSourceControl( - path: "/Juice", - requirement: .upToNextMajor(from: "1.0.0") - ) - ], - targets: [ - TargetDescription( - name: "Drink", - dependencies: [ - .product( - name: "Coffee", - package: "Caffeine", - condition: .init(traits: ["EnableCoffeeDep"]) - ), - .product( - name: "AppleJuice", - package: "Juice", - condition: .init(traits: ["EnableAppleJuiceDep"]) - ) - ], - ), - ], - traits: [ - .init(name: "default", enabledTraits: ["EnableCoffeeDep"]), - .init(name: "EnableCoffeeDep"), - .init(name: "EnableAppleJuiceDep"), - ], - ), - Manifest.createFileSystemManifest( - displayName: "Caffeine", - path: "/Caffeine", - toolsVersion: .v5_9, - products: [ - .init( - name: "Coffee", - type: .library(.automatic), - targets: ["CoffeeTarget"] - ), - ], - targets: [ - TargetDescription( - name: "CoffeeTarget", - ), - ], - ), - Manifest.createFileSystemManifest( - displayName: "Juice", - path: "/Juice", - toolsVersion: .v5_9, - products: [ - .init( - name: "AppleJuice", - type: .library(.automatic), - targets: ["AppleJuiceTarget"] - ), - ], - targets: [ - TargetDescription( - name: "AppleJuiceTarget", - ), - ], - ) - ] - - // Test graph with default trait configuration - let observability = ObservabilitySystem.makeForTesting() - let graph = try loadModulesGraph( - fileSystem: fs, - manifests: manifests, - observabilityScope: observability.topScope - ) - - #expect(observability.diagnostics.count == 0) - try PackageGraphTester(graph) { result in - try result.checkPackage("Lunch") { package in - #expect(package.enabledTraits == ["EnableCoffeeDep"]) - #expect(package.dependencies.count == 1) - } - try result.checkTarget("Drink") { target in - target.check(dependencies: "Coffee") - } - try result.checkPackage("Caffeine") { package in - #expect(package.enabledTraits == ["default"]) - } - try result.checkPackage("Juice") { package in - #expect(package.enabledTraits == ["default"]) - } - } - - // Test graph when disabling all traits - let graphWithTraitsDisabled = try loadModulesGraph( - fileSystem: fs, - manifests: manifests, - observabilityScope: observability.topScope, - traitConfiguration: .disableAllTraits - ) - #expect(observability.diagnostics.count == 0) - - try PackageGraphTester(graphWithTraitsDisabled) { result in - try result.checkPackage("Lunch") { package in - #expect(package.enabledTraits == []) - #expect(package.dependencies.count == 0) - } - try result.checkTarget("Drink") { target in - #expect(target.target.dependencies.isEmpty) - } - try result.checkPackage("Caffeine") { package in - #expect(package.enabledTraits == ["default"]) - } - try result.checkPackage("Juice") { package in - #expect(package.enabledTraits == ["default"]) - } - } - - // Test graph when we set a trait configuration that enables different traits than the defaults - let graphWithDifferentEnabledTraits = try loadModulesGraph( - fileSystem: fs, - manifests: manifests, - observabilityScope: observability.topScope, - traitConfiguration: .enabledTraits(["EnableAppleJuiceDep"]) - ) - #expect(observability.diagnostics.count == 0) - - try PackageGraphTester(graphWithDifferentEnabledTraits) { result in - try result.checkPackage("Lunch") { package in - #expect(package.enabledTraits == ["EnableAppleJuiceDep"]) - #expect(package.dependencies.count == 1) - } - try result.checkTarget("Drink") { target in - target.check(dependencies: "AppleJuice") - } - try result.checkPackage("Caffeine") { package in - #expect(package.enabledTraits == ["default"]) - } - try result.checkPackage("Juice") { package in - #expect(package.enabledTraits == ["default"]) - } - } - } } extension Manifest { diff --git a/Tests/PackageGraphTests/PubGrubTests.swift b/Tests/PackageGraphTests/PubGrubTests.swift index 8f2bbcfa2dd..ba04635930d 100644 --- a/Tests/PackageGraphTests/PubGrubTests.swift +++ b/Tests/PackageGraphTests/PubGrubTests.swift @@ -3165,11 +3165,11 @@ public class MockContainer: PackageContainer { return version } - public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + public func getDependencies(at version: Version, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { return try getDependencies(at: version.description, productFilter: productFilter, enabledTraits) } - public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + public func getDependencies(at revision: String, productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { guard let revisionDependencies = dependencies[revision] else { throw _MockLoadingError.unknownRevision } @@ -3183,7 +3183,7 @@ public class MockContainer: PackageContainer { }) } - public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: Set = ["default"]) throws -> [PackageContainerConstraint] { + public func getUnversionedDependencies(productFilter: ProductFilter, _ enabledTraits: EnabledTraits = ["default"]) throws -> [PackageContainerConstraint] { // FIXME: This is messy, remove unversionedDeps property. if !unversionedDeps.isEmpty { return unversionedDeps diff --git a/Tests/PackageModelTests/EnabledTraitTests.swift b/Tests/PackageModelTests/EnabledTraitTests.swift new file mode 100644 index 00000000000..35b8a5fc7f7 --- /dev/null +++ b/Tests/PackageModelTests/EnabledTraitTests.swift @@ -0,0 +1,1006 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Testing +import struct PackageModel.EnabledTrait +import struct PackageModel.EnabledTraits +import struct PackageModel.EnabledTraitsMap + +@Suite( + .tags( + .TestSize.small + ) +) +struct EnabledTraitTests { + + // MARK: - EnabledTrait Tests + + /// Verifies that `EnabledTrait` equality is based solely on the trait name, not the setter. + /// Two traits with the same name but different setters are equal, while traits with different names are not equal. + @Test + func enabledTrait_checkEquality() { + let appleTraitSetByApplePie = EnabledTrait.init(name: "Apple", setBy: .package(.init(identity: "ApplePie"))) + let appleTraitSetByAppleJuice = EnabledTrait.init(name: "Apple", setBy: .trait("AppleJuice")) + let appleCoreTrait = EnabledTrait.init(name: "AppleCore", setBy: .default) + + #expect(appleTraitSetByApplePie == appleTraitSetByAppleJuice) + #expect(appleCoreTrait != appleTraitSetByApplePie) + #expect(appleCoreTrait != appleTraitSetByAppleJuice) + } + + /// Tests that unifying two `EnabledTrait` instances with the same name merges their setters + /// into a single set containing both original setters. + @Test + func enabledTrait_unifyEqualTraits() throws { + let bananaTraitSetByFruit = EnabledTrait(name: "Banana", setBy: .package(.init(identity: "Fruit"))) + let bananaTraitSetByBread = EnabledTrait(name: "Banana", setBy: .trait("Bread")) + + let unifiedBananaTrait = try #require(bananaTraitSetByBread.unify(bananaTraitSetByFruit)) + let setters: Set = [ + EnabledTrait.Setter.package(.init(identity: "Fruit")), + EnabledTrait.Setter.trait(.init("Bread")) + ] + + #expect(unifiedBananaTrait.setters == setters) + } + + /// Verifies that attempting to unify two traits with different names returns `nil`, + /// as they cannot be unified. + @Test + func enabledTrait_unifyDifferentTraits() { + let bananaTrait = EnabledTrait(name: "Banana", setBy: .package(.init(identity: "Fruit"))) + let appleTrait = EnabledTrait(name: "Apple", setBy: .package(.init(identity: "Fruit"))) + + let unifiedTrait = bananaTrait.unify(appleTrait) + + #expect(unifiedTrait == nil) + #expect(bananaTrait.setters == appleTrait.setters) + } + + /// Tests that `EnabledTrait` can be compared to a string literal for equality in both + /// directions (trait == string and string == trait). + @Test + func enabledTrait_compareToStringLiteral() { + let appleTrait = EnabledTrait(name: "Apple", setBy: .default) + + #expect("Apple" == appleTrait) // test when EnabledTrait rhs + #expect(appleTrait == "Apple") // test when EnabledTrait lhs + } + + /// Tests that `EnabledTrait` can be compared to a `String` for equality in both + /// directions (trait == string and string == trait). + @Test + func enabledTrait_compareToStringAsEnabledTraitConvertible() { + let appleTrait = EnabledTrait(name: "Apple", setBy: .default) + let stringTrait = "Apple" + + #expect(stringTrait.asEnabledTrait == appleTrait) // test when EnabledTrait rhs + #expect(appleTrait == stringTrait.asEnabledTrait) // test when EnabledTrait lhs + } + + /// Verifies that an `EnabledTrait` can be initialized using a string literal and is + /// equivalent to initialization with `setBy: .default`. + @Test + func enabledTrait_initializedByStringLiteral() { + let appleTraitByString: EnabledTrait = "Apple" + let appleTraitByInit = EnabledTrait(name: "Apple", setBy: .default) + + #expect(appleTraitByString == appleTraitByInit) + } + + /// Confirms that the `id` property of an `EnabledTrait` equals its `name` property. + @Test + func enabledTrait_assertIdIsName() { + let appleTrait = EnabledTrait(name: "Apple", setBy: .default) + + #expect(appleTrait.id == appleTrait.name) + } + + /// Tests the `isDefault` property to verify that a trait named "default" is correctly + /// identified as a default trait. + @Test + func enabledTrait_CheckIfDefault() { + let defaultTrait: EnabledTrait = "default" + + #expect(defaultTrait.isDefault) + } + + /// Verifies that `EnabledTrait` instances can be sorted alphabetically by name and + /// compared using comparison operators (`<`, `>`). + @Test + func enabledTrait_SortAndCompare() { + let appleTrait: EnabledTrait = "Apple" + let bananaTrait: EnabledTrait = "Banana" + let orangeTrait: EnabledTrait = "Orange" + + let traits = [orangeTrait, appleTrait, bananaTrait] + let sortedTraits = traits.sorted() + + #expect(sortedTraits == [appleTrait, bananaTrait, orangeTrait]) + #expect(sortedTraits != traits) + #expect(appleTrait < bananaTrait) + #expect(orangeTrait > bananaTrait) + } + + /// Tests the `parentPackages` property to ensure it correctly filters and returns only + /// package-based setters, excluding trait and trait configuration setters. + @Test + func enabledTrait_getParentPackageSetters() throws { + let traitSetByPackages = EnabledTrait( + name: "Coffee", + setBy: [ + .package(.init(identity: "Cafe")), + .package(.init(identity:"Home")), + .package(.init(identity: "Breakfast")), + .trait("NotAPackage"), + .traitConfiguration + ]) + + let parentPackagesFromTrait = traitSetByPackages.parentPackages + + #expect(Set(parentPackagesFromTrait) == ["Cafe", "Home", "Breakfast"]) + } + + // MARK: - EnabledTraits Tests + + /// Verifies that `EnabledTraits` can be initialized from an array literal of strings, + /// comparing it against another set of traits initialized by a list of `EnabledTrait` + /// containing traits of the same names. + @Test + func enabledTraits_initWithStrings() { + let enabledTraits: EnabledTraits = ["One", "Two", "Three"] + let toTestAgainst = EnabledTraits([ + EnabledTrait(name: "One", setBy: .default), + EnabledTrait(name: "Two", setBy: .default), + EnabledTrait(name: "Three", setBy: .default) + ]) + + #expect(enabledTraits == toTestAgainst) + } + + /// Tests the `.defaults` static property returns an `EnabledTraits` set containing + /// only the "default" trait. + @Test + func enabledTraits_defaultSet() { + let defaults: EnabledTraits = .defaults + + #expect(defaults == ["default"]) + #expect(defaults == [EnabledTrait(name: "default", setBy: .default)]) + } + + /// Verifies the `contains` method works with both string literals and `EnabledTrait` instances, + /// and correctly identifies traits that are and aren't in the set. + @Test + func enabledTraits_containsTrait() { + let enabledTraits: EnabledTraits = ["Apple", "Banana"] + + // Test against a string literal + #expect(enabledTraits.contains("Apple")) + + // Test against an explicitly initialized EnabledTrait + #expect(enabledTraits.contains(EnabledTrait(name: "Apple", setBy: .default))) + + // Test against string literal that is not in the set + #expect(!enabledTraits.contains("Orange")) + + // Test against initialized EnabledTrait that is not in the set + #expect(!enabledTraits.contains(EnabledTrait(name: "Pineapple", setBy: .trait("Apple")))) + } + + /// Tests inserting a trait that already exists (unifying setters), removing it, and verifying + /// the removed trait has the merged setters. Also tests inserting a new trait via string literal. + @Test + func enabledTraits_insertAndRemoveExistingTrait() throws { + var enabledTraits: EnabledTraits = ["Apple", "Banana", "Orange"] + + let newTrait = EnabledTrait(name: "Apple", setBy: [.package(.init(identity: "Fruit")), .trait("FavouriteFruit")]) + + // Assert amount of elements before adding trait + #expect(enabledTraits.count == 3) + + // Insert trait; this should update the existing "Apple" trait by unifying its setters + enabledTraits.insert(newTrait) + #expect(enabledTraits.count == 3) + #expect(enabledTraits == ["Apple", "Banana", "Orange"]) + + + // Assure that Apple trait is removed and returned + let appleTrait = enabledTraits.remove("Apple") + let unwrappedAppleTrait = try #require(appleTrait) + #expect(enabledTraits.count == 2) + #expect(!enabledTraits.contains("Apple")) + + // Assure that Apple trait now has updated setters + #expect(unwrappedAppleTrait.setters == [.package(.init(identity: "Fruit")), .trait("FavouriteFruit")]) + + // Insert trait via string literal + enabledTraits.insert("MyStringTrait") + #expect(enabledTraits.count == 3) + #expect(enabledTraits.contains("MyStringTrait")) + } + + /// Verifies that removing a non-existent trait returns `nil`, and inserting a new trait + /// adds it to the set. + @Test + func enabledTraits_insertAndRemoveNonExistingTrait() throws { + var enabledTraits: EnabledTraits = ["Banana"] + + let newTrait = EnabledTrait(name: "Apple", setBy: [.package(.init(identity: "Fruit")), .trait("FavouriteFruit")]) + + + // Try to remove Apple trait before inserting: + #expect(enabledTraits.remove("Apple") == nil) + #expect(enabledTraits.count == 1) + + // Insert trait + enabledTraits.insert(newTrait) + #expect(enabledTraits.count == 2) + #expect(enabledTraits.contains("Apple")) + } + + /// Tests the `map` method to transform each trait in the set by adding a new setter to each. + @Test + func enabledTraits_flatMapAgainstSetOfTraits() { + let enabledTraits: EnabledTraits = ["Apple", "Coffee", "Cookie"] + let transformedTraits = enabledTraits.map({ oldTrait in + var newTrait = oldTrait + newTrait.setters.insert(.package(.init(identity: "Breakfast"))) + return newTrait + }) + + #expect( + transformedTraits == EnabledTraits([ + EnabledTrait(name: "Apple", setBy: .package(.init(identity: "Breakfast"))), + EnabledTrait(name: "Coffee", setBy: .package(.init(identity: "Breakfast"))), + EnabledTrait(name: "Cookie", setBy: .package(.init(identity: "Breakfast"))) + ]) + ) + } + + /// Verifies that unioning two sets with no overlapping traits combines them into a single larger set + /// containing the traits of both sets. + @Test + func enabledTraits_unionWithNewTraits() { + let enabledTraits: EnabledTraits = ["Banana"] + let newTraits: EnabledTraits = ["Cookie", "Pancakes", "Milkshake"] + + let unifiedSetOfTraits = enabledTraits.union(newTraits) + + #expect(unifiedSetOfTraits.count == 4) + #expect(unifiedSetOfTraits == ["Banana", "Cookie", "Pancakes", "Milkshake"]) + } + + /// Tests unioning sets with overlapping traits, verifying that duplicate traits have their + /// setters merged correctly. + @Test + func enabledTraits_unionWithExistingTraits() throws { + let enabledTraits: EnabledTraits = [ + EnabledTrait(name: "Banana", setBy: .default), + EnabledTrait(name: "Apple", setBy: .package(.init(identity: "MyFruits"))) + ] + let newTraits: EnabledTraits = [ + EnabledTrait(name: "Banana", setBy: [.package(.init(identity: "OtherFruits")), .trait("Bread")]), + EnabledTrait(name: "Apple", setBy: .default), + "Milkshake" + ] + + var unifiedSetOfTraits = enabledTraits.union(newTraits) + + #expect(unifiedSetOfTraits.count == 3) + #expect(unifiedSetOfTraits == ["Banana", "Apple", "Milkshake"]) + + // Check each of the setters for each enabled trait, and assure + // that they can be succesfully removed from the set. + let bananaTrait = try unifiedSetOfTraits.unwrapRemove("Banana") + + #expect(unifiedSetOfTraits.count == 2) + #expect( + bananaTrait.setters == Set([ + .package(.init(identity: "OtherFruits")), + .trait("Bread"), + .default + ]) + ) + + let appleTrait = try unifiedSetOfTraits.unwrapRemove(EnabledTrait(name: "Apple", setBy: .default)) + #expect(unifiedSetOfTraits.count == 1) + #expect( + appleTrait.setters == Set([ + .package(.init(identity: "MyFruits")), + .default + ]) + ) + + let milkshakeTrait = try unifiedSetOfTraits.unwrapRemove("Milkshake") + #expect(unifiedSetOfTraits.isEmpty) + #expect(milkshakeTrait.setters.isEmpty) + } + + /// Verifies that initializing `EnabledTraits` with duplicate trait names in the array + /// results in a single trait (set behavior). + @Test + func enabledTraits_testInitWithArrayOfSameString() throws { + var traits: EnabledTraits = [ + "Banana", + EnabledTrait(name: "Banana", setBy: .default), + "Chocolate" + ] + + #expect(traits.count == 2) + #expect(traits.contains("Banana")) + #expect(traits.contains("Chocolate")) + + let bananaTrait = try traits.unwrapRemove("Banana") + #expect(traits.count == 1) + #expect(traits.contains("Chocolate")) + #expect(!traits.contains(bananaTrait)) + } + + /// Tests that intersecting with an empty set returns an empty set. + @Test + func enabledTraits_testIntersectionWithEmptySet() { + let enabledTraits: EnabledTraits = ["Apple", "Banana", "Cheese"] + let emptyTraits = EnabledTraits() + + let intersection = enabledTraits.intersection(emptyTraits) + #expect(intersection.isEmpty) + } + + /// Verifies that intersecting with an identical set returns the same set. + @Test + func enabledTraits_testIntersectionWithIdenticalSet() { + let enabledTraits: EnabledTraits = ["Apple", "Banana", "Cheese"] + let otherEnabledTraits: EnabledTraits = ["Apple", "Banana", "Cheese"] + #expect(enabledTraits == otherEnabledTraits) + + let intersection = enabledTraits.intersection(otherEnabledTraits) + #expect(intersection == enabledTraits) + #expect(intersection == otherEnabledTraits) + } + + /// Tests intersection of two sets with partial overlap, verifying only common traits are returned. + @Test + func enabledTraits_testIntersectionWithDifferentSets() throws { + let enabledTraits: EnabledTraits = ["Apple", "Banana", "Orange"] + var otherEnabledTraits: EnabledTraits = ["Banana", "Chocolate"] + #expect(enabledTraits != otherEnabledTraits) + + let intersection = enabledTraits.intersection(otherEnabledTraits) + #expect(intersection.count == 1) + #expect(intersection.contains("Banana")) + + let bananaTrait = try otherEnabledTraits.unwrapRemove("Banana") + #expect(!otherEnabledTraits.contains(bananaTrait)) + + let newIntersection = enabledTraits.intersection(otherEnabledTraits) + #expect(newIntersection.isEmpty) + } + + /// Verifies intersection behavior with single-element sets containing the same trait. + @Test + func enabledTraits_testIntersectionWithOneElementSets() throws { + let enabledTraits: EnabledTraits = ["Apple"] + let otherEnabledTraits: EnabledTraits = [EnabledTrait(name: "Apple", setBy: .package(.init(identity: "MyFruits")))] + #expect(enabledTraits == otherEnabledTraits) + + let intersection = enabledTraits.intersection(otherEnabledTraits) + #expect(intersection.count == 1) + #expect(intersection == enabledTraits) + #expect(intersection == otherEnabledTraits) + } + + /// Verifies that isExplicitlySetDefault returns true when "default" is set with an explicit setter + @Test + func enabledTraits_isExplicitlySetDefaultWithSetter() { + let defaultWithSetter = EnabledTraits( + ["default"], + setBy: .package("Package") + ) + + #expect(defaultWithSetter.isExplicitlySetDefault == true) + } + + /// Verifies that isExplicitlySetDefault returns false for the sentinel .defaults value + @Test + func enabledTraits_isExplicitlySetDefaultForSentinel() { + let sentinelDefault = EnabledTraits.defaults + + #expect(sentinelDefault.isExplicitlySetDefault == false) + } + + /// Verifies that isExplicitlySetDefault returns false for non-default traits + @Test + func enabledTraits_isExplicitlySetDefaultForNonDefault() { + let feature = EnabledTraits( + ["Feature1"], + setBy: .package("Package") + ) + + #expect(feature.isExplicitlySetDefault == false) + } + + /// Verifies that isExplicitlySetDefault returns false for multiple traits including default + @Test + func enabledTraits_isExplicitlySetDefaultWithMultipleTraits() { + let mixed = EnabledTraits( + ["default", "Feature1"], + setBy: .package("Package") + ) + + #expect(mixed.isExplicitlySetDefault == false) + } + + // MARK: - EnabledTraitsMap Tests + + /// Tests basic initialization of an empty `EnabledTraitsMap` and verifies default trait behavior. + @Test + func enabledTraitsMap_initEmpty() { + let map = EnabledTraitsMap() + + // Accessing a non-existent package should return ["default"] + #expect(map["PackageNotFound"] == ["default"]) + } + + /// Verifies that `EnabledTraitsMap` can be initialized using dictionary literal syntax. + @Test + func enabledTraitsMap_initWithDictionaryLiteral() { + let map: EnabledTraitsMap = [ + "PackageA": ["Apple", "Banana"], + "PackageB": ["Coffee"] + ] + + #expect(map["PackageA"] == ["Apple", "Banana"]) + #expect(map["PackageB"] == ["Coffee"]) + } + + /// Tests that `EnabledTraitsMap` can be initialized from a dictionary. + @Test + func enabledTraitsMap_initWithDictionary() { + let dictionary: [String: EnabledTraits] = [ + "PackageA": ["Apple", "Banana"], + "PackageB": ["Coffee"] + ] + + let map = EnabledTraitsMap(dictionary) + + #expect(map["PackageA"] == ["Apple", "Banana"]) + #expect(map["PackageB"] == ["Coffee"]) + } + + /// Verifies that setting traits via subscript adds them to the map. + @Test + func enabledTraitsMap_setTraitsViaSubscript() { + var map = EnabledTraitsMap() + + map["MyPackage"] = ["Apple", "Banana"] + + #expect(map["MyPackage"] == ["Apple", "Banana"]) + } + + /// Tests that setting "default" traits explicitly does not store them in the map, + /// since the map returns "default" by default for packages without explicitly + /// set traits. + @Test + func enabledTraitsMap_setDefaultTraitsDoesNotStore() { + var map = EnabledTraitsMap() + + // Setting ["default"] should be omitted from storage + map["MyPackage"] = ["default"] + + // The package should still return ["default"] when accessed + #expect(map["MyPackage"] == ["default"]) + + // But there should be no explicit entry in storage + #expect(map[explicitlyEnabledTraitsFor: "MyPackage"] == nil) + } + + /// Verifies that setting traits multiple times on the same package unifies them + /// (forms union) rather than replacing them. + @Test + func enabledTraitsMap_multipleSetsCombineTraits() { + var map = EnabledTraitsMap() + + map["MyPackage"] = ["Apple", "Banana"] + map["MyPackage"] = ["Coffee", "Chocolate"] + + // Should contain all four traits + #expect(map["MyPackage"].contains("Apple")) + #expect(map["MyPackage"].contains("Banana")) + #expect(map["MyPackage"].contains("Coffee")) + #expect(map["MyPackage"].contains("Chocolate")) + #expect(map["MyPackage"].count == 4) + } + + /// Tests that setting overlapping traits unifies the setters correctly. + @Test + func enabledTraitsMap_overlappingTraitsUnifySetters() throws { + var map = EnabledTraitsMap() + let packageId = "MyPackage" + let parentPackage1 = "Parent1" + let parentPackage2 = "Parent2" + + map[packageId] = EnabledTraits([ + EnabledTrait(name: "Apple", setBy: .package(.init(identity: parentPackage1))) + ]) + + map[packageId] = EnabledTraits([ + EnabledTrait(name: "Apple", setBy: .package(.init(identity: parentPackage2))) + ]) + + var traits = map[packageId] + let appleTrait = try traits.unwrapRemove("Apple") + + // The Apple trait should have both setters + #expect(appleTrait.setters.count == 2) + #expect(appleTrait.setters.contains(.package(.init(identity: parentPackage1)))) + #expect(appleTrait.setters.contains(.package(.init(identity: parentPackage2)))) + } + + /// Verifies the `explicitlyEnabledTraitsFor` subscript returns `nil` for packages + /// without explicitly set traits. + @Test + func enabledTraitsMap_explicitlyEnabledTraitsReturnsNilForDefault() { + let map = EnabledTraitsMap() + let packageId = "MyPackage" + + // No traits have been set, so explicit traits should be nil + #expect(map[explicitlyEnabledTraitsFor: packageId] == nil) + + // But regular subscript should return ["default"] + #expect(map[packageId] == ["default"]) + } + + /// Tests that `explicitlyEnabledTraitsFor` returns the actual traits when they are set. + @Test + func enabledTraitsMap_explicitlyEnabledTraitsReturnsSetTraits() { + var map = EnabledTraitsMap() + let packageId = "MyPackage" + + map[packageId] = ["Apple", "Banana"] + + let explicitTraits = map[explicitlyEnabledTraitsFor: packageId] + + #expect(explicitTraits != nil) + #expect(explicitTraits == ["Apple", "Banana"]) + } + + /// Verifies that `dictionaryLiteral` property returns the underlying storage as a dictionary. + @Test + func enabledTraitsMap_dictionaryLiteralReturnsStorage() { + var map = EnabledTraitsMap() + + map["PackageA"] = ["Apple", "Banana"] + map["PackageB"] = ["Coffee"] + + let dictionary = map.dictionaryLiteral + + #expect(dictionary.count == 2) + #expect(dictionary["PackageA"] == ["Apple", "Banana"]) + #expect(dictionary["PackageB"] == ["Coffee"]) + } + + /// Tests that after setting default traits explicitly, they are omitted from `dictionaryLiteral`. + @Test + func enabledTraitsMap_dictionaryLiteralOmitsDefaultTraits() { + var map = EnabledTraitsMap() + + map["PackageA"] = ["Apple", "Banana"] + map["PackageB"] = ["default"] // Should not be stored + + let dictionary = map.dictionaryLiteral + + // Only PackageA should be in the dictionary + #expect(dictionary.count == 1) + #expect(dictionary["PackageA"] == ["Apple", "Banana"]) + #expect(dictionary["PackageB"] == nil) + } + + /// Verifies behavior when mixing default and non-default traits in a single set operation. + @Test + func enabledTraitsMap_setMixedDefaultAndNonDefaultTraits() { + var map = EnabledTraitsMap() + let packageId = "MyPackage" + + // Set traits including "default" + map[packageId] = ["Apple", "default", "Banana"] + + // The traits should be stored since there are non-default traits + #expect(map[packageId].contains("Apple")) + #expect(map[packageId].contains("Banana")) + #expect(map[packageId].contains("default")) + + // Should have explicit entry + #expect(map[explicitlyEnabledTraitsFor: packageId] != nil) + } + + /// Tests that multiple packages can be stored independently in the map. + @Test + func enabledTraitsMap_multiplePackagesIndependent() { + var map = EnabledTraitsMap() + let packageA = "PackageA" + let packageB = "PackageB" + let packageC = "PackageC" + + map[packageA] = ["Apple"] + map[packageB] = ["Banana"] + // PackageC not set, should default + + #expect(map[packageA] == ["Apple"]) + #expect(map[packageB] == ["Banana"]) + #expect(map[packageC] == ["default"]) + + #expect(map[explicitlyEnabledTraitsFor: packageA] != nil) + #expect(map[explicitlyEnabledTraitsFor: packageB] != nil) + #expect(map[explicitlyEnabledTraitsFor: packageC] == nil) + } + + // MARK: - Disablers Tests + + /// Verifies that setting an empty trait set with a setter records the disabler. + /// Disablers track explicit [] assignments, which disable default traits. + @Test + func enabledTraitsMap_emptyTraitsRecordsDisabler() throws { + var map = EnabledTraitsMap() + let packageId = "MyPackage" + let parentPackage = "ParentPackage" + + // Parent package explicitly sets [] to disable default traits + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) + + // Should record the disabler + let disablers = try #require(map[disablersFor: packageId]) + #expect(disablers.count == 1) + #expect(disablers.first == .package(.init(identity: parentPackage))) + } + + /// Tests that the `disabledBy` property correctly identifies the setter that explicitly set []. + /// This tracks who disabled default traits. + @Test + func enabledTraits_disabledByIdentifiesSetter() { + let parentPackage = "ParentPackage" + let traits = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) + + #expect(traits.isEmpty) + #expect(traits.disabledBy == .package(.init(identity: parentPackage))) + } + + /// Verifies that a non-empty trait set has no disabler. + /// Disablers only track explicit [] assignments. + @Test + func enabledTraits_nonEmptyTraitsHaveNoDisabler() { + let traits = EnabledTraits(["Apple", "Banana"], setBy: .traitConfiguration) + + #expect(!traits.isEmpty) + #expect(traits.disabledBy == nil) + } + + /// Tests that multiple disablers can be recorded for the same package. + /// Multiple parties can each explicitly disable default traits with []. + @Test + func enabledTraitsMap_multipleDisablersRecorded() throws { + var map = EnabledTraitsMap() + let packageId = "MyPackage" + let parentPackage1 = "Parent1" + let parentPackage2 = "Parent2" + + // First parent explicitly disables defaults with [] + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage1))) + + // Second parent also explicitly disables defaults with [] + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage2))) + + let disablers = try #require(map[disablersFor: packageId]) + #expect(disablers.count == 2) + #expect(disablers.contains(.package(.init(identity: parentPackage1))) == true) + #expect(disablers.contains(.package(.init(identity: parentPackage2))) == true) + } + + /// Verifies that disablers from trait configuration are recorded. + /// User can explicitly disable default traits via command line with []. + @Test + func enabledTraitsMap_traitConfigurationDisabler() throws { + var map = EnabledTraitsMap() + let packageId = "MyPackage" + + // User explicitly disables defaults via command line with [] + map[packageId] = EnabledTraits([], setBy: .traitConfiguration) + + let disablers = try #require(map[disablersFor: packageId]) + #expect(disablers.count == 1) + #expect(disablers.contains(.traitConfiguration) == true) + } + + /// Tests that a package with no disablers returns nil for the disablers subscript. + /// Non-empty trait sets don't create disablers. + @Test + func enabledTraitsMap_noDisablersReturnsNil() { + var map = EnabledTraitsMap() + let packageId = "MyPackage" + + // Set some traits (not empty, so no disabler) + map[packageId] = EnabledTraits(["Apple"], setBy: .traitConfiguration) + + let disablers = map[disablersFor: packageId] + #expect(disablers == nil) + } + + /// Verifies that disablers track explicit disablement while traits can still be enabled by other setters. + /// This demonstrates the unified nature: a package can have both disablers AND enabled traits. + @Test + func enabledTraitsMap_disablersCoexistWithEnabledTraits() { + var map = EnabledTraitsMap() + let packageId = "MyPackage" + let parentPackage = "ParentPackage" + + // Parent package explicitly disables default traits with [] + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) + + // Then trait configuration explicitly enables some traits + map[packageId] = EnabledTraits(["Apple"], setBy: .traitConfiguration) + + // Disablers should be recorded (parent disabled defaults) + let disablers = map[disablersFor: packageId] + #expect(disablers != nil) + #expect(disablers?.contains(.package(.init(identity: parentPackage))) == true) + + // And traits should be present (configuration enabled traits) + #expect(map[packageId].contains("Apple")) + #expect(map[packageId].count == 1) + #expect(!map[packageId].contains("default")) + } + + /// Tests the distinction between an unset package and a package with explicitly disabled default traits. + /// Disabling (setting []) means "don't use default traits", but the package still returns defaults + /// if no other traits are explicitly enabled. + @Test + func enabledTraitsMap_distinguishUnsetVsDisabled() { + var map = EnabledTraitsMap() + let unsetPackage = "UnsetPackage" + let disabledPackage = "DisabledPackage" + let parentPackage = "ParentPackage" + + // Parent explicitly disables default traits with [] + map[disabledPackage] = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) + + // Unset package: never touched, no disablers + #expect(map[unsetPackage] == ["default"]) + #expect(map[explicitlyEnabledTraitsFor: unsetPackage] == nil) + #expect(map[disablersFor: unsetPackage] == nil) + + // Disabled package: explicitly set to [], has disablers, and returns empty set + #expect(map[disabledPackage] == []) + #expect(map[explicitlyEnabledTraitsFor: disabledPackage] == []) + #expect(map[disablersFor: disabledPackage] != nil) + } + + /// Verifies that initializing EnabledTraits with an empty string collection creates a disabler. + /// Empty [] means "explicitly disable default traits". + @Test + func enabledTraits_initWithEmptyCollectionCreatesDisabler() { + let emptyTraits: [String] = [] + let traits = EnabledTraits(emptyTraits, setBy: .traitConfiguration) + + #expect(traits.isEmpty) + #expect(traits.disabledBy == .traitConfiguration) + } + + /// Verifies that the same disabler set multiple times only appears once in the set. + /// Set semantics ensure unique disablers. + @Test + func enabledTraitsMap_duplicateDisablerOnlyStoredOnce() { + var map = EnabledTraitsMap() + let packageId = "MyPackage" + let parentPackage = "ParentPackage" + + // Set the same disabler multiple times + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage))) + + let disablers = map[disablersFor: packageId] + #expect(disablers != nil) + #expect(disablers?.count == 1) + #expect(disablers?.contains(.package(.init(identity: parentPackage))) == true) + } + + /// Tests that when one package disables defaults with [] but another package enables traits + /// (including default), the unified map contains the enabled traits plus records the disabler. + @Test + func enabledTraitsMap_disablerAndEnabledTraitsCoexist() { + var map = EnabledTraitsMap() + let packageId = "MyPackage" + let parentPackage1 = "Parent1" + let parentPackage2 = "Parent2" + + // Parent1 explicitly disables default traits with [] + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage1))) + + // Parent2 enables default trait for the same package + map[packageId] = EnabledTraits(["default"], setBy: .package(.init(identity: parentPackage2))) + + // The disabler should be recorded + let disablers = map[disablersFor: packageId] + #expect(disablers != nil) + #expect(disablers?.contains(.package(.init(identity: parentPackage1))) == true) + + // And the default trait should be returned from the map, + // but not included in the explicitly enabled traits set itself. + #expect(map[packageId].contains("default")) + #expect(map[explicitlyEnabledTraitsFor: packageId] == nil) + } + + /// Tests that when one package disables defaults and another enables non-default traits, + /// both the disabler and the enabled traits are tracked. + @Test + func enabledTraitsMap_disablerWithNonDefaultTraitsEnabled() { + var map = EnabledTraitsMap() + let packageId = "MyPackage" + let parentPackage1 = "Parent1" + let parentPackage2 = "Parent2" + + // Parent1 disables defaults with [] + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parentPackage1))) + + // Parent2 enables specific traits + map[packageId] = EnabledTraits(["Apple", "Banana"], setBy: .package(.init(identity: parentPackage2))) + + // Disabler should be recorded + let disablers = map[disablersFor: packageId] + #expect(disablers != nil) + #expect(disablers?.contains(.package(.init(identity: parentPackage1))) == true) + + // Traits should be present + #expect(map[packageId].contains("Apple")) + #expect(map[packageId].contains("Banana")) + #expect(!map[packageId].contains("default")) + #expect(map[packageId].count == 2) + #expect(map[explicitlyEnabledTraitsFor: packageId] == ["Apple", "Banana"]) + } + + // MARK: - Default Setters Tests + + /// Verifies that explicitly-set defaults are tracked in _defaultSetters and not stored + @Test + func enabledTraitsMap_defaultSettersTrackedNotStored() { + var map = EnabledTraitsMap() + let packageId = "ChildPackage" + let parentId = "ParentPackage" + + // Parent explicitly sets default + map[packageId] = EnabledTraits(["default"], setBy: .package(.init(identity: parentId))) + + // Default setter should be tracked + let defaultSetters = map[defaultSettersFor: packageId] + #expect(defaultSetters != nil) + #expect(defaultSetters?.contains(.package(.init(identity: parentId))) == true) + + // But "default" should NOT be in storage + #expect(map[explicitlyEnabledTraitsFor: packageId] == nil) + + // The getter should still return ["default"] (sentinel value) + #expect(map[packageId] == ["default"]) + } + + /// Verifies that multiple parents can set defaults and all are tracked + @Test + func enabledTraitsMap_multipleDefaultSetters() { + var map = EnabledTraitsMap() + let packageId = "ChildPackage" + let parent1 = "Parent1" + let parent2 = "Parent2" + + // Both parents set defaults + map[packageId] = EnabledTraits(["default"], setBy: .package(.init(identity: parent1))) + map[packageId] = EnabledTraits(["default"], setBy: .package(.init(identity: parent2))) + + // Both should be tracked + let defaultSetters = map[defaultSettersFor: packageId] + #expect(defaultSetters?.count == 2) + #expect(defaultSetters?.contains(.package(.init(identity: parent1))) == true) + #expect(defaultSetters?.contains(.package(.init(identity: parent2))) == true) + } + + /// Verifies that default setters coexist with disablers + @Test + func enabledTraitsMap_defaultSettersCoexistWithDisablers() { + var map = EnabledTraitsMap() + let packageId = "ChildPackage" + let parent1 = "Parent1" + let parent2 = "Parent2" + + // Parent1 disables traits + map[packageId] = EnabledTraits([], setBy: .package(.init(identity: parent1))) + + // Parent2 wants defaults + map[packageId] = EnabledTraits(["default"], setBy: .package(.init(identity: parent2))) + + // Both should be tracked independently + let disablers = map[disablersFor: packageId] + let defaultSetters = map[defaultSettersFor: packageId] + + #expect(disablers?.contains(.package(.init(identity: parent1))) == true) + #expect(defaultSetters?.contains(.package(.init(identity: parent2))) == true) + } + + /// Verifies that default setters coexist with explicit traits + @Test + func enabledTraitsMap_defaultSettersCoexistWithExplicitTraits() { + var map = EnabledTraitsMap() + let packageId = "ChildPackage" + let parent1 = "Parent1" + let parent2 = "Parent2" + + // Parent1 explicitly enables Feature1 + map[packageId] = EnabledTraits(["Feature1"], setBy: .package(.init(identity: parent1))) + + // Parent2 wants defaults + map[packageId] = EnabledTraits(["default"], setBy: .package(.init(identity: parent2))) + + // Default setters should be tracked + let defaultSetters = map[defaultSettersFor: packageId] + #expect(defaultSetters?.contains(.package(.init(identity: parent2))) == true) + + // And explicit traits should be stored + #expect(map[packageId].contains("Feature1")) + } + + /// Verifies that setting sentinel .defaults doesn't create a default setter + @Test + func enabledTraitsMap_sentinelDefaultsDoesNotCreateSetter() { + var map = EnabledTraitsMap() + let packageId = "Package" + + // Set sentinel .defaults + map[packageId] = .defaults + + // No default setters should be recorded + #expect(map[defaultSettersFor: packageId] == nil) + #expect(map[explicitlyEnabledTraitsFor: packageId] == nil) + } + + /// Verifies that traitConfiguration can also set defaults and be tracked + @Test + func enabledTraitsMap_traitConfigurationAsDefaultSetter() throws { + var map = EnabledTraitsMap() + let packageId = "Package" + + // Trait configuration sets default + map[packageId] = EnabledTraits(["default"], setBy: .traitConfiguration) + + // Should be tracked + let defaultSetters = try #require(map[defaultSettersFor: packageId]) + #expect(defaultSetters.contains(.traitConfiguration) == true) + } + + /// Verifies that no default setters exist for unset packages + @Test + func enabledTraitsMap_noDefaultSettersForUnsetPackage() { + let map = EnabledTraitsMap() + let packageId = "UnsetPackage" + + // Never touched + #expect(map[defaultSettersFor: packageId] == nil) + } +} + + +// MARK: - Test Helpers +extension EnabledTraits { + /// Helper method that removes a trait from the set and unwraps the returned optional. + /// This method asserts that the trait exists in the set before removal, making tests + /// more concise by combining removal and nil-checking in a single operation. + package mutating func unwrapRemove(_ trait: Element) throws -> Element { + let optionalTrait = self.remove(trait) + let trait = try #require(optionalTrait) + return trait + } +} diff --git a/Tests/PackageModelTests/ManifestTests.swift b/Tests/PackageModelTests/ManifestTests.swift index 19b84a0c56d..0a0b6195a22 100644 --- a/Tests/PackageModelTests/ManifestTests.swift +++ b/Tests/PackageModelTests/ManifestTests.swift @@ -202,8 +202,9 @@ class ManifestTests: XCTestCase { pruneDependencies: true // Since all dependencies are used, this shouldn't affect the outcome. ) + let enabledTraits = EnabledTraits(traits.map(\.name), setBy: .traitConfiguration) for trait in traits.sorted(by: { $0.name < $1.name }) { - XCTAssertThrowsError(try manifest.isTraitEnabled(trait, Set(traits.map(\.name)))) { error in + XCTAssertThrowsError(try manifest.isTraitEnabled(trait, enabledTraits)) { error in XCTAssertEqual("\(error)", """ Trait '\( trait @@ -320,7 +321,7 @@ class ManifestTests: XCTestCase { // When passed .disableAllTraits configuration XCTAssertThrowsError(try manifest.enabledTraits(using: .disableAllTraits)) { error in XCTAssertEqual("\(error)", """ - Disabled default traits on package 'foo' (Foo) that declares no traits. This is prohibited to allow packages to adopt traits initially without causing an API break. + Disabled default traits by command-line trait configuration on package 'foo' (Foo) that declares no traits. This is prohibited to allow packages to adopt traits initially without causing an API break. """) } @@ -337,14 +338,14 @@ class ManifestTests: XCTestCase { // Enabled Traits when passed explicitly enabled traits list: // If given a parent package, and the enabled traits being passed don't exist: - XCTAssertThrowsError(try manifest.enabledTraits(using: ["Trait1"], .init(identity: "qux"))) { error in + XCTAssertThrowsError(try manifest.enabledTraits(using: [EnabledTrait(name: "Trait1", setBy: .package(.init(identity: "qux")))])) { error in XCTAssertEqual("\(error)", """ Package 'qux' enables traits [Trait1] on package 'foo' (Foo) that declares no traits. """) } // If given a parent package, and the default traits are disabled: - XCTAssertThrowsError(try manifest.enabledTraits(using: [], .init(identity: "qux"))) { error in + XCTAssertThrowsError(try manifest.enabledTraits(using: .init([], setBy: .package("qux")))) { error in XCTAssertEqual("\(error)", """ Disabled default traits by package 'qux' on package 'foo' (Foo) that declares no traits. This is prohibited to allow packages to adopt traits initially without causing an API break. """) @@ -458,8 +459,13 @@ class ManifestTests: XCTestCase { traits: traits ) + // Like the above configuration, Trait1 is on by default. When calling `isTraitEnabled`, + // it should calculate transitively enabled traits from here which would eventualy uncover + // that each trait is enabled. + let enabledTraits = EnabledTraits(["Trait1"], setBy: .trait("default")) + for trait in traits.sorted(by: { $0.name < $1.name }) { - XCTAssertTrue(try manifest.isTraitEnabled(trait, Set(traits.map(\.name)))) + XCTAssertTrue(try manifest.isTraitEnabled(trait, enabledTraits)) } } } diff --git a/Tests/WorkspaceTests/WorkspaceTests+Traits.swift b/Tests/WorkspaceTests/WorkspaceTests+Traits.swift new file mode 100644 index 00000000000..4cad90c1de3 --- /dev/null +++ b/Tests/WorkspaceTests/WorkspaceTests+Traits.swift @@ -0,0 +1,1209 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import _InternalTestSupport +import Basics +import PackageModel +import XCTest + +extension WorkspaceTests { + func testTraitConfigurationExists_NoDefaultTraits() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "Foo", + targets: [ + MockTarget( + name: "Foo", + dependencies: [ + .product( + name: "Baz", + package: "Baz", + // Trait1 enabled; should be present in list of dependencies + condition: .init(traits: ["Trait1"]) + ), + .product( + name: "Boo", + package: "Boo", + // Trait2 disabled; should remove this dependency from graph + condition: .init(traits: ["Trait2"]) + ), + ] + ), + MockTarget(name: "Bar", dependencies: ["Baz"]), + ], + products: [ + MockProduct(name: "Foo", modules: ["Foo", "Bar"]), + ], + dependencies: [ + .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(path: "./Boo", requirement: .upToNextMajor(from: "1.0.0")), + ], + traits: ["Trait1", "Trait2"] + ), + ], + packages: [ + MockPackage( + name: "Baz", + targets: [ + MockTarget(name: "Baz"), + ], + products: [ + MockProduct(name: "Baz", modules: ["Baz"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + MockPackage( + name: "Boo", + targets: [ + MockTarget(name: "Boo"), + ], + products: [ + MockProduct(name: "Boo", modules: ["Boo"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + ], + // Only Trait1 is configured to be enabled; since `pruneDependencies` is false + // by default, there will be unused dependencies present + traitConfiguration: .init(enabledTraits: ["Trait1"], enableAllTraits: false) + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"])), + ] + + try await workspace.checkPackageGraph(roots: ["Foo"], deps: deps) { graph, diagnostics in + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Foo") + result.check(packages: "Baz", "Foo") + result.check(modules: "Bar", "Baz", "Foo") + result.checkTarget("Foo") { result in result.check(dependencies: "Baz") } + result.checkTarget("Bar") { result in result.check(dependencies: "Baz") } + } + XCTAssertNoDiagnostics(diagnostics) + } + await workspace.checkManagedDependencies { result in + result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) + } + } + + func testTraitConfigurationExists_WithDefaultTraits() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "Foo", + targets: [ + MockTarget( + name: "Foo", + dependencies: [ + .product( + name: "Baz", + package: "Baz", + condition: .init(traits: ["Trait1"]) + ), + .product( + name: "Boo", + package: "Boo", + condition: .init(traits: ["Trait2"]) + ), + ] + ), + MockTarget(name: "Bar", dependencies: ["Baz"]), + ], + products: [ + MockProduct(name: "Foo", modules: ["Foo", "Bar"]), + ], + dependencies: [ + .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(path: "./Boo", requirement: .upToNextMajor(from: "1.0.0")), + ], + traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] + ), + ], + packages: [ + MockPackage( + name: "Baz", + targets: [ + MockTarget(name: "Baz"), + ], + products: [ + MockProduct(name: "Baz", modules: ["Baz"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + MockPackage( + name: "Boo", + targets: [ + MockTarget(name: "Boo"), + ], + products: [ + MockProduct(name: "Boo", modules: ["Boo"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + ], + // Trait configuration overrides default traits; all traits set to enabled. + traitConfiguration: .init(enabledTraits: [], enableAllTraits: true), + // With this configuration, no dependencies are unused so nothing should be pruned + // despite the `pruneDependencies` flag being set to true. + pruneDependencies: true + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"])), + .sourceControl(path: "./Boo", requirement: .exact("1.0.0"), products: .specific(["Boo"])), + ] + + try await workspace.checkPackageGraph(roots: ["Foo"], deps: deps) { graph, diagnostics in + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Foo") + result.check(packages: "Baz", "Foo", "Boo") + result.check(modules: "Bar", "Baz", "Boo", "Foo") + result.checkTarget("Foo") { result in result.check(dependencies: "Baz", "Boo") } + result.checkTarget("Bar") { result in result.check(dependencies: "Baz") } + } + XCTAssertNoDiagnostics(diagnostics) + } + await workspace.checkManagedDependencies { result in + result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) + result.check(dependency: "boo", at: .checkout(.version("1.0.0"))) + } + } + + func testTraitConfiguration_WithPrunedDependencies() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "Foo", + targets: [ + MockTarget( + name: "Foo", + dependencies: [ + .product( + name: "Baz", + package: "Baz", + condition: .init(traits: ["Trait1"]) + ), + .product( + name: "Boo", + package: "Boo", + condition: .init(traits: ["Trait2"]) + ), + ] + ), + MockTarget(name: "Bar", dependencies: ["Baz"]), + ], + products: [ + MockProduct(name: "Foo", modules: ["Foo", "Bar"]), + ], + dependencies: [ + .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0")), + // unused dependency due to trait guarding; should be omitted + .sourceControl(path: "./Boo", requirement: .upToNextMajor(from: "1.0.0")), + // unused dependency; should be omitted + .sourceControl(path: "./Bam", requirement: .upToNextMajor(from: "1.0.0")), + ], + traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] + ), + ], + packages: [ + MockPackage( + name: "Baz", + targets: [ + MockTarget(name: "Baz"), + ], + products: [ + MockProduct(name: "Baz", modules: ["Baz"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + ], + // Trait configuration overrides default traits; no traits enabled + traitConfiguration: .init(enabledTraits: [], enableAllTraits: false), + pruneDependencies: true + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"])), + .sourceControl(path: "./Boo", requirement: .exact("1.0.0"), products: .specific(["Boo"])), + ] + + try await workspace.checkPackageGraph(roots: ["Foo"], deps: deps) { graph, diagnostics in + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Foo") + result.check(packages: "Baz", "Foo") + result.check(modules: "Bar", "Baz", "Foo") + result.checkTarget("Foo") { result in result.check(dependencies: []) } + result.checkTarget("Bar") { result in result.check(dependencies: "Baz") } + } + XCTAssertNoDiagnostics(diagnostics) + } + await workspace.checkManagedDependencies { result in + result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) + } + } + + func testNoTraitConfiguration_WithDefaultTraits() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "Foo", + targets: [ + MockTarget( + name: "Foo", + dependencies: [ + .product( + name: "Baz", + package: "Baz", + condition: .init(traits: ["Trait1"]) // Baz dependency guarded by traits. + ), + .product( + name: "Boo", + package: "Boo", + condition: .init(traits: ["Trait2"]) + ), + ] + ), + MockTarget(name: "Bar", dependencies: ["Baz"]), // Baz dependency not guarded by traits. + ], + products: [ + MockProduct(name: "Foo", modules: ["Foo", "Bar"]), + ], + dependencies: [ + .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(path: "./Boo", requirement: .upToNextMajor(from: "1.0.0")), + ], + traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] + ), + ], + packages: [ + MockPackage( + name: "Baz", + targets: [ + MockTarget(name: "Baz"), + ], + products: [ + MockProduct(name: "Baz", modules: ["Baz"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + MockPackage( + name: "Boo", + targets: [ + MockTarget(name: "Boo"), + ], + products: [ + MockProduct(name: "Boo", modules: ["Boo"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + ] + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"])), + .sourceControl(path: "./Boo", requirement: .exact("1.0.0"), products: .specific(["Boo"])), + ] + try await workspace.checkPackageGraph(roots: ["Foo"], deps: deps) { graph, diagnostics in + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Foo") + result.check(packages: "Baz", "Boo", "Foo") + result.check(modules: "Bar", "Baz", "Boo", "Foo") + result.checkTarget("Foo") { result in result.check(dependencies: "Boo") } + result.checkTarget("Bar") { result in result.check(dependencies: "Baz") } + } + XCTAssertNoDiagnostics(diagnostics) + } + await workspace.checkManagedDependencies { result in + result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) + } + } + + func testInvalidTrait_WhenParentPackageEnablesTraits() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "Foo", + targets: [ + MockTarget( + name: "Foo", + dependencies: [ + .product( + name: "Baz", + package: "Baz", + condition: .init(traits: ["Trait1"]) + ), + ] + ), + MockTarget(name: "Bar", dependencies: ["Baz"]), + ], + products: [ + MockProduct(name: "Foo", modules: ["Foo", "Bar"]), + ], + dependencies: [ + .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0"), traits: ["TraitNotFound"]), + ], + traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] + ), + ], + packages: [ + MockPackage( + name: "Baz", + targets: [ + MockTarget(name: "Baz"), + ], + products: [ + MockProduct(name: "Baz", modules: ["Baz"]), + ], + traits: ["TraitFound"], + versions: ["1.0.0", "1.5.0"] + ), + ] + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"]), traits: ["TraitFound"]), + ] + + try await workspace.checkPackageGraphFailure(roots: ["Foo"], deps: deps) { diagnostics in + testDiagnostics(diagnostics) { result in + result.check(diagnostic: .equal("Trait 'TraitNotFound' enabled by package 'foo' (Foo) is not declared by package 'baz' (Baz). The available traits declared by this package are: TraitFound."), severity: .error) + } + } + await workspace.checkManagedDependencies { result in + result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) + } + } + + func testInvalidTraitConfiguration_ForRootPackage() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "Foo", + targets: [ + MockTarget( + name: "Foo", + dependencies: [ + .product( + name: "Baz", + package: "Baz", + condition: .init(traits: ["Trait1"]) + ), + ] + ), + MockTarget(name: "Bar", dependencies: ["Baz"]), + ], + products: [ + MockProduct(name: "Foo", modules: ["Foo", "Bar"]), + ], + dependencies: [ + .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0"), traits: ["TraitFound"]), + ], + traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] + ), + ], + packages: [ + MockPackage( + name: "Baz", + targets: [ + MockTarget(name: "Baz"), + ], + products: [ + MockProduct(name: "Baz", modules: ["Baz"]), + ], + traits: ["TraitFound"], + versions: ["1.0.0", "1.5.0"] + ), + ], + // Trait configuration containing trait that isn't defined in the root package. + traitConfiguration: .enabledTraits(["TraitNotFound"]), + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"]), traits: ["TraitFound"]), + ] + + try await workspace.checkPackageGraphFailure(roots: ["Foo"], deps: deps) { diagnostics in + testDiagnostics(diagnostics) { result in + result.check(diagnostic: .equal("Trait 'TraitNotFound' enabled by command-line trait configuration is not declared by package 'foo' (Foo). The available traits declared by this package are: Trait1, Trait2, default."), severity: .error) + } + } + } + + func testManyTraitsEnableTargetDependency() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + func createMockWorkspace(_ traitConfiguration: TraitConfiguration) async throws -> MockWorkspace { + try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "Cereal", + targets: [ + MockTarget( + name: "Wheat", + dependencies: [ + .product( + name: "Icing", + package: "Sugar", + condition: .init(traits: ["BreakfastOfChampions", "DontTellMom"]) + ), + ] + ), + ], + products: [ + MockProduct(name: "YummyBreakfast", modules: ["Wheat"]) + ], + dependencies: [ + .sourceControl(path: "./Sugar", requirement: .upToNextMajor(from: "1.0.0")), + ], + traits: ["BreakfastOfChampions", "DontTellMom"] + ), + ], + packages: [ + MockPackage( + name: "Sugar", + targets: [ + MockTarget(name: "Icing"), + ], + products: [ + MockProduct(name: "Icing", modules: ["Icing"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + ], + traitConfiguration: traitConfiguration + ) + } + + + let deps: [MockDependency] = [ + .sourceControl(path: "./Sugar", requirement: .exact("1.0.0"), products: .specific(["Icing"])), + ] + + let workspaceOfChampions = try await createMockWorkspace(.enabledTraits(["BreakfastOfChampions"])) + try await workspaceOfChampions.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Cereal") + result.check(packages: "cereal", "sugar") + result.check(modules: "Wheat", "Icing") + result.check(products: "YummyBreakfast", "Icing") + result.checkTarget("Wheat") { result in + result.check(dependencies: "Icing") + } + } + } + + let dontTellMomAboutThisWorkspace = try await createMockWorkspace(.enabledTraits(["DontTellMom"])) + try await dontTellMomAboutThisWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Cereal") + result.check(packages: "cereal", "sugar") + result.check(modules: "Wheat", "Icing") + result.check(products: "YummyBreakfast", "Icing") + result.checkTarget("Wheat") { result in + result.check(dependencies: "Icing") + } + } + } + + let allEnabledTraitsWorkspace = try await createMockWorkspace(.enableAllTraits) + try await allEnabledTraitsWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Cereal") + result.check(packages: "cereal", "sugar") + result.check(modules: "Wheat", "Icing") + result.check(products: "YummyBreakfast", "Icing") + result.checkTarget("Wheat") { result in + result.check(dependencies: "Icing") + } + } + } + + let noSugarForBreakfastWorkspace = try await createMockWorkspace(.disableAllTraits) + try await noSugarForBreakfastWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Cereal") + result.check(packages: "cereal") + result.check(modules: "Wheat") + result.check(products: "YummyBreakfast") + } + } + } + + /// Tests that different trait configurations correctly control which conditional dependencies are included. + /// Verifies that enabling different traits (BreakfastOfChampions vs Healthy) includes different + /// dependencies, and that both are included with `enableAllTraits` while neither is included with `disableAllTraits`. + func testTraitsConditionalDependencies() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + func createMockWorkspace(_ traitConfiguration: TraitConfiguration) async throws -> MockWorkspace { + try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "Cereal", + targets: [ + MockTarget( + name: "Wheat", + dependencies: [ + .product( + name: "Icing", + package: "Sugar", + condition: .init(traits: ["BreakfastOfChampions"]) + ), + .product( + name: "Raisin", + package: "Fruit", + condition: .init(traits: ["Healthy"]) + ) + ] + ), + ], + products: [ + MockProduct(name: "YummyBreakfast", modules: ["Wheat"]) + ], + dependencies: [ + .sourceControl(path: "./Sugar", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(path: "./Fruit", requirement: .upToNextMajor(from: "1.0.0")), + ], + traits: ["Healthy", "BreakfastOfChampions"] + ), + ], + packages: [ + MockPackage( + name: "Sugar", + targets: [ + MockTarget(name: "Icing"), + ], + products: [ + MockProduct(name: "Icing", modules: ["Icing"]), + ], + versions: ["1.0.0", "1.5.0"] + ), + MockPackage( + name: "Fruit", + targets: [ + MockTarget(name: "Raisin"), + ], + products: [ + MockProduct(name: "Raisin", modules: ["Raisin"]), + ], + versions: ["1.0.0", "1.2.0"] + ), + ], + traitConfiguration: traitConfiguration + ) + } + + + let deps: [MockDependency] = [ + .sourceControl(path: "./Sugar", requirement: .exact("1.0.0"), products: .specific(["Icing"])), + .sourceControl(path: "./Fruit", requirement: .exact("1.0.0"), products: .specific(["Raisin"])), + ] + + let workspaceOfChampions = try await createMockWorkspace(.enabledTraits(["BreakfastOfChampions"])) + try await workspaceOfChampions.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Cereal") + result.check(packages: "cereal", "sugar") + result.check(modules: "Wheat", "Icing") + result.checkTarget("Wheat") { result in + result.check(dependencies: "Icing") + } + } + } + + let healthyWorkspace = try await createMockWorkspace(.enabledTraits(["Healthy"])) + try await healthyWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Cereal") + result.check(packages: "cereal", "fruit") + result.check(modules: "Wheat", "Raisin") + result.check(products: "YummyBreakfast", "Raisin") + result.checkTarget("Wheat") { result in + result.check(dependencies: "Raisin") + } + } + } + + let allEnabledTraitsWorkspace = try await createMockWorkspace(.enableAllTraits) + try await allEnabledTraitsWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Cereal") + result.check(packages: "cereal", "sugar", "fruit") + result.check(modules: "Wheat", "Icing", "Raisin") + result.check(products: "YummyBreakfast", "Icing", "Raisin") + result.checkTarget("Wheat") { result in + result.check(dependencies: "Icing", "Raisin") + } + } + } + + let boringBreakfastWorkspace = try await createMockWorkspace(.disableAllTraits) + try await boringBreakfastWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "Cereal") + result.check(packages: "cereal") + result.check(modules: "Wheat") + result.check(products: "YummyBreakfast") + } + } + } + + /// Tests that default traits of a dependency package are automatically enabled when + //// the parent doesn't specify traits. + /// Verifies that the default trait enables its configured traits (Enabled1 and + /// Enabled2), which in turn enables trait-guarded dependencies in the dependency's package graph. + func testDefaultTraitsEnabledInPackageDependency() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "RootPackage", + targets: [ + MockTarget( + name: "MyTarget", + dependencies: [ + .product( + name: "MyProduct", + package: "PackageWithDefaultTraits", + ), + ] + ), + ], + products: [ + MockProduct(name: "RootProduct", modules: ["MyTarget"]) + ], + dependencies: [ + .sourceControl(path: "./PackageWithDefaultTraits", requirement: .upToNextMajor(from: "1.0.0")), + ], + ), + ], + packages: [ + MockPackage( + name: "PackageWithDefaultTraits", + targets: [ + MockTarget( + name: "PackageTarget", + dependencies: [ + .product( + name: "GuardedProduct", + package: "GuardedDependency", + condition: .init(traits: ["Enabled1"]) + ) + ] + ), + ], + products: [ + MockProduct(name: "MyProduct", modules: ["PackageTarget"]), + ], + dependencies: [ + .sourceControl(path: "./GuardedDependency", requirement: .upToNextMajor(from: "1.0.0")) + ], + traits: [ + "Enabled1", + "Enabled2", + TraitDescription(name: "default", enabledTraits: ["Enabled1", "Enabled2"]), + "NotEnabled" + ], + versions: ["1.0.0", "1.5.0"] + ), + MockPackage( + name: "GuardedDependency", + targets: [ + MockTarget( + name: "GuardedTarget" + ) + ], + products: [ + MockProduct(name: "GuardedProduct", modules: ["GuardedTarget"]) + ], + versions: ["1.0.0", "1.5.0"] + ) + ], + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./PackageWithDefaultTraits", requirement: .upToNextMajor(from: "1.0.0")), + ] + + try await workspace.checkPackageGraph(roots: ["RootPackage"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "RootPackage") + result.checkPackage("PackageWithDefaultTraits") { package in + guard let enabledTraits = package.enabledTraits else { + XCTFail("No enabled traits on resolved package \(package.identity.description) that is expected to have enabled traits.") + return + } + + let deps = package.dependencies + XCTAssertEqual(deps, [PackageIdentity(urlString: "./GuardedDependency")]) + XCTAssertEqual(enabledTraits, ["Enabled1", "Enabled2"]) + } + } + } + } + + /// Tests the unified trait system where one parent disables default traits with [] + /// while another parent doesn't specify traits (defaults to default traits). + /// The resulting EnabledTraitsMap should have both disablers AND enabled default traits. + func testDefaultTraitDisablersCoexistWithDefaultTraits() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "RootPackage", + targets: [ + MockTarget( + name: "RootTarget", + dependencies: [ + .product(name: "Parent1Product", package: "Parent1"), + .product(name: "Parent2Product", package: "Parent2"), + ] + ), + ], + products: [ + MockProduct(name: "RootProduct", modules: ["RootTarget"]) + ], + dependencies: [ + .sourceControl(path: "./Parent1", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(path: "./Parent2", requirement: .upToNextMajor(from: "1.0.0")), + ] + ), + ], + packages: [ + MockPackage( + name: "Parent1", + targets: [ + MockTarget( + name: "Parent1Target", + dependencies: [ + .product(name: "ChildProduct", package: "ChildPackage") + ] + ), + ], + products: [ + MockProduct(name: "Parent1Product", modules: ["Parent1Target"]) + ], + dependencies: [ + // Parent1 explicitly disables ChildPackage's traits with [] + .sourceControl(path: "./ChildPackage", requirement: .upToNextMajor(from: "1.0.0"), traits: []) + ], + versions: ["1.0.0"] + ), + MockPackage( + name: "Parent2", + targets: [ + MockTarget( + name: "Parent2Target", + dependencies: [ + .product(name: "ChildProduct", package: "ChildPackage") + ] + ), + ], + products: [ + MockProduct(name: "Parent2Product", modules: ["Parent2Target"]) + ], + dependencies: [ + // Parent2 doesn't specify traits, so ChildPackage defaults to default traits + .sourceControl(path: "./ChildPackage", requirement: .upToNextMajor(from: "1.0.0")) + ], + versions: ["1.0.0"] + ), + MockPackage( + name: "ChildPackage", + targets: [ + MockTarget( + name: "ChildTarget", + dependencies: [ + .product( + name: "GuardedProduct", + package: "GuardedDependency", + condition: .init(traits: ["Feature1"]) + ) + ] + ), + ], + products: [ + MockProduct(name: "ChildProduct", modules: ["ChildTarget"]) + ], + dependencies: [ + .sourceControl(path: "./GuardedDependency", requirement: .upToNextMajor(from: "1.0.0")) + ], + traits: [ + "Feature1", + "Feature2", + TraitDescription(name: "default", enabledTraits: ["Feature1"]) + ], + versions: ["1.0.0"] + ), + MockPackage( + name: "GuardedDependency", + targets: [ + MockTarget(name: "GuardedTarget") + ], + products: [ + MockProduct(name: "GuardedProduct", modules: ["GuardedTarget"]) + ], + versions: ["1.0.0"] + ) + ] + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./Parent1", requirement: .exact("1.0.0")), + .sourceControl(path: "./Parent2", requirement: .exact("1.0.0")), + ] + + try await workspace.checkPackageGraph(roots: ["RootPackage"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "RootPackage") + result.check(packages: "RootPackage", "Parent1", "Parent2", "ChildPackage", "GuardedDependency") + + // Verify ChildPackage has default traits enabled (from Parent2) + result.checkPackage("ChildPackage") { package in + guard let enabledTraits = package.enabledTraits else { + XCTFail("No enabled traits on ChildPackage") + return + } + + // Should contain Feature1 from default trait (enabled by Parent2) + XCTAssertEqual(enabledTraits, ["Feature1"]) + + // Verify the dependency on GuardedDependency is included + let deps = package.dependencies + XCTAssertEqual(deps, [PackageIdentity(urlString: "./GuardedDependency")]) + } + + // The graph should include GuardedDependency since Feature1 is enabled + result.check(modules: "RootTarget", "Parent1Target", "Parent2Target", "ChildTarget", "GuardedTarget") + } + } + } + + /// Verifies that when a parent requests defaults (doesn't specify traits), + /// the defaults are properly expanded when the dependency's manifest loads. + /// The "default" trait itself should never appear in the final enabled traits list. + func testDefaultTraitSettersFlattenedOnManifestLoad() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "RootPackage", + targets: [ + MockTarget( + name: "RootTarget", + dependencies: [.product(name: "ChildProduct", package: "ChildPackage")] + ), + ], + products: [MockProduct(name: "RootProduct", modules: ["RootTarget"])], + dependencies: [ + // Root doesn't specify traits - wants defaults + .sourceControl(path: "./ChildPackage", requirement: .upToNextMajor(from: "1.0.0")) + ] + ), + ], + packages: [ + MockPackage( + name: "ChildPackage", + targets: [ + MockTarget(name: "ChildTarget"), + ], + products: [MockProduct(name: "ChildProduct", modules: ["ChildTarget"])], + traits: [ + // Default trait enables Feature1 + .init(name: "default", enabledTraits: ["Feature1"]), + .init(name: "Feature1"), + ], + versions: ["1.0.0"] + ), + ] + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./ChildPackage", requirement: .upToNextMajor(from: "1.0.0")) + ] + + try await workspace.checkPackageGraph(roots: ["RootPackage"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.check(roots: "RootPackage") + result.check(packages: "RootPackage", "ChildPackage") + + // Verify ChildPackage has Feature1 enabled (from expanded default) + // The "default" trait should NOT appear - it should be flattened to Feature1 + result.checkPackage("ChildPackage") { package in + guard let enabledTraits = package.enabledTraits else { + XCTFail("No enabled traits on ChildPackage") + return + } + + // Should contain Feature1 (expanded from default) + XCTAssertTrue(enabledTraits.contains("Feature1")) + // Should NOT contain "default" - it should be flattened + XCTAssertFalse(enabledTraits.contains("default")) + // Should only have Feature1 + XCTAssertEqual(enabledTraits.count, 1) + } + } + } + } + + /// Verifies that when multiple parents don't specify traits (want defaults), + /// all their default requests are tracked and the result is the same regardless of load order. + /// The "default" trait should be flattened to the actual traits it enables. + func testMultipleDefaultTraitSettersOrderIndependent() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "RootPackage", + targets: [ + MockTarget( + name: "RootTarget", + dependencies: [ + .product(name: "Parent1Product", package: "Parent1"), + .product(name: "Parent2Product", package: "Parent2"), + ] + ), + ], + products: [MockProduct(name: "RootProduct", modules: ["RootTarget"])], + dependencies: [ + .sourceControl(path: "./Parent1", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(path: "./Parent2", requirement: .upToNextMajor(from: "1.0.0")), + ] + ), + ], + packages: [ + MockPackage( + name: "Parent1", + targets: [ + MockTarget( + name: "Parent1Target", + dependencies: [.product(name: "ChildProduct", package: "ChildPackage")] + ), + ], + products: [MockProduct(name: "Parent1Product", modules: ["Parent1Target"])], + dependencies: [ + // Parent1 doesn't specify traits - wants defaults + .sourceControl(path: "./ChildPackage", requirement: .upToNextMajor(from: "1.0.0")) + ], + versions: ["1.0.0"] + ), + MockPackage( + name: "Parent2", + targets: [ + MockTarget( + name: "Parent2Target", + dependencies: [.product(name: "ChildProduct", package: "ChildPackage")] + ), + ], + products: [MockProduct(name: "Parent2Product", modules: ["Parent2Target"])], + dependencies: [ + // Parent2 also doesn't specify traits - wants defaults + .sourceControl(path: "./ChildPackage", requirement: .upToNextMajor(from: "1.0.0")) + ], + versions: ["1.0.0"] + ), + MockPackage( + name: "ChildPackage", + targets: [MockTarget(name: "ChildTarget")], + products: [MockProduct(name: "ChildProduct", modules: ["ChildTarget"])], + traits: [ + .init(name: "default", enabledTraits: ["Feature1"]), + .init(name: "Feature1"), + ], + versions: ["1.0.0"] + ), + ] + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./Parent1", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(path: "./Parent2", requirement: .upToNextMajor(from: "1.0.0")), + ] + + try await workspace.checkPackageGraph(roots: ["RootPackage"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.checkPackage("ChildPackage") { package in + guard let enabledTraits = package.enabledTraits else { + XCTFail("No enabled traits on ChildPackage") + return + } + + // Should have Feature1 enabled from both parents wanting defaults + XCTAssertTrue(enabledTraits.contains("Feature1")) + // Should NOT contain "default" - it should be flattened + XCTAssertFalse(enabledTraits.contains("default")) + } + } + } + } + + /// Verifies that when all parents disable traits, no defaults are enabled. + /// The final enabled traits should be empty. + func testAllParentsDisableDefaultTraits() async throws { + let sandbox = AbsolutePath("/tmp/ws/") + let fs = InMemoryFileSystem() + + let workspace = try await MockWorkspace( + sandbox: sandbox, + fileSystem: fs, + roots: [ + MockPackage( + name: "RootPackage", + targets: [ + MockTarget( + name: "RootTarget", + dependencies: [ + .product(name: "Parent1Product", package: "Parent1"), + .product(name: "Parent2Product", package: "Parent2"), + ] + ), + ], + products: [MockProduct(name: "RootProduct", modules: ["RootTarget"])], + dependencies: [ + .sourceControl(path: "./Parent1", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(path: "./Parent2", requirement: .upToNextMajor(from: "1.0.0")), + ] + ), + ], + packages: [ + MockPackage( + name: "Parent1", + targets: [ + MockTarget( + name: "Parent1Target", + dependencies: [.product(name: "ChildProduct", package: "ChildPackage")] + ), + ], + products: [MockProduct(name: "Parent1Product", modules: ["Parent1Target"])], + dependencies: [ + // Parent1 disables all traits + .sourceControl(path: "./ChildPackage", requirement: .upToNextMajor(from: "1.0.0"), traits: []) + ], + versions: ["1.0.0"] + ), + MockPackage( + name: "Parent2", + targets: [ + MockTarget( + name: "Parent2Target", + dependencies: [.product(name: "ChildProduct", package: "ChildPackage")] + ), + ], + products: [MockProduct(name: "Parent2Product", modules: ["Parent2Target"])], + dependencies: [ + // Parent2 also disables all traits + .sourceControl(path: "./ChildPackage", requirement: .upToNextMajor(from: "1.0.0"), traits: []) + ], + versions: ["1.0.0"] + ), + MockPackage( + name: "ChildPackage", + targets: [MockTarget(name: "ChildTarget")], + products: [MockProduct(name: "ChildProduct", modules: ["ChildTarget"])], + traits: [ + .init(name: "default", enabledTraits: ["Feature1"]), + .init(name: "Feature1"), + ], + versions: ["1.0.0"] + ), + ] + ) + + let deps: [MockDependency] = [ + .sourceControl(path: "./Parent1", requirement: .upToNextMajor(from: "1.0.0")), + .sourceControl(path: "./Parent2", requirement: .upToNextMajor(from: "1.0.0")), + ] + + try await workspace.checkPackageGraph(roots: ["RootPackage"], deps: deps) { graph, diagnostics in + XCTAssertNoDiagnostics(diagnostics) + PackageGraphTesterXCTest(graph) { result in + result.checkPackage("ChildPackage") { package in + guard let enabledTraits = package.enabledTraits else { + XCTFail("No enabled traits on ChildPackage") + return + } + + // Should have NO traits enabled (both parents disabled) + XCTAssertTrue(enabledTraits.isEmpty) + } + } + } + } + +} + diff --git a/Tests/WorkspaceTests/WorkspaceTests.swift b/Tests/WorkspaceTests/WorkspaceTests.swift index eaca8c6e7e9..ea2974d13c0 100644 --- a/Tests/WorkspaceTests/WorkspaceTests.swift +++ b/Tests/WorkspaceTests/WorkspaceTests.swift @@ -15852,690 +15852,6 @@ final class WorkspaceTests: XCTestCase { } } - func testTraitConfigurationExists_NoDefaultTraits() async throws { - let sandbox = AbsolutePath("/tmp/ws/") - let fs = InMemoryFileSystem() - - let workspace = try await MockWorkspace( - sandbox: sandbox, - fileSystem: fs, - roots: [ - MockPackage( - name: "Foo", - targets: [ - MockTarget( - name: "Foo", - dependencies: [ - .product( - name: "Baz", - package: "Baz", - // Trait1 enabled; should be present in list of dependencies - condition: .init(traits: ["Trait1"]) - ), - .product( - name: "Boo", - package: "Boo", - // Trait2 disabled; should remove this dependency from graph - condition: .init(traits: ["Trait2"]) - ), - ] - ), - MockTarget(name: "Bar", dependencies: ["Baz"]), - ], - products: [ - MockProduct(name: "Foo", modules: ["Foo", "Bar"]), - ], - dependencies: [ - .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0")), - .sourceControl(path: "./Boo", requirement: .upToNextMajor(from: "1.0.0")), - ], - traits: ["Trait1", "Trait2"] - ), - ], - packages: [ - MockPackage( - name: "Baz", - targets: [ - MockTarget(name: "Baz"), - ], - products: [ - MockProduct(name: "Baz", modules: ["Baz"]), - ], - versions: ["1.0.0", "1.5.0"] - ), - MockPackage( - name: "Boo", - targets: [ - MockTarget(name: "Boo"), - ], - products: [ - MockProduct(name: "Boo", modules: ["Boo"]), - ], - versions: ["1.0.0", "1.5.0"] - ), - ], - // Only Trait1 is configured to be enabled; since `pruneDependencies` is false - // by default, there will be unused dependencies present - traitConfiguration: .init(enabledTraits: ["Trait1"], enableAllTraits: false) - ) - - let deps: [MockDependency] = [ - .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"])), - ] - - try await workspace.checkPackageGraph(roots: ["Foo"], deps: deps) { graph, diagnostics in - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Foo") - result.check(packages: "Baz", "Foo") - result.check(modules: "Bar", "Baz", "Foo") - result.checkTarget("Foo") { result in result.check(dependencies: "Baz") } - result.checkTarget("Bar") { result in result.check(dependencies: "Baz") } - } - XCTAssertNoDiagnostics(diagnostics) - } - await workspace.checkManagedDependencies { result in - result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) - } - } - - func testTraitConfigurationExists_WithDefaultTraits() async throws { - let sandbox = AbsolutePath("/tmp/ws/") - let fs = InMemoryFileSystem() - - let workspace = try await MockWorkspace( - sandbox: sandbox, - fileSystem: fs, - roots: [ - MockPackage( - name: "Foo", - targets: [ - MockTarget( - name: "Foo", - dependencies: [ - .product( - name: "Baz", - package: "Baz", - condition: .init(traits: ["Trait1"]) - ), - .product( - name: "Boo", - package: "Boo", - condition: .init(traits: ["Trait2"]) - ), - ] - ), - MockTarget(name: "Bar", dependencies: ["Baz"]), - ], - products: [ - MockProduct(name: "Foo", modules: ["Foo", "Bar"]), - ], - dependencies: [ - .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0")), - .sourceControl(path: "./Boo", requirement: .upToNextMajor(from: "1.0.0")), - ], - traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] - ), - ], - packages: [ - MockPackage( - name: "Baz", - targets: [ - MockTarget(name: "Baz"), - ], - products: [ - MockProduct(name: "Baz", modules: ["Baz"]), - ], - versions: ["1.0.0", "1.5.0"] - ), - MockPackage( - name: "Boo", - targets: [ - MockTarget(name: "Boo"), - ], - products: [ - MockProduct(name: "Boo", modules: ["Boo"]), - ], - versions: ["1.0.0", "1.5.0"] - ), - ], - // Trait configuration overrides default traits; all traits set to enabled. - traitConfiguration: .init(enabledTraits: [], enableAllTraits: true), - // With this configuration, no dependencies are unused so nothing should be pruned - // despite the `pruneDependencies` flag being set to true. - pruneDependencies: true - ) - - let deps: [MockDependency] = [ - .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"])), - .sourceControl(path: "./Boo", requirement: .exact("1.0.0"), products: .specific(["Boo"])), - ] - - try await workspace.checkPackageGraph(roots: ["Foo"], deps: deps) { graph, diagnostics in - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Foo") - result.check(packages: "Baz", "Foo", "Boo") - result.check(modules: "Bar", "Baz", "Boo", "Foo") - result.checkTarget("Foo") { result in result.check(dependencies: "Baz", "Boo") } - result.checkTarget("Bar") { result in result.check(dependencies: "Baz") } - } - XCTAssertNoDiagnostics(diagnostics) - } - await workspace.checkManagedDependencies { result in - result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) - result.check(dependency: "boo", at: .checkout(.version("1.0.0"))) - } - } - - func testTraitConfiguration_WithPrunedDependencies() async throws { - let sandbox = AbsolutePath("/tmp/ws/") - let fs = InMemoryFileSystem() - - let workspace = try await MockWorkspace( - sandbox: sandbox, - fileSystem: fs, - roots: [ - MockPackage( - name: "Foo", - targets: [ - MockTarget( - name: "Foo", - dependencies: [ - .product( - name: "Baz", - package: "Baz", - condition: .init(traits: ["Trait1"]) - ), - .product( - name: "Boo", - package: "Boo", - condition: .init(traits: ["Trait2"]) - ), - ] - ), - MockTarget(name: "Bar", dependencies: ["Baz"]), - ], - products: [ - MockProduct(name: "Foo", modules: ["Foo", "Bar"]), - ], - dependencies: [ - .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0")), - // unused dependency due to trait guarding; should be omitted - .sourceControl(path: "./Boo", requirement: .upToNextMajor(from: "1.0.0")), - // unused dependency; should be omitted - .sourceControl(path: "./Bam", requirement: .upToNextMajor(from: "1.0.0")), - ], - traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] - ), - ], - packages: [ - MockPackage( - name: "Baz", - targets: [ - MockTarget(name: "Baz"), - ], - products: [ - MockProduct(name: "Baz", modules: ["Baz"]), - ], - versions: ["1.0.0", "1.5.0"] - ), - ], - // Trait configuration overrides default traits; no traits enabled - traitConfiguration: .init(enabledTraits: [], enableAllTraits: false), - pruneDependencies: true - ) - - let deps: [MockDependency] = [ - .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"])), - .sourceControl(path: "./Boo", requirement: .exact("1.0.0"), products: .specific(["Boo"])), - ] - - try await workspace.checkPackageGraph(roots: ["Foo"], deps: deps) { graph, diagnostics in - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Foo") - result.check(packages: "Baz", "Foo") - result.check(modules: "Bar", "Baz", "Foo") - result.checkTarget("Foo") { result in result.check(dependencies: []) } - result.checkTarget("Bar") { result in result.check(dependencies: "Baz") } - } - XCTAssertNoDiagnostics(diagnostics) - } - await workspace.checkManagedDependencies { result in - result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) - } - } - - func testNoTraitConfiguration_WithDefaultTraits() async throws { - let sandbox = AbsolutePath("/tmp/ws/") - let fs = InMemoryFileSystem() - - let workspace = try await MockWorkspace( - sandbox: sandbox, - fileSystem: fs, - roots: [ - MockPackage( - name: "Foo", - targets: [ - MockTarget( - name: "Foo", - dependencies: [ - .product( - name: "Baz", - package: "Baz", - condition: .init(traits: ["Trait1"]) // Baz dependency guarded by traits. - ), - .product( - name: "Boo", - package: "Boo", - condition: .init(traits: ["Trait2"]) - ), - ] - ), - MockTarget(name: "Bar", dependencies: ["Baz"]), // Baz dependency not guarded by traits. - ], - products: [ - MockProduct(name: "Foo", modules: ["Foo", "Bar"]), - ], - dependencies: [ - .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0")), - .sourceControl(path: "./Boo", requirement: .upToNextMajor(from: "1.0.0")), - ], - traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] - ), - ], - packages: [ - MockPackage( - name: "Baz", - targets: [ - MockTarget(name: "Baz"), - ], - products: [ - MockProduct(name: "Baz", modules: ["Baz"]), - ], - versions: ["1.0.0", "1.5.0"] - ), - MockPackage( - name: "Boo", - targets: [ - MockTarget(name: "Boo"), - ], - products: [ - MockProduct(name: "Boo", modules: ["Boo"]), - ], - versions: ["1.0.0", "1.5.0"] - ), - ] - ) - - let deps: [MockDependency] = [ - .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"])), - .sourceControl(path: "./Boo", requirement: .exact("1.0.0"), products: .specific(["Boo"])), - ] - try await workspace.checkPackageGraph(roots: ["Foo"], deps: deps) { graph, diagnostics in - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Foo") - result.check(packages: "Baz", "Boo", "Foo") - result.check(modules: "Bar", "Baz", "Boo", "Foo") - result.checkTarget("Foo") { result in result.check(dependencies: "Boo") } - result.checkTarget("Bar") { result in result.check(dependencies: "Baz") } - } - XCTAssertNoDiagnostics(diagnostics) - } - await workspace.checkManagedDependencies { result in - result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) - } - } - - func testInvalidTrait_WhenParentPackageEnablesTraits() async throws { - let sandbox = AbsolutePath("/tmp/ws/") - let fs = InMemoryFileSystem() - - let workspace = try await MockWorkspace( - sandbox: sandbox, - fileSystem: fs, - roots: [ - MockPackage( - name: "Foo", - targets: [ - MockTarget( - name: "Foo", - dependencies: [ - .product( - name: "Baz", - package: "Baz", - condition: .init(traits: ["Trait1"]) - ), - ] - ), - MockTarget(name: "Bar", dependencies: ["Baz"]), - ], - products: [ - MockProduct(name: "Foo", modules: ["Foo", "Bar"]), - ], - dependencies: [ - .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0"), traits: ["TraitNotFound"]), - ], - traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] - ), - ], - packages: [ - MockPackage( - name: "Baz", - targets: [ - MockTarget(name: "Baz"), - ], - products: [ - MockProduct(name: "Baz", modules: ["Baz"]), - ], - traits: ["TraitFound"], - versions: ["1.0.0", "1.5.0"] - ), - ] - ) - - let deps: [MockDependency] = [ - .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"]), traits: ["TraitFound"]), - ] - - try await workspace.checkPackageGraphFailure(roots: ["Foo"], deps: deps) { diagnostics in - testDiagnostics(diagnostics) { result in - result.check(diagnostic: .equal("Trait 'TraitNotFound' enabled by parent package 'foo' (Foo) is not declared by package 'baz' (Baz). The available traits declared by this package are: TraitFound."), severity: .error) - } - } - await workspace.checkManagedDependencies { result in - result.check(dependency: "baz", at: .checkout(.version("1.0.0"))) - } - } - - func testInvalidTraitConfiguration_ForRootPackage() async throws { - let sandbox = AbsolutePath("/tmp/ws/") - let fs = InMemoryFileSystem() - - let workspace = try await MockWorkspace( - sandbox: sandbox, - fileSystem: fs, - roots: [ - MockPackage( - name: "Foo", - targets: [ - MockTarget( - name: "Foo", - dependencies: [ - .product( - name: "Baz", - package: "Baz", - condition: .init(traits: ["Trait1"]) - ), - ] - ), - MockTarget(name: "Bar", dependencies: ["Baz"]), - ], - products: [ - MockProduct(name: "Foo", modules: ["Foo", "Bar"]), - ], - dependencies: [ - .sourceControl(path: "./Baz", requirement: .upToNextMajor(from: "1.0.0"), traits: ["TraitFound"]), - ], - traits: [.init(name: "default", enabledTraits: ["Trait2"]), "Trait1", "Trait2"] - ), - ], - packages: [ - MockPackage( - name: "Baz", - targets: [ - MockTarget(name: "Baz"), - ], - products: [ - MockProduct(name: "Baz", modules: ["Baz"]), - ], - traits: ["TraitFound"], - versions: ["1.0.0", "1.5.0"] - ), - ], - // Trait configuration containing trait that isn't defined in the root package. - traitConfiguration: .enabledTraits(["TraitNotFound"]), - ) - - let deps: [MockDependency] = [ - .sourceControl(path: "./Baz", requirement: .exact("1.0.0"), products: .specific(["Baz"]), traits: ["TraitFound"]), - ] - - try await workspace.checkPackageGraphFailure(roots: ["Foo"], deps: deps) { diagnostics in - testDiagnostics(diagnostics) { result in - result.check(diagnostic: .equal("Trait 'TraitNotFound' is not declared by package 'foo' (Foo). The available traits declared by this package are: Trait1, Trait2, default."), severity: .error) - } - } - } - - func testManyTraitsEnableTargetDependency() async throws { - let sandbox = AbsolutePath("/tmp/ws/") - let fs = InMemoryFileSystem() - - func createMockWorkspace(_ traitConfiguration: TraitConfiguration) async throws -> MockWorkspace { - try await MockWorkspace( - sandbox: sandbox, - fileSystem: fs, - roots: [ - MockPackage( - name: "Cereal", - targets: [ - MockTarget( - name: "Wheat", - dependencies: [ - .product( - name: "Icing", - package: "Sugar", - condition: .init(traits: ["BreakfastOfChampions", "DontTellMom"]) - ), - ] - ), - ], - products: [ - MockProduct(name: "YummyBreakfast", modules: ["Wheat"]) - ], - dependencies: [ - .sourceControl(path: "./Sugar", requirement: .upToNextMajor(from: "1.0.0")), - ], - traits: ["BreakfastOfChampions", "DontTellMom"] - ), - ], - packages: [ - MockPackage( - name: "Sugar", - targets: [ - MockTarget(name: "Icing"), - ], - products: [ - MockProduct(name: "Icing", modules: ["Icing"]), - ], - versions: ["1.0.0", "1.5.0"] - ), - ], - traitConfiguration: traitConfiguration - ) - } - - - let deps: [MockDependency] = [ - .sourceControl(path: "./Sugar", requirement: .exact("1.0.0"), products: .specific(["Icing"])), - ] - - let workspaceOfChampions = try await createMockWorkspace(.enabledTraits(["BreakfastOfChampions"])) - try await workspaceOfChampions.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in - XCTAssertNoDiagnostics(diagnostics) - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Cereal") - result.check(packages: "cereal", "sugar") - result.check(modules: "Wheat", "Icing") - result.check(products: "YummyBreakfast", "Icing") - result.checkTarget("Wheat") { result in - result.check(dependencies: "Icing") - } - } - } - - let dontTellMomAboutThisWorkspace = try await createMockWorkspace(.enabledTraits(["DontTellMom"])) - try await dontTellMomAboutThisWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in - XCTAssertNoDiagnostics(diagnostics) - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Cereal") - result.check(packages: "cereal", "sugar") - result.check(modules: "Wheat", "Icing") - result.check(products: "YummyBreakfast", "Icing") - result.checkTarget("Wheat") { result in - result.check(dependencies: "Icing") - } - } - } - - let allEnabledTraitsWorkspace = try await createMockWorkspace(.enableAllTraits) - try await allEnabledTraitsWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in - XCTAssertNoDiagnostics(diagnostics) - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Cereal") - result.check(packages: "cereal", "sugar") - result.check(modules: "Wheat", "Icing") - result.check(products: "YummyBreakfast", "Icing") - result.checkTarget("Wheat") { result in - result.check(dependencies: "Icing") - } - } - } - - let noSugarForBreakfastWorkspace = try await createMockWorkspace(.disableAllTraits) - try await noSugarForBreakfastWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in - XCTAssertNoDiagnostics(diagnostics) - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Cereal") - result.check(packages: "cereal") - result.check(modules: "Wheat") - result.check(products: "YummyBreakfast") - } - } - } - - func testTraitsConditionalDependencies() async throws { - let sandbox = AbsolutePath("/tmp/ws/") - let fs = InMemoryFileSystem() - - func createMockWorkspace(_ traitConfiguration: TraitConfiguration) async throws -> MockWorkspace { - try await MockWorkspace( - sandbox: sandbox, - fileSystem: fs, - roots: [ - MockPackage( - name: "Cereal", - targets: [ - MockTarget( - name: "Wheat", - dependencies: [ - .product( - name: "Icing", - package: "Sugar", - condition: .init(traits: ["BreakfastOfChampions"]) - ), - .product( - name: "Raisin", - package: "Fruit", - condition: .init(traits: ["Healthy"]) - ) - ] - ), - ], - products: [ - MockProduct(name: "YummyBreakfast", modules: ["Wheat"]) - ], - dependencies: [ - .sourceControl(path: "./Sugar", requirement: .upToNextMajor(from: "1.0.0")), - .sourceControl(path: "./Fruit", requirement: .upToNextMajor(from: "1.0.0")), - ], - traits: ["Healthy", "BreakfastOfChampions"] - ), - ], - packages: [ - MockPackage( - name: "Sugar", - targets: [ - MockTarget(name: "Icing"), - ], - products: [ - MockProduct(name: "Icing", modules: ["Icing"]), - ], - versions: ["1.0.0", "1.5.0"] - ), - MockPackage( - name: "Fruit", - targets: [ - MockTarget(name: "Raisin"), - ], - products: [ - MockProduct(name: "Raisin", modules: ["Raisin"]), - ], - versions: ["1.0.0", "1.2.0"] - ), - ], - traitConfiguration: traitConfiguration - ) - } - - - let deps: [MockDependency] = [ - .sourceControl(path: "./Sugar", requirement: .exact("1.0.0"), products: .specific(["Icing"])), - .sourceControl(path: "./Fruit", requirement: .exact("1.0.0"), products: .specific(["Raisin"])), - ] - - let workspaceOfChampions = try await createMockWorkspace(.enabledTraits(["BreakfastOfChampions"])) - try await workspaceOfChampions.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in - XCTAssertNoDiagnostics(diagnostics) - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Cereal") - result.check(packages: "cereal", "sugar") - result.check(modules: "Wheat", "Icing") - result.checkTarget("Wheat") { result in - result.check(dependencies: "Icing") - } - } - } - - let healthyWorkspace = try await createMockWorkspace(.enabledTraits(["Healthy"])) - try await healthyWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in - XCTAssertNoDiagnostics(diagnostics) - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Cereal") - result.check(packages: "cereal", "fruit") - result.check(modules: "Wheat", "Raisin") - result.check(products: "YummyBreakfast", "Raisin") - result.checkTarget("Wheat") { result in - result.check(dependencies: "Raisin") - } - } - } - - let allEnabledTraitsWorkspace = try await createMockWorkspace(.enableAllTraits) - try await allEnabledTraitsWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in - XCTAssertNoDiagnostics(diagnostics) - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Cereal") - result.check(packages: "cereal", "sugar", "fruit") - result.check(modules: "Wheat", "Icing", "Raisin") - result.check(products: "YummyBreakfast", "Icing", "Raisin") - result.checkTarget("Wheat") { result in - result.check(dependencies: "Icing", "Raisin") - } - } - } - - let boringBreakfastWorkspace = try await createMockWorkspace(.disableAllTraits) - try await boringBreakfastWorkspace.checkPackageGraph(roots: ["Cereal"], deps: deps) { graph, diagnostics in - XCTAssertNoDiagnostics(diagnostics) - PackageGraphTesterXCTest(graph) { result in - result.check(roots: "Cereal") - result.check(packages: "cereal") - result.check(modules: "Wheat") - result.check(products: "YummyBreakfast") - } - } - } - func makeRegistryClient( packageIdentity: PackageIdentity, packageVersion: Version,