dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.48k stars 10.03k forks source link

WebApplicationFactory has content root issue when running tests on WSL2 #55867

Open markm77 opened 5 months ago

markm77 commented 5 months ago

Is there an existing issue for this?

Describe the bug

I am using WebApplicationFactory to run tests on Windows but am having problems when I try to run the same tests on WSL2 (e.g. using Visual Studio remote testing).

I give a repro below with the exception produced.

In the case of the repro, the problem is due to the argument --contentRoot=C:\Repos\AspNetCore.Docs.Samples\test\integration-tests\8.x\IntegrationTestsSample\src\RazorPagesProject being passed to WebApplication.CreateBuilder(args) which is unsuitable for WSL2.

Expected Behavior

I would greatly appreciate if you could provide a fix/workaround that allows running the same WebApplicationFactory tests on both Windows and WSL2. I use Visual Studio remote testing for WSL2 testing. It is important for me to test using WSL2 before releases as I use Linux in production.

Steps To Reproduce

In order to repro this for you, I cloned the official demo project (https://github.com/dotnet/AspNetCore.Docs.Samples/tree/main/test/integration-tests/8.x/IntegrationTestsSample) and created a solution in Visual Studio with both the test and SUT projects plus the following testEnvironments.json (which points to WSL on my machine):

{
  "version": "1",
  "environments": [
    {
      "name": "WSL-Ubuntu-22.04",
      "type": "wsl",
      "wslDistribution": "Ubuntu-22.04"
    }
  ]
}

Running the tests for the official demo project works fine on Windows but as soon as I switch to WSL2, I get the exception shown for all tests.

Exceptions (if any)

Message:  System.IO.DirectoryNotFoundException : /mnt/c/Repos/AspNetCore.Docs.Samples/test/integration-tests/8.x/IntegrationTestsSample/tests/RazorPagesProject.Tests/bin/Debug/net8.0/C:\Repos\AspNetCore.Docs.Samples\test\integration-tests\8.x\IntegrationTestsSample\src\RazorPagesProject/

Stack Trace:  PhysicalFileProvider.ctor(String root, ExclusionFilters filters) HostBuilder.CreateHostingEnvironment(IConfiguration hostConfiguration) HostApplicationBuilder.Initialize(HostApplicationBuilderSettings settings, HostBuilderContext& hostBuilderContext, IHostEnvironment& environment, LoggingBuilder& logging, MetricsBuilder& metrics) HostApplicationBuilder.ctor(HostApplicationBuilderSettings settings) WebApplicationBuilder.ctor(WebApplicationOptions options, Action1 configureDefaults) WebApplication.CreateBuilder(String[] args) Program.<Main>$(String[] args) line 10 InvokeStub_Program.<Main>$(Object, Span1) MethodBaseInvoker.InvokeWithOneArg(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) --- End of stack trace from previous location --- HostingListener.CreateHost() <>c__DisplayClass10_0.b__0(String[] args) DeferredHostBuilder.Build() WebApplicationFactory1.CreateHost(IHostBuilder builder) DelegatedWebApplicationFactory.CreateHost(IHostBuilder builder) WebApplicationFactory1.ConfigureHostBuilder(IHostBuilder hostBuilder) WebApplicationFactory1.EnsureServer() WebApplicationFactory1.CreateDefaultClient(DelegatingHandler[] handlers) WebApplicationFactory1.CreateDefaultClient(Uri baseAddress, DelegatingHandler[] handlers) WebApplicationFactory1.CreateClient(WebApplicationFactoryClientOptions options) WebApplicationFactory`1.CreateClient()

.NET Version

8.0.300

Anything else?

I cannot find a simple workaround for this issue.

If I add the following to CustomWebApplicationFactory:

if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
    builder.UseContentRoot("/mnt/c/Repos/AspNetCore.Docs.Samples/test/integration-tests/8.x/IntegrationTestsSample/src/RazorPagesProject");
}

I then get the following exception:

Message: 
System.ArgumentException : The path must be absolute. (Parameter 'root')

  Stack Trace: 
PhysicalFileProvider.ctor(String root, ExclusionFilters filters)
<>c.<UseStaticWebAssetsCore>b__1_0(String contentRoot)
ManifestStaticWebAssetFileProvider.ctor(StaticWebAssetManifest manifest, Func`2 fileProviderFactory)
StaticWebAssetsLoader.UseStaticWebAssetsCore(IWebHostEnvironment environment, Stream manifest)
StaticWebAssetsLoader.UseStaticWebAssets(IWebHostEnvironment environment, IConfiguration configuration)
BootstrapHostBuilder.RunDefaultCallbacks()
WebApplicationBuilder.InitializeHosting(BootstrapHostBuilder bootstrapHostBuilder)
WebApplicationBuilder.ctor(WebApplicationOptions options, Action`1 configureDefaults)
WebApplication.CreateBuilder(String[] args)
Program.<Main>$(String[] args) line 10
RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)
--- End of stack trace from previous location ---
HostingListener.CreateHost()
<>c__DisplayClass10_0.<ResolveHostFactory>b__0(String[] args)
DeferredHostBuilder.Build()
WebApplicationFactory`1.CreateHost(IHostBuilder builder)
DelegatedWebApplicationFactory.CreateHost(IHostBuilder builder)
WebApplicationFactory`1.ConfigureHostBuilder(IHostBuilder hostBuilder)
WebApplicationFactory`1.EnsureServer()
WebApplicationFactory`1.CreateDefaultClient(DelegatingHandler[] handlers)
WebApplicationFactory`1.CreateDefaultClient(Uri baseAddress, DelegatingHandler[] handlers)
WebApplicationFactory`1.CreateClient(WebApplicationFactoryClientOptions options)
AuthTests.Get_SecurePageIsReturnedForAnAuthenticatedUser() line 105
--- End of stack trace from previous location ---
martincostello commented 5 months ago

I think the underlying issue here is that out-of-the-box, the Microsoft.AspNetCore.Mvc.Testing package puts the solution root into the assembly as MSBuild [AssemblyMetadata], and then that's read using reflection at runtime in the tests to find it. Because you're building on Windows and then running on Linux via the mount (or vice versa) you end up mixing file systems/path separators, and then they don't work together.

I'll leave it for the team to answer if there's a sensible solution to this use case to make it "just work", but it it feels like a weird kind of workflow to me. I would suggest you build and then test the app from the same single operating system, rather than mixing the roles across operating systems.

markm77 commented 5 months ago

Hi Martin,

Thanks for this.

In terms of workflow, I believe the main benefit of the remote testing functionality is that a person doesn't need to continually sync source code between OSes to ensure cross-platform compat. For example, I can make an edit and test it on both Windows and Linux before committing. I can also generally test on Windows (where I develop) but then run a regression run on Linux with the same source code before making a product release. This feature saves a huge amount of time and to me this sort of cross-platform convenience is part of the attraction of .NET.

I don't mind a workaround such as

if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
    builder.UseContentRoot("/mnt/c/Repos/AspNetCore.Docs.Samples/test/integration-tests/8.x/IntegrationTestsSample/src/RazorPagesProject");
}

but even that doesn't work which seems a bug in itself.

I also want to point out you can run the same ASP.NET Core project just fine on Windows and WSL in Visual Studio from Windows source code (no issue with content root), and also run pretty complex tests without issue. So this seems just a unique issue with WebApplicationFactory and how it works.

BR, Mark

paulomorgado commented 4 months ago

I'm having the same problem on Windows with a console application that uses WebApplicationFactory<TEntryPoint>.

It works fine running from Visual Studio, but from the command-line, it sets the contentRoot to the root of the solution, instead of the project.

wbsnipes commented 3 weeks ago

Had the same problem seen here but resolved it with the following declaration based on OP's suggestion. Works in Visual Studio 17.11.5 or perhaps earlier. Also works in Azure DevOps build.

var factory = new WebApplicationFactory<Program>() .WithWebHostBuilder(builder => { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { string buildDir = AppDomain.CurrentDomain.BaseDirectory; builder.UseContentRoot(buildDir); } builder.ConfigureServices((context, services) => { ... }); });

Mrsevic commented 1 week ago

@wbsnipes Thanks Will, your solution worked in the end for me.