Atomic install tool (#8518)

* Make dotnet install tool atomic

Apply TransactionScope to tool install. It can handle the correct timing
of roll back and commit.

Convert existing ToolPackageObtainer and ShellShimMaker by passing logic
via lambda to an object that has IEnlistmentNotification interface. It
turns out the very clean.

Use .stage as staging place to verify of package content, and shim. It
should roll back when something is wrong. When there is ctrl-c, there
will be garbage in .stage folder but not the root of the package folder.
This commit is contained in:
William Lee 2018-02-06 13:38:06 -08:00 committed by GitHub
parent 38e452204c
commit 5fa558a2ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 995 additions and 207 deletions

View file

@ -47,5 +47,10 @@ namespace Microsoft.Extensions.EnvironmentAbstractions
{ {
Directory.CreateDirectory(path); Directory.CreateDirectory(path);
} }
public void Delete(string path, bool recursive)
{
Directory.Delete(path, recursive);
}
} }
} }

View file

@ -45,5 +45,10 @@ namespace Microsoft.Extensions.EnvironmentAbstractions
{ {
File.WriteAllText(path, content); File.WriteAllText(path, content);
} }
public void Delete(string path)
{
File.Delete(path);
}
} }
} }

View file

@ -16,5 +16,7 @@ namespace Microsoft.Extensions.EnvironmentAbstractions
string GetDirectoryFullName(string path); string GetDirectoryFullName(string path);
void CreateDirectory(string path); void CreateDirectory(string path);
void Delete(string path, bool recursive);
} }
} }

View file

@ -24,5 +24,7 @@ namespace Microsoft.Extensions.EnvironmentAbstractions
void CreateEmptyFile(string path); void CreateEmptyFile(string path);
void WriteAllText(string path, string content); void WriteAllText(string path, string content);
void Delete(string path);
} }
} }

View file

@ -588,4 +588,7 @@ Output: {1}</value>
<data name="ToolPackageMissingSettingsFile" xml:space="preserve"> <data name="ToolPackageMissingSettingsFile" xml:space="preserve">
<value>Package '{0}' is missing tool settings file DotnetToolSettings.xml.</value> <value>Package '{0}' is missing tool settings file DotnetToolSettings.xml.</value>
</data> </data>
<data name="ToolPackageConflictPackageId" xml:space="preserve">
<value>Tool '{0}' is already installed.</value>
</data>
</root> </root>

View file

@ -0,0 +1,52 @@
// 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.Transactions;
using Microsoft.Extensions.EnvironmentAbstractions;
namespace Microsoft.DotNet.ShellShim
{
internal class CreateShimTransaction : IEnlistmentNotification
{
private readonly Action<List<FilePath>> _createShim;
private readonly Action<List<FilePath>> _rollback;
private List<FilePath> _locationOfShimDuringTransaction = new List<FilePath>();
public CreateShimTransaction(
Action<List<FilePath>> createShim,
Action<List<FilePath>> rollback)
{
_createShim = createShim ?? throw new ArgumentNullException(nameof(createShim));
_rollback = rollback ?? throw new ArgumentNullException(nameof(rollback));
}
public void CreateShim()
{
_createShim(_locationOfShimDuringTransaction);
}
public void Commit(Enlistment enlistment)
{
enlistment.Done();
}
public void InDoubt(Enlistment enlistment)
{
Rollback(enlistment);
}
public void Prepare(PreparingEnlistment preparingEnlistment)
{
preparingEnlistment.Done();
}
public void Rollback(Enlistment enlistment)
{
_rollback(_locationOfShimDuringTransaction);
enlistment.Done();
}
}
}

View file

