Skip to content

Adopt Aspire workflows and parallelize integration tests#2205

Open
ejsmith wants to merge 27 commits intomainfrom
parallel-integration-test-slices
Open

Adopt Aspire workflows and parallelize integration tests#2205
ejsmith wants to merge 27 commits intomainfrom
parallel-integration-test-slices

Conversation

@ejsmith
Copy link
Copy Markdown
Member

@ejsmith ejsmith commented Apr 17, 2026

Summary

Adopt Aspire-first local and CI workflows while enabling bounded parallel execution for backend integration tests.

What Changed

  • share Aspire-backed integration test infrastructure across the test process instead of restarting it per class
  • allocate one AppScope slice per integration test fixture using test, test-1, and so on
  • reset Elasticsearch, queues, storage, and cache within each fixture scope rather than behind a global lock
  • make TestServer readiness tracking safe for multiple concurrent hosts without retaining disposed servers
  • harden baseline and fixture-file tests for parallel execution and line-ending differences
  • update the AppHost, devcontainer, tasks, and docs to use aspire run as the default local startup path
  • tighten Docker build and publish behavior, including keeping the all-in-one exceptionless image tag-only
  • simplify dev environment start/stop retention from hours to days and run scheduled stop checks at 5am America/Chicago
  • trim and correct README, AGENTS, and repo skill guidance so they match the current Angular/Svelte split and Aspire startup flow

Verification

  • dotnet test --project "tests\Exceptionless.Tests\Exceptionless.Tests.csproj"
  • dotnet test --project ".\tests\Exceptionless.Tests\Exceptionless.Tests.csproj" --configuration Release -- --filter-class Exceptionless.Tests.Controllers.OpenApiControllerTests
  • dotnet test --project ".\tests\Exceptionless.Tests\Exceptionless.Tests.csproj" --no-restore --no-build --configuration Release -- --filter-class Exceptionless.Tests.Controllers.AuthControllerTests
  • verified aspire run from the repo root starts the app

Notes

  • the first integration test fixture keeps the plain test scope so single-test runs are easy to inspect
  • the legacy Angular app in src/Exceptionless.Web/ClientApp.angular still powers the main site while the Svelte 5 app in src/Exceptionless.Web/ClientApp remains under development

Copilot AI review requested due to automatic review settings April 17, 2026 22:11
Comment thread tests/Exceptionless.Tests/AppWebHostFactory.cs Fixed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enables bounded class-level parallelism for the backend integration test suite by scoping test data per fixture (AppScope) while sharing the Aspire-managed infrastructure across the overall test process.

Changes:

  • Enable xUnit parallel execution with a fixed MaxParallelThreads value.
  • Introduce per-fixture AppScope slices (test, test-1, …) while sharing a single Aspire-backed Elasticsearch instance.
  • Update test helpers/baselines to behave correctly under parallel execution (server readiness tracking, file sharing, OpenAPI baseline comparison).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/Exceptionless.Tests/Utility/StackData.cs Allow reading stack JSON files without exclusive locks (parallel-safe file access).
tests/Exceptionless.Tests/Properties/PropertyInfo.cs Enable bounded xUnit class-level parallelism.
tests/Exceptionless.Tests/IntegrationTestsBase.cs Remove global reset lock; reset data per fixture scope and clear additional queues.
tests/Exceptionless.Tests/Extensions/TestServerExtensions.cs Make readiness wait tracking per-TestServer instead of global.
tests/Exceptionless.Tests/Controllers/OpenApiControllerTests.cs Compare OpenAPI baseline structurally and attempt to normalize line endings.
tests/Exceptionless.Tests/AppWebHostFactory.cs Share Aspire infrastructure across the run and allocate/reuse AppScope slices per fixture.
AGENTS.md Document new parallel integration test behavior and AppScope slicing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 16 to 55
@@ -26,9 +46,11 @@ public async ValueTask InitializeAsync()
.WithContainerName("Exceptionless-Elasticsearch-Test")
.WithLifetime(ContainerLifetime.Persistent);

_app = builder.Build();
var app = builder.Build();

await _app.StartAsync();
await app.StartAsync();

return app;
}

Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shared Aspire DistributedApplication is started via the static s_sharedApplication but there is no corresponding disposal/shutdown path at the end of the test run. This can leave background resources running longer than necessary and can cause hangs or port contention in some runners/IDE scenarios. Consider adding a single assembly-/collection-level fixture to dispose the shared application once after all tests complete (while still avoiding per-class restarts).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not added a one-time teardown for the shared Aspire application in this PR. The current change keeps the shared app alive for the duration of the test process to avoid per-class restarts, so I’m leaving this thread open rather than claiming it is addressed.

