diff --git a/TestAssets/TestProjects/AppWithToolDependency/project.json b/TestAssets/TestProjects/AppWithToolDependency/project.json index f8982d688..9537feb8f 100644 --- a/TestAssets/TestProjects/AppWithToolDependency/project.json +++ b/TestAssets/TestProjects/AppWithToolDependency/project.json @@ -15,6 +15,6 @@ }, "tools": { - "dotnet-hello": { "version": "1.0.0", "target": "package" } + "dotnet-hello": { "version": "2.0.0", "target": "package" } } } diff --git a/src/Microsoft.DotNet.Cli.Utils/Command.cs b/src/Microsoft.DotNet.Cli.Utils/Command.cs index 3779deeb0..6bc62c57c 100644 --- a/src/Microsoft.DotNet.Cli.Utils/Command.cs +++ b/src/Microsoft.DotNet.Cli.Utils/Command.cs @@ -85,6 +85,11 @@ namespace Microsoft.DotNet.Cli.Utils return command; } + + public static Command Create(CommandSpec commandSpec) + { + return new Command(commandSpec); + } public static Command CreateForScript( string commandName, diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/AbstractPathBasedCommandResolver.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/AbstractPathBasedCommandResolver.cs new file mode 100644 index 000000000..73167305d --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/AbstractPathBasedCommandResolver.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.Extensions.PlatformAbstractions; +using NuGet.Frameworks; +using NuGet.Packaging; +namespace Microsoft.DotNet.Cli.Utils +{ + public abstract class AbstractPathBasedCommandResolver : ICommandResolver + { + protected IEnvironmentProvider _environment; + protected IPlatformCommandSpecFactory _commandSpecFactory; + + public AbstractPathBasedCommandResolver(IEnvironmentProvider environment, + IPlatformCommandSpecFactory commandSpecFactory) + { + if (environment == null) + { + throw new ArgumentNullException("environment"); + } + + if (commandSpecFactory == null) + { + throw new ArgumentNullException("commandSpecFactory"); + } + + _environment = environment; + _commandSpecFactory = commandSpecFactory; + } + + public CommandSpec Resolve(CommandResolverArguments commandResolverArguments) + { + if (commandResolverArguments.CommandName == null) + { + return null; + } + + var commandPath = ResolveCommandPath(commandResolverArguments); + + if (commandPath == null) + { + return null; + } + + return _commandSpecFactory.CreateCommandSpec( + commandResolverArguments.CommandName, + commandResolverArguments.CommandArguments.OrEmptyIfNull(), + commandPath, + GetCommandResolutionStrategy(), + _environment); + } + + internal abstract string ResolveCommandPath(CommandResolverArguments commandResolverArguments); + internal abstract CommandResolutionStrategy GetCommandResolutionStrategy(); + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/AppBaseCommandResolver.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/AppBaseCommandResolver.cs new file mode 100644 index 000000000..26668183f --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/AppBaseCommandResolver.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.Extensions.PlatformAbstractions; +using NuGet.Frameworks; +using NuGet.Packaging; +namespace Microsoft.DotNet.Cli.Utils +{ + public class AppBaseCommandResolver : AbstractPathBasedCommandResolver + { + public AppBaseCommandResolver(IEnvironmentProvider environment, + IPlatformCommandSpecFactory commandSpecFactory) : base(environment, commandSpecFactory) { } + + internal override string ResolveCommandPath(CommandResolverArguments commandResolverArguments) + { + return _environment.GetCommandPathFromRootPath( + PlatformServices.Default.Application.ApplicationBasePath, + commandResolverArguments.CommandName); + } + + internal override CommandResolutionStrategy GetCommandResolutionStrategy() + { + return CommandResolutionStrategy.BaseDirectory; + } + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolutionStrategy.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolutionStrategy.cs new file mode 100644 index 000000000..c3059c8a0 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolutionStrategy.cs @@ -0,0 +1,26 @@ +namespace Microsoft.DotNet.Cli.Utils +{ + public enum CommandResolutionStrategy + { + // command loaded from project dependencies nuget package + ProjectDependenciesPackage, + + // command loaded from project tools nuget package + ProjectToolsPackage, + + // command loaded from the same directory as the executing assembly + BaseDirectory, + + // command loaded from the same directory as a project.json file + ProjectLocal, + + // command loaded from PATH environment variable + Path, + + // command loaded from rooted path + RootedPath, + + // command not found + None + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolverArguments.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolverArguments.cs new file mode 100644 index 000000000..bdd9d83b5 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolverArguments.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class CommandResolverArguments + { + public string CommandName { get; set; } + + public IEnumerable CommandArguments { get; set; } + + public NuGetFramework Framework { get; set; } + + public string OutputPath { get; set; } + + public string ProjectDirectory { get; set; } + + public string Configuration { get; set; } + + public IEnumerable InferredExtensions { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/CompositeCommandResolver.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/CompositeCommandResolver.cs new file mode 100644 index 000000000..71bb589dc --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/CompositeCommandResolver.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.Extensions.PlatformAbstractions; +using NuGet.Frameworks; +using NuGet.Packaging; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class CompositeCommandResolver : ICommandResolver + { + private IList _orderedCommandResolvers; + + public IEnumerable OrderedCommandResolvers + { + get + { + return _orderedCommandResolvers; + } + } + + public CompositeCommandResolver() + { + _orderedCommandResolvers = new List(); + } + + public void AddCommandResolver(ICommandResolver commandResolver) + { + _orderedCommandResolvers.Add(commandResolver); + } + + public CommandSpec Resolve(CommandResolverArguments commandResolverArguments) + { + foreach (var commandResolver in _orderedCommandResolvers) + { + var commandSpec = commandResolver.Resolve(commandResolverArguments); + + if (commandSpec != null) + { + return commandSpec; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/DefaultCommandResolverPolicy.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/DefaultCommandResolverPolicy.cs new file mode 100644 index 000000000..af4244158 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/DefaultCommandResolverPolicy.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.Extensions.PlatformAbstractions; +using NuGet.Frameworks; +using NuGet.Packaging; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class DefaultCommandResolverPolicy + { + public static CompositeCommandResolver Create() + { + var environment = new EnvironmentProvider(); + var packagedCommandSpecFactory = new PackagedCommandSpecFactory(); + + var platformCommandSpecFactory = default(IPlatformCommandSpecFactory); + if (PlatformServices.Default.Runtime.OperatingSystemPlatform == Platform.Windows) + { + platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory(); + } + else + { + platformCommandSpecFactory = new GenericPlatformCommandSpecFactory(); + } + + return CreateDefaultCommandResolver(environment, packagedCommandSpecFactory, platformCommandSpecFactory); + } + + public static CompositeCommandResolver CreateDefaultCommandResolver( + IEnvironmentProvider environment, + IPackagedCommandSpecFactory packagedCommandSpecFactory, + IPlatformCommandSpecFactory platformCommandSpecFactory) + { + var compositeCommandResolver = new CompositeCommandResolver(); + + compositeCommandResolver.AddCommandResolver(new RootedCommandResolver()); + compositeCommandResolver.AddCommandResolver(new ProjectToolsCommandResolver(packagedCommandSpecFactory)); + compositeCommandResolver.AddCommandResolver(new AppBaseCommandResolver(environment, platformCommandSpecFactory)); + compositeCommandResolver.AddCommandResolver(new PathCommandResolver(environment, platformCommandSpecFactory)); + + return compositeCommandResolver; + } + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/GenericPlatformCommandSpecFactory.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/GenericPlatformCommandSpecFactory.cs new file mode 100644 index 000000000..eea6365e4 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/GenericPlatformCommandSpecFactory.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.Extensions.PlatformAbstractions; +using NuGet.Frameworks; +using NuGet.Packaging; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class GenericPlatformCommandSpecFactory : IPlatformCommandSpecFactory + { + public CommandSpec CreateCommandSpec( + string commandName, + IEnumerable args, + string commandPath, + CommandResolutionStrategy resolutionStrategy, + IEnvironmentProvider environment) + { + var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(args); + return new CommandSpec(commandPath, escapedArgs, resolutionStrategy); + } + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ICommandResolver.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ICommandResolver.cs new file mode 100644 index 000000000..25b983598 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ICommandResolver.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.Cli.Utils +{ + public interface ICommandResolver + { + CommandSpec Resolve(CommandResolverArguments arguments); + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/IPackagedCommandSpecFactory.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/IPackagedCommandSpecFactory.cs new file mode 100644 index 000000000..34c14e56c --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/IPackagedCommandSpecFactory.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.DotNet.ProjectModel.Graph; + +namespace Microsoft.DotNet.Cli.Utils +{ + public interface IPackagedCommandSpecFactory + { + CommandSpec CreateCommandSpecFromLibrary( + LockFilePackageLibrary library, + string commandName, + IEnumerable commandArguments, + IEnumerable allowedExtensions, + string nugetPackagesRoot, + CommandResolutionStrategy commandResolutionStrategy, + string depsFilePath); + + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/IPlatformCommandSpecFactory.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/IPlatformCommandSpecFactory.cs new file mode 100644 index 000000000..1463bc9ec --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/IPlatformCommandSpecFactory.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.Extensions.PlatformAbstractions; +using NuGet.Frameworks; +using NuGet.Packaging; + +namespace Microsoft.DotNet.Cli.Utils +{ + public interface IPlatformCommandSpecFactory + { + CommandSpec CreateCommandSpec( + string commandName, + IEnumerable args, + string commandPath, + CommandResolutionStrategy resolutionStrategy, + IEnvironmentProvider environment); + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/PackagedCommandSpecFactory.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/PackagedCommandSpecFactory.cs new file mode 100644 index 000000000..ee5bd7ecb --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/PackagedCommandSpecFactory.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.Extensions.PlatformAbstractions; +using NuGet.Frameworks; +using NuGet.Packaging; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class PackagedCommandSpecFactory : IPackagedCommandSpecFactory + { + public CommandSpec CreateCommandSpecFromLibrary( + LockFilePackageLibrary library, + string commandName, + IEnumerable commandArguments, + IEnumerable allowedExtensions, + string nugetPackagesRoot, + CommandResolutionStrategy commandResolutionStrategy, + string depsFilePath) + { + var packageDirectory = GetPackageDirectoryFullPath(library, nugetPackagesRoot); + + if (!Directory.Exists(packageDirectory)) + { + return null; + } + + var commandFile = GetCommandFileRelativePath(library, commandName, allowedExtensions); + + if (commandFile == null) + { + return null; + } + + var commandPath = Path.Combine(packageDirectory, commandFile); + + return CreateCommandSpecWrappingWithCorehostfDll( + commandPath, + commandArguments, + depsFilePath, + commandResolutionStrategy); + } + + private string GetPackageDirectoryFullPath(LockFilePackageLibrary library, string nugetPackagesRoot) + { + var packageDirectory = new VersionFolderPathResolver(nugetPackagesRoot) + .GetInstallPath(library.Name, library.Version); + + return packageDirectory; + } + + private string GetCommandFileRelativePath( + LockFilePackageLibrary library, + string commandName, + IEnumerable allowedExtensions) + { + // TODO: Should command names be case sensitive? + return library.Files + .Where(f => Path.GetFileNameWithoutExtension(f) == commandName) + .Where(e => allowedExtensions.Contains(Path.GetExtension(e))) + .FirstOrDefault(); + } + + private CommandSpec CreateCommandSpecWrappingWithCorehostfDll( + string commandPath, + IEnumerable commandArguments, + string depsFilePath, + CommandResolutionStrategy commandResolutionStrategy) + { + var commandExtension = Path.GetExtension(commandPath); + + if (commandExtension == FileNameSuffixes.DotNet.DynamicLib) + { + return CreatePackageCommandSpecUsingCorehost( + commandPath, + commandArguments, + depsFilePath, + commandResolutionStrategy); + } + + return CreateCommandSpec(commandPath, commandArguments, commandResolutionStrategy); + } + + private CommandSpec CreatePackageCommandSpecUsingCorehost( + string commandPath, + IEnumerable commandArguments, + string depsFilePath, + CommandResolutionStrategy commandResolutionStrategy) + { + var corehost = CoreHost.HostExePath; + + var arguments = new List(); + arguments.Add(commandPath); + + if (depsFilePath != null) + { + arguments.Add($"--depsfile:{depsFilePath}"); + } + + arguments.AddRange(commandArguments); + + return CreateCommandSpec(corehost, arguments, commandResolutionStrategy); + } + + private CommandSpec CreateCommandSpec( + string commandPath, + IEnumerable commandArguments, + CommandResolutionStrategy commandResolutionStrategy) + { + var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(commandArguments); + + return new CommandSpec(commandPath, escapedArgs, commandResolutionStrategy); + } + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/PathCommandResolver.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/PathCommandResolver.cs new file mode 100644 index 000000000..a1e63c801 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/PathCommandResolver.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.Extensions.PlatformAbstractions; +using NuGet.Frameworks; +using NuGet.Packaging; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class PathCommandResolver : AbstractPathBasedCommandResolver + { + public PathCommandResolver(IEnvironmentProvider environment, + IPlatformCommandSpecFactory commandSpecFactory) : base(environment, commandSpecFactory) { } + + internal override string ResolveCommandPath(CommandResolverArguments commandResolverArguments) + { + return _environment.GetCommandPath(commandResolverArguments.CommandName); + } + + internal override CommandResolutionStrategy GetCommandResolutionStrategy() + { + return CommandResolutionStrategy.Path; + } + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectDependenciesCommandResolver.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectDependenciesCommandResolver.cs new file mode 100644 index 000000000..f5436186c --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectDependenciesCommandResolver.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.Extensions.PlatformAbstractions; +using NuGet.Frameworks; +using NuGet.Packaging; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class ProjectDependenciesCommandResolver : ICommandResolver + { + private static readonly CommandResolutionStrategy s_commandResolutionStrategy = + CommandResolutionStrategy.ProjectDependenciesPackage; + + private IEnvironmentProvider _environment; + private IPackagedCommandSpecFactory _packagedCommandSpecFactory; + + public ProjectDependenciesCommandResolver( + IEnvironmentProvider environment, + IPackagedCommandSpecFactory packagedCommandSpecFactory) + { + if (environment == null) + { + throw new ArgumentNullException("environment"); + } + + if (packagedCommandSpecFactory == null) + { + throw new ArgumentNullException("packagedCommandSpecFactory"); + } + + _environment = environment; + _packagedCommandSpecFactory = packagedCommandSpecFactory; + } + + public CommandSpec Resolve(CommandResolverArguments commandResolverArguments) + { + if (commandResolverArguments.Framework == null + || commandResolverArguments.ProjectDirectory == null + || commandResolverArguments.Configuration == null + || commandResolverArguments.CommandName == null) + { + return null; + } + + return ResolveFromProjectDependencies( + commandResolverArguments.ProjectDirectory, + commandResolverArguments.Framework, + commandResolverArguments.Configuration, + commandResolverArguments.CommandName, + commandResolverArguments.CommandArguments.OrEmptyIfNull(), + commandResolverArguments.OutputPath); + } + + private CommandSpec ResolveFromProjectDependencies( + string projectDirectory, + NuGetFramework framework, + string configuration, + string commandName, + IEnumerable commandArguments, + string outputPath) + { + var allowedExtensions = GetAllowedCommandExtensionsFromEnvironment(_environment); + + var projectContext = GetProjectContextFromDirectory( + projectDirectory, + framework); + + if (projectContext == null) + { + return null; + } + + var depsFilePath = projectContext.GetOutputPaths(configuration, outputPath: outputPath).RuntimeFiles.Deps; + + var dependencyLibraries = GetAllDependencyLibraries(projectContext); + + return ResolveFromDependencyLibraries( + dependencyLibraries, + depsFilePath, + commandName, + allowedExtensions, + commandArguments, + projectContext); + } + + private CommandSpec ResolveFromDependencyLibraries( + IEnumerable dependencyLibraries, + string depsFilePath, + string commandName, + IEnumerable allowedExtensions, + IEnumerable commandArguments, + ProjectContext projectContext) + { + foreach (var dependencyLibrary in dependencyLibraries) + { + var commandSpec = ResolveFromDependencyLibrary( + dependencyLibrary, + depsFilePath, + commandName, + allowedExtensions, + commandArguments, + projectContext); + + if (commandSpec != null) + { + return commandSpec; + } + } + + return null; + } + + private CommandSpec ResolveFromDependencyLibrary( + LockFilePackageLibrary dependencyLibrary, + string depsFilePath, + string commandName, + IEnumerable allowedExtensions, + IEnumerable commandArguments, + ProjectContext projectContext) + { + return _packagedCommandSpecFactory.CreateCommandSpecFromLibrary( + dependencyLibrary, + commandName, + commandArguments, + allowedExtensions, + projectContext.PackagesDirectory, + s_commandResolutionStrategy, + depsFilePath); + } + + private IEnumerable GetAllDependencyLibraries( + ProjectContext projectContext) + { + return projectContext.LibraryManager.GetLibraries() + .Where(l => l.GetType() == typeof(PackageDescription)) + .Select(l => l as PackageDescription) + .Select(p => p.Library); + } + + private ProjectContext GetProjectContextFromDirectory(string directory, NuGetFramework framework) + { + if (directory == null || framework == null) + { + return null; + } + + var projectRootPath = directory; + + if (!File.Exists(Path.Combine(projectRootPath, Project.FileName))) + { + return null; + } + + var projectContext = ProjectContext.Create( + projectRootPath, + framework, + PlatformServices.Default.Runtime.GetAllCandidateRuntimeIdentifiers()); + + if (projectContext.RuntimeIdentifier == null) + { + return null; + } + + return projectContext; + } + + private IEnumerable GetAllowedCommandExtensionsFromEnvironment(IEnvironmentProvider environment) + { + var allowedCommandExtensions = new List(); + allowedCommandExtensions.AddRange(environment.ExecutableExtensions); + allowedCommandExtensions.Add(FileNameSuffixes.DotNet.DynamicLib); + + return allowedCommandExtensions; + } + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectPathCommandResolver.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectPathCommandResolver.cs new file mode 100644 index 000000000..d857ba890 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectPathCommandResolver.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.Extensions.PlatformAbstractions; +using NuGet.Frameworks; +using NuGet.Packaging; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class ProjectPathCommandResolver : AbstractPathBasedCommandResolver + { + public ProjectPathCommandResolver(IEnvironmentProvider environment, + IPlatformCommandSpecFactory commandSpecFactory) : base(environment, commandSpecFactory) { } + + internal override string ResolveCommandPath(CommandResolverArguments commandResolverArguments) + { + if (commandResolverArguments.ProjectDirectory == null) + { + return null; + } + + return _environment.GetCommandPathFromRootPath( + commandResolverArguments.ProjectDirectory, + commandResolverArguments.CommandName, + commandResolverArguments.InferredExtensions.OrEmptyIfNull()); + } + + internal override CommandResolutionStrategy GetCommandResolutionStrategy() + { + return CommandResolutionStrategy.ProjectLocal; + } + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectToolsCommandResolver.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectToolsCommandResolver.cs new file mode 100644 index 000000000..906d660e9 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectToolsCommandResolver.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.Extensions.PlatformAbstractions; +using NuGet.Frameworks; +using NuGet.Packaging; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class ProjectToolsCommandResolver : ICommandResolver + { + private static readonly NuGetFramework s_toolPackageFramework = FrameworkConstants.CommonFrameworks.NetStandardApp15; + private static readonly CommandResolutionStrategy s_commandResolutionStrategy = + CommandResolutionStrategy.ProjectToolsPackage; + + private List _allowedCommandExtensions; + private IPackagedCommandSpecFactory _packagedCommandSpecFactory; + + public ProjectToolsCommandResolver(IPackagedCommandSpecFactory packagedCommandSpecFactory) + { + _packagedCommandSpecFactory = packagedCommandSpecFactory; + + _allowedCommandExtensions = new List() + { + FileNameSuffixes.DotNet.DynamicLib + }; + } + + public CommandSpec Resolve(CommandResolverArguments commandResolverArguments) + { + if (commandResolverArguments.CommandName == null + || commandResolverArguments.ProjectDirectory == null) + { + return null; + } + + return ResolveFromProjectTools( + commandResolverArguments.CommandName, + commandResolverArguments.CommandArguments.OrEmptyIfNull(), + commandResolverArguments.ProjectDirectory); + } + + private CommandSpec ResolveFromProjectTools( + string commandName, + IEnumerable args, + string projectDirectory) + { + var projectContext = GetProjectContextFromDirectory(projectDirectory, s_toolPackageFramework); + + if (projectContext == null) + { + return null; + } + + var toolsLibraries = projectContext.ProjectFile.Tools.OrEmptyIfNull(); + + return ResolveCommandSpecFromAllToolLibraries( + toolsLibraries, + commandName, + args, + projectContext); + } + + private CommandSpec ResolveCommandSpecFromAllToolLibraries( + IEnumerable toolsLibraries, + string commandName, + IEnumerable args, + ProjectContext projectContext) + { + foreach (var toolLibrary in toolsLibraries) + { + var commandSpec = ResolveCommandSpecFromToolLibrary(toolLibrary, commandName, args, projectContext); + + if (commandSpec != null) + { + return commandSpec; + } + } + + return null; + } + + private CommandSpec ResolveCommandSpecFromToolLibrary( + LibraryRange toolLibrary, + string commandName, + IEnumerable args, + ProjectContext projectContext) + { + //todo: change this for new resolution strategy + var lockFilePath = Path.Combine( + projectContext.ProjectDirectory, + "artifacts", "Tools", toolLibrary.Name, + "project.lock.json"); + + if (!File.Exists(lockFilePath)) + { + return null; + } + + var lockFile = LockFileReader.Read(lockFilePath); + + var lockFilePackageLibrary = lockFile.PackageLibraries.FirstOrDefault(l => l.Name == toolLibrary.Name); + + var nugetPackagesRoot = projectContext.PackagesDirectory; + + return _packagedCommandSpecFactory.CreateCommandSpecFromLibrary( + lockFilePackageLibrary, + commandName, + args, + _allowedCommandExtensions, + projectContext.PackagesDirectory, + s_commandResolutionStrategy, + null); + } + + private ProjectContext GetProjectContextFromDirectory(string directory, NuGetFramework framework) + { + if (directory == null || framework == null) + { + return null; + } + + var projectRootPath = directory; + + if (!File.Exists(Path.Combine(projectRootPath, Project.FileName))) + { + return null; + } + + var projectContext = ProjectContext.Create( + projectRootPath, + framework, + PlatformServices.Default.Runtime.GetAllCandidateRuntimeIdentifiers()); + + if (projectContext.RuntimeIdentifier == null) + { + return null; + } + + return projectContext; + } + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/RootedCommandResolver.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/RootedCommandResolver.cs new file mode 100644 index 000000000..f5a6d08a1 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/RootedCommandResolver.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.Extensions.PlatformAbstractions; +using NuGet.Frameworks; +using NuGet.Packaging; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class RootedCommandResolver : ICommandResolver + { + public CommandSpec Resolve(CommandResolverArguments commandResolverArguments) + { + if (commandResolverArguments.CommandName == null) + { + return null; + } + + if (Path.IsPathRooted(commandResolverArguments.CommandName)) + { + var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart( + commandResolverArguments.CommandArguments.OrEmptyIfNull()); + + return new CommandSpec(commandResolverArguments.CommandName, escapedArgs, CommandResolutionStrategy.RootedPath); + } + + return null; + } + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ScriptCommandResolverPolicy.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ScriptCommandResolverPolicy.cs new file mode 100644 index 000000000..cb18e0f8e --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ScriptCommandResolverPolicy.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.Extensions.PlatformAbstractions; +using NuGet.Frameworks; +using NuGet.Packaging; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class ScriptCommandResolverPolicy + { + public static CompositeCommandResolver Create() + { + var environment = new EnvironmentProvider(); + + var platformCommandSpecFactory = default(IPlatformCommandSpecFactory); + if (PlatformServices.Default.Runtime.OperatingSystemPlatform == Platform.Windows) + { + platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory(); + } + else + { + platformCommandSpecFactory = new GenericPlatformCommandSpecFactory(); + } + + return CreateScriptCommandResolver(environment, platformCommandSpecFactory); + } + + public static CompositeCommandResolver CreateScriptCommandResolver( + IEnvironmentProvider environment, + IPlatformCommandSpecFactory platformCommandSpecFactory) + { + var compositeCommandResolver = new CompositeCommandResolver(); + + compositeCommandResolver.AddCommandResolver(new RootedCommandResolver()); + compositeCommandResolver.AddCommandResolver(new ProjectPathCommandResolver(environment, platformCommandSpecFactory)); + compositeCommandResolver.AddCommandResolver(new AppBaseCommandResolver(environment, platformCommandSpecFactory)); + compositeCommandResolver.AddCommandResolver(new PathCommandResolver(environment, platformCommandSpecFactory)); + + return compositeCommandResolver; + } + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/WindowsExePreferredCommandSpecFactory.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/WindowsExePreferredCommandSpecFactory.cs new file mode 100644 index 000000000..aacd356b9 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/WindowsExePreferredCommandSpecFactory.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.Extensions.PlatformAbstractions; +using NuGet.Frameworks; +using NuGet.Packaging; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class WindowsExePreferredCommandSpecFactory : IPlatformCommandSpecFactory + { + public CommandSpec CreateCommandSpec( + string commandName, + IEnumerable args, + string commandPath, + CommandResolutionStrategy resolutionStrategy, + IEnvironmentProvider environment) + { + var useCmdWrapper = false; + + if (Path.GetExtension(commandPath).Equals(".cmd", StringComparison.OrdinalIgnoreCase)) + { + var preferredCommandPath = environment.GetCommandPath(commandName, ".exe"); + + if (preferredCommandPath == null) + { + useCmdWrapper = true; + } + else + { + commandPath = preferredCommandPath; + } + } + + return useCmdWrapper + ? CreateCommandSpecWrappedWithCmd(commandPath, args, resolutionStrategy) + : CreateCommandSpecFromExecutable(commandPath, args, resolutionStrategy); + } + + private CommandSpec CreateCommandSpecFromExecutable( + string command, + IEnumerable args, + CommandResolutionStrategy resolutionStrategy) + { + var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(args); + return new CommandSpec(command, escapedArgs, resolutionStrategy); + } + + private CommandSpec CreateCommandSpecWrappedWithCmd( + string command, + IEnumerable args, + CommandResolutionStrategy resolutionStrategy) + { + var comSpec = Environment.GetEnvironmentVariable("ComSpec") ?? "cmd.exe"; + + // Handle the case where ComSpec is already the command + if (command.Equals(comSpec, StringComparison.OrdinalIgnoreCase)) + { + command = args.FirstOrDefault(); + args = args.Skip(1); + } + + var cmdEscapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForCmdProcessStart(args); + + if (ArgumentEscaper.ShouldSurroundWithQuotes(command)) + { + command = $"\"{command}\""; + } + + var escapedArgString = $"/s /c \"{command} {cmdEscapedArgs}\""; + + return new CommandSpec(comSpec, escapedArgString, resolutionStrategy); + } + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolutionStrategy.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolutionStrategy.cs deleted file mode 100644 index 83589d989..000000000 --- a/src/Microsoft.DotNet.Cli.Utils/CommandResolutionStrategy.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Microsoft.DotNet.Cli.Utils -{ - public enum CommandResolutionStrategy - { - //command loaded from a nuget package - NugetPackage, - - //command loaded from the same directory as the executing assembly - BaseDirectory, - - //command loaded from the same directory as a project.json file - ProjectLocal, - - //command loaded from path - Path, - - //command not found - None - } -} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolver.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolver.cs index 0c5b67724..6c797b4a1 100644 --- a/src/Microsoft.DotNet.Cli.Utils/CommandResolver.cs +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolver.cs @@ -14,264 +14,44 @@ namespace Microsoft.DotNet.Cli.Utils internal static class CommandResolver { public static CommandSpec TryResolveCommandSpec( - string commandName, - IEnumerable args, - NuGetFramework framework = null, - string configuration = Constants.DefaultConfiguration, - string outputPath = null) + string commandName, + IEnumerable args, + NuGetFramework framework = null, + string configuration=Constants.DefaultConfiguration, + string outputPath=null) { - return ResolveFromRootedCommand(commandName, args) ?? - ResolveFromProjectDependencies(commandName, args, framework, configuration, outputPath) ?? - ResolveFromProjectTools(commandName, args) ?? - ResolveFromAppBase(commandName, args) ?? - ResolveFromPath(commandName, args); - } - - public static CommandSpec TryResolveScriptCommandSpec(string commandName, IEnumerable args, Project project, string[] inferredExtensionList) - { - return ResolveFromRootedCommand(commandName, args) ?? - ResolveFromProjectPath(commandName, args, project, inferredExtensionList) ?? - ResolveFromAppBase(commandName, args) ?? - ResolveFromPath(commandName, args); - } - - - private static CommandSpec ResolveFromPath(string commandName, IEnumerable args) - { - var commandPath = Env.GetCommandPath(commandName); - return commandPath == null - ? null - : CreateCommandSpecPreferringExe(commandName, args, commandPath, CommandResolutionStrategy.Path); - } - - private static CommandSpec ResolveFromAppBase(string commandName, IEnumerable args) - { - var commandPath = Env.GetCommandPathFromRootPath(PlatformServices.Default.Application.ApplicationBasePath, commandName); - return commandPath == null - ? null - : CreateCommandSpecPreferringExe(commandName, args, commandPath, CommandResolutionStrategy.BaseDirectory); - } - - private static CommandSpec ResolveFromProjectPath(string commandName, IEnumerable args, Project project, string[] inferredExtensionList) - { - var commandPath = Env.GetCommandPathFromRootPath(project.ProjectDirectory, commandName, inferredExtensionList); - return commandPath == null - ? null - : CreateCommandSpecPreferringExe(commandName, args, commandPath, CommandResolutionStrategy.ProjectLocal); - } - - private static CommandSpec ResolveFromRootedCommand(string commandName, IEnumerable args) - { - if (Path.IsPathRooted(commandName)) + var commandResolverArgs = new CommandResolverArguments { - var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(args); - return new CommandSpec(commandName, escapedArgs, CommandResolutionStrategy.Path); - } - - return null; - } - - public static CommandSpec ResolveFromProjectDependencies( - string commandName, - IEnumerable args, - NuGetFramework framework, - string configuration, - string outputPath) - { - if (framework == null) return null; - - var projectContext = GetProjectContext(framework); - - if (projectContext == null) return null; - - var commandPackage = GetCommandPackage(projectContext, commandName); - - if (commandPackage == null) return null; - - var depsPath = projectContext.GetOutputPaths(configuration, outputPath: outputPath).RuntimeFiles.Deps; - - return ConfigureCommandFromPackage(commandName, args, commandPackage, projectContext, depsPath); - } - - private static ProjectContext GetProjectContext(NuGetFramework framework) - { - var projectRootPath = Directory.GetCurrentDirectory(); - - if (!File.Exists(Path.Combine(projectRootPath, Project.FileName))) - { - return null; - } - - var projectContext = ProjectContext.Create(projectRootPath, framework, PlatformServices.Default.Runtime.GetAllCandidateRuntimeIdentifiers()); - return projectContext; - } - - private static PackageDescription GetCommandPackage(ProjectContext projectContext, string commandName) - { - return projectContext.LibraryManager.GetLibraries() - .Where(l => l.GetType() == typeof(PackageDescription)) - .Select(l => l as PackageDescription) - .FirstOrDefault(p => p.Library.Files - .Select(Path.GetFileName) - .Where(f => Path.GetFileNameWithoutExtension(f) == commandName) - .Select(Path.GetExtension) - .Any(e => Env.ExecutableExtensions.Contains(e) || - e == FileNameSuffixes.DotNet.DynamicLib)); - } - - public static CommandSpec ResolveFromProjectTools(string commandName, IEnumerable args) - { - var context = GetProjectContext(FrameworkConstants.CommonFrameworks.NetStandardApp15); - - if (context == null) - { - return null; - } - - var commandLibrary = context.ProjectFile.Tools - .FirstOrDefault(l => l.Name == commandName); - - if (commandLibrary == default(LibraryRange)) - { - return null; - } - - var lockPath = Path.Combine(context.ProjectDirectory, "artifacts", "Tools", commandName, - "project.lock.json"); - - if (!File.Exists(lockPath)) - { - return null; - } - - var lockFile = LockFileReader.Read(lockPath); - - var lib = lockFile.PackageLibraries.FirstOrDefault(l => l.Name == commandName); - var packageDir = new VersionFolderPathResolver(context.PackagesDirectory) - .GetInstallPath(lib.Name, lib.Version); - - return Directory.Exists(packageDir) - ? ConfigureCommandFromPackage(commandName, args, lib.Files, packageDir) - : null; - } - - private static CommandSpec ConfigureCommandFromPackage(string commandName, IEnumerable args, string packageDir) - { - var commandPackage = new PackageFolderReader(packageDir); - - var files = commandPackage.GetFiles(); - - return ConfigureCommandFromPackage(commandName, args, files, packageDir); - } - - private static CommandSpec ConfigureCommandFromPackage(string commandName, IEnumerable args, - PackageDescription commandPackage, ProjectContext projectContext, string depsPath = null) - { - var files = commandPackage.Library.Files; - - var packageRoot = projectContext.PackagesDirectory; - - var packagePath = commandPackage.Path; - - var packageDir = Path.Combine(packageRoot, packagePath); - - return ConfigureCommandFromPackage(commandName, args, files, packageDir, depsPath); - } - - private static CommandSpec ConfigureCommandFromPackage(string commandName, IEnumerable args, - IEnumerable files, string packageDir, string depsPath = null) - { - var fileName = string.Empty; - - var commandPath = files - .FirstOrDefault(f => Env.ExecutableExtensions.Contains(Path.GetExtension(f))); - - if (commandPath == null) - { - var dllPath = files - .Where(f => Path.GetFileName(f) == commandName + FileNameSuffixes.DotNet.DynamicLib) - .Select(f => Path.Combine(packageDir, f)) - .FirstOrDefault(); - - fileName = CoreHost.HostExePath; - - var additionalArgs = new List(); - additionalArgs.Add(dllPath); - - if (depsPath != null) - { - additionalArgs.Add($"--depsfile:{depsPath}"); - } - - args = additionalArgs.Concat(args); - } - else - { - fileName = Path.Combine(packageDir, commandPath); - } - - var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(args); - return new CommandSpec(fileName, escapedArgs, CommandResolutionStrategy.NugetPackage); - } - - private static CommandSpec CreateCommandSpecPreferringExe( - string commandName, - IEnumerable args, - string commandPath, - CommandResolutionStrategy resolutionStrategy) - { - var useComSpec = false; + CommandName = commandName, + CommandArguments = args, + Framework = framework, + ProjectDirectory = Directory.GetCurrentDirectory(), + Configuration = configuration, + OutputPath = outputPath + }; - if (PlatformServices.Default.Runtime.OperatingSystemPlatform == Platform.Windows && - Path.GetExtension(commandPath).Equals(".cmd", StringComparison.OrdinalIgnoreCase)) - { - var preferredCommandPath = Env.GetCommandPath(commandName, ".exe"); - - // Use cmd if we can't find an exe - if (preferredCommandPath == null) - { - useComSpec = true; - } - else - { - commandPath = preferredCommandPath; - } - } - - if (useComSpec) - { - return CreateCmdCommandSpec(commandPath, args, resolutionStrategy); - } - else - { - var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(args); - return new CommandSpec(commandPath, escapedArgs, resolutionStrategy); - } - } - - private static CommandSpec CreateCmdCommandSpec( - string command, - IEnumerable args, - CommandResolutionStrategy resolutionStrategy) - { - var comSpec = Environment.GetEnvironmentVariable("ComSpec"); + var defaultCommandResolver = DefaultCommandResolverPolicy.Create(); - // Handle the case where ComSpec is already the command - if (command.Equals(comSpec, StringComparison.OrdinalIgnoreCase)) + return defaultCommandResolver.Resolve(commandResolverArgs); + } + + public static CommandSpec TryResolveScriptCommandSpec( + string commandName, + IEnumerable args, + Project project, + string[] inferredExtensionList) + { + var commandResolverArgs = new CommandResolverArguments { - command = args.FirstOrDefault(); - args = args.Skip(1); - } - var cmdEscapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForCmdProcessStart(args); + CommandName = commandName, + CommandArguments = args, + ProjectDirectory = project.ProjectDirectory, + InferredExtensions = inferredExtensionList + }; - if (ArgumentEscaper.ShouldSurroundWithQuotes(command)) - { - command = $"\"{command}\""; - } - - var escapedArgString = $"/s /c \"{command} {cmdEscapedArgs}\""; - - return new CommandSpec(comSpec, escapedArgString, resolutionStrategy); + var scriptCommandResolver = ScriptCommandResolverPolicy.Create(); + + return scriptCommandResolver.Resolve(commandResolverArgs); } } } diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandSpec.cs b/src/Microsoft.DotNet.Cli.Utils/CommandSpec.cs index 4ab5e8441..5b941515c 100644 --- a/src/Microsoft.DotNet.Cli.Utils/CommandSpec.cs +++ b/src/Microsoft.DotNet.Cli.Utils/CommandSpec.cs @@ -1,6 +1,6 @@ namespace Microsoft.DotNet.Cli.Utils { - internal class CommandSpec + public class CommandSpec { public CommandSpec(string path, string args, CommandResolutionStrategy resolutionStrategy) { diff --git a/src/Microsoft.DotNet.Cli.Utils/CoreHost.cs b/src/Microsoft.DotNet.Cli.Utils/CoreHost.cs index 7775800ec..2dae756b0 100644 --- a/src/Microsoft.DotNet.Cli.Utils/CoreHost.cs +++ b/src/Microsoft.DotNet.Cli.Utils/CoreHost.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using Microsoft.Extensions.PlatformAbstractions; diff --git a/src/Microsoft.DotNet.Cli.Utils/Env.cs b/src/Microsoft.DotNet.Cli.Utils/Env.cs index 6fdac60f1..30a117bcc 100644 --- a/src/Microsoft.DotNet.Cli.Utils/Env.cs +++ b/src/Microsoft.DotNet.Cli.Utils/Env.cs @@ -9,74 +9,29 @@ namespace Microsoft.DotNet.Cli.Utils { public static class Env { - private static IEnumerable _searchPaths; - private static IEnumerable _executableExtensions; + private static IEnvironmentProvider _environment = new EnvironmentProvider(); public static IEnumerable ExecutableExtensions { get { - if (_executableExtensions == null) - { - - _executableExtensions = PlatformServices.Default.Runtime.OperatingSystemPlatform == Platform.Windows - ? Environment.GetEnvironmentVariable("PATHEXT") - .Split(';') - .Select(e => e.ToLower().Trim('"')) - : new [] { string.Empty }; - } - - return _executableExtensions; - } - } - - private static IEnumerable SearchPaths - { - get - { - if (_searchPaths == null) - { - var searchPaths = new List { PlatformServices.Default.Application.ApplicationBasePath }; - - searchPaths.AddRange(Environment - .GetEnvironmentVariable("PATH") - .Split(Path.PathSeparator) - .Select(p => p.Trim('"'))); - - _searchPaths = searchPaths; - } - - return _searchPaths; + return _environment.ExecutableExtensions; } } public static string GetCommandPath(string commandName, params string[] extensions) { - if (!extensions.Any()) - { - extensions = Env.ExecutableExtensions.ToArray(); - } - - var commandPath = Env.SearchPaths.Join( - extensions, - p => true, s => true, - (p, s) => Path.Combine(p, commandName + s)) - .FirstOrDefault(File.Exists); - - return commandPath; + return _environment.GetCommandPath(commandName, extensions); } public static string GetCommandPathFromRootPath(string rootPath, string commandName, params string[] extensions) { - if (!extensions.Any()) - { - extensions = Env.ExecutableExtensions.ToArray(); - } + return _environment.GetCommandPathFromRootPath(rootPath, commandName, extensions); + } - var commandPath = extensions.Select(e => Path.Combine(rootPath, commandName + e)) - .FirstOrDefault(File.Exists); - - return commandPath; + public static string GetCommandPathFromRootPath(string rootPath, string commandName, IEnumerable extensions) + { + return _environment.GetCommandPathFromRootPath(rootPath, commandName, extensions); } } } diff --git a/src/Microsoft.DotNet.Cli.Utils/EnvironmentProvider.cs b/src/Microsoft.DotNet.Cli.Utils/EnvironmentProvider.cs new file mode 100644 index 000000000..1060443ff --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/EnvironmentProvider.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.Extensions.PlatformAbstractions; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class EnvironmentProvider : IEnvironmentProvider + { + private IEnumerable _searchPaths; + private IEnumerable _executableExtensions; + + public IEnumerable ExecutableExtensions + { + get + { + if (_executableExtensions == null) + { + + _executableExtensions = PlatformServices.Default.Runtime.OperatingSystemPlatform == Platform.Windows + ? Environment.GetEnvironmentVariable("PATHEXT") + .Split(';') + .Select(e => e.ToLower().Trim('"')) + : new [] { string.Empty }; + } + + return _executableExtensions; + } + } + + private IEnumerable SearchPaths + { + get + { + if (_searchPaths == null) + { + var searchPaths = new List { PlatformServices.Default.Application.ApplicationBasePath }; + + searchPaths.AddRange(Environment + .GetEnvironmentVariable("PATH") + .Split(Path.PathSeparator) + .Select(p => p.Trim('"'))); + + _searchPaths = searchPaths; + } + + return _searchPaths; + } + } + + public EnvironmentProvider( + IEnumerable extensionsOverride = null, + IEnumerable searchPathsOverride = null) + { + _executableExtensions = extensionsOverride; + _searchPaths = searchPathsOverride; + } + + public string GetCommandPath(string commandName, params string[] extensions) + { + if (!extensions.Any()) + { + extensions = ExecutableExtensions.ToArray(); + } + + var commandPath = SearchPaths.Join( + extensions, + p => true, s => true, + (p, s) => Path.Combine(p, commandName + s)) + .FirstOrDefault(File.Exists); + + return commandPath; + } + + public string GetCommandPathFromRootPath(string rootPath, string commandName, params string[] extensions) + { + if (!extensions.Any()) + { + extensions = ExecutableExtensions.ToArray(); + } + + var commandPath = extensions.Select(e => Path.Combine(rootPath, commandName + e)) + .FirstOrDefault(File.Exists); + + return commandPath; + } + + public string GetCommandPathFromRootPath(string rootPath, string commandName, IEnumerable extensions) + { + var extensionsArr = extensions.OrEmptyIfNull().ToArray(); + + return GetCommandPathFromRootPath(rootPath, commandName, extensionsArr); + } + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/CollectionsExtensions.cs b/src/Microsoft.DotNet.Cli.Utils/Extensions/CollectionsExtensions.cs similarity index 60% rename from src/Microsoft.DotNet.Cli.Utils/CollectionsExtensions.cs rename to src/Microsoft.DotNet.Cli.Utils/Extensions/CollectionsExtensions.cs index 575ee62ba..6cea049c3 100644 --- a/src/Microsoft.DotNet.Cli.Utils/CollectionsExtensions.cs +++ b/src/Microsoft.DotNet.Cli.Utils/Extensions/CollectionsExtensions.cs @@ -1,6 +1,4 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - +using System; using System.Collections.Generic; using System.Linq; @@ -10,7 +8,7 @@ namespace Microsoft.DotNet.Cli.Utils { public static IEnumerable OrEmptyIfNull(this IEnumerable enumerable) { - return enumerable == null + return enumerable == null ? Enumerable.Empty() : enumerable; } diff --git a/src/Microsoft.DotNet.Cli.Utils/FixedPathCommandFactory.cs b/src/Microsoft.DotNet.Cli.Utils/FixedPathCommandFactory.cs deleted file mode 100644 index f0eab5391..000000000 --- a/src/Microsoft.DotNet.Cli.Utils/FixedPathCommandFactory.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Collections.Generic; -using NuGet.Frameworks; - -namespace Microsoft.DotNet.Cli.Utils -{ - public class FixedPathCommandFactory : ICommandFactory - { - private readonly NuGetFramework _nugetFramework; - private readonly string _configuration; - private readonly string _outputPath; - - public FixedPathCommandFactory(NuGetFramework nugetFramework, string configuration, string outputPath) - { - _nugetFramework = nugetFramework; - _configuration = configuration; - _outputPath = outputPath; - } - - public ICommand Create( - string commandName, - IEnumerable args, - NuGetFramework framework = null, - string configuration = Constants.DefaultConfiguration) - { - if (string.IsNullOrEmpty(configuration)) - { - configuration = _configuration; - } - - if (framework == null) - { - framework = _nugetFramework; - } - - return Command.Create(commandName, args, framework, configuration, _outputPath); - } - } -} diff --git a/src/Microsoft.DotNet.Cli.Utils/IEnvironmentProvider.cs b/src/Microsoft.DotNet.Cli.Utils/IEnvironmentProvider.cs new file mode 100644 index 000000000..db04ebc0b --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/IEnvironmentProvider.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.Extensions.PlatformAbstractions; + +namespace Microsoft.DotNet.Cli.Utils +{ + public interface IEnvironmentProvider + { + IEnumerable ExecutableExtensions { get; } + + string GetCommandPath(string commandName, params string[] extensions); + + string GetCommandPathFromRootPath(string rootPath, string commandName, params string[] extensions); + + string GetCommandPathFromRootPath(string rootPath, string commandName, IEnumerable extensions); + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/ProjectDependenciesCommandFactory.cs b/src/Microsoft.DotNet.Cli.Utils/ProjectDependenciesCommandFactory.cs new file mode 100644 index 000000000..3b6d943f9 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/ProjectDependenciesCommandFactory.cs @@ -0,0 +1,92 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class ProjectDependenciesCommandFactory : ICommandFactory + { + private readonly NuGetFramework _nugetFramework; + private readonly string _configuration; + private readonly string _outputPath; + private readonly string _projectDirectory; + + public ProjectDependenciesCommandFactory( + NuGetFramework nugetFramework, + string configuration, + string outputPath, + string projectDirectory) + { + _nugetFramework = nugetFramework; + _configuration = configuration; + _outputPath = outputPath; + _projectDirectory = projectDirectory; + } + + public ICommand Create( + string commandName, + IEnumerable args, + NuGetFramework framework = null, + string configuration = Constants.DefaultConfiguration) + { + if (string.IsNullOrEmpty(configuration)) + { + configuration = _configuration; + } + + if (framework == null) + { + framework = _nugetFramework; + } + + var commandSpec = FindProjectDependencyCommands( + commandName, + args, + configuration, + framework, + _outputPath, + _projectDirectory); + + return Command.Create(commandSpec); + } + + private CommandSpec FindProjectDependencyCommands( + string commandName, + IEnumerable commandArgs, + string configuration, + NuGetFramework framework, + string outputPath, + string projectDirectory) + { + var commandResolverArguments = new CommandResolverArguments + { + CommandName = commandName, + CommandArguments = commandArgs, + Framework = framework, + Configuration = configuration, + OutputPath = outputPath, + ProjectDirectory = projectDirectory + }; + + var commandResolver = GetProjectDependenciesCommandResolver(); + + var commandSpec = commandResolver.Resolve(commandResolverArguments); + if (commandSpec == null) + { + throw new CommandUnknownException(commandName); + } + + return commandSpec; + } + + private ICommandResolver GetProjectDependenciesCommandResolver() + { + var environment = new EnvironmentProvider(); + var packagedCommandSpecFactory = new PackagedCommandSpecFactory(); + + return new ProjectDependenciesCommandResolver(environment, packagedCommandSpecFactory); + } + } +} diff --git a/src/dotnet/commands/dotnet-test/Program.cs b/src/dotnet/commands/dotnet-test/Program.cs index 7d9eed583..24d12c11b 100644 --- a/src/dotnet/commands/dotnet-test/Program.cs +++ b/src/dotnet/commands/dotnet-test/Program.cs @@ -102,12 +102,19 @@ namespace Microsoft.DotNet.Tools.Test var commandArgs = new List { GetAssemblyUnderTest(projectContext, configuration, outputPath) }; commandArgs.AddRange(app.RemainingArguments); - return Command.Create( + var commandFactory = + new ProjectDependenciesCommandFactory( + projectContext.TargetFramework, + configuration, + outputPath, + projectContext.ProjectDirectory); + + + return commandFactory.Create( $"dotnet-{GetCommandName(testRunner)}", commandArgs, projectContext.TargetFramework, - configuration: configuration, - outputPath: outputPath) + configuration) .ForwardStdErr() .ForwardStdOut() .Execute() @@ -159,8 +166,13 @@ namespace Microsoft.DotNet.Tools.Test var messages = new TestMessagesCollection(); using (var dotnetTest = new DotnetTest(messages, assemblyUnderTest)) { - var commandFactory = - new FixedPathCommandFactory(projectContext.TargetFramework, configuration, outputPath); + var commandFactory = + new ProjectDependenciesCommandFactory( + projectContext.TargetFramework, + configuration, + outputPath, + projectContext.ProjectDirectory); + var testRunnerFactory = new TestRunnerFactory(GetCommandName(testRunner), commandFactory); dotnetTest diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/CommandResolverTestUtils.cs b/test/Microsoft.DotNet.Cli.Utils.Tests/CommandResolverTestUtils.cs new file mode 100644 index 000000000..c83237012 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils.Tests/CommandResolverTestUtils.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; + +namespace Microsoft.DotNet.Cli.Utils.Tests +{ + public static class CommandResolverTestUtils + { + public static string CreateNonRunnableTestCommand(string directory, string filename, string extension=".dll") + { + Directory.CreateDirectory(directory); + + var filePath = Path.Combine(directory, filename + extension); + + File.WriteAllText(filePath, "test command that does nothing."); + + return filePath; + } + + public static IEnvironmentProvider SetupEnvironmentProviderWhichFindsExtensions(params string[] extensions) + { + return new EnvironmentProvider(extensions); + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/GivenACompositeCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenACompositeCommandResolver.cs new file mode 100644 index 000000000..988d9e319 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenACompositeCommandResolver.cs @@ -0,0 +1,86 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Linq; +using Xunit; +using Moq; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Tools.Test.Utilities; +using Microsoft.Extensions.PlatformAbstractions; +using System.Threading; +using FluentAssertions; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils.Tests +{ + public class GivenACompositeCommandResolver + { + [Fact] + public void It_iterates_through_all_added_resolvers_in_order_when_they_return_null() + { + var compositeCommandResolver = new CompositeCommandResolver(); + + var resolverCalls = new List(); + + var mockResolver1 = new Mock(); + mockResolver1.Setup(r => r + .Resolve(It.IsAny())) + .Returns(default(CommandSpec)) + .Callback(() => resolverCalls.Add(1)); + + var mockResolver2 = new Mock(); + mockResolver2.Setup(r => r + .Resolve(It.IsAny())) + .Returns(default(CommandSpec)) + .Callback(() => resolverCalls.Add(2)); + + compositeCommandResolver.AddCommandResolver(mockResolver1.Object); + compositeCommandResolver.AddCommandResolver(mockResolver2.Object); + + compositeCommandResolver.Resolve(default(CommandResolverArguments)); + + resolverCalls.Should() + .HaveCount(2) + .And + .ContainInOrder(new [] {1, 2}); + + } + + [Fact] + public void It_stops_iterating_through_added_resolvers_when_one_returns_nonnull() + { + var compositeCommandResolver = new CompositeCommandResolver(); + + var resolverCalls = new List(); + + var mockResolver1 = new Mock(); + mockResolver1.Setup(r => r + .Resolve(It.IsAny())) + .Returns(new CommandSpec(null, null, default(CommandResolutionStrategy))) + .Callback(() => resolverCalls.Add(1)); + + var mockResolver2 = new Mock(); + mockResolver2.Setup(r => r + .Resolve(It.IsAny())) + .Returns(default(CommandSpec)) + .Callback(() => resolverCalls.Add(2)); + + compositeCommandResolver.AddCommandResolver(mockResolver1.Object); + compositeCommandResolver.AddCommandResolver(mockResolver2.Object); + + compositeCommandResolver.Resolve(default(CommandResolverArguments)); + + resolverCalls.Should() + .HaveCount(1) + .And + .ContainInOrder(new [] {1}); + + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/GivenADefaultCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenADefaultCommandResolver.cs new file mode 100644 index 000000000..5c1a5a5ec --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenADefaultCommandResolver.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Linq; +using Xunit; +using Moq; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Tools.Test.Utilities; +using Microsoft.Extensions.PlatformAbstractions; +using System.Threading; +using FluentAssertions; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils.Tests +{ + public class GivenADefaultCommandResolver + { + [Fact] + public void It_contains_resolvers_in_the_right_order() + { + var defaultCommandResolver = DefaultCommandResolverPolicy.Create(); + + var resolvers = defaultCommandResolver.OrderedCommandResolvers; + + resolvers.Should().HaveCount(4); + + resolvers.Select(r => r.GetType()) + .Should() + .ContainInOrder( + new []{ + typeof(RootedCommandResolver), + typeof(ProjectToolsCommandResolver), + typeof(AppBaseCommandResolver), + typeof(PathCommandResolver) + }); + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAPathCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAPathCommandResolver.cs new file mode 100644 index 000000000..4004c5b00 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAPathCommandResolver.cs @@ -0,0 +1,218 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Linq; +using Xunit; +using Moq; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Tools.Test.Utilities; +using Microsoft.Extensions.PlatformAbstractions; +using System.Threading; +using FluentAssertions; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils.Tests +{ + public class GivenAPathCommandResolver + { + private static readonly string s_testDirectory = Path.Combine(AppContext.BaseDirectory, "pathTestDirectory"); + + [Fact] + public void It_returns_null_when_CommandName_is_null() + { + var pathCommandResolver = SetupPlatformPathCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = null, + CommandArguments = null + }; + + var result = pathCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_returns_null_when_CommandName_does_not_exist_in_PATH() + { + var emptyPathEnvironmentMock = new Mock(); + emptyPathEnvironmentMock.Setup(e => e + .GetCommandPath(It.IsAny(), It.IsAny())) + .Returns((string)null); + + var pathCommandResolver = SetupPlatformPathCommandResolver(emptyPathEnvironmentMock.Object); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "nonexistent-command", + CommandArguments = null + }; + + var result = pathCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_returns_a_CommandSpec_with_CommandName_as_FileName_when_CommandName_exists_in_PATH() + { + var testCommandPath = CommandResolverTestUtils.CreateNonRunnableTestCommand( + s_testDirectory, + "pathtestcommand1", + ".exe"); + + var staticPathEnvironmentMock = new Mock(); + staticPathEnvironmentMock.Setup(e => e + .GetCommandPath(It.IsAny(), It.IsAny())) + .Returns(testCommandPath); + + var pathCommandResolver = SetupPlatformPathCommandResolver(staticPathEnvironmentMock.Object, forceGeneric: true); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = Path.GetFileNameWithoutExtension(testCommandPath), + CommandArguments = null + }; + + var result = pathCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + + var commandFile = Path.GetFileNameWithoutExtension(result.Path); + + commandFile.Should().Be(Path.GetFileNameWithoutExtension(testCommandPath)); + } + + [Fact] + public void It_escapes_CommandArguments_when_returning_a_CommandSpec() + { + var testCommandPath = CommandResolverTestUtils.CreateNonRunnableTestCommand( + s_testDirectory, + "pathtestcommand1", + ".exe"); + + var staticPathEnvironmentMock = new Mock(); + staticPathEnvironmentMock.Setup(e => e + .GetCommandPath(It.IsAny(), It.IsAny())) + .Returns(testCommandPath); + + var pathCommandResolver = SetupPlatformPathCommandResolver(staticPathEnvironmentMock.Object, forceGeneric: true); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = Path.GetFileNameWithoutExtension(testCommandPath), + CommandArguments = new [] {"arg with space"} + }; + + var result = pathCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + result.Args.Should().Be("\"arg with space\""); + } + + [Fact] + public void It_returns_a_CommandSpec_with_Args_as_stringEmpty_when_returning_a_CommandSpec_and_CommandArguments_are_null() + { + var testCommandPath = CommandResolverTestUtils.CreateNonRunnableTestCommand( + s_testDirectory, + "pathtestcommand1", + ".exe"); + + var staticPathEnvironmentMock = new Mock(); + staticPathEnvironmentMock.Setup(e => e + .GetCommandPath(It.IsAny(), It.IsAny())) + .Returns(testCommandPath); + + var pathCommandResolver = SetupPlatformPathCommandResolver(staticPathEnvironmentMock.Object, forceGeneric: true); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = Path.GetFileNameWithoutExtension(testCommandPath), + CommandArguments = null + }; + + var result = pathCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + result.Args.Should().Be(string.Empty); + } + + [Fact] + public void It_prefers_EXE_over_CMD_when_two_command_candidates_exist_and_using_WindowsExePreferredCommandSpecFactory() + { + var environment = new EnvironmentProvider(new [] {".exe", ".cmd"}, new[] { s_testDirectory }); + var platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory(); + + var pathCommandResolver = new PathCommandResolver(environment, platformCommandSpecFactory); + + CommandResolverTestUtils.CreateNonRunnableTestCommand(s_testDirectory, "extensionPreferenceCommand", ".exe"); + CommandResolverTestUtils.CreateNonRunnableTestCommand(s_testDirectory, "extensionPreferenceCommand", ".cmd"); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "extensionPreferenceCommand", + CommandArguments = null + }; + + var result = pathCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + + var commandFile = Path.GetFileName(result.Path); + commandFile.Should().Be("extensionPreferenceCommand.exe"); + } + + [Fact] + public void It_wraps_command_with_CMD_EXE_when_command_has_CMD_Extension_and_using_WindowsExePreferredCommandSpecFactory() + { + var environment = new EnvironmentProvider(new [] {".cmd"}, new[] { s_testDirectory }); + var platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory(); + + var pathCommandResolver = new PathCommandResolver(environment, platformCommandSpecFactory); + + var testCommandPath = + CommandResolverTestUtils.CreateNonRunnableTestCommand(s_testDirectory, "cmdWrapCommand", ".cmd"); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "cmdWrapCommand", + CommandArguments = null + }; + + var result = pathCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + + var commandFile = Path.GetFileName(result.Path); + commandFile.Should().Be("cmd.exe"); + + result.Args.Should().Contain(testCommandPath); + } + + private PathCommandResolver SetupPlatformPathCommandResolver( + IEnvironmentProvider environment = null, + bool forceGeneric = false) + { + environment = environment ?? new EnvironmentProvider(); + + IPlatformCommandSpecFactory platformCommandSpecFactory = new GenericPlatformCommandSpecFactory(); + + if (PlatformServices.Default.Runtime.OperatingSystemPlatform == Platform.Windows + && !forceGeneric) + { + platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory(); + } + + var pathCommandResolver = new PathCommandResolver(environment, platformCommandSpecFactory); + + return pathCommandResolver; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAProjectDependencyCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAProjectDependencyCommandResolver.cs new file mode 100644 index 000000000..3fc93ae72 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAProjectDependencyCommandResolver.cs @@ -0,0 +1,221 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Linq; +using Xunit; +using Moq; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Tools.Test.Utilities; +using Microsoft.Extensions.PlatformAbstractions; +using System.Threading; +using FluentAssertions; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils.Tests +{ + public class GivenAProjectDependenciesCommandResolver + { + + private static readonly string s_liveProjectDirectory = + Path.Combine(AppContext.BaseDirectory, "TestAssets/TestProjects/AppWithDirectDependency"); + + [Fact] + public void It_returns_null_when_CommandName_is_null() + { + var projectDependenciesCommandResolver = SetupProjectDependenciesCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = null, + CommandArguments = new string[] {""}, + ProjectDirectory = "/some/directory", + Configuration = "Debug", + Framework = FrameworkConstants.CommonFrameworks.NetStandardApp15 + }; + + var result = projectDependenciesCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_returns_null_when_ProjectDirectory_is_null() + { + var projectDependenciesCommandResolver = SetupProjectDependenciesCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "command", + CommandArguments = new string[] {""}, + ProjectDirectory = null, + Configuration = "Debug", + Framework = FrameworkConstants.CommonFrameworks.NetStandardApp15 + }; + + var result = projectDependenciesCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_returns_null_when_Framework_is_null() + { + var projectDependenciesCommandResolver = SetupProjectDependenciesCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "command", + CommandArguments = new string[] {""}, + ProjectDirectory = s_liveProjectDirectory, + Configuration = "Debug", + Framework = null + }; + + var result = projectDependenciesCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_returns_null_when_Configuration_is_null() + { + var projectDependenciesCommandResolver = SetupProjectDependenciesCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "command", + CommandArguments = new string[] {""}, + ProjectDirectory = s_liveProjectDirectory, + Configuration = null, + Framework = FrameworkConstants.CommonFrameworks.NetStandardApp15 + }; + + var result = projectDependenciesCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_returns_null_when_CommandName_does_not_exist_in_ProjectDependencies() + { + var projectDependenciesCommandResolver = SetupProjectDependenciesCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "nonexistent-command", + CommandArguments = null, + ProjectDirectory = s_liveProjectDirectory, + Configuration = "Debug", + Framework = FrameworkConstants.CommonFrameworks.NetStandardApp15 + }; + + var result = projectDependenciesCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_returns_a_CommandSpec_with_CoreHost_as_FileName_and_CommandName_in_Args_when_CommandName_exists_in_ProjectDependencies() + { + var projectDependenciesCommandResolver = SetupProjectDependenciesCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "dotnet-hello", + CommandArguments = null, + ProjectDirectory = s_liveProjectDirectory, + Configuration = "Debug", + Framework = FrameworkConstants.CommonFrameworks.NetStandardApp15 + }; + + var result = projectDependenciesCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + + var commandFile = Path.GetFileNameWithoutExtension(result.Path); + + commandFile.Should().Be("corehost"); + + result.Args.Should().Contain(commandResolverArguments.CommandName); + } + + [Fact] + public void It_escapes_CommandArguments_when_returning_a_CommandSpec() + { + var projectDependenciesCommandResolver = SetupProjectDependenciesCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "dotnet-hello", + CommandArguments = new [] { "arg with space"}, + ProjectDirectory = s_liveProjectDirectory, + Configuration = "Debug", + Framework = FrameworkConstants.CommonFrameworks.NetStandardApp15 + }; + + var result = projectDependenciesCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + result.Args.Should().Contain("\"arg with space\""); + } + + [Fact] + public void It_passes_depsfile_arg_to_corehost_when_returning_a_commandspec() + { + var projectDependenciesCommandResolver = SetupProjectDependenciesCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "dotnet-hello", + CommandArguments = null, + ProjectDirectory = s_liveProjectDirectory, + Configuration = "Debug", + Framework = FrameworkConstants.CommonFrameworks.NetStandardApp15 + }; + + var result = projectDependenciesCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + result.Args.Should().Contain("--depsfile"); + } + + [Fact] + public void It_returns_a_CommandSpec_with_CommandName_in_Args_when_returning_a_CommandSpec_and_CommandArguments_are_null() + { + var projectDependenciesCommandResolver = SetupProjectDependenciesCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "dotnet-hello", + CommandArguments = null, + ProjectDirectory = s_liveProjectDirectory, + Configuration = "Debug", + Framework = FrameworkConstants.CommonFrameworks.NetStandardApp15 + }; + + var result = projectDependenciesCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + + result.Args.Should().Contain("dotnet-hello"); + } + + private ProjectDependenciesCommandResolver SetupProjectDependenciesCommandResolver( + IEnvironmentProvider environment = null, + IPackagedCommandSpecFactory packagedCommandSpecFactory = null) + { + environment = environment ?? new EnvironmentProvider(); + packagedCommandSpecFactory = packagedCommandSpecFactory ?? new PackagedCommandSpecFactory(); + + var projectDependenciesCommandResolver = new ProjectDependenciesCommandResolver(environment, packagedCommandSpecFactory); + + return projectDependenciesCommandResolver; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAProjectPathCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAProjectPathCommandResolver.cs new file mode 100644 index 000000000..9c0a0ce6a --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAProjectPathCommandResolver.cs @@ -0,0 +1,264 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Linq; +using Xunit; +using Moq; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Tools.Test.Utilities; +using Microsoft.Extensions.PlatformAbstractions; +using System.Threading; +using FluentAssertions; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils.Tests +{ + public class GivenAProjectPathCommandResolver + { + private static readonly string s_testProjectDirectory = Path.Combine(AppContext.BaseDirectory, "testprojectdirectory"); + + [Fact] + public void It_returns_null_when_CommandName_is_null() + { + var projectPathCommandResolver = SetupPlatformProjectPathCommandResolver(forceGeneric: true); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = null, + CommandArguments = new string[] {""}, + ProjectDirectory = "/some/directory" + }; + + var result = projectPathCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_returns_null_when_ProjectDirectory_is_null() + { + var projectPathCommandResolver = SetupPlatformProjectPathCommandResolver(forceGeneric: true); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "command", + CommandArguments = new string[] {""}, + ProjectDirectory = null + }; + + var result = projectPathCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_returns_null_when_CommandName_does_not_exist_in_ProjectDirectory() + { + var projectPathCommandResolver = SetupPlatformProjectPathCommandResolver(forceGeneric: true); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "nonexistent-command", + CommandArguments = null, + ProjectDirectory = s_testProjectDirectory + }; + + var result = projectPathCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_returns_null_when_CommandName_exists_in_a_subdirectory_of_ProjectDirectory() + { + var environment = CommandResolverTestUtils.SetupEnvironmentProviderWhichFindsExtensions(".exe"); + var projectPathCommandResolver = SetupPlatformProjectPathCommandResolver(environment, forceGeneric: true); + + var testDir = Path.Combine(s_testProjectDirectory, "projectpathtestsubdir"); + CommandResolverTestUtils.CreateNonRunnableTestCommand(testDir, "projectpathtestsubdircommand", ".exe"); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "projectpathtestsubdircommand", + CommandArguments = null, + ProjectDirectory = s_testProjectDirectory + }; + + var result = projectPathCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_returns_a_CommandSpec_with_CommandName_as_FileName_when_CommandName_exists_in_ProjectDirectory() + { + var environment = CommandResolverTestUtils.SetupEnvironmentProviderWhichFindsExtensions(".exe"); + var projectPathCommandResolver = SetupPlatformProjectPathCommandResolver(environment, forceGeneric: true); + + CommandResolverTestUtils.CreateNonRunnableTestCommand(s_testProjectDirectory, "projectpathtestcommand1", ".exe"); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "projectpathtestcommand1", + CommandArguments = null, + ProjectDirectory = s_testProjectDirectory + }; + + var result = projectPathCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + + var commandFile = Path.GetFileNameWithoutExtension(result.Path); + + commandFile.Should().Be("projectpathtestcommand1"); + } + + [Fact] + public void It_escapes_CommandArguments_when_returning_a_CommandSpec() + { + var environment = CommandResolverTestUtils.SetupEnvironmentProviderWhichFindsExtensions(".exe"); + var projectPathCommandResolver = SetupPlatformProjectPathCommandResolver(environment, forceGeneric: true); + + CommandResolverTestUtils.CreateNonRunnableTestCommand(s_testProjectDirectory, "projectpathtestcommand1", ".exe"); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "projectpathtestcommand1", + CommandArguments = new [] { "arg with space"}, + ProjectDirectory = s_testProjectDirectory + }; + + var result = projectPathCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + result.Args.Should().Be("\"arg with space\""); + } + + [Fact] + public void It_resolves_commands_with_extensions_defined_in_InferredExtensions() + { + var extensions = new string[] {".sh", ".cmd", ".foo", ".exe"}; + var projectPathCommandResolver = SetupPlatformProjectPathCommandResolver(forceGeneric: true); + + foreach (var extension in extensions) + { + var extensionTestDir = Path.Combine(s_testProjectDirectory, "testext" + extension); + + CommandResolverTestUtils.CreateNonRunnableTestCommand(extensionTestDir, "projectpathexttest", extension); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "projectpathexttest", + CommandArguments = null, + ProjectDirectory = extensionTestDir, + InferredExtensions = extensions + }; + + var result = projectPathCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + + var commandFileName = Path.GetFileName(result.Path); + commandFileName.Should().Be("projectpathexttest" + extension); + } + } + + [Fact] + public void It_returns_a_CommandSpec_with_Args_as_stringEmpty_when_returning_a_CommandSpec_and_CommandArguments_are_null() + { + var environment = CommandResolverTestUtils.SetupEnvironmentProviderWhichFindsExtensions(".exe"); + var projectPathCommandResolver = SetupPlatformProjectPathCommandResolver(environment, forceGeneric: true); + + CommandResolverTestUtils.CreateNonRunnableTestCommand(s_testProjectDirectory, "projectpathtestcommand1", ".exe"); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "projectpathtestcommand1", + CommandArguments = null, + ProjectDirectory = s_testProjectDirectory + }; + + var result = projectPathCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + result.Args.Should().Be(string.Empty); + } + + [Fact] + public void It_prefers_EXE_over_CMD_when_two_command_candidates_exist_and_using_WindowsExePreferredCommandSpecFactory() + { + var environment = CommandResolverTestUtils.SetupEnvironmentProviderWhichFindsExtensions(".exe"); + var platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory(); + + var projectPathCommandResolver = new ProjectPathCommandResolver(environment, platformCommandSpecFactory); + + CommandResolverTestUtils.CreateNonRunnableTestCommand(s_testProjectDirectory, "projectpathtestcommand1", ".exe"); + CommandResolverTestUtils.CreateNonRunnableTestCommand(s_testProjectDirectory, "projectpathtestcommand1", ".cmd"); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "projectpathtestcommand1", + CommandArguments = null, + ProjectDirectory = s_testProjectDirectory + }; + + var result = projectPathCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + + var commandFile = Path.GetFileName(result.Path); + commandFile.Should().Be("projectpathtestcommand1.exe"); + } + + public void It_wraps_command_with_CMD_EXE_when_command_has_CMD_Extension_and_using_WindowsExePreferredCommandSpecFactory() + { + var environment = new EnvironmentProvider(new [] {".cmd"}); + var platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory(); + + var pathCommandResolver = new PathCommandResolver(environment, platformCommandSpecFactory); + + var testCommandPath = + CommandResolverTestUtils.CreateNonRunnableTestCommand(s_testProjectDirectory, "cmdWrapCommand", ".cmd"); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "cmdWrapCommand", + CommandArguments = null + }; + + var result = pathCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + + var commandFile = Path.GetFileName(result.Path); + commandFile.Should().Be("cmd.exe"); + + result.Args.Should().Contain(testCommandPath); + } + + private ProjectPathCommandResolver SetupPlatformProjectPathCommandResolver( + IEnvironmentProvider environment = null, + bool forceGeneric = false) + { + environment = environment ?? new EnvironmentProvider(); + + IPlatformCommandSpecFactory platformCommandSpecFactory = new GenericPlatformCommandSpecFactory(); + + if (PlatformServices.Default.Runtime.OperatingSystemPlatform == Platform.Windows + && !forceGeneric) + { + platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory(); + } + + var projectPathCommandResolver = new ProjectPathCommandResolver(environment, platformCommandSpecFactory); + + return projectPathCommandResolver; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAProjectToolsCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAProjectToolsCommandResolver.cs new file mode 100644 index 000000000..c414b1345 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAProjectToolsCommandResolver.cs @@ -0,0 +1,152 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Linq; +using Xunit; +using Moq; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Tools.Test.Utilities; +using Microsoft.Extensions.PlatformAbstractions; +using System.Threading; +using FluentAssertions; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils.Tests +{ + public class GivenAProjectToolsCommandResolver + { + + private static readonly string s_liveProjectDirectory = + Path.Combine(AppContext.BaseDirectory, "TestAssets/TestProjects/AppWithToolDependency"); + + [Fact] + public void It_returns_null_when_CommandName_is_null() + { + var projectToolsCommandResolver = SetupProjectToolsCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = null, + CommandArguments = new string[] {""}, + ProjectDirectory = "/some/directory" + }; + + var result = projectToolsCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_returns_null_when_ProjectDirectory_is_null() + { + var projectToolsCommandResolver = SetupProjectToolsCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "command", + CommandArguments = new string[] {""}, + ProjectDirectory = null + }; + + var result = projectToolsCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_returns_null_when_CommandName_does_not_exist_in_ProjectTools() + { + var projectToolsCommandResolver = SetupProjectToolsCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "nonexistent-command", + CommandArguments = null, + ProjectDirectory = s_liveProjectDirectory + }; + + var result = projectToolsCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_returns_a_CommandSpec_with_CoreHost_as_FileName_and_CommandName_in_Args_when_CommandName_exists_in_ProjectTools() + { + var projectToolsCommandResolver = SetupProjectToolsCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "dotnet-hello", + CommandArguments = null, + ProjectDirectory = s_liveProjectDirectory + }; + + var result = projectToolsCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + + var commandFile = Path.GetFileNameWithoutExtension(result.Path); + + commandFile.Should().Be("corehost"); + + result.Args.Should().Contain(commandResolverArguments.CommandName); + } + + [Fact] + public void It_escapes_CommandArguments_when_returning_a_CommandSpec() + { + var projectToolsCommandResolver = SetupProjectToolsCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "dotnet-hello", + CommandArguments = new [] { "arg with space"}, + ProjectDirectory = s_liveProjectDirectory + }; + + var result = projectToolsCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + result.Args.Should().Contain("\"arg with space\""); + } + + [Fact] + public void It_returns_a_CommandSpec_with_Args_as_CommandPath_when_returning_a_CommandSpec_and_CommandArguments_are_null() + { + var projectToolsCommandResolver = SetupProjectToolsCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "dotnet-hello", + CommandArguments = null, + ProjectDirectory = s_liveProjectDirectory + }; + + var result = projectToolsCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + + var commandPath = result.Args.Trim('"'); + commandPath.Should().Contain("dotnet-hello"); + + File.Exists(commandPath).Should().BeTrue(); + } + + private ProjectToolsCommandResolver SetupProjectToolsCommandResolver( + IPackagedCommandSpecFactory packagedCommandSpecFactory = null) + { + packagedCommandSpecFactory = packagedCommandSpecFactory ?? new PackagedCommandSpecFactory(); + + var projectToolsCommandResolver = new ProjectToolsCommandResolver(packagedCommandSpecFactory); + + return projectToolsCommandResolver; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/GivenARootedCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenARootedCommandResolver.cs new file mode 100644 index 000000000..bcaba5148 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenARootedCommandResolver.cs @@ -0,0 +1,111 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Linq; +using Xunit; +using Moq; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Tools.Test.Utilities; +using Microsoft.Extensions.PlatformAbstractions; +using System.Threading; +using FluentAssertions; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils.Tests +{ + public class GivenARootedCommandResolver + { + [Fact] + public void It_returns_null_when_CommandName_is_null() + { + var rootedCommandResolver = new RootedCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = null, + CommandArguments = null + }; + + var result = rootedCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_returns_null_when_CommandName_is_not_rooted() + { + var rootedCommandResolver = new RootedCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "some/relative/path", + CommandArguments = null + }; + + var result = rootedCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_returns_a_CommandSpec_with_CommandName_as_Path_when_CommandName_is_rooted() + { + var rootedCommandResolver = new RootedCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "/some/rooted/path", + CommandArguments = null + }; + + var result = rootedCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + result.Path.Should().Be(commandResolverArguments.CommandName); + } + + [Fact] + public void It_escapes_CommandArguments_when_returning_a_CommandSpec() + { + var rootedCommandResolver = new RootedCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "/some/rooted/path", + CommandArguments = new [] { "arg with space"} + }; + + var result = rootedCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + result.Path.Should().Be(commandResolverArguments.CommandName); + + result.Args.Should().Be("\"arg with space\""); + } + + [Fact] + public void It_returns_a_CommandSpec_with_Args_as_stringEmpty_when_returning_a_CommandSpec_and_CommandArguments_are_null() + { + var rootedCommandResolver = new RootedCommandResolver(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "/some/rooted/path", + CommandArguments = null + }; + + var result = rootedCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + result.Path.Should().Be(commandResolverArguments.CommandName); + + result.Args.Should().Be(string.Empty); + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAScriptCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAScriptCommandResolver.cs new file mode 100644 index 000000000..3dd23c6e5 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAScriptCommandResolver.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Xunit; +using Moq; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Tools.Test.Utilities; +using Microsoft.Extensions.PlatformAbstractions; +using System.Threading; +using FluentAssertions; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils.Tests +{ + public class GivenAScriptCommandResolver + { + [Fact] + public void It_contains_resolvers_in_the_right_order() + { + var scriptCommandResolver = ScriptCommandResolverPolicy.Create(); + + var resolvers = scriptCommandResolver.OrderedCommandResolvers; + + resolvers.Should().HaveCount(4); + + resolvers.Select(r => r.GetType()) + .Should() + .ContainInOrder( + new []{ + typeof(RootedCommandResolver), + typeof(ProjectPathCommandResolver), + typeof(AppBaseCommandResolver), + typeof(PathCommandResolver) + }); + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAnAppBaseCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAnAppBaseCommandResolver.cs new file mode 100644 index 000000000..2f1bd56db --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAnAppBaseCommandResolver.cs @@ -0,0 +1,209 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Linq; +using Xunit; +using Moq; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.Tools.Test.Utilities; +using Microsoft.Extensions.PlatformAbstractions; +using System.Threading; +using FluentAssertions; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils.Tests +{ + public class GivenAnAppBaseCommandResolver + { + [Fact] + public void It_returns_null_when_CommandName_is_null() + { + var appBaseCommandResolver = SetupPlatformAppBaseCommandResolver(forceGeneric: true); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = null, + CommandArguments = null + }; + + var result = appBaseCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_returns_null_when_CommandName_does_not_exist_applocal() + { + var appBaseCommandResolver = SetupPlatformAppBaseCommandResolver(forceGeneric: true); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "nonexistent-command", + CommandArguments = null + }; + + var result = appBaseCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_returns_a_CommandSpec_with_CommandName_as_FileName_when_CommandName_exists_applocal() + { + var environment = CommandResolverTestUtils.SetupEnvironmentProviderWhichFindsExtensions(".exe"); + var appBaseCommandResolver = SetupPlatformAppBaseCommandResolver(environment, forceGeneric: true); + + CommandResolverTestUtils.CreateNonRunnableTestCommand(AppContext.BaseDirectory, "appbasetestcommand1", ".exe"); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "appbasetestcommand1", + CommandArguments = null + }; + + var result = appBaseCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + + var commandFile = Path.GetFileNameWithoutExtension(result.Path); + + commandFile.Should().Be("appbasetestcommand1"); + } + + [Fact] + public void It_returns_null_when_CommandName_exists_applocal_in_a_subdirectory() + { + var environment = CommandResolverTestUtils.SetupEnvironmentProviderWhichFindsExtensions(".exe"); + var appBaseCommandResolver = SetupPlatformAppBaseCommandResolver(environment, forceGeneric: true); + + var testDir = Path.Combine(AppContext.BaseDirectory, "appbasetestsubdir"); + CommandResolverTestUtils.CreateNonRunnableTestCommand(testDir, "appbasetestsubdircommand", ".exe"); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "appbasetestsubdircommand", + CommandArguments = null + }; + + var result = appBaseCommandResolver.Resolve(commandResolverArguments); + + result.Should().BeNull(); + } + + [Fact] + public void It_escapes_CommandArguments_when_returning_a_CommandSpec() + { + var environment = CommandResolverTestUtils.SetupEnvironmentProviderWhichFindsExtensions(".exe"); + var appBaseCommandResolver = SetupPlatformAppBaseCommandResolver(environment, forceGeneric: true); + + CommandResolverTestUtils.CreateNonRunnableTestCommand(AppContext.BaseDirectory, "appbasetestcommand1", ".exe"); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "appbasetestcommand1", + CommandArguments = new [] { "arg with space"} + }; + + var result = appBaseCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + result.Args.Should().Be("\"arg with space\""); + } + + [Fact] + public void It_returns_a_CommandSpec_with_Args_as_stringEmpty_when_returning_a_CommandSpec_and_CommandArguments_are_null() + { + var environment = CommandResolverTestUtils.SetupEnvironmentProviderWhichFindsExtensions(".exe"); + var appBaseCommandResolver = SetupPlatformAppBaseCommandResolver(environment, forceGeneric: true); + + CommandResolverTestUtils.CreateNonRunnableTestCommand(AppContext.BaseDirectory, "appbasetestcommand1", ".exe"); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "appbasetestcommand1", + CommandArguments = null + }; + + var result = appBaseCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + result.Args.Should().Be(string.Empty); + } + + [Fact] + public void It_prefers_EXE_over_CMD_when_two_command_candidates_exist_and_using_WindowsExePreferredCommandSpecFactory() + { + var environment = CommandResolverTestUtils.SetupEnvironmentProviderWhichFindsExtensions(".exe"); + var platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory(); + + var appBaseCommandResolver = new AppBaseCommandResolver(environment, platformCommandSpecFactory); + + CommandResolverTestUtils.CreateNonRunnableTestCommand(AppContext.BaseDirectory, "appbasetestcommand1", ".exe"); + CommandResolverTestUtils.CreateNonRunnableTestCommand(AppContext.BaseDirectory, "appbasetestcommand1", ".cmd"); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "appbasetestcommand1", + CommandArguments = null + }; + + var result = appBaseCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + + var commandFile = Path.GetFileName(result.Path); + commandFile.Should().Be("appbasetestcommand1.exe"); + } + + public void It_wraps_command_with_CMD_EXE_when_command_has_CMD_Extension_and_using_WindowsExePreferredCommandSpecFactory() + { + var environment = new EnvironmentProvider(new [] {".cmd"}); + var platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory(); + + var pathCommandResolver = new PathCommandResolver(environment, platformCommandSpecFactory); + + var testCommandPath = + CommandResolverTestUtils.CreateNonRunnableTestCommand(AppContext.BaseDirectory, "cmdWrapCommand", ".cmd"); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "cmdWrapCommand", + CommandArguments = null + }; + + var result = pathCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + + var commandFile = Path.GetFileName(result.Path); + commandFile.Should().Be("cmd.exe"); + + result.Args.Should().Contain(testCommandPath); + } + + private AppBaseCommandResolver SetupPlatformAppBaseCommandResolver( + IEnvironmentProvider environment = null, + bool forceGeneric = false) + { + environment = environment ?? new EnvironmentProvider(); + + IPlatformCommandSpecFactory platformCommandSpecFactory = new GenericPlatformCommandSpecFactory(); + + if (PlatformServices.Default.Runtime.OperatingSystemPlatform == Platform.Windows + && !forceGeneric) + { + platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory(); + } + + var appBaseCommandResolver = new AppBaseCommandResolver(environment, platformCommandSpecFactory); + + return appBaseCommandResolver; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/project.json b/test/Microsoft.DotNet.Cli.Utils.Tests/project.json index 4aae182ed..97536b3c7 100644 --- a/test/Microsoft.DotNet.Cli.Utils.Tests/project.json +++ b/test/Microsoft.DotNet.Cli.Utils.Tests/project.json @@ -13,6 +13,7 @@ "Microsoft.DotNet.Tools.Tests.Utilities": { "target": "project" }, + "moq.netcore": "4.4.0-beta8", "xunit": "2.1.0", "dotnet-test-xunit": "1.0.0-dev-79755-47" }, @@ -27,7 +28,11 @@ }, "content": [ - "../../TestAssets/TestProjects/OutputStandardOutputAndError/*" + "../../TestAssets/TestProjects/OutputStandardOutputAndError/*", + "../../TestAssets/TestProjects/TestAppWithArgs/*", + "../../TestAssets/TestProjects/AppWithDirectAndToolDependency/**/*", + "../../TestAssets/TestProjects/AppWithDirectDependency/**/*", + "../../TestAssets/TestProjects/AppWithToolDependency/**/*" ], "testRunner": "xunit" diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultAssertions.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultAssertions.cs index 38812e448..00516044b 100644 --- a/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultAssertions.cs +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultAssertions.cs @@ -32,6 +32,13 @@ namespace Microsoft.DotNet.Tools.Test.Utilities return new AndConstraint(this); } + public AndConstraint NotPass() + { + Execute.Assertion.ForCondition(_commandResult.ExitCode != 0) + .FailWith(AppendDiagnosticsTo($"Expected command to fail but it did not.")); + return new AndConstraint(this); + } + public AndConstraint Fail() { Execute.Assertion.ForCondition(_commandResult.ExitCode != 0) diff --git a/test/dotnet.Tests/PackagedCommandTests.cs b/test/dotnet.Tests/PackagedCommandTests.cs index c02b7b2b1..941d3f258 100644 --- a/test/dotnet.Tests/PackagedCommandTests.cs +++ b/test/dotnet.Tests/PackagedCommandTests.cs @@ -6,6 +6,7 @@ using System.IO; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Tools.Test.Utilities; using Xunit; +using FluentAssertions; namespace Microsoft.DotNet.Tests { @@ -20,11 +21,10 @@ namespace Microsoft.DotNet.Tests [Theory] [InlineData("AppWithDirectAndToolDependency")] - [InlineData("AppWithDirectDependency")] [InlineData("AppWithToolDependency")] - public void TestPackagedCommandDependency(string appName) + public void TestProjectToolIsAvailableThroughDriver(string appName) { - string appDirectory = Path.Combine(_testProjectsRoot, appName); + var appDirectory = Path.Combine(_testProjectsRoot, appName); new BuildCommand(Path.Combine(appDirectory, "project.json")) .Execute() @@ -38,7 +38,7 @@ namespace Microsoft.DotNet.Tests { CommandResult result = new HelloCommand().ExecuteWithCapturedOutput(); - result.Should().HaveStdOut("Hello" + Environment.NewLine); + result.Should().HaveStdOut("Hello World!" + Environment.NewLine); result.Should().NotHaveStdErr(); result.Should().Pass(); } @@ -48,6 +48,33 @@ namespace Microsoft.DotNet.Tests } } + [Fact] + public void TestProjectDependencyIsNotAvailableThroughDriver() + { + var appName = "AppWithDirectDependency"; + var appDirectory = Path.Combine(_testProjectsRoot, appName); + + new BuildCommand(Path.Combine(appDirectory, "project.json")) + .Execute() + .Should() + .Pass(); + + var currentDirectory = Directory.GetCurrentDirectory(); + Directory.SetCurrentDirectory(appDirectory); + + try + { + CommandResult result = new HelloCommand().ExecuteWithCapturedOutput(); + + result.StdOut.Should().Contain("No executable found matching command"); + result.Should().NotPass(); + } + finally + { + Directory.SetCurrentDirectory(currentDirectory); + } + } + class HelloCommand : TestCommand { public HelloCommand()