Skip to content

Commit

Permalink
Add cancellation e2e tests
Browse files Browse the repository at this point in the history
  • Loading branch information
liliankasem committed Jan 15, 2025
1 parent d242ed6 commit f871d5b
Show file tree
Hide file tree
Showing 13 changed files with 306 additions and 56 deletions.
2 changes: 1 addition & 1 deletion setup-e2e-tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ if ($SkipBuildOnPack -eq $true)
$AdditionalPackArgs += "--no-build"
}

.\tools\devpack.ps1 -E2E -AdditionalPackArgs $AdditionalPackArgs
.\tools\devpack.ps1 -DotnetVersion $DotnetVersion -E2E -AdditionalPackArgs $AdditionalPackArgs

if ($SkipStorageEmulator -And $SkipCosmosDBEmulator)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;

namespace Microsoft.Azure.Functions.Worker.E2EApp
{
public class CancellationHttpFunctions(ILogger<CancellationHttpFunctions> logger)
{
private readonly ILogger<CancellationHttpFunctions> _logger = logger;

[Function(nameof(HttpWithCancellationTokenNotUsed))]
public async Task<IActionResult> HttpWithCancellationTokenNotUsed(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req)
{
_logger.LogInformation("HttpWithCancellationTokenNotUsed processed a request.");

await SimulateWork(CancellationToken.None);

return new OkObjectResult("Processing completed successfully.");
}

[Function(nameof(HttpWithCancellationTokenIgnored))]
public async Task<IActionResult> HttpWithCancellationTokenIgnored(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req,
CancellationToken cancellationToken)
{
_logger.LogInformation("HttpWithCancellationTokenIgnored processed a request.");

await SimulateWork(cancellationToken);

return new OkObjectResult("Processing completed successfully.");
}

[Function(nameof(HttpWithCancellationTokenHandled))]
public async Task<IActionResult> HttpWithCancellationTokenHandled(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
CancellationToken cancellationToken)
{
_logger.LogInformation("HttpWithCancellationTokenHandled processed a request.");

try
{
await SimulateWork(cancellationToken);

return new OkObjectResult("Processing completed successfully.");
}
catch (OperationCanceledException)
{
_logger.LogWarning("Request was cancelled.");

// Take precautions like noting how far along you are with processing the batch
await Task.Delay(1000);

return new ObjectResult(new { statusCode = StatusCodes.Status499ClientClosedRequest, message = "Request was cancelled." });
}
}

private async Task SimulateWork(CancellationToken cancellationToken)
{
_logger.LogInformation("Starting work...");

for (int i = 0; i < 5; i++)
{
// Simulate work
await Task.Delay(1000, cancellationToken);
_logger.LogWarning($"Work iteration {i + 1} completed.");
}

_logger.LogInformation("Work completed.");
}
}
}
32 changes: 32 additions & 0 deletions test/E2ETests/E2EApps/E2EAspNetCoreApp/E2EAspNetCoreApp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Microsoft.Azure.Functions.Worker.E2EAspNetCoreApp</AssemblyName>
<RootNamespace>Microsoft.Azure.Functions.Worker.E2EAspNetCoreApp</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\extensions\Worker.Extensions.Http\src\Worker.Extensions.Http.csproj" />
<ProjectReference Include="..\..\..\..\extensions\Worker.Extensions.Http.AspNetCore\src\Worker.Extensions.Http.AspNetCore.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.0" />
<PackageReference Condition="$(TestBuild) != 'true'" Include="Microsoft.Azure.Functions.Worker" Version="2.0.0" />
</ItemGroup>
</Project>
7 changes: 7 additions & 0 deletions test/E2ETests/E2EApps/E2EAspNetCoreApp/NuGet.Config
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="AzureFunctionsTempStaging" value="https://azfunc.pkgs.visualstudio.com/e6a70c92-4128-439f-8012-382fe78d6396/_packaging/AzureFunctionsTempStaging/nuget/v3/index.json" />
</packageSources>
</configuration>
9 changes: 9 additions & 0 deletions test/E2ETests/E2EApps/E2EAspNetCoreApp/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;

var host = new HostBuilder()
.ConfigureFunctionsWebApplication()
.Build();

host.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"profiles": {
"dni_net8_cancellation": {
"commandName": "Project",
"commandLineArgs": "--port 7097",
"launchBrowser": false
}
}
}
12 changes: 12 additions & 0 deletions test/E2ETests/E2EApps/E2EAspNetCoreApp/host.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
},
"enableLiveMetricsFilters": true
}
}
}
69 changes: 69 additions & 0 deletions test/E2ETests/E2ETests/AspNetCore/CancellationEndToEndTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Functions.Tests;
using Microsoft.Azure.Functions.Tests.E2ETests;
using Xunit;
using Xunit.Abstractions;

