Skip to content

Commit

Permalink
Add Aspire.Hosting.Testing to facilitate integration testing (#2310)
Browse files Browse the repository at this point in the history
  • Loading branch information
ReubenBond committed Feb 29, 2024
1 parent 55d081d commit 0391a69
Show file tree
Hide file tree
Showing 36 changed files with 1,683 additions and 24 deletions.
47 changes: 47 additions & 0 deletions Aspire.sln
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureStorageEndToEnd.ApiSer
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Dashboard.Components.Tests", "tests\Aspire.Dashboard.Components.Tests\Aspire.Dashboard.Components.Tests.csproj", "{0870A667-FB0C-4758-AEAF-9E5F092AD7C1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{A7C6452C-FEDB-4883-9AE7-29892D260AA3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Testing", "src\Aspire.Hosting.Testing\Aspire.Hosting.Testing.csproj", "{3216CF59-84B0-46FF-8572-D0AFB0155423}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Testing.Tests", "tests\Aspire.Hosting.Testing.Tests\Aspire.Hosting.Testing.Tests.csproj", "{BE46B5B3-DFD4-4565-A2CD-7D95C623B03D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DatabaseMigration", "DatabaseMigration", "{8B1802BC-6CB0-4027-850C-2AED42A82C9E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatabaseMigration.ApiService", "playground\DatabaseMigration\DatabaseMigration.ApiService\DatabaseMigration.ApiService.csproj", "{7CC9BADD-B444-49E0-B6F0-BE2CD3A4669E}"
Expand All @@ -279,6 +285,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatabaseMigration.Migration
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatabaseMigration.ApiModel", "playground\DatabaseMigration\DatabaseMigration.ApiModel\DatabaseMigration.ApiModel.csproj", "{C15F3F13-AB63-47CF-AAFE-D319F02E7B33}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestingAppHost1", "TestingAppHost1", "{A4F7DD00-59FE-4754-B0CF-3CC8C0856E17}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestingAppHost1.AppHost", "tests\TestingAppHost1\TestingAppHost1.AppHost\TestingAppHost1.AppHost.csproj", "{041C0A17-0968-4EB8-9208-5B8E257A0B33}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestingAppHost1.ServiceDefaults", "tests\TestingAppHost1\TestingAppHost1.ServiceDefaults\TestingAppHost1.ServiceDefaults.csproj", "{3D2D2428-BCB4-464F-9D03-7E9E6739D54B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestingAppHost1.MyWebApp", "tests\TestingAppHost1\TestingAppHost1.MyWebApp\TestingAppHost1.MyWebApp.csproj", "{6723830C-7E39-4709-836B-17E5EE426751}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestingAppHost1.MyWorker", "tests\TestingAppHost1\TestingAppHost1.MyWorker\TestingAppHost1.MyWorker.csproj", "{5345A33F-B845-4F2D-A934-F9D73327B1CE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "signalr", "signalr", "{E6985EED-47E3-4EAC-8222-074E5410CEDC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SignalRAppHost", "playground\signalr\SignalRAppHost\SignalRAppHost.csproj", "{89904E0B-9394-4A49-BA21-E02FF543587B}"
Expand Down Expand Up @@ -733,6 +749,14 @@ Global
{0870A667-FB0C-4758-AEAF-9E5F092AD7C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0870A667-FB0C-4758-AEAF-9E5F092AD7C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0870A667-FB0C-4758-AEAF-9E5F092AD7C1}.Release|Any CPU.Build.0 = Release|Any CPU
{3216CF59-84B0-46FF-8572-D0AFB0155423}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3216CF59-84B0-46FF-8572-D0AFB0155423}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3216CF59-84B0-46FF-8572-D0AFB0155423}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3216CF59-84B0-46FF-8572-D0AFB0155423}.Release|Any CPU.Build.0 = Release|Any CPU
{BE46B5B3-DFD4-4565-A2CD-7D95C623B03D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BE46B5B3-DFD4-4565-A2CD-7D95C623B03D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BE46B5B3-DFD4-4565-A2CD-7D95C623B03D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE46B5B3-DFD4-4565-A2CD-7D95C623B03D}.Release|Any CPU.Build.0 = Release|Any CPU
{7CC9BADD-B444-49E0-B6F0-BE2CD3A4669E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7CC9BADD-B444-49E0-B6F0-BE2CD3A4669E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7CC9BADD-B444-49E0-B6F0-BE2CD3A4669E}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -769,6 +793,22 @@ Global
{4231B6F1-1110-4992-A727-8F1176A47440}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4231B6F1-1110-4992-A727-8F1176A47440}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4231B6F1-1110-4992-A727-8F1176A47440}.Release|Any CPU.Build.0 = Release|Any CPU
{041C0A17-0968-4EB8-9208-5B8E257A0B33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{041C0A17-0968-4EB8-9208-5B8E257A0B33}.Debug|Any CPU.Build.0 = Debug|Any CPU
{041C0A17-0968-4EB8-9208-5B8E257A0B33}.Release|Any CPU.ActiveCfg = Release|Any CPU
{041C0A17-0968-4EB8-9208-5B8E257A0B33}.Release|Any CPU.Build.0 = Release|Any CPU
{3D2D2428-BCB4-464F-9D03-7E9E6739D54B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D2D2428-BCB4-464F-9D03-7E9E6739D54B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D2D2428-BCB4-464F-9D03-7E9E6739D54B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D2D2428-BCB4-464F-9D03-7E9E6739D54B}.Release|Any CPU.Build.0 = Release|Any CPU
{6723830C-7E39-4709-836B-17E5EE426751}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6723830C-7E39-4709-836B-17E5EE426751}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6723830C-7E39-4709-836B-17E5EE426751}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6723830C-7E39-4709-836B-17E5EE426751}.Release|Any CPU.Build.0 = Release|Any CPU
{5345A33F-B845-4F2D-A934-F9D73327B1CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5345A33F-B845-4F2D-A934-F9D73327B1CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5345A33F-B845-4F2D-A934-F9D73327B1CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5345A33F-B845-4F2D-A934-F9D73327B1CE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -897,6 +937,8 @@ Global
{157A434E-E3CA-4080-96CF-903CC3DF66E9} = {8AA07A14-A4A7-45EC-B0F6-4690B516B16D}
{921CB408-5E37-4354-B4CF-EAE517F633DC} = {8AA07A14-A4A7-45EC-B0F6-4690B516B16D}
{0870A667-FB0C-4758-AEAF-9E5F092AD7C1} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
{3216CF59-84B0-46FF-8572-D0AFB0155423} = {A7C6452C-FEDB-4883-9AE7-29892D260AA3}
{BE46B5B3-DFD4-4565-A2CD-7D95C623B03D} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
{8B1802BC-6CB0-4027-850C-2AED42A82C9E} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
{7CC9BADD-B444-49E0-B6F0-BE2CD3A4669E} = {8B1802BC-6CB0-4027-850C-2AED42A82C9E}
{E5C93F8B-D31B-4268-89EB-830EDC5524D0} = {8B1802BC-6CB0-4027-850C-2AED42A82C9E}
Expand All @@ -907,6 +949,11 @@ Global
{27B6DDFB-374D-43D5-918D-91B2EE7AEF72} = {E6985EED-47E3-4EAC-8222-074E5410CEDC}
{9C30FFD6-2262-45E7-B010-24B30E0433C2} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
{51654CD7-2E05-4664-B2EB-95308A300609} = {9C30FFD6-2262-45E7-B010-24B30E0433C2}
{A4F7DD00-59FE-4754-B0CF-3CC8C0856E17} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
{041C0A17-0968-4EB8-9208-5B8E257A0B33} = {A4F7DD00-59FE-4754-B0CF-3CC8C0856E17}
{3D2D2428-BCB4-464F-9D03-7E9E6739D54B} = {A4F7DD00-59FE-4754-B0CF-3CC8C0856E17}
{6723830C-7E39-4709-836B-17E5EE426751} = {A4F7DD00-59FE-4754-B0CF-3CC8C0856E17}
{5345A33F-B845-4F2D-A934-F9D73327B1CE} = {A4F7DD00-59FE-4754-B0CF-3CC8C0856E17}
{0244203D-7491-4414-9C88-10BFED9C5B2D} = {9C30FFD6-2262-45E7-B010-24B30E0433C2}
{867A00A7-AF8E-4396-9583-982FBB31762C} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
{4231B6F1-1110-4992-A727-8F1176A47440} = {867A00A7-AF8E-4396-9583-982FBB31762C}
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Hosting.Orleans/Aspire.Hosting.Orleans.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>$(NetCurrent)</TargetFramework>
<IsPackable>true</IsPackable>
<PackageTags>aspire hosting orleans</PackageTags>
<Description>Orleans support for .NET Aspire.</Description>
</PropertyGroup>

Expand Down
21 changes: 21 additions & 0 deletions src/Aspire.Hosting.Testing/Aspire.Hosting.Testing.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(NetCurrent)</TargetFramework>
<OutputType>Library</OutputType>
<IsPackable>true</IsPackable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>$(NoWarn);CS8002</NoWarn>
<PackageTags>aspire testing</PackageTags>
<Description>Testing support for the .NET Aspire application model.</Description>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Aspire.Hosting\Aspire.Hosting.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http.Resilience" />
</ItemGroup>

</Project>
208 changes: 208 additions & 0 deletions src/Aspire.Hosting.Testing/DistributedApplicationEntryPointInvoker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
using System.Diagnostics;
using System.Reflection;
using Microsoft.Extensions.Hosting;

namespace Aspire.Hosting.Testing;

internal sealed class DistributedApplicationEntryPointInvoker
{
// This helpers encapsulates all of the complex logic required to:
// 1. Execute the entry point of the specified assembly in a different thread.
// 2. Wait for the diagnostic source events to fire
// 3. Give the caller a chance to execute logic to mutate the IDistributedApplicationBuilder
// 4. Resolve the instance of the DistributedApplication
// 5. Allow the caller to determine if the entry point has completed
public static Func<string[], CancellationToken, Task<DistributedApplication>>? ResolveEntryPoint(
Assembly assembly,
Action<DistributedApplicationOptions, HostApplicationBuilderSettings>? onConstructing = null,
Action<DistributedApplicationBuilder>? onConstructed = null,
Action<DistributedApplicationBuilder>? onBuilding = null,
Action<Exception?>? entryPointCompleted = null)
{
if (assembly.EntryPoint is null)
{
return null;
}

return async (args, ct) =>
{
var invoker = new EntryPointInvoker(
args,
assembly.EntryPoint,
onConstructing,
onConstructed,
onBuilding,
entryPointCompleted);
return await invoker.InvokeAsync(ct).ConfigureAwait(false);
};
}

private sealed class EntryPointInvoker : IObserver<DiagnosticListener>
{
private static readonly AsyncLocal<EntryPointInvoker> s_currentListener = new();
private readonly string[] _args;
private readonly MethodInfo _entryPoint;
private readonly TaskCompletionSource<DistributedApplication> _appTcs = new();
private readonly ApplicationBuilderDiagnosticListener _applicationBuilderListener;
private readonly Action<DistributedApplicationOptions, HostApplicationBuilderSettings>? _onConstructing;
private readonly Action<DistributedApplicationBuilder>? _onConstructed;
private readonly Action<DistributedApplicationBuilder>? _onBuilding;
private readonly Action<Exception?>? _entryPointCompleted;

public EntryPointInvoker(
string[] args,
MethodInfo entryPoint,
Action<DistributedApplicationOptions, HostApplicationBuilderSettings>? onConstructing,
Action<DistributedApplicationBuilder>? onConstructed,
Action<DistributedApplicationBuilder>? onBuilding,
Action<Exception?>? entryPointCompleted)
{
_args = args;
_entryPoint = entryPoint;
_onConstructing = onConstructing;
_onConstructed = onConstructed;
_onBuilding = onBuilding;
_entryPointCompleted = entryPointCompleted;
_applicationBuilderListener = new(this);
}

public async Task<DistributedApplication> InvokeAsync(CancellationToken cancellationToken)
{
using var subscription = DiagnosticListener.AllListeners.Subscribe(this);

// Kick off the entry point on a new thread so we don't block the current one
// in case we need to timeout the execution
var thread = new Thread(() =>
{
Exception? exception = null;
try
{
// Set the async local to the instance of the HostingListener so we can filter events that
// aren't scoped to this execution of the entry point.
s_currentListener.Value = this;
var parameters = _entryPoint.GetParameters();
object? result;
if (parameters.Length == 0)
{
result = _entryPoint.Invoke(null, []);
}
else
{
result = _entryPoint.Invoke(null, [_args]);
}
// Try to set an exception if the entry point returns gracefully, this will force
// build to throw
_appTcs.TrySetException(new InvalidOperationException($"The entry point exited without building a {nameof(DistributedApplication)}."));
}
catch (TargetInvocationException tie) when (tie.InnerException?.GetType().Name == "HostAbortedException")
{
// The host was stopped by our own logic
}
catch (TargetInvocationException tie)
{
exception = tie.InnerException ?? tie;
// Another exception happened, propagate that to the caller
_appTcs.TrySetException(exception);
}
catch (Exception ex)
{
exception = ex;
// Another exception happened, propagate that to the caller
_appTcs.TrySetException(exception);
}
finally
{
// Signal that the entry point is completed
_entryPointCompleted?.Invoke(exception);
}
})
{
// Make sure this doesn't hang the process
IsBackground = true,
Name = $"{_entryPoint.DeclaringType?.Assembly.GetName().Name ?? "Unknown"}.EntryPoint"
};

// Start the thread
thread.Start();

return await _appTcs.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
}

public void OnCompleted()
{
}

public void OnError(Exception error)
{

}

public void OnNext(DiagnosticListener value)
{
if (s_currentListener.Value != this)
{
// Ignore events that aren't for this listener
return;
}

if (value.Name == "Aspire.Hosting")
{
_applicationBuilderListener.Subscribe(value);
}
}

private sealed class ApplicationBuilderDiagnosticListener(EntryPointInvoker owner) : IObserver<KeyValuePair<string, object?>>
{
private IDisposable? _disposable;

public void Subscribe(DiagnosticListener listener)
{
_disposable = listener.Subscribe(this);
}

public void OnCompleted()
{
_disposable?.Dispose();
}

public void OnError(Exception error)
{
}

public void OnNext(KeyValuePair<string, object?> value)
{
if (s_currentListener.Value != owner)
{
// Ignore events that aren't for this listener
return;
}

if (value.Key == "DistributedApplicationBuilderConstructing")
{
var args = ((DistributedApplicationOptions Options, HostApplicationBuilderSettings InnerBuilderOptions))value.Value!;
owner._onConstructing?.Invoke(args.Options, args.InnerBuilderOptions);
}

if (value.Key == "DistributedApplicationBuilderConstructed")
{
owner._onConstructed?.Invoke((DistributedApplicationBuilder)value.Value!);
}

if (value.Key == "DistributedApplicationBuilding")
{
owner._onBuilding?.Invoke((DistributedApplicationBuilder)value.Value!);
}

if (value.Key == "DistributedApplicationBuilt")
{
owner._appTcs.TrySetResult((DistributedApplication)value.Value!);
}
}
}
}
}
Loading

0 comments on commit 0391a69

Please sign in to comment.