182 lines
5.8 KiB
C#
182 lines
5.8 KiB
C#
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; }
|
|
}
|
|
}
|