@ -2,10 +2,12 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information. // Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Transactions;
using System.Xml.Linq; using System.Xml.Linq;
using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Tools; using Microsoft.DotNet.Tools;
@ -22,11 +24,38 @@ namespace Microsoft.DotNet.ShellShim
public ShellShimMaker(string pathToPlaceShim) public ShellShimMaker(string pathToPlaceShim)
{ {
_pathToPlaceShim = _pathToPlaceShim = pathToPlaceShim ?? throw new ArgumentNullException(nameof(pathToPlaceShim));
pathToPlaceShim ?? throw new ArgumentNullException(nameof(pathToPlaceShim));
} }
public void CreateShim(FilePath packageExecutable, string shellCommandName) public void CreateShim(FilePath packageExecutable, string shellCommandName)
{
var createShimTransaction = new CreateShimTransaction(
createShim: locationOfShimDuringTransaction =>
{
EnsureCommandNameUniqueness(shellCommandName);
PlaceShim(packageExecutable, shellCommandName, locationOfShimDuringTransaction);
},
rollback: locationOfShimDuringTransaction =>
{
foreach (FilePath f in locationOfShimDuringTransaction)
{
if (File.Exists(f.Value))
{
File.Delete(f.Value);
}
}
});
using (var transactionScope = new TransactionScope())
{
Transaction.Current.EnlistVolatile(createShimTransaction, EnlistmentOptions.None);
createShimTransaction.CreateShim();
transactionScope.Complete();
}
}
private void PlaceShim(FilePath packageExecutable, string shellCommandName, List<FilePath> locationOfShimDuringTransaction)
{ {
FilePath shimPath = GetShimPath(shellCommandName); FilePath shimPath = GetShimPath(shellCommandName);
@ -37,12 +66,20 @@ namespace Microsoft.DotNet.ShellShim
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
CreateConfigFile(shimPath.Value + ".config", entryPoint: packageExecutable, runner: "dotnet"); FilePath windowsConfig = GetWindowsConfigPath(shellCommandName);
CreateConfigFile(
windowsConfig,
entryPoint: packageExecutable,
runner: "dotnet");
locationOfShimDuringTransaction.Add(windowsConfig);
using (var shim = File.Create(shimPath.Value)) using (var shim = File.Create(shimPath.Value))
using (var exe = typeof(ShellShimMaker).Assembly.GetManifestResourceStream(LauncherExeResourceName)) using (var exe = typeof(ShellShimMaker).Assembly.GetManifestResourceStream(LauncherExeResourceName))
{ {
exe.CopyTo(shim); exe.CopyTo(shim);
} }
locationOfShimDuringTransaction.Add(shimPath);
} }
else else
{ {
@ -51,6 +88,7 @@ namespace Microsoft.DotNet.ShellShim
script.AppendLine($"dotnet {packageExecutable.ToQuotedString()} \"$@\""); script.AppendLine($"dotnet {packageExecutable.ToQuotedString()} \"$@\"");
File.WriteAllText(shimPath.Value, script.ToString()); File.WriteAllText(shimPath.Value, script.ToString());
locationOfShimDuringTransaction.Add(shimPath);
SetUserExecutionPermissionToShimFile(shimPath); SetUserExecutionPermissionToShimFile(shimPath);
} }
@ -58,7 +96,7 @@ namespace Microsoft.DotNet.ShellShim
public void EnsureCommandNameUniqueness(string shellCommandName) public void EnsureCommandNameUniqueness(string shellCommandName)
{ {
if (File.Exists(Path.Combine(_pathToPlaceShim, shellCommandName))) if (File.Exists(GetShimPath(shellCommandName).Value))
{ {
throw new GracefulException( throw new GracefulException(
string.Format(CommonLocalizableStrings.FailInstallToolSameName, string.Format(CommonLocalizableStrings.FailInstallToolSameName,
@ -66,7 +104,7 @@ namespace Microsoft.DotNet.ShellShim
} }
} }
internal void CreateConfigFile(string outputPath, FilePath entryPoint, string runner) internal void CreateConfigFile(FilePath outputPath, FilePath entryPoint, string runner)
{ {
XDocument config; XDocument config;
using(var resource = typeof(ShellShimMaker).Assembly.GetManifestResourceStream(LauncherConfigResourceName)) using(var resource = typeof(ShellShimMaker).Assembly.GetManifestResourceStream(LauncherConfigResourceName))
@ -77,11 +115,16 @@ namespace Microsoft.DotNet.ShellShim
var appSettings = config.Descendants("appSettings").First(); var appSettings = config.Descendants("appSettings").First();
appSettings.Add(new XElement("add", new XAttribute("key", "entryPoint"), new XAttribute("value", entryPoint.Value))); appSettings.Add(new XElement("add", new XAttribute("key", "entryPoint"), new XAttribute("value", entryPoint.Value)));
appSettings.Add(new XElement("add", new XAttribute("key", "runner"), new XAttribute("value", runner ?? string.Empty))); appSettings.Add(new XElement("add", new XAttribute("key", "runner"), new XAttribute("value", runner ?? string.Empty)));
config.Save(outputPath); config.Save(outputPath.Value);
} }
public void Remove(string shellCommandName) public void Remove(string shellCommandName)
{ {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
File.Delete(GetWindowsConfigPath(shellCommandName).Value);
}
File.Delete(GetShimPath(shellCommandName).Value); File.Delete(GetShimPath(shellCommandName).Value);
} }
@ -96,6 +139,11 @@ namespace Microsoft.DotNet.ShellShim
return new FilePath(scriptPath); return new FilePath(scriptPath);
} }
private FilePath GetWindowsConfigPath(string shellCommandName)
{
return new FilePath(GetShimPath(shellCommandName).Value + ".config");
}
private static void SetUserExecutionPermissionToShimFile(FilePath scriptPath) private static void SetUserExecutionPermissionToShimFile(FilePath scriptPath)
{ {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))

View file

@ -0,0 +1,52 @@
// 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.Transactions;
using Microsoft.Extensions.EnvironmentAbstractions;
namespace Microsoft.DotNet.ToolPackage
{
internal class ToolPackageObtainTransaction : IEnlistmentNotification
{
private readonly Func<List<DirectoryPath>, ToolConfigurationAndExecutablePath> _obtainAndReturnExecutablePath;
private readonly Action<List<DirectoryPath>> _rollback;
private List<DirectoryPath> _locationOfPackageDuringTransaction = new List<DirectoryPath>();
public ToolPackageObtainTransaction(
Func<List<DirectoryPath>, ToolConfigurationAndExecutablePath> obtainAndReturnExecutablePath,
Action<List<DirectoryPath>> rollback)
{
_obtainAndReturnExecutablePath = obtainAndReturnExecutablePath ?? throw new ArgumentNullException(nameof(obtainAndReturnExecutablePath));
_rollback = rollback ?? throw new ArgumentNullException(nameof(rollback));
}
public ToolConfigurationAndExecutablePath ObtainAndReturnExecutablePath()
{
return _obtainAndReturnExecutablePath(_locationOfPackageDuringTransaction);
}
public void Commit(Enlistment enlistment)
{
enlistment.Done();
}
public void InDoubt(Enlistment enlistment)
{
Rollback(enlistment);
}
public void Prepare(PreparingEnlistment preparingEnlistment)
{
preparingEnlistment.Done();
}
public void Rollback(Enlistment enlistment)
{
_rollback(_locationOfPackageDuringTransaction);
enlistment.Done();
}
}
}

View file

@ -1,12 +1,11 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Transactions;
using System.Xml.Linq; using System.Xml.Linq;
using Microsoft.DotNet.Tools; using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Configurer; using Microsoft.DotNet.Tools;
using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.Extensions.EnvironmentAbstractions;
using NuGet.ProjectModel; using NuGet.ProjectModel;
@ -42,6 +41,69 @@ namespace Microsoft.DotNet.ToolPackage
string targetframework = null, string targetframework = null,
string source = null, string source = null,
string verbosity = null) string verbosity = null)
{
var stageDirectory = _toolsPath.WithSubDirectories(".stage", Path.GetRandomFileName());
var toolPackageObtainTransaction = new ToolPackageObtainTransaction(
obtainAndReturnExecutablePath: (locationOfPackageDuringTransaction) =>
{
if (Directory.Exists(_toolsPath.WithSubDirectories(packageId).Value))
{
throw new PackageObtainException(
string.Format(CommonLocalizableStrings.ToolPackageConflictPackageId, packageId));
}
locationOfPackageDuringTransaction.Add(stageDirectory);
var toolConfigurationAndExecutablePath = ObtainAndReturnExecutablePathInStageFolder(
packageId,
stageDirectory,
packageVersion,
nugetconfig,
targetframework,
source,
verbosity);
DirectoryPath destinationDirectory = _toolsPath.WithSubDirectories(packageId);
Directory.Move(
stageDirectory.Value,
destinationDirectory.Value);
locationOfPackageDuringTransaction.Clear();
locationOfPackageDuringTransaction.Add(destinationDirectory);
return toolConfigurationAndExecutablePath;
},
rollback: (locationOfPackageDuringTransaction) =>
{
foreach (DirectoryPath l in locationOfPackageDuringTransaction)
{
if (Directory.Exists(l.Value))
{
Directory.Delete(l.Value, recursive: true);
}
}
}
);
using (var transactionScope = new TransactionScope())
{
Transaction.Current.EnlistVolatile(toolPackageObtainTransaction, EnlistmentOptions.None);
var toolConfigurationAndExecutablePath = toolPackageObtainTransaction.ObtainAndReturnExecutablePath();
transactionScope.Complete();
return toolConfigurationAndExecutablePath;
}
}
private ToolConfigurationAndExecutablePath ObtainAndReturnExecutablePathInStageFolder(
string packageId,
DirectoryPath stageDirectory,
string packageVersion = null,
FilePath? nugetconfig = null,
string targetframework = null,
string source = null,
string verbosity = null)
{ {
if (packageId == null) if (packageId == null)
{ {
@ -65,34 +127,34 @@ namespace Microsoft.DotNet.ToolPackage
var packageVersionOrPlaceHolder = new PackageVersion(packageVersion); var packageVersionOrPlaceHolder = new PackageVersion(packageVersion);
DirectoryPath toolDirectory = DirectoryPath nugetSandboxDirectory =
CreateIndividualToolVersionDirectory(packageId, packageVersionOrPlaceHolder); CreateNugetSandboxDirectory(packageVersionOrPlaceHolder, stageDirectory);
FilePath tempProjectPath = CreateTempProject( FilePath tempProjectPath = CreateTempProject(
packageId, packageId,
packageVersionOrPlaceHolder, packageVersionOrPlaceHolder,
targetframework, targetframework,
toolDirectory); nugetSandboxDirectory);
_projectRestorer.Restore(tempProjectPath, toolDirectory, nugetconfig, source, verbosity); _projectRestorer.Restore(tempProjectPath, nugetSandboxDirectory, nugetconfig, source, verbosity);
if (packageVersionOrPlaceHolder.IsPlaceholder) if (packageVersionOrPlaceHolder.IsPlaceholder)
{ {
var concreteVersion = var concreteVersion =
new DirectoryInfo( new DirectoryInfo(
Directory.GetDirectories( Directory.GetDirectories(
toolDirectory.WithSubDirectories(packageId).Value).Single()).Name; nugetSandboxDirectory.WithSubDirectories(packageId).Value).Single()).Name;
DirectoryPath versioned = DirectoryPath versioned =
toolDirectory.GetParentPath().WithSubDirectories(concreteVersion); nugetSandboxDirectory.GetParentPath().WithSubDirectories(concreteVersion);
MoveToVersionedDirectory(versioned, toolDirectory); MoveToVersionedDirectory(versioned, nugetSandboxDirectory);
toolDirectory = versioned; nugetSandboxDirectory = versioned;
packageVersion = concreteVersion; packageVersion = concreteVersion;
} }
LockFile lockFile = new LockFileFormat() LockFile lockFile = new LockFileFormat()
.ReadWithLock(toolDirectory.WithFile("project.assets.json").Value) .ReadWithLock(nugetSandboxDirectory.WithFile("project.assets.json").Value)
.Result; .Result;
LockFileItem dotnetToolSettings = FindAssetInLockFile(lockFile, "DotnetToolSettings.xml", packageId); LockFileItem dotnetToolSettings = FindAssetInLockFile(lockFile, "DotnetToolSettings.xml", packageId);
@ -104,7 +166,7 @@ namespace Microsoft.DotNet.ToolPackage
} }
FilePath toolConfigurationPath = FilePath toolConfigurationPath =
toolDirectory nugetSandboxDirectory
.WithSubDirectories(packageId, packageVersion) .WithSubDirectories(packageId, packageVersion)
.WithFile(dotnetToolSettings.Path); .WithFile(dotnetToolSettings.Path);
@ -122,7 +184,9 @@ namespace Microsoft.DotNet.ToolPackage
return new ToolConfigurationAndExecutablePath( return new ToolConfigurationAndExecutablePath(
toolConfiguration, toolConfiguration,
toolDirectory.WithSubDirectories( _toolsPath.WithSubDirectories(
packageId,
packageVersion,
packageId, packageId,
packageVersion) packageVersion)
.WithFile(entryPointFromLockFile.Path)); .WithFile(entryPointFromLockFile.Path));
@ -186,18 +250,19 @@ namespace Microsoft.DotNet.ToolPackage
)) ))
)); ));
File.WriteAllText(tempProjectPath.Value, File.WriteAllText(tempProjectPath.Value,
tempProjectContent.ToString()); tempProjectContent.ToString());
return tempProjectPath; return tempProjectPath;
} }
private DirectoryPath CreateIndividualToolVersionDirectory( private DirectoryPath CreateNugetSandboxDirectory(
string packageId, PackageVersion packageVersion,
PackageVersion packageVersion) DirectoryPath stageDirectory
)
{ {
DirectoryPath individualTool = _toolsPath.WithSubDirectories(packageId); DirectoryPath individualToolVersion = stageDirectory.WithSubDirectories(packageVersion.Value);
DirectoryPath individualToolVersion = individualTool.WithSubDirectories(packageVersion.Value);
EnsureDirectoryExists(individualToolVersion); EnsureDirectoryExists(individualToolVersion);
return individualToolVersion; return individualToolVersion;
} }

