Skip to content
Open
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
75 changes: 75 additions & 0 deletions .github/workflows/ci.yml
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 }}
7 changes: 7 additions & 0 deletions src/AcceptanceTests/.editorconfig
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
3 changes: 0 additions & 3 deletions src/AcceptanceTests/Class.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\NServiceBusTests.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

<ItemGroup>
Expand All @@ -16,22 +15,10 @@
<PackageReference Include="NUnit.Analyzers" Version="4.11.2" PrivateAssets="All" />
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" PrivateAssets="All" />
<PackageReference Include="Particular.Approvals" Version="2.0.1" PrivateAssets="All" />
<PackageReference Include="Particular.Packaging" Version="4.5.0" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="NServiceBus.AcceptanceTesting" Version="10.1.0" />
</ItemGroup>

<PropertyGroup>
<PackageId>NServiceBus.AzureFunctions.AcceptanceTests.Sources</PackageId>
<Description>Acceptance tests for NServiceBus Azure Functions functionality</Description>
<IncludeBuildOutput>false</IncludeBuildOutput>
<IncludeSourceFilesInPackage>true</IncludeSourceFilesInPackage>
</PropertyGroup>

<ItemGroup>
<RemoveSourceFileFromPackage Include="AssemblyInfo.cs" />
</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions src/AcceptanceTests/PlaceholderTest.cs
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");
}
}
3 changes: 3 additions & 0 deletions src/IntegrationTest.Contracts/ExceptionInfo.cs
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);
3 changes: 3 additions & 0 deletions src/IntegrationTest.Contracts/InfoResult.cs
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>
3 changes: 3 additions & 0 deletions src/IntegrationTest.Contracts/MessageReceived.cs
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);
3 changes: 3 additions & 0 deletions src/IntegrationTest.Contracts/Payload.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace IntegrationTest.Contracts;

public record Payload(MessageReceived[] MessagesReceived);
3 changes: 3 additions & 0 deletions src/IntegrationTest/.editorconfig
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
7 changes: 3 additions & 4 deletions src/IntegrationTest/HttpSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,16 @@ class HttpSender([FromKeyedServices("SenderEndpoint")] IMessageSession session,
{
[Function("HttpSenderV4")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequestData req,
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData req,
FunctionContext executionContext)
{
_ = executionContext; // For now
logger.LogInformation("C# HTTP trigger function received a request.");

await session.Send("ReceiverEndpoint", new TriggerMessage()).ConfigureAwait(false);
await session.StartTestWithMessage("FirstTest", "ReceiverEndpoint", new TriggerMessage());

var r = req.CreateResponse(HttpStatusCode.OK);
await r.WriteStringAsync($"{nameof(TriggerMessage)} sent.")
.ConfigureAwait(false);
await r.WriteStringAsync($"{nameof(TriggerMessage)} sent.");
return r;
}
}
36 changes: 36 additions & 0 deletions src/IntegrationTest/Infrastructure/ExceptionTrackingMiddleware.cs
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()

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?

Copy link
Member Author

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.

{
while (collectedExceptions.Count > 20)

Choose a reason for hiding this comment

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

Why do we only propagate the last twenty?

Copy link
Member Author

Choose a reason for hiding this comment

The 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];
}
}
14 changes: 14 additions & 0 deletions src/IntegrationTest/Infrastructure/MessageSessionExtensions.cs
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);
}
}
52 changes: 52 additions & 0 deletions src/IntegrationTest/Infrastructure/TestBehaviors.cs
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);
}
}
48 changes: 48 additions & 0 deletions src/IntegrationTest/Infrastructure/TestDataFunction.cs
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();
}
Loading
Loading