Dependency resolution tests for hostpolicy (dotnet/core-setup#6707)
authorVitek Karas <vitek.karas@microsoft.com>
Fri, 7 Jun 2019 07:19:37 +0000 (00:19 -0700)
committerGitHub <noreply@github.com>
Fri, 7 Jun 2019 07:19:37 +0000 (00:19 -0700)
New test infra for testing .deps.json processin.
Introduces `NetCoreAppBuilder` helper which can build "fake" apps with `.runtimeconfig.json` and `.deps.json`.
* Has builder APIs to describe the apps dependencies (very similar to Microsoft.Extensions.DependencyModel API, but builders)
* Turns the builder APIs into OM from Microsoft.Extensions.DependencyModel and then uses it to write `.deps.json`.
* Also creates fake or real files for all assets in the app
* Can produce `.runtimeconfig.json` using the `RuntimeConfig` builder.

This change also introduces new test type DependencyResolutionTest along with base class, helpers and so on. These tests are meant to validate the processing of `.deps.json` files in the `hostpolicy`.

Refactorings:
* Use the NetCoreAppBuilder in DotNetBuilder to produce real-looking mock frameworks. This is needed for real hostpolicy to work on top of such frameworks.
* This includes support for RID fallback graphs, which allows tests to validate RID specific behaviors independent of the platform it runs on. Went with existing RID values, but could have used completely made up values.
* Expose CommandResultAssertions members to be able to extend functionality via extension methods
* Some new file helpers
* Some new command extensions (didn't try to cleanup all tests to use these... too much of a churn without any real value).

Finally this change adds a few tests using this infra around RID specified asset resolution.

Commit migrated from https://github.com/dotnet/core-setup/commit/d732222381cf588fbc06bf6bfe2ae5fc7c5d4808

src/installer/test/HostActivationTests/CommandExtensions.cs [new file with mode: 0644]
src/installer/test/HostActivationTests/Constants.cs
src/installer/test/HostActivationTests/DependencyResolution/DependencyResolutionBase.cs [new file with mode: 0644]
src/installer/test/HostActivationTests/DependencyResolution/DependencyResolutionCommandResultExtensions.cs [new file with mode: 0644]
src/installer/test/HostActivationTests/DependencyResolution/PortableAppRidAssetResolution.cs [new file with mode: 0644]
src/installer/test/HostActivationTests/DotNetBuilder.cs
src/installer/test/HostActivationTests/NetCoreAppBuilder.cs [new file with mode: 0644]
src/installer/test/TestUtils/Assertions/CommandResultAssertions.cs
src/installer/test/TestUtils/FileUtils.cs

diff --git a/src/installer/test/HostActivationTests/CommandExtensions.cs b/src/installer/test/HostActivationTests/CommandExtensions.cs
new file mode 100644 (file)
index 0000000..8121825
--- /dev/null
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.DotNet.Cli.Build.Framework;
+
+namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
+{
+    public static class CommandExtensions
+    {
+        public static Command EnableHostTracing(this Command command)
+        {
+            return command.EnvironmentVariable(Constants.HostTracing.TraceLevelEnvironmentVariable, "1");
+        }
+
+        public static Command EnableTracingAndCaptureOutputs(this Command command)
+        {
+            return command
+                .EnableHostTracing()
+                .CaptureStdOut()
+                .CaptureStdErr();
+        }
+
+        public static Command RuntimeId(this Command command, string rid)
+        {
+            return command.EnvironmentVariable(Constants.RuntimeId.EnvironmentVariable, rid);
+        }
+    }
+}
index dbd10e9..85a58b9 100644 (file)
@@ -56,5 +56,21 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
             public const string GloballyRegisteredPath = "_DOTNET_TEST_GLOBALLY_REGISTERED_PATH";
             public const string InstallLocationFilePath = "_DOTNET_TEST_INSTALL_LOCATION_FILE_PATH";
         }
+
+        public static class RuntimeId
+        {
+            public const string EnvironmentVariable = "DOTNET_RUNTIME_ID";
+        }
+
+        public static class MultilevelLookup
+        {
+            public const string EnvironmentVariable = "DOTNET_MULTILEVEL_LOOKUP";
+        }
+
+        public static class HostTracing
+        {
+            public const string TraceLevelEnvironmentVariable = "COREHOST_TRACE";
+            public const string TraceFileEnvironmentVariable = "COREHOST_TRACEFILE";
+        }
     }
 }
