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..45b4137 100644
--- a/src/libplctag.Tests/libplctag.Tests.csproj
+++ b/src/libplctag.Tests/libplctag.Tests.csproj
@@ -4,9 +4,12 @@
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/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..e143702 100644
--- a/src/libplctag/Tag.cs
+++ b/src/libplctag/Tag.cs
@@ -68,7 +68,7 @@ public Tag() : this(new Native())
{
}
- internal Tag(INative nativeMethods)
+ public Tag(INative nativeMethods)
{
_native = nativeMethods;
@@ -93,6 +93,16 @@ public bool IsInitialized
}
}
+ public int NativeTagHandle
+ {
+ get
+ {
+ ThrowIfAlreadyDisposed();
+ InitializeIfRequired();
+ return nativeTagHandle;
+ }
+ }
+
///
///
///
@@ -1156,7 +1166,7 @@ private void SetField(ref T field, T value)
field = value;
}
- private string GetAttributeString()
+ public string GetAttributeString()
{
string FormatNullableBoolean(bool? value)