Skip to content
Merged
Show file tree
Hide file tree
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
9 changes: 5 additions & 4 deletions TUnit.Pipeline/Modules/RunRpcTestsModule.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Runtime.InteropServices;
using ModularPipelines.Attributes;
using ModularPipelines.Attributes;
using ModularPipelines.Context;
using ModularPipelines.DotNet.Options;
using ModularPipelines.Extensions;
Expand All @@ -12,8 +11,10 @@ namespace TUnit.Pipeline.Modules;
[NotInParallel("NetworkTests")]
public class RunRpcTestsModule : TestBaseModule
{
// Skipped globally — see https://github.com/thomhurst/TUnit/issues/5540
protected override IEnumerable<string> TestableFrameworks => [];
protected override IEnumerable<string> TestableFrameworks =>
[
"net10.0"
];

protected override Task<(DotNetRunOptions Options, CommandExecutionOptions? ExecutionOptions)> GetTestOptions(IModuleContext context, string framework, CancellationToken cancellationToken)
{
Expand Down
24 changes: 0 additions & 24 deletions TUnit.RpcTests/Clients/IProcessHandle.cs

This file was deleted.

37 changes: 0 additions & 37 deletions TUnit.RpcTests/Clients/ProcessHandle.cs

This file was deleted.

119 changes: 32 additions & 87 deletions TUnit.RpcTests/Clients/TestingPlatformClient.cs
Original file line number Diff line number Diff line change
@@ -1,38 +1,29 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Collections.Concurrent;
using System.Net.Sockets;
using System.Text;
using Microsoft.Extensions.Logging;
using StreamJsonRpc;
using TUnit.RpcTests.Models;
// ReSharper disable All

namespace TUnit.RpcTests.Clients;

public sealed class TestingPlatformClient : IDisposable
{
private readonly TcpClient _tcpClient = new();
private readonly IProcessHandle _processHandler;
private readonly TcpClient _tcpClient;
private readonly TargetHandler _targetHandler = new();
private readonly StringBuilder _disconnectionReason = new();

public TestingPlatformClient(JsonRpc jsonRpc, TcpClient tcpClient, IProcessHandle processHandler, bool enableDiagnostic = false)
public TestingPlatformClient(JsonRpc jsonRpc, TcpClient tcpClient)
{
JsonRpcClient = jsonRpc;
_tcpClient = tcpClient;
_processHandler = processHandler;
JsonRpcClient.AddLocalRpcTarget(
_targetHandler,
new JsonRpcTargetOptions
{
MethodNameTransform = CommonMethodNameTransforms.CamelCase,
});

if (enableDiagnostic)
{
JsonRpcClient.TraceSource.Switch.Level = SourceLevels.All;
JsonRpcClient.TraceSource.Listeners.Add(new ConsoleRpcListener());
}

JsonRpcClient.Disconnected += JsonRpcClient_Disconnected;
JsonRpcClient.StartListening();
}
Expand All @@ -45,53 +36,18 @@ private void JsonRpcClient_Disconnected(object? sender, JsonRpcDisconnectedEvent
_disconnectionReason.AppendLine(e.Exception?.ToString());
}

public int ExitCode => _processHandler.ExitCode;

public async Task<int> WaitServerProcessExitAsync()
{
await _processHandler.WaitForExitAsync();
return _processHandler.ExitCode;
}

public JsonRpc JsonRpcClient { get; }

private async Task CheckedInvokeAsync(Func<Task> func)
{
try
{
await func();
}
catch (Exception ex)
{
if (_disconnectionReason.Length > 0)
{
throw new InvalidOperationException($"{ex.Message}\n{_disconnectionReason}", ex);
}

throw;
}
}

private async Task<T> CheckedInvokeAsync<T>(Func<Task<T>> func, bool @checked = true)
private async Task<T> CheckedInvokeAsync<T>(Func<Task<T>> func)
{
try
{
return await func();
}
catch (Exception ex)
catch (Exception ex) when (_disconnectionReason.Length > 0)
{
if (@checked)
{
if (_disconnectionReason.Length > 0)
{
throw new InvalidOperationException($"{ex.Message}\n{_disconnectionReason}", ex);
}

throw;
}
throw new InvalidOperationException($"{ex.Message}\n{_disconnectionReason}", ex);
}

return default!;
}

public void RegisterLogListener(LogsCollector listener)
Expand All @@ -100,47 +56,41 @@ public void RegisterLogListener(LogsCollector listener)
public void RegisterTelemetryListener(TelemetryCollector listener)
=> _targetHandler.RegisterTelemetryListener(listener);

public async Task<InitializeResponse> InitializeAsync()
{
using CancellationTokenSource cancellationTokenSource = new(TimeSpan.FromMinutes(3));
return await CheckedInvokeAsync(async () => await JsonRpcClient.InvokeWithParameterObjectAsync<InitializeResponse>(
public async Task<InitializeResponse> InitializeAsync(CancellationToken cancellationToken = default)
=> await CheckedInvokeAsync(async () => await JsonRpcClient.InvokeWithParameterObjectAsync<InitializeResponse>(
"initialize",
new InitializeRequest(Environment.ProcessId, new ClientInfo("test-client"),
new ClientCapabilities(new ClientTestingCapabilities(DebuggerProvider: false))), cancellationToken: cancellationTokenSource.Token));
}
new ClientCapabilities(new ClientTestingCapabilities(DebuggerProvider: false))),
cancellationToken: cancellationToken));

public async Task ExitAsync(bool gracefully = true)
public async Task ExitAsync()
{
if (gracefully)
try
{
using CancellationTokenSource cancellationTokenSource = new(TimeSpan.FromMinutes(3));
await CheckedInvokeAsync(async () => await JsonRpcClient.NotifyWithParameterObjectAsync("exit", new object()));
await JsonRpcClient.NotifyWithParameterObjectAsync("exit", new object());
}
else
catch
{
_tcpClient.Dispose();
// Best effort — connection may already be gone
}
}

public async Task<ResponseListener> DiscoverTestsAsync(Guid requestId, Func<TestNodeUpdate[], Task> action, bool @checked = true)
=> await CheckedInvokeAsync(
async () =>
{
using CancellationTokenSource cancellationTokenSource = new(TimeSpan.FromMinutes(3));
var discoveryListener = new TestNodeUpdatesResponseListener(requestId, action);
_targetHandler.RegisterResponseListener(discoveryListener);
await JsonRpcClient.InvokeWithParameterObjectAsync("testing/discoverTests", new DiscoveryRequest(RunId: requestId), cancellationToken: cancellationTokenSource.Token);
return discoveryListener;
}, @checked);

public async Task<ResponseListener> RunTestsAsync(Guid requestId, Func<TestNodeUpdate[], Task> action, TestNode[]? testNodes = null)
public async Task<ResponseListener> DiscoverTestsAsync(Guid requestId, Func<TestNodeUpdate[], Task> action, CancellationToken cancellationToken = default)
=> await CheckedInvokeAsync(async () =>
{
var discoveryListener = new TestNodeUpdatesResponseListener(requestId, action);
_targetHandler.RegisterResponseListener(discoveryListener);
await JsonRpcClient.InvokeWithParameterObjectAsync("testing/discoverTests", new DiscoveryRequest(RunId: requestId), cancellationToken: cancellationToken);
return (ResponseListener)discoveryListener;
});

public async Task<ResponseListener> RunTestsAsync(Guid requestId, Func<TestNodeUpdate[], Task> action, TestNode[]? testNodes = null, CancellationToken cancellationToken = default)
=> await CheckedInvokeAsync(async () =>
{
using CancellationTokenSource cancellationTokenSource = new(TimeSpan.FromMinutes(3));
var runListener = new TestNodeUpdatesResponseListener(requestId, action);
_targetHandler.RegisterResponseListener(runListener);
await JsonRpcClient.InvokeWithParameterObjectAsync("testing/runTests", new RunTestsRequest(RunId: requestId, TestCases: testNodes), cancellationToken: cancellationTokenSource.Token);
return runListener;
await JsonRpcClient.InvokeWithParameterObjectAsync("testing/runTests", new RunTestsRequest(RunId: requestId, Tests: testNodes), cancellationToken: cancellationToken);
return (ResponseListener)runListener;
});

public void Dispose()
Expand All @@ -153,14 +103,9 @@ public record Log(LogLevel LogLevel, string Message);

private sealed class TargetHandler
{
private readonly ConcurrentDictionary<Guid, ResponseListener> _listeners
= new();

private readonly ConcurrentBag<LogsCollector> _logListeners
= new();

private readonly ConcurrentBag<TelemetryCollector> _telemetryPayloads
= new();
private readonly ConcurrentDictionary<Guid, ResponseListener> _listeners = new();
private readonly ConcurrentBag<LogsCollector> _logListeners = [];
private readonly ConcurrentBag<TelemetryCollector> _telemetryPayloads = [];

public void RegisterTelemetryListener(TelemetryCollector listener)
=> _telemetryPayloads.Add(listener);
Expand Down Expand Up @@ -233,5 +178,5 @@ public sealed class TestNodeUpdatesResponseListener(Guid requestId, Func<TestNod
: ResponseListener(requestId)
{
public override async Task OnMessageReceiveAsync(object message)
=> await action((TestNodeUpdate[]) message);
=> await action((TestNodeUpdate[])message);
}
10 changes: 0 additions & 10 deletions TUnit.RpcTests/Models/ConsoleRpcListener.cs

This file was deleted.

12 changes: 0 additions & 12 deletions TUnit.RpcTests/Models/LogLevel.cs

This file was deleted.

7 changes: 0 additions & 7 deletions TUnit.RpcTests/Models/RunRequest.cs

This file was deleted.

4 changes: 2 additions & 2 deletions TUnit.RpcTests/Models/RunTestsRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ namespace TUnit.RpcTests.Models;
public sealed record RunTestsRequest(
[property:JsonPropertyName("runId")]
Guid RunId,
[property:JsonPropertyName("testCases")]
TestNode[]? TestCases = null);
[property:JsonPropertyName("tests")]
TestNode[]? Tests = null);
28 changes: 18 additions & 10 deletions TUnit.RpcTests/Models/TestNode.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace TUnit.RpcTests.Models;

public sealed record TestNode
(
[property: JsonPropertyName("uid")]
string Uid,
{
[JsonPropertyName("uid")]
public required string Uid { get; init; }

[property: JsonPropertyName("display-name")]
string DisplayName,
[JsonPropertyName("display-name")]
public required string DisplayName { get; init; }

[property: JsonPropertyName("node-type")]
string NodeType,
[JsonPropertyName("node-type")]
public string? NodeType { get; init; }

[property: JsonPropertyName("execution-state")]
string ExecutionState);
[JsonPropertyName("execution-state")]
public string? ExecutionState { get; init; }

// Captures every other server-sent field (traits, location.*, error.*, etc.)
// so that when we round-trip the node back in testCases, the server sees
// the full payload it originally produced.
[JsonExtensionData]
public Dictionary<string, JsonElement>? ExtensionData { get; init; }
}
4 changes: 2 additions & 2 deletions TUnit.RpcTests/Models/TestNodeUpdate.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;

namespace TUnit.RpcTests.Models;

Expand All @@ -8,4 +8,4 @@ public sealed record TestNodeUpdate
TestNode Node,

[property: JsonPropertyName("parent")]
string ParentUid);
string? ParentUid);
7 changes: 6 additions & 1 deletion TUnit.RpcTests/RpcJsonSerializerOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace TUnit.RpcTests;

Expand All @@ -12,6 +13,10 @@ static RpcJsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, allowIntegerValues: true),
},
};
}
}
Loading
Loading