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(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(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(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(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 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 { [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), 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), typeof(ulong), typeof(bool), typeof(bool)])); AssertNotNull("asb options add monitored", typeof(MxAsbDataClient).GetMethod( nameof(MxAsbDataClient.AddMonitoredItems), [typeof(long), typeof(IEnumerable), 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("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( "empty endpoint options", () => new AsbConnectionOptions { Endpoint = " " }.Validate()); AssertThrows( "null options connect", () => MxAsbDataClient.Connect((AsbConnectionOptions)null!)); AssertThrows( "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( "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( "register many null tags", () => uninitializedClient.RegisterMany(null!)); AssertThrows( "unregister many null items", () => uninitializedClient.UnregisterMany(null!)); AssertThrows( "read many null tags", () => uninitializedClient.ReadMany(null!)); AssertThrows( "add monitored null tags", () => uninitializedClient.AddMonitoredItems(123, null!, AsbMonitoredItemOptions.Default)); AssertThrows( "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 nullItems = AsbResultMapper.ToItemSummaries(null); AssertEqual("item summaries null", 0, nullItems.Count); IReadOnlyList emptyItems = AsbResultMapper.ToItemSummaries([]); AssertEqual("item summaries empty", 0, emptyItems.Count); IReadOnlyList 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( "write completion timeout", () => new AsbWriteCompletionOptions { Timeout = TimeSpan.FromMilliseconds(-1) }.ValidatePolling()); AssertThrows( "write completion poll interval", () => new AsbWriteCompletionOptions { PollInterval = TimeSpan.Zero }.ValidatePolling()); AssertThrows( "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( "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( "reconnect attempts", () => new AsbReconnectOptions { MaxAttempts = 0 }.Validate()); AssertThrows( "reconnect delay", () => new AsbReconnectOptions { Delay = TimeSpan.FromMilliseconds(-1) }.Validate()); AssertThrows( "reconnect cleanup options", () => new AsbReconnectOptions { CleanupOptions = null! }.Validate()); AssertThrows( "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( "cleanup disconnect timeout", () => new AsbClientCleanupOptions { DisconnectTimeout = TimeSpan.FromMilliseconds(-1) }.Validate()); AssertThrows( "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); } }