File: GivenADependencyContextBuilder.cs
Web Access
Project: ..\..\..\src\Tasks\Microsoft.NET.Build.Tasks.UnitTests\Microsoft.NET.Build.Tasks.UnitTests.csproj (Microsoft.NET.Build.Tasks.UnitTests)
// 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 FluentAssertions;
using FluentAssertions.Json;
using Microsoft.Build.Framework;
using Microsoft.Extensions.DependencyModel;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NuGet.Frameworks;
using NuGet.Packaging.Core;
using NuGet.ProjectModel;
using NuGet.RuntimeModel;
using NuGet.Versioning;
using Xunit;
 
namespace Microsoft.NET.Build.Tasks.UnitTests
{
    public class GivenADependencyContextBuilder
    {
        /// <summary>
        /// Tests that DependencyContextBuilder generates DependencyContexts correctly.
        /// </summary>
        [Theory]
        [MemberData(nameof(ProjectData))]
        public void ItBuildsDependencyContextsFromProjectLockFiles(
            string mainProjectName,
            string mainProjectVersion,
            CompilationOptions compilationOptions,
            string baselineFileName,
            string runtime,
            ITaskItem[] assemblySatelliteAssemblies,
            ITaskItem[] referencePaths,
            ITaskItem[] referenceSatellitePaths,
            object[] resolvedNuGetFiles)
        {
            LockFile lockFile = TestLockFiles.GetLockFile(mainProjectName);
            LockFileLookup lockFileLookup = new(lockFile);
 
            SingleProjectInfo mainProject = SingleProjectInfo.Create(
                "/usr/Path",
                mainProjectName,
                ".dll",
                mainProjectVersion,
                assemblySatelliteAssemblies ?? new ITaskItem[] { });
 
            IEnumerable<ReferenceInfo> directReferences =
                ReferenceInfo.CreateDirectReferenceInfos(
                    referencePaths ?? new ITaskItem[] { },
                    referenceSatellitePaths ?? new ITaskItem[] { },
                    lockFileLookup: lockFileLookup,
                    i => true,
                    true);
 
            ProjectContext projectContext = lockFile.CreateProjectContext(
                FrameworkConstants.CommonFrameworks.NetCoreApp10.GetShortFolderName(),
                runtime,
                Constants.DefaultPlatformLibrary,
                runtimeFrameworks: null,
                isSelfContained: !string.IsNullOrEmpty(runtime));
 
            if (resolvedNuGetFiles == null)
            {
                resolvedNuGetFiles = Array.Empty<ResolvedFile>();
            }
 
            DependencyContext dependencyContext = new DependencyContextBuilder(mainProject, includeRuntimeFileVersions: false, runtimeGraph: null, projectContext: projectContext, libraryLookup: lockFileLookup)
                .WithDirectReferences(directReferences)
                .WithCompilationOptions(compilationOptions)
                .WithResolvedNuGetFiles((ResolvedFile[])resolvedNuGetFiles)
                .Build();
 
            JObject result = Save(dependencyContext);
            JObject baseline = ReadJson($"{baselineFileName}.deps.json");
 
            try
            {
                baseline
                    .Should()
                    .BeEquivalentTo(result);
            }
            catch
            {
                // write the result file out on failure for easy comparison
 
                using (JsonTextWriter writer = new(File.CreateText($"result-{baselineFileName}.deps.json")))
                {
                    JsonSerializer serializer = new()
                    {
                        Formatting = Formatting.Indented
                    };
                    serializer.Serialize(writer, result);
                }
 
                throw;
            }
        }
 
