-
Notifications
You must be signed in to change notification settings - Fork 0
Testing infrastructure #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2b9c313
a961dd0
7c0be86
d4e95f6
5654689
78acfee
77e97c0
3b3a4f7
bf52bec
aada789
907a5c5
44c2d59
71ca1c2
1e44cce
f9e89f4
c595153
64276ef
c21c10d
e85a30e
1a60e2b
0425dc7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| name: CI | ||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| - release-* | ||
| pull_request: | ||
| workflow_dispatch: | ||
| env: | ||
| DOTNET_NOLOGO: true | ||
| defaults: | ||
| run: | ||
| shell: pwsh | ||
| jobs: | ||
| build: | ||
| name: ${{ matrix.name }} | ||
| runs-on: ${{ matrix.os }} | ||
| strategy: | ||
| matrix: | ||
| include: | ||
| - os: windows-latest | ||
| name: Windows | ||
| - os: ubuntu-latest | ||
| name: Linux | ||
| fail-fast: false | ||
| steps: | ||
| - name: Check for secrets | ||
| env: | ||
| SECRETS_AVAILABLE: ${{ secrets.SECRETS_AVAILABLE }} | ||
| run: exit $(If ($env:SECRETS_AVAILABLE -eq 'true') { 0 } Else { 1 }) | ||
| - name: Checkout | ||
| uses: actions/checkout@v6.0.2 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Setup .NET SDK | ||
| uses: actions/setup-dotnet@v5.1.0 | ||
| with: | ||
| global-json-file: global.json | ||
| - name: Build | ||
| run: dotnet build src --configuration Release | ||
| - name: Upload packages | ||
| if: matrix.name == 'Windows' | ||
| uses: actions/upload-artifact@v6.0.0 | ||
| with: | ||
| name: NuGet packages | ||
| path: nugets/ | ||
| retention-days: 7 | ||
| - name: Azure login | ||
| uses: azure/login@v2.3.0 | ||
| with: | ||
| creds: ${{ secrets.AZURE_ACI_CREDENTIALS }} | ||
| - name: Setup Azure Service Bus | ||
| uses: Particular/setup-azureservicebus-action@v2.0.0 | ||
| with: | ||
| connection-string-name: AzureWebJobsServiceBus | ||
| azure-credentials: ${{ secrets.AZURE_ACI_CREDENTIALS }} | ||
| tag: ASBWorkerFunctions | ||
| - name: Setup functions app | ||
| id: setup-functions | ||
| uses: Particular/setup-azurefunctions-action@v1.0.0 | ||
| with: | ||
| azure-credentials: ${{ secrets.AZURE_ACI_CREDENTIALS }} | ||
| tag: NSB.AzureFunctions | ||
| env-vars-to-promote: AzureWebJobsServiceBus | ||
| - name: Deploy IntegrationTest app | ||
| uses: azure/webapps-deploy@v2.2.17 # Do not upgrade to v3, see https://github.com/Azure/webapps-deploy/issues/386 | ||
| with: | ||
| app-name: ${{ steps.setup-functions.outputs.app-name }} | ||
| publish-profile: ${{ steps.setup-functions.outputs.publish-profile }} | ||
| # TODO: Build to a non-tfm-dependent path | ||
| package: src/IntegrationTest/bin/Release/net10.0 | ||
| - name: Run tests | ||
| uses: Particular/run-tests-action@v1.7.0 | ||
| env: | ||
| INTEGRATION_APP_HOSTNAME: ${{ steps.setup-functions.outputs.hostname }} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| [*.cs] | ||
|
|
||
| # ConfigureAwait - not a library | ||
| dotnet_diagnostic.CA2007.severity = none | ||
|
|
||
| # Add a CancellationToken - not a library | ||
| dotnet_diagnostic.PS0018.severity = none |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| namespace NServiceBus.AzureFunctions.AcceptanceTests; | ||
|
|
||
| using NUnit.Framework; | ||
|
|
||
| public class PlaceholderTest | ||
| { | ||
| [Test] | ||
| public void Placeholder() | ||
| { | ||
| Assert.Pass("Just a placeholder for now"); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| namespace IntegrationTest.Contracts; | ||
|
|
||
| public record ExceptionInfo(string FunctionName, string Type, string Message, string StackTrace); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| namespace IntegrationTest.Contracts; | ||
|
|
||
| public record InfoResult(string Version, TimeSpan Uptime); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| </PropertyGroup> | ||
|
|
||
| </Project> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| namespace IntegrationTest.Contracts; | ||
|
|
||
| public record MessageReceived(string MessageType, int Order, string SendingEndpoint, string ReceivingEndpoint); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| namespace IntegrationTest.Contracts; | ||
|
|
||
| public record Payload(MessageReceived[] MessagesReceived); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,7 @@ | ||
| [*.cs] | ||
|
|
||
| # ConfigureAwait - not a library | ||
| dotnet_diagnostic.CA2007.severity = none | ||
|
|
||
| # Add a CancellationToken - not a library | ||
| dotnet_diagnostic.PS0018.severity = none |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| namespace IntegrationTest.Infrastructure; | ||
|
|
||
| using System.Collections.Concurrent; | ||
| using Contracts; | ||
| using Microsoft.Azure.Functions.Worker; | ||
| using Microsoft.Azure.Functions.Worker.Middleware; | ||
|
|
||
| public class ExceptionTrackingMiddleware : IFunctionsWorkerMiddleware | ||
| { | ||
| static ConcurrentQueue<ExceptionInfo> collectedExceptions = []; | ||
|
|
||
| public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next) | ||
| { | ||
| try | ||
| { | ||
| await next(context); | ||
| } | ||
| catch (Exception e) | ||
| { | ||
|
|
||
| var info = new ExceptionInfo(context.FunctionDefinition.Name, e.GetType().FullName ?? "<UnknownType>", e.Message, e.ToString()); | ||
| collectedExceptions.Enqueue(info); | ||
| throw; | ||
| } | ||
| } | ||
|
|
||
| public static ExceptionInfo[] GetErrors() | ||
| { | ||
| while (collectedExceptions.Count > 20) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we only propagate the last twenty?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only so that it doesn't grow out of control and make it difficult to download an over-large JSON blob. Picked a random number. 🤷 |
||
| { | ||
| _ = collectedExceptions.TryDequeue(out _); | ||
| } | ||
|
|
||
| return [.. collectedExceptions]; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| namespace IntegrationTest; | ||
|
|
||
| public static class MessageSessionExtensions | ||
| { | ||
| public static Task StartTestWithMessage<T>(this IMessageSession session, string testCaseName, string destination, T message) | ||
| where T : class | ||
| { | ||
| var opts = new SendOptions(); | ||
| opts.SetDestination(destination); | ||
| opts.SetHeader("TestCaseName", testCaseName); | ||
|
|
||
| return session.Send(message, opts); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| namespace IntegrationTest.Infrastructure; | ||
|
|
||
| using NServiceBus.Pipeline; | ||
|
|
||
| public class IncomingTestBehavior(TestStorage storage) : IBehavior<IInvokeHandlerContext, IInvokeHandlerContext> | ||
| { | ||
| public async Task Invoke(IInvokeHandlerContext context, Func<IInvokeHandlerContext, Task> next) | ||
| { | ||
| var testCaseName = context.MessageHeaders.GetValueOrDefault("TestCaseName") ?? "<unknown-test>"; | ||
| context.Extensions.Set("TestCaseName", testCaseName); | ||
|
|
||
| var orderString = context.MessageHeaders.GetValueOrDefault("TestStorageOrder"); | ||
| var order = int.TryParse(orderString, out var storageOrder) ? storageOrder : 0; | ||
| context.Extensions.Set("TestStorageOrder", order); | ||
|
|
||
| try | ||
| { | ||
| await next(context); | ||
| storage.LogMessage(testCaseName, context.MessageBeingHandled, context); | ||
| } | ||
| catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested) | ||
| { | ||
| } | ||
| catch (Exception e) | ||
| { | ||
| Console.WriteLine(e); | ||
| throw; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public class OutgoingTestBehavior : IBehavior<IOutgoingPhysicalMessageContext, IOutgoingPhysicalMessageContext> | ||
| { | ||
| public Task Invoke(IOutgoingPhysicalMessageContext context, Func<IOutgoingPhysicalMessageContext, Task> next) | ||
| { | ||
| if (context.Extensions.TryGet<string>("TestCaseName", out var testCaseName)) | ||
| { | ||
| context.Headers.Add("TestCaseName", testCaseName); | ||
| } | ||
|
|
||
| if (!context.Extensions.TryGet<int>("TestStorageOrder", out var order)) | ||
| { | ||
| order = 0; | ||
| } | ||
|
|
||
| order++; | ||
|
|
||
| context.Headers.Add("TestStorageOrder", order.ToString()); | ||
|
|
||
| return next(context); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| namespace IntegrationTest.Infrastructure; | ||
|
|
||
| using System.Reflection; | ||
| using Contracts; | ||
| using Microsoft.AspNetCore.Mvc; | ||
| using Microsoft.Azure.Functions.Worker; | ||
| using Microsoft.Azure.Functions.Worker.Http; | ||
|
|
||
| class TestDataFunction(GlobalTestStorage storage) | ||
| { | ||
| static readonly string InformationalVersion; | ||
| static readonly DateTime StartTime; | ||
|
|
||
| static TestDataFunction() | ||
| { | ||
| InformationalVersion = typeof(TestDataFunction).Assembly | ||
| .GetCustomAttributes<AssemblyInformationalVersionAttribute>() | ||
| .First() | ||
| .InformationalVersion; | ||
|
|
||
| StartTime = DateTime.UtcNow; | ||
| } | ||
|
|
||
| [Function(nameof(GetTestData))] | ||
| public Payload GetTestData([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "testing/data/{testName}")] HttpRequestData _, string testName) | ||
| { | ||
| var payload = storage.CreatePayload(testName); | ||
| return payload; | ||
| } | ||
|
|
||
| [Function(nameof(ClearTestData))] | ||
| public IActionResult ClearTestData([HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "testing/data/{testName}")] HttpRequestData _, string testName) | ||
| { | ||
| storage.Clear(testName); | ||
| return new OkResult(); | ||
| } | ||
|
|
||
| [Function(nameof(GetInfo))] | ||
| public InfoResult GetInfo([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "testing")] HttpRequestData _) | ||
| { | ||
| var result = new InfoResult(InformationalVersion, DateTime.UtcNow - StartTime); | ||
| return result; | ||
| } | ||
|
|
||
| [Function(nameof(GetErrors))] | ||
| public ExceptionInfo[] GetErrors([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "testing/errors")] HttpRequestData _) | ||
| => ExceptionTrackingMiddleware.GetErrors(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity, why not https://learn.microsoft.com/en-us/dotnet/api/system.runtime.exceptionservices.exceptiondispatchinfo? Is it because we want to serialize and we do not care about capturing the exception and freeze the stack trace?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only care about seeing the app exceptions in the test output to be able to diagnose issues.