Skip to content

Start simpler test framework #309

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Test/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project>
<PropertyGroup>
<TargetFrameworks>net6.0;net6.0-windows</TargetFrameworks>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<Import Project="$(SolutionDir)Directory.Build.props" />
<ItemGroup>
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Activities;
using System.Activities.Hosting;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace WorkflowApplicationTestExtensions
{
/// <summary>
/// Activity that induces Idle for a few milliseconds but not PersistableIdle.
/// This is similar to UiPath asynchronous in-process activities.
/// </summary>
public class NoPersistAsyncActivity : NativeActivity
{
private readonly Variable<NoPersistHandle> _noPersist = new();

protected override bool CanInduceIdle => true;

protected override void CacheMetadata(NativeActivityMetadata metadata)
{
metadata.AddImplementationVariable(_noPersist);
metadata.AddDefaultExtensionProvider(() => new BookmarkResumer());
base.CacheMetadata(metadata);
}

protected override void Execute(NativeActivityContext context)
{
_noPersist.Get(context).Enter(context);
context.GetExtension<BookmarkResumer>().ResumeSoon(context.CreateBookmark());
}
}

public class BookmarkResumer : IWorkflowInstanceExtension
{
private WorkflowInstanceProxy _instance;
public IEnumerable<object> GetAdditionalExtensions() => [];
public void SetInstance(WorkflowInstanceProxy instance) => _instance = instance;
public void ResumeSoon(Bookmark bookmark) => Task.Delay(10).ContinueWith(_ =>
{
_instance.BeginResumeBookmark(bookmark, null, null, null);
});
}
}
68 changes: 68 additions & 0 deletions src/Test/WorkflowApplicationTestExtensions/SuspendingWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;
using System.Activities;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.ExceptionServices;

