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_upgrade_test; | |
6 | |
7 import 'dart:async'; | |
8 | |
9 import 'package:pub_semver/pub_semver.dart'; | |
10 import 'package:unittest/unittest.dart'; | |
11 | |
12 import '../lib/src/lock_file.dart'; | |
13 import '../lib/src/log.dart' as log; | |
14 import '../lib/src/package.dart'; | |
15 import '../lib/src/pubspec.dart'; | |
16 import '../lib/src/sdk.dart' as sdk; | |
17 import '../lib/src/source/cached.dart'; | |
18 import '../lib/src/system_cache.dart'; | |
19 import '../lib/src/utils.dart'; | |
20 import '../lib/src/solver/version_solver.dart'; | |
21 import 'test_pub.dart'; | |
22 | |
23 MockSource source1; | |
24 MockSource source2; | |
25 | |
26 main() { | |
27 initConfig(); | |
28 | |
29 // Uncomment this to debug failing tests. | |
30 // log.verbosity = log.Verbosity.SOLVER; | |
31 | |
32 // Since this test isn't run from the SDK, it can't find the "version" file | |
33 // to load. Instead, just manually inject a version. | |
34 sdk.version = new Version(1, 2, 3); | |
35 | |
36 group('basic graph', basicGraph); | |
37 group('with lockfile', withLockFile); | |
38 group('root dependency', rootDependency); | |
39 group('dev dependency', devDependency); | |
40 group('unsolvable', unsolvable); | |
41 group('bad source', badSource); | |
42 group('backtracking', backtracking); | |
43 group('SDK constraint', sdkConstraint); | |
44 group('pre-release', prerelease); | |
45 group('override', override); | |
46 group('downgrade', downgrade); | |
47 } | |
48 | |
49 void basicGraph() { | |
50 testResolve('no dependencies', { | |
51 'myapp 0.0.0': {} | |
52 }, result: { | |
53 'myapp from root': '0.0.0' | |
54 }); | |
55 | |
56 testResolve('simple dependency tree', { | |
57 'myapp 0.0.0': { | |
58 'a': '1.0.0', | |
59 'b': '1.0.0' | |
60 }, | |
61 'a 1.0.0': { | |
62 'aa': '1.0.0', | |
63 'ab': '1.0.0' | |
64 }, | |
65 'aa 1.0.0': {}, | |
66 'ab 1.0.0': {}, | |
67 'b 1.0.0': { | |
68 'ba': '1.0.0', | |
69 'bb': '1.0.0' | |
70 }, | |
71 'ba 1.0.0': {}, | |
72 'bb 1.0.0': {} | |
73 }, result: { | |
74 'myapp from root': '0.0.0', | |
75 'a': '1.0.0', | |
76 'aa': '1.0.0', | |
77 'ab': '1.0.0', | |
78 'b': '1.0.0', | |
79 'ba': '1.0.0', | |
80 'bb': '1.0.0' | |
81 }); | |
82 | |
83 testResolve('shared dependency with overlapping constraints', { | |
84 'myapp 0.0.0': { | |
85 'a': '1.0.0', | |
86 'b': '1.0.0' | |
87 }, | |
88 'a 1.0.0': { | |
89 'shared': '>=2.0.0 <4.0.0' | |
90 }, | |
91 'b 1.0.0': { | |
92 'shared': '>=3.0.0 <5.0.0' | |
93 }, | |
94 'shared 2.0.0': {}, | |
95 'shared 3.0.0': {}, | |
96 'shared 3.6.9': {}, | |
97 'shared 4.0.0': {}, | |
98 'shared 5.0.0': {}, | |
99 }, result: { | |
100 'myapp from root': '0.0.0', | |
101 'a': '1.0.0', | |
102 'b': '1.0.0', | |
103 'shared': '3.6.9' | |
104 }); | |
105 | |
106 testResolve( | |
107 'shared dependency where dependent version in turn affects ' | |
108 'other dependencies', | |
109 { | |
110 'myapp 0.0.0': { | |
111 'foo': '<=1.0.2', | |
112 'bar': '1.0.0' | |
113 }, | |
114 'foo 1.0.0': {}, | |
115 'foo 1.0.1': { | |
116 'bang': '1.0.0' | |
117 }, | |
118 'foo 1.0.2': { | |
119 'whoop': '1.0.0' | |
120 }, | |
121 'foo 1.0.3': { | |
122 'zoop': '1.0.0' | |
123 }, | |
124 'bar 1.0.0': { | |
125 'foo': '<=1.0.1' | |
126 }, | |
127 'bang 1.0.0': {}, | |
128 'whoop 1.0.0': {}, | |
129 'zoop 1.0.0': {} | |
130 }, result: { | |
131 'myapp from root': '0.0.0', | |
132 'foo': '1.0.1', | |
133 'bar': '1.0.0', | |
134 'bang': '1.0.0' | |
135 }, maxTries: 2); | |
136 | |
137 testResolve('circular dependency', { | |
138 'myapp 1.0.0': { | |
139 'foo': '1.0.0' | |
140 }, | |
141 'foo 1.0.0': { | |
142 'bar': '1.0.0' | |
143 }, | |
144 'bar 1.0.0': { | |
145 'foo': '1.0.0' | |
146 } | |
147 }, result: { | |
148 'myapp from root': '1.0.0', | |
149 'foo': '1.0.0', | |
150 'bar': '1.0.0' | |
151 }); | |
152 } | |
153 | |
154 withLockFile() { | |
155 testResolve('with compatible locked dependency', { | |
156 'myapp 0.0.0': { | |
157 'foo': 'any' | |
158 }, | |
159 'foo 1.0.0': { | |
160 'bar': '1.0.0' | |
161 }, | |
162 'foo 1.0.1': { | |
163 'bar': '1.0.1' | |
164 }, | |
165 'foo 1.0.2': { | |
166 'bar': '1.0.2' | |
167 }, | |
168 'bar 1.0.0': {}, | |
169 'bar 1.0.1': {}, | |
170 'bar 1.0.2': {} | |
171 }, lockfile: { | |
172 'foo': '1.0.1' | |
173 }, result: { | |
174 'myapp from root': '0.0.0', | |
175 'foo': '1.0.1', | |
176 'bar': '1.0.1' | |
177 }); | |
178 | |
179 testResolve('with incompatible locked dependency', { | |
180 'myapp 0.0.0': { | |
181 'foo': '>1.0.1' | |
182 }, | |
183 'foo 1.0.0': { | |
184 'bar': '1.0.0' | |
185 }, | |
186 'foo 1.0.1': { | |
187 'bar': '1.0.1' | |
188 }, | |
189 'foo 1.0.2': { | |
190 'bar': '1.0.2' | |
191 }, | |
192 'bar 1.0.0': {}, | |
193 'bar 1.0.1': {}, | |
194 'bar 1.0.2': {} | |
195 }, lockfile: { | |
196 'foo': '1.0.1' | |
197 }, result: { | |
198 'myapp from root': '0.0.0', | |
199 'foo': '1.0.2', | |
200 'bar': '1.0.2' | |
201 }); | |
202 | |
203 testResolve('with unrelated locked dependency', { | |
204 'myapp 0.0.0': { | |
205 'foo': 'any' | |
206 }, | |
207 'foo 1.0.0': { | |
208 'bar': '1.0.0' | |
209 }, | |
210 'foo 1.0.1': { | |
211 'bar': '1.0.1' | |
212 }, | |
213 'foo 1.0.2': { | |
214 'bar': '1.0.2' | |
215 }, | |
216 'bar 1.0.0': {}, | |
217 'bar 1.0.1': {}, | |
218 'bar 1.0.2': {}, | |
219 'baz 1.0.0': {} | |
220 }, lockfile: { | |
221 'baz': '1.0.0' | |
222 }, result: { | |
223 'myapp from root': '0.0.0', | |
224 'foo': '1.0.2', | |
225 'bar': '1.0.2' | |
226 }); | |
227 | |
228 testResolve( | |
229 'unlocks dependencies if necessary to ensure that a new ' | |
230 'dependency is satisfied', | |
231 { | |
232 'myapp 0.0.0': { | |
233 'foo': 'any', | |
234 'newdep': 'any' | |
235 }, | |
236 'foo 1.0.0': { | |
237 'bar': '<2.0.0' | |
238 }, | |
239 'bar 1.0.0': { | |
240 'baz': '<2.0.0' | |
241 }, | |
242 'baz 1.0.0': { | |
243 'qux': '<2.0.0' | |
244 }, | |
245 'qux 1.0.0': {}, | |
246 'foo 2.0.0': { | |
247 'bar': '<3.0.0' | |
248 }, | |
249 'bar 2.0.0': { | |
250 'baz': '<3.0.0' | |
251 }, | |
252 'baz 2.0.0': { | |
253 'qux': '<3.0.0' | |
254 }, | |
255 'qux 2.0.0': {}, | |
256 'newdep 2.0.0': { | |
257 'baz': '>=1.5.0' | |
258 } | |
259 }, lockfile: { | |
260 'foo': '1.0.0', | |
261 'bar': '1.0.0', | |
262 'baz': '1.0.0', | |
263 'qux': '1.0.0' | |
264 }, result: { | |
265 'myapp from root': '0.0.0', | |
266 'foo': '2.0.0', | |
267 'bar': '2.0.0', | |
268 'baz': '2.0.0', | |
269 'qux': '1.0.0', | |
270 'newdep': '2.0.0' | |
271 }, maxTries: 3); | |
272 } | |
273 | |
274 rootDependency() { | |
275 testResolve('with root source', { | |
276 'myapp 1.0.0': { | |
277 'foo': '1.0.0' | |
278 }, | |
279 'foo 1.0.0': { | |
280 'myapp from root': '>=1.0.0' | |
281 } | |
282 }, result: { | |
283 'myapp from root': '1.0.0', | |
284 'foo': '1.0.0' | |
285 }); | |
286 | |
287 testResolve('with different source', { | |
288 'myapp 1.0.0': { | |
289 'foo': '1.0.0' | |
290 }, | |
291 'foo 1.0.0': { | |
292 'myapp': '>=1.0.0' | |
293 } | |
294 }, result: { | |
295 'myapp from root': '1.0.0', | |
296 'foo': '1.0.0' | |
297 }); | |
298 | |
299 testResolve('with mismatched sources', { | |
300 'myapp 1.0.0': { | |
301 'foo': '1.0.0', | |
302 'bar': '1.0.0' | |
303 }, | |
304 'foo 1.0.0': { | |
305 'myapp': '>=1.0.0' | |
306 }, | |
307 'bar 1.0.0': { | |
308 'myapp from mock2': '>=1.0.0' | |
309 } | |
310 }, error: sourceMismatch('myapp', 'foo', 'bar')); | |
311 | |
312 testResolve('with wrong version', { | |
313 'myapp 1.0.0': { | |
314 'foo': '1.0.0' | |
315 }, | |
316 'foo 1.0.0': { | |
317 'myapp': '<1.0.0' | |
318 } | |
319 }, error: couldNotSolve); | |
320 } | |
321 | |
322 devDependency() { | |
323 testResolve("includes root package's dev dependencies", { | |
324 'myapp 1.0.0': { | |
325 '(dev) foo': '1.0.0', | |
326 '(dev) bar': '1.0.0' | |
327 }, | |
328 'foo 1.0.0': {}, | |
329 'bar 1.0.0': {} | |
330 }, result: { | |
331 'myapp from root': '1.0.0', | |
332 'foo': '1.0.0', | |
333 'bar': '1.0.0' | |
334 }); | |
335 | |
336 testResolve("includes dev dependency's transitive dependencies", { | |
337 'myapp 1.0.0': { | |
338 '(dev) foo': '1.0.0' | |
339 }, | |
340 'foo 1.0.0': { | |
341 'bar': '1.0.0' | |
342 }, | |
343 'bar 1.0.0': {} | |
344 }, result: { | |
345 'myapp from root': '1.0.0', | |
346 'foo': '1.0.0', | |
347 'bar': '1.0.0' | |
348 }); | |
349 | |
350 testResolve("ignores transitive dependency's dev dependencies", { | |
351 'myapp 1.0.0': { | |
352 'foo': '1.0.0' | |
353 }, | |
354 'foo 1.0.0': { | |
355 '(dev) bar': '1.0.0' | |
356 }, | |
357 'bar 1.0.0': {} | |
358 }, result: { | |
359 'myapp from root': '1.0.0', | |
360 'foo': '1.0.0' | |
361 }); | |
362 } | |
363 | |
364 unsolvable() { | |
365 testResolve('no version that matches requirement', { | |
366 'myapp 0.0.0': { | |
367 'foo': '>=1.0.0 <2.0.0' | |
368 }, | |
369 'foo 2.0.0': {}, | |
370 'foo 2.1.3': {} | |
371 }, error: noVersion(['myapp', 'foo'])); | |
372 | |
373 testResolve('no version that matches combined constraint', { | |
374 'myapp 0.0.0': { | |
375 'foo': '1.0.0', | |
376 'bar': '1.0.0' | |
377 }, | |
378 'foo 1.0.0': { | |
379 'shared': '>=2.0.0 <3.0.0' | |
380 }, | |
381 'bar 1.0.0': { | |
382 'shared': '>=2.9.0 <4.0.0' | |
383 }, | |
384 'shared 2.5.0': {}, | |
385 'shared 3.5.0': {} | |
386 }, error: noVersion(['shared', 'foo', 'bar'])); | |
387 | |
388 testResolve('disjoint constraints', { | |
389 'myapp 0.0.0': { | |
390 'foo': '1.0.0', | |
391 'bar': '1.0.0' | |
392 }, | |
393 'foo 1.0.0': { | |
394 'shared': '<=2.0.0' | |
395 }, | |
396 'bar 1.0.0': { | |
397 'shared': '>3.0.0' | |
398 }, | |
399 'shared 2.0.0': {}, | |
400 'shared 4.0.0': {} | |
401 }, error: disjointConstraint(['shared', 'foo', 'bar'])); | |
402 | |
403 testResolve('mismatched descriptions', { | |
404 'myapp 0.0.0': { | |
405 'foo': '1.0.0', | |
406 'bar': '1.0.0' | |
407 }, | |
408 'foo 1.0.0': { | |
409 'shared-x': '1.0.0' | |
410 }, | |
411 'bar 1.0.0': { | |
412 'shared-y': '1.0.0' | |
413 }, | |
414 'shared-x 1.0.0': {}, | |
415 'shared-y 1.0.0': {} | |
416 }, error: descriptionMismatch('shared', 'foo', 'bar')); | |
417 | |
418 testResolve('mismatched sources', { | |
419 'myapp 0.0.0': { | |
420 'foo': '1.0.0', | |
421 'bar': '1.0.0' | |
422 }, | |
423 'foo 1.0.0': { | |
424 'shared': '1.0.0' | |
425 }, | |
426 'bar 1.0.0': { | |
427 'shared from mock2': '1.0.0' | |
428 }, | |
429 'shared 1.0.0': {}, | |
430 'shared 1.0.0 from mock2': {} | |
431 }, error: sourceMismatch('shared', 'foo', 'bar')); | |
432 | |
433 testResolve('no valid solution', { | |
434 'myapp 0.0.0': { | |
435 'a': 'any', | |
436 'b': 'any' | |
437 }, | |
438 'a 1.0.0': { | |
439 'b': '1.0.0' | |
440 }, | |
441 'a 2.0.0': { | |
442 'b': '2.0.0' | |
443 }, | |
444 'b 1.0.0': { | |
445 'a': '2.0.0' | |
446 }, | |
447 'b 2.0.0': { | |
448 'a': '1.0.0' | |
449 } | |
450 }, error: couldNotSolve, maxTries: 4); | |
451 | |
452 // This is a regression test for #15550. | |
453 testResolve('no version that matches while backtracking', { | |
454 'myapp 0.0.0': { | |
455 'a': 'any', | |
456 'b': '>1.0.0' | |
457 }, | |
458 'a 1.0.0': {}, | |
459 'b 1.0.0': {} | |
460 }, error: noVersion(['myapp', 'b']), maxTries: 1); | |
461 | |
462 | |
463 // This is a regression test for #18300. | |
464 testResolve('...', { | |
465 "myapp 0.0.0": { | |
466 "angular": "any", | |
467 "collection": "any" | |
468 }, | |
469 "analyzer 0.12.2": {}, | |
470 "angular 0.10.0": { | |
471 "di": ">=0.0.32 <0.1.0", | |
472 "collection": ">=0.9.1 <1.0.0" | |
473 }, | |
474 "angular 0.9.11": { | |
475 "di": ">=0.0.32 <0.1.0", | |
476 "collection": ">=0.9.1 <1.0.0" | |
477 }, | |
478 "angular 0.9.10": { | |
479 "di": ">=0.0.32 <0.1.0", | |
480 "collection": ">=0.9.1 <1.0.0" | |
481 }, | |
482 "collection 0.9.0": {}, | |
483 "collection 0.9.1": {}, | |
484 "di 0.0.37": { | |
485 "analyzer": ">=0.13.0 <0.14.0" | |
486 }, | |
487 "di 0.0.36": { | |
488 "analyzer": ">=0.13.0 <0.14.0" | |
489 } | |
490 }, error: noVersion(['myapp', 'angular', 'collection']), maxTries: 9); | |
491 } | |
492 | |
493 badSource() { | |
494 testResolve('fail if the root package has a bad source in dep', { | |
495 'myapp 0.0.0': { | |
496 'foo from bad': 'any' | |
497 }, | |
498 }, error: unknownSource('myapp', 'foo', 'bad')); | |
499 | |
500 testResolve('fail if the root package has a bad source in dev dep', { | |
501 'myapp 0.0.0': { | |
502 '(dev) foo from bad': 'any' | |
503 }, | |
504 }, error: unknownSource('myapp', 'foo', 'bad')); | |
505 | |
506 testResolve('fail if all versions have bad source in dep', { | |
507 'myapp 0.0.0': { | |
508 'foo': 'any' | |
509 }, | |
510 'foo 1.0.0': { | |
511 'bar from bad': 'any' | |
512 }, | |
513 'foo 1.0.1': { | |
514 'baz from bad': 'any' | |
515 }, | |
516 'foo 1.0.3': { | |
517 'bang from bad': 'any' | |
518 }, | |
519 }, error: unknownSource('foo', 'bar', 'bad'), maxTries: 3); | |
520 | |
521 testResolve('ignore versions with bad source in dep', { | |
522 'myapp 1.0.0': { | |
523 'foo': 'any' | |
524 }, | |
525 'foo 1.0.0': { | |
526 'bar': 'any' | |
527 }, | |
528 'foo 1.0.1': { | |
529 'bar from bad': 'any' | |
530 }, | |
531 'foo 1.0.3': { | |
532 'bar from bad': 'any' | |
533 }, | |
534 'bar 1.0.0': {} | |
535 }, result: { | |
536 'myapp from root': '1.0.0', | |
537 'foo': '1.0.0', | |
538 'bar': '1.0.0' | |
539 }, maxTries: 3); | |
540 } | |
541 | |
542 backtracking() { | |
543 testResolve('circular dependency on older version', { | |
544 'myapp 0.0.0': { | |
545 'a': '>=1.0.0' | |
546 }, | |
547 'a 1.0.0': {}, | |
548 'a 2.0.0': { | |
549 'b': '1.0.0' | |
550 }, | |
551 'b 1.0.0': { | |
552 'a': '1.0.0' | |
553 } | |
554 }, result: { | |
555 'myapp from root': '0.0.0', | |
556 'a': '1.0.0' | |
557 }, maxTries: 2); | |
558 | |
559 // The latest versions of a and b disagree on c. An older version of either | |
560 // will resolve the problem. This test validates that b, which is farther | |
561 // in the dependency graph from myapp is downgraded first. | |
562 testResolve('rolls back leaf versions first', { | |
563 'myapp 0.0.0': { | |
564 'a': 'any' | |
565 }, | |
566 'a 1.0.0': { | |
567 'b': 'any' | |
568 }, | |
569 'a 2.0.0': { | |
570 'b': 'any', | |
571 'c': '2.0.0' | |
572 }, | |
573 'b 1.0.0': {}, | |
574 'b 2.0.0': { | |
575 'c': '1.0.0' | |
576 }, | |
577 'c 1.0.0': {}, | |
578 'c 2.0.0': {} | |
579 }, result: { | |
580 'myapp from root': '0.0.0', | |
581 'a': '2.0.0', | |
582 'b': '1.0.0', | |
583 'c': '2.0.0' | |
584 }, maxTries: 2); | |
585 | |
586 // Only one version of baz, so foo and bar will have to downgrade until they | |
587 // reach it. | |
588 testResolve('simple transitive', { | |
589 'myapp 0.0.0': { | |
590 'foo': 'any' | |
591 }, | |
592 'foo 1.0.0': { | |
593 'bar': '1.0.0' | |
594 }, | |
595 'foo 2.0.0': { | |
596 'bar': '2.0.0' | |
597 }, | |
598 'foo 3.0.0': { | |
599 'bar': '3.0.0' | |
600 }, | |
601 'bar 1.0.0': { | |
602 'baz': 'any' | |
603 }, | |
604 'bar 2.0.0': { | |
605 'baz': '2.0.0' | |
606 }, | |
607 'bar 3.0.0': { | |
608 'baz': '3.0.0' | |
609 }, | |
610 'baz 1.0.0': {} | |
611 }, result: { | |
612 'myapp from root': '0.0.0', | |
613 'foo': '1.0.0', | |
614 'bar': '1.0.0', | |
615 'baz': '1.0.0' | |
616 }, maxTries: 3); | |
617 | |
618 // This ensures it doesn't exhaustively search all versions of b when it's | |
619 // a-2.0.0 whose dependency on c-2.0.0-nonexistent led to the problem. We | |
620 // make sure b has more versions than a so that the solver tries a first | |
621 // since it sorts sibling dependencies by number of versions. | |
622 testResolve('backjump to nearer unsatisfied package', { | |
623 'myapp 0.0.0': { | |
624 'a': 'any', | |
625 'b': 'any' | |
626 }, | |
627 'a 1.0.0': { | |
628 'c': '1.0.0' | |
629 }, | |
630 'a 2.0.0': { | |
631 'c': '2.0.0-nonexistent' | |
632 }, | |
633 'b 1.0.0': {}, | |
634 'b 2.0.0': {}, | |
635 'b 3.0.0': {}, | |
636 'c 1.0.0': {}, | |
637 }, result: { | |
638 'myapp from root': '0.0.0', | |
639 'a': '1.0.0', | |
640 'b': '3.0.0', | |
641 'c': '1.0.0' | |
642 }, maxTries: 2); | |
643 | |
644 // Tests that the backjumper will jump past unrelated selections when a | |
645 // source conflict occurs. This test selects, in order: | |
646 // - myapp -> a | |
647 // - myapp -> b | |
648 // - myapp -> c (1 of 5) | |
649 // - b -> a | |
650 // It selects a and b first because they have fewer versions than c. It | |
651 // traverses b's dependency on a after selecting a version of c because | |
652 // dependencies are traversed breadth-first (all of myapps's immediate deps | |
653 // before any other their deps). | |
654 // | |
655 // This means it doesn't discover the source conflict until after selecting | |
656 // c. When that happens, it should backjump past c instead of trying older | |
657 // versions of it since they aren't related to the conflict. | |
658 testResolve('backjump to conflicting source', { | |
659 'myapp 0.0.0': { | |
660 'a': 'any', | |
661 'b': 'any', | |
662 'c': 'any' | |
663 }, | |
664 'a 1.0.0': {}, | |
665 'a 1.0.0 from mock2': {}, | |
666 'b 1.0.0': { | |
667 'a': 'any' | |
668 }, | |
669 'b 2.0.0': { | |
670 'a from mock2': 'any' | |
671 }, | |
672 'c 1.0.0': {}, | |
673 'c 2.0.0': {}, | |
674 'c 3.0.0': {}, | |
675 'c 4.0.0': {}, | |
676 'c 5.0.0': {}, | |
677 }, result: { | |
678 'myapp from root': '0.0.0', | |
679 'a': '1.0.0', | |
680 'b': '1.0.0', | |
681 'c': '5.0.0' | |
682 }, maxTries: 2); | |
683 | |
684 // Like the above test, but for a conflicting description. | |
685 testResolve('backjump to conflicting description', { | |
686 'myapp 0.0.0': { | |
687 'a-x': 'any', | |
688 'b': 'any', | |
689 'c': 'any' | |
690 }, | |
691 'a-x 1.0.0': {}, | |
692 'a-y 1.0.0': {}, | |
693 'b 1.0.0': { | |
694 'a-x': 'any' | |
695 }, | |
696 'b 2.0.0': { | |
697 'a-y': 'any' | |
698 }, | |
699 'c 1.0.0': {}, | |
700 'c 2.0.0': {}, | |
701 'c 3.0.0': {}, | |
702 'c 4.0.0': {}, | |
703 'c 5.0.0': {}, | |
704 }, result: { | |
705 'myapp from root': '0.0.0', | |
706 'a': '1.0.0', | |
707 'b': '1.0.0', | |
708 'c': '5.0.0' | |
709 }, maxTries: 2); | |
710 | |
711 // Similar to the above two tests but where there is no solution. It should | |
712 // fail in this case with no backtracking. | |
713 testResolve('backjump to conflicting source', { | |
714 'myapp 0.0.0': { | |
715 'a': 'any', | |
716 'b': 'any', | |
717 'c': 'any' | |
718 }, | |
719 'a 1.0.0': {}, | |
720 'a 1.0.0 from mock2': {}, | |
721 'b 1.0.0': { | |
722 'a from mock2': 'any' | |
723 }, | |
724 'c 1.0.0': {}, | |
725 'c 2.0.0': {}, | |
726 'c 3.0.0': {}, | |
727 'c 4.0.0': {}, | |
728 'c 5.0.0': {}, | |
729 }, error: sourceMismatch('a', 'myapp', 'b'), maxTries: 1); | |
730 | |
731 testResolve('backjump to conflicting description', { | |
732 'myapp 0.0.0': { | |
733 'a-x': 'any', | |
734 'b': 'any', | |
735 'c': 'any' | |
736 }, | |
737 'a-x 1.0.0': {}, | |
738 'a-y 1.0.0': {}, | |
739 'b 1.0.0': { | |
740 'a-y': 'any' | |
741 }, | |
742 'c 1.0.0': {}, | |
743 'c 2.0.0': {}, | |
744 'c 3.0.0': {}, | |
745 'c 4.0.0': {}, | |
746 'c 5.0.0': {}, | |
747 }, error: descriptionMismatch('a', 'myapp', 'b'), maxTries: 1); | |
748 | |
749 // Dependencies are ordered so that packages with fewer versions are tried | |
750 // first. Here, there are two valid solutions (either a or b must be | |
751 // downgraded once). The chosen one depends on which dep is traversed first. | |
752 // Since b has fewer versions, it will be traversed first, which means a will | |
753 // come later. Since later selections are revised first, a gets downgraded. | |
754 testResolve('traverse into package with fewer versions first', { | |
755 'myapp 0.0.0': { | |
756 'a': 'any', | |
757 'b': 'any' | |
758 }, | |
759 'a 1.0.0': { | |
760 'c': 'any' | |
761 }, | |
762 'a 2.0.0': { | |
763 'c': 'any' | |
764 }, | |
765 'a 3.0.0': { | |
766 'c': 'any' | |
767 }, | |
768 'a 4.0.0': { | |
769 'c': 'any' | |
770 }, | |
771 'a 5.0.0': { | |
772 'c': '1.0.0' | |
773 }, | |
774 'b 1.0.0': { | |
775 'c': 'any' | |
776 }, | |
777 'b 2.0.0': { | |
778 'c': 'any' | |
779 }, | |
780 'b 3.0.0': { | |
781 'c': 'any' | |
782 }, | |
783 'b 4.0.0': { | |
784 'c': '2.0.0' | |
785 }, | |
786 'c 1.0.0': {}, | |
787 'c 2.0.0': {}, | |
788 }, result: { | |
789 'myapp from root': '0.0.0', | |
790 'a': '4.0.0', | |
791 'b': '4.0.0', | |
792 'c': '2.0.0' | |
793 }, maxTries: 2); | |
794 | |
795 // This is similar to the above test. When getting the number of versions of | |
796 // a package to determine which to traverse first, versions that are | |
797 // disallowed by the root package's constraints should not be considered. | |
798 // Here, foo has more versions of bar in total (4), but fewer that meet | |
799 // myapp's constraints (only 2). There is no solution, but we will do less | |
800 // backtracking if foo is tested first. | |
801 testResolve('take root package constraints into counting versions', { | |
802 "myapp 0.0.0": { | |
803 "foo": ">2.0.0", | |
804 "bar": "any" | |
805 }, | |
806 "foo 1.0.0": { | |
807 "none": "2.0.0" | |
808 }, | |
809 "foo 2.0.0": { | |
810 "none": "2.0.0" | |
811 }, | |
812 "foo 3.0.0": { | |
813 "none": "2.0.0" | |
814 }, | |
815 "foo 4.0.0": { | |
816 "none": "2.0.0" | |
817 }, | |
818 "bar 1.0.0": {}, | |
819 "bar 2.0.0": {}, | |
820 "bar 3.0.0": {}, | |
821 "none 1.0.0": {} | |
822 }, error: noVersion(["foo", "none"]), maxTries: 2); | |
823 | |
824 // This sets up a hundred versions of foo and bar, 0.0.0 through 9.9.0. Each | |
825 // version of foo depends on a baz with the same major version. Each version | |
826 // of bar depends on a baz with the same minor version. There is only one | |
827 // version of baz, 0.0.0, so only older versions of foo and bar will | |
828 // satisfy it. | |
829 var map = { | |
830 'myapp 0.0.0': { | |
831 'foo': 'any', | |
832 'bar': 'any' | |
833 }, | |
834 'baz 0.0.0': {} | |
835 }; | |
836 | |
837 for (var i = 0; i < 10; i++) { | |
838 for (var j = 0; j < 10; j++) { | |
839 map['foo $i.$j.0'] = { | |
840 'baz': '$i.0.0' | |
841 }; | |
842 map['bar $i.$j.0'] = { | |
843 'baz': '0.$j.0' | |
844 }; | |
845 } | |
846 } | |
847 | |
848 testResolve('complex backtrack', map, result: { | |
849 'myapp from root': '0.0.0', | |
850 'foo': '0.9.0', | |
851 'bar': '9.0.0', | |
852 'baz': '0.0.0' | |
853 }, maxTries: 100); | |
854 | |
855 // If there's a disjoint constraint on a package, then selecting other | |
856 // versions of it is a waste of time: no possible versions can match. We need | |
857 // to jump past it to the most recent package that affected the constraint. | |
858 testResolve('backjump past failed package on disjoint constraint', { | |
859 'myapp 0.0.0': { | |
860 'a': 'any', | |
861 'foo': '>2.0.0' | |
862 }, | |
863 'a 1.0.0': { | |
864 'foo': 'any' // ok | |
865 }, | |
866 'a 2.0.0': { | |
867 'foo': '<1.0.0' // disjoint with myapp's constraint on foo | |
868 }, | |
869 'foo 2.0.0': {}, | |
870 'foo 2.0.1': {}, | |
871 'foo 2.0.2': {}, | |
872 'foo 2.0.3': {}, | |
873 'foo 2.0.4': {} | |
874 }, result: { | |
875 'myapp from root': '0.0.0', | |
876 'a': '1.0.0', | |
877 'foo': '2.0.4' | |
878 }, maxTries: 2); | |
879 | |
880 // This is a regression test for #18666. It was possible for the solver to | |
881 // "forget" that a package had previously led to an error. In that case, it | |
882 // would backtrack over the failed package instead of trying different | |
883 // versions of it. | |
884 testResolve("finds solution with less strict constraint", { | |
885 "myapp 1.0.0": { | |
886 "a": "any", | |
887 "c": "any", | |
888 "d": "any" | |
889 }, | |
890 "a 2.0.0": {}, | |
891 "a 1.0.0": {}, | |
892 "b 1.0.0": { | |
893 "a": "1.0.0" | |
894 }, | |
895 "c 1.0.0": { | |
896 "b": "any" | |
897 }, | |
898 "d 2.0.0": { | |
899 "myapp": "any" | |
900 }, | |
901 "d 1.0.0": { | |
902 "myapp": "<1.0.0" | |
903 } | |
904 }, result: { | |
905 'myapp from root': '1.0.0', | |
906 'a': '1.0.0', | |
907 'b': '1.0.0', | |
908 'c': '1.0.0', | |
909 'd': '2.0.0' | |
910 }, maxTries: 3); | |
911 } | |
912 | |
913 sdkConstraint() { | |
914 var badVersion = '0.0.0-nope'; | |
915 var goodVersion = sdk.version.toString(); | |
916 | |
917 testResolve('root matches SDK', { | |
918 'myapp 0.0.0': { | |
919 'sdk': goodVersion | |
920 } | |
921 }, result: { | |
922 'myapp from root': '0.0.0' | |
923 }); | |
924 | |
925 testResolve('root does not match SDK', { | |
926 'myapp 0.0.0': { | |
927 'sdk': badVersion | |
928 } | |
929 }, error: couldNotSolve); | |
930 | |
931 testResolve('dependency does not match SDK', { | |
932 'myapp 0.0.0': { | |
933 'foo': 'any' | |
934 }, | |
935 'foo 0.0.0': { | |
936 'sdk': badVersion | |
937 } | |
938 }, error: couldNotSolve); | |
939 | |
940 testResolve('transitive dependency does not match SDK', { | |
941 'myapp 0.0.0': { | |
942 'foo': 'any' | |
943 }, | |
944 'foo 0.0.0': { | |
945 'bar': 'any' | |
946 }, | |
947 'bar 0.0.0': { | |
948 'sdk': badVersion | |
949 } | |
950 }, error: couldNotSolve); | |
951 | |
952 testResolve('selects a dependency version that allows the SDK', { | |
953 'myapp 0.0.0': { | |
954 'foo': 'any' | |
955 }, | |
956 'foo 1.0.0': { | |
957 'sdk': goodVersion | |
958 }, | |
959 'foo 2.0.0': { | |
960 'sdk': goodVersion | |
961 }, | |
962 'foo 3.0.0': { | |
963 'sdk': badVersion | |
964 }, | |
965 'foo 4.0.0': { | |
966 'sdk': badVersion | |
967 } | |
968 }, result: { | |
969 'myapp from root': '0.0.0', | |
970 'foo': '2.0.0' | |
971 }, maxTries: 3); | |
972 | |
973 testResolve('selects a transitive dependency version that allows the SDK', { | |
974 'myapp 0.0.0': { | |
975 'foo': 'any' | |
976 }, | |
977 'foo 1.0.0': { | |
978 'bar': 'any' | |
979 }, | |
980 'bar 1.0.0': { | |
981 'sdk': goodVersion | |
982 }, | |
983 'bar 2.0.0': { | |
984 'sdk': goodVersion | |
985 }, | |
986 'bar 3.0.0': { | |
987 'sdk': badVersion | |
988 }, | |
989 'bar 4.0.0': { | |
990 'sdk': badVersion | |
991 } | |
992 }, result: { | |
993 'myapp from root': '0.0.0', | |
994 'foo': '1.0.0', | |
995 'bar': '2.0.0' | |
996 }, maxTries: 3); | |
997 | |
998 testResolve( | |
999 'selects a dependency version that allows a transitive ' | |
1000 'dependency that allows the SDK', | |
1001 { | |
1002 'myapp 0.0.0': { | |
1003 'foo': 'any' | |
1004 }, | |
1005 'foo 1.0.0': { | |
1006 'bar': '1.0.0' | |
1007 }, | |
1008 'foo 2.0.0': { | |
1009 'bar': '2.0.0' | |
1010 }, | |
1011 'foo 3.0.0': { | |
1012 'bar': '3.0.0' | |
1013 }, | |
1014 'foo 4.0.0': { | |
1015 'bar': '4.0.0' | |
1016 }, | |
1017 'bar 1.0.0': { | |
1018 'sdk': goodVersion | |
1019 }, | |
1020 'bar 2.0.0': { | |
1021 'sdk': goodVersion | |
1022 }, | |
1023 'bar 3.0.0': { | |
1024 'sdk': badVersion | |
1025 }, | |
1026 'bar 4.0.0': { | |
1027 'sdk': badVersion | |
1028 } | |
1029 }, result: { | |
1030 'myapp from root': '0.0.0', | |
1031 'foo': '2.0.0', | |
1032 'bar': '2.0.0' | |
1033 }, maxTries: 3); | |
1034 } | |
1035 | |
1036 void prerelease() { | |
1037 testResolve('prefer stable versions over unstable', { | |
1038 'myapp 0.0.0': { | |
1039 'a': 'any' | |
1040 }, | |
1041 'a 1.0.0': {}, | |
1042 'a 1.1.0-dev': {}, | |
1043 'a 2.0.0-dev': {}, | |
1044 'a 3.0.0-dev': {} | |
1045 }, result: { | |
1046 'myapp from root': '0.0.0', | |
1047 'a': '1.0.0' | |
1048 }); | |
1049 | |
1050 testResolve('use latest allowed prerelease if no stable versions match', { | |
1051 'myapp 0.0.0': { | |
1052 'a': '<2.0.0' | |
1053 }, | |
1054 'a 1.0.0-dev': {}, | |
1055 'a 1.1.0-dev': {}, | |
1056 'a 1.9.0-dev': {}, | |
1057 'a 3.0.0': {} | |
1058 }, result: { | |
1059 'myapp from root': '0.0.0', | |
1060 'a': '1.9.0-dev' | |
1061 }); | |
1062 | |
1063 testResolve('use an earlier stable version on a < constraint', { | |
1064 'myapp 0.0.0': { | |
1065 'a': '<2.0.0' | |
1066 }, | |
1067 'a 1.0.0': {}, | |
1068 'a 1.1.0': {}, | |
1069 'a 2.0.0-dev': {}, | |
1070 'a 2.0.0': {} | |
1071 }, result: { | |
1072 'myapp from root': '0.0.0', | |
1073 'a': '1.1.0' | |
1074 }); | |
1075 | |
1076 testResolve('prefer a stable version even if constraint mentions unstable', { | |
1077 'myapp 0.0.0': { | |
1078 'a': '<=2.0.0-dev' | |
1079 }, | |
1080 'a 1.0.0': {}, | |
1081 'a 1.1.0': {}, | |
1082 'a 2.0.0-dev': {}, | |
1083 'a 2.0.0': {} | |
1084 }, result: { | |
1085 'myapp from root': '0.0.0', | |
1086 'a': '1.1.0' | |
1087 }); | |
1088 } | |
1089 | |
1090 void override() { | |
1091 testResolve('chooses best version matching override constraint', { | |
1092 'myapp 0.0.0': { | |
1093 'a': 'any' | |
1094 }, | |
1095 'a 1.0.0': {}, | |
1096 'a 2.0.0': {}, | |
1097 'a 3.0.0': {} | |
1098 }, overrides: { | |
1099 'a': '<3.0.0' | |
1100 }, result: { | |
1101 'myapp from root': '0.0.0', | |
1102 'a': '2.0.0' | |
1103 }); | |
1104 | |
1105 testResolve('uses override as dependency', { | |
1106 'myapp 0.0.0': {}, | |
1107 'a 1.0.0': {}, | |
1108 'a 2.0.0': {}, | |
1109 'a 3.0.0': {} | |
1110 }, overrides: { | |
1111 'a': '<3.0.0' | |
1112 }, result: { | |
1113 'myapp from root': '0.0.0', | |
1114 'a': '2.0.0' | |
1115 }); | |
1116 | |
1117 testResolve('ignores other constraints on overridden package', { | |
1118 'myapp 0.0.0': { | |
1119 'b': 'any', | |
1120 'c': 'any' | |
1121 }, | |
1122 'a 1.0.0': {}, | |
1123 'a 2.0.0': {}, | |
1124 'a 3.0.0': {}, | |
1125 'b 1.0.0': { | |
1126 'a': '1.0.0' | |
1127 }, | |
1128 'c 1.0.0': { | |
1129 'a': '3.0.0' | |
1130 } | |
1131 }, overrides: { | |
1132 'a': '2.0.0' | |
1133 }, result: { | |
1134 'myapp from root': '0.0.0', | |
1135 'a': '2.0.0', | |
1136 'b': '1.0.0', | |
1137 'c': '1.0.0' | |
1138 }); | |
1139 | |
1140 testResolve('backtracks on overidden package for its constraints', { | |
1141 'myapp 0.0.0': { | |
1142 'shared': '2.0.0' | |
1143 }, | |
1144 'a 1.0.0': { | |
1145 'shared': 'any' | |
1146 }, | |
1147 'a 2.0.0': { | |
1148 'shared': '1.0.0' | |
1149 }, | |
1150 'shared 1.0.0': {}, | |
1151 'shared 2.0.0': {} | |
1152 }, overrides: { | |
1153 'a': '<3.0.0' | |
1154 }, result: { | |
1155 'myapp from root': '0.0.0', | |
1156 'a': '1.0.0', | |
1157 'shared': '2.0.0' | |
1158 }, maxTries: 2); | |
1159 | |
1160 testResolve('override compatible with locked dependency', { | |
1161 'myapp 0.0.0': { | |
1162 'foo': 'any' | |
1163 }, | |
1164 'foo 1.0.0': { | |
1165 'bar': '1.0.0' | |
1166 }, | |
1167 'foo 1.0.1': { | |
1168 'bar': '1.0.1' | |
1169 }, | |
1170 'foo 1.0.2': { | |
1171 'bar': '1.0.2' | |
1172 }, | |
1173 'bar 1.0.0': {}, | |
1174 'bar 1.0.1': {}, | |
1175 'bar 1.0.2': {} | |
1176 }, lockfile: { | |
1177 'foo': '1.0.1' | |
1178 }, overrides: { | |
1179 'foo': '<1.0.2' | |
1180 }, result: { | |
1181 'myapp from root': '0.0.0', | |
1182 'foo': '1.0.1', | |
1183 'bar': '1.0.1' | |
1184 }); | |
1185 | |
1186 testResolve('override incompatible with locked dependency', { | |
1187 'myapp 0.0.0': { | |
1188 'foo': 'any' | |
1189 }, | |
1190 'foo 1.0.0': { | |
1191 'bar': '1.0.0' | |
1192 }, | |
1193 'foo 1.0.1': { | |
1194 'bar': '1.0.1' | |
1195 }, | |
1196 'foo 1.0.2': { | |
1197 'bar': '1.0.2' | |
1198 }, | |
1199 'bar 1.0.0': {}, | |
1200 'bar 1.0.1': {}, | |
1201 'bar 1.0.2': {} | |
1202 }, lockfile: { | |
1203 'foo': '1.0.1' | |
1204 }, overrides: { | |
1205 'foo': '>1.0.1' | |
1206 }, result: { | |
1207 'myapp from root': '0.0.0', | |
1208 'foo': '1.0.2', | |
1209 'bar': '1.0.2' | |
1210 }); | |
1211 | |
1212 testResolve('no version that matches override', { | |
1213 'myapp 0.0.0': {}, | |
1214 'foo 2.0.0': {}, | |
1215 'foo 2.1.3': {} | |
1216 }, overrides: { | |
1217 'foo': '>=1.0.0 <2.0.0' | |
1218 }, error: noVersion(['myapp'])); | |
1219 | |
1220 testResolve('override a bad source without error', { | |
1221 'myapp 0.0.0': { | |
1222 'foo from bad': 'any' | |
1223 }, | |
1224 'foo 0.0.0': {} | |
1225 }, overrides: { | |
1226 'foo': 'any' | |
1227 }, result: { | |
1228 'myapp from root': '0.0.0', | |
1229 'foo': '0.0.0' | |
1230 }); | |
1231 } | |
1232 | |
1233 void downgrade() { | |
1234 testResolve("downgrades a dependency to the lowest matching version", { | |
1235 'myapp 0.0.0': { | |
1236 'foo': '>=2.0.0 <3.0.0' | |
1237 }, | |
1238 'foo 1.0.0': {}, | |
1239 'foo 2.0.0-dev': {}, | |
1240 'foo 2.0.0': {}, | |
1241 'foo 2.1.0': {} | |
1242 }, lockfile: { | |
1243 'foo': '2.1.0' | |
1244 }, result: { | |
1245 'myapp from root': '0.0.0', | |
1246 'foo': '2.0.0' | |
1247 }, downgrade: true); | |
1248 | |
1249 testResolve( | |
1250 'use earliest allowed prerelease if no stable versions match ' | |
1251 'while downgrading', | |
1252 { | |
1253 'myapp 0.0.0': { | |
1254 'a': '>=2.0.0-dev.1 <3.0.0' | |
1255 }, | |
1256 'a 1.0.0': {}, | |
1257 'a 2.0.0-dev.1': {}, | |
1258 'a 2.0.0-dev.2': {}, | |
1259 'a 2.0.0-dev.3': {} | |
1260 }, result: { | |
1261 'myapp from root': '0.0.0', | |
1262 'a': '2.0.0-dev.1' | |
1263 }, downgrade: true); | |
1264 } | |
1265 | |
1266 testResolve(String description, Map packages, {Map lockfile, Map overrides, | |
1267 Map result, FailMatcherBuilder error, int maxTries, bool downgrade: false})
{ | |
1268 _testResolve( | |
1269 test, | |
1270 description, | |
1271 packages, | |
1272 lockfile: lockfile, | |
1273 overrides: overrides, | |
1274 result: result, | |
1275 error: error, | |
1276 maxTries: maxTries, | |
1277 downgrade: downgrade); | |
1278 } | |
1279 | |
1280 solo_testResolve(String description, Map packages, {Map lockfile, Map overrides, | |
1281 Map result, FailMatcherBuilder error, int maxTries, bool downgrade: false})
{ | |
1282 log.verbosity = log.Verbosity.SOLVER; | |
1283 _testResolve( | |
1284 solo_test, | |
1285 description, | |
1286 packages, | |
1287 lockfile: lockfile, | |
1288 overrides: overrides, | |
1289 result: result, | |
1290 error: error, | |
1291 maxTries: maxTries, | |
1292 downgrade: downgrade); | |
1293 } | |
1294 | |
1295 _testResolve(void testFn(String description, Function body), String description, | |
1296 Map packages, {Map lockfile, Map overrides, Map result, | |
1297 FailMatcherBuilder error, int maxTries, bool downgrade: false}) { | |
1298 if (maxTries == null) maxTries = 1; | |
1299 | |
1300 testFn(description, () { | |
1301 var cache = new SystemCache('.'); | |
1302 source1 = new MockSource('mock1'); | |
1303 source2 = new MockSource('mock2'); | |
1304 cache.register(source1); | |
1305 cache.register(source2); | |
1306 cache.sources.setDefault(source1.name); | |
1307 | |
1308 // Build the test package graph. | |
1309 var root; | |
1310 packages.forEach((description, dependencies) { | |
1311 var id = parseSpec(description); | |
1312 var package = | |
1313 mockPackage(id, dependencies, id.name == 'myapp' ? overrides : null); | |
1314 if (id.name == 'myapp') { | |
1315 // Don't add the root package to the server, so we can verify that Pub | |
1316 // doesn't try to look up information about the local package on the | |
1317 // remote server. | |
1318 root = package; | |
1319 } else { | |
1320 (cache.sources[id.source] as MockSource).addPackage( | |
1321 id.description, | |
1322 package); | |
1323 } | |
1324 }); | |
1325 | |
1326 // Clean up the expectation. | |
1327 if (result != null) { | |
1328 var newResult = {}; | |
1329 result.forEach((description, version) { | |
1330 var id = parseSpec(description, version); | |
1331 newResult[id.name] = id; | |
1332 }); | |
1333 result = newResult; | |
1334 } | |
1335 | |
1336 // Parse the lockfile. | |
1337 var realLockFile = new LockFile.empty(); | |
1338 if (lockfile != null) { | |
1339 lockfile.forEach((name, version) { | |
1340 version = new Version.parse(version); | |
1341 realLockFile.packages[name] = | |
1342 new PackageId(name, source1.name, version, name); | |
1343 }); | |
1344 } | |
1345 | |
1346 // Resolve the versions. | |
1347 var future = resolveVersions( | |
1348 downgrade ? SolveType.DOWNGRADE : SolveType.GET, | |
1349 cache.sources, | |
1350 root, | |
1351 lockFile: realLockFile); | |
1352 | |
1353 var matcher; | |
1354 if (result != null) { | |
1355 matcher = new SolveSuccessMatcher(result, maxTries); | |
1356 } else if (error != null) { | |
1357 matcher = error(maxTries); | |
1358 } | |
1359 | |
1360 expect(future, completion(matcher)); | |
1361 }); | |
1362 } | |
1363 | |
1364 typedef SolveFailMatcher FailMatcherBuilder(int maxTries); | |
1365 | |
1366 FailMatcherBuilder noVersion(List<String> packages) { | |
1367 return (maxTries) => | |
1368 new SolveFailMatcher(packages, maxTries, NoVersionException); | |
1369 } | |
1370 | |
1371 FailMatcherBuilder disjointConstraint(List<String> packages) { | |
1372 return (maxTries) => | |
1373 new SolveFailMatcher(packages, maxTries, DisjointConstraintException); | |
1374 } | |
1375 | |
1376 FailMatcherBuilder descriptionMismatch(String package, String depender1, | |
1377 String depender2) { | |
1378 return (maxTries) => | |
1379 new SolveFailMatcher( | |
1380 [package, depender1, depender2], | |
1381 maxTries, | |
1382 DescriptionMismatchException); | |
1383 } | |
1384 | |
1385 // If no solution can be found, the solver just reports the last failure that | |
1386 // happened during propagation. Since we don't specify the order that solutions | |
1387 // are tried, this just validates that *some* failure occurred, but not which. | |
1388 SolveFailMatcher couldNotSolve(maxTries) => | |
1389 new SolveFailMatcher([], maxTries, null); | |
1390 | |
1391 FailMatcherBuilder sourceMismatch(String package, String depender1, | |
1392 String depender2) { | |
1393 return (maxTries) => | |
1394 new SolveFailMatcher( | |
1395 [package, depender1, depender2], | |
1396 maxTries, | |
1397 SourceMismatchException); | |
1398 } | |
1399 | |
1400 unknownSource(String depender, String dependency, String source) { | |
1401 return (maxTries) => | |
1402 new SolveFailMatcher( | |
1403 [depender, dependency, source], | |
1404 maxTries, | |
1405 UnknownSourceException); | |
1406 } | |
1407 | |
1408 class SolveSuccessMatcher implements Matcher { | |
1409 /// The expected concrete package selections. | |
1410 final Map<String, PackageId> _expected; | |
1411 | |
1412 /// The maximum number of attempts that should have been tried before finding | |
1413 /// the solution. | |
1414 final int _maxTries; | |
1415 | |
1416 SolveSuccessMatcher(this._expected, this._maxTries); | |
1417 | |
1418 Description describe(Description description) { | |
1419 return description.add( | |
1420 'Solver to use at most $_maxTries attempts to find:\n' | |
1421 '${_listPackages(_expected.values)}'); | |
1422 } | |
1423 | |
1424 Description describeMismatch(SolveResult result, Description description, | |
1425 Map state, bool verbose) { | |
1426 if (!result.succeeded) { | |
1427 description.add('Solver failed with:\n${result.error}'); | |
1428 return null; | |
1429 } | |
1430 | |
1431 description.add('Resolved:\n${_listPackages(result.packages)}\n'); | |
1432 description.add(state['failures']); | |
1433 return description; | |
1434 } | |
1435 | |
1436 bool matches(SolveResult result, Map state) { | |
1437 if (!result.succeeded) return false; | |
1438 | |
1439 var expected = new Map.from(_expected); | |
1440 var failures = new StringBuffer(); | |
1441 | |
1442 for (var id in result.packages) { | |
1443 if (!expected.containsKey(id.name)) { | |
1444 failures.writeln('Should not have selected $id'); | |
1445 } else { | |
1446 var expectedId = expected.remove(id.name); | |
1447 if (id != expectedId) { | |
1448 failures.writeln('Expected $expectedId, not $id'); | |
1449 } | |
1450 } | |
1451 } | |
1452 | |
1453 if (!expected.isEmpty) { | |
1454 failures.writeln('Missing:\n${_listPackages(expected.values)}'); | |
1455 } | |
1456 | |
1457 // Allow 1 here because the greedy solver will only make one attempt. | |
1458 if (result.attemptedSolutions != 1 && | |
1459 result.attemptedSolutions != _maxTries) { | |
1460 failures.writeln('Took ${result.attemptedSolutions} attempts'); | |
1461 } | |
1462 | |
1463 if (!failures.isEmpty) { | |
1464 state['failures'] = failures.toString(); | |
1465 return false; | |
1466 } | |
1467 | |
1468 return true; | |
1469 } | |
1470 | |
1471 String _listPackages(Iterable<PackageId> packages) { | |
1472 return '- ${packages.join('\n- ')}'; | |
1473 } | |
1474 } | |
1475 | |
1476 class SolveFailMatcher implements Matcher { | |
1477 /// The strings that should appear in the resulting error message. | |
1478 // TODO(rnystrom): This seems to always be package names. Make that explicit. | |
1479 final Iterable<String> _expected; | |
1480 | |
1481 /// The maximum number of attempts that should be tried before failing. | |
1482 final int _maxTries; | |
1483 | |
1484 /// The concrete error type that should be found, or `null` if any | |
1485 /// [SolveFailure] is allowed. | |
1486 final Type _expectedType; | |
1487 | |
1488 SolveFailMatcher(this._expected, this._maxTries, this._expectedType); | |
1489 | |
1490 Description describe(Description description) { | |
1491 description.add('Solver should fail after at most $_maxTries attempts.'); | |
1492 if (!_expected.isEmpty) { | |
1493 var textList = _expected.map((s) => '"$s"').join(", "); | |
1494 description.add(' The error should contain $textList.'); | |
1495 } | |
1496 return description; | |
1497 } | |
1498 | |
1499 Description describeMismatch(SolveResult result, Description description, | |
1500 Map state, bool verbose) { | |
1501 description.add(state['failures']); | |
1502 return description; | |
1503 } | |
1504 | |
1505 bool matches(SolveResult result, Map state) { | |
1506 var failures = new StringBuffer(); | |
1507 | |
1508 if (result.succeeded) { | |
1509 failures.writeln('Solver succeeded'); | |
1510 } else { | |
1511 if (_expectedType != null && result.error.runtimeType != _expectedType) { | |
1512 failures.writeln( | |
1513 'Should have error type $_expectedType, got ' '${result.error.runtim
eType}'); | |
1514 } | |
1515 | |
1516 var message = result.error.toString(); | |
1517 for (var expected in _expected) { | |
1518 if (!message.contains(expected)) { | |
1519 failures.writeln( | |
1520 'Expected error to contain "$expected", got:\n$message'); | |
1521 } | |
1522 } | |
1523 | |
1524 // Allow 1 here because the greedy solver will only make one attempt. | |
1525 if (result.attemptedSolutions != 1 && | |
1526 result.attemptedSolutions != _maxTries) { | |
1527 failures.writeln('Took ${result.attemptedSolutions} attempts'); | |
1528 } | |
1529 } | |
1530 | |
1531 if (!failures.isEmpty) { | |
1532 state['failures'] = failures.toString(); | |
1533 return false; | |
1534 } | |
1535 | |
1536 return true; | |
1537 } | |
1538 } | |
1539 | |
1540 /// A source used for testing. This both creates mock package objects and acts | |
1541 /// as a source for them. | |
1542 /// | |
1543 /// In order to support testing packages that have the same name but different | |
1544 /// descriptions, a package's name is calculated by taking the description | |
1545 /// string and stripping off any trailing hyphen followed by non-hyphen | |
1546 /// characters. | |
1547 class MockSource extends CachedSource { | |
1548 final _packages = <String, Map<Version, Package>>{}; | |
1549 | |
1550 /// Keeps track of which package version lists have been requested. Ensures | |
1551 /// that a source is only hit once for a given package and that pub | |
1552 /// internally caches the results. | |
1553 final _requestedVersions = new Set<String>(); | |
1554 | |
1555 /// Keeps track of which package pubspecs have been requested. Ensures that a | |
1556 /// source is only hit once for a given package and that pub internally | |
1557 /// caches the results. | |
1558 final _requestedPubspecs = new Map<String, Set<Version>>(); | |
1559 | |
1560 final String name; | |
1561 final hasMultipleVersions = true; | |
1562 | |
1563 MockSource(this.name); | |
1564 | |
1565 dynamic parseDescription(String containingPath, description, | |
1566 {bool fromLockFile: false}) => | |
1567 description; | |
1568 | |
1569 bool descriptionsEqual(description1, description2) => | |
1570 description1 == description2; | |
1571 | |
1572 Future<String> getDirectory(PackageId id) { | |
1573 return new Future.value('${id.name}-${id.version}'); | |
1574 } | |
1575 | |
1576 Future<List<Version>> getVersions(String name, String description) { | |
1577 return new Future.sync(() { | |
1578 // Make sure the solver doesn't request the same thing twice. | |
1579 if (_requestedVersions.contains(description)) { | |
1580 throw new Exception( | |
1581 'Version list for $description was already ' 'requested.'); | |
1582 } | |
1583 | |
1584 _requestedVersions.add(description); | |
1585 | |
1586 if (!_packages.containsKey(description)) { | |
1587 throw new Exception( | |
1588 'MockSource does not have a package matching ' '"$description".'); | |
1589 } | |
1590 | |
1591 return _packages[description].keys.toList(); | |
1592 }); | |
1593 } | |
1594 | |
1595 Future<Pubspec> describeUncached(PackageId id) { | |
1596 return new Future.sync(() { | |
1597 // Make sure the solver doesn't request the same thing twice. | |
1598 if (_requestedPubspecs.containsKey(id.description) && | |
1599 _requestedPubspecs[id.description].contains(id.version)) { | |
1600 throw new Exception('Pubspec for $id was already requested.'); | |
1601 } | |
1602 | |
1603 _requestedPubspecs.putIfAbsent(id.description, () => new Set<Version>()); | |
1604 _requestedPubspecs[id.description].add(id.version); | |
1605 | |
1606 return _packages[id.description][id.version].pubspec; | |
1607 }); | |
1608 } | |
1609 | |
1610 Future<Package> downloadToSystemCache(PackageId id) => | |
1611 throw new UnsupportedError('Cannot download mock packages'); | |
1612 | |
1613 List<Package> getCachedPackages() => | |
1614 throw new UnsupportedError('Cannot get mock packages'); | |
1615 | |
1616 Future<Pair<int, int>> repairCachedPackages() => | |
1617 throw new UnsupportedError('Cannot repair mock packages'); | |
1618 | |
1619 void addPackage(String description, Package package) { | |
1620 _packages.putIfAbsent(description, () => new Map<Version, Package>()); | |
1621 _packages[description][package.version] = package; | |
1622 } | |
1623 } | |
1624 | |
1625 Package mockPackage(PackageId id, Map dependencyStrings, Map overrides) { | |
1626 var sdkConstraint = null; | |
1627 | |
1628 // Build the pubspec dependencies. | |
1629 var dependencies = <PackageDep>[]; | |
1630 var devDependencies = <PackageDep>[]; | |
1631 | |
1632 dependencyStrings.forEach((spec, constraint) { | |
1633 var isDev = spec.startsWith("(dev) "); | |
1634 if (isDev) { | |
1635 spec = spec.substring("(dev) ".length); | |
1636 } | |
1637 | |
1638 var dep = | |
1639 parseSpec(spec).withConstraint(new VersionConstraint.parse(constraint)); | |
1640 | |
1641 if (dep.name == 'sdk') { | |
1642 sdkConstraint = dep.constraint; | |
1643 return; | |
1644 } | |
1645 | |
1646 if (isDev) { | |
1647 devDependencies.add(dep); | |
1648 } else { | |
1649 dependencies.add(dep); | |
1650 } | |
1651 }); | |
1652 | |
1653 var dependencyOverrides = <PackageDep>[]; | |
1654 if (overrides != null) { | |
1655 overrides.forEach((spec, constraint) { | |
1656 dependencyOverrides.add( | |
1657 parseSpec(spec).withConstraint(new VersionConstraint.parse(constraint)
)); | |
1658 }); | |
1659 } | |
1660 | |
1661 return new Package.inMemory( | |
1662 new Pubspec( | |
1663 id.name, | |
1664 version: id.version, | |
1665 dependencies: dependencies, | |
1666 devDependencies: devDependencies, | |
1667 dependencyOverrides: dependencyOverrides, | |
1668 sdkConstraint: sdkConstraint)); | |
1669 } | |
1670 | |
1671 /// Creates a new [PackageId] parsed from [text], which looks something like | |
1672 /// this: | |
1673 /// | |
1674 /// foo-xyz 1.0.0 from mock | |
1675 /// | |
1676 /// The package name is "foo". A hyphenated suffix like "-xyz" here is part | |
1677 /// of the package description, but not its name, so the description here is | |
1678 /// "foo-xyz". | |
1679 /// | |
1680 /// This is followed by an optional [Version]. If [version] is provided, then | |
1681 /// it is parsed to a [Version], and [text] should *not* also contain a | |
1682 /// version string. | |
1683 /// | |
1684 /// The "from mock" optional suffix is the name of a source for the package. | |
1685 /// If omitted, it defaults to "mock1". | |
1686 PackageId parseSpec(String text, [String version]) { | |
1687 var pattern = new RegExp(r"(([a-z_]*)(-[a-z_]+)?)( ([^ ]+))?( from (.*))?$"); | |
1688 var match = pattern.firstMatch(text); | |
1689 if (match == null) { | |
1690 throw new FormatException("Could not parse spec '$text'."); | |
1691 } | |
1692 | |
1693 var description = match[1]; | |
1694 var name = match[2]; | |
1695 | |
1696 var parsedVersion; | |
1697 if (version != null) { | |
1698 // Spec string shouldn't also contain a version. | |
1699 if (match[5] != null) { | |
1700 throw new ArgumentError( | |
1701 "Spec '$text' should not contain a version " | |
1702 "since '$version' was passed in explicitly."); | |
1703 } | |
1704 parsedVersion = new Version.parse(version); | |
1705 } else { | |
1706 if (match[5] != null) { | |
1707 parsedVersion = new Version.parse(match[5]); | |
1708 } else { | |
1709 parsedVersion = Version.none; | |
1710 } | |
1711 } | |
1712 | |
1713 var source = "mock1"; | |
1714 if (match[7] != null) { | |
1715 source = match[7]; | |
1716 if (source == "root") source = null; | |
1717 } | |
1718 | |
1719 return new PackageId(name, source, parsedVersion, description); | |
1720 } | |
OLD | NEW |