perf(worker): exact-format timestamp parse and compiled status-field accessors on the event path
This commit is contained in:
@@ -135,6 +135,90 @@ public sealed class MxStatusProxyConverterTests
|
||||
Assert.Equal("Invalid reference", second.DiagnosticText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies every accessor strategy converts the same status values
|
||||
/// identically. PERF-20 reads a public field of a publicly visible type
|
||||
/// through a delegate compiled from an expression tree instead of
|
||||
/// <c>FieldInfo.GetValue</c>; a field type whose conversion to int is not
|
||||
/// a lossless widening (<see cref="FakeWideStatusProxy"/>) and a type the
|
||||
/// compiled delegate may not touch (<see cref="HiddenStatusProxy"/>, not
|
||||
/// visible outside this assembly) keep the reflection read. All four
|
||||
/// doubles below carry the same logical status, so all four messages must
|
||||
/// be equal.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Convert_AcrossAccessorStrategies_ProducesIdenticalMessages()
|
||||
{
|
||||
// Struct, widening field types: the compiled accessor unboxes in place.
|
||||
MxStatusProxy compiledStruct = _converter.Convert(new FakeMxStatusProxy
|
||||
{
|
||||
success = 1,
|
||||
category = 5,
|
||||
detectedBy = 3,
|
||||
detail = 21,
|
||||
});
|
||||
|
||||
// Reference type, widening field types: the compiled accessor casts.
|
||||
MxStatusProxy compiledClass = _converter.Convert(new FakeStatusProxyClass
|
||||
{
|
||||
success = 1,
|
||||
category = 5,
|
||||
detectedBy = 3,
|
||||
detail = 21,
|
||||
});
|
||||
|
||||
// Non-widening field types: reflection + Convert.ToInt32 is kept.
|
||||
MxStatusProxy reflectedWide = _converter.Convert(new FakeWideStatusProxy
|
||||
{
|
||||
success = 1L,
|
||||
category = 5L,
|
||||
detectedBy = 3L,
|
||||
detail = 21L,
|
||||
});
|
||||
|
||||
// Type not visible outside the assembly: reflection is kept.
|
||||
MxStatusProxy reflectedHidden = _converter.Convert(new HiddenStatusProxy
|
||||
{
|
||||
success = 1,
|
||||
category = 5,
|
||||
detectedBy = 3,
|
||||
detail = 21,
|
||||
});
|
||||
|
||||
Assert.Equal(compiledStruct, compiledClass);
|
||||
Assert.Equal(compiledStruct, reflectedWide);
|
||||
Assert.Equal(compiledStruct, reflectedHidden);
|
||||
Assert.Equal(MxStatusCategory.OperationalError, compiledStruct.Category);
|
||||
Assert.Equal(MxStatusSource.RespondingNmx, compiledStruct.DetectedBy);
|
||||
Assert.Equal("Invalid reference", compiledStruct.DiagnosticText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the compiled accessor widens a negative 16-bit field the way
|
||||
/// <c>Convert.ToInt32</c> did — sign-extended, not reinterpreted. The
|
||||
/// interop MXSTATUS_PROXY declares 16-bit fields, so a sign-extension
|
||||
/// mistake here would silently change every failing status the gateway
|
||||
/// reports.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Convert_WithExtremeInt16FieldValues_SignExtendsLikeConvertToInt32()
|
||||
{
|
||||
FakeMxStatusProxy status = new()
|
||||
{
|
||||
success = short.MinValue,
|
||||
category = 2,
|
||||
detectedBy = 0,
|
||||
detail = short.MaxValue,
|
||||
};
|
||||
|
||||
MxStatusProxy converted = _converter.Convert(status);
|
||||
|
||||
Assert.Equal((int)short.MinValue, converted.Success);
|
||||
Assert.Equal((int)short.MaxValue, converted.Detail);
|
||||
Assert.Equal(MxStatusCategory.Warning, converted.Category);
|
||||
Assert.Equal(MxStatusSource.RequestingLmx, converted.DetectedBy);
|
||||
}
|
||||
|
||||
public struct FakeMxStatusProxy
|
||||
{
|
||||
public short success;
|
||||
@@ -146,6 +230,39 @@ public sealed class MxStatusProxyConverterTests
|
||||
public short detail;
|
||||
}
|
||||
|
||||
public struct FakeWideStatusProxy
|
||||
{
|
||||
public long success;
|
||||
|
||||
public long category;
|
||||
|
||||
public long detectedBy;
|
||||
|
||||
public long detail;
|
||||
}
|
||||
|
||||
public sealed class FakeStatusProxyClass
|
||||
{
|
||||
public int success;
|
||||
|
||||
public int category;
|
||||
|
||||
public int detectedBy;
|
||||
|
||||
public int detail;
|
||||
}
|
||||
|
||||
private struct HiddenStatusProxy
|
||||
{
|
||||
public short success;
|
||||
|
||||
public int category;
|
||||
|
||||
public int detectedBy;
|
||||
|
||||
public short detail;
|
||||
}
|
||||
|
||||
private sealed class MissingFields
|
||||
{
|
||||
}
|
||||
|
||||
@@ -238,6 +238,97 @@ public sealed class MxAccessEventMapperTests
|
||||
Assert.False(MxAccessEventMapper.TryParseSourceTimestamp(text, out _));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the exact-format fast path PERF-20 added in front of the
|
||||
/// general parse chain returns exactly what that chain returned. The
|
||||
/// cases cover the captured MXAccess shape (which the fast path is meant
|
||||
/// to catch on a US-shaped host), its zero-padded and second-precision
|
||||
/// siblings, and strings the derived formats cannot match — an ISO
|
||||
/// round-trip form, a long-date form, and a day-first European form —
|
||||
/// which must fall through to the unchanged two-stage chain. The
|
||||
/// expectation is computed with the pre-PERF-20 chain in this test, so it
|
||||
/// holds on any host culture: the fast path is a shortcut, never a
|
||||
/// different answer.
|
||||
/// </summary>
|
||||
/// <param name="text">Timestamp string to parse.</param>
|
||||
[Theory]
|
||||
[InlineData("3/26/2026 1:38:22.907 PM")]
|
||||
[InlineData("3/26/2026 1:38:22 PM")]
|
||||
[InlineData("03/26/2026 01:38:22 PM")]
|
||||
[InlineData("12/31/2026 11:59:59.999 PM")]
|
||||
[InlineData("2026-03-26T13:38:22.9070000")]
|
||||
[InlineData("Thursday, March 26, 2026 1:38:22 PM")]
|
||||
[InlineData("26.03.2026 13:38:22")]
|
||||
[InlineData("not a timestamp")]
|
||||
public void TryParseSourceTimestamp_MatchesPreFastPathChain(string text)
|
||||
{
|
||||
bool expectedParsed = TryParseWithGeneralChainOnly(text, out DateTime expectedUtc);
|
||||
|
||||
bool parsed = MxAccessEventMapper.TryParseSourceTimestamp(text, out DateTime utc);
|
||||
|
||||
Assert.Equal(expectedParsed, parsed);
|
||||
Assert.Equal(expectedUtc, utc);
|
||||
if (parsed)
|
||||
{
|
||||
Assert.Equal(DateTimeKind.Utc, utc.Kind);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a timestamp written in the host culture's own patterns —
|
||||
/// including the millisecond fraction MXAccess appends, which no culture
|
||||
/// publishes in its long-time pattern — parses as local wall-clock time
|
||||
/// and comes back as UTC. This is the string shape the derived exact
|
||||
/// formats are built for, so it exercises the fast path on the worker's
|
||||
/// own host regardless of which culture that host runs.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TryParseSourceTimestamp_WithCultureShapedMillisecondTimestamp_ReturnsLocalWallClockAsUtc()
|
||||
{
|
||||
CultureInfo culture = CultureInfo.CurrentCulture;
|
||||
string longTime = culture.DateTimeFormat.LongTimePattern;
|
||||
int secondsIndex = longTime.IndexOf("ss", StringComparison.Ordinal);
|
||||
|
||||
// Every Windows culture publishes seconds in its long-time pattern; the
|
||||
// guard keeps the test honest rather than green-by-accident if one does not.
|
||||
Assert.True(secondsIndex >= 0, $"Long-time pattern '{longTime}' has no seconds specifier.");
|
||||
|
||||
DateTime localWall = new(2026, 3, 26, 13, 38, 22, 907, DateTimeKind.Unspecified);
|
||||
string text = localWall.ToString(
|
||||
culture.DateTimeFormat.ShortDatePattern + " " + longTime.Insert(secondsIndex + 2, ".fff"),
|
||||
culture);
|
||||
|
||||
Assert.True(MxAccessEventMapper.TryParseSourceTimestamp(text, out DateTime utc));
|
||||
Assert.Equal(DateTimeKind.Utc, utc.Kind);
|
||||
Assert.Equal(DateTime.SpecifyKind(localWall, DateTimeKind.Local).ToUniversalTime(), utc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The parse chain exactly as it stood before PERF-20 added the
|
||||
/// exact-format stage, used as the parity oracle above.
|
||||
/// </summary>
|
||||
/// <param name="text">Timestamp string to parse.</param>
|
||||
/// <param name="utc">The parsed UTC timestamp on success.</param>
|
||||
/// <returns><see langword="true"/> when the string parsed successfully.</returns>
|
||||
private static bool TryParseWithGeneralChainOnly(string? text, out DateTime utc)
|
||||
{
|
||||
utc = default;
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const DateTimeStyles styles = DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal;
|
||||
if (DateTime.TryParse(text, CultureInfo.CurrentCulture, styles, out DateTime parsed)
|
||||
|| DateTime.TryParse(text, CultureInfo.InvariantCulture, styles, out parsed))
|
||||
{
|
||||
utc = DateTime.SpecifyKind(parsed, DateTimeKind.Utc);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private sealed class FakeStatus
|
||||
{
|
||||
public int success;
|
||||
|
||||
Reference in New Issue
Block a user