10
10
11
11
import XCTest
12
12
13
+ import Basic
13
14
import PackageGraph
14
15
15
16
import struct Utility. Version
@@ -52,13 +53,17 @@ private struct MockPackageContainer: PackageContainer {
52
53
private struct MockPackagesProvider : PackageContainerProvider {
53
54
typealias Container = MockPackageContainer
54
55
55
- let containers : [ MockPackageContainer ]
56
+ let containers : [ Container ]
57
+ let containersByIdentifier : [ Container . Identifier : Container ]
58
+
59
+ init ( containers: [ MockPackageContainer ] ) {
60
+ self . containers = containers
61
+ self . containersByIdentifier = Dictionary ( items: containers. map { ( $0. identifier, $0) } )
62
+ }
56
63
57
64
func getContainer( for identifier: Container . Identifier ) throws -> Container {
58
- for container in containers {
59
- if container. name == identifier {
60
- return container
61
- }
65
+ if let container = containersByIdentifier [ identifier] {
66
+ return container
62
67
}
63
68
throw MockLoadingError . unknownModule
64
69
}
@@ -75,6 +80,7 @@ private class MockResolverDelegate: DependencyResolverDelegate {
75
80
}
76
81
77
82
private typealias MockDependencyResolver = DependencyResolver < MockPackagesProvider , MockResolverDelegate >
83
+ private typealias MockVersionAssignmentSet = VersionAssignmentSet < MockPackageContainer >
78
84
79
85
// Some handy ranges.
80
86
//
@@ -172,7 +178,7 @@ class DependencyResolverTests: XCTestCase {
172
178
let c = MockPackageContainer ( name: " C " , dependenciesByVersion: [
173
179
v1: [ ] ] )
174
180
175
- var assignment = VersionAssignmentSet < MockPackageContainer > ( )
181
+ var assignment = MockVersionAssignmentSet ( )
176
182
XCTAssertEqual ( assignment. constraints, [ : ] )
177
183
XCTAssert ( assignment. isValid ( binding: . version( v2) , for: b) )
178
184
// An empty assignment is valid.
@@ -210,7 +216,7 @@ class DependencyResolverTests: XCTestCase {
210
216
let d = MockPackageContainer ( name: " D " , dependenciesByVersion: [
211
217
v1: [ ( container: " E " , versionRequirement: v1Range) ] ,
212
218
v2: [ ] ] )
213
- var assignment2 = VersionAssignmentSet < MockPackageContainer > ( )
219
+ var assignment2 = MockVersionAssignmentSet ( )
214
220
assignment2 [ d] = . version( v1)
215
221
if let mergedAssignment = assignment. merging ( assignment2) {
216
222
assignment = mergedAssignment
@@ -222,12 +228,12 @@ class DependencyResolverTests: XCTestCase {
222
228
// Check merger of an assignment with incompatible constraints.
223
229
let d2 = MockPackageContainer ( name: " D2 " , dependenciesByVersion: [
224
230
v1: [ ( container: " E " , versionRequirement: v2Range) ] ] )
225
- var assignment3 = VersionAssignmentSet < MockPackageContainer > ( )
231
+ var assignment3 = MockVersionAssignmentSet ( )
226
232
assignment3 [ d2] = . version( v1)
227
233
XCTAssertEqual ( assignment. merging ( assignment3) , nil )
228
234
229
235
// Check merger of an incompatible assignment.
230
- var assignment4 = VersionAssignmentSet < MockPackageContainer > ( )
236
+ var assignment4 = MockVersionAssignmentSet ( )
231
237
assignment4 [ d] = . version( v2)
232
238
XCTAssertEqual ( assignment. merging ( assignment4) , nil )
233
239
}
@@ -362,16 +368,189 @@ class DependencyResolverTests: XCTestCase {
362
368
}
363
369
}
364
370
371
+ /// Check completeness on a variety of synthetic graphs.
372
+ func testCompleteness( ) throws {
373
+ typealias ConstraintSet = MockDependencyResolver . ConstraintSet
374
+
375
+ // We check correctness by comparing the result to an oracle which implements a trivial brute force solver.
376
+
377
+ // Check respect for the input constraints on version selection.
378
+ do {
379
+ let provider = MockPackagesProvider ( containers: [
380
+ MockPackageContainer ( name: " A " , dependenciesByVersion: [
381
+ v1: [ ] , v1_1: [ ] ] ) ,
382
+ MockPackageContainer ( name: " B " , dependenciesByVersion: [
383
+ v1: [ ] , v1_1: [ ] ] )
384
+ ] )
385
+ let resolver = MockDependencyResolver ( provider, MockResolverDelegate ( ) )
386
+
387
+ // Check the maximal solution is picked.
388
+ try checkResolution ( resolver, constraints: [
389
+ MockPackageConstraint ( container: " A " , versionRequirement: v1Range) ,
390
+ MockPackageConstraint ( container: " B " , versionRequirement: v1Range) ] )
391
+ }
392
+ }
393
+
365
394
static var allTests = [
366
395
( " testBasics " , testBasics) ,
367
396
( " testVersionSetSpecifier " , testVersionSetSpecifier) ,
368
397
( " testContainerConstraintSet " , testContainerConstraintSet) ,
369
398
( " testVersionAssignment " , testVersionAssignment) ,
370
399
( " testResolveSubtree " , testResolveSubtree) ,
371
400
( " testResolve " , testResolve) ,
401
+ ( " testCompleteness " , testCompleteness) ,
372
402
]
373
403
}
374
404
405
+ /// Validate the solution made by `resolver` for the given `constraints`.
406
+ ///
407
+ /// This checks that the solution is complete, correct, and maximal and that it
408
+ /// does not contain spurious assignments.
409
+ private func checkResolution( _ resolver: MockDependencyResolver , constraints: [ MockPackageConstraint ] ) throws {
410
+ // Compute the complete set of valid solution by brute force enumeration.
411
+ func satisfiesConstraints( _ assignment: MockVersionAssignmentSet ) -> Bool {
412
+ for constraint in constraints {
413
+ // FIXME: This is ambiguous, but currently the presence of a
414
+ // constraint means the package is required.
415
+ guard case let . version( version) ? = assignment [ constraint. identifier] else { return false }
416
+ if !constraint. versionRequirement. contains ( version) {
417
+ return false
418
+ }
419
+ }
420
+ return true
421
+ }
422
+ func isValidSolution( _ assignment: MockVersionAssignmentSet ) -> Bool {
423
+ // A solution is valid if it is consistent and complete, meets the input
424
+ // constraints, and doesn't contain any unnecessary bindings.
425
+ guard assignment. checkIfValidAndComplete ( ) && satisfiesConstraints ( assignment) else { return false }
426
+
427
+ // Check the assignment doesn't contain unnecessary bindings.
428
+ let requiredContainers = transitiveClosure ( constraints. map { $0. identifier } , successors: { identifier in
429
+ guard case let . version( version) ? = assignment [ identifier] else {
430
+ fatalError ( " unexpected assignment " )
431
+ }
432
+ let container = try ! resolver. provider. getContainer ( for: identifier)
433
+ return [ identifier] + container. getDependencies ( at: version) . map { $0. identifier }
434
+ } )
435
+ for (container, _) in assignment {
436
+ if !requiredContainers. contains ( container. identifier) {
437
+ return false
438
+ }
439
+ }
440
+
441
+ return true
442
+ }
443
+ let validSolutions = allPossibleAssignments ( for: resolver. provider) . filter ( isValidSolution)
444
+
445
+ // Compute the list of maximal solutions.
446
+ var maximalSolutions = [ MockVersionAssignmentSet] ( )
447
+ for solution in validSolutions {
448
+ // Eliminate any currently maximal solutions this one is greater than.
449
+ let numPreviousSolutions = maximalSolutions. count
450
+ maximalSolutions = maximalSolutions. filter { !solution. isStrictlyGreater ( than: $0) }
451
+
452
+ // If we eliminated any solution, then this is a new maximal solution.
453
+ if maximalSolutions. count != numPreviousSolutions {
454
+ assert ( maximalSolutions. first ( where: { $0. isStrictlyGreater ( than: solution) } ) == nil )
455
+ maximalSolutions. append ( solution)
456
+ } else {
457
+ // Otherwise, this is still a new maximal solution if it isn't comparable to any other one.
458
+ if maximalSolutions. first ( where: { $0. isStrictlyGreater ( than: solution) } ) == nil {
459
+ maximalSolutions. append ( solution)
460
+ }
461
+ }
462
+ }
463
+
464
+ // FIXME: It is possible there are multiple maximal solutions, we don't yet
465
+ // define the ordering required to establish what the "correct" answer is
466
+ // here.
467
+ if maximalSolutions. count > 1 {
468
+ return XCTFail ( " unable to find a unique solution for input test case " )
469
+ }
470
+
471
+ // Get the resolver's solution.
472
+ var solution : MockVersionAssignmentSet ?
473
+ do {
474
+ solution = try resolver. resolveAssignment ( constraints: constraints)
475
+ } catch DependencyResolverError . unsatisfiable {
476
+ solution = nil
477
+ }
478
+
479
+ // Check the solution against our oracle.
480
+ if let solution = solution {
481
+ if maximalSolutions. count != 1 {
482
+ return XCTFail ( " solver unexpectedly found: \( solution) when there are no viable solutions " )
483
+ }
484
+ if solution != maximalSolutions [ 0 ] {
485
+ return XCTFail ( " solver result: \( solution. map { ( $0. 0 . identifier, $0. 1 ) } ) does not match expected result: \( maximalSolutions [ 0 ] . map { ( $0. 0 . identifier, $0. 1 ) } ) " )
486
+ }
487
+ } else {
488
+ if maximalSolutions. count != 0 {
489
+ return XCTFail ( " solver was unable to find the valid solution: \( validSolutions [ 0 ] ) " )
490
+ }
491
+ }
492
+ }
493
+
494
+ /// Compute a sequence of all possible assignments.
495
+ private func allPossibleAssignments( for provider: MockPackagesProvider ) -> AnySequence < MockVersionAssignmentSet > {
496
+ func allPossibleAssignments( for containers: AnyIterator < MockPackageContainer > ) -> [ MockVersionAssignmentSet ] {
497
+ guard let container = containers. next ( ) else {
498
+ // The empty list only has one assignment.
499
+ return [ MockVersionAssignmentSet ( ) ]
500
+ }
501
+
502
+ // The result is all other assignments amended with an assignment of
503
+ // this container to each possible version, or not included.
504
+ //
505
+ // FIXME: It would be nice to be lazy here...
506
+ let otherAssignments = allPossibleAssignments ( for: containers)
507
+ return otherAssignments + container. versions. reversed ( ) . flatMap { version in
508
+ return otherAssignments. map { assignment in
509
+ var assignment = assignment
510
+ assignment [ container] = . version( version)
511
+ return assignment
512
+ }
513
+ }
514
+ }
515
+
516
+ return AnySequence ( allPossibleAssignments ( for: AnyIterator ( provider. containers. makeIterator ( ) ) ) )
517
+ }
518
+
519
+ extension VersionAssignmentSet {
520
+ /// Define a partial ordering among assignments.
521
+ ///
522
+ /// This checks if an assignment has bindings which are strictly greater (as
523
+ /// semantic versions) than those of `rhs`. Binding with excluded
524
+ /// assignments are incomparable when the assignments differ.
525
+ func isStrictlyGreater( than rhs: VersionAssignmentSet ) -> Bool {
526
+ // This set is strictly greater than `rhs` if every assigned version in
527
+ // it is greater than or equal to those in `rhs`, and some assignment is
528
+ // strictly greater.
529
+ var hasGreaterAssignment = false
530
+ for (container, rhsBinding) in rhs {
531
+ guard let lhsBinding = self [ container] else { return false }
532
+
533
+ switch ( lhsBinding, rhsBinding) {
534
+ case ( . excluded, . excluded) :
535
+ // If the container is excluded in both assignments, it is ok.
536
+ break
537
+ case ( . excluded, _) , ( _, . excluded) :
538
+ // If the container is excluded in one of the assignments, they are incomparable.
539
+ return false
540
+ case let ( . version( lhsVersion) , . version( rhsVersion) ) :
541
+ if lhsVersion < rhsVersion {
542
+ return false
543
+ } else if lhsVersion > rhsVersion {
544
+ hasGreaterAssignment = true
545
+ }
546
+ default :
547
+ fatalError ( " unreachable " )
548
+ }
549
+ }
550
+ return hasGreaterAssignment
551
+ }
552
+ }
553
+
375
554
private extension DependencyResolver {
376
555
func resolveSubtree(
377
556
_ container: Container ,
@@ -410,14 +589,11 @@ where C.Identifier == String
410
589
411
590
private func XCTAssertEqual< C: PackageContainer > (
412
591
_ assignment: VersionAssignmentSet < C > ? ,
413
- _ expected: [ String : Version ] ? ,
592
+ _ expected: [ String : Version ] ,
414
593
file: StaticString = #file, line: UInt = #line)
415
594
where C. Identifier == String
416
595
{
417
596
if let assignment = assignment {
418
- guard let expected = expected else {
419
- return XCTFail ( " unexpected satisfying assignment (expected failure): \( assignment) " , file: file, line: line)
420
- }
421
597
var actual = [ String: Version] ( )
422
598
for (container, binding) in assignment {
423
599
guard case . version( let version) = binding else {
@@ -427,9 +603,7 @@ where C.Identifier == String
427
603
}
428
604
XCTAssertEqual ( actual, expected, file: file, line: line)
429
605
} else {
430
- if let expected = expected {
431
- return XCTFail ( " unexpected missing assignment, expected: \( expected) " , file: file, line: line)
432
- }
606
+ return XCTFail ( " unexpected missing assignment, expected: \( expected) " , file: file, line: line)
433
607
}
434
608
}
435
609
0 commit comments