From a1a247064507621a9a719963f9dd823be7733724 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum <nweiz@google.com> Date: Thu, 21 Jul 2016 13:46:49 -0700 Subject: [PATCH] Refactor version_solver_test. This brings these tests in line with others, making them integration tests that invoke a pub process rather than testing the version solver APIs directly. This will make it easier to add Flutter support in the future (see #1431 and #1432). R=rnystrom@google.com Review URL: https://codereview.chromium.org//2172523002 . --- lib/src/sdk.dart | 2 +- lib/src/solver/backtracking_solver.dart | 1 + test/package_server.dart | 3 + test/test_pub.dart | 54 +- test/version_solver_test.dart | 2279 ++++++++++------------- 5 files changed, 990 insertions(+), 1349 deletions(-) diff --git a/lib/src/sdk.dart b/lib/src/sdk.dart index e2e48f2d..ca4d093f 100644 --- a/lib/src/sdk.dart +++ b/lib/src/sdk.dart @@ -29,7 +29,7 @@ final String rootDirectory = (() { /// /// This can be set so that the version solver tests can artificially select /// different SDK versions. -Version version = _getVersion(); +final version = _getVersion(); /// Determine the SDK's version number. Version _getVersion() { diff --git a/lib/src/solver/backtracking_solver.dart b/lib/src/solver/backtracking_solver.dart index 7f3cc432..45fccba0 100644 --- a/lib/src/solver/backtracking_solver.dart +++ b/lib/src/solver/backtracking_solver.dart @@ -189,6 +189,7 @@ class BacktrackingSolver { // Gather some solving metrics. var buffer = new StringBuffer(); buffer.writeln('${runtimeType} took ${stopwatch.elapsed} seconds.'); + buffer.writeln('- Tried $_attemptedSolutions solutions'); buffer.writeln(cache.describeResults()); log.solver(buffer); } diff --git a/test/package_server.dart b/test/package_server.dart index bfe29405..7561a5ae 100644 --- a/test/package_server.dart +++ b/test/package_server.dart @@ -74,6 +74,9 @@ class PackageServer { /// A future that will complete to the port used for the server. Future<int> get port => _inner.port; + /// A future that will complete to the URL for the server. + Future<String> get url async => 'http://localhost:${await port}'; + /// Creates an HTTP server that replicates the structure of pub.dartlang.org. /// /// Calls [callback] with a [PackageServerBuilder] that's used to specify diff --git a/test/test_pub.dart b/test/test_pub.dart index 6c2dd680..d23a3a1e 100644 --- a/test/test_pub.dart +++ b/test/test_pub.dart @@ -219,9 +219,10 @@ void scheduleSymlink(String target, String symlink) { /// Schedules a call to the Pub command-line utility. /// /// Runs Pub with [args] and validates that its results match [output] (or -/// [outputJson]), [error], and [exitCode]. +/// [outputJson]), [error], [silent] (for logs that are silent by default), and +/// [exitCode]. /// -/// [output] and [error] can be [String]s, [RegExp]s, or [Matcher]s. +/// [output], [error], and [silent] can be [String]s, [RegExp]s, or [Matcher]s. /// /// If [outputJson] is given, validates that pub outputs stringified JSON /// matching that object, which can be a literal JSON object or any other @@ -229,7 +230,7 @@ void scheduleSymlink(String target, String symlink) { /// /// If [environment] is given, any keys in it will override the environment /// variables passed to the spawned process. -void schedulePub({List args, output, error, outputJson, +void schedulePub({List args, output, error, outputJson, silent, int exitCode: exit_codes.SUCCESS, Map<String, String> environment}) { // Cannot pass both output and outputJson. assert(output == null || outputJson == null); @@ -237,30 +238,24 @@ void schedulePub({List args, output, error, outputJson, var pub = startPub(args: args, environment: environment); pub.shouldExit(exitCode); - var failures = []; - var stderr; - - expect(Future.wait([ - pub.stdoutStream().toList(), - pub.stderrStream().toList() - ]).then((results) { - var stdout = results[0].join("\n"); - stderr = results[1].join("\n"); + expect(() async { + var actualOutput = (await pub.stdoutStream().toList()).join("\n"); + var actualError = (await pub.stderrStream().toList()).join("\n"); + var actualSilent = (await pub.silentStream().toList()).join("\n"); + var failures = []; if (outputJson == null) { - _validateOutput(failures, 'stdout', output, stdout); - return null; + _validateOutput(failures, 'stdout', output, actualOutput); + } else { + _validateOutputJson( + failures, 'stdout', await awaitObject(outputJson), actualOutput); } - // Allow the expected JSON to contain futures. - return awaitObject(outputJson).then((resolved) { - _validateOutputJson(failures, 'stdout', resolved, stdout); - }); - }).then((_) { - _validateOutput(failures, 'stderr', error, stderr); + _validateOutput(failures, 'stderr', error, actualError); + _validateOutput(failures, 'silent', silent, actualSilent); if (!failures.isEmpty) throw new TestFailure(failures.join('\n')); - }), completes); + }(), completes); } /// Like [startPub], but runs `pub lish` in particular with [server] used both @@ -373,6 +368,7 @@ class PubProcess extends ScheduledProcess { Stream<Pair<log.Level, String>> _log; Stream<String> _stdout; Stream<String> _stderr; + Stream<String> _silent; PubProcess.start(executable, arguments, {workingDirectory, environment, String description, @@ -446,6 +442,22 @@ class PubProcess extends ScheduledProcess { _stderr = pair.first; return pair.last; } + + /// A stream of log messages that are silent by default. + Stream<String> silentStream() { + if (_silent == null) { + _silent = _logStream().expand((entry) { + if (entry.first == log.Level.MESSAGE) return []; + if (entry.first == log.Level.ERROR) return []; + if (entry.first == log.Level.WARNING) return []; + return [entry.last]; + }); + } + + var pair = tee(_silent); + _silent = pair.first; + return pair.last; + } } /// Fails the current test if Git is not installed. diff --git a/test/version_solver_test.dart b/test/version_solver_test.dart index da890621..4bb66d8a 100644 --- a/test/version_solver_test.dart +++ b/test/version_solver_test.dart @@ -4,6 +4,9 @@ import 'dart:async'; +import 'package:path/path.dart' as p; +import 'package:scheduled_test/scheduled_test.dart'; + import 'package:pub/src/lock_file.dart'; import 'package:pub/src/log.dart' as log; import 'package:pub/src/package.dart'; @@ -12,23 +15,16 @@ import 'package:pub/src/sdk.dart' as sdk; import 'package:pub/src/solver/version_solver.dart'; import 'package:pub/src/source.dart'; import 'package:pub/src/source/cached.dart'; +import 'package:pub/src/source/hosted.dart'; import 'package:pub/src/source_registry.dart'; import 'package:pub/src/system_cache.dart'; import 'package:pub/src/utils.dart'; import 'package:pub_semver/pub_semver.dart'; -import 'package:test/test.dart'; -MockSource source1; -MockSource source2; +import 'descriptor.dart' as d; +import 'test_pub.dart'; main() { - // Uncomment this to debug failing tests. - // log.verbosity = log.Verbosity.SOLVER; - - // Since this test isn't run from the SDK, it can't find the "version" file - // to load. Instead, just manually inject a version. - sdk.version = new Version(1, 2, 3); - group('basic graph', basicGraph); group('with lockfile', withLockFile); group('root dependency', rootDependency); @@ -43,553 +39,516 @@ main() { } void basicGraph() { - testResolve('no dependencies', { - 'myapp 0.0.0': {} - }, result: { - 'myapp from root': '0.0.0' + integration('no dependencies', () { + d.appDir().create(); + expectResolves(result: {}); }); - testResolve('simple dependency tree', { - 'myapp 0.0.0': { + integration('simple dependency tree', () { + servePackages((builder) { + builder.serve('a', '1.0.0', deps: {'aa': '1.0.0', 'ab': '1.0.0'}); + builder.serve('aa', '1.0.0'); + builder.serve('ab', '1.0.0'); + builder.serve('b', '1.0.0', deps: {'ba': '1.0.0', 'bb': '1.0.0'}); + builder.serve('ba', '1.0.0'); + builder.serve('bb', '1.0.0'); + }); + + d.appDir({'a': '1.0.0', 'b': '1.0.0'}).create(); + expectResolves(result: { 'a': '1.0.0', - 'b': '1.0.0' - }, - 'a 1.0.0': { 'aa': '1.0.0', - 'ab': '1.0.0' - }, - 'aa 1.0.0': {}, - 'ab 1.0.0': {}, - 'b 1.0.0': { + 'ab': '1.0.0', + 'b': '1.0.0', 'ba': '1.0.0', 'bb': '1.0.0' - }, - 'ba 1.0.0': {}, - 'bb 1.0.0': {} - }, result: { - 'myapp from root': '0.0.0', - 'a': '1.0.0', - 'aa': '1.0.0', - 'ab': '1.0.0', - 'b': '1.0.0', - 'ba': '1.0.0', - 'bb': '1.0.0' + }); }); - testResolve('shared dependency with overlapping constraints', { - 'myapp 0.0.0': { - 'a': '1.0.0', - 'b': '1.0.0' - }, - 'a 1.0.0': { - 'shared': '>=2.0.0 <4.0.0' - }, - 'b 1.0.0': { - 'shared': '>=3.0.0 <5.0.0' - }, - 'shared 2.0.0': {}, - 'shared 3.0.0': {}, - 'shared 3.6.9': {}, - 'shared 4.0.0': {}, - 'shared 5.0.0': {}, - }, result: { - 'myapp from root': '0.0.0', - 'a': '1.0.0', - 'b': '1.0.0', - 'shared': '3.6.9' + integration('shared dependency with overlapping constraints', () { + servePackages((builder) { + builder.serve('a', '1.0.0', deps: {'shared': '>=2.0.0 <4.0.0'}); + builder.serve('b', '1.0.0', deps: {'shared': '>=3.0.0 <5.0.0'}); + builder.serve('shared', '2.0.0'); + builder.serve('shared', '3.0.0'); + builder.serve('shared', '3.6.9'); + builder.serve('shared', '4.0.0'); + builder.serve('shared', '5.0.0'); + }); + + d.appDir({'a': '1.0.0', 'b': '1.0.0'}).create(); + expectResolves(result: {'a': '1.0.0', 'b': '1.0.0', 'shared': '3.6.9'}); }); - testResolve('shared dependency where dependent version in turn affects ' - 'other dependencies', { - 'myapp 0.0.0': { - 'foo': '<=1.0.2', - 'bar': '1.0.0' - }, - 'foo 1.0.0': {}, - 'foo 1.0.1': { 'bang': '1.0.0' }, - 'foo 1.0.2': { 'whoop': '1.0.0' }, - 'foo 1.0.3': { 'zoop': '1.0.0' }, - 'bar 1.0.0': { 'foo': '<=1.0.1' }, - 'bang 1.0.0': {}, - 'whoop 1.0.0': {}, - 'zoop 1.0.0': {} - }, result: { - 'myapp from root': '0.0.0', - 'foo': '1.0.1', - 'bar': '1.0.0', - 'bang': '1.0.0' - }, maxTries: 2); - - testResolve('circular dependency', { - 'myapp 1.0.0': { - 'foo': '1.0.0' - }, - 'foo 1.0.0': { - 'bar': '1.0.0' - }, - 'bar 1.0.0': { - 'foo': '1.0.0' - } - }, result: { - 'myapp from root': '1.0.0', - 'foo': '1.0.0', - 'bar': '1.0.0' + integration('shared dependency where dependent version in turn affects other ' + 'dependencies', () { + servePackages((builder) { + builder.serve('foo', '1.0.0'); + builder.serve('foo', '1.0.1', deps: {'bang': '1.0.0'}); + builder.serve('foo', '1.0.2', deps: {'whoop': '1.0.0'}); + builder.serve('foo', '1.0.3', deps: {'zoop': '1.0.0'}); + builder.serve('bar', '1.0.0', deps: {'foo': '<=1.0.1'}); + builder.serve('bang', '1.0.0'); + builder.serve('whoop', '1.0.0'); + builder.serve('zoop', '1.0.0'); + }); + + d.appDir({'foo': '<=1.0.2', 'bar': '1.0.0'}).create(); + expectResolves(result: {'foo': '1.0.1', 'bar': '1.0.0', 'bang': '1.0.0'}); }); - testResolve('removed dependency', { - 'myapp 1.0.0': { - 'foo': '1.0.0', - 'bar': 'any' - }, - 'foo 1.0.0': {}, - 'foo 2.0.0': {}, - 'bar 1.0.0': {}, - 'bar 2.0.0': { - 'baz': '1.0.0' - }, - 'baz 1.0.0': { - 'foo': '2.0.0' - } - }, result: { - 'myapp from root': '1.0.0', - 'foo': '1.0.0', - 'bar': '1.0.0' - }, maxTries: 2); + integration('circular dependency', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'bar': '1.0.0'}); + builder.serve('bar', '1.0.0', deps: {'foo': '1.0.0'}); + }); + + d.appDir({'foo': '1.0.0'}).create(); + expectResolves(result: {'foo': '1.0.0', 'bar': '1.0.0'}); + }); + + integration('removed dependency', () { + servePackages((builder) { + builder.serve('foo', '1.0.0'); + builder.serve('foo', '2.0.0'); + builder.serve('bar', '1.0.0'); + builder.serve('bar', '2.0.0', deps: {'baz': '1.0.0'}); + builder.serve('baz', '1.0.0', deps: {'foo': '2.0.0'}); + }); + + d.appDir({'foo': '1.0.0', 'bar': 'any'}).create(); + expectResolves(result: {'foo': '1.0.0', 'bar': '1.0.0'}, tries: 2); + }); } -withLockFile() { - testResolve('with compatible locked dependency', { - 'myapp 0.0.0': { - 'foo': 'any' - }, - 'foo 1.0.0': { 'bar': '1.0.0' }, - 'foo 1.0.1': { 'bar': '1.0.1' }, - 'foo 1.0.2': { 'bar': '1.0.2' }, - 'bar 1.0.0': {}, - 'bar 1.0.1': {}, - 'bar 1.0.2': {} - }, lockfile: { - 'foo': '1.0.1' - }, result: { - 'myapp from root': '0.0.0', - 'foo': '1.0.1', - 'bar': '1.0.1' +void withLockFile() { + integration('with compatible locked dependency', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'bar': '1.0.0'}); + builder.serve('foo', '1.0.1', deps: {'bar': '1.0.1'}); + builder.serve('foo', '1.0.2', deps: {'bar': '1.0.2'}); + builder.serve('bar', '1.0.0'); + builder.serve('bar', '1.0.1'); + builder.serve('bar', '1.0.2'); + }); + + d.appDir({'foo': '1.0.1'}).create(); + expectResolves(result: {'foo': '1.0.1', 'bar': '1.0.1'}); + + d.appDir({'foo': 'any'}).create(); + expectResolves(result: {'foo': '1.0.1', 'bar': '1.0.1'}); }); - testResolve('with incompatible locked dependency', { - 'myapp 0.0.0': { - 'foo': '>1.0.1' - }, - 'foo 1.0.0': { 'bar': '1.0.0' }, - 'foo 1.0.1': { 'bar': '1.0.1' }, - 'foo 1.0.2': { 'bar': '1.0.2' }, - 'bar 1.0.0': {}, - 'bar 1.0.1': {}, - 'bar 1.0.2': {} - }, lockfile: { - 'foo': '1.0.1' - }, result: { - 'myapp from root': '0.0.0', - 'foo': '1.0.2', - 'bar': '1.0.2' + integration('with incompatible locked dependency', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'bar': '1.0.0'}); + builder.serve('foo', '1.0.1', deps: {'bar': '1.0.1'}); + builder.serve('foo', '1.0.2', deps: {'bar': '1.0.2'}); + builder.serve('bar', '1.0.0'); + builder.serve('bar', '1.0.1'); + builder.serve('bar', '1.0.2'); + }); + + d.appDir({'foo': '1.0.1'}).create(); + expectResolves(result: {'foo': '1.0.1', 'bar': '1.0.1'}); + + d.appDir({'foo': '>1.0.1'}).create(); + expectResolves(result: {'foo': '1.0.2', 'bar': '1.0.2'}); }); - testResolve('with unrelated locked dependency', { - 'myapp 0.0.0': { - 'foo': 'any' - }, - 'foo 1.0.0': { 'bar': '1.0.0' }, - 'foo 1.0.1': { 'bar': '1.0.1' }, - 'foo 1.0.2': { 'bar': '1.0.2' }, - 'bar 1.0.0': {}, - 'bar 1.0.1': {}, - 'bar 1.0.2': {}, - 'baz 1.0.0': {} - }, lockfile: { - 'baz': '1.0.0' - }, result: { - 'myapp from root': '0.0.0', - 'foo': '1.0.2', - 'bar': '1.0.2' + integration('with unrelated locked dependency', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'bar': '1.0.0'}); + builder.serve('foo', '1.0.1', deps: {'bar': '1.0.1'}); + builder.serve('foo', '1.0.2', deps: {'bar': '1.0.2'}); + builder.serve('bar', '1.0.0'); + builder.serve('bar', '1.0.1'); + builder.serve('bar', '1.0.2'); + builder.serve('baz', '1.0.0'); + }); + + d.appDir({'baz': '1.0.0'}).create(); + expectResolves(result: {'baz': '1.0.0'}); + + d.appDir({'foo': 'any'}).create(); + expectResolves(result: {'foo': '1.0.2', 'bar': '1.0.2'}); }); - testResolve('unlocks dependencies if necessary to ensure that a new ' - 'dependency is satisfied', { - 'myapp 0.0.0': { - 'foo': 'any', - 'newdep': 'any' - }, - 'foo 1.0.0': { 'bar': '<2.0.0' }, - 'bar 1.0.0': { 'baz': '<2.0.0' }, - 'baz 1.0.0': { 'qux': '<2.0.0' }, - 'qux 1.0.0': {}, - 'foo 2.0.0': { 'bar': '<3.0.0' }, - 'bar 2.0.0': { 'baz': '<3.0.0' }, - 'baz 2.0.0': { 'qux': '<3.0.0' }, - 'qux 2.0.0': {}, - 'newdep 2.0.0': { 'baz': '>=1.5.0' } - }, lockfile: { - 'foo': '1.0.0', - 'bar': '1.0.0', - 'baz': '1.0.0', - 'qux': '1.0.0' - }, result: { - 'myapp from root': '0.0.0', - 'foo': '2.0.0', - 'bar': '2.0.0', - 'baz': '2.0.0', - 'qux': '1.0.0', - 'newdep': '2.0.0' - }, maxTries: 4); + integration('unlocks dependencies if necessary to ensure that a new ' + 'dependency is satisfied', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'bar': '<2.0.0'}); + builder.serve('bar', '1.0.0', deps: {'baz': '<2.0.0'}); + builder.serve('baz', '1.0.0', deps: {'qux': '<2.0.0'}); + builder.serve('qux', '1.0.0'); + builder.serve('foo', '2.0.0', deps: {'bar': '<3.0.0'}); + builder.serve('bar', '2.0.0', deps: {'baz': '<3.0.0'}); + builder.serve('baz', '2.0.0', deps: {'qux': '<3.0.0'}); + builder.serve('qux', '2.0.0'); + builder.serve('newdep', '2.0.0', deps: {'baz': '>=1.5.0'}); + }); + + d.appDir({'foo': '1.0.0'}).create(); + expectResolves(result: { + 'foo': '1.0.0', + 'bar': '1.0.0', + 'baz': '1.0.0', + 'qux': '1.0.0' + }); + + d.appDir({'foo': 'any', 'newdep': '2.0.0'}).create(); + expectResolves(result: { + 'foo': '2.0.0', + 'bar': '2.0.0', + 'baz': '2.0.0', + 'qux': '1.0.0', + 'newdep': '2.0.0' + }, tries: 4); + }); } -rootDependency() { - testResolve('with root source', { - 'myapp 1.0.0': { - 'foo': '1.0.0' - }, - 'foo 1.0.0': { - 'myapp from root': '>=1.0.0' - } - }, result: { - 'myapp from root': '1.0.0', - 'foo': '1.0.0' +void rootDependency() { + integration('with root source', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'myapp': 'any'}); + }); + + d.appDir({'foo': '1.0.0'}).create(); + expectResolves(result: {'foo': '1.0.0'}); }); - testResolve('with different source', { - 'myapp 1.0.0': { - 'foo': '1.0.0' - }, - 'foo 1.0.0': { - 'myapp': '>=1.0.0' - } - }, result: { - 'myapp from root': '1.0.0', - 'foo': '1.0.0' + integration('with mismatched sources', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'myapp': 'any'}); + builder.serve('bar', '1.0.0', deps: {'myapp': {'git': 'nowhere'}}); + }); + + d.appDir({'foo': '1.0.0', 'bar': '1.0.0'}).create(); + expectResolves( + error: "Incompatible dependencies on myapp:\n" + "- bar 1.0.0 depends on it from source git\n" + "- foo 1.0.0 depends on it from source hosted"); }); - testResolve('with mismatched sources', { - 'myapp 1.0.0': { - 'foo': '1.0.0', - 'bar': '1.0.0' - }, - 'foo 1.0.0': { - 'myapp': '>=1.0.0' - }, - 'bar 1.0.0': { - 'myapp from mock2': '>=1.0.0' - } - }, error: sourceMismatch('myapp', 'foo', 'bar')); - - testResolve('with wrong version', { - 'myapp 1.0.0': { - 'foo': '1.0.0' - }, - 'foo 1.0.0': { - 'myapp': '<1.0.0' - } - }, error: couldNotSolve); + integration('with wrong version', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'myapp': '>0.0.0'}); + }); + + d.appDir({'foo': '1.0.0'}).create(); + expectResolves( + error: "Package myapp has no versions that match >0.0.0 derived from:\n" + "- foo 1.0.0 depends on version >0.0.0"); + }); } -devDependency() { - testResolve("includes root package's dev dependencies", { - 'myapp 1.0.0': { - '(dev) foo': '1.0.0', - '(dev) bar': '1.0.0' - }, - 'foo 1.0.0': {}, - 'bar 1.0.0': {} - }, result: { - 'myapp from root': '1.0.0', - 'foo': '1.0.0', - 'bar': '1.0.0' +void devDependency() { + integration("includes root package's dev dependencies", () { + servePackages((builder) { + builder.serve('foo', '1.0.0'); + builder.serve('bar', '1.0.0'); + }); + + d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'dev_dependencies': { + 'foo': '1.0.0', + 'bar': '1.0.0' + } + }) + ]).create(); + + expectResolves(result: {'foo': '1.0.0', 'bar': '1.0.0'}); }); - testResolve("includes dev dependency's transitive dependencies", { - 'myapp 1.0.0': { - '(dev) foo': '1.0.0' - }, - 'foo 1.0.0': { - 'bar': '1.0.0' - }, - 'bar 1.0.0': {} - }, result: { - 'myapp from root': '1.0.0', - 'foo': '1.0.0', - 'bar': '1.0.0' + integration("includes dev dependency's transitive dependencies", () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'bar': '1.0.0'}); + builder.serve('bar', '1.0.0'); + }); + + d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'dev_dependencies': {'foo': '1.0.0'} + }) + ]).create(); + + expectResolves(result: {'foo': '1.0.0', 'bar': '1.0.0'}); }); - testResolve("ignores transitive dependency's dev dependencies", { - 'myapp 1.0.0': { - 'foo': '1.0.0' - }, - 'foo 1.0.0': { - '(dev) bar': '1.0.0' - }, - 'bar 1.0.0': {} - }, result: { - 'myapp from root': '1.0.0', - 'foo': '1.0.0' + integration("ignores transitive dependency's dev dependencies", () { + servePackages((builder) { + builder.serve('foo', '1.0.0', pubspec: { + 'dev_dependencies': {'bar': '1.0.0'} + }); + }); + + d.appDir({'foo': '1.0.0'}).create(); + expectResolves(result: {'foo': '1.0.0'}); }); } -unsolvable() { - testResolve('no version that matches requirement', { - 'myapp 0.0.0': { - 'foo': '>=1.0.0 <2.0.0' - }, - 'foo 2.0.0': {}, - 'foo 2.1.3': {} - }, error: noVersion(['myapp', 'foo'])); - - testResolve('no version that matches combined constraint', { - 'myapp 0.0.0': { - 'foo': '1.0.0', - 'bar': '1.0.0' - }, - 'foo 1.0.0': { - 'shared': '>=2.0.0 <3.0.0' - }, - 'bar 1.0.0': { - 'shared': '>=2.9.0 <4.0.0' - }, - 'shared 2.5.0': {}, - 'shared 3.5.0': {} - }, error: noVersion(['shared', 'foo', 'bar'])); - - testResolve('disjoint constraints', { - 'myapp 0.0.0': { - 'foo': '1.0.0', - 'bar': '1.0.0' - }, - 'foo 1.0.0': { - 'shared': '<=2.0.0' - }, - 'bar 1.0.0': { - 'shared': '>3.0.0' - }, - 'shared 2.0.0': {}, - 'shared 4.0.0': {} - }, error: disjointConstraint(['shared', 'foo', 'bar'])); - - testResolve('mismatched descriptions', { - 'myapp 0.0.0': { - 'foo': '1.0.0', - 'bar': '1.0.0' - }, - 'foo 1.0.0': { - 'shared-x': '1.0.0' - }, - 'bar 1.0.0': { - 'shared-y': '1.0.0' - }, - 'shared-x 1.0.0': {}, - 'shared-y 1.0.0': {} - }, error: descriptionMismatch('shared', 'foo', 'bar')); - - testResolve('mismatched sources', { - 'myapp 0.0.0': { - 'foo': '1.0.0', - 'bar': '1.0.0' - }, - 'foo 1.0.0': { - 'shared': '1.0.0' - }, - 'bar 1.0.0': { - 'shared from mock2': '1.0.0' - }, - 'shared 1.0.0': {}, - 'shared 1.0.0 from mock2': {} - }, error: sourceMismatch('shared', 'foo', 'bar')); - - testResolve('no valid solution', { - 'myapp 0.0.0': { - 'a': 'any', - 'b': 'any' - }, - 'a 1.0.0': { - 'b': '1.0.0' - }, - 'a 2.0.0': { - 'b': '2.0.0' - }, - 'b 1.0.0': { - 'a': '2.0.0' - }, - 'b 2.0.0': { - 'a': '1.0.0' - } - }, error: couldNotSolve, maxTries: 2); +void unsolvable() { + integration('no version that matches constraint', () { + servePackages((builder) { + builder.serve('foo', '2.0.0'); + builder.serve('foo', '2.1.3'); + }); + + d.appDir({'foo': '>=1.0.0 <2.0.0'}).create(); + expectResolves( + error: 'Package foo has no versions that match >=1.0.0 <2.0.0 derived ' + 'from:\n' + '- myapp depends on version >=1.0.0 <2.0.0'); + }); + + integration('no version that matches combined constraint', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'shared': '>=2.0.0 <3.0.0'}); + builder.serve('bar', '1.0.0', deps: {'shared': '>=2.9.0 <4.0.0'}); + builder.serve('shared', '2.5.0'); + builder.serve('shared', '3.5.0'); + }); + + d.appDir({'foo': '1.0.0', 'bar': '1.0.0'}).create(); + expectResolves( + error: 'Package shared has no versions that match >=2.9.0 <3.0.0 ' + 'derived from:\n' + '- bar 1.0.0 depends on version >=2.9.0 <4.0.0\n' + '- foo 1.0.0 depends on version >=2.0.0 <3.0.0'); + }); + + integration('disjoint constraints', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'shared': '<=2.0.0'}); + builder.serve('bar', '1.0.0', deps: {'shared': '>3.0.0'}); + builder.serve('shared', '2.0.0'); + builder.serve('shared', '4.0.0'); + }); + + d.appDir({'foo': '1.0.0', 'bar': '1.0.0'}).create(); + expectResolves( + error: 'Incompatible version constraints on shared:\n' + '- bar 1.0.0 depends on version >3.0.0\n' + '- foo 1.0.0 depends on version <=2.0.0'); + }); + + integration('mismatched descriptions', () { + var otherServer = new PackageServer((builder) { + builder.serve('shared', '1.0.0'); + }); + + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'shared': '1.0.0'}); + builder.serve('bar', '1.0.0', deps: { + 'shared': { + 'hosted': {'name': 'shared', 'url': otherServer.url}, + 'version': '1.0.0' + } + }); + builder.serve('shared', '1.0.0'); + }); + + d.appDir({'foo': '1.0.0', 'bar': '1.0.0'}).create(); + expectResolves(error: allOf([ + contains('Incompatible dependencies on shared:'), + contains('- bar 1.0.0 depends on it with description'), + contains('- foo 1.0.0 depends on it with description "shared"') + ])); + }); + + integration('mismatched sources', () { + d.dir('shared', [d.libPubspec('shared', '1.0.0')]).create(); + + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'shared': '1.0.0'}); + builder.serve('bar', '1.0.0', deps: { + 'shared': {'path': p.join(sandboxDir, 'shared')} + }); + builder.serve('shared', '1.0.0'); + }); + + d.appDir({'foo': '1.0.0', 'bar': '1.0.0'}).create(); + expectResolves( + error: 'Incompatible dependencies on shared:\n' + '- bar 1.0.0 depends on it from source path\n' + '- foo 1.0.0 depends on it from source hosted'); + }); + + integration('no valid solution', () { + servePackages((builder) { + builder.serve('a', '1.0.0', deps: {'b': '1.0.0'}); + builder.serve('a', '2.0.0', deps: {'b': '2.0.0'}); + builder.serve('b', '1.0.0', deps: {'a': '2.0.0'}); + builder.serve('b', '2.0.0', deps: {'a': '1.0.0'}); + }); + + d.appDir({'a': 'any', 'b': 'any'}).create(); + expectResolves( + error: 'Package a has no versions that match 2.0.0 derived from:\n' + '- b 1.0.0 depends on version 2.0.0\n' + '- myapp depends on version any', + tries: 2); + }); // This is a regression test for #15550. - testResolve('no version that matches while backtracking', { - 'myapp 0.0.0': { - 'a': 'any', - 'b': '>1.0.0' - }, - 'a 1.0.0': {}, - 'b 1.0.0': {} - }, error: noVersion(['myapp', 'b']), maxTries: 1); + integration('no version that matches while backtracking', () { + servePackages((builder) { + builder.serve('a', '1.0.0'); + builder.serve('b', '1.0.0'); + }); + d.appDir({'a': 'any', 'b': '>1.0.0'}).create(); + expectResolves( + error: 'Package b has no versions that match >1.0.0 derived from:\n' + '- myapp depends on version >1.0.0'); + }); // This is a regression test for #18300. - testResolve('...', { - "myapp 0.0.0": { - "angular": "any", - "collection": "any" - }, - "analyzer 0.12.2": {}, - "angular 0.10.0": { - "di": ">=0.0.32 <0.1.0", - "collection": ">=0.9.1 <1.0.0" - }, - "angular 0.9.11": { - "di": ">=0.0.32 <0.1.0", - "collection": ">=0.9.1 <1.0.0" - }, - "angular 0.9.10": { - "di": ">=0.0.32 <0.1.0", - "collection": ">=0.9.1 <1.0.0" - }, - "collection 0.9.0": {}, - "collection 0.9.1": {}, - "di 0.0.37": {"analyzer": ">=0.13.0 <0.14.0"}, - "di 0.0.36": {"analyzer": ">=0.13.0 <0.14.0"} - }, error: noVersion(['analyzer', 'di']), maxTries: 2); + integration('issue 18300', () { + servePackages((builder) { + builder.serve('analyzer', '0.12.2'); + builder.serve('angular', '0.10.0', deps: { + 'di': '>=0.0.32 <0.1.0', + 'collection': '>=0.9.1 <1.0.0' + }); + builder.serve('angular', '0.9.11', deps: { + 'di': '>=0.0.32 <0.1.0', + 'collection': '>=0.9.1 <1.0.0' + }); + builder.serve('angular', '0.9.10', deps: { + 'di': '>=0.0.32 <0.1.0', + 'collection': '>=0.9.1 <1.0.0' + }); + builder.serve('collection', '0.9.0'); + builder.serve('collection', '0.9.1'); + builder.serve('di', '0.0.37', deps: {'analyzer': '>=0.13.0 <0.14.0'}); + builder.serve('di', '0.0.36', deps: {'analyzer': '>=0.13.0 <0.14.0'}); + }); + + d.appDir({'angular': 'any', 'collection': 'any'}).create(); + expectResolves( + error: 'Package analyzer has no versions that match >=0.13.0 <0.14.0 ' + 'derived from:\n' + '- di 0.0.36 depends on version >=0.13.0 <0.14.0', + tries: 2); + }); } -badSource() { - testResolve('fail if the root package has a bad source in dep', { - 'myapp 0.0.0': { - 'foo from bad': 'any' - }, - }, error: unknownSource('myapp', 'foo', 'bad')); - - testResolve('fail if the root package has a bad source in dev dep', { - 'myapp 0.0.0': { - '(dev) foo from bad': 'any' - }, - }, error: unknownSource('myapp', 'foo', 'bad')); - - testResolve('fail if all versions have bad source in dep', { - 'myapp 0.0.0': { - 'foo': 'any' - }, - 'foo 1.0.0': { - 'bar from bad': 'any' - }, - 'foo 1.0.1': { - 'baz from bad': 'any' - }, - 'foo 1.0.3': { - 'bang from bad': 'any' - }, - }, error: unknownSource('foo', 'bar', 'bad'), maxTries: 3); - - testResolve('ignore versions with bad source in dep', { - 'myapp 1.0.0': { - 'foo': 'any' - }, - 'foo 1.0.0': { - 'bar': 'any' - }, - 'foo 1.0.1': { - 'bar from bad': 'any' - }, - 'foo 1.0.3': { - 'bar from bad': 'any' - }, - 'bar 1.0.0': {} - }, result: { - 'myapp from root': '1.0.0', - 'foo': '1.0.0', - 'bar': '1.0.0' - }, maxTries: 3); +void badSource() { + integration('fail if the root package has a bad source in dep', () { + d.appDir({'foo': {'bad': 'any'}}).create(); + expectResolves( + error: 'Package myapp depends on foo from unknown source "bad".'); + }); + + integration('fail if the root package has a bad source in dev dep', () { + d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'dev_dependencies': {'foo': {'bad': 'any'}} + }) + ]).create(); + + expectResolves( + error: 'Package myapp depends on foo from unknown source "bad".'); + }); + + integration('fail if all versions have bad source in dep', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'bar': {'bad': 'any'}}); + builder.serve('foo', '1.0.1', deps: {'baz': {'bad': 'any'}}); + builder.serve('foo', '1.0.2', deps: {'bang': {'bad': 'any'}}); + }); + + d.appDir({'foo': 'any'}).create(); + expectResolves( + error: 'Package foo depends on bar from unknown source "bad".'); + }); + + integration('ignore versions with bad source in dep', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'bar': 'any'}); + builder.serve('foo', '1.0.1', deps: {'bar': {'bad': 'any'}}); + builder.serve('foo', '1.0.2', deps: {'bar': {'bad': 'any'}}); + builder.serve('bar', '1.0.0'); + }); + + d.appDir({'foo': 'any'}).create(); + expectResolves(result: {'foo': '1.0.0', 'bar': '1.0.0'}); + }); } -backtracking() { - testResolve('circular dependency on older version', { - 'myapp 0.0.0': { - 'a': '>=1.0.0' - }, - 'a 1.0.0': {}, - 'a 2.0.0': { - 'b': '1.0.0' - }, - 'b 1.0.0': { - 'a': '1.0.0' - } - }, result: { - 'myapp from root': '0.0.0', - 'a': '1.0.0' - }, maxTries: 2); +void backtracking() { + integration('circular dependency on older version', () { + servePackages((builder) { + builder.serve('a', '1.0.0'); + builder.serve('a', '2.0.0', deps: {'b': '1.0.0'}); + builder.serve('b', '1.0.0', deps: {'a': '1.0.0'}); + }); + + d.appDir({'a': '>=1.0.0'}).create(); + expectResolves(result: {'a': '1.0.0'}, tries: 2); + }); // The latest versions of a and b disagree on c. An older version of either // will resolve the problem. This test validates that b, which is farther // in the dependency graph from myapp is downgraded first. - testResolve('rolls back leaf versions first', { - 'myapp 0.0.0': { - 'a': 'any' - }, - 'a 1.0.0': { - 'b': 'any' - }, - 'a 2.0.0': { - 'b': 'any', - 'c': '2.0.0' - }, - 'b 1.0.0': {}, - 'b 2.0.0': { - 'c': '1.0.0' - }, - 'c 1.0.0': {}, - 'c 2.0.0': {} - }, result: { - 'myapp from root': '0.0.0', - 'a': '2.0.0', - 'b': '1.0.0', - 'c': '2.0.0' - }, maxTries: 2); + integration('rolls back leaf versions first', () { + servePackages((builder) { + builder.serve('a', '1.0.0', deps: {'b': 'any'}); + builder.serve('a', '2.0.0', deps: {'b': 'any', 'c': '2.0.0'}); + builder.serve('b', '1.0.0'); + builder.serve('b', '2.0.0', deps: {'c': '1.0.0'}); + builder.serve('c', '1.0.0'); + builder.serve('c', '2.0.0'); + }); + + d.appDir({'a': 'any'}).create(); + expectResolves( + result: {'a': '2.0.0', 'b': '1.0.0', 'c': '2.0.0'}); + }); // Only one version of baz, so foo and bar will have to downgrade until they // reach it. - testResolve('simple transitive', { - 'myapp 0.0.0': {'foo': 'any'}, - 'foo 1.0.0': {'bar': '1.0.0'}, - 'foo 2.0.0': {'bar': '2.0.0'}, - 'foo 3.0.0': {'bar': '3.0.0'}, - 'bar 1.0.0': {'baz': 'any'}, - 'bar 2.0.0': {'baz': '2.0.0'}, - 'bar 3.0.0': {'baz': '3.0.0'}, - 'baz 1.0.0': {} - }, result: { - 'myapp from root': '0.0.0', - 'foo': '1.0.0', - 'bar': '1.0.0', - 'baz': '1.0.0' - }, maxTries: 3); + integration('simple transitive', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'bar': '1.0.0'}); + builder.serve('foo', '2.0.0', deps: {'bar': '2.0.0'}); + builder.serve('foo', '3.0.0', deps: {'bar': '3.0.0'}); + builder.serve('bar', '1.0.0', deps: {'baz': 'any'}); + builder.serve('bar', '2.0.0', deps: {'baz': '2.0.0'}); + builder.serve('bar', '3.0.0', deps: {'baz': '3.0.0'}); + builder.serve('baz', '1.0.0'); + }); + + d.appDir({'foo': 'any'}).create(); + expectResolves( + result: {'foo': '1.0.0', 'bar': '1.0.0', 'baz': '1.0.0'}, + tries: 3); + }); // This ensures it doesn't exhaustively search all versions of b when it's // a-2.0.0 whose dependency on c-2.0.0-nonexistent led to the problem. We // make sure b has more versions than a so that the solver tries a first // since it sorts sibling dependencies by number of versions. - testResolve('backjump to nearer unsatisfied package', { - 'myapp 0.0.0': { - 'a': 'any', - 'b': 'any' - }, - 'a 1.0.0': { 'c': '1.0.0' }, - 'a 2.0.0': { 'c': '2.0.0-nonexistent' }, - 'b 1.0.0': {}, - 'b 2.0.0': {}, - 'b 3.0.0': {}, - 'c 1.0.0': {}, - }, result: { - 'myapp from root': '0.0.0', - 'a': '1.0.0', - 'b': '3.0.0', - 'c': '1.0.0' - }, maxTries: 2); + integration('backjump to nearer unsatisfied package', () { + servePackages((builder) { + builder.serve('a', '1.0.0', deps: {'c': '1.0.0'}); + builder.serve('a', '2.0.0', deps: {'c': '2.0.0-nonexistent'}); + builder.serve('b', '1.0.0'); + builder.serve('b', '2.0.0'); + builder.serve('b', '3.0.0'); + builder.serve('c', '1.0.0'); + }); + + d.appDir({'a': 'any', 'b': 'any'}).create(); + expectResolves( + result: {'a': '1.0.0', 'b': '3.0.0', 'c': '1.0.0'}, + tries: 2); + }); // Tests that the backjumper will jump past unrelated selections when a // source conflict occurs. This test selects, in order: @@ -605,124 +564,121 @@ backtracking() { // This means it doesn't discover the source conflict until after selecting // c. When that happens, it should backjump past c instead of trying older // versions of it since they aren't related to the conflict. - testResolve('backjump to conflicting source', { - 'myapp 0.0.0': { - 'a': 'any', - 'b': 'any', - 'c': 'any' - }, - 'a 1.0.0': {}, - 'a 1.0.0 from mock2': {}, - 'b 1.0.0': { - 'a': 'any' - }, - 'b 2.0.0': { - 'a from mock2': 'any' - }, - 'c 1.0.0': {}, - 'c 2.0.0': {}, - 'c 3.0.0': {}, - 'c 4.0.0': {}, - 'c 5.0.0': {}, - }, result: { - 'myapp from root': '0.0.0', - 'a': '1.0.0', - 'b': '1.0.0', - 'c': '5.0.0' - }, maxTries: 2); + integration('successful backjump to conflicting source', () { + d.dir('a', [d.libPubspec('a', '1.0.0')]).create(); + + servePackages((builder) { + builder.serve('a', '1.0.0'); + builder.serve('b', '1.0.0', deps: {'a': 'any'}); + builder.serve('b', '2.0.0', deps: { + 'a': {'path': p.join(sandboxDir, 'a')} + }); + builder.serve('c', '1.0.0'); + builder.serve('c', '2.0.0'); + builder.serve('c', '3.0.0'); + builder.serve('c', '4.0.0'); + builder.serve('c', '5.0.0'); + }); + + d.appDir({'a': 'any', 'b': 'any', 'c': 'any'}).create(); + expectResolves(result: {'a': '1.0.0', 'b': '1.0.0', 'c': '5.0.0'}); + }); // Like the above test, but for a conflicting description. - testResolve('backjump to conflicting description', { - 'myapp 0.0.0': { - 'a-x': 'any', - 'b': 'any', - 'c': 'any' - }, - 'a-x 1.0.0': {}, - 'a-y 1.0.0': {}, - 'b 1.0.0': { - 'a-x': 'any' - }, - 'b 2.0.0': { - 'a-y': 'any' - }, - 'c 1.0.0': {}, - 'c 2.0.0': {}, - 'c 3.0.0': {}, - 'c 4.0.0': {}, - 'c 5.0.0': {}, - }, result: { - 'myapp from root': '0.0.0', - 'a-x': '1.0.0', - 'b': '1.0.0', - 'c': '5.0.0' - }, maxTries: 2); + integration('successful backjump to conflicting description', () { + var otherServer = new PackageServer((builder) { + builder.serve('a', '1.0.0'); + }); + + servePackages((builder) { + builder.serve('a', '1.0.0'); + builder.serve('b', '1.0.0', deps: {'a': 'any'}); + builder.serve('b', '2.0.0', deps: { + 'a': {'hosted': {'name': 'a', 'url': otherServer.url}} + }); + builder.serve('c', '1.0.0'); + builder.serve('c', '2.0.0'); + builder.serve('c', '3.0.0'); + builder.serve('c', '4.0.0'); + builder.serve('c', '5.0.0'); + }); + + d.appDir({'a': 'any', 'b': 'any', 'c': 'any'}).create(); + expectResolves(result: {'a': '1.0.0', 'b': '1.0.0', 'c': '5.0.0'}); + }); // Similar to the above two tests but where there is no solution. It should // fail in this case with no backtracking. - testResolve('backjump to conflicting source', { - 'myapp 0.0.0': { - 'a': 'any', - 'b': 'any', - 'c': 'any' - }, - 'a 1.0.0': {}, - 'a 1.0.0 from mock2': {}, - 'b 1.0.0': { - 'a from mock2': 'any' - }, - 'c 1.0.0': {}, - 'c 2.0.0': {}, - 'c 3.0.0': {}, - 'c 4.0.0': {}, - 'c 5.0.0': {}, - }, error: sourceMismatch('a', 'myapp', 'b'), maxTries: 1); - - testResolve('backjump to conflicting description', { - 'myapp 0.0.0': { - 'a-x': 'any', - 'b': 'any', - 'c': 'any' - }, - 'a-x 1.0.0': {}, - 'a-y 1.0.0': {}, - 'b 1.0.0': { - 'a-y': 'any' - }, - 'c 1.0.0': {}, - 'c 2.0.0': {}, - 'c 3.0.0': {}, - 'c 4.0.0': {}, - 'c 5.0.0': {}, - }, error: descriptionMismatch('a', 'myapp', 'b'), maxTries: 1); + integration('failing backjump to conflicting source', () { + d.dir('a', [d.libPubspec('a', '1.0.0')]).create(); + + servePackages((builder) { + builder.serve('a', '1.0.0'); + builder.serve('b', '1.0.0', deps: { + 'a': {'path': p.join(sandboxDir, 'shared')} + }); + builder.serve('c', '1.0.0'); + builder.serve('c', '2.0.0'); + builder.serve('c', '3.0.0'); + builder.serve('c', '4.0.0'); + builder.serve('c', '5.0.0'); + }); + + d.appDir({'a': 'any', 'b': 'any', 'c': 'any'}).create(); + expectResolves( + error: 'Incompatible dependencies on a:\n' + '- b 1.0.0 depends on it from source path\n' + '- myapp depends on it from source hosted'); + }); + + integration('failing backjump to conflicting description', () { + var otherServer = new PackageServer((builder) { + builder.serve('a', '1.0.0'); + }); + + servePackages((builder) { + builder.serve('a', '1.0.0'); + builder.serve('b', '1.0.0', deps: { + 'a': {'hosted': {'name': 'a', 'url': otherServer.url}} + }); + builder.serve('c', '1.0.0'); + builder.serve('c', '2.0.0'); + builder.serve('c', '3.0.0'); + builder.serve('c', '4.0.0'); + builder.serve('c', '5.0.0'); + }); + + d.appDir({'a': 'any', 'b': 'any', 'c': 'any'}).create(); + expectResolves(error: allOf([ + contains('Incompatible dependencies on a:'), + contains('- b 1.0.0 depends on it with description'), + contains('- myapp depends on it with description "a"') + ])); + }); // Dependencies are ordered so that packages with fewer versions are tried // first. Here, there are two valid solutions (either a or b must be // downgraded once). The chosen one depends on which dep is traversed first. // Since b has fewer versions, it will be traversed first, which means a will // come later. Since later selections are revised first, a gets downgraded. - testResolve('traverse into package with fewer versions first', { - 'myapp 0.0.0': { - 'a': 'any', - 'b': 'any' - }, - 'a 1.0.0': {'c': 'any'}, - 'a 2.0.0': {'c': 'any'}, - 'a 3.0.0': {'c': 'any'}, - 'a 4.0.0': {'c': 'any'}, - 'a 5.0.0': {'c': '1.0.0'}, - 'b 1.0.0': {'c': 'any'}, - 'b 2.0.0': {'c': 'any'}, - 'b 3.0.0': {'c': 'any'}, - 'b 4.0.0': {'c': '2.0.0'}, - 'c 1.0.0': {}, - 'c 2.0.0': {}, - }, result: { - 'myapp from root': '0.0.0', - 'a': '4.0.0', - 'b': '4.0.0', - 'c': '2.0.0' - }, maxTries: 2); + integration('traverse into package with fewer versions first', () { + servePackages((builder) { + builder.serve('a', '1.0.0', deps: {'c': 'any'}); + builder.serve('a', '2.0.0', deps: {'c': 'any'}); + builder.serve('a', '3.0.0', deps: {'c': 'any'}); + builder.serve('a', '4.0.0', deps: {'c': 'any'}); + builder.serve('a', '5.0.0', deps: {'c': '1.0.0'}); + builder.serve('b', '1.0.0', deps: {'c': 'any'}); + builder.serve('b', '2.0.0', deps: {'c': 'any'}); + builder.serve('b', '3.0.0', deps: {'c': 'any'}); + builder.serve('b', '4.0.0', deps: {'c': '2.0.0'}); + builder.serve('c', '1.0.0'); + builder.serve('c', '2.0.0'); + }); + + d.appDir({'a': 'any', 'b': 'any'}).create(); + expectResolves(result: {'a': '4.0.0', 'b': '4.0.0', 'c': '2.0.0'}); + }); // This is similar to the above test. When getting the number of versions of // a package to determine which to traverse first, versions that are @@ -730,804 +686,473 @@ backtracking() { // Here, foo has more versions of bar in total (4), but fewer that meet // myapp's constraints (only 2). There is no solution, but we will do less // backtracking if foo is tested first. - testResolve('take root package constraints into counting versions', { - "myapp 0.0.0": { - "foo": ">2.0.0", - "bar": "any" - }, - "foo 1.0.0": {"none": "2.0.0"}, - "foo 2.0.0": {"none": "2.0.0"}, - "foo 3.0.0": {"none": "2.0.0"}, - "foo 4.0.0": {"none": "2.0.0"}, - "bar 1.0.0": {}, - "bar 2.0.0": {}, - "bar 3.0.0": {}, - "none 1.0.0": {} - }, error: noVersion(["foo", "none"]), maxTries: 2); - - // This sets up a hundred versions of foo and bar, 0.0.0 through 9.9.0. Each - // version of foo depends on a baz with the same major version. Each version - // of bar depends on a baz with the same minor version. There is only one - // version of baz, 0.0.0, so only older versions of foo and bar will - // satisfy it. - var map = { - 'myapp 0.0.0': { - 'foo': 'any', - 'bar': 'any' - }, - 'baz 0.0.0': {} - }; - - for (var i = 0; i < 10; i++) { - for (var j = 0; j < 10; j++) { - map['foo $i.$j.0'] = {'baz': '$i.0.0'}; - map['bar $i.$j.0'] = {'baz': '0.$j.0'}; - } - } + integration('take root package constraints into counting versions', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'none': '2.0.0'}); + builder.serve('foo', '2.0.0', deps: {'none': '2.0.0'}); + builder.serve('foo', '3.0.0', deps: {'none': '2.0.0'}); + builder.serve('foo', '4.0.0', deps: {'none': '2.0.0'}); + builder.serve('bar', '1.0.0'); + builder.serve('bar', '2.0.0'); + builder.serve('bar', '3.0.0'); + builder.serve('none', '1.0.0'); + }); - testResolve('complex backtrack', map, result: { - 'myapp from root': '0.0.0', - 'foo': '0.9.0', - 'bar': '9.0.0', - 'baz': '0.0.0' - }, maxTries: 10); + d.appDir({"foo": ">2.0.0", "bar": "any"}).create(); + expectResolves( + error: 'Package none has no versions that match 2.0.0 derived from:\n' + '- foo 3.0.0 depends on version 2.0.0', + tries: 2); + }); + + integration('complex backtrack', () { + servePackages((builder) { + // This sets up a hundred versions of foo and bar, 0.0.0 through 9.9.0. Each + // version of foo depends on a baz with the same major version. Each version + // of bar depends on a baz with the same minor version. There is only one + // version of baz, 0.0.0, so only older versions of foo and bar will + // satisfy it. + builder.serve('baz', '0.0.0'); + for (var i = 0; i < 10; i++) { + for (var j = 0; j < 10; j++) { + builder.serve('foo', '$i.$j.0', deps: {'baz': '$i.0.0'}); + builder.serve('bar', '$i.$j.0', deps: {'baz': '0.$j.0'}); + } + } + }); + + d.appDir({'foo': 'any', 'bar': 'any'}).create(); + expectResolves( + result: {'foo': '0.9.0', 'bar': '9.0.0', 'baz': '0.0.0'}, + tries: 10); + }); // If there's a disjoint constraint on a package, then selecting other // versions of it is a waste of time: no possible versions can match. We need // to jump past it to the most recent package that affected the constraint. - testResolve('backjump past failed package on disjoint constraint', { - 'myapp 0.0.0': { - 'a': 'any', - 'foo': '>2.0.0' - }, - 'a 1.0.0': { - 'foo': 'any' // ok - }, - 'a 2.0.0': { - 'foo': '<1.0.0' // disjoint with myapp's constraint on foo - }, - 'foo 2.0.0': {}, - 'foo 2.0.1': {}, - 'foo 2.0.2': {}, - 'foo 2.0.3': {}, - 'foo 2.0.4': {} - }, result: { - 'myapp from root': '0.0.0', - 'a': '1.0.0', - 'foo': '2.0.4' - }, maxTries: 2); + integration('backjump past failed package on disjoint constraint', () { + servePackages((builder) { + builder.serve('a', '1.0.0', deps: { + 'foo': 'any' // ok + }); + builder.serve('a', '2.0.0', deps: { + 'foo': '<1.0.0' // disjoint with myapp's constraint on foo + }); + builder.serve('foo', '2.0.0'); + builder.serve('foo', '2.0.1'); + builder.serve('foo', '2.0.2'); + builder.serve('foo', '2.0.3'); + builder.serve('foo', '2.0.4'); + }); + + d.appDir({'a': 'any', 'foo': '>2.0.0'}).create(); + expectResolves(result: {'a': '1.0.0', 'foo': '2.0.4'}); + }); // This is a regression test for #18666. It was possible for the solver to // "forget" that a package had previously led to an error. In that case, it // would backtrack over the failed package instead of trying different // versions of it. - testResolve("finds solution with less strict constraint", { - "myapp 1.0.0": { - "a": "any", - "c": "any", - "d": "any" - }, - "a 2.0.0": {}, - "a 1.0.0": {}, - "b 1.0.0": {"a": "1.0.0"}, - "c 1.0.0": {"b": "any"}, - "d 2.0.0": {"myapp": "any"}, - "d 1.0.0": {"myapp": "<1.0.0"} - }, result: { - 'myapp from root': '1.0.0', - 'a': '1.0.0', - 'b': '1.0.0', - 'c': '1.0.0', - 'd': '2.0.0' - }, maxTries: 3); -} - -sdkConstraint() { - var badVersion = '0.0.0-nope'; - var goodVersion = sdk.version.toString(); + integration("finds solution with less strict constraint", () { + servePackages((builder) { + builder.serve('a', '2.0.0'); + builder.serve('a', '1.0.0'); + builder.serve('b', '1.0.0', deps: {'a': '1.0.0'}); + builder.serve('c', '1.0.0', deps: {'b': 'any'}); + builder.serve('d', '2.0.0', deps: {'myapp': 'any'}); + builder.serve('d', '1.0.0', deps: {'myapp': '<1.0.0'}); + }); - testResolve('root matches SDK', { - 'myapp 0.0.0': {'sdk': goodVersion } - }, result: { - 'myapp from root': '0.0.0' + d.appDir({"a": "any", "c": "any", "d": "any"}).create(); + expectResolves( + result: {'a': '1.0.0', 'b': '1.0.0', 'c': '1.0.0', 'd': '2.0.0'}); }); - - testResolve('root does not match SDK', { - 'myapp 0.0.0': {'sdk': badVersion } - }, error: couldNotSolve); - - testResolve('dependency does not match SDK', { - 'myapp 0.0.0': {'foo': 'any'}, - 'foo 0.0.0': {'sdk': badVersion } - }, error: couldNotSolve); - - testResolve('transitive dependency does not match SDK', { - 'myapp 0.0.0': {'foo': 'any'}, - 'foo 0.0.0': {'bar': 'any'}, - 'bar 0.0.0': {'sdk': badVersion } - }, error: couldNotSolve); - - testResolve('selects a dependency version that allows the SDK', { - 'myapp 0.0.0': {'foo': 'any'}, - 'foo 1.0.0': {'sdk': goodVersion }, - 'foo 2.0.0': {'sdk': goodVersion }, - 'foo 3.0.0': {'sdk': badVersion }, - 'foo 4.0.0': {'sdk': badVersion } - }, result: { - 'myapp from root': '0.0.0', - 'foo': '2.0.0' - }, maxTries: 3); - - testResolve('selects a transitive dependency version that allows the SDK', { - 'myapp 0.0.0': {'foo': 'any'}, - 'foo 1.0.0': {'bar': 'any'}, - 'bar 1.0.0': {'sdk': goodVersion }, - 'bar 2.0.0': {'sdk': goodVersion }, - 'bar 3.0.0': {'sdk': badVersion }, - 'bar 4.0.0': {'sdk': badVersion } - }, result: { - 'myapp from root': '0.0.0', - 'foo': '1.0.0', - 'bar': '2.0.0' - }, maxTries: 3); - - testResolve('selects a dependency version that allows a transitive ' - 'dependency that allows the SDK', { - 'myapp 0.0.0': {'foo': 'any'}, - 'foo 1.0.0': {'bar': '1.0.0'}, - 'foo 2.0.0': {'bar': '2.0.0'}, - 'foo 3.0.0': {'bar': '3.0.0'}, - 'foo 4.0.0': {'bar': '4.0.0'}, - 'bar 1.0.0': {'sdk': goodVersion }, - 'bar 2.0.0': {'sdk': goodVersion }, - 'bar 3.0.0': {'sdk': badVersion }, - 'bar 4.0.0': {'sdk': badVersion } - }, result: { - 'myapp from root': '0.0.0', - 'foo': '2.0.0', - 'bar': '2.0.0' - }, maxTries: 3); } -void prerelease() { - testResolve('prefer stable versions over unstable', { - 'myapp 0.0.0': { - 'a': 'any' - }, - 'a 1.0.0': {}, - 'a 1.1.0-dev': {}, - 'a 2.0.0-dev': {}, - 'a 3.0.0-dev': {} - }, result: { - 'myapp from root': '0.0.0', - 'a': '1.0.0' - }); +void sdkConstraint() { + integration('root matches SDK', () { + d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'environment': {'sdk': '0.1.2+3'} + }) + ]).create(); - testResolve('use latest allowed prerelease if no stable versions match', { - 'myapp 0.0.0': { - 'a': '<2.0.0' - }, - 'a 1.0.0-dev': {}, - 'a 1.1.0-dev': {}, - 'a 1.9.0-dev': {}, - 'a 3.0.0': {} - }, result: { - 'myapp from root': '0.0.0', - 'a': '1.9.0-dev' + expectResolves(result: {}); }); - testResolve('use an earlier stable version on a < constraint', { - 'myapp 0.0.0': { - 'a': '<2.0.0' - }, - 'a 1.0.0': {}, - 'a 1.1.0': {}, - 'a 2.0.0-dev': {}, - 'a 2.0.0': {} - }, result: { - 'myapp from root': '0.0.0', - 'a': '1.1.0' - }); + integration('root does not match SDK', () { + d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'environment': {'sdk': '0.0.0'} + }) + ]).create(); - testResolve('prefer a stable version even if constraint mentions unstable', { - 'myapp 0.0.0': { - 'a': '<=2.0.0-dev' - }, - 'a 1.0.0': {}, - 'a 1.1.0': {}, - 'a 2.0.0-dev': {}, - 'a 2.0.0': {} - }, result: { - 'myapp from root': '0.0.0', - 'a': '1.1.0' + expectResolves(error: 'Package myapp requires SDK version 0.0.0 but the ' + 'current SDK is 0.1.2+3.'); }); -} -void override() { - testResolve('chooses best version matching override constraint', { - 'myapp 0.0.0': { - 'a': 'any' - }, - 'a 1.0.0': {}, - 'a 2.0.0': {}, - 'a 3.0.0': {} - }, overrides: { - 'a': '<3.0.0' - }, result: { - 'myapp from root': '0.0.0', - 'a': '2.0.0' - }); + integration('dependency does not match SDK', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', pubspec: {'environment': {'sdk': '0.0.0'}}); + }); - testResolve('uses override as dependency', { - 'myapp 0.0.0': {}, - 'a 1.0.0': {}, - 'a 2.0.0': {}, - 'a 3.0.0': {} - }, overrides: { - 'a': '<3.0.0' - }, result: { - 'myapp from root': '0.0.0', - 'a': '2.0.0' + d.appDir({'foo': 'any'}).create(); + expectResolves( + error: 'Package foo requires SDK version 0.0.0 but the ' + 'current SDK is 0.1.2+3.'); }); - testResolve('ignores other constraints on overridden package', { - 'myapp 0.0.0': { - 'b': 'any', - 'c': 'any' - }, - 'a 1.0.0': {}, - 'a 2.0.0': {}, - 'a 3.0.0': {}, - 'b 1.0.0': { - 'a': '1.0.0' - }, - 'c 1.0.0': { - 'a': '3.0.0' - } - }, overrides: { - 'a': '2.0.0' - }, result: { - 'myapp from root': '0.0.0', - 'a': '2.0.0', - 'b': '1.0.0', - 'c': '1.0.0' - }); + integration('transitive dependency does not match SDK', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'bar': 'any'}); + builder.serve('bar', '1.0.0', pubspec: {'environment': {'sdk': '0.0.0'}}); + }); - testResolve('backtracks on overidden package for its constraints', { - 'myapp 0.0.0': { - 'shared': '2.0.0' - }, - 'a 1.0.0': { - 'shared': 'any' - }, - 'a 2.0.0': { - 'shared': '1.0.0' - }, - 'shared 1.0.0': {}, - 'shared 2.0.0': {} - }, overrides: { - 'a': '<3.0.0' - }, result: { - 'myapp from root': '0.0.0', - 'a': '1.0.0', - 'shared': '2.0.0' - }, maxTries: 2); - - testResolve('override compatible with locked dependency', { - 'myapp 0.0.0': { - 'foo': 'any' - }, - 'foo 1.0.0': { 'bar': '1.0.0' }, - 'foo 1.0.1': { 'bar': '1.0.1' }, - 'foo 1.0.2': { 'bar': '1.0.2' }, - 'bar 1.0.0': {}, - 'bar 1.0.1': {}, - 'bar 1.0.2': {} - }, lockfile: { - 'foo': '1.0.1' - }, overrides: { - 'foo': '<1.0.2' - }, result: { - 'myapp from root': '0.0.0', - 'foo': '1.0.1', - 'bar': '1.0.1' + d.appDir({'foo': 'any'}).create(); + expectResolves( + error: 'Package bar requires SDK version 0.0.0 but the ' + 'current SDK is 0.1.2+3.'); }); - testResolve('override incompatible with locked dependency', { - 'myapp 0.0.0': { - 'foo': 'any' - }, - 'foo 1.0.0': { 'bar': '1.0.0' }, - 'foo 1.0.1': { 'bar': '1.0.1' }, - 'foo 1.0.2': { 'bar': '1.0.2' }, - 'bar 1.0.0': {}, - 'bar 1.0.1': {}, - 'bar 1.0.2': {} - }, lockfile: { - 'foo': '1.0.1' - }, overrides: { - 'foo': '>1.0.1' - }, result: { - 'myapp from root': '0.0.0', - 'foo': '1.0.2', - 'bar': '1.0.2' - }); + integration('selects a dependency version that allows the SDK', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', + pubspec: {'environment': {'sdk': '0.1.2+3'}}); + builder.serve('foo', '2.0.0', + pubspec: {'environment': {'sdk': '0.1.2+3'}}); + builder.serve('foo', '3.0.0', pubspec: {'environment': {'sdk': '0.0.0'}}); + builder.serve('foo', '4.0.0', pubspec: {'environment': {'sdk': '0.0.0'}}); + }); - testResolve('no version that matches override', { - 'myapp 0.0.0': {}, - 'foo 2.0.0': {}, - 'foo 2.1.3': {} - }, overrides: { - 'foo': '>=1.0.0 <2.0.0' - }, error: noVersion(['myapp'])); - - testResolve('override a bad source without error', { - 'myapp 0.0.0': { - 'foo from bad': 'any' - }, - 'foo 0.0.0': {} - }, overrides: { - 'foo': 'any' - }, result: { - 'myapp from root': '0.0.0', - 'foo': '0.0.0' + d.appDir({'foo': 'any'}).create(); + expectResolves(result: {'foo': '2.0.0'}); }); -} -void downgrade() { - testResolve("downgrades a dependency to the lowest matching version", { - 'myapp 0.0.0': { - 'foo': '>=2.0.0 <3.0.0' - }, - 'foo 1.0.0': {}, - 'foo 2.0.0-dev': {}, - 'foo 2.0.0': {}, - 'foo 2.1.0': {} - }, lockfile: { - 'foo': '2.1.0' - }, result: { - 'myapp from root': '0.0.0', - 'foo': '2.0.0' - }, downgrade: true); - - testResolve('use earliest allowed prerelease if no stable versions match ' - 'while downgrading', { - 'myapp 0.0.0': { - 'a': '>=2.0.0-dev.1 <3.0.0' - }, - 'a 1.0.0': {}, - 'a 2.0.0-dev.1': {}, - 'a 2.0.0-dev.2': {}, - 'a 2.0.0-dev.3': {} - }, result: { - 'myapp from root': '0.0.0', - 'a': '2.0.0-dev.1' - }, downgrade: true); -} - -testResolve(String description, Map packages, { - Map lockfile, Map overrides, Map result, FailMatcherBuilder error, - int maxTries, bool downgrade: false}) { - if (maxTries == null) maxTries = 1; - - test(description, () { - source1 = new MockSource('mock1'); - source2 = new MockSource('mock2'); - - var cache = new SystemCache(rootDir: '.'); - cache.sources.register(source1); - cache.sources.register(source2); - cache.sources.setDefault(source1.name); - - // Build the test package graph. - var root; - packages.forEach((description, dependencies) { - var id = parseSpec(cache.sources, description); - var package = mockPackage(cache.sources, id, dependencies, - id.name == 'myapp' ? overrides : null); - if (id.name == 'myapp') { - // Don't add the root package to the server, so we can verify that Pub - // doesn't try to look up information about the local package on the - // remote server. - root = package; - } else { - (cache.source(id.source) as BoundMockSource) - .addPackage(id.description, package); - } + integration('selects a transitive dependency version that allows the SDK', + () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'bar': 'any'}); + builder.serve('bar', '1.0.0', + pubspec: {'environment': {'sdk': '0.1.2+3'}}); + builder.serve('bar', '2.0.0', + pubspec: {'environment': {'sdk': '0.1.2+3'}}); + builder.serve('bar', '3.0.0', pubspec: {'environment': {'sdk': '0.0.0'}}); + builder.serve('bar', '4.0.0', pubspec: {'environment': {'sdk': '0.0.0'}}); }); - // Clean up the expectation. - if (result != null) { - var newResult = {}; - result.forEach((description, version) { - var id = parseSpec(cache.sources, description, version); - newResult[id.name] = id; - }); - result = newResult; - } - - // Parse the lockfile. - var realLockFile; - if (lockfile == null) { - realLockFile = new LockFile.empty(); - } else { - realLockFile = new LockFile(lockfile.keys.map((name) { - var version = new Version.parse(lockfile[name]); - return new PackageId(name, source1, version, name); - })); - } - - // Resolve the versions. - log.verbosity = log.Verbosity.NONE; - var future = resolveVersions( - downgrade ? SolveType.DOWNGRADE : SolveType.GET, - cache, root, lockFile: realLockFile); - - var matcher; - if (result != null) { - matcher = new SolveSuccessMatcher(result, maxTries); - } else if (error != null) { - matcher = error(maxTries); - } - - expect(future, completion(matcher)); + d.appDir({'foo': 'any'}).create(); + expectResolves(result: {'foo': '1.0.0', 'bar': '2.0.0'}); }); -} - -typedef SolveFailMatcher FailMatcherBuilder(int maxTries); - -FailMatcherBuilder noVersion(List<String> packages) { - return (maxTries) => new SolveFailMatcher(packages, maxTries, - NoVersionException); -} - -FailMatcherBuilder disjointConstraint(List<String> packages) { - return (maxTries) => new SolveFailMatcher(packages, maxTries, - DisjointConstraintException); -} - -FailMatcherBuilder descriptionMismatch( - String package, String depender1, String depender2) { - return (maxTries) => new SolveFailMatcher([package, depender1, depender2], - maxTries, DescriptionMismatchException); -} - -// If no solution can be found, the solver just reports the last failure that -// happened during propagation. Since we don't specify the order that solutions -// are tried, this just validates that *some* failure occurred, but not which. -SolveFailMatcher couldNotSolve(maxTries) => - new SolveFailMatcher([], maxTries, null); - -FailMatcherBuilder sourceMismatch( - String package, String depender1, String depender2) { - return (maxTries) => new SolveFailMatcher([package, depender1, depender2], - maxTries, SourceMismatchException); -} - -unknownSource(String depender, String dependency, String source) { - return (maxTries) => new SolveFailMatcher([depender, dependency, source], - maxTries, UnknownSourceException); -} - -class SolveSuccessMatcher implements Matcher { - /// The expected concrete package selections. - final Map<String, PackageId> _expected; - - /// The maximum number of attempts that should have been tried before finding - /// the solution. - final int _maxTries; - - SolveSuccessMatcher(this._expected, this._maxTries); - - Description describe(Description description) { - return description.add( - 'Solver to use at most $_maxTries attempts to find:\n' - '${_listPackages(_expected.values)}'); - } - - Description describeMismatch(SolveResult result, - Description description, - Map state, bool verbose) { - if (!result.succeeded) { - description.add('Solver failed with:\n${result.error}'); - return null; - } - description.add('Resolved:\n${_listPackages(result.packages)}\n'); - description.add(state['failures']); - return description; - } - - bool matches(SolveResult result, Map state) { - if (!result.succeeded) return false; - - var expected = new Map.from(_expected); - var failures = new StringBuffer(); - - for (var id in result.packages) { - if (!expected.containsKey(id.name)) { - failures.writeln('Should not have selected $id'); - } else { - var expectedId = expected.remove(id.name); - if (id != expectedId) { - failures.writeln('Expected $expectedId, not $id'); - } - } - } - - if (!expected.isEmpty) { - failures.writeln('Missing:\n${_listPackages(expected.values)}'); - } - - // Allow 1 here because the greedy solver will only make one attempt. - if (result.attemptedSolutions != 1 && - result.attemptedSolutions > _maxTries) { - failures.writeln('Took ${result.attemptedSolutions} attempts'); - } - - if (!failures.isEmpty) { - state['failures'] = failures.toString(); - return false; - } - - return true; - } + integration('selects a dependency version that allows a transitive ' + 'dependency that allows the SDK', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'bar': '1.0.0'}); + builder.serve('foo', '2.0.0', deps: {'bar': '2.0.0'}); + builder.serve('foo', '3.0.0', deps: {'bar': '3.0.0'}); + builder.serve('foo', '4.0.0', deps: {'bar': '4.0.0'}); + builder.serve('bar', '1.0.0', + pubspec: {'environment': {'sdk': '0.1.2+3'}}); + builder.serve('bar', '2.0.0', + pubspec: {'environment': {'sdk': '0.1.2+3'}}); + builder.serve('bar', '3.0.0', pubspec: {'environment': {'sdk': '0.0.0'}}); + builder.serve('bar', '4.0.0', pubspec: {'environment': {'sdk': '0.0.0'}}); + }); - String _listPackages(Iterable<PackageId> packages) { - return '- ${packages.join('\n- ')}'; - } + d.appDir({'foo': 'any'}).create(); + expectResolves(result: {'foo': '2.0.0', 'bar': '2.0.0'}, tries: 3); + }); } -class SolveFailMatcher implements Matcher { - /// The strings that should appear in the resulting error message. - // TODO(rnystrom): This seems to always be package names. Make that explicit. - final Iterable<String> _expected; - - /// The maximum number of attempts that should be tried before failing. - final int _maxTries; +void prerelease() { + integration('prefer stable versions over unstable', () { + servePackages((builder) { + builder.serve('a', '1.0.0'); + builder.serve('a', '1.1.0-dev'); + builder.serve('a', '2.0.0-dev'); + builder.serve('a', '3.0.0-dev'); + }); - /// The concrete error type that should be found, or `null` if any - /// [SolveFailure] is allowed. - final Type _expectedType; + d.appDir({'a': 'any'}).create(); + expectResolves(result: {'a': '1.0.0'}); + }); - SolveFailMatcher(this._expected, this._maxTries, this._expectedType); + integration('use latest allowed prerelease if no stable versions match', () { + servePackages((builder) { + builder.serve('a', '1.0.0-dev'); + builder.serve('a', '1.1.0-dev'); + builder.serve('a', '1.9.0-dev'); + builder.serve('a', '3.0.0'); + }); - Description describe(Description description) { - description.add('Solver should fail after at most $_maxTries attempts.'); - if (!_expected.isEmpty) { - var textList = _expected.map((s) => '"$s"').join(", "); - description.add(' The error should contain $textList.'); - } - return description; - } - - Description describeMismatch(SolveResult result, - Description description, - Map state, bool verbose) { - description.add(state['failures']); - return description; - } - - bool matches(SolveResult result, Map state) { - var failures = new StringBuffer(); - - if (result.succeeded) { - failures.writeln('Solver succeeded'); - } else { - if (_expectedType != null && result.error.runtimeType != _expectedType) { - failures.writeln('Should have error type $_expectedType, got ' - '${result.error.runtimeType}'); - } + d.appDir({'a': '<2.0.0'}).create(); + expectResolves(result: {'a': '1.9.0-dev'}); + }); - var message = result.error.toString(); - for (var expected in _expected) { - if (!message.contains(expected)) { - failures.writeln( - 'Expected error to contain "$expected", got:\n$message'); - } - } + integration('use an earlier stable version on a < constraint', () { + servePackages((builder) { + builder.serve('a', '1.0.0'); + builder.serve('a', '1.1.0'); + builder.serve('a', '2.0.0-dev'); + builder.serve('a', '2.0.0'); + }); - // Allow 1 here because the greedy solver will only make one attempt. - if (result.attemptedSolutions != 1 && - result.attemptedSolutions > _maxTries) { - failures.writeln('Took ${result.attemptedSolutions} attempts'); - } - } + d.appDir({'a': '<2.0.0'}).create(); + expectResolves(result: {'a': '1.1.0'}); + }); - if (!failures.isEmpty) { - state['failures'] = failures.toString(); - return false; - } + integration('prefer a stable version even if constraint mentions unstable', + () { + servePackages((builder) { + builder.serve('a', '1.0.0'); + builder.serve('a', '1.1.0'); + builder.serve('a', '2.0.0-dev'); + builder.serve('a', '2.0.0'); + }); - return true; - } + d.appDir({'a': '<=2.0.0-dev'}).create(); + expectResolves(result: {'a': '1.1.0'}); + }); } -/// A source used for testing. This both creates mock package objects and acts -/// as a source for them. -/// -/// In order to support testing packages that have the same name but different -/// descriptions, a package's name is calculated by taking the description -/// string and stripping off any trailing hyphen followed by non-hyphen -/// characters. -class MockSource extends Source { - final String name; - final hasMultipleVersions = true; - - MockSource(this.name); - - BoundSource bind(SystemCache cache) => new BoundMockSource(this, cache); - - PackageRef parseRef(String name, description, {String containingPath}) => - new PackageRef(name, this, description); +void override() { + integration('chooses best version matching override constraint', () { + servePackages((builder) { + builder.serve('a', '1.0.0'); + builder.serve('a', '2.0.0'); + builder.serve('a', '3.0.0'); + }); - PackageId parseId(String name, Version version, description) => - new PackageId(name, this, version, description); + d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'dependencies': {'a': 'any'}, + 'dependency_overrides': {'a': '<3.0.0'} + }) + ]).create(); - bool descriptionsEqual(description1, description2) => - description1 == description2; + expectResolves(result: {'a': '2.0.0'}); + }); - int hashDescription(description) => description.hashCode; -} + integration('uses override as dependency', () { + servePackages((builder) { + builder.serve('a', '1.0.0'); + builder.serve('a', '2.0.0'); + builder.serve('a', '3.0.0'); + }); -class BoundMockSource extends CachedSource { - final SystemCache systemCache; + d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'dependency_overrides': {'a': '<3.0.0'} + }) + ]).create(); - final MockSource source; + expectResolves(result: {'a': '2.0.0'}); + }); - final _packages = <String, Map<Version, Package>>{}; + integration('ignores other constraints on overridden package', () { + servePackages((builder) { + builder.serve('a', '1.0.0'); + builder.serve('a', '2.0.0'); + builder.serve('a', '3.0.0'); + builder.serve('b', '1.0.0', deps: {'a': '1.0.0'}); + builder.serve('c', '1.0.0', deps: {'a': '3.0.0'}); + }); - /// Keeps track of which package version lists have been requested. Ensures - /// that a source is only hit once for a given package and that pub - /// internally caches the results. - final _requestedVersions = new Set<String>(); + d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'dependencies': {'b': 'any', 'c': 'any'}, + 'dependency_overrides': {'a': '2.0.0'} + }) + ]).create(); - /// Keeps track of which package pubspecs have been requested. Ensures that a - /// source is only hit once for a given package and that pub internally - /// caches the results. - final _requestedPubspecs = new Map<String, Set<Version>>(); + expectResolves(result: {'a': '2.0.0', 'b': '1.0.0', 'c': '1.0.0'}); + }); - BoundMockSource(this.source, this.systemCache); + integration('backtracks on overidden package for its constraints', () { + servePackages((builder) { + builder.serve('a', '1.0.0', deps: {'shared': 'any'}); + builder.serve('a', '2.0.0', deps: {'shared': '1.0.0'}); + builder.serve('shared', '1.0.0'); + builder.serve('shared', '2.0.0'); + }); - String getDirectory(PackageId id) => '${id.name}-${id.version}'; + d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'dependencies': {'shared': '2.0.0'}, + 'dependency_overrides': {'a': '<3.0.0'} + }) + ]).create(); - Future<List<PackageId>> doGetVersions(PackageRef ref) async { - // Make sure the solver doesn't request the same thing twice. - if (_requestedVersions.contains(ref.description)) { - throw new Exception('Version list for ${ref.description} was already ' - 'requested.'); - } + expectResolves(result: {'a': '1.0.0', 'shared': '2.0.0'}); + }); - _requestedVersions.add(ref.description); + integration('override compatible with locked dependency', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'bar': '1.0.0'}); + builder.serve('foo', '1.0.1', deps: {'bar': '1.0.1'}); + builder.serve('foo', '1.0.2', deps: {'bar': '1.0.2'}); + builder.serve('bar', '1.0.0'); + builder.serve('bar', '1.0.1'); + builder.serve('bar', '1.0.2'); + }); - if (!_packages.containsKey(ref.description)){ - throw new Exception('MockSource does not have a package matching ' - '"${ref.description}".'); - } + d.appDir({'foo': '1.0.1'}).create(); + expectResolves(result: {'foo': '1.0.1', 'bar': '1.0.1'}); + + d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'dependency_overrides': {'foo': '<1.0.2'} + }) + ]).create(); + + expectResolves(result: {'foo': '1.0.1', 'bar': '1.0.1'}); + }); - return _packages[ref.description].values.map((package) { - return new PackageId(ref.name, source, package.version, ref.description); - }).toList(); - } - - Future<Pubspec> describeUncached(PackageId id) { - return new Future.sync(() { - // Make sure the solver doesn't request the same thing twice. - if (_requestedPubspecs.containsKey(id.description) && - _requestedPubspecs[id.description].contains(id.version)) { - throw new Exception('Pubspec for $id was already requested.'); - } + integration('override incompatible with locked dependency', () { + servePackages((builder) { + builder.serve('foo', '1.0.0', deps: {'bar': '1.0.0'}); + builder.serve('foo', '1.0.1', deps: {'bar': '1.0.1'}); + builder.serve('foo', '1.0.2', deps: {'bar': '1.0.2'}); + builder.serve('bar', '1.0.0'); + builder.serve('bar', '1.0.1'); + builder.serve('bar', '1.0.2'); + }); - _requestedPubspecs.putIfAbsent(id.description, () => new Set<Version>()); - _requestedPubspecs[id.description].add(id.version); + d.appDir({'foo': '1.0.1'}).create(); + expectResolves(result: {'foo': '1.0.1', 'bar': '1.0.1'}); + + d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'dependency_overrides': {'foo': '>1.0.1'} + }) + ]).create(); + + expectResolves(result: {'foo': '1.0.2', 'bar': '1.0.2'}); + }); - return _packages[id.description][id.version].pubspec; + integration('no version that matches override', () { + servePackages((builder) { + builder.serve('foo', '2.0.0'); + builder.serve('foo', '2.1.3'); }); - } - Future<Package> downloadToSystemCache(PackageId id) => - throw new UnsupportedError('Cannot download mock packages'); + d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'dependency_overrides': {'foo': '>=1.0.0 <2.0.0'} + }) + ]).create(); + + expectResolves( + error: 'Package foo has no versions that match >=1.0.0 <2.0.0 derived ' + 'from:\n' + '- myapp depends on version >=1.0.0 <2.0.0'); + }); - List<Package> getCachedPackages() => - throw new UnsupportedError('Cannot get mock packages'); + integration('override a bad source without error', () { + servePackages((builder) { + builder.serve('foo', '0.0.0'); + }); - Future<Pair<List<PackageId>, List<PackageId>>> repairCachedPackages() => - throw new UnsupportedError('Cannot repair mock packages'); + d.dir(appPath, [ + d.pubspec({ + 'name': 'myapp', + 'dependencies': {'foo': {'bad': 'any'}}, + 'dependency_overrides': {'foo': 'any'} + }) + ]).create(); - void addPackage(String description, Package package) { - _packages.putIfAbsent(description, () => new Map<Version, Package>()); - _packages[description][package.version] = package; - } + expectResolves(result: {'foo': '0.0.0'}); + }); } -Package mockPackage(SourceRegistry sources, PackageId id, Map dependencyStrings, - Map overrides) { - var sdkConstraint = null; - - // Build the pubspec dependencies. - var dependencies = <PackageDep>[]; - var devDependencies = <PackageDep>[]; - - dependencyStrings.forEach((spec, constraint) { - var isDev = spec.startsWith("(dev) "); - if (isDev) { - spec = spec.substring("(dev) ".length); - } +void downgrade() { + integration("downgrades a dependency to the lowest matching version", () { + servePackages((builder) { + builder.serve('foo', '1.0.0'); + builder.serve('foo', '2.0.0-dev'); + builder.serve('foo', '2.0.0'); + builder.serve('foo', '2.1.0'); + }); - var dep = parseSpec(sources, spec).withConstraint( - new VersionConstraint.parse(constraint)); + d.appDir({'foo': '2.1.0'}).create(); + expectResolves(result: {'foo': '2.1.0'}); - if (dep.name == 'sdk') { - sdkConstraint = dep.constraint; - return; - } - - if (isDev) { - devDependencies.add(dep); - } else { - dependencies.add(dep); - } + d.appDir({'foo': '>=2.0.0 <3.0.0'}).create(); + expectResolves(result: {'foo': '2.0.0'}, downgrade: true); }); - var dependencyOverrides = <PackageDep>[]; - if (overrides != null) { - overrides.forEach((spec, constraint) { - dependencyOverrides.add(parseSpec(sources, spec) - .withConstraint(new VersionConstraint.parse(constraint))); + integration('use earliest allowed prerelease if no stable versions match ' + 'while downgrading', () { + servePackages((builder) { + builder.serve('a', '1.0.0'); + builder.serve('a', '2.0.0-dev.1'); + builder.serve('a', '2.0.0-dev.2'); + builder.serve('a', '2.0.0-dev.3'); }); - } - - return new Package.inMemory(new Pubspec(id.name, - version: id.version, - dependencies: dependencies, - devDependencies: devDependencies, - dependencyOverrides: dependencyOverrides, - sdkConstraint: sdkConstraint)); + + d.appDir({'a': '>=2.0.0-dev.1 <3.0.0'}).create(); + expectResolves(result: {'a': '2.0.0-dev.1'}, downgrade: true); + }); } -/// Creates a new [PackageId] parsed from [text], which looks something like -/// this: +/// Runs "pub get" and makes assertions about its results. /// -/// foo-xyz 1.0.0 from mock +/// If [result] is passed, it's parsed as a pubspec-style dependency map, and +/// this asserts that the resulting lockfile matches those dependencies, and +/// that it contains only packages listed in [result]. /// -/// The package name is "foo". A hyphenated suffix like "-xyz" here is part -/// of the package description, but not its name, so the description here is -/// "foo-xyz". +/// If [error] is passed, this asserts that pub's error output matches the +/// value. It may be a String, a [RegExp], or a [Matcher]. /// -/// This is followed by an optional [Version]. If [version] is provided, then -/// it is parsed to a [Version], and [text] should *not* also contain a -/// version string. +/// Asserts that version solving looks at exactly [tries] solutions. It defaults +/// to allowing only a single solution. /// -/// The "from mock" optional suffix is the name of a source for the package. -/// If omitted, it defaults to "mock1". -PackageId parseSpec(SourceRegistry sources, String text, [String version]) { - var pattern = new RegExp(r"(([a-z_]*)(-[a-z_]+)?)( ([^ ]+))?( from (.*))?$"); - var match = pattern.firstMatch(text); - if (match == null) { - throw new FormatException("Could not parse spec '$text'."); - } - - var description = match[1]; - var name = match[2]; - - var parsedVersion; - if (version != null) { - // Spec string shouldn't also contain a version. - if (match[5] != null) { - throw new ArgumentError("Spec '$text' should not contain a version " - "since '$version' was passed in explicitly."); - } - parsedVersion = new Version.parse(version); - } else { - if (match[5] != null) { - parsedVersion = new Version.parse(match[5]); - } else { - parsedVersion = Version.none; +/// If [downgrade] is `true`, this runs "pub downgrade" instead of "pub get". +void expectResolves({Map result, error, int tries, bool downgrade: false}) { + schedulePub( + args: [downgrade ? 'downgrade' : 'get'], + output: error == null + ? anyOf( + contains('Got dependencies!'), + matches(new RegExp(r'Changed \d+ dependenc(ies|y)!'))) + : null, + error: error, + silent: contains('Tried ${tries ?? 1} solutions'), + exitCode: error == null ? 0 : 1); + + if (result == null) return; + + schedule(() async { + var registry = new SourceRegistry(); + var lockFile = new LockFile.load( + p.join(sandboxDir, appPath, 'pubspec.lock'), + registry); + var resultPubspec = new Pubspec.fromMap({"dependencies": result}, registry); + + var ids = new Map.from(lockFile.packages); + for (var dep in resultPubspec.dependencies) { + expect(ids, contains(dep.name)); + var id = ids.remove(dep.name); + + if (dep.source is HostedSource && dep.description is String) { + // If the dep uses the default hosted source, grab it from the test + // package server rather than pub.dartlang.org. + dep = registry.hosted + .refFor(dep.name, url: await globalPackageServer.url) + .withConstraint(dep.constraint); + } + expect(dep.allows(id), isTrue, reason: "Expected $id to match $dep."); } - } - var source = sources["mock1"]; - if (match[7] != null) source = match[7] == "root" ? null : sources[match[7]]; - - return new PackageId(name, source, parsedVersion, description); + expect(ids, isEmpty, reason: "Expected no additional packages."); + }); } -- GitLab