Skip to content

Commit ca977b5

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

File tree

11 files changed

+278
-5
lines changed

11 files changed

+278
-5
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: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
public class NoPersistAsyncActivity : NativeActivity
9+
{
10+
private readonly Variable<NoPersistHandle> _noPersist = new();
11+
12+
protected override bool CanInduceIdle => true;
13+
14+
protected override void CacheMetadata(NativeActivityMetadata metadata)
15+
{
16+
metadata.AddImplementationVariable(_noPersist);
17+
metadata.AddDefaultExtensionProvider(() => new BookmarkResumer());
18+
base.CacheMetadata(metadata);
19+
}
20+
21+
protected override void Execute(NativeActivityContext context)
22+
{
23+
_noPersist.Get(context).Enter(context);
24+
context.GetExtension<BookmarkResumer>().ResumeSoon(context.CreateBookmark());
25+
}
26+
}
27+
28+
public class BookmarkResumer : IWorkflowInstanceExtension
29+
{
30+
private WorkflowInstanceProxy _instance;
31+
public IEnumerable<object> GetAdditionalExtensions() => [];
32+
public void SetInstance(WorkflowInstanceProxy instance) => _instance = instance;
33+
public void ResumeSoon(Bookmark bookmark) => Task.Delay(10).ContinueWith(_ =>
34+
{
35+
_instance.BeginResumeBookmark(bookmark, null, null, null);
36+
});
37+
}
38+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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 sequencial activities that blocks and automatically
11+
/// resumes as much as possible to validate as serialization/deserialization.
12+
/// </summary>
13+
public class SuspendingWrapper(IEnumerable<Activity> activities) : NativeActivity
14+
{
15+
protected override bool CanInduceIdle => true;
16+
17+
public SuspendingWrapper(Activity activity) : this([activity])
18+
{
19+
}
20+
21+
public SuspendingWrapper() : this([])
22+
{
23+
}
24+
25+
private readonly Variable<int> _nextIndexToExecute = new();
26+
public List<Activity> Activities { get; } = activities.ToList();
27+
28+
protected override void CacheMetadata(NativeActivityMetadata metadata)
29+
{
30+
metadata.AddImplementationVariable(_nextIndexToExecute);
31+
base.CacheMetadata(metadata);
32+
}
33+
34+
protected override void Execute(NativeActivityContext context) => ExecuteNext(context);
35+
36+
private void OnChildCompleted(NativeActivityContext context, ActivityInstance completedInstance) =>
37+
ExecuteNext(context);
38+
39+
private void OnChildFaulted(NativeActivityFaultContext faultContext, Exception propagatedException, ActivityInstance propagatedFrom) =>
40+
ExceptionDispatchInfo.Capture(propagatedException).Throw();
41+
42+
private void ExecuteNext(NativeActivityContext context) =>
43+
context.CreateBookmark(
44+
$"{WorkflowApplicationTestExtensions.AutoResumedBookmarkNamePrefix}{Guid.NewGuid()}",
45+
AfterResume);
46+
47+
private void AfterResume(NativeActivityContext context, Bookmark bookmark, object value)
48+
{
49+
var nextIndex = _nextIndexToExecute.Get(context);
50+
if (nextIndex == Activities.Count)
51+
{
52+
return;
53+
}
54+
_nextIndexToExecute.Set(context, nextIndex + 1);
55+
context.ScheduleActivity(Activities[nextIndex], OnChildCompleted, OnChildFaulted);
56+
}
57+
}
58+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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+
// TODO: clone extensions
90+
return clone;
91+
}
92+
}
93+
}
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/ActivityContext.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,11 @@ public WorkflowDataContext DataContext
8686
}
8787
}
8888

89-
internal bool IsDisposed => _isDisposed;
89+
internal bool IsDisposed => _isDisposed;
90+
91+
public void AddAutomationTrackerId() => CurrentInstance.AddAutomationTrackerId();
92+
93+
public string GetAutomationTrackerId() => CurrentInstance.GetAutomationTrackerId();
9094

9195
public T GetExtension<T>()
9296
where T : class

src/UiPath.Workflow.Runtime/ActivityInstance.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,16 @@ internal string OwnerName
402402
}
403403

404404
[DataMember(EmitDefaultValue = false)]
405-
public Version ImplementationVersion { get; internal set; }
405+
public Version ImplementationVersion { get; internal set; }
406+
407+
public void AddAutomationTrackerId()
408+
{
409+
var properties = new ExecutionProperties(null, this, PropertyManager);
410+
var existing = properties.Find("BPOId") as string;
411+
properties.Add("BPOId", (existing + "-" + Guid.NewGuid().ToString("N")).Trim('-'), true, false);
412+
}
413+
414+
public string GetAutomationTrackerId() => PropertyManager?.GetPropertyAtCurrentScope("BPOId") as string;
406415

407416
internal static ActivityInstance CreateCompletedInstance(Activity activity)
408417
{

src/UiPath.Workflow.Runtime/Statements/Parallel.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
using System.Activities.Runtime.Collections;
55
using System.Collections.ObjectModel;
6+
using System.Reflection;
67
using System.Windows.Markup;
8+
using static System.Activities.XD;
79

810
#if DYNAMICUPDATE
911
using System.Activities.DynamicUpdate;
@@ -129,7 +131,8 @@ protected override void Execute(NativeActivityContext context)
129131

130132
for (int i = Branches.Count - 1; i >= 0; i--)
131133
{
132-
context.ScheduleActivity(Branches[i], onBranchComplete);
134+
var instance = context.ScheduleActivity(Branches[i], onBranchComplete);
135+
instance.AddAutomationTrackerId();
133136
}
134137
}
135138
}

src/UiPath.Workflow.Runtime/Statements/ParallelForEach.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ protected override void Execute(NativeActivityContext context)
7373
{
7474
if (Body != null)
7575
{
76-
context.ScheduleAction(Body, valueEnumerator.Current, onBodyComplete);
76+
var instance = context.ScheduleAction(Body, valueEnumerator.Current, onBodyComplete);
77+
instance.AddAutomationTrackerId();
7778
}
7879
}
7980
valueEnumerator.Dispose();

0 commit comments

Comments
 (0)