Changes per discussion. Add a test

This commit is contained in:
Pranav K 2020-08-18 17:52:17 -07:00
parent 2f0cf8d3e9
commit ad8a927e85
No known key found for this signature in database
GPG Key ID: F748807460A27E91
7 changed files with 159 additions and 49 deletions

View File

@ -17,9 +17,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class RelatedAssemblyAttribute : Attribute public sealed class RelatedAssemblyAttribute : Attribute
{ {
private static readonly Func<string, Assembly> LoadFromAssemblyPathDelegate =
AssemblyLoadContext.GetLoadContext(typeof(RelatedAssemblyAttribute).Assembly).LoadFromAssemblyPath;
/// <summary> /// <summary>
/// Initializes a new instance of <see cref="RelatedAssemblyAttribute"/>. /// Initializes a new instance of <see cref="RelatedAssemblyAttribute"/>.
/// </summary> /// </summary>
@ -52,14 +49,15 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts
throw new ArgumentNullException(nameof(assembly)); throw new ArgumentNullException(nameof(assembly));
} }
return GetRelatedAssemblies(assembly, throwOnError, File.Exists, LoadFromAssemblyPathDelegate); var loadContext = AssemblyLoadContext.GetLoadContext(assembly) ?? AssemblyLoadContext.Default;
return GetRelatedAssemblies(assembly, throwOnError, File.Exists, new AssemblyLoadContextWrapper(loadContext));
} }
internal static IReadOnlyList<Assembly> GetRelatedAssemblies( internal static IReadOnlyList<Assembly> GetRelatedAssemblies(
Assembly assembly, Assembly assembly,
bool throwOnError, bool throwOnError,
Func<string, bool> fileExists, Func<string, bool> fileExists,
Func<string, Assembly> loadFile) AssemblyLoadContextWrapper assemblyLoadContext)
{ {
if (assembly == null) if (assembly == null)
{ {
@ -95,42 +93,46 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts
Resources.FormatRelatedAssemblyAttribute_AssemblyCannotReferenceSelf(nameof(RelatedAssemblyAttribute), assemblyName)); Resources.FormatRelatedAssemblyAttribute_AssemblyCannotReferenceSelf(nameof(RelatedAssemblyAttribute), assemblyName));
} }
var relatedAssemblyName = new AssemblyName(attribute.AssemblyFileName);
Assembly relatedAssembly; Assembly relatedAssembly;
try
{
// Perform a cursory check to determine if the Assembly has already been loaded
// before going to disk. In the ordinary case, related parts that are part of
// application's reference closure should already be loaded.
relatedAssembly = Assembly.Load(relatedAssemblyName);
relatedAssemblies.Add(relatedAssembly);
continue;
}
catch (IOException)
{
// The assembly isn't already loaded. Patience, we'll attempt to load it from disk next.
}
var relatedAssemblyLocation = Path.Combine(assemblyDirectory, attribute.AssemblyFileName + ".dll"); var relatedAssemblyLocation = Path.Combine(assemblyDirectory, attribute.AssemblyFileName + ".dll");
if (!fileExists(relatedAssemblyLocation)) if (fileExists(relatedAssemblyLocation))
{ {
if (throwOnError) relatedAssembly = assemblyLoadContext.LoadFromAssemblyPath(relatedAssemblyLocation);
}
else
{
try
{ {
throw new FileNotFoundException( var relatedAssemblyName = new AssemblyName(attribute.AssemblyFileName);
Resources.FormatRelatedAssemblyAttribute_CouldNotBeFound(attribute.AssemblyFileName, assemblyName, assemblyDirectory), relatedAssembly = assemblyLoadContext.LoadFromAssemblyName(relatedAssemblyName);
relatedAssemblyLocation);
} }
else catch when (!throwOnError)
{ {
// Ignore assembly load failures when throwOnError = false.
continue; continue;
} }
} }
relatedAssembly = loadFile(relatedAssemblyLocation);
relatedAssemblies.Add(relatedAssembly); relatedAssemblies.Add(relatedAssembly);
} }
return relatedAssemblies; return relatedAssemblies;
} }
internal class AssemblyLoadContextWrapper
{
private readonly AssemblyLoadContext _loadContext;
public AssemblyLoadContextWrapper(AssemblyLoadContext loadContext)
{
_loadContext = loadContext;
}
public virtual Assembly LoadFromAssemblyName(AssemblyName assemblyName)
=> _loadContext.LoadFromAssemblyName(assemblyName);
public virtual Assembly LoadFromAssemblyPath(string assemblyPath)
=> _loadContext.LoadFromAssemblyPath(assemblyPath);
}
} }
} }