View file

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Transactions;
using Microsoft.DotNet.Cli; using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.CommandLine; using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils;
@ -21,6 +22,7 @@ namespace Microsoft.DotNet.Tools.Install.Tool
private readonly IEnvironmentPathInstruction _environmentPathInstruction; private readonly IEnvironmentPathInstruction _environmentPathInstruction;
private readonly IShellShimMaker _shellShimMaker; private readonly IShellShimMaker _shellShimMaker;
private readonly IReporter _reporter; private readonly IReporter _reporter;
private readonly IReporter _errorReporter;
private readonly string _packageId; private readonly string _packageId;
private readonly string _packageVersion; private readonly string _packageVersion;
@ -69,13 +71,12 @@ namespace Microsoft.DotNet.Tools.Install.Tool
_shellShimMaker = shellShimMaker ?? new ShellShimMaker(cliFolderPathCalculator.ToolsShimPath); _shellShimMaker = shellShimMaker ?? new ShellShimMaker(cliFolderPathCalculator.ToolsShimPath);
_reporter = reporter; _reporter = (reporter ?? Reporter.Output);
_errorReporter = (reporter ?? Reporter.Error);
} }
public override int Execute() public override int Execute()
{ {
var reporter = (_reporter ?? Reporter.Output);
var errorReporter = (_reporter ?? Reporter.Error);
if (!_global) if (!_global)
{ {
throw new GracefulException(LocalizableStrings.InstallToolCommandOnlySupportGlobal); throw new GracefulException(LocalizableStrings.InstallToolCommandOnlySupportGlobal);
@ -83,10 +84,23 @@ namespace Microsoft.DotNet.Tools.Install.Tool
try try
{ {
var toolConfigurationAndExecutablePath = ObtainPackage(); FilePath? configFile = null;
if (_configFilePath != null)
{
configFile = new FilePath(_configFilePath);
}
using (var transactionScope = new TransactionScope())
{
var toolConfigurationAndExecutablePath = _toolPackageObtainer.ObtainAndReturnExecutablePath(
packageId: _packageId,
packageVersion: _packageVersion,
nugetconfig: configFile,
targetframework: _framework,
source: _source,
verbosity: _verbosity);
var commandName = toolConfigurationAndExecutablePath.Configuration.CommandName; var commandName = toolConfigurationAndExecutablePath.Configuration.CommandName;
_shellShimMaker.EnsureCommandNameUniqueness(commandName);
_shellShimMaker.CreateShim( _shellShimMaker.CreateShim(
toolConfigurationAndExecutablePath.Executable, toolConfigurationAndExecutablePath.Executable,
@ -95,42 +109,28 @@ namespace Microsoft.DotNet.Tools.Install.Tool
_environmentPathInstruction _environmentPathInstruction
.PrintAddPathInstructionIfPathDoesNotExist(); .PrintAddPathInstructionIfPathDoesNotExist();
reporter.WriteLine( _reporter.WriteLine(
string.Format(LocalizableStrings.InstallationSucceeded, commandName)); string.Format(LocalizableStrings.InstallationSucceeded, commandName));
transactionScope.Complete();
}
} }
catch (PackageObtainException ex) catch (PackageObtainException ex)
{ {
errorReporter.WriteLine(ex.Message.Red()); _errorReporter.WriteLine(ex.Message.Red());
errorReporter.WriteLine(string.Format(LocalizableStrings.ToolInstallationFailed, _packageId).Red()); _errorReporter.WriteLine(string.Format(LocalizableStrings.ToolInstallationFailed, _packageId).Red());
return 1; return 1;
} }
catch (ToolConfigurationException ex) catch (ToolConfigurationException ex)
{ {
errorReporter.WriteLine( _errorReporter.WriteLine(
string.Format( string.Format(
LocalizableStrings.InvalidToolConfiguration, LocalizableStrings.InvalidToolConfiguration,
ex.Message).Red()); ex.Message).Red());
errorReporter.WriteLine(string.Format(LocalizableStrings.ToolInstallationFailedContactAuthor, _packageId).Red()); _errorReporter.WriteLine(string.Format(LocalizableStrings.ToolInstallationFailedContactAuthor, _packageId).Red());
return 1; return 1;
} }
return 0; return 0;
} }
private ToolConfigurationAndExecutablePath ObtainPackage()
{
FilePath? configFile = null;
if (_configFilePath != null)
{
configFile = new FilePath(_configFilePath);
}
return _toolPackageObtainer.ObtainAndReturnExecutablePath(
packageId: _packageId,
packageVersion: _packageVersion,
nugetconfig: configFile,
targetframework: _framework,
source: _source,
verbosity: _verbosity);
}
} }
} }

View file

@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target> <target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="ToolPackageConflictPackageId">
<source>Tool '{0}' is already installed.</source>
<target state="new">Tool '{0}' is already installed.</target>
<note />
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target> <target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="ToolPackageConflictPackageId">
<source>Tool '{0}' is already installed.</source>
<target state="new">Tool '{0}' is already installed.</target>
<note />
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target> <target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="ToolPackageConflictPackageId">
<source>Tool '{0}' is already installed.</source>
<target state="new">Tool '{0}' is already installed.</target>
<note />
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target> <target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="ToolPackageConflictPackageId">
<source>Tool '{0}' is already installed.</source>
<target state="new">Tool '{0}' is already installed.</target>
<note />
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target> <target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="ToolPackageConflictPackageId">
<source>Tool '{0}' is already installed.</source>
<target state="new">Tool '{0}' is already installed.</target>
<note />
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target> <target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="ToolPackageConflictPackageId">
<source>Tool '{0}' is already installed.</source>
<target state="new">Tool '{0}' is already installed.</target>
<note />
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target> <target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="ToolPackageConflictPackageId">
<source>Tool '{0}' is already installed.</source>
<target state="new">Tool '{0}' is already installed.</target>
<note />
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target> <target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="ToolPackageConflictPackageId">
<source>Tool '{0}' is already installed.</source>
<target state="new">Tool '{0}' is already installed.</target>
<note />
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target> <target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="ToolPackageConflictPackageId">
<source>Tool '{0}' is already installed.</source>
<target state="new">Tool '{0}' is already installed.</target>
<note />
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target> <target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="ToolPackageConflictPackageId">
<source>Tool '{0}' is already installed.</source>
<target state="new">Tool '{0}' is already installed.</target>
<note />
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target> <target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="ToolPackageConflictPackageId">
<source>Tool '{0}' is already installed.</source>
<target state="new">Tool '{0}' is already installed.</target>
<note />
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target> <target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="ToolPackageConflictPackageId">
<source>Tool '{0}' is already installed.</source>
<target state="new">Tool '{0}' is already installed.</target>
<note />
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -793,6 +793,11 @@ setx PATH "%PATH%;{1}"</target>
<target state="new">Command '{0}' uses unsupported runner '{1}'."</target> <target state="new">Command '{0}' uses unsupported runner '{1}'."</target>
<note /> <note />
</trans-unit> </trans-unit>
<trans-unit id="ToolPackageConflictPackageId">
<source>Tool '{0}' is already installed.</source>
<target state="new">Tool '{0}' is already installed.</target>
<note />
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -170,6 +170,11 @@ namespace Microsoft.DotNet.Configurer.UnitTests
_directories.Add(path); _directories.Add(path);
CreateDirectoryInvoked = true; CreateDirectoryInvoked = true;
} }
public void Delete(string path, bool recursive)
{
throw new NotImplementedException();
}
} }
} }
} }

View file

@ -124,6 +124,11 @@ namespace Microsoft.DotNet.Configurer.UnitTests
{ {
throw new UnauthorizedAccessException(); throw new UnauthorizedAccessException();
} }
public void Delete(string path)
{
throw new UnauthorizedAccessException();
}
} }
private class NoPermissionDirectoryFake : IDirectory private class NoPermissionDirectoryFake : IDirectory
@ -153,6 +158,11 @@ namespace Microsoft.DotNet.Configurer.UnitTests
{ {
throw new UnauthorizedAccessException(); throw new UnauthorizedAccessException();
} }
public void Delete(string path, bool recursive)
{
throw new NotImplementedException();
}
} }
private class Counter private class Counter

View file

@ -197,6 +197,11 @@ namespace Microsoft.DotNet.Configurer.UnitTests
{ {
throw new UnauthorizedAccessException(); throw new UnauthorizedAccessException();
} }
public void Delete(string path, bool recursive)
{
throw new NotImplementedException();
}
} }
private class FileMock : IFile private class FileMock : IFile
@ -256,6 +261,11 @@ namespace Microsoft.DotNet.Configurer.UnitTests
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public void Delete(string path)
{
throw new NotImplementedException();
}
} }
private class MockStream : MemoryStream private class MockStream : MemoryStream

View file

@ -50,6 +50,11 @@ namespace Microsoft.DotNet.ShellShim.Tests
_files[path] = content; _files[path] = content;
} }
public void Delete(string path)
{
throw new NotImplementedException();
}
public static FakeFile Empty => new FakeFile(new Dictionary<string, string>()); public static FakeFile Empty => new FakeFile(new Dictionary<string, string>());
} }
} }

View file