namespace WorkflowApplicationTestExtensions
{
/// <summary>
/// Wrapper over one/multiple sequential activities.
/// Between scheduling the activity/activities, it induces PersistableIdle
/// by creating bookmarks.
/// The idea is to induce unload/load as much as possible to test persistence
/// serialization/deserialization.
/// When using <see cref="WorkflowApplicationTestExtensions"/>, the bookmarks
/// can be automatically resumed and workflow continued transparently until
/// completion.
/// </summary>
public class SuspendingWrapper : NativeActivity
{
private readonly Variable<int> _nextIndexToExecute = new();
public List<Activity> Activities { get; }
protected override bool CanInduceIdle => true;

public SuspendingWrapper(IEnumerable<Activity> activities)
{
Activities = activities.ToList();
}

public SuspendingWrapper(Activity activity) : this([activity])
{
}

public SuspendingWrapper() : this([])
{
}

protected override void CacheMetadata(NativeActivityMetadata metadata)
{
metadata.AddImplementationVariable(_nextIndexToExecute);
base.CacheMetadata(metadata);
}

protected override void Execute(NativeActivityContext context) => ExecuteNext(context);

private void OnChildCompleted(NativeActivityContext context, ActivityInstance completedInstance) =>
ExecuteNext(context);

private void OnChildFaulted(NativeActivityFaultContext faultContext, Exception propagatedException, ActivityInstance propagatedFrom) =>
ExceptionDispatchInfo.Capture(propagatedException).Throw();

private void ExecuteNext(NativeActivityContext context) =>
context.CreateBookmark(
$"{WorkflowApplicationTestExtensions.AutoResumedBookmarkNamePrefix}{Guid.NewGuid()}",
AfterResume);

private void AfterResume(NativeActivityContext context, Bookmark bookmark, object value)
{
var nextIndex = _nextIndexToExecute.Get(context);
if (nextIndex == Activities.Count)
{
return;
}
_nextIndexToExecute.Set(context, nextIndex + 1);
context.ScheduleActivity(Activities[nextIndex], OnChildCompleted, OnChildFaulted);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using JsonFileInstanceStore;
using System;
using System.Activities;
using System.Diagnostics;
using System.Threading.Tasks;
using StringToObject = System.Collections.Generic.IDictionary<string, object>;

namespace WorkflowApplicationTestExtensions
{
public static class WorkflowApplicationTestExtensions
{
public const string AutoResumedBookmarkNamePrefix = "AutoResumedBookmark_";

public record WorkflowApplicationResult(StringToObject Outputs, int PersistenceCount);

/// <summary>
/// Simple API to wait for the workflow to complete or propagate to the caller any error.
/// Also, when PersistableIdle, will automatically Unload, Load, resume some bookmarks
/// (those named "AutoResumedBookmark_...") and continue execution.
/// </summary>
public static WorkflowApplicationResult RunUntilCompletion(this WorkflowApplication application)
{
var persistenceCount = 0;
var output = new TaskCompletionSource<WorkflowApplicationResult>();
application.Completed += (WorkflowApplicationCompletedEventArgs args) =>
{
if (args.TerminationException is { } ex)
{
output.TrySetException(ex);
}
if (args.CompletionState == ActivityInstanceState.Canceled)
{
throw new OperationCanceledException("Workflow canceled.");
}
output.TrySetResult(new(args.Outputs, persistenceCount));
};

application.Aborted += args => output.TrySetException(args.Reason);

application.InstanceStore = new FileInstanceStore(Environment.CurrentDirectory);
application.PersistableIdle += (WorkflowApplicationIdleEventArgs args) =>
{
Debug.WriteLine("PersistableIdle");
var bookmarks = args.Bookmarks;
Task.Delay(100).ContinueWith(_ =>
{
try
{
if (++persistenceCount > 100)
{
throw new Exception("Persisting too many times, aborting test.");
}
application = CloneWorkflowApplication(application);
application.Load(args.InstanceId);
foreach (var bookmark in bookmarks)
{
application.ResumeBookmark(new Bookmark(bookmark.BookmarkName), null);
}
}
catch (Exception ex)
{
output.TrySetException(ex);
}
});
return PersistableIdleAction.Unload;
};

application.BeginRun(null, null);

try
{
output.Task.Wait(TimeSpan.FromSeconds(15));
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
}
return output.Task.GetAwaiter().GetResult();
}

private static WorkflowApplication CloneWorkflowApplication(WorkflowApplication application)
{
var clone = new WorkflowApplication(application.WorkflowDefinition, application.DefinitionIdentity)
{
Aborted = application.Aborted,
Completed = application.Completed,
PersistableIdle = application.PersistableIdle,
InstanceStore = application.InstanceStore,
};
foreach (var extension in application.Extensions.GetAllSingletonExtensions())
{
clone.Extensions.Add(extension);
}
return clone;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\JsonFileInstanceStore\JsonFileInstanceStore.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Shouldly;
using System;
using System.Activities;
using System.Activities.Statements;
using System.Threading.Tasks;
using Xunit;

namespace WorkflowApplicationTestExtensions
{
public class WorkflowApplicationTestSamples
{
[Fact]
public void RunUntilCompletion_Outputs()
{
var app = new WorkflowApplication(new DynamicActivity
{
Properties = { new DynamicActivityProperty { Name = "result", Type = typeof(OutArgument<string>) } },
Implementation = () => new Assign<string> { To = new Reference<string>("result"), Value = "value" }
});
app.RunUntilCompletion().Outputs["result"].ShouldBe("value");
}

[Fact]
public void RunUntilCompletion_Faulted()
{
var app = new WorkflowApplication(new Throw { Exception = new InArgument<Exception>(_ => new ArgumentException()) });
Should.Throw<ArgumentException>(app.RunUntilCompletion);
}

[Fact]
public void RunUntilCompletion_Aborted()
{
var app = new WorkflowApplication(new Delay { Duration = TimeSpan.MaxValue });
Task.Delay(10).ContinueWith(_ => app.Abort());
Should.Throw<WorkflowApplicationAbortedException>(app.RunUntilCompletion);
}

[Fact]
public void RunUntilCompletion_AutomaticPersistence()
{
var app = new WorkflowApplication(new SuspendingWrapper
{
Activities =
{
new WriteLine(),
new NoPersistAsyncActivity(),
new WriteLine()
}
});
var result = app.RunUntilCompletion();
result.PersistenceCount.ShouldBe(4);
}
}
}
Original file line number Diff line number Diff line change
@@ -90,7 +90,7 @@ public virtual void Add<T>(Func<T> extensionCreationFunction) where T : class
ExtensionProviders.Add(new KeyValuePair<Type, WorkflowInstanceExtensionProvider>(typeof(T), new WorkflowInstanceExtensionProvider<T>(extensionCreationFunction)));
}

internal List<object> GetAllSingletonExtensions() => _allSingletonExtensions;
public List<object> GetAllSingletonExtensions() => _allSingletonExtensions;

internal void AddAllExtensionTypes(HashSet<Type> extensionTypes)
{
9 changes: 8 additions & 1 deletion src/UiPath.Workflow.sln
Original file line number Diff line number Diff line change
@@ -46,7 +46,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Perf", "Perf", "{8E6A125F-0
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreWf.Benchmarks", "Perf\CoreWf.Benchmarks\CoreWf.Benchmarks.csproj", "{FE1D4185-DF43-467C-8380-CBA60C6B92DA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomTestObjects", "Test\CustomTestObjects\CustomTestObjects.csproj", "{E8CED08F-0838-4167-AA20-5BD42372558E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomTestObjects", "Test\CustomTestObjects\CustomTestObjects.csproj", "{E8CED08F-0838-4167-AA20-5BD42372558E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowApplicationTestExtensions", "Test\WorkflowApplicationTestExtensions\WorkflowApplicationTestExtensions.csproj", "{3942321A-09CE-4CF0-A4B7-BF29EF9A17AF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -118,6 +120,10 @@ Global
{E8CED08F-0838-4167-AA20-5BD42372558E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E8CED08F-0838-4167-AA20-5BD42372558E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E8CED08F-0838-4167-AA20-5BD42372558E}.Release|Any CPU.Build.0 = Release|Any CPU
{3942321A-09CE-4CF0-A4B7-BF29EF9A17AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3942321A-09CE-4CF0-A4B7-BF29EF9A17AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3942321A-09CE-4CF0-A4B7-BF29EF9A17AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3942321A-09CE-4CF0-A4B7-BF29EF9A17AF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -134,6 +140,7 @@ Global
{154DD961-CA7E-410A-B31F-E057A06E2A68} = {4D92EFCA-7902-49DE-B98F-8CF7675ED86C}
{FE1D4185-DF43-467C-8380-CBA60C6B92DA} = {8E6A125F-0530-4D8E-8CD4-3429DAF57706}
{E8CED08F-0838-4167-AA20-5BD42372558E} = {4D92EFCA-7902-49DE-B98F-8CF7675ED86C}
{3942321A-09CE-4CF0-A4B7-BF29EF9A17AF} = {4D92EFCA-7902-49DE-B98F-8CF7675ED86C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1ED6A6D7-ACEE-4780-B630-50DCAED74BA9}