        public static IEnumerable<object[]> ProjectData
        {
            get
            {
                ITaskItem[] dotnetNewSatelliteAssemblies = new ITaskItem[]
                {
                    new MockTaskItem(
                        @"de\dotnet.new.resources.dll",
                        new Dictionary<string, string>
                        {
                            { "Culture", "de" },
                            { "TargetPath", @"de\dotnet.new.resources.dll" },
                        }),
                    new MockTaskItem(
                        @"fr\dotnet.new.resources.dll",
                        new Dictionary<string, string>
                        {
                            { "Culture", "fr" },
                            { "TargetPath", @"fr\dotnet.new.resources.dll" },
                        }),
                };
 
                var resolvedNuGetFiles = new[]
                {
                    new ResolvedFile("Newtonsoft.Json.dll", "",
                        new PackageIdentity("Newtonsoft.Json", new NuGetVersion("9.0.1")),
                        AssetType.Runtime,
                        "lib/netstandard1.0/Newtonsoft.Json.dll"),
 
                    new ResolvedFile("System.Collections.NonGeneric.dll", "",
                        new PackageIdentity("System.Collections.NonGeneric", new NuGetVersion("4.0.1")),
                        AssetType.Runtime,
                        "lib/netstandard1.3/System.Collections.NonGeneric.dll"),
 
                    new ResolvedFile("System.Runtime.Serialization.Primitives.dll", "",
                        new PackageIdentity("System.Runtime.Serialization.Primitives", new NuGetVersion("4.1.1")),
                        AssetType.Runtime,
                        "lib/netstandard1.3/System.Runtime.Serialization.Primitives.dll")
                };
 
                return new[]
                {
                    new object[] { "dotnet.new", "1.0.0", null, "dotnet.new", null, null, null, null, null},
                    new object[] { "dotnet.new", "1.0.0", null, "dotnet.new.resources", null, dotnetNewSatelliteAssemblies, null, null, null },
                    new object[] { "simple.dependencies", "1.0.0", null, "simple.dependencies", null, null, null, null, resolvedNuGetFiles },
                };
            }
        }
 
        private static JObject ReadJson(string path)
        {
            using (JsonTextReader jsonReader = new(File.OpenText(path)))
            {
                return JObject.Load(jsonReader);
            }
        }
 
        private JObject Save(DependencyContext dependencyContext)
        {
            using (var memoryStream = new MemoryStream())
            {
                new DependencyContextWriter().Write(dependencyContext, memoryStream);
                using (var readStream = new MemoryStream(memoryStream.ToArray()))
                {
                    using (var textReader = new StreamReader(readStream))
                    {
                        using (var reader = new JsonTextReader(textReader))
                        {
                            return JObject.Load(reader);
                        }
                    }
                }
            }
        }
 
        [Fact]
        public void ItDoesntCreateReferenceAssembliesWhenNoCompilationOptions()
        {
            DependencyContext dependencyContext = BuildDependencyContextWithReferenceAssemblies(useCompilationOptions: false);
 
            dependencyContext.CompileLibraries.Should().BeEmpty();
            dependencyContext
                .RuntimeLibraries
                .Should()
                .NotContain(l => l.Type == "referenceassembly");
            dependencyContext
                .RuntimeLibraries
                .SelectMany(l => l.Dependencies)
                .Should()
                .BeEmpty();
        }
 
        [Fact]
        public void ItDoesntCreateKeepUnneededRuntimeReferences()
        {
            DependencyContext dependencyContext = BuildDependencyContextWithReferenceAssemblies(useCompilationOptions: false);
 
            dependencyContext.RuntimeLibraries.Count.Should().Be(1);
            dependencyContext.RuntimeLibraries[0].Name.Should().Be("simple.dependencies"); // This is the entrypoint
        }
 
        [Fact]
        public void ItHandlesReferenceAndPackageReferenceNameCollisions()
        {
            DependencyContext dependencyContext = BuildDependencyContextWithReferenceAssemblies(useCompilationOptions: true);
 
            dependencyContext.CompileLibraries.Should()
                .Contain(c => c.Name == "System.NotConflicting" && c.Type == "referenceassembly");
 
            // Note: System.Collections.NonGeneric is referenced in the lockfile, so DependencyContextBuilder
            // appends ".Reference" to make it unique
            dependencyContext.CompileLibraries.Should()
                .Contain(c => c.Name == "System.Collections.NonGeneric.Reference" && c.Type == "referenceassembly");
            dependencyContext.CompileLibraries.Should()
                .Contain(c => c.Name == "System.Collections.NonGeneric.Reference.Reference" && c.Type == "referenceassembly");
        }
 