diff --git a/src/installer/test/HostActivationTests/DependencyResolution/DependencyResolutionBase.cs b/src/installer/test/HostActivationTests/DependencyResolution/DependencyResolutionBase.cs
new file mode 100644 (file)
index 0000000..1a4dfc7
--- /dev/null
@@ -0,0 +1,49 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.IO;
+
+namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.DependencyResolution
+{
+    public abstract class DependencyResolutionBase
+    {
+        protected const string MicrosoftNETCoreApp = "Microsoft.NETCore.App";
+
+        public abstract class SharedTestStateBase : TestArtifact
+        {
+            private readonly string _builtDotnet;
+
+            private static string GetBaseDir(string name)
+            {
+                string baseDir = Path.Combine(TestArtifactsPath, name);
+                return SharedFramework.CalculateUniqueTestDirectory(baseDir);
+            }
+
+            public SharedTestStateBase(string name)
+                : base(GetBaseDir(name), name)
+            {
+                _builtDotnet = Path.Combine(TestArtifactsPath, "sharedFrameworkPublish");
+            }
+
+            public DotNetBuilder DotNet(string name)
+            {
+                return new DotNetBuilder(Location, _builtDotnet, name);
+            }
+
+            public TestApp CreateFrameworkReferenceApp(string fxName, string fxVersion)
+            {
+                // Prepare the app mock - we're not going to run anything really, so we just need the basic files
+                string testAppDir = Path.Combine(Location, "FrameworkReferenceApp");
+                Directory.CreateDirectory(testAppDir);
+
+                TestApp testApp = new TestApp(testAppDir);
+                RuntimeConfig.Path(testApp.RuntimeConfigJson)
+                    .WithFramework(fxName, fxVersion)
+                    .Save();
+
+                return testApp;
+            }
+        }
+    }
+}
diff --git a/src/installer/test/HostActivationTests/DependencyResolution/DependencyResolutionCommandResultExtensions.cs b/src/installer/test/HostActivationTests/DependencyResolution/DependencyResolutionCommandResultExtensions.cs
new file mode 100644 (file)
index 0000000..0cc6593
--- /dev/null
@@ -0,0 +1,84 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using FluentAssertions;
+using FluentAssertions.Execution;
+using System;
+using System.IO;
+
+namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.DependencyResolution
+{
+    public static class DependencyResolutionCommandResultExtensions
+    {
+        public const string TRUSTED_PLATFORM_ASSEMBLIES = "TRUSTED_PLATFORM_ASSEMBLIES";
+        public const string NATIVE_DLL_SEARCH_DIRECTORIES = "NATIVE_DLL_SEARCH_DIRECTORIES";
+
+        public static AndConstraint<CommandResultAssertions> HaveRuntimePropertyContaining(this CommandResultAssertions assertion, string propertyName, string value)
+        {
+            string propertyValue = GetMockPropertyValue(assertion, propertyName);
+
+            Execute.Assertion.ForCondition(propertyValue != null && propertyValue.Contains(value))
+                .FailWith("The property {0} doesn't contain expected value: {1}{2}{3}", propertyName, value, propertyValue, assertion.GetDiagnosticsInfo());
+            return new AndConstraint<CommandResultAssertions>(assertion);
+        }
+
+        public static AndConstraint<CommandResultAssertions> NotHaveRuntimePropertyContaining(this CommandResultAssertions assertion, string propertyName, string value)
+        {
+            string propertyValue = GetMockPropertyValue(assertion, propertyName);
+
+            Execute.Assertion.ForCondition(propertyValue != null && !propertyValue.Contains(value))
+                .FailWith("The property {0} contains unexpected value: {1}{2}{3}", propertyName, value, propertyValue, assertion.GetDiagnosticsInfo());
+            return new AndConstraint<CommandResultAssertions>(assertion);
+        }
+
+        public static AndConstraint<CommandResultAssertions> HaveResolvedAssembly(this CommandResultAssertions assertion, string assemblyPath, TestApp app = null)
+        {
+            return assertion.HaveRuntimePropertyContaining(TRUSTED_PLATFORM_ASSEMBLIES, RelativePathToAbsoluteAppPath(assemblyPath, app));
+        }
+
+        public static AndConstraint<CommandResultAssertions> NotHaveResolvedAssembly(this CommandResultAssertions assertion, string assemblyPath, TestApp app = null)
+        {
+            return assertion.NotHaveRuntimePropertyContaining(TRUSTED_PLATFORM_ASSEMBLIES, RelativePathToAbsoluteAppPath(assemblyPath, app));
+        }
+
+        public static AndConstraint<CommandResultAssertions> HaveResolvedNativeLibraryPath(this CommandResultAssertions assertion, string path, TestApp app = null)
+        {
+            return assertion.HaveRuntimePropertyContaining(NATIVE_DLL_SEARCH_DIRECTORIES, RelativePathToAbsoluteAppPath(path, app));
+        }
+
+        public static AndConstraint<CommandResultAssertions> NotHaveResolvedNativeLibraryPath(this CommandResultAssertions assertion, string path, TestApp app = null)
+        {
+            return assertion.NotHaveRuntimePropertyContaining(NATIVE_DLL_SEARCH_DIRECTORIES, RelativePathToAbsoluteAppPath(path, app));
+        }
+
+        private static string GetMockPropertyValue(CommandResultAssertions assertion, string propertyName)
+        {
+            string propertyHeader = $"mock property[{propertyName}] = ";
+            string stdout = assertion.Result.StdOut;
+            int i = stdout.IndexOf(propertyHeader);
+            if (i >= 0)
+            {
+                i += propertyHeader.Length;
+                int end = assertion.Result.StdOut.IndexOf(Environment.NewLine, i);
+                if (end >= i)
+                {
+                    return stdout.Substring(i, end - i);
+                }
+            }
+
+            return null;
+        }
+
+        private static string RelativePathToAbsoluteAppPath(string relativePath, TestApp app)
+        {
+            string path = relativePath.Replace('/', Path.DirectorySeparatorChar);
+            if (app != null)
+            {
+                path = Path.Combine(app.Location, path);
+            }
+
+            return path;
+        }
+    }
+}
diff --git a/src/installer/test/HostActivationTests/DependencyResolution/PortableAppRidAssetResolution.cs b/src/installer/test/HostActivationTests/DependencyResolution/PortableAppRidAssetResolution.cs
new file mode 100644 (file)
index 0000000..c4b3c41
--- /dev/null
@@ -0,0 +1,129 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.DotNet.Cli.Build;
+using Xunit;
+
+namespace Microsoft.DotNet.CoreSetup.Test.HostActivation.DependencyResolution
+{
+    public class PortableAppRidAssetResolution : 
+        DependencyResolutionBase,
+        IClassFixture<PortableAppRidAssetResolution.SharedTestState>
+    {
+        private SharedTestState SharedState { get; }
+
+        public PortableAppRidAssetResolution(SharedTestState sharedState)
+        {
+            SharedState = sharedState;
+        }
+
+        [Theory]
+        [InlineData("win", "win/WindowsAssembly.dll", "linux/LinuxAssembly.dll")]
+        [InlineData("win10-x64", "win/WindowsAssembly.dll", "linux/LinuxAssembly.dll")]
+        [InlineData("linux", "linux/LinuxAssembly.dll", "win/WindowsAssembly.dll")]
+        public void RidSpecificAssembly(string rid, string includedPath, string excludedPath)
+        {
+            using (TestApp app = NetCoreAppBuilder.PortableForNETCoreApp(SharedState.FrameworkReferenceApp)
+                .WithProject(p => p
+                    .WithAssemblyGroup(null, g => g.WithMainAssembly())
+                    .WithAssemblyGroup("win", g => g.WithAsset("win/WindowsAssembly.dll"))
+                    .WithAssemblyGroup("linux", g => g.WithAsset("linux/LinuxAssembly.dll")))
+                .Build())
+            {
+                SharedState.DotNetWithNetCoreApp.Exec(app.AppDll)
+                    .EnableTracingAndCaptureOutputs()
+                    .RuntimeId(rid)
+                    .Execute()
+                    .Should().Pass()
+                    .And.HaveResolvedAssembly(includedPath, app)
+                    .And.NotHaveResolvedAssembly(excludedPath, app);
+            }
+        }
+
+        [Theory]
+        [InlineData("win", "win", "linux")]
+        [InlineData("win10-x64", "win", "linux")]
+        [InlineData("linux", "linux", "win")]
+        public void RidSpecificNativeLibrary(string rid, string includedPath, string excludedPath)
+        {
+            using (TestApp app = NetCoreAppBuilder.PortableForNETCoreApp(SharedState.FrameworkReferenceApp)
+                .WithProject(p => p
+                    .WithAssemblyGroup(null, g => g.WithMainAssembly())
+                    .WithNativeLibraryGroup("win", g => g.WithAsset("win/WindowsNativeLibrary.dll"))
+                    .WithNativeLibraryGroup("linux", g => g.WithAsset("linux/LinuxNativeLibrary.so")))
+                .Build())
+            {
+                SharedState.DotNetWithNetCoreApp.Exec(app.AppDll)
+                    .EnableTracingAndCaptureOutputs()
+                    .RuntimeId(rid)
+                    .Execute()
+                    .Should().Pass()
+                    .And.HaveResolvedNativeLibraryPath(includedPath, app)
+                    .And.NotHaveResolvedNativeLibraryPath(excludedPath, app);
+            }
+        }
+
+        [Theory]
+        [InlineData("win10-x64", "win-x64/ManagedWin64.dll")]
+        [InlineData("win10-x86", "win/ManagedWin.dll")]
+        [InlineData("linux", "any/ManagedAny.dll")]
+        public void MostSpecificRidAssemblySelected(string rid, string expectedPath)
+        {
+            using (TestApp app = NetCoreAppBuilder.PortableForNETCoreApp(SharedState.FrameworkReferenceApp)
+                .WithProject(p => p
+                    .WithAssemblyGroup(null, g => g.WithMainAssembly())
+                    .WithAssemblyGroup("any", g => g.WithAsset("any/ManagedAny.dll"))
+                    .WithAssemblyGroup("win", g => g.WithAsset("win/ManagedWin.dll"))
+                    .WithAssemblyGroup("win-x64", g => g.WithAsset("win-x64/ManagedWin64.dll")))
+                .Build())
+            {
+                SharedState.DotNetWithNetCoreApp.Exec(app.AppDll)
+                    .EnableTracingAndCaptureOutputs()
+                    .RuntimeId(rid)
+                    .Execute()
+                    .Should().Pass()
+                    .And.HaveResolvedAssembly(expectedPath, app);
+            }
+        }
+
+        [Theory]
+        [InlineData("win10-x64", "win-x64")]
+        [InlineData("win10-x86", "win")]
+        [InlineData("linux", "any")]
+        public void MostSpecificRidNativeLibrarySelected(string rid, string expectedPath)
+        {
+            using (TestApp app = NetCoreAppBuilder.PortableForNETCoreApp(SharedState.FrameworkReferenceApp)
+                .WithProject(p => p
+                    .WithAssemblyGroup(null, g => g.WithMainAssembly())
+                    .WithNativeLibraryGroup("any", g => g.WithAsset("any/NativeAny.dll"))
+                    .WithNativeLibraryGroup("win", g => g.WithAsset("win/NativeWin.dll"))
+                    .WithNativeLibraryGroup("win-x64", g => g.WithAsset("win-x64/NativeWin64.dll")))
+                .Build())
+            {
+                SharedState.DotNetWithNetCoreApp.Exec(app.AppDll)
+                    .EnableTracingAndCaptureOutputs()
+                    .RuntimeId(rid)
+                    .Execute()
+                    .Should().Pass()
+                    .And.HaveResolvedNativeLibraryPath(expectedPath, app);
+            }
+        }
+
+        public class SharedTestState : SharedTestStateBase
+        {
+            public TestApp FrameworkReferenceApp { get; }
+
+            public DotNetCli DotNetWithNetCoreApp { get; }
+
+            public SharedTestState() : base("DependencyResolution")
+            {
+                DotNetWithNetCoreApp = DotNet("WithNetCoreApp")
+                    .AddMicrosoftNETCoreAppFrameworkMockCoreClr("4.0.0")
+                    .Build();
+
+                FrameworkReferenceApp = CreateFrameworkReferenceApp(MicrosoftNETCoreApp, "4.0.0");
+            }
+        }
+    }
+}
index 06597cb..1a33f4f 100644 (file)
@@ -79,51 +79,30 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
             string netCoreAppPath = Path.Combine(_path, "shared", "Microsoft.NETCore.App", version);
             Directory.CreateDirectory(netCoreAppPath);
 
