feat(tooling): add standalone rendezvous test client (#25)
quality-gate / quality (push) Failing after 1m3s
quality-gate / quality (push) Failing after 1m3s
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user