Skip to content

Commit f7e1712

Browse files
authored
Add support for generating app manifest files for tests (#28575)
* Add initial support for reading content paths from file * Add task for generating app manifest file * Completed support for generating AppManifest.json * Fix deserializing JSON and getting path by assembly name * Address feedback from peer review * Fix up target definition and formatting * Undo typo fix * Fix target name and JSON parsing * Update test scenario when using app manifest * Add new project to ProjectReferences * Generate file before PrepareResources * Address feedback from peer review
1 parent facc8c7 commit f7e1712

File tree

8 files changed

+137
-29
lines changed

8 files changed

+137
-29
lines changed

Directory.Build.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@
201201
<DeterministicSourcePaths Condition="'$(IsSampleProject)' == 'true' OR '$(IsTestAssetProject)' == 'true'">false</DeterministicSourcePaths>
202202
<!-- Projects which reference Microsoft.AspNetCore.Mvc.Testing should import this targets file to ensure dependency .deps.json files are copied into test output. -->
203203
<MvcTestingTargets>$(MSBuildThisFileDirectory)src\Mvc\Mvc.Testing\src\Microsoft.AspNetCore.Mvc.Testing.targets</MvcTestingTargets>
204+
<_MvcTestingTasksAssembly>$(ArtifactsBinDir)\Microsoft.AspNetCore.Mvc.Testing.Tasks\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Mvc.Testing.Tasks.dll</_MvcTestingTasksAssembly>
204205
<!-- IIS native projects can only be built on Windows for x86 and x64. -->
205206
<BuildIisNativeProjects Condition=" '$(TargetOsName)' == 'win' AND ('$(TargetArchitecture)' == 'x86' OR '$(TargetArchitecture)' == 'x64') ">true</BuildIisNativeProjects>
206207
<!-- This property is shared by several projects to layout the AspNetCore.App targeting pack for installers -->

eng/ProjectReferences.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.RazorPages" ProjectPath="$(RepoRoot)src\Mvc\Mvc.RazorPages\src\Microsoft.AspNetCore.Mvc.RazorPages.csproj" />
120120
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Razor" ProjectPath="$(RepoRoot)src\Mvc\Mvc.Razor\src\Microsoft.AspNetCore.Mvc.Razor.csproj" />
121121
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.TagHelpers" ProjectPath="$(RepoRoot)src\Mvc\Mvc.TagHelpers\src\Microsoft.AspNetCore.Mvc.TagHelpers.csproj" />
122+
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Testing.Tasks" ProjectPath="$(RepoRoot)src\Mvc\Mvc.Testing.Tasks\src\Microsoft.AspNetCore.Mvc.Testing.Tasks.csproj" />
122123
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Testing" ProjectPath="$(RepoRoot)src\Mvc\Mvc.Testing\src\Microsoft.AspNetCore.Mvc.Testing.csproj" />
123124
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.ViewFeatures" ProjectPath="$(RepoRoot)src\Mvc\Mvc.ViewFeatures\src\Microsoft.AspNetCore.Mvc.ViewFeatures.csproj" />
124125
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc" ProjectPath="$(RepoRoot)src\Mvc\Mvc\src\Microsoft.AspNetCore.Mvc.csproj" />
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Reflection;
8+
using System.Runtime.Serialization.Json;
9+
using System.Text;
10+
using Microsoft.Build.Framework;
11+
using Microsoft.Build.Utilities;
12+
13+
namespace Microsoft.AspNetCore.Mvc.Testing.Tasks
14+
{
15+
/// <summary>
16+
/// Generate a JSON file mapping assemblies to content root paths.
17+
/// </summary>
18+
public class GenerateMvcTestManifestTask : Task
19+
{
20+
/// <summary>
21+
/// The path to output the manifest file to.
22+
/// </summary>
23+
[Required]
24+
public string ManifestPath { get; set; }
25+
26+
/// <summary>
27+
/// A list of content root paths and assembly names to generate the
28+
/// manifest from.
29+
/// </summary>
30+
[Required]
31+
public ITaskItem[] Projects { get; set; }
32+
33+
/// <inheritdoc />
34+
public override bool Execute()
35+
{
36+
using var fileStream = File.Create(ManifestPath);
37+
var output = new Dictionary<string, string>();
38+
39+
foreach (var project in Projects)
40+
{
41+
var contentRoot = project.GetMetadata("ContentRoot");
42+
var assemblyName = project.GetMetadata("Identity");
43+
output[assemblyName] = contentRoot;
44+
}
45+
46+
var serializer = new DataContractJsonSerializer(typeof(Dictionary<string, string>), new DataContractJsonSerializerSettings
47+
{
48+
UseSimpleDictionaryFormat = true
49+
});
50+
using var writer = JsonReaderWriterFactory.CreateJsonWriter(fileStream, Encoding.UTF8, ownsStream: false, indent: true);
51+
serializer.WriteObject(writer, output);
52+
53+
return !Log.HasLoggedErrors;
54+
}
55+
}
56+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<Description>Build tasks for functional tests.</Description>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
6+
<GenerateDocumentationFile>false</GenerateDocumentationFile>
7+
<AddPublicApiAnalyzers>false</AddPublicApiAnalyzers>
8+
<IsPackable>false</IsPackable>
9+
<IsShipping>false</IsShipping>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<Reference Include="Microsoft.Build.Framework" />
14+
<Reference Include="Microsoft.Build.Utilities.Core" />
15+
</ItemGroup>
16+
</Project>

src/Mvc/Mvc.Testing/src/Microsoft.AspNetCore.Mvc.Testing.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@
1616
<Reference Include="Microsoft.Extensions.HostFactoryResolver.Sources" />
1717
</ItemGroup>
1818

19+
<ItemGroup>
20+
<Reference
21+
Include="Microsoft.AspNetCore.Mvc.Testing.Tasks"
22+
Targets="Build"
23+
ReferenceOutputAssembly="false"
24+
SkipGetTargetFrameworkProperties="true"
25+
UndefineProperties="TargetFramework;TargetFrameworks;RuntimeIdentifier;PublishDir" />
26+
</ItemGroup>
27+
1928
<ItemGroup>
2029
<Content Include="Microsoft.AspNetCore.Mvc.Testing.targets" Pack="true" PackagePath="build/$(TargetFramework)/" />
2130
</ItemGroup>
Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
22

3+
<PropertyGroup>
4+
<_MvcTestingTasksAssembly Condition="$(_MvcTestingTasksAssembly) == ''">$(MSBuildThisFileDirectory)..\tasks\Microsoft.AspNetCore.Mvc.Testing.dll</_MvcTestingTasksAssembly>
5+
</PropertyGroup>
6+
<UsingTask TaskName="GenerateMvcTestManifestTask" AssemblyFile="$(_MvcTestingTasksAssembly)"/>
7+
38
<!--
49
Work around https://github.com/NuGet/Home/issues/4412. MVC uses DependencyContext.Load() which looks next to a .dll
510
for a .deps.json. Information isn't available elsewhere. Need the .deps.json file for all web site applications.
@@ -23,30 +28,25 @@
2328
</ItemGroup>
2429
</Target>
2530

26-
<Target Name="_AddContentRootForProjectReferences" BeforeTargets="GetAssemblyAttributes" DependsOnTargets="_ResolveMvcTestProjectReferences">
31+
<Target Name="_AddContentRootForProjectReferences" BeforeTargets="PrepareResources" DependsOnTargets="_ResolveMvcTestProjectReferences">
2732
<ItemGroup>
28-
<WebApplicationFactoryContentRootAttribute
29-
Condition="'%(_ContentRootProjectReferences.Identity)' != ''"
30-
Include="%(_ContentRootProjectReferences.Identity)"
31-
AssemblyName="%(_ContentRootProjectReferences.FusionName)"
32-
ContentRootPath="$([System.IO.Path]::GetDirectoryName(%(_ContentRootProjectReferences.MSBuildSourceProjectFile)))"
33-
ContentRootTest="$([System.IO.Path]::GetFileName(%(_ContentRootProjectReferences.MSBuildSourceProjectFile)))"
34-
Priority="0" />
33+
<_ManifestProjects Include="%(_ContentRootProjectReferences.FusionName)">
34+
<ContentRoot>$([System.IO.Path]::GetDirectoryName(%(_ContentRootProjectReferences.MSBuildSourceProjectFile)))</ContentRoot>
35+
</_ManifestProjects>
3536
</ItemGroup>
3637

38+
<GenerateMvcTestManifestTask ManifestPath="$(IntermediateOutputPath)MvcTestingAppManifest.json" Projects="@(_ManifestProjects)"/>
39+
3740
<ItemGroup>
38-
<AssemblyAttribute
39-
Condition=" '%(WebApplicationFactoryContentRootAttribute.Identity)' != '' "
40-
Include="Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryContentRootAttribute">
41-
<_Parameter1>%(WebApplicationFactoryContentRootAttribute.AssemblyName)</_Parameter1>
42-
<_Parameter2>%(WebApplicationFactoryContentRootAttribute.ContentRootPath)</_Parameter2>
43-
<_Parameter3>%(WebApplicationFactoryContentRootAttribute.ContentRootTest)</_Parameter3>
44-
<_Parameter4>%(WebApplicationFactoryContentRootAttribute.Priority)</_Parameter4>
45-
</AssemblyAttribute>
41+
<ContentWithTargetPath Include="$(IntermediateOutputPath)MvcTestingAppManifest.json"
42+
TargetPath="MvcTestingAppManifest.json"
43+
CopyToOutputDirectory="PreserveNewest"
44+
CopyToPublishDirectory="Never"/>
45+
<FileWrites Include="$(IntermediateOutputPath)MvcTestingAppManifest.json" />
4646
</ItemGroup>
4747
</Target>
4848

49-
<Target Name="CopyAditionalFiles" AfterTargets="Build;_ResolveMvcTestProjectReferences" Condition="'$(TargetFramework)'!=''">
49+
<Target Name="_MvcCopyDependencyFiles" AfterTargets="Build;_ResolveMvcTestProjectReferences" Condition="'$(TargetFramework)'!=''">
5050
<ItemGroup>
5151
<DepsFilePaths
5252
Condition="'%(_ContentRootProjectReferences.Identity)' != ''"
@@ -56,4 +56,4 @@
5656
<Copy SourceFiles="%(DepsFilePaths.FullPath)" DestinationFolder="$(OutDir)" Condition="Exists('%(DepsFilePaths.FullPath)')" />
5757
</Target>
5858

59-
</Project>
59+
</Project>

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
using System.Linq;
88
using System.Net.Http;
99
using System.Reflection;
10+
using System.Text.Json;
11+
using System.Runtime.Serialization.Json;
1012
using Microsoft.AspNetCore.Hosting;
1113
using Microsoft.AspNetCore.Hosting.Server;
1214
using Microsoft.AspNetCore.TestHost;
@@ -93,7 +95,7 @@ public virtual IServiceProvider Services
9395

9496
/// <summary>
9597
/// Gets the <see cref="IReadOnlyList{WebApplicationFactory}"/> of factories created from this factory
96-
/// by further customizing the <see cref="IWebHostBuilder"/> when calling
98+
/// by further customizing the <see cref="IWebHostBuilder"/> when calling
9799
/// <see cref="WebApplicationFactory{TEntryPoint}.WithWebHostBuilder(Action{IWebHostBuilder})"/>.
98100
/// </summary>
99101
public IReadOnlyList<WebApplicationFactory<TEntryPoint>> Factories => _derivedFactories.AsReadOnly();
@@ -171,6 +173,35 @@ private void SetContentRoot(IWebHostBuilder builder)
171173
return;
172174
}
173175

176+
var fromFile = File.Exists("MvcTestingAppManifest.json");
177+
var contentRoot = fromFile ? GetContentRootFromFile("MvcTestingAppManifest.json") : GetContentRootFromAssembly();
178+
179+
if (contentRoot != null)
180+
{
181+
builder.UseContentRoot(contentRoot);
182+
}
183+
else
184+
{
185+
builder.UseSolutionRelativeContentRoot(typeof(TEntryPoint).Assembly.GetName().Name);
186+
}
187+
}
188+
189+
private string GetContentRootFromFile(string file)
190+
{
191+
var data = JsonSerializer.Deserialize<IDictionary<string, string>>(File.ReadAllBytes(file));
192+
var key = typeof(TEntryPoint).Assembly.GetName().FullName;
193+
try
194+
{
195+
return data[key];
196+
} catch
197+
{
198+
throw new KeyNotFoundException($"Could not find content root for project '{key}' in test manifest file '{file}'");
199+
}
200+
201+
}
202+
203+
private string GetContentRootFromAssembly()
204+
{
174205
var metadataAttributes = GetContentRootMetadataAttributes(
175206
typeof(TEntryPoint).Assembly.FullName,
176207
typeof(TEntryPoint).Assembly.GetName().Name);
@@ -194,14 +225,7 @@ private void SetContentRoot(IWebHostBuilder builder)
194225
}
195226
}
196227