-            // ./shared/Microsoft.NETCore.App/<version>/hostpolicy.dll - this is the real component and will load CoreClr library
             string hostPolicyFileName = RuntimeInformationExtensions.GetSharedLibraryFileNameForCurrentPlatform("hostpolicy");
-            File.Copy(
-                Path.Combine(_repoDirectories.Artifacts, "corehost", hostPolicyFileName),
-                Path.Combine(netCoreAppPath, hostPolicyFileName),
-                true);
-
-            // ./shared/Microsoft.NETCore.App/<version>/coreclr.dll - this is a mock, will not actually run CoreClr
             string coreclrFileName = RuntimeInformationExtensions.GetSharedLibraryFileNameForCurrentPlatform("coreclr");
             string mockCoreclrFileName = RuntimeInformationExtensions.GetSharedLibraryFileNameForCurrentPlatform("mockcoreclr");
-            File.Copy(
-                Path.Combine(_repoDirectories.Artifacts, "corehost_test", mockCoreclrFileName),
-                Path.Combine(netCoreAppPath, coreclrFileName),
-                true);
 
             string netCoreAppPathDepsJson = Path.Combine(netCoreAppPath, "Microsoft.NETCore.App.deps.json");
 
             string currentRid = _repoDirectories.TargetRID;
 