@ -6,12 +6,13 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Transactions;
using System.Xml.Linq; using System.Xml.Linq;
using FluentAssertions; using FluentAssertions;
using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.TestFramework; using Microsoft.DotNet.TestFramework;
using Microsoft.DotNet.ToolPackage;
using Microsoft.DotNet.Tools.Test.Utilities; using Microsoft.DotNet.Tools.Test.Utilities;
using Microsoft.DotNet.Tools.Test.Utilities.Mock;
using Microsoft.DotNet.Tools.Tests.ComponentMocks; using Microsoft.DotNet.Tools.Tests.ComponentMocks;
using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.Extensions.EnvironmentAbstractions;
using Xunit; using Xunit;
@ -38,15 +39,16 @@ namespace Microsoft.DotNet.ShellShim.Tests
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return; return;
var shellShimMaker = new ShellShimMaker(TempRoot.Root); var cleanFolderUnderTempRoot = GetNewCleanFolderUnderTempRoot();
var shellShimMaker = new ShellShimMaker(cleanFolderUnderTempRoot);
var tmpFile = Path.Combine(TempRoot.Root, Path.GetRandomFileName()); var tmpFile = new FilePath(Path.Combine(cleanFolderUnderTempRoot, Path.GetRandomFileName()));
shellShimMaker.CreateConfigFile(tmpFile, entryPoint, runner); shellShimMaker.CreateConfigFile(tmpFile, entryPoint, runner);
new FileInfo(tmpFile).Should().Exist(); new FileInfo(tmpFile.Value).Should().Exist();
var generated = XDocument.Load(tmpFile); var generated = XDocument.Load(tmpFile.Value);
generated.Descendants("appSettings") generated.Descendants("appSettings")
.Descendants("add") .Descendants("add")
@ -61,13 +63,33 @@ namespace Microsoft.DotNet.ShellShim.Tests
{ {
var outputDll = MakeHelloWorldExecutableDll(); var outputDll = MakeHelloWorldExecutableDll();
var shellShimMaker = new ShellShimMaker(TempRoot.Root); var cleanFolderUnderTempRoot = GetNewCleanFolderUnderTempRoot();
var shellShimMaker = new ShellShimMaker(cleanFolderUnderTempRoot);
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName(); var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
shellShimMaker.CreateShim( shellShimMaker.CreateShim(outputDll, shellCommandName);
new FilePath(outputDll.FullName),
shellCommandName); var stdOut = ExecuteInShell(shellCommandName, cleanFolderUnderTempRoot);
var stdOut = ExecuteInShell(shellCommandName);
stdOut.Should().Contain("Hello World");
}
[Fact]
public void GivenAnExecutablePathItCanGenerateShimFileInTransaction()
{
var outputDll = MakeHelloWorldExecutableDll();
var cleanFolderUnderTempRoot = GetNewCleanFolderUnderTempRoot();
var shellShimMaker = new ShellShimMaker(cleanFolderUnderTempRoot);
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
using (var transactionScope = new TransactionScope())
{
shellShimMaker.CreateShim(outputDll, shellCommandName);
transactionScope.Complete();
}
var stdOut = ExecuteInShell(shellCommandName, cleanFolderUnderTempRoot);
stdOut.Should().Contain("Hello World"); stdOut.Should().Contain("Hello World");
} }
@ -80,9 +102,8 @@ namespace Microsoft.DotNet.ShellShim.Tests
var shellShimMaker = new ShellShimMaker(Path.Combine(TempRoot.Root, extraNonExistDirectory)); var shellShimMaker = new ShellShimMaker(Path.Combine(TempRoot.Root, extraNonExistDirectory));
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName(); var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
Action a = () => shellShimMaker.CreateShim( Action a = () => shellShimMaker.CreateShim(outputDll, shellCommandName);
new FilePath(outputDll.FullName),
shellCommandName);
a.ShouldNotThrow<DirectoryNotFoundException>(); a.ShouldNotThrow<DirectoryNotFoundException>();
} }
@ -94,14 +115,13 @@ namespace Microsoft.DotNet.ShellShim.Tests
{ {
var outputDll = MakeHelloWorldExecutableDll(); var outputDll = MakeHelloWorldExecutableDll();
var shellShimMaker = new ShellShimMaker(TempRoot.Root); var cleanFolderUnderTempRoot = GetNewCleanFolderUnderTempRoot();
var shellShimMaker = new ShellShimMaker(cleanFolderUnderTempRoot);
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName(); var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
shellShimMaker.CreateShim( shellShimMaker.CreateShim(outputDll, shellCommandName);
new FilePath(outputDll.FullName),
shellCommandName);
var stdOut = ExecuteInShell(shellCommandName, arguments); var stdOut = ExecuteInShell(shellCommandName, cleanFolderUnderTempRoot, arguments);
for (int i = 0; i < expectedPassThru.Length; i++) for (int i = 0; i < expectedPassThru.Length; i++)
{ {
@ -115,17 +135,17 @@ namespace Microsoft.DotNet.ShellShim.Tests
public void GivenAnExecutablePathWithExistingSameNameShimItThrows(bool testMockBehaviorIsInSync) public void GivenAnExecutablePathWithExistingSameNameShimItThrows(bool testMockBehaviorIsInSync)
{ {
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName(); var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
var cleanFolderUnderTempRoot = GetNewCleanFolderUnderTempRoot();
MakeNameConflictingCommand(TempRoot.Root, shellCommandName); MakeNameConflictingCommand(cleanFolderUnderTempRoot, shellCommandName);
IShellShimMaker shellShimMaker; IShellShimMaker shellShimMaker;
if (testMockBehaviorIsInSync) if (testMockBehaviorIsInSync)
{ {
shellShimMaker = new ShellShimMakerMock(TempRoot.Root); shellShimMaker = new ShellShimMakerMock(cleanFolderUnderTempRoot);
} }
else else
{ {
shellShimMaker = new ShellShimMaker(TempRoot.Root); shellShimMaker = new ShellShimMaker(cleanFolderUnderTempRoot);
} }
Action a = () => shellShimMaker.EnsureCommandNameUniqueness(shellCommandName); Action a = () => shellShimMaker.EnsureCommandNameUniqueness(shellCommandName);
@ -138,18 +158,89 @@ namespace Microsoft.DotNet.ShellShim.Tests
[Theory] [Theory]
[InlineData(false)] [InlineData(false)]
[InlineData(true)] [InlineData(true)]
public void GivenAnExecutablePathWithoutExistingSameNameShimItShouldNotThrow(bool testMockBehaviorIsInSync) public void GivenAnExecutablePathWithExistingSameNameShimItRollsBack(bool testMockBehaviorIsInSync)
{ {
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName(); var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
var pathToShim = GetNewCleanFolderUnderTempRoot();
MakeNameConflictingCommand(pathToShim, shellCommandName);
IShellShimMaker shellShimMaker; IShellShimMaker shellShimMaker;
if (testMockBehaviorIsInSync) if (testMockBehaviorIsInSync)
{ {
shellShimMaker = new ShellShimMakerMock(TempRoot.Root); shellShimMaker = new ShellShimMakerMock(pathToShim);
} }
else else
{ {
shellShimMaker = new ShellShimMaker(TempRoot.Root); shellShimMaker = new ShellShimMaker(pathToShim);
}
Action a = () =>
{
using (var t = new TransactionScope())
{
shellShimMaker.CreateShim(new FilePath("dummy.dll"), shellCommandName);
t.Complete();
}
};
a.ShouldThrow<GracefulException>();
Directory.GetFiles(pathToShim).Should().HaveCount(1, "there is only intent conflicted command");
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GivenAnExecutablePathErrorHappensItRollsBack(bool testMockBehaviorIsInSync)
{
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
var pathToShim = GetNewCleanFolderUnderTempRoot();
IShellShimMaker shellShimMaker;
if (testMockBehaviorIsInSync)
{
shellShimMaker = new ShellShimMakerMock(pathToShim);
}
else
{
shellShimMaker = new ShellShimMaker(pathToShim);
}
Action intendedError = () => throw new PackageObtainException();
Action a = () =>
{
using (var t = new TransactionScope())
{
shellShimMaker.CreateShim(new FilePath("dummy.dll"), shellCommandName);
intendedError();
t.Complete();
}
};
a.ShouldThrow<PackageObtainException>();
Directory.GetFiles(pathToShim).Should().BeEmpty();
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GivenAnExecutablePathWithoutExistingSameNameShimItShouldNotThrow(bool testMockBehaviorIsInSync)
{
var shellCommandName = nameof(ShellShimMakerTests) + Path.GetRandomFileName();
var cleanFolderUnderTempRoot = GetNewCleanFolderUnderTempRoot();
IShellShimMaker shellShimMaker;
if (testMockBehaviorIsInSync)
{
shellShimMaker = new ShellShimMakerMock(cleanFolderUnderTempRoot);
}
else
{
shellShimMaker = new ShellShimMaker(cleanFolderUnderTempRoot);
} }
Action a = () => shellShimMaker.EnsureCommandNameUniqueness(shellCommandName); Action a = () => shellShimMaker.EnsureCommandNameUniqueness(shellCommandName);
@ -158,16 +249,21 @@ namespace Microsoft.DotNet.ShellShim.Tests
private static void MakeNameConflictingCommand(string pathToPlaceShim, string shellCommandName) private static void MakeNameConflictingCommand(string pathToPlaceShim, string shellCommandName)
{ {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
shellCommandName = shellCommandName + ".exe";
}
File.WriteAllText(Path.Combine(pathToPlaceShim, shellCommandName), string.Empty); File.WriteAllText(Path.Combine(pathToPlaceShim, shellCommandName), string.Empty);
} }
private string ExecuteInShell(string shellCommandName, string arguments = "") private string ExecuteInShell(string shellCommandName, string cleanFolderUnderTempRoot, string arguments = "")
{ {
ProcessStartInfo processStartInfo; ProcessStartInfo processStartInfo;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
var file = Path.Combine(TempRoot.Root, shellCommandName + ".exe"); var file = Path.Combine(cleanFolderUnderTempRoot, shellCommandName + ".exe");
processStartInfo = new ProcessStartInfo processStartInfo = new ProcessStartInfo
{ {
FileName = file, FileName = file,
@ -186,7 +282,7 @@ namespace Microsoft.DotNet.ShellShim.Tests
} }
_output.WriteLine($"Launching '{processStartInfo.FileName} {processStartInfo.Arguments}'"); _output.WriteLine($"Launching '{processStartInfo.FileName} {processStartInfo.Arguments}'");
processStartInfo.WorkingDirectory = TempRoot.Root; processStartInfo.WorkingDirectory = cleanFolderUnderTempRoot;
processStartInfo.EnvironmentVariables["PATH"] = Path.GetDirectoryName(new Muxer().MuxerPath); processStartInfo.EnvironmentVariables["PATH"] = Path.GetDirectoryName(new Muxer().MuxerPath);
processStartInfo.ExecuteAndCaptureOutput(out var stdOut, out var stdErr); processStartInfo.ExecuteAndCaptureOutput(out var stdOut, out var stdErr);
@ -196,7 +292,7 @@ namespace Microsoft.DotNet.ShellShim.Tests
return stdOut ?? ""; return stdOut ?? "";
} }
private static FileInfo MakeHelloWorldExecutableDll() private static FilePath MakeHelloWorldExecutableDll()
{ {
const string testAppName = "TestAppSimple"; const string testAppName = "TestAppSimple";
const string emptySpaceToTestSpaceInPath = " "; const string emptySpaceToTestSpaceInPath = " ";
@ -212,7 +308,15 @@ namespace Microsoft.DotNet.ShellShim.Tests
.GetDirectories().Single() .GetDirectories().Single()
.GetFile($"{testAppName}.dll"); .GetFile($"{testAppName}.dll");
return outputDll; return new FilePath(outputDll.FullName);
}
private static string GetNewCleanFolderUnderTempRoot()
{
DirectoryInfo CleanFolderUnderTempRoot = new DirectoryInfo(Path.Combine(TempRoot.Root, "cleanfolder" + Path.GetRandomFileName()));
CleanFolderUnderTempRoot.Create();
return CleanFolderUnderTempRoot.FullName;
} }
} }
} }

View file

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Transactions;
using FluentAssertions; using FluentAssertions;
using Microsoft.DotNet.Tools.Test.Utilities; using Microsoft.DotNet.Tools.Test.Utilities;
using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.Extensions.EnvironmentAbstractions;
@ -58,8 +59,7 @@ namespace Microsoft.DotNet.ToolPackage.Tests
bundledTargetFrameworkMoniker: new Lazy<string>(), bundledTargetFrameworkMoniker: new Lazy<string>(),
projectRestorer: new ProjectRestorer(reporter)); projectRestorer: new ProjectRestorer(reporter));
ToolConfigurationAndExecutablePath toolConfigurationAndExecutablePath = ToolConfigurationAndExecutablePath toolConfigurationAndExecutablePath = packageObtainer.ObtainAndReturnExecutablePath(
packageObtainer.ObtainAndReturnExecutablePath(
packageId: TestPackageId, packageId: TestPackageId,
packageVersion: TestPackageVersion, packageVersion: TestPackageVersion,
targetframework: _testTargetframework); targetframework: _testTargetframework);
@ -109,6 +109,43 @@ namespace Microsoft.DotNet.ToolPackage.Tests
File.Delete(executable.Value); File.Delete(executable.Value);
} }
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GivenNugetConfigAndPackageNameAndVersionAndTargetFrameworkWhenCallItCanDownloadThePackageInTransaction(
bool testMockBehaviorIsInSync)
{
var reporter = new BufferedReporter();
FilePath nugetConfigPath = WriteNugetConfigFileToPointToTheFeed();
var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName());
var packageObtainer =
ConstructDefaultPackageObtainer(toolsPath, reporter, testMockBehaviorIsInSync, nugetConfigPath.Value);
ToolConfigurationAndExecutablePath toolConfigurationAndExecutablePath;
using (var transactionScope = new TransactionScope())
{
toolConfigurationAndExecutablePath
= packageObtainer.ObtainAndReturnExecutablePath(
packageId: TestPackageId,
packageVersion: TestPackageVersion,
nugetconfig: nugetConfigPath,
targetframework: _testTargetframework);
transactionScope.Complete();
}
reporter.Lines.Should().BeEmpty();
FilePath executable = toolConfigurationAndExecutablePath.Executable;
File.Exists(executable.Value)
.Should()
.BeTrue(executable + " should have the executable");
File.Delete(executable.Value);
}
[Theory] [Theory]
[InlineData(false)] [InlineData(false)]
[InlineData(true)] [InlineData(true)]
@ -122,7 +159,7 @@ namespace Microsoft.DotNet.ToolPackage.Tests
var packageObtainer = var packageObtainer =
ConstructDefaultPackageObtainer(toolsPath, reporter, testMockBehaviorIsInSync, nugetConfigPath.Value); ConstructDefaultPackageObtainer(toolsPath, reporter, testMockBehaviorIsInSync, nugetConfigPath.Value);
ToolConfigurationAndExecutablePath toolConfigurationAndExecutableDirectory = ToolConfigurationAndExecutablePath toolConfigurationAndExecutablePath =
packageObtainer.ObtainAndReturnExecutablePath( packageObtainer.ObtainAndReturnExecutablePath(
packageId: TestPackageId, packageId: TestPackageId,
packageVersion: TestPackageVersion, packageVersion: TestPackageVersion,
@ -138,7 +175,7 @@ namespace Microsoft.DotNet.ToolPackage.Tests
/dependency2 package id/ /dependency2 package id/
/project.assets.json /project.assets.json
*/ */
var assetJsonPath = toolConfigurationAndExecutableDirectory var assetJsonPath = toolConfigurationAndExecutablePath
.Executable .Executable
.GetDirectoryPath() .GetDirectoryPath()
.GetParentPath() .GetParentPath()
@ -177,7 +214,7 @@ namespace Microsoft.DotNet.ToolPackage.Tests
IToolPackageObtainer packageObtainer; IToolPackageObtainer packageObtainer;
if (testMockBehaviorIsInSync) if (testMockBehaviorIsInSync)
{ {
packageObtainer = new ToolPackageObtainerMock(); packageObtainer = new ToolPackageObtainerMock(toolsPath: toolsPath);
} }
else else
{ {
@ -212,7 +249,7 @@ namespace Microsoft.DotNet.ToolPackage.Tests
public void GivenAllButNoPackageVersionItCanDownloadThePackage(bool testMockBehaviorIsInSync) public void GivenAllButNoPackageVersionItCanDownloadThePackage(bool testMockBehaviorIsInSync)
{ {
var reporter = new BufferedReporter(); var reporter = new BufferedReporter();
var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed(); FilePath nugetConfigPath = WriteNugetConfigFileToPointToTheFeed();
var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName()); var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName());
var packageObtainer = var packageObtainer =
@ -235,35 +272,6 @@ namespace Microsoft.DotNet.ToolPackage.Tests
File.Delete(executable.Value); File.Delete(executable.Value);
} }
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GivenAllButNoPackageVersionAndInvokeTwiceItShouldNotThrow(bool testMockBehaviorIsInSync)
{
var reporter = new BufferedReporter();
var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed();
var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName());
var packageObtainer =
ConstructDefaultPackageObtainer(toolsPath, reporter, testMockBehaviorIsInSync, nugetConfigPath.Value);
packageObtainer.ObtainAndReturnExecutablePath(
packageId: TestPackageId,
nugetconfig: nugetConfigPath,
targetframework: _testTargetframework);
reporter.Lines.Should().BeEmpty();
Action secondCall = () => packageObtainer.ObtainAndReturnExecutablePath(
packageId: TestPackageId,
nugetconfig: nugetConfigPath,
targetframework: _testTargetframework);
reporter.Lines.Should().BeEmpty();
secondCall.ShouldNotThrow();
}
[Theory] [Theory]
[InlineData(false)] [InlineData(false)]
[InlineData(true)] [InlineData(true)]
@ -292,7 +300,8 @@ namespace Microsoft.DotNet.ToolPackage.Tests
} }
} }
} }
}); },
toolsPath: toolsPath);
} }
else else
{ {
@ -334,6 +343,7 @@ namespace Microsoft.DotNet.ToolPackage.Tests
var nonExistNugetConfigFile = new FilePath("NonExistent.file"); var nonExistNugetConfigFile = new FilePath("NonExistent.file");
Action a = () => Action a = () =>
{ {
ToolConfigurationAndExecutablePath toolConfigurationAndExecutablePath =
packageObtainer.ObtainAndReturnExecutablePath( packageObtainer.ObtainAndReturnExecutablePath(
packageId: TestPackageId, packageId: TestPackageId,
packageVersion: TestPackageVersion, packageVersion: TestPackageVersion,
@ -358,8 +368,13 @@ namespace Microsoft.DotNet.ToolPackage.Tests
var reporter = new BufferedReporter(); var reporter = new BufferedReporter();
var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName()); var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName());
var packageObtainer = ConstructDefaultPackageObtainer(toolsPath, reporter); var packageObtainer = ConstructDefaultPackageObtainer(
var toolConfigurationAndExecutableDirectory = packageObtainer.ObtainAndReturnExecutablePath( toolsPath,
reporter,
testMockBehaviorIsInSync: testMockBehaviorIsInSync,
addSourceFeedWithFilePath: GetTestLocalFeedPath());
ToolConfigurationAndExecutablePath toolConfigurationAndExecutablePath =
packageObtainer.ObtainAndReturnExecutablePath(
packageId: TestPackageId, packageId: TestPackageId,
packageVersion: TestPackageVersion, packageVersion: TestPackageVersion,
targetframework: _testTargetframework, targetframework: _testTargetframework,
@ -367,7 +382,7 @@ namespace Microsoft.DotNet.ToolPackage.Tests
reporter.Lines.Should().BeEmpty(); reporter.Lines.Should().BeEmpty();
var executable = toolConfigurationAndExecutableDirectory.Executable; var executable = toolConfigurationAndExecutablePath.Executable;
File.Exists(executable.Value) File.Exists(executable.Value)
.Should() .Should()
@ -376,6 +391,158 @@ namespace Microsoft.DotNet.ToolPackage.Tests
File.Delete(executable.Value); File.Delete(executable.Value);
} }
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GivenFailedRestoreItCanRollBack(bool testMockBehaviorIsInSync)
{
var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName());
var reporter = new BufferedReporter();
var packageObtainer = ConstructDefaultPackageObtainer(toolsPath, reporter, testMockBehaviorIsInSync);
try
{
using (var t = new TransactionScope())
{
packageObtainer.ObtainAndReturnExecutablePath(
packageId: "non exist package id",
packageVersion: TestPackageVersion,
targetframework: _testTargetframework);
t.Complete();
}
}
catch (PackageObtainException)
{
// catch the intent error
}
AssertRollBack(toolsPath);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GiveSucessRestoreButFailedOnNextStepItCanRollBack(bool testMockBehaviorIsInSync)
{
FilePath nugetConfigPath = WriteNugetConfigFileToPointToTheFeed();
var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName());
var reporter = new BufferedReporter();
var packageObtainer = ConstructDefaultPackageObtainer(toolsPath, reporter, testMockBehaviorIsInSync);
void FailedStepAfterSuccessRestore() => throw new GracefulException("simulated error");
try
{
using (var t = new TransactionScope())
{
ToolConfigurationAndExecutablePath obtainAndReturnExecutablePathtransactional
= packageObtainer.ObtainAndReturnExecutablePath(
packageId: TestPackageId,
packageVersion: TestPackageVersion,
targetframework: _testTargetframework);
FailedStepAfterSuccessRestore();
t.Complete();
}
}
catch (GracefulException)
{
// catch the simulated error
}
AssertRollBack(toolsPath);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GivenAllButNoPackageVersionAndInvokeTwiceItShouldNotThrow(bool testMockBehaviorIsInSync)
{
var reporter = new BufferedReporter();
var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed();
var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName());
var packageObtainer =
ConstructDefaultPackageObtainer(toolsPath, reporter, testMockBehaviorIsInSync, nugetConfigPath.Value);
try
{
using (var t = new TransactionScope())
{
packageObtainer.ObtainAndReturnExecutablePath(
packageId: TestPackageId,
packageVersion: TestPackageVersion,
targetframework: _testTargetframework);
packageObtainer.ObtainAndReturnExecutablePath(
packageId: TestPackageId,
packageVersion: TestPackageVersion,
targetframework: _testTargetframework);
t.Complete();
}
}
catch (PackageObtainException)
{
// catch the simulated error
}
AssertRollBack(toolsPath);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GivenAllButNoPackageVersionAndInvokeTwiceInTransactionItShouldRollback(bool testMockBehaviorIsInSync)
{
var reporter = new BufferedReporter();
var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed();
var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName());
var packageObtainer =
ConstructDefaultPackageObtainer(toolsPath, reporter, testMockBehaviorIsInSync, nugetConfigPath.Value);
packageObtainer.ObtainAndReturnExecutablePath(
packageId: TestPackageId,
nugetconfig: nugetConfigPath,
targetframework: _testTargetframework);
reporter.Lines.Should().BeEmpty();
Action secondCall = () => packageObtainer.ObtainAndReturnExecutablePath(
packageId: TestPackageId,
nugetconfig: nugetConfigPath,
targetframework: _testTargetframework);
reporter.Lines.Should().BeEmpty();
secondCall.ShouldThrow<PackageObtainException>();
Directory.Exists(Path.Combine(toolsPath, TestPackageId))
.Should().BeTrue("The result of first one is still here");
Directory.GetDirectories(Path.Combine(toolsPath, ".stage"))
.Should().BeEmpty("nothing in stage folder, already rolled back");
}
private static void AssertRollBack(string toolsPath)
{
if (!Directory.Exists(toolsPath))
{
return; // nothing at all
}
Directory.GetFiles(toolsPath).Should().BeEmpty();
Directory.GetDirectories(toolsPath)
.Should().NotContain(d => !new DirectoryInfo(d).Name.Equals(".stage"),
"no broken folder, exclude stage folder");
Directory.GetDirectories(Path.Combine(toolsPath, ".stage"))
.Should().BeEmpty("nothing in stage folder");
}
private static readonly Func<FilePath> GetUniqueTempProjectPathEachTest = () => private static readonly Func<FilePath> GetUniqueTempProjectPathEachTest = () =>
{ {
var tempProjectDirectory = var tempProjectDirectory =
@ -412,7 +579,7 @@ namespace Microsoft.DotNet.ToolPackage.Tests
} }
} }
} }
}); }, toolsPath: toolsPath);
} }
if (addSourceFeedWithFilePath != null) if (addSourceFeedWithFilePath != null)
@ -422,7 +589,7 @@ namespace Microsoft.DotNet.ToolPackage.Tests
{ {
new MockFeed new MockFeed
{ {
Type = MockFeedType.ExplicitNugetConfig, Type = MockFeedType.Source,
Uri = addSourceFeedWithFilePath, Uri = addSourceFeedWithFilePath,
Packages = new List<MockFeedPackage> Packages = new List<MockFeedPackage>
{ {
@ -433,10 +600,11 @@ namespace Microsoft.DotNet.ToolPackage.Tests
} }
} }
} }
}); },
toolsPath: toolsPath);
} }
return new ToolPackageObtainerMock(); return new ToolPackageObtainerMock(toolsPath: toolsPath);
} }
return new ToolPackageObtainer( return new ToolPackageObtainer(
@ -465,7 +633,6 @@ namespace Microsoft.DotNet.ToolPackage.Tests
} }
private static string GetTestLocalFeedPath() => Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TestAssetLocalNugetFeed"); private static string GetTestLocalFeedPath() => Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TestAssetLocalNugetFeed");
private readonly string _testTargetframework = BundledTargetFramework.GetTargetFrameworkMoniker(); private readonly string _testTargetframework = BundledTargetFramework.GetTargetFrameworkMoniker();
private const string TestPackageVersion = "1.0.4"; private const string TestPackageVersion = "1.0.4";
private const string TestPackageId = "global.tool.console.demo"; private const string TestPackageId = "global.tool.console.demo";

