File: GivenThatWeWantToResolveConflicts.cs
Web Access
Project: ..\..\..\test\Microsoft.NET.Build.Tests\Microsoft.NET.Build.Tests.csproj (Microsoft.NET.Build.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.Text.Json.Nodes;
using System.Text.RegularExpressions;
using NuGet.Common;
using NuGet.Frameworks;
using NuGet.ProjectModel;
using NuGet.Versioning;
 
namespace Microsoft.NET.Build.Tests
{
    public class GivenThatWeWantToResolveConflicts : SdkTest
    {
        public GivenThatWeWantToResolveConflicts(ITestOutputHelper log) : base(log)
        {
        }
 
        [Theory]
        [InlineData("netcoreapp2.0")]
        [InlineData("netstandard2.0")]
        public void The_same_references_are_used_with_or_without_DisableDefaultPackageConflictOverrides(string targetFramework)
        {
            var defaultProject = new TestProject()
            {
                Name = "DefaultProject",
                TargetFrameworks = targetFramework,
            };
            AddConflictReferences(defaultProject);
            GetReferences(
                defaultProject,
                expectConflicts: false,
                references: out List<string> defaultReferences,
                referenceCopyLocalPaths: out List<string> defaultReferenceCopyLocalPaths,
                targetFramework);
 
            var disableProject = new TestProject()
            {
                Name = "DisableProject",
                TargetFrameworks = targetFramework,
            };
            disableProject.AdditionalProperties.Add("DisableDefaultPackageConflictOverrides", "true");
            AddConflictReferences(disableProject);
            GetReferences(
                disableProject,
                expectConflicts: true,
                references: out List<string> disableReferences,
                referenceCopyLocalPaths: out List<string> disableReferenceCopyLocalPaths,
                targetFramework);
 
            Assert.Equal(defaultReferences, disableReferences);
            Assert.Equal(defaultReferenceCopyLocalPaths, disableReferenceCopyLocalPaths);
        }
 
        private void AddConflictReferences(TestProject testProject)
        {
            foreach (var dependency in ConflictResolutionAssets.ConflictResolutionDependencies)
            {
                testProject.PackageReferences.Add(new TestPackageReference(dependency.Item1, dependency.Item2));
            }
        }
 
        private void GetReferences(TestProject testProject, bool expectConflicts, out List<string> references, out List<string> referenceCopyLocalPaths, string identifier)
        {
            string targetFramework = testProject.TargetFrameworks;
            TestAsset tempTestAsset = _testAssetsManager.CreateTestProject(testProject, identifier: identifier);
 
            string projectFolder = Path.Combine(tempTestAsset.TestRoot, testProject.Name);
 
            var getReferenceCommand = new GetValuesCommand(
                Log,
                projectFolder,
                targetFramework,
                "Reference",
                GetValuesCommand.ValueType.Item)
            {
                DependsOnTargets = "Build"
            };
            var result = getReferenceCommand.Execute("/v:detailed").Should().Pass();
            if (expectConflicts)
            {
                result.And.HaveStdOutMatching("Encountered conflict", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
            }
            else
            {
                result.And.NotHaveStdOutMatching("Encountered conflict", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
            }
 
            references = getReferenceCommand.GetValues();
 
            var getReferenceCopyLocalPathsCommand = new GetValuesCommand(
                Log,
                projectFolder,
                targetFramework,
                "ReferenceCopyLocalPaths",
                GetValuesCommand.ValueType.Item)
            {
                DependsOnTargets = "Build"
            };
            getReferenceCopyLocalPathsCommand.Execute().Should().Pass();
 
            referenceCopyLocalPaths = getReferenceCopyLocalPathsCommand.GetValues();
        }
 
        [Fact]
        public void CompileConflictsAreNotRemovedFromRuntimeDepsAssets()
        {
            TestProject testProject = new()
            {
                Name = "NetStandard2Library",
                TargetFrameworks = "netstandard2.0",
                //  In deps file, assets are under the ".NETStandard,Version=v2.0/" target (ie with empty RID) for some reason
                RuntimeIdentifier = string.Empty
            };
 
            testProject.PackageReferences.Add(new TestPackageReference("Microsoft.AspNetCore.Mvc.Razor", "2.1.0"));
 
            //  This test relies on a package that would be pruned.  This doesn't seem to be a customer scenario, it looks like it was
            //  an easier way to test that files that were removed 
            testProject.AdditionalProperties["RestoreEnablePackagePruning"] = "false";
 
            var testAsset = _testAssetsManager.CreateTestProject(testProject);
 
            var buildCommand = new BuildCommand(testAsset);
 
            buildCommand
                .Execute()
                .Should()
                .Pass();
 
            string outputFolder = buildCommand.GetOutputDirectory(testProject.TargetFrameworks,
                runtimeIdentifier: testProject.RuntimeIdentifier).FullName;
 
            string depsJsonPath = Path.Combine(outputFolder, $"{testProject.Name}.deps.json");
 
            var assets = DepsFileSkipTests.GetDepsJsonAssets(depsJsonPath, testProject, "runtime")
                .Select(DepsFileSkipTests.GetDepsJsonFilename)
                .ToList();
 
            assets.Should().Contain("System.ValueTuple.dll");
 
        }
 
        [Fact]
        public void AProjectCanReferenceADllInAPackageDirectly()
        {
            TestProject testProject = new()
            {
                Name = "ReferencePackageDllDirectly",
                TargetFrameworks = ToolsetInfo.CurrentTargetFramework,
                IsExe = true
            };
 
            testProject.PackageReferences.Add(new TestPackageReference("Microsoft.VisualStudio.Composition", "15.8.112"));
 
            var testAsset = _testAssetsManager.CreateTestProject(testProject)
                .WithProjectChanges(p =>
                {
                    var ns = p.Root.Name.Namespace;
 
                    var itemGroup = p.Root.Element(ns + "ItemGroup");
                    itemGroup.Add(new XElement(ns + "Reference",
                        new XAttribute("Include", @"$(NuGetPackageRoot)/microsoft.visualstudio.composition/15.8.112/lib/net45/Microsoft.VisualStudio.Composition.dll"),
                        new XAttribute("Private", "true")));
                });
 
            var buildCommand = new BuildCommand(testAsset);
 
            buildCommand
                .Execute()
                .Should()
                .Pass();
        }
 
        [Fact]
        public void DuplicateFrameworkAssembly()
        {
            TestProject testProject = new()
            {
                Name = "DuplicateFrameworkAssembly",
                TargetFrameworks = "net472",
                IsExe = true
            };
            testProject.References.Add("System.Runtime");
            testProject.References.Add("System.Runtime");
            testProject.PackageReferences.Add(new TestPackageReference("System.Runtime", "4.3.0"));
 
            var testAsset = _testAssetsManager.CreateTestProject(testProject);
 
            var buildCommand = new BuildCommand(testAsset);
 
            buildCommand
                .Execute()
                .Should()
                .Pass();
        }
 
        [Fact]
        public void FilesFromAspNetCoreSharedFrameworkAreNotIncluded()
        {
            var testProject = new TestProject()
            {
                Name = "AspNetCoreProject",
                TargetFrameworks = ToolsetInfo.CurrentTargetFramework,
                IsExe = true
            };
 
            testProject.PackageReferences.Add(new TestPackageReference("Microsoft.Extensions.DependencyInjection.Abstractions", "2.2.0"));
 
            var testAsset = _testAssetsManager.CreateTestProject(testProject)
                .WithProjectChanges(project =>
                {
                    var ns = project.Root.Name.Namespace;
                    var itemGroup = new XElement(ns + "ItemGroup");
                    project.Root.Add(itemGroup);
                    itemGroup.Add(new XElement(ns + "FrameworkReference",
                                    new XAttribute("Include", "Microsoft.AspNetCore.App")));
                });
 
            var buildCommand = new BuildCommand(testAsset);
 
            buildCommand
                .Execute()
                .Should()
                .Pass();
 
            var outputDirectory = buildCommand.GetOutputDirectory(testProject.TargetFrameworks);
 
            outputDirectory.Should().NotHaveFile("Microsoft.Extensions.DependencyInjection.Abstractions.dll");
        }
 
        [CoreMSBuildOnlyFact]
        public void AnalyzersAreConflictResolved()
        {
            var testProject = new TestProject()
            {
                Name = nameof(AnalyzersAreConflictResolved),
                TargetFrameworks = ToolsetInfo.CurrentTargetFramework
            };
 
            // add the package referenced analyzers
            testProject.PackageReferences.Add(new TestPackageReference("Microsoft.CodeAnalysis.NetAnalyzers", "5.0.3"));
 
            // enable inbox analyzers too
            var testAsset = _testAssetsManager.CreateTestProject(testProject)
                .WithProjectChanges(project =>
                {
                    var ns = project.Root.Name.Namespace;
                    var itemGroup = new XElement(ns + "PropertyGroup");
                    project.Root.Add(itemGroup);
                    itemGroup.Add(new XElement(ns + "EnableNETAnalyzers", "true"));
                    itemGroup.Add(new XElement(ns + "TreatWarningsAsErrors", "true"));
 
                    // Don't error when generators/analyzers can't be loaded.
                    // This can occur when running tests against FullFramework MSBuild
                    // if the build machine has an MSBuild install with an older version of Roslyn
                    // than the generators in the SDK reference. We aren't testing the generators here
                    // and this failure will occur more clearly in other places when it's
                    // actually an important failure, so don't error out here.
                    itemGroup.Add(new XElement(ns + "WarningsNotAsErrors", "CS9057"));
                });
 
            var buildCommand = new BuildCommand(testAsset);
 
            buildCommand
                .Execute()
                .Should()
                .Pass();
        }
 
        //  Should also run on full framework, but needs the right version of NuGet, which isn't on CI yet
        [CoreMSBuildOnlyTheory]
        [InlineData(true)]
        [InlineData(false)]
        public void PlatformPackagesCanBePruned(bool prunePackages)
        {
            var referencedProject = new TestProject("ReferencedProject")
            {
                TargetFrameworks = ToolsetInfo.CurrentTargetFramework,
                IsExe = false
            };
            referencedProject.PackageReferences.Add(new TestPackageReference("System.Text.Json", "8.0.0"));
            referencedProject.AdditionalProperties["RestoreEnablePackagePruning"] = prunePackages.ToString();
 
            var testProject = new TestProject()
            {
                TargetFrameworks = ToolsetInfo.CurrentTargetFramework
            };
 
            testProject.AdditionalProperties["RestoreEnablePackagePruning"] = prunePackages.ToString();
            testProject.ReferencedProjects.Add(referencedProject);
 
            var testAsset = _testAssetsManager.CreateTestProject(testProject, identifier: prunePackages.ToString());
 
            var buildCommand = new BuildCommand(testAsset);
 
            buildCommand.Execute().Should().Pass();
 
            var assetsFilePath = Path.Combine(buildCommand.GetBaseIntermediateDirectory().FullName, "project.assets.json");
            var lockFile = LockFileUtilities.GetLockFile(assetsFilePath, new NullLogger());
            var lockFileTarget = lockFile.GetTarget(NuGetFramework.Parse(ToolsetInfo.CurrentTargetFramework), runtimeIdentifier: null);
            
            if (prunePackages)
            {
                lockFileTarget.Libraries.Should().NotContain(library => library.Name.Equals("System.Text.Json", StringComparison.OrdinalIgnoreCase));
            }
            else
            {
                lockFileTarget.Libraries.Should().Contain(library => library.Name.Equals("System.Text.Json", StringComparison.OrdinalIgnoreCase));
            }
        }
 
        [CoreMSBuildOnlyTheory]
        [InlineData(ToolsetInfo.CurrentTargetFramework)]
        [InlineData("net9.0")]
        [InlineData("net8.0")]
        [InlineData("net7.0")]
        [InlineData("net6.0")]
        [InlineData("netcoreapp3.1")]
        [InlineData("netcoreapp3.0")]
        [InlineData("netcoreapp2.1")]
        [InlineData("netcoreapp2.0")]
        [InlineData("netcoreapp1.1", false)]
        [InlineData("netcoreapp1.0", false)]
        [InlineData("netstandard2.1")]
        [InlineData("netstandard2.0")]
        [InlineData("netstandard1.1", false)]
        [InlineData("netstandard1.0", false)]
        [InlineData("net451", false)]
        [InlineData("net462")]
        [InlineData("net481")]
        //  These target frameworks shouldn't prune packages unless explicitly enabled
        [InlineData("net9.0", false, "")]
        [InlineData("netstandard2.1", false, "")]
        //  .NET 10 and up should prune packages by default
        [InlineData("net10.0", true, "")]
        public void PrunePackageDataSucceeds(string targetFramework, bool shouldPrune = true, string enablePackagePruning = "True")
        {
            var nugetFramework = NuGetFramework.Parse(targetFramework);
 
            List<KeyValuePair<string,string>> GetPrunedPackages(string frameworkReference)
            {
                var testProject = new TestProject()
                {
                    TargetFrameworks = targetFramework
                };
 
                testProject.AdditionalProperties["RestoreEnablePackagePruning"] = enablePackagePruning;
 
                if (!string.IsNullOrEmpty(frameworkReference))
                {
                    testProject.FrameworkReferences.Add(frameworkReference);
                }
 
                if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && frameworkReference != null && frameworkReference.StartsWith("Microsoft.WindowsDesktop", StringComparison.OrdinalIgnoreCase))
                {
                    testProject.AdditionalProperties["EnableWindowsTargeting"] = "True";
                }
 
                var testAsset = _testAssetsManager.CreateTestProject(testProject, callingMethod: nameof(PrunePackageDataSucceeds), identifier: targetFramework + frameworkReference);
 
                var buildCommand = new BuildCommand(testAsset);
 
                var prunePackageItemFile = Path.Combine(testAsset.TestRoot, "prunePackageItems.txt");
 
                buildCommand.Execute("/t:CollectPrunePackageReferences", "-getItem:PrunePackageReference", $"-getResultOutputFile:{prunePackageItemFile}").Should().Pass();
 
                var prunedPackages = ParsePrunePackageReferenceJson(File.ReadAllText(prunePackageItemFile));
 
                foreach (var kvp in prunedPackages)
                {
                    var prunedPackageVersion = NuGetVersion.Parse(kvp.Value);
                    if (nugetFramework.Framework.Equals(".NETCoreApp", StringComparison.OrdinalIgnoreCase) && !prunedPackageVersion.IsPrerelease)
                    {
                        prunedPackageVersion.Patch.Should().BeGreaterThan(99, $"Patch for {kvp.Key} should be at least 100");
                    }
                    else
                    {
                        prunedPackageVersion.Patch.Should().BeLessThan(1000, $"Patch for {kvp.Key} should be less than 1000");
                    }
                }
 
 
                return prunedPackages;
            }
 
            var prunedPackages = GetPrunedPackages("");
            if (shouldPrune)
            {
                prunedPackages.Should().NotBeEmpty();
            }
            else
            {
                prunedPackages.Should().BeEmpty();
            }
 
            if (shouldPrune && nugetFramework.Framework.Equals(".NETCoreApp", StringComparison.OrdinalIgnoreCase) && nugetFramework.Version.Major >= 3)
            {
                foreach(var frameworkReference in new [] {
                        "Microsoft.AspNetCore.App",
                        "Microsoft.WindowsDesktop.App",
                        "Microsoft.WindowsDesktop.App.WindowsForms",
                    })
                {
                    var frameworkPrunedPackages = GetPrunedPackages(frameworkReference);
                    frameworkPrunedPackages.Count.Should().BeGreaterThan(prunedPackages.Count, frameworkReference + " should have more pruned packages than base framework");
                }
            }
        }
 
        [Fact]
        public void TransitiveFrameworkReferencesDoNotAffectPruning()
        {
            var referencedProject = new TestProject("ReferencedProject")
            {
                TargetFrameworks = ToolsetInfo.CurrentTargetFramework,
                IsExe = false
            };
            referencedProject.PackageReferences.Add(new TestPackageReference("System.Text.Json", "8.0.0"));
            referencedProject.FrameworkReferences.Add("Microsoft.AspNetCore.App");
 
            var testProject = new TestProject()
            {
                TargetFrameworks = ToolsetInfo.CurrentTargetFramework
            };
 
            testProject.AdditionalProperties["RestoreEnablePackagePruning"] = "True";
            testProject.ReferencedProjects.Add(referencedProject);
 
            var testAsset = _testAssetsManager.CreateTestProject(testProject);
 
            new BuildCommand(testAsset).Execute().Should().Pass();
 
            var getItemsCommand1 = new MSBuildCommand(testAsset, "AddPrunePackageReferences");
            var itemsResult1 = getItemsCommand1.Execute("-getItem:PrunePackageReference");
            itemsResult1.Should().Pass();
 
            var items1 = ParsePrunePackageReferenceJson(itemsResult1.StdOut);
 
            var getItemsCommand2 = new MSBuildCommand(testAsset, "ResolvePackageAssets;AddTransitiveFrameworkReferences;AddPrunePackageReferences");
            var itemsResult2 = getItemsCommand2.Execute("-getItem:PrunePackageReference");
            itemsResult2.Should().Pass();
 
            var items2 = ParsePrunePackageReferenceJson(itemsResult2.StdOut);
 
            items2.Should().BeEquivalentTo(items1);
 
        }
 
        [CoreMSBuildOnlyTheory]
        [InlineData("net10.0;net9.0", true)]
        [InlineData("net10.0;net8.0", true)]
        [InlineData("net6.0;net7.0", false)]
        public void WithMultitargetedProjects_PruningsDefaultsAreApplies(string frameworks, bool prunePackages)
        {
            var referencedProject = new TestProject("ReferencedProject")
            {
                TargetFrameworks = frameworks,
                IsExe = false
            };
            referencedProject.PackageReferences.Add(new TestPackageReference("System.Text.Json", "6.0.0"));
            referencedProject.AdditionalProperties["RestoreEnablePackagePruning"] = "false";
 
            var testProject = new TestProject()
            {
                TargetFrameworks = frameworks,
            };
 
            testProject.ReferencedProjects.Add(referencedProject);
 
            var testAsset = _testAssetsManager.CreateTestProject(testProject, identifier: prunePackages.ToString());
 
            var buildCommand = new BuildCommand(testAsset);
 
            buildCommand.Execute().Should().Pass();
 
            var assetsFilePath = Path.Combine(buildCommand.GetBaseIntermediateDirectory().FullName, "project.assets.json");
            var lockFile = LockFileUtilities.GetLockFile(assetsFilePath, new NullLogger());
 
            foreach(var lockFileTarget in lockFile.Targets)
            {
                if (prunePackages)
                {
                    lockFileTarget.Libraries.Should().NotContain(library => library.Name.Equals("System.Text.Json", StringComparison.OrdinalIgnoreCase));
                }
                else
                {
                    lockFileTarget.Libraries.Should().Contain(library => library.Name.Equals("System.Text.Json", StringComparison.OrdinalIgnoreCase));
                }
            }
        }
 
        static List<KeyValuePair<string, string>> ParsePrunePackageReferenceJson(string json)
        {
            List<KeyValuePair<string, string>> ret = new();
            var root = JsonNode.Parse(json);
            var items = (JsonArray)root["Items"]["PrunePackageReference"];
            foreach (var item in items)
            {
                ret.Add(new KeyValuePair<string, string>((string)item["Identity"], (string)item["Version"]));
            }
            return ret;
        }
    }
}