diff --git a/.Zeugwerk/config.json b/.Zeugwerk/config.json index cbaa726..bbc581d 100644 --- a/.Zeugwerk/config.json +++ b/.Zeugwerk/config.json @@ -6,7 +6,7 @@ "name": "TcHaxx.Snappy", "plcs": [ { - "version": "0.1.0.0", + "version": "0.2.0.0", "name": "snappy", "type": "Library", "packages": [ diff --git a/.editorconfig b/.editorconfig index 9630ed9..e7a491e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -197,4 +197,5 @@ csharp_style_prefer_extended_property_pattern = true:suggestion # SonarAnalyzer dotnet_diagnostic.S1134.severity = suggestion -dotnet_diagnostic.S1135.severity = suggestion \ No newline at end of file +dotnet_diagnostic.S1135.severity = suggestion +dotnet_diagnostic.S6602.severity = suggestion \ No newline at end of file diff --git a/README.md b/README.md index e126e39..720c067 100644 --- a/README.md +++ b/README.md @@ -119,15 +119,15 @@ Activate Configuration | Execute | `cmd /c start TcHaxx.Snappy.CLI verify -d \"% ## Source control: Received and Verified files When dealing with source control, consider the following guidelines for handling **Received** and **Verified** files: -1. **Exclusion**: +1. **Exclude files**: - Exclude all files with the pattern `*.received.*` from source control. - To achieve this, add the following line to your `.gitignore` file: ``` *.received.* ``` -2. **Commitment**: - - On the other hand, **commit** all files with the pattern `*.verified.*` to source control. +2. **Commit files**: + - **Commit** all files with the pattern `*.verified.*` to source control. > See [Verify/README](https://github.com/VerifyTests/Verify?tab=readme-ov-file#source-control-received-and-verified-files) @@ -169,4 +169,5 @@ Option | Required | Default | Description * [TcUnit](https://github.com/tcunit/TcUnit) - A unit testing framework for Beckhoff's TwinCAT 3 * [CommandLineParser](https://github.com/commandlineparser/commandline) - A command line parsing library for .NET applications. * [Verify](https://github.com/VerifyTests/Verify) - A library used for snapshot testing. -* [Serilog](https://github.com/serilog/serilog) - A logging library for .NET applications. \ No newline at end of file +* [Serilog](https://github.com/serilog/serilog) - A logging library for .NET applications. +* [TF6000_ADS_DOTNET_V5_Samples](https://github.com/Beckhoff/TF6000_ADS_DOTNET_V5_Samples) - Sample code for the Version 6.X series of the TwinCAT ADS .NET Packages \ No newline at end of file diff --git a/src/TcHaxx.Snappy.CLI/TcHaxx.Snappy.CLI.csproj b/src/TcHaxx.Snappy.CLI/TcHaxx.Snappy.CLI.csproj index c233345..d942cb3 100644 --- a/src/TcHaxx.Snappy.CLI/TcHaxx.Snappy.CLI.csproj +++ b/src/TcHaxx.Snappy.CLI/TcHaxx.Snappy.CLI.csproj @@ -10,7 +10,7 @@ A Snapshot Testing framework for TwinCAT 3 Copyright (c) 2024 densogiaichned TwinCAT Snapshot Testing framework - 0.1.0 + 0.2.0 $(Version) $(Version) True diff --git a/src/TcHaxx.Snappy.Common/RPC/IRpcMethodDescriptor.cs b/src/TcHaxx.Snappy.Common/RPC/IRpcMethodDescriptor.cs index 53d9825..e323b11 100644 --- a/src/TcHaxx.Snappy.Common/RPC/IRpcMethodDescriptor.cs +++ b/src/TcHaxx.Snappy.Common/RPC/IRpcMethodDescriptor.cs @@ -1,6 +1,4 @@ -using TcHaxx.Snappy.Common.Verify; - -namespace TcHaxx.Snappy.Common.RPC; +namespace TcHaxx.Snappy.Common.RPC; /// /// Describes an descriptor for RPC methods. @@ -14,8 +12,8 @@ public interface IRpcMethodDescriptor public IEnumerable GetRpcMethodDescription(); /// - /// Registers a implementation with . + /// Registers a implementation with . /// - /// - public void Register(IVerifyMethod rpcVerifyMethod); + /// + public void Register(IRpcMethodMarker rpcMethod); } diff --git a/src/TcHaxx.Snappy.Common/RPC/IRpcMethodMarker.cs b/src/TcHaxx.Snappy.Common/RPC/IRpcMethodMarker.cs new file mode 100644 index 0000000..48a411e --- /dev/null +++ b/src/TcHaxx.Snappy.Common/RPC/IRpcMethodMarker.cs @@ -0,0 +1,8 @@ +namespace TcHaxx.Snappy.Common.RPC; + +/// +/// Empty inteface to mark RPC methods. +/// +public interface IRpcMethodMarker +{ +} diff --git a/src/TcHaxx.Snappy.Common/RPC/RpcMethodDescription.cs b/src/TcHaxx.Snappy.Common/RPC/RpcMethodDescription.cs index 5d714dc..7bf0e38 100644 --- a/src/TcHaxx.Snappy.Common/RPC/RpcMethodDescription.cs +++ b/src/TcHaxx.Snappy.Common/RPC/RpcMethodDescription.cs @@ -1,6 +1,8 @@ using System.Reflection; -using TcHaxx.Snappy.Common.Verify; namespace TcHaxx.Snappy.Common.RPC; -public record RpcMethodDescription(MethodInfo Method, IEnumerable Parameters, ParameterInfo ReturnValue, IVerifyMethod RpcInvocableMethod); +public record RpcMethodDescription(MethodInfo Method, IEnumerable Parameters, ParameterInfo ReturnValue, IRpcMethodMarker RpcInvocableMethod, string? Alias) +{ + public string InstanceName => RpcInvocableMethod.GetType().FullName!; +} diff --git a/src/TcHaxx.Snappy.Common/RPC/RpcMethodDescriptor.cs b/src/TcHaxx.Snappy.Common/RPC/RpcMethodDescriptor.cs index b736257..2cf887f 100644 --- a/src/TcHaxx.Snappy.Common/RPC/RpcMethodDescriptor.cs +++ b/src/TcHaxx.Snappy.Common/RPC/RpcMethodDescriptor.cs @@ -1,10 +1,12 @@ -using TcHaxx.Snappy.Common.Verify; +using System.Data; +using System.Reflection; +using TcHaxx.Snappy.Common.RPC.Attributes; namespace TcHaxx.Snappy.Common.RPC; public class RpcMethodDescriptor : IRpcMethodDescriptor { - private readonly Queue _verifyMethods = new(); + private readonly Queue _verifyMethods = new(); public RpcMethodDescriptor() { @@ -18,20 +20,57 @@ public IEnumerable GetRpcMethodDescription() } } - public void Register(IVerifyMethod rpcVerifyMethod) + public void Register(IRpcMethodMarker rpcMethod) { - _verifyMethods.Enqueue(rpcVerifyMethod); + _verifyMethods.Enqueue(rpcMethod); } - - private RpcMethodDescription Transform(IVerifyMethod rpcVerifyMethod) + private static RpcMethodDescription Transform(IRpcMethodMarker rpcMethod) { - var method = rpcVerifyMethod.GetType().GetMethod(nameof(IVerifyMethod.Verify)) ?? - throw new RpcMethodTransformException($"Method \"{nameof(IVerifyMethod.Verify)}\" not found."); + var method = GetMethodInfo(rpcMethod); + var parameters = method.GetParameters() ?? - throw new RpcMethodTransformException($"Expected method \"{nameof(IVerifyMethod.Verify)}\" to have parameters"); + throw new RpcMethodTransformException($"Expected method \"{method.Name}\" to have parameters"); var retVal = method.ReturnParameter; - return new RpcMethodDescription(method, parameters, retVal, rpcVerifyMethod); + var alias = GetAliasAttriubte(rpcMethod); + return new RpcMethodDescription(method, parameters, retVal, rpcMethod, alias); + } + + private static MethodInfo GetMethodInfo(IRpcMethodMarker rpcMethod) + { + var typeName = rpcMethod.GetType().Name; + + var methodInfos = GetMethodInfos(rpcMethod); + + if (methodInfos is null || methodInfos.Length == 0) + { + throw new RpcMethodTransformException($"No RPC method found in type \"{typeName}\"."); + } + + if (methodInfos.Length > 1) + { + throw new RpcMethodTransformException($"Only one RPC method supported per type ({typeName})."); + } + + return methodInfos[0]; + } + + private static string? GetAliasAttriubte(IRpcMethodMarker rpcMethod) + { + var aliasAttribute = rpcMethod.GetType() + .GetMethods() + .Select(x => x.GetCustomAttribute()) + .FirstOrDefault(x => x is not null); + return aliasAttribute?.AliasName; + } + + private static MethodInfo[]? GetMethodInfos(IRpcMethodMarker rpcMethod) + { + var interfaces = rpcMethod.GetType().GetInterfaces(); + var rpcInterfaces = interfaces + .FirstOrDefault(i => typeof(IRpcMethodMarker).IsAssignableFrom(i) && i.GetInterfaces().Length == 1 && i.GetInterfaces().Contains(typeof(IRpcMethodMarker))); + + return rpcInterfaces?.GetMethods(); } } diff --git a/src/TcHaxx.Snappy.Common/Verify/IVerifyMethod.cs b/src/TcHaxx.Snappy.Common/Verify/IVerifyMethod.cs index 8d4f20b..e558489 100644 --- a/src/TcHaxx.Snappy.Common/Verify/IVerifyMethod.cs +++ b/src/TcHaxx.Snappy.Common/Verify/IVerifyMethod.cs @@ -1,8 +1,9 @@ -using TcHaxx.Snappy.Common.RPC.Attributes; +using TcHaxx.Snappy.Common.RPC; +using TcHaxx.Snappy.Common.RPC.Attributes; namespace TcHaxx.Snappy.Common.Verify; -public interface IVerifyMethod +public interface IVerifyMethod : IRpcMethodMarker { public VerificationResult Verify( [String(Constants.DEFAULT_TEST_NAMES_LENGTH)] string testSuiteName, diff --git a/src/TcHaxx.Snappy.TcADS/SymbolicServer.cs b/src/TcHaxx.Snappy.TcADS/SymbolicServer.cs index a9885e9..070968f 100644 --- a/src/TcHaxx.Snappy.TcADS/SymbolicServer.cs +++ b/src/TcHaxx.Snappy.TcADS/SymbolicServer.cs @@ -60,6 +60,36 @@ protected override void OnConnected() } protected override AdsErrorCode OnRpcInvoke(IInterfaceInstance structInstance, IRpcMethod method, object[] values, out object? returnValue) + { + returnValue = null; + var retVal = OnRpcInvokeProxy(structInstance, method, values, out returnValue); + + if (retVal != AdsErrorCode.NoError) + { + // Note: + // Due to a bug (?) in ADS.Net, all parameter values and the returnValue must not be NULL! + // Otherwise, it will throw an exception during marshalling, hence the client will only receive error 0x1 + returnValue = 0; + PresetParameterValues(ref values); + } + + return retVal; + } + + private static void PresetParameterValues(ref object[] parameterValues) + { + for (var i = 0; i < parameterValues.Length; i++) + { + if (parameterValues[i] is not null) + { + continue; + } + + parameterValues[i] = 0; + } + } + + private AdsErrorCode OnRpcInvokeProxy(IInterfaceInstance structInstance, IRpcMethod method, object[] parameterValues, out object? returnValue) { var iDataType = structInstance.DataType; if (iDataType is null) @@ -68,7 +98,8 @@ protected override AdsErrorCode OnRpcInvoke(IInterfaceInstance structInstance, I _logger?.LogError("{OnRpcInvoke}: {IDataType} is null", nameof(OnRpcInvoke), nameof(IDataType)); return AdsErrorCode.DeviceInvalidContext; } + _logger?.LogInformation("{OnRpcInvoke}: Invoking method {IRpcMethod} of {IDataTypeFullName}", nameof(OnRpcInvoke), method, iDataType.FullName); - return _symbolFactory.InvokeRpcMethod(iDataType, values, out returnValue); + return _symbolFactory.InvokeRpcMethod(structInstance, method, parameterValues, out returnValue); } } diff --git a/src/TcHaxx.Snappy.TcADS/Symbols/ISymbolFactory.cs b/src/TcHaxx.Snappy.TcADS/Symbols/ISymbolFactory.cs index 9831210..1f8b58c 100644 --- a/src/TcHaxx.Snappy.TcADS/Symbols/ISymbolFactory.cs +++ b/src/TcHaxx.Snappy.TcADS/Symbols/ISymbolFactory.cs @@ -7,5 +7,5 @@ namespace TcHaxx.Snappy.TcADS.Symbols; internal interface ISymbolFactory { void AddSymbols(ServerSymbolFactory? serverSymbolFactory); - AdsErrorCode InvokeRpcMethod(IDataType mappedType, object[] values, out object? returnValue); + AdsErrorCode InvokeRpcMethod(IInterfaceInstance structInstance, IRpcMethod method, object[] parameterValues, out object? returnValue); } diff --git a/src/TcHaxx.Snappy.TcADS/Symbols/SymbolFactory.cs b/src/TcHaxx.Snappy.TcADS/Symbols/SymbolFactory.cs index 9ff4e09..d797748 100644 --- a/src/TcHaxx.Snappy.TcADS/Symbols/SymbolFactory.cs +++ b/src/TcHaxx.Snappy.TcADS/Symbols/SymbolFactory.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Logging; using TcHaxx.Snappy.Common.RPC; using TcHaxx.Snappy.Common.RPC.Attributes; -using TcHaxx.Snappy.Common.Verify; using TwinCAT.Ads; using TwinCAT.Ads.Server.TypeSystem; using TwinCAT.Ads.TypeSystem; @@ -17,7 +16,7 @@ internal class SymbolFactory : ISymbolFactory private readonly IRpcMethodDescriptor _rpcMethodDescriptor; private readonly ILogger? _logger; - private readonly Dictionary _mappedStructTypeToRpcMethod = []; + private readonly Dictionary _mappedStructTypeToRpcMethod = []; internal SymbolFactory(IRpcMethodDescriptor rpcMethodDescriptor, ILogger? logger) { @@ -42,47 +41,57 @@ public void AddSymbols(ServerSymbolFactory? serverSymbolFactory) var retValKvp = GetMethodReturnValue(rpcMethodDescription.ReturnValue); AddToServerSymbolFactory(serverSymbolFactory, [retValKvp]); - var fullName = rpcMethodDescription.Method.ReflectedType?.FullName ?? rpcMethodDescription.Method.Name; + var fullName = rpcMethodDescription.InstanceName; var dataArea = new DataArea($"DATA::{fullName}", idxGrp, idxOffset++, 0x10000); _ = serverSymbolFactory.AddDataArea(dataArea); - var rpc = BuildRpcMethod(rpcMethodDescription.Method, paramsKvp, retValKvp); + var rpc = BuildRpcMethod(rpcMethodDescription, paramsKvp, retValKvp); var dtStructRpc = new StructType($"STRUCT::{fullName}"); _ = dtStructRpc.AddMethod(rpc); _ = serverSymbolFactory.AddType(dtStructRpc); _ = serverSymbolFactory.AddSymbol(fullName, dtStructRpc, dataArea); + _logger?.LogInformation("Adding RPC method {MappedTypeFullName}#{MethodName}", fullName, rpc.Name); _mappedStructTypeToRpcMethod.Add(dtStructRpc, rpcMethodDescription.RpcInvocableMethod); } } - public AdsErrorCode InvokeRpcMethod(IDataType mappedType, object[] values, out object? returnValue) + public AdsErrorCode InvokeRpcMethod(IInterfaceInstance structInstance, IRpcMethod method, object[] parameterValues, out object? returnValue) { - returnValue = null; - if (!_mappedStructTypeToRpcMethod.TryGetValue(mappedType, out var value)) + returnValue = Activator.CreateInstance(Type.GetType(method.ReturnType, false) ?? typeof(int)); + var mappedType = structInstance.DataType!; + + if (!_mappedStructTypeToRpcMethod.TryGetValue(mappedType, out var rpcMethodType)) { + _logger?.LogError("No matching type found ({MappedTypeFullName})", mappedType.FullName); return AdsErrorCode.DeviceServiceNotSupported; } - var rpcMethodToInvoke = value; + var rpcMethodToInvoke = rpcMethodType.GetType() + .GetMethods() + .FirstOrDefault(x => string.Equals(x.Name, method.Name, StringComparison.OrdinalIgnoreCase)); + if (rpcMethodToInvoke is null) + { + _logger?.LogError("Method \"{MethodName}\" not found in type \"{MappedTypeFullName}\"", method.Name, mappedType.FullName); + return AdsErrorCode.DeviceServiceNotSupported; + } - if (values.Length != rpcMethodToInvoke.GetType().GetMethod(nameof(IVerifyMethod.Verify))!.GetParameters().Length) + if (parameterValues.Length != rpcMethodToInvoke.GetParameters().Length) { + _logger?.LogError("Different method parameter length: {ParameterValuesLength} != {RpcMethodParameterLength}", parameterValues.Length, rpcMethodToInvoke.GetParameters().Length); return AdsErrorCode.DeviceInvalidParam; } - // FIXME: This is ugly... - // Find a better solution, e.g. reflection. - returnValue = rpcMethodToInvoke.Verify((string)values[0], (string)values[1], (string)values[2]); + returnValue = rpcMethodToInvoke.Invoke(rpcMethodType, parameterValues); return AdsErrorCode.NoError; } - private static RpcMethod BuildRpcMethod(MethodInfo methodInfo, IEnumerable> paramsKvp, KeyValuePair retValKvp) + private static RpcMethod BuildRpcMethod(RpcMethodDescription rpcMethodDescription, IEnumerable> paramsKvp, KeyValuePair retValKvp) { - var nameOrAlias = methodInfo.GetCustomAttribute()?.AliasName ?? methodInfo.Name; + var nameOrAlias = rpcMethodDescription.Alias ?? rpcMethodDescription.Method.Name; var rpc = new RpcMethod(nameOrAlias); foreach (var (k, v) in paramsKvp) { diff --git a/src/TcHaxx.Snappy/snappy/Version/Global_Version.TcGVL b/src/TcHaxx.Snappy/snappy/Version/Global_Version.TcGVL new file mode 100644 index 0000000..43e1e34 --- /dev/null +++ b/src/TcHaxx.Snappy/snappy/Version/Global_Version.TcGVL @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/src/TcHaxx.Snappy/snappy/snappy.plcproj b/src/TcHaxx.Snappy/snappy/snappy.plcproj index 89b4cd5..7bce371 100644 --- a/src/TcHaxx.Snappy/snappy/snappy.plcproj +++ b/src/TcHaxx.Snappy/snappy/snappy.plcproj @@ -19,7 +19,7 @@ TcHaxx false snappy - 0.1.0.0 + 0.2.0.0 {9c7e50a7-dead-beef-897b-4cdbc169222d} @@ -142,6 +142,9 @@ Code + + Code + @@ -156,6 +159,7 @@ +