Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(33)

Side by Side Diff: utils/tests/pub/version_solver_test.dart

Issue 13095015: Use backtracking when solving dependency constraints. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Respond to review. Created 7 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 library pub_update_test; 5 library pub_update_test;
6 6
7 import 'dart:async'; 7 import 'dart:async';
8 import 'dart:io'; 8 import 'dart:io';
9 9
10 import 'package:unittest/unittest.dart'; 10 import 'package:unittest/unittest.dart';
11 11
12 import '../../pub/lock_file.dart'; 12 import '../../pub/lock_file.dart';
13 import '../../pub/package.dart'; 13 import '../../pub/package.dart';
14 import '../../pub/pubspec.dart'; 14 import '../../pub/pubspec.dart';
15 import '../../pub/source.dart'; 15 import '../../pub/source.dart';
16 import '../../pub/source_registry.dart'; 16 import '../../pub/source_registry.dart';
17 import '../../pub/system_cache.dart'; 17 import '../../pub/system_cache.dart';
18 import '../../pub/utils.dart'; 18 import '../../pub/utils.dart';
19 import '../../pub/version.dart'; 19 import '../../pub/version.dart';
20 import '../../pub/version_solver.dart'; 20 import '../../pub/solver/version_solver.dart';
21 import 'test_pub.dart'; 21 import 'test_pub.dart';
22 22
23 Matcher noVersion(List<String> packages) { 23 Matcher noVersion(List<String> packages) {
24 return predicate((x) { 24 return predicate((x) {
25 if (x is! NoVersionException) return false; 25 if (x is! NoVersionException) return false;
26 26
27 // Make sure the error string mentions the conflicting dependers. 27 // Make sure the error string mentions the conflicting dependers.
28 var message = x.toString(); 28 var message = x.toString();
29 return packages.every((package) => message.contains(package)); 29 return packages.every((package) => message.contains(package));
30 }, "is a NoVersionException"); 30 }, "is a NoVersionException");
(...skipping 14 matching lines...) Expand all
45 if (x is! DescriptionMismatchException) return false; 45 if (x is! DescriptionMismatchException) return false;
46 46
47 // Make sure the error string mentions the conflicting dependers. 47 // Make sure the error string mentions the conflicting dependers.
48 if (!x.toString().contains(package1)) return false; 48 if (!x.toString().contains(package1)) return false;
49 if (!x.toString().contains(package2)) return false; 49 if (!x.toString().contains(package2)) return false;
50 50
51 return true; 51 return true;
52 }, "is a DescriptionMismatchException"); 52 }, "is a DescriptionMismatchException");
53 } 53 }
54 54
55 final couldNotSolve = predicate((x) => x is CouldNotSolveException, 55 // If no solution can be found, the solver just reports the last failure that
56 "is a CouldNotSolveException"); 56 // happened during propagation. Since we don't specify the order that solutions
57 // are tried, this just validates that *some* failure occurred, but not which.
58 final couldNotSolve = predicate((x) => x is SolverFailure,
59 "is a SolverFailure");
57 60
58 Matcher sourceMismatch(String package1, String package2) { 61 Matcher sourceMismatch(String package1, String package2) {
59 return predicate((x) { 62 return predicate((x) {
60 if (x is! SourceMismatchException) return false; 63 if (x is! SourceMismatchException) return false;
61 64
62 // Make sure the error string mentions the conflicting dependers. 65 // Make sure the error string mentions the conflicting dependers.
63 if (!x.toString().contains(package1)) return false; 66 if (!x.toString().contains(package1)) return false;
64 if (!x.toString().contains(package2)) return false; 67 if (!x.toString().contains(package2)) return false;
65 68
66 return true; 69 return true;
(...skipping 131 matching lines...) Expand 10 before | Expand all | Expand 10 after
198 'bar 1.0.2': {}, 201 'bar 1.0.2': {},
199 'baz 1.0.0': {} 202 'baz 1.0.0': {}
200 }, lockfile: { 203 }, lockfile: {
201 'baz': '1.0.0' 204 'baz': '1.0.0'
202 }, result: { 205 }, result: {
203 'myapp from root': '0.0.0', 206 'myapp from root': '0.0.0',
204 'foo': '1.0.2', 207 'foo': '1.0.2',
205 'bar': '1.0.2' 208 'bar': '1.0.2'
206 }); 209 });
207 210
211 testResolve('unlocks dependencies if necessary to ensure that a new '
212 'dependency is satisfied', {
213 'myapp 0.0.0': {
214 'foo': 'any',
215 'newdep': 'any'
216 },
217 'foo 1.0.0': { 'bar': '<2.0.0' },
218 'bar 1.0.0': { 'baz': '<2.0.0' },
219 'baz 1.0.0': { 'qux': '<2.0.0' },
220 'qux 1.0.0': {},
221 'foo 2.0.0': { 'bar': '<3.0.0' },
222 'bar 2.0.0': { 'baz': '<3.0.0' },
223 'baz 2.0.0': { 'qux': '<3.0.0' },
224 'qux 2.0.0': {},
225 'newdep 2.0.0': { 'baz': '>=1.5.0' }
226 }, lockfile: {
227 'foo': '1.0.0',
228 'bar': '1.0.0',
229 'baz': '1.0.0',
230 'qux': '1.0.0'
231 }, result: {
232 'myapp from root': '0.0.0',
233 'foo': '2.0.0',
234 'bar': '2.0.0',
235 'baz': '2.0.0',
236 'qux': '1.0.0',
237 'newdep': '2.0.0'
238 });
239
208 testResolve('circular dependency', { 240 testResolve('circular dependency', {
209 'myapp 1.0.0': { 241 'myapp 1.0.0': {
210 'foo': '1.0.0' 242 'foo': '1.0.0'
211 }, 243 },
212 'foo 1.0.0': { 244 'foo 1.0.0': {
213 'bar': '1.0.0' 245 'bar': '1.0.0'
214 }, 246 },
215 'bar 1.0.0': { 247 'bar 1.0.0': {
216 'foo': '1.0.0' 248 'foo': '1.0.0'
217 } 249 }
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after
258 } 290 }
259 }, error: sourceMismatch('foo', 'bar')); 291 }, error: sourceMismatch('foo', 'bar'));
260 292
261 testResolve('dependency back onto root package with wrong version', { 293 testResolve('dependency back onto root package with wrong version', {
262 'myapp 1.0.0': { 294 'myapp 1.0.0': {
263 'foo': '1.0.0' 295 'foo': '1.0.0'
264 }, 296 },
265 'foo 1.0.0': { 297 'foo 1.0.0': {
266 'myapp': '<1.0.0' 298 'myapp': '<1.0.0'
267 } 299 }
268 }, error: disjointConstraint(['foo'])); 300 }, error: couldNotSolve);
269 301
270 testResolve('no version that matches requirement', { 302 testResolve('no version that matches requirement', {
271 'myapp 0.0.0': { 303 'myapp 0.0.0': {
272 'foo': '>=1.0.0 <2.0.0' 304 'foo': '>=1.0.0 <2.0.0'
273 }, 305 },
274 'foo 2.0.0': {}, 306 'foo 2.0.0': {},
275 'foo 2.1.3': {} 307 'foo 2.1.3': {}
276 }, error: noVersion(['myapp'])); 308 }, error: noVersion(['myapp']));
277 309
278 testResolve('no version that matches combined constraint', { 310 testResolve('no version that matches combined constraint', {
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after
328 'foo 1.0.0': { 360 'foo 1.0.0': {
329 'shared': '1.0.0' 361 'shared': '1.0.0'
330 }, 362 },
331 'bar 1.0.0': { 363 'bar 1.0.0': {
332 'shared from mock2': '1.0.0' 364 'shared from mock2': '1.0.0'
333 }, 365 },
334 'shared 1.0.0': {}, 366 'shared 1.0.0': {},
335 'shared 1.0.0 from mock2': {} 367 'shared 1.0.0 from mock2': {}
336 }, error: sourceMismatch('foo', 'bar')); 368 }, error: sourceMismatch('foo', 'bar'));
337 369
338 testResolve('unstable dependency graph', { 370 testResolve('circular dependency on older version', {
339 'myapp 0.0.0': { 371 'myapp 0.0.0': {
340 'a': '>=1.0.0' 372 'a': '>=1.0.0'
341 }, 373 },
342 'a 1.0.0': {}, 374 'a 1.0.0': {},
343 'a 2.0.0': { 375 'a 2.0.0': {
344 'b': '1.0.0' 376 'b': '1.0.0'
345 }, 377 },
346 'b 1.0.0': { 378 'b 1.0.0': {
347 'a': '1.0.0' 379 'a': '1.0.0'
348 } 380 }
381 }, result: {
382 'myapp from root': '0.0.0',
383 'a': '1.0.0'
384 }, requiresBacktracking: true);
385
386 testResolve('unsolvable', {
387 'myapp 0.0.0': {
388 'a': 'any',
389 'b': 'any'
390 },
391 'a 1.0.0': {
392 'b': '1.0.0'
393 },
394 'a 2.0.0': {
395 'b': '2.0.0'
396 },
397 'b 1.0.0': {
398 'a': '2.0.0'
399 },
400 'b 2.0.0': {
401 'a': '1.0.0'
402 }
349 }, error: couldNotSolve); 403 }, error: couldNotSolve);
350 404
351 group('dev dependencies', () { 405 group('dev dependencies', () {
352 testResolve("includes root package's dev dependencies", { 406 testResolve("includes root package's dev dependencies", {
353 'myapp 1.0.0': { 407 'myapp 1.0.0': {
354 '(dev) foo': '1.0.0', 408 '(dev) foo': '1.0.0',
355 '(dev) bar': '1.0.0' 409 '(dev) bar': '1.0.0'
356 }, 410 },
357 'foo 1.0.0': {}, 411 'foo 1.0.0': {},
358 'bar 1.0.0': {} 412 'bar 1.0.0': {}
(...skipping 23 matching lines...) Expand all
382 }, 436 },
383 'foo 1.0.0': { 437 'foo 1.0.0': {
384 '(dev) bar': '1.0.0' 438 '(dev) bar': '1.0.0'
385 }, 439 },
386 'bar 1.0.0': {} 440 'bar 1.0.0': {}
387 }, result: { 441 }, result: {
388 'myapp from root': '1.0.0', 442 'myapp from root': '1.0.0',
389 'foo': '1.0.0' 443 'foo': '1.0.0'
390 }); 444 });
391 }); 445 });
446
447 // Only one version of baz, so foo and bar will have to downgrade until they
448 // reach it.
449 testResolve('simple backtracking', {
450 'myapp 0.0.0': {'foo': 'any'},
451 'foo 1.0.0': {'bar': '1.0.0'},
452 'foo 2.0.0': {'bar': '2.0.0'},
453 'foo 3.0.0': {'bar': '3.0.0'},
454 'bar 1.0.0': {'baz': 'any'},
455 'bar 2.0.0': {'baz': '2.0.0'},
456 'bar 3.0.0': {'baz': '3.0.0'},
457 'baz 1.0.0': {}
458 }, result: {
459 'myapp from root': '0.0.0',
460 'foo': '1.0.0',
461 'bar': '1.0.0',
462 'baz': '1.0.0'
463 }, requiresBacktracking: true);
464
465 // This sets up a hundred versions of foo and bar, 0.0.0 through 9.9.0. Each
466 // version of foo depends on a baz with the same major version. Each version
467 // of bar depends on a baz with the same minor version. There is only one
468 // version of baz, 0.0.0, so only older versions of foo and bar will
469 // satisfy it.
470 var map = {
471 'myapp 0.0.0': {
472 'foo': 'any',
473 'bar': 'any'
474 },
475 'baz 0.0.0': {}
476 };
477
478 for (var i = 0; i < 10; i++) {
479 for (var j = 0; j < 10; j++) {
480 map['foo $i.$j.0'] = {'baz': '$i.0.0'};
481 map['bar $i.$j.0'] = {'baz': '0.$j.0'};
482 }
483 }
484
485 testResolve('complex backtrack', map, result: {
486 'myapp from root': '0.0.0',
487 'foo': '0.9.0',
488 'bar': '9.0.0',
489 'baz': '0.0.0'
490 }, requiresBacktracking: true);
392 } 491 }
393 492
394 // TODO(rnystrom): More stuff to test: 493 // TODO(rnystrom): More stuff to test:
395 // - Depending on a non-existent package. 494 // - Depending on a non-existent package.
396 // - Test that only a certain number requests are sent to the mock source so we
397 // can keep track of server traffic.
398 495
399 testResolve(description, packages, {lockfile, result, Matcher error}) { 496 testResolve(description, packages,
497 {lockfile, result, Matcher error, bool requiresBacktracking}) {
498 _testResolveWithSolver(description, packages,
499 allowBacktracking: false, lockfile: lockfile, result: result, error: error ,
500 requiresBacktracking: requiresBacktracking);
501 _testResolveWithSolver(description, packages,
502 allowBacktracking: true, lockfile: lockfile, result: result, error: error,
503 requiresBacktracking: requiresBacktracking);
504 }
505
506 _testResolveWithSolver(description, packages, {bool allowBacktracking, lockfile,
507 result, Matcher error, bool requiresBacktracking}) {
508 if (requiresBacktracking == null) requiresBacktracking = false;
509
510 if (allowBacktracking) {
511 description = 'backtracking solver $description';
512 } else {
513 description = 'greedy solver $description';
514
515 // The greedy solver should fail any graph that does require backtracking.
516 if (requiresBacktracking) {
517 result = null;
518 error = couldNotSolve;
519 }
520 }
521
400 test(description, () { 522 test(description, () {
401 var cache = new SystemCache('.'); 523 var cache = new SystemCache('.');
402 source1 = new MockSource('mock1'); 524 source1 = new MockSource('mock1');
403 source2 = new MockSource('mock2'); 525 source2 = new MockSource('mock2');
404 cache.register(source1); 526 cache.register(source1);
405 cache.register(source2); 527 cache.register(source2);
406 cache.sources.setDefault(source1.name); 528 cache.sources.setDefault(source1.name);
407 529
408 // Build the test package graph. 530 // Build the test package graph.
409 var root; 531 var root;
410 packages.forEach((nameVersion, dependencies) { 532 packages.forEach((nameVersion, dependencies) {
411 var parsed = parseSource(nameVersion, (isDev, nameVersion, source) { 533 var parsed = parseSource(nameVersion, (isDev, nameVersion, source) {
412 var parts = nameVersion.split(' '); 534 var parts = nameVersion.split(' ');
413 var name = parts[0]; 535 var name = parts[0];
414 var version = parts[1]; 536 var version = parts[1];
415 537
416 var package = source1.mockPackage(name, version, dependencies); 538 var package = mockPackage(name, version, dependencies);
417 if (name == 'myapp') { 539 if (name == 'myapp') {
418 // Don't add the root package to the server, so we can verify that Pub 540 // Don't add the root package to the server, so we can verify that Pub
419 // doesn't try to look up information about the local package on the 541 // doesn't try to look up information about the local package on the
420 // remote server. 542 // remote server.
421 root = package; 543 root = package;
422 } else { 544 } else {
423 source.addPackage(package); 545 source.addPackage(name, package);
424 } 546 }
425 }); 547 });
426 }); 548 });
427 549
428 // Clean up the expectation. 550 // Clean up the expectation.
429 if (result != null) { 551 if (result != null) {
430 var newResult = {}; 552 var newResult = {};
431 result.forEach((name, version) { 553 result.forEach((name, version) {
432 parseSource(name, (isDev, name, source) { 554 parseSource(name, (isDev, name, source) {
433 version = new Version.parse(version); 555 version = new Version.parse(version);
434 newResult[name] = new PackageId(name, source, version, name); 556 newResult[name] = new PackageId(name, source, version, name);
435 }); 557 });
436 }); 558 });
437 result = newResult; 559 result = newResult;
438 } 560 }
439 561
440 var realLockFile = new LockFile.empty(); 562 var realLockFile = new LockFile.empty();
441 if (lockfile != null) { 563 if (lockfile != null) {
442 lockfile.forEach((name, version) { 564 lockfile.forEach((name, version) {
443 version = new Version.parse(version); 565 version = new Version.parse(version);
444 realLockFile.packages[name] = 566 realLockFile.packages[name] =
445 new PackageId(name, source1, version, name); 567 new PackageId(name, source1, version, name);
446 }); 568 });
447 } 569 }
448 570
449 // Resolve the versions. 571 // Resolve the versions.
450 var future = resolveVersions(cache.sources, root, realLockFile); 572 var future = resolveVersions(cache.sources, root,
573 allowBacktracking: allowBacktracking, lockFile: realLockFile);
451 574
452 if (result != null) { 575 if (result != null) {
453 expect(future, completion(predicate((actualResult) { 576 expect(future, completion(predicate((actualResult) {
454 for (var actualId in actualResult) { 577 for (var actualId in actualResult) {
455 if (!result.containsKey(actualId.name)) return false; 578 if (!result.containsKey(actualId.name)) return false;
456 var expectedId = result.remove(actualId.name); 579 var expectedId = result.remove(actualId.name);
457 if (actualId != expectedId) return false; 580 if (actualId != expectedId) return false;
458 } 581 }
459 return result.isEmpty; 582 return result.isEmpty;
460 }, 'packages to match $result'))); 583 }, 'packages to match $result')));
461 } else if (error != null) { 584 } else if (error != null) {
462 expect(future, throwsA(error)); 585 expect(future, throwsA(error));
463 } 586 }
464 }); 587 });
465 } 588 }
466 589
467 /// A source used for testing. This both creates mock package objects and acts 590 /// A source used for testing. This both creates mock package objects and acts
468 /// as a source for them. 591 /// as a source for them.
469 /// 592 ///
470 /// In order to support testing packages that have the same name but different 593 /// In order to support testing packages that have the same name but different
471 /// descriptions, a package's name is calculated by taking the description 594 /// descriptions, a package's name is calculated by taking the description
472 /// string and stripping off any trailing hyphen followed by non-hyphen 595 /// string and stripping off any trailing hyphen followed by non-hyphen
473 /// characters. 596 /// characters.
474 class MockSource extends Source { 597 class MockSource extends Source {
475 final Map<String, Map<Version, Package>> _packages; 598 final _packages = <String, Map<Version, Package>>{};
599
600 /// Keeps track of which package version lists have been requested. Ensures
601 /// that a source is only hit once for a given package and that pub
602 /// internally caches the results.
603 final _requestedVersions = new Set<String>();
604
605 /// Keeps track of which package pubspecs have been requested. Ensures that a
606 /// source is only hit once for a given package and that pub internally
607 /// caches the results.
608 final _requestedPubspecs = new Map<String, Set<Version>>();
476 609
477 final String name; 610 final String name;
478 bool get shouldCache => true; 611 bool get shouldCache => true;
479 612
480 MockSource(this.name) 613 MockSource(this.name);
481 : _packages = <String, Map<Version, Package>>{};
482 614
483 Future<List<Version>> getVersions(String name, String description) { 615 Future<List<Version>> getVersions(String name, String description) {
484 return defer(() => _packages[description].keys.toList()); 616 return defer(() {
617 // Make sure the solver doesn't request the same thing twice.
618 if (_requestedVersions.contains(description)) {
619 throw 'Version list for $description was already requested.';
620 }
621
622 _requestedVersions.add(description);
623
624 if (!_packages.containsKey(description)){
625 throw 'MockSource does not have a package matching "$description".';
626 }
627 return _packages[description].keys.toList();
628 });
485 } 629 }
486 630
487 Future<Pubspec> describe(PackageId id) { 631 Future<Pubspec> describe(PackageId id) {
488 return defer(() { 632 return defer(() {
489 return _packages[id.name][id.version].pubspec; 633 // Make sure the solver doesn't request the same thing twice.
634 if (_requestedPubspecs.containsKey(id.description) &&
635 _requestedPubspecs[id.description].contains(id.version)) {
636 throw 'Pubspec for $id was already requested.';
637 }
638
639 _requestedPubspecs.putIfAbsent(id.description, () => new Set<Version>());
640 _requestedPubspecs[id.description].add(id.version);
641
642 return _packages[id.description][id.version].pubspec;
490 }); 643 });
491 } 644 }
492 645
493 Future<bool> install(PackageId id, String path) { 646 Future<bool> install(PackageId id, String path) {
494 throw 'no'; 647 throw 'no';
495 } 648 }
496 649
497 Package mockPackage(String description, String version, 650 void addPackage(String description, Package package) {
498 Map dependencyStrings) { 651 _packages.putIfAbsent(description, () => new Map<Version, Package>());
499 // Build the pubspec dependencies. 652 _packages[description][package.version] = package;
500 var dependencies = <PackageRef>[];
501 var devDependencies = <PackageRef>[];
502
503 dependencyStrings.forEach((name, constraint) {
504 parseSource(name, (isDev, name, source) {
505 var packageName = name.replaceFirst(new RegExp(r"-[^-]+$"), "");
506 var ref = new PackageRef(packageName, source,
507 new VersionConstraint.parse(constraint), name);
508
509 if (isDev) {
510 devDependencies.add(ref);
511 } else {
512 dependencies.add(ref);
513 }
514 });
515 });
516
517 var pubspec = new Pubspec(
518 description, new Version.parse(version), dependencies, devDependencies,
519 new PubspecEnvironment());
520 return new Package.inMemory(pubspec);
521 }
522
523 void addPackage(Package package) {
524 _packages.putIfAbsent(package.name, () => new Map<Version, Package>());
525 _packages[package.name][package.version] = package;
526 } 653 }
527 } 654 }
528 655
656 Package mockPackage(String description, String version,
657 Map dependencyStrings) {
658 // Build the pubspec dependencies.
659 var dependencies = <PackageRef>[];
660 var devDependencies = <PackageRef>[];
661
662 dependencyStrings.forEach((name, constraint) {
663 parseSource(name, (isDev, name, source) {
664 var packageName = name.replaceFirst(new RegExp(r"-[^-]+$"), "");
665 var ref = new PackageRef(packageName, source,
666 new VersionConstraint.parse(constraint), name);
667
668 if (isDev) {
669 devDependencies.add(ref);
670 } else {
671 dependencies.add(ref);
672 }
673 });
674 });
675
676 var name = description.replaceFirst(new RegExp(r"-[^-]+$"), "");
677 var pubspec = new Pubspec(
678 name, new Version.parse(version), dependencies, devDependencies,
679 new PubspecEnvironment());
680 return new Package.inMemory(pubspec);
681 }
682
529 void parseSource(String description, 683 void parseSource(String description,
530 callback(bool isDev, String name, Source source)) { 684 callback(bool isDev, String name, Source source)) {
531 var isDev = false; 685 var isDev = false;
532 686
533 if (description.startsWith("(dev) ")) { 687 if (description.startsWith("(dev) ")) {
534 description = description.substring("(dev) ".length); 688 description = description.substring("(dev) ".length);
535 isDev = true; 689 isDev = true;
536 } 690 }
537 691
538 var name = description; 692 var name = description;
539 var source = source1; 693 var source = source1;
540 694
541 var sourceNames = { 695 var sourceNames = {
542 'mock1': source1, 696 'mock1': source1,
543 'mock2': source2, 697 'mock2': source2,
544 'root': null 698 'root': null
545 }; 699 };
546 700
547 var match = new RegExp(r"(.*) from (.*)").firstMatch(description); 701 var match = new RegExp(r"(.*) from (.*)").firstMatch(description);
548 if (match != null) { 702 if (match != null) {
549 name = match[1]; 703 name = match[1];
550 source = sourceNames[match[2]]; 704 source = sourceNames[match[2]];
551 } 705 }
552 706
553 callback(isDev, name, source); 707 callback(isDev, name, source);
554 } 708 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698