View file

@ -2,7 +2,11 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information. // Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.InteropServices;
using System.Transactions;
using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.ShellShim; using Microsoft.DotNet.ShellShim;
using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.Extensions.EnvironmentAbstractions;
@ -18,12 +22,56 @@ namespace Microsoft.DotNet.Tools.Tests.ComponentMocks
public ShellShimMakerMock(string pathToPlaceShim, IFileSystem fileSystem = null) public ShellShimMakerMock(string pathToPlaceShim, IFileSystem fileSystem = null)
{ {
_pathToPlaceShim = _pathToPlaceShim =
pathToPlaceShim ?? throw new ArgumentNullException(nameof(pathToPlaceShim)); pathToPlaceShim ??
throw new ArgumentNullException(nameof(pathToPlaceShim));
_fileSystem = fileSystem ?? new FileSystemWrapper(); _fileSystem = fileSystem ?? new FileSystemWrapper();
} }
public void EnsureCommandNameUniqueness(string shellCommandName)
{
if (_fileSystem.File.Exists(GetShimPath(shellCommandName).Value))
{
throw new GracefulException(
string.Format(CommonLocalizableStrings.FailInstallToolSameName,
shellCommandName));
}
}
public void CreateShim(FilePath packageExecutable, string shellCommandName) public void CreateShim(FilePath packageExecutable, string shellCommandName)
{
var createShimTransaction = new CreateShimTransaction(
createShim: locationOfShimDuringTransaction =>
{
EnsureCommandNameUniqueness(shellCommandName);
PlaceShim(packageExecutable, shellCommandName, locationOfShimDuringTransaction);
},
rollback: locationOfShimDuringTransaction =>
{
foreach (FilePath f in locationOfShimDuringTransaction)
{
if (File.Exists(f.Value))
{
File.Delete(f.Value);
}
}
});
using (var transactionScope = new TransactionScope())
{
Transaction.Current.EnlistVolatile(createShimTransaction, EnlistmentOptions.None);
createShimTransaction.CreateShim();
transactionScope.Complete();
}
}
public void Remove(string shellCommandName)
{
File.Delete(GetShimPath(shellCommandName).Value);
}
private void PlaceShim(FilePath packageExecutable, string shellCommandName, List<FilePath> locationOfShimDuringTransaction)
{ {
var fakeshim = new FakeShim var fakeshim = new FakeShim
{ {
@ -32,18 +80,20 @@ namespace Microsoft.DotNet.Tools.Tests.ComponentMocks
}; };
var script = JsonConvert.SerializeObject(fakeshim); var script = JsonConvert.SerializeObject(fakeshim);
FilePath scriptPath = new FilePath(Path.Combine(_pathToPlaceShim, shellCommandName)); FilePath scriptPath = GetShimPath(shellCommandName);
_fileSystem.File.WriteAllText(scriptPath.Value, script); _fileSystem.File.WriteAllText(scriptPath.Value, script);
locationOfShimDuringTransaction.Add(scriptPath);
} }
public void EnsureCommandNameUniqueness(string shellCommandName) private FilePath GetShimPath(string shellCommandName)
{ {
if (_fileSystem.File.Exists(Path.Combine(_pathToPlaceShim, shellCommandName))) var scriptPath = Path.Combine(_pathToPlaceShim, shellCommandName);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
throw new GracefulException( scriptPath += ".exe";
string.Format(CommonLocalizableStrings.FailInstallToolSameName,
shellCommandName));
} }
return new FilePath(scriptPath);
} }
public class FakeShim public class FakeShim

