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)