Make pub publish more user friendly:

- Show the contents of their package.
- Let the confirm the upload even if there are no warnings.
- Make the error/warning text less scary.
- Validate that the pubspec has a version client-side.

Addresses #7175 and #7219.

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

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@16068 260f80e4-7a28-3924-810f-c04153c831b5
This commit is contained in:
rnystrom@google.com 2012-12-12 22:22:38 +00:00
parent 7050101ea8
commit 0f9bed70a5
15 changed files with 449 additions and 67 deletions

View file

@ -10,6 +10,7 @@ import 'dart:uri';
import '../../pkg/args/lib/args.dart';
import '../../pkg/http/lib/http.dart' as http;
import 'directory_tree.dart';
import 'git.dart' as git;
import 'io.dart';
import 'log.dart' as log;
@ -33,21 +34,13 @@ class LishCommand extends PubCommand {
/// The URL of the server to which to upload the package.
Uri get server => new Uri.fromString(commandOptions['server']);
Future onRun() {
Future _publish(packageBytes) {
var cloudStorageUrl;
return oauth2.withClient(cache, (client) {
// TODO(nweiz): Cloud Storage can provide an XML-formatted error. We
// should report that error and exit.
return Futures.wait([
client.get(server.resolve("/packages/versions/new.json")),
_filesToPublish.transform((files) {
log.fine('Archiving and publishing ${entrypoint.root}.');
return createTarGz(files, baseDir: entrypoint.root.dir);
}).chain(consumeInputStream),
_validate()
]).chain((results) {
var response = results[0];
var packageBytes = results[1];
var newUri = server.resolve("/packages/versions/new.json");
return client.get(newUri).chain((response) {
var parameters = _parseJson(response);
var url = _expectField(parameters, 'url', response);
@ -98,19 +91,38 @@ class LishCommand extends PubCommand {
}
} else if (e is oauth2.ExpirationException) {
log.error("Pub's authorization to upload packages has expired and "
"can't be automatically refreshed.");
return onRun();
"can't be automatically refreshed.");
return _publish(packageBytes);
} else if (e is oauth2.AuthorizationException) {
var message = "OAuth2 authorization failed";
if (e.description != null) message = "$message (${e.description})";
log.error("$message.");
return oauth2.clearCredentials(cache).chain((_) => onRun());
return oauth2.clearCredentials(cache).chain((_) =>
_publish(packageBytes));
} else {
throw e;
}
});
}
Future onRun() {
var files;
return _filesToPublish.transform((f) {
files = f;
log.fine('Archiving and publishing ${entrypoint.root}.');
return createTarGz(files, baseDir: entrypoint.root.dir);
}).chain(consumeInputStream).chain((packageBytes) {
// Show the package contents so the user can verify they look OK.
var package = entrypoint.root;
log.message(
'Publishing "${package.name}" ${package.version}:\n'
'${generateTree(files)}');
// Validate the package.
return _validate().chain((_) => _publish(packageBytes));
});
}
/// The basenames of files that are automatically excluded from archives.
final _BLACKLISTED_FILES = const ['pubspec.lock'];
@ -123,6 +135,14 @@ class LishCommand extends PubCommand {
/// will return all non-hidden files.
Future<List<String>> get _filesToPublish {
var rootDir = entrypoint.root.dir;
// TODO(rnystrom): listDir() returns real file paths after symlinks are
// resolved. This means if libDir contains a symlink, the resulting paths
// won't appear to be within it, which confuses relativeTo(). Work around
// that here by making sure we have the real path to libDir. Remove this
// when #7346 is fixed.
rootDir = new File(rootDir).fullPathSync();
return Futures.wait([
dirExists(join(rootDir, '.git')),
git.isInstalled
@ -135,15 +155,25 @@ class LishCommand extends PubCommand {
return listDir(rootDir, recursive: true).chain((entries) {
return Futures.wait(entries.map((entry) {
return fileExists(entry).transform((isFile) => isFile ? entry : null);
return fileExists(entry).transform((isFile) {
// Skip directories.
if (!isFile) return null;
// TODO(rnystrom): Making these relative will break archive
// creation if the cwd is ever *not* the package root directory.
// Should instead only make these relative right before generating
// the tree display (which is what really needs them to be).
// Make it relative to the package root.
return relativeTo(entry, rootDir);
});
}));
});
}).transform((files) => files.filter((file) {
if (file == null || _BLACKLISTED_FILES.contains(basename(file))) {
return false;
}
return !splitPath(relativeTo(file, rootDir))
.some(_BLACKLISTED_DIRECTORIES.contains);
return !splitPath(file).some(_BLACKLISTED_DIRECTORIES.contains);
}));
}
@ -180,15 +210,22 @@ class LishCommand extends PubCommand {
var errors = pair.first;
var warnings = pair.last;
if (errors.isEmpty && warnings.isEmpty) return new Future.immediate(null);
if (!errors.isEmpty) throw "Package validation failed.";
if (!errors.isEmpty) {
throw "Sorry, your package is missing "
"${(errors.length > 1) ? 'some requirements' : 'a requirement'} "
"and can't be published yet.\nFor more information, see: "
"http://pub.dartlang.org/doc/pub-lish.html.\n";
}
var s = warnings.length == 1 ? '' : 's';
stdout.writeString("Package has ${warnings.length} warning$s. Upload "
"anyway (y/n)? ");
return readLine().transform((line) {
if (new RegExp(r"^[yY]").hasMatch(line)) return;
throw "Package upload canceled.";
var message = 'Looks great! Are you ready to upload your package';
if (!warnings.isEmpty) {
var s = warnings.length == 1 ? '' : 's';
message = "Package has ${warnings.length} warning$s. Upload anyway";
}
return confirm(message).transform((confirmed) {
if (!confirmed) throw "Package upload canceled.";
});
});
}

View file

@ -0,0 +1,133 @@
// 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.
/// A simple library for rendering a list of files as a directory tree.
library directory_tree;
import 'log.dart' as log;
import 'path.dart' as path;
/// Draws a directory tree for the given list of files. Given a list of files
/// like:
///
/// TODO
/// example/console_example.dart
/// example/main.dart
/// example/web copy/web_example.dart
/// test/absolute_test.dart
/// test/basename_test.dart
/// test/dirname_test.dart
/// test/extension_test.dart
/// test/is_absolute_test.dart
/// test/is_relative_test.dart
/// test/join_test.dart
/// test/normalize_test.dart
/// test/relative_test.dart
/// test/split_test.dart
/// .gitignore
/// README.md
/// lib/path.dart
/// pubspec.yaml
/// test/all_test.dart
/// test/path_posix_test.dart
/// test/path_windows_test.dart
///
/// this will render:
///
/// |-- .gitignore
/// |-- README.md
/// |-- TODO
/// |-- example
/// | |-- console_example.dart
/// | |-- main.dart
/// | '-- web copy
/// | '-- web_example.dart
/// |-- lib
/// | '-- path.dart
/// |-- pubspec.yaml
/// '-- test
/// |-- absolute_test.dart
/// |-- all_test.dart
/// |-- basename_test.dart
/// | (7 more...)
/// |-- path_windows_test.dart
/// |-- relative_test.dart
/// '-- split_test.dart
///
String generateTree(List<String> files) {
// Parse out the files into a tree of nested maps.
var root = {};
for (var file in files) {
var parts = path.split(file);
var directory = root;
for (var part in path.split(file)) {
directory = directory.putIfAbsent(part, () => {});
}
}
// Walk the map recursively and render to a string.
var buffer = new StringBuffer();
_draw(buffer, '', false, null, root);
return buffer.toString();
}
void _drawLine(StringBuffer buffer, String prefix, bool isLastChild,
String name) {
// Print lines.
buffer.add(prefix);
if (name != null) {
if (isLastChild) {
buffer.add("'-- ");
} else {
buffer.add("|-- ");
}
}
// Print name.
buffer.add(name);
buffer.add('\n');
}
String _getPrefix(bool isRoot, bool isLast) {
if (isRoot) return "";
if (isLast) return " ";
return "| ";
}
void _draw(StringBuffer buffer, String prefix, bool isLast,
String name, Map children) {
// Don't draw a line for the root node.
if (name != null) _drawLine(buffer, prefix, isLast, name);
// Recurse to the children.
var childNames = new List.from(children.keys);
childNames.sort();
_drawChild(bool isLastChild, String child) {
var childPrefix = _getPrefix(name == null, isLast);
_draw(buffer, '$prefix$childPrefix', isLastChild, child, children[child]);
}
if (childNames.length <= 10) {
// Not too many, so show all the children.
for (var i = 0; i < childNames.length; i++) {
_drawChild(i == childNames.length - 1, childNames[i]);
}
} else {
// Show the first few.
_drawChild(false, childNames[0]);
_drawChild(false, childNames[1]);
_drawChild(false, childNames[2]);
// Elide the middle ones.
buffer.add(prefix);
buffer.add(_getPrefix(name == null, isLast));
buffer.add('| (${childNames.length - 6} more...)\n');
// Show the last few.
_drawChild(false, childNames[childNames.length - 3]);
_drawChild(false, childNames[childNames.length - 2]);
_drawChild(true, childNames[childNames.length - 1]);
}
}

View file

@ -446,6 +446,18 @@ String relativeToPub(String target) {
/// A StringInputStream reading from stdin.
final _stringStdin = new StringInputStream(stdin);
/// Displays a message and reads a yes/no confirmation from the user. Returns
/// a [Future] that completes to `true` if the user confirms or `false` if they
/// do not.
///
/// This will automatically append " (y/n)?" to the message, so [message]
/// should just be a fragment like, "Are you sure you want to proceed".
Future<bool> confirm(String message) {
log.fine('Showing confirm message: $message');
stdout.writeString("$message (y/n)? ");
return readLine().transform((line) => new RegExp(r"^[yY]").hasMatch(line));
}
/// Returns a single line read from a [StringInputStream]. By default, reads
/// from stdin.
///
@ -475,7 +487,9 @@ Future<String> readLine([StringInputStream stream]) {
stream.onLine = () {
removeCallbacks();
completer.complete(stream.readLine());
var line = stream.readLine();
log.io('Read line: $line');
completer.complete(line);
};
stream.onError = (e) {
@ -929,7 +943,7 @@ Future<bool> _extractTarGzWindows(InputStream stream, String destination) {
InputStream createTarGz(List contents, {baseDir}) {
var buffer = new StringBuffer();
buffer.add('Creating .tag.gz stream containing:\n');
contents.forEach(buffer.add);
contents.forEach((file) => buffer.add('$file\n'));
log.fine(buffer.toString());
// TODO(nweiz): Propagate errors to the returned stream (including non-zero

View file

@ -102,10 +102,13 @@ String join(String part1, [String part2, String part3, String part4,
// TODO(nweiz): add a UNC example for Windows once issue 7323 is fixed.
/// Splits [path] into its components using the current platform's [separator].
/// Example:
///
/// path.split('path/to/foo'); // -> ['path', 'to', 'foo']
///
/// The path will *not* be normalized before splitting.
///
/// path.split('path/../foo'); // -> ['path', '..', 'foo']
///
/// If [path] is absolute, the root directory will be the first element in the
/// array. Example:
///
@ -315,6 +318,10 @@ class Builder {
///
/// builder.split('path/to/foo'); // -> ['path', 'to', 'foo']
///
/// The path will *not* be normalized before splitting.
///
/// builder.split('path/../foo'); // -> ['path', '..', 'foo']
///
/// If [path] is absolute, the root directory will be the first element in the
/// array. Example:
///

View file

@ -57,17 +57,17 @@ abstract class Validator {
var warnings = flatten(validators.map((validator) => validator.warnings));
if (!errors.isEmpty) {
log.error("== Errors:");
log.error("Missing requirements:");
for (var error in errors) {
log.error("* $error");
log.error("* ${Strings.join(error.split('\n'), '\n ')}");
}
log.error("");
}
if (!warnings.isEmpty) {
log.warning("== Warnings:");
log.warning("Suggestions:");
for (var warning in warnings) {
log.warning("* $warning");
log.warning("* ${Strings.join(warning.split('\n'), '\n ')}");
}
log.warning("");
}

View file

@ -24,8 +24,8 @@ class LibValidator extends Validator {
return dirExists(libDir).chain((libDirExists) {
if (!libDirExists) {
errors.add('Your package must have a "lib/" directory so users have '
'something to import.');
errors.add('You must have a "lib" directory.\n'
"Without that, users cannot import any code from your package.");
return new Future.immediate(null);
}
@ -39,11 +39,12 @@ class LibValidator extends Validator {
return listDir(libDir).transform((files) {
files = files.map((file) => relativeTo(file, libDir));
if (files.isEmpty) {
errors.add('The "lib/" directory may not be empty so users have '
'something to import');
errors.add('You must have a non-empty "lib" directory.\n'
"Without that, users cannot import any code from your package.");
} else if (files.length == 1 && files.first == "src") {
errors.add('The "lib/" directory must contain something other than '
'"src/" so users have something to import');
errors.add('The "lib" directory must contain something other than '
'"src".\n'
"Otherwise, users cannot import any code from your package.");
}
});
});

View file

@ -20,9 +20,10 @@ class LicenseValidator extends Validator {
r"^([a-zA-Z0-9]+[-_])?(LICENSE|COPYING)(\..*)?$");
if (files.map(basename).some(licenseLike.hasMatch)) return;
errors.add("Your package must have a COPYING or LICENSE file containing "
"an open-source license. For more details, see "
"http://pub.dartlang.org/doc/pub-lish.html.");
errors.add(
"You must have a COPYING or LICENSE file in the root directory.\n"
"An open-source license helps ensure people can legally use your "
"code.");
});
}
}

View file

@ -45,14 +45,15 @@ class NameValidator extends Validator {
if (name == "") {
errors.add("$description may not be empty.");
} else if (!new RegExp(r"^[a-zA-Z0-9_]*$").hasMatch(name)) {
errors.add("$description must be a valid Dart identifier: it may only "
"contain letters, numbers, and underscores.");
errors.add("$description may only contain letters, numbers, and "
"underscores.\n"
"Using a valid Dart identifier makes the name usable in Dart code.");
} else if (!new RegExp(r"^[a-zA-Z]").hasMatch(name)) {
errors.add("$description must be a valid Dart identifier: it must begin "
"with a letter.");
errors.add("$description must begin with letter.\n"
"Using a valid Dart identifier makes the name usable in Dart code.");
} else if (_RESERVED_WORDS.contains(name.toLowerCase())) {
errors.add("$description must be a valid Dart identifier: it may not be "
"a reserved word in Dart.");
errors.add("$description may not be a reserved word in Dart.\n"
"Using a valid Dart identifier makes the name usable in Dart code.");
} else if (new RegExp(r"[A-Z]").hasMatch(name)) {
warnings.add('$description should be lower-case. Maybe use '
'"${_unCamelCase(name)}"?');

View file

@ -7,6 +7,7 @@ library pubspec_field_validator;
import '../entrypoint.dart';
import '../system_cache.dart';
import '../validator.dart';
import '../version.dart';
/// A validator that checks that the pubspec has valid "author" and "homepage"
/// fields.
@ -20,7 +21,7 @@ class PubspecFieldValidator extends Validator {
var author = pubspec.fields['author'];
var authors = pubspec.fields['authors'];
if (author == null && authors == null) {
errors.add('pubspec.yaml is missing an "author" or "authors" field.');
errors.add('Your pubspec.yaml must have an "author" or "authors" field.');
} else {
if (authors == null) authors = [author];
@ -28,24 +29,29 @@ class PubspecFieldValidator extends Validator {
var hasEmail = new RegExp(r"<[^>]+> *$");
for (var authorName in authors) {
if (!hasName.hasMatch(authorName)) {
warnings.add('Author "$authorName" in pubspec.yaml is missing a '
warnings.add('Author "$authorName" in pubspec.yaml should have a '
'name.');
}
if (!hasEmail.hasMatch(authorName)) {
warnings.add('Author "$authorName" in pubspec.yaml is missing an '
'email address (e.g. "name <email>").');
warnings.add('Author "$authorName" in pubspec.yaml should have an '
'email address\n(e.g. "name <email>").');
}
}
}
var homepage = pubspec.fields['homepage'];
if (homepage == null) {
errors.add('pubspec.yaml is missing a "homepage" field.');
errors.add('Your pubspec.yaml is missing a "homepage" field.');
}
var description = pubspec.fields['description'];
if (description == null) {
errors.add('pubspec.yaml is missing a "description" field.');
errors.add('Your pubspec.yaml is missing a "description" field.');
}
var version = pubspec.fields['version'];
if (version == null) {
errors.add('Your pubspec.yaml is missing a "version" field.');
}
return new Future.immediate(null);

View file

@ -0,0 +1,114 @@
// 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 lock_file_test;
import '../../../pkg/unittest/lib/unittest.dart';
import '../../pub/directory_tree.dart';
main() {
test('no files', () {
expect(generateTree([]), equals(""));
});
test('up to ten files in one directory are shown', () {
var files = [
"a.dart",
"b.dart",
"c.dart",
"d.dart",
"e.dart",
"f.dart",
"g.dart",
"h.dart",
"i.dart",
"j.dart"
];
expect(generateTree(files), equals("""
|-- a.dart
|-- b.dart
|-- c.dart
|-- d.dart
|-- e.dart
|-- f.dart
|-- g.dart
|-- h.dart
|-- i.dart
'-- j.dart
"""));
});
test('files are elided if there are more than ten', () {
var files = [
"a.dart",
"b.dart",
"c.dart",
"d.dart",
"e.dart",
"f.dart",
"g.dart",
"h.dart",
"i.dart",
"j.dart",
"k.dart"
];
expect(generateTree(files), equals("""
|-- a.dart
|-- b.dart
|-- c.dart
| (5 more...)
|-- i.dart
|-- j.dart
'-- k.dart
"""));
});
test('a complex example', () {
var files = [
"TODO",
"example/console_example.dart",
"example/main.dart",
"example/web copy/web_example.dart",
"test/absolute_test.dart",
"test/basename_test.dart",
"test/dirname_test.dart",
"test/extension_test.dart",
"test/is_absolute_test.dart",
"test/is_relative_test.dart",
"test/join_test.dart",
"test/normalize_test.dart",
"test/relative_test.dart",
"test/split_test.dart",
".gitignore",
"README.md",
"lib/path.dart",
"pubspec.yaml",
"test/all_test.dart",
"test/path_posix_test.dart",
"test/path_windows_test.dart"
];
expect(generateTree(files), equals("""
|-- .gitignore
|-- README.md
|-- TODO
|-- example
| |-- console_example.dart
| |-- main.dart
| '-- web copy
| '-- web_example.dart
|-- lib
| '-- path.dart
|-- pubspec.yaml
'-- test
|-- absolute_test.dart
|-- all_test.dart
|-- basename_test.dart
| (7 more...)
|-- path_windows_test.dart
|-- relative_test.dart
'-- split_test.dart
"""));
});
}

View file

@ -21,6 +21,7 @@ main() {
() {
var server = new ScheduledServer();
var pub = startPubLish(server);
confirmPublish(pub);
authorizePub(pub, server);
server.handle('GET', '/packages/versions/new.json', (request, response) {
@ -41,6 +42,7 @@ main() {
var server = new ScheduledServer();
credentialsFile(server, 'access token').scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
server.handle('GET', '/packages/versions/new.json', (request, response) {
expect(request.headers.value('authorization'),
@ -63,6 +65,7 @@ main() {
.scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
server.handle('POST', '/token', (request, response) {
return consumeInputStream(request.inputStream).transform((bytes) {
@ -102,6 +105,7 @@ main() {
.scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
expectLater(pub.nextErrLine(), equals("Pub's authorization to upload "
"packages has expired and can't be automatically refreshed."));
@ -129,6 +133,7 @@ main() {
]).scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
authorizePub(pub, server, "new access token");
server.handle('GET', '/packages/versions/new.json', (request, response) {
@ -148,8 +153,12 @@ main() {
void authorizePub(ScheduledProcess pub, ScheduledServer server,
[String accessToken="access token"]) {
expectLater(pub.nextLine(), equals('Pub needs your '
'authorization to upload packages on your behalf.'));
// TODO(rnystrom): The confirm line is run together with this one because
// in normal usage, the user will have entered a newline on stdin which
// gets echoed to the terminal. Do something better here?
expectLater(pub.nextLine(), equals(
'Looks great! Are you ready to upload your package (y/n)? '
'Pub needs your authorization to upload packages on your behalf.'));
expectLater(pub.nextLine().chain((line) {
var match = new RegExp(r'[?&]redirect_uri=([0-9a-zA-Z%+-]+)[$&]')

View file

@ -153,8 +153,11 @@ main() {
group('split', () {
test('simple cases', () {
expect(builder.split(''), []);
expect(builder.split('.'), ['.']);
expect(builder.split('..'), ['..']);
expect(builder.split('foo'), equals(['foo']));
expect(builder.split('foo/bar'), equals(['foo', 'bar']));
expect(builder.split('foo/bar.txt'), equals(['foo', 'bar.txt']));
expect(builder.split('foo/bar/baz'), equals(['foo', 'bar', 'baz']));
expect(builder.split('foo/../bar/./baz'),
equals(['foo', '..', 'bar', '.', 'baz']));

View file

@ -172,9 +172,12 @@ main() {
group('split', () {
test('simple cases', () {
expect(builder.split(''), []);
expect(builder.split('.'), ['.']);
expect(builder.split('..'), ['..']);
expect(builder.split('foo'), equals(['foo']));
expect(builder.split(r'foo\bar'), equals(['foo', 'bar']));
expect(builder.split(r'foo\bar\baz'), equals(['foo', 'bar', 'baz']));
expect(builder.split(r'foo\bar.txt'), equals(['foo', 'bar.txt']));
expect(builder.split(r'foo\bar/baz'), equals(['foo', 'bar', 'baz']));
expect(builder.split(r'foo\..\bar\.\baz'),
equals(['foo', '..', 'bar', '.', 'baz']));
expect(builder.split(r'foo\\bar\\\baz'), equals(['foo', 'bar', 'baz']));

View file

@ -53,6 +53,8 @@ main() {
var server = new ScheduledServer();
credentialsFile(server, 'access token').scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
handleUploadForm(server);
handleUpload(server);
@ -63,7 +65,12 @@ main() {
response.outputStream.close();
});
expectLater(pub.nextLine(), equals('Package test_pkg 1.0.0 uploaded!'));
// TODO(rnystrom): The confirm line is run together with this one because
// in normal usage, the user will have entered a newline on stdin which
// gets echoed to the terminal. Do something better here?
expectLater(pub.nextLine(), equals(
'Looks great! Are you ready to upload your package (y/n)?'
' Package test_pkg 1.0.0 uploaded!'));
pub.shouldExit(0);
run();
@ -77,6 +84,8 @@ main() {
credentialsFile(server, 'access token').scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
server.handle('GET', '/packages/versions/new.json', (request, response) {
response.statusCode = 401;
response.headers.set('www-authenticate', 'Bearer error="invalid_token",'
@ -89,10 +98,13 @@ main() {
expectLater(pub.nextErrLine(), equals('OAuth2 authorization failed (your '
'token sucks).'));
expectLater(pub.nextLine(), equals('Pub needs your authorization to upload '
'packages on your behalf.'));
// TODO(rnystrom): The confirm line is run together with this one because
// in normal usage, the user will have entered a newline on stdin which
// gets echoed to the terminal. Do something better here?
expectLater(pub.nextLine(), equals(
'Looks great! Are you ready to upload your package (y/n)? '
'Pub needs your authorization to upload packages on your behalf.'));
pub.kill();
run();
});
@ -102,12 +114,12 @@ main() {
dir(appPath, [pubspec(package)]).scheduleCreate();
var server = new ScheduledServer();
credentialsFile(server, 'access token').scheduleCreate();
var pub = startPubLish(server);
server.ignore('GET', '/packages/versions/new.json');
pub.shouldExit(1);
expectLater(pub.remainingStderr(), contains("Package validation failed."));
expectLater(pub.remainingStderr(),
contains("Sorry, your package is missing a requirement and can't be "
"published yet."));
run();
});
@ -118,9 +130,7 @@ main() {
dir(appPath, [pubspec(package)]).scheduleCreate();
var server = new ScheduledServer();
credentialsFile(server, 'access token').scheduleCreate();
var pub = startPubLish(server);
server.ignore('GET', '/packages/versions/new.json');
pub.writeLine("n");
pub.shouldExit(1);
@ -160,6 +170,8 @@ main() {
credentialsFile(server, 'access token').scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
server.handle('GET', '/packages/versions/new.json', (request, response) {
response.statusCode = 400;
response.outputStream.writeString(JSON.stringify({
@ -179,6 +191,8 @@ main() {
credentialsFile(server, 'access token').scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
server.handle('GET', '/packages/versions/new.json', (request, response) {
response.outputStream.writeString('{not json');
response.outputStream.close();
@ -196,6 +210,8 @@ main() {
credentialsFile(server, 'access token').scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
var body = {
'fields': {
'field1': 'value1',
@ -216,6 +232,8 @@ main() {
credentialsFile(server, 'access token').scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
var body = {
'url': 12,
'fields': {
@ -237,6 +255,8 @@ main() {
credentialsFile(server, 'access token').scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
var body = {'url': 'http://example.com/upload'};
handleUploadForm(server, body);
expectLater(pub.nextErrLine(), equals('Invalid server response:'));
@ -251,6 +271,8 @@ main() {
credentialsFile(server, 'access token').scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
var body = {'url': 'http://example.com/upload', 'fields': 12};
handleUploadForm(server, body);
expectLater(pub.nextErrLine(), equals('Invalid server response:'));
@ -265,6 +287,8 @@ main() {
credentialsFile(server, 'access token').scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
var body = {
'url': 'http://example.com/upload',
'fields': {'field': 12}
@ -281,6 +305,8 @@ main() {
var server = new ScheduledServer();
credentialsFile(server, 'access token').scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
handleUploadForm(server);
server.handle('POST', '/upload', (request, response) {
@ -303,6 +329,8 @@ main() {
var server = new ScheduledServer();
credentialsFile(server, 'access token').scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
handleUploadForm(server);
server.handle('POST', '/upload', (request, response) {
@ -320,6 +348,8 @@ main() {
var server = new ScheduledServer();
credentialsFile(server, 'access token').scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
handleUploadForm(server);
handleUpload(server);
@ -341,6 +371,8 @@ main() {
var server = new ScheduledServer();
credentialsFile(server, 'access token').scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
handleUploadForm(server);
handleUpload(server);
@ -360,6 +392,8 @@ main() {
var server = new ScheduledServer();
credentialsFile(server, 'access token').scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
handleUploadForm(server);
handleUpload(server);
@ -381,6 +415,8 @@ main() {
var server = new ScheduledServer();
credentialsFile(server, 'access token').scheduleCreate();
var pub = startPubLish(server);
confirmPublish(pub);
handleUploadForm(server);
handleUpload(server);

View file

@ -629,6 +629,23 @@ ScheduledProcess startPubLish(ScheduledServer server, {List<String> args}) {
return new ScheduledProcess("pub lish", process);
}
/// Handles the beginning confirmation process for uploading a packages.
/// Ensures that the right output is shown and then enters "y" to confirm the
/// upload.
void confirmPublish(ScheduledProcess pub) {
// TODO(rnystrom): This is overly specific and inflexible regarding different
// test packages. Should validate this a little more loosely.
expectLater(pub.nextLine(), equals('Publishing "test_pkg" 1.0.0:'));
expectLater(pub.nextLine(), equals("|-- LICENSE"));
expectLater(pub.nextLine(), equals("|-- lib"));
expectLater(pub.nextLine(), equals("| '-- test_pkg.dart"));
expectLater(pub.nextLine(), equals("'-- pubspec.yaml"));
expectLater(pub.nextLine(), equals(""));
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].