Skip to content

Commit

Permalink
new test suite for isolated entities.
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastianburckhardt committed Oct 5, 2023
1 parent 991c8f0 commit 9f4cb5b
Show file tree
Hide file tree
Showing 37 changed files with 2,826 additions and 0 deletions.
7 changes: 7 additions & 0 deletions WebJobs.Extensions.DurableTask.sln
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PerfTests", "PerfTests", "{
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DFPerfScenariosV4", "test\DFPerfScenarios\DFPerfScenariosV4.csproj", "{FC8AD123-F949-4D21-B817-E5A4BBF7F69B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IsolatedEntities", "test\IsolatedEntities\IsolatedEntities.csproj", "{8CBB856D-2D77-4052-9E50-2F635DE5C88F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -182,6 +184,10 @@ Global
{FC8AD123-F949-4D21-B817-E5A4BBF7F69B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FC8AD123-F949-4D21-B817-E5A4BBF7F69B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC8AD123-F949-4D21-B817-E5A4BBF7F69B}.Release|Any CPU.Build.0 = Release|Any CPU
{8CBB856D-2D77-4052-9E50-2F635DE5C88F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8CBB856D-2D77-4052-9E50-2F635DE5C88F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8CBB856D-2D77-4052-9E50-2F635DE5C88F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8CBB856D-2D77-4052-9E50-2F635DE5C88F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -215,6 +221,7 @@ Global
{65F904AA-0F6F-48CB-BE19-593B7D68152A} = {7387E723-E153-4B7A-B105-8C67BFBD48CF}
{7387E723-E153-4B7A-B105-8C67BFBD48CF} = {78BCF152-C22C-408F-9FB1-0F8C99B154B5}
{FC8AD123-F949-4D21-B817-E5A4BBF7F69B} = {7387E723-E153-4B7A-B105-8C67BFBD48CF}
{8CBB856D-2D77-4052-9E50-2F635DE5C88F} = {78BCF152-C22C-408F-9FB1-0F8C99B154B5}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5E9AC327-DE18-41A5-A55D-E44CB4281943}
Expand Down
79 changes: 79 additions & 0 deletions test/IsolatedEntities/Common/HttpTriggers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Runtime.Serialization;
using System.Text;
using System.Text.RegularExpressions;
using Azure.Core;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Extensions.DurableTask;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Entities;
using Microsoft.Extensions.Logging;

namespace IsolatedEntities;

/// <summary>
/// Provides an http trigger to run functional tests for entities.
/// </summary>
public static class HttpTriggers
{
[Function(nameof(RunAllTests))]
public static async Task<HttpResponseData> RunAllTests(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "tests/")] HttpRequestData request,
[DurableClient] DurableTaskClient client,
FunctionContext executionContext)
{
var context = new TestContext(client, executionContext);
string result = await TestRunner.RunAsync(context, filter: null);
HttpResponseData response = request.CreateResponse(System.Net.HttpStatusCode.OK);
response.WriteString(result);
return response;
}

[Function(nameof(RunFilteredTests))]
public static async Task<HttpResponseData> RunFilteredTests(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "tests/{filter}")] HttpRequestData request,
[DurableClient] DurableTaskClient client,
FunctionContext executionContext,
string filter)
{
var context = new TestContext(client, executionContext);
string result = await TestRunner.RunAsync(context, filter);
HttpResponseData response = request.CreateResponse(System.Net.HttpStatusCode.OK);
response.WriteString(result);
return response;
}

[Function(nameof(ListAllTests))]
public static async Task<HttpResponseData> ListAllTests(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "tests/")] HttpRequestData request,
[DurableClient] DurableTaskClient client,
FunctionContext executionContext)
{
var context = new TestContext(client, executionContext);
string result = await TestRunner.RunAsync(context, filter: null, listOnly: true);
HttpResponseData response = request.CreateResponse(System.Net.HttpStatusCode.OK);
response.WriteString(result);
return response;
}

[Function(nameof(ListFilteredTests))]
public static async Task<HttpResponseData> ListFilteredTests(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "tests/{filter}")] HttpRequestData request,
[DurableClient] DurableTaskClient client,
FunctionContext executionContext,
string filter)
{
var context = new TestContext(client, executionContext);
string result = await TestRunner.RunAsync(context, filter, listOnly: true);
HttpResponseData response = request.CreateResponse(System.Net.HttpStatusCode.OK);
response.WriteString(result);
return response;
}
}



69 changes: 69 additions & 0 deletions test/IsolatedEntities/Common/ProblematicObject.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.Runtime.Serialization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Azure.Core.Serialization;

