mirror of
https://github.com/dart-lang/sdk
synced 2024-11-02 08:07:11 +00:00
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:
parent
7050101ea8
commit
0f9bed70a5
15 changed files with 449 additions and 67 deletions
|
@ -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.";
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
133
utils/pub/directory_tree.dart
Normal file
133
utils/pub/directory_tree.dart
Normal 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]);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
///
|
||||
|
|
|
@ -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("");
|
||||
}
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)}"?');
|
||||
|
|
|
@ -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);
|
||||
|
|
114
utils/tests/pub/directory_tree_test.dart
Normal file
114
utils/tests/pub/directory_tree_test.dart
Normal 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
|
||||
"""));
|
||||
});
|
||||
}
|
|
@ -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%+-]+)[$&]')
|
||||
|
|
|
@ -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']));
|
||||
|
|
|
@ -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']));
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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].
|
||||
|
|
Loading…
Reference in a new issue