-
Notifications
You must be signed in to change notification settings - Fork 273
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
new test suite for isolated entities.
- Loading branch information
1 parent
991c8f0
commit 9f4cb5b
Showing
37 changed files
with
2,826 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!"); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
Oops, something went wrong.