Initial project state: .NET reference, design, Rust port (M0+M1), evidence
rust / build / test / clippy / fmt (push) Has been cancelled
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>
This commit is contained in:
@@ -0,0 +1,779 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user