Cleanup getExecutablePath() to better respect the platform (#24)

diff --git a/.gitignore b/.gitignore
index 5a472d6..fda1750 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 ### Dart template
 # Don’t commit the following directories created by pub.
 .buildlog
+.dart_tool
 .pub/
 build/
 packages
diff --git a/.travis.yml b/.travis.yml
index 83a837c..5722c5b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,6 @@
 language: dart
 sudo: false
 dart:
-  - stable
   - dev
 install:
   - gem install coveralls-lcov
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 53b1990..8973425 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+#### 3.0.0
+
+* Cleanup getExecutablePath() to better respect the platform
+
 #### 2.0.9
 
 * Bumped `package:file` dependency
diff --git a/lib/src/interface/common.dart b/lib/src/interface/common.dart
index c0e80b1..c115332 100644
--- a/lib/src/interface/common.dart
+++ b/lib/src/interface/common.dart
@@ -2,58 +2,89 @@
 // 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 'dart:io' show File, Directory;
-
-import 'package:path/path.dart' as p;
+import 'package:file/file.dart';
+import 'package:file/local.dart';
+import 'package:path/path.dart' show Context, Style;
 import 'package:platform/platform.dart';
 
-/// Searches the `PATH` for the actual executable that [commandName] is supposed
-/// to launch.
+const Map<String, String> _osToPathStyle = const <String, String>{
+  'linux': 'posix',
+  'macos': 'posix',
+  'android': 'posix',
+  'ios': 'posix',
+  'fuchsia': 'posix',
+  'windows': 'windows',
+};
+
+/// Searches the `PATH` for the executable that [command] is supposed to launch.
 ///
-/// Return `null` if the executable cannot be found.
-String getExecutablePath(String commandName, String workingDirectory,
-    {Platform platform}) {
-  platform ??= new LocalPlatform();
-  workingDirectory ??= Directory.current.path;
+/// This first builds a list of candidate paths where the executable may reside.
+/// If [command] is already an absolute path, then the `PATH` environment
+/// variable will not be consulted, and the specified absolute path will be the
+/// only candidate that is considered.
+///
+/// Once the list of candidate paths has been constructed, this will pick the
+/// first such path that represents an existent file.
+///
+/// Return `null` if there were no viable candidates, meaning the executable
+/// could not be found.
+///
+/// If [platform] is not specified, it will default to the current platform.
+String getExecutablePath(
+  String command,
+  String workingDirectory, {
+  Platform platform: const LocalPlatform(),
+  FileSystem fs: const LocalFileSystem(),
+}) {
+  assert(_osToPathStyle[platform.operatingSystem] == fs.path.style.name);
+
+  workingDirectory ??= fs.currentDirectory.path;
+  Context context =
+      new Context(style: fs.path.style, current: workingDirectory);
+
   // TODO(goderbauer): refactor when github.com/google/platform.dart/issues/2
   //     is available.
   String pathSeparator = platform.isWindows ? ';' : ':';
 
   List<String> extensions = <String>[];
-  if (platform.isWindows && p.extension(commandName).isEmpty) {
+  if (platform.isWindows && context.extension(command).isEmpty) {
     extensions = platform.environment['PATHEXT'].split(pathSeparator);
   }
 
   List<String> candidates = <String>[];
-  if (commandName.contains(p.separator)) {
-    candidates =
-        _getCandidatePaths(commandName, <String>[workingDirectory], extensions);
+  if (command.contains(context.separator)) {
+    candidates = _getCandidatePaths(
+        command, <String>[workingDirectory], extensions, context);
   } else {
     List<String> searchPath = platform.environment['PATH'].split(pathSeparator);
-    candidates = _getCandidatePaths(commandName, searchPath, extensions);
+    candidates = _getCandidatePaths(command, searchPath, extensions, context);
   }
-  return candidates.firstWhere((String path) => new File(path).existsSync(),
+  return candidates.firstWhere((String path) => fs.file(path).existsSync(),
       orElse: () => null);
 }
 
-/// Returns all possible combinations of `$searchPath\$commandName.$ext` for
+/// Returns all possible combinations of `$searchPath\$command.$ext` for
 /// `searchPath` in [searchPaths] and `ext` in [extensions].
 ///
 /// If [extensions] is empty, it will just enumerate all
-/// `$searchPath\$commandName`.
-/// If [commandName] is an absolute path, it will just enumerate
-/// `$commandName.$ext`.
+/// `$searchPath\$command`.
+/// If [command] is an absolute path, it will just enumerate
+/// `$command.$ext`.
 Iterable<String> _getCandidatePaths(
-    String commandName, List<String> searchPaths, List<String> extensions) {
+  String command,
+  List<String> searchPaths,
+  List<String> extensions,
+  Context context,
+) {
   List<String> withExtensions = extensions.isNotEmpty
-      ? extensions.map((String ext) => '$commandName$ext').toList()
-      : <String>[commandName];
-  if (p.isAbsolute(commandName)) {
+      ? extensions.map((String ext) => '$command$ext').toList()
+      : <String>[command];
+  if (context.isAbsolute(command)) {
     return withExtensions;
   }
   return searchPaths
       .map((String path) =>
-          withExtensions.map((String command) => p.join(path, command)))
+          withExtensions.map((String command) => context.join(path, command)))
       .expand((Iterable<String> e) => e)
       .toList();
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index 07637b8..a70deb8 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: process
-version: 2.0.9
+version: 3.0.0
 authors:
 - Todd Volkert <tvolkert@google.com>
 - Michael Goderbauer <goderbauer@google.com>
@@ -7,14 +7,14 @@
 homepage: https://github.com/google/process.dart
 
 dependencies:
-  file: '>=2.0.1 <4.0.0'
+  file: '>=2.0.1'
   intl: '>=0.14.0 <0.16.0'
-  meta: ^1.0.4
-  path: ^1.4.0
-  platform: '>=1.0.1 <3.0.0'
+  meta: ^1.1.2
+  path: ^1.5.1
+  platform: '>=1.0.1'
 
 dev_dependencies:
-  test: ^0.12.10
+  test: ^0.12.33
 
 environment:
-  sdk: '>=1.21.0 <2.0.0'
+  sdk: '>=2.0.0-dev.28.0 <2.0.0'
diff --git a/test/src/interface/common_test.dart b/test/src/interface/common_test.dart
index 1c82252..ba2bc5d 100644
--- a/test/src/interface/common_test.dart
+++ b/test/src/interface/common_test.dart
@@ -3,19 +3,19 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:file/file.dart';
-import 'package:file/local.dart';
+import 'package:file/memory.dart';
 import 'package:path/path.dart' as p;
 import 'package:platform/platform.dart';
 import 'package:process/src/interface/common.dart';
 import 'package:test/test.dart';
 
 void main() {
-  FileSystem fs = new LocalFileSystem();
-
   group('getExecutablePath', () {
+    FileSystem fs;
     Directory workingDir, dir1, dir2, dir3;
 
     setUp(() {
+      fs = new MemoryFileSystem();
       workingDir = fs.systemTempDirectory.createTempSync('work_dir_');
       dir1 = fs.systemTempDirectory.createTempSync('dir1_');
       dir2 = fs.systemTempDirectory.createTempSync('dir2_');
@@ -32,11 +32,12 @@
 
       setUp(() {
         platform = new FakePlatform(
-            operatingSystem: 'windows',
-            environment: <String, String>{
-              'PATH': '${dir1.path};${dir2.path}',
-              'PATHEXT': '.exe;.bat'
-            });
+          operatingSystem: 'windows',
+          environment: <String, String>{
+            'PATH': '${dir1.path};${dir2.path}',
+            'PATHEXT': '.exe;.bat'
+          },
+        );
       });
 
       test('absolute', () {
@@ -44,13 +45,21 @@
         String expectedPath = command;
         fs.file(command).createSync();
 
-        String executablePath =
-            getExecutablePath(command, workingDir.path, platform: platform);
+        String executablePath = getExecutablePath(
+          command,
+          workingDir.path,
+          platform: platform,
+          fs: fs,
+        );
         _expectSamePath(executablePath, expectedPath);
 
         command = p.withoutExtension(command);
-        executablePath =
-            getExecutablePath(command, workingDir.path, platform: platform);
+        executablePath = getExecutablePath(
+          command,
+          workingDir.path,
+          platform: platform,
+          fs: fs,
+        );
         _expectSamePath(executablePath, expectedPath);
       });
 
@@ -59,13 +68,21 @@
         String expectedPath = p.join(dir2.path, command);
         fs.file(expectedPath).createSync();
 
-        String executablePath =
-            getExecutablePath(command, workingDir.path, platform: platform);
+        String executablePath = getExecutablePath(
+          command,
+          workingDir.path,
+          platform: platform,
+          fs: fs,
+        );
         _expectSamePath(executablePath, expectedPath);
 
         command = p.withoutExtension(command);
-        executablePath =
-            getExecutablePath(command, workingDir.path, platform: platform);
+        executablePath = getExecutablePath(
+          command,
+          workingDir.path,
+          platform: platform,
+          fs: fs,
+        );
         _expectSamePath(executablePath, expectedPath);
       });
 
@@ -76,13 +93,21 @@
         fs.file(expectedPath).createSync();
         fs.file(wrongPath).createSync();
 
-        String executablePath =
-            getExecutablePath(command, workingDir.path, platform: platform);
+        String executablePath = getExecutablePath(
+          command,
+          workingDir.path,
+          platform: platform,
+          fs: fs,
+        );
         _expectSamePath(executablePath, expectedPath);
 
         command = p.withoutExtension(command);
-        executablePath =
-            getExecutablePath(command, workingDir.path, platform: platform);
+        executablePath = getExecutablePath(
+          command,
+          workingDir.path,
+          platform: platform,
+          fs: fs,
+        );
         _expectSamePath(executablePath, expectedPath);
       });
 
@@ -91,13 +116,21 @@
         String expectedPath = p.join(workingDir.path, command);
         fs.file(expectedPath).createSync(recursive: true);
 
-        String executablePath =
-            getExecutablePath(command, workingDir.path, platform: platform);
+        String executablePath = getExecutablePath(
+          command,
+          workingDir.path,
+          platform: platform,
+          fs: fs,
+        );
         _expectSamePath(executablePath, expectedPath);
 
         command = p.withoutExtension(command);
-        executablePath =
-            getExecutablePath(command, workingDir.path, platform: platform);
+        executablePath = getExecutablePath(
+          command,
+          workingDir.path,
+          platform: platform,
+          fs: fs,
+        );
         _expectSamePath(executablePath, expectedPath);
       });
 
@@ -108,13 +141,21 @@
         fs.file(expectedPath).createSync();
         fs.file(wrongPath).createSync();
 
-        String executablePath =
-            getExecutablePath(command, workingDir.path, platform: platform);
+        String executablePath = getExecutablePath(
+          command,
+          workingDir.path,
+          platform: platform,
+          fs: fs,
+        );
         _expectSamePath(executablePath, expectedPath);
 
         command = p.withoutExtension(command);
-        executablePath =
-            getExecutablePath(command, workingDir.path, platform: platform);
+        executablePath = getExecutablePath(
+          command,
+          workingDir.path,
+          platform: platform,
+          fs: fs,
+        );
         _expectSamePath(executablePath, expectedPath);
       });
 
@@ -127,19 +168,27 @@
         fs.file(wrongPath1).createSync();
         fs.file(wrongPath2).createSync();
 
-        String executablePath =
-            getExecutablePath(command, workingDir.path, platform: platform);
+        String executablePath = getExecutablePath(
+          command,
+          workingDir.path,
+          platform: platform,
+          fs: fs,
+        );
         _expectSamePath(executablePath, expectedPath);
       });
 
       test('not found', () {
         String command = 'foo.exe';
 
-        String executablePath =
-            getExecutablePath(command, workingDir.path, platform: platform);
+        String executablePath = getExecutablePath(
+          command,
+          workingDir.path,
+          platform: platform,
+          fs: fs,
+        );
         expect(executablePath, isNull);
       });
