Skip to content

Commit f6ad51d

Browse files
[ROBO-4985] RemoteException - Client StackTrace (#121)
* Enhance exception handling and testing Added new test methods in `ComputingTests.cs` to verify remote exception stack traces, including client-side and server-side frames. Introduced `ShouldPartiallyContainInOrder` in `ShouldlyHelpers.cs` for ordered string verification. Updated `ComputingCallback.cs` and `ComputingService.cs` with methods to simulate divide-by-zero exceptions. Modified interfaces in `IComputingService.cs` to support new methods. Adjusted using directives across several files for consistency and removed unused ones. Enhanced `RemoteException` stack trace handling in `Dtos.cs` for different .NET versions. * simplification
1 parent 61952e8 commit f6ad51d

File tree

8 files changed

+108
-8
lines changed

8 files changed

+108
-8
lines changed

src/UiPath.CoreIpc.Tests/ComputingTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Nito.AsyncEx;
33
using Nito.Disposables;
44
using NSubstitute;
5+
using System.Runtime.CompilerServices;
56
using System.Runtime.InteropServices;
67
using System.Text;
78
using UiPath.Ipc.Transport.NamedPipe;
@@ -258,6 +259,38 @@ await Enumerable.Range(1, CParallelism)
258259
.WhenAll();
259260
}
260261

262+
263+
[Theory]
264+
[InlineData(false,
265+
nameof(ComputingService.ServerFrame2),
266+
nameof(ComputingService.ServerFrame1),
267+
nameof(ClientFrame2),
268+
nameof(ClientFrame1))]
269+
[InlineData(true,
270+
nameof(ComputingCallback.CallbackFrame2),
271+
nameof(ComputingCallback.CallbackFrame1),
272+
nameof(ComputingService.GatewayFrame2),
273+
nameof(ComputingService.GatewayFrame1),
274+
nameof(ClientFrame2),
275+
nameof(ClientFrame1))]
276+
public async Task RemoteExceptionStackTrace_ShouldAlsoIncludeRemoteCallerFrames(bool callGateway, params string[] expectedFrames)
277+
{
278+
var act = async () => await ClientFrame1(callGateway);
279+
var exception = await act.ShouldThrowAsync<RemoteException>();
280+
281+
exception.StackTrace
282+
.ShouldNotBeNull()
283+
.Split('\n')
284+
.ShouldPartiallyContainInOrder(expectedFrames);
285+
}
286+
287+
[MethodImpl(MethodImplOptions.NoInlining)]
288+
private async Task ClientFrame1(bool callGateway) => await ClientFrame2(callGateway);
289+
290+
[MethodImpl(MethodImplOptions.NoInlining)]
291+
private async Task ClientFrame2(bool callGateway) => _ = await (callGateway ? Proxy.DivideByZeroGateway() : Proxy.DivideByZero());
292+
293+
261294
public abstract IAsyncDisposable? RandomTransportPair(out ServerTransport listener, out ClientTransport transport);
262295

263296
public abstract ExternalServerParams RandomServerParams();

src/UiPath.CoreIpc.Tests/Helpers/ShouldlyHelpers.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System.Runtime.CompilerServices;
2+
using System.Text;
3+
using System.Threading.Tasks;
24

35
namespace UiPath.Ipc.Tests;
46

@@ -75,7 +77,7 @@ public static async Task ShouldStallForAtLeastAsync<T>(this Task<T> task, TimeSp
7577
throw new ShouldAssertException($"The task {taskExpression} should stall for at least {lease} but it completed faster.");
7678
}
7779
catch (OperationCanceledException ex) when (ex.CancellationToken == cts.Token)
78-
{
80+
{
7981
}
8082
}
8183

@@ -114,4 +116,23 @@ private static async Task<T> Return<T>(this Task task, T value = default!)
114116
await task;
115117
return value;
116118
}
119+
120+
public static void ShouldPartiallyContainInOrder(this IEnumerable<string> haystack, IReadOnlyList<string> needles)
121+
{
122+
int expected = 0;
123+
if (ConditionFulfilled()) { return; }
124+
125+
foreach (var candidate in haystack)
126+
{
127+
if (candidate.Contains(needles[expected]))
128+
{
129+
expected++;
130+
if (ConditionFulfilled()) { return; }
131+
}
132+
}
133+
134+
throw new ShouldAssertException($"Expected `{nameof(haystack)}` to contain {string.Join("\r\n", needles)} partially and in order. First missing item is: {needles[expected]}");
135+
136+
bool ConditionFulfilled() => expected >= needles.Count;
137+
}
117138
}

src/UiPath.CoreIpc.Tests/Services/ComputingCallback.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,13 @@ public sealed class ComputingCallback : IComputingCallback
77
public async Task<string> GetThreadName() => Thread.CurrentThread.Name!;
88

