Giter VIP home page Giter VIP logo

Comments (3)

qrzychu avatar qrzychu commented on August 18, 2024

I guess you could roll out your own version of SyncWorkflowRunner, with a method AdvanceSingleStep to achieve this

from workflow-core.

Ralf1108 avatar Ralf1108 commented on August 18, 2024

Thx for the tip!
I was successful using an own implementation of the IWorkflowExecutor.
Now the workflow can be run stepwise... perfect for unittesting when setting mocks and verifications per step.

Here is my solution:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using WorkflowCore.Interface;
using WorkflowCore.Models;
using WorkflowCore.Services;

namespace WorkflowCoreStepwiseWorkflow;

/// <summary>
/// Test stepwise workflow execution
/// </summary>
internal class Program3
{
    static async Task Main(string[] args)
    {
        File.Delete("mydb.db");

        var services = new ServiceCollection();
        services.AddLogging(x => x.AddConsole());
        services.AddTransient<LogStep>();
        services.AddWorkflow(x => { x.UseSqlite("Data Source=mydb.db", true); });

        services.AddSingleton<IWorkflowExecutor, StepwiseWorkflowExecutor>();

        var serviceProvider = services.BuildServiceProvider();
        var host = serviceProvider.GetRequiredService<IWorkflowHost>();
        host.RegisterWorkflow<TestWorkflow, TestWorkflowData>();

        host.OnStepError += Host_OnStepError;
        host.Start();

        var workflowId = await host.StartWorkflow("TestWorkflow", new TestWorkflowData());
        Console.WriteLine($"Started workflow: {workflowId}");

        var workflowExecutor = (StepwiseWorkflowExecutor)serviceProvider.GetRequiredService<IWorkflowExecutor>();
        var timeout = TimeSpan.FromSeconds(10);

        WaitForKey();
        workflowExecutor.ExecuteNextStep(timeout);
            
        WaitForKey();
        workflowExecutor.ExecuteNextStep(timeout);

        WaitForKey();
        workflowExecutor.ExecuteNextStep(timeout);

        WaitForKey();
        workflowExecutor.ExecuteNextStep(timeout);

        WaitForKey();
        workflowExecutor.ExecuteRemainingSteps();
    }

    private static void WaitForKey()
    {
        Console.WriteLine("Press key to continue");
        Console.ReadKey();
    }

    private static void Host_OnStepError(WorkflowInstance workflow, WorkflowStep step, Exception exception)
    {
        Console.WriteLine("Workflow Error: " + exception.Message);
    }

    class TestWorkflow : IWorkflow<TestWorkflowData>
    {
        public string Id => "TestWorkflow";
        public int Version => 1;

        public void Build(IWorkflowBuilder<TestWorkflowData> builder)
        {
            builder
                .StartWith<LogStep>()
                .Input(x => x.Number, x => 1)
                .Then<LogStep>()
                .Input(x => x.Number, x => 2)
                .Then<LogStep>()
                .Input(x => x.Number, x => 3)
                .Then<LogStep>()
                .Input(x => x.Number, x => 4);
        }
    }

    class LogStep : IStepBody
    {
        public int Number { get; set; }

        public async Task<ExecutionResult> RunAsync(IStepExecutionContext context)
        {
            Console.WriteLine($"LogStep: {Number}");
            return ExecutionResult.Next();
        }
    }

    class TestWorkflowData
    {
    }
}

/// <summary>
/// Issue: https://github.com/danielgerlag/workflow-core/issues/1190
/// Wraps default implementation: https://github.com/danielgerlag/workflow-core/blob/master/src/WorkflowCore/Services/WorkflowExecutor.cs
/// 
/// Register via: services.AddSingleton&lt;IWorkflowExecutor, StepwiseWorkflowExecutor&gt;();
/// </summary>
public class StepwiseWorkflowExecutor : IWorkflowExecutor
{
    private readonly WorkflowExecutor _wrapped;

    private readonly Semaphore _executeStep = new(0, int.MaxValue);
    private readonly Semaphore _executedStep = new(0, 1);