-    });
+    }, skip: 'https://github.com/google/file.dart/issues/68');
 
     group('on Linux', () {
       Platform platform;
@@ -157,8 +206,12 @@
         fs.file(command).createSync();
         fs.file(wrongPath).createSync();
 
-        String executablePath =
-            getExecutablePath(command, workingDir.path, platform: platform);
+        String executablePath = getExecutablePath(
+          command,
+          workingDir.path,
+          platform: platform,
+          fs: fs,
+        );
         _expectSamePath(executablePath, expectedPath);
       });
 
@@ -169,16 +222,24 @@
         fs.file(expectedPath).createSync();
         fs.file(wrongPath).createSync();
 
-        String executablePath =
-            getExecutablePath(command, workingDir.path, platform: platform);
+        String executablePath = getExecutablePath(
+          command,
+          workingDir.path,
+          platform: platform,
+          fs: fs,
+        );
         _expectSamePath(executablePath, expectedPath);
       });
 
       test('not found', () {
         String command = 'foo';
 
-        String executablePath =
-            getExecutablePath(command, workingDir.path, platform: platform);
+        String executablePath = getExecutablePath(
+          command,
+          workingDir.path,
+          platform: platform,
+          fs: fs,
+        );
         expect(executablePath, isNull);
       });
     });