Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/libplctag.Tests/MyUdt.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
29 changes: 29 additions & 0 deletions src/libplctag.Tests/StructMarshallingExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Runtime.InteropServices;

namespace libplctag.Tests
{
public static class StructMarshallingExtensions
{
/// <summary>
/// Converts a blittable struct to a byte array.
/// </summary>
public static byte[] ToByteArray<T>(this T value) where T : struct
{
byte[] bytes = new byte[Marshal.SizeOf<T>()];
MemoryMarshal.Write(bytes, ref value); // zero-allocation, fast
return bytes;
}

/// <summary>
/// Reads a blittable struct from a byte array.
/// </summary>
public static T ToStruct<T>(this byte[] bytes) where T : struct
{
if (bytes.Length < Marshal.SizeOf<T>())
throw new ArgumentException($"Byte array too small for struct {typeof(T)}");

return MemoryMarshal.Read<T>(bytes);
}
}
}
27 changes: 27 additions & 0 deletions src/libplctag.Tests/TagExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Runtime.InteropServices;

namespace libplctag.Tests
{
public static class TagExtensions
{
public static T GetValue<T>(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<T>(new IntPtr(handle.AddrOfPinnedObject().ToInt64()))!;

return (retVal);
}
finally
{
handle.Free();
}
}
}
}
93 changes: 93 additions & 0 deletions src/libplctag.Tests/TagTests.cs
Original file line number Diff line number Diff line change
@@ -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<INative> _iNativeMock;
private readonly Tag _underTest;


public TagTests()
{
_mockRepository = new MockRepository(MockBehavior.Strict);
_iNativeMock = _mockRepository.Create<INative>();
_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<string>(),
It.IsAny<plctag.callback_func_ex>(),
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<MyUdt>();

// ASSERT
currentTagValue.Should().Be(expectedValue);
}

private void GivenMarshalledDataIsReturnedInBuffer<T>(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<byte[]>(), 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);
}
}
}
3 changes: 3 additions & 0 deletions src/libplctag.Tests/libplctag.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
<TargetFramework>net8.0</TargetFramework>

<IsPackable>false</IsPackable>

<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="8.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="xunit" Version="2.9.0" />
Expand Down
25 changes: 25 additions & 0 deletions src/libplctag.Tests/stubbing/IDeviceStub.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Collections.Generic;

namespace libplctag.Tests.stubbing
{
//

public interface IDeviceStub : INative
{
List<TagStub> 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));
}
}
}
56 changes: 56 additions & 0 deletions src/libplctag.Tests/stubbing/NativeToDeviceStubsDispatchProxy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using static libplctag.Tests.stubbing.StubRouting;

namespace libplctag.Tests.stubbing
{
/// <summary>
/// Responsible for creating a proxy for the INative interface and then delegating calls to it to the matching registered DeviceStub.
/// </summary>
public class NativeToDeviceStubsDispatchProxy : DispatchProxy
{
private List<IDeviceStub> 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<IDeviceStub> tagMocks)
{
var iNativeProxy = Create<INative, NativeToDeviceStubsDispatchProxy>();
NativeToDeviceStubsDispatchProxy dispatchProxy = (NativeToDeviceStubsDispatchProxy)iNativeProxy;
dispatchProxy.DeviceMocks = tagMocks;
return iNativeProxy;
}
}
}
38 changes: 38 additions & 0 deletions src/libplctag.Tests/stubbing/StubRouting.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace libplctag.Tests.stubbing
{
/// <summary>
/// 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.
/// </summary>
public static class StubRouting
{
private const string TagParameterName = "tag";
private const string LpStringParameterName = "lpString";

public static int FindIndexOfTagParameter(MethodInfo? targetMethod)
{
List<ParameterInfo> parameters = targetMethod?.GetParameters().ToList() ?? [];
return parameters.FindIndex(IsTagParameter);
}

public static int FindIndexOfLpStringParameter(MethodInfo? targetMethod)
{
List<ParameterInfo> 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);
}
}
}
80 changes: 80 additions & 0 deletions src/libplctag.Tests/stubbing/TagStub.cs
Original file line number Diff line number Diff line change
@@ -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);
}



}
Loading