diff --git a/test/InProcessTestFramework/HelloWorldOrchestrator.cs b/test/InProcessTestFramework/HelloWorldOrchestrator.cs new file mode 100644 index 000000000..c45224869 --- /dev/null +++ b/test/InProcessTestFramework/HelloWorldOrchestrator.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask; + +namespace Microsoft.DurableTask.InProcessTestFramework; + +/// +/// A simple "Hello World" orchestrator for demonstration and testing purposes. +/// +public class HelloWorldOrchestrator : TaskOrchestrator +{ + /// + public override async Task RunAsync(TaskOrchestrationContext context, string input) + { + // Call an activity to say hello + string greeting = await context.CallActivityAsync("SayHello", input); + + // Call another activity to get the current time + string timeInfo = await context.CallActivityAsync("GetCurrentTime", null); + + // Combine the results + return $"{greeting} at {timeInfo}"; + } +} + +/// +/// A simple activity that says hello to the provided name. +/// +[DurableTask("SayHello")] +public class SayHelloActivity : TaskActivity +{ + /// + public override Task RunAsync(TaskActivityContext context, string? name) + { + return Task.FromResult($"Hello, {name ?? "World"}!"); + } +} + +/// +/// A simple activity that returns the current time. +/// +[DurableTask("GetCurrentTime")] +public class GetCurrentTimeActivity : TaskActivity +{ + /// + public override Task RunAsync(TaskActivityContext context, object? input) + { + return Task.FromResult(DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss UTC")); + } +} diff --git a/test/InProcessTestFramework/InProcessHelloWorldOrchestrationTest.cs b/test/InProcessTestFramework/InProcessHelloWorldOrchestrationTest.cs new file mode 100644 index 000000000..9f363f6b7 --- /dev/null +++ b/test/InProcessTestFramework/InProcessHelloWorldOrchestrationTest.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Client; +using Xunit; + +namespace Microsoft.DurableTask.InProcessTestFramework; + +/// +/// Simple test example showing how to test orchestrations with the in-process framework. +/// +public class SimpleTest +{ + [Fact] + public async Task TestHelloWorldOrchestration_WithRealActivities() + { + // 1. Create the test framework + using var framework = new InProcessTestFramework(); + + // 2. Register your orchestration and activities (your existing code, unchanged) + framework + .RegisterOrchestrator("HelloWorld", new HelloWorldOrchestrator()) + .RegisterActivity("SayHello", new SayHelloActivity()) + .RegisterActivity("GetCurrentTime", new GetCurrentTimeActivity()); + + // 3. Schedule the orchestration using the mock client (just like real client) + string instanceId = await framework.Client.ScheduleNewOrchestrationInstanceAsync( + "HelloWorld", + "Alice"); + + // 4. Wait for it to finish + var result = await framework.Client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true); + + // 5. Check the output is what you want + Assert.Equal(OrchestrationRuntimeStatus.Completed, result.RuntimeStatus); + + // The output should contain "Hello, Alice!" from SayHello activity + Assert.Contains("Hello, Alice!", result.SerializedOutput ?? ""); + + // The output should contain "UTC" from GetCurrentTime activity + Assert.Contains("UTC", result.SerializedOutput ?? ""); + + // The format should be "Hello, Alice! at [timestamp] UTC" + Assert.Matches(@"Hello, Alice! at .+ UTC", result.SerializedOutput ?? ""); + } + + [Fact] + public async Task TestHelloWorldOrchestration_WithDifferentInput() + { + using var framework = new InProcessTestFramework(); + + // Register the same orchestration and activities + framework + .RegisterOrchestrator("HelloWorld", new HelloWorldOrchestrator()) + .RegisterActivity("SayHello", new SayHelloActivity()) + .RegisterActivity("GetCurrentTime", new GetCurrentTimeActivity()); + + // Test with different input + string instanceId = await framework.Client.ScheduleNewOrchestrationInstanceAsync( + "HelloWorld", + "Bob"); + + var result = await framework.Client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true); + + // Verify the orchestration ran successfully + Assert.Equal(OrchestrationRuntimeStatus.Completed, result.RuntimeStatus); + + // Verify the output reflects the different input + Assert.Contains("Hello, Bob!", result.SerializedOutput ?? ""); + Assert.Contains("UTC", result.SerializedOutput ?? ""); + } +} diff --git a/test/InProcessTestFramework/InProcessTaskOrchestrationContext.cs b/test/InProcessTestFramework/InProcessTaskOrchestrationContext.cs new file mode 100644 index 000000000..2afabf000 --- /dev/null +++ b/test/InProcessTestFramework/InProcessTaskOrchestrationContext.cs @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Entities; +using Microsoft.Extensions.Logging; + +namespace Microsoft.DurableTask.InProcessTestFramework; + +/// +/// An in-process implementation of TaskOrchestrationContext that can execute activities directly. +/// This context allows orchestrations to run without requiring the full backend infrastructure. +/// +public sealed class InProcessTaskOrchestrationContext : TaskOrchestrationContext +{ + readonly Dictionary>> activityRegistry; + readonly Dictionary externalEvents = new(); + readonly Dictionary customStatuses = new(); + readonly ILoggerFactory loggerFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The orchestration name. + /// The instance ID. + /// The orchestration input. + /// The current UTC date time. + /// The logger factory. + /// The registry of real activities. + public InProcessTaskOrchestrationContext( + TaskName name, + string instanceId, + object? input, + DateTime? currentUtcDateTime = null, + ILoggerFactory? loggerFactory = null, + Dictionary>>? activityRegistry = null) + { + this.Name = name; + this.InstanceId = instanceId; + this.Input = input; + this.CurrentUtcDateTime = currentUtcDateTime ?? DateTime.UtcNow; + this.loggerFactory = loggerFactory ?? Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory.Instance; + this.IsReplaying = false; + this.activityRegistry = activityRegistry ?? new Dictionary>>(); + } + + /// + public override TaskName Name { get; } + + /// + public override string InstanceId { get; } + + /// + public override ParentOrchestrationInstance? Parent => null; + + /// + public override DateTime CurrentUtcDateTime { get; } + + /// + public override bool IsReplaying { get; } + + /// + protected override ILoggerFactory LoggerFactory => this.loggerFactory; + + /// + /// Gets the orchestration input. + /// + public object? Input { get; } + + /// + /// Sets an external event for the orchestration. + /// + /// The name of the event. + /// The event data. + public void SetExternalEvent(string eventName, object? eventData) + { + this.externalEvents[eventName] = eventData; + } + + /// + public override T? GetInput() where T : default + { + if (this.Input is T typedInput) + { + return typedInput; + } + return default; + } + + /// + public override async Task CallActivityAsync( + TaskName name, + object? input = null, + TaskOptions? options = null) + { + if (this.activityRegistry.TryGetValue(name.Name, out var activityFunc)) + { + object? result = await activityFunc(name.Name, input); + if (result is TResult typedResult) + { + return typedResult; + } + else{ + throw new InvalidOperationException($"The activity '{name.Name}' returned a result of type '{result.GetType()}' but the expected type is '{typeof(TResult)}'."); + }; + } + + throw new InvalidOperationException($"No activity found for '{name.Name}'. Register the activity using RegisterActivity() or setup a mock using MockActivity()."); + } + + /// + public override Task CallSubOrchestratorAsync( + TaskName orchestratorName, + object? input = null, + TaskOptions? options = null) + { + throw new NotSupportedException("Sub-orchestrations are not supported in the in-process context. Use the full framework for complex scenarios."); + } + + /// + public override Task CreateTimer(DateTime fireAt, CancellationToken cancellationToken) + { + // For testing, we complete timers immediately + return Task.CompletedTask; + } + + /// + public override async Task WaitForExternalEvent(string eventName, CancellationToken cancellationToken = default) + { + // Keep checking until the event is available + while (!cancellationToken.IsCancellationRequested) + { + if (this.externalEvents.TryGetValue(eventName, out var eventData)) + { + if (eventData is T typedEvent) + { + // Remove the event from dictionary after consuming it + this.externalEvents.Remove(eventName); + return typedEvent; + } + } + + // Wait a bit before checking again + await Task.Delay(10, cancellationToken); + } + + // If cancelled, throw + cancellationToken.ThrowIfCancellationRequested(); + + // This should never be reached, but needed for compiler + return default(T)!; + } + + /// + public override Guid NewGuid() + { + return Guid.NewGuid(); + } + + /// + public override void SetCustomStatus(object? customStatus) + { + this.customStatuses[this.InstanceId] = customStatus; + } + + /// + public override void SendEvent(string instanceId, string eventName, object payload) + { + throw new NotSupportedException("SendEventis not supported in the in-process context."); + } + + /// + public override void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true) + { + throw new NotSupportedException("ContinueAsNew is not supported in the in-process context."); + } + + /// + /// Gets the custom status that was set for the given instance. + /// + /// The instance ID. + /// The custom status or null if none was set. + public object? GetCustomStatus(string instanceId) + { + return this.customStatuses.TryGetValue(instanceId, out var status) ? status : null; + } + +} diff --git a/test/InProcessTestFramework/InProcessTestFramework.Tests.csproj b/test/InProcessTestFramework/InProcessTestFramework.Tests.csproj new file mode 100644 index 000000000..413b068cf --- /dev/null +++ b/test/InProcessTestFramework/InProcessTestFramework.Tests.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + + + + + + + + + + + + + + diff --git a/test/InProcessTestFramework/InProcessTestFramework.cs b/test/InProcessTestFramework/InProcessTestFramework.cs new file mode 100644 index 000000000..4d4e9d417 --- /dev/null +++ b/test/InProcessTestFramework/InProcessTestFramework.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Client; +using Microsoft.Extensions.Logging; + +namespace Microsoft.DurableTask.InProcessTestFramework; + +/// +/// A test framework for running Durable Task orchestrations in-process without requiring +/// external backend services. This framework allows you to mock activities and test +/// orchestration logic directly. +/// +public sealed class InProcessTestFramework : IDisposable +{ + readonly InProcessTestOrchestrationService orchestrationService; + readonly MockDurableTaskClient client; + + /// + /// Initializes a new instance of the class. + /// + /// The logger factory for debugging and tracing. + public InProcessTestFramework(ILoggerFactory? loggerFactory = null) + { + this.orchestrationService = new InProcessTestOrchestrationService(loggerFactory); + this.client = new MockDurableTaskClient(this.orchestrationService); + } + + /// + /// Gets the mock DurableTaskClient for scheduling orchestrations. + /// + public DurableTaskClient Client => this.client; + + /// + /// Registers an orchestrator function. + /// + /// The orchestrator name. + /// The orchestrator function. + /// This framework instance for method chaining. + public InProcessTestFramework RegisterOrchestrator( + string name, + Func> orchestratorFunc) + { + this.orchestrationService.RegisterOrchestrator(name, orchestratorFunc); + return this; + } + + /// + /// Registers a typed orchestrator. + /// + /// The input type. + /// The output type. + /// The orchestrator name. + /// The orchestrator instance. + /// This framework instance for method chaining. + public InProcessTestFramework RegisterOrchestrator( + string name, + TaskOrchestrator orchestrator) + { + this.orchestrationService.RegisterOrchestrator(name, orchestrator); + return this; + } + + /// + /// Registers an activity function. + /// + /// The activity name. + /// The activity function. + /// This framework instance for method chaining. + public InProcessTestFramework RegisterActivity(string name, Func> activityFunc) + { + this.orchestrationService.RegisterActivity(name, activityFunc); + return this; + } + + /// + /// Registers a typed activity. + /// + /// The input type. + /// The output type. + /// The activity name. + /// The activity instance. + /// This framework instance for method chaining. + public InProcessTestFramework RegisterActivity( + string name, + TaskActivity activity) + { + this.orchestrationService.RegisterActivity(name, activity); + return this; + } + + + + /// + /// Disposes the test framework. + /// + public void Dispose() + { + this.client?.DisposeAsync().AsTask().Wait(); + this.orchestrationService?.Dispose(); + } +} + diff --git a/test/InProcessTestFramework/InProcessTestOrchestrationService.cs b/test/InProcessTestFramework/InProcessTestOrchestrationService.cs new file mode 100644 index 000000000..bac7d43c2 --- /dev/null +++ b/test/InProcessTestFramework/InProcessTestOrchestrationService.cs @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Client; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; + +namespace Microsoft.DurableTask.InProcessTestFramework; + +/// +/// An in-process orchestration service for testing. +/// This service manages orchestration instances without requiring external backend services. +/// +public sealed class InProcessTestOrchestrationService : IDisposable +{ + readonly ConcurrentDictionary instances = new(); + readonly Dictionary>> orchestratorRegistry = new(); + readonly Dictionary>> activityRegistry = new(); + readonly ILoggerFactory loggerFactory; + readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The logger factory. + public InProcessTestOrchestrationService(ILoggerFactory? loggerFactory = null) + { + this.loggerFactory = loggerFactory ?? Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory.Instance; + this.logger = this.loggerFactory.CreateLogger(); + } + + /// + /// Registers an orchestrator function. + /// + /// The orchestrator name. + /// The orchestrator function. + public void RegisterOrchestrator(string name, Func> orchestratorFunc) + { + this.orchestratorRegistry[name] = orchestratorFunc; + this.logger.LogDebug("Registered orchestrator: {OrchestratorName}", name); + } + + /// + /// Registers an orchestrator from a TaskOrchestrator implementation. + /// + /// The input type. + /// The output type. + /// The orchestrator name. + /// The orchestrator instance. + public void RegisterOrchestrator(string name, TaskOrchestrator orchestrator) + { + this.orchestratorRegistry[name] = async (context, input) => + { + TInput? typedInput = input is TInput ? (TInput)input : default; + return await orchestrator.RunAsync(context, typedInput!); + }; + this.logger.LogDebug("Registered typed orchestrator: {OrchestratorName}", name); + } + + /// + /// Registers an activity function. + /// + /// The activity name. + /// The activity function. + public void RegisterActivity(string name, Func> activityFunc) + { + this.activityRegistry[name] = (activityName, input) => activityFunc(input); + this.logger.LogDebug("Registered activity: {ActivityName}", name); + } + + /// + /// Registers an activity from a TaskActivity implementation. + /// + /// The input type. + /// The output type. + /// The activity name. + /// The activity instance. + public void RegisterActivity(string name, TaskActivity activity) + { + this.activityRegistry[name] = async (activityName, input) => + { + TInput? typedInput = input is TInput ? (TInput)input : default; + + // Create activity context with real activity name + var activityContext = new MockActivityContext(activityName); + return await activity.RunAsync(activityContext, typedInput!); + }; + this.logger.LogDebug("Registered typed activity: {ActivityName}", name); + } + + /// + /// Schedules a new orchestration instance. + /// + /// The orchestrator name. + /// The instance ID. + /// The orchestration input. + /// The start orchestration options. + /// The cancellation token. + /// A task that completes when the orchestration is scheduled. + public async Task ScheduleOrchestrationAsync( + TaskName orchestratorName, + string instanceId, + object? input, + StartOrchestrationOptions? options, + CancellationToken cancellation) + { + if (!this.orchestratorRegistry.TryGetValue(orchestratorName.Name, out var orchestratorFunc)) + { + throw new InvalidOperationException($"Orchestrator '{orchestratorName.Name}' is not registered. Use RegisterOrchestrator() to register it."); + } + + var instance = new MockOrchestrationInstance(instanceId, orchestratorName, input); + + this.instances[instanceId] = instance; + this.logger.LogInformation("Scheduled orchestration: {InstanceId} ({OrchestratorName})", instanceId, orchestratorName.Name); + + // Start execution in the background + _ = Task.Run(async () => + { + try + { + await this.ExecuteOrchestrationAsync(instance, orchestratorFunc, cancellation); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Error executing orchestration {InstanceId}", instanceId); + instance.RuntimeStatus = OrchestrationRuntimeStatus.Failed; + instance.FailureDetails = TaskFailureDetails.FromException(ex); + instance.LastUpdatedAt = DateTime.UtcNow; + } + }, cancellation); + + await Task.Delay(10, cancellation); // Small delay to allow status to update + } + + async Task ExecuteOrchestrationAsync( + MockOrchestrationInstance instance, + Func> orchestratorFunc, + CancellationToken cancellation) + { + instance.RuntimeStatus = OrchestrationRuntimeStatus.Running; + instance.LastUpdatedAt = DateTime.UtcNow; + + this.logger.LogInformation("Starting orchestration execution: {InstanceId}", instance.InstanceId); + + var context = new InProcessTaskOrchestrationContext( + instance.Name, + instance.InstanceId, + instance.Input, + DateTime.UtcNow, + this.loggerFactory, + this.activityRegistry); + + // Store the context so external events can be sent to it + instance.Context = context; + + object? result = await orchestratorFunc(context, instance.Input); + + instance.RuntimeStatus = OrchestrationRuntimeStatus.Completed; + instance.Output = result; + instance.LastUpdatedAt = DateTime.UtcNow; + + this.logger.LogInformation("Completed orchestration execution: {InstanceId}", instance.InstanceId); + } + + /// + /// Gets an orchestration instance. + /// + /// The instance ID. + /// Whether to include inputs and outputs. + /// The cancellation token. + /// The orchestration metadata. + public Task GetInstanceAsync( + string instanceId, + bool getInputsAndOutputs, + CancellationToken cancellation) + { + if (this.instances.TryGetValue(instanceId, out var instance)) + { + var metadata = CreateMetadata(instance, getInputsAndOutputs); + return Task.FromResult(metadata); + } + + throw new InvalidOperationException($"Orchestration instance '{instanceId}' not found."); + } + + /// + /// Waits for an orchestration instance to start. + /// + /// The instance ID. + /// Whether to include inputs and outputs. + /// The cancellation token. + /// The orchestration metadata. + public async Task WaitForInstanceStartAsync( + string instanceId, + bool getInputsAndOutputs, + CancellationToken cancellation) + { + while (!cancellation.IsCancellationRequested) + { + if (this.instances.TryGetValue(instanceId, out var instance) && + instance.RuntimeStatus != OrchestrationRuntimeStatus.Pending) + { + return CreateMetadata(instance, getInputsAndOutputs); + } + + await Task.Delay(50, cancellation); + } + + throw new OperationCanceledException(); + } + + /// + /// Waits for an orchestration instance to complete. + /// + /// The instance ID. + /// Whether to include inputs and outputs. + /// The cancellation token. + /// The orchestration metadata. + public async Task WaitForInstanceCompletionAsync( + string instanceId, + bool getInputsAndOutputs, + CancellationToken cancellation) + { + while (!cancellation.IsCancellationRequested) + { + if (this.instances.TryGetValue(instanceId, out var instance) && + (instance.RuntimeStatus == OrchestrationRuntimeStatus.Completed || + instance.RuntimeStatus == OrchestrationRuntimeStatus.Failed || + instance.RuntimeStatus == OrchestrationRuntimeStatus.Terminated)) + { + return CreateMetadata(instance, getInputsAndOutputs); + } + + await Task.Delay(50, cancellation); + } + + throw new OperationCanceledException(); + } + + /// + /// Raises an external event for an orchestration instance. + /// + /// The instance ID. + /// The event name. + /// The event payload. + /// The cancellation token. + /// A task that completes when the event is raised. + public Task RaiseEventAsync(string instanceId, string eventName, object? eventPayload, CancellationToken cancellation) + { + if (!this.instances.TryGetValue(instanceId, out var instance)) + { + throw new InvalidOperationException($"Orchestration instance '{instanceId}' not found"); + } + + if (instance.Context == null) + { + throw new InvalidOperationException($"Orchestration context not available for instance '{instanceId}'"); + } + + instance.Context.SetExternalEvent(eventName, eventPayload); + return Task.CompletedTask; + } + + /// + /// Suspends an orchestration instance. + /// + /// The instance ID. + /// The suspension reason. + /// The cancellation token. + /// A task that completes when the instance is suspended. + public Task SuspendInstanceAsync(string instanceId, string? reason, CancellationToken cancellation) + { + if (this.instances.TryGetValue(instanceId, out var instance)) + { + instance.RuntimeStatus = OrchestrationRuntimeStatus.Suspended; + instance.LastUpdatedAt = DateTime.UtcNow; + } + return Task.CompletedTask; + } + + /// + /// Resumes an orchestration instance. + /// + /// The instance ID. + /// The resume reason. + /// The cancellation token. + /// A task that completes when the instance is resumed. + public Task ResumeInstanceAsync(string instanceId, string? reason, CancellationToken cancellation) + { + if (this.instances.TryGetValue(instanceId, out var instance)) + { + instance.RuntimeStatus = OrchestrationRuntimeStatus.Running; + instance.LastUpdatedAt = DateTime.UtcNow; + } + return Task.CompletedTask; + } + + /// + /// Terminates an orchestration instance. + /// + /// The instance ID. + /// The termination output. + /// The cancellation token. + /// A task that completes when the instance is terminated. + public Task TerminateInstanceAsync(string instanceId, object? output, CancellationToken cancellation) + { + if (this.instances.TryGetValue(instanceId, out var instance)) + { + instance.RuntimeStatus = OrchestrationRuntimeStatus.Terminated; + instance.Output = output; + instance.LastUpdatedAt = DateTime.UtcNow; + } + return Task.CompletedTask; + } + + static OrchestrationMetadata CreateMetadata(MockOrchestrationInstance instance, bool getInputsAndOutputs) + { + return new OrchestrationMetadata(instance.Name.Name, instance.InstanceId) + { + RuntimeStatus = instance.RuntimeStatus, + CreatedAt = instance.CreatedAt, + LastUpdatedAt = instance.LastUpdatedAt, + SerializedInput = getInputsAndOutputs ? instance.Input?.ToString() : null, + SerializedOutput = getInputsAndOutputs ? instance.Output?.ToString() : null, + SerializedCustomStatus = null, + FailureDetails = instance.FailureDetails + }; + } + + /// + /// Disposes the orchestration service. + /// + public void Dispose() + { + this.instances.Clear(); + } +} diff --git a/test/InProcessTestFramework/MockActivityContext.cs b/test/InProcessTestFramework/MockActivityContext.cs new file mode 100644 index 000000000..2f5df5f47 --- /dev/null +++ b/test/InProcessTestFramework/MockActivityContext.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.DurableTask.InProcessTestFramework; + +/// +/// A simple mock implementation of TaskActivityContext for activity execution. +/// Provides the activity name and a generic instance ID for in-process testing. +/// +internal class MockActivityContext : TaskActivityContext +{ + readonly TaskName name; + + public MockActivityContext(string activityName) + { + this.name = new TaskName(activityName); + } + + public override TaskName Name => this.name; + + // Instance id is not used in the in-process test framework, so we use a fixed value. + public override string InstanceId => "in-process-test"; +} diff --git a/test/InProcessTestFramework/MockDurableTaskClient.cs b/test/InProcessTestFramework/MockDurableTaskClient.cs new file mode 100644 index 000000000..bf76c0acc --- /dev/null +++ b/test/InProcessTestFramework/MockDurableTaskClient.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.Entities; + +namespace Microsoft.DurableTask.InProcessTestFramework; + +/// +/// A mock implementation of DurableTaskClient for in-process testing. +/// This client schedules orchestrations to run directly in the same process +/// without requiring a full backend service. +/// +public sealed class MockDurableTaskClient : DurableTaskClient +{ + readonly InProcessTestOrchestrationService orchestrationService; + + /// + /// Initializes a new instance of the class. + /// + /// The in-process orchestration service. + public MockDurableTaskClient(InProcessTestOrchestrationService orchestrationService) + : base("MockDurableTaskClient") + { + this.orchestrationService = orchestrationService ?? throw new ArgumentNullException(nameof(orchestrationService)); + } + + /// + public override async Task ScheduleNewOrchestrationInstanceAsync( + TaskName orchestratorName, + object? input = null, + StartOrchestrationOptions? options = null, + CancellationToken cancellation = default) + { + var instanceId = options?.InstanceId ?? Guid.NewGuid().ToString("N"); + + await this.orchestrationService.ScheduleOrchestrationAsync( + orchestratorName, + instanceId, + input, + options, + cancellation); + + return instanceId; + } + + /// + public override Task GetInstanceAsync( + string instanceId, + bool getInputsAndOutputs = false, + CancellationToken cancellation = default) + { + return this.orchestrationService.GetInstanceAsync(instanceId, getInputsAndOutputs, cancellation); + } + + /// + public override Task WaitForInstanceStartAsync( + string instanceId, + bool getInputsAndOutputs = false, + CancellationToken cancellation = default) + { + return this.orchestrationService.WaitForInstanceStartAsync(instanceId, getInputsAndOutputs, cancellation); + } + + /// + public override Task WaitForInstanceCompletionAsync( + string instanceId, + bool getInputsAndOutputs = false, + CancellationToken cancellation = default) + { + return this.orchestrationService.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs, cancellation); + } + + /// + public override Task RaiseEventAsync( + string instanceId, + string eventName, + object? eventPayload = null, + CancellationToken cancellation = default) + { + return this.orchestrationService.RaiseEventAsync(instanceId, eventName, eventPayload, cancellation); + } + + /// + public override Task SuspendInstanceAsync(string instanceId, string? reason = null, CancellationToken cancellation = default) + { + return this.orchestrationService.SuspendInstanceAsync(instanceId, reason, cancellation); + } + + /// + public override Task ResumeInstanceAsync(string instanceId, string? reason = null, CancellationToken cancellation = default) + { + return this.orchestrationService.ResumeInstanceAsync(instanceId, reason, cancellation); + } + + /// + public override Task TerminateInstanceAsync(string instanceId, object? output = null, CancellationToken cancellation = default) + { + return this.orchestrationService.TerminateInstanceAsync(instanceId, output, cancellation); + } + + /// + public override DurableEntityClient Entities => throw new NotSupportedException("Entity support not implemented in mock client"); + + /// + public override AsyncPageable GetAllInstancesAsync(OrchestrationQuery? filter = null) + => throw new NotSupportedException("GetAllInstancesAsync is not supported in the mock client"); + + /// + public override async Task GetInstancesAsync( + string instanceId, + bool getInputsAndOutputs = false, + CancellationToken cancellation = default) + => throw new NotSupportedException("GetInstancesAsync is not supported in the mock client"); + + /// + public override async ValueTask DisposeAsync() + { + this.orchestrationService?.Dispose(); + await Task.CompletedTask; + } +} diff --git a/test/InProcessTestFramework/MockOrchestrationInstance.cs b/test/InProcessTestFramework/MockOrchestrationInstance.cs new file mode 100644 index 000000000..06bda53f2 --- /dev/null +++ b/test/InProcessTestFramework/MockOrchestrationInstance.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.DurableTask.Client; + +namespace Microsoft.DurableTask.InProcessTestFramework; + +/// +/// Represents an orchestration instance for in-process testing. +/// Tracks the state, metadata, and execution context of a running orchestration. +/// +internal class MockOrchestrationInstance +{ + public MockOrchestrationInstance(string instanceId, TaskName name, object? input) + { + this.InstanceId = instanceId; + this.Name = name; + this.Input = input; + this.CreatedAt = DateTime.UtcNow; + this.LastUpdatedAt = DateTime.UtcNow; + this.RuntimeStatus = OrchestrationRuntimeStatus.Pending; + } + + public string InstanceId { get; } + public TaskName Name { get; } + public object? Input { get; } + public object? Output { get; set; } + public OrchestrationRuntimeStatus RuntimeStatus { get; set; } + public DateTime CreatedAt { get; } + public DateTime LastUpdatedAt { get; set; } + public TaskFailureDetails? FailureDetails { get; set; } + public InProcessTaskOrchestrationContext? Context { get; set; } +}