From 73f82a4bac5fd3cd50451c562e512f00d32e2add Mon Sep 17 00:00:00 2001 From: "rnystrom@google.com" <rnystrom@google.com> Date: Thu, 18 Sep 2014 22:23:06 +0000 Subject: [PATCH] Create binstubs for executables when activating a package. BUG=https://code.google.com/p/dart/issues/detail?id=18539 R=nweiz@google.com Review URL: https://codereview.chromium.org//566093003 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge@40471 260f80e4-7a28-3924-810f-c04153c831b5 --- lib/src/barback/asset_environment.dart | 5 +- lib/src/command/global_activate.dart | 33 ++- lib/src/command/global_deactivate.dart | 2 +- lib/src/global_packages.dart | 262 ++++++++++++++++-- lib/src/package.dart | 16 +- lib/src/pubspec.dart | 55 ++++ lib/src/utils.dart | 12 + lib/src/validator.dart | 2 + lib/src/validator/executable.dart | 37 +++ lib/src/validator/name.dart | 3 +- test/global/activate/bad_version_test.dart | 14 +- .../activate/constraint_with_path_test.dart | 13 +- .../activate/missing_package_arg_test.dart | 12 +- .../activate/unexpected_arguments_test.dart | 12 +- .../creates_executables_in_pubspec_test.dart | 40 +++ ...licit_and_no_executables_options_test.dart | 24 ++ .../binstubs/explicit_executables_test.dart | 40 +++ test/global/binstubs/missing_script_test.dart | 32 +++ test/global/binstubs/name_collision_test.dart | 61 ++++ .../name_collision_with_overwrite_test.dart | 62 +++++ .../binstubs/no_executables_flag_test.dart | 38 +++ test/global/binstubs/path_package_test.dart | 34 +++ ...activate_removes_old_executables_test.dart | 48 ++++ .../removes_even_if_not_in_pubspec_test.dart | 45 +++ .../removes_when_deactivated_test.dart | 37 +++ .../unknown_explicit_executable_test.dart | 35 +++ test/pubspec_test.dart | 53 ++++ test/validator/executable_test.dart | 57 ++++ 28 files changed, 1013 insertions(+), 71 deletions(-) create mode 100644 lib/src/validator/executable.dart create mode 100644 test/global/binstubs/creates_executables_in_pubspec_test.dart create mode 100644 test/global/binstubs/explicit_and_no_executables_options_test.dart create mode 100644 test/global/binstubs/explicit_executables_test.dart create mode 100644 test/global/binstubs/missing_script_test.dart create mode 100644 test/global/binstubs/name_collision_test.dart create mode 100644 test/global/binstubs/name_collision_with_overwrite_test.dart create mode 100644 test/global/binstubs/no_executables_flag_test.dart create mode 100644 test/global/binstubs/path_package_test.dart create mode 100644 test/global/binstubs/reactivate_removes_old_executables_test.dart create mode 100644 test/global/binstubs/removes_even_if_not_in_pubspec_test.dart create mode 100644 test/global/binstubs/removes_when_deactivated_test.dart create mode 100644 test/global/binstubs/unknown_explicit_executable_test.dart create mode 100644 test/validator/executable_test.dart diff --git a/lib/src/barback/asset_environment.dart b/lib/src/barback/asset_environment.dart index 613d94e8..7b0fca29 100644 --- a/lib/src/barback/asset_environment.dart +++ b/lib/src/barback/asset_environment.dart @@ -583,15 +583,12 @@ class AssetEnvironment { /// this is optimized for our needs in here instead of using the more general /// but slower [listDir]. Iterable<AssetId> _listDirectorySources(Package package, String dir) { - var subdirectory = path.join(package.dir, dir); - if (!dirExists(subdirectory)) return []; - // This is used in some performance-sensitive paths and can list many, many // files. As such, it leans more havily towards optimization as opposed to // readability than most code in pub. In particular, it avoids using the // path package, since re-parsing a path is very expensive relative to // string operations. - return package.listFiles(beneath: subdirectory).map((file) { + return package.listFiles(beneath: dir).map((file) { // From profiling, path.relative here is just as fast as a raw substring // and is correct in the case where package.dir has a trailing slash. var relative = path.relative(file, from: package.dir); diff --git a/lib/src/command/global_activate.dart b/lib/src/command/global_activate.dart index 8448af58..e78641b4 100644 --- a/lib/src/command/global_activate.dart +++ b/lib/src/command/global_activate.dart @@ -22,9 +22,33 @@ class GlobalActivateCommand extends PubCommand { help: "The source used to find the package.", allowed: ["git", "hosted", "path"], defaultsTo: "hosted"); + + commandParser.addFlag("no-executables", negatable: false, + help: "Do not put executables on PATH."); + + commandParser.addOption("executable", abbr: "x", + help: "Executable(s) to place on PATH.", + allowMultiple: true); + + commandParser.addFlag("overwrite", negatable: false, + help: "Overwrite executables from other packages with the same name."); } Future onRun() { + // Default to `null`, which means all executables. + var executables; + if (commandOptions.wasParsed("executable")) { + if (commandOptions.wasParsed("no-executables")) { + usageError("Cannot pass both --no-executables and --executable."); + } + + executables = commandOptions["executable"]; + } else if (commandOptions["no-executables"]) { + // An empty list means no executables. + executables = []; + } + + var overwrite = commandOptions["overwrite"]; var args = commandOptions.rest; readArg([String error]) { @@ -46,7 +70,8 @@ class GlobalActivateCommand extends PubCommand { var repo = readArg("No Git repository given."); // TODO(rnystrom): Allow passing in a Git ref too. validateNoExtraArgs(); - return globals.activateGit(repo); + return globals.activateGit(repo, executables, + overwriteBinStubs: overwrite); case "hosted": var package = readArg("No package to activate given."); @@ -62,12 +87,14 @@ class GlobalActivateCommand extends PubCommand { } validateNoExtraArgs(); - return globals.activateHosted(package, constraint); + return globals.activateHosted(package, constraint, executables, + overwriteBinStubs: overwrite); case "path": var path = readArg("No package to activate given."); validateNoExtraArgs(); - return globals.activatePath(path); + return globals.activatePath(path, executables, + overwriteBinStubs: overwrite); } throw "unreachable"; diff --git a/lib/src/command/global_deactivate.dart b/lib/src/command/global_deactivate.dart index 21483479..4e966552 100644 --- a/lib/src/command/global_deactivate.dart +++ b/lib/src/command/global_deactivate.dart @@ -29,7 +29,7 @@ class GlobalDeactivateCommand extends PubCommand { usageError("Unexpected $arguments ${toSentence(unexpected)}."); } - if (!globals.deactivate(commandOptions.rest.first, logDeactivate: true)) { + if (!globals.deactivate(commandOptions.rest.first)) { dataError("No active package ${log.bold(commandOptions.rest.first)}."); } diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart index f5a7d658..2b9d5edb 100644 --- a/lib/src/global_packages.dart +++ b/lib/src/global_packages.dart @@ -18,14 +18,19 @@ import 'lock_file.dart'; import 'log.dart' as log; import 'package.dart'; import 'pubspec.dart'; -import 'system_cache.dart'; +import 'sdk.dart' as sdk; import 'solver/version_solver.dart'; import 'source/cached.dart'; import 'source/git.dart'; import 'source/path.dart'; +import 'system_cache.dart'; import 'utils.dart'; import 'version.dart'; +/// Matches the package name that a binstub was created for inside the contents +/// of the shell script. +final _binStubPackagePattern = new RegExp(r"Package: ([a-zA-Z0-9_-]+)"); + /// Maintains the set of packages that have been globally activated. /// /// These have been hand-chosen by the user to make their executables in bin/ @@ -57,6 +62,9 @@ class GlobalPackages { /// The directory where the lockfiles for activated packages are stored. String get _directory => p.join(cache.rootDir, "global_packages"); + /// The directory where binstubs for global package executables are stored. + String get _binStubDir => p.join(cache.rootDir, "bin"); + /// Creates a new global package registry backed by the given directory on /// the user's file system. /// @@ -66,7 +74,16 @@ class GlobalPackages { /// Caches the package located in the Git repository [repo] and makes it the /// active global version. - Future activateGit(String repo) async { + /// + /// [executables] is the names of the executables that should have binstubs. + /// If `null`, all executables in the package will get binstubs. If empty, no + /// binstubs will be created. + /// + /// if [overwriteBinStubs] is `true`, any binstubs that collide with + /// existing binstubs in other packages will be overwritten by this one's. + /// Otherwise, the previous ones will be preserved. + Future activateGit(String repo, List<String> executables, + {bool overwriteBinStubs}) async { var source = cache.sources["git"] as GitSource; var name = await source.getPackageNameFromRepo(repo); // Call this just to log what the current active package is, if any. @@ -76,19 +93,42 @@ class GlobalPackages { // dependencies. Their executables shouldn't be cached, and there should // be a mechanism for redoing dependency resolution if a path pubspec has // changed (see also issue 20499). - await _installInCache( + var package = await _installInCache( new PackageDep(name, "git", VersionConstraint.any, repo)); + _updateBinStubs(package, executables, + overwriteBinStubs: overwriteBinStubs); } /// Finds the latest version of the hosted package with [name] that matches /// [constraint] and makes it the active global version. - Future activateHosted(String name, VersionConstraint constraint) { + /// + /// [executables] is the names of the executables that should have binstubs. + /// If `null`, all executables in the package will get binstubs. If empty, no + /// binstubs will be created. + /// + /// if [overwriteBinStubs] is `true`, any binstubs that collide with + /// existing binstubs in other packages will be overwritten by this one's. + /// Otherwise, the previous ones will be preserved. + Future activateHosted(String name, VersionConstraint constraint, + List<String> executables, {bool overwriteBinStubs}) async { _describeActive(name); - return _installInCache(new PackageDep(name, "hosted", constraint, name)); + var package = await _installInCache( + new PackageDep(name, "hosted", constraint, name)); + _updateBinStubs(package, executables, + overwriteBinStubs: overwriteBinStubs); } /// Makes the local package at [path] globally active. - Future activatePath(String path) async { + /// + /// [executables] is the names of the executables that should have binstubs. + /// If `null`, all executables in the package will get binstubs. If empty, no + /// binstubs will be created. + /// + /// if [overwriteBinStubs] is `true`, any binstubs that collide with + /// existing binstubs in other packages will be overwritten by this one's. + /// Otherwise, the previous ones will be preserved. + Future activatePath(String path, List<String> executables, + {bool overwriteBinStubs}) async { var entrypoint = new Entrypoint(path, cache); // Get the package's dependencies. @@ -109,10 +149,15 @@ class GlobalPackages { var binDir = p.join(_directory, name, 'bin'); if (dirExists(binDir)) deleteEntry(binDir); + + _updateBinStubs(entrypoint.root, executables, + overwriteBinStubs: overwriteBinStubs); } /// Installs the package [dep] and its dependencies into the system cache. - Future _installInCache(PackageDep dep) async { + /// + /// Returns the cached root [Package]. + Future<Package> _installInCache(PackageDep dep) async { var source = cache.sources[dep.source]; // Create a dummy package with just [dep] so we can do resolution on it. @@ -140,6 +185,8 @@ class GlobalPackages { .loadPackageGraph(result); await _precompileExecutables(graph.entrypoint, dep.name); _writeLockFile(dep.name, lockFile); + + return graph.packages[dep.name]; } /// Precompiles the executables for [package] and saves them in the global @@ -188,7 +235,6 @@ class GlobalPackages { var id = lockFile.packages[package]; log.message('Activated ${_formatPackage(id)}.'); - } /// Shows the user the currently active package with [name], if any. @@ -217,19 +263,16 @@ class GlobalPackages { /// Deactivates a previously-activated package named [name]. /// - /// If [logDeactivate] is true, displays to the user when a package is - /// deactivated. Otherwise, deactivates silently. - /// /// Returns `false` if no package with [name] was currently active. - bool deactivate(String name, {bool logDeactivate: false}) { + bool deactivate(String name) { var dir = p.join(_directory, name); if (!dirExists(dir)) return false; - if (logDeactivate) { - var lockFile = new LockFile.load(_getLockFilePath(name), cache.sources); - var id = lockFile.packages[name]; - log.message('Deactivated package ${_formatPackage(id)}.'); - } + _deleteBinStubs(name); + + var lockFile = new LockFile.load(_getLockFilePath(name), cache.sources); + var id = lockFile.packages[name]; + log.message('Deactivated package ${_formatPackage(id)}.'); deleteEntry(dir); @@ -366,4 +409,189 @@ class GlobalPackages { return '${log.bold(id.name)} ${id.version}'; } } + + /// Updates the binstubs for [package]. + /// + /// A binstub is a little shell script in `PUB_CACHE/bin` that runs an + /// executable from a globally activated package. This removes any old + /// binstubs from the previously activated version of the package and + /// (optionally) creates new ones for the executables listed in the package's + /// pubspec. + /// + /// [executables] is the names of the executables that should have binstubs. + /// If `null`, all executables in the package will get binstubs. If empty, no + /// binstubs will be created. + /// + /// if [overwriteBinStubs] is `true`, any binstubs that collide with + /// existing binstubs in other packages will be overwritten by this one's. + /// Otherwise, the previous ones will be preserved. + void _updateBinStubs(Package package, List<String> executables, + {bool overwriteBinStubs}) { + // Remove any previously activated binstubs for this package, in case the + // list of executables has changed. + _deleteBinStubs(package.name); + + if ((executables != null && executables.isEmpty) || + package.pubspec.executables.isEmpty) { + return; + } + + ensureDir(_binStubDir); + + var installed = []; + var collided = {}; + var allExecutables = ordered(package.pubspec.executables.keys); + for (var executable in allExecutables) { + if (executables != null && !executables.contains(executable)) continue; + + var script = package.pubspec.executables[executable]; + + var previousPackage = _createBinStub(package, executable, script, + overwrite: overwriteBinStubs); + if (previousPackage != null) { + collided[executable] = previousPackage; + + if (!overwriteBinStubs) continue; + } + + installed.add(executable); + } + + if (installed.isNotEmpty) { + var names = namedSequence("executable", installed.map(log.bold)); + log.message("Installed $names."); + // TODO(rnystrom): Show the user how to add the binstub directory to + // their PATH if not already on it. + } + + // Show errors for any collisions. + if (collided.isNotEmpty) { + for (var command in ordered(collided.keys)) { + if (overwriteBinStubs) { + log.warning("Replaced ${log.bold(command)} previously installed from " + "${log.bold(collided[command])}."); + } else { + log.warning("Executable ${log.bold(command)} was already installed " + "from ${log.bold(collided[command])}."); + } + } + + if (!overwriteBinStubs) { + log.warning("Deactivate the other package(s) or activate " + "${log.bold(package.name)} using --overwrite."); + } + } + + // Show errors for any unknown executables. + if (executables != null) { + var unknown = ordered(executables.where( + (exe) => !package.pubspec.executables.keys.contains(exe))); + if (unknown.isNotEmpty) { + dataError("Unknown ${namedSequence('executable', unknown)}."); + } + } + + // Show errors for any missing scripts. + // TODO(rnystrom): This can print false positives since a script may be + // produced by a transformer. Do something better. + var binFiles = package.listFiles(beneath: "bin", recursive: false) + .map((path) => p.relative(path, from: package.dir)) + .toList(); + for (var executable in installed) { + var script = package.pubspec.executables[executable]; + var scriptPath = p.join("bin", "$script.dart"); + if (!binFiles.contains(scriptPath)) { + log.warning('Warning: Executable "$executable" runs "$scriptPath", ' + 'which was not found in ${log.bold(package.name)}.'); + } + } + } + + /// Creates a binstub named [executable] that runs [script] from [package]. + /// + /// If [overwrite] is `true`, this will replace an existing binstub with that + /// name for another package. + /// + /// If a collision occurs, returns the name of the package that owns the + /// existing binstub. Otherwise returns `null`. + String _createBinStub(Package package, String executable, String script, + {bool overwrite}) { + var binStubPath = p.join(_binStubDir, executable); + + // See if the binstub already exists. If so, it's for another package + // since we already deleted all of this package's binstubs. + var previousPackage; + if (fileExists(binStubPath)) { + var contents = readTextFile(binStubPath); + var match = _binStubPackagePattern.firstMatch(contents); + if (match != null) { + previousPackage = match[1]; + if (!overwrite) return previousPackage; + } else { + log.fine("Could not parse binstub $binStubPath:\n$contents"); + } + } + + // TODO(rnystrom): If the script was precompiled to a snapshot, make the + // script just invoke that directly and skip pub global run entirely. + + if (Platform.operatingSystem == "windows") { + var batch = """ +@echo off +rem This file was created by pub v${sdk.version}. +rem Package: ${package.name} +rem Version: ${package.version} +rem Executable: ${executable} +rem Script: ${script} +pub global run ${package.name}:$script "%*" +"""; + writeTextFile(binStubPath, batch); + } else { + var bash = """ +# This file was created by pub v${sdk.version}. +# Package: ${package.name} +# Version: ${package.version} +# Executable: ${executable} +# Script: ${script} +pub global run ${package.name}:$script "\$@" +"""; + writeTextFile(binStubPath, bash); + + // Make it executable. + var result = Process.runSync('chmod', ['+x', binStubPath]); + if (result.exitCode != 0) { + // Couldn't make it executable so don't leave it laying around. + try { + deleteEntry(binStubPath); + } on IOException catch (err) { + // Do nothing. We're going to fail below anyway. + log.fine("Could not delete binstub:\n$err"); + } + + fail('Could not make "$binStubPath" executable (exit code ' + '${result.exitCode}):\n${result.stderr}'); + } + } + + return previousPackage; + } + + /// Deletes all existing binstubs for [package]. + void _deleteBinStubs(String package) { + if (!dirExists(_binStubDir)) return; + + for (var file in listDir(_binStubDir, includeDirs: false)) { + var contents = readTextFile(file); + var match = _binStubPackagePattern.firstMatch(contents); + if (match == null) { + log.fine("Could not parse binstub $file:\n$contents"); + continue; + } + + if (match[1] == package) { + log.fine("Deleting old binstub $file"); + deleteEntry(file); + } + } + } } diff --git a/lib/src/package.dart b/lib/src/package.dart index 3dd84e2d..e5fff8b6 100644 --- a/lib/src/package.dart +++ b/lib/src/package.dart @@ -79,10 +79,7 @@ class Package { /// Returns a list of asset ids for all Dart executables in this package's bin /// directory. List<AssetId> get executableIds { - var binDir = path.join(dir, 'bin'); - if (!dirExists(binDir)) return []; - - return ordered(listFiles(beneath: binDir, recursive: false)) + return ordered(listFiles(beneath: "bin", recursive: false)) .where((executable) => path.extension(executable) == '.dart') .map((executable) { return new AssetId( @@ -142,11 +139,18 @@ class Package { /// If this is a Git repository, this will respect .gitignore; otherwise, it /// will return all non-hidden, non-blacklisted files. /// - /// If [beneath] is passed, this will only return files beneath that path. If + /// If [beneath] is passed, this will only return files beneath that path, + /// which is expected to be relative to the package's root directory. If /// [recursive] is true, this will return all files beneath that path; /// otherwise, it will only return files one level beneath it. List<String> listFiles({String beneath, recursive: true}) { - if (beneath == null) beneath = dir; + if (beneath == null) { + beneath = dir; + } else { + beneath = path.join(dir, beneath); + } + + if (!dirExists(beneath)) return []; // This is used in some performance-sensitive paths and can list many, many // files. As such, it leans more havily towards optimization as opposed to diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart index 7aa57350..fad03b2a 100644 --- a/lib/src/pubspec.dart +++ b/lib/src/pubspec.dart @@ -239,6 +239,61 @@ class Pubspec { bool _parsedPublishTo = false; String _publishTo; + /// The executables that should be placed on the user's PATH when this + /// package is globally activated. + /// + /// It is a map of strings to string. Each key is the name of the command + /// that will be placed on the user's PATH. The value is the name of the + /// .dart script (without extension) in the package's `bin` directory that + /// should be run for that command. Both key and value must be "simple" + /// strings: alphanumerics, underscores and hypens only. If a value is + /// omitted, it is inferred to use the same name as the key. + Map<String, String> get executables { + if (_executables != null) return _executables; + + _executables = {}; + var yaml = fields['executables']; + if (yaml == null) return _executables; + + if (yaml is! Map) { + _error('"executables" field must be a map.', + fields.nodes['executables'].span); + } + + yaml.nodes.forEach((key, value) { + // Don't allow path separators or other stuff meaningful to the shell. + validateName(name, description) { + } + + if (key.value is! String) { + _error('"executables" keys must be strings.', key.span); + } + + final keyPattern = new RegExp(r"^[a-zA-Z0-9_-]+$"); + if (!keyPattern.hasMatch(key.value)) { + _error('"executables" keys may only contain letters, ' + 'numbers, hyphens and underscores.', key.span); + } + + if (value.value == null) { + value = key; + } else if (value.value is! String) { + _error('"executables" values must be strings or null.', value.span); + } + + final valuePattern = new RegExp(r"[/\\]"); + if (valuePattern.hasMatch(value.value)) { + _error('"executables" values may not contain path separators.', + value.span); + } + + _executables[key.value] = value.value; + }); + + return _executables; + } + Map<String, String> _executables; + /// Whether the package is private and cannot be published. /// /// This is specified in the pubspec by setting "publish_to" to "none". diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 7af41e5f..e8bbb1d5 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -167,6 +167,18 @@ String padRight(String source, int length) { return result.toString(); } +/// Returns a labelled sentence fragment starting with [name] listing the +/// elements [iter]. +/// +/// If [iter] does not have one item, name will be pluralized by adding "s" or +/// using [plural], if given. +String namedSequence(String name, Iterable iter, [String plural]) { + if (iter.length == 1) return "$name ${iter.single}"; + + if (plural == null) plural = "${name}s"; + return "$plural ${toSentence(iter)}"; +} + /// Returns a sentence fragment listing the elements of [iter]. /// /// This converts each element of [iter] to a string and separates them with diff --git a/lib/src/validator.dart b/lib/src/validator.dart index 32194fec..17e46458 100644 --- a/lib/src/validator.dart +++ b/lib/src/validator.dart @@ -13,6 +13,7 @@ import 'validator/compiled_dartdoc.dart'; import 'validator/dependency.dart'; import 'validator/dependency_override.dart'; import 'validator/directory.dart'; +import 'validator/executable.dart'; import 'validator/license.dart'; import 'validator/name.dart'; import 'validator/pubspec_field.dart'; @@ -62,6 +63,7 @@ abstract class Validator { new DependencyValidator(entrypoint), new DependencyOverrideValidator(entrypoint), new DirectoryValidator(entrypoint), + new ExecutableValidator(entrypoint), new CompiledDartdocValidator(entrypoint), new Utf8ReadmeValidator(entrypoint) ]; diff --git a/lib/src/validator/executable.dart b/lib/src/validator/executable.dart new file mode 100644 index 00000000..23bc5e53 --- /dev/null +++ b/lib/src/validator/executable.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library pub.validator.executable; + +import 'dart:async'; + +import 'package:path/path.dart' as p; + +import '../entrypoint.dart'; +import '../io.dart'; +import '../utils.dart'; +import '../validator.dart'; + +/// Validates that a package's pubspec doesn't contain executables that +/// reference non-existent scripts. +class ExecutableValidator extends Validator { + ExecutableValidator(Entrypoint entrypoint) + : super(entrypoint); + + Future validate() async { + // TODO(rnystrom): This can print false positives since a script may be + // produced by a transformer. Do something better. + var binFiles = entrypoint.root.listFiles(beneath: "bin", recursive: false) + .map((path) => p.relative(path, from: entrypoint.root.dir)) + .toList(); + + entrypoint.root.pubspec.executables.forEach((executable, script) { + var scriptPath = p.join("bin", "$script.dart"); + if (binFiles.contains(scriptPath)) return; + + warnings.add('Your pubspec.yaml lists an executable "$executable" that ' + 'points to a script "$scriptPath" that does not exist.'); + }); + } +} diff --git a/lib/src/validator/name.dart b/lib/src/validator/name.dart index 14da9120..585a3df8 100644 --- a/lib/src/validator/name.dart +++ b/lib/src/validator/name.dart @@ -52,8 +52,7 @@ class NameValidator extends Validator { /// to the package's root directory. List<String> get _libraries { var libDir = path.join(entrypoint.root.dir, "lib"); - if (!dirExists(libDir)) return []; - return entrypoint.root.listFiles(beneath: libDir) + return entrypoint.root.listFiles(beneath: "lib") .map((file) => path.relative(file, from: path.dirname(libDir))) .where((file) => !path.split(file).contains("src") && path.extension(file) == '.dart') diff --git a/test/global/activate/bad_version_test.dart b/test/global/activate/bad_version_test.dart index 73cb2ae3..9db057ad 100644 --- a/test/global/activate/bad_version_test.dart +++ b/test/global/activate/bad_version_test.dart @@ -2,6 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'package:scheduled_test/scheduled_test.dart'; + import '../../../lib/src/exit_codes.dart' as exit_codes; import '../../test_pub.dart'; @@ -9,16 +11,8 @@ main() { initConfig(); integration('fails if the version constraint cannot be parsed', () { schedulePub(args: ["global", "activate", "foo", "1.0"], - error: """ - Could not parse version "1.0". Unknown text at "1.0". - - Usage: pub global activate <package...> - -h, --help Print usage information for this command. - -s, --source The source used to find the package. - [git, hosted (default), path] - - Run "pub help" to see global options. - """, + error: contains( + 'Could not parse version "1.0". Unknown text at "1.0".'), exitCode: exit_codes.USAGE); }); } diff --git a/test/global/activate/constraint_with_path_test.dart b/test/global/activate/constraint_with_path_test.dart index 2795ca41..1e5412f4 100644 --- a/test/global/activate/constraint_with_path_test.dart +++ b/test/global/activate/constraint_with_path_test.dart @@ -2,6 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'package:scheduled_test/scheduled_test.dart'; + import '../../../lib/src/exit_codes.dart' as exit_codes; import '../../test_pub.dart'; @@ -9,16 +11,7 @@ main() { initConfig(); integration('fails if a version is passed with the path source', () { schedulePub(args: ["global", "activate", "-spath", "foo", "1.2.3"], - error: """ - Unexpected argument "1.2.3". - - Usage: pub global activate <package...> - -h, --help Print usage information for this command. - -s, --source The source used to find the package. - [git, hosted (default), path] - - Run "pub help" to see global options. - """, + error: contains('Unexpected argument "1.2.3".'), exitCode: exit_codes.USAGE); }); } diff --git a/test/global/activate/missing_package_arg_test.dart b/test/global/activate/missing_package_arg_test.dart index 9469de2d..72fbc972 100644 --- a/test/global/activate/missing_package_arg_test.dart +++ b/test/global/activate/missing_package_arg_test.dart @@ -2,6 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'package:scheduled_test/scheduled_test.dart'; + import '../../../lib/src/exit_codes.dart' as exit_codes; import '../../test_pub.dart'; @@ -9,15 +11,7 @@ main() { initConfig(); integration('fails if no package was given', () { schedulePub(args: ["global", "activate"], - error: """ - No package to activate given. - - Usage: pub global activate <package...> - -h, --help Print usage information for this command. - -s, --source The source used to find the package. - [git, hosted (default), path] - - Run "pub help" to see global options.""", + error: contains("No package to activate given."), exitCode: exit_codes.USAGE); }); } diff --git a/test/global/activate/unexpected_arguments_test.dart b/test/global/activate/unexpected_arguments_test.dart index 5d519177..24d0e2ce 100644 --- a/test/global/activate/unexpected_arguments_test.dart +++ b/test/global/activate/unexpected_arguments_test.dart @@ -2,6 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'package:scheduled_test/scheduled_test.dart'; + import '../../../lib/src/exit_codes.dart' as exit_codes; import '../../test_pub.dart'; @@ -9,15 +11,7 @@ main() { initConfig(); integration('fails if there are extra arguments', () { schedulePub(args: ["global", "activate", "foo", "1.0.0", "bar", "baz"], - error: """ - Unexpected arguments "bar" and "baz". - - Usage: pub global activate <package...> - -h, --help Print usage information for this command. - -s, --source The source used to find the package. - [git, hosted (default), path] - - Run "pub help" to see global options.""", + error: contains('Unexpected arguments "bar" and "baz".'), exitCode: exit_codes.USAGE); }); } diff --git a/test/global/binstubs/creates_executables_in_pubspec_test.dart b/test/global/binstubs/creates_executables_in_pubspec_test.dart new file mode 100644 index 00000000..616ce006 --- /dev/null +++ b/test/global/binstubs/creates_executables_in_pubspec_test.dart @@ -0,0 +1,40 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; + +main() { + initConfig(); + integration("creates binstubs for each executable in the pubspec", () { + servePackages((builder) { + builder.serve("foo", "1.0.0", pubspec: { + "executables": { + "one": null, + "two-renamed": "two" + } + }, contents: [ + d.dir("bin", [ + d.file("one.dart", "main(args) => print('one');"), + d.file("two.dart", "main(args) => print('two');"), + d.file("nope.dart", "main(args) => print('nope');") + ]) + ]); + }); + + schedulePub(args: ["global", "activate", "foo"], output: + contains("Installed executables one and two-renamed.")); + + d.dir(cachePath, [ + d.dir("bin", [ + d.matcherFile("one", contains("pub global run foo:one")), + d.matcherFile("two-renamed", contains("pub global run foo:two")), + d.nothing("two"), + d.nothing("nope") + ]) + ]).validate(); + }); +} diff --git a/test/global/binstubs/explicit_and_no_executables_options_test.dart b/test/global/binstubs/explicit_and_no_executables_options_test.dart new file mode 100644 index 00000000..65c67ada --- /dev/null +++ b/test/global/binstubs/explicit_and_no_executables_options_test.dart @@ -0,0 +1,24 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../../lib/src/exit_codes.dart' as exit_codes; +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; + +main() { + initConfig(); + integration("errors if -x and --no-executables are both passed", () { + d.dir("foo", [ + d.libPubspec("foo", "1.0.0") + ]).create(); + + schedulePub(args: [ + "global", "activate", "--source", "path", "../foo", + "-x", "anything", "--no-executables" + ], error: contains("Cannot pass both --no-executables and --executable."), + exitCode: exit_codes.USAGE); + }); +} diff --git a/test/global/binstubs/explicit_executables_test.dart b/test/global/binstubs/explicit_executables_test.dart new file mode 100644 index 00000000..632eb461 --- /dev/null +++ b/test/global/binstubs/explicit_executables_test.dart @@ -0,0 +1,40 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; + +main() { + initConfig(); + integration("only creates binstubs for the listed executables", () { + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "executables": { + "one": "script", + "two": "script", + "three": "script" + } + }), + d.dir("bin", [ + d.file("script.dart", "main() => print('ok');") + ]) + ]).create(); + + schedulePub(args: [ + "global", "activate", "--source", "path", "../foo", + "-x", "one", "--executable", "three" + ], output: contains("Installed executables one and three.")); + + d.dir(cachePath, [ + d.dir("bin", [ + d.matcherFile("one", contains("pub global run foo:script")), + d.nothing("two"), + d.matcherFile("three", contains("pub global run foo:script")) + ]) + ]).validate(); + }); +} diff --git a/test/global/binstubs/missing_script_test.dart b/test/global/binstubs/missing_script_test.dart new file mode 100644 index 00000000..084c0c76 --- /dev/null +++ b/test/global/binstubs/missing_script_test.dart @@ -0,0 +1,32 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:scheduled_test/scheduled_stream.dart'; + +import '../../../lib/src/exit_codes.dart' as exit_codes; +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; + +main() { + initConfig(); + integration("errors if an executable's script can't be found", () { + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "executables": { + "missing": "not_here", + "nope": null + } + }) + ]).create(); + + var pub = startPub(args: ["global", "activate", "-spath", "../foo"]); + + pub.stderr.expect('Warning: Executable "missing" runs ' + '"bin/not_here.dart", which was not found in foo.'); + pub.stderr.expect('Warning: Executable "nope" runs "bin/nope.dart", which ' + 'was not found in foo.'); + pub.shouldExit(); + }); +} diff --git a/test/global/binstubs/name_collision_test.dart b/test/global/binstubs/name_collision_test.dart new file mode 100644 index 00000000..7a3120d6 --- /dev/null +++ b/test/global/binstubs/name_collision_test.dart @@ -0,0 +1,61 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:scheduled_test/scheduled_stream.dart'; +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; + +main() { + initConfig(); + integration("does not overwrite an existing binstub", () { + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "executables": { + "foo": "foo", + "collide1": "foo", + "collide2": "foo" + } + }), + d.dir("bin", [ + d.file("foo.dart", "main() => print('ok');") + ]) + ]).create(); + + d.dir("bar", [ + d.pubspec({ + "name": "bar", + "executables": { + "bar": "bar", + "collide1": "bar", + "collide2": "bar" + } + }), + d.dir("bin", [ + d.file("bar.dart", "main() => print('ok');") + ]) + ]).create(); + + schedulePub(args: ["global", "activate", "-spath", "../foo"]); + + var pub = startPub(args: ["global", "activate", "-spath", "../bar"]); + pub.stdout.expect(consumeThrough("Installed executable bar.")); + pub.stderr.expect("Executable collide1 was already installed from foo."); + pub.stderr.expect("Executable collide2 was already installed from foo."); + pub.stderr.expect("Deactivate the other package(s) or activate bar using " + "--overwrite."); + pub.shouldExit(); + + d.dir(cachePath, [ + d.dir("bin", [ + d.matcherFile("foo", contains("foo:foo")), + d.matcherFile("bar", contains("bar:bar")), + d.matcherFile("collide1", contains("foo:foo")), + d.matcherFile("collide2", contains("foo:foo")) + ]) + ]).validate(); + }); +} diff --git a/test/global/binstubs/name_collision_with_overwrite_test.dart b/test/global/binstubs/name_collision_with_overwrite_test.dart new file mode 100644 index 00000000..41a2dcda --- /dev/null +++ b/test/global/binstubs/name_collision_with_overwrite_test.dart @@ -0,0 +1,62 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:scheduled_test/scheduled_stream.dart'; +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; + +main() { + initConfig(); + integration("overwrites an existing binstub if --overwrite is passed", () { + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "executables": { + "foo": "foo", + "collide1": "foo", + "collide2": "foo" + } + }), + d.dir("bin", [ + d.file("foo.dart", "main() => print('ok');") + ]) + ]).create(); + + d.dir("bar", [ + d.pubspec({ + "name": "bar", + "executables": { + "bar": "bar", + "collide1": "bar", + "collide2": "bar" + } + }), + d.dir("bin", [ + d.file("bar.dart", "main() => print('ok');") + ]) + ]).create(); + + schedulePub(args: ["global", "activate", "-spath", "../foo"]); + + var pub = startPub(args: [ + "global", "activate", "-spath", "../bar", "--overwrite" + ]); + pub.stdout.expect(consumeThrough( + "Installed executables bar, collide1 and collide2.")); + pub.stderr.expect("Replaced collide1 previously installed from foo."); + pub.stderr.expect("Replaced collide2 previously installed from foo."); + pub.shouldExit(); + + d.dir(cachePath, [ + d.dir("bin", [ + d.matcherFile("foo", contains("foo:foo")), + d.matcherFile("bar", contains("bar:bar")), + d.matcherFile("collide1", contains("bar:bar")), + d.matcherFile("collide2", contains("bar:bar")) + ]) + ]).validate(); + }); +} diff --git a/test/global/binstubs/no_executables_flag_test.dart b/test/global/binstubs/no_executables_flag_test.dart new file mode 100644 index 00000000..0f7816a4 --- /dev/null +++ b/test/global/binstubs/no_executables_flag_test.dart @@ -0,0 +1,38 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; + +main() { + initConfig(); + integration("does not create binstubs if --no-executables is passed", () { + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "executables": { + "one": null + } + }), + d.dir("bin", [ + d.file("one.dart", "main() => print('ok');") + ]) + ]).create(); + + schedulePub(args: ["global", "activate", "--source", "path", "../foo"]); + + schedulePub(args: [ + "global", "activate", "--source", "path", "../foo", "--no-executables" + ]); + + // Should still delete old one. + d.dir(cachePath, [ + d.dir("bin", [ + d.nothing("one") + ]) + ]).validate(); + }); +} diff --git a/test/global/binstubs/path_package_test.dart b/test/global/binstubs/path_package_test.dart new file mode 100644 index 00000000..a837f73a --- /dev/null +++ b/test/global/binstubs/path_package_test.dart @@ -0,0 +1,34 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; + +main() { + initConfig(); + integration("creates binstubs when activating a path package", () { + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "executables": { + "foo": null + } + }), + d.dir("bin", [ + d.file("foo.dart", "main() => print('ok');") + ]) + ]).create(); + + schedulePub(args: ["global", "activate", "--source", "path", "../foo"], + output: contains("Installed executable foo.")); + + d.dir(cachePath, [ + d.dir("bin", [ + d.matcherFile("foo", contains("pub global run foo:foo")) + ]) + ]).validate(); + }); +} diff --git a/test/global/binstubs/reactivate_removes_old_executables_test.dart b/test/global/binstubs/reactivate_removes_old_executables_test.dart new file mode 100644 index 00000000..c468d09a --- /dev/null +++ b/test/global/binstubs/reactivate_removes_old_executables_test.dart @@ -0,0 +1,48 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; + +main() { + initConfig(); + integration("removes previous binstubs when reactivating a package", () { + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "executables": { + "one": null, + "two": null + } + }), + d.dir("bin", [ + d.file("one.dart", "main() => print('ok');"), + d.file("two.dart", "main() => print('ok');") + ]) + ]).create(); + + schedulePub(args: ["global", "activate", "--source", "path", "../foo"]); + + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "executables": { + // Remove "one". + "two": null + } + }), + ]).create(); + + schedulePub(args: ["global", "activate", "--source", "path", "../foo"]); + + d.dir(cachePath, [ + d.dir("bin", [ + d.nothing("one"), + d.matcherFile("two", contains("two")) + ]) + ]).validate(); + }); +} diff --git a/test/global/binstubs/removes_even_if_not_in_pubspec_test.dart b/test/global/binstubs/removes_even_if_not_in_pubspec_test.dart new file mode 100644 index 00000000..03cd2f74 --- /dev/null +++ b/test/global/binstubs/removes_even_if_not_in_pubspec_test.dart @@ -0,0 +1,45 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; + +main() { + initConfig(); + integration("removes all binstubs for package", () { + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "executables": { + "foo": null + } + }), + d.dir("bin", [ + d.file("foo.dart", "main() => print('ok');") + ]) + ]).create(); + + // Create the binstub for foo. + schedulePub(args: ["global", "activate", "--source", "path", "../foo"]); + + // Remove it from the pubspec. + d.dir("foo", [ + d.pubspec({ + "name": "foo" + }) + ]).create(); + + // Deactivate. + schedulePub(args: ["global", "deactivate", "foo"]); + + // It should still be deleted. + d.dir(cachePath, [ + d.dir("bin", [ + d.nothing("foo") + ]) + ]).validate(); + }); +} diff --git a/test/global/binstubs/removes_when_deactivated_test.dart b/test/global/binstubs/removes_when_deactivated_test.dart new file mode 100644 index 00000000..3a8e149c --- /dev/null +++ b/test/global/binstubs/removes_when_deactivated_test.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; + +main() { + initConfig(); + integration("removes binstubs when the package is deactivated", () { + servePackages((builder) { + builder.serve("foo", "1.0.0", pubspec: { + "executables": { + "one": null, + "two": null + } + }, contents: [ + d.dir("bin", [ + d.file("one.dart", "main(args) => print('one');"), + d.file("two.dart", "main(args) => print('two');") + ]) + ]); + }); + + schedulePub(args: ["global", "activate", "foo"]); + schedulePub(args: ["global", "deactivate", "foo"]); + + d.dir(cachePath, [ + d.dir("bin", [ + d.nothing("one"), + d.nothing("two") + ]) + ]).validate(); + }); +} diff --git a/test/global/binstubs/unknown_explicit_executable_test.dart b/test/global/binstubs/unknown_explicit_executable_test.dart new file mode 100644 index 00000000..ae1e0e0f --- /dev/null +++ b/test/global/binstubs/unknown_explicit_executable_test.dart @@ -0,0 +1,35 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:scheduled_test/scheduled_stream.dart'; + +import '../../../lib/src/exit_codes.dart' as exit_codes; +import '../../descriptor.dart' as d; +import '../../test_pub.dart'; + +main() { + initConfig(); + integration("errors on an unknown explicit executable", () { + d.dir("foo", [ + d.pubspec({ + "name": "foo", + "executables": { + "one": "one" + } + }), + d.dir("bin", [ + d.file("one.dart", "main() => print('ok');") + ]) + ]).create(); + + var pub = startPub(args: [ + "global", "activate", "--source", "path", "../foo", + "-x", "who", "-x", "one", "--executable", "wat" + ]); + + pub.stdout.expect(consumeThrough("Installed executable one.")); + pub.stderr.expect("Unknown executables wat and who."); + pub.shouldExit(exit_codes.DATA); + }); +} diff --git a/test/pubspec_test.dart b/test/pubspec_test.dart index e34ce431..fec3077d 100644 --- a/test/pubspec_test.dart +++ b/test/pubspec_test.dart @@ -444,5 +444,58 @@ publish_to: none (pubspec) => pubspec.publishTo); }); }); + + group("executables", () { + test("defaults to an empty map if omitted", () { + var pubspec = new Pubspec.parse('', sources); + expect(pubspec.executables, isEmpty); + }); + + test("allows simple names for keys and most characters in values", () { + var pubspec = new Pubspec.parse(''' +executables: + abcDEF-123_: "abc DEF-123._" +''', sources); + expect(pubspec.executables['abcDEF-123_'], equals('abc DEF-123._')); + }); + + test("throws if not a map", () { + expectPubspecException('executables: not map', + (pubspec) => pubspec.executables); + }); + + test("throws if key is not a string", () { + expectPubspecException('executables: {123: value}', + (pubspec) => pubspec.executables); + }); + + test("throws if a key isn't a simple name", () { + expectPubspecException('executables: {funny/name: ok}', + (pubspec) => pubspec.executables); + }); + + test("throws if a value is not a string", () { + expectPubspecException('executables: {command: 123}', + (pubspec) => pubspec.executables); + }); + + test("throws if a value contains a path separator", () { + expectPubspecException('executables: {command: funny_name/part}', + (pubspec) => pubspec.executables); + }); + + test("throws if a value contains a windows path separator", () { + expectPubspecException(r'executables: {command: funny_name\part}', + (pubspec) => pubspec.executables); + }); + + test("uses the key if the value is null", () { + var pubspec = new Pubspec.parse(''' +executables: + command: +''', sources); + expect(pubspec.executables['command'], equals('command')); + }); + }); }); } diff --git a/test/validator/executable_test.dart b/test/validator/executable_test.dart new file mode 100644 index 00000000..6d1fb816 --- /dev/null +++ b/test/validator/executable_test.dart @@ -0,0 +1,57 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:scheduled_test/scheduled_test.dart'; + +import '../../lib/src/entrypoint.dart'; +import '../../lib/src/validator.dart'; +import '../../lib/src/validator/executable.dart'; +import '../descriptor.dart' as d; +import '../test_pub.dart'; +import 'utils.dart'; + +Validator executable(Entrypoint entrypoint) => + new ExecutableValidator(entrypoint); + +main() { + initConfig(); + + setUp(d.validPackage.create); + + group('should consider a package valid if it', () { + integration('has executables that are present', () { + d.dir(appPath, [ + d.pubspec({ + "name": "test_pkg", + "version": "1.0.0", + "executables": { + "one": "one_script", + "two": null + } + }), + d.dir("bin", [ + d.file("one_script.dart", "main() => print('ok');"), + d.file("two.dart", "main() => print('ok');") + ]) + ]).create(); + expectNoValidationError(executable); + }); + }); + + group("should consider a package invalid if it", () { + integration('is missing one or more listed executables', () { + d.dir(appPath, [ + d.pubspec({ + "name": "test_pkg", + "version": "1.0.0", + "executables": { + "nope": "not_there", + "nada": null + } + }) + ]).create(); + expectValidationWarning(executable); + }); + }); +} -- GitLab