View file

@ -5,6 +5,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Transactions;
using Microsoft.DotNet.Cli;
using Microsoft.DotNet.ToolPackage; using Microsoft.DotNet.ToolPackage;
using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.Extensions.EnvironmentAbstractions;
@ -12,20 +14,27 @@ namespace Microsoft.DotNet.Tools.Tests.ComponentMocks
{ {
internal class ToolPackageObtainerMock : IToolPackageObtainer internal class ToolPackageObtainerMock : IToolPackageObtainer
{ {
private readonly Action _beforeRunObtain; private readonly string _toolsPath;
public const string FakeEntrypointName = "SimulatorEntryPoint.dll"; public const string FakeEntrypointName = "SimulatorEntryPoint.dll";
public const string FakeCommandName = "SimulatorCommand"; public const string FakeCommandName = "SimulatorCommand";
private readonly Action _beforeRunObtain;
private readonly Action _duringObtain;
private static IFileSystem _fileSystem; private static IFileSystem _fileSystem;
private string _fakeExecutableDirectory; private string _fakeExecutableDirectory;
private List<MockFeed> _mockFeeds; private List<MockFeed> _mockFeeds;
private string _packageIdVersionDirectory;
public ToolPackageObtainerMock( public ToolPackageObtainerMock(
IFileSystem fileSystemWrapper = null, IFileSystem fileSystemWrapper = null,
bool useDefaultFeed = true, bool useDefaultFeed = true,
IEnumerable<MockFeed> additionalFeeds = null, IEnumerable<MockFeed> additionalFeeds = null,
Action beforeRunObtain = null) Action beforeRunObtain = null,
Action duringObtain = null,
string toolsPath = null)
{ {
_toolsPath = toolsPath ?? "toolsPath";
_beforeRunObtain = beforeRunObtain ?? (() => {}); _beforeRunObtain = beforeRunObtain ?? (() => {});
_duringObtain = duringObtain ?? (() => {});
_fileSystem = fileSystemWrapper ?? new FileSystemWrapper(); _fileSystem = fileSystemWrapper ?? new FileSystemWrapper();
_mockFeeds = new List<MockFeed>(); _mockFeeds = new List<MockFeed>();
@ -59,6 +68,18 @@ namespace Microsoft.DotNet.Tools.Tests.ComponentMocks
string source = null, string source = null,
string verbosity = null) string verbosity = null)
{ {
var stagedFile = Path.Combine(_toolsPath, ".stage", Path.GetRandomFileName());
bool afterStage = false;
var toolPackageObtainTransaction = new ToolPackageObtainTransaction(
obtainAndReturnExecutablePath: (_) =>
{
if (Directory.Exists(Path.Combine(_toolsPath, packageId)))
{
throw new PackageObtainException(
string.Format(CommonLocalizableStrings.ToolPackageConflictPackageId, packageId));
}
_beforeRunObtain(); _beforeRunObtain();
PickFeedByNugetConfig(nugetconfig); PickFeedByNugetConfig(nugetconfig);
@ -77,24 +98,67 @@ namespace Microsoft.DotNet.Tools.Tests.ComponentMocks
packageVersion = package.Version; packageVersion = package.Version;
targetframework = targetframework ?? "targetframework"; targetframework = targetframework ?? "targetframework";
var packageIdVersionDirectory = Path.Combine("toolPath", packageId, packageVersion); _packageIdVersionDirectory = Path.Combine(_toolsPath, packageId, packageVersion);
_fakeExecutableDirectory = Path.Combine(packageIdVersionDirectory, _fakeExecutableDirectory = Path.Combine(_packageIdVersionDirectory,
packageId, packageVersion, "morefolders", "tools", packageId, packageVersion, "morefolders", "tools",
targetframework); targetframework);
var fakeExecutable = Path.Combine(_fakeExecutableDirectory, FakeEntrypointName);
if (!_fileSystem.Directory.Exists(_fakeExecutableDirectory)) SimulateStageFile();
{ _duringObtain();
_fileSystem.File.Delete(stagedFile);
afterStage = true;
_fileSystem.Directory.CreateDirectory(_packageIdVersionDirectory);
_fileSystem.File.CreateEmptyFile(Path.Combine(_packageIdVersionDirectory, "project.assets.json"));
_fileSystem.Directory.CreateDirectory(_fakeExecutableDirectory); _fileSystem.Directory.CreateDirectory(_fakeExecutableDirectory);
} var fakeExecutable = Path.Combine(_fakeExecutableDirectory, FakeEntrypointName);
_fileSystem.File.CreateEmptyFile(Path.Combine(packageIdVersionDirectory, "project.assets.json"));
_fileSystem.File.CreateEmptyFile(fakeExecutable); _fileSystem.File.CreateEmptyFile(fakeExecutable);
return new ToolConfigurationAndExecutablePath( return new ToolConfigurationAndExecutablePath(
toolConfiguration: new ToolConfiguration(FakeCommandName, FakeEntrypointName), toolConfiguration: new ToolConfiguration(FakeCommandName, FakeEntrypointName),
executable: new FilePath(fakeExecutable)); executable : new FilePath(fakeExecutable));;
},
rollback: (_) =>
{
if (afterStage == false)
{
if (_fileSystem.File.Exists(stagedFile))
{
_fileSystem.File.Delete(stagedFile);
}
}
else
{
if (_fileSystem.Directory.Exists(Path.Combine(_toolsPath, packageId)))
{
_fileSystem.Directory.Delete(Path.Combine(_toolsPath, packageId), true);
}
}
}
);
using(var transactionScope = new TransactionScope())
{
Transaction.Current.EnlistVolatile(toolPackageObtainTransaction, EnlistmentOptions.None);
var toolConfigurationAndExecutablePath = toolPackageObtainTransaction.ObtainAndReturnExecutablePath();
transactionScope.Complete();
return toolConfigurationAndExecutablePath;
}
}
private void SimulateStageFile()
{
var stageDirectory = Path.Combine(_toolsPath, ".stage");
if (!_fileSystem.Directory.Exists(stageDirectory))
{
_fileSystem.Directory.CreateDirectory(stageDirectory);
}
_fileSystem.File.CreateEmptyFile(Path.Combine(stageDirectory, "stagedfile"));
} }
private void PickFeedBySource(string source) private void PickFeedBySource(string source)

View file

@ -106,6 +106,16 @@ namespace Microsoft.Extensions.DependencyModel.Tests
{ {
_files[path] = content; _files[path] = content;
} }
public void Delete(string path)
{
if (!Exists(path))
{
return;
}
_files.Remove(path);
}
} }
private class DirectoryMock : IDirectory private class DirectoryMock : IDirectory
@ -143,6 +153,22 @@ namespace Microsoft.Extensions.DependencyModel.Tests
{ {
_files.Add(path, path); _files.Add(path, path);
} }
public void Delete(string path, bool recursive)
{
if (!recursive && Exists(path) == true)
{
if (_files.Keys.Where(k => k.StartsWith(path)).Count() > 1)
{
throw new IOException("The directory is not empty");
}
}
foreach (var k in _files.Keys.Where(k => k.StartsWith(path)).ToList())
{
_files.Remove(k);
}
}
} }
private class TemporaryDirectoryMock : ITemporaryDirectoryMock private class TemporaryDirectoryMock : ITemporaryDirectoryMock