-            string depsJsonBody = $@"{{
-              ""runtimeTarget"": "".NETCoreApp"",
-              ""targets"": {{
-                "".NETCoreApp"": {{
-                  ""Microsoft.NETCore.App/{version}"": {{
-                    ""native"": {{
-                      ""runtimes/{currentRid}/native/{coreclrFileName}"": {{ }}
-                    }}
-                  }},
-                  ""runtime.{currentRid}.Microsoft.NETCore.DotNetHostPolicy/{version}"": {{
-                    ""native"": {{
-                      ""runtimes/{currentRid}/native/{hostPolicyFileName}"": {{}}
-                    }}
-                  }}
-                }}
-              }},
-              ""libraries"": {{
-                ""Microsoft.NETCore.App/{version}"": {{
-                  ""type"": ""package"",
-                  ""serviceable"": true,
-                  ""sha512"": """"
-                }}
-              }}
-            }}";
-
-            File.WriteAllText(netCoreAppPathDepsJson, depsJsonBody);
+            NetCoreAppBuilder.ForNETCoreApp("Microsoft.NETCore.App", currentRid)
+                .WithStandardRuntimeFallbacks()
+                .WithProject("Microsoft.NETCore.App", version, p => p
+                    .WithNativeLibraryGroup(null, g => g
+                        // ./shared/Microsoft.NETCore.App/<version>/coreclr.dll - this is a mock, will not actually run CoreClr
+                        .WithAsset((new NetCoreAppBuilder.RuntimeFileBuilder($"runtimes/{currentRid}/native/{coreclrFileName}"))
+                            .CopyFromFile(Path.Combine(_repoDirectories.Artifacts, "corehost_test", mockCoreclrFileName))
+                            .WithFileOnDiskPath(coreclrFileName))))
+                .WithPackage($"runtime.{currentRid}.Microsoft.NETCore.DotNetHostPolicy", version, p => p
+                    .WithNativeLibraryGroup(null, g => g
+                        // ./shared/Microsoft.NETCore.App/<version>/hostpolicy.dll - this is the real component and will load CoreClr library
+                        .WithAsset((new NetCoreAppBuilder.RuntimeFileBuilder($"runtimes/{currentRid}/native/{hostPolicyFileName}"))
+                            .CopyFromFile(Path.Combine(_repoDirectories.Artifacts, "corehost", hostPolicyFileName))
+                            .WithFileOnDiskPath(hostPolicyFileName))))
+                .Build(new TestApp(netCoreAppPath, "Microsoft.NETCore.App"));
+
             return this;
         }
 
diff --git a/src/installer/test/HostActivationTests/NetCoreAppBuilder.cs b/src/installer/test/HostActivationTests/NetCoreAppBuilder.cs
new file mode 100644 (file)
index 0000000..0339b7a
--- /dev/null
@@ -0,0 +1,329 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.DependencyModel;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
+{
+    public class NetCoreAppBuilder
+    {
+        public string Name { get; set; }
+        public string Framework { get; set; }
+        public string Runtime { get; set; }
+
+        private TestApp _sourceApp;
+
+        public Action<RuntimeConfig> RuntimeConfigCustomizer { get; set; }
+
+        public List<RuntimeLibraryBuilder> RuntimeLibraries { get; } = new List<RuntimeLibraryBuilder>();
+
+        public List<RuntimeFallbacksBuilder> RuntimeFallbacks { get; } = new List<RuntimeFallbacksBuilder>();
+
+        internal class BuildContext
+        {
+            public TestApp App { get; set; }
+        }
+
+        public class RuntimeFileBuilder
+        {
+            public string Path { get; set; }
+            public string AssemblyVersion { get; set; }
+            public string FileVersion { get; set; }
+
+            public string SourcePath { get; set; }
+            public string FileOnDiskPath { get; set; }
+
+            public RuntimeFileBuilder(string path)
+            {
+                Path = path;
+            }
+
+            public RuntimeFileBuilder CopyFromFile(string sourcePath)
+            {
+                SourcePath = sourcePath;
+                return this;
+            }
+
+            public RuntimeFileBuilder WithFileOnDiskPath(string relativePath)
+            {
+                FileOnDiskPath = relativePath;
+                return this;
+            }
+
+            internal RuntimeFile Build(BuildContext context)
+            {
+                string path = ToDiskPath(FileOnDiskPath ?? Path);
+                string absolutePath = System.IO.Path.Combine(context.App.Location, path);
+                if (SourcePath != null)
+                {
+                    FileUtils.EnsureFileDirectoryExists(absolutePath);
+                    File.Copy(SourcePath, absolutePath);
+                }
+                else
+                {
+                    FileUtils.CreateEmptyFile(absolutePath);
+                }
+
+                return new RuntimeFile(Path, AssemblyVersion, FileVersion);
+            }
+
+            private static string ToDiskPath(string assetPath)
+            {
+                return assetPath.Replace('/', System.IO.Path.DirectorySeparatorChar);
+            }
+        }
+
+        public class RuntimeAssetGroupBuilder
+        {
+            public string Runtime { get; set; }
+
+            public bool IncludeMainAssembly { get; set; }
+
+            public List<RuntimeFileBuilder> Assets { get; } = new List<RuntimeFileBuilder>();
+
+            public RuntimeAssetGroupBuilder(string runtime)
+            {
+                Runtime = runtime ?? string.Empty;
+            }
+
+            public RuntimeAssetGroupBuilder WithMainAssembly()
+            {
+                IncludeMainAssembly = true;
+                return this;
+            }
+
+            public RuntimeAssetGroupBuilder WithAsset(RuntimeFileBuilder asset)
+            {
+                Assets.Add(asset);
+                return this;
+            }
+
+            public RuntimeAssetGroupBuilder WithAsset(string path)
+            {
+                return WithAsset(new RuntimeFileBuilder(path));
+            }
+
+            internal RuntimeAssetGroup Build(BuildContext context)
+            {
+                IEnumerable<RuntimeFileBuilder> assets = Assets;
+                if (IncludeMainAssembly)
+                {
+                    assets = assets.Append(new RuntimeFileBuilder(Path.GetFileName(context.App.AppDll)));
+                }
+
+                return new RuntimeAssetGroup(
+                    Runtime,
+                    assets.Select(a => a.Build(context)));
+            }
+        }
+
+        public enum RuntimeLibraryType
+        {
+            project,
+            package
+        }
+
+        public class RuntimeLibraryBuilder
+        {
+            public string Type { get; set; }
+            public string Name { get; set; }
+            public string Version { get; set; }
+
+            public List<RuntimeAssetGroupBuilder> AssemblyGroups { get; } = new List<RuntimeAssetGroupBuilder>();
+            public List<RuntimeAssetGroupBuilder> NativeLibraryGroups { get; } = new List<RuntimeAssetGroupBuilder>();
+
+            public RuntimeLibraryBuilder(RuntimeLibraryType type, string name, string version)
+            {
+                Type = type.ToString();
+                Name = name;
+                Version = version;
+            }
+
+            public RuntimeLibraryBuilder WithAssemblyGroup(string runtime, Action<RuntimeAssetGroupBuilder> customizer = null)
+            {
+                return WithRuntimeAssetGroup(runtime, AssemblyGroups, customizer);
+            }
+
+            public RuntimeLibraryBuilder WithNativeLibraryGroup(string runtime, Action<RuntimeAssetGroupBuilder> customizer = null)
+            {
+                return WithRuntimeAssetGroup(runtime, NativeLibraryGroups, customizer);
+            }
+
+            private RuntimeLibraryBuilder WithRuntimeAssetGroup(
+                string runtime,
+                IList<RuntimeAssetGroupBuilder> list,
+                Action<RuntimeAssetGroupBuilder> customizer)
+            {
+                RuntimeAssetGroupBuilder runtimeAssetGroup = new RuntimeAssetGroupBuilder(runtime);
+                customizer?.Invoke(runtimeAssetGroup);
+
+                list.Add(runtimeAssetGroup);
+                return this;
+            }
+
+            internal RuntimeLibrary Build(BuildContext context)
+            {
+                return new RuntimeLibrary(
+                    Type,
+                    Name,
+                    Version,
+                    string.Empty,
+                    AssemblyGroups.Select(g => g.Build(context)).ToList(),
+                    NativeLibraryGroups.Select(g => g.Build(context)).ToList(),
+                    Enumerable.Empty<ResourceAssembly>(),
+                    Enumerable.Empty<Dependency>(),
+                    false);
+            }
+        }
+
+        public class RuntimeFallbacksBuilder
+        {
+            public string Runtime { get; set; }
+            public List<string> Fallbacks { get; } = new List<string>();
+
+            public RuntimeFallbacksBuilder(string runtime, params string[] fallbacks)
+            {
+                Runtime = runtime;
+                Fallbacks.AddRange(fallbacks);
+            }
+
+            public RuntimeFallbacksBuilder WithFallback(params string[] fallback)
+            {
+                Fallbacks.AddRange(fallback);
+                return this;
+            }
+
+            internal RuntimeFallbacks Build()
+            {
+                return new RuntimeFallbacks(Runtime, Fallbacks);
+            }
+        }
+
+        public static NetCoreAppBuilder PortableForNETCoreApp(TestApp sourceApp)
+        {
+            return new NetCoreAppBuilder()
+            {
+                _sourceApp = sourceApp,
+                Name = sourceApp.Name,
+                Framework = ".NETCoreApp,Version=v3.0",
+                Runtime = null
+            };
+        }
+
+        public static NetCoreAppBuilder ForNETCoreApp(string name, string runtime)
+        {
+            return new NetCoreAppBuilder()
+            {
+                _sourceApp = null,
+                Name = name,
+                Framework = ".NETCoreApp,Version=v3.0",
+                Runtime = runtime
+            };
+        }
+
+        public NetCoreAppBuilder WithRuntimeConfig(Action<RuntimeConfig> runtimeConfigCustomizer)
+        {
+            RuntimeConfigCustomizer = runtimeConfigCustomizer;
+            return this;
+        }
+
+        public NetCoreAppBuilder WithRuntimeLibrary(
+            RuntimeLibraryType type,
+            string name,
+            string version,
+            Action<RuntimeLibraryBuilder> customizer = null)
+        {
+            RuntimeLibraryBuilder runtimeLibrary = new RuntimeLibraryBuilder(type, name, version);
+            customizer?.Invoke(runtimeLibrary);
+
+            RuntimeLibraries.Add(runtimeLibrary);
+            return this;
+        }
+
+        public NetCoreAppBuilder WithProject(string name, string version, Action<RuntimeLibraryBuilder> customizer = null)
+        {
+            return WithRuntimeLibrary(RuntimeLibraryType.project, name, version, customizer);
+        }
+
+        public NetCoreAppBuilder WithProject(Action<RuntimeLibraryBuilder> customizer = null)
+        {
+            return WithRuntimeLibrary(RuntimeLibraryType.project, Name, "1.0.0", customizer);
+        }
+
+        public NetCoreAppBuilder WithPackage(string name, string version, Action<RuntimeLibraryBuilder> customizer = null)
+        {
+            return WithRuntimeLibrary(RuntimeLibraryType.package, name, version, customizer);
+        }
+
+        public NetCoreAppBuilder WithRuntimeFallbacks(string runtime, params string[] fallbacks)
+        {
+            RuntimeFallbacks.Add(new RuntimeFallbacksBuilder(runtime, fallbacks));
+            return this;
+        }
+
+        public NetCoreAppBuilder WithStandardRuntimeFallbacks()
+        {
+            return
+                WithRuntimeFallbacks("win10-x64", "win10", "win-x64", "win", "any")
+                .WithRuntimeFallbacks("win10-x86", "win10", "win-x86", "win", "any")
+                .WithRuntimeFallbacks("win10", "win", "any")
+                .WithRuntimeFallbacks("win-x64", "win", "any")
+                .WithRuntimeFallbacks("win-x86", "win", "any")
+                .WithRuntimeFallbacks("win", "any")
+                .WithRuntimeFallbacks("linux", "any");
+        }
+
+        private DependencyContext BuildDependencyContext(BuildContext context)
+        {
+            return new DependencyContext(
+                new TargetInfo(Framework, Runtime, null, Runtime == null),
+                CompilationOptions.Default,
+                Enumerable.Empty<CompilationLibrary>(),
+                RuntimeLibraries.Select(rl => rl.Build(context)),
+                RuntimeFallbacks.Select(rf => rf.Build()));
+        }
+
+        public TestApp Build()
+        {
+            return Build(_sourceApp.Copy());
+        }
+
+        public TestApp Build(TestApp testApp)
+        {
+            RuntimeConfig runtimeConfig = null;
+            if (File.Exists(testApp.RuntimeConfigJson))
+            {
+                runtimeConfig = RuntimeConfig.FromFile(testApp.RuntimeConfigJson);
+            }
+            else if (RuntimeConfigCustomizer != null)
+            {
+                runtimeConfig = new RuntimeConfig(testApp.RuntimeConfigJson);
+            }
+
+            if (runtimeConfig != null)
+            {
+                RuntimeConfigCustomizer?.Invoke(runtimeConfig);
+                runtimeConfig.Save();
+            }
+
+            BuildContext buildContext = new BuildContext()
+            {
+                App = testApp
+            };
+            DependencyContext dependencyContext = BuildDependencyContext(buildContext);
+
+            DependencyContextWriter writer = new DependencyContextWriter();
+            using (FileStream stream = new FileStream(testApp.DepsJson, FileMode.Create))
+            {
+                writer.Write(dependencyContext, stream);
+            }
+
+            return testApp;
+        }
+    }
+}
\ No newline at end of file
index f87cf6d..d746e1f 100644 (file)
@@ -12,107 +12,107 @@ namespace Microsoft.DotNet.CoreSetup.Test
 {
     public class CommandResultAssertions
     {
-        private CommandResult _commandResult;
+        public CommandResult Result { get; }
 
         public CommandResultAssertions(CommandResult commandResult)
         {
-            _commandResult = commandResult;
+            Result = commandResult;
         }
 
         public AndConstraint<CommandResultAssertions> ExitWith(int expectedExitCode)
         {
-            Execute.Assertion.ForCondition(_commandResult.ExitCode == expectedExitCode)
+            Execute.Assertion.ForCondition(Result.ExitCode == expectedExitCode)
                 .FailWith("Expected command to exit with {0} but it did not.{1}", expectedExitCode, GetDiagnosticsInfo());
             return new AndConstraint<CommandResultAssertions>(this);
         }
 
         public AndConstraint<CommandResultAssertions> Pass()
         {
-            Execute.Assertion.ForCondition(_commandResult.ExitCode == 0)
+            Execute.Assertion.ForCondition(Result.ExitCode == 0)
                 .FailWith("Expected command to pass but it did not.{0}", GetDiagnosticsInfo());
             return new AndConstraint<CommandResultAssertions>(this);
         }
 
         public AndConstraint<CommandResultAssertions> Fail()
         {
-            Execute.Assertion.ForCondition(_commandResult.ExitCode != 0)
+            Execute.Assertion.ForCondition(Result.ExitCode != 0)
                 .FailWith("Expected command to fail but it did not.{0}", GetDiagnosticsInfo());
             return new AndConstraint<CommandResultAssertions>(this);
         }
 
         public AndConstraint<CommandResultAssertions> HaveStdOut()
         {
-            Execute.Assertion.ForCondition(!string.IsNullOrEmpty(_commandResult.StdOut))
+            Execute.Assertion.ForCondition(!string.IsNullOrEmpty(Result.StdOut))
                 .FailWith("Command did not output anything to stdout{0}", GetDiagnosticsInfo());
             return new AndConstraint<CommandResultAssertions>(this);
         }
 
         public AndConstraint<CommandResultAssertions> HaveStdOut(string expectedOutput)
         {
-            Execute.Assertion.ForCondition(_commandResult.StdOut.Equals(expectedOutput, StringComparison.Ordinal))
+            Execute.Assertion.ForCondition(Result.StdOut.Equals(expectedOutput, StringComparison.Ordinal))
                 .FailWith("Command did not output with Expected Output. Expected: {0}{1}", expectedOutput, GetDiagnosticsInfo());
             return new AndConstraint<CommandResultAssertions>(this);
         }
 
         public AndConstraint<CommandResultAssertions> HaveStdOutContaining(string pattern)
         {
-            Execute.Assertion.ForCondition(_commandResult.StdOut.Contains(pattern))
+            Execute.Assertion.ForCondition(Result.StdOut.Contains(pattern))
                 .FailWith("The command output did not contain expected result: {0}{1}", pattern, GetDiagnosticsInfo());
             return new AndConstraint<CommandResultAssertions>(this);
         }
 
         public AndConstraint<CommandResultAssertions> NotHaveStdOutContaining(string pattern)
         {
-            Execute.Assertion.ForCondition(!_commandResult.StdOut.Contains(pattern))
+            Execute.Assertion.ForCondition(!Result.StdOut.Contains(pattern))
                 .FailWith("The command output contained a result it should not have contained: {0}{1}", pattern, GetDiagnosticsInfo());
             return new AndConstraint<CommandResultAssertions>(this);
         }
 
         public AndConstraint<CommandResultAssertions> HaveStdOutMatching(string pattern, RegexOptions options = RegexOptions.None)
         {
-            Execute.Assertion.ForCondition(Regex.Match(_commandResult.StdOut, pattern, options).Success)
+            Execute.Assertion.ForCondition(Regex.Match(Result.StdOut, pattern, options).Success)
                 .FailWith("Matching the command output failed. Pattern: {0}{1}", pattern, GetDiagnosticsInfo());
             return new AndConstraint<CommandResultAssertions>(this);
         }
 
         public AndConstraint<CommandResultAssertions> HaveStdErr()
         {
-            Execute.Assertion.ForCondition(!string.IsNullOrEmpty(_commandResult.StdErr))
+            Execute.Assertion.ForCondition(!string.IsNullOrEmpty(Result.StdErr))
                 .FailWith("Command did not output anything to stderr.{0}", GetDiagnosticsInfo());
             return new AndConstraint<CommandResultAssertions>(this);
         }
 
         public AndConstraint<CommandResultAssertions> HaveStdErrContaining(string pattern)
         {
-            Execute.Assertion.ForCondition(_commandResult.StdErr.Contains(pattern))
+            Execute.Assertion.ForCondition(Result.StdErr.Contains(pattern))
                 .FailWith("The command error output did not contain expected result: {0}{1}", pattern, GetDiagnosticsInfo());
             return new AndConstraint<CommandResultAssertions>(this);
         }
 
         public AndConstraint<CommandResultAssertions> NotHaveStdErrContaining(string pattern)
         {
-            Execute.Assertion.ForCondition(!_commandResult.StdErr.Contains(pattern))
+            Execute.Assertion.ForCondition(!Result.StdErr.Contains(pattern))
                 .FailWith("The command error output contained a result it should not have contained: {0}{1}", pattern, GetDiagnosticsInfo());
             return new AndConstraint<CommandResultAssertions>(this);
         }
 
         public AndConstraint<CommandResultAssertions> HaveStdErrMatching(string pattern, RegexOptions options = RegexOptions.None)
         {
-            Execute.Assertion.ForCondition(Regex.Match(_commandResult.StdErr, pattern, options).Success)
+            Execute.Assertion.ForCondition(Regex.Match(Result.StdErr, pattern, options).Success)
                 .FailWith("Matching the command error output failed. Pattern: {0}{1}", pattern, GetDiagnosticsInfo());
             return new AndConstraint<CommandResultAssertions>(this);
         }
 
         public AndConstraint<CommandResultAssertions> NotHaveStdOut()
         {
-            Execute.Assertion.ForCondition(string.IsNullOrEmpty(_commandResult.StdOut))
+            Execute.Assertion.ForCondition(string.IsNullOrEmpty(Result.StdOut))
                 .FailWith("Expected command to not output to stdout but it was not:{0}", GetDiagnosticsInfo());
             return new AndConstraint<CommandResultAssertions>(this);
         }
 
         public AndConstraint<CommandResultAssertions> NotHaveStdErr()
         {
-            Execute.Assertion.ForCondition(string.IsNullOrEmpty(_commandResult.StdErr))
+            Execute.Assertion.ForCondition(string.IsNullOrEmpty(Result.StdErr))
                 .FailWith("Expected command to not output to stderr but it was not:{0}", GetDiagnosticsInfo());
             return new AndConstraint<CommandResultAssertions>(this);
         }
@@ -138,26 +138,26 @@ namespace Microsoft.DotNet.CoreSetup.Test
             return new AndConstraint<CommandResultAssertions>(this);
         }
 
-        private string GetDiagnosticsInfo()
+        public string GetDiagnosticsInfo()
         {
             return $"{Environment.NewLine}" +
-                $"File Name: {_commandResult.StartInfo.FileName}{Environment.NewLine}" +
-                $"Arguments: {_commandResult.StartInfo.Arguments}{Environment.NewLine}" +
-                $"Exit Code: {_commandResult.ExitCode}{Environment.NewLine}" +
-                $"StdOut:{Environment.NewLine}{_commandResult.StdOut}{Environment.NewLine}" +
-                $"StdErr:{Environment.NewLine}{_commandResult.StdErr}{Environment.NewLine}"; ;
+                $"File Name: {Result.StartInfo.FileName}{Environment.NewLine}" +
+                $"Arguments: {Result.StartInfo.Arguments}{Environment.NewLine}" +
+                $"Exit Code: {Result.ExitCode}{Environment.NewLine}" +
+                $"StdOut:{Environment.NewLine}{Result.StdOut}{Environment.NewLine}" +
+                $"StdErr:{Environment.NewLine}{Result.StdErr}{Environment.NewLine}"; ;
         }
 
         public AndConstraint<CommandResultAssertions> HaveSkippedProjectCompilation(string skippedProject, string frameworkFullName)
         {
-            _commandResult.StdOut.Should().Contain("Project {0} ({1}) was previously compiled. Skipping compilation.", skippedProject, frameworkFullName);
+            Result.StdOut.Should().Contain("Project {0} ({1}) was previously compiled. Skipping compilation.", skippedProject, frameworkFullName);
 
             return new AndConstraint<CommandResultAssertions>(this);
         }
 
         public AndConstraint<CommandResultAssertions> HaveCompiledProject(string compiledProject, string frameworkFullName)
         {
-            _commandResult.StdOut.Should().Contain($"Project {0} ({1}) will be compiled", compiledProject, frameworkFullName);
+            Result.StdOut.Should().Contain($"Project {0} ({1}) will be compiled", compiledProject, frameworkFullName);
 
             return new AndConstraint<CommandResultAssertions>(this);
         }
index a13c852..aecfbae 100644 (file)
@@ -127,5 +127,20 @@ namespace Microsoft.DotNet.CoreSetup.Test.HostActivation
                 }
             }
         }
+
+        public static void EnsureFileDirectoryExists(string filePath)
+        {
+            string directory = Path.GetDirectoryName(filePath);
+            if (!Directory.Exists(directory))
+            {
+                Directory.CreateDirectory(directory);
+            }
+        }
+
+        public static void CreateEmptyFile(string filePath)
+        {
+            EnsureFileDirectoryExists(filePath);
+            File.WriteAllText(filePath, string.Empty);
+        }
     }
 }