Chromium Code Reviews| 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) { | 23 Matcher noVersion(List<String> packages) { |
| 24 return predicate((x) { | 24 return predicate((x) { |
| 25 if (x is! NoVersionException) return false; | 25 if (x is! NoVersionException) return false; |
| 26 | 26 |
| 27 // Make sure the error string mentions the conflicting dependers. | 27 // Make sure the error string mentions the conflicting dependers. |
| 28 var message = x.toString(); | 28 var message = x.toString(); |
| 29 return packages.every((package) => message.contains(package)); | 29 return packages.every((package) => message.contains(package)); |
| 30 }, "is a NoVersionException"); | 30 }, "is a NoVersionException"); |
| (...skipping 14 matching lines...) Expand all Loading... | |
| 45 if (x is! DescriptionMismatchException) return false; | 45 if (x is! DescriptionMismatchException) return false; |
| 46 | 46 |
| 47 // Make sure the error string mentions the conflicting dependers. | 47 // Make sure the error string mentions the conflicting dependers. |
| 48 if (!x.toString().contains(package1)) return false; | 48 if (!x.toString().contains(package1)) return false; |
| 49 if (!x.toString().contains(package2)) return false; | 49 if (!x.toString().contains(package2)) return false; |
| 50 | 50 |
| 51 return true; | 51 return true; |
| 52 }, "is a DescriptionMismatchException"); | 52 }, "is a DescriptionMismatchException"); |
| 53 } | 53 } |
| 54 | 54 |
| 55 final couldNotSolve = predicate((x) => x is CouldNotSolveException, | 55 // If no solution can be found, the solver just reports the last failure that |
| 56 "is a CouldNotSolveException"); | 56 // happened during propagation. Since we don't specify the order that solutions |
| 57 // are tried, this just validates that *some* failure occurred, but not which. | |
| 58 final couldNotSolve = predicate((x) => x is SolverFailure, | |
| 59 "is a SolverFailure"); | |
| 57 | 60 |
| 58 Matcher sourceMismatch(String package1, String package2) { | 61 Matcher sourceMismatch(String package1, String package2) { |
| 59 return predicate((x) { | 62 return predicate((x) { |
| 60 if (x is! SourceMismatchException) return false; | 63 if (x is! SourceMismatchException) return false; |
| 61 | 64 |
| 62 // Make sure the error string mentions the conflicting dependers. | 65 // Make sure the error string mentions the conflicting dependers. |
| 63 if (!x.toString().contains(package1)) return false; | 66 if (!x.toString().contains(package1)) return false; |
| 64 if (!x.toString().contains(package2)) return false; | 67 if (!x.toString().contains(package2)) return false; |
| 65 | 68 |
| 66 return true; | 69 return true; |
| 67 }, "is a SourceMismatchException"); | 70 }, "is a SourceMismatchException"); |
| 68 } | 71 } |
| 69 | 72 |
| 70 MockSource source1; | 73 MockSource source1; |
| 71 MockSource source2; | 74 MockSource source2; |
| 72 | 75 |
| 76 var allowBacktracking; | |
|
nweiz
2013/04/03 00:28:43
Declare this as a bool, since it doesn't have an i
Bob Nystrom
2013/04/08 22:13:00
Done.
| |
| 77 | |
| 73 main() { | 78 main() { |
| 74 initConfig(); | 79 initConfig(); |
| 75 | 80 |
| 81 for (allowBacktracking in [false, true]) { | |
| 82 group(allowBacktracking ? 'BackTrackingSolver' : 'GreedySolver', () { | |
| 83 group('basic graph', basicGraph); | |
| 84 group('with lockfile', withLockFile); | |
| 85 group('root dependency', rootDependency); | |
| 86 group('dev dependency', devDependency); | |
| 87 group('unsolvable', unsolvable); | |
| 88 group('backtracking', backtracking); | |
| 89 }); | |
| 90 } | |
| 91 | |
| 92 allowBacktracking = 'wtf'; | |
|
nweiz
2013/04/03 00:28:43
wtf?
Bob Nystrom
2013/04/08 22:13:00
Heh. Debug code. Removed. :)
| |
| 93 } | |
| 94 | |
| 95 void basicGraph() { | |
| 76 testResolve('no dependencies', { | 96 testResolve('no dependencies', { |
| 77 'myapp 0.0.0': {} | 97 'myapp 0.0.0': {} |
| 78 }, result: { | 98 }, result: { |
| 79 'myapp from root': '0.0.0' | 99 'myapp from root': '0.0.0' |
| 80 }); | 100 }); |
| 81 | 101 |
| 82 testResolve('simple dependency tree', { | 102 testResolve('simple dependency tree', { |
| 83 'myapp 0.0.0': { | 103 'myapp 0.0.0': { |
| 84 'a': '1.0.0', | 104 'a': '1.0.0', |
| 85 'b': '1.0.0' | 105 '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' }, | 161 'foo 1.0.3': { 'zoop': '1.0.0' }, |
| 142 'bar 1.0.0': { 'foo': '<=1.0.1' }, | 162 'bar 1.0.0': { 'foo': '<=1.0.1' }, |
| 143 'bang 1.0.0': {}, | 163 'bang 1.0.0': {}, |
| 144 'whoop 1.0.0': {}, | 164 'whoop 1.0.0': {}, |
| 145 'zoop 1.0.0': {} | 165 'zoop 1.0.0': {} |
| 146 }, result: { | 166 }, result: { |
| 147 'myapp from root': '0.0.0', | 167 'myapp from root': '0.0.0', |
| 148 'foo': '1.0.1', | 168 'foo': '1.0.1', |
| 149 'bar': '1.0.0', | 169 'bar': '1.0.0', |
| 150 'bang': '1.0.0' | 170 'bang': '1.0.0' |
| 171 }, maxTries: 3, hasGreedySolution: true); | |
|
nweiz
2013/04/03 00:28:43
This case will take about N iterations for the bac
Bob Nystrom
2013/04/08 22:13:00
That sounds about right though I think it gets mor
| |
| 172 | |
| 173 testResolve('circular dependency', { | |
| 174 'myapp 1.0.0': { | |
| 175 'foo': '1.0.0' | |
| 176 }, | |
| 177 'foo 1.0.0': { | |
| 178 'bar': '1.0.0' | |
| 179 }, | |
| 180 'bar 1.0.0': { | |
| 181 'foo': '1.0.0' | |
| 182 } | |
| 183 }, result: { | |
| 184 'myapp from root': '1.0.0', | |
| 185 'foo': '1.0.0', | |
| 186 'bar': '1.0.0' | |
| 151 }); | 187 }); |
| 188 } | |
| 152 | 189 |
| 190 withLockFile() { | |
| 153 testResolve('with compatible locked dependency', { | 191 testResolve('with compatible locked dependency', { |
| 154 'myapp 0.0.0': { | 192 'myapp 0.0.0': { |
| 155 'foo': 'any' | 193 'foo': 'any' |
| 156 }, | 194 }, |
| 157 'foo 1.0.0': { 'bar': '1.0.0' }, | 195 'foo 1.0.0': { 'bar': '1.0.0' }, |
| 158 'foo 1.0.1': { 'bar': '1.0.1' }, | 196 'foo 1.0.1': { 'bar': '1.0.1' }, |
| 159 'foo 1.0.2': { 'bar': '1.0.2' }, | 197 'foo 1.0.2': { 'bar': '1.0.2' }, |
| 160 'bar 1.0.0': {}, | 198 'bar 1.0.0': {}, |
| 161 'bar 1.0.1': {}, | 199 'bar 1.0.1': {}, |
| 162 'bar 1.0.2': {} | 200 'bar 1.0.2': {} |
| (...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 198 'bar 1.0.2': {}, | 236 'bar 1.0.2': {}, |
| 199 'baz 1.0.0': {} | 237 'baz 1.0.0': {} |
| 200 }, lockfile: { | 238 }, lockfile: { |
| 201 'baz': '1.0.0' | 239 'baz': '1.0.0' |
| 202 }, result: { | 240 }, result: { |
| 203 'myapp from root': '0.0.0', | 241 'myapp from root': '0.0.0', |
| 204 'foo': '1.0.2', | 242 'foo': '1.0.2', |
| 205 'bar': '1.0.2' | 243 'bar': '1.0.2' |
| 206 }); | 244 }); |
| 207 | 245 |
| 208 testResolve('circular dependency', { | 246 testResolve('unlocks dependencies if necessary to ensure that a new ' |
| 247 'dependency is satisfied', { | |
| 248 'myapp 0.0.0': { | |
| 249 'foo': 'any', | |
| 250 'newdep': 'any' | |
| 251 }, | |
| 252 'foo 1.0.0': { 'bar': '<2.0.0' }, | |
| 253 'bar 1.0.0': { 'baz': '<2.0.0' }, | |
| 254 'baz 1.0.0': { 'qux': '<2.0.0' }, | |
| 255 'qux 1.0.0': {}, | |
| 256 'foo 2.0.0': { 'bar': '<3.0.0' }, | |
| 257 'bar 2.0.0': { 'baz': '<3.0.0' }, | |
| 258 'baz 2.0.0': { 'qux': '<3.0.0' }, | |
| 259 'qux 2.0.0': {}, | |
| 260 'newdep 2.0.0': { 'baz': '>=1.5.0' } | |
| 261 }, lockfile: { | |
| 262 'foo': '1.0.0', | |
| 263 'bar': '1.0.0', | |
| 264 'baz': '1.0.0', | |
| 265 'qux': '1.0.0' | |
| 266 }, result: { | |
| 267 'myapp from root': '0.0.0', | |
| 268 'foo': '2.0.0', | |
| 269 'bar': '2.0.0', | |
| 270 'baz': '2.0.0', | |
| 271 'qux': '1.0.0', | |
| 272 'newdep': '2.0.0' | |
| 273 }, maxTries: 5, hasGreedySolution: true); | |
|
nweiz
2013/04/03 00:28:43
These numbers feel very opaque to me. Could you in
Bob Nystrom
2013/04/08 22:13:00
Explaining where the iterations come from would ba
nweiz
2013/04/10 22:56:34
The generalized behavior of the solver is very com
Bob Nystrom
2013/04/11 00:55:10
It's useful, but I think that documentation belong
| |
| 274 } | |
| 275 | |
| 276 rootDependency() { | |
| 277 testResolve('with root source', { | |
| 209 'myapp 1.0.0': { | 278 'myapp 1.0.0': { |
| 210 'foo': '1.0.0' | 279 'foo': '1.0.0' |
| 211 }, | 280 }, |
| 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': { | 281 'foo 1.0.0': { |
| 229 'myapp from root': '>=1.0.0' | 282 'myapp from root': '>=1.0.0' |
| 230 } | 283 } |
| 231 }, result: { | 284 }, result: { |
| 232 'myapp from root': '1.0.0', | 285 'myapp from root': '1.0.0', |
| 233 'foo': '1.0.0' | 286 'foo': '1.0.0' |
| 234 }); | 287 }); |
| 235 | 288 |
| 236 testResolve('dependency back onto root package with different source', { | 289 testResolve('with different source', { |
| 237 'myapp 1.0.0': { | 290 'myapp 1.0.0': { |
| 238 'foo': '1.0.0' | 291 'foo': '1.0.0' |
| 239 }, | 292 }, |
| 240 'foo 1.0.0': { | 293 'foo 1.0.0': { |
| 241 'myapp': '>=1.0.0' | 294 'myapp': '>=1.0.0' |
| 242 } | 295 } |
| 243 }, result: { | 296 }, result: { |
| 244 'myapp from root': '1.0.0', | 297 'myapp from root': '1.0.0', |
| 245 'foo': '1.0.0' | 298 'foo': '1.0.0' |
| 246 }); | 299 }); |
| 247 | 300 |
| 248 testResolve('mismatched dependencies back onto root package', { | 301 testResolve('with mismatched sources', { |
| 249 'myapp 1.0.0': { | 302 'myapp 1.0.0': { |
| 250 'foo': '1.0.0', | 303 'foo': '1.0.0', |
| 251 'bar': '1.0.0' | 304 'bar': '1.0.0' |
| 252 }, | 305 }, |
| 253 'foo 1.0.0': { | 306 'foo 1.0.0': { |
| 254 'myapp': '>=1.0.0' | 307 'myapp': '>=1.0.0' |
| 255 }, | 308 }, |
| 256 'bar 1.0.0': { | 309 'bar 1.0.0': { |
| 257 'myapp from mock2': '>=1.0.0' | 310 'myapp from mock2': '>=1.0.0' |
| 258 } | 311 } |
| 259 }, error: sourceMismatch('foo', 'bar')); | 312 }, error: sourceMismatch('foo', 'bar')); |
| 260 | 313 |
| 261 testResolve('dependency back onto root package with wrong version', { | 314 testResolve('with wrong version', { |
| 262 'myapp 1.0.0': { | 315 'myapp 1.0.0': { |
| 263 'foo': '1.0.0' | 316 'foo': '1.0.0' |
| 264 }, | 317 }, |
| 265 'foo 1.0.0': { | 318 'foo 1.0.0': { |
| 266 'myapp': '<1.0.0' | 319 'myapp': '<1.0.0' |
| 267 } | 320 } |
| 268 }, error: disjointConstraint(['foo'])); | 321 }, error: couldNotSolve); |
| 322 } | |
| 269 | 323 |
| 324 devDependency() { | |
| 325 testResolve("includes root package's dev dependencies", { | |
| 326 'myapp 1.0.0': { | |
| 327 '(dev) foo': '1.0.0', | |
| 328 '(dev) bar': '1.0.0' | |
| 329 }, | |
| 330 'foo 1.0.0': {}, | |
| 331 'bar 1.0.0': {} | |
| 332 }, result: { | |
| 333 'myapp from root': '1.0.0', | |
| 334 'foo': '1.0.0', | |
| 335 'bar': '1.0.0' | |
| 336 }); | |
| 337 | |
| 338 testResolve("includes dev dependency's transitive dependencies", { | |
| 339 'myapp 1.0.0': { | |
| 340 '(dev) foo': '1.0.0' | |
| 341 }, | |
| 342 'foo 1.0.0': { | |
| 343 'bar': '1.0.0' | |
| 344 }, | |
| 345 'bar 1.0.0': {} | |
| 346 }, result: { | |
| 347 'myapp from root': '1.0.0', | |
| 348 'foo': '1.0.0', | |
| 349 'bar': '1.0.0' | |
| 350 }); | |
| 351 | |
| 352 testResolve("ignores transitive dependency's dev dependencies", { | |
| 353 'myapp 1.0.0': { | |
| 354 'foo': '1.0.0' | |
| 355 }, | |
| 356 'foo 1.0.0': { | |
| 357 '(dev) bar': '1.0.0' | |
| 358 }, | |
| 359 'bar 1.0.0': {} | |
| 360 }, result: { | |
| 361 'myapp from root': '1.0.0', | |
| 362 'foo': '1.0.0' | |
| 363 }); | |
| 364 } | |
| 365 | |
| 366 unsolvable() { | |
|
nweiz
2013/04/03 00:28:43
Iteration counts seem very relevant for testing th
Bob Nystrom
2013/04/08 22:13:00
Done.
| |
| 270 testResolve('no version that matches requirement', { | 367 testResolve('no version that matches requirement', { |
| 271 'myapp 0.0.0': { | 368 'myapp 0.0.0': { |
| 272 'foo': '>=1.0.0 <2.0.0' | 369 'foo': '>=1.0.0 <2.0.0' |
| 273 }, | 370 }, |
| 274 'foo 2.0.0': {}, | 371 'foo 2.0.0': {}, |
| 275 'foo 2.1.3': {} | 372 'foo 2.1.3': {} |
| 276 }, error: noVersion(['myapp'])); | 373 }, error: noVersion(['myapp'])); |
| 277 | 374 |
| 278 testResolve('no version that matches combined constraint', { | 375 testResolve('no version that matches combined constraint', { |
| 279 'myapp 0.0.0': { | 376 'myapp 0.0.0': { |
| (...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 328 'foo 1.0.0': { | 425 'foo 1.0.0': { |
| 329 'shared': '1.0.0' | 426 'shared': '1.0.0' |
| 330 }, | 427 }, |
| 331 'bar 1.0.0': { | 428 'bar 1.0.0': { |
| 332 'shared from mock2': '1.0.0' | 429 'shared from mock2': '1.0.0' |
| 333 }, | 430 }, |
| 334 'shared 1.0.0': {}, | 431 'shared 1.0.0': {}, |
| 335 'shared 1.0.0 from mock2': {} | 432 'shared 1.0.0 from mock2': {} |
| 336 }, error: sourceMismatch('foo', 'bar')); | 433 }, error: sourceMismatch('foo', 'bar')); |
| 337 | 434 |
| 338 testResolve('unstable dependency graph', { | 435 testResolve('no valid solution', { |
| 436 'myapp 0.0.0': { | |
| 437 'a': 'any', | |
| 438 'b': 'any' | |
| 439 }, | |
| 440 'a 1.0.0': { | |
| 441 'b': '1.0.0' | |
| 442 }, | |
| 443 'a 2.0.0': { | |
| 444 'b': '2.0.0' | |
| 445 }, | |
| 446 'b 1.0.0': { | |
| 447 'a': '2.0.0' | |
| 448 }, | |
| 449 'b 2.0.0': { | |
| 450 'a': '1.0.0' | |
| 451 } | |
| 452 }, error: couldNotSolve); | |
| 453 } | |
| 454 | |
| 455 backtracking() { | |
| 456 testResolve('circular dependency on older version', { | |
| 339 'myapp 0.0.0': { | 457 'myapp 0.0.0': { |
| 340 'a': '>=1.0.0' | 458 'a': '>=1.0.0' |
| 341 }, | 459 }, |
| 342 'a 1.0.0': {}, | 460 'a 1.0.0': {}, |
| 343 'a 2.0.0': { | 461 'a 2.0.0': { |
| 344 'b': '1.0.0' | 462 'b': '1.0.0' |
| 345 }, | 463 }, |
| 346 'b 1.0.0': { | 464 'b 1.0.0': { |
| 347 'a': '1.0.0' | 465 'a': '1.0.0' |
| 348 } | 466 } |
| 349 }, error: couldNotSolve); | 467 }, result: { |
| 468 'myapp from root': '0.0.0', | |
| 469 'a': '1.0.0' | |
| 470 }, maxTries: 3); | |
| 350 | 471 |
| 351 group('dev dependencies', () { | 472 /// The latest versions of a and b disagree on c. An older version of either |
| 352 testResolve("includes root package's dev dependencies", { | 473 /// will resolve the problem. This test validates that b, which is farther |
| 353 'myapp 1.0.0': { | 474 /// in the dependency graph from myapp is downgraded first. |
| 354 '(dev) foo': '1.0.0', | 475 testResolve('rolls back leaf versions first', { |
| 355 '(dev) bar': '1.0.0' | 476 'myapp 0.0.0': { |
| 356 }, | 477 'a': 'any' |
| 357 'foo 1.0.0': {}, | 478 }, |
| 358 'bar 1.0.0': {} | 479 'a 1.0.0': { |
| 359 }, result: { | 480 'b': 'any' |
| 360 'myapp from root': '1.0.0', | 481 }, |
| 361 'foo': '1.0.0', | 482 'a 2.0.0': { |
| 362 'bar': '1.0.0' | 483 'b': 'any', |
| 363 }); | 484 'c': '2.0.0' |
| 485 }, | |
| 486 'b 1.0.0': {}, | |
| 487 'b 2.0.0': { | |
| 488 'c': '1.0.0' | |
| 489 }, | |
| 490 'c 1.0.0': {}, | |
| 491 'c 2.0.0': {} | |
| 492 }, result: { | |
| 493 'myapp from root': '0.0.0', | |
| 494 'a': '2.0.0', | |
| 495 'b': '1.0.0', | |
| 496 'c': '2.0.0' | |
| 497 }, maxTries: 3); | |
| 364 | 498 |
| 365 testResolve("includes dev dependency's transitive dependencies", { | 499 // Only one version of baz, so foo and bar will have to downgrade until they |
| 366 'myapp 1.0.0': { | 500 // reach it. |
| 367 '(dev) foo': '1.0.0' | 501 testResolve('simple transitive', { |
| 368 }, | 502 'myapp 0.0.0': {'foo': 'any'}, |
| 369 'foo 1.0.0': { | 503 'foo 1.0.0': {'bar': '1.0.0'}, |
| 370 'bar': '1.0.0' | 504 'foo 2.0.0': {'bar': '2.0.0'}, |
| 371 }, | 505 'foo 3.0.0': {'bar': '3.0.0'}, |
| 372 'bar 1.0.0': {} | 506 'bar 1.0.0': {'baz': 'any'}, |
| 373 }, result: { | 507 'bar 2.0.0': {'baz': '2.0.0'}, |
| 374 'myapp from root': '1.0.0', | 508 'bar 3.0.0': {'baz': '3.0.0'}, |
| 375 'foo': '1.0.0', | 509 'baz 1.0.0': {} |
| 376 'bar': '1.0.0' | 510 }, result: { |
| 377 }); | 511 'myapp from root': '0.0.0', |
| 512 'foo': '1.0.0', | |
| 513 'bar': '1.0.0', | |
| 514 'baz': '1.0.0' | |
| 515 }, maxTries: 5); | |
| 378 | 516 |
| 379 testResolve("ignores transitive dependency's dev dependencies", { | 517 // This sets up a hundred versions of foo and bar, 0.0.0 through 9.9.0. Each |
| 380 'myapp 1.0.0': { | 518 // version of foo depends on a baz with the same major version. Each version |
| 381 'foo': '1.0.0' | 519 // of bar depends on a baz with the same minor version. There is only one |
|
nweiz
2013/04/03 00:28:43
"the same minor version" is inaccurate; foo depend
Bob Nystrom
2013/04/08 22:13:00
Done.
| |
| 382 }, | 520 // version of baz, 0.0.0, so only older versions of foo and bar will |
| 383 'foo 1.0.0': { | 521 // satisfy it. |
| 384 '(dev) bar': '1.0.0' | 522 var map = { |
| 385 }, | 523 'myapp 0.0.0': { |
| 386 'bar 1.0.0': {} | 524 'foo': 'any', |
| 387 }, result: { | 525 'bar': 'any' |
| 388 'myapp from root': '1.0.0', | 526 }, |
| 389 'foo': '1.0.0' | 527 'baz 0.0.0': {} |
| 390 }); | 528 }; |
| 391 }); | 529 |
| 530 for (var i = 0; i < 10; i++) { | |
| 531 for (var j = 0; j < 10; j++) { | |
| 532 map['foo $i.$j.0'] = {'baz': '$i.0.0'}; | |
| 533 map['bar $i.$j.0'] = {'baz': '0.$j.0'}; | |
| 534 } | |
| 535 } | |
| 536 | |
| 537 testResolve('complex backtrack', map, result: { | |
| 538 'myapp from root': '0.0.0', | |
| 539 'foo': '0.9.0', | |
| 540 'bar': '9.0.0', | |
| 541 'baz': '0.0.0' | |
| 542 }, maxTries: 1090); // TODO(rnystrom): Is this acceptable? | |
| 392 } | 543 } |
| 393 | 544 |
| 394 // TODO(rnystrom): More stuff to test: | 545 testResolve(description, packages, |
| 395 // - Depending on a non-existent package. | 546 {lockfile, result, Matcher error, int maxTries, |
| 396 // - Test that only a certain number requests are sent to the mock source so we | 547 bool hasGreedySolution}) { |
| 397 // can keep track of server traffic. | 548 // Close over the top-level variable since it will be mutated. |
| 549 var allowBacktracking_ = allowBacktracking; | |
| 398 | 550 |
| 399 testResolve(description, packages, {lockfile, result, Matcher error}) { | 551 if (maxTries == null) maxTries = 1; |
| 552 if (hasGreedySolution == null) hasGreedySolution = false; | |
| 553 | |
| 554 if (!allowBacktracking_) { | |
| 555 // The greedy solver should fail any graph that does expect multiple tries | |
| 556 // and isn't explicitly annotated to have a greedy solution. | |
| 557 if (maxTries > 1 && !hasGreedySolution) { | |
|
nweiz
2013/04/03 00:28:43
It would be more readable if you assigned hasGreed
Bob Nystrom
2013/04/08 22:13:00
Done.
| |
| 558 result = null; | |
| 559 error = couldNotSolve; | |
| 560 } | |
| 561 } | |
| 562 | |
| 400 test(description, () { | 563 test(description, () { |
| 401 var cache = new SystemCache('.'); | 564 var cache = new SystemCache('.'); |
| 402 source1 = new MockSource('mock1'); | 565 source1 = new MockSource('mock1'); |
| 403 source2 = new MockSource('mock2'); | 566 source2 = new MockSource('mock2'); |
| 404 cache.register(source1); | 567 cache.register(source1); |
| 405 cache.register(source2); | 568 cache.register(source2); |
| 406 cache.sources.setDefault(source1.name); | 569 cache.sources.setDefault(source1.name); |
| 407 | 570 |
| 408 // Build the test package graph. | 571 // Build the test package graph. |
| 409 var root; | 572 var root; |
| 410 packages.forEach((nameVersion, dependencies) { | 573 packages.forEach((nameVersion, dependencies) { |
| 411 var parsed = parseSource(nameVersion, (isDev, nameVersion, source) { | 574 var parsed = parseSource(nameVersion, (isDev, nameVersion, source) { |
| 412 var parts = nameVersion.split(' '); | 575 var parts = nameVersion.split(' '); |
| 413 var name = parts[0]; | 576 var name = parts[0]; |
| 414 var version = parts[1]; | 577 var version = parts[1]; |
| 415 | 578 |
| 416 var package = source1.mockPackage(name, version, dependencies); | 579 var package = mockPackage(name, version, dependencies); |
| 417 if (name == 'myapp') { | 580 if (name == 'myapp') { |
| 418 // Don't add the root package to the server, so we can verify that Pub | 581 // 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 | 582 // doesn't try to look up information about the local package on the |
| 420 // remote server. | 583 // remote server. |
| 421 root = package; | 584 root = package; |
| 422 } else { | 585 } else { |
| 423 source.addPackage(package); | 586 source.addPackage(name, package); |
| 424 } | 587 } |
| 425 }); | 588 }); |
| 426 }); | 589 }); |
| 427 | 590 |
| 428 // Clean up the expectation. | 591 // Clean up the expectation. |
| 429 if (result != null) { | 592 if (result != null) { |
| 430 var newResult = {}; | 593 var newResult = {}; |
| 431 result.forEach((name, version) { | 594 result.forEach((name, version) { |
| 432 parseSource(name, (isDev, name, source) { | 595 parseSource(name, (isDev, name, source) { |
| 433 version = new Version.parse(version); | 596 version = new Version.parse(version); |
| 434 newResult[name] = new PackageId(name, source, version, name); | 597 newResult[name] = new PackageId(name, source, version, name); |
| 435 }); | 598 }); |
| 436 }); | 599 }); |
| 437 result = newResult; | 600 result = newResult; |
| 438 } | 601 } |
| 439 | 602 |
| 440 var realLockFile = new LockFile.empty(); | 603 var realLockFile = new LockFile.empty(); |
| 441 if (lockfile != null) { | 604 if (lockfile != null) { |
| 442 lockfile.forEach((name, version) { | 605 lockfile.forEach((name, version) { |
| 443 version = new Version.parse(version); | 606 version = new Version.parse(version); |
| 444 realLockFile.packages[name] = | 607 realLockFile.packages[name] = |
| 445 new PackageId(name, source1, version, name); | 608 new PackageId(name, source1, version, name); |
| 446 }); | 609 }); |
| 447 } | 610 } |
| 448 | 611 |
| 449 // Resolve the versions. | 612 // Resolve the versions. |
| 450 var future = resolveVersions(cache.sources, root, realLockFile); | 613 var future = resolveVersions(cache.sources, root, |
| 614 allowBacktracking: allowBacktracking_, lockFile: realLockFile); | |
| 451 | 615 |
| 452 if (result != null) { | 616 if (result != null) { |
| 453 expect(future, completion(predicate((actualResult) { | 617 expect(future, completion(predicate((actual) { |
| 454 for (var actualId in actualResult) { | 618 for (var actualId in actual.packages) { |
| 455 if (!result.containsKey(actualId.name)) return false; | 619 if (!result.containsKey(actualId.name)) return false; |
| 456 var expectedId = result.remove(actualId.name); | 620 var expectedId = result.remove(actualId.name); |
| 457 if (actualId != expectedId) return false; | 621 if (actualId != expectedId) return false; |
| 458 } | 622 } |
| 623 | |
| 459 return result.isEmpty; | 624 return result.isEmpty; |
| 625 | |
| 460 }, 'packages to match $result'))); | 626 }, 'packages to match $result'))); |
| 627 | |
| 628 expect(future, completion(predicate( | |
| 629 (actual) => actual.attemptedSolutions <= maxTries, | |
| 630 'does not backtrack too much'))); | |
| 461 } else if (error != null) { | 631 } else if (error != null) { |
| 462 expect(future, throwsA(error)); | 632 expect(future, throwsA(error)); |
| 463 } | 633 } |
| 464 }); | 634 }); |
| 465 } | 635 } |
| 466 | 636 |
| 467 /// A source used for testing. This both creates mock package objects and acts | 637 /// A source used for testing. This both creates mock package objects and acts |
| 468 /// as a source for them. | 638 /// as a source for them. |
| 469 /// | 639 /// |
| 470 /// In order to support testing packages that have the same name but different | 640 /// 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 | 641 /// descriptions, a package's name is calculated by taking the description |
| 472 /// string and stripping off any trailing hyphen followed by non-hyphen | 642 /// string and stripping off any trailing hyphen followed by non-hyphen |
| 473 /// characters. | 643 /// characters. |
| 474 class MockSource extends Source { | 644 class MockSource extends Source { |
| 475 final Map<String, Map<Version, Package>> _packages; | 645 final _packages = <String, Map<Version, Package>>{}; |
| 646 | |
| 647 /// Keeps track of which package version lists have been requested. Ensures | |
| 648 /// that a source is only hit once for a given package and that pub | |
| 649 /// internally caches the results. | |
| 650 final _requestedVersions = new Set<String>(); | |
| 651 | |
| 652 /// Keeps track of which package pubspecs have been requested. Ensures that a | |
| 653 /// source is only hit once for a given package and that pub internally | |
| 654 /// caches the results. | |
| 655 final _requestedPubspecs = new Map<String, Set<Version>>(); | |
| 476 | 656 |
| 477 final String name; | 657 final String name; |
| 478 bool get shouldCache => true; | 658 bool get shouldCache => true; |
| 479 | 659 |
| 480 MockSource(this.name) | 660 MockSource(this.name); |
| 481 : _packages = <String, Map<Version, Package>>{}; | |
| 482 | 661 |
| 483 Future<List<Version>> getVersions(String name, String description) { | 662 Future<List<Version>> getVersions(String name, String description) { |
| 484 return new Future.of(() => _packages[description].keys.toList()); | 663 return new Future.of(() { |
| 664 // Make sure the solver doesn't request the same thing twice. | |
| 665 if (_requestedVersions.contains(description)) { | |
| 666 throw 'Version list for $description was already requested.'; | |
| 667 } | |
| 668 | |
| 669 _requestedVersions.add(description); | |
| 670 | |
| 671 if (!_packages.containsKey(description)){ | |
| 672 throw 'MockSource does not have a package matching "$description".'; | |
| 673 } | |
| 674 return _packages[description].keys.toList(); | |
| 675 }); | |
| 485 } | 676 } |
| 486 | 677 |
| 487 Future<Pubspec> describe(PackageId id) { | 678 Future<Pubspec> describe(PackageId id) { |
| 488 return new Future.of(() => _packages[id.name][id.version].pubspec); | 679 return new Future.of(() { |
| 680 // Make sure the solver doesn't request the same thing twice. | |
| 681 if (_requestedPubspecs.containsKey(id.description) && | |
| 682 _requestedPubspecs[id.description].contains(id.version)) { | |
| 683 throw 'Pubspec for $id was already requested.'; | |
| 684 } | |
| 685 | |
| 686 _requestedPubspecs.putIfAbsent(id.description, () => new Set<Version>()); | |
| 687 _requestedPubspecs[id.description].add(id.version); | |
| 688 | |
| 689 return _packages[id.description][id.version].pubspec; | |
| 690 }); | |
| 489 } | 691 } |
| 490 | 692 |
| 491 Future<bool> install(PackageId id, String path) { | 693 Future<bool> install(PackageId id, String path) { |
| 492 throw 'no'; | 694 throw 'no'; |
| 493 } | 695 } |
| 494 | 696 |
| 495 Package mockPackage(String description, String version, | 697 void addPackage(String description, Package package) { |
| 496 Map dependencyStrings) { | 698 _packages.putIfAbsent(description, () => new Map<Version, Package>()); |
| 497 // Build the pubspec dependencies. | 699 _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 } | 700 } |
| 525 } | 701 } |
| 526 | 702 |
| 703 Package mockPackage(String description, String version, | |
| 704 Map dependencyStrings) { | |
| 705 // Build the pubspec dependencies. | |
| 706 var dependencies = <PackageRef>[]; | |
| 707 var devDependencies = <PackageRef>[]; | |
| 708 | |
| 709 dependencyStrings.forEach((name, constraint) { | |
| 710 parseSource(name, (isDev, name, source) { | |
| 711 var packageName = name.replaceFirst(new RegExp(r"-[^-]+$"), ""); | |
| 712 var ref = new PackageRef(packageName, source, | |
| 713 new VersionConstraint.parse(constraint), name); | |
| 714 | |
| 715 if (isDev) { | |
| 716 devDependencies.add(ref); | |
| 717 } else { | |
| 718 dependencies.add(ref); | |
| 719 } | |
| 720 }); | |
| 721 }); | |
| 722 | |
| 723 var name = description.replaceFirst(new RegExp(r"-[^-]+$"), ""); | |
| 724 var pubspec = new Pubspec( | |
| 725 name, new Version.parse(version), dependencies, devDependencies, | |
| 726 new PubspecEnvironment()); | |
| 727 return new Package.inMemory(pubspec); | |
| 728 } | |
| 729 | |
| 527 void parseSource(String description, | 730 void parseSource(String description, |
| 528 callback(bool isDev, String name, Source source)) { | 731 callback(bool isDev, String name, Source source)) { |
| 529 var isDev = false; | 732 var isDev = false; |
| 530 | 733 |
| 531 if (description.startsWith("(dev) ")) { | 734 if (description.startsWith("(dev) ")) { |
| 532 description = description.substring("(dev) ".length); | 735 description = description.substring("(dev) ".length); |
| 533 isDev = true; | 736 isDev = true; |
| 534 } | 737 } |
| 535 | 738 |
| 536 var name = description; | 739 var name = description; |
| 537 var source = source1; | 740 var source = source1; |
| 538 | 741 |
| 539 var sourceNames = { | 742 var sourceNames = { |
| 540 'mock1': source1, | 743 'mock1': source1, |
| 541 'mock2': source2, | 744 'mock2': source2, |
| 542 'root': null | 745 'root': null |
| 543 }; | 746 }; |
| 544 | 747 |
| 545 var match = new RegExp(r"(.*) from (.*)").firstMatch(description); | 748 var match = new RegExp(r"(.*) from (.*)").firstMatch(description); |
| 546 if (match != null) { | 749 if (match != null) { |
| 547 name = match[1]; | 750 name = match[1]; |
| 548 source = sourceNames[match[2]]; | 751 source = sourceNames[match[2]]; |
| 549 } | 752 } |
| 550 | 753 |
| 551 callback(isDev, name, source); | 754 callback(isDev, name, source); |
| 552 } | 755 } |
| OLD | NEW |