    public StepwiseWorkflowExecutor(
        IWorkflowRegistry registry,
        IServiceProvider serviceProvider,
        IScopeProvider scopeProvider,
        IDateTimeProvider datetimeProvider,
        IExecutionResultProcessor executionResultProcessor,
        ILifeCycleEventPublisher publisher,
        ICancellationProcessor cancellationProcessor,
        WorkflowOptions options,
        ILoggerFactory loggerFactory)
    {
        _wrapped = new WorkflowExecutor(registry,
            serviceProvider, scopeProvider, datetimeProvider,
            executionResultProcessor,
            publisher,
            cancellationProcessor,
            options,
            loggerFactory);
    }

    public async Task<WorkflowExecutorResult> Execute(
        WorkflowInstance workflow,
        CancellationToken cancellationToken = new())
    {
        while (!_executeStep.WaitOne(100))
            cancellationToken.ThrowIfCancellationRequested();

        var workflowExecutorResult = await _wrapped.Execute(workflow, cancellationToken);
        _executedStep.Release(1);
        return workflowExecutorResult;
    }

    public bool ExecuteNextStep(TimeSpan timeout, bool throwIfTimeout = true)
    {
        _executeStep.Release(1);
        var result = _executedStep.WaitOne(timeout);

        if (throwIfTimeout && !result)
            throw new InvalidOperationException("Timeout when waiting for next workflow step");

        return result;
    }

    /// <summary>
    /// WaitFor consists of 2 steps
    /// </summary>
    public bool ExecuteNextWaitFor(TimeSpan timeout, bool throwIfTimeout = true)
    {
        if (!ExecuteNextStep(timeout)) // WaitFor
            return false;

        return ExecuteNextStep(timeout); // Receive
    }

    /// <summary>
    /// Decide/Branch contains of 1 step
    /// </summary>
    public bool ExecuteNextDecideBranch(TimeSpan timeout, bool throwIfTimeout = true)
    {
        return ExecuteNextStep(timeout);
    }

    public void ExecuteRemainingSteps()
    {
        // let all remaining steps run to end
        _executeStep.Release(int.MaxValue / 2);
    }
}

Console output:

info: WorkflowCore.Services.WorkflowHost[0]
Starting background tasks
Started workflow: f2c41576-0fb2-4072-81cd-3f5a551ec7e1
Press key to continue
LogStep: 1
Press key to continue
LogStep: 2
Press key to continue
LogStep: 3
Press key to continue
LogStep: 4
Press key to continue

from workflow-core.

Ralf1108 avatar Ralf1108 commented on August 18, 2024

For reference: I created a base class for my unittests in case someone else needs something similar:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Moq;
using WorkflowCore.Interface;
using WorkflowCore.Models;
using WorkflowCore.Testing;
using WorkflowCoreStepwiseWorkflow;
using Xunit;
using Xunit.Abstractions;

namespace WorkflowCoreStepwiseTestBaseClass;

