fe2a6db786
rust / build / test / clippy / fmt (push) Has been cancelled
Layout:
- src/ .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
MxAsbClient, probes, tests, harnesses. Executable spec.
- design/ Architectural plan for the Rust port (M0–M6), error
model, protocol invariants, risks (R1–R16), adversarial
review log (review.md).
- rust/ Rust workspace. M0 skeleton + M1 codec parity.
mxaccess-codec: 215 unit tests + 2 cross-implementation
parity tests (byte-identical against .NET reference).
Other crates are M0 stubs awaiting M2+.
- captures/ Frida + netsh + pcap evidence per CLAUDE.md
("captures are evidence, not throwaway logs").
- analysis/ Decompiled C# (frida/proxy/decompiled-*),
Ghidra exports for native DLLs (`exports/` only —
working state at `projects/` and AVEVA's input
binaries at `input/` are gitignored).
- docs/ Reverse-engineering reference docs.
- tools/ Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/ Rust CI: fmt + build + test + clippy on Windows.
- LICENSE MIT (Joseph Doherty, 2026).
Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly
Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
780 lines
35 KiB
C#
780 lines
35 KiB
C#
using MxAsbClient;
|
|
using System.ServiceModel;
|
|
|
|
RunVariantFactoryTests();
|
|
RunMonitoredItemValueTests();
|
|
RunPublishMapperTests();
|
|
RunCompatibilitySurfaceTests();
|
|
RunConnectionOptionsTests();
|
|
RunWriteOptionsTests();
|
|
RunCollectionArgumentGuardTests();
|
|
RunResultMapperTests();
|
|
RunWriteCompletionOptionsTests();
|
|
RunSubscriptionOptionsTests();
|
|
RunCleanupOptionsTests();
|
|
RunCommunicationCleanupTests();
|
|
RunReconnectSurfaceTests();
|
|
|
|
Console.WriteLine("MxAsbClient ASB contract tests passed.");
|
|
|
|
static void RunVariantFactoryTests()
|
|
{
|
|
AssertVariant("bool", AsbDataType.TypeBool, 1, AsbVariantFactory.FromBoolean(true), true);
|
|
AssertVariant("int", AsbDataType.TypeInt32, 4, AsbVariantFactory.FromInt32(123), 123);
|
|
AssertVariant("float", AsbDataType.TypeFloat, 4, AsbVariantFactory.FromSingle(1.25f), 1.25f);
|
|
AssertVariant("double", AsbDataType.TypeDouble, 8, AsbVariantFactory.FromDouble(1.125d), 1.125d);
|
|
AssertVariant("string", AsbDataType.TypeString, 10, AsbVariantFactory.FromString("Alpha"), "Alpha");
|
|
|
|
DateTime timestamp = new(2026, 4, 26, 12, 34, 56, DateTimeKind.Utc);
|
|
AssertVariant("datetime", AsbDataType.TypeDateTime, 8, AsbVariantFactory.FromDateTime(timestamp), timestamp);
|
|
|
|
TimeSpan duration = TimeSpan.FromSeconds(12.5);
|
|
AssertVariant("duration", AsbDataType.TypeDuration, 8, AsbVariantFactory.FromDuration(duration), duration);
|
|
|
|
AssertVariant("int[]", AsbDataType.TypeInt32Array, 12, AsbVariantFactory.FromInt32Array([1, 2, 3]), new[] { 1, 2, 3 });
|
|
AssertVariant("bool[]", AsbDataType.TypeBoolArray, 3, AsbVariantFactory.FromBooleanArray([true, false, true]), new[] { true, false, true });
|
|
AssertVariant("float[]", AsbDataType.TypeFloatArray, 8, AsbVariantFactory.FromSingleArray([1.25f, 2.5f]), new[] { 1.25f, 2.5f });
|
|
AssertVariant("double[]", AsbDataType.TypeDoubleArray, 16, AsbVariantFactory.FromDoubleArray([1.125d, 2.25d]), new[] { 1.125d, 2.25d });
|
|
AssertVariant("string[]", AsbDataType.TypeStringArray, 18, AsbVariantFactory.FromStringArray(["A", "", "BC"]), new[] { "A", "", "BC" });
|
|
AssertVariant("datetime[]", AsbDataType.TypeDateTimeArray, 16, AsbVariantFactory.FromDateTimeArray([timestamp, timestamp.AddMinutes(1)]), new[] { timestamp, timestamp.AddMinutes(1) });
|
|
AssertVariant("duration[]", AsbDataType.TypeDurationArray, 16, AsbVariantFactory.FromDurationArray([duration, duration.Add(TimeSpan.FromSeconds(1))]), new[] { duration, duration.Add(TimeSpan.FromSeconds(1)) });
|
|
}
|
|
|
|
static void AssertVariant<T>(string name, AsbDataType type, int length, Variant variant, T expected)
|
|
{
|
|
AssertEqual(name + " type", (ushort)type, variant.Type);
|
|
AssertEqual(name + " length", length, variant.Length);
|
|
AssertEqual(name + " payload length", length, variant.Payload?.Length ?? 0);
|
|
|
|
object? decoded = MxAsbDataClient.DecodeVariant(variant);
|
|
AssertValue(name + " decoded", expected, decoded);
|
|
}
|
|
|
|
static void AssertValue<T>(string name, T expected, object? actual)
|
|
{
|
|
if (expected is Array expectedArray)
|
|
{
|
|
if (actual is not Array actualArray || expectedArray.Length != actualArray.Length)
|
|
{
|
|
throw new InvalidOperationException($"{name}: array mismatch.");
|
|
}
|
|
|
|
for (int i = 0; i < expectedArray.Length; i++)
|
|
{
|
|
object? expectedItem = expectedArray.GetValue(i);
|
|
object? actualItem = actualArray.GetValue(i);
|
|
if (!Equals(expectedItem, actualItem))
|
|
{
|
|
throw new InvalidOperationException($"{name}[{i}]: expected {expectedItem}, got {actualItem}.");
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (!Equals(expected, actual))
|
|
{
|
|
throw new InvalidOperationException($"{name}: expected {expected}, got {actual}.");
|
|
}
|
|
}
|
|
|
|
static void AssertEqual<T>(string name, T expected, T actual)
|
|
{
|
|
if (!Equals(expected, actual))
|
|
{
|
|
throw new InvalidOperationException($"{name}: expected {expected}, got {actual}.");
|
|
}
|
|
}
|
|
|
|
static void AssertNotNull(string name, object? value)
|
|
{
|
|
if (value is null)
|
|
{
|
|
throw new InvalidOperationException($"{name}: expected non-null.");
|
|
}
|
|
}
|
|
|
|
static void AssertThrows<TException>(string name, Action action)
|
|
where TException : Exception
|
|
{
|
|
try
|
|
{
|
|
action();
|
|
}
|
|
catch (TException)
|
|
{
|
|
return;
|
|
}
|
|
|
|
throw new InvalidOperationException($"{name}: expected {typeof(TException).Name}.");
|
|
}
|
|
|
|
static void RunMonitoredItemValueTests()
|
|
{
|
|
MonitoredItemValue original = new()
|
|
{
|
|
Item = new ItemIdentity
|
|
{
|
|
Type = (ushort)ItemIdentityType.Name,
|
|
ReferenceType = (ushort)ItemReferenceType.Absolute,
|
|
Name = "TestChildObject.TestInt",
|
|
ContextName = string.Empty,
|
|
Id = 123,
|
|
IdSpecified = true,
|
|
},
|
|
Value = new RuntimeValue
|
|
{
|
|
Timestamp = new DateTime(2026, 4, 26, 16, 0, 0, DateTimeKind.Utc),
|
|
TimestampSpecified = true,
|
|
Value = AsbVariantFactory.FromInt32(412),
|
|
Status = new AsbStatus { Count = 0, Payload = [] },
|
|
},
|
|
UserData = AsbVariantFactory.Empty,
|
|
};
|
|
|
|
using MemoryStream stream = new();
|
|
using BinaryWriter writer = new(stream, System.Text.Encoding.UTF8, leaveOpen: true);
|
|
original.WriteToStream(writer);
|
|
writer.Flush();
|
|
stream.Position = 0;
|
|
|
|
using BinaryReader reader = new(stream, System.Text.Encoding.UTF8, leaveOpen: true);
|
|
MonitoredItemValue decoded = new();
|
|
decoded.InitializeFromStream(reader);
|
|
|
|
AssertEqual("monitored item name", original.Item.Name, decoded.Item.Name);
|
|
AssertEqual("monitored item id", original.Item.Id, decoded.Item.Id);
|
|
AssertEqual("monitored value timestamp", original.Value.Timestamp, decoded.Value.Timestamp);
|
|
AssertValue("monitored value decoded", 412, MxAsbDataClient.DecodeVariant(decoded.Value.Value));
|
|
}
|
|
|
|
static void RunPublishMapperTests()
|
|
{
|
|
AsbStatus status = new()
|
|
{
|
|
Count = 7,
|
|
Payload = [0x85, 0x06, 0x10, 0x00, 0x07, 0xC0, 0x00],
|
|
};
|
|
IReadOnlyList<AsbStatusElement> elements = AsbPublishMapper.DecodeStatus(status);
|
|
AssertEqual("status count", 3, elements.Count);
|
|
AssertEqual("status category type", AsbStatusElementType.MxStatusCategory, elements[0].Type);
|
|
AssertEqual("status category value", (ushort)0, elements[0].Value);
|
|
AssertEqual("status detail type", AsbStatusElementType.MxStatusDetail, elements[1].Type);
|
|
AssertEqual("status detail value", (ushort)16, elements[1].Value);
|
|
AssertEqual("status quality type", AsbStatusElementType.MxQuality, elements[2].Type);
|
|
AssertEqual("status quality value", (ushort)0x00C0, elements[2].Value);
|
|
|
|
MonitoredItemValue item = new()
|
|
{
|
|
Item = new ItemIdentity
|
|
{
|
|
Type = (ushort)ItemIdentityType.Id,
|
|
ReferenceType = (ushort)ItemReferenceType.Absolute,
|
|
Id = 456,
|
|
IdSpecified = true,
|
|
},
|
|
Value = new RuntimeValue
|
|
{
|
|
Timestamp = new DateTime(2026, 4, 26, 16, 5, 0, DateTimeKind.Utc),
|
|
TimestampSpecified = true,
|
|
Value = AsbVariantFactory.FromString("mapped"),
|
|
Status = status,
|
|
},
|
|
UserData = AsbVariantFactory.Empty,
|
|
};
|
|
AsbPublishedValue mapped = AsbPublishMapper.ToPublishedValue(item, new Dictionary<ulong, string> { [456] = "Test.Tag" });
|
|
AssertEqual("published item name", "Test.Tag", mapped.ItemName);
|
|
AssertEqual("published quality", (ushort?)0x00C0, mapped.Quality);
|
|
AssertEqual("published preview", "mapped", mapped.Preview);
|
|
AssertEqual("published value", "mapped", mapped.Value);
|
|
AssertEqual("published status summary quality", AsbStatusQuality.Good, mapped.StatusSummary.Quality);
|
|
AssertEqual("published status summary detail", AsbMxStatusDetail.RequestTimedOut, mapped.StatusSummary.Detail);
|
|
|
|
AsbPublishResult result = new(
|
|
new PublishResponse { Result = new ArchestrAResult { ErrorCode = 32, Success = false } },
|
|
[mapped]);
|
|
AssertEqual("publish result summary", AsbErrorCode.PublishComplete, result.Result.Error);
|
|
AssertEqual("publish result success-like", true, result.Result.IsSuccessLike);
|
|
AssertEqual("publish result has values", true, result.HasValues);
|
|
|
|
AsbPublishResult emptyResult = new(
|
|
new PublishResponse { Result = new ArchestrAResult { ErrorCode = 0, Success = true } },
|
|
[]);
|
|
AssertEqual("empty publish result summary", AsbErrorCode.Success, emptyResult.Result.Error);
|
|
AssertEqual("empty publish result success-like", true, emptyResult.Result.IsSuccessLike);
|
|
AssertEqual("empty publish result has values", false, emptyResult.HasValues);
|
|
}
|
|
|
|
static void RunCompatibilitySurfaceTests()
|
|
{
|
|
Type serverType = typeof(MxAsbCompatibilityServer);
|
|
AssertNotNull("asb compat data-change event", serverType.GetEvent(nameof(MxAsbCompatibilityServer.DataChanged)));
|
|
AssertNotNull("asb compat register", serverType.GetMethod(
|
|
nameof(MxAsbCompatibilityServer.Register),
|
|
[typeof(string), typeof(string), typeof(Action<string>), typeof(bool)]));
|
|
AssertNotNull("asb compat register options", serverType.GetMethod(nameof(MxAsbCompatibilityServer.Register), [typeof(AsbConnectionOptions)]));
|
|
AssertNotNull("asb compat unregister", serverType.GetMethod(nameof(MxAsbCompatibilityServer.Unregister)));
|
|
AssertNotNull("asb compat add item", serverType.GetMethod(nameof(MxAsbCompatibilityServer.AddItem)));
|
|
AssertNotNull("asb compat remove item", serverType.GetMethod(nameof(MxAsbCompatibilityServer.RemoveItem)));
|
|
AssertNotNull("asb compat advise", serverType.GetMethod(
|
|
nameof(MxAsbCompatibilityServer.Advise),
|
|
[typeof(int), typeof(int), typeof(ulong), typeof(long)]));
|
|
AssertNotNull("asb compat advise options", serverType.GetMethod(
|
|
nameof(MxAsbCompatibilityServer.Advise),
|
|
[typeof(int), typeof(int), typeof(AsbSubscriptionOptions), typeof(AsbMonitoredItemOptions)]));
|
|
AssertNotNull("asb compat poll", serverType.GetMethod(nameof(MxAsbCompatibilityServer.Poll)));
|
|
AssertNotNull("asb buffered add monitored", typeof(MxAsbDataClient).GetMethod(
|
|
nameof(MxAsbDataClient.AddMonitoredItems),
|
|
[typeof(long), typeof(IEnumerable<string>), typeof(ulong), typeof(bool), typeof(bool)]));
|
|
AssertNotNull("asb options add monitored", typeof(MxAsbDataClient).GetMethod(
|
|
nameof(MxAsbDataClient.AddMonitoredItems),
|
|
[typeof(long), typeof(IEnumerable<string>), typeof(AsbMonitoredItemOptions)]));
|
|
|
|
MxAsbDataChangeEvent dataChange = new(
|
|
ServerHandle: 1,
|
|
ItemHandle: 2,
|
|
Value: 412,
|
|
Quality: 0x00C0,
|
|
TimestampUtc: new DateTime(2026, 4, 26, 17, 0, 0, DateTimeKind.Utc),
|
|
Status:
|
|
[
|
|
new AsbStatusElement(AsbStatusElementType.MxStatusCategory, 0),
|
|
new AsbStatusElement(AsbStatusElementType.MxStatusDetail, 0),
|
|
new AsbStatusElement(AsbStatusElementType.MxQuality, 0x00C0),
|
|
]);
|
|
AssertEqual("compat status summary quality", AsbStatusQuality.Good, dataChange.StatusSummary.Quality);
|
|
AssertEqual("compat status summary error", AsbErrorCode.Success, dataChange.StatusSummary.StatusError);
|
|
|
|
using MxAsbCompatibilityServer server = new();
|
|
AssertThrows<ArgumentException>("unknown asb server handle", () => server.AddItem(404, "TestChildObject.TestInt"));
|
|
}
|
|
|
|
static void RunConnectionOptionsTests()
|
|
{
|
|
AssertNotNull("asb connect options overload", typeof(MxAsbDataClient).GetMethod(
|
|
nameof(MxAsbDataClient.Connect),
|
|
[typeof(AsbConnectionOptions)]));
|
|
|
|
AsbConnectionOptions defaultOptions = new();
|
|
AssertEqual("default endpoint", string.Empty, defaultOptions.Endpoint);
|
|
AssertEqual("default solution", null, defaultOptions.SolutionName);
|
|
AssertEqual("default trace", null, defaultOptions.Trace);
|
|
AssertEqual("default dump messages", false, defaultOptions.DumpMessages);
|
|
|
|
AsbConnectionOptions options = new()
|
|
{
|
|
Endpoint = "net.tcp://example/ASB/IDataV2",
|
|
SolutionName = "Galaxy",
|
|
Trace = _ => { },
|
|
DumpMessages = true,
|
|
};
|
|
options.Validate();
|
|
AssertEqual("connection endpoint", "net.tcp://example/ASB/IDataV2", options.Endpoint);
|
|
AssertEqual("connection solution", "Galaxy", options.SolutionName);
|
|
AssertNotNull("connection trace", options.Trace);
|
|
AssertEqual("connection dump messages", true, options.DumpMessages);
|
|
|
|
AssertThrows<ArgumentException>(
|
|
"empty endpoint options",
|
|
() => new AsbConnectionOptions { Endpoint = " " }.Validate());
|
|
AssertThrows<ArgumentNullException>(
|
|
"null options connect",
|
|
() => MxAsbDataClient.Connect((AsbConnectionOptions)null!));
|
|
AssertThrows<ArgumentException>(
|
|
"empty endpoint connect",
|
|
() => MxAsbDataClient.Connect(" "));
|
|
AssertEqual("payload debug non-public", false, typeof(AsbPayloadDebug).IsPublic);
|
|
}
|
|
|
|
static void RunWriteOptionsTests()
|
|
{
|
|
AssertNotNull("asb write positional overload", typeof(MxAsbDataClient).GetMethod(
|
|
nameof(MxAsbDataClient.Write),
|
|
[typeof(string), typeof(Variant), typeof(uint), typeof(string)]));
|
|
AssertNotNull("asb write options overload", typeof(MxAsbDataClient).GetMethod(
|
|
nameof(MxAsbDataClient.Write),
|
|
[typeof(string), typeof(Variant), typeof(AsbWriteOptions)]));
|
|
|
|
AsbWriteOptions defaultOptions = new();
|
|
AssertEqual("write options default handle", 0u, defaultOptions.WriteHandle);
|
|
AssertEqual("write options default comment", null, defaultOptions.Comment);
|
|
|
|
AsbWriteOptions options = new()
|
|
{
|
|
WriteHandle = 0xA5B21001,
|
|
Comment = "contract write",
|
|
};
|
|
AssertEqual("write options handle", (uint)0xA5B21001, options.WriteHandle);
|
|
AssertEqual("write options comment", "contract write", options.Comment);
|
|
|
|
MxAsbDataClient uninitializedClient =
|
|
(MxAsbDataClient)System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(typeof(MxAsbDataClient));
|
|
AssertThrows<ArgumentNullException>(
|
|
"write null options",
|
|
() => uninitializedClient.Write("Test.Tag", AsbVariantFactory.Empty, (AsbWriteOptions)null!));
|
|
}
|
|
|
|
static void RunCollectionArgumentGuardTests()
|
|
{
|
|
MxAsbDataClient uninitializedClient =
|
|
(MxAsbDataClient)System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(typeof(MxAsbDataClient));
|
|
|
|
AssertThrows<ArgumentNullException>(
|
|
"register many null tags",
|
|
() => uninitializedClient.RegisterMany(null!));
|
|
AssertThrows<ArgumentNullException>(
|
|
"unregister many null items",
|
|
() => uninitializedClient.UnregisterMany(null!));
|
|
AssertThrows<ArgumentNullException>(
|
|
"read many null tags",
|
|
() => uninitializedClient.ReadMany(null!));
|
|
AssertThrows<ArgumentNullException>(
|
|
"add monitored null tags",
|
|
() => uninitializedClient.AddMonitoredItems(123, null!, AsbMonitoredItemOptions.Default));
|
|
AssertThrows<ArgumentNullException>(
|
|
"delete monitored null items",
|
|
() => uninitializedClient.DeleteMonitoredItems(123, null!));
|
|
}
|
|
|
|
static void RunResultMapperTests()
|
|
{
|
|
AsbResultSummary success = AsbResultMapper.ToSummary(new ArchestrAResult { ErrorCode = 0, Success = true });
|
|
AssertEqual("success error", AsbErrorCode.Success, success.Error);
|
|
AssertEqual("success-like success", true, success.IsSuccessLike);
|
|
|
|
AsbResultSummary publishComplete = AsbResultMapper.ToSummary(new ArchestrAResult { ErrorCode = 32, Success = false });
|
|
AssertEqual("publish complete error", AsbErrorCode.PublishComplete, publishComplete.Error);
|
|
AssertEqual("publish complete success", false, publishComplete.IsSuccess);
|
|
AssertEqual("publish complete success-like", true, publishComplete.IsSuccessLike);
|
|
|
|
AsbResultSummary unknown = AsbResultMapper.ToSummary(new ArchestrAResult { ErrorCode = 12345, Success = false });
|
|
AssertEqual("unknown error", AsbErrorCode.Unknown, unknown.Error);
|
|
AssertEqual("unknown raw", 12345, unknown.RawErrorCode);
|
|
|
|
AsbResultSummary outOfRange = AsbResultMapper.ToSummary(new ArchestrAResult { ErrorCode = 65536, Success = false });
|
|
AssertEqual("out-of-range error", AsbErrorCode.Unknown, outOfRange.Error);
|
|
AssertEqual("out-of-range raw", 65536, outOfRange.RawErrorCode);
|
|
|
|
AsbItemStatusSummary item = AsbResultMapper.ToItemSummary(new ItemStatus
|
|
{
|
|
Item = new ItemIdentity { Name = "Bad.Tag", Id = 42, IdSpecified = true },
|
|
ErrorCode = 10,
|
|
Status = new AsbStatus { Count = 1, Payload = [0x85] },
|
|
});
|
|
AssertEqual("item status error", AsbErrorCode.InvalidMonitoredItems, item.Error);
|
|
AssertEqual("item status success", false, item.IsSuccess);
|
|
AssertEqual("item status element", AsbStatusElementType.MxStatusCategory, item.Status[0].Type);
|
|
AssertEqual("item status summary category", AsbMxStatusCategory.Ok, item.StatusSummary.Category);
|
|
|
|
IReadOnlyList<AsbItemStatusSummary> nullItems = AsbResultMapper.ToItemSummaries(null);
|
|
AssertEqual("item summaries null", 0, nullItems.Count);
|
|
|
|
IReadOnlyList<AsbItemStatusSummary> emptyItems = AsbResultMapper.ToItemSummaries([]);
|
|
AssertEqual("item summaries empty", 0, emptyItems.Count);
|
|
|
|
IReadOnlyList<AsbItemStatusSummary> oneItem = AsbResultMapper.ToItemSummaries(
|
|
[
|
|
new ItemStatus
|
|
{
|
|
Item = new ItemIdentity { Name = "Good.Tag", Id = 43, IdSpecified = true },
|
|
ErrorCode = 0,
|
|
Status = StatusBytes(0x05, 0x00, 0x00, 0x06, 0x00, 0x00, 0x07, 0xC0, 0x00),
|
|
},
|
|
]);
|
|
AssertEqual("item summaries one count", 1, oneItem.Count);
|
|
AssertEqual("item summaries one item", "Good.Tag", oneItem[0].ItemName);
|
|
AssertEqual("item summaries one error", AsbErrorCode.Success, oneItem[0].Error);
|
|
AssertEqual("item summaries one status", AsbStatusQuality.Good, oneItem[0].StatusSummary.Quality);
|
|
|
|
AsbStatusSummary good = AsbResultMapper.ToStatusSummary(StatusBytes(
|
|
0x05, 0x00, 0x00,
|
|
0x06, 0x00, 0x00,
|
|
0x07, 0xC0, 0x00));
|
|
AssertEqual("good status category", AsbMxStatusCategory.Ok, good.Category);
|
|
AssertEqual("good status detail", AsbMxStatusDetail.None, good.Detail);
|
|
AssertEqual("good status quality", AsbStatusQuality.Good, good.Quality);
|
|
AssertEqual("good status error", AsbErrorCode.Success, good.StatusError);
|
|
AssertEqual("good status success-like", true, good.IsSuccessLike);
|
|
|
|
AsbStatusSummary badQuality = AsbResultMapper.ToStatusSummary(StatusBytes(
|
|
0x05, 0x00, 0x00,
|
|
0x06, 0x00, 0x00,
|
|
0x07, 0x00, 0x00));
|
|
AssertEqual("bad quality summary", AsbStatusQuality.Bad, badQuality.Quality);
|
|
AssertEqual("bad quality raw", (ushort?)0x0000, badQuality.RawQuality);
|
|
AssertEqual("bad quality success-like", false, badQuality.IsSuccessLike);
|
|
|
|
AsbStatusSummary timedOut = AsbResultMapper.ToStatusSummary(StatusBytes(
|
|
0x05, 0x03, 0x00,
|
|
0x06, 0x10, 0x00,
|
|
0x07, 0x00, 0x00));
|
|
AssertEqual("timed out category", AsbMxStatusCategory.CommunicationError, timedOut.Category);
|
|
AssertEqual("timed out detail", AsbMxStatusDetail.RequestTimedOut, timedOut.Detail);
|
|
AssertEqual("timed out error", AsbErrorCode.RequestTimedOut, timedOut.StatusError);
|
|
AssertEqual("timed out quality", AsbStatusQuality.Bad, timedOut.Quality);
|
|
|
|
AsbStatusSummary noCommunication = AsbResultMapper.ToStatusSummary(StatusBytes(
|
|
0x05, 0x03, 0x00,
|
|
0x06, 0x11, 0x00,
|
|
0x07, 0x00, 0x00));
|
|
AssertEqual("no communication category", AsbMxStatusCategory.CommunicationError, noCommunication.Category);
|
|
AssertEqual("no communication detail", AsbMxStatusDetail.PlatformCommunicationError, noCommunication.Detail);
|
|
AssertEqual("no communication error", AsbErrorCode.BadNoCommunication, noCommunication.StatusError);
|
|
|
|
AsbStatusSummary accessDenied = AsbResultMapper.ToStatusSummary(StatusBytes(
|
|
0x05, 0x06, 0x00,
|
|
0x06, 0x21, 0x00,
|
|
0x07, 0x00, 0x00));
|
|
AssertEqual("access denied category", AsbMxStatusCategory.SecurityError, accessDenied.Category);
|
|
AssertEqual("access denied detail", AsbMxStatusDetail.WriteAccessDenied, accessDenied.Detail);
|
|
AssertEqual("access denied error", AsbErrorCode.WriteFailedAccessDenied, accessDenied.StatusError);
|
|
AssertEqual("access denied success-like", false, accessDenied.IsSuccessLike);
|
|
|
|
AsbStatusSummary unknownStatus = AsbResultMapper.ToStatusSummary(StatusBytes(
|
|
0x63, 0x77, 0x77,
|
|
0x05, 0xEE, 0x02,
|
|
0x06, 0xE7, 0x03,
|
|
0x07, 0x80, 0x00));
|
|
AssertEqual("unknown status category", AsbMxStatusCategory.Unknown, unknownStatus.Category);
|
|
AssertEqual("unknown status raw category", (ushort?)750, unknownStatus.RawCategory);
|
|
AssertEqual("unknown status detail", AsbMxStatusDetail.Unknown, unknownStatus.Detail);
|
|
AssertEqual("unknown status raw detail", (ushort?)999, unknownStatus.RawDetail);
|
|
AssertEqual("unknown status quality", AsbStatusQuality.Unknown, unknownStatus.Quality);
|
|
AssertEqual("unknown status raw quality", (ushort?)0x0080, unknownStatus.RawQuality);
|
|
AssertEqual("unknown status raw type", (AsbStatusElementType)99, unknownStatus.Elements[0].Type);
|
|
AssertEqual("unknown status raw value", (ushort)0x7777, unknownStatus.Elements[0].Value);
|
|
}
|
|
|
|
static void RunWriteCompletionOptionsTests()
|
|
{
|
|
AssertNotNull("asb wait write completion overload", typeof(MxAsbDataClient).GetMethod(
|
|
nameof(MxAsbDataClient.WaitForWriteComplete),
|
|
[typeof(uint), typeof(AsbWriteCompletionOptions)]));
|
|
AssertNotNull("asb wait write completion readback overload", typeof(MxAsbDataClient).GetMethod(
|
|
nameof(MxAsbDataClient.WaitForWriteCompleteAndRead),
|
|
[typeof(string), typeof(uint), typeof(AsbWriteCompletionOptions)]));
|
|
|
|
AssertEqual("write completion default timeout", TimeSpan.FromSeconds(5), AsbWriteCompletionOptions.Default.Timeout);
|
|
AssertEqual("write completion default poll", TimeSpan.FromMilliseconds(250), AsbWriteCompletionOptions.Default.PollInterval);
|
|
AssertEqual("write completion default readback", TimeSpan.Zero, AsbWriteCompletionOptions.Default.ReadbackDelay);
|
|
AssertEqual("write completion default cancellation", false, AsbWriteCompletionOptions.Default.CancellationToken.IsCancellationRequested);
|
|
AsbWriteCompletionOptions.Default.ValidateReadback();
|
|
AssertNotNull("write completion cancellation option", typeof(AsbWriteCompletionOptions).GetProperty(nameof(AsbWriteCompletionOptions.CancellationToken)));
|
|
AssertEqual(
|
|
"write completion canceled token",
|
|
true,
|
|
new AsbWriteCompletionOptions { CancellationToken = new CancellationToken(canceled: true) }.CancellationToken.IsCancellationRequested);
|
|
new AsbWriteCompletionOptions { Timeout = TimeSpan.Zero, PollInterval = TimeSpan.FromMilliseconds(1) }.ValidatePolling();
|
|
AssertThrows<ArgumentOutOfRangeException>(
|
|
"write completion timeout",
|
|
() => new AsbWriteCompletionOptions { Timeout = TimeSpan.FromMilliseconds(-1) }.ValidatePolling());
|
|
AssertThrows<ArgumentOutOfRangeException>(
|
|
"write completion poll interval",
|
|
() => new AsbWriteCompletionOptions { PollInterval = TimeSpan.Zero }.ValidatePolling());
|
|
AssertThrows<ArgumentOutOfRangeException>(
|
|
"write completion readback delay",
|
|
() => new AsbWriteCompletionOptions { ReadbackDelay = TimeSpan.FromMilliseconds(-1) }.ValidateReadback());
|
|
|
|
ItemWriteComplete matchingComplete = new()
|
|
{
|
|
WriteHandle = 0xA5B21001,
|
|
Status =
|
|
[
|
|
new ItemStatus
|
|
{
|
|
Item = new ItemIdentity { Name = "Test.Tag", Id = 12, IdSpecified = true },
|
|
ErrorCode = 0,
|
|
Status = new AsbStatus { Count = 0, Payload = [] },
|
|
},
|
|
],
|
|
};
|
|
PublishWriteCompleteResponse response = new()
|
|
{
|
|
Result = new ArchestrAResult { ErrorCode = 32 },
|
|
CompleteWrites = [matchingComplete],
|
|
};
|
|
AsbWriteCompletionResult completion = new(
|
|
0xA5B21001,
|
|
Completed: true,
|
|
TimedOut: false,
|
|
TimeSpan.FromMilliseconds(20),
|
|
PollCount: 2,
|
|
[response],
|
|
[matchingComplete],
|
|
matchingComplete);
|
|
AssertEqual("write completion result handle", (uint)0xA5B21001, completion.WriteHandle);
|
|
AssertEqual("write completion result completed", true, completion.Completed);
|
|
AssertEqual("write completion result timed out", false, completion.TimedOut);
|
|
AssertEqual("write completion result polls", 2, completion.PollCount);
|
|
AssertEqual("write completion result responses", 1, completion.Responses.Count);
|
|
AssertEqual("write completion result raw", 1, completion.CompleteWrites.Count);
|
|
AssertEqual("write completion result match", (uint)0xA5B21001, completion.MatchingComplete?.WriteHandle);
|
|
|
|
AsbWriteCompletionReadbackResult readback = new(completion, Readback: null);
|
|
AssertEqual("write completion readback completion", completion, readback.Completion);
|
|
AssertEqual("write completion readback null", null, readback.Readback);
|
|
}
|
|
|
|
static void RunSubscriptionOptionsTests()
|
|
{
|
|
AssertNotNull("asb create subscription options overload", typeof(MxAsbDataClient).GetMethod(
|
|
nameof(MxAsbDataClient.CreateSubscription),
|
|
[typeof(AsbSubscriptionOptions)]));
|
|
|
|
AssertEqual("subscription default queue", 128L, AsbSubscriptionOptions.Default.MaxQueueSize);
|
|
AssertEqual("subscription default sample", (ulong)1000, AsbSubscriptionOptions.Default.SampleInterval);
|
|
AsbSubscriptionOptions.Default.Validate();
|
|
AsbSubscriptionOptions subscription = new() { MaxQueueSize = 128, SampleInterval = 1000 };
|
|
subscription.Validate();
|
|
AssertEqual("subscription queue", 128L, subscription.MaxQueueSize);
|
|
AssertEqual("subscription sample", (ulong)1000, subscription.SampleInterval);
|
|
AssertThrows<ArgumentOutOfRangeException>(
|
|
"subscription max queue size",
|
|
() => new AsbSubscriptionOptions { MaxQueueSize = 0 }.Validate());
|
|
|
|
AssertEqual("monitored item default sample", (ulong)1000, AsbMonitoredItemOptions.Default.SampleInterval);
|
|
AssertEqual("monitored item default active", true, AsbMonitoredItemOptions.Default.Active);
|
|
AssertEqual("monitored item default buffered", false, AsbMonitoredItemOptions.Default.Buffered);
|
|
AsbMonitoredItemOptions monitored = new()
|
|
{
|
|
SampleInterval = 250,
|
|
Active = false,
|
|
Buffered = true,
|
|
};
|
|
AssertEqual("monitored item sample", (ulong)250, monitored.SampleInterval);
|
|
AssertEqual("monitored item active", false, monitored.Active);
|
|
AssertEqual("monitored item buffered", true, monitored.Buffered);
|
|
}
|
|
|
|
static AsbStatus StatusBytes(params byte[] payload)
|
|
{
|
|
return new AsbStatus { Count = checked((sbyte)payload.Length), Payload = payload };
|
|
}
|
|
|
|
static void RunReconnectSurfaceTests()
|
|
{
|
|
AssertNotNull("asb reconnect method", typeof(MxAsbDataClient).GetMethod(nameof(MxAsbDataClient.Reconnect)));
|
|
AssertNotNull("asb channel state property", typeof(MxAsbDataClient).GetProperty(nameof(MxAsbDataClient.ChannelState)));
|
|
AssertNotNull("asb disposed property", typeof(MxAsbDataClient).GetProperty(nameof(MxAsbDataClient.IsDisposed)));
|
|
|
|
AsbReconnectOptions.Default.Validate();
|
|
AssertThrows<ArgumentOutOfRangeException>(
|
|
"reconnect attempts",
|
|
() => new AsbReconnectOptions { MaxAttempts = 0 }.Validate());
|
|
AssertThrows<ArgumentOutOfRangeException>(
|
|
"reconnect delay",
|
|
() => new AsbReconnectOptions { Delay = TimeSpan.FromMilliseconds(-1) }.Validate());
|
|
AssertThrows<ArgumentNullException>(
|
|
"reconnect cleanup options",
|
|
() => new AsbReconnectOptions { CleanupOptions = null! }.Validate());
|
|
AssertThrows<ArgumentOutOfRangeException>(
|
|
"reconnect cleanup disconnect timeout",
|
|
() => new AsbReconnectOptions
|
|
{
|
|
CleanupOptions = new AsbClientCleanupOptions { DisconnectTimeout = TimeSpan.FromMilliseconds(-1) },
|
|
}.Validate());
|
|
|
|
InvalidOperationException failure = new("failed");
|
|
AsbReconnectResult result = new(
|
|
Succeeded: false,
|
|
Client: null,
|
|
CleanupResult: null,
|
|
[new AsbReconnectAttempt(1, Succeeded: false, failure)]);
|
|
AssertEqual("reconnect last exception", failure, result.LastException);
|
|
}
|
|
|
|
static void RunCleanupOptionsTests()
|
|
{
|
|
AssertNotNull("asb cleanup overload", typeof(MxAsbDataClient).GetMethod(nameof(MxAsbDataClient.Cleanup), [typeof(AsbClientCleanupOptions)]));
|
|
|
|
AsbClientCleanupOptions.Default.Validate();
|
|
AssertThrows<ArgumentOutOfRangeException>(
|
|
"cleanup disconnect timeout",
|
|
() => new AsbClientCleanupOptions { DisconnectTimeout = TimeSpan.FromMilliseconds(-1) }.Validate());
|
|
AssertThrows<ArgumentOutOfRangeException>(
|
|
"cleanup close timeout",
|
|
() => new AsbClientCleanupOptions { CloseTimeout = TimeSpan.FromMilliseconds(-1) }.Validate());
|
|
AssertEqual(
|
|
"cleanup cancellation token",
|
|
true,
|
|
new AsbClientCleanupOptions { CancellationToken = new CancellationToken(canceled: true) }.CancellationToken.IsCancellationRequested);
|
|
}
|
|
|
|
static void RunCommunicationCleanupTests()
|
|
{
|
|
FakeCommunicationObject closed = new(CommunicationState.Closed);
|
|
CommunicationObjectCleanupResult closedResult = AsbCommunicationCleanup.CloseOrAbort(
|
|
closed,
|
|
"closed",
|
|
TimeSpan.FromSeconds(1),
|
|
trace: null);
|
|
AssertEqual("closed cleanup close attempted", false, closedResult.CloseAttempted);
|
|
AssertEqual("closed cleanup closed", true, closedResult.Closed);
|
|
AssertEqual("closed cleanup abort attempted", false, closedResult.AbortAttempted);
|
|
AssertEqual("closed cleanup succeeded", true, closedResult.Succeeded);
|
|
|
|
FakeCommunicationObject faulted = new(CommunicationState.Faulted);
|
|
CommunicationObjectCleanupResult faultedResult = AsbCommunicationCleanup.CloseOrAbort(
|
|
faulted,
|
|
"faulted",
|
|
TimeSpan.FromSeconds(1),
|
|
trace: null);
|
|
AssertEqual("faulted cleanup close attempted", false, faultedResult.CloseAttempted);
|
|
AssertEqual("faulted cleanup abort attempted", true, faultedResult.AbortAttempted);
|
|
AssertEqual("faulted cleanup aborted", true, faultedResult.Aborted);
|
|
AssertEqual("faulted cleanup succeeded", true, faultedResult.Succeeded);
|
|
|
|
FakeCommunicationObject closeFailure = new(CommunicationState.Opened)
|
|
{
|
|
ThrowOnClose = true,
|
|
};
|
|
CommunicationObjectCleanupResult closeFailureResult = AsbCommunicationCleanup.CloseOrAbort(
|
|
closeFailure,
|
|
"closeFailure",
|
|
TimeSpan.FromMilliseconds(25),
|
|
trace: null);
|
|
AssertEqual("close failure timeout", TimeSpan.FromMilliseconds(25), closeFailure.CloseTimeout);
|
|
AssertEqual("close failure close attempted", true, closeFailureResult.CloseAttempted);
|
|
AssertNotNull("close failure captured", closeFailureResult.CloseFailure);
|
|
AssertEqual("close failure abort attempted", true, closeFailureResult.AbortAttempted);
|
|
AssertEqual("close failure aborted", true, closeFailureResult.Aborted);
|
|
AssertEqual("close failure requires fallback", true, closeFailureResult.Succeeded is false);
|
|
|
|
FakeCommunicationObject abortFailure = new(CommunicationState.Faulted)
|
|
{
|
|
ThrowOnAbort = true,
|
|
};
|
|
CommunicationObjectCleanupResult abortFailureResult = AsbCommunicationCleanup.CloseOrAbort(
|
|
abortFailure,
|
|
"abortFailure",
|
|
TimeSpan.FromSeconds(1),
|
|
trace: null);
|
|
AssertEqual("abort failure abort attempted", true, abortFailureResult.AbortAttempted);
|
|
AssertEqual("abort failure aborted", false, abortFailureResult.Aborted);
|
|
AssertNotNull("abort failure captured", abortFailureResult.AbortFailure);
|
|
AssertEqual("abort failure succeeded", false, abortFailureResult.Succeeded);
|
|
|
|
FakeCommunicationObject canceledOpen = new(CommunicationState.Opened);
|
|
CommunicationObjectCleanupResult canceledOpenResult = AsbCommunicationCleanup.AbortOnly(
|
|
canceledOpen,
|
|
"canceledOpen",
|
|
trace: null);
|
|
AssertEqual("canceled open close attempted", false, canceledOpenResult.CloseAttempted);
|
|
AssertEqual("canceled open abort attempted", true, canceledOpenResult.AbortAttempted);
|
|
AssertEqual("canceled open aborted", true, canceledOpenResult.Aborted);
|
|
AssertEqual("canceled open closed", true, canceledOpenResult.Closed);
|
|
|
|
FakeCommunicationObject canceledClosed = new(CommunicationState.Closed);
|
|
CommunicationObjectCleanupResult canceledClosedResult = AsbCommunicationCleanup.AbortOnly(
|
|
canceledClosed,
|
|
"canceledClosed",
|
|
trace: null);
|
|
AssertEqual("canceled closed close attempted", false, canceledClosedResult.CloseAttempted);
|
|
AssertEqual("canceled closed abort attempted", false, canceledClosedResult.AbortAttempted);
|
|
AssertEqual("canceled closed closed", true, canceledClosedResult.Closed);
|
|
}
|
|
|
|
sealed class FakeCommunicationObject(CommunicationState initialState) : ICommunicationObject
|
|
{
|
|
public bool ThrowOnClose { get; init; }
|
|
|
|
public bool ThrowOnAbort { get; init; }
|
|
|
|
public TimeSpan? CloseTimeout { get; private set; }
|
|
|
|
public CommunicationState State { get; private set; } = initialState;
|
|
|
|
public event EventHandler? Closed;
|
|
public event EventHandler? Closing;
|
|
public event EventHandler? Faulted;
|
|
public event EventHandler? Opened;
|
|
public event EventHandler? Opening;
|
|
|
|
public void Abort()
|
|
{
|
|
if (ThrowOnAbort)
|
|
{
|
|
throw new InvalidOperationException("abort failed");
|
|
}
|
|
|
|
State = CommunicationState.Closed;
|
|
Closed?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
public IAsyncResult BeginClose(AsyncCallback? callback, object? state)
|
|
{
|
|
Close();
|
|
Task task = Task.CompletedTask;
|
|
callback?.Invoke(task);
|
|
return task;
|
|
}
|
|
|
|
public IAsyncResult BeginClose(TimeSpan timeout, AsyncCallback? callback, object? state)
|
|
{
|
|
Close(timeout);
|
|
Task task = Task.CompletedTask;
|
|
callback?.Invoke(task);
|
|
return task;
|
|
}
|
|
|
|
public IAsyncResult BeginOpen(AsyncCallback? callback, object? state)
|
|
{
|
|
Open();
|
|
Task task = Task.CompletedTask;
|
|
callback?.Invoke(task);
|
|
return task;
|
|
}
|
|
|
|
public IAsyncResult BeginOpen(TimeSpan timeout, AsyncCallback? callback, object? state)
|
|
{
|
|
Open(timeout);
|
|
Task task = Task.CompletedTask;
|
|
callback?.Invoke(task);
|
|
return task;
|
|
}
|
|
|
|
public void Close()
|
|
{
|
|
Close(TimeSpan.Zero);
|
|
}
|
|
|
|
public void Close(TimeSpan timeout)
|
|
{
|
|
CloseTimeout = timeout;
|
|
if (ThrowOnClose)
|
|
{
|
|
throw new InvalidOperationException("close failed");
|
|
}
|
|
|
|
Closing?.Invoke(this, EventArgs.Empty);
|
|
State = CommunicationState.Closed;
|
|
Closed?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
public void EndClose(IAsyncResult result)
|
|
{
|
|
}
|
|
|
|
public void EndOpen(IAsyncResult result)
|
|
{
|
|
}
|
|
|
|
public void Open()
|
|
{
|
|
Open(TimeSpan.Zero);
|
|
}
|
|
|
|
public void Open(TimeSpan timeout)
|
|
{
|
|
Opening?.Invoke(this, EventArgs.Empty);
|
|
State = CommunicationState.Opened;
|
|
Opened?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
public void RaiseFaulted()
|
|
{
|
|
State = CommunicationState.Faulted;
|
|
Faulted?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
}
|