Skip to content

Commit 8f7396f

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

File tree

13 files changed

+294
-6
lines changed

13 files changed

+294
-6
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: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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 persistence Idle
12+
/// by creating bookmarks.
13+
/// The idea is to induce unload/load as much as possible to test persistence
14+
/// serialization/deserialization.
15+
/// </summary>
16+
public class SuspendingWrapper : NativeActivity
17+
{
18+
private readonly Variable<int> _nextIndexToExecute = new();
19+
public List<Activity> Activities { get; }
20+
protected override bool CanInduceIdle => true;
21+
22+
public SuspendingWrapper(IEnumerable<Activity> activities)
23+
{
24+
Activities = activities.ToList();
25+
}
26+
27+
public SuspendingWrapper(Activity activity) : this([activity])
28+
{
29+
}
30+
31+
public SuspendingWrapper() : this([])
32+
{
33+
}
34+
35+
protected override void CacheMetadata(NativeActivityMetadata metadata)
36+
{
37+
metadata.AddImplementationVariable(_nextIndexToExecute);
38+
base.CacheMetadata(metadata);
39+
}
40+
41+
protected override void Execute(NativeActivityContext context) => ExecuteNext(context);
42+
43+
private void OnChildCompleted(NativeActivityContext context, ActivityInstance completedInstance) =>
44+
ExecuteNext(context);
45+
46+
private void OnChildFaulted(NativeActivityFaultContext faultContext, Exception propagatedException, ActivityInstance propagatedFrom) =>
47+
ExceptionDispatchInfo.Capture(propagatedException).Throw();
48+
49+
private void ExecuteNext(NativeActivityContext context) =>
50+
context.CreateBookmark(
51+
$"{WorkflowApplicationTestExtensions.AutoResumedBookmarkNamePrefix}{Guid.NewGuid()}",
52+
AfterResume);
53+
54+
private void AfterResume(NativeActivityContext context, Bookmark bookmark, object value)
55+
{
56+
var nextIndex = _nextIndexToExecute.Get(context);
57+
if (nextIndex == Activities.Count)
58+
{
59+
return;
60+
}
61+
_nextIndexToExecute.Set(context, nextIndex + 1);
62+
context.ScheduleActivity(Activities[nextIndex], OnChildCompleted, OnChildFaulted);
63+
}
64+
}
65+
}
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/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/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.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
}

0 commit comments

Comments
 (0)