public class WorkflowTestBase<TWorkflow, TData> : WorkflowTest<TWorkflow, TData>
    where TWorkflow : IWorkflow<TData>, new()
    where TData : class, new()
{
    private readonly ITestOutputHelper _testOutputHelper;

    protected TimeSpan TestTimeout { get; } = TimeSpan.FromSeconds(System.Diagnostics.Debugger.IsAttached ? 30 : 3);

    private StepwiseWorkflowExecutor _workflowExecutor = null!; // will be set in setup
    
    public WorkflowTestBase(ITestOutputHelper testOutputHelper)
    {
        _testOutputHelper = testOutputHelper;
    }

    /// <summary>
    /// We need to access the ServiceProvider
    /// Method copied from: https://github.com/danielgerlag/workflow-core/blob/master/src/WorkflowCore.Testing/WorkflowTest.cs
    /// </summary>
    protected override void Setup()
    {
        var services = new ServiceCollection();
        services.AddLogging();
        ConfigureServices(services);

        var serviceProvider = services.BuildServiceProvider();

        PersistenceProvider = serviceProvider.GetRequiredService<IPersistenceProvider>();
        Host = serviceProvider.GetRequiredService<IWorkflowHost>();
        Host.RegisterWorkflow<TWorkflow, TData>();
        Host.OnStepError += Host_OnStepError;
        Host.Start();

        _workflowExecutor = (StepwiseWorkflowExecutor)serviceProvider.GetRequiredService<IWorkflowExecutor>();
    }

    protected override void ConfigureServices(IServiceCollection services)
    {
        base.ConfigureServices(services);

        services.AddLogging(x =>
                                x.AddConsole());

        services.AddSingleton<IWorkflowExecutor, StepwiseWorkflowExecutor>();

        services.AddTransient<LogStep>();
    }

    protected void TestStep(string stepName)
    {
        _testOutputHelper.WriteLine($"Testing step '{stepName}'...");
    }

    protected void TestWaitFor(string waitName)
    {
        _testOutputHelper.WriteLine($"Testing wait for '{waitName}'...");
    }

    protected void TestDecideBranch(string decideName)
    {
        _testOutputHelper.WriteLine($"Testing decide/branch '{decideName}'...");
    }

    protected bool ExecuteNextStep(TimeSpan timeout, bool throwIfTimeout = true)
    {
        return _workflowExecutor.ExecuteNextStep(timeout, throwIfTimeout);
    }

    protected bool ExecuteNextWaitFor(TimeSpan timeout, bool throwIfTimeout = true)
    {
        return _workflowExecutor.ExecuteNextWaitFor(timeout, throwIfTimeout);
    }

    protected bool ExecuteNextDecideBranch(TimeSpan timeout, bool throwIfTimeout = true)
    {
        return _workflowExecutor.ExecuteNextDecideBranch(timeout, throwIfTimeout);
    }
}

public class TestWorkflowTests : WorkflowTestBase<TestWorkflow, TestWorkflowData>
{
    private readonly Mock<IMonitor> _monitor;

    public TestWorkflowTests(ITestOutputHelper testOutputHelper) 
        : base(testOutputHelper)
    {
        _monitor = new Mock<IMonitor>();
    }

    protected override void ConfigureServices(IServiceCollection services)
    {
        base.ConfigureServices(services);

        services.AddTransient(_ => _monitor.Object);
    }

    [Fact]
    public async Task SimpleTest()
    {
        // Arrange
        Setup();

        // Act
        var data = new TestWorkflowData();
        var workflowId = await StartWorkflowAsync(data);

        TestStep(nameof(LogStep));
        ExecuteNextStep(TestTimeout);
        _monitor.Verify(x => x.Execute(1));

        TestStep(nameof(LogStep));
        ExecuteNextStep(TestTimeout);
        _monitor.Verify(x => x.Execute(2));

        TestStep(nameof(LogStep));
        ExecuteNextStep(TestTimeout);
        _monitor.Verify(x => x.Execute(3));

        TestStep(nameof(LogStep));
        ExecuteNextStep(TestTimeout);
        _monitor.Verify(x => x.Execute(4));
    }
}

public class TestWorkflow : IWorkflow<TestWorkflowData>
{
    public string Id => "TestWorkflow";
    public int Version => 1;

    public void Build(IWorkflowBuilder<TestWorkflowData> builder)
    {
        builder
            .StartWith<LogStep>()
            .Input(x => x.Number, x => 1)
            .Then<LogStep>()
            .Input(x => x.Number, x => 2)
            .Then<LogStep>()
            .Input(x => x.Number, x => 3)
            .Then<LogStep>()
            .Input(x => x.Number, x => 4);
    }
}

public class LogStep : IStepBody
{
    private readonly IMonitor _monitor;

    public int Number { get; set; }

    public LogStep(IMonitor monitor)
    {
        _monitor = monitor;
    }

    public async Task<ExecutionResult> RunAsync(IStepExecutionContext context)
    {
        Console.WriteLine($"LogStep: {Number}");
        _monitor.Execute(Number);
        return ExecutionResult.Next();
    }
}

public interface IMonitor
{
    void Execute(int number);
}

public class TestWorkflowData
{
}

from workflow-core.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.