Skip to content

Commit 9f2a0e5

Browse files
standalone test project for entities on DTS
1 parent 65bbff1 commit 9f2a0e5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+3701
-0
lines changed

Microsoft.DurableTask.sln

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ EndProject
4444
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.IntegrationTests", "test\Grpc.IntegrationTests\Grpc.IntegrationTests.csproj", "{7825CFEA-2923-4C44-BA36-8E16259B9777}"
4545
EndProject
4646
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{EFF7632B-821E-4CFC-B4A0-ED4B24296B17}"
47+
ProjectSection(SolutionItems) = preProject
48+
Directory.Packages.props = Directory.Packages.props
49+
EndProjectSection
4750
EndProject
4851
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureFunctionsApp", "samples\AzureFunctionsApp\AzureFunctionsApp.csproj", "{848FC5BD-4A99-4A0D-9099-9597700AA7BC}"
4952
EndProject
@@ -101,6 +104,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InProcessTestHost", "src\In
101104
EndProject
102105
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InProcessTestHost.Tests", "test\InProcessTestHost.Tests\InProcessTestHost.Tests.csproj", "{B894780C-338F-475E-8E84-56AFA8197A06}"
103106
EndProject
107+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DtsPortableSdkEntityTests", "samples\DtsPortableSdkEntityTests\DtsPortableSdkEntityTests.csproj", "{B2BAE32E-C558-BB99-DC82-282613525497}"
108+
EndProject
104109
Global
105110
GlobalSection(SolutionConfigurationPlatforms) = preSolution
106111
Debug|Any CPU = Debug|Any CPU
@@ -271,6 +276,10 @@ Global
271276
{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C}.Debug|Any CPU.Build.0 = Debug|Any CPU
272277
{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C}.Release|Any CPU.ActiveCfg = Release|Any CPU
273278
{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C}.Release|Any CPU.Build.0 = Release|Any CPU
279+
{B2BAE32E-C558-BB99-DC82-282613525497}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
280+
{B2BAE32E-C558-BB99-DC82-282613525497}.Debug|Any CPU.Build.0 = Debug|Any CPU
281+
{B2BAE32E-C558-BB99-DC82-282613525497}.Release|Any CPU.ActiveCfg = Release|Any CPU
282+
{B2BAE32E-C558-BB99-DC82-282613525497}.Release|Any CPU.Build.0 = Release|Any CPU
274283
EndGlobalSection
275284
GlobalSection(SolutionProperties) = preSolution
276285
HideSolutionNode = FALSE
@@ -321,6 +330,7 @@ Global
321330
{B894780C-338F-475E-8E84-56AFA8197A06} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
322331
{6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
323332
{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
333+
{B2BAE32E-C558-BB99-DC82-282613525497} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
324334
EndGlobalSection
325335
GlobalSection(ExtensibilityGlobals) = postSolution
326336
SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="xunit" />
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<ProjectReference Include="..\..\src\Client\AzureManaged\Client.AzureManaged.csproj" />
15+
<ProjectReference Include="..\..\src\Worker\AzureManaged\Worker.AzureManaged.csproj" />
16+
</ItemGroup>
17+
18+
</Project>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
using Azure.Core;
6+
using Azure.Identity;
7+
using DtsPortableSdkEntityTests;
8+
using DurableTask.Core.Entities;
9+
using Microsoft.DurableTask;
10+
using Microsoft.DurableTask.Client;
11+
using Microsoft.DurableTask.Client.AzureManaged;
12+
using Microsoft.DurableTask.Worker;
13+
using Microsoft.DurableTask.Worker.AzureManaged;
14+
15+
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
16+
17+
string connectionString = builder.Configuration["DTS_CONNECTION_STRING"] ??
18+
// By default, use the connection string for the local development emulator
19+
"Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
20+
21+
// Add all the generated orchestrations and activities automatically
22+
builder.Services.AddDurableTaskWorker(builder =>
23+
{
24+
builder.AddTasks(r =>
25+
{
26+
// TODO consider using source generator
27+
28+
// register all orchestrations and activities used in the tests
29+
HashSet<Type> registeredTestTypes = [];
30+
foreach(var test in All.GetAllTests())
31+
{
32+
if (!registeredTestTypes.Contains(test.GetType()))
33+
{
34+
test.Register(r, builder.Services);
35+
registeredTestTypes.Add(test.GetType());
36+
}
37+
}
38+
39+
// register all entities
40+
BatchEntity.Register(r);
41+
Counter.Register(r);
42+
FaultyEntity.Register(r);
43+
Launcher.Register(r);
44+
Relay.Register(r);
45+
SchedulerEntity.Register(r);
46+
SelfSchedulingEntity.Register(r);
47+
StringStore.Register(r);
48+
StringStore2.Register(r);
49+
StringStore3.Register(r);
50+
});
51+
52+
builder.UseDurableTaskScheduler(connectionString);
53+
});
54+
55+
// Register the client, which can be used to start orchestrations
56+
builder.Services.AddDurableTaskClient(builder =>
57+
{
58+
builder.UseDurableTaskScheduler(connectionString);
59+
});
60+
61+
// Configure the HTTP request pipeline
62+
builder.Services.AddControllers().AddJsonOptions(options =>
63+
{
64+
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
65+
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
66+
});
67+
68+
// The actual listen URL can be configured in environment variables named "ASPNETCORE_URLS" or "ASPNETCORE_URLS_HTTPS"
69+
WebApplication app = builder.Build();
70+
app.MapControllers();
71+
app.Run();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"$schema": "https://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"http": {
5+
"commandName": "Project",
6+
"dotnetRunMessages": true,
7+
"launchBrowser": false,
8+
"applicationUrl": "http://localhost:5203",
9+
"environmentVariables": {
10+
"ASPNETCORE_ENVIRONMENT": "Development"
11+
}
12+
},
13+
"https": {
14+
"commandName": "Project",
15+
"dotnetRunMessages": true,
16+
"launchBrowser": false,
17+
"applicationUrl": "https://localhost:7225;http://localhost:5203",
18+
"environmentVariables": {
19+
"ASPNETCORE_ENVIRONMENT": "Development"
20+
}
21+
}
22+
}
23+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"DetailedErrors": true,
3+
"Logging": {
4+
"LogLevel": {
5+
"Default": "Information",
6+
"Microsoft.AspNetCore": "Warning"
7+
}
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
},
8+
"AllowedHosts": "*"
9+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Runtime.Serialization;
8+
using System.Text;
9+
using System.Text.Json;
10+
using System.Text.Json.Serialization;
11+
using System.Threading.Tasks;
12+
using Azure.Core.Serialization;
13+
14+
namespace DtsPortableSdkEntityTests
15+
{
16+
internal static class CustomSerialization
17+
{
18+
public static ProblematicObject CreateUnserializableObject()
19+
{
20+
return new ProblematicObject(serializable: false, deserializable: false);
21+
}
22+
23+
public static ProblematicObject CreateUndeserializableObject()
24+
{
25+
return new ProblematicObject(serializable: true, deserializable: false);
26+
}
27+
28+
/// <summary>
29+
/// An object for which we can inject errors on serialization or deserialization, to test
30+
// how those are handled by the framework.
31+
/// </summary>
32+
public class ProblematicObject
33+
{
34+
public ProblematicObject(bool serializable = true, bool deserializable = true)
35+
{
36+
this.Serializable = serializable;
37+
this.Deserializable = deserializable;
38+
}
39+
40+
public bool Serializable { get; set; }
41+
42+
public bool Deserializable { get; set; }
43+
}
44+
45+
public class ProblematicObjectJsonConverter : JsonConverter<ProblematicObject>
46+
{
47+
public override ProblematicObject Read(
48+
ref Utf8JsonReader reader,
49+
Type typeToConvert,
50+
JsonSerializerOptions options)
51+
{
52+
bool deserializable = reader.GetBoolean();
53+
if (!deserializable)
54+
{
55+
throw new JsonException("problematic object: is not deserializable");
56+
}
57+
return new ProblematicObject(serializable: true, deserializable: true);
58+
}
59+
60+
public override void Write(
61+
Utf8JsonWriter writer,
62+
ProblematicObject value,
63+
JsonSerializerOptions options)
64+
{
65+
if (!value.Serializable)
66+
{
67+
throw new JsonException("problematic object: is not serializable");
68+
}
69+
writer.WriteBooleanValue(value.Deserializable);
70+
}
71+
}
72+
}
73+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
using Microsoft.DurableTask;
10+
11+
namespace DtsPortableSdkEntityTests;
12+
13+
internal abstract class Test
14+
{
15+
public virtual string Name => this.GetType().Name;
16+
17+
public abstract Task RunAsync(TestContext context);
18+
19+
public virtual TimeSpan Timeout => TimeSpan.FromSeconds(30);
20+
21+
public virtual void Register(DurableTaskRegistry registry, IServiceCollection services)
22+
{
23+
}
24+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Linq;
8+
using System.Text;
9+
using System.Threading.Tasks;
10+
using Microsoft.DurableTask.Client;
11+
using Microsoft.DurableTask.Client.Entities;
12+
using Microsoft.DurableTask.Entities;
13+
using Microsoft.Extensions.Logging;
14+
15+
namespace DtsPortableSdkEntityTests;
16+
17+
internal class TestContext
18+
{
19+
public TestContext(DurableTaskClient client, ILogger logger, CancellationToken cancellationToken)
20+
{
21+
this.Client = client;
22+
this.Logger = logger;
23+
this.CancellationToken = cancellationToken;
24+
}
25+
26+
public DurableTaskClient Client { get; }
27+
28+
public ILogger Logger { get; }
29+
30+
public CancellationToken CancellationToken { get; set; }
31+
32+
public bool BackendSupportsImplicitEntityDeletion { get; set; } = true; // false for Azure Storage, true for Netherite, MSSQL, and DTS
33+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Linq;
8+
using System.Text;
9+
using System.Threading.Tasks;
10+
using Microsoft.DurableTask.Client.Entities;
11+
using Microsoft.DurableTask.Entities;
12+
using Microsoft.Extensions.Logging;
13+
14+
namespace DtsPortableSdkEntityTests;
15+
16+
internal static class TestContextExtensions
17+
{
18+
public static async Task<T> WaitForEntityStateAsync<T>(
19+
this TestContext context,
20+
EntityInstanceId entityInstanceId,
21+
TimeSpan? timeout = null,
22+
Func<T, string?>? describeWhatWeAreWaitingFor = null)
23+
{
24+
if (timeout == null)
25+
{
26+
timeout = Debugger.IsAttached ? TimeSpan.FromMinutes(5) : TimeSpan.FromSeconds(30);
27+
}
28+
29+
Stopwatch sw = Stopwatch.StartNew();
30+
31+
EntityMetadata? response;
32+
33+
do
34+
{
35+
response = await context.Client.Entities.GetEntityAsync(entityInstanceId, includeState: true);
36+
37+
if (response != null)
38+
{
39+
if (describeWhatWeAreWaitingFor == null)
40+
{
41+
break;
42+
}
43+
else
44+
{
45+
var waitForResult = describeWhatWeAreWaitingFor(response.State.ReadAs<T>());
46+
47+
if (string.IsNullOrEmpty(waitForResult))
48+
{
49+
break;
50+
}
51+
else
52+
{
53+
context.Logger.LogInformation($"Waiting for {entityInstanceId} : {waitForResult}");
54+
}
55+
}
56+
}
57+
else
58+
{
59+
context.Logger.LogInformation($"Waiting for {entityInstanceId} to have state.");
60+
}
61+
62+
await Task.Delay(TimeSpan.FromMilliseconds(100));
63+
}
64+
while (sw.Elapsed < timeout);
65+
66+
if (response != null)
67+
{
68+
string serializedState = response.State.Value;
69+
context.Logger.LogInformation($"Found state: {serializedState}");
70+
return response.State.ReadAs<T>();
71+
}
72+
else
73+
{
74+
throw new TimeoutException($"Durable entity '{entityInstanceId}' still doesn't have any state!");
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)