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