#if !NETFRAMEWORK // Exclude class for .NET Framework (netfx) as AspNetCore is not supported
namespace Microsoft.Azure.Functions.Worker.E2ETests.AspNetCore
{
public class CancellationEndToEndTests : IClassFixture<CancellationEndToEndTests.TestFixture>
{
private readonly TestFixture _fixture;

public CancellationEndToEndTests(TestFixture fixture, ITestOutputHelper testOutputHelper)
{
_fixture = fixture;
_fixture.TestLogs.UseTestLogger(testOutputHelper);
}

[Theory]
[InlineData("HttpWithCancellationTokenNotUsed", "Work completed.", "Succeeded")]
[InlineData("HttpWithCancellationTokenIgnored", "TaskCanceledException: A task was canceled", "Failed")]
[InlineData("HttpWithCancellationTokenHandled", "Request was cancelled.", "Succeeded")]
public async Task Functions_WithCancellationToken_BehaveAsExpected(string functionName, string expectedMessage, string invocationResult)
{
using var cts = new CancellationTokenSource();

try
{
var task = HttpHelpers.InvokeHttpTrigger(functionName, cancellationToken: cts.Token);

await Task.Delay(3000);
cts.Cancel();

var response = await task;
}
catch (Exception)
{
IEnumerable<string> logs = null;
await TestUtility.RetryAsync(() =>
{
logs = _fixture.TestLogs.CoreToolsLogs.Where(p => p.Contains($"Executed 'Functions.{functionName}'"));

return Task.FromResult(logs.Count() >= 1);
});

Assert.Contains(_fixture.TestLogs.CoreToolsLogs, log => log.Contains(expectedMessage, StringComparison.OrdinalIgnoreCase));

// TODO: 2/3 of the test invocations will fail until the host with the ForwarderProxy fix is released - uncomment this line when the fix is released
// Assert.Contains(_fixture.TestLogs.CoreToolsLogs, log => log.Contains($"'Functions.{functionName}' ({invocationResult}", StringComparison.OrdinalIgnoreCase));
}
}

public class TestFixture : FunctionAppFixture
{
public TestFixture(IMessageSink messageSink) : base(messageSink, Constants.TestAppNames.E2EAspNetCoreApp)
{
}
}
}
}
#endif
6 changes: 6 additions & 0 deletions test/E2ETests/E2ETests/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,5 +98,11 @@ public static class Tables
public const string TablesConnectionStringSetting = EmulatorConnectionString;
public const string TableName = "TestTable";
}