        // If an assembly is in withResources, it has to be a key in dependencies, even with an empty list.
        private static DependencyContext BuildDependencyContextFromDependenciesWithResources(Dictionary<string, List<string>> dependencies, List<string> withResources, List<string> references, bool dllReference)
        {
            string mainProjectName = "simpleApp";
            LockFile lockFile = TestLockFiles.GetLockFile(mainProjectName);
 
            SingleProjectInfo mainProject = SingleProjectInfo.Create(
                "/usr/Path",
                mainProjectName,
                ".dll",
                "1.0.0",
                []);
            string mainProjectDirectory = Path.GetDirectoryName(mainProject.ProjectPath);
 
            
            ITaskItem[] referencePaths = dllReference ? references.Select(reference =>
                new MockTaskItem($"/usr/Path/{reference}.dll", new Dictionary<string, string> {
                    { "CopyLocal", "false" },
                    { "FusionName", $"{reference}, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null" },
                    { "Version", "" },
                })).ToArray() : [];
 
            ProjectContext projectContext = lockFile.CreateProjectContext(
                FrameworkConstants.CommonFrameworks.Net10_0.GetShortFolderName(),
                runtime: null,
                platformLibraryName: Constants.DefaultPlatformLibrary,
                runtimeFrameworks: null,
                isSelfContained: false);
 
            if (!dllReference)
            {
                projectContext.LockFile.ProjectFileDependencyGroups.Add(new ProjectFileDependencyGroup(string.Empty, references));
            }
 
            Dictionary<string, SingleProjectInfo> referenceProjectInfos = new();
 
            foreach (KeyValuePair<string, List<string>> kvp in dependencies)
            {
                projectContext.LockFileTarget.Libraries = projectContext.LockFileTarget.Libraries.Concat([
                    new LockFileTargetLibrary()
                    {
                        Name = kvp.Key,
                        Version = new NuGetVersion(4, 0, 0),
                        Type = withResources.Contains(kvp.Key) ? "project" : "unrealType",
                        Dependencies = kvp.Value.Select(n => new PackageDependency(n)).ToList()
                    }]).ToList();
 
                if (withResources.Contains(kvp.Key))
                {
                    var fullPath = Path.GetFullPath(Path.Combine(mainProjectDirectory, kvp.Key));
                    lockFile.Libraries = lockFile.Libraries.Concat([new LockFileLibrary()
                    {
                        Name = kvp.Key,
                        Version = new NuGetVersion(4, 0, 0),
                        Type = "project",
                        MSBuildProject = fullPath
                    }]).ToList();
 
                    referenceProjectInfos.Add(fullPath, SingleProjectInfo.Create(kvp.Key, kvp.Key, ".dll", "4.0.0",
                        [new MockTaskItem($"{kvp.Key}.resource", new Dictionary<string, string>() {
                            { "Culture", "en-us" },
                            { "TargetPath", $"{kvp.Key}.resource" }
                        })]));
                }
            }
 
            CompilationOptions compilationOptions = CreateCompilationOptions();
 
            return new DependencyContextBuilder(mainProject, includeRuntimeFileVersions: false, runtimeGraph: null, projectContext: projectContext, libraryLookup: new LockFileLookup(lockFile))
                .WithReferenceAssemblies(ReferenceInfo.CreateReferenceInfos(referencePaths))
                .WithCompilationOptions(compilationOptions)
                .WithReferenceProjectInfos(referenceProjectInfos)
                .Build();
        }
 
        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public void DirectReferenceToPackageWithNoAssets(bool dllReference)
        {
            DependencyContext dependencyContext = BuildDependencyContextFromDependenciesWithResources([], [], ["System.A"], dllReference);
            Save(dependencyContext);
            dependencyContext.RuntimeLibraries.Count.Should().Be(1);
        }
 
        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public void IndirectReferenceToPackageWithNoAssets(bool dllReference)
        {
            DependencyContext dependencyContext = BuildDependencyContextFromDependenciesWithResources(new Dictionary<string, List<string>>() {
                { "System.A", ["System.B"] }
            }, ["System.A"], ["System.A"], dllReference);
            Save(dependencyContext);
            dependencyContext.RuntimeLibraries.Count.Should().Be(2);
            dependencyContext.RuntimeLibraries.Should().Contain(x => x.Name.Equals("System.A"));
        }
 
        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public void PackageWithNoAssetsReferencesPackageWithNoAssets(bool dllReference)
        {
            DependencyContext dependencyContext = BuildDependencyContextFromDependenciesWithResources(new Dictionary<string, List<string>>() {
                { "System.A", ["System.B"] },
                { "System.B", [] }
            }, [], ["System.A"], dllReference);
            Save(dependencyContext);
            dependencyContext.RuntimeLibraries.Count.Should().Be(1);
        }
 
        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public void PackageWithNoAssetsReferencesPackageWithAssets(bool dllReference)
        {
            DependencyContext dependencyContext = BuildDependencyContextFromDependenciesWithResources(new Dictionary<string, List<string>>() {
                { "System.A", ["System.B"] },
                { "System.B", [] }
            }, ["System.B"], ["System.A"], dllReference);
            Save(dependencyContext);
            dependencyContext.RuntimeLibraries.Count.Should().Be(3);
            dependencyContext.RuntimeLibraries.Should().Contain(x => x.Name.Equals("System.A"));
            dependencyContext.RuntimeLibraries.Should().Contain(x => x.Name.Equals("System.B"));
        }
 
        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public void PackageWithNoAssetsReferencesPackageReferencesByOtherPackage(bool dllReference)
        {
            DependencyContext dependencyContext = BuildDependencyContextFromDependenciesWithResources(new Dictionary<string, List<string>>()
            {
                { "System.A", ["System.B"] },
                { "System.B", [] },
            }, ["System.B"], ["System.A", "System.B"], dllReference);
            Save(dependencyContext);
            dependencyContext.RuntimeLibraries.Count.Should().Be(2);
            dependencyContext.RuntimeLibraries.Should().Contain(x => x.Name.Equals("System.B"));
        }
 
        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public void PackageWithNoAssetsReferencesPackageWithAssetsWithOtherReferencer(bool dllReference)
        {
            DependencyContext dependencyContext = BuildDependencyContextFromDependenciesWithResources(new Dictionary<string, List<string>>()
            {
                { "System.A", ["System.B"] },
                { "System.B", [] },
                { "System.C", ["System.B"] }
            }, ["System.B", "System.C"], ["System.A", "System.C"], dllReference);
            Save(dependencyContext);
            dependencyContext.RuntimeLibraries.Count.Should().Be(3);
            dependencyContext.RuntimeLibraries.Should().Contain(x => x.Name.Equals("System.C"));
            dependencyContext.RuntimeLibraries.Should().Contain(x => x.Name.Equals("System.B"));
        }
 
        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public void TwoPackagesWithNoAssetsReferencePackageWithAssets(bool dllReference)
        {
            DependencyContext dependencyContext = BuildDependencyContextFromDependenciesWithResources(new Dictionary<string, List<string>>()
            {
                { "System.A", ["System.B"] },
                { "System.C", ["System.B"] },
                { "System.B", [] }
            }, ["System.B"], ["System.A", "System.C"], dllReference);
            Save(dependencyContext);
            dependencyContext.RuntimeLibraries.Count.Should().Be(3);
            dependencyContext.RuntimeLibraries.Should().Contain(x => x.Name.Equals("System.B"));
            if (dependencyContext.RuntimeLibraries.Any(x => x.Name.Equals("System.A")))
            {
                dependencyContext.RuntimeLibraries.Should().NotContain(x => x.Name.Equals("System.C"));
            }
            else
            {
                dependencyContext.RuntimeLibraries.Should().Contain(x => x.Name.Equals("System.C"));
            }
        }
 
