Take Sam Elkhateeb's path for "path" dependencies and clean it up some.

BUG=http://code.google.com/p/dart/issues/detail?id=3732

Review URL: https://codereview.chromium.org//12208138

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@18465 260f80e4-7a28-3924-810f-c04153c831b5
This commit is contained in:
rnystrom@google.com 2013-02-13 18:47:19 +00:00
parent 5694a140e3
commit bd0d874a53
11 changed files with 396 additions and 123 deletions

View file

@ -315,7 +315,7 @@ Future<File> createSymlink(from, to) {
from = _getPath(from);
to = _getPath(to);
log.fine("Create symlink $from -> $to.");
log.fine("Creating symlink ($to is a symlink to $from)");
var command = 'ln';
var args = ['-s', from, to];

View file

@ -0,0 +1,78 @@
// Copyright (c) 2013, 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 path_source;
import 'dart:async';
import 'dart:io';
import '../../pkg/path/lib/path.dart' as path;
import 'io.dart';
import 'package.dart';
import 'pubspec.dart';
import 'version.dart';
import 'source.dart';
import 'utils.dart';
// TODO(rnystrom): Support relative paths. (See comment in _validatePath().)
/// A package [Source] that installs packages from a given local file path.
class PathSource extends Source {
final name = 'path';
final shouldCache = false;
Future<Pubspec> describe(PackageId id) {
return defer(() {
_validatePath(id.name, id.description);
return new Pubspec.load(id.name, id.description, systemCache.sources);
});
}
Future<bool> install(PackageId id, String path) {
return defer(() {
try {
_validatePath(id.name, id.description);
} on FormatException catch(err) {
return false;
}
return createPackageSymlink(id.name, id.description, path);
}).then((_) => true);
}
void validateDescription(description, {bool fromLockFile: false}) {
if (description is! String) {
throw new FormatException("The description must be a path string.");
}
}
/// Ensures that [dir] is a valid path. It must be an absolute path that
/// points to an existing directory. Throws a [FormatException] if the path
/// is invalid.
void _validatePath(String name, String dir) {
// Relative paths are not (currently) allowed because the user would expect
// them to be relative to the pubspec where the dependency appears. That in
// turn means that two pubspecs in different locations with the same
// relative path dependency could refer to two different packages. That
// violates pub's rule that a description should uniquely identify a
// package.
//
// At some point, we may want to loosen this, but it will mean tracking
// where a given PackageId appeared.
if (!path.isAbsolute(dir)) {
throw new FormatException(
"Path dependency for package '$name' must be an absolute path. "
"Was '$dir'.");
}
if (fileExists(dir)) {
throw new FormatException(
"Path dependency for package '$name' must refer to a "
"directory, not a file. Was '$dir'.");
}
if (!dirExists(dir)) {
throw new FormatException("Could not find package '$name' at '$dir'.");
}
}
}

View file

@ -13,6 +13,7 @@ import 'io.dart';
import 'io.dart' as io show createTempDir;
import 'log.dart' as log;
import 'package.dart';
import 'path_source.dart';
import 'pubspec.dart';
import 'sdk_source.dart';
import 'source.dart';
@ -46,9 +47,10 @@ class SystemCache {
/// Creates a system cache and registers the standard set of sources.
factory SystemCache.withSources(String rootDir) {
var cache = new SystemCache(rootDir);
cache.register(new SdkSource());
cache.register(new GitSource());
cache.register(new HostedSource());
cache.register(new PathSource());
cache.register(new SdkSource());
cache.sources.setDefault('hosted');
return cache;
}

View file

@ -10,6 +10,7 @@ import '../entrypoint.dart';
import '../hosted_source.dart';
import '../http.dart';
import '../package.dart';
import '../path_source.dart';
import '../utils.dart';
import '../validator.dart';
import '../version.dart';
@ -62,7 +63,13 @@ class DependencyValidator extends Validator {
}
}
warnings.add('Don\'t depend on "${ref.name}" from the ${ref.source.name} '
// Path sources are errors. Other sources are just warnings.
var messages = warnings;
if (ref.source is PathSource) {
messages = errors;
}
messages.add('Don\'t depend on "${ref.name}" from the ${ref.source.name} '
'source. Use the hosted source instead. For example:\n'
'\n'
'dependencies:\n'

View file

@ -0,0 +1,46 @@
// Copyright (c) 2013, 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.
import '../../../../../pkg/path/lib/path.dart' as path;
import '../../test_pub.dart';
main() {
initConfig();
integration('path dependency with absolute path', () {
dir('foo', [
libDir('foo'),
libPubspec('foo', '0.0.1')
]).scheduleCreate();
dir(appPath, [
pubspec({
"name": "myapp",
"dependencies": {
"foo": {"path": path.join(sandboxDir, "foo")}
}
})
]).scheduleCreate();
schedulePub(args: ["install"],
output: new RegExp(r"Dependencies installed!$"));
dir(packagesPath, [
dir("foo", [
file("foo.dart", 'main() => "foo";')
])
]).scheduleValidate();
// Move the packages directory and ensure the symlink still works. That
// will validate that we actually created an absolute symlink.
dir("moved").scheduleCreate();
scheduleRename(packagesPath, "moved/packages");
dir("moved/packages", [
dir("foo", [
file("foo.dart", 'main() => "foo";')
])
]).scheduleValidate();
});
}

View file

@ -0,0 +1,32 @@
// Copyright (c) 2013, 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.
import 'dart:io';
import '../../../../../pkg/path/lib/path.dart' as path;
import '../../test_pub.dart';
main() {
initConfig();
integration('path dependency to non-package directory', () {
// Make an empty directory.
dir('foo').scheduleCreate();
var fooPath = path.join(sandboxDir, "foo");
dir(appPath, [
pubspec({
"name": "myapp",
"dependencies": {
"foo": {"path": fooPath}
}
})
]).scheduleCreate();
schedulePub(args: ['install'],
error:
new RegExp('Package "foo" doesn\'t have a pubspec.yaml file.'),
exitCode: 1);
});
}

View file

@ -0,0 +1,31 @@
// Copyright (c) 2013, 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.
import 'dart:io';
import '../../../../../pkg/path/lib/path.dart' as path;
import '../../../../pub/exit_codes.dart' as exit_codes;
import '../../test_pub.dart';
main() {
initConfig();
integration('path dependency to non-existent directory', () {
var badPath = path.join(sandboxDir, "bad_path");
dir(appPath, [
pubspec({
"name": "myapp",
"dependencies": {
"foo": {"path": badPath}
}
})
]).scheduleCreate();
schedulePub(args: ['install'],
error:
new RegExp("Could not find package 'foo' at '$badPath'."),
exitCode: exit_codes.DATA);
});
}

View file

@ -0,0 +1,35 @@
// Copyright (c) 2013, 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.
import '../../../../../pkg/path/lib/path.dart' as path;
import '../../../../pub/exit_codes.dart' as exit_codes;
import '../../test_pub.dart';
main() {
initConfig();
integration('path dependency when path is a file', () {
dir('foo', [
libDir('foo'),
libPubspec('foo', '0.0.1')
]).scheduleCreate();
file('dummy.txt', '').scheduleCreate();
var dummyPath = path.join(sandboxDir, 'dummy.txt');
dir(appPath, [
pubspec({
"name": "myapp",
"dependencies": {
"foo": {"path": dummyPath}
}
})
]).scheduleCreate();
schedulePub(args: ['install'],
error: new RegExp("Path dependency for package 'foo' must refer to a "
"directory, not a file. Was '$dummyPath'."),
exitCode: exit_codes.DATA);
});
}

View file

@ -0,0 +1,25 @@
// Copyright (c) 2013, 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.
import '../../../../pub/exit_codes.dart' as exit_codes;
import '../../test_pub.dart';
main() {
initConfig();
integration('path dependencies cannot use relative paths', () {
dir(appPath, [
pubspec({
"name": "myapp",
"dependencies": {
"foo": {"path": "../foo"}
}
})
]).scheduleCreate();
schedulePub(args: ['install'],
error: new RegExp("Path dependency for package 'foo' must be an "
"absolute path. Was '../foo'."),
exitCode: exit_codes.DATA);
});
}

View file

@ -31,6 +31,7 @@ import '../../pub/git_source.dart';
import '../../pub/hosted_source.dart';
import '../../pub/http.dart';
import '../../pub/io.dart';
import '../../pub/path_source.dart';
import '../../pub/sdk_source.dart';
import '../../pub/system_cache.dart';
import '../../pub/utils.dart';
@ -404,6 +405,9 @@ Future<Map> _dependencyListToMap(List<Map> dependencies) {
case "hosted":
source = new HostedSource();
break;
case "path":
source = new PathSource();
break;
case "sdk":
source = new SdkSource();
break;
@ -427,6 +431,8 @@ String _packageName(String sourceName, description) {
case "hosted":
if (description is String) return description;
return description['name'];
case "path":
return basename(description);
case "sdk":
return description;
default:
@ -434,6 +440,10 @@ String _packageName(String sourceName, description) {
}
}
/// The full path to the created sandbox directory for an integration test.
String get sandboxDir => _sandboxDir.path;
Directory _sandboxDir;
/// The path of the package cache directory used for tests. Relative to the
/// sandbox directory.
final String cachePath = "cache";
@ -488,27 +498,32 @@ void _integration(String description, void body(), [Function testFn]) {
file('version', '0.1.2.3')
]).scheduleCreate();
_sandboxDir = createTempDir();
// Schedule the test.
body();
// Run all of the scheduled tasks. If an error occurs, it will propagate
// through the futures back up to here where we can hand it off to unittest.
var asyncDone = expectAsync0(() {});
var sandboxDir = createTempDir();
return timeout(_runScheduled(sandboxDir, _scheduled),
return timeout(_runScheduled(_scheduled),
_TIMEOUT, 'waiting for a test to complete').catchError((e) {
return _runScheduled(sandboxDir, _scheduledOnException).then((_) {
return _runScheduled(_scheduledOnException).then((_) {
// Rethrow the original error so it keeps propagating.
throw e;
});
}).whenComplete(() {
// Clean up after ourselves. Do this first before reporting back to
// unittest because it will advance to the next test immediately.
return _runScheduled(sandboxDir, _scheduledCleanup).then((_) {
return _runScheduled(_scheduledCleanup).then((_) {
_scheduled = null;
_scheduledCleanup = null;
_scheduledOnException = null;
if (sandboxDir != null) return deleteDir(sandboxDir);
if (_sandboxDir != null) {
var dir = _sandboxDir;
_sandboxDir = null;
return deleteDir(dir);
}
});
}).then((_) {
// If we got here, the test completed successfully so tell unittest so.
@ -532,6 +547,14 @@ String get testDirectory {
return getFullPath(dir);
}
/// Schedules renaming (moving) the directory at [from] to [to], both of which
/// are assumed to be relative to [sandboxDir].
void scheduleRename(String from, String to) {
_schedule((sandboxDir) {
return renameDir(join(sandboxDir, from), join(sandboxDir, to));
});
}
/// Schedules a call to the Pub command-line utility. Runs Pub with [args] and
/// validates that its results match [output], [error], and [exitCode].
void schedulePub({List args, Pattern output, Pattern error,
@ -602,7 +625,6 @@ void confirmPublish(ScheduledProcess pub) {
pub.writeLine("y");
}
/// Calls [fn] with appropriately modified arguments to run a pub process. [fn]
/// should have the same signature as [startProcess], except that the returned
/// [Future] may have a type other than [Process].
@ -677,7 +699,7 @@ void useMockClient(MockClient client) {
});
}
Future _runScheduled(Directory parentDir, List<_ScheduledEvent> scheduled) {
Future _runScheduled(List<_ScheduledEvent> scheduled) {
if (scheduled == null) return new Future.immediate(null);
var iterator = scheduled.iterator;
@ -688,7 +710,7 @@ Future _runScheduled(Directory parentDir, List<_ScheduledEvent> scheduled) {
return new Future.immediate(null);
}
var future = iterator.current(parentDir);
var future = iterator.current(_sandboxDir);
if (future != null) {
return future.then(runNextEvent);
} else {
@ -1169,7 +1191,7 @@ class NothingDescriptor extends Descriptor {
typedef Validator ValidatorCreator(Entrypoint entrypoint);
/// Schedules a single [Validator] to run on the [appPath]. Returns a scheduled
/// Future that contains the erros and warnings produced by that validator.
/// Future that contains the errors and warnings produced by that validator.
Future<Pair<List<String>, List<String>>> schedulePackageValidation(
ValidatorCreator fn) {
return _scheduleValue((sandboxDir) {

View file

@ -10,9 +10,10 @@ import 'dart:json' as json;
import 'dart:math' as math;
import 'test_pub.dart';
import '../../../pkg/unittest/lib/unittest.dart';
import '../../../pkg/http/lib/http.dart' as http;
import '../../../pkg/http/lib/testing.dart';
import '../../../pkg/path/lib/path.dart' as path;
import '../../../pkg/unittest/lib/unittest.dart';
import '../../pub/entrypoint.dart';
import '../../pub/io.dart';
import '../../pub/validator.dart';
@ -38,6 +39,16 @@ void expectValidationWarning(ValidatorCreator fn) {
expectLater(schedulePackageValidation(fn), pairOf(isEmpty, isNot(isEmpty)));
}
expectDependencyValidationError(String error) {
expectLater(schedulePackageValidation(dependency),
pairOf(someElement(contains(error)), isEmpty));
}
expectDependencyValidationWarning(String warning) {
expectLater(schedulePackageValidation(dependency),
pairOf(isEmpty, someElement(contains(warning))));
}
Validator compiledDartdoc(Entrypoint entrypoint) =>
new CompiledDartdocValidator(entrypoint);
@ -66,7 +77,31 @@ Validator utf8Readme(Entrypoint entrypoint) =>
void scheduleNormalPackage() => normalPackage.scheduleCreate();
/// Sets up a test package with dependency [dep] and mocks a server with
/// [hostedVersions] of the package available.
setUpDependency(Map dep, {List<String> hostedVersions}) {
useMockClient(new MockClient((request) {
expect(request.method, equals("GET"));
expect(request.url.path, equals("/packages/foo.json"));
if (hostedVersions == null) {
return new Future.immediate(new http.Response("not found", 404));
} else {
return new Future.immediate(new http.Response(json.stringify({
"name": "foo",
"uploaders": ["nweiz@google.com"],
"versions": hostedVersions
}), 200));
}
}));
dir(appPath, [
libPubspec("test_pkg", "1.0.0", deps: [dep])
]).scheduleCreate();
}
main() {
initConfig();
group('should consider a package valid if it', () {
setUp(scheduleNormalPackage);
@ -326,124 +361,88 @@ main() {
expectValidationError(lib);
});
group('has a dependency with a non-hosted source', () {
group('where a hosted version of that dependency exists', () {
integration("and should suggest the hosted package's primary "
"version", () {
useMockClient(new MockClient((request) {
expect(request.method, equals("GET"));
expect(request.url.path, equals("/packages/foo.json"));
return new Future.immediate(new http.Response(json.stringify({
"name": "foo",
"uploaders": ["nweiz@google.com"],
"versions": ["3.0.0-pre", "2.0.0", "1.0.0"]
}), 200));
}));
dir(appPath, [
libPubspec("test_pkg", "1.0.0", deps: [
{'git': 'git://github.com/dart-lang/foo'}
])
]).scheduleCreate();
expectLater(schedulePackageValidation(dependency),
pairOf(isEmpty, someElement(contains(
' foo: ">=2.0.0 <3.0.0"'))));
group('has a git dependency', () {
group('where a hosted version exists', () {
integration("and should suggest the hosted primary version", () {
setUpDependency({'git': 'git://github.com/dart-lang/foo'},
hostedVersions: ["3.0.0-pre", "2.0.0", "1.0.0"]);
expectDependencyValidationWarning(' foo: ">=2.0.0 <3.0.0"');
});
integration("and should suggest the hosted package's prerelease "
"version if it's the only version available", () {
useMockClient(new MockClient((request) {
expect(request.method, equals("GET"));
expect(request.url.path, equals("/packages/foo.json"));
return new Future.immediate(new http.Response(json.stringify({
"name": "foo",
"uploaders": ["nweiz@google.com"],
"versions": ["3.0.0-pre", "2.0.0-pre"]
}), 200));
}));
dir(appPath, [
libPubspec("test_pkg", "1.0.0", deps: [
{'git': 'git://github.com/dart-lang/foo'}
])
]).scheduleCreate();
expectLater(schedulePackageValidation(dependency),
pairOf(isEmpty, someElement(contains(
' foo: ">=3.0.0-pre <4.0.0"'))));
integration("and should suggest the hosted prerelease version if "
"it's the only version available", () {
setUpDependency({'git': 'git://github.com/dart-lang/foo'},
hostedVersions: ["3.0.0-pre", "2.0.0-pre"]);
expectDependencyValidationWarning(' foo: ">=3.0.0-pre <4.0.0"');
});
integration("and should suggest a tighter constraint if the primary "
"version is pre-1.0.0", () {
useMockClient(new MockClient((request) {
expect(request.method, equals("GET"));
expect(request.url.path, equals("/packages/foo.json"));
return new Future.immediate(new http.Response(json.stringify({
"name": "foo",
"uploaders": ["nweiz@google.com"],
"versions": ["0.0.1", "0.0.2"]
}), 200));
}));
dir(appPath, [
libPubspec("test_pkg", "1.0.0", deps: [
{'git': 'git://github.com/dart-lang/foo'}
])
]).scheduleCreate();
expectLater(schedulePackageValidation(dependency),
pairOf(isEmpty, someElement(contains(
' foo: ">=0.0.2 <0.0.3"'))));
integration("and should suggest a tighter constraint if primary is "
"pre-1.0.0", () {
setUpDependency({'git': 'git://github.com/dart-lang/foo'},
hostedVersions: ["0.0.1", "0.0.2"]);
expectDependencyValidationWarning(' foo: ">=0.0.2 <0.0.3"');
});
});
group('where no hosted version of that dependency exists', () {
group('where no hosted version exists', () {
integration("and should use the other source's version", () {
useMockClient(new MockClient((request) {
expect(request.method, equals("GET"));
expect(request.url.path, equals("/packages/foo.json"));
return new Future.immediate(new http.Response("not found", 404));
}));
dir(appPath, [
libPubspec("test_pkg", "1.0.0", deps: [
{
'git': {'url': 'git://github.com/dart-lang/foo'},
'version': '>=1.0.0 <2.0.0'
}
])
]).scheduleCreate();
expectLater(schedulePackageValidation(dependency),
pairOf(isEmpty, someElement(contains(
' foo: ">=1.0.0 <2.0.0"'))));
setUpDependency({
'git': 'git://github.com/dart-lang/foo',
'version': '>=1.0.0 <2.0.0'
});
expectDependencyValidationWarning(' foo: ">=1.0.0 <2.0.0"');
});
integration("and should use the other source's unquoted version if "
"it's concrete", () {
useMockClient(new MockClient((request) {
expect(request.method, equals("GET"));
expect(request.url.path, equals("/packages/foo.json"));
"concrete", () {
setUpDependency({
'git': 'git://github.com/dart-lang/foo',
'version': '0.2.3'
});
expectDependencyValidationWarning(' foo: 0.2.3');
});
});
});
return new Future.immediate(new http.Response("not found", 404));
}));
group('has a path dependency', () {
group('where a hosted version exists', () {
integration("and should suggest the hosted primary version", () {
setUpDependency({'path': path.join(sandboxDir, 'foo')},
hostedVersions: ["3.0.0-pre", "2.0.0", "1.0.0"]);
expectDependencyValidationError(' foo: ">=2.0.0 <3.0.0"');
});
dir(appPath, [
libPubspec("test_pkg", "1.0.0", deps: [
{
'git': {'url': 'git://github.com/dart-lang/foo'},
'version': '0.2.3'
}
])
]).scheduleCreate();
integration("and should suggest the hosted prerelease version if "
"it's the only version available", () {
setUpDependency({'path': path.join(sandboxDir, 'foo')},
hostedVersions: ["3.0.0-pre", "2.0.0-pre"]);
expectDependencyValidationError(' foo: ">=3.0.0-pre <4.0.0"');
});
expectLater(schedulePackageValidation(dependency),
pairOf(isEmpty, someElement(contains(' foo: 0.2.3'))));
integration("and should suggest a tighter constraint if primary is "
"pre-1.0.0", () {
setUpDependency({'path': path.join(sandboxDir, 'foo')},
hostedVersions: ["0.0.1", "0.0.2"]);
expectDependencyValidationError(' foo: ">=0.0.2 <0.0.3"');
});
});
group('where no hosted version exists', () {
integration("and should use the other source's version", () {
setUpDependency({
'path': path.join(sandboxDir, 'foo'),
'version': '>=1.0.0 <2.0.0'
});
expectDependencyValidationError(' foo: ">=1.0.0 <2.0.0"');
});
integration("and should use the other source's unquoted version if "
"concrete", () {
setUpDependency({
'path': path.join(sandboxDir, 'foo'),
'version': '0.2.3'
});
expectDependencyValidationError(' foo: 0.2.3');
});
});
});
@ -507,9 +506,7 @@ main() {
}))
]).scheduleCreate();
expectLater(schedulePackageValidation(dependency),
pairOf(isEmpty, someElement(contains(
' foo: ">=1.2.3 <2.0.0"'))));
expectDependencyValidationWarning(' foo: ">=1.2.3 <2.0.0"');
});
integration('and it should suggest a concrete constraint if the locked '
@ -532,9 +529,7 @@ main() {
}))
]).scheduleCreate();
expectLater(schedulePackageValidation(dependency),
pairOf(isEmpty, someElement(contains(
' foo: ">=0.1.2 <0.1.3"'))));
expectDependencyValidationWarning(' foo: ">=0.1.2 <0.1.3"');
});
});
});