namespace IsolatedEntities
{
internal static class CustomSerialization
{
public static ProblematicObject CreateUnserializableObject()
{
return new ProblematicObject(serializable: false, deserializable: false);
}

public static ProblematicObject CreateUndeserializableObject()
{
return new ProblematicObject(serializable: true, deserializable: false);
}

public class ProblematicObject
{
public ProblematicObject(bool serializable = true, bool deserializable = true)
{
this.Serializable = serializable;
this.Deserializable = deserializable;
}

public bool Serializable { get; set; }

public bool Deserializable { get; set; }
}

public class ProblematicObjectJsonConverter : JsonConverter<ProblematicObject>
{
public override ProblematicObject Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
bool deserializable = reader.GetBoolean();
if (!deserializable)
{
throw new JsonException("problematic object: is not deserializable");
}
return new ProblematicObject(serializable: true, deserializable: true);
}

public override void Write(
Utf8JsonWriter writer,
ProblematicObject value,
JsonSerializerOptions options)
{
if (!value.Serializable)
{
throw new JsonException("problematic object: is not serializable");
}
writer.WriteBooleanValue(value.Deserializable);
}
}
}
}
19 changes: 19 additions & 0 deletions test/IsolatedEntities/Common/Test.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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.Text;
using System.Threading.Tasks;

namespace IsolatedEntities;

internal abstract class Test
{
public virtual string Name => this.GetType().Name;

public abstract Task RunAsync(TestContext context);

public virtual TimeSpan Timeout => TimeSpan.FromSeconds(30);
}
34 changes: 34 additions & 0 deletions test/IsolatedEntities/Common/TestContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 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.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Client.Entities;
using Microsoft.DurableTask.Entities;
using Microsoft.Extensions.Logging;

namespace IsolatedEntities;

internal class TestContext
{
public TestContext(DurableTaskClient client, FunctionContext executionContext)
{
this.ExecutionContext = executionContext;
this.Client = client;
this.Logger = executionContext.GetLogger(nameof(IsolatedEntities));
}

public FunctionContext ExecutionContext { get; }

public DurableTaskClient Client { get; }

public ILogger Logger { get; }

public CancellationToken CancellationToken { get; set; }
}
77 changes: 77 additions & 0 deletions test/IsolatedEntities/Common/TestContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.DurableTask.Client.Entities;
using Microsoft.DurableTask.Entities;
using Microsoft.Extensions.Logging;

namespace IsolatedEntities;

internal static class TestContextExtensions
{
public static async Task<T> WaitForEntityStateAsync<T>(
this TestContext context,
EntityInstanceId entityInstanceId,
TimeSpan? timeout = null,
Func<T, string?>? describeWhatWeAreWaitingFor = null)
{
if (timeout == null)
{
timeout = Debugger.IsAttached ? TimeSpan.FromMinutes(5) : TimeSpan.FromSeconds(30);
}

Stopwatch sw = Stopwatch.StartNew();

EntityMetadata? response;

do
{
response = await context.Client.Entities.GetEntityAsync(entityInstanceId, includeState: true);

if (response != null)
{
if (describeWhatWeAreWaitingFor == null)
{
break;
}
else
{
var waitForResult = describeWhatWeAreWaitingFor(response.State.ReadAs<T>());

if (string.IsNullOrEmpty(waitForResult))
{
break;
}
else
{
context.Logger.LogInformation($"Waiting for {entityInstanceId} : {waitForResult}");
}
}
}
else
{
context.Logger.LogInformation($"Waiting for {entityInstanceId} to have state.");
}

await Task.Delay(TimeSpan.FromMilliseconds(100));
}
while (sw.Elapsed < timeout);

if (response != null)
{
string serializedState = response.State.Value;
context.Logger.LogInformation($"Found state: {serializedState}");
return response.State.ReadAs<T>();
}
else
{
throw new TimeoutException($"Durable entity '{entityInstanceId}' still doesn't have any state!");
}
}
}
57 changes: 57 additions & 0 deletions test/IsolatedEntities/Common/TestRunner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

namespace IsolatedEntities;

internal static class TestRunner
{
public static async Task<string> RunAsync(TestContext context, string? filter = null, bool listOnly = false)
{
var sb = new StringBuilder();

foreach (var test in All.GetAllTests())
{
if (filter == null || test.Name.ToLowerInvariant().Equals(filter.ToLowerInvariant()))
{
if (listOnly)
{
sb.AppendLine(test.Name);
}
else
{
context.Logger.LogWarning("------------ starting {testName}", test.Name);

// if debugging, time out after 60m
// otherwise, time out either when the http request times out or when the individual test time limit is exceeded
using CancellationTokenSource cancellationTokenSource
= Debugger.IsAttached ? new() : CancellationTokenSource.CreateLinkedTokenSource(context.ExecutionContext.CancellationToken);
cancellationTokenSource.CancelAfter(Debugger.IsAttached ? TimeSpan.FromMinutes(60) : test.Timeout);
context.CancellationToken = cancellationTokenSource.Token;

try
{
await test.RunAsync(context);
sb.AppendLine($"PASSED {test.Name}");
}
catch (Exception ex)
{
context.Logger.LogError(ex, "test {testName} failed", test.Name);
sb.AppendLine($"FAILED {test.Name} {ex.ToString()}");
break;
}
}
}
}

return sb.ToString();
}
}
Loading

0 comments on commit 9f4cb5b

Please sign in to comment.