| 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 |