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