Skip to content

Commit 5316fb4

Browse files
author
vuplea
committed
Start simpler test framework
1 parent 3ce0de1 commit 5316fb4

File tree

8 files changed

+275
-2
lines changed

8 files changed

+275
-2
lines changed

src/Test/Directory.Build.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<Project>
22
<PropertyGroup>
33
<TargetFrameworks>net6.0;net6.0-windows</TargetFrameworks>
4+
<LangVersion>latest</LangVersion>
45
</PropertyGroup>
56
<Import Project="$(SolutionDir)Directory.Build.props" />
67
<ItemGroup>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System.Activities;
2+
using System.Activities.Hosting;
3+
using System.Collections.Generic;
4+
using System.Threading.Tasks;
5+
6+
namespace WorkflowApplicationTestExtensions
7+
{
8+
/// <summary>
9+
/// Activity that induces Idle for a few milliseconds but not PersistableIdle.
10+
/// This is similar to UiPath asynchronous in-process activities.
11+
/// </summary>
12+
public class NoPersistAsyncActivity : NativeActivity
13+
{
14+
private readonly Variable<NoPersistHandle> _noPersist = new();
15+
16+
protected override bool CanInduceIdle => true;
17+
18+
protected override void CacheMetadata(NativeActivityMetadata metadata)
19+
{
20+
metadata.AddImplementationVariable(_noPersist);
21+
metadata.AddDefaultExtensionProvider(() => new BookmarkResumer());
22+
base.CacheMetadata(metadata);
23+
}
24+
25+
protected override void Execute(NativeActivityContext context)
26+
{
27+
_noPersist.Get(context).Enter(context);
28+
context.GetExtension<BookmarkResumer>().ResumeSoon(context.CreateBookmark());
29+
}
30+
}
31+
32+
public class BookmarkResumer : IWorkflowInstanceExtension
33+
{
34+
private WorkflowInstanceProxy _instance;
35+
public IEnumerable<object> GetAdditionalExtensions() => [];
36+
public void SetInstance(WorkflowInstanceProxy instance) => _instance = instance;
37+
public void ResumeSoon(Bookmark bookmark) => Task.Delay(10).ContinueWith(_ =>
38+
{
39+
_instance.BeginResumeBookmark(bookmark, null, null, null);
40+
});
41+
}
42+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System;
2+
using System.Activities;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Runtime.ExceptionServices;
6+
7+
namespace WorkflowApplicationTestExtensions
8+
{
9+
/// <summary>
10+
/// Wrapper over one/multiple sequential activities.
11+
/// Between scheduling the activity/activities, it induces PersistableIdle
12+
/// by creating bookmarks.
13+
/// The idea is to induce unload/load as much as possible to test persistence
14+
/// serialization/deserialization.
15+
/// When using <see cref="WorkflowApplicationTestExtensions"/>, the bookmarks
16+
/// can be automatically resumed and workflow continued transparently until
17+
/// completion.
18+
/// </summary>
19+
public class SuspendingWrapper : NativeActivity
20+
{
21+
private readonly Variable<int> _nextIndexToExecute = new();
22+
public List<Activity> Activities { get; }
23+
protected override bool CanInduceIdle => true;
24+
25+
public SuspendingWrapper(IEnumerable<Activity> activities)
26+
{
27+
Activities = activities.ToList();
28+
}
29+
30+
public SuspendingWrapper(Activity activity) : this([activity])
31+
{
32+
}
33+
34+
public SuspendingWrapper() : this([])
35+
{
36+
}
37+
38+
protected override void CacheMetadata(NativeActivityMetadata metadata)
39+
{
40+
metadata.AddImplementationVariable(_nextIndexToExecute);
41+
base.CacheMetadata(metadata);
42+
}
43+
44+
protected override void Execute(NativeActivityContext context) => ExecuteNext(context);
45+
46+
private void OnChildCompleted(NativeActivityContext context, ActivityInstance completedInstance) =>
47+
ExecuteNext(context);
48+
49+
private void OnChildFaulted(NativeActivityFaultContext faultContext, Exception propagatedException, ActivityInstance propagatedFrom) =>
50+
ExceptionDispatchInfo.Capture(propagatedException).Throw();
51+
52+
private void ExecuteNext(NativeActivityContext context) =>
53+
context.CreateBookmark(
54+
$"{WorkflowApplicationTestExtensions.AutoResumedBookmarkNamePrefix}{Guid.NewGuid()}",
55+
AfterResume);
56+
57+
private void AfterResume(NativeActivityContext context, Bookmark bookmark, object value)
58+
{
59+
var nextIndex = _nextIndexToExecute.Get(context);
60+
if (nextIndex == Activities.Count)
61+
{
62+
return;
63+
}
64+
_nextIndexToExecute.Set(context, nextIndex + 1);
65+
context.ScheduleActivity(Activities[nextIndex], OnChildCompleted, OnChildFaulted);
66+
}
67+
}
68+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using JsonFileInstanceStore;
2+
using System;
3+
using System.Activities;
4+
using System.Diagnostics;
5+
using System.Threading.Tasks;
6+
using StringToObject = System.Collections.Generic.IDictionary<string, object>;
7+
8+
namespace WorkflowApplicationTestExtensions
9+
{
10+
public static class WorkflowApplicationTestExtensions
11+
{
12+
public const string AutoResumedBookmarkNamePrefix = "AutoResumedBookmark_";
13+
14+
public record WorkflowApplicationResult(StringToObject Outputs, int PersistenceCount);
15+
16+
/// <summary>
17+
/// Simple API to wait for the workflow to complete or propagate to the caller any error.
18+
/// Also, when PersistableIdle, will automatically Unload, Load, resume some bookmarks
19+
/// (those named "AutoResumedBookmark_...") and continue execution.
20+
/// </summary>
21+
public static WorkflowApplicationResult RunUntilCompletion(this WorkflowApplication application)
22+
{
23+
var persistenceCount = 0;
24+
var output = new TaskCompletionSource<WorkflowApplicationResult>();
25+
application.Completed += (WorkflowApplicationCompletedEventArgs args) =>
26+
{
27+
if (args.TerminationException is { } ex)
28+
{
29+
output.TrySetException(ex);
30+
}
31+
if (args.CompletionState == ActivityInstanceState.Canceled)
32+
{
33+
throw new OperationCanceledException("Workflow canceled.");
34+
}
35+
output.TrySetResult(new(args.Outputs, persistenceCount));
36+
};
37+
38+
application.Aborted += args => output.TrySetException(args.Reason);
39+
40+
application.InstanceStore = new FileInstanceStore(Environment.CurrentDirectory);
41+
application.PersistableIdle += (WorkflowApplicationIdleEventArgs args) =>
42+
{
43+
Debug.WriteLine("PersistableIdle");
44+
var bookmarks = args.Bookmarks;
45+
Task.Delay(100).ContinueWith(_ =>
46+
{
47+
try
48+
{
49+
if (++persistenceCount > 100)
50+
{
51+
throw new Exception("Persisting too many times, aborting test.");
52+
}
53+
application = CloneWorkflowApplication(application);
54+
application.Load(args.InstanceId);
55+
foreach (var bookmark in bookmarks)
56+
{
57+
application.ResumeBookmark(new Bookmark(bookmark.BookmarkName), null);
58+
}
59+
}
60+
catch (Exception ex)
61+
{
62+
output.TrySetException(ex);
63+
}
64+
});
65+
return PersistableIdleAction.Unload;
66+
};
67+
68+
application.BeginRun(null, null);
69+
70+
try
71+
{
72+
output.Task.Wait(TimeSpan.FromSeconds(15));
73+
}
74+
catch (Exception ex) when (ex is not OperationCanceledException)
75+
{
76+
}
77+
return output.Task.GetAwaiter().GetResult();
78+
}
79+
80+
private static WorkflowApplication CloneWorkflowApplication(WorkflowApplication application)
81+
{
82+
var clone = new WorkflowApplication(application.WorkflowDefinition, application.DefinitionIdentity)
83+
{
84+
Aborted = application.Aborted,
85+
Completed = application.Completed,
86+
PersistableIdle = application.PersistableIdle,
87+
InstanceStore = application.InstanceStore,
88+
};
89+
foreach (var extension in application.Extensions.GetAllSingletonExtensions())
90+
{
91+
clone.Extensions.Add(extension);
92+
}
93+
return clone;
94+
}
95+
}
96+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<ItemGroup>
3+
<ProjectReference Include="..\JsonFileInstanceStore\JsonFileInstanceStore.csproj" />
4+
</ItemGroup>
5+
</Project>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using Shouldly;
2+
using System;
3+
using System.Activities;
4+
using System.Activities.Statements;
5+
using System.Threading.Tasks;
6+
using Xunit;
7+
8+
namespace WorkflowApplicationTestExtensions
9+
{
10+
public class WorkflowApplicationTestSamples
11+
{
12+
[Fact]
13+
public void RunUntilCompletion_Outputs()
14+
{
15+
var app = new WorkflowApplication(new DynamicActivity
16+
{
17+
Properties = { new DynamicActivityProperty { Name = "result", Type = typeof(OutArgument<string>) } },
18+
Implementation = () => new Assign<string> { To = new Reference<string>("result"), Value = "value" }
19+
});
20+
app.RunUntilCompletion().Outputs["result"].ShouldBe("value");
21+
}
22+
23+
[Fact]
24+
public void RunUntilCompletion_Faulted()
25+
{
26+
var app = new WorkflowApplication(new Throw { Exception = new InArgument<Exception>(_ => new ArgumentException()) });
27+
Should.Throw<ArgumentException>(app.RunUntilCompletion);
28+
}
29+
30+
[Fact]
31+
public void RunUntilCompletion_Aborted()
32+
{
33+
var app = new WorkflowApplication(new Delay { Duration = TimeSpan.MaxValue });
34+
Task.Delay(10).ContinueWith(_ => app.Abort());
35+
Should.Throw<WorkflowApplicationAbortedException>(app.RunUntilCompletion);
36+
}
37+
38+
[Fact]
39+
public void RunUntilCompletion_AutomaticPersistence()
40+
{
41+
var app = new WorkflowApplication(new SuspendingWrapper
42+
{
43+
Activities =
44+
{
45+
new WriteLine(),
46+
new NoPersistAsyncActivity(),
47+
new WriteLine()
48+
}
49+
});
50+
var result = app.RunUntilCompletion();
51+
result.PersistenceCount.ShouldBe(4);
52+
}
53+
}
54+
}

src/UiPath.Workflow.Runtime/Hosting/WorkflowInstanceExtensionManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public virtual void Add<T>(Func<T> extensionCreationFunction) where T : class
9090
ExtensionProviders.Add(new KeyValuePair<Type, WorkflowInstanceExtensionProvider>(typeof(T), new WorkflowInstanceExtensionProvider<T>(extensionCreationFunction)));
9191
}
9292

93-
internal List<object> GetAllSingletonExtensions() => _allSingletonExtensions;
93+
public List<object> GetAllSingletonExtensions() => _allSingletonExtensions;
9494

9595
internal void AddAllExtensionTypes(HashSet<Type> extensionTypes)
9696
{

src/UiPath.Workflow.sln

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Perf", "Perf", "{8E6A125F-0
4646
EndProject
4747
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreWf.Benchmarks", "Perf\CoreWf.Benchmarks\CoreWf.Benchmarks.csproj", "{FE1D4185-DF43-467C-8380-CBA60C6B92DA}"
4848
EndProject
49-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomTestObjects", "Test\CustomTestObjects\CustomTestObjects.csproj", "{E8CED08F-0838-4167-AA20-5BD42372558E}"
49+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomTestObjects", "Test\CustomTestObjects\CustomTestObjects.csproj", "{E8CED08F-0838-4167-AA20-5BD42372558E}"
50+
EndProject
51+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowApplicationTestExtensions", "Test\WorkflowApplicationTestExtensions\WorkflowApplicationTestExtensions.csproj", "{3942321A-09CE-4CF0-A4B7-BF29EF9A17AF}"
5052
EndProject
5153
Global
5254
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -118,6 +120,10 @@ Global
118120
{E8CED08F-0838-4167-AA20-5BD42372558E}.Debug|Any CPU.Build.0 = Debug|Any CPU
119121
{E8CED08F-0838-4167-AA20-5BD42372558E}.Release|Any CPU.ActiveCfg = Release|Any CPU
120122
{E8CED08F-0838-4167-AA20-5BD42372558E}.Release|Any CPU.Build.0 = Release|Any CPU
123+
{3942321A-09CE-4CF0-A4B7-BF29EF9A17AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
124+
{3942321A-09CE-4CF0-A4B7-BF29EF9A17AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
125+
{3942321A-09CE-4CF0-A4B7-BF29EF9A17AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
126+
{3942321A-09CE-4CF0-A4B7-BF29EF9A17AF}.Release|Any CPU.Build.0 = Release|Any CPU
121127
EndGlobalSection
122128
GlobalSection(SolutionProperties) = preSolution
123129
HideSolutionNode = FALSE
@@ -134,6 +140,7 @@ Global
134140
{154DD961-CA7E-410A-B31F-E057A06E2A68} = {4D92EFCA-7902-49DE-B98F-8CF7675ED86C}
135141
{FE1D4185-DF43-467C-8380-CBA60C6B92DA} = {8E6A125F-0530-4D8E-8CD4-3429DAF57706}
136142
{E8CED08F-0838-4167-AA20-5BD42372558E} = {4D92EFCA-7902-49DE-B98F-8CF7675ED86C}
143+
{3942321A-09CE-4CF0-A4B7-BF29EF9A17AF} = {4D92EFCA-7902-49DE-B98F-8CF7675ED86C}
137144
EndGlobalSection
138145
GlobalSection(ExtensibilityGlobals) = postSolution
139146
SolutionGuid = {1ED6A6D7-ACEE-4780-B630-50DCAED74BA9}

0 commit comments

Comments
 (0)