|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable disable
using System.CommandLine;
using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.Commands;
using Microsoft.DotNet.Cli.Commands.Tool.Install;
using Microsoft.DotNet.Cli.ToolManifest;
using Microsoft.DotNet.Cli.ToolPackage;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
using Microsoft.DotNet.Tools.Tests.ComponentMocks;
using Microsoft.Extensions.DependencyModel.Tests;
using Microsoft.Extensions.EnvironmentAbstractions;
using NuGet.Frameworks;
using NuGet.Versioning;
using Parser = Microsoft.DotNet.Cli.Parser;
namespace Microsoft.DotNet.Tests.Commands.Tool
{
public class ToolInstallLocalCommandTests : SdkTest
{
private readonly IFileSystem _fileSystem;
private readonly IToolPackageStore _toolPackageStore;
private readonly ToolPackageDownloaderMock _toolPackageDownloaderMock;
private readonly ParseResult _parseResult;
private readonly BufferedReporter _reporter;
private readonly string _temporaryDirectory;
private readonly string _pathToPlacePackages;
private readonly ILocalToolsResolverCache _localToolsResolverCache;
private readonly string _manifestFilePath;
private readonly PackageId _packageIdA = new("local.tool.console.a");
private readonly NuGetVersion _packageVersionA;
private readonly NuGetVersion _packageNewVersionA;
private readonly ToolCommandName _toolCommandNameA = new("a");
private readonly ToolManifestFinder _toolManifestFinder;
private readonly ToolManifestEditor _toolManifestEditor;
public ToolInstallLocalCommandTests(ITestOutputHelper log) : base(log)
{
_packageVersionA = NuGetVersion.Parse("1.0.4");
_packageNewVersionA = NuGetVersion.Parse("2.0.0");
_reporter = new BufferedReporter();
_fileSystem = new FileSystemMockBuilder().UseCurrentSystemTemporaryDirectory().Build();
_temporaryDirectory = _fileSystem.Directory.CreateTemporaryDirectory().DirectoryPath;
_pathToPlacePackages = Path.Combine(_temporaryDirectory, "pathToPlacePackage");
ToolPackageStoreMock toolPackageStoreMock =
new(new DirectoryPath(_pathToPlacePackages), _fileSystem);
_toolPackageStore = toolPackageStoreMock;
_toolPackageDownloaderMock = new ToolPackageDownloaderMock(
store: _toolPackageStore,
fileSystem: _fileSystem,
reporter: _reporter,
new List<MockFeed>
{
new MockFeed
{
Type = MockFeedType.ImplicitAdditionalFeed,
Packages = new List<MockFeedPackage>
{
new MockFeedPackage
{
PackageId = _packageIdA.ToString(),
Version = _packageVersionA.ToNormalizedString(),
ToolCommandName = _toolCommandNameA.ToString()
}
}
}
});
_localToolsResolverCache
= new LocalToolsResolverCache(
_fileSystem,
new DirectoryPath(Path.Combine(_temporaryDirectory, "cache")),
1);
_manifestFilePath = Path.Combine(_temporaryDirectory, "dotnet-tools.json");
_fileSystem.File.WriteAllText(Path.Combine(_temporaryDirectory, _manifestFilePath), _jsonContent);
_toolManifestFinder = new ToolManifestFinder(new DirectoryPath(_temporaryDirectory), _fileSystem, new FakeDangerousFileDetector());
_toolManifestEditor = new ToolManifestEditor(_fileSystem);
_parseResult = Parser.Parse($"dotnet tool install {_packageIdA.ToString()}");
_localToolsResolverCache
= new LocalToolsResolverCache(
_fileSystem,
new DirectoryPath(Path.Combine(_temporaryDirectory, "cache")),
1);
}
[Fact]
public void WhenPassingRestoreActionConfigOptions()
{
var parseResult = Parser.Parse($"dotnet tool install {_packageIdA.ToString()} --ignore-failed-sources");
var toolInstallCommand = new ToolInstallLocalCommand(parseResult);
toolInstallCommand.restoreActionConfig.IgnoreFailedSources.Should().BeTrue();
}
[Fact]
public void WhenPassingIgnoreFailedSourcesItShouldNotThrow()
{
_fileSystem.File.WriteAllText(Path.Combine(_temporaryDirectory, "nuget.config"), _nugetConfigWithInvalidSources);
var parseResult = Parser.Parse($"dotnet tool install {_packageIdA.ToString()} --ignore-failed-sources");
var toolInstallCommand = new ToolInstallLocalCommand(parseResult,
_packageIdA,
_toolPackageDownloaderMock,
_toolManifestFinder,
_toolManifestEditor,
_localToolsResolverCache,
_reporter);
toolInstallCommand.Execute().Should().Be(0);
_fileSystem.File.Delete(Path.Combine(_temporaryDirectory, "nuget.config"));
}
[Fact]
public void WhenRunWithPackageIdItShouldSaveToCacheAndAddToManifestFile()
{
var toolInstallLocalCommand = GetDefaultTestToolInstallLocalCommand();
toolInstallLocalCommand.Execute().Should().Be(0);
AssertDefaultInstallSuccess();
}
[Fact]
public void GivenCreateManifestIfNeededWithoutArgumentTheDefaultIsTrueForLegacyBehavior()
{
_fileSystem.File.Delete(_manifestFilePath);
ParseResult parseResult =
Parser.Parse(
$"dotnet tool install {_packageIdA.ToString()} --create-manifest-if-needed");
var toolInstallLocalCommand = new ToolInstallLocalCommand(
parseResult,
_packageIdA,
_toolPackageDownloaderMock,
_toolManifestFinder,
_toolManifestEditor,
_localToolsResolverCache,
_reporter);
toolInstallLocalCommand.Execute().Should().Be(0);
}
[Fact]
public void GivenNoManifestFileItShouldThrowAndContainNoManifestGuide()
{
_fileSystem.File.Delete(_manifestFilePath);
ParseResult parseResult =
Parser.Parse(
$"dotnet tool install {_packageIdA.ToString()} --create-manifest-if-needed false");
var toolInstallLocalCommand = new ToolInstallLocalCommand(
parseResult,
_packageIdA,
_toolPackageDownloaderMock,
_toolManifestFinder,
_toolManifestEditor,
_localToolsResolverCache,
_reporter);
Action a = () => toolInstallLocalCommand.Execute();
a.Should().Throw<GracefulException>()
.And.Message.Should()
.Contain(string.Format(CliStrings.CannotFindAManifestFile, ""));
}
[Fact]
public void WhenRunWithExplicitManifestFileItShouldAddEntryToExplicitManifestFile()
{
var explicitManifestFilePath = Path.Combine(_temporaryDirectory, "subdirectory", "dotnet-tools.json");
_fileSystem.File.Delete(_manifestFilePath);
_fileSystem.Directory.CreateDirectory(Path.Combine(_temporaryDirectory, "subdirectory"));
_fileSystem.File.WriteAllText(explicitManifestFilePath, _jsonContent);
ParseResult parseResult =
Parser.Parse(
$"dotnet tool install {_packageIdA.ToString()} --tool-manifest {explicitManifestFilePath}");
var installLocalCommand = new ToolInstallLocalCommand(
parseResult,
_packageIdA,
_toolPackageDownloaderMock,
_toolManifestFinder,
_toolManifestEditor,
_localToolsResolverCache,
_reporter);
installLocalCommand.Execute().Should().Be(0);
_toolManifestFinder.Find(new FilePath(explicitManifestFilePath)).Should().HaveCount(1);
}
[Fact]
public void WhenRunWithRollForwardItShouldRollForwardToTrueInManifestFile()
{
ParseResult parseResult =
Parser.Parse(
$"dotnet tool install {_packageIdA.ToString()} --allow-roll-forward");
var installLocalCommand = new ToolInstallLocalCommand(
parseResult,
_packageIdA,
_toolPackageDownloaderMock,
_toolManifestFinder,
_toolManifestEditor,
_localToolsResolverCache,
_reporter);
installLocalCommand.Execute().Should().Be(0);
_fileSystem.File.ReadAllText(_manifestFilePath).Should()
.Contain("\"rollForward\": true");
}
[Fact]
public void WhenRunWithoutRollForwardItShouldDefaultRollForwardToFalseInManifestFile()
{
ParseResult parseResult =
Parser.Parse(
$"dotnet tool install {_packageIdA.ToString()}");
var installLocalCommand = new ToolInstallLocalCommand(
parseResult,
_packageIdA,
_toolPackageDownloaderMock,
_toolManifestFinder,
_toolManifestEditor,
_localToolsResolverCache,
_reporter);
installLocalCommand.Execute().Should().Be(0);
_fileSystem.File.ReadAllText(_manifestFilePath).Should()
.Contain("\"rollForward\": false");
}
[Fact]
public void WhenRunFromToolInstallRedirectCommandWithPackageIdItShouldSaveToCacheAndAddToManifestFile()
{
var toolInstallLocalCommand = GetDefaultTestToolInstallLocalCommand();
var toolInstallCommand = new ToolInstallCommand(
_parseResult,
toolInstallLocalCommand: toolInstallLocalCommand);
toolInstallCommand.Execute().Should().Be(0);
AssertDefaultInstallSuccess();
}
[Fact]
public void WhenRunWithPackageIdItShouldShowSuccessMessage()
{
var toolInstallLocalCommand = GetDefaultTestToolInstallLocalCommand();
toolInstallLocalCommand.Execute().Should().Be(0);
_reporter.Lines[0].Should()
.Contain(
string.Format(CliCommandStrings.LocalToolInstallationSucceeded,
_toolCommandNameA.ToString(),
_packageIdA,
_packageVersionA.ToNormalizedString(),
_manifestFilePath).Green());
}
[Fact]
public void GivenFailedPackageInstallWhenRunWithPackageIdItShouldNotChangeManifestFile()
{
ParseResult result = Parser.Parse($"dotnet tool install non-exist");
var installLocalCommand = new ToolInstallLocalCommand(
result,
new PackageId("non-exist"),
_toolPackageDownloaderMock,
_toolManifestFinder,
_toolManifestEditor,
_localToolsResolverCache,
_reporter);
Action a = () => installLocalCommand.Execute();
a.Should().Throw<GracefulException>()
.And.Message.Should()
.Contain(CliCommandStrings.ToolInstallationRestoreFailed);
_fileSystem.File.ReadAllText(_manifestFilePath).Should()
.Be(_jsonContent, "Manifest file should not be changed");
}
[Fact]
public void GivenManifestFileConflictItShouldNotAddToCache()
{
_toolManifestEditor.Add(
new FilePath(_manifestFilePath),
_packageIdA,
new NuGetVersion(1, 1, 1),
new[] { _toolCommandNameA });
var toolInstallLocalCommand = GetDefaultTestToolInstallLocalCommand();
Action a = () => toolInstallLocalCommand.Execute();
a.Should().Throw<GracefulException>();
_localToolsResolverCache.TryLoad(new RestoredCommandIdentifier(
_packageIdA,
_packageVersionA,
NuGetFramework.Parse(BundledTargetFramework.GetTargetFrameworkMoniker()),
Constants.AnyRid,
_toolCommandNameA),
out ToolCommand restoredCommand
).Should().BeFalse("it should not add to cache if add to manifest failed. " +
"But restore do not need to 'revert' since it just set in nuget global directory");
}
[Fact]
public void WhenRunWithExistingManifestInConfigDirectoryItShouldAddToExistingManifest()
{
// Test backward compatibility: ensure tools can be added to existing manifests in .config directories
_fileSystem.File.Delete(_manifestFilePath);
var configDirectory = Path.Combine(_temporaryDirectory, ".config");
_fileSystem.Directory.CreateDirectory(configDirectory);
var configManifestPath = Path.Combine(configDirectory, "dotnet-tools.json");
_fileSystem.File.WriteAllText(configManifestPath, _jsonContent);
var toolInstallLocalCommand = GetDefaultTestToolInstallLocalCommand();
toolInstallLocalCommand.Execute().Should().Be(0);
// Verify the tool was added to the existing .config manifest
var manifestPackages = _toolManifestFinder.Find();
manifestPackages.Should().HaveCount(1);
manifestPackages.First().PackageId.Should().Be(_packageIdA);
// Verify that the manifest under the .config folder has been updated
_fileSystem.File.Exists(configManifestPath).Should().BeTrue("The .config manifest file should exist");
var configManifestContent = _fileSystem.File.ReadAllText(configManifestPath);
configManifestContent.Should().Contain(_packageIdA.ToString(), "The .config manifest should contain the installed tool");
configManifestContent.Should().NotBe(_jsonContent, "The .config manifest should have been updated with the new tool");
// Verify that no manifest exists in the root folder after the install command is run
_fileSystem.File.Exists(_manifestFilePath).Should().BeFalse("No manifest should exist in the root folder");
}
private ToolInstallLocalCommand GetDefaultTestToolInstallLocalCommand()
{
return new ToolInstallLocalCommand(
_parseResult,
_packageIdA,
_toolPackageDownloaderMock,
_toolManifestFinder,
_toolManifestEditor,
_localToolsResolverCache,
_reporter);
}
[Fact]
public void WhenRunWithExactVersionItShouldSucceed()
{
ParseResult result = Parser.Parse(
$"dotnet tool install {_packageIdA.ToString()} --version {_packageVersionA.ToNormalizedString()}");
var installLocalCommand = new ToolInstallLocalCommand(
result,
_packageIdA,
_toolPackageDownloaderMock,
_toolManifestFinder,
_toolManifestEditor,
_localToolsResolverCache,
_reporter);
installLocalCommand.Execute().Should().Be(0);
AssertDefaultInstallSuccess();
}
[Fact]
public void WhenRunWithValidVersionRangeItShouldSucceed()
{
ParseResult result = Parser.Parse(
$"dotnet tool install {_packageIdA.ToString()} --version 1.*");
var installLocalCommand = new ToolInstallLocalCommand(
result,
_packageIdA,
_toolPackageDownloaderMock,
_toolManifestFinder,
_toolManifestEditor,
_localToolsResolverCache,
_reporter);
installLocalCommand.Execute().Should().Be(0);
AssertDefaultInstallSuccess();
}
[Fact]
public void WhenRunWithPrereleaseAndPackageVersionItShouldSucceed()
{
ParseResult result =
Parser.Parse($"dotnet tool install {_packageIdA.ToString()} --prerelease");
var installLocalCommand = new ToolInstallLocalCommand(
result,
_packageIdA,
GetToolToolPackageInstallerWithPreviewInFeed(),
_toolManifestFinder,
_toolManifestEditor,
_localToolsResolverCache,
_reporter);
installLocalCommand.Execute().Should().Be(0);
var manifestPackages = _toolManifestFinder.Find();
manifestPackages.Should().HaveCount(1);
var addedPackage = manifestPackages.Single();
_localToolsResolverCache.TryLoad(new RestoredCommandIdentifier(
addedPackage.PackageId,
new NuGetVersion("2.0.1-preview1"),
NuGetFramework.Parse(BundledTargetFramework.GetTargetFrameworkMoniker()),
Constants.AnyRid,
addedPackage.CommandNames.Single()),
out ToolCommand restoredCommand
).Should().BeTrue();
_fileSystem.File.Exists(restoredCommand.Executable.Value);
}
[Fact]
public void GivenNoManifestFileAndCreateManifestIfNeededFlagItShouldCreateManifestInGit()
{
_fileSystem.Directory.CreateDirectory(Path.Combine(_temporaryDirectory, ".git"));
_fileSystem.File.Delete(_manifestFilePath);
var currentFolder = Path.Combine(_temporaryDirectory, "subdirectory1", "subdirectory2");
_fileSystem.Directory.CreateDirectory(currentFolder);
ParseResult parseResult =
Parser.Parse(
$"dotnet tool install {_packageIdA.ToString()} --create-manifest-if-needed");
var installLocalCommand = new ToolInstallLocalCommand(
parseResult,
_packageIdA,
_toolPackageDownloaderMock,
_toolManifestFinder,
_toolManifestEditor,
_localToolsResolverCache,
_reporter);
installLocalCommand.Execute().Should().Be(0);
_fileSystem.File.Exists(Path.Combine(_temporaryDirectory, "dotnet-tools.json")).Should().BeTrue();
}
[Fact]
public void GivenNoManifestFileItUsesCreateManifestIfNeededByDefault()
{
_fileSystem.File.Delete(_manifestFilePath);
ParseResult parseResult =
Parser.Parse(
$"dotnet tool install {_packageIdA.ToString()}");
var installLocalCommand = new ToolInstallLocalCommand(
parseResult,
_packageIdA,
_toolPackageDownloaderMock,
_toolManifestFinder,
_toolManifestEditor,
_localToolsResolverCache,
_reporter);
installLocalCommand.Execute().Should().Be(0);
_fileSystem.File.Exists(Path.Combine(_temporaryDirectory, "dotnet-tools.json")).Should().BeTrue();
}
[Fact]
public void GivenNoManifestFileAndCreateManifestIfNeededFlagItShouldCreateManifestInSln()
{
_fileSystem.Directory.CreateDirectory(Path.Combine(_temporaryDirectory, "test1.sln"));
_fileSystem.File.Delete(_manifestFilePath);
var currentFolder = Path.Combine(_temporaryDirectory, "subdirectory1", "subdirectory2");
_fileSystem.Directory.CreateDirectory(currentFolder);
ParseResult parseResult =
Parser.Parse(
$"dotnet tool install {_packageIdA.ToString()} --create-manifest-if-needed");
var installLocalCommand = new ToolInstallLocalCommand(
parseResult,
_packageIdA,
_toolPackageDownloaderMock,
_toolManifestFinder,
_toolManifestEditor,
_localToolsResolverCache,
_reporter);
installLocalCommand.Execute().Should().Be(0);
_fileSystem.File.Exists(Path.Combine(_temporaryDirectory, "dotnet-tools.json")).Should().BeTrue();
}
[Fact]
public void GivenNoManifestFileAndCreateManifestIfNeededFlagItShouldCreateManifestInCurrentFolder()
{
_fileSystem.File.Delete(_manifestFilePath);
ParseResult parseResult =
Parser.Parse(
$"dotnet tool install {_packageIdA.ToString()} --create-manifest-if-needed");
var installLocalCommand = new ToolInstallLocalCommand(
parseResult,
_packageIdA,
_toolPackageDownloaderMock,
_toolManifestFinder,
_toolManifestEditor,
_localToolsResolverCache,
_reporter);
installLocalCommand.Execute().Should().Be(0);
_fileSystem.File.Exists(Path.Combine(_temporaryDirectory, "dotnet-tools.json")).Should().BeTrue();
}
private IToolPackageDownloader GetToolToolPackageInstallerWithPreviewInFeed()
{
List<MockFeed> feeds = new()
{
new MockFeed
{
Type = MockFeedType.ImplicitAdditionalFeed,
Packages = new List<MockFeedPackage>
{
new MockFeedPackage
{
PackageId = _packageIdA.ToString(),
Version = "1.0.4",
ToolCommandName = "SimulatorCommand"
},
new MockFeedPackage
{
PackageId = _packageIdA.ToString(),
Version = "2.0.1-preview1",
ToolCommandName = "SimulatorCommand"
}
}
}
};
var toolToolPackageDownloader = (IToolPackageDownloader)new ToolPackageDownloaderMock(
fileSystem: _fileSystem,
store: _toolPackageStore,
reporter: _reporter,
feeds: feeds,
downloadCallback: null);
return toolToolPackageDownloader;
}
private void AssertDefaultInstallSuccess()
{
var manifestPackages = _toolManifestFinder.Find();
manifestPackages.Should().HaveCount(1);
var addedPackage = manifestPackages.Single();
_localToolsResolverCache.TryLoad(new RestoredCommandIdentifier(
addedPackage.PackageId,
addedPackage.Version,
NuGetFramework.Parse(BundledTargetFramework.GetTargetFrameworkMoniker()),
Constants.AnyRid,
addedPackage.CommandNames.Single()),
out ToolCommand restoredCommand
).Should().BeTrue();
_fileSystem.File.Exists(restoredCommand.Executable.Value);
}
private string _jsonContent =
@"{
""version"":1,
""isRoot"":true,
""tools"":{
}
}";
private string _nugetConfigWithInvalidSources = @"{
<?xml version=""1.0"" encoding=""utf-8""?>
<configuration>
<packageSources>
<add key=""nuget"" value=""https://api.nuget.org/v3/index.json"" />
<add key=""invalid_source"" value=""https://api.nuget.org/v3/invalid.json"" />
</packageSources>
</configuration>
}";
}
}
|