diff --git a/Sources/Basics/Dictionary+Extensions.swift b/Sources/Basics/Dictionary+Extensions.swift index 821419a8350..751e14b5f26 100644 --- a/Sources/Basics/Dictionary+Extensions.swift +++ b/Sources/Basics/Dictionary+Extensions.swift @@ -20,3 +20,13 @@ extension Dictionary { return value } } + +extension OrderedDictionary { + public subscript(key: Key, `default` `default`: Value) -> Value { + set { + self[key] = newValue + } get { + self[key] ?? `default` + } + } +} diff --git a/Sources/PackageGraph/DependencyResolutionNode.swift b/Sources/PackageGraph/DependencyResolutionNode.swift index cdfbacae429..4dc655dc172 100644 --- a/Sources/PackageGraph/DependencyResolutionNode.swift +++ b/Sources/PackageGraph/DependencyResolutionNode.swift @@ -39,7 +39,7 @@ public enum DependencyResolutionNode { /// They derive from the manifest. /// /// Tools versions before 5.2 do not know which products belong to which packages, so each product is required from every dependency. - /// Since a non‐existant product ends up with only its implicit dependency on its own package, + /// 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) @@ -52,7 +52,7 @@ public enum DependencyResolutionNode { /// /// - zero or more dependencies on each external product node required to build any of its targets (vended or not). /// - zero or more dependencies directly on external empty package nodes. - /// This special case occurs when a dependecy is declared but not used. + /// This special case occurs when a dependency is declared but not used. /// 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. diff --git a/Sources/PackageGraph/Pubgrub/PubgrubDependencyResolver.swift b/Sources/PackageGraph/Pubgrub/PubgrubDependencyResolver.swift index 4959556d677..fca816b682b 100644 --- a/Sources/PackageGraph/Pubgrub/PubgrubDependencyResolver.swift +++ b/Sources/PackageGraph/Pubgrub/PubgrubDependencyResolver.swift @@ -132,33 +132,38 @@ public struct PubgrubDependencyResolver { /// Execute the resolution algorithm to find a valid assignment of versions. public func solve(constraints: [Constraint]) -> Result<[DependencyResolver.Binding], Error> { - let root = DependencyResolutionNode.root(package: .root( - identity: .plain(""), - path: .root - )) + // the graph resolution root + let root: DependencyResolutionNode + if constraints.count == 1, let constraint = constraints.first, constraint.package.kind.isRoot { + // root level package, use it as our resolution root + root = .root(package: constraint.package) + } else { + // more complex setup requires a synthesized root + root = .root(package: .root( + identity: .plain(""), + path: .root + )) + } do { // strips state return .success(try self.solve(root: root, constraints: constraints).bindings) } catch { - var error = error - // If version solving failing, build the user-facing diagnostic. if let pubGrubError = error as? PubgrubError, let rootCause = pubGrubError.rootCause, let incompatibilities = pubGrubError.incompatibilities { - var builder = DiagnosticReportBuilder( - root: root, - incompatibilities: incompatibilities, - provider: self.provider - ) - do { + var builder = DiagnosticReportBuilder( + root: root, + incompatibilities: incompatibilities, + provider: self.provider + ) let diagnostic = try builder.makeErrorReport(for: rootCause) - error = PubgrubError.unresolvable(diagnostic) + return.failure(PubgrubError.unresolvable(diagnostic)) } catch { // failed to construct the report, will report the original error + return .failure(error) } } - return .failure(error) } } @@ -262,13 +267,13 @@ public struct PubgrubDependencyResolver { var overriddenPackages: [PackageReference: (version: BoundVersion, products: ProductFilter)] = [:] // The list of version-based references reachable via local and branch-based references. - // These are added as top-level incompatibilities since they always need to be statisfied. + // These are added as top-level incompatibilities since they always need to be satisfied. // Some of these might be overridden as we discover local and branch-based references. - var versionBasedDependencies: [DependencyResolutionNode: [VersionBasedConstraint]] = [:] + var versionBasedDependencies = OrderedDictionary() // Process unversioned constraints in first phase. We go through all of the unversioned packages // and collect them and their dependencies. This gives us the complete list of unversioned - // packages in the graph since unversioned packages can only be refered by other + // packages in the graph since unversioned packages can only be referred by other // unversioned packages. while let constraint = constraints.first(where: { $0.requirement == .unversioned }) { constraints.remove(constraint) @@ -370,7 +375,7 @@ public struct PubgrubDependencyResolver { case .versionSet(let req): for node in dependency.nodes() { let versionedBasedConstraint = VersionBasedConstraint(node: node, req: req) - versionBasedDependencies[node, default: []].append(versionedBasedConstraint) + versionBasedDependencies[.root(package: constraint.package), default: []].append(versionedBasedConstraint) } case .revision: constraints.append(dependency) @@ -386,13 +391,11 @@ public struct PubgrubDependencyResolver { // At this point, we should be left with only version-based requirements in our constraints // list. Add them to our version-based dependency list. - for dependency in constraints { - switch dependency.requirement { + for constraint in constraints { + switch constraint.requirement { case .versionSet(let req): - for node in dependency.nodes() { + for node in constraint.nodes() { let versionedBasedConstraint = VersionBasedConstraint(node: node, req: req) - // FIXME: It would be better to record where this constraint came from, instead of just - // using root. versionBasedDependencies[root, default: []].append(versionedBasedConstraint) } case .revision, .unversioned: @@ -401,11 +404,11 @@ public struct PubgrubDependencyResolver { } // Finally, compute the root incompatibilities (which will be all version-based). + // note versionBasedDependencies may point to the root package dependencies, or the dependencies of root's non-versioned dependencies var rootIncompatibilities: [Incompatibility] = [] for (node, constraints) in versionBasedDependencies { for constraint in constraints { if overriddenPackages.keys.contains(constraint.node.package) { continue } - let incompat = try Incompatibility( Term(root, .exact("1.0.0")), Term(not: constraint.node, constraint.requirement), @@ -850,14 +853,20 @@ private struct DiagnosticReportBuilder { private func description(for incompatibility: Incompatibility) throws -> String { switch incompatibility.cause { - case .dependency(node: _): + case .dependency(let causeNode): assert(incompatibility.terms.count == 2) let depender = incompatibility.terms.first! let dependee = incompatibility.terms.last! assert(depender.isPositive) assert(!dependee.isPositive) - let dependerDesc = try description(for: depender, normalizeRange: true) + let dependerDesc: String + // when depender is the root node, the causeNode may be different as it may represent root's indirect dependencies (e.g. dependencies of root's unversioned dependencies) + if depender.node == self.rootNode, causeNode != self.rootNode { + dependerDesc = causeNode.nameForDiagnostics + } else { + dependerDesc = try description(for: depender, normalizeRange: true) + } let dependeeDesc = try description(for: dependee) return "\(dependerDesc) depends on \(dependeeDesc)" case .noAvailableVersion: @@ -871,6 +880,8 @@ private struct DiagnosticReportBuilder { let term = incompatibility.terms.first! assert(term.isPositive) return "\(term.node.nameForDiagnostics) is \(term.requirement)" + case .conflict where incompatibility.terms.count == 1 && incompatibility.terms.first?.node == self.rootNode: + return "dependencies could not be resolved" case .conflict: break case .versionBasedDependencyContainsUnversionedDependency(let versionedDependency, let unversionedDependency): @@ -880,10 +891,6 @@ private struct DiagnosticReportBuilder { return "\(try description(for: term, normalizeRange: true)) contains incompatible tools version (\(version))" } - if isFailure(incompatibility) { - return "dependencies could not be resolved" - } - let terms = incompatibility.terms if terms.count == 1 { let term = terms.first! @@ -957,11 +964,6 @@ private struct DiagnosticReportBuilder { return !lineNumbers.keys.contains(complex) } - // FIXME: This is duplicated and wrong. - private func isFailure(_ incompatibility: Incompatibility) -> Bool { - return incompatibility.terms.count == 1 && incompatibility.terms.first?.node.package.identity == .plain("") - } - private func description(for term: Term, normalizeRange: Bool = false) throws -> String { let name = term.node.nameForDiagnostics @@ -1393,6 +1395,7 @@ private extension PackageRequirement { private extension DependencyResolutionNode { var nameForDiagnostics: String { - return "'\(package.identity)'" + return "'\(self.package.identity)'" } } + diff --git a/Tests/PackageGraphTests/PubgrubTests.swift b/Tests/PackageGraphTests/PubgrubTests.swift index 4f49dcc3e2e..32178f006c2 100644 --- a/Tests/PackageGraphTests/PubgrubTests.swift +++ b/Tests/PackageGraphTests/PubgrubTests.swift @@ -49,6 +49,7 @@ private let v1_5: Version = "1.5.0" private let v2: Version = "2.0.0" private let v3: Version = "3.0.0" private let v1Range: VersionSetSpecifier = .range(v1.. version -> version + // root -> version -> version + func testHappyPath1() { + builder.serve("foo", at: v1_1, with: ["foo": ["config": (.versionSet(v1Range), .specific(["config"]))]]) + builder.serve("bar", at: v1_1, with: ["bar": ["config": (.versionSet(v1_1Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + builder.serve("config", at: v1_1) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + "foo": (.versionSet(v1Range), .specific(["foo"])), + "bar": (.versionSet(v1Range), .specific(["bar"])) + ]) + + let result = resolver.solve(constraints: dependencies) + + AssertResult(result, [ + ("foo", .version(v1_1)), + ("bar", .version(v1_1)), + ("config", .version(v1_1)), + ]) + } + + // root -> version -> version + // root -> non-versioned -> version + func testHappyPath2() { + builder.serve("foo", at: v1_1, with: ["foo": ["config": (.versionSet(v1Range), .specific(["config"]))]]) + builder.serve("bar", at: .unversioned, with: ["bar": ["config": (.versionSet(v1_1Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + builder.serve("config", at: v1_1) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + "foo": (.versionSet(v1Range), .specific(["foo"])), + "bar": (.unversioned, .specific(["bar"])) + ]) + + let result = resolver.solve(constraints: dependencies) + + AssertResult(result, [ + ("foo", .version(v1_1)), + ("bar", .unversioned), + ("config", .version(v1_1)), + ]) + } + + // root -> version -> version + // root -> non-versioned -> non-versioned -> version + func testHappyPath3() { + builder.serve("foo", at: v1_1, with: ["foo": ["config": (.versionSet(v1Range), .specific(["config"]))]]) + builder.serve("bar", at: .unversioned, with: ["bar": ["baz": (.unversioned, .specific(["baz"]))]]) + builder.serve("baz", at: .unversioned, with: ["baz": ["config": (.versionSet(v1_1Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + builder.serve("config", at: v1_1) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + "foo": (.versionSet(v1Range), .specific(["foo"])), + "bar": (.unversioned, .specific(["bar"])) + ]) + + let result = resolver.solve(constraints: dependencies) + + AssertResult(result, [ + ("foo", .version(v1_1)), + ("bar", .unversioned), + ("baz", .unversioned), + ("config", .version(v1_1)), + ]) + } + + // root -> version + // root -> version -> version + func testHappyPath4() { + builder.serve("foo", at: v1_1, with: ["foo": ["config": (.versionSet(v1_1Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + builder.serve("config", at: v1_1) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + "config": (.versionSet(v1Range), .specific(["config"])), + "foo": (.versionSet(v1Range), .specific(["foo"])) + ]) + + let result = resolver.solve(constraints: dependencies) + + AssertResult(result, [ + ("foo", .version(v1_1)), + ("config", .version(v1_1)), + ]) + } + + // root -> version + // root -> non-versioned -> version + func testHappyPath5() { + builder.serve("foo", at: .unversioned, with: ["foo": ["config": (.versionSet(v1_1Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + builder.serve("config", at: v1_1) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + "config": (.versionSet(v1Range), .specific(["config"])), + "foo": (.unversioned, .specific(["foo"])) + ]) + + let result = resolver.solve(constraints: dependencies) + + AssertResult(result, [ + ("foo", .unversioned), + ("config", .version(v1_1)), + ]) + } + + + // root -> version + // root -> non-versioned -> non-versioned -> version + func testHappyPath6() { + builder.serve("foo", at: .unversioned, with: ["foo": ["bar": (.unversioned, .specific(["bar"]))]]) + builder.serve("bar", at: .unversioned, with: ["bar": ["config": (.versionSet(v1_1Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + builder.serve("config", at: v1_1) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + "config": (.versionSet(v1Range), .specific(["config"])), + "foo": (.unversioned, .specific(["foo"])) + ]) + + let result = resolver.solve(constraints: dependencies) + + AssertResult(result, [ + ("foo", .unversioned), + ("bar", .unversioned), + ("config", .version(v1_1)), + ]) + } + + // top level package -> version + // top level package -> version -> version + func testHappyPath7() { + let package = PackageReference.root(identity: .plain("package"), path: .root) + builder.serve(package, at: .unversioned, with: [ + "module": [ + "config": (.versionSet(v1Range), .specific(["config"])), + "foo": (.versionSet(v1Range), .specific(["foo"])) + ]]) + builder.serve("foo", at: v1_1, with: ["foo": ["config": (.versionSet(v1_1Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + builder.serve("config", at: v1_1) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + package: (.unversioned, .everything) + ]) + + let result = resolver.solve(constraints: dependencies) + + AssertResult(result, [ + ("package", .unversioned), + ("foo", .version(v1_1)), + ("config", .version(v1_1)), + ]) + } + + + // top level package -> version + // top level package -> non-versioned -> version + func testHappyPath8() { + let package = PackageReference.root(identity: .plain("package"), path: .root) + builder.serve(package, at: .unversioned, with: [ + "module": [ + "config": (.versionSet(v1Range), .specific(["config"])), + "foo": (.unversioned, .specific(["foo"])) + ]]) + builder.serve("foo", at: .unversioned, with: ["foo": ["config": (.versionSet(v1_1Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + builder.serve("config", at: v1_1) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + package: (.unversioned, .everything) + ]) + + let result = resolver.solve(constraints: dependencies) + + AssertResult(result, [ + ("package", .unversioned), + ("foo", .unversioned), + ("config", .version(v1_1)), + ]) + } + + // top level pacakge -> version + // top level pacakge -> non-versioned -> non-versioned -> version + func testHappyPath9() { + let package = PackageReference.root(identity: .plain("package"), path: .root) + builder.serve(package, at: .unversioned, with: [ + "module": [ + "config": (.versionSet(v1Range), .specific(["config"])), + "foo": (.unversioned, .specific(["foo"])) + ]]) + builder.serve("foo", at: .unversioned, with: ["foo": ["bar": (.unversioned, .specific(["bar"]))]]) + builder.serve("bar", at: .unversioned, with: ["bar": ["baz": (.unversioned, .specific(["baz"]))]]) + builder.serve("baz", at: .unversioned, with: ["baz": ["config": (.versionSet(v1_1Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + builder.serve("config", at: v1_1) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + package: (.unversioned, .everything) + ]) + + let result = resolver.solve(constraints: dependencies) + + AssertResult(result, [ + ("package", .unversioned), + ("foo", .unversioned), + ("bar", .unversioned), + ("baz", .unversioned), + ("config", .version(v1_1)), + ]) + } + func testResolutionWithSimpleBranchBasedDependency() { builder.serve("foo", at: .revision("master"), with: ["foo": ["bar": (.versionSet(v1Range), .specific(["bar"]))]]) builder.serve("bar", at: v1) @@ -1434,9 +1658,9 @@ final class PubGrubDiagnosticsTests: XCTestCase { XCTAssertEqual(result.errorMsg, """ Dependencies could not be resolved because root depends on 'foo' 1.0.0..<2.0.0 and root depends on 'baz' 1.0.0..<2.0.0. - 'foo\' >= 1.0.0 practically depends on 'baz' 3.0.0..<4.0.0. - 'bar\' >= 2.0.0 practically depends on 'baz' 3.0.0..<4.0.0 because 'bar' 2.0.0 depends on 'baz' 3.0.0..<4.0.0 and no versions of 'bar' match the requirement 2.0.1..<3.0.0. - 'foo\' >= 1.0.0 practically depends on 'bar' 2.0.0..<3.0.0 because 'foo' 1.0.0 depends on 'bar' 2.0.0..<3.0.0 and no versions of 'foo' match the requirement 1.0.1..<2.0.0. + 'foo' >= 1.0.0 practically depends on 'baz' 3.0.0..<4.0.0. + 'bar' >= 2.0.0 practically depends on 'baz' 3.0.0..<4.0.0 because 'bar' 2.0.0 depends on 'baz' 3.0.0..<4.0.0 and no versions of 'bar' match the requirement 2.0.1..<3.0.0. + 'foo' >= 1.0.0 practically depends on 'bar' 2.0.0..<3.0.0 because 'foo' 1.0.0 depends on 'bar' 2.0.0..<3.0.0 and no versions of 'foo' match the requirement 1.0.1..<2.0.0. """) } @@ -1529,7 +1753,7 @@ final class PubGrubDiagnosticsTests: XCTestCase { XCTAssertEqual(result2.errorMsg, """ Dependencies could not be resolved because root depends on 'foo' 1.0.0..<2.0.0 and root depends on 'config' 2.0.0..<3.0.0. - 'foo' >= 1.0.0 practically depends on \'config\' 1.0.0..<2.0.0 because 'foo' 1.0.0 depends on 'config' 1.0.0..<2.0.0 and no versions of 'foo' match the requirement 1.0.1..<2.0.0. + 'foo' >= 1.0.0 practically depends on 'config' 1.0.0..<2.0.0 because 'foo' 1.0.0 depends on 'config' 1.0.0..<2.0.0 and no versions of 'foo' match the requirement 1.0.1..<2.0.0. """) } @@ -1549,6 +1773,337 @@ final class PubGrubDiagnosticsTests: XCTestCase { """) } + func testConflict4() { + builder.serve("foo", at: v1, with: [ + "foo": ["shared": (.versionSet(.range("2.0.0"..<"3.0.0")), .specific(["shared"]))], + ]) + builder.serve("bar", at: v1, with: [ + "bar": ["shared": (.versionSet(.range("2.9.0"..<"4.0.0")), .specific(["shared"]))], + ]) + builder.serve("shared", at: "2.5.0") + builder.serve("shared", at: "3.5.0") + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + "bar": (.versionSet(.exact(v1)), .specific(["bar"])), + "foo": (.versionSet(.exact(v1)), .specific(["foo"])), + ]) + + let result = resolver.solve(constraints: dependencies) + + XCTAssertEqual(result.errorMsg, """ + Dependencies could not be resolved because root depends on 'bar' 1.0.0 and root depends on 'foo' 1.0.0. + 'foo' is incompatible with 'bar' because 'foo' 1.0.0 depends on 'shared' 2.0.0..<3.0.0. + 'bar' 1.0.0 practically depends on 'shared' 3.0.0..<4.0.0 because 'bar' 1.0.0 depends on 'shared' 2.9.0..<4.0.0 and no versions of 'shared' match the requirement 2.9.0..<3.0.0. + """) + } + + func testConflict5() { + builder.serve("a", at: v1, with: [ + "a": ["b": (.versionSet(.exact("1.0.0")), .specific(["b"]))], + ]) + builder.serve("a", at: "2.0.0", with: [ + "a": ["b": (.versionSet(.exact("2.0.0")), .specific(["b"]))], + ]) + builder.serve("b", at: "1.0.0", with: [ + "b": ["a": (.versionSet(.exact("2.0.0")), .specific(["a"]))], + ]) + builder.serve("b", at: "2.0.0", with: [ + "b": ["a": (.versionSet(.exact("1.0.0")), .specific(["a"]))], + ]) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + "b": (.versionSet(.range("0.0.0"..<"5.0.0")), .specific(["b"])), + "a": (.versionSet(.range("0.0.0"..<"5.0.0")), .specific(["a"])), + ]) + + let result = resolver.solve(constraints: dependencies) + + XCTAssertEqual(result.errorMsg, """ + Dependencies could not be resolved because root depends on 'a' 0.0.0..<5.0.0. + 'a' cannot be used. + 'a' 2.0.0 cannot be used because 'b' 2.0.0 depends on 'a' 1.0.0 and 'a' 2.0.0 depends on 'b' 2.0.0. + 'a' {0.0.0..<2.0.0, 2.0.1..<5.0.0} cannot be used because 'b' 1.0.0 depends on 'a' 2.0.0. + 'a' {0.0.0..<2.0.0, 2.0.1..<5.0.0} practically depends on 'b' 1.0.0 because no versions of 'a' match the requirement {0.0.0..<1.0.0, 1.0.1..<2.0.0, 2.0.1..<5.0.0} and 'a' 1.0.0 depends on 'b' 1.0.0. + """) + } + + // root -> version -> version + // root -> version -> conflicting version + func testConflict6() { + builder.serve("foo", at: v1, with: ["foo": ["config": (.versionSet(v1Range), .specific(["config"]))]]) + builder.serve("bar", at: v1, with: ["bar": ["config": (.versionSet(v2Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + builder.serve("config", at: v2) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + "foo": (.versionSet(v1Range), .specific(["foo"])), + "bar": (.versionSet(v1Range), .specific(["bar"])) + ]) + + let result = resolver.solve(constraints: dependencies) + + XCTAssertEqual(result.errorMsg, """ + Dependencies could not be resolved because root depends on 'foo' 1.0.0..<2.0.0 and root depends on 'bar' 1.0.0..<2.0.0. + 'bar' is incompatible with 'foo' because 'foo' 1.0.0 depends on 'config' 1.0.0..<2.0.0 and no versions of 'foo' match the requirement 1.0.1..<2.0.0. + 'bar' >= 1.0.0 practically depends on 'config' 2.0.0..<3.0.0 because no versions of 'bar' match the requirement 1.0.1..<2.0.0 and 'bar' 1.0.0 depends on 'config' 2.0.0..<3.0.0. + """) + } + + // root -> version -> version + // root -> non-versioned -> conflicting version + func testConflict7() { + builder.serve("foo", at: v1, with: ["foo": ["config": (.versionSet(v1Range), .specific(["config"]))]]) + builder.serve("bar", at: .unversioned, with: ["bar": ["config": (.versionSet(v2Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + builder.serve("config", at: v2) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + "foo": (.versionSet(v1Range), .specific(["foo"])), + "bar": (.unversioned, .specific(["bar"])) + ]) + + let result = resolver.solve(constraints: dependencies) + + XCTAssertEqual(result.errorMsg, """ + Dependencies could not be resolved because 'bar' depends on 'config' 2.0.0..<3.0.0 and root depends on 'foo' 1.0.0..<2.0.0. + 'foo' >= 1.0.0 practically depends on 'config' 1.0.0..<2.0.0 because no versions of 'foo' match the requirement 1.0.1..<2.0.0 and 'foo' 1.0.0 depends on 'config' 1.0.0..<2.0.0. + """) + } + + // root -> version -> version + // root -> non-versioned -> non-versioned -> conflicting version + func testConflict8() { + builder.serve("foo", at: v1, with: ["foo": ["config": (.versionSet(v1Range), .specific(["config"]))]]) + builder.serve("bar", at: .unversioned, with: ["bar": ["baz": (.unversioned, .specific(["baz"]))]]) + builder.serve("baz", at: .unversioned, with: ["baz": ["config": (.versionSet(v2Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + builder.serve("config", at: v2) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + "foo": (.versionSet(v1Range), .specific(["foo"])), + "bar": (.unversioned, .specific(["bar"])) + ]) + + let result = resolver.solve(constraints: dependencies) + + XCTAssertEqual(result.errorMsg, """ + Dependencies could not be resolved because 'baz' depends on 'config' 2.0.0..<3.0.0 and root depends on 'foo' 1.0.0..<2.0.0. + 'foo' >= 1.0.0 practically depends on 'config' 1.0.0..<2.0.0 because no versions of 'foo' match the requirement 1.0.1..<2.0.0 and 'foo' 1.0.0 depends on 'config' 1.0.0..<2.0.0. + """) + } + + // root -> version -> version + // root -> non-versioned -> non-existing version + func testConflict9() { + builder.serve("foo", at: v1, with: ["foo": ["config": (.versionSet(v1Range), .specific(["config"]))]]) + builder.serve("bar", at: .unversioned, with: ["bar": ["config": (.versionSet(v2Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + "foo": (.versionSet(v1Range), .specific(["foo"])), + "bar": (.unversioned, .specific(["bar"])) + ]) + + let result = resolver.solve(constraints: dependencies) + + XCTAssertEqual(result.errorMsg, """ + Dependencies could not be resolved because no versions of 'config' match the requirement 2.0.0..<3.0.0 and 'bar' depends on 'config' 2.0.0..<3.0.0. + """) + } + + // root -> version + // root -> non-versioned -> conflicting version + func testConflict10() { + builder.serve("foo", at: .unversioned, with: ["foo": ["config": (.versionSet(v2Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + builder.serve("config", at: v2) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + "config": (.versionSet(v1Range), .specific(["config"])), + "foo": (.unversioned, .specific(["foo"])) + ]) + + let result = resolver.solve(constraints: dependencies) + + XCTAssertEqual(result.errorMsg, """ + Dependencies could not be resolved because 'foo' depends on 'config' 2.0.0..<3.0.0 and root depends on 'config' 1.0.0..<2.0.0. + """) + } + + // root -> version + // root -> non-versioned -> non-existing version + func testConflict11() { + builder.serve("foo", at: .unversioned, with: ["foo": ["config": (.versionSet(v2Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + "config": (.versionSet(v1Range), .specific(["config"])), + "foo": (.unversioned, .specific(["foo"])) + ]) + + let result = resolver.solve(constraints: dependencies) + + XCTAssertEqual(result.errorMsg, """ + Dependencies could not be resolved because 'foo' depends on 'config' 2.0.0..<3.0.0 and root depends on 'config' 1.0.0..<2.0.0. + """) + } + + // root -> version + // root -> non-versioned -> non-versioned -> conflicting version + func testConflict12() { + builder.serve("foo", at: .unversioned, with: ["foo": ["bar": (.unversioned, .specific(["bar"]))]]) + builder.serve("bar", at: .unversioned, with: ["bar": ["baz": (.unversioned, .specific(["baz"]))]]) + builder.serve("baz", at: .unversioned, with: ["baz": ["config": (.versionSet(v2Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + "config": (.versionSet(v1Range), .specific(["config"])), + "foo": (.unversioned, .specific(["foo"])) + ]) + + let result = resolver.solve(constraints: dependencies) + + XCTAssertEqual(result.errorMsg, """ + Dependencies could not be resolved because 'baz' depends on 'config' 2.0.0..<3.0.0 and root depends on 'config' 1.0.0..<2.0.0. + """) + } + + // top level package -> version + // top level package -> version -> conflicting version + func testConflict13() { + let package = PackageReference.root(identity: .plain("package"), path: .root) + builder.serve(package, at: .unversioned, with: [ + "module": [ + "config": (.versionSet(v1Range), .specific(["config"])), + "foo": (.versionSet(v1Range), .specific(["foo"])) + ]]) + builder.serve("foo", at: v1, with: ["foo": ["config": (.versionSet(v2Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + builder.serve("config", at: v2) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + package: (.unversioned, .everything) + ]) + + let result = resolver.solve(constraints: dependencies) + + XCTAssertEqual(result.errorMsg, """ + Dependencies could not be resolved because root depends on 'config' 1.0.0..<2.0.0 and root depends on 'foo' 1.0.0..<2.0.0. + 'foo' >= 1.0.0 practically depends on 'config' 2.0.0..<3.0.0 because no versions of 'foo' match the requirement 1.0.1..<2.0.0 and 'foo' 1.0.0 depends on 'config' 2.0.0..<3.0.0. + """) + } + + // top level package -> version + // top level package -> version -> non-existing version + func testConflict14() { + let package = PackageReference.root(identity: .plain("package"), path: .root) + builder.serve(package, at: .unversioned, with: [ + "module": [ + "config": (.versionSet(v1Range), .specific(["config"])), + "foo": (.versionSet(v1Range), .specific(["foo"])) + ]]) + builder.serve("foo", at: v1, with: ["foo": ["config": (.versionSet(v2Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + package: (.unversioned, .everything) + ]) + + let result = resolver.solve(constraints: dependencies) + + XCTAssertEqual(result.errorMsg, """ + Dependencies could not be resolved because root depends on 'config' 1.0.0..<2.0.0 and root depends on 'foo' 1.0.0..<2.0.0. + 'foo' >= 1.0.0 practically depends on 'config' 2.0.0..<3.0.0 because no versions of 'foo' match the requirement 1.0.1..<2.0.0 and 'foo' 1.0.0 depends on 'config' 2.0.0..<3.0.0. + """) + } + + // top level package -> version + // top level package -> non-versioned -> conflicting version + func testConflict15() { + let package = PackageReference.root(identity: .plain("package"), path: .root) + builder.serve(package, at: .unversioned, with: [ + "module": [ + "config": (.versionSet(v1Range), .specific(["config"])), + "foo": (.unversioned, .specific(["foo"])) + ]]) + builder.serve("foo", at: .unversioned, with: ["foo": ["config": (.versionSet(v2Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + builder.serve("config", at: v2) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + package: (.unversioned, .everything) + ]) + + let result = resolver.solve(constraints: dependencies) + + XCTAssertEqual(result.errorMsg, """ + Dependencies could not be resolved because root depends on 'config' 1.0.0..<2.0.0 and 'foo' depends on 'config' 2.0.0..<3.0.0. + """) + } + + // top level package -> version + // top level package -> non-versioned -> non-existing version + func testConflict16() { + let package = PackageReference.root(identity: .plain("package"), path: .root) + builder.serve(package, at: .unversioned, with: [ + "module": [ + "config": (.versionSet(v1Range), .specific(["config"])), + "foo": (.unversioned, .specific(["foo"])) + ]]) + builder.serve("foo", at: .unversioned, with: ["foo": ["config": (.versionSet(v2Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + package: (.unversioned, .everything) + ]) + + let result = resolver.solve(constraints: dependencies) + + XCTAssertEqual(result.errorMsg, """ + Dependencies could not be resolved because root depends on 'config' 1.0.0..<2.0.0 and 'foo' depends on 'config' 2.0.0..<3.0.0. + """) + } + + // top level pacakge -> version + // top level pacakge -> non-versioned -> non-versioned -> conflicting version + func testConflict17() { + let package = PackageReference.root(identity: .plain("package"), path: .root) + builder.serve(package, at: .unversioned, with: [ + "module": [ + "config": (.versionSet(v1Range), .specific(["config"])), + "foo": (.unversioned, .specific(["foo"])) + ]]) + builder.serve("foo", at: .unversioned, with: ["foo": ["bar": (.unversioned, .specific(["bar"]))]]) + builder.serve("bar", at: .unversioned, with: ["bar": ["baz": (.unversioned, .specific(["baz"]))]]) + builder.serve("baz", at: .unversioned, with: ["baz": ["config": (.versionSet(v2Range), .specific(["config"]))]]) + builder.serve("config", at: v1) + builder.serve("config", at: v2) + + let resolver = builder.create() + let dependencies = builder.create(dependencies: [ + package: (.unversioned, .everything) + ]) + + let result = resolver.solve(constraints: dependencies) + + XCTAssertEqual(result.errorMsg, """ + Dependencies could not be resolved because root depends on 'config' 1.0.0..<2.0.0 and 'baz' depends on 'config' 2.0.0..<3.0.0. + """) + } + func testUnversioned6() { builder.serve("foo", at: .unversioned) builder.serve("bar", at: .revision("master"), with: [ @@ -1704,71 +2259,15 @@ final class PubGrubDiagnosticsTests: XCTestCase { let result = resolver.solve(constraints: dependencies) XCTAssertEqual(result.errorMsg, """ - Dependencies could not be resolved because \'a\' >= 3.2.1 contains incompatible tools version (\(ToolsVersion.v4)) and root depends on 'a' 3.2.0..<4.0.0. + Dependencies could not be resolved because 'a' >= 3.2.1 contains incompatible tools version (\(ToolsVersion.v4)) and root depends on 'a' 3.2.0..<4.0.0. 'a' 3.2.0 cannot be used because 'a' 3.2.0 depends on 'b' 1.0.0..<2.0.0. 'b' >= 1.0.0 cannot be used because 'b' 1.0.0 contains incompatible tools version (\(ToolsVersion.v3)) and no versions of 'b' match the requirement 1.0.1..<2.0.0. """) } - func testConflict4() { - builder.serve("foo", at: v1, with: [ - "foo": ["shared": (.versionSet(.range("2.0.0"..<"3.0.0")), .specific(["shared"]))], - ]) - builder.serve("bar", at: v1, with: [ - "bar": ["shared": (.versionSet(.range("2.9.0"..<"4.0.0")), .specific(["shared"]))], - ]) - builder.serve("shared", at: "2.5.0") - builder.serve("shared", at: "3.5.0") - - let resolver = builder.create() - let dependencies = builder.create(dependencies: [ - "bar": (.versionSet(.exact(v1)), .specific(["bar"])), - "foo": (.versionSet(.exact(v1)), .specific(["foo"])), - ]) - - let result = resolver.solve(constraints: dependencies) - - XCTAssertEqual(result.errorMsg, """ - Dependencies could not be resolved because root depends on 'bar' 1.0.0 and root depends on 'foo' 1.0.0. - 'foo' is incompatible with 'bar' because 'foo' 1.0.0 depends on 'shared' 2.0.0..<3.0.0. - 'bar' 1.0.0 practically depends on 'shared' 3.0.0..<4.0.0 because 'bar' 1.0.0 depends on 'shared' 2.9.0..<4.0.0 and no versions of 'shared' match the requirement 2.9.0..<3.0.0. - """) - } - - func testConflict5() { - builder.serve("a", at: v1, with: [ - "a": ["b": (.versionSet(.exact("1.0.0")), .specific(["b"]))], - ]) - builder.serve("a", at: "2.0.0", with: [ - "a": ["b": (.versionSet(.exact("2.0.0")), .specific(["b"]))], - ]) - builder.serve("b", at: "1.0.0", with: [ - "b": ["a": (.versionSet(.exact("2.0.0")), .specific(["a"]))], - ]) - builder.serve("b", at: "2.0.0", with: [ - "b": ["a": (.versionSet(.exact("1.0.0")), .specific(["a"]))], - ]) - - let resolver = builder.create() - let dependencies = builder.create(dependencies: [ - "b": (.versionSet(.range("0.0.0"..<"5.0.0")), .specific(["b"])), - "a": (.versionSet(.range("0.0.0"..<"5.0.0")), .specific(["a"])), - ]) - - let result = resolver.solve(constraints: dependencies) - - XCTAssertEqual(result.errorMsg, """ - Dependencies could not be resolved because root depends on 'a' 0.0.0..<5.0.0. - 'a' cannot be used. - 'a' 2.0.0 cannot be used because 'b' 2.0.0 depends on 'a' 1.0.0 and 'a' 2.0.0 depends on 'b' 2.0.0. - 'a' {0.0.0..<2.0.0, 2.0.1..<5.0.0} cannot be used because 'b' 1.0.0 depends on 'a' 2.0.0. - 'a' {0.0.0..<2.0.0, 2.0.1..<5.0.0} practically depends on 'b' 1.0.0 because no versions of 'a' match the requirement {0.0.0..<1.0.0, 1.0.1..<2.0.0, 2.0.1..<5.0.0} and 'a' 1.0.0 depends on 'b' 1.0.0. - """) - } - func testProductsCannotResolveToDifferentVersions() { - builder.serve("root", at: .unversioned, with: [ - "root": [ + builder.serve("package", at: .unversioned, with: [ + "package": [ "intermediate_a": (.versionSet(v1Range), .specific(["Intermediate A"])), "intermediate_b": (.versionSet(v1Range), .specific(["Intermediate B"])) ] @@ -1794,7 +2293,7 @@ final class PubGrubDiagnosticsTests: XCTestCase { let resolver = builder.create() let dependencies = builder.create(dependencies: [ - "root": (.unversioned, .everything) + "package": (.unversioned, .everything) ]) let result = resolver.solve(constraints: dependencies) @@ -1802,7 +2301,7 @@ final class PubGrubDiagnosticsTests: XCTestCase { XCTAssertEqual( result.errorMsg, """ - Dependencies could not be resolved because root depends on 'intermediate_a' 1.0.0..<2.0.0 and root depends on 'intermediate_b' 1.0.0..<2.0.0. + Dependencies could not be resolved because 'package' depends on 'intermediate_a' 1.0.0..<2.0.0 and 'package' depends on 'intermediate_b' 1.0.0..<2.0.0. 'intermediate_b' is incompatible with 'intermediate_a' because 'intermediate_a' 1.0.0 depends on 'transitive' 1.0.0 and no versions of 'intermediate_a' match the requirement 1.0.1..<2.0.0. 'intermediate_b' is incompatible with 'transitive' because 'transitive' 1.1.0 depends on 'transitive' 1.1.0 and 'transitive' 1.0.0 depends on 'transitive' 1.0.0. 'intermediate_b' >= 1.0.0 practically depends on 'transitive' 1.1.0 because no versions of 'intermediate_b' match the requirement 1.0.1..<2.0.0 and 'intermediate_b' 1.0.0 depends on 'transitive' 1.1.0. @@ -2302,9 +2801,19 @@ class DependencyGraphBuilder { func create( dependencies: OrderedDictionary + ) -> [PackageContainerConstraint] { + var refDependencies = OrderedDictionary() + for dependency in dependencies { + refDependencies[reference(for: dependency.key)] = dependency.value + } + return self.create(dependencies: refDependencies) + } + + func create( + dependencies: OrderedDictionary ) -> [PackageContainerConstraint] { return dependencies.map { - PackageContainerConstraint(package: reference(for: $0), requirement: $1.0, products: $1.1) + PackageContainerConstraint(package: $0, requirement: $1.0, products: $1.1) } } @@ -2314,7 +2823,7 @@ class DependencyGraphBuilder { toolsVersion: ToolsVersion? = nil, with dependencies: KeyValuePairs> = [:] ) { - serve(package, at: .version(version), toolsVersion: toolsVersion, with: dependencies) + self.serve(package, at: .version(version), toolsVersion: toolsVersion, with: dependencies) } func serve( @@ -2324,7 +2833,21 @@ class DependencyGraphBuilder { with dependencies: KeyValuePairs> = [:] ) { let packageReference = reference(for: package) - let container = self.containers[package] ?? MockContainer(package: packageReference) + self.serve( + packageReference, + at: version, + toolsVersion: toolsVersion, + with: dependencies + ) + } + + func serve( + _ packageReference: PackageReference, + at version: BoundVersion, + toolsVersion: ToolsVersion? = nil, + with dependencies: KeyValuePairs> = [:] + ) { + let container = self.containers[packageReference.identity.description] ?? MockContainer(package: packageReference) if case .version(let v) = version { container.versionsToolsVersions[v] = toolsVersion ?? container.toolsVersion @@ -2341,7 +2864,7 @@ class DependencyGraphBuilder { } container.dependencies[version.description, default: [:]][product, default: []] += packageDependencies } - self.containers[package] = container + self.containers[packageReference.identity.description] = container } /// Creates a pins store with the given pins.