        private DependencyContext BuildDependencyContextWithReferenceAssemblies(bool useCompilationOptions)
        {
            string mainProjectName = "simple.dependencies";
            LockFile lockFile = TestLockFiles.GetLockFile(mainProjectName);
 
            SingleProjectInfo mainProject = SingleProjectInfo.Create(
                "/usr/Path",
                mainProjectName,
                ".dll",
                "1.0.0",
                new ITaskItem[] { });
 
            ITaskItem[] referencePaths = new ITaskItem[]
            {
                new MockTaskItem(
                    "/usr/Path/System.NotConflicting.dll",
                    new Dictionary<string, string>
                    {
                        { "CopyLocal", "false" },
                        { "FusionName", "System.NotConflicting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null" },
                        { "Version", "" },
                    }),
                new MockTaskItem(
                    "/usr/Path/System.Collections.NonGeneric.dll",
                    new Dictionary<string, string>
                    {
                        { "CopyLocal", "false" },
                        { "FusionName", "System.Collections.NonGeneric, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null" },
                        { "Version", "" },
                    }),
                new MockTaskItem(
                    "/usr/Path/System.Collections.NonGeneric.Reference.dll",
                    new Dictionary<string, string>
                    {
                        { "CopyLocal", "false" },
                        { "FusionName", "System.Collections.NonGeneric.Reference, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null" },
                        { "Version", "" },
                    }),
            };
 
            ProjectContext projectContext = lockFile.CreateProjectContext(
                FrameworkConstants.CommonFrameworks.NetCoreApp10.GetShortFolderName(),
                runtime: null,
                platformLibraryName: Constants.DefaultPlatformLibrary,
                runtimeFrameworks: null,
                isSelfContained: false);
 
            CompilationOptions compilationOptions =
                useCompilationOptions ? CreateCompilationOptions() :
                null;
 
            DependencyContext dependencyContext = new DependencyContextBuilder(mainProject, includeRuntimeFileVersions: false, runtimeGraph: null, projectContext: projectContext, libraryLookup: new LockFileLookup(lockFile))
                .WithReferenceAssemblies(ReferenceInfo.CreateReferenceInfos(referencePaths))
                .WithCompilationOptions(compilationOptions)
                .Build();
 
            // ensure the DependencyContext can be written out successfully - it has no duplicate dependency names
            Save(dependencyContext);
 
            return dependencyContext;
        }
 
