Skip to content
Closed
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
1 change: 1 addition & 0 deletions jasperfx.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<Project Path="src/Widgets4/Widgets4.csproj" />
<Project Path="src/Widgets5/Widgets5.csproj" />
<Project Path="src/TestRunnerStandIn/TestRunnerStandIn.csproj" />
<Project Path="src/ExtensionStandIn/ExtensionStandIn.csproj" />
</Folder>
<Project Path="build/Build.csproj">
<Build Project="false" />
Expand Down
1 change: 1 addition & 0 deletions src/CoreTests/CoreTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<ProjectReference Include="..\FSharpTypes\FSharpTypes.fsproj" />
<ProjectReference Include="..\Widgets5\Widgets5.csproj" />
<ProjectReference Include="..\TestRunnerStandIn\TestRunnerStandIn.csproj" />
<ProjectReference Include="..\ExtensionStandIn\ExtensionStandIn.csproj" />
</ItemGroup>

</Project>
94 changes: 94 additions & 0 deletions src/CoreTests/JasperFxOptionsTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using ExtensionStandIn;
using JasperFx;
using JasperFx.CodeGeneration;
using JasperFx.CommandLine.Descriptions;
Expand Down Expand Up @@ -395,4 +396,97 @@ public void never_adopts_a_test_runner_as_the_calling_assembly()
assembly.ShouldNotBeNull();
JasperFxOptions.IsTestRunnerAssembly(assembly.GetName().Name!).ShouldBeFalse();
}

// GH-601: UseWolverine() and AddMarten() both call services.AddJasperFx() from inside their own
// assembly, so the first frame outside JasperFx belongs to the extension rather than the application.

[Theory]
[InlineData("JasperFx")]
[InlineData("JasperFx.Events")]
[InlineData("JasperFx.RuntimeCompiler")]
[InlineData("Wolverine")]
[InlineData("Wolverine.SqlServer")]
[InlineData("WolverineFx.RabbitMQ")]
[InlineData("Marten")]
[InlineData("Marten.AspNetCore")]
[InlineData("Weasel.Postgresql")]
[InlineData("Polecat")]
[InlineData("CritterWatch.Server")]
[InlineData("Oakton")]
public void recognizes_critter_stack_framework_assemblies(string assemblyName)
{
JasperFxOptions.IsCritterStackAssembly(assemblyName).ShouldBeTrue();
}

[Theory]
// An application is not framework code just because it is named for a product. Matching is by exact
// name or dotted prefix, never a bare StartsWith.
[InlineData("MartenPlayground")]
[InlineData("WolverineDemo")]
[InlineData("JasperFxSamples")]
[InlineData("MyApp")]
[InlineData("CoreTests")]
// And the Critter Stack repos' own test assemblies ARE the application under test -- their handlers
// and documents are the types discovery has to find.
[InlineData("Wolverine.RabbitMQ.Tests")]
[InlineData("Marten.Testing")]
[InlineData("JasperFx.Events.Tests")]
public void does_not_mistake_applications_or_suites_for_framework_assemblies(string assemblyName)
{
JasperFxOptions.IsCritterStackAssembly(assemblyName).ShouldBeFalse();
}

[Fact]
public async Task registration_is_attributed_to_the_app_not_the_extension_that_registered_for_it()
{
var original = JasperFxOptions.RememberedApplicationAssembly;
try
{
JasperFxOptions.RememberedApplicationAssembly = null;

// AddSomeCritterStackTool lives in an assembly named "Wolverine.StackWalkStandIn" and calls
// AddJasperFx() on our behalf, which is exactly what UseWolverine()/AddMarten() do.
using var host = await Host.CreateDefaultBuilder()
.ConfigureServices(s => s.AddSomeCritterStackTool())
.UseEnvironment("Development")
.StartAsync(TestContext.Current.CancellationToken);

var options = host.Services.GetRequiredService<JasperFxOptions>();

options.RegistrationCallingAssembly.ShouldBe(GetType().Assembly);

// ...and because registration is now attributed correctly, the GH-3521 divergence warning
// stays quiet on a host where nothing is actually wrong.
options.ApplicationAssemblyReuseWarning.ShouldBeNull();
}
finally
{
JasperFxOptions.RememberedApplicationAssembly = original;
}
}