View file

@ -19,6 +19,7 @@ using Newtonsoft.Json;
using Xunit; using Xunit;
using Parser = Microsoft.DotNet.Cli.Parser; using Parser = Microsoft.DotNet.Cli.Parser;
using LocalizableStrings = Microsoft.DotNet.Tools.Install.Tool.LocalizableStrings; using LocalizableStrings = Microsoft.DotNet.Tools.Install.Tool.LocalizableStrings;
using System.Runtime.InteropServices;
namespace Microsoft.DotNet.Tests.InstallToolCommandTests namespace Microsoft.DotNet.Tests.InstallToolCommandTests
{ {
@ -32,20 +33,22 @@ namespace Microsoft.DotNet.Tests.InstallToolCommandTests
private readonly ParseResult _parseResult; private readonly ParseResult _parseResult;
private readonly BufferedReporter _reporter; private readonly BufferedReporter _reporter;
private const string PathToPlaceShim = "pathToPlace"; private const string PathToPlaceShim = "pathToPlace";
private const string PathToPlacePackages = PathToPlaceShim + "pkg";
private const string PackageId = "global.tool.console.demo";
public InstallToolCommandTests() public InstallToolCommandTests()
{ {
_fileSystemWrapper = new FileSystemMockBuilder().Build(); _fileSystemWrapper = new FileSystemMockBuilder().Build();
_toolPackageObtainerMock = new ToolPackageObtainerMock(_fileSystemWrapper); _toolPackageObtainerMock = new ToolPackageObtainerMock(_fileSystemWrapper, toolsPath: PathToPlacePackages);
_shellShimMakerMock = new ShellShimMakerMock(PathToPlaceShim, _fileSystemWrapper); _shellShimMakerMock = new ShellShimMakerMock(PathToPlaceShim, _fileSystemWrapper);
_reporter = new BufferedReporter(); _reporter = new BufferedReporter();
_environmentPathInstructionMock = _environmentPathInstructionMock =
new EnvironmentPathInstructionMock(_reporter, PathToPlaceShim); new EnvironmentPathInstructionMock(_reporter, PathToPlaceShim);
ParseResult result = Parser.Instance.Parse("dotnet install tool -g global.tool.console.demo"); ParseResult result = Parser.Instance.Parse($"dotnet install tool -g {PackageId}");
_appliedCommand = result["dotnet"]["install"]["tool"]; _appliedCommand = result["dotnet"]["install"]["tool"];
var parser = Parser.Instance; var parser = Parser.Instance;
_parseResult = parser.ParseFrom("dotnet install", new[] {"tool", "global.tool.console.demo"}); _parseResult = parser.ParseFrom("dotnet install", new[] {"tool", PackageId});
} }
[Fact] [Fact]
@ -60,12 +63,9 @@ namespace Microsoft.DotNet.Tests.InstallToolCommandTests
installToolCommand.Execute().Should().Be(0); installToolCommand.Execute().Should().Be(0);
// It is hard to simulate shell behavior. Only Assert shim can point to executable dll // It is hard to simulate shell behavior. Only Assert shim can point to executable dll
_fileSystemWrapper.File.Exists(Path.Combine("pathToPlace", ToolPackageObtainerMock.FakeCommandName)) _fileSystemWrapper.File.Exists(ExpectedCommandPath()).Should().BeTrue();
.Should().BeTrue();
var deserializedFakeShim = JsonConvert.DeserializeObject<ShellShimMakerMock.FakeShim>( var deserializedFakeShim = JsonConvert.DeserializeObject<ShellShimMakerMock.FakeShim>(
_fileSystemWrapper.File.ReadAllText( _fileSystemWrapper.File.ReadAllText(ExpectedCommandPath()));
Path.Combine("pathToPlace",
ToolPackageObtainerMock.FakeCommandName)));
_fileSystemWrapper.File.Exists(deserializedFakeShim.ExecutablePath).Should().BeTrue(); _fileSystemWrapper.File.Exists(deserializedFakeShim.ExecutablePath).Should().BeTrue();
} }
@ -73,11 +73,10 @@ namespace Microsoft.DotNet.Tests.InstallToolCommandTests
public void WhenRunWithPackageIdWithSourceItShouldCreateValidShim() public void WhenRunWithPackageIdWithSourceItShouldCreateValidShim()
{ {
const string sourcePath = "http://mysouce.com"; const string sourcePath = "http://mysouce.com";
ParseResult result = Parser.Instance.Parse($"dotnet install tool -g global.tool.console.demo --source {sourcePath}"); ParseResult result = Parser.Instance.Parse($"dotnet install tool -g {PackageId} --source {sourcePath}");
AppliedOption appliedCommand = result["dotnet"]["install"]["tool"]; AppliedOption appliedCommand = result["dotnet"]["install"]["tool"];
const string packageId = "global.tool.console.demo";
ParseResult parseResult = ParseResult parseResult =
Parser.Instance.ParseFrom("dotnet install", new[] {"tool", packageId, "--source", sourcePath}); Parser.Instance.ParseFrom("dotnet install", new[] { "tool", PackageId, "--source", sourcePath });
var installToolCommand = new InstallToolCommand(appliedCommand, var installToolCommand = new InstallToolCommand(appliedCommand,
parseResult, parseResult,
@ -91,7 +90,7 @@ namespace Microsoft.DotNet.Tests.InstallToolCommandTests
{ {
new MockFeedPackage new MockFeedPackage
{ {
PackageId = packageId, PackageId = PackageId,
Version = "1.0.4" Version = "1.0.4"
} }
} }
@ -103,13 +102,11 @@ namespace Microsoft.DotNet.Tests.InstallToolCommandTests
installToolCommand.Execute().Should().Be(0); installToolCommand.Execute().Should().Be(0);
// It is hard to simulate shell behavior. Only Assert shim can point to executable dll // It is hard to simulate shell behavior. Only Assert shim can point to executable dll
_fileSystemWrapper.File.Exists(Path.Combine("pathToPlace", ToolPackageObtainerMock.FakeCommandName)) _fileSystemWrapper.File.Exists(ExpectedCommandPath())
.Should().BeTrue(); .Should().BeTrue();
ShellShimMakerMock.FakeShim deserializedFakeShim = ShellShimMakerMock.FakeShim deserializedFakeShim =
JsonConvert.DeserializeObject<ShellShimMakerMock.FakeShim>( JsonConvert.DeserializeObject<ShellShimMakerMock.FakeShim>(
_fileSystemWrapper.File.ReadAllText( _fileSystemWrapper.File.ReadAllText(ExpectedCommandPath()));
Path.Combine("pathToPlace",
ToolPackageObtainerMock.FakeCommandName)));
_fileSystemWrapper.File.Exists(deserializedFakeShim.ExecutablePath).Should().BeTrue(); _fileSystemWrapper.File.Exists(deserializedFakeShim.ExecutablePath).Should().BeTrue();
} }
@ -157,6 +154,49 @@ namespace Microsoft.DotNet.Tests.InstallToolCommandTests
.Contain(string.Format(LocalizableStrings.ToolInstallationFailed, "global.tool.console.demo")); .Contain(string.Format(LocalizableStrings.ToolInstallationFailed, "global.tool.console.demo"));
} }
[Fact]
public void GivenFailedPackageObtainWhenRunWithPackageIdItShouldHaveNoBrokenFolderOnDisk()
{
var toolPackageObtainerSimulatorThatThrows
= new ToolPackageObtainerMock(
_fileSystemWrapper, true, null,
duringObtain: () => throw new PackageObtainException("Simulated error"),
toolsPath: PathToPlacePackages);
var installToolCommand = new InstallToolCommand(
_appliedCommand,
_parseResult,
toolPackageObtainerSimulatorThatThrows,
_shellShimMakerMock,
_environmentPathInstructionMock,
_reporter);
installToolCommand.Execute();
_fileSystemWrapper.Directory.Exists(Path.Combine(PathToPlacePackages, PackageId)).Should().BeFalse();
}
[Fact]
public void GivenCreateShimItShouldHaveNoBrokenFolderOnDisk()
{
_fileSystemWrapper.File.CreateEmptyFile(ExpectedCommandPath()); // Create conflict shim
var toolPackageObtainerSimulatorThatThrows
= new ToolPackageObtainerMock(
_fileSystemWrapper, true, null,
toolsPath: PathToPlacePackages);
var installToolCommand = new InstallToolCommand(
_appliedCommand,
_parseResult,
toolPackageObtainerSimulatorThatThrows,
_shellShimMakerMock,
_environmentPathInstructionMock,
_reporter);
Action a = () => installToolCommand.Execute();
a.ShouldThrow<GracefulException>();
_fileSystemWrapper.Directory.Exists(Path.Combine(PathToPlacePackages, PackageId)).Should().BeFalse();
}
[Fact] [Fact]
public void GivenInCorrectToolConfigurationWhenRunWithPackageIdItShouldFail() public void GivenInCorrectToolConfigurationWhenRunWithPackageIdItShouldFail()
{ {
@ -208,5 +248,13 @@ namespace Microsoft.DotNet.Tests.InstallToolCommandTests
.Single().Should() .Single().Should()
.Contain(string.Format(LocalizableStrings.InstallationSucceeded, "SimulatorCommand")); .Contain(string.Format(LocalizableStrings.InstallationSucceeded, "SimulatorCommand"));
} }
private static string ExpectedCommandPath()
{
var extension = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty;
return Path.Combine(
"pathToPlace",
ToolPackageObtainerMock.FakeCommandName + extension);
}
} }
} }