| OLD | NEW |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library pub_update_test; | 5 library pub_update_test; |
| 6 | 6 |
| 7 import 'dart:async'; | 7 import 'dart:async'; |
| 8 import 'dart:io'; | 8 import 'dart:io'; |
| 9 | 9 |
| 10 import 'package:unittest/unittest.dart'; | 10 import 'package:unittest/unittest.dart'; |
| 11 | 11 |
| 12 import '../../pub/lock_file.dart'; | 12 import '../../pub/lock_file.dart'; |
| 13 import '../../pub/package.dart'; | 13 import '../../pub/package.dart'; |
| 14 import '../../pub/pubspec.dart'; | 14 import '../../pub/pubspec.dart'; |
| 15 import '../../pub/source.dart'; | 15 import '../../pub/source.dart'; |
| 16 import '../../pub/source_registry.dart'; | 16 import '../../pub/source_registry.dart'; |
| 17 import '../../pub/system_cache.dart'; | 17 import '../../pub/system_cache.dart'; |
| 18 import '../../pub/utils.dart'; | 18 import '../../pub/utils.dart'; |
| 19 import '../../pub/version.dart'; | 19 import '../../pub/version.dart'; |
| 20 import '../../pub/version_solver.dart'; | 20 import '../../pub/solver/version_solver.dart'; |
| 21 import 'test_pub.dart'; | 21 import 'test_pub.dart'; |
| 22 | 22 |
| 23 Matcher noVersion(List<String> packages) { | |
| 24 return predicate((x) { | |
| 25 if (x is! NoVersionException) return false; | |
| 26 | |
| 27 // Make sure the error string mentions the conflicting dependers. | |
| 28 var message = x.toString(); | |
| 29 return packages.every((package) => message.contains(package)); | |
| 30 }, "is a NoVersionException"); | |
| 31 } | |
| 32 | |
| 33 Matcher disjointConstraint(List<String> packages) { | |
| 34 return predicate((x) { | |
| 35 if (x is! DisjointConstraintException) return false; | |
| 36 | |
| 37 // Make sure the error string mentions the conflicting dependers. | |
| 38 var message = x.toString(); | |
| 39 return packages.every((package) => message.contains(package)); | |
| 40 }, "is a DisjointConstraintException"); | |
| 41 } | |
| 42 | |
| 43 Matcher descriptionMismatch(String package1, String package2) { | |
| 44 return predicate((x) { | |
| 45 if (x is! DescriptionMismatchException) return false; | |
| 46 | |
| 47 // Make sure the error string mentions the conflicting dependers. | |
| 48 if (!x.toString().contains(package1)) return false; | |
| 49 if (!x.toString().contains(package2)) return false; | |
| 50 | |
| 51 return true; | |
| 52 }, "is a DescriptionMismatchException"); | |
| 53 } | |
| 54 | |
| 55 final couldNotSolve = predicate((x) => x is CouldNotSolveException, | |
| 56 "is a CouldNotSolveException"); | |
| 57 | |
| 58 Matcher sourceMismatch(String package1, String package2) { | |
| 59 return predicate((x) { | |
| 60 if (x is! SourceMismatchException) return false; | |
| 61 | |
| 62 // Make sure the error string mentions the conflicting dependers. | |
| 63 if (!x.toString().contains(package1)) return false; | |
| 64 if (!x.toString().contains(package2)) return false; | |
| 65 | |
| 66 return true; | |
| 67 }, "is a SourceMismatchException"); | |
| 68 } | |
| 69 | |
| 70 MockSource source1; | 23 MockSource source1; |
| 71 MockSource source2; | 24 MockSource source2; |
| 72 | 25 |
| 26 bool allowBacktracking; |
| 27 |
| 73 main() { | 28 main() { |
| 74 initConfig(); | 29 initConfig(); |
| 75 | 30 |
| 31 for (allowBacktracking in [false, true]) { |
| 32 group(allowBacktracking ? 'BackTrackingSolver' : 'GreedySolver', () { |
| 33 group('basic graph', basicGraph); |
| 34 group('with lockfile', withLockFile); |
| 35 group('root dependency', rootDependency); |
| 36 group('dev dependency', devDependency); |
| 37 group('unsolvable', unsolvable); |
| 38 group('backtracking', backtracking); |
| 39 }); |
| 40 } |
| 41 } |
| 42 |
| 43 void basicGraph() { |
| 76 testResolve('no dependencies', { | 44 testResolve('no dependencies', { |
| 77 'myapp 0.0.0': {} | 45 'myapp 0.0.0': {} |
| 78 }, result: { | 46 }, result: { |
| 79 'myapp from root': '0.0.0' | 47 'myapp from root': '0.0.0' |
| 80 }); | 48 }); |
| 81 | 49 |
| 82 testResolve('simple dependency tree', { | 50 testResolve('simple dependency tree', { |
| 83 'myapp 0.0.0': { | 51 'myapp 0.0.0': { |
| 84 'a': '1.0.0', | 52 'a': '1.0.0', |
| 85 'b': '1.0.0' | 53 'b': '1.0.0' |
| (...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 141 'foo 1.0.3': { 'zoop': '1.0.0' }, | 109 'foo 1.0.3': { 'zoop': '1.0.0' }, |
| 142 'bar 1.0.0': { 'foo': '<=1.0.1' }, | 110 'bar 1.0.0': { 'foo': '<=1.0.1' }, |
| 143 'bang 1.0.0': {}, | 111 'bang 1.0.0': {}, |
| 144 'whoop 1.0.0': {}, | 112 'whoop 1.0.0': {}, |
| 145 'zoop 1.0.0': {} | 113 'zoop 1.0.0': {} |
| 146 }, result: { | 114 }, result: { |
| 147 'myapp from root': '0.0.0', | 115 'myapp from root': '0.0.0', |
| 148 'foo': '1.0.1', | 116 'foo': '1.0.1', |
| 149 'bar': '1.0.0', | 117 'bar': '1.0.0', |
| 150 'bang': '1.0.0' | 118 'bang': '1.0.0' |
| 119 }, maxTries: 2, hasGreedySolution: true); |
| 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' |
| 151 }); | 135 }); |
| 136 } |
| 152 | 137 |
| 138 withLockFile() { |
| 153 testResolve('with compatible locked dependency', { | 139 testResolve('with compatible locked dependency', { |
| 154 'myapp 0.0.0': { | 140 'myapp 0.0.0': { |
| 155 'foo': 'any' | 141 'foo': 'any' |
| 156 }, | 142 }, |
| 157 'foo 1.0.0': { 'bar': '1.0.0' }, | 143 'foo 1.0.0': { 'bar': '1.0.0' }, |
| 158 'foo 1.0.1': { 'bar': '1.0.1' }, | 144 'foo 1.0.1': { 'bar': '1.0.1' }, |
| 159 'foo 1.0.2': { 'bar': '1.0.2' }, | 145 'foo 1.0.2': { 'bar': '1.0.2' }, |
| 160 'bar 1.0.0': {}, | 146 'bar 1.0.0': {}, |
| 161 'bar 1.0.1': {}, | 147 'bar 1.0.1': {}, |
| 162 'bar 1.0.2': {} | 148 'bar 1.0.2': {} |
| (...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 198 'bar 1.0.2': {}, | 184 'bar 1.0.2': {}, |
| 199 'baz 1.0.0': {} | 185 'baz 1.0.0': {} |
| 200 }, lockfile: { | 186 }, lockfile: { |
| 201 'baz': '1.0.0' | 187 'baz': '1.0.0' |
| 202 }, result: { | 188 }, result: { |
| 203 'myapp from root': '0.0.0', | 189 'myapp from root': '0.0.0', |
| 204 'foo': '1.0.2', | 190 'foo': '1.0.2', |
| 205 'bar': '1.0.2' | 191 'bar': '1.0.2' |
| 206 }); | 192 }); |
| 207 | 193 |
| 208 testResolve('circular dependency', { | 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, hasGreedySolution: true); |
| 222 } |
| 223 |
| 224 rootDependency() { |
| 225 testResolve('with root source', { |
| 209 'myapp 1.0.0': { | 226 'myapp 1.0.0': { |
| 210 'foo': '1.0.0' | 227 'foo': '1.0.0' |
| 211 }, | 228 }, |
| 212 'foo 1.0.0': { | |
| 213 'bar': '1.0.0' | |
| 214 }, | |
| 215 'bar 1.0.0': { | |
| 216 'foo': '1.0.0' | |
| 217 } | |
| 218 }, result: { | |
| 219 'myapp from root': '1.0.0', | |
| 220 'foo': '1.0.0', | |
| 221 'bar': '1.0.0' | |
| 222 }); | |
| 223 | |
| 224 testResolve('dependency back onto root package', { | |
| 225 'myapp 1.0.0': { | |
| 226 'foo': '1.0.0' | |
| 227 }, | |
| 228 'foo 1.0.0': { | 229 'foo 1.0.0': { |
| 229 'myapp from root': '>=1.0.0' | 230 'myapp from root': '>=1.0.0' |
| 230 } | 231 } |
| 231 }, result: { | 232 }, result: { |
| 232 'myapp from root': '1.0.0', | 233 'myapp from root': '1.0.0', |
| 233 'foo': '1.0.0' | 234 'foo': '1.0.0' |
| 234 }); | 235 }); |
| 235 | 236 |
| 236 testResolve('dependency back onto root package with different source', { | 237 testResolve('with different source', { |
| 237 'myapp 1.0.0': { | 238 'myapp 1.0.0': { |
| 238 'foo': '1.0.0' | 239 'foo': '1.0.0' |
| 239 }, | 240 }, |
| 240 'foo 1.0.0': { | 241 'foo 1.0.0': { |
| 241 'myapp': '>=1.0.0' | 242 'myapp': '>=1.0.0' |
| 242 } | 243 } |
| 243 }, result: { | 244 }, result: { |
| 244 'myapp from root': '1.0.0', | 245 'myapp from root': '1.0.0', |
| 245 'foo': '1.0.0' | 246 'foo': '1.0.0' |
| 246 }); | 247 }); |
| 247 | 248 |
| 248 testResolve('mismatched dependencies back onto root package', { | 249 testResolve('with mismatched sources', { |
| 249 'myapp 1.0.0': { | 250 'myapp 1.0.0': { |
| 250 'foo': '1.0.0', | 251 'foo': '1.0.0', |
| 251 'bar': '1.0.0' | 252 'bar': '1.0.0' |
| 252 }, | 253 }, |
| 253 'foo 1.0.0': { | 254 'foo 1.0.0': { |
| 254 'myapp': '>=1.0.0' | 255 'myapp': '>=1.0.0' |
| 255 }, | 256 }, |
| 256 'bar 1.0.0': { | 257 'bar 1.0.0': { |
| 257 'myapp from mock2': '>=1.0.0' | 258 'myapp from mock2': '>=1.0.0' |
| 258 } | 259 } |
| 259 }, error: sourceMismatch('foo', 'bar')); | 260 }, error: sourceMismatch('foo', 'bar')); |
| 260 | 261 |
| 261 testResolve('dependency back onto root package with wrong version', { | 262 testResolve('with wrong version', { |
| 262 'myapp 1.0.0': { | 263 'myapp 1.0.0': { |
| 263 'foo': '1.0.0' | 264 'foo': '1.0.0' |
| 264 }, | 265 }, |
| 265 'foo 1.0.0': { | 266 'foo 1.0.0': { |
| 266 'myapp': '<1.0.0' | 267 'myapp': '<1.0.0' |
| 267 } | 268 } |
| 268 }, error: disjointConstraint(['foo'])); | 269 }, error: couldNotSolve); |
| 270 } |
| 269 | 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() { |
| 270 testResolve('no version that matches requirement', { | 315 testResolve('no version that matches requirement', { |
| 271 'myapp 0.0.0': { | 316 'myapp 0.0.0': { |
| 272 'foo': '>=1.0.0 <2.0.0' | 317 'foo': '>=1.0.0 <2.0.0' |
| 273 }, | 318 }, |
| 274 'foo 2.0.0': {}, | 319 'foo 2.0.0': {}, |
| 275 'foo 2.1.3': {} | 320 'foo 2.1.3': {} |
| 276 }, error: noVersion(['myapp'])); | 321 }, error: noVersion(['myapp'])); |
| 277 | 322 |
| 278 testResolve('no version that matches combined constraint', { | 323 testResolve('no version that matches combined constraint', { |
| 279 'myapp 0.0.0': { | 324 'myapp 0.0.0': { |
| (...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 328 'foo 1.0.0': { | 373 'foo 1.0.0': { |
| 329 'shared': '1.0.0' | 374 'shared': '1.0.0' |
| 330 }, | 375 }, |
| 331 'bar 1.0.0': { | 376 'bar 1.0.0': { |
| 332 'shared from mock2': '1.0.0' | 377 'shared from mock2': '1.0.0' |
| 333 }, | 378 }, |
| 334 'shared 1.0.0': {}, | 379 'shared 1.0.0': {}, |
| 335 'shared 1.0.0 from mock2': {} | 380 'shared 1.0.0 from mock2': {} |
| 336 }, error: sourceMismatch('foo', 'bar')); | 381 }, error: sourceMismatch('foo', 'bar')); |
| 337 | 382 |
| 338 testResolve('unstable dependency graph', { | 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', { |
| 339 'myapp 0.0.0': { | 405 'myapp 0.0.0': { |
| 340 'a': '>=1.0.0' | 406 'a': '>=1.0.0' |
| 341 }, | 407 }, |
| 342 'a 1.0.0': {}, | 408 'a 1.0.0': {}, |
| 343 'a 2.0.0': { | 409 'a 2.0.0': { |
| 344 'b': '1.0.0' | 410 'b': '1.0.0' |
| 345 }, | 411 }, |
| 346 'b 1.0.0': { | 412 'b 1.0.0': { |
| 347 'a': '1.0.0' | 413 'a': '1.0.0' |
| 348 } | 414 } |
| 349 }, error: couldNotSolve); | 415 }, result: { |
| 416 'myapp from root': '0.0.0', |
| 417 'a': '1.0.0' |
| 418 }, maxTries: 2); |
| 350 | 419 |
| 351 group('dev dependencies', () { | 420 /// The latest versions of a and b disagree on c. An older version of either |
| 352 testResolve("includes root package's dev dependencies", { | 421 /// will resolve the problem. This test validates that b, which is farther |
| 353 'myapp 1.0.0': { | 422 /// in the dependency graph from myapp is downgraded first. |
| 354 '(dev) foo': '1.0.0', | 423 testResolve('rolls back leaf versions first', { |
| 355 '(dev) bar': '1.0.0' | 424 'myapp 0.0.0': { |
| 356 }, | 425 'a': 'any' |
| 357 'foo 1.0.0': {}, | 426 }, |
| 358 'bar 1.0.0': {} | 427 'a 1.0.0': { |
| 359 }, result: { | 428 'b': 'any' |
| 360 'myapp from root': '1.0.0', | 429 }, |
| 361 'foo': '1.0.0', | 430 'a 2.0.0': { |
| 362 'bar': '1.0.0' | 431 'b': 'any', |
| 363 }); | 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); |
| 364 | 446 |
| 365 testResolve("includes dev dependency's transitive dependencies", { | 447 // Only one version of baz, so foo and bar will have to downgrade until they |
| 366 'myapp 1.0.0': { | 448 // reach it. |
| 367 '(dev) foo': '1.0.0' | 449 testResolve('simple transitive', { |
| 368 }, | 450 'myapp 0.0.0': {'foo': 'any'}, |
| 369 'foo 1.0.0': { | 451 'foo 1.0.0': {'bar': '1.0.0'}, |
| 370 'bar': '1.0.0' | 452 'foo 2.0.0': {'bar': '2.0.0'}, |
| 371 }, | 453 'foo 3.0.0': {'bar': '3.0.0'}, |
| 372 'bar 1.0.0': {} | 454 'bar 1.0.0': {'baz': 'any'}, |
| 373 }, result: { | 455 'bar 2.0.0': {'baz': '2.0.0'}, |
| 374 'myapp from root': '1.0.0', | 456 'bar 3.0.0': {'baz': '3.0.0'}, |
| 375 'foo': '1.0.0', | 457 'baz 1.0.0': {} |
| 376 'bar': '1.0.0' | 458 }, result: { |
| 377 }); | 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); |
| 378 | 464 |
| 379 testResolve("ignores transitive dependency's dev dependencies", { | 465 // This ensures it doesn't exhaustively search all versions of b when it's |
| 380 'myapp 1.0.0': { | 466 // a-2.0.0 whose dependency on c-2.0.0-nonexistent led to the problem. We |
| 381 'foo': '1.0.0' | 467 // make sure b has more versions than a so that the solver tries a first |
| 382 }, | 468 // since it sorts sibling dependencies by number of versions. |
| 383 'foo 1.0.0': { | 469 testResolve('backjump to nearer unsatisfied package', { |
| 384 '(dev) bar': '1.0.0' | 470 'myapp 0.0.0': { |
| 385 }, | 471 'a': 'any', |
| 386 'bar 1.0.0': {} | 472 'b': 'any' |
| 387 }, result: { | 473 }, |
| 388 'myapp from root': '1.0.0', | 474 'a 1.0.0': { 'c': '1.0.0' }, |
| 389 'foo': '1.0.0' | 475 'a 2.0.0': { 'c': '2.0.0-nonexistent' }, |
| 390 }); | 476 'b 1.0.0': {}, |
| 391 }); | 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. |
| 392 } | 547 } |
| 393 | 548 |
| 394 // TODO(rnystrom): More stuff to test: | 549 testResolve(description, packages, |
| 395 // - Depending on a non-existent package. | 550 {lockfile, result, FailMatcherBuilder error, int maxTries, |
| 396 // - Test that only a certain number requests are sent to the mock source so we | 551 bool hasGreedySolution}) { |
| 397 // can keep track of server traffic. | 552 // Close over the top-level variable since it will be mutated. |
| 553 var allowBacktracking_ = allowBacktracking; |
| 398 | 554 |
| 399 testResolve(description, packages, {lockfile, result, Matcher error}) { | 555 if (maxTries == null) maxTries = 1; |
| 556 if (hasGreedySolution == null) hasGreedySolution = maxTries == 1; |
| 557 |
| 558 if (!allowBacktracking_) { |
| 559 // The greedy solver should fail any graph that does expect multiple tries |
| 560 // and isn't explicitly annotated to have a greedy solution. |
| 561 if (!hasGreedySolution) { |
| 562 result = null; |
| 563 error = couldNotSolve; |
| 564 } |
| 565 } |
| 566 |
| 400 test(description, () { | 567 test(description, () { |
| 401 var cache = new SystemCache('.'); | 568 var cache = new SystemCache('.'); |
| 402 source1 = new MockSource('mock1'); | 569 source1 = new MockSource('mock1'); |
| 403 source2 = new MockSource('mock2'); | 570 source2 = new MockSource('mock2'); |
| 404 cache.register(source1); | 571 cache.register(source1); |
| 405 cache.register(source2); | 572 cache.register(source2); |
| 406 cache.sources.setDefault(source1.name); | 573 cache.sources.setDefault(source1.name); |
| 407 | 574 |
| 408 // Build the test package graph. | 575 // Build the test package graph. |
| 409 var root; | 576 var root; |
| 410 packages.forEach((nameVersion, dependencies) { | 577 packages.forEach((nameVersion, dependencies) { |
| 411 var parsed = parseSource(nameVersion, (isDev, nameVersion, source) { | 578 var parsed = parseSource(nameVersion, (isDev, nameVersion, source) { |
| 412 var parts = nameVersion.split(' '); | 579 var parts = nameVersion.split(' '); |
| 413 var name = parts[0]; | 580 var name = parts[0]; |
| 414 var version = parts[1]; | 581 var version = parts[1]; |
| 415 | 582 |
| 416 var package = source1.mockPackage(name, version, dependencies); | 583 var package = mockPackage(name, version, dependencies); |
| 417 if (name == 'myapp') { | 584 if (name == 'myapp') { |
| 418 // Don't add the root package to the server, so we can verify that Pub | 585 // Don't add the root package to the server, so we can verify that Pub |
| 419 // doesn't try to look up information about the local package on the | 586 // doesn't try to look up information about the local package on the |
| 420 // remote server. | 587 // remote server. |
| 421 root = package; | 588 root = package; |
| 422 } else { | 589 } else { |
| 423 source.addPackage(package); | 590 source.addPackage(name, package); |
| 424 } | 591 } |
| 425 }); | 592 }); |
| 426 }); | 593 }); |
| 427 | 594 |
| 428 // Clean up the expectation. | 595 // Clean up the expectation. |
| 429 if (result != null) { | 596 if (result != null) { |
| 430 var newResult = {}; | 597 var newResult = {}; |
| 431 result.forEach((name, version) { | 598 result.forEach((name, version) { |
| 432 parseSource(name, (isDev, name, source) { | 599 parseSource(name, (isDev, name, source) { |
| 433 version = new Version.parse(version); | 600 version = new Version.parse(version); |
| 434 newResult[name] = new PackageId(name, source, version, name); | 601 newResult[name] = new PackageId(name, source, version, name); |
| 435 }); | 602 }); |
| 436 }); | 603 }); |
| 437 result = newResult; | 604 result = newResult; |
| 438 } | 605 } |
| 439 | 606 |
| 440 var realLockFile = new LockFile.empty(); | 607 var realLockFile = new LockFile.empty(); |
| 441 if (lockfile != null) { | 608 if (lockfile != null) { |
| 442 lockfile.forEach((name, version) { | 609 lockfile.forEach((name, version) { |
| 443 version = new Version.parse(version); | 610 version = new Version.parse(version); |
| 444 realLockFile.packages[name] = | 611 realLockFile.packages[name] = |
| 445 new PackageId(name, source1, version, name); | 612 new PackageId(name, source1, version, name); |
| 446 }); | 613 }); |
| 447 } | 614 } |
| 448 | 615 |
| 449 // Resolve the versions. | 616 // Resolve the versions. |
| 450 var future = resolveVersions(cache.sources, root, realLockFile); | 617 var future = resolveVersions(cache.sources, root, |
| 618 allowBacktracking: allowBacktracking_, lockFile: realLockFile); |
| 451 | 619 |
| 620 var matcher; |
| 452 if (result != null) { | 621 if (result != null) { |
| 453 expect(future, completion(predicate((actualResult) { | 622 matcher = new SolveSuccessMatcher(result, maxTries); |
| 454 for (var actualId in actualResult) { | 623 } else if (error != null) { |
| 455 if (!result.containsKey(actualId.name)) return false; | 624 matcher = error(maxTries); |
| 456 var expectedId = result.remove(actualId.name); | 625 } |
| 457 if (actualId != expectedId) return false; | 626 |
| 627 expect(future, completion(matcher)); |
| 628 }); |
| 629 } |
| 630 |
| 631 typedef SolveFailMatcher FailMatcherBuilder(int maxTries); |
| 632 |
| 633 FailMatcherBuilder noVersion(List<String> packages) { |
| 634 return (maxTries) => new SolveFailMatcher(packages, maxTries, |
| 635 NoVersionException); |
| 636 } |
| 637 |
| 638 FailMatcherBuilder disjointConstraint(List<String> packages) { |
| 639 return (maxTries) => new SolveFailMatcher(packages, maxTries, |
| 640 DisjointConstraintException); |
| 641 } |
| 642 |
| 643 FailMatcherBuilder descriptionMismatch(String package1, String package2) { |
| 644 return (maxTries) => new SolveFailMatcher([package1, package2], maxTries, |
| 645 DescriptionMismatchException); |
| 646 } |
| 647 |
| 648 // If no solution can be found, the solver just reports the last failure that |
| 649 // happened during propagation. Since we don't specify the order that solutions |
| 650 // are tried, this just validates that *some* failure occurred, but not which. |
| 651 SolveFailMatcher couldNotSolve(maxTries) => |
| 652 new SolveFailMatcher([], maxTries, null); |
| 653 |
| 654 FailMatcherBuilder sourceMismatch(String package1, String package2) { |
| 655 return (maxTries) => new SolveFailMatcher([package1, package2], maxTries, |
| 656 SourceMismatchException); |
| 657 } |
| 658 |
| 659 class SolveSuccessMatcher implements Matcher { |
| 660 /// The expected concrete package selections. |
| 661 final Map<String, PackageId> _expected; |
| 662 |
| 663 /// The maximum number of attempts that should have been tried before finding |
| 664 /// the solution. |
| 665 final int _maxTries; |
| 666 |
| 667 SolveSuccessMatcher(this._expected, this._maxTries); |
| 668 |
| 669 Description describe(Description description) { |
| 670 return description.add( |
| 671 'Solver to use at most $_maxTries attempts to find:\n' |
| 672 '${_listPackages(_expected.values)}'); |
| 673 } |
| 674 |
| 675 Description describeMismatch(SolveResult result, |
| 676 Description description, |
| 677 MatchState state, bool verbose) { |
| 678 if (!result.succeeded) { |
| 679 description.add('Solver failed with:\n${result.error}'); |
| 680 return; |
| 681 } |
| 682 |
| 683 description.add('Resolved:\n${_listPackages(result.packages)}\n'); |
| 684 description.add(state.state); |
| 685 return description; |
| 686 } |
| 687 |
| 688 bool matches(SolveResult result, MatchState state) { |
| 689 if (!result.succeeded) return false; |
| 690 |
| 691 var expected = new Map.from(_expected); |
| 692 var failures = new StringBuffer(); |
| 693 |
| 694 for (var id in result.packages) { |
| 695 if (!expected.containsKey(id.name)) { |
| 696 failures.writeln('Should not have selected $id'); |
| 697 } else { |
| 698 var expectedId = expected.remove(id.name); |
| 699 if (id != expectedId) { |
| 700 failures.writeln('Expected $expectedId, not $id'); |
| 458 } | 701 } |
| 459 return result.isEmpty; | 702 } |
| 460 }, 'packages to match $result'))); | |
| 461 } else if (error != null) { | |
| 462 expect(future, throwsA(error)); | |
| 463 } | 703 } |
| 464 }); | 704 |
| 705 if (!expected.isEmpty) { |
| 706 failures.writeln('Missing:\n${_listPackages(expected.values)}'); |
| 707 } |
| 708 |
| 709 // Allow 1 here because the greedy solver will only make one attempt. |
| 710 if (result.attemptedSolutions != 1 && |
| 711 result.attemptedSolutions != _maxTries) { |
| 712 failures.writeln('Took ${result.attemptedSolutions} attempts'); |
| 713 } |
| 714 |
| 715 if (!failures.isEmpty) { |
| 716 state.state = failures.toString(); |
| 717 return false; |
| 718 } |
| 719 |
| 720 return true; |
| 721 } |
| 722 |
| 723 String _listPackages(Iterable<PackageId> packages) { |
| 724 return '- ${packages.join('\n- ')}'; |
| 725 } |
| 726 } |
| 727 |
| 728 class SolveFailMatcher implements Matcher { |
| 729 /// The strings that should appear in the resulting error message. |
| 730 // TODO(rnystrom): This seems to always be package names. Make that explicit. |
| 731 final Iterable<String> _expected; |
| 732 |
| 733 /// The maximum number of attempts that should be tried before failing. |
| 734 final int _maxTries; |
| 735 |
| 736 /// The concrete error type that should be found, or `null` if any |
| 737 /// [SolveFailure] is allowed. |
| 738 final Type _expectedType; |
| 739 |
| 740 SolveFailMatcher(this._expected, this._maxTries, this._expectedType); |
| 741 |
| 742 Description describe(Description description) { |
| 743 description.add('Solver should fail after at most $_maxTries attempts.'); |
| 744 if (!_expected.isEmpty) { |
| 745 var textList = _expected.map((s) => '"$s"').join(", "); |
| 746 description.add(' The error should contain $textList.'); |
| 747 } |
| 748 return description; |
| 749 } |
| 750 |
| 751 Description describeMismatch(SolveResult result, |
| 752 Description description, |
| 753 MatchState state, bool verbose) { |
| 754 description.add(state.state); |
| 755 return description; |
| 756 } |
| 757 |
| 758 bool matches(SolveResult result, MatchState state) { |
| 759 var failures = new StringBuffer(); |
| 760 |
| 761 if (result.succeeded) { |
| 762 failures.writeln('Solver succeeded'); |
| 763 } else { |
| 764 if (_expectedType != null && result.error.runtimeType != _expectedType) { |
| 765 failures.writeln('Should have error type $_expectedType, got ' |
| 766 '${result.error.runtimeType}'); |
| 767 } |
| 768 |
| 769 var message = result.error.toString(); |
| 770 for (var expected in _expected) { |
| 771 if (!message.contains(expected)) { |
| 772 failures.writeln( |
| 773 'Expected error to contain "$expected", got:\n$message'); |
| 774 } |
| 775 } |
| 776 |
| 777 // Allow 1 here because the greedy solver will only make one attempt. |
| 778 if (result.attemptedSolutions != 1 && |
| 779 result.attemptedSolutions != _maxTries) { |
| 780 failures.writeln('Took ${result.attemptedSolutions} attempts'); |
| 781 } |
| 782 } |
| 783 |
| 784 if (!failures.isEmpty) { |
| 785 state.state = failures.toString(); |
| 786 return false; |
| 787 } |
| 788 |
| 789 return true; |
| 790 } |
| 465 } | 791 } |
| 466 | 792 |
| 467 /// A source used for testing. This both creates mock package objects and acts | 793 /// A source used for testing. This both creates mock package objects and acts |
| 468 /// as a source for them. | 794 /// as a source for them. |
| 469 /// | 795 /// |
| 470 /// In order to support testing packages that have the same name but different | 796 /// In order to support testing packages that have the same name but different |
| 471 /// descriptions, a package's name is calculated by taking the description | 797 /// descriptions, a package's name is calculated by taking the description |
| 472 /// string and stripping off any trailing hyphen followed by non-hyphen | 798 /// string and stripping off any trailing hyphen followed by non-hyphen |
| 473 /// characters. | 799 /// characters. |
| 474 class MockSource extends Source { | 800 class MockSource extends Source { |
| 475 final Map<String, Map<Version, Package>> _packages; | 801 final _packages = <String, Map<Version, Package>>{}; |
| 802 |
| 803 /// Keeps track of which package version lists have been requested. Ensures |
| 804 /// that a source is only hit once for a given package and that pub |
| 805 /// internally caches the results. |
| 806 final _requestedVersions = new Set<String>(); |
| 807 |
| 808 /// Keeps track of which package pubspecs have been requested. Ensures that a |
| 809 /// source is only hit once for a given package and that pub internally |
| 810 /// caches the results. |
| 811 final _requestedPubspecs = new Map<String, Set<Version>>(); |
| 476 | 812 |
| 477 final String name; | 813 final String name; |
| 478 bool get shouldCache => true; | 814 bool get shouldCache => true; |
| 479 | 815 |
| 480 MockSource(this.name) | 816 MockSource(this.name); |
| 481 : _packages = <String, Map<Version, Package>>{}; | |
| 482 | 817 |
| 483 Future<List<Version>> getVersions(String name, String description) { | 818 Future<List<Version>> getVersions(String name, String description) { |
| 484 return new Future.sync(() => _packages[description].keys.toList()); | 819 return new Future.sync(() { |
| 820 // Make sure the solver doesn't request the same thing twice. |
| 821 if (_requestedVersions.contains(description)) { |
| 822 throw 'Version list for $description was already requested.'; |
| 823 } |
| 824 |
| 825 _requestedVersions.add(description); |
| 826 |
| 827 if (!_packages.containsKey(description)){ |
| 828 throw 'MockSource does not have a package matching "$description".'; |
| 829 } |
| 830 return _packages[description].keys.toList(); |
| 831 }); |
| 485 } | 832 } |
| 486 | 833 |
| 487 Future<Pubspec> describe(PackageId id) { | 834 Future<Pubspec> describe(PackageId id) { |
| 488 return new Future.sync(() => _packages[id.name][id.version].pubspec); | 835 return new Future.sync(() { |
| 836 // Make sure the solver doesn't request the same thing twice. |
| 837 if (_requestedPubspecs.containsKey(id.description) && |
| 838 _requestedPubspecs[id.description].contains(id.version)) { |
| 839 throw 'Pubspec for $id was already requested.'; |
| 840 } |
| 841 |
| 842 _requestedPubspecs.putIfAbsent(id.description, () => new Set<Version>()); |
| 843 _requestedPubspecs[id.description].add(id.version); |
| 844 |
| 845 return _packages[id.description][id.version].pubspec; |
| 846 }); |
| 489 } | 847 } |
| 490 | 848 |
| 491 Future<bool> install(PackageId id, String path) { | 849 Future<bool> install(PackageId id, String path) { |
| 492 throw 'no'; | 850 throw 'no'; |
| 493 } | 851 } |
| 494 | 852 |
| 495 Package mockPackage(String description, String version, | 853 void addPackage(String description, Package package) { |
| 496 Map dependencyStrings) { | 854 _packages.putIfAbsent(description, () => new Map<Version, Package>()); |
| 497 // Build the pubspec dependencies. | 855 _packages[description][package.version] = package; |
| 498 var dependencies = <PackageRef>[]; | |
| 499 var devDependencies = <PackageRef>[]; | |
| 500 | |
| 501 dependencyStrings.forEach((name, constraint) { | |
| 502 parseSource(name, (isDev, name, source) { | |
| 503 var packageName = name.replaceFirst(new RegExp(r"-[^-]+$"), ""); | |
| 504 var ref = new PackageRef(packageName, source, | |
| 505 new VersionConstraint.parse(constraint), name); | |
| 506 | |
| 507 if (isDev) { | |
| 508 devDependencies.add(ref); | |
| 509 } else { | |
| 510 dependencies.add(ref); | |
| 511 } | |
| 512 }); | |
| 513 }); | |
| 514 | |
| 515 var pubspec = new Pubspec( | |
| 516 description, new Version.parse(version), dependencies, devDependencies, | |
| 517 new PubspecEnvironment()); | |
| 518 return new Package.inMemory(pubspec); | |
| 519 } | |
| 520 | |
| 521 void addPackage(Package package) { | |
| 522 _packages.putIfAbsent(package.name, () => new Map<Version, Package>()); | |
| 523 _packages[package.name][package.version] = package; | |
| 524 } | 856 } |
| 525 } | 857 } |
| 526 | 858 |
| 859 Package mockPackage(String description, String version, |
| 860 Map dependencyStrings) { |
| 861 // Build the pubspec dependencies. |
| 862 var dependencies = <PackageRef>[]; |
| 863 var devDependencies = <PackageRef>[]; |
| 864 |
| 865 dependencyStrings.forEach((name, constraint) { |
| 866 parseSource(name, (isDev, name, source) { |
| 867 var packageName = name.replaceFirst(new RegExp(r"-[^-]+$"), ""); |
| 868 var ref = new PackageRef(packageName, source, |
| 869 new VersionConstraint.parse(constraint), name); |
| 870 |
| 871 if (isDev) { |
| 872 devDependencies.add(ref); |
| 873 } else { |
| 874 dependencies.add(ref); |
| 875 } |
| 876 }); |
| 877 }); |
| 878 |
| 879 var name = description.replaceFirst(new RegExp(r"-[^-]+$"), ""); |
| 880 var pubspec = new Pubspec( |
| 881 name, new Version.parse(version), dependencies, devDependencies, |
| 882 new PubspecEnvironment()); |
| 883 return new Package.inMemory(pubspec); |
| 884 } |
| 885 |
| 527 void parseSource(String description, | 886 void parseSource(String description, |
| 528 callback(bool isDev, String name, Source source)) { | 887 callback(bool isDev, String name, Source source)) { |
| 529 var isDev = false; | 888 var isDev = false; |
| 530 | 889 |
| 531 if (description.startsWith("(dev) ")) { | 890 if (description.startsWith("(dev) ")) { |
| 532 description = description.substring("(dev) ".length); | 891 description = description.substring("(dev) ".length); |
| 533 isDev = true; | 892 isDev = true; |
| 534 } | 893 } |
| 535 | 894 |
| 536 var name = description; | 895 var name = description; |
| 537 var source = source1; | 896 var source = source1; |
| 538 | 897 |
| 539 var sourceNames = { | 898 var sourceNames = { |
| 540 'mock1': source1, | 899 'mock1': source1, |
| 541 'mock2': source2, | 900 'mock2': source2, |
| 542 'root': null | 901 'root': null |
| 543 }; | 902 }; |
| 544 | 903 |
| 545 var match = new RegExp(r"(.*) from (.*)").firstMatch(description); | 904 var match = new RegExp(r"(.*) from (.*)").firstMatch(description); |
| 546 if (match != null) { | 905 if (match != null) { |
| 547 name = match[1]; | 906 name = match[1]; |
| 548 source = sourceNames[match[2]]; | 907 source = sourceNames[match[2]]; |
| 549 } | 908 } |
| 550 | 909 |
| 551 callback(isDev, name, source); | 910 callback(isDev, name, source); |
| 552 } | 911 } |
| OLD | NEW |