File: LocalToolsResolverCacheTests.cs
Web Access
Project: ..\..\..\test\Microsoft.DotNet.PackageInstall.Tests\Microsoft.DotNet.PackageInstall.Tests.csproj (Microsoft.DotNet.PackageInstall.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.DotNet.Cli.ToolPackage;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.DependencyModel.Tests;
using Microsoft.Extensions.EnvironmentAbstractions;
using NuGet.Frameworks;
using NuGet.Versioning;
 
namespace Microsoft.DotNet.PackageInstall.Tests
{
    public class LocalToolsResolverCacheTests : SdkTest
    {
        public LocalToolsResolverCacheTests(ITestOutputHelper log) : base(log)
        {
        }
 
        private static
            (DirectoryPath nuGetGlobalPackagesFolder,
            LocalToolsResolverCache localToolsResolverCache) Setup()
        {
            IFileSystem fileSystem = new FileSystemMockBuilder().UseCurrentSystemTemporaryDirectory().Build();
            DirectoryPath tempDirectory =
                new(fileSystem.Directory.CreateTemporaryDirectory().DirectoryPath);
            DirectoryPath cacheDirectory = tempDirectory.WithSubDirectories("cacheDirectory");
            DirectoryPath nuGetGlobalPackagesFolder = tempDirectory.WithSubDirectories("nugetGlobalPackageLocation");
            fileSystem.Directory.CreateDirectory(cacheDirectory.Value);
            const int version = 1;
 
            LocalToolsResolverCache localToolsResolverCache =
                new(fileSystem, cacheDirectory, version);
            return (nuGetGlobalPackagesFolder, localToolsResolverCache);
        }
 
        [Fact]
        public void GivenExecutableIdentifierItCanSaveAndCannotLoadWithMismatches()
        {
            (DirectoryPath nuGetGlobalPackagesFolder, LocalToolsResolverCache localToolsResolverCache) = Setup();
 
            NuGetFramework targetFramework = NuGetFramework.Parse(ToolsetInfo.CurrentTargetFramework);
            string runtimeIdentifier = Constants.AnyRid;
            PackageId packageId = new("my.toolBundle");
            NuGetVersion nuGetVersion = NuGetVersion.Parse("1.0.2");
            IReadOnlyList<ToolCommand> restoredCommands = new[]
            {
                new ToolCommand(new ToolCommandName("tool1"), "dotnet", nuGetGlobalPackagesFolder.WithFile("tool1.dll")),
                new ToolCommand(new ToolCommandName("tool2"), "dotnet", nuGetGlobalPackagesFolder.WithFile("tool2.dll"))
            };
 
            localToolsResolverCache.Save(
                restoredCommands.ToDictionary(
                    c => new RestoredCommandIdentifier(packageId, nuGetVersion, targetFramework, runtimeIdentifier,
                        c.Name)));
 
            localToolsResolverCache
                .TryLoad(
                    new RestoredCommandIdentifier(packageId, NuGetVersion.Parse("1.0.0-wrong-version"), targetFramework,
                        runtimeIdentifier, restoredCommands[0].Name), out _)
                .Should().BeFalse();
 
            localToolsResolverCache
                .TryLoad(
                    new RestoredCommandIdentifier(packageId, nuGetVersion, NuGetFramework.Parse("wrongFramework"),
                        runtimeIdentifier, restoredCommands[0].Name), out _)
                .Should().BeFalse();
 
            localToolsResolverCache
                .TryLoad(
                    new RestoredCommandIdentifier(packageId, nuGetVersion, targetFramework,
                        "wrongRuntimeIdentifier", restoredCommands[0].Name),
                    out _)
                .Should().BeFalse();
        }
 
        [Fact]
        public void GivenExecutableIdentifierItCanSaveAndLoad()
        {
            (DirectoryPath nuGetGlobalPackagesFolder, LocalToolsResolverCache localToolsResolverCache) = Setup();
 
            NuGetFramework targetFramework = NuGetFramework.Parse(ToolsetInfo.CurrentTargetFramework);
            string runtimeIdentifier = Constants.AnyRid;
            PackageId packageId = new("my.toolBundle");
            NuGetVersion nuGetVersion = NuGetVersion.Parse("1.0.2");
            IReadOnlyList<ToolCommand> restoredCommands = new[]
            {
                new ToolCommand(new ToolCommandName("tool1"), "dotnet", nuGetGlobalPackagesFolder.WithFile("tool1.dll")),
                new ToolCommand(new ToolCommandName("tool2"), "dotnet", nuGetGlobalPackagesFolder.WithFile("tool2.dll"))
            };
 
            localToolsResolverCache.Save(
                restoredCommands.ToDictionary(
                    c => new RestoredCommandIdentifier(packageId, nuGetVersion, targetFramework, runtimeIdentifier,
                        c.Name)));
 
            localToolsResolverCache.TryLoad(
                new RestoredCommandIdentifier(packageId, nuGetVersion, targetFramework, runtimeIdentifier,
                    restoredCommands[0].Name),
                out ToolCommand tool1).Should().BeTrue();
 
            localToolsResolverCache.TryLoad(
                new RestoredCommandIdentifier(packageId, nuGetVersion, targetFramework, runtimeIdentifier,
                    restoredCommands[1].Name),
                out ToolCommand tool2).Should().BeTrue();
 
            tool1.Should().BeEquivalentTo(restoredCommands[0]);
            tool2.Should().BeEquivalentTo(restoredCommands[1]);
        }
 
        [Fact]
        public void GivenExecutableIdentifierItCanSaveMultipleSameAndLoadContainsOnlyOne()
        {
            (DirectoryPath nuGetGlobalPackagesFolder, LocalToolsResolverCache localToolsResolverCache) = Setup();
 
            NuGetFramework targetFramework = NuGetFramework.Parse(ToolsetInfo.CurrentTargetFramework);
            string runtimeIdentifier = Constants.AnyRid;
            PackageId packageId = new("my.toolBundle");
            NuGetVersion nuGetVersion = NuGetVersion.Parse("1.0.2");
            IReadOnlyList<ToolCommand> restoredCommands = new[]
            {
                new ToolCommand(new ToolCommandName("tool1"), "dotnet", nuGetGlobalPackagesFolder.WithFile("tool1.dll")),
                new ToolCommand(new ToolCommandName("tool2"), "dotnet", nuGetGlobalPackagesFolder.WithFile("tool2.dll"))
            };
 
            localToolsResolverCache.Save(
                restoredCommands.ToDictionary(
                    c => new RestoredCommandIdentifier(packageId, nuGetVersion, targetFramework, runtimeIdentifier,
                        c.Name)));
 
            localToolsResolverCache.Save(
                restoredCommands.ToDictionary(
                    c => new RestoredCommandIdentifier(packageId, nuGetVersion, targetFramework, runtimeIdentifier,
                        c.Name)));
 
            localToolsResolverCache.TryLoad(
                new RestoredCommandIdentifier(packageId, nuGetVersion, targetFramework, runtimeIdentifier,
                    restoredCommands[0].Name),
                out ToolCommand tool1);
 
            localToolsResolverCache.TryLoad(
                new RestoredCommandIdentifier(packageId, nuGetVersion, targetFramework, runtimeIdentifier,
                    restoredCommands[1].Name),
                out ToolCommand tool2);
 
 
            tool1.Should().BeEquivalentTo(restoredCommands[0]);
            tool2.Should().BeEquivalentTo(restoredCommands[1]);
        }
 
        [Fact]
        public void GivenExecutableIdentifierItCanSaveMultipleVersionAndLoad()
        {
            (DirectoryPath nuGetGlobalPackagesFolder, LocalToolsResolverCache localToolsResolverCache) = Setup();
 
            NuGetFramework targetFramework = NuGetFramework.Parse(ToolsetInfo.CurrentTargetFramework);
            string runtimeIdentifier = Constants.AnyRid;
            PackageId packageId = new("my.toolBundle");
            NuGetVersion nuGetVersion = NuGetVersion.Parse("1.0.2");
            IReadOnlyList<ToolCommand> restoredCommands = new[]
            {
                new ToolCommand(new ToolCommandName("tool1"), "dotnet", nuGetGlobalPackagesFolder.WithFile("tool1.dll")),
            };
 
            NuGetVersion newerNuGetVersion = NuGetVersion.Parse("2.0.2");
            IReadOnlyList<ToolCommand> restoredCommandsNewer = new[]
            {
                new ToolCommand(new ToolCommandName("tool1"), "dotnet", nuGetGlobalPackagesFolder.WithFile("tool1new.dll")),
                new ToolCommand(new ToolCommandName("tool2"), "dotnet", nuGetGlobalPackagesFolder.WithFile("tool2new.dll")),
            };
 
            localToolsResolverCache.Save(
                restoredCommands.ToDictionary(
                    c => new RestoredCommandIdentifier(packageId, nuGetVersion, targetFramework, runtimeIdentifier,
                        c.Name)));
 
            localToolsResolverCache.Save(
                restoredCommandsNewer.ToDictionary(
                    c => new RestoredCommandIdentifier(packageId, newerNuGetVersion, targetFramework, runtimeIdentifier,
                        c.Name)));
 
            localToolsResolverCache.TryLoad(
                new RestoredCommandIdentifier(packageId, nuGetVersion, targetFramework, runtimeIdentifier,
                    restoredCommands[0].Name),
                out ToolCommand tool1);
            localToolsResolverCache.TryLoad(
                new RestoredCommandIdentifier(packageId, newerNuGetVersion, targetFramework, runtimeIdentifier,
                    restoredCommandsNewer[0].Name),
                out ToolCommand tool1Newer);
 
            localToolsResolverCache.TryLoad(
                new RestoredCommandIdentifier(packageId, newerNuGetVersion, targetFramework, runtimeIdentifier,
                    restoredCommandsNewer[1].Name),
                out ToolCommand tool2Newer);
 
            tool1.Should().BeEquivalentTo(restoredCommands[0]);
            tool1Newer.Should().BeEquivalentTo(restoredCommandsNewer[0]);
            tool2Newer.Should().BeEquivalentTo(restoredCommandsNewer[1]);
        }
 
        [Fact]
        public void WhenTheCacheIsCorruptedByAppendingLineItShouldLoadAsEmpty()
        {
            WhenTheCacheIsCorruptedItShouldLoadAsEmpty(
                useRealFileSystem: false,
                corruptCache: (fileSystem, cachePath, existingCache) =>
                    fileSystem.File.WriteAllText(cachePath, existingCache + " !!!Corrupted")
            );
        }
 
        [Fact]
        public void WhenTheCacheIsCorruptedByNotAJsonItShouldLoadAsEmpty()
        {
            WhenTheCacheIsCorruptedItShouldLoadAsEmpty(
                useRealFileSystem: true,
                corruptCache: (fileSystem, cachePath, existingCache) =>
                {
                    File.WriteAllBytes(cachePath, new byte[] { 0x12, 0x23, 0x34, 0x45 });
                }
            );
        }
 
        [Fact]
        public void WhenTheCacheIsCorruptedItShouldNotAffectNextSaveAndLoad()
        {
            IFileSystem fileSystem = new FileSystemMockBuilder().UseCurrentSystemTemporaryDirectory().Build();
 
            DirectoryPath tempDirectory =
                new(fileSystem.Directory.CreateTemporaryDirectory().DirectoryPath);
            DirectoryPath cacheDirectory = tempDirectory.WithSubDirectories("cacheDirectory");
            DirectoryPath nuGetGlobalPackagesFolder = tempDirectory.WithSubDirectories("nugetGlobalPackageLocation");
            fileSystem.Directory.CreateDirectory(cacheDirectory.Value);
            const int version = 1;
 
            LocalToolsResolverCache localToolsResolverCache =
                new(fileSystem, cacheDirectory, version);
 
            NuGetFramework targetFramework = NuGetFramework.Parse(ToolsetInfo.CurrentTargetFramework);
            string runtimeIdentifier = Constants.AnyRid;
            PackageId packageId = new("my.toolBundle");
            NuGetVersion nuGetVersion = NuGetVersion.Parse("1.0.2");
            IReadOnlyList<ToolCommand> restoredCommands = new[]
            {
                new ToolCommand(new ToolCommandName("tool1"), "dotnet", nuGetGlobalPackagesFolder.WithFile("tool1.dll")),
            };
 
            localToolsResolverCache.Save(
                restoredCommands.ToDictionary(
                    c => new RestoredCommandIdentifier(packageId, nuGetVersion, targetFramework, runtimeIdentifier,
                        c.Name)));
 
            var cachePath = cacheDirectory
                .WithSubDirectories(version.ToString())
                .WithSubDirectories(packageId.ToString()).Value;
            var existingCache =
                fileSystem.File.ReadAllText(
                    cachePath);
            existingCache.Should().NotBeEmpty();
 
            fileSystem.File.WriteAllText(cachePath, existingCache + " !!!Corrupted");
 
            // Save after corruption
            localToolsResolverCache.Save(
                restoredCommands.ToDictionary(
                    c => new RestoredCommandIdentifier(packageId, nuGetVersion, targetFramework, runtimeIdentifier,
                        c.Name)));
 
            localToolsResolverCache.TryLoad(
                new RestoredCommandIdentifier(packageId, nuGetVersion, targetFramework, runtimeIdentifier,
                    restoredCommands[0].Name),
                out ToolCommand restoredCommand);
 
            restoredCommand.Should().BeEquivalentTo(restoredCommands[0]);
        }
 
        private static void WhenTheCacheIsCorruptedItShouldLoadAsEmpty(
            bool useRealFileSystem,
            Action<IFileSystem, string, string> corruptCache)
        {
            IFileSystem fileSystem =
                useRealFileSystem == false
                    ? new FileSystemMockBuilder().UseCurrentSystemTemporaryDirectory().Build()
                    : new FileSystemWrapper();
 
            DirectoryPath tempDirectory =
                new(fileSystem.Directory.CreateTemporaryDirectory().DirectoryPath);
            DirectoryPath cacheDirectory = tempDirectory.WithSubDirectories("cacheDirectory");
            DirectoryPath nuGetGlobalPackagesFolder = tempDirectory.WithSubDirectories("nugetGlobalPackageLocation");
            fileSystem.Directory.CreateDirectory(cacheDirectory.Value);
            const int version = 1;
 
            LocalToolsResolverCache localToolsResolverCache =
                new(fileSystem, cacheDirectory, version);
 
            NuGetFramework targetFramework = NuGetFramework.Parse(ToolsetInfo.CurrentTargetFramework);
            string runtimeIdentifier = Constants.AnyRid;
            PackageId packageId = new("my.toolBundle");
            NuGetVersion nuGetVersion = NuGetVersion.Parse("1.0.2");
            IReadOnlyList<ToolCommand> restoredCommands = new[]
            {
                new ToolCommand(new ToolCommandName("tool1"), "dotnet", nuGetGlobalPackagesFolder.WithFile("tool1.dll")),
            };
 
            localToolsResolverCache.Save(
                restoredCommands.ToDictionary(
                    c => new RestoredCommandIdentifier(packageId, nuGetVersion, targetFramework, runtimeIdentifier,
                        c.Name)));
 
            var cachePath = cacheDirectory
                .WithSubDirectories(version.ToString())
                .WithSubDirectories(packageId.ToString()).Value;
            var existingCache =
                fileSystem.File.ReadAllText(
                    cachePath);
            existingCache.Should().NotBeEmpty();
 
            corruptCache(fileSystem, cachePath, existingCache);
 
            localToolsResolverCache.TryLoad(
                new RestoredCommandIdentifier(packageId, nuGetVersion, targetFramework, runtimeIdentifier,
                    restoredCommands[0].Name),
                out _).Should().BeFalse("Consider corrupted file cache miss");
        }
    }
}