[Fact]
public void an_extension_registration_does_not_pin_the_extension_as_the_application_assembly()
{
var original = JasperFxOptions.RememberedApplicationAssembly;
try
{
JasperFxOptions.RememberedApplicationAssembly = null;

// No meaningful IHostEnvironment.ApplicationName here, so establishApplicationAssembly falls
// through to the process-wide pin that AddJasperFx seeded from the same stack walk. Before the
// fix that pin -- and therefore type discovery -- was the extension assembly.
var services = new ServiceCollection();
services.AddSomeCritterStackTool();

var options = services.BuildServiceProvider().GetRequiredService<JasperFxOptions>();

options.ApplicationAssembly.ShouldBe(GetType().Assembly);
JasperFxOptions.RememberedApplicationAssembly.ShouldBe(GetType().Assembly);
}
finally
{
JasperFxOptions.RememberedApplicationAssembly = original;
}
}
}
18 changes: 18 additions & 0 deletions src/ExtensionStandIn/CritterStackExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using JasperFx;
using Microsoft.Extensions.DependencyInjection;

namespace ExtensionStandIn;

/// <summary>
/// Stands in for a Critter Stack extension in the one respect that matters to
/// <c>JasperFxOptions.DetermineCallingAssembly</c>: it calls <c>AddJasperFx()</c> from inside its own
/// assembly on the application's behalf, exactly the way <c>UseWolverine()</c> and <c>AddMarten()</c> do.
/// This assembly is named "Wolverine.StackWalkStandIn" so the walk sees it as framework code. See GH-601.
/// </summary>
public static class CritterStackExtension
{
public static IServiceCollection AddSomeCritterStackTool(this IServiceCollection services)
{
return services.AddJasperFx();
}
}
13 changes: 13 additions & 0 deletions src/ExtensionStandIn/ExtensionStandIn.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- GH-601: like TestRunnerStandIn, the point of this project is its assembly NAME. It stands in
for a Critter Stack extension that calls services.AddJasperFx() on the application's behalf,
which is what UseWolverine() and AddMarten() both do. -->
<AssemblyName>Wolverine.StackWalkStandIn</AssemblyName>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\JasperFx\JasperFx.csproj" />
</ItemGroup>
</Project>
53 changes: 52 additions & 1 deletion src/JasperFx/JasperFxOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ private void checkForDivergentApplicationAssembly(Assembly adopted)
}

if (assemblyName.StartsWith("System") || assemblyName.StartsWith("Microsoft") ||
IsTestRunnerAssembly(assemblyName))
IsTestRunnerAssembly(assemblyName) || IsCritterStackAssembly(assemblyName))
{
continue;
}
Expand Down Expand Up @@ -273,6 +273,57 @@ internal static bool IsTestRunnerAssembly(string assemblyName)
|| assemblyName.StartsWith("NCrunch.", StringComparison.OrdinalIgnoreCase);
}

// GH-601: a Critter Stack extension registers JasperFx on the application's behalf --
// UseWolverine() and AddMarten() both call services.AddJasperFx() from inside their own assembly --
// so the first frame outside JasperFx belongs to the EXTENSION, not the app. RegistrationCallingAssembly
// then stopped meaning what its name says, and checkForDivergentApplicationAssembly compared the
// extension against the (correctly resolved) application assembly and warned on healthy hosts. That
// inverts the warning's whole value: it trains readers to ignore it, and its absence stops being
// evidence -- on JasperFx/wolverine#3776 "zero occurrences of the warning" was explicitly recorded as
// grounds for ruling out an application-assembly problem, which was exactly the bug.
//
// Matched as an exact name or a dotted prefix rather than a bare StartsWith, so an application named
// "MartenPlayground" or "WolverineDemo" is left alone.
private static readonly string[] _critterStackAssemblies =
[
"JasperFx",
"Wolverine",
"WolverineFx",
"Marten",
"Weasel",
"Polecat",
"CritterWatch",
"Oakton"
];

internal static bool IsCritterStackAssembly(string assemblyName)
{
// The test assemblies inside the Critter Stack repos themselves -- Wolverine.RabbitMQ.Tests,
// Marten.Testing and friends -- ARE the application as far as discovery is concerned, because the
// handlers and documents under test live in them. Skipping those would reintroduce GH-600's defect
// from the other direction.
if (assemblyName.EndsWith("Tests", StringComparison.OrdinalIgnoreCase)
|| assemblyName.EndsWith("Testing", StringComparison.OrdinalIgnoreCase))
{
return false;
}

foreach (var name in _critterStackAssemblies)
{
if (assemblyName.Equals(name, StringComparison.OrdinalIgnoreCase))
{
return true;
}

if (assemblyName.StartsWith(name + ".", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

return false;
}

/// <summary>
/// Attempts to resolve the project root directory by climbing up from the current path
/// looking for .csproj or .sln files. This is useful for Console apps where
Expand Down