OLD | NEW |
---|---|
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 Loading... | |
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 Loading... | |
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 Loading... | |
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 and c when |
380 'myapp 1.0.0': { | 466 // it's really a-2.0.0 which has an unsatisfied dependency. |
381 'foo': '1.0.0' | 467 testResolve('backjump to nearer unsatisfied package', { |
382 }, | 468 'myapp 0.0.0': {'a': 'any'}, |
383 'foo 1.0.0': { | 469 'a 1.0.0': {'b': 'any'}, |
384 '(dev) bar': '1.0.0' | 470 'a 2.0.0': { |
385 }, | 471 'b': 'any', |
386 'bar 1.0.0': {} | 472 'd': '2.0.0-nonexistent' |
387 }, result: { | 473 }, |
388 'myapp from root': '1.0.0', | 474 'b 1.0.0': {'c': 'any'}, |
389 'foo': '1.0.0' | 475 'b 2.0.0': {'c': 'any'}, |
390 }); | 476 'b 3.0.0': {'c': 'any'}, |
391 }); | 477 'c 1.0.0': {}, |
478 'c 2.0.0': {}, | |
479 'c 3.0.0': {}, | |
480 'd 1.0.0': {}, | |
481 }, result: { | |
482 'myapp from root': '0.0.0', | |
483 'a': '1.0.0', | |
484 'b': '3.0.0', | |
485 'c': '3.0.0' | |
486 }, maxTries: 2); | |
nweiz
2013/04/10 22:56:35
This would pass if you used bundler's sort heurist
Bob Nystrom
2013/04/11 00:55:11
Sorting by number of versions will require us to d
nweiz
2013/04/11 22:12:04
For this test, I was pointing it out from a black-
Bob Nystrom
2013/04/16 18:34:17
Added some more documentation and revised the test
| |
487 | |
488 // This sets up a hundred versions of foo and bar, 0.0.0 through 9.9.0. Each | |
489 // version of foo depends on a baz with the same major version. Each version | |
490 // of bar depends on a baz with the same minor version. There is only one | |
491 // version of baz, 0.0.0, so only older versions of foo and bar will | |
492 // satisfy it. | |
493 var map = { | |
494 'myapp 0.0.0': { | |
495 'foo': 'any', | |
496 'bar': 'any' | |
497 }, | |
498 'baz 0.0.0': {} | |
499 }; | |
500 | |
501 for (var i = 0; i < 10; i++) { | |
502 for (var j = 0; j < 10; j++) { | |
503 map['foo $i.$j.0'] = {'baz': '$i.0.0'}; | |
504 map['bar $i.$j.0'] = {'baz': '0.$j.0'}; | |
505 } | |
506 } | |
507 | |
508 testResolve('complex backtrack', map, result: { | |
509 'myapp from root': '0.0.0', | |
510 'foo': '0.9.0', | |
511 'bar': '9.0.0', | |
512 'baz': '0.0.0' | |
513 }, maxTries: 100); | |
nweiz
2013/04/10 22:56:35
woo
Bob Nystrom
2013/04/11 00:55:11
Yes, much better.
| |
514 | |
515 // TODO(rnystrom): More tests. In particular: | |
516 // - Tests that demonstrate backtracking for every case that can cause a | |
517 // solution to fail (no versions, disjoint, etc.) | |
518 // - Tests where there are multiple valid solutions and "best" is possibly | |
519 // ambiguous to nail down which order the backtracker tries solutions. | |
392 } | 520 } |
393 | 521 |
394 // TODO(rnystrom): More stuff to test: | 522 testResolve(description, packages, |
395 // - Depending on a non-existent package. | 523 {lockfile, result, FailMatcherBuilder error, int maxTries, |
396 // - Test that only a certain number requests are sent to the mock source so we | 524 bool hasGreedySolution}) { |
397 // can keep track of server traffic. | 525 // Close over the top-level variable since it will be mutated. |
526 var allowBacktracking_ = allowBacktracking; | |
398 | 527 |
399 testResolve(description, packages, {lockfile, result, Matcher error}) { | 528 if (maxTries == null) maxTries = 1; |
529 if (hasGreedySolution == null) hasGreedySolution = maxTries == 1; | |
530 | |
531 if (!allowBacktracking_) { | |
532 // The greedy solver should fail any graph that does expect multiple tries | |
533 // and isn't explicitly annotated to have a greedy solution. | |
534 if (!hasGreedySolution) { | |
535 result = null; | |
536 error = couldNotSolve; | |
537 } | |
538 } | |
539 | |
400 test(description, () { | 540 test(description, () { |
401 var cache = new SystemCache('.'); | 541 var cache = new SystemCache('.'); |
402 source1 = new MockSource('mock1'); | 542 source1 = new MockSource('mock1'); |
403 source2 = new MockSource('mock2'); | 543 source2 = new MockSource('mock2'); |
404 cache.register(source1); | 544 cache.register(source1); |
405 cache.register(source2); | 545 cache.register(source2); |
406 cache.sources.setDefault(source1.name); | 546 cache.sources.setDefault(source1.name); |
407 | 547 |
408 // Build the test package graph. | 548 // Build the test package graph. |
409 var root; | 549 var root; |
410 packages.forEach((nameVersion, dependencies) { | 550 packages.forEach((nameVersion, dependencies) { |
411 var parsed = parseSource(nameVersion, (isDev, nameVersion, source) { | 551 var parsed = parseSource(nameVersion, (isDev, nameVersion, source) { |
412 var parts = nameVersion.split(' '); | 552 var parts = nameVersion.split(' '); |
413 var name = parts[0]; | 553 var name = parts[0]; |
414 var version = parts[1]; | 554 var version = parts[1]; |
415 | 555 |
416 var package = source1.mockPackage(name, version, dependencies); | 556 var package = mockPackage(name, version, dependencies); |
417 if (name == 'myapp') { | 557 if (name == 'myapp') { |
418 // Don't add the root package to the server, so we can verify that Pub | 558 // 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 | 559 // doesn't try to look up information about the local package on the |
420 // remote server. | 560 // remote server. |
421 root = package; | 561 root = package; |
422 } else { | 562 } else { |
423 source.addPackage(package); | 563 source.addPackage(name, package); |
424 } | 564 } |
425 }); | 565 }); |
426 }); | 566 }); |
427 | 567 |
428 // Clean up the expectation. | 568 // Clean up the expectation. |
429 if (result != null) { | 569 if (result != null) { |
430 var newResult = {}; | 570 var newResult = {}; |
431 result.forEach((name, version) { | 571 result.forEach((name, version) { |
432 parseSource(name, (isDev, name, source) { | 572 parseSource(name, (isDev, name, source) { |
433 version = new Version.parse(version); | 573 version = new Version.parse(version); |
434 newResult[name] = new PackageId(name, source, version, name); | 574 newResult[name] = new PackageId(name, source, version, name); |
435 }); | 575 }); |
436 }); | 576 }); |
437 result = newResult; | 577 result = newResult; |
438 } | 578 } |
439 | 579 |
440 var realLockFile = new LockFile.empty(); | 580 var realLockFile = new LockFile.empty(); |
441 if (lockfile != null) { | 581 if (lockfile != null) { |
442 lockfile.forEach((name, version) { | 582 lockfile.forEach((name, version) { |
443 version = new Version.parse(version); | 583 version = new Version.parse(version); |
444 realLockFile.packages[name] = | 584 realLockFile.packages[name] = |
445 new PackageId(name, source1, version, name); | 585 new PackageId(name, source1, version, name); |
446 }); | 586 }); |
447 } | 587 } |
448 | 588 |
449 // Resolve the versions. | 589 // Resolve the versions. |
450 var future = resolveVersions(cache.sources, root, realLockFile); | 590 var future = resolveVersions(cache.sources, root, |
591 allowBacktracking: allowBacktracking_, lockFile: realLockFile); | |
451 | 592 |
593 var matcher; | |
452 if (result != null) { | 594 if (result != null) { |
453 expect(future, completion(predicate((actualResult) { | 595 matcher = new SolveSuccessMatcher(result, maxTries); |
454 for (var actualId in actualResult) { | |
455 if (!result.containsKey(actualId.name)) return false; | |
456 var expectedId = result.remove(actualId.name); | |
457 if (actualId != expectedId) return false; | |
458 } | |
459 return result.isEmpty; | |
460 }, 'packages to match $result'))); | |
461 } else if (error != null) { | 596 } else if (error != null) { |
462 expect(future, throwsA(error)); | 597 matcher = error(maxTries); |
463 } | 598 } |
599 | |
600 expect(future, completion(matcher)); | |
464 }); | 601 }); |
465 } | 602 } |
466 | 603 |
604 typedef SolveFailMatcher FailMatcherBuilder(int maxTries); | |
605 | |
606 FailMatcherBuilder noVersion(List<String> packages) { | |
607 return (maxTries) => new SolveFailMatcher(packages, maxTries, | |
608 NoVersionException); | |
609 } | |
610 | |
611 FailMatcherBuilder disjointConstraint(List<String> packages) { | |
612 return (maxTries) => new SolveFailMatcher(packages, maxTries, | |
613 DisjointConstraintException); | |
614 } | |
615 | |
616 FailMatcherBuilder descriptionMismatch(String package1, String package2) { | |
617 return (maxTries) => new SolveFailMatcher([package1, package2], maxTries, | |
618 DescriptionMismatchException); | |
619 } | |
620 | |
621 // If no solution can be found, the solver just reports the last failure that | |
622 // happened during propagation. Since we don't specify the order that solutions | |
623 // are tried, this just validates that *some* failure occurred, but not which. | |
624 SolveFailMatcher couldNotSolve(maxTries) => | |
625 new SolveFailMatcher([], maxTries, null); | |
626 | |
627 FailMatcherBuilder sourceMismatch(String package1, String package2) { | |
628 return (maxTries) => new SolveFailMatcher([package1, package2], maxTries, | |
629 SourceMismatchException); | |
630 } | |
631 | |
632 class SolveSuccessMatcher implements Matcher { | |
633 /// The expected concrete package selections. | |
634 final Map<String, PackageId> _expected; | |
635 | |
636 /// The maximum number of attempts that should have been tried before finding | |
637 /// the solution. | |
638 final int _maxTries; | |
639 | |
640 SolveSuccessMatcher(this._expected, this._maxTries); | |
641 | |
642 Description describe(Description description) { | |
643 return description.add( | |
644 'Solver to use at most $_maxTries attempts to find:\n' | |
645 '${_listPackages(_expected.values)}'); | |
646 } | |
647 | |
648 Description describeMismatch(SolveResult result, | |
649 Description description, | |
650 MatchState state, bool verbose) { | |
651 if (!result.succeeded) { | |
652 description.add('Solver failed with:\n${result.error}'); | |
653 return; | |
654 } | |
655 | |
656 description.add('Resolved:\n${_listPackages(result.packages)}\n'); | |
657 description.add(state.state); | |
658 return description; | |
659 } | |
660 | |
661 bool matches(SolveResult result, MatchState state) { | |
662 if (!result.succeeded) return false; | |
663 | |
664 var expected = new Map.from(_expected); | |
665 for (var id in result.packages) { | |
666 if (!expected.containsKey(id.name)) { | |
667 state.state = 'Should not have selected $id'; | |
668 return false; | |
669 } | |
670 | |
671 var expectedId = expected.remove(id.name); | |
672 if (id != expectedId) { | |
673 state.state = 'Expected $expectedId, not $id'; | |
674 return false; | |
675 } | |
676 } | |
677 | |
678 if (!expected.isEmpty) { | |
679 state.state = 'Missing:\n${_listPackages(expected.values)}'; | |
680 return false; | |
681 } | |
682 | |
683 // Allow 1 here because the greedy solver will only make one attempt. | |
684 if (result.attemptedSolutions != 1 && | |
685 result.attemptedSolutions != _maxTries) { | |
686 state.state = 'Took ${result.attemptedSolutions} attempts'; | |
687 return false; | |
688 } | |
689 | |
690 return true; | |
691 } | |
692 | |
693 String _listPackages(Iterable<PackageId> packages) { | |
694 return '- ${packages.join('\n- ')}'; | |
695 } | |
696 } | |
697 | |
698 class SolveFailMatcher implements Matcher { | |
699 /// The strings that should appear in the resulting error message. | |
700 final Iterable<String> _expected; | |
701 | |
702 /// The maximum number of attempts that should be tried before failing. | |
703 final int _maxTries; | |
704 | |
705 /// The concrete error type that should be found, or `null` if any | |
706 /// [SolveFailure] is allowed. | |
707 final Type _expectedType; | |
708 | |
709 SolveFailMatcher(this._expected, this._maxTries, this._expectedType); | |
710 | |
711 Description describe(Description description) { | |
712 description.add('Solver should fail after at most $_maxTries attempts.'); | |
713 if (!_expected.isEmpty) { | |
714 var textList = _expected.map((s) => '"$s"').join(", "); | |
715 description.add(' The error should contain $textList.'); | |
716 } | |
717 return description; | |
718 } | |
719 | |
720 Description describeMismatch(SolveResult result, | |
721 Description description, | |
722 MatchState state, bool verbose) { | |
723 if (result.succeeded) { | |
724 description.add('Solver succeeded'); | |
725 return; | |
726 } | |
727 | |
728 description.add(state.state); | |
729 return description; | |
730 } | |
731 | |
732 bool matches(SolveResult result, MatchState state) { | |
733 if (result.succeeded) return false; | |
734 | |
735 if (_expectedType != null && result.error.runtimeType != _expectedType) { | |
736 state.state = 'Should have error type $_expectedType, got ' | |
737 '${result.error.runtimeType}'; | |
738 return false; | |
739 } | |
740 | |
741 var message = result.error.toString(); | |
742 for (var expected in _expected) { | |
743 if (!message.contains(expected)) { | |
744 state = 'Expected error to contain "$expected", got:\n$message'; | |
745 return false; | |
746 } | |
747 } | |
748 | |
749 // Allow 1 here because the greedy solver will only make one attempt. | |
750 if (result.attemptedSolutions != 1 && | |
751 result.attemptedSolutions != _maxTries) { | |
752 state.state = 'Took ${result.attemptedSolutions} attempts'; | |
753 return false; | |
754 } | |
755 | |
756 return true; | |
757 } | |
758 } | |
759 | |
467 /// A source used for testing. This both creates mock package objects and acts | 760 /// A source used for testing. This both creates mock package objects and acts |
468 /// as a source for them. | 761 /// as a source for them. |
469 /// | 762 /// |
470 /// In order to support testing packages that have the same name but different | 763 /// 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 | 764 /// descriptions, a package's name is calculated by taking the description |
472 /// string and stripping off any trailing hyphen followed by non-hyphen | 765 /// string and stripping off any trailing hyphen followed by non-hyphen |
473 /// characters. | 766 /// characters. |
474 class MockSource extends Source { | 767 class MockSource extends Source { |
475 final Map<String, Map<Version, Package>> _packages; | 768 final _packages = <String, Map<Version, Package>>{}; |
769 | |
770 /// Keeps track of which package version lists have been requested. Ensures | |
771 /// that a source is only hit once for a given package and that pub | |
772 /// internally caches the results. | |
773 final _requestedVersions = new Set<String>(); | |
774 | |
775 /// Keeps track of which package pubspecs have been requested. Ensures that a | |
776 /// source is only hit once for a given package and that pub internally | |
777 /// caches the results. | |
778 final _requestedPubspecs = new Map<String, Set<Version>>(); | |
476 | 779 |
477 final String name; | 780 final String name; |
478 bool get shouldCache => true; | 781 bool get shouldCache => true; |
479 | 782 |
480 MockSource(this.name) | 783 MockSource(this.name); |
481 : _packages = <String, Map<Version, Package>>{}; | |
482 | 784 |
483 Future<List<Version>> getVersions(String name, String description) { | 785 Future<List<Version>> getVersions(String name, String description) { |
484 return new Future.of(() => _packages[description].keys.toList()); | 786 return new Future.of(() { |
787 // Make sure the solver doesn't request the same thing twice. | |
788 if (_requestedVersions.contains(description)) { | |
789 throw 'Version list for $description was already requested.'; | |
790 } | |
791 | |
792 _requestedVersions.add(description); | |
793 | |
794 if (!_packages.containsKey(description)){ | |
795 throw 'MockSource does not have a package matching "$description".'; | |
796 } | |
797 return _packages[description].keys.toList(); | |
798 }); | |
485 } | 799 } |
486 | 800 |
487 Future<Pubspec> describe(PackageId id) { | 801 Future<Pubspec> describe(PackageId id) { |
488 return new Future.of(() => _packages[id.name][id.version].pubspec); | 802 return new Future.of(() { |
803 // Make sure the solver doesn't request the same thing twice. | |
804 if (_requestedPubspecs.containsKey(id.description) && | |
805 _requestedPubspecs[id.description].contains(id.version)) { | |
806 throw 'Pubspec for $id was already requested.'; | |
807 } | |
808 | |
809 _requestedPubspecs.putIfAbsent(id.description, () => new Set<Version>()); | |
810 _requestedPubspecs[id.description].add(id.version); | |
811 | |
812 return _packages[id.description][id.version].pubspec; | |
813 }); | |
489 } | 814 } |
490 | 815 |
491 Future<bool> install(PackageId id, String path) { | 816 Future<bool> install(PackageId id, String path) { |
492 throw 'no'; | 817 throw 'no'; |
493 } | 818 } |
494 | 819 |
495 Package mockPackage(String description, String version, | 820 void addPackage(String description, Package package) { |
496 Map dependencyStrings) { | 821 _packages.putIfAbsent(description, () => new Map<Version, Package>()); |
497 // Build the pubspec dependencies. | 822 _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 } | 823 } |
525 } | 824 } |
526 | 825 |
826 Package mockPackage(String description, String version, | |
827 Map dependencyStrings) { | |
828 // Build the pubspec dependencies. | |
829 var dependencies = <PackageRef>[]; | |
830 var devDependencies = <PackageRef>[]; | |
831 | |
832 dependencyStrings.forEach((name, constraint) { | |
833 parseSource(name, (isDev, name, source) { | |
834 var packageName = name.replaceFirst(new RegExp(r"-[^-]+$"), ""); | |
835 var ref = new PackageRef(packageName, source, | |
836 new VersionConstraint.parse(constraint), name); | |
837 | |
838 if (isDev) { | |
839 devDependencies.add(ref); | |
840 } else { | |
841 dependencies.add(ref); | |
842 } | |
843 }); | |
844 }); | |
845 | |
846 var name = description.replaceFirst(new RegExp(r"-[^-]+$"), ""); | |
847 var pubspec = new Pubspec( | |
848 name, new Version.parse(version), dependencies, devDependencies, | |
849 new PubspecEnvironment()); | |
850 return new Package.inMemory(pubspec); | |
851 } | |
852 | |
527 void parseSource(String description, | 853 void parseSource(String description, |
528 callback(bool isDev, String name, Source source)) { | 854 callback(bool isDev, String name, Source source)) { |
529 var isDev = false; | 855 var isDev = false; |
530 | 856 |
531 if (description.startsWith("(dev) ")) { | 857 if (description.startsWith("(dev) ")) { |
532 description = description.substring("(dev) ".length); | 858 description = description.substring("(dev) ".length); |
533 isDev = true; | 859 isDev = true; |
534 } | 860 } |
535 | 861 |
536 var name = description; | 862 var name = description; |
537 var source = source1; | 863 var source = source1; |
538 | 864 |
539 var sourceNames = { | 865 var sourceNames = { |
540 'mock1': source1, | 866 'mock1': source1, |
541 'mock2': source2, | 867 'mock2': source2, |
542 'root': null | 868 'root': null |
543 }; | 869 }; |
544 | 870 |
545 var match = new RegExp(r"(.*) from (.*)").firstMatch(description); | 871 var match = new RegExp(r"(.*) from (.*)").firstMatch(description); |
546 if (match != null) { | 872 if (match != null) { |
547 name = match[1]; | 873 name = match[1]; |
548 source = sourceNames[match[2]]; | 874 source = sourceNames[match[2]]; |
549 } | 875 } |
550 | 876 |
551 callback(isDev, name, source); | 877 callback(isDev, name, source); |
552 } | 878 } |
OLD | NEW |