From e5c7b655d33b8aa2e02186b777bb8078e0b793e6 Mon Sep 17 00:00:00 2001 From: Sebastian Zitzelsberger Date: Thu, 18 Sep 2025 11:36:32 +0200 Subject: [PATCH 1/3] #450 - Make Tag testable without devices. --- src/libplctag.Tests/MyUdt.cs | 11 + .../StructMarshallingExtensions.cs | 29 ++ src/libplctag.Tests/TagExtensions.cs | 27 ++ src/libplctag.Tests/TagTests.cs | 93 ++++++ src/libplctag.Tests/libplctag.Tests.csproj | 1 + src/libplctag/INative.cs | 2 +- src/libplctag/Tag.cs | 288 ++++++++++-------- 7 files changed, 324 insertions(+), 127 deletions(-) create mode 100644 src/libplctag.Tests/MyUdt.cs create mode 100644 src/libplctag.Tests/StructMarshallingExtensions.cs create mode 100644 src/libplctag.Tests/TagExtensions.cs create mode 100644 src/libplctag.Tests/TagTests.cs diff --git a/src/libplctag.Tests/MyUdt.cs b/src/libplctag.Tests/MyUdt.cs new file mode 100644 index 0000000..b34af9e --- /dev/null +++ b/src/libplctag.Tests/MyUdt.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace libplctag.Tests +{ + [StructLayout(LayoutKind.Sequential, Pack = 4)] + public struct MyUdt + { + public short shortField; + public int intField; + } +} \ No newline at end of file diff --git a/src/libplctag.Tests/StructMarshallingExtensions.cs b/src/libplctag.Tests/StructMarshallingExtensions.cs new file mode 100644 index 0000000..4d4e84b --- /dev/null +++ b/src/libplctag.Tests/StructMarshallingExtensions.cs @@ -0,0 +1,29 @@ +using System; +using System.Runtime.InteropServices; + +namespace libplctag.Tests +{ + public static class StructMarshallingExtensions + { + /// + /// Converts a blittable struct to a byte array. + /// + public static byte[] ToByteArray(this T value) where T : struct + { + byte[] bytes = new byte[Marshal.SizeOf()]; + MemoryMarshal.Write(bytes, ref value); // zero-allocation, fast + return bytes; + } + + /// + /// Reads a blittable struct from a byte array. + /// + public static T ToStruct(this byte[] bytes) where T : struct + { + if (bytes.Length < Marshal.SizeOf()) + throw new ArgumentException($"Byte array too small for struct {typeof(T)}"); + + return MemoryMarshal.Read(bytes); + } + } +} \ No newline at end of file diff --git a/src/libplctag.Tests/TagExtensions.cs b/src/libplctag.Tests/TagExtensions.cs new file mode 100644 index 0000000..0ffc445 --- /dev/null +++ b/src/libplctag.Tests/TagExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Runtime.InteropServices; + +namespace libplctag.Tests +{ + public static class TagExtensions + { + public static T GetValue(this Tag tag) where T : struct + { + byte[] buffer = tag.GetBuffer(); + GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); + + try + { + //ToInt64 is used because it's assumed the code will compile and run on 64-bit platform. + //If it's going to run on a 32-platform, ToInt32 should be used. + T retVal = Marshal.PtrToStructure(new IntPtr(handle.AddrOfPinnedObject().ToInt64()))!; + + return (retVal); + } + finally + { + handle.Free(); + } + } + } +} \ No newline at end of file diff --git a/src/libplctag.Tests/TagTests.cs b/src/libplctag.Tests/TagTests.cs new file mode 100644 index 0000000..a188519 --- /dev/null +++ b/src/libplctag.Tests/TagTests.cs @@ -0,0 +1,93 @@ +using FluentAssertions; +using libplctag.NativeImport; +using Moq; +using System; +using System.Net; +using System.Runtime.InteropServices; +using Xunit; + +namespace libplctag.Tests +{ + public class TagTests : IDisposable + { + private const int TimeoutInMilliSeconds = 1000; + private const int NativeTagHandle = 1; + private readonly MockRepository _mockRepository; + private readonly Mock _iNativeMock; + private readonly Tag _underTest; + + + public TagTests() + { + _mockRepository = new MockRepository(MockBehavior.Strict); + _iNativeMock = _mockRepository.Create(); + _underTest = new Tag(_iNativeMock.Object); + _underTest.Timeout = TimeSpan.FromMilliseconds(TimeoutInMilliSeconds); + } + + [Fact] + public void TagCanBeInitialized() + { + // ARRANGE + GivenTagCanBeInitializedWithHandle(NativeTagHandle); + + // ACT + _underTest.Initialize(); + + + // ASSERT + _underTest.IsInitialized.Should().BeTrue(); + _underTest.NativeTagHandle.Should().Be(NativeTagHandle); + } + + private void GivenTagCanBeInitializedWithHandle(int nativeTagHandle) + { + _iNativeMock.Setup(native => native.plc_tag_create_ex( + It.IsAny(), + It.IsAny(), + IntPtr.Zero, + TimeoutInMilliSeconds)) + .Returns(nativeTagHandle); + } + + + [Fact] + public void TagForMyUdtShouldReturnMockedValue() + { + // ARRANGE + MyUdt expectedValue = new() { intField = 1, shortField = 2 }; + _underTest.ElementSize = Marshal.SizeOf(typeof(MyUdt)); + + GivenTagCanBeInitializedWithHandle(NativeTagHandle); + GivenMarshalledDataIsReturnedInBuffer(NativeTagHandle, expectedValue); + + // ACT + _underTest.Initialize(); + MyUdt currentTagValue = _underTest.GetValue(); + + // ASSERT + currentTagValue.Should().Be(expectedValue); + } + + private void GivenMarshalledDataIsReturnedInBuffer(int nativeTagHandle, T expectedData) where T : struct + { + byte[] byteData = expectedData.ToByteArray(); + int size = Marshal.SizeOf(typeof(T)); + + _iNativeMock.Setup(native => native.plc_tag_get_size(nativeTagHandle)).Returns(size); + _iNativeMock.Setup(native => native.plc_tag_get_raw_bytes(nativeTagHandle, 0, It.IsAny(), size)) + .Returns((int)Status.Ok) + .Callback((int tag, int start_offset, byte[] buffer, int buffer_length) => + { + Array.Copy(byteData, buffer, byteData.Length); + }); + } + + + public void Dispose() + { + _mockRepository.VerifyAll(); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/libplctag.Tests/libplctag.Tests.csproj b/src/libplctag.Tests/libplctag.Tests.csproj index 6e48ea9..fd7714b 100644 --- a/src/libplctag.Tests/libplctag.Tests.csproj +++ b/src/libplctag.Tests/libplctag.Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/src/libplctag/INative.cs b/src/libplctag/INative.cs index b2d50d1..d71fc7b 100644 --- a/src/libplctag/INative.cs +++ b/src/libplctag/INative.cs @@ -16,7 +16,7 @@ namespace libplctag { - interface INative + public interface INative { int plc_tag_abort(int tag); int plc_tag_check_lib_version(int req_major, int req_minor, int req_patch); diff --git a/src/libplctag/Tag.cs b/src/libplctag/Tag.cs index 730b346..e8cb40e 100644 --- a/src/libplctag/Tag.cs +++ b/src/libplctag/Tag.cs @@ -20,7 +20,6 @@ namespace libplctag { - public sealed class Tag : IDisposable { private const int TIMEOUT_VALUE_THAT_INDICATES_ASYNC_OPERATION = 0; @@ -68,7 +67,7 @@ public Tag() : this(new Native()) { } - internal Tag(INative nativeMethods) + public Tag(INative nativeMethods) { _native = nativeMethods; @@ -93,6 +92,16 @@ public bool IsInitialized } } + public int NativeTagHandle + { + get + { + ThrowIfAlreadyDisposed(); + InitializeIfRequired(); + return nativeTagHandle; + } + } + /// /// /// @@ -199,7 +208,7 @@ public string Path get => GetField(ref _path); set => SetField(ref _path, value); } - + /// /// [OPTIONAL] /// An integer number of elements per tag. @@ -309,7 +318,6 @@ public int? ReadCacheMillisecondDuration { ThrowIfAlreadyDisposed(); return _readCacheMillisecondDuration; - } set { @@ -317,11 +325,10 @@ public int? ReadCacheMillisecondDuration if (_isInitialized) SetIntAttribute("read_cache_ms", value.Value); - + // Set after writing to underlying tag in case SetIntAttribute fails. // Ensures the two have the same value. _readCacheMillisecondDuration = value; - } } @@ -348,11 +355,11 @@ public TimeSpan? AutoSyncReadInterval if (_isInitialized) { if (value is null) - SetIntAttribute("auto_sync_read_ms", 0); // 0 is a special value that turns off auto sync + SetIntAttribute("auto_sync_read_ms", 0); // 0 is a special value that turns off auto sync else SetIntAttribute("auto_sync_read_ms", (int)value.Value.TotalMilliseconds); } - + // Set after writing to underlying tag in case SetIntAttribute fails. // Ensures the two have the same value. _autoSyncReadInterval = value; @@ -383,11 +390,11 @@ public TimeSpan? AutoSyncWriteInterval if (_isInitialized) { if (value is null) - SetIntAttribute("auto_sync_write_ms", 0); // 0 is a special value that turns off auto sync + SetIntAttribute("auto_sync_write_ms", 0); // 0 is a special value that turns off auto sync else SetIntAttribute("auto_sync_write_ms", (int)value.Value.TotalMilliseconds); } - + // Set after writing to underlying tag in case SetIntAttribute fails. // Ensures the two have the same value. _autoSyncWriteInterval = value; @@ -566,6 +573,7 @@ public bool? StringIsZeroTerminated } private uint? _stringMaxCapacity; + /// /// [OPTIONAL] /// Determines the maximum number of character bytes in a string. @@ -660,7 +668,6 @@ public void Abort() } - /// /// Creates the underlying data structures and references required before tag operations. /// @@ -672,7 +679,6 @@ public void Abort() /// public void Initialize() { - ThrowIfAlreadyDisposed(); ThrowIfAlreadyInitialized(); @@ -681,8 +687,9 @@ public void Initialize() SetUpEvents(); var attributeString = GetAttributeString(); - - var result = _native.plc_tag_create_ex(attributeString, coreLibCallbackFuncExDelegate, IntPtr.Zero, millisecondTimeout); + + var result = _native.plc_tag_create_ex(attributeString, coreLibCallbackFuncExDelegate, IntPtr.Zero, + millisecondTimeout); if (result < 0) { RemoveEvents(); @@ -717,7 +724,8 @@ public async Task InitializeAsync(CancellationToken token = default) createTasks.Push(createTask); var attributeString = GetAttributeString(); - var result = _native.plc_tag_create_ex(attributeString, coreLibCallbackFuncExDelegate, IntPtr.Zero, TIMEOUT_VALUE_THAT_INDICATES_ASYNC_OPERATION); + var result = _native.plc_tag_create_ex(attributeString, coreLibCallbackFuncExDelegate, IntPtr.Zero, + TIMEOUT_VALUE_THAT_INDICATES_ASYNC_OPERATION); // Something went wrong locally if (result < 0) @@ -736,24 +744,25 @@ public async Task InitializeAsync(CancellationToken token = default) cts.CancelAfter(Timeout); using (cts.Token.Register(() => - { - if (createTasks.TryPop(out _)) - { - RemoveCallback(); - Abort(); - var destroyResult = (Status)_native.plc_tag_destroy(result); - Debug.Assert(destroyResult == Status.Ok); - RemoveEvents(); - - if (token.IsCancellationRequested) - createTask.SetCanceled(); - else - createTask.SetException(new LibPlcTagException(Status.ErrorTimeout)); - } - })) + { + if (createTasks.TryPop(out _)) + { + RemoveCallback(); + Abort(); + var destroyResult = (Status)_native.plc_tag_destroy(result); + Debug.Assert(destroyResult == Status.Ok); + RemoveEvents(); + + if (token.IsCancellationRequested) + createTask.SetCanceled(); + else + createTask.SetException(new LibPlcTagException(Status.ErrorTimeout)); + } + })) { // Await while Pending - if (GetStatus() == Status.Pending) // wouldn't it be safer to await anyway to avoid possible race conditions? + if (GetStatus() == + Status.Pending) // wouldn't it be safer to await anyway to avoid possible race conditions? { await createTask.Task.ConfigureAwait(false); } @@ -814,17 +823,17 @@ public async Task ReadAsync(CancellationToken token = default) cts.CancelAfter(Timeout); using (cts.Token.Register(() => - { - if (readTasks.TryPop(out var readTask)) - { - Abort(); - - if (token.IsCancellationRequested) - readTask.SetCanceled(); - else - readTask.SetException(new LibPlcTagException(Status.ErrorTimeout)); - } - })) + { + if (readTasks.TryPop(out var readTask)) + { + Abort(); + + if (token.IsCancellationRequested) + readTask.SetCanceled(); + else + readTask.SetException(new LibPlcTagException(Status.ErrorTimeout)); + } + })) { var readTask = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); readTasks.Push(readTask); @@ -872,19 +881,20 @@ public async Task WriteAsync(CancellationToken token = default) cts.CancelAfter(Timeout); using (cts.Token.Register(() => + { + if (writeTasks.TryPop(out var writeTask)) + { + Abort(); + + if (token.IsCancellationRequested) + writeTask.SetCanceled(); + else + writeTask.SetException(new LibPlcTagException(Status.ErrorTimeout)); + } + })) { - if (writeTasks.TryPop(out var writeTask)) - { - Abort(); - - if (token.IsCancellationRequested) - writeTask.SetCanceled(); - else - writeTask.SetException(new LibPlcTagException(Status.ErrorTimeout)); - } - })) - { - var writeTask = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var writeTask = + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); writeTasks.Push(writeTask); _native.plc_tag_write(nativeTagHandle, TIMEOUT_VALUE_THAT_INDICATES_ASYNC_OPERATION); await writeTask.Task.ConfigureAwait(false); @@ -1008,7 +1018,8 @@ public byte[] GetByteArrayAttribute(string attributeName) var bufferLength = GetIntAttribute(bufferLengthAttributeName); var buffer = new byte[bufferLength]; - var result = _native.plc_tag_get_byte_array_attribute(nativeTagHandle, attributeName, buffer, buffer.Length); + var result = + _native.plc_tag_get_byte_array_attribute(nativeTagHandle, attributeName, buffer, buffer.Length); if (result < 0) throw new LibPlcTagException((Status)result); else @@ -1033,44 +1044,73 @@ public bool GetBit(int offset) throw new LibPlcTagException((Status)result); } - public void SetBit(int offset, bool value) => SetNativeTagValue(_native.plc_tag_set_bit, offset, value == true ? 1 : 0); + public void SetBit(int offset, bool value) => + SetNativeTagValue(_native.plc_tag_set_bit, offset, value == true ? 1 : 0); - public ulong GetUInt64(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_uint64, offset, ulong.MaxValue); - public void SetUInt64(int offset, ulong value) => SetNativeTagValue(_native.plc_tag_set_uint64, offset, value); + public ulong GetUInt64(int offset) => + GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_uint64, offset, ulong.MaxValue); - public long GetInt64(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_int64, offset, long.MinValue); - public void SetInt64(int offset, long value) => SetNativeTagValue(_native.plc_tag_set_int64, offset, value); + public void SetUInt64(int offset, ulong value) => SetNativeTagValue(_native.plc_tag_set_uint64, offset, value); - public uint GetUInt32(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_uint32, offset, uint.MaxValue); - public void SetUInt32(int offset, uint value) => SetNativeTagValue(_native.plc_tag_set_uint32, offset, value); + public long GetInt64(int offset) => + GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_int64, offset, long.MinValue); - public int GetInt32(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_int32, offset, int.MinValue); - public void SetInt32(int offset, int value) => SetNativeTagValue(_native.plc_tag_set_int32, offset, value); + public void SetInt64(int offset, long value) => SetNativeTagValue(_native.plc_tag_set_int64, offset, value); - public ushort GetUInt16(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_uint16, offset, ushort.MaxValue); - public void SetUInt16(int offset, ushort value) => SetNativeTagValue(_native.plc_tag_set_uint16, offset, value); + public uint GetUInt32(int offset) => + GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_uint32, offset, uint.MaxValue); - public short GetInt16(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_int16, offset, short.MinValue); - public void SetInt16(int offset, short value) => SetNativeTagValue(_native.plc_tag_set_int16, offset, value); + public void SetUInt32(int offset, uint value) => SetNativeTagValue(_native.plc_tag_set_uint32, offset, value); - public byte GetUInt8(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_uint8, offset, byte.MaxValue); - public void SetUInt8(int offset, byte value) => SetNativeTagValue(_native.plc_tag_set_uint8, offset, value); + public int GetInt32(int offset) => + GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_int32, offset, int.MinValue); - public sbyte GetInt8(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_int8, offset, sbyte.MinValue); - public void SetInt8(int offset, sbyte value) => SetNativeTagValue(_native.plc_tag_set_int8, offset, value); + public void SetInt32(int offset, int value) => SetNativeTagValue(_native.plc_tag_set_int32, offset, value); - public double GetFloat64(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_float64, offset, double.MinValue); - public void SetFloat64(int offset, double value) => SetNativeTagValue(_native.plc_tag_set_float64, offset, value); + public ushort GetUInt16(int offset) => + GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_uint16, offset, ushort.MaxValue); - public float GetFloat32(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_float32, offset, float.MinValue); - public void SetFloat32(int offset, float value) => SetNativeTagValue(_native.plc_tag_set_float32, offset, value); + public void SetUInt16(int offset, ushort value) => SetNativeTagValue(_native.plc_tag_set_uint16, offset, value); + public short GetInt16(int offset) => + GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_int16, offset, short.MinValue); + public void SetInt16(int offset, short value) => SetNativeTagValue(_native.plc_tag_set_int16, offset, value); + + public byte GetUInt8(int offset) => + GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_uint8, offset, byte.MaxValue); + + public void SetUInt8(int offset, byte value) => SetNativeTagValue(_native.plc_tag_set_uint8, offset, value); + + public sbyte GetInt8(int offset) => + GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_int8, offset, sbyte.MinValue); + + public void SetInt8(int offset, sbyte value) => SetNativeTagValue(_native.plc_tag_set_int8, offset, value); + + public double GetFloat64(int offset) => + GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_float64, offset, double.MinValue); + + public void SetFloat64(int offset, double value) => + SetNativeTagValue(_native.plc_tag_set_float64, offset, value); + + public float GetFloat32(int offset) => + GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_float32, offset, float.MinValue); + + public void SetFloat32(int offset, float value) => + SetNativeTagValue(_native.plc_tag_set_float32, offset, value); + + + public void SetString(int offset, string value) => SetNativeTagValue(_native.plc_tag_set_string, offset, value); + + public int GetStringLength(int offset) => + GetNativeValueAndThrowOnNegativeResult(_native.plc_tag_get_string_length, offset); + + public int GetStringCapacity(int offset) => + GetNativeValueAndThrowOnNegativeResult(_native.plc_tag_get_string_capacity, offset); + + public int GetStringTotalLength(int offset) => + GetNativeValueAndThrowOnNegativeResult(_native.plc_tag_get_string_total_length, offset); - public void SetString(int offset, string value) => SetNativeTagValue(_native.plc_tag_set_string, offset, value); - public int GetStringLength(int offset) => GetNativeValueAndThrowOnNegativeResult(_native.plc_tag_get_string_length, offset); - public int GetStringCapacity(int offset) => GetNativeValueAndThrowOnNegativeResult(_native.plc_tag_get_string_capacity, offset); - public int GetStringTotalLength(int offset) => GetNativeValueAndThrowOnNegativeResult(_native.plc_tag_get_string_total_length, offset); public string GetString(int offset) { ThrowIfAlreadyDisposed(); @@ -1116,7 +1156,6 @@ private void ThrowIfStatusNotOk(Status? status = null) } - private void SetNativeTagValue(Func nativeMethod, int offset, T value) { ThrowIfAlreadyDisposed(); @@ -1133,7 +1172,8 @@ private int GetNativeValueAndThrowOnNegativeResult(Func nativeMet return result; } - private T GetNativeValueAndThrowOnSpecificResult(Func nativeMethod, int offset, T valueIndicatingPossibleError) + private T GetNativeValueAndThrowOnSpecificResult(Func nativeMethod, int offset, + T valueIndicatingPossibleError) where T : struct { ThrowIfAlreadyDisposed(); @@ -1158,7 +1198,6 @@ private void SetField(ref T field, T value) private string GetAttributeString() { - string FormatNullableBoolean(bool? value) => value.HasValue ? (value.Value ? "1" : "0") : null; @@ -1172,7 +1211,7 @@ string FormatPlcType(PlcType? type) string FormatTimeSpan(TimeSpan? timespan) { - if(timespan.HasValue) + if (timespan.HasValue) return ((int)timespan.Value.TotalMilliseconds).ToString(); else return null; @@ -1180,58 +1219,52 @@ string FormatTimeSpan(TimeSpan? timespan) var attributes = new Dictionary { - { "protocol", Protocol?.ToString() }, - { "gateway", Gateway }, - { "path", Path }, - { "plc", FormatPlcType(PlcType) }, - { "elem_size", ElementSize?.ToString() }, - { "elem_count", ElementCount?.ToString() }, - { "name", Name }, - { "read_cache_ms", ReadCacheMillisecondDuration?.ToString() }, - { "use_connected_msg", FormatNullableBoolean(UseConnectedMessaging) }, - { "allow_packing", FormatNullableBoolean(AllowPacking) }, - { "auto_sync_read_ms", FormatTimeSpan(AutoSyncReadInterval) }, - { "auto_sync_write_ms", FormatTimeSpan(AutoSyncWriteInterval) }, - { "debug", DebugLevel == DebugLevel.None ? null : ((int)DebugLevel).ToString() }, - { "int16_byte_order", Int16ByteOrder }, - { "int32_byte_order", Int32ByteOrder }, - { "int64_byte_order", Int64ByteOrder }, - { "float32_byte_order", Float32ByteOrder }, - { "float64_byte_order", Float64ByteOrder }, - { "str_count_word_bytes", StringCountWordBytes?.ToString() }, - { "str_is_byte_swapped", FormatNullableBoolean(StringIsByteSwapped) }, - { "str_is_counted", FormatNullableBoolean(StringIsCounted) }, - { "str_is_fixed_length", FormatNullableBoolean(StringIsFixedLength) }, - { "str_is_zero_terminated", FormatNullableBoolean(StringIsFixedLength) }, - { "str_max_capacity", StringMaxCapacity?.ToString() }, - { "str_pad_bytes", StringPadBytes?.ToString() }, - { "str_total_length", StringTotalLength?.ToString() }, + { "protocol", Protocol?.ToString() }, + { "gateway", Gateway }, + { "path", Path }, + { "plc", FormatPlcType(PlcType) }, + { "elem_size", ElementSize?.ToString() }, + { "elem_count", ElementCount?.ToString() }, + { "name", Name }, + { "read_cache_ms", ReadCacheMillisecondDuration?.ToString() }, + { "use_connected_msg", FormatNullableBoolean(UseConnectedMessaging) }, + { "allow_packing", FormatNullableBoolean(AllowPacking) }, + { "auto_sync_read_ms", FormatTimeSpan(AutoSyncReadInterval) }, + { "auto_sync_write_ms", FormatTimeSpan(AutoSyncWriteInterval) }, + { "debug", DebugLevel == DebugLevel.None ? null : ((int)DebugLevel).ToString() }, + { "int16_byte_order", Int16ByteOrder }, + { "int32_byte_order", Int32ByteOrder }, + { "int64_byte_order", Int64ByteOrder }, + { "float32_byte_order", Float32ByteOrder }, + { "float64_byte_order", Float64ByteOrder }, + { "str_count_word_bytes", StringCountWordBytes?.ToString() }, + { "str_is_byte_swapped", FormatNullableBoolean(StringIsByteSwapped) }, + { "str_is_counted", FormatNullableBoolean(StringIsCounted) }, + { "str_is_fixed_length", FormatNullableBoolean(StringIsFixedLength) }, + { "str_is_zero_terminated", FormatNullableBoolean(StringIsFixedLength) }, + { "str_max_capacity", StringMaxCapacity?.ToString() }, + { "str_pad_bytes", StringPadBytes?.ToString() }, + { "str_total_length", StringTotalLength?.ToString() }, { "max_requests_in_flight", MaxRequestsInFlight?.ToString() }, - { "allow_field_resize", FormatNullableBoolean(AllowFieldResize) }, + { "allow_field_resize", FormatNullableBoolean(AllowFieldResize) }, }; string separator = "&"; - return string.Join(separator, attributes.Where(attr => attr.Value != null).Select(attr => $"{attr.Key}={attr.Value}")); - + return string.Join(separator, + attributes.Where(attr => attr.Value != null).Select(attr => $"{attr.Key}={attr.Value}")); } - - void SetUpEvents() { - // Used to finalize the asynchronous read/write task completion sources ReadCompleted += ReadTaskCompleter; WriteCompleted += WriteTaskCompleter; Created += CreatedTaskCompleter; - - } void RemoveEvents() { - // Used to finalize the read/write task completion sources ReadCompleted -= ReadTaskCompleter; WriteCompleted -= WriteTaskCompleter; @@ -1243,7 +1276,9 @@ Status RemoveCallback() return (Status)_native.plc_tag_unregister_callback(nativeTagHandle); } - private readonly ConcurrentStack> createTasks = new ConcurrentStack>(); + private readonly ConcurrentStack> createTasks = + new ConcurrentStack>(); + void CreatedTaskCompleter(object sender, TagEventArgs e) { if (createTasks.TryPop(out var createTask)) @@ -1260,7 +1295,9 @@ void CreatedTaskCompleter(object sender, TagEventArgs e) } } - private readonly ConcurrentStack> readTasks = new ConcurrentStack>(); + private readonly ConcurrentStack> readTasks = + new ConcurrentStack>(); + void ReadTaskCompleter(object sender, TagEventArgs e) { if (readTasks.TryPop(out var readTask)) @@ -1277,7 +1314,9 @@ void ReadTaskCompleter(object sender, TagEventArgs e) } } - private readonly ConcurrentStack> writeTasks = new ConcurrentStack>(); + private readonly ConcurrentStack> writeTasks = + new ConcurrentStack>(); + void WriteTaskCompleter(object sender, TagEventArgs e) { if (writeTasks.TryPop(out var writeTask)) @@ -1290,7 +1329,6 @@ void WriteTaskCompleter(object sender, TagEventArgs e) default: writeTask?.SetResult(e.Status); break; - } } } @@ -1336,7 +1374,5 @@ void coreLibEventCallback(int eventTagHandle, int eventCode, int statusCode, Int throw new NotImplementedException(); } } - } - -} +} \ No newline at end of file From c771d1f1a8dd9c0f28f90af80e67afef4615d7ed Mon Sep 17 00:00:00 2001 From: Sebastian Zitzelsberger Date: Fri, 19 Sep 2025 10:01:58 +0200 Subject: [PATCH 2/3] Remove formatting changes. --- src/libplctag/Tag.cs | 276 ++++++++++++++++++++----------------------- 1 file changed, 125 insertions(+), 151 deletions(-) diff --git a/src/libplctag/Tag.cs b/src/libplctag/Tag.cs index e8cb40e..35c2b9e 100644 --- a/src/libplctag/Tag.cs +++ b/src/libplctag/Tag.cs @@ -20,6 +20,7 @@ namespace libplctag { + public sealed class Tag : IDisposable { private const int TIMEOUT_VALUE_THAT_INDICATES_ASYNC_OPERATION = 0; @@ -208,7 +209,7 @@ public string Path get => GetField(ref _path); set => SetField(ref _path, value); } - + /// /// [OPTIONAL] /// An integer number of elements per tag. @@ -318,6 +319,7 @@ public int? ReadCacheMillisecondDuration { ThrowIfAlreadyDisposed(); return _readCacheMillisecondDuration; + } set { @@ -325,10 +327,11 @@ public int? ReadCacheMillisecondDuration if (_isInitialized) SetIntAttribute("read_cache_ms", value.Value); - + // Set after writing to underlying tag in case SetIntAttribute fails. // Ensures the two have the same value. _readCacheMillisecondDuration = value; + } } @@ -355,11 +358,11 @@ public TimeSpan? AutoSyncReadInterval if (_isInitialized) { if (value is null) - SetIntAttribute("auto_sync_read_ms", 0); // 0 is a special value that turns off auto sync + SetIntAttribute("auto_sync_read_ms", 0); // 0 is a special value that turns off auto sync else SetIntAttribute("auto_sync_read_ms", (int)value.Value.TotalMilliseconds); } - + // Set after writing to underlying tag in case SetIntAttribute fails. // Ensures the two have the same value. _autoSyncReadInterval = value; @@ -390,11 +393,11 @@ public TimeSpan? AutoSyncWriteInterval if (_isInitialized) { if (value is null) - SetIntAttribute("auto_sync_write_ms", 0); // 0 is a special value that turns off auto sync + SetIntAttribute("auto_sync_write_ms", 0); // 0 is a special value that turns off auto sync else SetIntAttribute("auto_sync_write_ms", (int)value.Value.TotalMilliseconds); } - + // Set after writing to underlying tag in case SetIntAttribute fails. // Ensures the two have the same value. _autoSyncWriteInterval = value; @@ -573,7 +576,6 @@ public bool? StringIsZeroTerminated } private uint? _stringMaxCapacity; - /// /// [OPTIONAL] /// Determines the maximum number of character bytes in a string. @@ -668,6 +670,7 @@ public void Abort() } + /// /// Creates the underlying data structures and references required before tag operations. /// @@ -679,6 +682,7 @@ public void Abort() /// public void Initialize() { + ThrowIfAlreadyDisposed(); ThrowIfAlreadyInitialized(); @@ -687,9 +691,8 @@ public void Initialize() SetUpEvents(); var attributeString = GetAttributeString(); - - var result = _native.plc_tag_create_ex(attributeString, coreLibCallbackFuncExDelegate, IntPtr.Zero, - millisecondTimeout); + + var result = _native.plc_tag_create_ex(attributeString, coreLibCallbackFuncExDelegate, IntPtr.Zero, millisecondTimeout); if (result < 0) { RemoveEvents(); @@ -724,8 +727,7 @@ public async Task InitializeAsync(CancellationToken token = default) createTasks.Push(createTask); var attributeString = GetAttributeString(); - var result = _native.plc_tag_create_ex(attributeString, coreLibCallbackFuncExDelegate, IntPtr.Zero, - TIMEOUT_VALUE_THAT_INDICATES_ASYNC_OPERATION); + var result = _native.plc_tag_create_ex(attributeString, coreLibCallbackFuncExDelegate, IntPtr.Zero, TIMEOUT_VALUE_THAT_INDICATES_ASYNC_OPERATION); // Something went wrong locally if (result < 0) @@ -744,25 +746,24 @@ public async Task InitializeAsync(CancellationToken token = default) cts.CancelAfter(Timeout); using (cts.Token.Register(() => - { - if (createTasks.TryPop(out _)) - { - RemoveCallback(); - Abort(); - var destroyResult = (Status)_native.plc_tag_destroy(result); - Debug.Assert(destroyResult == Status.Ok); - RemoveEvents(); - - if (token.IsCancellationRequested) - createTask.SetCanceled(); - else - createTask.SetException(new LibPlcTagException(Status.ErrorTimeout)); - } - })) + { + if (createTasks.TryPop(out _)) + { + RemoveCallback(); + Abort(); + var destroyResult = (Status)_native.plc_tag_destroy(result); + Debug.Assert(destroyResult == Status.Ok); + RemoveEvents(); + + if (token.IsCancellationRequested) + createTask.SetCanceled(); + else + createTask.SetException(new LibPlcTagException(Status.ErrorTimeout)); + } + })) { // Await while Pending - if (GetStatus() == - Status.Pending) // wouldn't it be safer to await anyway to avoid possible race conditions? + if (GetStatus() == Status.Pending) // wouldn't it be safer to await anyway to avoid possible race conditions? { await createTask.Task.ConfigureAwait(false); } @@ -823,17 +824,17 @@ public async Task ReadAsync(CancellationToken token = default) cts.CancelAfter(Timeout); using (cts.Token.Register(() => - { - if (readTasks.TryPop(out var readTask)) - { - Abort(); - - if (token.IsCancellationRequested) - readTask.SetCanceled(); - else - readTask.SetException(new LibPlcTagException(Status.ErrorTimeout)); - } - })) + { + if (readTasks.TryPop(out var readTask)) + { + Abort(); + + if (token.IsCancellationRequested) + readTask.SetCanceled(); + else + readTask.SetException(new LibPlcTagException(Status.ErrorTimeout)); + } + })) { var readTask = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); readTasks.Push(readTask); @@ -881,20 +882,19 @@ public async Task WriteAsync(CancellationToken token = default) cts.CancelAfter(Timeout); using (cts.Token.Register(() => - { - if (writeTasks.TryPop(out var writeTask)) - { - Abort(); - - if (token.IsCancellationRequested) - writeTask.SetCanceled(); - else - writeTask.SetException(new LibPlcTagException(Status.ErrorTimeout)); - } - })) { - var writeTask = - new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + if (writeTasks.TryPop(out var writeTask)) + { + Abort(); + + if (token.IsCancellationRequested) + writeTask.SetCanceled(); + else + writeTask.SetException(new LibPlcTagException(Status.ErrorTimeout)); + } + })) + { + var writeTask = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); writeTasks.Push(writeTask); _native.plc_tag_write(nativeTagHandle, TIMEOUT_VALUE_THAT_INDICATES_ASYNC_OPERATION); await writeTask.Task.ConfigureAwait(false); @@ -1018,8 +1018,7 @@ public byte[] GetByteArrayAttribute(string attributeName) var bufferLength = GetIntAttribute(bufferLengthAttributeName); var buffer = new byte[bufferLength]; - var result = - _native.plc_tag_get_byte_array_attribute(nativeTagHandle, attributeName, buffer, buffer.Length); + var result = _native.plc_tag_get_byte_array_attribute(nativeTagHandle, attributeName, buffer, buffer.Length); if (result < 0) throw new LibPlcTagException((Status)result); else @@ -1044,73 +1043,44 @@ public bool GetBit(int offset) throw new LibPlcTagException((Status)result); } - public void SetBit(int offset, bool value) => - SetNativeTagValue(_native.plc_tag_set_bit, offset, value == true ? 1 : 0); - - public ulong GetUInt64(int offset) => - GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_uint64, offset, ulong.MaxValue); - - public void SetUInt64(int offset, ulong value) => SetNativeTagValue(_native.plc_tag_set_uint64, offset, value); - - public long GetInt64(int offset) => - GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_int64, offset, long.MinValue); + public void SetBit(int offset, bool value) => SetNativeTagValue(_native.plc_tag_set_bit, offset, value == true ? 1 : 0); - public void SetInt64(int offset, long value) => SetNativeTagValue(_native.plc_tag_set_int64, offset, value); + public ulong GetUInt64(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_uint64, offset, ulong.MaxValue); + public void SetUInt64(int offset, ulong value) => SetNativeTagValue(_native.plc_tag_set_uint64, offset, value); - public uint GetUInt32(int offset) => - GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_uint32, offset, uint.MaxValue); + public long GetInt64(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_int64, offset, long.MinValue); + public void SetInt64(int offset, long value) => SetNativeTagValue(_native.plc_tag_set_int64, offset, value); - public void SetUInt32(int offset, uint value) => SetNativeTagValue(_native.plc_tag_set_uint32, offset, value); + public uint GetUInt32(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_uint32, offset, uint.MaxValue); + public void SetUInt32(int offset, uint value) => SetNativeTagValue(_native.plc_tag_set_uint32, offset, value); - public int GetInt32(int offset) => - GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_int32, offset, int.MinValue); + public int GetInt32(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_int32, offset, int.MinValue); + public void SetInt32(int offset, int value) => SetNativeTagValue(_native.plc_tag_set_int32, offset, value); - public void SetInt32(int offset, int value) => SetNativeTagValue(_native.plc_tag_set_int32, offset, value); + public ushort GetUInt16(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_uint16, offset, ushort.MaxValue); + public void SetUInt16(int offset, ushort value) => SetNativeTagValue(_native.plc_tag_set_uint16, offset, value); - public ushort GetUInt16(int offset) => - GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_uint16, offset, ushort.MaxValue); + public short GetInt16(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_int16, offset, short.MinValue); + public void SetInt16(int offset, short value) => SetNativeTagValue(_native.plc_tag_set_int16, offset, value); - public void SetUInt16(int offset, ushort value) => SetNativeTagValue(_native.plc_tag_set_uint16, offset, value); + public byte GetUInt8(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_uint8, offset, byte.MaxValue); + public void SetUInt8(int offset, byte value) => SetNativeTagValue(_native.plc_tag_set_uint8, offset, value); - public short GetInt16(int offset) => - GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_int16, offset, short.MinValue); + public sbyte GetInt8(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_int8, offset, sbyte.MinValue); + public void SetInt8(int offset, sbyte value) => SetNativeTagValue(_native.plc_tag_set_int8, offset, value); - public void SetInt16(int offset, short value) => SetNativeTagValue(_native.plc_tag_set_int16, offset, value); + public double GetFloat64(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_float64, offset, double.MinValue); + public void SetFloat64(int offset, double value) => SetNativeTagValue(_native.plc_tag_set_float64, offset, value); - public byte GetUInt8(int offset) => - GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_uint8, offset, byte.MaxValue); + public float GetFloat32(int offset) => GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_float32, offset, float.MinValue); + public void SetFloat32(int offset, float value) => SetNativeTagValue(_native.plc_tag_set_float32, offset, value); - public void SetUInt8(int offset, byte value) => SetNativeTagValue(_native.plc_tag_set_uint8, offset, value); - public sbyte GetInt8(int offset) => - GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_int8, offset, sbyte.MinValue); - - public void SetInt8(int offset, sbyte value) => SetNativeTagValue(_native.plc_tag_set_int8, offset, value); - - public double GetFloat64(int offset) => - GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_float64, offset, double.MinValue); - - public void SetFloat64(int offset, double value) => - SetNativeTagValue(_native.plc_tag_set_float64, offset, value); - - public float GetFloat32(int offset) => - GetNativeValueAndThrowOnSpecificResult(_native.plc_tag_get_float32, offset, float.MinValue); - - public void SetFloat32(int offset, float value) => - SetNativeTagValue(_native.plc_tag_set_float32, offset, value); - - - public void SetString(int offset, string value) => SetNativeTagValue(_native.plc_tag_set_string, offset, value); - - public int GetStringLength(int offset) => - GetNativeValueAndThrowOnNegativeResult(_native.plc_tag_get_string_length, offset); - - public int GetStringCapacity(int offset) => - GetNativeValueAndThrowOnNegativeResult(_native.plc_tag_get_string_capacity, offset); - - public int GetStringTotalLength(int offset) => - GetNativeValueAndThrowOnNegativeResult(_native.plc_tag_get_string_total_length, offset); + public void SetString(int offset, string value) => SetNativeTagValue(_native.plc_tag_set_string, offset, value); + public int GetStringLength(int offset) => GetNativeValueAndThrowOnNegativeResult(_native.plc_tag_get_string_length, offset); + public int GetStringCapacity(int offset) => GetNativeValueAndThrowOnNegativeResult(_native.plc_tag_get_string_capacity, offset); + public int GetStringTotalLength(int offset) => GetNativeValueAndThrowOnNegativeResult(_native.plc_tag_get_string_total_length, offset); public string GetString(int offset) { ThrowIfAlreadyDisposed(); @@ -1156,6 +1126,7 @@ private void ThrowIfStatusNotOk(Status? status = null) } + private void SetNativeTagValue(Func nativeMethod, int offset, T value) { ThrowIfAlreadyDisposed(); @@ -1172,8 +1143,7 @@ private int GetNativeValueAndThrowOnNegativeResult(Func nativeMet return result; } - private T GetNativeValueAndThrowOnSpecificResult(Func nativeMethod, int offset, - T valueIndicatingPossibleError) + private T GetNativeValueAndThrowOnSpecificResult(Func nativeMethod, int offset, T valueIndicatingPossibleError) where T : struct { ThrowIfAlreadyDisposed(); @@ -1198,6 +1168,7 @@ private void SetField(ref T field, T value) private string GetAttributeString() { + string FormatNullableBoolean(bool? value) => value.HasValue ? (value.Value ? "1" : "0") : null; @@ -1211,7 +1182,7 @@ string FormatPlcType(PlcType? type) string FormatTimeSpan(TimeSpan? timespan) { - if (timespan.HasValue) + if(timespan.HasValue) return ((int)timespan.Value.TotalMilliseconds).ToString(); else return null; @@ -1219,52 +1190,58 @@ string FormatTimeSpan(TimeSpan? timespan) var attributes = new Dictionary { - { "protocol", Protocol?.ToString() }, - { "gateway", Gateway }, - { "path", Path }, - { "plc", FormatPlcType(PlcType) }, - { "elem_size", ElementSize?.ToString() }, - { "elem_count", ElementCount?.ToString() }, - { "name", Name }, - { "read_cache_ms", ReadCacheMillisecondDuration?.ToString() }, - { "use_connected_msg", FormatNullableBoolean(UseConnectedMessaging) }, - { "allow_packing", FormatNullableBoolean(AllowPacking) }, - { "auto_sync_read_ms", FormatTimeSpan(AutoSyncReadInterval) }, - { "auto_sync_write_ms", FormatTimeSpan(AutoSyncWriteInterval) }, - { "debug", DebugLevel == DebugLevel.None ? null : ((int)DebugLevel).ToString() }, - { "int16_byte_order", Int16ByteOrder }, - { "int32_byte_order", Int32ByteOrder }, - { "int64_byte_order", Int64ByteOrder }, - { "float32_byte_order", Float32ByteOrder }, - { "float64_byte_order", Float64ByteOrder }, - { "str_count_word_bytes", StringCountWordBytes?.ToString() }, - { "str_is_byte_swapped", FormatNullableBoolean(StringIsByteSwapped) }, - { "str_is_counted", FormatNullableBoolean(StringIsCounted) }, - { "str_is_fixed_length", FormatNullableBoolean(StringIsFixedLength) }, - { "str_is_zero_terminated", FormatNullableBoolean(StringIsFixedLength) }, - { "str_max_capacity", StringMaxCapacity?.ToString() }, - { "str_pad_bytes", StringPadBytes?.ToString() }, - { "str_total_length", StringTotalLength?.ToString() }, + { "protocol", Protocol?.ToString() }, + { "gateway", Gateway }, + { "path", Path }, + { "plc", FormatPlcType(PlcType) }, + { "elem_size", ElementSize?.ToString() }, + { "elem_count", ElementCount?.ToString() }, + { "name", Name }, + { "read_cache_ms", ReadCacheMillisecondDuration?.ToString() }, + { "use_connected_msg", FormatNullableBoolean(UseConnectedMessaging) }, + { "allow_packing", FormatNullableBoolean(AllowPacking) }, + { "auto_sync_read_ms", FormatTimeSpan(AutoSyncReadInterval) }, + { "auto_sync_write_ms", FormatTimeSpan(AutoSyncWriteInterval) }, + { "debug", DebugLevel == DebugLevel.None ? null : ((int)DebugLevel).ToString() }, + { "int16_byte_order", Int16ByteOrder }, + { "int32_byte_order", Int32ByteOrder }, + { "int64_byte_order", Int64ByteOrder }, + { "float32_byte_order", Float32ByteOrder }, + { "float64_byte_order", Float64ByteOrder }, + { "str_count_word_bytes", StringCountWordBytes?.ToString() }, + { "str_is_byte_swapped", FormatNullableBoolean(StringIsByteSwapped) }, + { "str_is_counted", FormatNullableBoolean(StringIsCounted) }, + { "str_is_fixed_length", FormatNullableBoolean(StringIsFixedLength) }, + { "str_is_zero_terminated", FormatNullableBoolean(StringIsFixedLength) }, + { "str_max_capacity", StringMaxCapacity?.ToString() }, + { "str_pad_bytes", StringPadBytes?.ToString() }, + { "str_total_length", StringTotalLength?.ToString() }, { "max_requests_in_flight", MaxRequestsInFlight?.ToString() }, - { "allow_field_resize", FormatNullableBoolean(AllowFieldResize) }, + { "allow_field_resize", FormatNullableBoolean(AllowFieldResize) }, }; string separator = "&"; - return string.Join(separator, - attributes.Where(attr => attr.Value != null).Select(attr => $"{attr.Key}={attr.Value}")); + return string.Join(separator, attributes.Where(attr => attr.Value != null).Select(attr => $"{attr.Key}={attr.Value}")); + } + + void SetUpEvents() { + // Used to finalize the asynchronous read/write task completion sources ReadCompleted += ReadTaskCompleter; WriteCompleted += WriteTaskCompleter; Created += CreatedTaskCompleter; + + } void RemoveEvents() { + // Used to finalize the read/write task completion sources ReadCompleted -= ReadTaskCompleter; WriteCompleted -= WriteTaskCompleter; @@ -1276,9 +1253,7 @@ Status RemoveCallback() return (Status)_native.plc_tag_unregister_callback(nativeTagHandle); } - private readonly ConcurrentStack> createTasks = - new ConcurrentStack>(); - + private readonly ConcurrentStack> createTasks = new ConcurrentStack>(); void CreatedTaskCompleter(object sender, TagEventArgs e) { if (createTasks.TryPop(out var createTask)) @@ -1295,9 +1270,7 @@ void CreatedTaskCompleter(object sender, TagEventArgs e) } } - private readonly ConcurrentStack> readTasks = - new ConcurrentStack>(); - + private readonly ConcurrentStack> readTasks = new ConcurrentStack>(); void ReadTaskCompleter(object sender, TagEventArgs e) { if (readTasks.TryPop(out var readTask)) @@ -1314,9 +1287,7 @@ void ReadTaskCompleter(object sender, TagEventArgs e) } } - private readonly ConcurrentStack> writeTasks = - new ConcurrentStack>(); - + private readonly ConcurrentStack> writeTasks = new ConcurrentStack>(); void WriteTaskCompleter(object sender, TagEventArgs e) { if (writeTasks.TryPop(out var writeTask)) @@ -1329,6 +1300,7 @@ void WriteTaskCompleter(object sender, TagEventArgs e) default: writeTask?.SetResult(e.Status); break; + } } } @@ -1374,5 +1346,7 @@ void coreLibEventCallback(int eventTagHandle, int eventCode, int statusCode, Int throw new NotImplementedException(); } } + } -} \ No newline at end of file + +} From 5c4c450cf0b58397bc291d639f36374795725d72 Mon Sep 17 00:00:00 2001 From: Sebastian Zitzelsberger Date: Wed, 24 Sep 2025 10:55:20 +0200 Subject: [PATCH 3/3] Add some examples how reusable logic could be created for stubbing device or tag behaviour. --- src/libplctag.Tests/libplctag.Tests.csproj | 2 + src/libplctag.Tests/stubbing/IDeviceStub.cs | 25 ++ .../NativeToDeviceStubsDispatchProxy.cs | 56 ++++ src/libplctag.Tests/stubbing/StubRouting.cs | 38 +++ src/libplctag.Tests/stubbing/TagStub.cs | 80 +++++ .../stubbing/examples/ModeTagStub.cs | 300 ++++++++++++++++++ .../stubbing/examples/MyControlLogixDevice.cs | 75 +++++ .../examples/MyControlLogixDeviceTests.cs | 49 +++ .../stubbing/examples/TagBrowsingStub.cs | 289 +++++++++++++++++ src/libplctag/Tag.cs | 2 +- 10 files changed, 915 insertions(+), 1 deletion(-) create mode 100644 src/libplctag.Tests/stubbing/IDeviceStub.cs create mode 100644 src/libplctag.Tests/stubbing/NativeToDeviceStubsDispatchProxy.cs create mode 100644 src/libplctag.Tests/stubbing/StubRouting.cs create mode 100644 src/libplctag.Tests/stubbing/TagStub.cs create mode 100644 src/libplctag.Tests/stubbing/examples/ModeTagStub.cs create mode 100644 src/libplctag.Tests/stubbing/examples/MyControlLogixDevice.cs create mode 100644 src/libplctag.Tests/stubbing/examples/MyControlLogixDeviceTests.cs create mode 100644 src/libplctag.Tests/stubbing/examples/TagBrowsingStub.cs diff --git a/src/libplctag.Tests/libplctag.Tests.csproj b/src/libplctag.Tests/libplctag.Tests.csproj index fd7714b..45b4137 100644 --- a/src/libplctag.Tests/libplctag.Tests.csproj +++ b/src/libplctag.Tests/libplctag.Tests.csproj @@ -4,6 +4,8 @@ net8.0 false + + enable diff --git a/src/libplctag.Tests/stubbing/IDeviceStub.cs b/src/libplctag.Tests/stubbing/IDeviceStub.cs new file mode 100644 index 0000000..5e1c0b4 --- /dev/null +++ b/src/libplctag.Tests/stubbing/IDeviceStub.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace libplctag.Tests.stubbing +{ + // + + public interface IDeviceStub : INative + { + List MockedTags { get; } + } + + + public static class DeviceStubExtensions + { + public static bool ShouldHandleCallsForTag(this IDeviceStub stub, int tag) + { + return stub.MockedTags.Exists(tagHandle => tagHandle.IsResponsibleForTag(tag)); + } + + public static bool ShouldHandleCallsForLpString(this IDeviceStub stub, string lpString) + { + return stub.MockedTags.Exists(tagHandle => tagHandle.IsResponsibleForLpString(lpString)); + } + } +} \ No newline at end of file diff --git a/src/libplctag.Tests/stubbing/NativeToDeviceStubsDispatchProxy.cs b/src/libplctag.Tests/stubbing/NativeToDeviceStubsDispatchProxy.cs new file mode 100644 index 0000000..1e80a7b --- /dev/null +++ b/src/libplctag.Tests/stubbing/NativeToDeviceStubsDispatchProxy.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using static libplctag.Tests.stubbing.StubRouting; + +namespace libplctag.Tests.stubbing +{ + /// + /// Responsible for creating a proxy for the INative interface and then delegating calls to it to the matching registered DeviceStub. + /// + public class NativeToDeviceStubsDispatchProxy : DispatchProxy + { + private List DeviceMocks { get; set; } = []; + + protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) + { + IDeviceStub? deviceMock = FindTargetDeviceMock(targetMethod, args); + + return deviceMock == null + ? throw new Exception($"No device-mock found for handling target method {targetMethod}") + : targetMethod!.Invoke(deviceMock, args); + } + + + private IDeviceStub? FindTargetDeviceMock(MethodInfo? targetMethod, object?[]? args) + { + int indexOfTagParameter = FindIndexOfTagParameter(targetMethod); + + if (indexOfTagParameter == -1) + { + int indexOfLpStringParameter = FindIndexOfLpStringParameter(targetMethod); + + if (indexOfLpStringParameter == -1) + { + return null; + } + + string lpsString = (string)args![indexOfLpStringParameter]!; + return DeviceMocks.Find(deviceMock => deviceMock.ShouldHandleCallsForLpString(lpsString)); + } + + int tag = (int)args![indexOfTagParameter]!; + + return DeviceMocks.Find(deviceMock => deviceMock.ShouldHandleCallsForTag(tag)); + } + + + public static INative Create(List tagMocks) + { + var iNativeProxy = Create(); + NativeToDeviceStubsDispatchProxy dispatchProxy = (NativeToDeviceStubsDispatchProxy)iNativeProxy; + dispatchProxy.DeviceMocks = tagMocks; + return iNativeProxy; + } + } +} \ No newline at end of file diff --git a/src/libplctag.Tests/stubbing/StubRouting.cs b/src/libplctag.Tests/stubbing/StubRouting.cs new file mode 100644 index 0000000..aad6bee --- /dev/null +++ b/src/libplctag.Tests/stubbing/StubRouting.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace libplctag.Tests.stubbing +{ + /// + /// Helper methods to determine which stub should be called for invocations of methods of the INative interface + /// depending on the tag and the lpString parameters. + /// + public static class StubRouting + { + private const string TagParameterName = "tag"; + private const string LpStringParameterName = "lpString"; + + public static int FindIndexOfTagParameter(MethodInfo? targetMethod) + { + List parameters = targetMethod?.GetParameters().ToList() ?? []; + return parameters.FindIndex(IsTagParameter); + } + + public static int FindIndexOfLpStringParameter(MethodInfo? targetMethod) + { + List parameters = targetMethod?.GetParameters().ToList() ?? []; + return parameters.FindIndex(IsLpStringParameter); + } + + private static bool IsLpStringParameter(ParameterInfo info) + { + return info.Name == LpStringParameterName && info.ParameterType == typeof(string); + } + + private static bool IsTagParameter(ParameterInfo info) + { + return info.Name == TagParameterName && info.ParameterType == typeof(int); + } + } +} \ No newline at end of file diff --git a/src/libplctag.Tests/stubbing/TagStub.cs b/src/libplctag.Tests/stubbing/TagStub.cs new file mode 100644 index 0000000..4b0aebe --- /dev/null +++ b/src/libplctag.Tests/stubbing/TagStub.cs @@ -0,0 +1,80 @@ +using libplctag.NativeImport; +using System; +using System.Text; + +namespace libplctag.Tests.stubbing +{ + public abstract class TagStub(int tagIdentifier, Tag tag) : INative + { + public readonly int TagIdentifier = tagIdentifier; + public readonly Tag Tag = tag; // We actually do not require the real tag, just its configuration parameters to match the Attribute-string, but there is no model-object for that + public readonly string AttributeString = tag.GetAttributeString(); + + public virtual bool IsResponsibleForLpString(string lpString) + { + return AttributeString == lpString; + } + + public virtual bool IsResponsibleForTag(int tagIdentifier) + { + return TagIdentifier == tagIdentifier; + } + + public abstract int plc_tag_abort(int tag); + public abstract int plc_tag_check_lib_version(int req_major, int req_minor, int req_patch); + public abstract int plc_tag_create(string lpString, int timeout); + public abstract int plc_tag_create_ex(string lpString, plctag.callback_func_ex func, IntPtr userdata, int timeout); + public abstract string plc_tag_decode_error(int err); + + + public abstract int plc_tag_destroy(int tag); + public abstract int plc_tag_get_bit(int tag, int offset_bit); + public abstract float plc_tag_get_float32(int tag, int offset); + public abstract double plc_tag_get_float64(int tag, int offset); + public abstract short plc_tag_get_int16(int tag, int offset); + public abstract int plc_tag_get_int32(int tag, int offset); + public abstract long plc_tag_get_int64(int tag, int offset); + public abstract sbyte plc_tag_get_int8(int tag, int offset); + public abstract int plc_tag_get_int_attribute(int tag, string attrib_name, int default_value); + public abstract int plc_tag_set_int_attribute(int tag, string attrib_name, int new_value); + public abstract int plc_tag_get_byte_array_attribute(int tag, string attrib_name, byte[] buffer, int buffer_length); + public abstract int plc_tag_get_size(int tag); + public abstract int plc_tag_set_size(int tag, int new_size); + public abstract ushort plc_tag_get_uint16(int tag, int offset); + public abstract uint plc_tag_get_uint32(int tag, int offset); + public abstract ulong plc_tag_get_uint64(int tag, int offset); + public abstract byte plc_tag_get_uint8(int tag, int offset); + public abstract int plc_tag_lock(int tag); + public abstract int plc_tag_read(int tag, int timeout); + public abstract int plc_tag_register_callback(int tag_id, plctag.callback_func func); + public abstract int plc_tag_register_logger(plctag.log_callback_func func); + public abstract int plc_tag_set_bit(int tag, int offset_bit, int val); + public abstract void plc_tag_set_debug_level(int debug_level); + public abstract int plc_tag_set_float32(int tag, int offset, float val); + public abstract int plc_tag_set_float64(int tag, int offset, double val); + public abstract int plc_tag_set_int16(int tag, int offset, short val); + public abstract int plc_tag_set_int32(int tag, int offset, int val); + public abstract int plc_tag_set_int64(int tag, int offset, long val); + public abstract int plc_tag_set_int8(int tag, int offset, sbyte val); + public abstract int plc_tag_set_uint16(int tag, int offset, ushort val); + public abstract int plc_tag_set_uint32(int tag, int offset, uint val); + public abstract int plc_tag_set_uint64(int tag, int offset, ulong val); + public abstract int plc_tag_set_uint8(int tag, int offset, byte val); + public abstract void plc_tag_shutdown(); + public abstract int plc_tag_status(int tag); + public abstract int plc_tag_unlock(int tag); + public abstract int plc_tag_unregister_callback(int tag_id); + public abstract int plc_tag_unregister_logger(int tag_id); + public abstract int plc_tag_write(int tag, int timeout); + public abstract int plc_tag_get_raw_bytes(int tag, int start_offset, byte[] buffer, int buffer_length); + public abstract int plc_tag_set_raw_bytes(int tag, int start_offset, byte[] buffer, int buffer_length); + public abstract int plc_tag_get_string_length(int tag, int string_start_offset); + public abstract int plc_tag_get_string(int tag, int string_start_offset, StringBuilder buffer, int buffer_length); + public abstract int plc_tag_get_string_total_length(int tag, int string_start_offset); + public abstract int plc_tag_get_string_capacity(int tag, int string_start_offset); + public abstract int plc_tag_set_string(int tag, int string_start_offset, string string_val); + } + + + +} \ No newline at end of file diff --git a/src/libplctag.Tests/stubbing/examples/ModeTagStub.cs b/src/libplctag.Tests/stubbing/examples/ModeTagStub.cs new file mode 100644 index 0000000..46986e4 --- /dev/null +++ b/src/libplctag.Tests/stubbing/examples/ModeTagStub.cs @@ -0,0 +1,300 @@ +using libplctag.NativeImport; +using System; +using System.Text; + +namespace libplctag.Tests.stubbing.examples +{ + public class ModeTagStub(int tagIdentifier, Tag tag) : TagStub(tagIdentifier, tag) + { + public static readonly byte[] ProgramMode = [0x01, 0x00, 0x00, 0x00]; + public static readonly byte[] RunMode = [0x02, 0x00, 0x00, 0x00]; + public static readonly int ModeTagHandle = 1; + private int _counter = 0; + + + public static Tag CreateModeTag(INative native) + { + Tag mode = new(native) + { + Gateway = MyControlLogixDevice.Gateway, + Path = MyControlLogixDevice.Path, + Protocol = MyControlLogixDevice.Protocol, + PlcType = MyControlLogixDevice.PlcType, + Name = "@Mode", + ElementSize = 4, + Timeout = MyControlLogixDevice.Timeout + }; + return mode; + } + + + public override int plc_tag_abort(int tag) + { + throw new NotImplementedException(); + } + + public override int plc_tag_check_lib_version(int req_major, int req_minor, int req_patch) + { + throw new NotImplementedException(); + } + + public override int plc_tag_create(string lpString, int timeout) + { + return ModeTagHandle; + } + + public override int plc_tag_create_ex(string lpString, plctag.callback_func_ex func, IntPtr userdata, int timeout) + { + return ModeTagHandle; + } + + public override string plc_tag_decode_error(int err) + { + throw new NotImplementedException(); + } + + public override int plc_tag_destroy(int tag) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_bit(int tag, int offset_bit) + { + throw new NotImplementedException(); + } + + public override float plc_tag_get_float32(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override double plc_tag_get_float64(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override short plc_tag_get_int16(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_int32(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override long plc_tag_get_int64(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override sbyte plc_tag_get_int8(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_int_attribute(int tag, string attrib_name, int default_value) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_int_attribute(int tag, string attrib_name, int new_value) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_byte_array_attribute(int tag, string attrib_name, byte[] buffer, int buffer_length) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_size(int tag) + { + return 4; + } + + public override int plc_tag_set_size(int tag, int new_size) + { + throw new NotImplementedException(); + } + + public override ushort plc_tag_get_uint16(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override uint plc_tag_get_uint32(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override ulong plc_tag_get_uint64(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override byte plc_tag_get_uint8(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override int plc_tag_lock(int tag) + { + throw new NotImplementedException(); + } + + public override int plc_tag_read(int tag, int timeout) + { + throw new NotImplementedException(); + } + + public override int plc_tag_register_callback(int tag_id, plctag.callback_func func) + { + throw new NotImplementedException(); + } + + public override int plc_tag_register_logger(plctag.log_callback_func func) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_bit(int tag, int offset_bit, int val) + { + throw new NotImplementedException(); + } + + public override void plc_tag_set_debug_level(int debug_level) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_float32(int tag, int offset, float val) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_float64(int tag, int offset, double val) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_int16(int tag, int offset, short val) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_int32(int tag, int offset, int val) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_int64(int tag, int offset, long val) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_int8(int tag, int offset, sbyte val) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_uint16(int tag, int offset, ushort val) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_uint32(int tag, int offset, uint val) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_uint64(int tag, int offset, ulong val) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_uint8(int tag, int offset, byte val) + { + throw new NotImplementedException(); + } + + public override void plc_tag_shutdown() + { + throw new NotImplementedException(); + } + + public override int plc_tag_status(int tag) + { + throw new NotImplementedException(); + } + + public override int plc_tag_unlock(int tag) + { + throw new NotImplementedException(); + } + + public override int plc_tag_unregister_callback(int tag_id) + { + throw new NotImplementedException(); + } + + public override int plc_tag_unregister_logger(int tag_id) + { + throw new NotImplementedException(); + } + + public override int plc_tag_write(int tag, int timeout) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_raw_bytes(int tag, int start_offset, byte[] buffer, int buffer_length) + { + + switch (_counter) + { + case 0: Array.Copy(ProgramMode, buffer, ProgramMode.Length); break; + case 1: Array.Copy(RunMode, buffer, RunMode.Length); break; + case 2: throw new TimeoutException(); + } + + // Move to the next method, wrap around + _counter = (_counter + 1) % 3; + + + return (int)Status.Ok; + } + + public override int plc_tag_set_raw_bytes(int tag, int start_offset, byte[] buffer, int buffer_length) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_string_length(int tag, int string_start_offset) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_string(int tag, int string_start_offset, StringBuilder buffer, int buffer_length) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_string_total_length(int tag, int string_start_offset) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_string_capacity(int tag, int string_start_offset) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_string(int tag, int string_start_offset, string string_val) + { + throw new NotImplementedException(); + } + + + } +} \ No newline at end of file diff --git a/src/libplctag.Tests/stubbing/examples/MyControlLogixDevice.cs b/src/libplctag.Tests/stubbing/examples/MyControlLogixDevice.cs new file mode 100644 index 0000000..9ab0275 --- /dev/null +++ b/src/libplctag.Tests/stubbing/examples/MyControlLogixDevice.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using static libplctag.Tests.stubbing.StubRouting; + +namespace libplctag.Tests.stubbing.examples +{ + public class MyControlLogixDevice : DispatchProxy + { + public static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2); + public const string Gateway = "127.0.0"; + public const string Path = "1,0"; + public const Protocol Protocol = libplctag.Protocol.ab_eip; + public const PlcType PlcType = libplctag.PlcType.ControlLogix; + + public List MockedTags { get; set; } = []; + + public static IDeviceStub Create() + { + var tagProxy = Create(); + MyControlLogixDevice device = (MyControlLogixDevice)tagProxy; + + Tag mode = ModeTagStub.CreateModeTag(new Native()); + // Register additional TagStubs here that should be available on the device + + TagBrowsingStub tagBrowsingStub = new(2, new Tag(new Native()) { Name = TagBrowsingStub.TagBrowsingParameterName }); + device.MockedTags = [new ModeTagStub(1, mode), tagBrowsingStub]; + return tagProxy; + } + + private TagStub? FindTargetTagMock(MethodInfo? targetMethod, object?[]? args) + { + int indexOfTagParameter = FindIndexOfTagParameter(targetMethod); + + if (indexOfTagParameter == -1) + { + int indexOfLpStringParameter = FindIndexOfLpStringParameter(targetMethod); + + if (indexOfLpStringParameter == -1) + { + return null; + } + + string lpsString = (string)args![indexOfLpStringParameter]!; + return MockedTags.Find(deviceMock => deviceMock.IsResponsibleForLpString(lpsString)); + } + + int tag = (int)args![indexOfTagParameter]!; + + return MockedTags.Find(deviceMock => deviceMock.IsResponsibleForTag(tag)); + } + + + protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) + { + var type = typeof(MyControlLogixDevice); + // Get the PropertyInfo + PropertyInfo property = type.GetProperty(nameof(MockedTags))!; + + // Get the MethodInfo for the getter + MethodInfo getter = property.GetMethod!; // or property.GetGetMethod() + + if (getter.Name.Equals(targetMethod?.Name)) + { + return MockedTags; + } + + TagStub? deviceMock = FindTargetTagMock(targetMethod, args); + + return deviceMock == null + ? throw new Exception($"No tag-mock found for handling target method {targetMethod}") + : targetMethod!.Invoke(deviceMock, args); + } + } +} \ No newline at end of file diff --git a/src/libplctag.Tests/stubbing/examples/MyControlLogixDeviceTests.cs b/src/libplctag.Tests/stubbing/examples/MyControlLogixDeviceTests.cs new file mode 100644 index 0000000..b219ae5 --- /dev/null +++ b/src/libplctag.Tests/stubbing/examples/MyControlLogixDeviceTests.cs @@ -0,0 +1,49 @@ +using FluentAssertions; +using libplctag.NativeImport; +using Moq; +using System; +using System.Reflection; +using Xunit; + +namespace libplctag.Tests.stubbing.examples +{ + public class MyControlLogixDeviceTests + { + [Fact] + public void TestRoutingWithStubs() + { + // ARRANGE + IDeviceStub myControlLogixDevice = MyControlLogixDevice.Create(); + INative nativeProxy = NativeToDeviceStubsDispatchProxy.Create([myControlLogixDevice]); + TagBrowsingStub tagBrowsingStub = (myControlLogixDevice.MockedTags[1] as TagBrowsingStub)!; + int tagBrowsingHandle = 2; + tagBrowsingStub.Mock.Setup(mock => mock.plc_tag_create_ex(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())).Returns(tagBrowsingHandle); + tagBrowsingStub.Mock.Setup(mock => mock.plc_tag_get_size(It.IsAny())).Returns(20); + Tag modeTag = ModeTagStub.CreateModeTag(nativeProxy); + Tag tagBrowsingTag = new(nativeProxy) { Name = "@tags" }; + + + // ACT + modeTag.Initialize(); + byte[] modeValueOne = modeTag.GetBuffer(); + byte[] modeValueTwo = modeTag.GetBuffer(); + Action modeValueThree = () => modeTag.GetBuffer(); + tagBrowsingTag.Initialize(); + + + + // ASSERT + modeTag.IsInitialized.Should().Be(true); + modeTag.NativeTagHandle.Should().Be(ModeTagStub.ModeTagHandle); + modeValueOne.Should().Equal(ModeTagStub.ProgramMode); + modeValueTwo.Should().Equal(ModeTagStub.RunMode); + modeValueThree.Should().Throw() + .WithInnerException() + .WithInnerException(); + + tagBrowsingTag.IsInitialized.Should().Be(true); + tagBrowsingTag.NativeTagHandle.Should().Be(tagBrowsingHandle); + } + } +} \ No newline at end of file diff --git a/src/libplctag.Tests/stubbing/examples/TagBrowsingStub.cs b/src/libplctag.Tests/stubbing/examples/TagBrowsingStub.cs new file mode 100644 index 0000000..2f9640c --- /dev/null +++ b/src/libplctag.Tests/stubbing/examples/TagBrowsingStub.cs @@ -0,0 +1,289 @@ +using libplctag.NativeImport; +using Moq; +using System; +using System.Text; + +namespace libplctag.Tests.stubbing.examples +{ + /// + /// A Tagstub that will be responsible for all INative calls that contain @tags in the lpString parameter and can delegate those calls to an exposed Mock that can be setup for each test individually. + /// + public class TagBrowsingStub : TagStub + { + public readonly Mock Mock; + + public TagBrowsingStub(int tagIdentifier, Tag tag) : base(tagIdentifier, tag) + { + MockRepository mockRepository = new(MockBehavior.Strict); + Mock = mockRepository.Create(); + } + + + public const string TagBrowsingParameterName = "@tags"; + + + public override bool IsResponsibleForLpString(string lpString) + { + return lpString.Contains(TagBrowsingParameterName); + } + + + public override int plc_tag_abort(int tag) + { + throw new NotImplementedException(); + } + + public override int plc_tag_check_lib_version(int req_major, int req_minor, int req_patch) + { + throw new NotImplementedException(); + } + + public override int plc_tag_create(string lpString, int timeout) + { + return Mock.Object.plc_tag_create(lpString, timeout); + } + + public override int plc_tag_create_ex(string lpString, plctag.callback_func_ex func, IntPtr userdata, + int timeout) + { + return Mock.Object.plc_tag_create_ex(lpString, func, userdata, timeout); + } + + public override string plc_tag_decode_error(int err) + { + throw new NotImplementedException(); + } + + public override int plc_tag_destroy(int tag) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_bit(int tag, int offset_bit) + { + throw new NotImplementedException(); + } + + public override float plc_tag_get_float32(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override double plc_tag_get_float64(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override short plc_tag_get_int16(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_int32(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override long plc_tag_get_int64(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override sbyte plc_tag_get_int8(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_int_attribute(int tag, string attrib_name, int default_value) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_int_attribute(int tag, string attrib_name, int new_value) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_byte_array_attribute(int tag, string attrib_name, byte[] buffer, + int buffer_length) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_size(int tag) + { + return Mock.Object.plc_tag_get_size(tag); + } + + public override int plc_tag_set_size(int tag, int new_size) + { + throw new NotImplementedException(); + } + + public override ushort plc_tag_get_uint16(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override uint plc_tag_get_uint32(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override ulong plc_tag_get_uint64(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override byte plc_tag_get_uint8(int tag, int offset) + { + throw new NotImplementedException(); + } + + public override int plc_tag_lock(int tag) + { + throw new NotImplementedException(); + } + + public override int plc_tag_read(int tag, int timeout) + { + throw new NotImplementedException(); + } + + public override int plc_tag_register_callback(int tag_id, plctag.callback_func func) + { + throw new NotImplementedException(); + } + + public override int plc_tag_register_logger(plctag.log_callback_func func) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_bit(int tag, int offset_bit, int val) + { + throw new NotImplementedException(); + } + + public override void plc_tag_set_debug_level(int debug_level) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_float32(int tag, int offset, float val) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_float64(int tag, int offset, double val) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_int16(int tag, int offset, short val) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_int32(int tag, int offset, int val) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_int64(int tag, int offset, long val) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_int8(int tag, int offset, sbyte val) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_uint16(int tag, int offset, ushort val) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_uint32(int tag, int offset, uint val) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_uint64(int tag, int offset, ulong val) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_uint8(int tag, int offset, byte val) + { + throw new NotImplementedException(); + } + + public override void plc_tag_shutdown() + { + throw new NotImplementedException(); + } + + public override int plc_tag_status(int tag) + { + throw new NotImplementedException(); + } + + public override int plc_tag_unlock(int tag) + { + throw new NotImplementedException(); + } + + public override int plc_tag_unregister_callback(int tag_id) + { + throw new NotImplementedException(); + } + + public override int plc_tag_unregister_logger(int tag_id) + { + throw new NotImplementedException(); + } + + public override int plc_tag_write(int tag, int timeout) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_raw_bytes(int tag, int start_offset, byte[] buffer, int buffer_length) + { + return Mock.Object.plc_tag_get_raw_bytes(tag, start_offset, buffer, buffer_length); + } + + public override int plc_tag_set_raw_bytes(int tag, int start_offset, byte[] buffer, int buffer_length) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_string_length(int tag, int string_start_offset) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_string(int tag, int string_start_offset, StringBuilder buffer, + int buffer_length) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_string_total_length(int tag, int string_start_offset) + { + throw new NotImplementedException(); + } + + public override int plc_tag_get_string_capacity(int tag, int string_start_offset) + { + throw new NotImplementedException(); + } + + public override int plc_tag_set_string(int tag, int string_start_offset, string string_val) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/libplctag/Tag.cs b/src/libplctag/Tag.cs index 35c2b9e..e143702 100644 --- a/src/libplctag/Tag.cs +++ b/src/libplctag/Tag.cs @@ -1166,7 +1166,7 @@ private void SetField(ref T field, T value) field = value; } - private string GetAttributeString() + public string GetAttributeString() { string FormatNullableBoolean(bool? value)