OLD | NEW |
| (Empty) |
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 | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library pub_update_test; | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:io'; | |
9 | |
10 import 'package:unittest/unittest.dart'; | |
11 | |
12 import '../../pub/lock_file.dart'; | |
13 import '../../pub/package.dart'; | |
14 import '../../pub/pubspec.dart'; | |
15 import '../../pub/sdk.dart' as sdk; | |
16 import '../../pub/source.dart'; | |
17 import '../../pub/source_registry.dart'; | |
18 import '../../pub/system_cache.dart'; | |
19 import '../../pub/utils.dart'; | |
20 import '../../pub/version.dart'; | |
21 import '../../pub/solver/version_solver.dart'; | |
22 import 'test_pub.dart'; | |
23 | |
24 MockSource source1; | |
25 MockSource source2; | |
26 | |
27 main() { | |
28 initConfig(); | |
29 | |
30 // Since this test isn't run from the SDK, it can't find the "version" file | |
31 // to load. Instead, just manually inject a version. | |
32 sdk.version = new Version(1, 2, 3); | |
33 | |
34 group('basic graph', basicGraph); | |
35 group('with lockfile', withLockFile); | |
36 group('root dependency', rootDependency); | |
37 group('dev dependency', devDependency); | |
38 group('unsolvable', unsolvable); | |
39 group('backtracking', backtracking); | |
40 group('SDK constraint', sdkConstraint); | |
41 } | |
42 | |
43 void basicGraph() { | |
44 testResolve('no dependencies', { | |
45 'myapp 0.0.0': {} | |
46 }, result: { | |
47 'myapp from root': '0.0.0' | |
48 }); | |
49 | |
50 testResolve('simple dependency tree', { | |
51 'myapp 0.0.0': { | |
52 'a': '1.0.0', | |
53 'b': '1.0.0' | |
54 }, | |
55 'a 1.0.0': { | |
56 'aa': '1.0.0', | |
57 'ab': '1.0.0' | |
58 }, | |
59 'aa 1.0.0': {}, | |
60 'ab 1.0.0': {}, | |
61 'b 1.0.0': { | |
62 'ba': '1.0.0', | |
63 'bb': '1.0.0' | |
64 }, | |
65 'ba 1.0.0': {}, | |
66 'bb 1.0.0': {} | |
67 }, result: { | |
68 'myapp from root': '0.0.0', | |
69 'a': '1.0.0', | |
70 'aa': '1.0.0', | |
71 'ab': '1.0.0', | |
72 'b': '1.0.0', | |
73 'ba': '1.0.0', | |
74 'bb': '1.0.0' | |
75 }); | |
76 | |
77 testResolve('shared dependency with overlapping constraints', { | |
78 'myapp 0.0.0': { | |
79 'a': '1.0.0', | |
80 'b': '1.0.0' | |
81 }, | |
82 'a 1.0.0': { | |
83 'shared': '>=2.0.0 <4.0.0' | |
84 }, | |
85 'b 1.0.0': { | |
86 'shared': '>=3.0.0 <5.0.0' | |
87 }, | |
88 'shared 2.0.0': {}, | |
89 'shared 3.0.0': {}, | |
90 'shared 3.6.9': {}, | |
91 'shared 4.0.0': {}, | |
92 'shared 5.0.0': {}, | |
93 }, result: { | |
94 'myapp from root': '0.0.0', | |
95 'a': '1.0.0', | |
96 'b': '1.0.0', | |
97 'shared': '3.6.9' | |
98 }); | |
99 | |
100 testResolve('shared dependency where dependent version in turn affects ' | |
101 'other dependencies', { | |
102 'myapp 0.0.0': { | |
103 'foo': '<=1.0.2', | |
104 'bar': '1.0.0' | |
105 }, | |
106 'foo 1.0.0': {}, | |
107 'foo 1.0.1': { 'bang': '1.0.0' }, | |
108 'foo 1.0.2': { 'whoop': '1.0.0' }, | |
109 'foo 1.0.3': { 'zoop': '1.0.0' }, | |
110 'bar 1.0.0': { 'foo': '<=1.0.1' }, | |
111 'bang 1.0.0': {}, | |
112 'whoop 1.0.0': {}, | |
113 'zoop 1.0.0': {} | |
114 }, result: { | |
115 'myapp from root': '0.0.0', | |
116 'foo': '1.0.1', | |
117 'bar': '1.0.0', | |
118 'bang': '1.0.0' | |
119 }, maxTries: 2); | |
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' | |
135 }); | |
136 } | |
137 | |
138 withLockFile() { | |
139 testResolve('with compatible locked dependency', { | |
140 'myapp 0.0.0': { | |
141 'foo': 'any' | |
142 }, | |
143 'foo 1.0.0': { 'bar': '1.0.0' }, | |
144 'foo 1.0.1': { 'bar': '1.0.1' }, | |
145 'foo 1.0.2': { 'bar': '1.0.2' }, | |
146 'bar 1.0.0': {}, | |
147 'bar 1.0.1': {}, | |
148 'bar 1.0.2': {} | |
149 }, lockfile: { | |
150 'foo': '1.0.1' | |
151 }, result: { | |
152 'myapp from root': '0.0.0', | |
153 'foo': '1.0.1', | |
154 'bar': '1.0.1' | |
155 }); | |
156 | |
157 testResolve('with incompatible locked dependency', { | |
158 'myapp 0.0.0': { | |
159 'foo': '>1.0.1' | |
160 }, | |
161 'foo 1.0.0': { 'bar': '1.0.0' }, | |
162 'foo 1.0.1': { 'bar': '1.0.1' }, | |
163 'foo 1.0.2': { 'bar': '1.0.2' }, | |
164 'bar 1.0.0': {}, | |
165 'bar 1.0.1': {}, | |
166 'bar 1.0.2': {} | |
167 }, lockfile: { | |
168 'foo': '1.0.1' | |
169 }, result: { | |
170 'myapp from root': '0.0.0', | |
171 'foo': '1.0.2', | |
172 'bar': '1.0.2' | |
173 }); | |
174 | |
175 testResolve('with unrelated locked dependency', { | |
176 'myapp 0.0.0': { | |
177 'foo': 'any' | |
178 }, | |
179 'foo 1.0.0': { 'bar': '1.0.0' }, | |
180 'foo 1.0.1': { 'bar': '1.0.1' }, | |
181 'foo 1.0.2': { 'bar': '1.0.2' }, | |
182 'bar 1.0.0': {}, | |
183 'bar 1.0.1': {}, | |
184 'bar 1.0.2': {}, | |
185 'baz 1.0.0': {} | |
186 }, lockfile: { | |
187 'baz': '1.0.0' | |
188 }, result: { | |
189 'myapp from root': '0.0.0', | |
190 'foo': '1.0.2', | |
191 'bar': '1.0.2' | |
192 }); | |
193 | |
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); | |
222 } | |
223 | |
224 rootDependency() { | |
225 testResolve('with root source', { | |
226 'myapp 1.0.0': { | |
227 'foo': '1.0.0' | |
228 }, | |
229 'foo 1.0.0': { | |
230 'myapp from root': '>=1.0.0' | |
231 } | |
232 }, result: { | |
233 'myapp from root': '1.0.0', | |
234 'foo': '1.0.0' | |
235 }); | |
236 | |
237 testResolve('with different source', { | |
238 'myapp 1.0.0': { | |
239 'foo': '1.0.0' | |
240 }, | |
241 'foo 1.0.0': { | |
242 'myapp': '>=1.0.0' | |
243 } | |
244 }, result: { | |
245 'myapp from root': '1.0.0', | |
246 'foo': '1.0.0' | |
247 }); | |
248 | |
249 testResolve('with mismatched sources', { | |
250 'myapp 1.0.0': { | |
251 'foo': '1.0.0', | |
252 'bar': '1.0.0' | |
253 }, | |
254 'foo 1.0.0': { | |
255 'myapp': '>=1.0.0' | |
256 }, | |
257 'bar 1.0.0': { | |
258 'myapp from mock2': '>=1.0.0' | |
259 } | |
260 }, error: sourceMismatch('foo', 'bar')); | |
261 | |
262 testResolve('with wrong version', { | |
263 'myapp 1.0.0': { | |
264 'foo': '1.0.0' | |
265 }, | |
266 'foo 1.0.0': { | |
267 'myapp': '<1.0.0' | |
268 } | |
269 }, error: couldNotSolve); | |
270 } | |
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() { | |
315 testResolve('no version that matches requirement', { | |
316 'myapp 0.0.0': { | |
317 'foo': '>=1.0.0 <2.0.0' | |
318 }, | |
319 'foo 2.0.0': {}, | |
320 'foo 2.1.3': {} | |
321 }, error: noVersion(['myapp'])); | |
322 | |
323 testResolve('no version that matches combined constraint', { | |
324 'myapp 0.0.0': { | |
325 'foo': '1.0.0', | |
326 'bar': '1.0.0' | |
327 }, | |
328 'foo 1.0.0': { | |
329 'shared': '>=2.0.0 <3.0.0' | |
330 }, | |
331 'bar 1.0.0': { | |
332 'shared': '>=2.9.0 <4.0.0' | |
333 }, | |
334 'shared 2.5.0': {}, | |
335 'shared 3.5.0': {} | |
336 }, error: noVersion(['foo', 'bar'])); | |
337 | |
338 testResolve('disjoint constraints', { | |
339 'myapp 0.0.0': { | |
340 'foo': '1.0.0', | |
341 'bar': '1.0.0' | |
342 }, | |
343 'foo 1.0.0': { | |
344 'shared': '<=2.0.0' | |
345 }, | |
346 'bar 1.0.0': { | |
347 'shared': '>3.0.0' | |
348 }, | |
349 'shared 2.0.0': {}, | |
350 'shared 4.0.0': {} | |
351 }, error: disjointConstraint(['foo', 'bar'])); | |
352 | |
353 testResolve('mismatched descriptions', { | |
354 'myapp 0.0.0': { | |
355 'foo': '1.0.0', | |
356 'bar': '1.0.0' | |
357 }, | |
358 'foo 1.0.0': { | |
359 'shared-x': '1.0.0' | |
360 }, | |
361 'bar 1.0.0': { | |
362 'shared-y': '1.0.0' | |
363 }, | |
364 'shared-x 1.0.0': {}, | |
365 'shared-y 1.0.0': {} | |
366 }, error: descriptionMismatch('foo', 'bar')); | |
367 | |
368 testResolve('mismatched sources', { | |
369 'myapp 0.0.0': { | |
370 'foo': '1.0.0', | |
371 'bar': '1.0.0' | |
372 }, | |
373 'foo 1.0.0': { | |
374 'shared': '1.0.0' | |
375 }, | |
376 'bar 1.0.0': { | |
377 'shared from mock2': '1.0.0' | |
378 }, | |
379 'shared 1.0.0': {}, | |
380 'shared 1.0.0 from mock2': {} | |
381 }, error: sourceMismatch('foo', 'bar')); | |
382 | |
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', { | |
405 'myapp 0.0.0': { | |
406 'a': '>=1.0.0' | |
407 }, | |
408 'a 1.0.0': {}, | |
409 'a 2.0.0': { | |
410 'b': '1.0.0' | |
411 }, | |
412 'b 1.0.0': { | |
413 'a': '1.0.0' | |
414 } | |
415 }, result: { | |
416 'myapp from root': '0.0.0', | |
417 'a': '1.0.0' | |
418 }, maxTries: 2); | |
419 | |
420 // The latest versions of a and b disagree on c. An older version of either | |
421 // will resolve the problem. This test validates that b, which is farther | |
422 // in the dependency graph from myapp is downgraded first. | |
423 testResolve('rolls back leaf versions first', { | |
424 'myapp 0.0.0': { | |
425 'a': 'any' | |
426 }, | |
427 'a 1.0.0': { | |
428 'b': 'any' | |
429 }, | |
430 'a 2.0.0': { | |
431 'b': 'any', | |
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); | |
446 | |
447 // Only one version of baz, so foo and bar will have to downgrade until they | |
448 // reach it. | |
449 testResolve('simple transitive', { | |
450 'myapp 0.0.0': {'foo': 'any'}, | |
451 'foo 1.0.0': {'bar': '1.0.0'}, | |
452 'foo 2.0.0': {'bar': '2.0.0'}, | |
453 'foo 3.0.0': {'bar': '3.0.0'}, | |
454 'bar 1.0.0': {'baz': 'any'}, | |
455 'bar 2.0.0': {'baz': '2.0.0'}, | |
456 'bar 3.0.0': {'baz': '3.0.0'}, | |
457 'baz 1.0.0': {} | |
458 }, result: { | |
459 'myapp from root': '0.0.0', | |
460 'foo': '1.0.0', | |
461 'bar': '1.0.0', | |
462 'baz': '1.0.0' | |
463 }, maxTries: 3); | |
464 | |
465 // This ensures it doesn't exhaustively search all versions of b when it's | |
466 // a-2.0.0 whose dependency on c-2.0.0-nonexistent led to the problem. We | |
467 // make sure b has more versions than a so that the solver tries a first | |
468 // since it sorts sibling dependencies by number of versions. | |
469 testResolve('backjump to nearer unsatisfied package', { | |
470 'myapp 0.0.0': { | |
471 'a': 'any', | |
472 'b': 'any' | |
473 }, | |
474 'a 1.0.0': { 'c': '1.0.0' }, | |
475 'a 2.0.0': { 'c': '2.0.0-nonexistent' }, | |
476 'b 1.0.0': {}, | |
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. | |
547 } | |
548 | |
549 sdkConstraint() { | |
550 var badVersion = '0.0.0-nope'; | |
551 var goodVersion = sdk.version.toString(); | |
552 | |
553 testResolve('root matches SDK', { | |
554 'myapp 0.0.0': {'sdk': goodVersion } | |
555 }, result: { | |
556 'myapp from root': '0.0.0' | |
557 }); | |
558 | |
559 testResolve('root does not match SDK', { | |
560 'myapp 0.0.0': {'sdk': badVersion } | |
561 }, error: couldNotSolve); | |
562 | |
563 testResolve('dependency does not match SDK', { | |
564 'myapp 0.0.0': {'foo': 'any'}, | |
565 'foo 0.0.0': {'sdk': badVersion } | |
566 }, error: couldNotSolve); | |
567 | |
568 testResolve('transitive dependency does not match SDK', { | |
569 'myapp 0.0.0': {'foo': 'any'}, | |
570 'foo 0.0.0': {'bar': 'any'}, | |
571 'bar 0.0.0': {'sdk': badVersion } | |
572 }, error: couldNotSolve); | |
573 | |
574 testResolve('selects a dependency version that allows the SDK', { | |
575 'myapp 0.0.0': {'foo': 'any'}, | |
576 'foo 1.0.0': {'sdk': goodVersion }, | |
577 'foo 2.0.0': {'sdk': goodVersion }, | |
578 'foo 3.0.0': {'sdk': badVersion }, | |
579 'foo 4.0.0': {'sdk': badVersion } | |
580 }, result: { | |
581 'myapp from root': '0.0.0', | |
582 'foo': '2.0.0' | |
583 }, maxTries: 3); | |
584 | |
585 testResolve('selects a transitive dependency version that allows the SDK', { | |
586 'myapp 0.0.0': {'foo': 'any'}, | |
587 'foo 1.0.0': {'bar': 'any'}, | |
588 'bar 1.0.0': {'sdk': goodVersion }, | |
589 'bar 2.0.0': {'sdk': goodVersion }, | |
590 'bar 3.0.0': {'sdk': badVersion }, | |
591 'bar 4.0.0': {'sdk': badVersion } | |
592 }, result: { | |
593 'myapp from root': '0.0.0', | |
594 'foo': '1.0.0', | |
595 'bar': '2.0.0' | |
596 }, maxTries: 3); | |
597 | |
598 testResolve('selects a dependency version that allows a transitive ' | |
599 'dependency that allows the SDK', { | |
600 'myapp 0.0.0': {'foo': 'any'}, | |
601 'foo 1.0.0': {'bar': '1.0.0'}, | |
602 'foo 2.0.0': {'bar': '2.0.0'}, | |
603 'foo 3.0.0': {'bar': '3.0.0'}, | |
604 'foo 4.0.0': {'bar': '4.0.0'}, | |
605 'bar 1.0.0': {'sdk': goodVersion }, | |
606 'bar 2.0.0': {'sdk': goodVersion }, | |
607 'bar 3.0.0': {'sdk': badVersion }, | |
608 'bar 4.0.0': {'sdk': badVersion } | |
609 }, result: { | |
610 'myapp from root': '0.0.0', | |
611 'foo': '2.0.0', | |
612 'bar': '2.0.0' | |
613 }, maxTries: 3); | |
614 | |
615 testResolve('ignores SDK constraints on bleeding edge', { | |
616 'myapp 0.0.0': {'sdk': badVersion } | |
617 }, result: { | |
618 'myapp from root': '0.0.0' | |
619 }, useBleedingEdgeSdkVersion: true); | |
620 } | |
621 | |
622 testResolve(description, packages, | |
623 {lockfile, result, FailMatcherBuilder error, int maxTries, | |
624 bool useBleedingEdgeSdkVersion}) { | |
625 if (maxTries == null) maxTries = 1; | |
626 if (useBleedingEdgeSdkVersion == null) useBleedingEdgeSdkVersion = false; | |
627 | |
628 test(description, () { | |
629 var cache = new SystemCache('.'); | |
630 source1 = new MockSource('mock1'); | |
631 source2 = new MockSource('mock2'); | |
632 cache.register(source1); | |
633 cache.register(source2); | |
634 cache.sources.setDefault(source1.name); | |
635 | |
636 // Build the test package graph. | |
637 var root; | |
638 packages.forEach((nameVersion, dependencies) { | |
639 var parsed = parseSource(nameVersion, (isDev, nameVersion, source) { | |
640 var parts = nameVersion.split(' '); | |
641 var name = parts[0]; | |
642 var version = parts[1]; | |
643 | |
644 var package = mockPackage(name, version, dependencies); | |
645 if (name == 'myapp') { | |
646 // Don't add the root package to the server, so we can verify that Pub | |
647 // doesn't try to look up information about the local package on the | |
648 // remote server. | |
649 root = package; | |
650 } else { | |
651 source.addPackage(name, package); | |
652 } | |
653 }); | |
654 }); | |
655 | |
656 // Clean up the expectation. | |
657 if (result != null) { | |
658 var newResult = {}; | |
659 result.forEach((name, version) { | |
660 parseSource(name, (isDev, name, source) { | |
661 version = new Version.parse(version); | |
662 newResult[name] = new PackageId(name, source, version, name); | |
663 }); | |
664 }); | |
665 result = newResult; | |
666 } | |
667 | |
668 var realLockFile = new LockFile.empty(); | |
669 if (lockfile != null) { | |
670 lockfile.forEach((name, version) { | |
671 version = new Version.parse(version); | |
672 realLockFile.packages[name] = | |
673 new PackageId(name, source1, version, name); | |
674 }); | |
675 } | |
676 | |
677 // Make a version number like the continuous build's version. | |
678 var previousVersion = sdk.version; | |
679 if (useBleedingEdgeSdkVersion) { | |
680 sdk.version = new Version(0, 1, 2, build: '0_r12345_juser'); | |
681 } | |
682 | |
683 // Resolve the versions. | |
684 var future = resolveVersions(cache.sources, root, | |
685 lockFile: realLockFile); | |
686 | |
687 var matcher; | |
688 if (result != null) { | |
689 matcher = new SolveSuccessMatcher(result, maxTries); | |
690 } else if (error != null) { | |
691 matcher = error(maxTries); | |
692 } | |
693 | |
694 future = future.whenComplete(() { | |
695 if (useBleedingEdgeSdkVersion) { | |
696 sdk.version = previousVersion; | |
697 } | |
698 }); | |
699 | |
700 expect(future, completion(matcher)); | |
701 }); | |
702 } | |
703 | |
704 typedef SolveFailMatcher FailMatcherBuilder(int maxTries); | |
705 | |
706 FailMatcherBuilder noVersion(List<String> packages) { | |
707 return (maxTries) => new SolveFailMatcher(packages, maxTries, | |
708 NoVersionException); | |
709 } | |
710 | |
711 FailMatcherBuilder disjointConstraint(List<String> packages) { | |
712 return (maxTries) => new SolveFailMatcher(packages, maxTries, | |
713 DisjointConstraintException); | |
714 } | |
715 | |
716 FailMatcherBuilder descriptionMismatch(String package1, String package2) { | |
717 return (maxTries) => new SolveFailMatcher([package1, package2], maxTries, | |
718 DescriptionMismatchException); | |
719 } | |
720 | |
721 // If no solution can be found, the solver just reports the last failure that | |
722 // happened during propagation. Since we don't specify the order that solutions | |
723 // are tried, this just validates that *some* failure occurred, but not which. | |
724 SolveFailMatcher couldNotSolve(maxTries) => | |
725 new SolveFailMatcher([], maxTries, null); | |
726 | |
727 FailMatcherBuilder sourceMismatch(String package1, String package2) { | |
728 return (maxTries) => new SolveFailMatcher([package1, package2], maxTries, | |
729 SourceMismatchException); | |
730 } | |
731 | |
732 class SolveSuccessMatcher implements Matcher { | |
733 /// The expected concrete package selections. | |
734 final Map<String, PackageId> _expected; | |
735 | |
736 /// The maximum number of attempts that should have been tried before finding | |
737 /// the solution. | |
738 final int _maxTries; | |
739 | |
740 SolveSuccessMatcher(this._expected, this._maxTries); | |
741 | |
742 Description describe(Description description) { | |
743 return description.add( | |
744 'Solver to use at most $_maxTries attempts to find:\n' | |
745 '${_listPackages(_expected.values)}'); | |
746 } | |
747 | |
748 Description describeMismatch(SolveResult result, | |
749 Description description, | |
750 MatchState state, bool verbose) { | |
751 if (!result.succeeded) { | |
752 description.add('Solver failed with:\n${result.error}'); | |
753 return; | |
754 } | |
755 | |
756 description.add('Resolved:\n${_listPackages(result.packages)}\n'); | |
757 description.add(state.state); | |
758 return description; | |
759 } | |
760 | |
761 bool matches(SolveResult result, MatchState state) { | |
762 if (!result.succeeded) return false; | |
763 | |
764 var expected = new Map.from(_expected); | |
765 var failures = new StringBuffer(); | |
766 | |
767 for (var id in result.packages) { | |
768 if (!expected.containsKey(id.name)) { | |
769 failures.writeln('Should not have selected $id'); | |
770 } else { | |
771 var expectedId = expected.remove(id.name); | |
772 if (id != expectedId) { | |
773 failures.writeln('Expected $expectedId, not $id'); | |
774 } | |
775 } | |
776 } | |
777 | |
778 if (!expected.isEmpty) { | |
779 failures.writeln('Missing:\n${_listPackages(expected.values)}'); | |
780 } | |
781 | |
782 // Allow 1 here because the greedy solver will only make one attempt. | |
783 if (result.attemptedSolutions != 1 && | |
784 result.attemptedSolutions != _maxTries) { | |
785 failures.writeln('Took ${result.attemptedSolutions} attempts'); | |
786 } | |
787 | |
788 if (!failures.isEmpty) { | |
789 state.state = failures.toString(); | |
790 return false; | |
791 } | |
792 | |
793 return true; | |
794 } | |
795 | |
796 String _listPackages(Iterable<PackageId> packages) { | |
797 return '- ${packages.join('\n- ')}'; | |
798 } | |
799 } | |
800 | |
801 class SolveFailMatcher implements Matcher { | |
802 /// The strings that should appear in the resulting error message. | |
803 // TODO(rnystrom): This seems to always be package names. Make that explicit. | |
804 final Iterable<String> _expected; | |
805 | |
806 /// The maximum number of attempts that should be tried before failing. | |
807 final int _maxTries; | |
808 | |
809 /// The concrete error type that should be found, or `null` if any | |
810 /// [SolveFailure] is allowed. | |
811 final Type _expectedType; | |
812 | |
813 SolveFailMatcher(this._expected, this._maxTries, this._expectedType); | |
814 | |
815 Description describe(Description description) { | |
816 description.add('Solver should fail after at most $_maxTries attempts.'); | |
817 if (!_expected.isEmpty) { | |
818 var textList = _expected.map((s) => '"$s"').join(", "); | |
819 description.add(' The error should contain $textList.'); | |
820 } | |
821 return description; | |
822 } | |
823 | |
824 Description describeMismatch(SolveResult result, | |
825 Description description, | |
826 MatchState state, bool verbose) { | |
827 description.add(state.state); | |
828 return description; | |
829 } | |
830 | |
831 bool matches(SolveResult result, MatchState state) { | |
832 var failures = new StringBuffer(); | |
833 | |
834 if (result.succeeded) { | |
835 failures.writeln('Solver succeeded'); | |
836 } else { | |
837 if (_expectedType != null && result.error.runtimeType != _expectedType) { | |
838 failures.writeln('Should have error type $_expectedType, got ' | |
839 '${result.error.runtimeType}'); | |
840 } | |
841 | |
842 var message = result.error.toString(); | |
843 for (var expected in _expected) { | |
844 if (!message.contains(expected)) { | |
845 failures.writeln( | |
846 'Expected error to contain "$expected", got:\n$message'); | |
847 } | |
848 } | |
849 | |
850 // Allow 1 here because the greedy solver will only make one attempt. | |
851 if (result.attemptedSolutions != 1 && | |
852 result.attemptedSolutions != _maxTries) { | |
853 failures.writeln('Took ${result.attemptedSolutions} attempts'); | |
854 } | |
855 } | |
856 | |
857 if (!failures.isEmpty) { | |
858 state.state = failures.toString(); | |
859 return false; | |
860 } | |
861 | |
862 return true; | |
863 } | |
864 } | |
865 | |
866 /// A source used for testing. This both creates mock package objects and acts | |
867 /// as a source for them. | |
868 /// | |
869 /// In order to support testing packages that have the same name but different | |
870 /// descriptions, a package's name is calculated by taking the description | |
871 /// string and stripping off any trailing hyphen followed by non-hyphen | |
872 /// characters. | |
873 class MockSource extends Source { | |
874 final _packages = <String, Map<Version, Package>>{}; | |
875 | |
876 /// Keeps track of which package version lists have been requested. Ensures | |
877 /// that a source is only hit once for a given package and that pub | |
878 /// internally caches the results. | |
879 final _requestedVersions = new Set<String>(); | |
880 | |
881 /// Keeps track of which package pubspecs have been requested. Ensures that a | |
882 /// source is only hit once for a given package and that pub internally | |
883 /// caches the results. | |
884 final _requestedPubspecs = new Map<String, Set<Version>>(); | |
885 | |
886 final String name; | |
887 bool get shouldCache => true; | |
888 | |
889 MockSource(this.name); | |
890 | |
891 Future<String> systemCacheDirectory(PackageId id) { | |
892 return new Future.value('${id.name}-${id.version}'); | |
893 } | |
894 | |
895 Future<List<Version>> getVersions(String name, String description) { | |
896 return new Future.sync(() { | |
897 // Make sure the solver doesn't request the same thing twice. | |
898 if (_requestedVersions.contains(description)) { | |
899 throw new Exception('Version list for $description was already ' | |
900 'requested.'); | |
901 } | |
902 | |
903 _requestedVersions.add(description); | |
904 | |
905 if (!_packages.containsKey(description)){ | |
906 throw new Exception('MockSource does not have a package matching ' | |
907 '"$description".'); | |
908 } | |
909 return _packages[description].keys.toList(); | |
910 }); | |
911 } | |
912 | |
913 Future<Pubspec> describe(PackageId id) { | |
914 return new Future.sync(() { | |
915 // Make sure the solver doesn't request the same thing twice. | |
916 if (_requestedPubspecs.containsKey(id.description) && | |
917 _requestedPubspecs[id.description].contains(id.version)) { | |
918 throw new Exception('Pubspec for $id was already requested.'); | |
919 } | |
920 | |
921 _requestedPubspecs.putIfAbsent(id.description, () => new Set<Version>()); | |
922 _requestedPubspecs[id.description].add(id.version); | |
923 | |
924 return _packages[id.description][id.version].pubspec; | |
925 }); | |
926 } | |
927 | |
928 Future<bool> install(PackageId id, String path) { | |
929 throw new Exception('no'); | |
930 } | |
931 | |
932 void addPackage(String description, Package package) { | |
933 _packages.putIfAbsent(description, () => new Map<Version, Package>()); | |
934 _packages[description][package.version] = package; | |
935 } | |
936 } | |
937 | |
938 Package mockPackage(String description, String version, | |
939 Map dependencyStrings) { | |
940 var sdkConstraint = null; | |
941 | |
942 // Build the pubspec dependencies. | |
943 var dependencies = <PackageRef>[]; | |
944 var devDependencies = <PackageRef>[]; | |
945 | |
946 dependencyStrings.forEach((name, constraint) { | |
947 parseSource(name, (isDev, name, source) { | |
948 var packageName = name.replaceFirst(new RegExp(r"-[^-]+$"), ""); | |
949 constraint = new VersionConstraint.parse(constraint); | |
950 | |
951 if (name == 'sdk') { | |
952 sdkConstraint = constraint; | |
953 return; | |
954 } | |
955 | |
956 var ref = new PackageRef(packageName, source, constraint, name); | |
957 | |
958 if (isDev) { | |
959 devDependencies.add(ref); | |
960 } else { | |
961 dependencies.add(ref); | |
962 } | |
963 }); | |
964 }); | |
965 | |
966 var name = description.replaceFirst(new RegExp(r"-[^-]+$"), ""); | |
967 var pubspec = new Pubspec( | |
968 name, new Version.parse(version), dependencies, devDependencies, | |
969 new PubspecEnvironment(sdkConstraint)); | |
970 return new Package.inMemory(pubspec); | |
971 } | |
972 | |
973 void parseSource(String description, | |
974 callback(bool isDev, String name, Source source)) { | |
975 var isDev = false; | |
976 | |
977 if (description.startsWith("(dev) ")) { | |
978 description = description.substring("(dev) ".length); | |
979 isDev = true; | |
980 } | |
981 | |
982 var name = description; | |
983 var source = source1; | |
984 | |
985 var sourceNames = { | |
986 'mock1': source1, | |
987 'mock2': source2, | |
988 'root': null | |
989 }; | |
990 | |
991 var match = new RegExp(r"(.*) from (.*)").firstMatch(description); | |
992 if (match != null) { | |
993 name = match[1]; | |
994 source = sourceNames[match[2]]; | |
995 } | |
996 | |
997 callback(isDev, name, source); | |
998 } | |
OLD | NEW |