diff --git a/lib/src/barback/build_environment.dart b/lib/src/barback/build_environment.dart index af2eae8c906043bad774c0258d8914ed871e643a..2870349dbe90d3c77519faef7b559f9395f26d71 100644 --- a/lib/src/barback/build_environment.dart +++ b/lib/src/barback/build_environment.dart @@ -33,10 +33,16 @@ class BuildEnvironment { /// Creates a new build environment for working with the assets used by /// [entrypoint] and its dependencies. /// - /// Spawns an HTTP server on [hostname] and [port]. Loads all used - /// transformers using [mode] (including dart2js if [useDart2JS] is true). + /// Spawns an HTTP server for each directory in [rootDirectories]. These + /// servers will be on [hostname] and have ports based on [basePort]. + /// [basePort] itself is reserved for "web/" and `basePort + 1` is reserved + /// for "test/"; further ports will be allocated for other root directories as + /// necessary. If [basePort] is zero, each server will have an ephemeral port. /// - /// Includes [buildDirectories] in the root package, as well as "lib" and + /// Loads all used transformers using [mode] (including dart2js if + /// [useDart2JS] is true). + /// + /// Includes [rootDirectories] in the root package, as well as "lib" and /// "asset". /// /// If [watcherType] is not [WatcherType.NONE], watches source assets for @@ -45,17 +51,17 @@ class BuildEnvironment { /// Returns a [Future] that completes to the environment once the inputs, /// transformers, and server are loaded and ready. static Future<BuildEnvironment> create(Entrypoint entrypoint, - String hostname, int port, BarbackMode mode, WatcherType watcherType, - Set<String> buildDirectories, + String hostname, int basePort, BarbackMode mode, WatcherType watcherType, + Iterable<String> rootDirectories, {bool useDart2JS: true}) { return entrypoint.loadPackageGraph().then((graph) { var barback = new Barback(new PubPackageProvider(graph)); barback.log.listen(_log); - return BarbackServer.bind(hostname, port, barback, - graph.entrypoint.root.name).then((server) { - var environment = new BuildEnvironment._(graph, server, mode, - watcherType, buildDirectories); + return _startServers(hostname, basePort, mode, graph, barback, + rootDirectories).then((servers) { + var environment = new BuildEnvironment._(graph, servers, mode, + watcherType, rootDirectories); // If the entrypoint package manually configures the dart2js // transformer, don't include it in the built-in transformer list. @@ -78,11 +84,43 @@ class BuildEnvironment { }); } - /// The server serving this environment's assets. - final BarbackServer server; + /// Start the [BarbackServer]s that will serve [rootDirectories]. + static Future<List<BarbackServer>> _startServers(String hostname, + int basePort, BarbackMode mode, PackageGraph graph, Barback barback, + Iterable<String> rootDirectories) { + _bind(port, rootDirectory) { + if (basePort == 0) port = 0; + return BarbackServer.bind(hostname, port, barback, + graph.entrypoint.root.name, rootDirectory); + } + + rootDirectories = rootDirectories.toList(); + + // For consistency, "web/" should always have the first available port and + // "test/" should always have the second. Other directories are assigned + // the following ports in alphabetical order. + var serverFutures = []; + if (rootDirectories.remove('web')) { + serverFutures.add(_bind(basePort, 'web')); + } + if (rootDirectories.remove('test')) { + serverFutures.add(_bind(basePort + 1, 'test')); + } + + var i = 0; + for (var dir in rootDirectories) { + serverFutures.add(_bind(basePort + 2 + i, dir)); + i += 1; + } + + return Future.wait(serverFutures); + } + + /// The servers serving this environment's assets. + final List<BarbackServer> servers; /// The [Barback] instance used to process assets in this environment. - Barback get barback => server.barback; + Barback get barback => servers.first.barback; /// The root package being built. Package get rootPackage => graph.entrypoint.root; @@ -100,12 +138,13 @@ class BuildEnvironment { /// How source files should be watched. final WatcherType _watcherType; - /// The set of top-level directories in the entrypoint package that should be - /// built. - final Set<String> _buildDirectories; + /// The set of top-level directories in the entrypoint package that will be + /// exposed. + final Set<String> _rootDirectories; - BuildEnvironment._(this.graph, this.server, this.mode, this._watcherType, - this._buildDirectories); + BuildEnvironment._(this.graph, this.servers, this.mode, this._watcherType, + Iterable<String> rootDirectories) + : _rootDirectories = rootDirectories.toSet(); /// Gets the built-in [Transformer]s that should be added to [package]. /// @@ -121,7 +160,7 @@ class BuildEnvironment { return _builtInTransformers; } - /// Creates a [BarbackServer] for this environment. + /// Loads the assets and transformers for this environment. /// /// This transforms and serves all library and asset files in all packages in /// the environment's package graph. It loads any transformer plugins defined @@ -134,21 +173,24 @@ class BuildEnvironment { return _provideSources(barback).then((_) { var completer = new Completer(); - // If any errors get emitted either by barback or by the server, + // If any errors get emitted either by barback or by the primary server, // including non-programmatic barback errors, they should take down the // whole program. var subscriptions = [ - server.barback.errors.listen((error) { + barback.errors.listen((error) { if (error is TransformerException) error = error.error; if (!completer.isCompleted) { completer.completeError(error, new Chain.current()); } }), - server.barback.results.listen((_) {}, onError: (error, stackTrace) { + barback.results.listen((_) {}, + onError: (error, stackTrace) { if (completer.isCompleted) return; completer.completeError(error, stackTrace); }), - server.results.listen((_) {}, onError: (error, stackTrace) { + // We only listen to the first server here because that's the one used + // to initialize all the transformers during the initial load. + servers.first.results.listen((_) {}, onError: (error, stackTrace) { if (completer.isCompleted) return; completer.completeError(error, stackTrace); }) @@ -296,7 +338,7 @@ class BuildEnvironment { var directories = ["asset", "lib"]; if (package.name == entrypoint.root.name) { - directories.addAll(_buildDirectories); + directories.addAll(_rootDirectories); } return directories; diff --git a/lib/src/barback/load_all_transformers.dart b/lib/src/barback/load_all_transformers.dart index e1ac463460e8689ae5f039280e9e2436915ac244..7ff0dc56c72f5907502b188454e9a4001780ac59 100644 --- a/lib/src/barback/load_all_transformers.dart +++ b/lib/src/barback/load_all_transformers.dart @@ -18,8 +18,9 @@ import '../utils.dart'; /// Loads all transformers depended on by packages in [environment]. /// -/// This uses [environment]'s server to serve the Dart files from which -/// transformers are loaded, then adds the transformers to `server.barback`. +/// This uses [environment]'s primary server to serve the Dart files from which +/// transformers are loaded, then adds the transformers to +/// `environment.barback`. /// /// Any built-in transformers that are provided by the environment will /// automatically be added to the end of the root package's cascade. diff --git a/lib/src/barback/load_transformers.dart b/lib/src/barback/load_transformers.dart index 30aefdee9abfdfb7f65a873e5b99536546e868d5..13567edcdbb7160869936af5ed02b0104fff68fc 100644 --- a/lib/src/barback/load_transformers.dart +++ b/lib/src/barback/load_transformers.dart @@ -366,8 +366,11 @@ Future<Set> loadTransformers(BuildEnvironment environment, TransformerId id) { return id.getAssetId(environment.barback).then((assetId) { var path = assetId.path.replaceFirst('lib/', ''); // TODO(nweiz): load from a "package:" URI when issue 12474 is fixed. - var baseUrl = baseUrlForAddress(environment.server.address, - environment.server.port); + + // We could load the transformers from any server, since they all serve the + // packages' library files. We choose the first one arbitrarily. + var baseUrl = baseUrlForAddress(environment.servers.first.address, + environment.servers.first.port); var uri = '$baseUrl/packages/${id.package}/$path'; var code = 'import "$uri";\n' + _TRANSFORMER_ISOLATE.replaceAll('<<URL_BASE>>', baseUrl); diff --git a/lib/src/barback/server.dart b/lib/src/barback/server.dart index 58864ca3fb7c3cde2aada90ed028e81d16fae284..f5fe5a10155b93c36129b4fe1eb447915b04241c 100644 --- a/lib/src/barback/server.dart +++ b/lib/src/barback/server.dart @@ -25,10 +25,14 @@ class BarbackServer { /// The underlying HTTP server. final HttpServer _server; - /// The name of the root package, from whose `web` directory root assets will - /// be served. + /// The name of the root package, from whose [rootDirectory] assets will be + /// served. final String _rootPackage; + /// The directory in [_rootPackage] which will serve as the root of this + /// server. + final String rootDirectory; + /// The barback instance from which this serves assets. final Barback barback; @@ -61,12 +65,14 @@ class BarbackServer { /// This server will serve assets from [barback], and use [rootPackage] as /// the root package. static Future<BarbackServer> bind(String host, int port, - Barback barback, String rootPackage) { - return Chain.track(HttpServer.bind(host, port)) - .then((server) => new BarbackServer._(server, barback, rootPackage)); + Barback barback, String rootPackage, String rootDirectory) { + return Chain.track(HttpServer.bind(host, port)).then((server) { + return new BarbackServer._(server, barback, rootPackage, rootDirectory); + }); } - BarbackServer._(HttpServer server, this.barback, this._rootPackage) + BarbackServer._(HttpServer server, this.barback, this._rootPackage, + this.rootDirectory) : _server = server, port = server.port, address = server.address { @@ -262,13 +268,13 @@ class BarbackServer { var id = specialUrlToId(url); if (id != null) return id; - // Otherwise, it's a path in current package's web directory. + // Otherwise, it's a path in current package's [rootDirectory]. var parts = path.url.split(url.path); // Strip the leading "/" from the URL. if (parts.isNotEmpty && parts.first == "/") parts = parts.skip(1); - var relativePath = path.url.join("web", path.url.joinAll(parts)); + var relativePath = path.url.join(rootDirectory, path.url.joinAll(parts)); return new AssetId(_rootPackage, relativePath); } diff --git a/lib/src/command.dart b/lib/src/command.dart index d5eb6eabef02e3226ffb7e730a550f997fb03b39..92cafec76f5cdef166211d16c612d8950fc88bd4 100644 --- a/lib/src/command.dart +++ b/lib/src/command.dart @@ -85,7 +85,7 @@ abstract class PubCommand { buffer.writeln('Available ${isSubcommand ? "sub" : ""}commands:'); for (var name in names) { buffer.writeln(' ${padRight(name, length)} ' - '${commands[name].description}'); + '${commands[name].description.split("\n").first}'); } return buffer.toString(); diff --git a/lib/src/command/build.dart b/lib/src/command/build.dart index eba1a101e0f0972a4a3791904ec47eed019254a4..aee823a5317db35af6ead95d1b1d0bf4d811b0af 100644 --- a/lib/src/command/build.dart +++ b/lib/src/command/build.dart @@ -68,12 +68,12 @@ class BuildCommand extends PubCommand { // Show in-progress errors, but not results. Those get handled implicitly // by getAllAssets(). - environment.server.barback.errors.listen((error) { + environment.barback.errors.listen((error) { log.error(log.red("Build error:\n$error")); }); return log.progress("Building ${entrypoint.root.name}", - () => environment.server.barback.getAllAssets()).then((assets) { + () => environment.barback.getAllAssets()).then((assets) { // Find all of the JS entrypoints we built. var dart2JSEntrypoints = assets .where((asset) => asset.id.path.endsWith(".dart.js")) diff --git a/lib/src/command/serve.dart b/lib/src/command/serve.dart index a03bdd9904d967b2992395de588b146178c9a413..eb7c517ff0b0e4ff93361c4fb6d16273e7a11ee3 100644 --- a/lib/src/command/serve.dart +++ b/lib/src/command/serve.dart @@ -5,8 +5,10 @@ library pub.command.serve; import 'dart:async'; +import 'dart:math' as math; import 'package:barback/barback.dart'; +import 'package:path/path.dart' as p; import '../barback/build_environment.dart'; import '../barback/pub_package_provider.dart'; @@ -20,8 +22,12 @@ final _arrow = getSpecial('\u2192', '=>'); /// Handles the `serve` pub command. class ServeCommand extends PubCommand { - String get description => "Run a local web development server."; - String get usage => "pub serve"; + String get description => + 'Run a local web development server.\n\n' + 'By default, this serves "web/" and "test/", but an explicit list of \n' + 'directories to serve can be provided as well.'; + String get usage => "pub serve [directories...]"; + final takesArguments = true; PubPackageProvider _provider; @@ -35,7 +41,7 @@ class ServeCommand extends PubCommand { ServeCommand() { commandParser.addOption('port', defaultsTo: '8080', - help: 'The port to listen on.'); + help: 'The base port to listen on.'); // A hidden option for the tests to work around a bug in some of the OS X // bots where "localhost" very rarely resolves to the IPv4 loopback address @@ -66,24 +72,26 @@ class ServeCommand extends PubCommand { WatcherType.POLLING : WatcherType.AUTO; return BuildEnvironment.create(entrypoint, hostname, port, mode, - watcherType, ["web"].toSet(), + watcherType, _directoriesToServe, useDart2JS: useDart2JS).then((environment) { // In release mode, strip out .dart files since all relevant ones have // been compiled to JavaScript already. if (mode == BarbackMode.RELEASE) { - environment.server.allowAsset = (url) => !url.path.endsWith(".dart"); + for (var server in environment.servers) { + server.allowAsset = (url) => !url.path.endsWith(".dart"); + } } /// This completer is used to keep pub running (by not completing) and /// to pipe fatal errors to pub's top-level error-handling machinery. var completer = new Completer(); - environment.server.barback.errors.listen((error) { + environment.barback.errors.listen((error) { log.error(log.red("Build error:\n$error")); }); - environment.server.barback.results.listen((result) { + environment.barback.results.listen((result) { if (result.succeeded) { // TODO(rnystrom): Report using growl/inotify-send where available. log.message("Build completed ${log.green('successfully')}"); @@ -95,28 +103,83 @@ class ServeCommand extends PubCommand { if (!completer.isCompleted) completer.completeError(error, stackTrace); }); - environment.server.results.listen((result) { - if (result.isSuccess) { - log.message("${log.green('GET')} ${result.url.path} $_arrow " - "${result.id}"); - return; - } - - var msg = "${log.red('GET')} ${result.url.path} $_arrow"; - var error = result.error.toString(); - if (error.contains("\n")) { - log.message("$msg\n${prefixLines(error)}"); - } else { - log.message("$msg $error"); - } - }, onError: (error, [stackTrace]) { - if (!completer.isCompleted) completer.completeError(error, stackTrace); - }); - - log.message("Serving ${entrypoint.root.name} " - "on http://$hostname:${environment.server.port}"); + var directoryLength = environment.servers + .map((server) => server.rootDirectory.length) + .reduce(math.max); + for (var server in environment.servers) { + // Add two characters to account for "[" and "]". + var directoryPrefix = log.gray( + padRight("[${server.rootDirectory}]", directoryLength + 2)); + server.results.listen((result) { + if (result.isSuccess) { + log.message("$directoryPrefix ${log.green('GET')} " + "${result.url.path} $_arrow ${result.id}"); + return; + } + + var msg = "$directoryPrefix ${log.red('GET')} ${result.url.path} " + "$_arrow"; + var error = result.error.toString(); + if (error.contains("\n")) { + log.message("$msg\n${prefixLines(error)}"); + } else { + log.message("$msg $error"); + } + }, onError: (error, [stackTrace]) { + if (completer.isCompleted) return; + completer.completeError(error, stackTrace); + }); + + log.message("Serving ${entrypoint.root.name} " + "${padRight(server.rootDirectory, directoryLength)} " + "on ${log.bold('http://$hostname:${server.port}')}"); + } return completer.future; }); } + + /// Returns the set of directories that will be served from servers exposed to + /// the user. + /// + /// Throws a [UsageException] if the command-line arguments are invalid. + List<String> get _directoriesToServe { + if (commandOptions.rest.isEmpty) { + var directories = ['web', 'test'].where(dirExists).toList(); + if (directories.isNotEmpty) return directories; + usageError( + 'Your package must have "web" and/or "test" directories to serve,\n' + 'or you must pass in directories to serve explicitly.'); + } + + var directories = commandOptions.rest.map(p.normalize).toList(); + var invalid = directories.where((dir) => !isBeneath(dir, '.')); + if (invalid.isNotEmpty) { + usageError("${_directorySentence(invalid, "isn't", "aren't")} in this " + "package."); + } + + var nonExistent = directories.where((dir) => !dirExists(dir)); + if (nonExistent.isNotEmpty) { + usageError("${_directorySentence(nonExistent, "doesn't", "don't")} " + "exist."); + } + + return directories; + } + + /// Converts a list of [directoryNames] to a sentence. + /// + /// After the list of directories, [singularVerb] will be used if there is + /// only one directory and [pluralVerb] will be used if there are more than + /// one. + String _directorySentence(Iterable<String> directoryNames, + String singularVerb, String pluralVerb) { + var directories = pluralize('Directory', directoryNames.length, + plural: 'Directories'); + var names = toSentence(ordered(directoryNames).map((dir) => '"$dir"')); + var verb = pluralize(singularVerb, directoryNames.length, + plural: pluralVerb); + return "$directories $names $verb"; + } } diff --git a/lib/src/log.dart b/lib/src/log.dart index 0dcf088d339cfbb43c1ce0d2275cab8b57d9d81c..fd1166d99644e29795f0301e41fbcf2c3e532e8b 100644 --- a/lib/src/log.dart +++ b/lib/src/log.dart @@ -41,6 +41,7 @@ final _green = getSpecial('\u001b[32m'); final _magenta = getSpecial('\u001b[35m'); final _red = getSpecial('\u001b[31m'); final _yellow = getSpecial('\u001b[33m'); +final _gray = getSpecial('\u001b[1;30m'); final _none = getSpecial('\u001b[0m'); final _bold = getSpecial('\u001b[1m'); @@ -237,6 +238,12 @@ Future progress(String message, Future callback()) { /// Use this to highlight the most important piece of a long chunk of text. String bold(text) => "$_bold$text$_none"; +/// Wraps [text] in the ANSI escape codes to make it gray when on a platform +/// that supports that. +/// +/// Use this for text that's less important than the text around it. +String gray(text) => "$_gray$text$_none"; + /// Wraps [text] in the ANSI escape codes to color it cyan when on a platform /// that supports that. /// diff --git a/test/pub_test.dart b/test/pub_test.dart index cc41e47456b94e2d2e5d5f35d501674eaa398964..3fc4c1370e5273f01b7f03d25bf4174c408f91a7 100644 --- a/test/pub_test.dart +++ b/test/pub_test.dart @@ -209,6 +209,28 @@ main() { '''); }); + integration('shows non-truncated help', () { + schedulePub(args: ['help', 'serve'], + output: ''' + Run a local web development server. + + By default, this serves "web/" and "test/", but an explicit list of + directories to serve can be provided as well. + + Usage: pub serve [directories...] + -h, --help Print usage information for this command. + --port The base port to listen on. + (defaults to "8080") + + --[no-]dart2js Compile Dart to JavaScript. + (defaults to on) + + --[no-]force-poll Force the use of a polling filesystem watcher. + --mode Mode to run transformers in. + (defaults to "debug") + '''); + }); + integration('shows help for a subcommand', () { schedulePub(args: ['help', 'cache', 'list'], output: ''' diff --git a/test/serve/roots/serves_urls_from_custom_roots_test.dart b/test/serve/roots/serves_urls_from_custom_roots_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..221b53aab078330b1e24117a893ffb0630b36349 --- /dev/null +++ b/test/serve/roots/serves_urls_from_custom_roots_test.dart @@ -0,0 +1,32 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS d.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_tests; + +import 'package:path/path.dart' as p; +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; +import '../utils.dart'; + +main() { + initConfig(); + integration("serves URLs from custom roots", () { + d.dir(appPath, [ + d.appPubspec(), + d.dir("example", [ + d.dir("foo", [d.file("bar", "contents")]) + ]), + d.dir("dir", [d.file("baz", "contents")]), + d.dir("web", [d.file("bang", "contents")]) + ]).create(); + + pubServe(args: [p.join("example", "foo"), "dir"]); + requestShouldSucceed("bar", "contents", root: p.join("example", "foo")); + requestShouldSucceed("baz", "contents", root: "dir"); + requestShould404("bang", root: "dir"); + endPubServe(); + }); +} diff --git a/test/serve/roots/serves_web_and_test_dirs_by_default_test.dart b/test/serve/roots/serves_web_and_test_dirs_by_default_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..c53e8ba621bd47e8736aff49b0b91119ad063a36 --- /dev/null +++ b/test/serve/roots/serves_web_and_test_dirs_by_default_test.dart @@ -0,0 +1,27 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS d.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_tests; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; +import '../utils.dart'; + +main() { + initConfig(); + integration("serves web/ and test/ dirs by default", () { + d.dir(appPath, [ + d.appPubspec(), + d.dir("web", [d.file("foo", "contents")]), + d.dir("test", [d.file("bar", "contents")]), + d.dir("example", [d.file("baz", "contents")]) + ]).create(); + + pubServe(); + requestShouldSucceed("foo", "contents", root: "web"); + requestShouldSucceed("bar", "contents", root: "test"); + requestShould404("baz", root: "web"); + endPubServe(); + }); +} diff --git a/test/serve/roots/throws_an_error_by_default_if_web_and_test_dont_exist_test.dart b/test/serve/roots/throws_an_error_by_default_if_web_and_test_dont_exist_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..11de9cdc3a87f70d42bc8c6cf32f7938eef50df1 --- /dev/null +++ b/test/serve/roots/throws_an_error_by_default_if_web_and_test_dont_exist_test.dart @@ -0,0 +1,29 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS d.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_tests; + +import 'package:scheduled_test/scheduled_stream.dart'; +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../../lib/src/exit_codes.dart' as exit_codes; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; +import '../utils.dart'; + +main() { + initConfig(); + integration("throws an error by default if web and test don't exist", () { + d.dir(appPath, [ + d.appPubspec() + ]).create(); + + var server = startPubServe(createWebDir: false); + server.stderr.expect(emitsLines( + 'Your package must have "web" and/or "test" directories to serve,\n' + 'or you must pass in directories to serve explicitly.')); + server.shouldExit(exit_codes.USAGE); + }); +} diff --git a/test/serve/roots/throws_an_error_if_custom_roots_are_outside_package_test.dart b/test/serve/roots/throws_an_error_if_custom_roots_are_outside_package_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..37dd2696a530e17d719a37121d80378d97ce33a3 --- /dev/null +++ b/test/serve/roots/throws_an_error_if_custom_roots_are_outside_package_test.dart @@ -0,0 +1,26 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS d.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_tests; + +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../../lib/src/exit_codes.dart' as exit_codes; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; +import '../utils.dart'; + +main() { + initConfig(); + integration("throws an error if custom roots are outside the package", () { + d.dir(appPath, [ + d.appPubspec() + ]).create(); + + var server = startPubServe(args: [".."]); + server.stderr.expect('Directory ".." isn\'t in this package.'); + server.shouldExit(exit_codes.USAGE); + }); +} diff --git a/test/serve/roots/throws_an_error_if_custom_roots_dont_exist_test.dart b/test/serve/roots/throws_an_error_if_custom_roots_dont_exist_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..c2275bbc16d16fe6c8788301726fb701447806e9 --- /dev/null +++ b/test/serve/roots/throws_an_error_if_custom_roots_dont_exist_test.dart @@ -0,0 +1,27 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS d.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_tests; + +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../../lib/src/exit_codes.dart' as exit_codes; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; +import '../utils.dart'; + +main() { + initConfig(); + integration("throws an error if custom roots don't exist", () { + d.dir(appPath, [ + d.appPubspec(), + d.dir("baz") + ]).create(); + + var server = startPubServe(args: ["foo", "bar", "baz"]); + server.stderr.expect('Directories "bar" and "foo" don\'t exist.'); + server.shouldExit(exit_codes.USAGE); + }); +} diff --git a/test/serve/utils.dart b/test/serve/utils.dart index 441e139eec65c929112c4ab1f72496dd7dd17d25..0f04f3fc86bd34bdc422ec0149f030161589d1e8 100644 --- a/test/serve/utils.dart +++ b/test/serve/utils.dart @@ -14,13 +14,15 @@ import 'package:scheduled_test/scheduled_process.dart'; import 'package:scheduled_test/scheduled_stream.dart'; import 'package:scheduled_test/scheduled_test.dart'; +import '../descriptor.dart' as d; import '../test_pub.dart'; /// The pub process running "pub serve". ScheduledProcess _pubServer; -/// The ephemeral port assigned to the running server. -int _port; +/// The ephemeral ports assigned to the running servers, associated with the +/// directories they're serving. +final _ports = new Map<String, int>(); /// The web socket connection to the running pub process, or `null` if no /// connection has been made. @@ -100,7 +102,8 @@ class DartTransformer extends Transformer { /// so may be used to test for errors in the initialization process. /// /// Returns the `pub serve` process. -ScheduledProcess startPubServe([Iterable<String> args]) { +ScheduledProcess startPubServe({Iterable<String> args, + bool createWebDir: true}) { // Use port 0 to get an ephemeral port. var pubArgs = ["serve", "--port=0", "--hostname=127.0.0.1", "--force-poll"]; @@ -110,6 +113,7 @@ ScheduledProcess startPubServe([Iterable<String> args]) { // timeout to cope with that. currentSchedule.timeout *= 1.5; + if (createWebDir) d.dir(appPath, [d.dir("web")]).create(); return startPub(args: pubArgs); } @@ -118,11 +122,17 @@ ScheduledProcess startPubServe([Iterable<String> args]) { /// /// If [shouldGetFirst] is `true`, validates that pub get is run first. /// +/// If [createWebDir] is `true`, creates a `web/` directory if one doesn't exist +/// so pub doesn't complain about having nothing to serve. +/// /// Returns the `pub serve` process. -ScheduledProcess pubServe({bool shouldGetFirst: false, Iterable<String> args}) { - _pubServer = startPubServe(args); +ScheduledProcess pubServe({bool shouldGetFirst: false, bool createWebDir: true, + Iterable<String> args}) { + _pubServer = startPubServe(args: args, createWebDir: createWebDir); currentSchedule.onComplete.schedule(() { + _ports.clear(); + if (_webSocket != null) { _webSocket.close(); _webSocket = null; @@ -134,16 +144,25 @@ ScheduledProcess pubServe({bool shouldGetFirst: false, Iterable<String> args}) { _pubServer.stdout.expect(consumeThrough("Got dependencies!")); } - expect(schedule(() => _pubServer.stdout.next()).then(_parsePort), completes); + // The server should emit one or more ports. + _pubServer.stdout.expect( + consumeWhile(predicate(_parsePort, 'emits server url'))); + schedule(() => expect(_ports, isNot(isEmpty))); + return _pubServer; } +/// The regular expression for parsing pub's output line describing the URL for +/// the server. +final _parsePortRegExp = new RegExp(r"([^ ]+) +on http://127\.0\.0\.1:(\d+)"); + /// Parses the port number from the "Serving blah on 127.0.0.1:1234" line /// printed by pub serve. -void _parsePort(String line) { - var match = new RegExp(r"127\.0\.0\.1:(\d+)").firstMatch(line); - assert(match != null); - _port = int.parse(match[1]); +bool _parsePort(String line) { + var match = _parsePortRegExp.firstMatch(line); + if (match == null) return false; + _ports[match[1]] = int.parse(match[2]); + return true; } void endPubServe() { @@ -154,10 +173,11 @@ void endPubServe() { /// verifies that it responds with a body that matches [expectation]. /// /// [expectation] may either be a [Matcher] or a string to match an exact body. +/// [root] indicates which server should be accessed, and defaults to "web". /// [headers] may be either a [Matcher] or a map to match an exact headers map. -void requestShouldSucceed(String urlPath, expectation, {headers}) { +void requestShouldSucceed(String urlPath, expectation, {String root, headers}) { schedule(() { - return http.get("http://127.0.0.1:$_port/$urlPath").then((response) { + return http.get("${_serverUrl(root)}/$urlPath").then((response) { if (expectation != null) expect(response.body, expectation); if (headers != null) expect(response.headers, headers); }); @@ -166,9 +186,11 @@ void requestShouldSucceed(String urlPath, expectation, {headers}) { /// Schedules an HTTP request to the running pub server with [urlPath] and /// verifies that it responds with a 404. -void requestShould404(String urlPath) { +/// +/// [root] indicates which server should be accessed, and defaults to "web". +void requestShould404(String urlPath, {String root}) { schedule(() { - return http.get("http://127.0.0.1:$_port/$urlPath").then((response) { + return http.get("${_serverUrl(root)}/$urlPath").then((response) { expect(response.statusCode, equals(404)); }); }, "request $urlPath"); @@ -178,11 +200,12 @@ void requestShould404(String urlPath) { /// verifies that it responds with a redirect to the given [redirectTarget]. /// /// [redirectTarget] may be either a [Matcher] or a string to match an exact -/// URL. -void requestShouldRedirect(String urlPath, redirectTarget) { +/// URL. [root] indicates which server should be accessed, and defaults to +/// "web". +void requestShouldRedirect(String urlPath, redirectTarget, {String root}) { schedule(() { var request = new http.Request("GET", - Uri.parse("http://127.0.0.1:$_port/$urlPath")); + Uri.parse("${_serverUrl(root)}/$urlPath")); request.followRedirects = false; return request.send().then((response) { expect(response.statusCode ~/ 100, equals(3)); @@ -194,9 +217,11 @@ void requestShouldRedirect(String urlPath, redirectTarget) { /// Schedules an HTTP POST to the running pub server with [urlPath] and verifies /// that it responds with a 405. -void postShould405(String urlPath) { +/// +/// [root] indicates which server should be accessed, and defaults to "web". +void postShould405(String urlPath, {String root}) { schedule(() { - return http.post("http://127.0.0.1:$_port/$urlPath").then((response) { + return http.post("${_serverUrl(root)}/$urlPath").then((response) { expect(response.statusCode, equals(405)); }); }, "request $urlPath"); @@ -217,10 +242,13 @@ Future _ensureWebSocket() { if (_webSocket != null) return new Future.value(); // Server should already be running. - assert(_pubServer != null); - assert(_port != null); + expect(_pubServer, isNotNull); + expect(_ports, isNot(isEmpty)); - return WebSocket.connect("ws://127.0.0.1:$_port").then((socket) { + // TODO(nweiz): once we have a separate port for a web interface into the + // server, use that port for the websocket interface. + var port = _ports.values.first; + return WebSocket.connect("ws://127.0.0.1:$port").then((socket) { _webSocket = socket; // TODO(rnystrom): Works around #13913. _webSocketBroadcastStream = _webSocket.asBroadcastStream(); @@ -241,4 +269,11 @@ void webSocketShouldReply(request, expectation, {bool encodeRequest: true}) { expect(JSON.decode(value), expectation); }); }), "send $request to web socket and expect reply that $expectation"); +} + +/// Returns the URL for the server serving from [root]. +String _serverUrl([String root]) { + if (root == null) root = 'web'; + expect(_ports, contains(root)); + return "http://127.0.0.1:${_ports[root]}"; } \ No newline at end of file