blob: eb37f640d893097449827f1a9fd546d3d2f9777f [file] [log] [blame]
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
library pub_update_test;
import 'dart:async';
import 'dart:io';
import '../../pub/lock_file.dart';
import '../../pub/package.dart';
import '../../pub/pubspec.dart';
import '../../pub/source.dart';
import '../../pub/source_registry.dart';
import '../../pub/system_cache.dart';
import '../../pub/utils.dart';
import '../../pub/version.dart';
import '../../pub/version_solver.dart';
import '../../../pkg/unittest/lib/unittest.dart';
Matcher noVersion(List<String> packages) {
return predicate((x) {
if (x is! NoVersionException) return false;
// Make sure the error string mentions the conflicting dependers.
var message = x.toString();
return packages.every((package) => message.contains(package));
}, "is a NoVersionException");
}
Matcher disjointConstraint(List<String> packages) {
return predicate((x) {
if (x is! DisjointConstraintException) return false;
// Make sure the error string mentions the conflicting dependers.
var message = x.toString();
return packages.every((package) => message.contains(package));
}, "is a DisjointConstraintException");
}
Matcher descriptionMismatch(String package1, String package2) {
return predicate((x) {
if (x is! DescriptionMismatchException) return false;
// Make sure the error string mentions the conflicting dependers.
if (!x.toString().contains(package1)) return false;
if (!x.toString().contains(package2)) return false;
return true;
}, "is a DescriptionMismatchException");
}
final couldNotSolve = predicate((x) => x is CouldNotSolveException,
"is a CouldNotSolveException");
Matcher sourceMismatch(String package1, String package2) {
return predicate((x) {
if (x is! SourceMismatchException) return false;
// Make sure the error string mentions the conflicting dependers.
if (!x.toString().contains(package1)) return false;
if (!x.toString().contains(package2)) return false;
return true;
}, "is a SourceMismatchException");
}
MockSource source1;
MockSource source2;
Source versionlessSource;
main() {
testResolve('no dependencies', {
'myapp 0.0.0': {}
}, result: {
'myapp from root': '0.0.0'
});
testResolve('simple dependency tree', {
'myapp 0.0.0': {
'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': {
'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'
});
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'
});
testResolve('from versionless source', {
'myapp 0.0.0': {
'foo from versionless': 'any'
},
'foo 1.2.3 from versionless': {}
}, result: {
'myapp from root': '0.0.0',
'foo from versionless': '1.2.3'
});
testResolve('transitively through versionless source', {
'myapp 0.0.0': {
'foo from versionless': 'any'
},
'foo 1.2.3 from versionless': {
'bar': '>=1.0.0'
},
'bar 1.1.0': {}
}, result: {
'myapp from root': '0.0.0',
'foo from versionless': '1.2.3',
'bar': '1.1.0'
});
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'
});
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'
});
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'
});
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'
});
testResolve('dependency back onto root package', {
'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'
});
testResolve('dependency back onto root package 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'
});
testResolve('mismatched dependencies back onto root package', {
'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('foo', 'bar'));
testResolve('dependency back onto root package with wrong version', {
'myapp 1.0.0': {
'foo': '1.0.0'
},
'foo 1.0.0': {
'myapp': '<1.0.0'
}
}, error: disjointConstraint(['foo']));
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']));
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(['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(['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('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('foo', 'bar'));
testResolve('unstable dependency graph', {
'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'
}
}, error: couldNotSolve);
// TODO(rnystrom): More stuff to test:
// - Depending on a non-existent package.
// - Test that only a certain number requests are sent to the mock source so we
// can keep track of server traffic.
}
testResolve(description, packages, {lockfile, result, Matcher error}) {
test(description, () {
var cache = new SystemCache('.');
source1 = new MockSource('mock1');
source2 = new MockSource('mock2');
versionlessSource = new MockVersionlessSource();
cache.register(source1);
cache.register(source2);
cache.register(versionlessSource);
cache.sources.setDefault(source1.name);
// Build the test package graph.
var root;
packages.forEach((nameVersion, dependencies) {
var parsed = parseSource(nameVersion);
nameVersion = parsed.first;
var source = parsed.last;
var parts = nameVersion.split(' ');
var name = parts[0];
var version = parts[1];
var package = source1.mockPackage(name, version, dependencies);
if (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 {
source.addPackage(package);
}
});
// Clean up the expectation.
if (result != null) {
var newResult = {};
result.forEach((name, version) {
var parsed = parseSource(name);
name = parsed.first;
var source = parsed.last;
version = new Version.parse(version);
newResult[name] = new PackageId(name, source, version, name);
});
result = newResult;
}
var realLockFile = new LockFile.empty();
if (lockfile != null) {
lockfile.forEach((name, version) {
version = new Version.parse(version);
realLockFile.packages[name] =
new PackageId(name, source1, version, name);
});
}
// Resolve the versions.
var future = resolveVersions(cache.sources, root, realLockFile);
if (result != null) {
expect(future, completion(predicate((actualResult) {
for (var actualId in actualResult) {
if (!result.containsKey(actualId.name)) return false;
var expectedId = result.remove(actualId.name);
if (actualId != expectedId) return false;
}
return result.isEmpty;
}, 'packages to match $result')));
} else if (error != null) {
expect(future, throwsA(error));
}
});
}
/// 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 Map<String, Map<Version, Package>> _packages;
final String name;
bool get shouldCache => true;
MockSource(this.name)
: _packages = <String, Map<Version, Package>>{};
Future<List<Version>> getVersions(String name, String description) {
return fakeAsync(() => _packages[description].keys.toList());
}
Future<Pubspec> describe(PackageId id) {
return fakeAsync(() {
return _packages[id.name][id.version].pubspec;
});
}
Future<bool> install(PackageId id, String path) {
throw 'no';
}
Package mockPackage(String description, String version,
Map dependencyStrings) {
// Build the pubspec dependencies.
var dependencies = <PackageRef>[];
dependencyStrings.forEach((name, constraint) {
var parsed = parseSource(name);
var description = parsed.first;
var packageName = description.replaceFirst(new RegExp(r"-[^-]+$"), "");
dependencies.add(new PackageRef(packageName, parsed.last,
new VersionConstraint.parse(constraint), description));
});
var pubspec = new Pubspec(
description, new Version.parse(version), dependencies,
new PubspecEnvironment());
return new Package.inMemory(pubspec);
}
void addPackage(Package package) {
_packages.putIfAbsent(package.name, () => new Map<Version, Package>());
_packages[package.name][package.version] = package;
}
}
/// A source used for testing that doesn't natively understand versioning,
/// similar to how the Git and SDK sources work.
class MockVersionlessSource extends Source {
final Map<String, Package> _packages;
final String name = 'versionless';
final bool shouldCache = false;
MockVersionlessSource()
: _packages = <String, Package>{};
Future<bool> install(PackageId id, String path) {
throw 'no';
}
Future<Pubspec> describe(PackageId id) {
return new Future<Pubspec>.immediate(_packages[id.description].pubspec);
}
void addPackage(Package package) {
_packages[package.name] = package;
}
}
Future fakeAsync(callback()) {
var completer = new Completer();
Timer.run(() {
completer.complete(callback());
});
return completer.future;
}
Pair<String, Source> parseSource(String name) {
var match = new RegExp(r"(.*) from (.*)").firstMatch(name);
if (match == null) return new Pair<String, Source>(name, source1);
switch (match[2]) {
case 'mock1': return new Pair<String, Source>(match[1], source1);
case 'mock2': return new Pair<String, Source>(match[1], source2);
case 'root': return new Pair<String, Source>(match[1], null);
case 'versionless':
return new Pair<String, Source>(match[1], versionlessSource);
}
}