197-
if (contentRoot != null)
198-
{
199-
builder.UseContentRoot(contentRoot);
200-
}
201-
else
202-
{
203-
builder.UseSolutionRelativeContentRoot(typeof(TEntryPoint).Assembly.GetName().Name);
204-
}
228+
return contentRoot;
205229
}
206230

207231
private static bool SetContentRootFromSetting(IWebHostBuilder builder)

src/Mvc/test/Mvc.FunctionalTests/TestingInfrastructureInheritanceTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ public void TestingInfrastructure_WebHost_WithWebHostBuilderRespectsCustomizatio
2929
Assert.Equal(new[] { "ConfigureWebHost", "Customization", "FurtherCustomization" }, factory.ConfigureWebHostCalled.ToArray());
3030
Assert.True(factory.CreateServerCalled);
3131
Assert.True(factory.CreateWebHostBuilderCalled);
32-
Assert.True(factory.GetTestAssembliesCalled);
32+
// GetTestAssemblies is not called when reading content roots from MvcAppManifest
33+
Assert.False(factory.GetTestAssembliesCalled);
3334
Assert.True(factory.CreateHostBuilderCalled);
3435
Assert.False(factory.CreateHostCalled);
3536
}
@@ -46,7 +47,7 @@ public void TestingInfrastructure_GenericHost_WithWithHostBuilderRespectsCustomi
4647

4748
// Assert
4849
Assert.Equal(new[] { "ConfigureWebHost", "Customization", "FurtherCustomization" }, factory.ConfigureWebHostCalled.ToArray());
49-
Assert.True(factory.GetTestAssembliesCalled);
50+
Assert.False(factory.GetTestAssembliesCalled);
5051
Assert.True(factory.CreateHostBuilderCalled);
5152
Assert.True(factory.CreateHostCalled);
5253
Assert.False(factory.CreateServerCalled);

0 commit comments

Comments
 (0)