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