270 lines
8.8 KiB
C#
270 lines
8.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
using ZB.MOM.WW.MxGateway.Worker.Conversion;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Worker.Tests.Conversion;
|
|
|
|
public sealed class MxStatusProxyConverterTests
|
|
{
|
|
private readonly MxStatusProxyConverter _converter = new();
|
|
|
|
/// <summary>Verifies that status struct fields are preserved during conversion.</summary>
|
|
[Fact]
|
|
public void Convert_WithStatusStruct_PreservesStatusFields()
|
|
{
|
|
FakeMxStatusProxy status = new()
|
|
{
|
|
success = 1,
|
|
category = 5,
|
|
detectedBy = 3,
|
|
detail = 21,
|
|
};
|
|
|
|
MxStatusProxy converted = _converter.Convert(status);
|
|
|
|
Assert.Equal(1, converted.Success);
|
|
Assert.Equal(MxStatusCategory.OperationalError, converted.Category);
|
|
Assert.Equal(MxStatusSource.RespondingNmx, converted.DetectedBy);
|
|
Assert.Equal(21, converted.Detail);
|
|
Assert.Equal(5, converted.RawCategory);
|
|
Assert.Equal(3, converted.RawDetectedBy);
|
|
Assert.Equal("Invalid reference", converted.DiagnosticText);
|
|
}
|
|
|
|
/// <summary>Verifies that status arrays are converted without collapsing duplicate entries.</summary>
|
|
[Fact]
|
|
public void ConvertMany_WithStatusArray_DoesNotCollapseEntries()
|
|
{
|
|
FakeMxStatusProxy[] statuses =
|
|
[
|
|
new()
|
|
{
|
|
success = 1,
|
|
category = 0,
|
|
detectedBy = 0,
|
|
detail = 0,
|
|
},
|
|
new()
|
|
{
|
|
success = 0,
|
|
category = 6,
|
|
detectedBy = 5,
|
|
detail = 33,
|
|
},
|
|
];
|
|
|
|
IReadOnlyList<MxStatusProxy> converted = _converter.ConvertMany(statuses);
|
|
|
|
Assert.Equal(2, converted.Count);
|
|
Assert.Equal(MxStatusCategory.Ok, converted[0].Category);
|
|
Assert.Equal(MxStatusCategory.SecurityError, converted[1].Category);
|
|
Assert.Equal(MxStatusSource.RespondingAutomationObject, converted[1].DetectedBy);
|
|
Assert.Equal("Write access denied", converted[1].DiagnosticText);
|
|
}
|
|
|
|
/// <summary>Verifies that unknown category and source values preserve raw field values.</summary>
|
|
[Fact]
|
|
public void Convert_WithUnknownCategoryAndSource_PreservesRawFields()
|
|
{
|
|
FakeMxStatusProxy status = new()
|
|
{
|
|
success = -1,
|
|
category = 99,
|
|
detectedBy = 42,
|
|
detail = 1234,
|
|
};
|
|
|
|
MxStatusProxy converted = _converter.Convert(status);
|
|
|
|
Assert.Equal(-1, converted.Success);
|
|
Assert.Equal(MxStatusCategory.Unknown, converted.Category);
|
|
Assert.Equal(MxStatusSource.Unknown, converted.DetectedBy);
|
|
Assert.Equal(99, converted.RawCategory);
|
|
Assert.Equal(42, converted.RawDetectedBy);
|
|
Assert.Equal(1234, converted.Detail);
|
|
Assert.Equal(string.Empty, converted.DiagnosticText);
|
|
}
|
|
|
|
/// <summary>Verifies that completion-only status bytes are preserved as hex metadata.</summary>
|
|
[Fact]
|
|
public void PreserveCompletionOnlyStatusBytes_ReturnsRawHexMetadata()
|
|
{
|
|
string rawStatus = _converter.PreserveCompletionOnlyStatusBytes(
|
|
[0x00, 0x00, 0x50, 0x80, 0x00]);
|
|
|
|
Assert.Equal("completion_only_status_hex=0000508000", rawStatus);
|
|
}
|
|
|
|
/// <summary>Verifies that missing status fields throw a conversion exception.</summary>
|
|
[Fact]
|
|
public void Convert_WithMissingStatusField_ThrowsConversionException()
|
|
{
|
|
MxStatusConversionException exception =
|
|
Assert.Throws<MxStatusConversionException>(() => _converter.Convert(new MissingFields()));
|
|
|
|
Assert.Contains("success", exception.Message);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that repeated conversions of the same status type produce
|
|
/// identical results. WRK-06 caches the resolved FieldInfo objects per
|
|
/// type after the first conversion; the cached path must yield the same
|
|
/// message the uncached first call produced.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Convert_RepeatedForSameType_ProducesIdenticalResults()
|
|
{
|
|
FakeMxStatusProxy status = new()
|
|
{
|
|
success = 0,
|
|
category = 3,
|
|
detectedBy = 1,
|
|
detail = 21,
|
|
};
|
|
|
|
// First call populates the per-type FieldInfo cache; the second reuses it.
|
|
MxStatusProxy first = _converter.Convert(status);
|
|
MxStatusProxy second = _converter.Convert(status);
|
|
|
|
Assert.Equal(first, second);
|
|
Assert.Equal(MxStatusCategory.CommunicationError, second.Category);
|
|
Assert.Equal(MxStatusSource.RespondingLmx, second.DetectedBy);
|
|
Assert.Equal(0, second.Success);
|
|
Assert.Equal(21, second.Detail);
|
|
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;
|
|
|
|
public int category;
|
|
|
|
public int detectedBy;
|
|
|
|
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
|
|
{
|
|
}
|
|
}
|