Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System.Reflection;
using JasperFx;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Shouldly;
using Wolverine;
using Xunit;

namespace CoreTests.Configuration;

/// <summary>
/// GH-3776: the application assembly is resolved by walking the stack for the first frame outside Wolverine
/// that isn't part of the framework. Under an async test fixture the frames between Wolverine and the test
/// class belong to the test RUNNER, so xUnit v3's own "xunit.v3.core" was being adopted for handler discovery.
/// Discovery then scanned an assembly containing none of the suite's handlers, and every InvokeAsync failed
/// with IndeterminateRoutesException — 43 of 86 tests in one CIPolecat shard, with the run's only visible
/// symptom being "Could not determine any valid subscribers or local handlers".
/// </summary>
public class application_assembly_is_never_a_test_runner
{
[Theory]
[InlineData("xunit.v3.core")]
[InlineData("xunit.execution.dotnet")]
[InlineData("xunit.runner.utility.netcoreapp10")]
[InlineData("nunit.framework")]
[InlineData("NUnit3.TestAdapter")]
[InlineData("TUnit.Engine")]
[InlineData("MSTest.TestAdapter")]
[InlineData("testhost")]
[InlineData("ReSharperTestRunner")]
[InlineData("JetBrains.ReSharper.TestRunner.Merged")]
public void recognizes_test_runner_assemblies(string assemblyName)
{
WolverineOptions.IsTestRunnerAssembly(assemblyName).ShouldBeTrue();
}

[Theory]
[InlineData("CoreTests")]
[InlineData("PolecatTests")]
[InlineData("Wolverine")]
[InlineData("Wolverine.SqlServer")]
[InlineData("MyApp.Api")]
// "Xenon" and "Nutrition" share a prefix boundary with xunit/nunit only if the check is sloppier
// than StartsWith on the full runner token — pin that they are ordinary application assemblies.
[InlineData("Xenon.Services")]
[InlineData("Nutrition.Domain")]
public void does_not_flag_ordinary_application_assemblies(string assemblyName)
{
WolverineOptions.IsTestRunnerAssembly(assemblyName).ShouldBeFalse();
}

[Fact]
public void refuses_a_test_runner_assembly_handed_over_by_jasperfx()
{
var options = new WolverineOptions();

// JasperFx does its own stack walk with no test-runner exclusion, so this is exactly what it handed
// Wolverine on the failing CI runs.
var xunitCore = typeof(FactAttribute).Assembly;
WolverineOptions.IsTestRunnerAssembly(xunitCore.GetName().Name!).ShouldBeTrue();

options.ReadJasperFxOptions(new JasperFxOptions { ApplicationAssembly = xunitCore });

options.ApplicationAssembly.ShouldNotBeNull();
WolverineOptions.IsTestRunnerAssembly(options.ApplicationAssembly!.GetName().Name!).ShouldBeFalse();
}

[Fact]
public async Task a_host_bootstrapped_from_a_test_assembly_never_adopts_the_runner()
{
// The end-to-end guard, and the one that would have caught GH-3776: whatever the stack happens to
// look like on this platform and runner, the assembly Wolverine scans for handlers must be the test
// assembly that registered the host — never the runner hosting it.
using var host = await Host.CreateDefaultBuilder().UseWolverine()
.StartAsync(cancellationToken: TestContext.Current.CancellationToken);

var options = host.Services.GetRequiredService<WolverineOptions>();
var adopted = options.ApplicationAssembly.ShouldNotBeNull();

WolverineOptions.IsTestRunnerAssembly(adopted.GetName().Name!)
.ShouldBeFalse($"handler discovery adopted the test runner assembly '{adopted.GetName().Name}'");

options.Assemblies.ShouldContain(typeof(application_assembly_is_never_a_test_runner).Assembly);
}
}
25 changes: 24 additions & 1 deletion src/Wolverine/WolverineOptions.Assemblies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ public Assembly? ApplicationAssembly
continue;
}

if (assemblyName.StartsWith("System") || assemblyName.StartsWith("Microsoft"))
if (assemblyName.StartsWith("System") || assemblyName.StartsWith("Microsoft") ||
IsTestRunnerAssembly(assemblyName))
{
continue;
}
Expand All @@ -109,6 +110,28 @@ public Assembly? ApplicationAssembly
return Assembly.GetEntryAssembly();
}

// GH-3776: a test-runner assembly is never the application assembly. determineCallingAssembly walks the
// stack for the first frame outside Wolverine that isn't System*/Microsoft*, but under an async test
// fixture the frames between Wolverine and the test class belong to the RUNNER, not the test assembly —
// so xUnit v3's own "xunit.v3.core" was adopted for handler discovery. Discovery then scanned an assembly
// with no handlers in it and every conventionally-discovered handler in the test assembly vanished, with
// only a downstream IndeterminateRoutesException ("Could not determine any valid subscribers or local
// handlers") to show for it. Skipping the runner lets the walk continue out to the test assembly; if
// nothing else matches, the Assembly.GetEntryAssembly() fallback is the test assembly anyway.
//
// Prefix matching is deliberate: it covers xunit.v3.core / xunit.execution.dotnet / nunit.framework and
// the runner hosts, without needing to track exact assembly names across runner versions.
internal static bool IsTestRunnerAssembly(string assemblyName)
{
return assemblyName.StartsWith("xunit", StringComparison.OrdinalIgnoreCase)
|| assemblyName.StartsWith("nunit", StringComparison.OrdinalIgnoreCase)
|| assemblyName.StartsWith("TUnit", StringComparison.OrdinalIgnoreCase)
|| assemblyName.StartsWith("MSTest", StringComparison.OrdinalIgnoreCase)
|| assemblyName.StartsWith("testhost", StringComparison.OrdinalIgnoreCase)
|| assemblyName.StartsWith("ReSharperTestRunner", StringComparison.OrdinalIgnoreCase)
|| assemblyName.StartsWith("JetBrains.", StringComparison.OrdinalIgnoreCase);
}

private void establishApplicationAssembly(string? assemblyName)
{
if (assemblyName.IsNotEmpty())
Expand Down
13 changes: 12 additions & 1 deletion src/Wolverine/WolverineOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,18 @@ internal void ReadJasperFxOptions(JasperFxOptions jasperfx)

if (_applicationAssembly == null)
{
ApplicationAssembly = jasperfx.ApplicationAssembly;
// GH-3776: JasperFx resolves its own application assembly with the same kind of stack walk and has
// no test-runner exclusion, so in a test process it can hand us the RUNNER assembly. Adopting that
// silently points handler discovery at an assembly with no handlers in it. Drop it and resolve
// locally instead, where determineCallingAssembly's IsTestRunnerAssembly check keeps the walk
// honest and ultimately falls back to the entry assembly.
var fromJasperFx = jasperfx.ApplicationAssembly;
if (fromJasperFx?.GetName().Name is { } pinned && IsTestRunnerAssembly(pinned))
{
fromJasperFx = null;
}

ApplicationAssembly = fromJasperFx;

if (ApplicationAssembly == null)
{
Expand Down
Loading