feat(tooling): add standalone rendezvous test client (#25)
quality-gate / quality (push) Failing after 1m3s

This commit is contained in:
KyuubiYoru
2026-07-16 11:05:56 +02:00
parent 94aba8a3bb
commit 7e3be2cad1
16 changed files with 2721 additions and 12 deletions
@@ -0,0 +1,181 @@
using System.Globalization;
using System.Text;
using System.Text.Json;
namespace FinalFactory.Rendezvous.TestClient;
internal sealed class TestClientOutput(TextWriter standardOutput, TextWriter standardError, bool json)
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = false,
};
private readonly object _gate = new();
private readonly TextWriter _standardOutput = standardOutput ?? throw new ArgumentNullException(nameof(standardOutput));
private readonly TextWriter _standardError = standardError ?? throw new ArgumentNullException(nameof(standardError));
private readonly bool _json = json;
internal void Write(
string eventName,
string status,
string? phase = null,
string? listingId = null,
string? displayName = null,
string? outcome = null,
string? endpointType = null,
int? count = null,
long? elapsedMilliseconds = null,
string? message = null) => WriteCore(
_standardOutput,
new TestClientEvent
{
Event = SafeToken(eventName) ?? string.Empty,
Status = SafeToken(status) ?? string.Empty,
Phase = SafeToken(phase),
ListingId = SafeToken(listingId),
DisplayName = SafeText(displayName),
Outcome = SafeToken(outcome),
EndpointType = SafeToken(endpointType),
Count = count,
ElapsedMilliseconds = elapsedMilliseconds,
Message = SafeText(message),
});
internal void WriteError(
string eventName,
string status,
string message,
string? phase = null,
string? outcome = null) => WriteCore(
_standardError,
new TestClientEvent
{
Event = SafeToken(eventName) ?? string.Empty,
Status = SafeToken(status) ?? string.Empty,
Phase = SafeToken(phase),
Outcome = SafeToken(outcome),
Message = SafeText(message),
});
internal void WritePrompt(string prompt)
{
if (_json)
{
return;
}
lock (_gate)
{
_standardOutput.Write(SafeText(prompt));
_standardOutput.Flush();
}
}
private void WriteCore(TextWriter writer, TestClientEvent item)
{
string line = _json
? JsonSerializer.Serialize(item, JsonOptions)
: HumanLine(item);
lock (_gate)
{
writer.WriteLine(line);
writer.Flush();
}
}
private static string HumanLine(TestClientEvent item)
{
StringBuilder line = new();
line.Append('[').Append(item.Status).Append("] ").Append(item.Event);
Append(line, "phase", item.Phase);
Append(line, "listing", item.ListingId);
Append(line, "name", item.DisplayName, quote: true);
Append(line, "outcome", item.Outcome);
Append(line, "endpoint", item.EndpointType);
if (item.Count.HasValue)
{
Append(line, "count", item.Count.Value.ToString(System.Globalization.CultureInfo.InvariantCulture));
}
if (item.ElapsedMilliseconds.HasValue)
{
Append(
line,
"elapsedMs",
item.ElapsedMilliseconds.Value.ToString(System.Globalization.CultureInfo.InvariantCulture));
}
Append(line, "message", item.Message, quote: true);
return line.ToString();
}
private static void Append(
StringBuilder builder,
string name,
string? value,
bool quote = false)
{
if (!string.IsNullOrEmpty(value))
{
builder.Append(' ').Append(name).Append('=');
if (quote)
{
builder.Append('"').Append(value.Replace("\\", "\\\\", StringComparison.Ordinal)
.Replace("\"", "\\\"", StringComparison.Ordinal)).Append('"');
}
else
{
builder.Append(value);
}
}
}
private static string? SafeToken(string? value)
{
if (value is null)
{
return null;
}
return new string(value
.Take(96)
.Select(static character => char.IsAsciiLetterOrDigit(character)
|| character is '.' or '-' or '_' or ':'
? char.ToLowerInvariant(character)
: '_')
.ToArray());
}
private static string? SafeText(string? value)
{
if (value is null)
{
return null;
}
return new string(value
.Take(160)
.Select(static character => IsUnsafeHumanCharacter(character) ? '?' : character)
.ToArray());
}
private static bool IsUnsafeHumanCharacter(char character) =>
char.GetUnicodeCategory(character) is
UnicodeCategory.Control
or UnicodeCategory.Format
or UnicodeCategory.LineSeparator
or UnicodeCategory.ParagraphSeparator
or UnicodeCategory.Surrogate
or UnicodeCategory.PrivateUse;
private sealed class TestClientEvent
{
public int Version { get; init; } = 1;
public string Event { get; init; } = string.Empty;
public string Status { get; init; } = string.Empty;
public string? Phase { get; init; }
public string? ListingId { get; init; }
public string? DisplayName { get; init; }
public string? Outcome { get; init; }
public string? EndpointType { get; init; }
public int? Count { get; init; }
public long? ElapsedMilliseconds { get; init; }
public string? Message { get; init; }
}
}