99
public async Task<int> AddInts(int x, int y) => x + y;
10+
11+
public async Task<bool> DivideByZeroOnClient()
12+
{
13+
return await CallbackFrame1();
14+
}
15+
16+
public async Task<bool> CallbackFrame1() => await CallbackFrame2();
17+
public async Task<bool> CallbackFrame2() => throw new DivideByZeroException();
1018
}
1119

src/UiPath.CoreIpc.Tests/Services/ComputingService.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.Extensions.Logging;
2+
using System.Runtime.CompilerServices;
23

34
namespace UiPath.Ipc.Tests;
45

@@ -67,4 +68,26 @@ public async Task<string> SendMessage(Message m = null!, CancellationToken ct =
6768
{
6869
return await m.Client.GetCallback<IComputingCallback>().GetThreadName();
6970
}
71+
72+
public async Task<bool> DivideByZero()
73+
{
74+
ServerFrame1();
75+
return true;
76+
}
77+
78+
public async Task<bool> DivideByZeroGateway(Message m = null!)
79+
{
80+
return await GatewayFrame1(m);
81+
}
82+
83+
[MethodImpl(MethodImplOptions.NoInlining)]
84+
public static void ServerFrame1() => ServerFrame2();
85+
[MethodImpl(MethodImplOptions.NoInlining)]
86+
public static void ServerFrame2() => throw new DivideByZeroException();
87+
88+
89+
[MethodImpl(MethodImplOptions.NoInlining)]
90+
public static async Task<bool> GatewayFrame1(Message m) => await GatewayFrame2(m);
91+
[MethodImpl(MethodImplOptions.NoInlining)]
92+
public static async Task<bool> GatewayFrame2(Message m) => await m.Client.GetCallback<IComputingCallback>().DivideByZeroOnClient();
7093
}

src/UiPath.CoreIpc.Tests/Services/IComputingService.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ public interface IComputingService : IComputingServiceBase
1515
Task<int> MultiplyInts(int x, int y, Message message = null!);
1616
Task<string?> GetCallContext();
1717
Task<string> SendMessage(Message m = null!, CancellationToken ct = default);
18+
Task<bool> DivideByZero();
19+
Task<bool> DivideByZeroGateway(Message m = null);
1820
}
1921

2022
public interface IComputingCallbackBase
@@ -25,6 +27,7 @@ public interface IComputingCallbackBase
2527
public interface IComputingCallback : IComputingCallbackBase
2628
{
2729
Task<string> GetThreadName();
30+
Task<bool> DivideByZeroOnClient();
2831
}
2932

3033
public interface IArithmeticCallback

src/UiPath.CoreIpc.Tests/Services/SystemService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
using Castle.Core;
2-
using System.Buffers;
1+
using System.Buffers;
32
using System.Globalization;
3+
using System.Runtime.CompilerServices;
44
using System.Text;
55

66
namespace UiPath.Ipc.Tests;

src/UiPath.CoreIpc.Tests/SystemTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using AutoFixture;
2-
using AutoFixture.Xunit2;
32
using Microsoft.Extensions.Hosting;
3+
using System.Runtime.CompilerServices;
44
using System.Text;
55
using System.Threading.Channels;
66
using Xunit.Abstractions;

src/UiPath.CoreIpc/Wire/Dtos.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
using System.Diagnostics.CodeAnalysis;
1+
using Newtonsoft.Json;
2+
using System.Diagnostics;
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.Runtime.ExceptionServices;
25
using System.Text;
3-
using Newtonsoft.Json;
46

57
namespace UiPath.Ipc;
68

@@ -66,13 +68,23 @@ public record Error(string Message, string StackTrace, string Type, Error? Inner
6668

6769
public class RemoteException : Exception
6870
{
71+
private const string StackTraceSeparator = "--- End of stack trace from previous location ---";
72+
private string? _remoteStackTrace = null;
73+
6974
public RemoteException(Error error) : base(error.Message, error.InnerError == null ? null : new RemoteException(error.InnerError))
7075
{
7176
Type = error.Type;
72-
StackTrace = error.StackTrace;
77+
if (error.StackTrace is not null)
78+
{
79+
_remoteStackTrace = error.StackTrace.TrimEnd('\r', '\n');
80+
}
7381
}
82+
83+
public override string? StackTrace => _remoteStackTrace is null
84+
? base.StackTrace
85+
: $"{_remoteStackTrace}\r\n{StackTraceSeparator}\r\n{base.StackTrace}";
86+
7487
public string Type { get; }
75-
public override string StackTrace { get; }
7688
public new RemoteException? InnerException => base.InnerException as RemoteException;
7789
public override string ToString()
7890
{

0 commit comments

Comments
 (0)