|
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 | }
|
@@ -363,16 +368,189 @@ class DependencyResolverTests: XCTestCase {
|
363 | 368 | }
|
364 | 369 | }
|
365 | 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 | + |
366 | 394 | static var allTests = [
|
367 | 395 | ("testBasics", testBasics),
|
368 | 396 | ("testVersionSetSpecifier", testVersionSetSpecifier),
|
369 | 397 | ("testContainerConstraintSet", testContainerConstraintSet),
|
370 | 398 | ("testVersionAssignment", testVersionAssignment),
|
371 | 399 | ("testResolveSubtree", testResolveSubtree),
|
372 | 400 | ("testResolve", testResolve),
|
| 401 | + ("testCompleteness", testCompleteness), |
373 | 402 | ]
|
374 | 403 | }
|
375 | 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 | + |
376 | 554 | private extension DependencyResolver {
|
377 | 555 | func resolveSubtree(
|
378 | 556 | _ container: Container,
|
|
0 commit comments