Skip to content

Add "pub run --list" command to show all available executables #2 #1680

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 89 additions & 12 deletions lib/src/command/deps.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,26 @@

import 'dart:collection';

import 'package:analyzer/analyzer.dart' as analyzer;
import 'package:path/path.dart' as p;

import '../ascii_tree.dart' as tree;
import '../command.dart';
import '../dart.dart';
import '../log.dart' as log;
import '../package.dart';
import '../utils.dart';

/// Returns `true` if [path] looks like a Dart entrypoint.
bool _isDartExecutable(String path) {
try {
var unit = analyzer.parseDartFile(path, parseFunctionBodies: false);
return isEntrypoint(unit);
} on analyzer.AnalyzerErrorGroup {
return false;
}
}

/// Handles the `deps` pub command.
class DepsCommand extends PubCommand {
String get name => "deps";
Expand All @@ -36,6 +50,9 @@ class DepsCommand extends PubCommand {
negatable: true,
help: "Whether to include dev dependencies.",
defaultsTo: true);

argParser.addFlag("executables",
negatable: false, help: "List all available executables.");
}

void run() {
Expand All @@ -44,18 +61,22 @@ class DepsCommand extends PubCommand {

_buffer = new StringBuffer();

_buffer.writeln(_labelPackage(entrypoint.root));

switch (argResults["style"]) {
case "compact":
_outputCompact();
break;
case "list":
_outputList();
break;
case "tree":
_outputTree();
break;
if (argResults['executables']) {
_outputExecutables();
} else {
_buffer.writeln(_labelPackage(entrypoint.root));

switch (argResults["style"]) {
case "compact":
_outputCompact();
break;
case "list":
_outputList();
break;
case "tree":
_outputTree();
break;
}
}

log.message(_buffer);
Expand Down Expand Up @@ -231,4 +252,60 @@ class DepsCommand extends PubCommand {
'was generated, please run "pub get" again.');
return null;
}

/// Outputs all executables reachable from [entrypoint].
void _outputExecutables() {
var packages = []
..add(entrypoint.root)
..addAll((_includeDev
? entrypoint.root.immediateDependencies
: entrypoint.root.dependencies)
.map((dep) => entrypoint.packageGraph.packages[dep.name]));

for (var package in packages) {
var executables = _getExecutablesFor(package);
if (executables.isNotEmpty) {
_buffer.writeln(_formatExecutables(package.name, executables.toList()));
}
}
}

/// Lists all Dart files in the `bin` directory of the [package].
///
/// Returns file names without extensions.
List<String> _getExecutablesFor(Package package) => package.executableIds
.where((e) => _isDartExecutable(p.absolute(package.dir, e.path)))
.map((e) => p.basenameWithoutExtension(e.path));

/// Returns formatted string that lists [executables] for the [packageName].
/// Examples:
///
/// _formatExecutables('foo', ['foo']) // -> 'foo'
/// _formatExecutables('foo', ['bar']) // -> 'foo:bar'
/// _formatExecutables('foo', ['bar', 'foo']) // -> 'foo: foo, bar'
///
/// Note the leading space before first executable and sorting order in the
/// last example.
String _formatExecutables(String packageName, List<String> executables) {
if (executables.length == 1) {
// If executable matches the package name omit the name of executable in
// the output.
return executables.first != packageName
? '${packageName}:${log.bold(executables.first)}'
: log.bold(executables.first);
}

// Sort executables to make executable that matches the package name to be
// the first in the list.
executables.sort((e1, e2) {
if (e1 == packageName)
return -1;
else if (e2 == packageName)
return 1;
else
return e1.compareTo(e2);
});

return '${packageName}: ${executables.map(log.bold).join(', ')}';
}
}
225 changes: 225 additions & 0 deletions test/deps/executables_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// Copyright (c) 2016, 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:test/test.dart';

import '../descriptor.dart' as d;
import '../test_pub.dart';

const _validMain = 'main() {}';

main() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't found a good way to test the same things.

I was choosing between:

  • for loop
  • current approach
  • even more test-fn-generators

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could consider wrapping multiple calls to test in _testExecutablesOutput, something like this:

_testExecutablesOutput(String name, output, {devOutput}) {
  devOutput ??= output;
  for (var dev in [true, false]) {
    test(name + dev ? ' --dev' : ' --no-dev', () async {
      await pubGet();
      await runPub(
        args: ['deps', '--executables']
           ..addAll(dev ? ['--dev'] : ['--no-dev']),
        output: dev ? devOutput : output);
    });
  }
}

Up to you though, that pattern can make it hard to support the named args to test methods etc.

_testExecutablesOutput(output, {bool dev: true}) => () async {
await pubGet();
await runPub(
args: ['deps', '--executables']
..addAll(dev ? ['--dev'] : ['--no-dev']),
output: output);
};

_testAllDepsOutput(output) => _testExecutablesOutput(output);
_testNonDevDepsOutput(output) => _testExecutablesOutput(output, dev: false);

group("lists nothing when no executables found", () {
setUp(() async {
await d.dir(appPath, [d.appPubspec()]).create();
});

test("all dependencies", _testAllDepsOutput('\n'));
test("non-dev dependencies", _testNonDevDepsOutput('\n'));
});

group("skips non-Dart executables", () {
setUp(() async {
await d.dir(appPath, [
d.appPubspec(),
d.dir('bin', [d.file('foo.py'), d.file('bar.sh')])
]).create();
});

test("all dependencies", _testAllDepsOutput('\n'));
test("non-dev dependencies", _testNonDevDepsOutput('\n'));
});

group("skips Dart executables which are not parsable", () {
setUp(() async {
await d.dir(appPath, [
d.appPubspec(),
d.dir('bin', [d.file('foo.dart', 'main() {')])
]).create();
});

test("all dependencies", _testAllDepsOutput('\n'));
test("non-dev dependencies", _testNonDevDepsOutput('\n'));
});

group("skips Dart executables without entrypoints", () {
setUp(() async {
await d.dir(appPath, [
d.appPubspec(),
d.dir(
'bin', [d.file('foo.dart'), d.file('bar.dart', 'main(x, y, z) {}')])
]).create();
});

test("all dependencies", _testAllDepsOutput('\n'));
test("non-dev dependencies", _testNonDevDepsOutput('\n'));
});

group("lists valid Dart executables with entrypoints", () {
setUp(() async {
await d.dir(appPath, [
d.appPubspec(),
d.dir('bin',
[d.file('foo.dart', _validMain), d.file('bar.dart', _validMain)])
]).create();
});

test("all dependencies", _testAllDepsOutput('myapp: bar, foo'));
test("non-dev dependencies", _testNonDevDepsOutput('myapp: bar, foo'));
});

group("skips executables in sub directories", () {
setUp(() async {
await d.dir(appPath, [
d.appPubspec(),
d.dir('bin', [
d.file('foo.dart', _validMain),
d.dir('sub', [d.file('bar.dart', _validMain)])
])
]).create();
});

test("all dependencies", _testAllDepsOutput('myapp:foo'));
test("non-dev dependencies", _testNonDevDepsOutput('myapp:foo'));
});

group("lists executables from a dependency", () {
setUp(() async {
await d.dir('foo', [
d.libPubspec('foo', '1.0.0'),
d.dir('bin', [d.file('bar.dart', _validMain)])
]).create();

await d.dir(appPath, [
d.appPubspec({
'foo': {'path': '../foo'}
})
]).create();
});

test("all dependencies", _testAllDepsOutput('foo:bar'));
test("non-dev dependencies", _testNonDevDepsOutput('foo:bar'));
});

group("lists executables only from immediate dependencies", () {
setUp(() async {
await d.dir(appPath, [
d.appPubspec({
'foo': {'path': '../foo'}
})
]).create();

await d.dir('foo', [
d.libPubspec('foo', '1.0.0', deps: {
'baz': {'path': '../baz'}
}),
d.dir('bin', [d.file('bar.dart', _validMain)])
]).create();

await d.dir('baz', [
d.libPubspec('baz', '1.0.0'),
d.dir('bin', [d.file('qux.dart', _validMain)])
]).create();
});

test("all dependencies", _testAllDepsOutput('foo:bar'));
test("non-dev dependencies", _testNonDevDepsOutput('foo:bar'));
});

group("applies formatting before printing executables", () {
setUp(() async {
await d.dir(appPath, [
d.appPubspec({
'foo': {'path': '../foo'},
'bar': {'path': '../bar'}
}),
d.dir('bin', [d.file('myapp.dart', _validMain)])
]).create();

await d.dir('foo', [
d.libPubspec('foo', '1.0.0'),
d.dir('bin',
[d.file('baz.dart', _validMain), d.file('foo.dart', _validMain)])
]).create();

await d.dir('bar', [
d.libPubspec('bar', '1.0.0'),
d.dir('bin', [d.file('qux.dart', _validMain)])
]).create();
});

test("all dependencies", _testAllDepsOutput('''
myapp
bar:qux
foo: foo, baz'''));
test("non-dev dependencies", _testNonDevDepsOutput('''
myapp
bar:qux
foo: foo, baz'''));
});

group("dev dependencies", () {
setUp(() async {
await d.dir('foo', [
d.libPubspec('foo', '1.0.0'),
d.dir('bin', [d.file('bar.dart', _validMain)])
]).create();

await d.dir(appPath, [
d.pubspec({
'name': 'myapp',
'dev_dependencies': {
'foo': {'path': '../foo'}
}
})
]).create();
});

test("are listed if --dev flag is set", _testAllDepsOutput('foo:bar'));
test("are skipped if --no-dev flag is set", _testNonDevDepsOutput('\n'));
});

group("overriden dependencies executables", () {
setUp(() async {
await d.dir('foo-1.0', [
d.libPubspec('foo', '1.0.0'),
d.dir('bin', [d.file('bar.dart', _validMain)])
]).create();

await d.dir('foo-2.0', [
d.libPubspec('foo', '2.0.0'),
d.dir('bin',
[d.file('bar.dart', _validMain), d.file('baz.dart', _validMain)])
]).create();

await d.dir(appPath, [
d.pubspec({
'name': 'myapp',
'dependencies': {
'foo': {'path': '../foo-1.0'}
},
'dependency_overrides': {
'foo': {'path': '../foo-2.0'}
}
})
]).create();
});

test(
'are listed if --dev flag is set', _testAllDepsOutput('foo: bar, baz'));
test('are listed if --no-dev flag is set',
_testNonDevDepsOutput('foo: bar, baz'));
});
}