View File

@ -1,10 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Reflection.Emit; using System.Reflection.Emit;
using System.Runtime.Loader;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ApplicationParts namespace Microsoft.AspNetCore.Mvc.ApplicationParts
@ -43,7 +45,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts
public void GetRelatedAssemblies_ThrowsIfAssemblyCannotBeFound() public void GetRelatedAssemblies_ThrowsIfAssemblyCannotBeFound()
{ {
// Arrange // Arrange
var expected = $"Related assembly 'DoesNotExist' specified by assembly 'MyAssembly' could not be found in the directory {AssemblyDirectory}. Related assemblies must be co-located with the specifying assemblies.";
var assemblyPath = Path.Combine(AssemblyDirectory, "MyAssembly.dll"); var assemblyPath = Path.Combine(AssemblyDirectory, "MyAssembly.dll");
var assembly = new TestAssembly var assembly = new TestAssembly
{ {
@ -51,27 +52,32 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts
}; };
// Act & Assert // Act & Assert
var ex = Assert.Throws<FileNotFoundException>(() => RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: true)); Assert.Throws<FileNotFoundException>(() => RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: true));
Assert.Equal(expected, ex.Message);
Assert.Equal(Path.Combine(AssemblyDirectory, "DoesNotExist.dll"), ex.FileName);
} }
[Fact] [Fact]
public void GetRelatedAssemblies_LoadsRelatedAssembly() public void GetRelatedAssemblies_ReadsAssemblyFromLoadContext_IfItAlreadyExists()
{ {
// Arrange // Arrange
var destination = Path.Combine(AssemblyDirectory, "RelatedAssembly.dll"); var expected = $"Related assembly 'DoesNotExist' specified by assembly 'MyAssembly' could not be found in the directory {AssemblyDirectory}. Related assemblies must be co-located with the specifying assemblies.";
var assemblyPath = Path.Combine(AssemblyDirectory, "MyAssembly.dll");
var relatedAssembly = typeof(RelatedAssemblyPartTest).Assembly;
var assembly = new TestAssembly var assembly = new TestAssembly
{ {
AttributeAssembly = "RelatedAssembly", AttributeAssembly = "RelatedAssembly"
}; };
var relatedAssembly = typeof(RelatedAssemblyPartTest).Assembly; var loadContext = new TestableAssemblyLoadContextWrapper
var result = RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: true, file => true, file =>
{ {
Assert.Equal(file, destination); Assemblies =
return relatedAssembly; {
}); ["RelatedAssembly"] = relatedAssembly,
}
};
// Act
var result = RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: true, file => false, loadContext);
// Assert
Assert.Equal(new[] { relatedAssembly }, result); Assert.Equal(new[] { relatedAssembly }, result);
} }
@ -94,5 +100,21 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts
return new[] { attribute }; return new[] { attribute };
} }
} }
private class TestableAssemblyLoadContextWrapper : RelatedAssemblyAttribute.AssemblyLoadContextWrapper
{
public TestableAssemblyLoadContextWrapper() : base(AssemblyLoadContext.Default)
{
}
public Dictionary<string, Assembly> Assemblies { get; } = new Dictionary<string, Assembly>();
public override Assembly LoadFromAssemblyPath(string assemblyPath) => throw new NotSupportedException();
public override Assembly LoadFromAssemblyName(AssemblyName assemblyName)
{
return Assemblies[assemblyName.Name];
}
}
} }
} }

View File

