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

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: Revise, update to latest corelib, and make backtracker default. 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) {
24 return predicate((x) {
25 if (x is! NoVersionException) return false;
26
27 // Make sure the error string mentions the conflicting dependers.
28 var message = x.toString();
29 return packages.every((package) => message.contains(package));
30 }, "is a NoVersionException");
31 }
32
33 Matcher disjointConstraint(List<String> packages) {
34 return predicate((x) {
35 if (x is! DisjointConstraintException) return false;
36
37 // Make sure the error string mentions the conflicting dependers.
38 var message = x.toString();
39 return packages.every((package) => message.contains(package));
40 }, "is a DisjointConstraintException");
41 }
42
43 Matcher descriptionMismatch(String package1, String package2) {
44 return predicate((x) {
45 if (x is! DescriptionMismatchException) return false;
46
47 // Make sure the error string mentions the conflicting dependers.
48 if (!x.toString().contains(package1)) return false;
49 if (!x.toString().contains(package2)) return false;
50
51 return true;
52 }, "is a DescriptionMismatchException");
53 }
54
55 final couldNotSolve = predicate((x) => x is CouldNotSolveException,
56 "is a CouldNotSolveException");
57
58 Matcher sourceMismatch(String package1, String package2) {
59 return predicate((x) {
60 if (x is! SourceMismatchException) return false;
61
62 // Make sure the error string mentions the conflicting dependers.
63 if (!x.toString().contains(package1)) return false;
64 if (!x.toString().contains(package2)) return false;
65
66 return true;
67 }, "is a SourceMismatchException");
68 }
69
70 MockSource source1; 23 MockSource source1;
71 MockSource source2; 24 MockSource source2;
72 25
26 bool allowBacktracking;
27
73 main() { 28 main() {
74 initConfig(); 29 initConfig();
75 30
31 for (allowBacktracking in [false, true]) {
32 group(allowBacktracking ? 'BackTrackingSolver' : 'GreedySolver', () {
33 group('basic graph', basicGraph);
34 group('with lockfile', withLockFile);
35 group('root dependency', rootDependency);
36 group('dev dependency', devDependency);
37 group('unsolvable', unsolvable);
38 group('backtracking', backtracking);
39 });
40 }
41 }
42
43 void basicGraph() {
76 testResolve('no dependencies', { 44 testResolve('no dependencies', {
77 'myapp 0.0.0': {} 45 'myapp 0.0.0': {}
78 }, result: { 46 }, result: {
79 'myapp from root': '0.0.0' 47 'myapp from root': '0.0.0'
80 }); 48 });
81 49
82 testResolve('simple dependency tree', { 50 testResolve('simple dependency tree', {
83 'myapp 0.0.0': { 51 'myapp 0.0.0': {
84 'a': '1.0.0', 52 'a': '1.0.0',
85 'b': '1.0.0' 53 'b': '1.0.0'
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after
141 'foo 1.0.3': { 'zoop': '1.0.0' }, 109 'foo 1.0.3': { 'zoop': '1.0.0' },
142 'bar 1.0.0': { 'foo': '<=1.0.1' }, 110 'bar 1.0.0': { 'foo': '<=1.0.1' },
143 'bang 1.0.0': {}, 111 'bang 1.0.0': {},
144 'whoop 1.0.0': {}, 112 'whoop 1.0.0': {},
145 'zoop 1.0.0': {} 113 'zoop 1.0.0': {}
146 }, result: { 114 }, result: {
147 'myapp from root': '0.0.0', 115 'myapp from root': '0.0.0',
148 'foo': '1.0.1', 116 'foo': '1.0.1',
149 'bar': '1.0.0', 117 'bar': '1.0.0',
150 'bang': '1.0.0' 118 'bang': '1.0.0'
119 }, maxTries: 2, hasGreedySolution: true);
120
121 testResolve('circular dependency', {
122 'myapp 1.0.0': {
123 'foo': '1.0.0'
124 },
125 'foo 1.0.0': {
126 'bar': '1.0.0'
127 },
128 'bar 1.0.0': {
129 'foo': '1.0.0'
130 }
131 }, result: {
132 'myapp from root': '1.0.0',
133 'foo': '1.0.0',
134 'bar': '1.0.0'
151 }); 135 });
136 }
152 137
138 withLockFile() {
153 testResolve('with compatible locked dependency', { 139 testResolve('with compatible locked dependency', {
154 'myapp 0.0.0': { 140 'myapp 0.0.0': {
155 'foo': 'any' 141 'foo': 'any'
156 }, 142 },
157 'foo 1.0.0': { 'bar': '1.0.0' }, 143 'foo 1.0.0': { 'bar': '1.0.0' },
158 'foo 1.0.1': { 'bar': '1.0.1' }, 144 'foo 1.0.1': { 'bar': '1.0.1' },
159 'foo 1.0.2': { 'bar': '1.0.2' }, 145 'foo 1.0.2': { 'bar': '1.0.2' },
160 'bar 1.0.0': {}, 146 'bar 1.0.0': {},
161 'bar 1.0.1': {}, 147 'bar 1.0.1': {},
162 'bar 1.0.2': {} 148 'bar 1.0.2': {}
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after
198 'bar 1.0.2': {}, 184 'bar 1.0.2': {},
199 'baz 1.0.0': {} 185 'baz 1.0.0': {}
200 }, lockfile: { 186 }, lockfile: {
201 'baz': '1.0.0' 187 'baz': '1.0.0'
202 }, result: { 188 }, result: {
203 'myapp from root': '0.0.0', 189 'myapp from root': '0.0.0',
204 'foo': '1.0.2', 190 'foo': '1.0.2',
205 'bar': '1.0.2' 191 'bar': '1.0.2'
206 }); 192 });
207 193
208 testResolve('circular dependency', { 194 testResolve('unlocks dependencies if necessary to ensure that a new '
195 'dependency is satisfied', {
196 'myapp 0.0.0': {
197 'foo': 'any',
198 'newdep': 'any'
199 },
200 'foo 1.0.0': { 'bar': '<2.0.0' },
201 'bar 1.0.0': { 'baz': '<2.0.0' },
202 'baz 1.0.0': { 'qux': '<2.0.0' },
203 'qux 1.0.0': {},
204 'foo 2.0.0': { 'bar': '<3.0.0' },
205 'bar 2.0.0': { 'baz': '<3.0.0' },
206 'baz 2.0.0': { 'qux': '<3.0.0' },
207 'qux 2.0.0': {},
208 'newdep 2.0.0': { 'baz': '>=1.5.0' }
209 }, lockfile: {
210 'foo': '1.0.0',
211 'bar': '1.0.0',
212 'baz': '1.0.0',
213 'qux': '1.0.0'
214 }, result: {
215 'myapp from root': '0.0.0',
216 'foo': '2.0.0',
217 'bar': '2.0.0',
218 'baz': '2.0.0',
219 'qux': '1.0.0',
220 'newdep': '2.0.0'
221 }, maxTries: 3, hasGreedySolution: true);
222 }
223
224 rootDependency() {
225 testResolve('with root source', {
209 'myapp 1.0.0': { 226 'myapp 1.0.0': {
210 'foo': '1.0.0' 227 'foo': '1.0.0'
211 }, 228 },
212 'foo 1.0.0': {
213 'bar': '1.0.0'
214 },
215 'bar 1.0.0': {
216 'foo': '1.0.0'
217 }
218 }, result: {
219 'myapp from root': '1.0.0',
220 'foo': '1.0.0',
221 'bar': '1.0.0'
222 });
223
224 testResolve('dependency back onto root package', {
225 'myapp 1.0.0': {
226 'foo': '1.0.0'
227 },
228 'foo 1.0.0': { 229 'foo 1.0.0': {
229 'myapp from root': '>=1.0.0' 230 'myapp from root': '>=1.0.0'
230 } 231 }
231 }, result: { 232 }, result: {
232 'myapp from root': '1.0.0', 233 'myapp from root': '1.0.0',
233 'foo': '1.0.0' 234 'foo': '1.0.0'
234 }); 235 });
235 236
236 testResolve('dependency back onto root package with different source', { 237 testResolve('with different source', {
237 'myapp 1.0.0': { 238 'myapp 1.0.0': {
238 'foo': '1.0.0' 239 'foo': '1.0.0'
239 }, 240 },
240 'foo 1.0.0': { 241 'foo 1.0.0': {
241 'myapp': '>=1.0.0' 242 'myapp': '>=1.0.0'
242 } 243 }
243 }, result: { 244 }, result: {
244 'myapp from root': '1.0.0', 245 'myapp from root': '1.0.0',
245 'foo': '1.0.0' 246 'foo': '1.0.0'
246 }); 247 });
247 248
248 testResolve('mismatched dependencies back onto root package', { 249 testResolve('with mismatched sources', {
249 'myapp 1.0.0': { 250 'myapp 1.0.0': {
250 'foo': '1.0.0', 251 'foo': '1.0.0',
251 'bar': '1.0.0' 252 'bar': '1.0.0'
252 }, 253 },
253 'foo 1.0.0': { 254 'foo 1.0.0': {
254 'myapp': '>=1.0.0' 255 'myapp': '>=1.0.0'
255 }, 256 },
256 'bar 1.0.0': { 257 'bar 1.0.0': {
257 'myapp from mock2': '>=1.0.0' 258 'myapp from mock2': '>=1.0.0'
258 } 259 }
259 }, error: sourceMismatch('foo', 'bar')); 260 }, error: sourceMismatch('foo', 'bar'));
260 261
261 testResolve('dependency back onto root package with wrong version', { 262 testResolve('with wrong version', {
262 'myapp 1.0.0': { 263 'myapp 1.0.0': {
263 'foo': '1.0.0' 264 'foo': '1.0.0'
264 }, 265 },
265 'foo 1.0.0': { 266 'foo 1.0.0': {
266 'myapp': '<1.0.0' 267 'myapp': '<1.0.0'
267 } 268 }
268 }, error: disjointConstraint(['foo'])); 269 }, error: couldNotSolve);
270 }
269 271
272 devDependency() {
273 testResolve("includes root package's dev dependencies", {
274 'myapp 1.0.0': {
275 '(dev) foo': '1.0.0',
276 '(dev) bar': '1.0.0'
277 },
278 'foo 1.0.0': {},
279 'bar 1.0.0': {}
280 }, result: {
281 'myapp from root': '1.0.0',
282 'foo': '1.0.0',
283 'bar': '1.0.0'
284 });
285
286 testResolve("includes dev dependency's transitive dependencies", {
287 'myapp 1.0.0': {
288 '(dev) foo': '1.0.0'
289 },
290 'foo 1.0.0': {
291 'bar': '1.0.0'
292 },
293 'bar 1.0.0': {}
294 }, result: {
295 'myapp from root': '1.0.0',
296 'foo': '1.0.0',
297 'bar': '1.0.0'
298 });
299
300 testResolve("ignores transitive dependency's dev dependencies", {
301 'myapp 1.0.0': {
302 'foo': '1.0.0'
303 },
304 'foo 1.0.0': {
305 '(dev) bar': '1.0.0'
306 },
307 'bar 1.0.0': {}
308 }, result: {
309 'myapp from root': '1.0.0',
310 'foo': '1.0.0'
311 });
312 }
313
314 unsolvable() {
270 testResolve('no version that matches requirement', { 315 testResolve('no version that matches requirement', {
271 'myapp 0.0.0': { 316 'myapp 0.0.0': {
272 'foo': '>=1.0.0 <2.0.0' 317 'foo': '>=1.0.0 <2.0.0'
273 }, 318 },
274 'foo 2.0.0': {}, 319 'foo 2.0.0': {},
275 'foo 2.1.3': {} 320 'foo 2.1.3': {}
276 }, error: noVersion(['myapp'])); 321 }, error: noVersion(['myapp']));
277 322
278 testResolve('no version that matches combined constraint', { 323 testResolve('no version that matches combined constraint', {
279 'myapp 0.0.0': { 324 'myapp 0.0.0': {
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after
328 'foo 1.0.0': { 373 'foo 1.0.0': {
329 'shared': '1.0.0' 374 'shared': '1.0.0'
330 }, 375 },
331 'bar 1.0.0': { 376 'bar 1.0.0': {
332 'shared from mock2': '1.0.0' 377 'shared from mock2': '1.0.0'
333 }, 378 },
334 'shared 1.0.0': {}, 379 'shared 1.0.0': {},
335 'shared 1.0.0 from mock2': {} 380 'shared 1.0.0 from mock2': {}
336 }, error: sourceMismatch('foo', 'bar')); 381 }, error: sourceMismatch('foo', 'bar'));
337 382
338 testResolve('unstable dependency graph', { 383 testResolve('no valid solution', {
384 'myapp 0.0.0': {
385 'a': 'any',
386 'b': 'any'
387 },
388 'a 1.0.0': {
389 'b': '1.0.0'
390 },
391 'a 2.0.0': {
392 'b': '2.0.0'
393 },
394 'b 1.0.0': {
395 'a': '2.0.0'
396 },
397 'b 2.0.0': {
398 'a': '1.0.0'
399 }
400 }, error: couldNotSolve, maxTries: 4);
401 }
402
403 backtracking() {
404 testResolve('circular dependency on older version', {
339 'myapp 0.0.0': { 405 'myapp 0.0.0': {
340 'a': '>=1.0.0' 406 'a': '>=1.0.0'
341 }, 407 },
342 'a 1.0.0': {}, 408 'a 1.0.0': {},
343 'a 2.0.0': { 409 'a 2.0.0': {
344 'b': '1.0.0' 410 'b': '1.0.0'
345 }, 411 },
346 'b 1.0.0': { 412 'b 1.0.0': {
347 'a': '1.0.0' 413 'a': '1.0.0'
348 } 414 }
349 }, error: couldNotSolve); 415 }, result: {
416 'myapp from root': '0.0.0',
417 'a': '1.0.0'
418 }, maxTries: 2);
350 419
351 group('dev dependencies', () { 420 /// The latest versions of a and b disagree on c. An older version of either
352 testResolve("includes root package's dev dependencies", { 421 /// will resolve the problem. This test validates that b, which is farther
353 'myapp 1.0.0': { 422 /// in the dependency graph from myapp is downgraded first.
354 '(dev) foo': '1.0.0', 423 testResolve('rolls back leaf versions first', {
355 '(dev) bar': '1.0.0' 424 'myapp 0.0.0': {
356 }, 425 'a': 'any'
357 'foo 1.0.0': {}, 426 },
358 'bar 1.0.0': {} 427 'a 1.0.0': {
359 }, result: { 428 'b': 'any'
360 'myapp from root': '1.0.0', 429 },
361 'foo': '1.0.0', 430 'a 2.0.0': {
362 'bar': '1.0.0' 431 'b': 'any',
363 }); 432 'c': '2.0.0'
433 },
434 'b 1.0.0': {},
435 'b 2.0.0': {
436 'c': '1.0.0'
437 },
438 'c 1.0.0': {},
439 'c 2.0.0': {}
440 }, result: {
441 'myapp from root': '0.0.0',
442 'a': '2.0.0',
443 'b': '1.0.0',
444 'c': '2.0.0'
445 }, maxTries: 2);
364 446
365 testResolve("includes dev dependency's transitive dependencies", { 447 // Only one version of baz, so foo and bar will have to downgrade until they
366 'myapp 1.0.0': { 448 // reach it.
367 '(dev) foo': '1.0.0' 449 testResolve('simple transitive', {
368 }, 450 'myapp 0.0.0': {'foo': 'any'},
369 'foo 1.0.0': { 451 'foo 1.0.0': {'bar': '1.0.0'},
370 'bar': '1.0.0' 452 'foo 2.0.0': {'bar': '2.0.0'},
371 }, 453 'foo 3.0.0': {'bar': '3.0.0'},
372 'bar 1.0.0': {} 454 'bar 1.0.0': {'baz': 'any'},
373 }, result: { 455 'bar 2.0.0': {'baz': '2.0.0'},
374 'myapp from root': '1.0.0', 456 'bar 3.0.0': {'baz': '3.0.0'},
375 'foo': '1.0.0', 457 'baz 1.0.0': {}
376 'bar': '1.0.0' 458 }, result: {
377 }); 459 'myapp from root': '0.0.0',
460 'foo': '1.0.0',
461 'bar': '1.0.0',
462 'baz': '1.0.0'
463 }, maxTries: 3);
378 464
379 testResolve("ignores transitive dependency's dev dependencies", { 465 // This ensures it doesn't exhaustively search all versions of b when it's
380 'myapp 1.0.0': { 466 // a-2.0.0 whose dependency on c-2.0.0-nonexistent led to the problem. We
381 'foo': '1.0.0' 467 // make sure b has more versions than a so that the solver tries a first
382 }, 468 // since it sorts sibling dependencies by number of versions.
383 'foo 1.0.0': { 469 testResolve('backjump to nearer unsatisfied package', {
384 '(dev) bar': '1.0.0' 470 'myapp 0.0.0': {
385 }, 471 'a': 'any',
386 'bar 1.0.0': {} 472 'b': 'any'
387 }, result: { 473 },
388 'myapp from root': '1.0.0', 474 'a 1.0.0': { 'c': '1.0.0' },
389 'foo': '1.0.0' 475 'a 2.0.0': { 'c': '2.0.0-nonexistent' },
390 }); 476 'b 1.0.0': {},
391 }); 477 'b 2.0.0': {},
478 'b 3.0.0': {},
479 'c 1.0.0': {},
480 }, result: {
481 'myapp from root': '0.0.0',
482 'a': '1.0.0',
483 'b': '3.0.0',
484 'c': '1.0.0'
485 }, maxTries: 2);
486
487 // Dependencies are ordered so that packages with fewer versions are tried
488 // first. Here, there are two valid solutions (either a or b must be
489 // downgraded once). The chosen one depends on which dep is traversed first.
490 // Since b has fewer versions, it will be traversed first, which means a will
491 // come later. Since later selections are revised first, a gets downgraded.
492 testResolve('traverse into package with fewer versions first', {
493 'myapp 0.0.0': {
494 'a': 'any',
495 'b': 'any'
496 },
497 'a 1.0.0': {'c': 'any'},
498 'a 2.0.0': {'c': 'any'},
499 'a 3.0.0': {'c': 'any'},
500 'a 4.0.0': {'c': 'any'},
501 'a 5.0.0': {'c': '1.0.0'},
502 'b 1.0.0': {'c': 'any'},
503 'b 2.0.0': {'c': 'any'},
504 'b 3.0.0': {'c': 'any'},
505 'b 4.0.0': {'c': '2.0.0'},
506 'c 1.0.0': {},
507 'c 2.0.0': {},
508 }, result: {
509 'myapp from root': '0.0.0',
510 'a': '4.0.0',
511 'b': '4.0.0',
512 'c': '2.0.0'
513 }, maxTries: 2);
514
515 // This sets up a hundred versions of foo and bar, 0.0.0 through 9.9.0. Each
516 // version of foo depends on a baz with the same major version. Each version
517 // of bar depends on a baz with the same minor version. There is only one
518 // version of baz, 0.0.0, so only older versions of foo and bar will
519 // satisfy it.
520 var map = {
521 'myapp 0.0.0': {
522 'foo': 'any',
523 'bar': 'any'
524 },
525 'baz 0.0.0': {}
526 };
527
528 for (var i = 0; i < 10; i++) {
529 for (var j = 0; j < 10; j++) {
530 map['foo $i.$j.0'] = {'baz': '$i.0.0'};
531 map['bar $i.$j.0'] = {'baz': '0.$j.0'};
532 }
533 }
534
535 testResolve('complex backtrack', map, result: {
536 'myapp from root': '0.0.0',
537 'foo': '0.9.0',
538 'bar': '9.0.0',
539 'baz': '0.0.0'
540 }, maxTries: 100);
541
542 // TODO(rnystrom): More tests. In particular:
543 // - Tests that demonstrate backtracking for every case that can cause a
544 // solution to fail (no versions, disjoint, etc.)
545 // - Tests where there are multiple valid solutions and "best" is possibly
546 // ambiguous to nail down which order the backtracker tries solutions.
392 } 547 }
393 548
394 // TODO(rnystrom): More stuff to test: 549 testResolve(description, packages,
395 // - Depending on a non-existent package. 550 {lockfile, result, FailMatcherBuilder error, int maxTries,
396 // - Test that only a certain number requests are sent to the mock source so we 551 bool hasGreedySolution}) {
397 // can keep track of server traffic. 552 // Close over the top-level variable since it will be mutated.
553 var allowBacktracking_ = allowBacktracking;
398 554
399 testResolve(description, packages, {lockfile, result, Matcher error}) { 555 if (maxTries == null) maxTries = 1;
556 if (hasGreedySolution == null) hasGreedySolution = maxTries == 1;
557
558 if (!allowBacktracking_) {
559 // The greedy solver should fail any graph that does expect multiple tries
560 // and isn't explicitly annotated to have a greedy solution.
561 if (!hasGreedySolution) {
562 result = null;
563 error = couldNotSolve;
564 }
565 }
566
400 test(description, () { 567 test(description, () {
401 var cache = new SystemCache('.'); 568 var cache = new SystemCache('.');
402 source1 = new MockSource('mock1'); 569 source1 = new MockSource('mock1');
403 source2 = new MockSource('mock2'); 570 source2 = new MockSource('mock2');
404 cache.register(source1); 571 cache.register(source1);
405 cache.register(source2); 572 cache.register(source2);
406 cache.sources.setDefault(source1.name); 573 cache.sources.setDefault(source1.name);
407 574
408 // Build the test package graph. 575 // Build the test package graph.
409 var root; 576 var root;
410 packages.forEach((nameVersion, dependencies) { 577 packages.forEach((nameVersion, dependencies) {
411 var parsed = parseSource(nameVersion, (isDev, nameVersion, source) { 578 var parsed = parseSource(nameVersion, (isDev, nameVersion, source) {
412 var parts = nameVersion.split(' '); 579 var parts = nameVersion.split(' ');
413 var name = parts[0]; 580 var name = parts[0];
414 var version = parts[1]; 581 var version = parts[1];
415 582
416 var package = source1.mockPackage(name, version, dependencies); 583 var package = mockPackage(name, version, dependencies);
417 if (name == 'myapp') { 584 if (name == 'myapp') {
418 // Don't add the root package to the server, so we can verify that Pub 585 // 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 586 // doesn't try to look up information about the local package on the
420 // remote server. 587 // remote server.
421 root = package; 588 root = package;
422 } else { 589 } else {
423 source.addPackage(package); 590 source.addPackage(name, package);
424 } 591 }
425 }); 592 });
426 }); 593 });
427 594
428 // Clean up the expectation. 595 // Clean up the expectation.
429 if (result != null) { 596 if (result != null) {
430 var newResult = {}; 597 var newResult = {};
431 result.forEach((name, version) { 598 result.forEach((name, version) {
432 parseSource(name, (isDev, name, source) { 599 parseSource(name, (isDev, name, source) {
433 version = new Version.parse(version); 600 version = new Version.parse(version);
434 newResult[name] = new PackageId(name, source, version, name); 601 newResult[name] = new PackageId(name, source, version, name);
435 }); 602 });
436 }); 603 });
437 result = newResult; 604 result = newResult;
438 } 605 }
439 606
440 var realLockFile = new LockFile.empty(); 607 var realLockFile = new LockFile.empty();
441 if (lockfile != null) { 608 if (lockfile != null) {
442 lockfile.forEach((name, version) { 609 lockfile.forEach((name, version) {
443 version = new Version.parse(version); 610 version = new Version.parse(version);
444 realLockFile.packages[name] = 611 realLockFile.packages[name] =
445 new PackageId(name, source1, version, name); 612 new PackageId(name, source1, version, name);
446 }); 613 });
447 } 614 }
448 615
449 // Resolve the versions. 616 // Resolve the versions.
450 var future = resolveVersions(cache.sources, root, realLockFile); 617 var future = resolveVersions(cache.sources, root,
618 allowBacktracking: allowBacktracking_, lockFile: realLockFile);
451 619
620 var matcher;
452 if (result != null) { 621 if (result != null) {
453 expect(future, completion(predicate((actualResult) { 622 matcher = new SolveSuccessMatcher(result, maxTries);
454 for (var actualId in actualResult) { 623 } else if (error != null) {
455 if (!result.containsKey(actualId.name)) return false; 624 matcher = error(maxTries);
456 var expectedId = result.remove(actualId.name); 625 }
457 if (actualId != expectedId) return false; 626
627 expect(future, completion(matcher));
628 });
629 }
630
631 typedef SolveFailMatcher FailMatcherBuilder(int maxTries);
632
633 FailMatcherBuilder noVersion(List<String> packages) {
634 return (maxTries) => new SolveFailMatcher(packages, maxTries,
635 NoVersionException);
636 }
637
638 FailMatcherBuilder disjointConstraint(List<String> packages) {
639 return (maxTries) => new SolveFailMatcher(packages, maxTries,
640 DisjointConstraintException);
641 }
642
643 FailMatcherBuilder descriptionMismatch(String package1, String package2) {
644 return (maxTries) => new SolveFailMatcher([package1, package2], maxTries,
645 DescriptionMismatchException);
646 }
647
648 // If no solution can be found, the solver just reports the last failure that
649 // happened during propagation. Since we don't specify the order that solutions
650 // are tried, this just validates that *some* failure occurred, but not which.
651 SolveFailMatcher couldNotSolve(maxTries) =>
652 new SolveFailMatcher([], maxTries, null);
653
654 FailMatcherBuilder sourceMismatch(String package1, String package2) {
655 return (maxTries) => new SolveFailMatcher([package1, package2], maxTries,
656 SourceMismatchException);
657 }
658
659 class SolveSuccessMatcher implements Matcher {
660 /// The expected concrete package selections.
661 final Map<String, PackageId> _expected;
662
663 /// The maximum number of attempts that should have been tried before finding
664 /// the solution.
665 final int _maxTries;
666
667 SolveSuccessMatcher(this._expected, this._maxTries);
668
669 Description describe(Description description) {
670 return description.add(
671 'Solver to use at most $_maxTries attempts to find:\n'
672 '${_listPackages(_expected.values)}');
673 }
674
675 Description describeMismatch(SolveResult result,
676 Description description,
677 MatchState state, bool verbose) {
678 if (!result.succeeded) {
679 description.add('Solver failed with:\n${result.error}');
680 return;
681 }
682
683 description.add('Resolved:\n${_listPackages(result.packages)}\n');
684 description.add(state.state);
685 return description;
686 }
687
688 bool matches(SolveResult result, MatchState state) {
689 if (!result.succeeded) return false;
690
691 var expected = new Map.from(_expected);
692 var failures = new StringBuffer();
693
694 for (var id in result.packages) {
695 if (!expected.containsKey(id.name)) {
696 failures.writeln('Should not have selected $id');
697 } else {
698 var expectedId = expected.remove(id.name);
699 if (id != expectedId) {
700 failures.writeln('Expected $expectedId, not $id');
458 } 701 }
459 return result.isEmpty; 702 }
460 }, 'packages to match $result')));
461 } else if (error != null) {
462 expect(future, throwsA(error));
463 } 703 }
464 }); 704
705 if (!expected.isEmpty) {
706 failures.writeln('Missing:\n${_listPackages(expected.values)}');
707 }
708
709 // Allow 1 here because the greedy solver will only make one attempt.
710 if (result.attemptedSolutions != 1 &&
711 result.attemptedSolutions != _maxTries) {
712 failures.writeln('Took ${result.attemptedSolutions} attempts');
713 }
714
715 if (!failures.isEmpty) {
716 state.state = failures.toString();
717 return false;
718 }
719
720 return true;
721 }
722
723 String _listPackages(Iterable<PackageId> packages) {
724 return '- ${packages.join('\n- ')}';
725 }
726 }
727
728 class SolveFailMatcher implements Matcher {
729 /// The strings that should appear in the resulting error message.
730 // TODO(rnystrom): This seems to always be package names. Make that explicit.
731 final Iterable<String> _expected;
732
733 /// The maximum number of attempts that should be tried before failing.
734 final int _maxTries;
735
736 /// The concrete error type that should be found, or `null` if any
737 /// [SolveFailure] is allowed.
738 final Type _expectedType;
739
740 SolveFailMatcher(this._expected, this._maxTries, this._expectedType);
741
742 Description describe(Description description) {
743 description.add('Solver should fail after at most $_maxTries attempts.');
744 if (!_expected.isEmpty) {
745 var textList = _expected.map((s) => '"$s"').join(", ");
746 description.add(' The error should contain $textList.');
747 }
748 return description;
749 }
750
751 Description describeMismatch(SolveResult result,
752 Description description,
753 MatchState state, bool verbose) {
754 description.add(state.state);
755 return description;
756 }
757
758 bool matches(SolveResult result, MatchState state) {
759 var failures = new StringBuffer();
760
761 if (result.succeeded) {
762 failures.writeln('Solver succeeded');
763 } else {
764 if (_expectedType != null && result.error.runtimeType != _expectedType) {
765 failures.writeln('Should have error type $_expectedType, got '
766 '${result.error.runtimeType}');
767 }
768
769 var message = result.error.toString();
770 for (var expected in _expected) {
771 if (!message.contains(expected)) {
772 failures.writeln(
773 'Expected error to contain "$expected", got:\n$message');
774 }
775 }
776
777 // Allow 1 here because the greedy solver will only make one attempt.
778 if (result.attemptedSolutions != 1 &&
779 result.attemptedSolutions != _maxTries) {
780 failures.writeln('Took ${result.attemptedSolutions} attempts');
781 }
782 }
783
784 if (!failures.isEmpty) {
785 state.state = failures.toString();
786 return false;
787 }
788
789 return true;
790 }
465 } 791 }
466 792
467 /// A source used for testing. This both creates mock package objects and acts 793 /// A source used for testing. This both creates mock package objects and acts
468 /// as a source for them. 794 /// as a source for them.
469 /// 795 ///
470 /// In order to support testing packages that have the same name but different 796 /// 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 797 /// descriptions, a package's name is calculated by taking the description
472 /// string and stripping off any trailing hyphen followed by non-hyphen 798 /// string and stripping off any trailing hyphen followed by non-hyphen
473 /// characters. 799 /// characters.
474 class MockSource extends Source { 800 class MockSource extends Source {
475 final Map<String, Map<Version, Package>> _packages; 801 final _packages = <String, Map<Version, Package>>{};
802
803 /// Keeps track of which package version lists have been requested. Ensures
804 /// that a source is only hit once for a given package and that pub
805 /// internally caches the results.
806 final _requestedVersions = new Set<String>();
807
808 /// Keeps track of which package pubspecs have been requested. Ensures that a
809 /// source is only hit once for a given package and that pub internally
810 /// caches the results.
811 final _requestedPubspecs = new Map<String, Set<Version>>();
476 812
477 final String name; 813 final String name;
478 bool get shouldCache => true; 814 bool get shouldCache => true;
479 815
480 MockSource(this.name) 816 MockSource(this.name);
481 : _packages = <String, Map<Version, Package>>{};
482 817
483 Future<List<Version>> getVersions(String name, String description) { 818 Future<List<Version>> getVersions(String name, String description) {
484 return new Future.sync(() => _packages[description].keys.toList()); 819 return new Future.sync(() {
820 // Make sure the solver doesn't request the same thing twice.
821 if (_requestedVersions.contains(description)) {
822 throw 'Version list for $description was already requested.';
823 }
824
825 _requestedVersions.add(description);
826
827 if (!_packages.containsKey(description)){
828 throw 'MockSource does not have a package matching "$description".';
829 }
830 return _packages[description].keys.toList();
831 });
485 } 832 }
486 833
487 Future<Pubspec> describe(PackageId id) { 834 Future<Pubspec> describe(PackageId id) {
488 return new Future.sync(() => _packages[id.name][id.version].pubspec); 835 return new Future.sync(() {
836 // Make sure the solver doesn't request the same thing twice.
837 if (_requestedPubspecs.containsKey(id.description) &&
838 _requestedPubspecs[id.description].contains(id.version)) {
839 throw 'Pubspec for $id was already requested.';
840 }
841
842 _requestedPubspecs.putIfAbsent(id.description, () => new Set<Version>());
843 _requestedPubspecs[id.description].add(id.version);
844
845 return _packages[id.description][id.version].pubspec;
846 });
489 } 847 }
490 848
491 Future<bool> install(PackageId id, String path) { 849 Future<bool> install(PackageId id, String path) {
492 throw 'no'; 850 throw 'no';
493 } 851 }
494 852
495 Package mockPackage(String description, String version, 853 void addPackage(String description, Package package) {
496 Map dependencyStrings) { 854 _packages.putIfAbsent(description, () => new Map<Version, Package>());
497 // Build the pubspec dependencies. 855 _packages[description][package.version] = package;
498 var dependencies = <PackageRef>[];
499 var devDependencies = <PackageRef>[];
500
501 dependencyStrings.forEach((name, constraint) {
502 parseSource(name, (isDev, name, source) {
503 var packageName = name.replaceFirst(new RegExp(r"-[^-]+$"), "");
504 var ref = new PackageRef(packageName, source,
505 new VersionConstraint.parse(constraint), name);
506
507 if (isDev) {
508 devDependencies.add(ref);
509 } else {
510 dependencies.add(ref);
511 }
512 });
513 });
514
515 var pubspec = new Pubspec(
516 description, new Version.parse(version), dependencies, devDependencies,
517 new PubspecEnvironment());
518 return new Package.inMemory(pubspec);
519 }
520
521 void addPackage(Package package) {
522 _packages.putIfAbsent(package.name, () => new Map<Version, Package>());
523 _packages[package.name][package.version] = package;
524 } 856 }
525 } 857 }
526 858
859 Package mockPackage(String description, String version,
860 Map dependencyStrings) {
861 // Build the pubspec dependencies.
862 var dependencies = <PackageRef>[];
863 var devDependencies = <PackageRef>[];
864
865 dependencyStrings.forEach((name, constraint) {
866 parseSource(name, (isDev, name, source) {
867 var packageName = name.replaceFirst(new RegExp(r"-[^-]+$"), "");
868 var ref = new PackageRef(packageName, source,
869 new VersionConstraint.parse(constraint), name);
870
871 if (isDev) {
872 devDependencies.add(ref);
873 } else {
874 dependencies.add(ref);
875 }
876 });
877 });
878
879 var name = description.replaceFirst(new RegExp(r"-[^-]+$"), "");
880 var pubspec = new Pubspec(
881 name, new Version.parse(version), dependencies, devDependencies,
882 new PubspecEnvironment());
883 return new Package.inMemory(pubspec);
884 }
885
527 void parseSource(String description, 886 void parseSource(String description,
528 callback(bool isDev, String name, Source source)) { 887 callback(bool isDev, String name, Source source)) {
529 var isDev = false; 888 var isDev = false;
530 889
531 if (description.startsWith("(dev) ")) { 890 if (description.startsWith("(dev) ")) {
532 description = description.substring("(dev) ".length); 891 description = description.substring("(dev) ".length);
533 isDev = true; 892 isDev = true;
534 } 893 }
535 894
536 var name = description; 895 var name = description;
537 var source = source1; 896 var source = source1;
538 897
539 var sourceNames = { 898 var sourceNames = {
540 'mock1': source1, 899 'mock1': source1,
541 'mock2': source2, 900 'mock2': source2,
542 'root': null 901 'root': null
543 }; 902 };
544 903
545 var match = new RegExp(r"(.*) from (.*)").firstMatch(description); 904 var match = new RegExp(r"(.*) from (.*)").firstMatch(description);
546 if (match != null) { 905 if (match != null) {
547 name = match[1]; 906 name = match[1];
548 source = sourceNames[match[2]]; 907 source = sourceNames[match[2]];
549 } 908 }
550 909
551 callback(isDev, name, source); 910 callback(isDev, name, source);
552 } 911 }
OLDNEW
« utils/pub/solver/backtracking_solver.dart ('K') | « utils/tests/pub/pub_test.dart ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698