Comment thread tests/Exceptionless.Tests/Controllers/OpenApiControllerTests.cs Outdated
Comment on lines +147 to +149
var oldLoggingLevel = Log.DefaultLogLevel;
Log.DefaultLogLevel = LogLevel.Warning;

Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResetDataAsync temporarily changes Log.DefaultLogLevel, which is a static/global setting. With class-level parallelism enabled, concurrent fixtures can interleave these assignments and restores, leading to the wrong log level being applied in other tests (and potentially affecting log-based assertions). Consider avoiding global log-level mutation here (e.g., adjust only specific categories) or guard the change/restore with a dedicated lock around just the log-level section.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not changed the Log.DefaultLogLevel mutation path yet. This thread is still open intentionally because the global log-level race concern is valid and still needs a separate follow-up decision.

Comment thread tests/Exceptionless.Tests/Extensions/TestServerExtensions.cs Outdated
public AppWebHostFactory()
{
if (!s_pool.TryDequeue(out var instanceId))
instanceId = Interlocked.Increment(ref s_counter);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not take this refactor-only suggestion in this PR. The constructor logic is still the same and the thread is left open intentionally rather than being resolved as addressed.

Comment on lines +72 to +74
catch (HttpRequestException)
{
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not change this retry loop yet. I’m leaving the thread open intentionally rather than marking it addressed, although I agree that attaching the last exception would improve timeout diagnostics.

Comment on lines +75 to +77
catch (TaskCanceledException)
{
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not apply this comment-only suppression change in this PR. This thread is still open intentionally so it is clear the empty-catch suggestion has not been taken yet.

@ejsmith ejsmith requested a review from niemyjski April 18, 2026 22:09
@ejsmith ejsmith changed the title Enable parallel integration test slices Adopt Aspire workflows and parallelize integration tests Apr 18, 2026
@github-actions
Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Complexity Health
Exceptionless.Core 66% 60% 7629
Exceptionless.Insulation 25% 23% 203
Exceptionless.Web 57% 43% 3643
Exceptionless.AppHost 24% 11% 69
Summary 61% (11920 / 19472) 54% (5961 / 11098) 11544

@ejsmith
Copy link
Copy Markdown
Member Author

ejsmith commented Apr 18, 2026

Follow-up on the review feedback:

Addressed in 9c7c6cb2b:

  • tests/Exceptionless.Tests/Controllers/OpenApiControllerTests.cs: normalized both physical line endings and escaped \\r\\n sequences so the OpenAPI baseline comparison is stable across formatting differences.
  • tests/Exceptionless.Tests/Extensions/TestServerExtensions.cs: switched readiness tracking to ConditionalWeakTable<TestServer, object> so disposed servers are not retained.

Verification:

  • dotnet test --project ".\\tests\\Exceptionless.Tests\\Exceptionless.Tests.csproj" --configuration Release -- --filter-class Exceptionless.Tests.Controllers.OpenApiControllerTests
  • dotnet test --project ".\\tests\\Exceptionless.Tests\\Exceptionless.Tests.csproj" --no-restore --no-build --configuration Release -- --filter-class Exceptionless.Tests.Controllers.AuthControllerTests

The matching OpenAPI and TestServer review threads are resolved.

Still not addressed:

  • the shared Aspire application teardown comment in AppWebHostFactory
  • the global Log.DefaultLogLevel mutation comment in IntegrationTestsBase
  • the remaining static-analysis suggestions in AppWebHostFactory around static state allocation and empty catch blocks

Those are still open on purpose rather than being silently ignored.

Comment on lines +29 to +33
.ReplaceLineEndings("\n")
.Replace("\\r\\n", "\\n");
actualJson = actualJson
.ReplaceLineEndings("\n")
.Replace("\\r\\n", "\\n");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this really needed? should we be using Environment.NewLine so it's less magic strings?

{
var startupContext = server.Services.GetService<StartupActionsContext>();
var maxWaitTime = !_alreadyWaited ? TimeSpan.FromSeconds(30) : TimeSpan.FromSeconds(2);
var maxWaitTime = !s_waitedServers.TryGetValue(server, out _) ? TimeSpan.FromSeconds(30) : TimeSpan.FromSeconds(2);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really wish we got rid of the s_

public class AppWebHostFactory : WebApplicationFactory<Startup>, IAsyncLifetime
{
private DistributedApplication? _app;
private static readonly Uri s_elasticsearchUri = new("http://127.0.0.1:9200");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why hard code this, can we also get rid of s_

public class AppWebHostFactory : WebApplicationFactory<Startup>, IAsyncLifetime
{
private DistributedApplication? _app;
private static readonly Uri s_elasticsearchUri = new("http://127.0.0.1:9200");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also shouldn't this come from configuration?

Comment thread Dockerfile
Comment on lines +21 to +26
FROM build AS build-node
RUN apt-get update -yq \
&& apt-get install -yq curl ca-certificates \
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -yq nodejs \
&& rm -rf /var/lib/apt/lists/*
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I never understood why we didn't do this first, so this layer could be cached more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants