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

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: Track amount of backtracking used in solver tests. 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;
67 }, "is a SourceMismatchException"); 70 }, "is a SourceMismatchException");
68 } 71 }
69 72
70 MockSource source1; 73 MockSource source1;
71 MockSource source2; 74 MockSource source2;
72 75
76 var allowBacktracking;
nweiz 2013/04/03 00:28:43 Declare this as a bool, since it doesn't have an i
Bob Nystrom 2013/04/08 22:13:00 Done.
77
73 main() { 78 main() {
74 initConfig(); 79 initConfig();
75 80
81 for (allowBacktracking in [false, true]) {
82 group(allowBacktracking ? 'BackTrackingSolver' : 'GreedySolver', () {
83 group('basic graph', basicGraph);
84 group('with lockfile', withLockFile);
85 group('root dependency', rootDependency);
86 group('dev dependency', devDependency);
87 group('unsolvable', unsolvable);
88 group('backtracking', backtracking);
89 });
90 }
91
92 allowBacktracking = 'wtf';
nweiz 2013/04/03 00:28:43 wtf?
Bob Nystrom 2013/04/08 22:13:00 Heh. Debug code. Removed. :)
93 }
94
95 void basicGraph() {
76 testResolve('no dependencies', { 96 testResolve('no dependencies', {
77 'myapp 0.0.0': {} 97 'myapp 0.0.0': {}
78 }, result: { 98 }, result: {
79 'myapp from root': '0.0.0' 99 'myapp from root': '0.0.0'
80 }); 100 });
81 101
82 testResolve('simple dependency tree', { 102 testResolve('simple dependency tree', {
83 'myapp 0.0.0': { 103 'myapp 0.0.0': {
84 'a': '1.0.0', 104 'a': '1.0.0',
85 'b': '1.0.0' 105 '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' }, 161 'foo 1.0.3': { 'zoop': '1.0.0' },
142 'bar 1.0.0': { 'foo': '<=1.0.1' }, 162 'bar 1.0.0': { 'foo': '<=1.0.1' },
143 'bang 1.0.0': {}, 163 'bang 1.0.0': {},
144 'whoop 1.0.0': {}, 164 'whoop 1.0.0': {},
145 'zoop 1.0.0': {} 165 'zoop 1.0.0': {}
146 }, result: { 166 }, result: {
147 'myapp from root': '0.0.0', 167 'myapp from root': '0.0.0',
148 'foo': '1.0.1', 168 'foo': '1.0.1',
149 'bar': '1.0.0', 169 'bar': '1.0.0',
150 'bang': '1.0.0' 170 'bang': '1.0.0'
171 }, maxTries: 3, hasGreedySolution: true);
nweiz 2013/04/03 00:28:43 This case will take about N iterations for the bac
Bob Nystrom 2013/04/08 22:13:00 That sounds about right though I think it gets mor
172
173 testResolve('circular dependency', {
174 'myapp 1.0.0': {
175 'foo': '1.0.0'
176 },
177 'foo 1.0.0': {
178 'bar': '1.0.0'
179 },
180 'bar 1.0.0': {
181 'foo': '1.0.0'
182 }
183 }, result: {
184 'myapp from root': '1.0.0',
185 'foo': '1.0.0',
186 'bar': '1.0.0'
151 }); 187 });
188 }
152 189
190 withLockFile() {
153 testResolve('with compatible locked dependency', { 191 testResolve('with compatible locked dependency', {
154 'myapp 0.0.0': { 192 'myapp 0.0.0': {
155 'foo': 'any' 193 'foo': 'any'
156 }, 194 },
157 'foo 1.0.0': { 'bar': '1.0.0' }, 195 'foo 1.0.0': { 'bar': '1.0.0' },
158 'foo 1.0.1': { 'bar': '1.0.1' }, 196 'foo 1.0.1': { 'bar': '1.0.1' },
159 'foo 1.0.2': { 'bar': '1.0.2' }, 197 'foo 1.0.2': { 'bar': '1.0.2' },
160 'bar 1.0.0': {}, 198 'bar 1.0.0': {},
161 'bar 1.0.1': {}, 199 'bar 1.0.1': {},
162 'bar 1.0.2': {} 200 'bar 1.0.2': {}
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after
198 'bar 1.0.2': {}, 236 'bar 1.0.2': {},
199 'baz 1.0.0': {} 237 'baz 1.0.0': {}
200 }, lockfile: { 238 }, lockfile: {
201 'baz': '1.0.0' 239 'baz': '1.0.0'
202 }, result: { 240 }, result: {
203 'myapp from root': '0.0.0', 241 'myapp from root': '0.0.0',
204 'foo': '1.0.2', 242 'foo': '1.0.2',
205 'bar': '1.0.2' 243 'bar': '1.0.2'
206 }); 244 });
207 245
208 testResolve('circular dependency', { 246 testResolve('unlocks dependencies if necessary to ensure that a new '
247 'dependency is satisfied', {
248 'myapp 0.0.0': {
249 'foo': 'any',
250 'newdep': 'any'
251 },
252 'foo 1.0.0': { 'bar': '<2.0.0' },
253 'bar 1.0.0': { 'baz': '<2.0.0' },
254 'baz 1.0.0': { 'qux': '<2.0.0' },
255 'qux 1.0.0': {},
256 'foo 2.0.0': { 'bar': '<3.0.0' },
257 'bar 2.0.0': { 'baz': '<3.0.0' },
258 'baz 2.0.0': { 'qux': '<3.0.0' },
259 'qux 2.0.0': {},
260 'newdep 2.0.0': { 'baz': '>=1.5.0' }
261 }, lockfile: {
262 'foo': '1.0.0',
263 'bar': '1.0.0',
264 'baz': '1.0.0',
265 'qux': '1.0.0'
266 }, result: {
267 'myapp from root': '0.0.0',
268 'foo': '2.0.0',
269 'bar': '2.0.0',
270 'baz': '2.0.0',
271 'qux': '1.0.0',
272 'newdep': '2.0.0'
273 }, maxTries: 5, hasGreedySolution: true);
nweiz 2013/04/03 00:28:43 These numbers feel very opaque to me. Could you in
Bob Nystrom 2013/04/08 22:13:00 Explaining where the iterations come from would ba
nweiz 2013/04/10 22:56:34 The generalized behavior of the solver is very com
Bob Nystrom 2013/04/11 00:55:10 It's useful, but I think that documentation belong
274 }
275
276 rootDependency() {
277 testResolve('with root source', {
209 'myapp 1.0.0': { 278 'myapp 1.0.0': {
210 'foo': '1.0.0' 279 'foo': '1.0.0'
211 }, 280 },
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': { 281 'foo 1.0.0': {
229 'myapp from root': '>=1.0.0' 282 'myapp from root': '>=1.0.0'
230 } 283 }
231 }, result: { 284 }, result: {
232 'myapp from root': '1.0.0', 285 'myapp from root': '1.0.0',
233 'foo': '1.0.0' 286 'foo': '1.0.0'
234 }); 287 });
235 288
236 testResolve('dependency back onto root package with different source', { 289 testResolve('with different source', {
237 'myapp 1.0.0': { 290 'myapp 1.0.0': {
238 'foo': '1.0.0' 291 'foo': '1.0.0'
239 }, 292 },
240 'foo 1.0.0': { 293 'foo 1.0.0': {
241 'myapp': '>=1.0.0' 294 'myapp': '>=1.0.0'
242 } 295 }
243 }, result: { 296 }, result: {
244 'myapp from root': '1.0.0', 297 'myapp from root': '1.0.0',
245 'foo': '1.0.0' 298 'foo': '1.0.0'
246 }); 299 });
247 300
248 testResolve('mismatched dependencies back onto root package', { 301 testResolve('with mismatched sources', {
249 'myapp 1.0.0': { 302 'myapp 1.0.0': {
250 'foo': '1.0.0', 303 'foo': '1.0.0',
251 'bar': '1.0.0' 304 'bar': '1.0.0'
252 }, 305 },
253 'foo 1.0.0': { 306 'foo 1.0.0': {
254 'myapp': '>=1.0.0' 307 'myapp': '>=1.0.0'
255 }, 308 },
256 'bar 1.0.0': { 309 'bar 1.0.0': {
257 'myapp from mock2': '>=1.0.0' 310 'myapp from mock2': '>=1.0.0'
258 } 311 }
259 }, error: sourceMismatch('foo', 'bar')); 312 }, error: sourceMismatch('foo', 'bar'));
260 313
261 testResolve('dependency back onto root package with wrong version', { 314 testResolve('with wrong version', {
262 'myapp 1.0.0': { 315 'myapp 1.0.0': {
263 'foo': '1.0.0' 316 'foo': '1.0.0'
264 }, 317 },
265 'foo 1.0.0': { 318 'foo 1.0.0': {
266 'myapp': '<1.0.0' 319 'myapp': '<1.0.0'
267 } 320 }
268 }, error: disjointConstraint(['foo'])); 321 }, error: couldNotSolve);
322 }
269 323
324 devDependency() {
325 testResolve("includes root package's dev dependencies", {
326 'myapp 1.0.0': {
327 '(dev) foo': '1.0.0',
328 '(dev) bar': '1.0.0'
329 },
330 'foo 1.0.0': {},
331 'bar 1.0.0': {}
332 }, result: {
333 'myapp from root': '1.0.0',
334 'foo': '1.0.0',
335 'bar': '1.0.0'
336 });
337
338 testResolve("includes dev dependency's transitive dependencies", {
339 'myapp 1.0.0': {
340 '(dev) foo': '1.0.0'
341 },
342 'foo 1.0.0': {
343 'bar': '1.0.0'
344 },
345 'bar 1.0.0': {}
346 }, result: {
347 'myapp from root': '1.0.0',
348 'foo': '1.0.0',
349 'bar': '1.0.0'
350 });
351
352 testResolve("ignores transitive dependency's dev dependencies", {
353 'myapp 1.0.0': {
354 'foo': '1.0.0'
355 },
356 'foo 1.0.0': {
357 '(dev) bar': '1.0.0'
358 },
359 'bar 1.0.0': {}
360 }, result: {
361 'myapp from root': '1.0.0',
362 'foo': '1.0.0'
363 });
364 }
365
366 unsolvable() {
nweiz 2013/04/03 00:28:43 Iteration counts seem very relevant for testing th
Bob Nystrom 2013/04/08 22:13:00 Done.
270 testResolve('no version that matches requirement', { 367 testResolve('no version that matches requirement', {
271 'myapp 0.0.0': { 368 'myapp 0.0.0': {
272 'foo': '>=1.0.0 <2.0.0' 369 'foo': '>=1.0.0 <2.0.0'
273 }, 370 },
274 'foo 2.0.0': {}, 371 'foo 2.0.0': {},
275 'foo 2.1.3': {} 372 'foo 2.1.3': {}
276 }, error: noVersion(['myapp'])); 373 }, error: noVersion(['myapp']));
277 374
278 testResolve('no version that matches combined constraint', { 375 testResolve('no version that matches combined constraint', {
279 'myapp 0.0.0': { 376 'myapp 0.0.0': {
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after
328 'foo 1.0.0': { 425 'foo 1.0.0': {
329 'shared': '1.0.0' 426 'shared': '1.0.0'
330 }, 427 },
331 'bar 1.0.0': { 428 'bar 1.0.0': {
332 'shared from mock2': '1.0.0' 429 'shared from mock2': '1.0.0'
333 }, 430 },
334 'shared 1.0.0': {}, 431 'shared 1.0.0': {},
335 'shared 1.0.0 from mock2': {} 432 'shared 1.0.0 from mock2': {}
336 }, error: sourceMismatch('foo', 'bar')); 433 }, error: sourceMismatch('foo', 'bar'));
337 434
338 testResolve('unstable dependency graph', { 435 testResolve('no valid solution', {
436 'myapp 0.0.0': {
437 'a': 'any',
438 'b': 'any'
439 },
440 'a 1.0.0': {
441 'b': '1.0.0'
442 },
443 'a 2.0.0': {
444 'b': '2.0.0'
445 },
446 'b 1.0.0': {
447 'a': '2.0.0'
448 },
449 'b 2.0.0': {
450 'a': '1.0.0'
451 }
452 }, error: couldNotSolve);
453 }
454
455 backtracking() {
456 testResolve('circular dependency on older version', {
339 'myapp 0.0.0': { 457 'myapp 0.0.0': {
340 'a': '>=1.0.0' 458 'a': '>=1.0.0'
341 }, 459 },
342 'a 1.0.0': {}, 460 'a 1.0.0': {},
343 'a 2.0.0': { 461 'a 2.0.0': {
344 'b': '1.0.0' 462 'b': '1.0.0'
345 }, 463 },
346 'b 1.0.0': { 464 'b 1.0.0': {
347 'a': '1.0.0' 465 'a': '1.0.0'
348 } 466 }
349 }, error: couldNotSolve); 467 }, result: {
468 'myapp from root': '0.0.0',
469 'a': '1.0.0'
470 }, maxTries: 3);
350 471
351 group('dev dependencies', () { 472 /// The latest versions of a and b disagree on c. An older version of either
352 testResolve("includes root package's dev dependencies", { 473 /// will resolve the problem. This test validates that b, which is farther
353 'myapp 1.0.0': { 474 /// in the dependency graph from myapp is downgraded first.
354 '(dev) foo': '1.0.0', 475 testResolve('rolls back leaf versions first', {
355 '(dev) bar': '1.0.0' 476 'myapp 0.0.0': {
356 }, 477 'a': 'any'
357 'foo 1.0.0': {}, 478 },
358 'bar 1.0.0': {} 479 'a 1.0.0': {
359 }, result: { 480 'b': 'any'
360 'myapp from root': '1.0.0', 481 },
361 'foo': '1.0.0', 482 'a 2.0.0': {
362 'bar': '1.0.0' 483 'b': 'any',
363 }); 484 'c': '2.0.0'
485 },
486 'b 1.0.0': {},
487 'b 2.0.0': {
488 'c': '1.0.0'
489 },
490 'c 1.0.0': {},
491 'c 2.0.0': {}
492 }, result: {
493 'myapp from root': '0.0.0',
494 'a': '2.0.0',
495 'b': '1.0.0',
496 'c': '2.0.0'
497 }, maxTries: 3);
364 498
365 testResolve("includes dev dependency's transitive dependencies", { 499 // Only one version of baz, so foo and bar will have to downgrade until they
366 'myapp 1.0.0': { 500 // reach it.
367 '(dev) foo': '1.0.0' 501 testResolve('simple transitive', {
368 }, 502 'myapp 0.0.0': {'foo': 'any'},
369 'foo 1.0.0': { 503 'foo 1.0.0': {'bar': '1.0.0'},
370 'bar': '1.0.0' 504 'foo 2.0.0': {'bar': '2.0.0'},
371 }, 505 'foo 3.0.0': {'bar': '3.0.0'},
372 'bar 1.0.0': {} 506 'bar 1.0.0': {'baz': 'any'},
373 }, result: { 507 'bar 2.0.0': {'baz': '2.0.0'},
374 'myapp from root': '1.0.0', 508 'bar 3.0.0': {'baz': '3.0.0'},
375 'foo': '1.0.0', 509 'baz 1.0.0': {}
376 'bar': '1.0.0' 510 }, result: {
377 }); 511 'myapp from root': '0.0.0',
512 'foo': '1.0.0',
513 'bar': '1.0.0',
514 'baz': '1.0.0'
515 }, maxTries: 5);
378 516
379 testResolve("ignores transitive dependency's dev dependencies", { 517 // This sets up a hundred versions of foo and bar, 0.0.0 through 9.9.0. Each
380 'myapp 1.0.0': { 518 // version of foo depends on a baz with the same major version. Each version
381 'foo': '1.0.0' 519 // of bar depends on a baz with the same minor version. There is only one
nweiz 2013/04/03 00:28:43 "the same minor version" is inaccurate; foo depend
Bob Nystrom 2013/04/08 22:13:00 Done.
382 }, 520 // version of baz, 0.0.0, so only older versions of foo and bar will
383 'foo 1.0.0': { 521 // satisfy it.
384 '(dev) bar': '1.0.0' 522 var map = {
385 }, 523 'myapp 0.0.0': {
386 'bar 1.0.0': {} 524 'foo': 'any',
387 }, result: { 525 'bar': 'any'
388 'myapp from root': '1.0.0', 526 },
389 'foo': '1.0.0' 527 'baz 0.0.0': {}
390 }); 528 };
391 }); 529
530 for (var i = 0; i < 10; i++) {
531 for (var j = 0; j < 10; j++) {
532 map['foo $i.$j.0'] = {'baz': '$i.0.0'};
533 map['bar $i.$j.0'] = {'baz': '0.$j.0'};
534 }
535 }
536
537 testResolve('complex backtrack', map, result: {
538 'myapp from root': '0.0.0',
539 'foo': '0.9.0',
540 'bar': '9.0.0',
541 'baz': '0.0.0'
542 }, maxTries: 1090); // TODO(rnystrom): Is this acceptable?
392 } 543 }
393 544
394 // TODO(rnystrom): More stuff to test: 545 testResolve(description, packages,
395 // - Depending on a non-existent package. 546 {lockfile, result, Matcher error, int maxTries,
396 // - Test that only a certain number requests are sent to the mock source so we 547 bool hasGreedySolution}) {
397 // can keep track of server traffic. 548 // Close over the top-level variable since it will be mutated.
549 var allowBacktracking_ = allowBacktracking;
398 550
399 testResolve(description, packages, {lockfile, result, Matcher error}) { 551 if (maxTries == null) maxTries = 1;
552 if (hasGreedySolution == null) hasGreedySolution = false;
553
554 if (!allowBacktracking_) {
555 // The greedy solver should fail any graph that does expect multiple tries
556 // and isn't explicitly annotated to have a greedy solution.
557 if (maxTries > 1 && !hasGreedySolution) {
nweiz 2013/04/03 00:28:43 It would be more readable if you assigned hasGreed
Bob Nystrom 2013/04/08 22:13:00 Done.
558 result = null;
559 error = couldNotSolve;
560 }
561 }
562
400 test(description, () { 563 test(description, () {
401 var cache = new SystemCache('.'); 564 var cache = new SystemCache('.');
402 source1 = new MockSource('mock1'); 565 source1 = new MockSource('mock1');
403 source2 = new MockSource('mock2'); 566 source2 = new MockSource('mock2');
404 cache.register(source1); 567 cache.register(source1);
405 cache.register(source2); 568 cache.register(source2);
406 cache.sources.setDefault(source1.name); 569 cache.sources.setDefault(source1.name);
407 570
408 // Build the test package graph. 571 // Build the test package graph.
409 var root; 572 var root;
410 packages.forEach((nameVersion, dependencies) { 573 packages.forEach((nameVersion, dependencies) {
411 var parsed = parseSource(nameVersion, (isDev, nameVersion, source) { 574 var parsed = parseSource(nameVersion, (isDev, nameVersion, source) {
412 var parts = nameVersion.split(' '); 575 var parts = nameVersion.split(' ');
413 var name = parts[0]; 576 var name = parts[0];
414 var version = parts[1]; 577 var version = parts[1];
415 578
416 var package = source1.mockPackage(name, version, dependencies); 579 var package = mockPackage(name, version, dependencies);
417 if (name == 'myapp') { 580 if (name == 'myapp') {
418 // Don't add the root package to the server, so we can verify that Pub 581 // 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 582 // doesn't try to look up information about the local package on the
420 // remote server. 583 // remote server.
421 root = package; 584 root = package;
422 } else { 585 } else {
423 source.addPackage(package); 586 source.addPackage(name, package);
424 } 587 }
425 }); 588 });
426 }); 589 });
427 590
428 // Clean up the expectation. 591 // Clean up the expectation.
429 if (result != null) { 592 if (result != null) {
430 var newResult = {}; 593 var newResult = {};
431 result.forEach((name, version) { 594 result.forEach((name, version) {
432 parseSource(name, (isDev, name, source) { 595 parseSource(name, (isDev, name, source) {
433 version = new Version.parse(version); 596 version = new Version.parse(version);
434 newResult[name] = new PackageId(name, source, version, name); 597 newResult[name] = new PackageId(name, source, version, name);
435 }); 598 });
436 }); 599 });
437 result = newResult; 600 result = newResult;
438 } 601 }
439 602
440 var realLockFile = new LockFile.empty(); 603 var realLockFile = new LockFile.empty();
441 if (lockfile != null) { 604 if (lockfile != null) {
442 lockfile.forEach((name, version) { 605 lockfile.forEach((name, version) {
443 version = new Version.parse(version); 606 version = new Version.parse(version);
444 realLockFile.packages[name] = 607 realLockFile.packages[name] =
445 new PackageId(name, source1, version, name); 608 new PackageId(name, source1, version, name);
446 }); 609 });
447 } 610 }
448 611
449 // Resolve the versions. 612 // Resolve the versions.
450 var future = resolveVersions(cache.sources, root, realLockFile); 613 var future = resolveVersions(cache.sources, root,
614 allowBacktracking: allowBacktracking_, lockFile: realLockFile);
451 615
452 if (result != null) { 616 if (result != null) {
453 expect(future, completion(predicate((actualResult) { 617 expect(future, completion(predicate((actual) {
454 for (var actualId in actualResult) { 618 for (var actualId in actual.packages) {
455 if (!result.containsKey(actualId.name)) return false; 619 if (!result.containsKey(actualId.name)) return false;
456 var expectedId = result.remove(actualId.name); 620 var expectedId = result.remove(actualId.name);
457 if (actualId != expectedId) return false; 621 if (actualId != expectedId) return false;
458 } 622 }
623
459 return result.isEmpty; 624 return result.isEmpty;
625
460 }, 'packages to match $result'))); 626 }, 'packages to match $result')));
627
628 expect(future, completion(predicate(
629 (actual) => actual.attemptedSolutions <= maxTries,
630 'does not backtrack too much')));
461 } else if (error != null) { 631 } else if (error != null) {
462 expect(future, throwsA(error)); 632 expect(future, throwsA(error));
463 } 633 }
464 }); 634 });
465 } 635 }
466 636
467 /// A source used for testing. This both creates mock package objects and acts 637 /// A source used for testing. This both creates mock package objects and acts
468 /// as a source for them. 638 /// as a source for them.
469 /// 639 ///
470 /// In order to support testing packages that have the same name but different 640 /// 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 641 /// descriptions, a package's name is calculated by taking the description
472 /// string and stripping off any trailing hyphen followed by non-hyphen 642 /// string and stripping off any trailing hyphen followed by non-hyphen
473 /// characters. 643 /// characters.
474 class MockSource extends Source { 644 class MockSource extends Source {
475 final Map<String, Map<Version, Package>> _packages; 645 final _packages = <String, Map<Version, Package>>{};
646
647 /// Keeps track of which package version lists have been requested. Ensures
648 /// that a source is only hit once for a given package and that pub
649 /// internally caches the results.
650 final _requestedVersions = new Set<String>();
651
652 /// Keeps track of which package pubspecs have been requested. Ensures that a
653 /// source is only hit once for a given package and that pub internally
654 /// caches the results.
655 final _requestedPubspecs = new Map<String, Set<Version>>();
476 656
477 final String name; 657 final String name;
478 bool get shouldCache => true; 658 bool get shouldCache => true;
479 659
480 MockSource(this.name) 660 MockSource(this.name);
481 : _packages = <String, Map<Version, Package>>{};
482 661
483 Future<List<Version>> getVersions(String name, String description) { 662 Future<List<Version>> getVersions(String name, String description) {
484 return new Future.of(() => _packages[description].keys.toList()); 663 return new Future.of(() {
664 // Make sure the solver doesn't request the same thing twice.
665 if (_requestedVersions.contains(description)) {
666 throw 'Version list for $description was already requested.';
667 }
668
669 _requestedVersions.add(description);
670
671 if (!_packages.containsKey(description)){
672 throw 'MockSource does not have a package matching "$description".';
673 }
674 return _packages[description].keys.toList();
675 });
485 } 676 }
486 677
487 Future<Pubspec> describe(PackageId id) { 678 Future<Pubspec> describe(PackageId id) {
488 return new Future.of(() => _packages[id.name][id.version].pubspec); 679 return new Future.of(() {
680 // Make sure the solver doesn't request the same thing twice.
681 if (_requestedPubspecs.containsKey(id.description) &&
682 _requestedPubspecs[id.description].contains(id.version)) {
683 throw 'Pubspec for $id was already requested.';
684 }
685
686 _requestedPubspecs.putIfAbsent(id.description, () => new Set<Version>());
687 _requestedPubspecs[id.description].add(id.version);
688
689 return _packages[id.description][id.version].pubspec;
690 });
489 } 691 }
490 692
491 Future<bool> install(PackageId id, String path) { 693 Future<bool> install(PackageId id, String path) {
492 throw 'no'; 694 throw 'no';
493 } 695 }
494 696
495 Package mockPackage(String description, String version, 697 void addPackage(String description, Package package) {
496 Map dependencyStrings) { 698 _packages.putIfAbsent(description, () => new Map<Version, Package>());
497 // Build the pubspec dependencies. 699 _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 } 700 }
525 } 701 }
526 702
703 Package mockPackage(String description, String version,
704 Map dependencyStrings) {
705 // Build the pubspec dependencies.
706 var dependencies = <PackageRef>[];
707 var devDependencies = <PackageRef>[];
708
709 dependencyStrings.forEach((name, constraint) {
710 parseSource(name, (isDev, name, source) {
711 var packageName = name.replaceFirst(new RegExp(r"-[^-]+$"), "");
712 var ref = new PackageRef(packageName, source,
713 new VersionConstraint.parse(constraint), name);
714
715 if (isDev) {
716 devDependencies.add(ref);
717 } else {
718 dependencies.add(ref);
719 }
720 });
721 });
722
723 var name = description.replaceFirst(new RegExp(r"-[^-]+$"), "");
724 var pubspec = new Pubspec(
725 name, new Version.parse(version), dependencies, devDependencies,
726 new PubspecEnvironment());
727 return new Package.inMemory(pubspec);
728 }
729
527 void parseSource(String description, 730 void parseSource(String description,
528 callback(bool isDev, String name, Source source)) { 731 callback(bool isDev, String name, Source source)) {
529 var isDev = false; 732 var isDev = false;
530 733
531 if (description.startsWith("(dev) ")) { 734 if (description.startsWith("(dev) ")) {
532 description = description.substring("(dev) ".length); 735 description = description.substring("(dev) ".length);
533 isDev = true; 736 isDev = true;
534 } 737 }
535 738
536 var name = description; 739 var name = description;
537 var source = source1; 740 var source = source1;
538 741
539 var sourceNames = { 742 var sourceNames = {
540 'mock1': source1, 743 'mock1': source1,
541 'mock2': source2, 744 'mock2': source2,
542 'root': null 745 'root': null
543 }; 746 };
544 747
545 var match = new RegExp(r"(.*) from (.*)").firstMatch(description); 748 var match = new RegExp(r"(.*) from (.*)").firstMatch(description);
546 if (match != null) { 749 if (match != null) {
547 name = match[1]; 750 name = match[1];
548 source = sourceNames[match[2]]; 751 source = sourceNames[match[2]];
549 } 752 }
550 753
551 callback(isDev, name, source); 754 callback(isDev, name, source);
552 } 755 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698