@ -35,7 +35,7 @@
MicrosoftNETSdkRazorPackageVersion=$(MicrosoftNETSdkRazorPackageVersion); MicrosoftNETSdkRazorPackageVersion=$(MicrosoftNETSdkRazorPackageVersion);
MicrosoftAspNetCoreAppRefPackageVersion=$(MicrosoftAspNetCoreAppRefPackageVersion); MicrosoftAspNetCoreAppRefPackageVersion=$(MicrosoftAspNetCoreAppRefPackageVersion);
MicrosoftAspNetCoreAppRuntimePackageVersion=@(_RuntimePackageVersionInfo->'%(PackageVersion)'); MicrosoftAspNetCoreAppRuntimePackageVersion=@(_RuntimePackageVersionInfo->'%(PackageVersion)');
SupportedRuntimeIdentifiers=$(SupportedRuntimeIdentifiers); SupportedRuntimeIdentifiers=$(SupportedRuntimeIdentifiers.Trim());
DefaultNetCoreTargetFramework=$(DefaultNetCoreTargetFramework); DefaultNetCoreTargetFramework=$(DefaultNetCoreTargetFramework);
RepoRoot=$(RepoRoot); RepoRoot=$(RepoRoot);
Configuration=$(Configuration); Configuration=$(Configuration);

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
@ -59,9 +60,21 @@ namespace Templates.Test.Helpers
output.WriteLine("Running ASP.NET Core application..."); output.WriteLine("Running ASP.NET Core application...");
var arguments = published ? $"exec {dllPath}" : "run --no-build"; string process;
string arguments;
if (published)
{
// When publishingu used the app host to run the app. This makes it easy to consistently run for regular and single-file publish
process = OperatingSystem.IsWindows() ? dllPath + ".exe" : dllPath;
arguments = null;
}
else
{
process = DotNetMuxer.MuxerPathOrDefault();
arguments = "run --no-build";
}
logger?.LogInformation($"AspNetProcess - process: {DotNetMuxer.MuxerPathOrDefault()} arguments: {arguments}"); logger?.LogInformation($"AspNetProcess - process: {process} arguments: {arguments}");
var finalEnvironmentVariables = new Dictionary<string, string>(environmentVariables) var finalEnvironmentVariables = new Dictionary<string, string>(environmentVariables)
{ {
@ -69,7 +82,7 @@ namespace Templates.Test.Helpers
["ASPNETCORE_Kestrel__Certificates__Default__Password"] = _developmentCertificate.CertificatePassword, ["ASPNETCORE_Kestrel__Certificates__Default__Password"] = _developmentCertificate.CertificatePassword,
}; };
Process = ProcessEx.Run(output, workingDirectory, DotNetMuxer.MuxerPathOrDefault(), arguments, envVars: finalEnvironmentVariables); Process = ProcessEx.Run(output, workingDirectory, process, arguments, envVars: finalEnvironmentVariables);
logger?.LogInformation("AspNetProcess - process started"); logger?.LogInformation("AspNetProcess - process started");

View File

@ -110,14 +110,16 @@ namespace Templates.Test.Helpers
} }
} }
internal async Task<ProcessResult> RunDotNetPublishAsync(IDictionary<string, string> packageOptions = null, string additionalArgs = null) internal async Task<ProcessResult> RunDotNetPublishAsync(IDictionary<string, string> packageOptions = null, string additionalArgs = null, bool noRestore = true)
{ {
Output.WriteLine("Publishing ASP.NET Core application..."); Output.WriteLine("Publishing ASP.NET Core application...");
// Avoid restoring as part of build or publish. These projects should have already restored as part of running dotnet new. Explicitly disabling restore // Avoid restoring as part of build or publish. These projects should have already restored as part of running dotnet new. Explicitly disabling restore
// should avoid any global contention and we can execute a build or publish in a lock-free way // should avoid any global contention and we can execute a build or publish in a lock-free way
using var result = ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), $"publish --no-restore -c Release /bl {additionalArgs}", packageOptions); var restoreArgs = noRestore ? "--no-restore" : null;
using var result = ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), $"publish {restoreArgs} -c Release /bl {additionalArgs}", packageOptions);
await result.Exited; await result.Exited;
CaptureBinLogOnFailure(result); CaptureBinLogOnFailure(result);
return new ProcessResult(result); return new ProcessResult(result);
@ -188,7 +190,7 @@ namespace Templates.Test.Helpers
["ASPNETCORE_Logging__Console__FormatterOptions__IncludeScopes"] = "true", ["ASPNETCORE_Logging__Console__FormatterOptions__IncludeScopes"] = "true",
}; };
var projectDll = $"{ProjectName}.dll"; var projectDll = Path.Combine(TemplatePublishDir, ProjectName);
return new AspNetProcess(Output, TemplatePublishDir, projectDll, environment, published: true, hasListeningUri: hasListeningUri); return new AspNetProcess(Output, TemplatePublishDir, projectDll, environment, published: true, hasListeningUri: hasListeningUri);
} }

