GivenAProjectToolsCommandResolver.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. using Microsoft.DotNet.Cli.Utils;
  4. using Microsoft.DotNet.CommandFactory;
  5. using Microsoft.DotNet.Tools.Test.Utilities;
  6. using NuGet.Frameworks;
  7. using NuGet.ProjectModel;
  8. using NuGet.Versioning;
  9. using LocalizableStrings = Microsoft.DotNet.CommandFactory.LocalizableStrings;
  10. namespace Microsoft.DotNet.Tests
  11. {
  12. public class GivenAProjectToolsCommandResolver : SdkTest
  13. {
  14. private static readonly NuGetFramework s_toolPackageFramework =
  15. FrameworkConstants.CommonFrameworks.NetCoreApp22;
  16. private const string TestProjectName = "AppWithToolDependency";
  17. public GivenAProjectToolsCommandResolver(ITestOutputHelper log) : base(log)
  18. {
  19. }
  20. [Fact]
  21. public void ItReturnsNullWhenCommandNameIsNull()
  22. {
  23. var projectToolsCommandResolver = SetupProjectToolsCommandResolver();
  24. var commandResolverArguments = new CommandResolverArguments()
  25. {
  26. CommandName = null,
  27. CommandArguments = new string[] { "" },
  28. ProjectDirectory = "/some/directory"
  29. };
  30. var result = projectToolsCommandResolver.Resolve(commandResolverArguments);
  31. result.Should().BeNull();
  32. }
  33. [Fact]
  34. public void ItReturnsNullWhenProjectDirectoryIsNull()
  35. {
  36. var projectToolsCommandResolver = SetupProjectToolsCommandResolver();
  37. var commandResolverArguments = new CommandResolverArguments()
  38. {
  39. CommandName = "command",
  40. CommandArguments = new string[] { "" },
  41. ProjectDirectory = null
  42. };
  43. var result = projectToolsCommandResolver.Resolve(commandResolverArguments);
  44. result.Should().BeNull();
  45. }
  46. [Fact]
  47. public void ItReturnsNullWhenProjectDirectoryDoesNotContainAProjectFile()
  48. {
  49. var projectToolsCommandResolver = SetupProjectToolsCommandResolver();
  50. var projectDirectory = _testAssetsManager.CreateTestDirectory();
  51. var commandResolverArguments = new CommandResolverArguments()
  52. {
  53. CommandName = "command",
  54. CommandArguments = new string[] { "" },
  55. ProjectDirectory = projectDirectory.Path
  56. };
  57. var result = projectToolsCommandResolver.Resolve(commandResolverArguments);
  58. result.Should().BeNull();
  59. }
  60. [Fact]
  61. public void ItReturnsNullWhenCommandNameDoesNotExistInProjectTools()
  62. {
  63. var projectToolsCommandResolver = SetupProjectToolsCommandResolver();
  64. var testInstance = _testAssetsManager.CopyTestAsset(TestProjectName)
  65. .WithSource();
  66. NuGetConfigWriter.Write(testInstance.Path, TestContext.Current.TestPackages);
  67. testInstance.Restore(Log);
  68. var commandResolverArguments = new CommandResolverArguments()
  69. {
  70. CommandName = "nonexistent-command",
  71. CommandArguments = null,
  72. ProjectDirectory = testInstance.Path
  73. };
  74. var result = projectToolsCommandResolver.Resolve(commandResolverArguments);
  75. result.Should().BeNull();
  76. }
  77. [Fact]
  78. public void ItReturnsACommandSpecWithDOTNETAsFileNameAndCommandNameInArgsWhenCommandNameExistsInProjectTools()
  79. {
  80. var projectToolsCommandResolver = SetupProjectToolsCommandResolver();
  81. var testInstance = _testAssetsManager.CopyTestAsset(TestProjectName)
  82. .WithSource();
  83. NuGetConfigWriter.Write(testInstance.Path, TestContext.Current.TestPackages);
  84. testInstance.Restore(Log);
  85. var commandResolverArguments = new CommandResolverArguments()
  86. {
  87. CommandName = "dotnet-portable",
  88. CommandArguments = null,
  89. ProjectDirectory = testInstance.Path
  90. };
  91. var result = projectToolsCommandResolver.Resolve(commandResolverArguments);
  92. result.Should().NotBeNull();
  93. var commandFile = Path.GetFileNameWithoutExtension(result.Path);
  94. commandFile.Should().Be("dotnet");
  95. result.Args.Should().Contain(commandResolverArguments.CommandName);
  96. }
  97. [Fact]
  98. public void ItEscapesCommandArgumentsWhenReturningACommandSpec()
  99. {
  100. var projectToolsCommandResolver = SetupProjectToolsCommandResolver();
  101. var testInstance = _testAssetsManager.CopyTestAsset(TestProjectName)
  102. .WithSource();
  103. NuGetConfigWriter.Write(testInstance.Path, TestContext.Current.TestPackages);
  104. testInstance.Restore(Log);
  105. var commandResolverArguments = new CommandResolverArguments()
  106. {
  107. CommandName = "dotnet-portable",
  108. CommandArguments = new[] { "arg with space" },
  109. ProjectDirectory = testInstance.Path
  110. };
  111. var result = projectToolsCommandResolver.Resolve(commandResolverArguments);
  112. result.Should().NotBeNull("Because the command is a project tool dependency");
  113. result.Args.Should().Contain("\"arg with space\"");
  114. }
  115. [Fact]
  116. public void ItReturnsACommandSpecWithArgsContainingCommandPathWhenReturningACommandSpecAndCommandArgumentsAreNull()
  117. {
  118. var projectToolsCommandResolver = SetupProjectToolsCommandResolver();
  119. var testInstance = _testAssetsManager.CopyTestAsset(TestProjectName)
  120. .WithSource();
  121. NuGetConfigWriter.Write(testInstance.Path, TestContext.Current.TestPackages);
  122. testInstance.Restore(Log);
  123. var commandResolverArguments = new CommandResolverArguments()
  124. {
  125. CommandName = "dotnet-portable",
  126. CommandArguments = null,
  127. ProjectDirectory = testInstance.Path
  128. };
  129. var result = projectToolsCommandResolver.Resolve(commandResolverArguments);
  130. result.Should().NotBeNull();
  131. var commandPath = result.Args.Trim('"');
  132. commandPath.Should().Contain("dotnet-portable.dll");
  133. }
  134. [Fact]
  135. public void ItReturnsACommandSpecWithArgsContainingCommandPathWhenInvokingAToolReferencedWithADifferentCasing()
  136. {
  137. var projectToolsCommandResolver = SetupProjectToolsCommandResolver();
  138. var testInstance = _testAssetsManager.CopyTestAsset(TestProjectName)
  139. .WithSource();
  140. NuGetConfigWriter.Write(testInstance.Path, TestContext.Current.TestPackages);
  141. testInstance.Restore(Log);
  142. var commandResolverArguments = new CommandResolverArguments()
  143. {
  144. CommandName = "dotnet-prefercliruntime",
  145. CommandArguments = null,
  146. ProjectDirectory = testInstance.Path
  147. };
  148. var result = projectToolsCommandResolver.Resolve(commandResolverArguments);
  149. result.Should().NotBeNull();
  150. var commandPath = result.Args.Trim('"');
  151. commandPath.Should().Contain("dotnet-prefercliruntime.dll");
  152. }
  153. [Fact]
  154. public void ItWritesADepsJsonFileNextToTheLockfile()
  155. {
  156. var projectToolsCommandResolver = SetupProjectToolsCommandResolver();
  157. var testInstance = _testAssetsManager.CopyTestAsset(TestProjectName)
  158. .WithSource()
  159. .WithRepoGlobalPackages();
  160. NuGetConfigWriter.Write(testInstance.Path, TestContext.Current.TestPackages);
  161. testInstance.Restore(Log);
  162. var commandResolverArguments = new CommandResolverArguments()
  163. {
  164. CommandName = "dotnet-portable",
  165. CommandArguments = null,
  166. ProjectDirectory = testInstance.Path
  167. };
  168. var nugetPackagesRoot = TestContext.Current.TestGlobalPackagesFolder;
  169. var toolPathCalculator = new ToolPathCalculator(nugetPackagesRoot);
  170. var lockFilePath = toolPathCalculator.GetLockFilePath(
  171. "dotnet-portable",
  172. new NuGetVersion("1.0.0"),
  173. s_toolPackageFramework);
  174. var directory = Path.GetDirectoryName(lockFilePath);
  175. var depsJsonFile = Directory
  176. .EnumerateFiles(directory)
  177. .FirstOrDefault(p => Path.GetFileName(p).EndsWith(FileNameSuffixes.DepsJson));
  178. if (depsJsonFile != null)
  179. {
  180. File.Delete(depsJsonFile);
  181. }
  182. var result = projectToolsCommandResolver.Resolve(commandResolverArguments);
  183. result.Should().NotBeNull();
  184. new DirectoryInfo(directory)
  185. .Should().HaveFilesMatching("*.deps.json", SearchOption.TopDirectoryOnly);
  186. }
  187. [Fact]
  188. public void GenerateDepsJsonMethodDoesntOverwriteWhenDepsFileAlreadyExists()
  189. {
  190. var testInstance = _testAssetsManager.CopyTestAsset(TestProjectName)
  191. .WithSource()
  192. .WithRepoGlobalPackages();
  193. NuGetConfigWriter.Write(testInstance.Path, TestContext.Current.TestPackages);
  194. testInstance.Restore(Log);
  195. var toolPathCalculator = new ToolPathCalculator(TestContext.Current.TestGlobalPackagesFolder);
  196. var lockFilePath = toolPathCalculator.GetLockFilePath(
  197. "dotnet-portable",
  198. new NuGetVersion("1.0.0"),
  199. s_toolPackageFramework);
  200. var lockFile = new LockFileFormat().Read(lockFilePath);
  201. // NOTE: We must not use the real deps.json path here as it will interfere with tests running in parallel.
  202. var depsJsonFile = Path.GetTempFileName();
  203. File.WriteAllText(depsJsonFile, "temp");
  204. var projectToolsCommandResolver = SetupProjectToolsCommandResolver();
  205. projectToolsCommandResolver.GenerateDepsJsonFile(
  206. lockFile,
  207. s_toolPackageFramework,
  208. depsJsonFile,
  209. new SingleProjectInfo("dotnet-portable", "1.0.0", Enumerable.Empty<ResourceAssemblyInfo>()),
  210. GetToolDepsJsonGeneratorProject());
  211. File.ReadAllText(depsJsonFile).Should().Be("temp");
  212. File.Delete(depsJsonFile);
  213. }
  214. [Fact]
  215. public void ItDoesNotAddFxVersionAsAParamWhenTheToolDoesNotHaveThePrefercliruntimeFile()
  216. {
  217. var projectToolsCommandResolver = SetupProjectToolsCommandResolver();
  218. var testInstance = _testAssetsManager.CopyTestAsset(TestProjectName)
  219. .WithSource();
  220. NuGetConfigWriter.Write(testInstance.Path, TestContext.Current.TestPackages);
  221. testInstance.Restore(Log);
  222. var commandResolverArguments = new CommandResolverArguments()
  223. {
  224. CommandName = "dotnet-portable",
  225. CommandArguments = null,
  226. ProjectDirectory = testInstance.Path
  227. };
  228. var result = projectToolsCommandResolver.Resolve(commandResolverArguments);
  229. result.Should().NotBeNull();
  230. result.Args.Should().NotContain("--fx-version");
  231. }
  232. [Fact]
  233. public void ItFindsToolsLocatedInTheNuGetFallbackFolder()
  234. {
  235. var testInstance = _testAssetsManager.CopyTestAsset("AppWithFallbackFolderToolDependency")
  236. .WithSource();
  237. var testProjectDirectory = testInstance.Path;
  238. var fallbackFolder = Path.Combine(testProjectDirectory, "fallbackFolder");
  239. var nugetConfig = UseNuGetConfigWithFallbackFolder(testInstance, fallbackFolder, TestContext.Current.TestPackages);
  240. PopulateFallbackFolder(testProjectDirectory, fallbackFolder);
  241. new DotnetRestoreCommand(Log)
  242. .WithWorkingDirectory(testProjectDirectory)
  243. .Execute()
  244. .Should()
  245. .Pass();
  246. new DotnetCommand(Log)
  247. .WithWorkingDirectory(testProjectDirectory)
  248. .Execute($"fallbackfoldertool").Should().Pass();
  249. }
  250. [Fact]
  251. public void ItShowsAnErrorWhenTheToolDllIsNotFound()
  252. {
  253. var testInstance = _testAssetsManager.CopyTestAsset("AppWithFallbackFolderToolDependency")
  254. .WithSource();
  255. var testProjectDirectory = testInstance.Path;
  256. var fallbackFolder = Path.Combine(testProjectDirectory, "fallbackFolder");
  257. var nugetPackages = Path.Combine(testProjectDirectory, "nugetPackages");
  258. var nugetConfig = UseNuGetConfigWithFallbackFolder(testInstance, fallbackFolder, TestContext.Current.TestPackages);
  259. PopulateFallbackFolder(testProjectDirectory, fallbackFolder);
  260. new DotnetRestoreCommand(Log)
  261. .WithWorkingDirectory(testProjectDirectory)
  262. .Execute($"/p:RestorePackagesPath={nugetPackages}")
  263. .Should()
  264. .Pass();
  265. // We need to run the tool once to generate the deps.json
  266. // otherwise we end up with a different error message.
  267. new DotnetCommand(Log)
  268. .WithWorkingDirectory(testProjectDirectory)
  269. .Execute("fallbackfoldertool", $"/p:RestorePackagesPath={nugetPackages}").Should().Pass();
  270. Directory.Delete(Path.Combine(fallbackFolder, "dotnet-fallbackfoldertool"), true);
  271. new DotnetCommand(Log)
  272. .WithWorkingDirectory(testProjectDirectory)
  273. .Execute("fallbackfoldertool", $"/p:RestorePackagesPath={nugetPackages}")
  274. .Should().Fail().And.NotHaveStdOutContaining(string.Format(LocalizableStrings.CommandAssembliesNotFound, "dotnet-fallbackfoldertool"));
  275. }
  276. private void PopulateFallbackFolder(string testProjectDirectory, string fallbackFolder)
  277. {
  278. var nugetConfigPath = Path.Combine(testProjectDirectory, "NuGet.Config");
  279. new DotnetRestoreCommand(Log)
  280. .WithWorkingDirectory(testProjectDirectory)
  281. .Execute("--packages", fallbackFolder)
  282. .Should()
  283. .Pass();
  284. Directory.Delete(Path.Combine(fallbackFolder, ".tools"), true);
  285. }
  286. private string UseNuGetConfigWithFallbackFolder(TestAsset testInstance, string fallbackFolder, string testPackagesSource)
  287. {
  288. var nugetConfig = Path.Combine(testInstance.Path, "NuGet.Config");
  289. File.WriteAllText(
  290. nugetConfig,
  291. $@"<?xml version=""1.0"" encoding=""utf-8""?>
  292. <configuration>
  293. <packageSources>
  294. <add key=""{Guid.NewGuid().ToString()}"" value=""{testPackagesSource}"" />
  295. </packageSources>
  296. <fallbackPackageFolders>
  297. <add key=""MachineWide"" value=""{fallbackFolder}""/>
  298. </fallbackPackageFolders>
  299. </configuration>
  300. ");
  301. return nugetConfig;
  302. }
  303. private ProjectToolsCommandResolver SetupProjectToolsCommandResolver()
  304. {
  305. var packagedCommandSpecFactory = new PackagedCommandSpecFactoryWithCliRuntime();
  306. var projectToolsCommandResolver =
  307. new ProjectToolsCommandResolver(packagedCommandSpecFactory, new EnvironmentProvider());
  308. return projectToolsCommandResolver;
  309. }
  310. private string GetToolDepsJsonGeneratorProject()
  311. {
  312. // When using the product, the ToolDepsJsonGeneratorProject property is used to get this path, but for testing
  313. // we'll hard code the path inside the SDK since we don't have a project to evaluate here
  314. return Path.Combine(TestContext.Current.ToolsetUnderTest.SdksPath, "Microsoft.NET.Sdk", "targets", "GenerateDeps", "GenerateDeps.proj");
  315. }
  316. }
  317. }