        private static CompilationOptions CreateCompilationOptions()
        {
            return new CompilationOptions(
                    defines: new[] { "DEBUG", "TRACE" },
                    languageVersion: "6",
                    platform: "x64",
                    allowUnsafe: true,
                    warningsAsErrors: false,
                    optimize: null,
                    keyFile: "../keyfile.snk",
                    delaySign: null,
                    publicSign: null,
                    debugType: "portable",
                    emitEntryPoint: true,
                    generateXmlDocumentation: true);
        }
 
        [Fact]
        public void ItCanGenerateTheRuntimeFallbackGraph()
        {
            string mainProjectName = "simple.dependencies";
            LockFile lockFile = TestLockFiles.GetLockFile(mainProjectName);
 
            SingleProjectInfo mainProject = SingleProjectInfo.Create(
                "/usr/Path",
                mainProjectName,
                ".dll",
                "1.0.0",
                new ITaskItem[] { });
 
            ProjectContext projectContext = lockFile.CreateProjectContext(
                FrameworkConstants.CommonFrameworks.NetCoreApp10.GetShortFolderName(),
                runtime: null,
                platformLibraryName: Constants.DefaultPlatformLibrary,
                runtimeFrameworks: null,
                isSelfContained: true);
 
            var runtimeGraph = new RuntimeGraph(
                new RuntimeDescription[]
                {
                    new RuntimeDescription("os-arch", new string [] { "os", "base" }),
                    new RuntimeDescription("new_os-arch", new string [] { "os-arch", "os", "base" }),
                    new RuntimeDescription("os-new_arch", new string [] { "os-arch", "os", "base" }),
                    new RuntimeDescription("new_os-new_arch", new string [] { "new_os-arch", "os-new_arch", "os-arch", "os", "base" }),
                    new RuntimeDescription("os-another_arch", new string [] { "os", "base" })
                });
 
            void CheckRuntimeFallbacks(string runtimeIdentifier, int fallbackCount)
            {
                projectContext.LockFileTarget.RuntimeIdentifier = runtimeIdentifier;
                var dependencyContextBuilder = new DependencyContextBuilder(mainProject, includeRuntimeFileVersions: false, runtimeGraph, projectContext, libraryLookup: new LockFileLookup(lockFile));
                var runtimeFallbacks = dependencyContextBuilder.Build().RuntimeGraph;
 
                runtimeFallbacks
                    .Count()
                    .Should()
                    .Be(fallbackCount);
 
                runtimeFallbacks
                    .Any(runtimeFallback => !runtimeFallback.Runtime.Equals(runtimeIdentifier) && !runtimeFallback.Fallbacks.Contains(runtimeIdentifier))
                    .Should()
                    .BeFalse();
            }
 
            CheckRuntimeFallbacks("os-arch", 4);
            CheckRuntimeFallbacks("new_os-arch", 2);
            CheckRuntimeFallbacks("os-new_arch", 2);
            CheckRuntimeFallbacks("new_os-new_arch", 1);
            CheckRuntimeFallbacks("unrelated_os-unknown_arch", 0);
        }
    }
}