Files
mxaccess/src/MxAsbClient.Tests/Program.cs
T
Joseph Doherty fe2a6db786
rust / build / test / clippy / fmt (push) Has been cancelled
Initial project state: .NET reference, design, Rust port (M0+M1), evidence
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>
2026-05-05 06:21:00 -04:00

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);
}
}