// 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.Diagnostics; using System.IO; using System.Runtime.InteropServices; using FluentAssertions; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.TestFramework; using Microsoft.DotNet.Tools.Test.Utilities; using Microsoft.DotNet.InternalAbstractions; using Xunit; using Xunit.Abstractions; using Microsoft.Build.Construction; using System.Linq; using Microsoft.Build.Evaluation; namespace Microsoft.DotNet.Tests { public class PackagedCommandTests : TestBase { private readonly ITestOutputHelper _output; public PackagedCommandTests(ITestOutputHelper output) { _output = output; } public static IEnumerable DependencyToolArguments { get { var rid = DotnetLegacyRuntimeIdentifiers.InferLegacyRestoreRuntimeIdentifier(); var projectOutputPath = $"AppWithProjTool2Fx\\bin\\Debug\\net451\\{rid}\\dotnet-desktop-and-portable.exe"; return new[] { new object[] { "CoreFX", ".NETCoreApp,Version=v1.0", "lib\\netcoreapp1.0\\dotnet-desktop-and-portable.dll" }, new object[] { "NetFX", ".NETFramework,Version=v4.5.1", projectOutputPath } }; } } public static IEnumerable LibraryDependencyToolArguments { get { var rid = DotnetLegacyRuntimeIdentifiers.InferLegacyRestoreRuntimeIdentifier(); var projectOutputPath = $"LibWithProjTool2Fx\\bin\\Debug\\net451\\dotnet-desktop-and-portable.exe"; return new[] { new object[] { "CoreFX", ".NETStandard,Version=v1.6", "lib\\netstandard1.6\\dotnet-desktop-and-portable.dll" }, new object[] { "NetFX", ".NETFramework,Version=v4.5.1", projectOutputPath } }; } } [Theory] [InlineData("AppWithDirectAndToolDep", true)] [InlineData("AppWithToolDependency", false)] public void TestProjectToolIsAvailableThroughDriver(string appName, bool useCurrentFrameworkRuntimeVersion) { var testInstance = TestAssets.Get(appName) .CreateInstance() .WithSourceFiles() .WithNuGetConfig(new RepoDirectoriesProvider().TestPackages); if (useCurrentFrameworkRuntimeVersion) { testInstance = testInstance.UseCurrentRuntimeFrameworkVersion(); } // restore again now that the project has changed new RestoreCommand() .WithWorkingDirectory(testInstance.Root) .Execute() .Should().Pass(); new BuildCommand() .WithProjectDirectory(testInstance.Root) .Execute() .Should().Pass(); new PortableCommand() .WithWorkingDirectory(testInstance.Root) .ExecuteWithCapturedOutput() .Should().HaveStdOutContaining("Hello Portable World!") .And.NotHaveStdErr() .And.Pass(); } [Fact] public void CanInvokeToolWhosePackageNameIsDifferentFromDllName() { var testInstance = TestAssets.Get("AppWithDepOnToolWithOutputName") .CreateInstance() .WithSourceFiles() .WithRestoreFiles(); new BuildCommand() .WithProjectDirectory(testInstance.Root) .Execute() .Should().Pass(); new GenericCommand("tool-with-output-name") .WithWorkingDirectory(testInstance.Root) .ExecuteWithCapturedOutput() .Should().HaveStdOutContaining("Tool with output name!") .And.NotHaveStdErr() .And.Pass(); } [Fact] public void CanInvokeToolFromDirectDependenciesIfPackageNameDifferentFromToolName() { var testInstance = TestAssets.Get("AppWithDirectDepWithOutputName") .CreateInstance() .WithSourceFiles() .WithRestoreFiles(); const string framework = ".NETCoreApp,Version=v2.0"; new BuildCommand() .WithProjectDirectory(testInstance.Root) .WithConfiguration("Debug") .Execute() .Should().Pass(); new DependencyToolInvokerCommand() .WithWorkingDirectory(testInstance.Root) .WithEnvironmentVariable(CommandContext.Variables.Verbose, "true") .ExecuteWithCapturedOutput($"tool-with-output-name", framework, "") .Should().HaveStdOutContaining("Tool with output name!") .And.NotHaveStdErr() .And.Pass(); } [Fact] public void ItShowsErrorWhenToolIsNotRestored() { var testInstance = TestAssets.Get("NonRestoredTestProjects", "AppWithNonExistingToolDependency") .CreateInstance() .WithSourceFiles(); new TestCommand("dotnet") .WithWorkingDirectory(testInstance.Root) .ExecuteWithCapturedOutput("nonexistingtool") .Should().Fail() .And.HaveStdErrContaining("No executable found matching command \"dotnet-nonexistingtool\""); } [Fact] public void ItRunsToolRestoredToSpecificPackageDir() { var testInstance = TestAssets.Get("NonRestoredTestProjects", "ToolWithRandomPackageName") .CreateInstance() .WithSourceFiles(); var appWithDepOnToolDir = testInstance.Root.Sub("AppWithDepOnTool"); var toolWithRandPkgNameDir = testInstance.Root.Sub("ToolWithRandomPackageName"); var pkgsDir = testInstance.Root.CreateSubdirectory("pkgs"); // 3ebdd4f1-a194-470a-b01a-4515672791d1 // ^-- index = 24 string randomPackageName = Guid.NewGuid().ToString().Substring(24); // TODO: This is a workround for https://github.com/dotnet/cli/issues/5020 SetGeneratedPackageName(appWithDepOnToolDir.GetFile("AppWithDepOnTool.csproj"), randomPackageName); SetGeneratedPackageName(toolWithRandPkgNameDir.GetFile("ToolWithRandomPackageName.csproj"), randomPackageName); new RestoreCommand() .WithWorkingDirectory(toolWithRandPkgNameDir) .Execute() .Should().Pass(); new PackCommand() .WithWorkingDirectory(toolWithRandPkgNameDir) .Execute($"-o \"{pkgsDir.FullName}\"") .Should().Pass(); new RestoreCommand() .WithWorkingDirectory(appWithDepOnToolDir) .Execute($"--packages \"{pkgsDir.FullName}\"") .Should().Pass(); new TestCommand("dotnet") .WithWorkingDirectory(appWithDepOnToolDir) .ExecuteWithCapturedOutput("randompackage") .Should().Pass() .And.HaveStdOutContaining("Hello World from tool!") .And.NotHaveStdErr(); } [WindowsOnlyTheory(Skip="https://github.com/dotnet/cli/issues/4514")] [MemberData("DependencyToolArguments")] public void TestFrameworkSpecificDependencyToolsCanBeInvoked(string identifier, string framework, string expectedDependencyToolPath) { var testInstance = TestAssets.Get(TestAssetKinds.DesktopTestProjects, "AppWithProjTool2Fx") .CreateInstance(identifier: identifier) .WithSourceFiles() .WithRestoreFiles(); new BuildCommand() .WithWorkingDirectory(testInstance.Root) .WithConfiguration("Debug") .Execute() .Should().Pass(); new DependencyToolInvokerCommand() .WithWorkingDirectory(testInstance.Root) .ExecuteWithCapturedOutput($"desktop-and-portable {framework} {identifier}") .Should().HaveStdOutContaining(framework) .And.HaveStdOutContaining(identifier) .And.HaveStdOutContaining(expectedDependencyToolPath) .And.NotHaveStdErr() .And.Pass(); } [WindowsOnlyTheory] [MemberData("LibraryDependencyToolArguments")] public void TestFrameworkSpecificLibraryDependencyToolsCannotBeInvoked(string identifier, string framework, string expectedDependencyToolPath) { var testInstance = TestAssets.Get(TestAssetKinds.DesktopTestProjects, "LibWithProjTool2Fx") .CreateInstance(identifier: identifier) .WithSourceFiles() .WithRestoreFiles(); new BuildCommand() .WithWorkingDirectory(testInstance.Root) .WithConfiguration("Debug") .Execute() .Should().Pass(); new DependencyToolInvokerCommand() .WithWorkingDirectory(testInstance.Root) .ExecuteWithCapturedOutput($"desktop-and-portable {framework} {identifier}") .Should().Fail(); } [Fact] public void ToolsCanAccessDependencyContextProperly() { var testInstance = TestAssets.Get("DependencyContextFromTool") .CreateInstance() .WithSourceFiles() .WithRestoreFiles(); new DependencyContextTestCommand() .WithWorkingDirectory(testInstance.Root) .Execute("") .Should().Pass(); } [Fact] public void TestProjectDependencyIsNotAvailableThroughDriver() { var testInstance = TestAssets.Get("AppWithDirectDep") .CreateInstance() .WithSourceFiles() .UseCurrentRuntimeFrameworkVersion() .WithNuGetConfig(new RepoDirectoriesProvider().TestPackages); // restore again now that the project has changed new RestoreCommand() .WithWorkingDirectory(testInstance.Root) .Execute() .Should().Pass(); new BuildCommand() .WithWorkingDirectory(testInstance.Root) .Execute() .Should().Pass(); var currentDirectory = Directory.GetCurrentDirectory(); CommandResult result = new HelloCommand() .WithWorkingDirectory(testInstance.Root) .ExecuteWithCapturedOutput(); result.StdErr.Should().Contain("No executable found matching command"); result.Should().Fail(); } [Fact] public void WhenToolAssetsFileIsInUseThenCLIRetriesLaunchingTheCommandForAtLeastOneSecond() { var testInstance = TestAssets.Get("AppWithToolDependency") .CreateInstance() .WithSourceFiles() .WithRestoreFiles(); var assetsFile = new DirectoryInfo(new RepoDirectoriesProvider().NugetPackages) .GetDirectory(".tools", "dotnet-portable", "1.0.0", "netcoreapp1.0") .GetFile("project.assets.json"); var stopWatch = Stopwatch.StartNew(); using (assetsFile.Lock() .DisposeAfter(TimeSpan.FromMilliseconds(1000))) { new PortableCommand() .WithWorkingDirectory(testInstance.Root) .ExecuteWithCapturedOutput() .Should().HaveStdOutContaining("Hello Portable World!") .And.NotHaveStdErr() .And.Pass(); } stopWatch.Stop(); stopWatch.ElapsedMilliseconds.Should().BeGreaterThan(1000, "Because dotnet should respect the NuGet lock"); } [Fact] public void WhenToolAssetsFileIsLockedByNuGetThenCLIRetriesLaunchingTheCommandForAtLeastOneSecond() { var testInstance = TestAssets.Get("AppWithToolDependency") .CreateInstance() .WithSourceFiles() .WithRestoreFiles(); var assetsFile = new DirectoryInfo(new RepoDirectoriesProvider().NugetPackages) .GetDirectory(".tools", "dotnet-portable", "1.0.0", "netcoreapp1.0") .GetFile("project.assets.json"); var stopWatch = Stopwatch.StartNew(); using (assetsFile.NuGetLock() .DisposeAfter(TimeSpan.FromMilliseconds(1000))) { new PortableCommand() .WithWorkingDirectory(testInstance.Root) .ExecuteWithCapturedOutput() .Should().HaveStdOutContaining("Hello Portable World!") .And.NotHaveStdErr() .And.Pass(); } stopWatch.Stop(); stopWatch.ElapsedMilliseconds.Should().BeGreaterThan(1000, "Because dotnet should respect the NuGet lock"); } private void SetGeneratedPackageName(FileInfo project, string packageName) { const string propertyName = "GeneratedPackageId"; var p = ProjectRootElement.Open(project.FullName, new ProjectCollection(), true); p.AddProperty(propertyName, packageName); p.Save(); } class HelloCommand : TestCommand { public HelloCommand() : base("dotnet") { } public override CommandResult Execute(string args = "") { args = $"hello {args}"; return base.Execute(args); } public override CommandResult ExecuteWithCapturedOutput(string args = "") { args = $"hello {args}"; return base.ExecuteWithCapturedOutput(args); } } class PortableCommand : TestCommand { public PortableCommand() : base("dotnet") { } public override CommandResult Execute(string args = "") { args = $"portable {args}"; return base.Execute(args); } public override CommandResult ExecuteWithCapturedOutput(string args = "") { args = $"portable {args}"; return base.ExecuteWithCapturedOutput(args); } } class GenericCommand : TestCommand { private readonly string _commandName; public GenericCommand(string commandName) : base("dotnet") { _commandName = commandName; } public override CommandResult Execute(string args = "") { args = $"{_commandName} {args}"; return base.Execute(args); } public override CommandResult ExecuteWithCapturedOutput(string args = "") { args = $"{_commandName} {args}"; return base.ExecuteWithCapturedOutput(args); } } class DependencyContextTestCommand : TestCommand { public DependencyContextTestCommand() : base("dotnet") { } public override CommandResult Execute(string path) { var args = $"dependency-context-test {path}"; return base.Execute(args); } public override CommandResult ExecuteWithCapturedOutput(string path) { var args = $"dependency-context-test {path}"; return base.ExecuteWithCapturedOutput(args); } } } }