Skip to content

Commit 2c67abc

Browse files
authored
fix an edge case where dependecy resolution can go into endless loop (#5535)
motivation: reliable dependency resolution changes: * before adding a synthesized requirement, check if the negative requirement already exists * add hard protection from infinite loop rdar://93335995
1 parent b0d16ac commit 2c67abc

File tree

3 files changed

+242
-6
lines changed

3 files changed

+242
-6
lines changed

Sources/PackageGraph/Pubgrub/PubgrubDependencyResolver.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,11 @@ public struct PubgrubDependencyResolver {
526526
var incompatibility = conflict
527527
var createdIncompatibility = false
528528

529+
// rdar://93335995
530+
// hard protection from infinite loops
531+
let maxIterations = 1000
532+
var iterations: Int = 0
533+
529534
while !isCompleteFailure(incompatibility, root: state.root) {
530535
var mostRecentTerm: Term?
531536
var mostRecentSatisfier: Assignment?
@@ -578,7 +583,11 @@ public struct PubgrubDependencyResolver {
578583
newTerms += priorCause.terms.filter { $0.node != _mostRecentSatisfier.term.node }
579584

580585
if let _difference = difference {
581-
newTerms.append(_difference.inverse)
586+
// rdar://93335995
587+
// do not add the exact inverse of a requirement as it can lead to endless loops
588+
if _difference.inverse != mostRecentTerm {
589+
newTerms.append(_difference.inverse)
590+
}
582591
}
583592

584593
incompatibility = try Incompatibility(
@@ -595,6 +604,13 @@ public struct PubgrubDependencyResolver {
595604
self.delegate?.satisfied(term: term, by: _mostRecentSatisfier, incompatibility: incompatibility)
596605
}
597606
}
607+
608+
// rdar://93335995
609+
// hard protection from infinite loops
610+
iterations = iterations + 1
611+
if iterations >= maxIterations {
612+
break
613+
}
598614
}
599615

600616
self.delegate?.failedToResolve(incompatibility: incompatibility)

Sources/SPMTestSupport/Observability.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import func XCTest.XCTFail
1717
import func XCTest.XCTAssertEqual
1818

1919
extension ObservabilitySystem {
20-
public static func makeForTesting() -> TestingObservability {
21-
let collector = TestingObservability.Collector()
20+
public static func makeForTesting(verbose: Bool = true) -> TestingObservability {
21+
let collector = TestingObservability.Collector(verbose: verbose)
2222
let observabilitySystem = ObservabilitySystem(collector)
2323
return TestingObservability(collector: collector, topScope: observabilitySystem.topScope)
2424
}
@@ -53,13 +53,18 @@ public struct TestingObservability {
5353
var diagnosticsHandler: DiagnosticsHandler { return self }
5454

5555
let diagnostics: ThreadSafeArrayStore<Basics.Diagnostic>
56+
private let verbose: Bool
5657

57-
init() {
58+
init(verbose: Bool) {
59+
self.verbose = verbose
5860
self.diagnostics = .init()
5961
}
6062

6163
// TODO: do something useful with scope
6264
func handleDiagnostic(scope: ObservabilityScope, diagnostic: Basics.Diagnostic) {
65+
if self.verbose {
66+
print(diagnostic.description)
67+
}
6368
self.diagnostics.append(diagnostic)
6469
}
6570

Tests/PackageGraphTests/PubgrubTests.swift

Lines changed: 217 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ let aRef = PackageReference.localSourceControl(identity: .plain("a"), path: .roo
6161
let bRef = PackageReference.localSourceControl(identity: .plain("b"), path: .root)
6262
let cRef = PackageReference.localSourceControl(identity: .plain("c"), path: .root)
6363

64-
let rootRef = PackageReference.root(identity: PackageIdentity("root"), path: .root)
64+
let rootRef = PackageReference.root(identity: .plain("root"), path: .root)
6565
let rootNode = DependencyResolutionNode.root(package: rootRef)
6666
let rootCause = try! Incompatibility(Term(rootNode, .exact(v1)), root: rootNode)
6767
let _cause = try! Incompatibility("[email protected]", root: rootNode)
@@ -409,11 +409,139 @@ final class PubgrubTests: XCTestCase {
409409
solution.decide(.product("a", package: aRef), at: v2)
410410
solution.derive("b^1.0.0", cause: _cause)
411411

412-
XCTAssertEqual(try solution.satisfier(for: Term("b^1.0.0")) .term, "b^1.0.0")
412+
XCTAssertEqual(try solution.satisfier(for: Term("b^1.0.0")).term, "b^1.0.0")
413413
XCTAssertEqual(try solution.satisfier(for: Term("¬a^1.0.0")).term, "[email protected]")
414414
XCTAssertEqual(try solution.satisfier(for: Term("a^2.0.0")).term, "[email protected]")
415415
}
416416

417+
// this test reconstruct the conditions described in radar/93335995
418+
func testRadar93335995() throws {
419+
let observability = ObservabilitySystem.makeForTesting()
420+
let delegate = ObservabilityDependencyResolverDelegate(observabilityScope: observability.topScope)
421+
let solver = PubgrubDependencyResolver(provider: emptyProvider, observabilityScope: observability.topScope, delegate: delegate)
422+
let state = PubgrubDependencyResolver.State(root: rootNode)
423+
424+
state.decide(.root(package: aRef), at: "1.1.0")
425+
426+
do {
427+
let cause = Incompatibility(
428+
terms: .init([
429+
Term(node: .root(package: aRef),
430+
requirement: .exact("1.1.0"),
431+
isPositive: true
432+
),
433+
Term(node: .root(package: bRef),
434+
requirement: .range("1.1.0" ..< "2.0.0"),
435+
isPositive: true
436+
)
437+
]),
438+
cause: .noAvailableVersion
439+
)
440+
state.derive(Term(node: .root(package: bRef), requirement: .range("1.1.0" ..< "2.0.0"), isPositive: true), cause: cause)
441+
}
442+
443+
do {
444+
let cause = Incompatibility(
445+
terms: .init([
446+
Term(node: .root(package: aRef),
447+
requirement: .exact("1.1.0"),
448+
isPositive: true
449+
),
450+
Term(node: .root(package: bRef),
451+
requirement: .range("1.3.1" ..< "2.0.0"),
452+
isPositive: true
453+
)
454+
]),
455+
cause: .noAvailableVersion
456+
)
457+
state.derive(Term(node: .root(package: bRef), requirement: .range("1.3.1" ..< "2.0.0"), isPositive: false), cause: cause)
458+
// order here matters to reproduce the issue
459+
state.derive(Term(node: .root(package: bRef), requirement: .exact("1.3.0"), isPositive: false), cause: cause)
460+
state.derive(Term(node: .root(package: bRef), requirement: .exact("1.3.1"), isPositive: false), cause: cause)
461+
state.derive(Term(node: .root(package: bRef), requirement: .exact("1.2.0"), isPositive: false), cause: cause)
462+
state.derive(Term(node: .root(package: bRef), requirement: .exact("1.1.0"), isPositive: false), cause: cause)
463+
}
464+
465+
let conflict = Incompatibility(
466+
terms: .init([
467+
Term(node: .root(package: aRef),
468+
requirement: .exact("1.1.0"),
469+
isPositive: true
470+
),
471+
Term(node: .root(package: bRef),
472+
requirement: .ranges(["1.1.1" ..< "1.2.0", "1.2.1" ..< "1.3.0"]),
473+
isPositive: true
474+
)
475+
]),
476+
cause: .noAvailableVersion
477+
)
478+
479+
_ = try solver.resolve(state: state, conflict: conflict)
480+
XCTAssertNoDiagnostics(observability.diagnostics)
481+
}
482+
483+
func testNoInfiniteLoop() throws {
484+
let observability = ObservabilitySystem.makeForTesting()
485+
let delegate = ObservabilityDependencyResolverDelegate(observabilityScope: observability.topScope)
486+
let solver = PubgrubDependencyResolver(provider: emptyProvider, observabilityScope: observability.topScope, delegate: delegate)
487+
let state = PubgrubDependencyResolver.State(root: rootNode)
488+
489+
do {
490+
let cause = Incompatibility(
491+
terms: .init([
492+
Term(node: .root(package: aRef),
493+
requirement: .exact("1.1.0"),
494+
isPositive: true
495+
),
496+
Term(node: .root(package: bRef),
497+
requirement: .range("1.1.0" ..< "2.0.0"),
498+
isPositive: true
499+
)
500+
]),
501+
cause: .noAvailableVersion
502+
)
503+
state.derive(Term(node: .root(package: aRef), requirement: .exact("1.1.0"), isPositive: true), cause: cause) // no decision available on this which will throw it into an infinite loop
504+
state.derive(Term(node: .root(package: bRef), requirement: .range("1.1.0" ..< "2.0.0"), isPositive: true), cause: cause)
505+
}
506+
507+
do {
508+
let cause = Incompatibility(
509+
terms: .init([
510+
Term(node: .root(package: aRef),
511+
requirement: .exact("1.1.0"),
512+
isPositive: true
513+
),
514+
Term(node: .root(package: bRef),
515+
requirement: .range("1.2.0" ..< "2.0.0"),
516+
isPositive: true
517+
)
518+
]),
519+
cause: .noAvailableVersion
520+
)
521+
state.derive(Term(node: .root(package: bRef), requirement: .range("1.2.0" ..< "2.0.0"), isPositive: false), cause: cause)
522+
state.derive(Term(node: .root(package: bRef), requirement: .exact("1.2.0"), isPositive: false), cause: cause)
523+
state.derive(Term(node: .root(package: bRef), requirement: .exact("1.1.0"), isPositive: false), cause: cause)
524+
}
525+
526+
let conflict = Incompatibility(
527+
terms: .init([
528+
Term(node: .root(package: aRef),
529+
requirement: .exact("1.1.0"),
530+
isPositive: true
531+
),
532+
Term(node: .root(package: bRef),
533+
requirement: .ranges(["1.1.1" ..< "1.2.0"]),
534+
isPositive: true
535+
)
536+
]),
537+
cause: .noAvailableVersion
538+
)
539+
540+
XCTAssertThrowsError(try solver.resolve(state: state, conflict: conflict)) { error in
541+
XCTAssertTrue(error is PubgrubDependencyResolver.PubgrubError)
542+
}
543+
}
544+
417545
func testResolutionNoConflicts() {
418546
builder.serve("a", at: v1, with: ["a": ["b": (.versionSet(v1Range), .specific(["b"]))]])
419547
builder.serve("b", at: v1)
@@ -500,6 +628,58 @@ final class PubgrubTests: XCTestCase {
500628
])
501629
}
502630

631+
func testUsecase1() throws {
632+
builder.serve("a",
633+
at: "1.0.0",
634+
with: [
635+
"a": [
636+
"b": (.versionSet(.range("1.1.0" ..< "2.0.0")), .everything),
637+
"c": (.versionSet(.range("1.2.1" ..< "2.0.0")), .everything)
638+
],
639+
]
640+
)
641+
642+
builder.serve("b",
643+
at: "1.0.0",
644+
with: [
645+
"b": [
646+
"c": (.versionSet(.range("1.3.1" ..< "2.0.0")), .everything),
647+
],
648+
]
649+
)
650+
builder.serve("b",
651+
at: "1.1.0",
652+
with: [
653+
"b": [
654+
"c": (.versionSet(.range("1.4.1" ..< "2.0.0")), .everything),
655+
],
656+
]
657+
)
658+
659+
builder.serve("c",
660+
at: ["1.0.0", "1.1.0", "1.2.0", "1.2.1", "1.3.0", "1.3.1", "1.4.0", "1.4.1", "2.0.0", "2.1.0"],
661+
with: [
662+
"c": ["d": (.versionSet(v1Range), .everything)],
663+
]
664+
)
665+
666+
builder.serve("d", at: ["1.0.0", "1.1.0", "1.1.1", "1.1.2", "1.2.0", "1.2.1"])
667+
668+
let resolver = builder.create()
669+
let dependencies = builder.create(dependencies: [
670+
"a": (.versionSet(v1Range), .everything),
671+
"c": (.versionSet(v1Range), .everything)
672+
])
673+
let result = resolver.solve(constraints: dependencies)
674+
675+
AssertResult(result, [
676+
("a", .version(v1)),
677+
("b", .version("1.1.0")),
678+
("c", .version("1.4.1")),
679+
("d", .version("1.2.1"))
680+
])
681+
}
682+
503683
func testCycle1() {
504684
builder.serve("foo", at: v1_1, with: ["foo": ["foo": (.versionSet(v1Range), .specific(["foo"]))]])
505685

@@ -2820,6 +3000,15 @@ class DependencyGraphBuilder {
28203000
}
28213001
}
28223002

3003+
func serve(
3004+
_ package: String,
3005+
at versions: [Version],
3006+
toolsVersion: ToolsVersion? = nil,
3007+
with dependencies: KeyValuePairs<String, OrderedCollections.OrderedDictionary<String, (PackageRequirement, ProductFilter)>> = [:]
3008+
) {
3009+
self.serve(package, at: versions.map{ .version($0) }, toolsVersion: toolsVersion, with: dependencies)
3010+
}
3011+
28233012
func serve(
28243013
_ package: String,
28253014
at version: Version,
@@ -2829,6 +3018,21 @@ class DependencyGraphBuilder {
28293018
self.serve(package, at: .version(version), toolsVersion: toolsVersion, with: dependencies)
28303019
}
28313020

3021+
func serve(
3022+
_ package: String,
3023+
at versions: [BoundVersion],
3024+
toolsVersion: ToolsVersion? = nil,
3025+
with dependencies: KeyValuePairs<String, OrderedCollections.OrderedDictionary<String, (PackageRequirement, ProductFilter)>> = [:]
3026+
) {
3027+
let packageReference = reference(for: package)
3028+
self.serve(
3029+
packageReference,
3030+
at: versions,
3031+
toolsVersion: toolsVersion,
3032+
with: dependencies
3033+
)
3034+
}
3035+
28323036
func serve(
28333037
_ package: String,
28343038
at version: BoundVersion,
@@ -2844,6 +3048,17 @@ class DependencyGraphBuilder {
28443048
)
28453049
}
28463050

3051+
func serve(
3052+
_ packageReference: PackageReference,
3053+
at versions: [BoundVersion],
3054+
toolsVersion: ToolsVersion? = nil,
3055+
with dependencies: KeyValuePairs<String, OrderedCollections.OrderedDictionary<String, (PackageRequirement, ProductFilter)>> = [:]
3056+
) {
3057+
for version in versions {
3058+
serve(packageReference, at: version, toolsVersion: toolsVersion, with: dependencies)
3059+
}
3060+
}
3061+
28473062
func serve(
28483063
_ packageReference: PackageReference,
28493064
at version: BoundVersion,

0 commit comments

Comments
 (0)