public static class TestAppNames
{
public const string E2EApp = "E2EApp";
public const string E2EAspNetCoreApp = "E2EAspNetCoreApp";
}
}
}
4 changes: 2 additions & 2 deletions test/E2ETests/E2ETests/Fixtures/FixtureHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ namespace Microsoft.Azure.Functions.Tests.E2ETests
{
public static class FixtureHelpers
{
public static Process GetFuncHostProcess(bool enableAuth = false)
public static Process GetFuncHostProcess(bool enableAuth = false, string testAppName = null)
{
var funcProcess = new Process();
var rootDir = Path.GetFullPath(@"../../../../../..");
var e2eAppBinPath = Path.Combine(rootDir, @"test/E2ETests/E2EApps/E2EApp/bin");
var e2eAppBinPath = Path.Combine(rootDir, "test", "E2ETests", "E2EApps", testAppName, "bin");
string e2eHostJson = Directory.GetFiles(e2eAppBinPath, "host.json", SearchOption.AllDirectories).FirstOrDefault();

if (e2eHostJson == null)
Expand Down
48 changes: 32 additions & 16 deletions test/E2ETests/E2ETests/Fixtures/FunctionAppFixture.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
Expand All @@ -19,18 +20,23 @@ public class FunctionAppFixture : IAsyncLifetime
private readonly ILogger _logger;
private bool _disposed;
private Process _funcProcess;

private JobObjectRegistry _jobObjectRegistry;
private string _testApp = Constants.TestAppNames.E2EApp;

public FunctionAppFixture(IMessageSink messageSink)
{
// initialize logging
// initialize logging
ILoggerFactory loggerFactory = new LoggerFactory();
TestLogs = new TestLoggerProvider(messageSink);
loggerFactory.AddProvider(TestLogs);
_logger = loggerFactory.CreateLogger<FunctionAppFixture>();
}

internal FunctionAppFixture(IMessageSink messageSink, string testApp) : this(messageSink)
{
_testApp = testApp;
}

public async Task InitializeAsync()
{
// start host via CLI if testing locally
Expand All @@ -42,7 +48,7 @@ public async Task InitializeAsync()

// start functions process
_logger.LogInformation($"Starting functions host for {Constants.FunctionAppCollectionName}...");
_funcProcess = FixtureHelpers.GetFuncHostProcess();
_funcProcess = FixtureHelpers.GetFuncHostProcess(testAppName: _testApp);
string workingDir = _funcProcess.StartInfo.WorkingDirectory;
_logger.LogInformation($" Working dir: '${workingDir}' Exists: '{Directory.Exists(workingDir)}'");
string fileName = _funcProcess.StartInfo.FileName;
Expand All @@ -51,18 +57,29 @@ public async Task InitializeAsync()
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
// Currently only HTTP is supported in Linux CI.
_funcProcess.StartInfo.ArgumentList.Add("--functions");
_funcProcess.StartInfo.ArgumentList.Add("HelloFromQuery");
_funcProcess.StartInfo.ArgumentList.Add("HelloFromJsonBody");
_funcProcess.StartInfo.ArgumentList.Add("HelloUsingPoco");
_funcProcess.StartInfo.ArgumentList.Add("HelloWithNoResponse");
_funcProcess.StartInfo.ArgumentList.Add("PocoFromBody");
_funcProcess.StartInfo.ArgumentList.Add("PocoBeforeRouteParameters");
_funcProcess.StartInfo.ArgumentList.Add("PocoAfterRouteParameters");
_funcProcess.StartInfo.ArgumentList.Add("ExceptionFunction");
_funcProcess.StartInfo.ArgumentList.Add("PocoWithoutBindingSource");
_funcProcess.StartInfo.ArgumentList.Add("HelloPascal");
_funcProcess.StartInfo.ArgumentList.Add("HelloAllCaps");
switch (_testApp)
{
case Constants.TestAppNames.E2EApp:
_funcProcess.StartInfo.ArgumentList.Add("--functions");
_funcProcess.StartInfo.ArgumentList.Add("HelloFromQuery");
_funcProcess.StartInfo.ArgumentList.Add("HelloFromJsonBody");
_funcProcess.StartInfo.ArgumentList.Add("HelloUsingPoco");
_funcProcess.StartInfo.ArgumentList.Add("HelloWithNoResponse");
_funcProcess.StartInfo.ArgumentList.Add("PocoFromBody");
_funcProcess.StartInfo.ArgumentList.Add("PocoBeforeRouteParameters");
_funcProcess.StartInfo.ArgumentList.Add("PocoAfterRouteParameters");
_funcProcess.StartInfo.ArgumentList.Add("ExceptionFunction");
_funcProcess.StartInfo.ArgumentList.Add("PocoWithoutBindingSource");
_funcProcess.StartInfo.ArgumentList.Add("HelloPascal");
_funcProcess.StartInfo.ArgumentList.Add("HelloAllCaps");
break;
case Constants.TestAppNames.E2EAspNetCoreApp:
_funcProcess.StartInfo.ArgumentList.Add("--functions");
_funcProcess.StartInfo.ArgumentList.Add("HttpWithCancellationTokenNotUsed");
_funcProcess.StartInfo.ArgumentList.Add("HttpWithCancellationTokenIgnored");
_funcProcess.StartInfo.ArgumentList.Add("HttpWithCancellationTokenHandled");
break;
}
}

await CosmosDBHelpers.TryCreateDocumentCollectionsAsync(_logger);
Expand Down Expand Up @@ -114,7 +131,6 @@ await TestUtility.RetryAsync(async () =>

internal TestLoggerProvider TestLogs { get; private set; }


public Task DisposeAsync()
{
if (!_disposed)
Expand Down
9 changes: 5 additions & 4 deletions test/E2ETests/E2ETests/Helpers/HttpHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Azure.Functions.Tests.E2ETests
{
class HttpHelpers
{
public static async Task<HttpResponseMessage> InvokeHttpTrigger(string functionName, string queryString = "")
public static async Task<HttpResponseMessage> InvokeHttpTrigger(string functionName, string queryString = "", CancellationToken cancellationToken = default)
{
// Basic http request
HttpRequestMessage request = GetTestRequest(functionName, queryString);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain"));
return await GetResponseMessage(request);
return await GetResponseMessage(request, cancellationToken);
}

public static async Task<HttpResponseMessage> InvokeHttpTriggerWithBody(string functionName, string body, string mediaType)
Expand Down Expand Up @@ -64,12 +65,12 @@ private static HttpRequestMessage GetTestRequest(string functionName, string que
};
}

private static async Task<HttpResponseMessage> GetResponseMessage(HttpRequestMessage request)
private static async Task<HttpResponseMessage> GetResponseMessage(HttpRequestMessage request, CancellationToken cancellationToken = default)
{
HttpResponseMessage response = null;
using (var httpClient = new HttpClient())
{
response = await httpClient.SendAsync(request);
response = await httpClient.SendAsync(request, cancellationToken);
}

return response;
Expand Down
Loading

0 comments on commit f871d5b

Please sign in to comment.