File: CommandTests\Workload\Install\GivenWorkloadManifestUpdater.cs
Web Access
Project: ..\..\..\test\dotnet.Tests\dotnet.Tests.csproj (dotnet.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using System.Runtime.CompilerServices;
using System.Text.Json;
using FluentAssertions.Extensions;
using ManifestReaderTests;
using Microsoft.DotNet.Cli.Commands;
using Microsoft.DotNet.Cli.Commands.Workload.Config;
using Microsoft.DotNet.Cli.Commands.Workload.Install;
using Microsoft.DotNet.Cli.NuGetPackageDownloader;
using Microsoft.DotNet.Cli.ToolPackage;
using Microsoft.Extensions.EnvironmentAbstractions;
using Microsoft.NET.Sdk.WorkloadManifestReader;
using NuGet.Versioning;
 
namespace Microsoft.DotNet.Cli.Workload.Install.Tests
{
    public class GivenWorkloadManifestUpdater : SdkTest
    {
        private readonly BufferedReporter _reporter;
        private readonly string _manifestFileName = "WorkloadManifest.json";
        private readonly string _manifestSentinelFileName = ".workloadAdvertisingManifestSentinel";
        private readonly ManifestId[] _installedManifests;
 
        public GivenWorkloadManifestUpdater(ITestOutputHelper log) : base(log)
        {
            _reporter = new BufferedReporter();
            _installedManifests = new ManifestId[] { new ManifestId("test-manifest-1"), new ManifestId("test-manifest-2"), new ManifestId("test-manifest-3") };
        }
 
        [Fact]
        public async Task GivenWorkloadManifestUpdateItCanUpdateAdvertisingManifests()
        {
            (var manifestUpdater, var nugetDownloader, _, _) = GetTestUpdater();
 
            await manifestUpdater.UpdateAdvertisingManifestsAsync(true);
            nugetDownloader.DownloadCallParams.Should().BeEquivalentTo(GetExpectedDownloadedPackages());
        }
 
        [Fact]
        public async Task GivenAdvertisingManifestUpdateItUpdatesWhenNoSentinelExists()
        {
            (var manifestUpdater, var nugetDownloader, var sentinelPath, var configCommand) = GetTestUpdater();
 
            configCommand.Execute().Should().Be(0);
            await manifestUpdater.BackgroundUpdateAdvertisingManifestsWhenRequiredAsync();
            nugetDownloader.DownloadCallParams.Should().BeEquivalentTo(GetExpectedDownloadedPackages());
            File.Exists(sentinelPath).Should().BeTrue();
        }
 
        [Fact]
        public async Task GivenAdvertisingManifestUpdateItUpdatesWhenDue()
        {
            Func<string, string> getEnvironmentVariable = (envVar) => envVar.Equals(EnvironmentVariableNames.WORKLOAD_UPDATE_NOTIFY_INTERVAL_HOURS) ? "0" : string.Empty;
            (var manifestUpdater, var nugetDownloader, var sentinelPath, var configCommand) = GetTestUpdater(getEnvironmentVariable: getEnvironmentVariable);
 
            File.WriteAllText(sentinelPath, string.Empty);
            var createTime = DateTime.Now;
 
            configCommand.Execute().Should().Be(0);
            await manifestUpdater.BackgroundUpdateAdvertisingManifestsWhenRequiredAsync();
 
            nugetDownloader.DownloadCallParams.Should().BeEquivalentTo(GetExpectedDownloadedPackages());
            File.Exists(sentinelPath).Should().BeTrue();
            File.GetLastAccessTime(sentinelPath).Should().BeAfter(createTime);
        }
 
        [Fact]
        public async Task GivenAdvertisingManifestUpdateItDoesNotUpdateWhenNotDue()
        {
            (var manifestUpdater, var nugetDownloader, var sentinelPath, _) = GetTestUpdater();
 
            File.Create(sentinelPath).Close();
            var createTime = DateTime.Now;
 
            await manifestUpdater.BackgroundUpdateAdvertisingManifestsWhenRequiredAsync();
            nugetDownloader.DownloadCallParams.Should().BeEmpty();
            File.GetLastAccessTime(sentinelPath).Should().BeBefore(createTime);
        }
 
        [Fact]
        public async Task GivenAdvertisingManifestUpdateItHonorsDisablingEnvVar()
        {
            Func<string, string> getEnvironmentVariable = (envVar) => envVar.Equals(EnvironmentVariableNames.WORKLOAD_UPDATE_NOTIFY_DISABLE) ? "true" : string.Empty;
            (var manifestUpdater, var nugetDownloader, _, _) = GetTestUpdater(getEnvironmentVariable: getEnvironmentVariable);
 
            await manifestUpdater.BackgroundUpdateAdvertisingManifestsWhenRequiredAsync();
            nugetDownloader.DownloadCallParams.Should().BeEmpty();
        }
 
        [Fact]
        public void GivenWorkloadManifestUpdateItCanCalculateUpdates()
        {
            var testDir = _testAssetsManager.CreateTestDirectory().Path;
            var featureBand = "6.0.100";
            var dotnetRoot = Path.Combine(testDir, "dotnet");
            var expectedManifestUpdates = new TestManifestUpdate[] {
                new TestManifestUpdate(new ManifestId("test-manifest-1"), new ManifestVersion("5.0.0"), featureBand, new ManifestVersion("7.0.0"), featureBand),
                new TestManifestUpdate(new ManifestId("test-manifest-2"), new ManifestVersion("3.0.0"), featureBand, new ManifestVersion("4.0.0"), featureBand) };
            var expectedManifestNotUpdated = new ManifestId[] { new ManifestId("test-manifest-3"), new ManifestId("test-manifest-4") };
 
            // Write mock manifests
            var installedManifestDir = Path.Combine(testDir, "dotnet", "sdk-manifests", featureBand);
            var adManifestDir = Path.Combine(testDir, ".dotnet", "sdk-advertising", featureBand);
            Directory.CreateDirectory(installedManifestDir);
            Directory.CreateDirectory(adManifestDir);
            foreach (var manifestUpdate in expectedManifestUpdates)
            {
                Directory.CreateDirectory(Path.Combine(installedManifestDir, manifestUpdate.ManifestId.ToString()));
                File.WriteAllText(Path.Combine(installedManifestDir, manifestUpdate.ManifestId.ToString(), _manifestFileName), GetManifestContent(manifestUpdate.ExistingVersion));
                Directory.CreateDirectory(Path.Combine(adManifestDir, manifestUpdate.ManifestId.ToString()));
                File.WriteAllText(Path.Combine(adManifestDir, manifestUpdate.ManifestId.ToString(), _manifestFileName), GetManifestContent(manifestUpdate.NewVersion));
            }
            foreach (var manifest in expectedManifestNotUpdated)
            {
                Directory.CreateDirectory(Path.Combine(installedManifestDir, manifest.ToString()));
                File.WriteAllText(Path.Combine(installedManifestDir, manifest.ToString(), _manifestFileName), GetManifestContent(new ManifestVersion("5.0.0")));
                Directory.CreateDirectory(Path.Combine(adManifestDir, manifest.ToString()));
                File.WriteAllText(Path.Combine(adManifestDir, manifest.ToString(), _manifestFileName), GetManifestContent(new ManifestVersion("5.0.0")));
            }
 
            var manifestDirs = expectedManifestUpdates.Select(manifest => manifest.ManifestId)
                .Concat(expectedManifestNotUpdated)
                .Select(manifest => Path.Combine(installedManifestDir, manifest.ToString(), _manifestFileName))
                .ToArray();
            var workloadManifestProvider = new MockManifestProvider(manifestDirs);
            var nugetDownloader = new MockNuGetPackageDownloader(dotnetRoot);
            var workloadResolver = WorkloadResolver.CreateForTests(workloadManifestProvider, dotnetRoot);
            var installationRepo = new MockInstallationRecordRepository();
            var manifestUpdater = new WorkloadManifestUpdater(_reporter, workloadResolver, nugetDownloader, userProfileDir: Path.Combine(testDir, ".dotnet"), installationRepo, new MockPackWorkloadInstaller(dotnetRoot));
 
            var manifestUpdates = manifestUpdater.CalculateManifestUpdates().Select(m => m.ManifestUpdate);
            manifestUpdates.Should().BeEquivalentTo(expectedManifestUpdates.Select(u => u.ToManifestVersionUpdate()));
        }
 
 
        [Fact]
        public void GivenAdvertisedManifestsItCalculatesCorrectUpdates()
        {
            var testDir = _testAssetsManager.CreateTestDirectory().Path;
            var currentFeatureBand = "6.0.300";
            var dotnetRoot = Path.Combine(testDir, "dotnet");
            var expectedManifestUpdates = new TestManifestUpdate[] {
                new TestManifestUpdate(new ManifestId("test-manifest-1"), new ManifestVersion("5.0.0"), "6.0.100", new ManifestVersion("7.0.0"), "6.0.100"),
                new TestManifestUpdate(new ManifestId("test-manifest-2"), new ManifestVersion("3.0.0"), "6.0.100", new ManifestVersion("4.0.0"), "6.0.300"),
                new TestManifestUpdate(new ManifestId("test-manifest-3"), new ManifestVersion("3.0.0"), "6.0.300", new ManifestVersion("4.0.0"), "6.0.300")};
            var expectedManifestNotUpdated = new ManifestId[] { new ManifestId("test-manifest-4") };
 
            // Write mock manifests
            var adManifestDir = Path.Combine(testDir, ".dotnet", "sdk-advertising", currentFeatureBand);
            Directory.CreateDirectory(adManifestDir);
            foreach (var manifestUpdate in expectedManifestUpdates)
            {
                var installedManifestDir = Path.Combine(testDir, "dotnet", "sdk-manifests", manifestUpdate.ExistingFeatureBand);
                if (!Directory.Exists(installedManifestDir))
                {
                    Directory.CreateDirectory(installedManifestDir);
                }
 
                Directory.CreateDirectory(Path.Combine(installedManifestDir, manifestUpdate.ManifestId.ToString()));
                File.WriteAllText(Path.Combine(installedManifestDir, manifestUpdate.ManifestId.ToString(), _manifestFileName), GetManifestContent(manifestUpdate.ExistingVersion));
 
                var AdManifestPath = Path.Combine(adManifestDir, manifestUpdate.ManifestId.ToString());
                Directory.CreateDirectory(AdManifestPath);
                File.WriteAllText(Path.Combine(AdManifestPath, _manifestFileName), GetManifestContent(manifestUpdate.NewVersion));
                File.WriteAllText(Path.Combine(AdManifestPath, "AdvertisedManifestFeatureBand.txt"), manifestUpdate.NewFeatureBand);
 
            }
            foreach (var manifest in expectedManifestNotUpdated)
            {
                var installedManifestDir = Path.Combine(testDir, "dotnet", "sdk-manifests", currentFeatureBand);
                if (!Directory.Exists(installedManifestDir))
                {
                    Directory.CreateDirectory(installedManifestDir);
                }
 
                Directory.CreateDirectory(Path.Combine(installedManifestDir, manifest.ToString()));
                File.WriteAllText(Path.Combine(installedManifestDir, manifest.ToString(), _manifestFileName), GetManifestContent(new ManifestVersion("5.0.0")));
 
                var AdManifestPath = Path.Combine(adManifestDir, manifest.ToString());
                Directory.CreateDirectory(AdManifestPath);
                File.WriteAllText(Path.Combine(AdManifestPath, _manifestFileName), GetManifestContent(new ManifestVersion("5.0.0")));
                File.WriteAllText(Path.Combine(AdManifestPath, "AdvertisedManifestFeatureBand.txt"), currentFeatureBand);
            }
 
            var manifestInfo = expectedManifestUpdates.Select(
                    manifest => (manifest.ManifestId.ToString(), Path.Combine(testDir, "dotnet", "sdk-manifests", manifest.ExistingFeatureBand, manifest.ManifestId.ToString(), "WorkloadManifest.json"), manifest.ExistingVersion.ToString(), manifest.ExistingFeatureBand))
                .Concat(expectedManifestNotUpdated.Select(
                    manifestId => (manifestId.ToString(), Path.Combine(testDir, "dotnet", "sdk-manifests", currentFeatureBand, manifestId.ToString(), "WorkloadManifest.json"), "2.0.0", currentFeatureBand)))
                .ToArray();
 
            var workloadManifestProvider = new MockManifestProvider(manifestInfo)
            {
                SdkFeatureBand = new SdkFeatureBand(currentFeatureBand)
            };
            var nugetDownloader = new MockNuGetPackageDownloader(dotnetRoot);
            var workloadResolver = WorkloadResolver.CreateForTests(workloadManifestProvider, dotnetRoot);
            var installationRepo = new MockInstallationRecordRepository();
            var manifestUpdater = new WorkloadManifestUpdater(_reporter, workloadResolver, nugetDownloader, userProfileDir: Path.Combine(testDir, ".dotnet"), installationRepo, new MockPackWorkloadInstaller(dotnetRoot));
 
            var manifestUpdates = manifestUpdater.CalculateManifestUpdates().Select(m => m.ManifestUpdate);
            manifestUpdates.Should().BeEquivalentTo(expectedManifestUpdates.Select(u => u.ToManifestVersionUpdate()));
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public async Task ItCanFallbackAndAdvertiseCorrectUpdate(bool useOfflineCache)
        {
            //  Currently installed - 6.0.200 workload manifest
            //  Current SDK - 6.0.300
            //  Run update
            //  Should not find 6.0.300 manifest
            //  Should fall back to 6.0.200 manifest and advertise it
 
            //  Arrange
            string sdkFeatureBand = "6.0.300";
            var testDir = _testAssetsManager.CreateTestDirectory(identifier: useOfflineCache.ToString()).Path;
            var dotnetRoot = Path.Combine(testDir, "dotnet");
            var installedManifestDir6_0_200 = Path.Combine(dotnetRoot, "sdk-manifests", "6.0.200");
            Directory.CreateDirectory(installedManifestDir6_0_200);
            var adManifestDir = Path.Combine(testDir, ".dotnet", "sdk-advertising", sdkFeatureBand);
            Directory.CreateDirectory(adManifestDir);
 
            //  Write installed test-manifest with feature band 6.0.200
            string testManifestName = "test-manifest";
            Directory.CreateDirectory(Path.Combine(installedManifestDir6_0_200, testManifestName));
            File.WriteAllText(Path.Combine(installedManifestDir6_0_200, testManifestName, _manifestFileName), GetManifestContent(new ManifestVersion("1.0.0")));
 
            string manifestPath = Path.Combine(installedManifestDir6_0_200, testManifestName, _manifestFileName);
 
            var workloadManifestProvider = new MockManifestProvider((testManifestName, manifestPath, "1.0.0", "6.0.200"))
            {
                SdkFeatureBand = new SdkFeatureBand(sdkFeatureBand)
            };
 
            var workloadResolver = WorkloadResolver.CreateForTests(workloadManifestProvider, dotnetRoot);
            var nugetDownloader = new MockNuGetPackageDownloader(dotnetRoot);
            nugetDownloader.PackageIdsToNotFind.Add($"{testManifestName}.Manifest-6.0.300");
            var installationRepo = new MockInstallationRecordRepository();
            var manifestUpdater = new WorkloadManifestUpdater(_reporter, workloadResolver, nugetDownloader, Path.Combine(testDir, ".dotnet"), installationRepo, new MockPackWorkloadInstaller(dotnetRoot));
 
            var offlineCacheDir = "";
            if (useOfflineCache)
            {
                offlineCacheDir = Path.Combine(testDir, "offlineCache");
                Directory.CreateDirectory(offlineCacheDir);
                File.Create(Path.Combine(offlineCacheDir, $"{testManifestName}.Manifest-6.0.200.nupkg")).Close();
 
                await manifestUpdater.UpdateAdvertisingManifestsAsync(includePreviews: true, offlineCache: new DirectoryPath(offlineCacheDir));
            }
            else
            {
                nugetDownloader.PackageIdsToNotFind.Add($"{testManifestName}.Manifest-6.0.300");
 
                await manifestUpdater.UpdateAdvertisingManifestsAsync(includePreviews: true);
 
                //  Assert
                //  6.0.300 manifest was requested and then 6.0.200 manifest was requested
                // we can't assert this for the offline cache
                nugetDownloader.DownloadCallParams[0].id.ToString().Should().Be($"{testManifestName}.manifest-6.0.300");
                nugetDownloader.DownloadCallParams[0].version.Should().BeNull();
                nugetDownloader.DownloadCallParams[1].id.ToString().Should().Be($"{testManifestName}.manifest-6.0.200");
                nugetDownloader.DownloadCallParams[1].version.Should().BeNull();
                nugetDownloader.DownloadCallParams.Count.Should().Be(2);
 
            }
 
            //  6.0.200 package was written to advertising manifest folder
            var advertisedManifestContents = File.ReadAllText(Path.Combine(adManifestDir, testManifestName, "WorkloadManifest.json"));
            advertisedManifestContents.Should().NotBeEmpty();
 
            //  AdvertisedManifestFeatureBand.txt file is set to 6.0.200
            var savedFeatureBand = File.ReadAllText(Path.Combine(adManifestDir, testManifestName, "AdvertisedManifestFeatureBand.txt"));
            savedFeatureBand.Should().Be("6.0.200");
 
            // check that update did not fail
            _reporter.Lines.Should().NotContain(l => l.ToLowerInvariant().Contains("fail"));
            _reporter.Lines.Should().NotContain(string.Format(CliCommandStrings.AdManifestPackageDoesNotExist, testManifestName));
 
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public async Task ItCanFallbackWithNoUpdates(bool useOfflineCache)
        {
            //  Currently installed - none
            //  Current SDK - 6.0.300
            //  Run update
            //  Should not find 6.0.300 manifest
            //  Should not find 6.0.200 manifest
            //  Manifest Updater should give appropriate message
 
            //  Arrange
            string sdkFeatureBand = "6.0.300";
            var testDir = _testAssetsManager.CreateTestDirectory().Path;
            var dotnetRoot = Path.Combine(testDir, "dotnet");
 
            var emptyInstalledManifestsDir = Path.Combine(dotnetRoot, "sdk-manifests", "6.0.200");
            Directory.CreateDirectory(emptyInstalledManifestsDir);
 
            var adManifestDir = Path.Combine(testDir, ".dotnet", "sdk-advertising", sdkFeatureBand);
            Directory.CreateDirectory(adManifestDir);
 
            string testManifestName = "test-manifest";
            Directory.CreateDirectory(Path.Combine(emptyInstalledManifestsDir, testManifestName));
            File.WriteAllText(Path.Combine(emptyInstalledManifestsDir, testManifestName, _manifestFileName), GetManifestContent(new ManifestVersion("1.0.0")));
 
            var workloadManifestProvider = new MockManifestProvider((testManifestName, Path.Combine(emptyInstalledManifestsDir, testManifestName, _manifestFileName), "1.0.0", "6.0.200"))
            {
                SdkFeatureBand = new SdkFeatureBand(sdkFeatureBand)
            };
 
            var workloadResolver = WorkloadResolver.CreateForTests(workloadManifestProvider, dotnetRoot);
            var nugetDownloader = new MockNuGetPackageDownloader(dotnetRoot);
            var installationRepo = new MockInstallationRecordRepository();
            var manifestUpdater = new WorkloadManifestUpdater(_reporter, workloadResolver, nugetDownloader, Path.Combine(testDir, ".dotnet"), installationRepo, new MockPackWorkloadInstaller(dotnetRoot));
 
            var offlineCacheDir = "";
            if (useOfflineCache)
            {
                offlineCacheDir = Path.Combine(testDir, "offlineCache");
                Directory.CreateDirectory(offlineCacheDir);             // empty dir because it shouldn't find any manifests to update
 
                await manifestUpdater.UpdateAdvertisingManifestsAsync(includePreviews: true, offlineCache: new DirectoryPath(offlineCacheDir));
            }
            else
            {
                nugetDownloader.PackageIdsToNotFind.Add($"{testManifestName}.Manifest-6.0.300");
                nugetDownloader.PackageIdsToNotFind.Add($"{testManifestName}.Manifest-6.0.200");
 
                await manifestUpdater.UpdateAdvertisingManifestsAsync(includePreviews: true);
 
                //  6.0.300 manifest was requested and then 6.0.200 manifest was requested
                // we can't assert this for the offline cache
                nugetDownloader.DownloadCallParams[0].id.ToString().Should().Be($"{testManifestName}.manifest-6.0.300");
                nugetDownloader.DownloadCallParams[0].version.Should().BeNull();
                nugetDownloader.DownloadCallParams[1].id.ToString().Should().Be($"{testManifestName}.manifest-6.0.200");
                nugetDownloader.DownloadCallParams[1].version.Should().BeNull();
                nugetDownloader.DownloadCallParams.Count.Should().Be(2);
            }
 
            //  Assert
            _reporter.Lines.Should().NotContain(l => l.ToLowerInvariant().Contains("fail"));
            _reporter.Lines.Should().Contain(string.Format(CliCommandStrings.AdManifestPackageDoesNotExist, testManifestName));
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public async Task GivenNoUpdatesAreAvailableAndNoRollbackItGivesAppropriateMessage(bool useOfflineCache)
        {
            //  Currently installed - none
            //  Current SDK - 6.0.300
            //  Run update
            //  Should not find 6.0.300 manifest
            //  Should not rollback
            //  Manifest updater should give appropriate message
 
            //  Arrange
            string sdkFeatureBand = "6.0.300";
            var testDir = _testAssetsManager.CreateTestDirectory().Path;
            var dotnetRoot = Path.Combine(testDir, "dotnet");
 
            var emptyInstalledManifestsDir = Path.Combine(dotnetRoot, "sdk-manifests", "6.0.300");
            Directory.CreateDirectory(emptyInstalledManifestsDir);
 
            var adManifestDir = Path.Combine(testDir, ".dotnet", "sdk-advertising", sdkFeatureBand);
            Directory.CreateDirectory(adManifestDir);
 
            string testManifestName = "test-manifest";
            Directory.CreateDirectory(Path.Combine(emptyInstalledManifestsDir, testManifestName));
            File.WriteAllText(Path.Combine(emptyInstalledManifestsDir, testManifestName, _manifestFileName), GetManifestContent(new ManifestVersion("1.0.0")));
 
            var workloadManifestProvider = new MockManifestProvider(Path.Combine(emptyInstalledManifestsDir, testManifestName, _manifestFileName))
            {
                SdkFeatureBand = new SdkFeatureBand(sdkFeatureBand)
            };
 
            var workloadResolver = WorkloadResolver.CreateForTests(workloadManifestProvider, dotnetRoot);
            var nugetDownloader = new MockNuGetPackageDownloader(dotnetRoot);
            var installationRepo = new MockInstallationRecordRepository();
            var manifestUpdater = new WorkloadManifestUpdater(_reporter, workloadResolver, nugetDownloader, Path.Combine(testDir, ".dotnet"), installationRepo, new MockPackWorkloadInstaller(dotnetRoot));
 
            var offlineCacheDir = "";
            if (useOfflineCache)
            {
                offlineCacheDir = Path.Combine(testDir, "offlineCache");
                Directory.CreateDirectory(offlineCacheDir);             // empty dir because it shouldn't find any manifests to update
 
                await manifestUpdater.UpdateAdvertisingManifestsAsync(includePreviews: true, offlineCache: new DirectoryPath(offlineCacheDir));
            }
            else
            {
                nugetDownloader.PackageIdsToNotFind.Add($"{testManifestName}.Manifest-6.0.300");
 
                await manifestUpdater.UpdateAdvertisingManifestsAsync(includePreviews: true);
 
 
                // only 6.0.300 manifest was requested
                // we can't assert this for the offline cache
                nugetDownloader.DownloadCallParams[0].id.ToString().Should().Be($"{testManifestName}.manifest-6.0.300");
                nugetDownloader.DownloadCallParams[0].version.Should().BeNull();
            }
 
            //  Assert
            //  Check nothing was written to advertising manifest folder
            Directory.GetFiles(adManifestDir).Should().BeEmpty();
 
            _reporter.Lines.Should().NotContain(l => l.ToLowerInvariant().Contains("fail"));
            _reporter.Lines.Should().Contain(string.Format(CliCommandStrings.AdManifestPackageDoesNotExist, testManifestName));
        }
 
        [Fact]
        public void GivenWorkloadManifestRollbackItCanCalculateUpdates()
        {
            var testDir = _testAssetsManager.CreateTestDirectory().Path;
            var currentFeatureBand = "6.0.100";
            var dotnetRoot = Path.Combine(testDir, "dotnet");
            var expectedManifestUpdates = new TestManifestUpdate[] {
                new TestManifestUpdate(new ManifestId("test-manifest-1"), new ManifestVersion("5.0.0"), currentFeatureBand, new ManifestVersion("4.0.0"), currentFeatureBand),
                new TestManifestUpdate(new ManifestId("test-manifest-2"), new ManifestVersion("3.0.0"), currentFeatureBand, new ManifestVersion("2.0.0"), currentFeatureBand) };
 
            // Write mock manifests
            var installedManifestDir = Path.Combine(testDir, "dotnet", "sdk-manifests", currentFeatureBand);
            var adManifestDir = Path.Combine(testDir, ".dotnet", "sdk-advertising", currentFeatureBand);
            Directory.CreateDirectory(installedManifestDir);
            Directory.CreateDirectory(adManifestDir);
            foreach (var manifestUpdate in expectedManifestUpdates)
            {
                Directory.CreateDirectory(Path.Combine(installedManifestDir, manifestUpdate.ManifestId.ToString()));
                File.WriteAllText(Path.Combine(installedManifestDir, manifestUpdate.ManifestId.ToString(), _manifestFileName), GetManifestContent(manifestUpdate.ExistingVersion));
            }
 
            var rollbackDefContent = JsonSerializer.Serialize(new Dictionary<string, string>() { { "test-manifest-1", "4.0.0" }, { "test-manifest-2", "2.0.0" } });
            var rollbackDefPath = Path.Combine(testDir, "testRollbackDef.txt");
            File.WriteAllText(rollbackDefPath, rollbackDefContent);
 
            var manifestDirs = expectedManifestUpdates.Select(manifest => manifest.ManifestId)
                .Select(manifest => Path.Combine(installedManifestDir, manifest.ToString(), _manifestFileName))
                .ToArray();
            var workloadManifestProvider = new MockManifestProvider(manifestDirs);
            var nugetDownloader = new MockNuGetPackageDownloader(dotnetRoot);
            var workloadResolver = WorkloadResolver.CreateForTests(workloadManifestProvider, dotnetRoot);
            var installationRepo = new MockInstallationRecordRepository();
            var manifestUpdater = new WorkloadManifestUpdater(_reporter, workloadResolver, nugetDownloader, testDir, installationRepo, new MockPackWorkloadInstaller(dotnetRoot));
 
            var manifestUpdates = manifestUpdater.CalculateManifestRollbacks(rollbackDefPath);
            manifestUpdates.Should().BeEquivalentTo(expectedManifestUpdates.Select(u => u.ToManifestVersionUpdate()));
        }
 
        [Fact]
        public void GivenFromRollbackDefinitionItErrorsOnInstalledExtraneousManifestId()
        {
            var testDir = _testAssetsManager.CreateTestDirectory().Path;
            var featureBand = "6.0.100";
            var dotnetRoot = Path.Combine(testDir, "dotnet");
            var expectedManifestUpdates = new (ManifestId, ManifestVersion, ManifestVersion)[] {
                (new ManifestId("test-manifest-1"), new ManifestVersion("5.0.0"), new ManifestVersion("4.0.0")),
                (new ManifestId("test-manifest-2"), new ManifestVersion("3.0.0"), new ManifestVersion("2.0.0")) };
 
            // Write mock manifests
            var installedManifestDir = Path.Combine(testDir, "dotnet", "sdk-manifests", featureBand);
            var adManifestDir = Path.Combine(testDir, ".dotnet", "sdk-advertising", featureBand);
            Directory.CreateDirectory(installedManifestDir);
            Directory.CreateDirectory(adManifestDir);
            foreach ((var manifestId, var existingVersion, _) in expectedManifestUpdates)
            {
                Directory.CreateDirectory(Path.Combine(installedManifestDir, manifestId.ToString()));
                File.WriteAllText(Path.Combine(installedManifestDir, manifestId.ToString(), _manifestFileName), GetManifestContent(existingVersion));
            }
 
            // Write extraneous manifest that the rollback definition will not have
            Directory.CreateDirectory(Path.Combine(installedManifestDir, "test-manifest-3"));
            File.WriteAllText(Path.Combine(installedManifestDir, "test-manifest-3", _manifestFileName), GetManifestContent(new ManifestVersion("1.0.0")));
 
            var rollbackDefContent = JsonSerializer.Serialize(new Dictionary<string, string>() { { "test-manifest-1", "4.0.0" }, { "test-manifest-2", "2.0.0" } });
            var rollbackDefPath = Path.Combine(testDir, "testRollbackDef.txt");
            File.WriteAllText(rollbackDefPath, rollbackDefContent);
 
            var manifestDirs = expectedManifestUpdates.Select(manifest => manifest.Item1)
                .Append(new ManifestId("test-manifest-3"))
                .Select(manifest => Path.Combine(installedManifestDir, manifest.ToString(), _manifestFileName))
                .ToArray();
            var workloadManifestProvider = new MockManifestProvider(manifestDirs);
            var nugetDownloader = new MockNuGetPackageDownloader(dotnetRoot);
            var workloadResolver = WorkloadResolver.CreateForTests(new MockManifestProvider(Array.Empty<string>()), dotnetRoot);
            var installationRepo = new MockInstallationRecordRepository();
            var manifestUpdater = new WorkloadManifestUpdater(_reporter, workloadResolver, nugetDownloader, testDir, installationRepo, new MockPackWorkloadInstaller(dotnetRoot));
 
            manifestUpdater.CalculateManifestRollbacks(rollbackDefPath);
            string.Join(" ", _reporter.Lines).Should().Contain(rollbackDefPath);
        }
 
        [Fact]
        public void GivenFromRollbackDefinitionItErrorsOnExtraneousManifestIdInRollbackDefinition()
        {
            var testDir = _testAssetsManager.CreateTestDirectory().Path;
            var featureBand = "6.0.100";
            var dotnetRoot = Path.Combine(testDir, "dotnet");
            var expectedManifestUpdates = new (ManifestId, ManifestVersion, ManifestVersion)[] {
                (new ManifestId("test-manifest-1"), new ManifestVersion("5.0.0"), new ManifestVersion("4.0.0")),
                (new ManifestId("test-manifest-2"), new ManifestVersion("3.0.0"), new ManifestVersion("2.0.0")) };
 
            // Write mock manifests
            var installedManifestDir = Path.Combine(testDir, "dotnet", "sdk-manifests", featureBand);
            var adManifestDir = Path.Combine(testDir, ".dotnet", "sdk-advertising", featureBand);
            Directory.CreateDirectory(installedManifestDir);
            Directory.CreateDirectory(adManifestDir);
            foreach ((var manifestId, var existingVersion, _) in expectedManifestUpdates)
            {
                Directory.CreateDirectory(Path.Combine(installedManifestDir, manifestId.ToString()));
                File.WriteAllText(Path.Combine(installedManifestDir, manifestId.ToString(), _manifestFileName), GetManifestContent(existingVersion));
            }
 
            var rollbackDefContent = JsonSerializer.Serialize(new Dictionary<string, string>() {
                { "test-manifest-1", "4.0.0" },
                { "test-manifest-2", "2.0.0" },
                { "test-manifest-3", "1.0.0" } // This manifest is not installed, should cause an error
            });
            var rollbackDefPath = Path.Combine(testDir, "testRollbackDef.txt");
            File.WriteAllText(rollbackDefPath, rollbackDefContent);
 
            var manifestDirs = expectedManifestUpdates.Select(manifest => manifest.Item1)
                .Select(manifest => Path.Combine(installedManifestDir, manifest.ToString(), _manifestFileName))
                .ToArray();
            var workloadManifestProvider = new MockManifestProvider(manifestDirs);
            var nugetDownloader = new MockNuGetPackageDownloader(dotnetRoot);
            var workloadResolver = WorkloadResolver.CreateForTests(new MockManifestProvider(Array.Empty<string>()), dotnetRoot);
            var installationRepo = new MockInstallationRecordRepository();
            var manifestUpdater = new WorkloadManifestUpdater(_reporter, workloadResolver, nugetDownloader, testDir, installationRepo, new MockPackWorkloadInstaller(dotnetRoot));
 
            manifestUpdater.CalculateManifestRollbacks(rollbackDefPath);
            string.Join(" ", _reporter.Lines).Should().Contain(rollbackDefPath);
        }
 
        [Fact]
        public async Task GivenWorkloadManifestUpdateItChoosesHighestManifestVersionInCache()
        {
            var manifestId = "mock-manifest";
            var testDir = _testAssetsManager.CreateTestDirectory().Path;
            var featureBand = "6.0.100";
            var dotnetRoot = Path.Combine(testDir, "dotnet");
 
            // Write mock manifest
            var installedManifestDir = Path.Combine(testDir, "dotnet", "sdk-manifests", featureBand);
            var adManifestDir = Path.Combine(testDir, ".dotnet", "sdk-advertising", featureBand);
            Directory.CreateDirectory(adManifestDir);
            Directory.CreateDirectory(Path.Combine(installedManifestDir, manifestId));
            File.WriteAllText(Path.Combine(installedManifestDir, manifestId, _manifestFileName), GetManifestContent(new ManifestVersion("1.0.0")));
 
            // Write multiple manifest packages to the offline cache
            var offlineCache = Path.Combine(testDir, "cache");
            Directory.CreateDirectory(offlineCache);
            File.Create(Path.Combine(offlineCache, $"{manifestId}.manifest-{featureBand}.2.0.0.nupkg")).Close();
            File.Create(Path.Combine(offlineCache, $"{manifestId}.manifest-{featureBand}.3.0.0.nupkg")).Close();
 
            var workloadManifestProvider = new MockManifestProvider(new string[] { Path.Combine(installedManifestDir, manifestId, _manifestFileName) });
            var nugetDownloader = new MockNuGetPackageDownloader(dotnetRoot);
            var workloadResolver = WorkloadResolver.CreateForTests(workloadManifestProvider, dotnetRoot);
            var installationRepo = new MockInstallationRecordRepository();
            var installer = new MockPackWorkloadInstaller(dotnetRoot);
            var manifestUpdater = new WorkloadManifestUpdater(_reporter, workloadResolver, nugetDownloader, testDir, installationRepo, installer);
            await manifestUpdater.UpdateAdvertisingManifestsAsync(false, false, new DirectoryPath(offlineCache));
 
            // We should have chosen the higher version manifest package to install/ extract
            installer.ExtractCallParams.Count().Should().Be(1);
            installer.ExtractCallParams[0].Item1.Should().Be(Path.Combine(offlineCache, $"{manifestId}.manifest-{featureBand}.3.0.0.nupkg"));
        }
 
        [Theory]
        [InlineData("build", true)]
        [InlineData("publish", true)]
        [InlineData("run", false)]
        public void GivenWorkloadsAreOutOfDateUpdatesAreAdvertisedOnRestoringCommands(string commandName, bool shouldShowUpdateNotification)
        {
            var testInstance = _testAssetsManager.CopyTestAsset("HelloWorld", identifier: commandName)
                .WithSource()
                .Restore(Log);
            var sdkFeatureBand = new SdkFeatureBand(TestContext.Current.ToolsetUnderTest.SdkVersion);
            // Write fake updates file
            Directory.CreateDirectory(Path.Combine(testInstance.Path, ".dotnet"));
            File.WriteAllText(Path.Combine(testInstance.Path, ".dotnet", $".workloadAdvertisingUpdates{sdkFeatureBand}"), @"[""maui""]");
            // Don't check for updates again and overwrite our existing updates file
            File.WriteAllText(Path.Combine(testInstance.Path, ".dotnet", $".workloadAdvertisingManifestSentinel{sdkFeatureBand}"), string.Empty);
 
            var command = new DotnetCommand(Log);
            var commandResult = command
                .WithEnvironmentVariable("DOTNET_CLI_HOME", testInstance.Path)
                .WithWorkingDirectory(testInstance.Path)
                .Execute(commandName);
 
            commandResult
                .Should()
                .Pass();
 
            if (shouldShowUpdateNotification)
            {
                commandResult
                    .Should()
                    .HaveStdOutContaining(CliCommandStrings.WorkloadInstallWorkloadUpdatesAvailable);
            }
            else
            {
                commandResult
                    .Should()
                    .NotHaveStdOutContaining(CliCommandStrings.WorkloadInstallWorkloadUpdatesAvailable);
            }
 
        }
 
        [Fact]
        public void WorkloadUpdatesForDifferentBandAreNotAdvertised()
        {
            var testInstance = _testAssetsManager.CopyTestAsset("HelloWorld")
                .WithSource()
                .Restore(Log);
            var sdkFeatureBand = new SdkFeatureBand(TestContext.Current.ToolsetUnderTest.SdkVersion);
            // Write fake updates file
            Directory.CreateDirectory(Path.Combine(testInstance.Path, ".dotnet"));
            File.WriteAllText(Path.Combine(testInstance.Path, ".dotnet", $".workloadAdvertisingUpdates6.0.100"), @"[""maui""]");
            // Don't check for updates again and overwrite our existing updates file
            File.WriteAllText(Path.Combine(testInstance.Path, ".dotnet", ".workloadAdvertisingManifestSentinel" + sdkFeatureBand.ToString()), string.Empty);
 
            var command = new DotnetCommand(Log);
            var commandResult = command
                .WithEnvironmentVariable("DOTNET_CLI_HOME", testInstance.Path)
                .WithWorkingDirectory(testInstance.Path)
                .Execute("build");
 
            commandResult
                .Should()
                .Pass();
 
            commandResult
                .Should()
                .NotHaveStdOutContaining(CliCommandStrings.WorkloadInstallWorkloadUpdatesAvailable);
        }
 
        [Fact]
        public async Task TestSideBySideUpdateChecks()
        {
            // this test checks that different version bands don't interfere with each other's update check timers
            var testDir = _testAssetsManager.CreateTestDirectory().Path;
 
            (var updater1, var downloader1, var sentinelPath1, var resolver1) = GetTestUpdater(testDir: testDir, featureBand: "6.0.100");
            (var updater2, var downloader2, var sentinelPath2, var resolver2) = GetTestUpdater(testDir: testDir, featureBand: "6.0.200");
 
            new WorkloadConfigCommand(
                Parser.Parse(["dotnet", "workload", "config", "--update-mode", "manifests"]),
                workloadResolverFactory: new MockWorkloadResolverFactory(testDir, "6.0.100", resolver1)).Execute().Should().Be(0);
            await updater1.BackgroundUpdateAdvertisingManifestsWhenRequiredAsync();
            File.Exists(sentinelPath2).Should().BeFalse();
 
            downloader1.DownloadCallParams.Should().BeEquivalentTo(GetExpectedDownloadedPackages("6.0.100"));
 
            new WorkloadConfigCommand(
                Parser.Parse(["dotnet", "workload", "config", "--update-mode", "manifests"]),
                workloadResolverFactory: new MockWorkloadResolverFactory(testDir, "6.0.200", resolver2)).Execute().Should().Be(0);
            await updater2.BackgroundUpdateAdvertisingManifestsWhenRequiredAsync();
            File.Exists(sentinelPath2).Should().BeTrue();
            downloader2.DownloadCallParams.Should().BeEquivalentTo(GetExpectedDownloadedPackages("6.0.200"));
            var updateTime2 = DateTime.Now;
 
            downloader1.DownloadCallParams.Clear();
            await updater1.BackgroundUpdateAdvertisingManifestsWhenRequiredAsync();
            downloader1.DownloadCallParams.Should().BeEmpty();
            File.GetLastAccessTime(sentinelPath1).Should().BeBefore(updateTime2);
 
            downloader2.DownloadCallParams.Clear();
            await updater2.BackgroundUpdateAdvertisingManifestsWhenRequiredAsync();
            // var updateTime1 = DateTime.Now;
            downloader2.DownloadCallParams.Should().BeEmpty();
            File.GetLastAccessTime(sentinelPath2).Should().BeCloseTo(updateTime2, 1.Seconds());
        }
 
        private List<(PackageId, NuGetVersion, DirectoryPath?, PackageSourceLocation)> GetExpectedDownloadedPackages(string sdkFeatureBand = "6.0.100")
        {
            var expectedDownloadedPackages = _installedManifests
                .Select(id => ((PackageId, NuGetVersion, DirectoryPath?, PackageSourceLocation))(new PackageId($"{id}.manifest-{sdkFeatureBand}"), null, null, null)).ToList();
            return expectedDownloadedPackages;
        }
 
        private (WorkloadManifestUpdater, MockNuGetPackageDownloader, string, WorkloadConfigCommand) GetTestUpdater([CallerMemberName] string testName = "", Func<string, string> getEnvironmentVariable = null)
        {
            var testDir = _testAssetsManager.CreateTestDirectory(testName: testName).Path;
 
            var featureBand = "6.0.100";
 
            (var manifestUpdater, var packageDownloader, var sentinelPath, var workloadResolver) = GetTestUpdater(testDir, featureBand, testName, getEnvironmentVariable);
            var configCommand = new WorkloadConfigCommand(
                Parser.Parse(["dotnet", "workload", "config", "--update-mode", "manifests"]),
                workloadResolverFactory: new MockWorkloadResolverFactory(testDir, featureBand, workloadResolver));
            return (manifestUpdater, packageDownloader, sentinelPath, configCommand);
        }
 
        private (WorkloadManifestUpdater, MockNuGetPackageDownloader, string, IWorkloadResolver) GetTestUpdater(string testDir, string featureBand, [CallerMemberName] string testName = "", Func<string, string> getEnvironmentVariable = null)
        {
            var dotnetRoot = Path.Combine(testDir, "dotnet");
 
            // Write mock manifests
            var installedManifestDir = Path.Combine(testDir, "dotnet", "sdk-manifests", featureBand);
            var adManifestDir = Path.Combine(testDir, ".dotnet", "sdk-advertising", featureBand);
            Directory.CreateDirectory(installedManifestDir);
            Directory.CreateDirectory(adManifestDir);
            foreach (var manifest in _installedManifests)
            {
                Directory.CreateDirectory(Path.Combine(installedManifestDir, manifest.ToString()));
                File.WriteAllText(Path.Combine(installedManifestDir, manifest.ToString(), _manifestFileName), GetManifestContent(new ManifestVersion("1.0.0")));
            }
 
            var manifestDirs = _installedManifests
                .Select(manifest => Path.Combine(installedManifestDir, manifest.ToString(), _manifestFileName))
                .ToArray();
            var workloadManifestProvider = new MockManifestProvider(manifestDirs)
            {
                SdkFeatureBand = new SdkFeatureBand(featureBand),
            };
            var workloadResolver = WorkloadResolver.CreateForTests(workloadManifestProvider, dotnetRoot);
            var nugetDownloader = new MockNuGetPackageDownloader(dotnetRoot);
            var installationRepo = new MockInstallationRecordRepository();
            var manifestUpdater = new WorkloadManifestUpdater(_reporter, workloadResolver, nugetDownloader, testDir, installationRepo, new MockPackWorkloadInstaller(dotnetRoot), getEnvironmentVariable: getEnvironmentVariable);
 
            var sentinelPath = Path.Combine(testDir, _manifestSentinelFileName + featureBand);
            return (manifestUpdater, nugetDownloader, sentinelPath, workloadResolver);
        }
 
        internal static string GetManifestContent(ManifestVersion version)
        {
            return $@"{{
  ""version"": ""{version.ToString()}"",
  ""workloads"": {{
    }}
  }},
  ""packs"": {{
  }}
}}";
        }
    }
}