Skip to content

Commit

Permalink
.Net: Shared (Cross-Runtime) integration tests for Processes (#9550)
Browse files Browse the repository at this point in the history
### Motivation and Context

This PR introduces integration tests for Processes that are shared
across all runtimes. This will allow us to run the exact same tests
using all supported runtimes.

### Description

This PR creates a few new projects that are needed to share the
integration tests across runtimes.

- `Process.IntegrationTests.Resources` contains all the definitions or
steps, state, processes, etc. that are used in the shared tests.
- `Process.IntegrationTests.Shared` contains all of the shared tests.
- `Process.IntegrationTestRunner.Local` is the test project that
executes the shared tests against the Local runtime.
- `Process.IntegrationTestRunner.Dapr` is the test project that executes
the shared tests against the Dapr runtime.
- `Process.IntegrationTestHost.Dapr` is the project that hosts Dapr
tests in an ASP.NET Core Web API.

The PR enables all of these tests to run locally, a follow up PR will
configure these tests to run in the GitHub pipeline.

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄

---------

Co-authored-by: alliscode <[email protected]>
Co-authored-by: Chris <[email protected]>
  • Loading branch information
3 people authored Nov 6, 2024
1 parent 7222246 commit c613ae4
Show file tree
Hide file tree
Showing 34 changed files with 1,382 additions and 325 deletions.
2 changes: 2 additions & 0 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<PackageVersion Include="Dapr.Actors" Version="1.14.0" />
<PackageVersion Include="Dapr.Actors.AspNetCore" Version="1.14.0" />
<PackageVersion Include="Dapr.AspNetCore" Version="1.14.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.VisualStudio.Threading" Version="17.11.20" />
<PackageVersion Include="MSTest.TestFramework" Version="3.6.1" />
<PackageVersion Include="OpenAI" Version="[2.1.0-beta.1]" />
Expand Down
45 changes: 45 additions & 0 deletions dotnet/SK-dotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStartedWithVectorSto
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Process.Runtime.Dapr.UnitTests", "src\Experimental\Process.Runtime.Dapr.UnitTests\Process.Runtime.Dapr.UnitTests.csproj", "{DB58FDD0-308E-472F-BFF5-508BC64C727E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Process.IntegrationTestRunner.Dapr", "src\Experimental\Process.IntegrationTestRunner.Dapr\Process.IntegrationTestRunner.Dapr.csproj", "{F9C9CBD7-0DBF-47BC-B22E-2FB6D08FF858}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Process.IntegrationTests.Shared", "src\Experimental\Process.IntegrationTests.Shared\Process.IntegrationTests.Shared.csproj", "{24CFE182-3342-4EB6-B0A7-91B211454BE9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Process.IntegrationTestRunner.Local", "src\Experimental\Process.IntegrationTestRunner.Local\Process.IntegrationTestRunner.Local.csproj", "{BB358B41-6F95-4513-BFA4-6F026005B5AB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Process.IntegrationTestHost.Dapr", "src\Experimental\Process.IntegrationTestHost.Dapr\Process.IntegrationTestHost.Dapr.csproj", "{A2D349C4-EA6E-465C-B86D-00C2942E3135}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Process.IntegrationTests.Resources", "src\Experimental\Process.IntegrationTests.Resources\Process.IntegrationTests.Resources.csproj", "{B35B1DEB-04DF-4141-9163-01031B22C5D1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1085,6 +1095,36 @@ Global
{DB58FDD0-308E-472F-BFF5-508BC64C727E}.Publish|Any CPU.Build.0 = Debug|Any CPU
{DB58FDD0-308E-472F-BFF5-508BC64C727E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DB58FDD0-308E-472F-BFF5-508BC64C727E}.Release|Any CPU.Build.0 = Release|Any CPU
{F9C9CBD7-0DBF-47BC-B22E-2FB6D08FF858}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F9C9CBD7-0DBF-47BC-B22E-2FB6D08FF858}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F9C9CBD7-0DBF-47BC-B22E-2FB6D08FF858}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
{F9C9CBD7-0DBF-47BC-B22E-2FB6D08FF858}.Publish|Any CPU.Build.0 = Debug|Any CPU
{F9C9CBD7-0DBF-47BC-B22E-2FB6D08FF858}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F9C9CBD7-0DBF-47BC-B22E-2FB6D08FF858}.Release|Any CPU.Build.0 = Release|Any CPU
{24CFE182-3342-4EB6-B0A7-91B211454BE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{24CFE182-3342-4EB6-B0A7-91B211454BE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{24CFE182-3342-4EB6-B0A7-91B211454BE9}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
{24CFE182-3342-4EB6-B0A7-91B211454BE9}.Publish|Any CPU.Build.0 = Debug|Any CPU
{24CFE182-3342-4EB6-B0A7-91B211454BE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{24CFE182-3342-4EB6-B0A7-91B211454BE9}.Release|Any CPU.Build.0 = Release|Any CPU
{BB358B41-6F95-4513-BFA4-6F026005B5AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BB358B41-6F95-4513-BFA4-6F026005B5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BB358B41-6F95-4513-BFA4-6F026005B5AB}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
{BB358B41-6F95-4513-BFA4-6F026005B5AB}.Publish|Any CPU.Build.0 = Debug|Any CPU
{BB358B41-6F95-4513-BFA4-6F026005B5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BB358B41-6F95-4513-BFA4-6F026005B5AB}.Release|Any CPU.Build.0 = Release|Any CPU
{A2D349C4-EA6E-465C-B86D-00C2942E3135}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A2D349C4-EA6E-465C-B86D-00C2942E3135}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A2D349C4-EA6E-465C-B86D-00C2942E3135}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
{A2D349C4-EA6E-465C-B86D-00C2942E3135}.Publish|Any CPU.Build.0 = Debug|Any CPU
{A2D349C4-EA6E-465C-B86D-00C2942E3135}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A2D349C4-EA6E-465C-B86D-00C2942E3135}.Release|Any CPU.Build.0 = Release|Any CPU
{B35B1DEB-04DF-4141-9163-01031B22C5D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B35B1DEB-04DF-4141-9163-01031B22C5D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B35B1DEB-04DF-4141-9163-01031B22C5D1}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
{B35B1DEB-04DF-4141-9163-01031B22C5D1}.Publish|Any CPU.Build.0 = Debug|Any CPU
{B35B1DEB-04DF-4141-9163-01031B22C5D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B35B1DEB-04DF-4141-9163-01031B22C5D1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1234,6 +1274,11 @@ Global
{DAC54048-A39A-4739-8307-EA5A291F2EA0} = {0D8C6358-5DAA-4EA6-A924-C268A9A21BC9}
{8C3DE41C-E2C8-42B9-8638-574F8946EB0E} = {FA3720F1-C99A-49B2-9577-A940257098BF}
{DB58FDD0-308E-472F-BFF5-508BC64C727E} = {0D8C6358-5DAA-4EA6-A924-C268A9A21BC9}
{F9C9CBD7-0DBF-47BC-B22E-2FB6D08FF858} = {0D8C6358-5DAA-4EA6-A924-C268A9A21BC9}
{24CFE182-3342-4EB6-B0A7-91B211454BE9} = {0D8C6358-5DAA-4EA6-A924-C268A9A21BC9}
{BB358B41-6F95-4513-BFA4-6F026005B5AB} = {0D8C6358-5DAA-4EA6-A924-C268A9A21BC9}
{A2D349C4-EA6E-465C-B86D-00C2942E3135} = {0D8C6358-5DAA-4EA6-A924-C268A9A21BC9}
{B35B1DEB-04DF-4141-9163-01031B22C5D1} = {0D8C6358-5DAA-4EA6-A924-C268A9A21BC9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Threading.Tasks;

namespace Microsoft.SemanticKernel.Process;

/// <summary>
/// Represents the context of a running process.
/// </summary>
public abstract class KernelProcessContext
{
/// <summary>
/// Sends a message to the process.
/// </summary>
/// <param name="processEvent">The event to sent to the process.</param>
/// <returns>A <see cref="Task"/></returns>
public abstract Task SendEventAsync(KernelProcessEvent processEvent);

/// <summary>
/// Stops the process.
/// </summary>
/// <returns>A <see cref="Task"/></returns>
public abstract Task StopAsync();

/// <summary>
/// Gets a snapshot of the current state of the process.
/// </summary>
/// <returns>A <see cref="Task{T}"/> where T is <see cref="KernelProcess"/></returns>
public abstract Task<KernelProcess> GetStateAsync();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.SemanticKernel;

namespace SemanticKernel.Process.IntegrationTests;

/// <summary>
/// Represents the body of a POST request to start a process in the test host.
/// </summary>
public record ProcessStartRequest
{
/// <summary>
/// The process to start.
/// </summary>
public required DaprProcessInfo Process { get; set; }

/// <summary>
/// The initial event to send to the process.
/// </summary>
public required KernelProcessEvent InitialEvent { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json;
using Dapr.Actors.Client;
using Microsoft.AspNetCore.Mvc;
using Microsoft.SemanticKernel;

namespace SemanticKernel.Process.IntegrationTests.Controllers;

/// <summary>
/// A controller for starting and managing processes.
/// </summary>
[ApiController]
[Route("/")]
[Produces("application/json")]
public class ProcessTestController : Controller
{
private static readonly Dictionary<string, DaprKernelProcessContext> s_processes = new();
private readonly Kernel _kernel;

/// <summary>
/// Initializes a new instance of the <see cref="ProcessTestController"/> class.
/// </summary>
/// <param name="kernel"></param>
public ProcessTestController(Kernel kernel)
{
this._kernel = kernel;
}

/// <summary>
/// Starts a process.
/// </summary>
/// <param name="processId">The Id of the process</param>
/// <param name="request">The request</param>
/// <returns></returns>
[HttpPost("processes/{processId}")]
public async Task<IActionResult> StartProcessAsync(string processId, [FromBody] ProcessStartRequest request)
{
if (s_processes.ContainsKey(processId))
{
return this.BadRequest("Process already started");
}

if (request.InitialEvent?.Data is JsonElement jsonElement)
{
object? data = jsonElement.Deserialize<string>();
request.InitialEvent = request.InitialEvent with { Data = data };
}

var kernelProcess = request.Process.ToKernelProcess();
var context = await kernelProcess.StartAsync(request.InitialEvent!);
s_processes.Add(processId, context);

return this.Ok();
}

/// <summary>
/// Retrieves information about a process.
/// </summary>
/// <param name="processId">The Id of the process.</param>
/// <returns></returns>
[HttpGet("processes/{processId}")]
public async Task<IActionResult> GetProcessAsync(string processId)
{
if (!s_processes.TryGetValue(processId, out DaprKernelProcessContext? context))
{
return this.NotFound();
}

var process = await context.GetStateAsync();
var daprProcess = DaprProcessInfo.FromKernelProcess(process);

var serialized = JsonSerializer.Serialize(daprProcess);

return this.Ok(daprProcess);
}

/// <summary>
/// Checks the health of the Dapr runtime by attempting to send a message to a health actor.
/// </summary>
/// <returns></returns>
[HttpGet("daprHealth")]
public async Task<IActionResult> HealthCheckAsync()
{
var healthActor = ActorProxy.Create<IHealthActor>(new Dapr.Actors.ActorId(Guid.NewGuid().ToString("n")), nameof(HealthActor));
await healthActor.HealthCheckAsync();
return this.Ok();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft. All rights reserved.

using Dapr.Actors.Runtime;

namespace SemanticKernel.Process.IntegrationTests;

/// <summary>
/// An implementation of the health actor that is only used for testing the health of the Dapr runtime.
/// </summary>
public class HealthActor : Actor, IHealthActor
{
/// <summary>
/// Initializes a new instance of the <see cref="HealthActor"/> class.
/// </summary>
/// <param name="host"></param>
public HealthActor(ActorHost host) : base(host)
{
}

/// <inheritdoc />
public Task HealthCheckAsync()
{
return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.

using Dapr.Actors;

namespace SemanticKernel.Process.IntegrationTests;

/// <summary>
/// An interface for a health actor that is only used for testing the health of the Dapr runtime.
/// </summary>
public interface IHealthActor : IActor
{
/// <summary>
/// An empty method used to determine if Dapr runtime is up and reachable.
/// </summary>
/// <returns></returns>
Task HealthCheckAsync();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<RootNamespace>SemanticKernel.Process.IntegrationTests</RootNamespace>
<AssemblyName>SemanticKernel.Process.IntegrationTestHost.Dapr</AssemblyName>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0110</NoWarn>
<UserSecretsId>b7762d10-e29b-4bb1-8b74-b6d69a667dd4</UserSecretsId>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Dapr.Actors" />
<PackageReference Include="Dapr.Actors.AspNetCore" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Process.Abstractions\Process.Abstractions.csproj" />
<ProjectReference Include="..\Process.Core\Process.Core.csproj" />
<ProjectReference Include="..\Process.IntegrationTests.Resources\Process.IntegrationTests.Resources.csproj" />
<ProjectReference Include="..\Process.Runtime.Dapr\Process.Runtime.Dapr.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Microsoft.SemanticKernel;

namespace SemanticKernel.Process.IntegrationTests;

/// <summary>
/// An implementation of <see cref="JsonTypeInfoResolver"/> that resolves the type information for <see cref="KernelProcessStepState{T}"/>.
/// </summary>
public class ProcessStateTypeResolver<T> : DefaultJsonTypeInfoResolver where T : KernelProcessStep
{
private static readonly Type s_genericType = typeof(KernelProcessStep<>);
private readonly Dictionary<string, Type> _types = new() { { "", typeof(KernelProcessState) } };

/// <summary>
/// Initializes a new instance of the <see cref="ProcessStateTypeResolver{T}"/> class.
/// </summary>
public ProcessStateTypeResolver()
{
// Load all types from the resources assembly that derive from KernelProcessStep
var assembly = typeof(T).Assembly;
var stepTypes = assembly.GetTypes().Where(t => t.IsSubclassOf(typeof(KernelProcessStep)));

foreach (var type in stepTypes)
{
if (TryGetSubtypeOfStatefulStep(type, out Type? genericStepType) && genericStepType is not null)
{
var userStateType = genericStepType.GetGenericArguments()[0];
var stateType = typeof(KernelProcessStepState<>).MakeGenericType(userStateType);
this._types.TryAdd(userStateType.Name, stateType);
}
}
}

/// <inheritdoc />
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
{
JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options);

Type baseType = typeof(KernelProcessStepState);
if (jsonTypeInfo.Type == baseType)
{
var jsonDerivedTypes = this._types.Select(t => new JsonDerivedType(t.Value, t.Key)).ToList();

jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
{
TypeDiscriminatorPropertyName = "$state-type",
IgnoreUnrecognizedTypeDiscriminators = true,
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization
};

// Add the known derived types to the collection
var derivedTypesCollection = jsonTypeInfo.PolymorphismOptions.DerivedTypes;
if (derivedTypesCollection is List<JsonDerivedType> list)
{
list.AddRange(jsonDerivedTypes);
}
else
{
foreach (var item in jsonDerivedTypes!)
{
derivedTypesCollection!.Add(item);
}
}
}
else if (jsonTypeInfo.Type == typeof(DaprStepInfo))
{
jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
{
TypeDiscriminatorPropertyName = "$state-type",
IgnoreUnrecognizedTypeDiscriminators = true,
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
DerivedTypes =
{
new JsonDerivedType(typeof(DaprProcessInfo), nameof(DaprProcessInfo))
}
};
}

return jsonTypeInfo;
}

private static bool TryGetSubtypeOfStatefulStep(Type? type, out Type? genericStateType)
{
while (type != null && type != typeof(object))
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == s_genericType)
{
genericStateType = type;
return true;
}

type = type.BaseType;
}

genericStateType = null;
return false;
}
}
Loading

0 comments on commit c613ae4

Please sign in to comment.