View File

@ -35,7 +35,7 @@
MicrosoftNETSdkRazorPackageVersion=$(MicrosoftNETSdkRazorPackageVersion); MicrosoftNETSdkRazorPackageVersion=$(MicrosoftNETSdkRazorPackageVersion);
MicrosoftAspNetCoreAppRefPackageVersion=$(MicrosoftAspNetCoreAppRefPackageVersion); MicrosoftAspNetCoreAppRefPackageVersion=$(MicrosoftAspNetCoreAppRefPackageVersion);
MicrosoftAspNetCoreAppRuntimePackageVersion=@(_RuntimePackageVersionInfo->'%(PackageVersion)'); MicrosoftAspNetCoreAppRuntimePackageVersion=@(_RuntimePackageVersionInfo->'%(PackageVersion)');
SupportedRuntimeIdentifiers=$(SupportedRuntimeIdentifiers); SupportedRuntimeIdentifiers=$(SupportedRuntimeIdentifiers.Trim());
DefaultNetCoreTargetFramework=$(DefaultNetCoreTargetFramework); DefaultNetCoreTargetFramework=$(DefaultNetCoreTargetFramework);
RepoRoot=$(RepoRoot); RepoRoot=$(RepoRoot);
Configuration=$(Configuration); Configuration=$(Configuration);

View File

@ -223,6 +223,77 @@ namespace Templates.Test
} }
} }
[ConditionalFact]
[SkipOnHelix("cert failure", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
public async Task MvcTemplate_SingleFileExe()
{
// This test verifies publishing an MVC app as a single file exe works. We'll limit testing
// this to a few operating systems to make our lives easier.
string runtimeIdentifer;
if (OperatingSystem.IsWindows())
{
runtimeIdentifer = "win-x64";
}
else if (OperatingSystem.IsLinux())
{
runtimeIdentifer = "linux-x64";
}
else
{
return;
}
Project = await ProjectFactory.GetOrCreateProject("mvcindividualuld", Output);
Project.RuntimeIdentifier = runtimeIdentifer;
var createResult = await Project.RunDotNetNewAsync("mvc", auth: "Individual", useLocalDB: true);
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", Project, createResult));
var publishResult = await Project.RunDotNetPublishAsync(additionalArgs: $"/p:PublishSingleFile=true -r {runtimeIdentifer}", noRestore: false);
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult));
var pages = new[]
{
new Page
{
// Verify a view from the app works
Url = PageUrls.HomeUrl,
Links = new []
{
PageUrls.HomeUrl,
PageUrls.RegisterUrl,
PageUrls.LoginUrl,
PageUrls.HomeUrl,
PageUrls.PrivacyUrl,
PageUrls.DocsUrl,
PageUrls.PrivacyUrl
}
},
new Page
{
// Verify a view from a RCL (in this case IdentityUI) works
Url = PageUrls.RegisterUrl,
Links = new []
{
PageUrls.HomeUrl,
PageUrls.RegisterUrl,
PageUrls.LoginUrl,
PageUrls.HomeUrl,
PageUrls.PrivacyUrl,
PageUrls.ExternalArticle,
PageUrls.PrivacyUrl
}
},
};
using var aspNetProcess = Project.StartPublishedProjectAsync();
Assert.False(
aspNetProcess.Process.HasExited,
ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", Project, aspNetProcess.Process));
await aspNetProcess.AssertPagesOk(pages);
}
[Fact] [Fact]
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/23993")] [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/23993")]
public async Task MvcTemplate_RazorRuntimeCompilation_BuildsAndPublishes() public async Task MvcTemplate_RazorRuntimeCompilation_BuildsAndPublishes()