diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index baa7f2e..2d1b059 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: sudo apt-get install gcc-i686-linux-gnu gcc-x86-64-linux-gnu gcc-aarch64-linux-gnu llvm-14 clang-14 - name: "Setup .NET" - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '8.x' @@ -44,7 +44,7 @@ jobs: dotnet test '${{ github.workspace }}/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/c2ffi.Tests.EndToEnd.Extract.csproj' --nologo --verbosity minimal --configuration Release - name: "Upload generated FFI files" - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: "ffi-${{ matrix.platform.name }}" path: | @@ -63,26 +63,26 @@ jobs: uses: actions/checkout@v4 - name: "Setup .NET" - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '8.x' - name: "Download generated FFI files: windows" - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: "ffi-windows" path: | ${{ github.workspace }}/src/c/tests - name: "Download generated FFI files: linux" - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: "ffi-linux" path: | ${{ github.workspace }}/src/c/tests - name: "Download generated FFI files: macos" - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: "ffi-macos" path: | @@ -92,3 +92,10 @@ jobs: run: | dotnet test '${{ github.workspace }}/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/c2ffi.Tests.EndToEnd.Merge.csproj' --nologo --verbosity minimal --configuration Release + - name: "Upload generated FFI files" + uses: actions/upload-artifact@v4 + with: + name: "ffi-x" + path: | + ${{ github.workspace }}/src/c/tests/**/ffi-x/*.json + diff --git a/.gitignore b/.gitignore index 9965f5f..b3433c8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ bin/ obj/ /artifacts/ src/c/tests/**/ffi/*.json +src/c/tests/**/ffi-x/*.json diff --git a/src/cs/c2json.sln b/src/cs/c2ffi.sln similarity index 100% rename from src/cs/c2json.sln rename to src/cs/c2ffi.sln diff --git a/src/cs/production/c2ffi.Data/Nodes/CMacroObject.cs b/src/cs/production/c2ffi.Data/Nodes/CMacroObject.cs index 62f6687..375b9a3 100644 --- a/src/cs/production/c2ffi.Data/Nodes/CMacroObject.cs +++ b/src/cs/production/c2ffi.Data/Nodes/CMacroObject.cs @@ -45,6 +45,16 @@ public override bool Equals(CNode? other) return TypeInfo.Equals(other2.TypeInfo) && Value == other2.Value; } + public bool EqualsWithoutValue(CMacroObject other) + { + if (!base.Equals(other)) + { + return false; + } + + return TypeInfo.Equals(other.TypeInfo); + } + /// public override int GetHashCode() { diff --git a/src/cs/production/c2ffi.Tool/Commands/Merge/Input/MergeInputSanitizer.cs b/src/cs/production/c2ffi.Tool/Commands/Merge/Input/MergeInputSanitizer.cs new file mode 100644 index 0000000..f05f0a3 --- /dev/null +++ b/src/cs/production/c2ffi.Tool/Commands/Merge/Input/MergeInputSanitizer.cs @@ -0,0 +1,46 @@ +// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved. +// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information. + +using System.Collections.Immutable; +using System.IO.Abstractions; +using bottlenoselabs.Common.Tools; +using c2ffi.Tool.Commands.Merge.Input.Sanitized; +using c2ffi.Tool.Commands.Merge.Input.Unsanitized; + +namespace c2ffi.Tool.Commands.Merge.Input; + +public sealed class MergeInputSanitizer +{ + private readonly IFileSystem _fileSystem; + + public MergeInputSanitizer(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + public MergeOptions Sanitize(UnsanitizedMergeOptions unsanitizedOptions) + { + var directoryPath = _fileSystem.Path.GetFullPath(unsanitizedOptions.InputDirectoryPath); + if (!_fileSystem.Directory.Exists(directoryPath)) + { + throw new ToolInputSanitizationException($"The directory '{directoryPath}' does not exist."); + } + + var filePaths = _fileSystem.Directory.GetFiles(directoryPath, "*.json").ToImmutableArray(); + + if (filePaths.IsDefaultOrEmpty) + { + throw new ToolInputSanitizationException($"The directory '{directoryPath}' does not contain any abstract syntax tree `.json` files."); + } + + var outputFilePath = _fileSystem.Path.GetFullPath(unsanitizedOptions.OutputFilePath); + + var result = new MergeOptions + { + OutputFilePath = outputFilePath, + InputFilePaths = filePaths + }; + + return result; + } +} diff --git a/src/cs/production/c2ffi.Tool/Commands/Merge/Input/Sanitized/MergeOptions.cs b/src/cs/production/c2ffi.Tool/Commands/Merge/Input/Sanitized/MergeOptions.cs new file mode 100644 index 0000000..304c73c --- /dev/null +++ b/src/cs/production/c2ffi.Tool/Commands/Merge/Input/Sanitized/MergeOptions.cs @@ -0,0 +1,13 @@ +// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved. +// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information. + +using System.Collections.Immutable; + +namespace c2ffi.Tool.Commands.Merge.Input.Sanitized; + +public class MergeOptions +{ + public ImmutableArray InputFilePaths { get; set; } = ImmutableArray.Empty; + + public string OutputFilePath { get; set; } = string.Empty; +} diff --git a/src/cs/production/c2ffi.Tool/Commands/Merge/Input/Unsanitized/UnsanitizedMergeOptions.cs b/src/cs/production/c2ffi.Tool/Commands/Merge/Input/Unsanitized/UnsanitizedMergeOptions.cs new file mode 100644 index 0000000..1a80806 --- /dev/null +++ b/src/cs/production/c2ffi.Tool/Commands/Merge/Input/Unsanitized/UnsanitizedMergeOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved. +// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information. + +namespace c2ffi.Tool.Commands.Merge.Input.Unsanitized; + +// NOTE: This class is considered un-sanitized input; all strings and other types could be null. +public class UnsanitizedMergeOptions +{ + public string InputDirectoryPath { get; set; } = string.Empty; + + public string OutputFilePath { get; set; } = string.Empty; +} diff --git a/src/cs/production/c2ffi.Tool/Commands/Merge/MergeFfisCommand.cs b/src/cs/production/c2ffi.Tool/Commands/Merge/MergeFfisCommand.cs index c4923ac..bd01586 100644 --- a/src/cs/production/c2ffi.Tool/Commands/Merge/MergeFfisCommand.cs +++ b/src/cs/production/c2ffi.Tool/Commands/Merge/MergeFfisCommand.cs @@ -2,23 +2,26 @@ // Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information. using System.CommandLine; -using JetBrains.Annotations; +using c2ffi.Tool.Commands.Merge.Input.Unsanitized; namespace c2ffi.Tool.Commands.Merge; -[UsedImplicitly] -public class MergeFfisCommand : Command +public sealed class MergeFfisCommand : Command { - public MergeFfisCommand() + private readonly MergeFfisTool _tool; + + public MergeFfisCommand(MergeFfisTool tool) : base( "merge", "Merge multiple target platform FFI (foreign function interface) `.json` files into a cross-platform FFI `.json` file.") { + _tool = tool; + var directoryOption = new Option( "--inputDirectoryPath", "The input directory where the multiple target platform FFI (foreign function interface) `.json` files are located.") - { - IsRequired = true - }; + { + IsRequired = true + }; AddOption(directoryOption); var fileOption = new Option( @@ -30,6 +33,6 @@ public MergeFfisCommand() private void Main(string inputDirectoryPath, string outputFilePath) { - Console.WriteLine("Merge!"); + _tool.Run(inputDirectoryPath, outputFilePath); } } diff --git a/src/cs/production/c2ffi.Tool/Commands/Merge/MergeFfisTool.cs b/src/cs/production/c2ffi.Tool/Commands/Merge/MergeFfisTool.cs new file mode 100644 index 0000000..8242676 --- /dev/null +++ b/src/cs/production/c2ffi.Tool/Commands/Merge/MergeFfisTool.cs @@ -0,0 +1,374 @@ +// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved. +// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information. + +using System.Collections.Immutable; +using System.IO.Abstractions; +using c2ffi.Data; +using c2ffi.Data.Nodes; +using c2ffi.Data.Serialization; +using c2ffi.Tool.Commands.Merge.Input; +using c2ffi.Tool.Commands.Merge.Input.Sanitized; +using c2ffi.Tool.Commands.Merge.Input.Unsanitized; +using Microsoft.Extensions.Logging; + +namespace c2ffi.Tool.Commands.Merge; + +public sealed partial class MergeFfisTool +{ + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly MergeInputSanitizer _mergeInputSanitizer; + + private readonly List _enums = new(); + private readonly List _variables = new(); + private readonly List _opaqueTypes = new(); + private readonly List _functions = new(); + private readonly List _records = new(); + private readonly List _functionPointers = new(); + private readonly List _macroObjects = new(); + private readonly List _typeAliases = new(); + private readonly List _enumConstants = new(); + + private sealed class CNodeWithTargetPlatform + { + public readonly CNode Node; + public readonly TargetPlatform TargetPlatform; + + public CNodeWithTargetPlatform(CNode node, TargetPlatform targetPlatform) + { + Node = node; + TargetPlatform = targetPlatform; + } + } + + public MergeFfisTool( + ILogger logger, + IFileSystem fileSystem, + MergeInputSanitizer mergeInputSanitizer) + { + _logger = logger; + _fileSystem = fileSystem; + _mergeInputSanitizer = mergeInputSanitizer; + } + + public void Run(string inputDirectoryPath, string outputFilePath) + { + var options = GetOptions(inputDirectoryPath, outputFilePath); + var platformFfis = + GetPlatformFfis(options.InputFilePaths); + var platforms = platformFfis. + Select(x => x.PlatformRequested).ToImmutableArray(); + var platformNodesByName = GetPlatformNodesByName(platformFfis); + var ffi = CreateCrossPlatformFfi(platforms, platformNodesByName); + + Json.WriteFfiCrossPlatform(_fileSystem, options.OutputFilePath, ffi); + LogWriteAbstractSyntaxTreeSuccess(string.Join(", ", platforms), options.OutputFilePath); + } + + private MergeOptions GetOptions(string inputDirectoryPath, string outputFilePath) + { + var unsanitizedOptions = new UnsanitizedMergeOptions + { + InputDirectoryPath = inputDirectoryPath, + OutputFilePath = outputFilePath + }; + return _mergeInputSanitizer.Sanitize(unsanitizedOptions); + } + + private CFfiCrossPlatform CreateCrossPlatformFfi( + ImmutableArray platforms, + ImmutableSortedDictionary> platformNodesByName) + { + var result = new CFfiCrossPlatform(); + + foreach (var (name, nodes) in platformNodesByName) + { + BuildCrossPlatformNodes(platforms, nodes, name); + } + + result.Platforms = platforms.Sort( + (a, b) => + string.Compare(a.ClangTargetTriple, b.ClangTargetTriple, StringComparison.Ordinal)); + result.Enums = _enums.ToImmutableDictionary(x => x.Name); + result.Variables = _variables.ToImmutableDictionary(x => x.Name); + result.OpaqueTypes = _opaqueTypes.ToImmutableDictionary(x => x.Name); + result.Functions = _functions.ToImmutableDictionary(x => x.Name); + result.Records = _records.ToImmutableDictionary(x => x.Name); + result.FunctionPointers = _functionPointers.ToImmutableDictionary(x => x.Name); + result.MacroObjects = _macroObjects.ToImmutableDictionary(x => x.Name); + result.TypeAliases = _typeAliases.ToImmutableDictionary(x => x.Name); + result.EnumConstants = _enumConstants.ToImmutableDictionary(x => x.Name); + return result; + } + + private void AddCrossPlatformNode(CNodeWithTargetPlatform nodeWithTargetPlatform) + { + var node = nodeWithTargetPlatform.Node; + + if (node is CNodeWithLocation nodeWithLocation) + { + nodeWithLocation.Location = null; + } + + switch (node) + { + case CEnum @enum: + ClearLocationForTypeInfo(@enum.IntegerTypeInfo); + _enums.Add(@enum); + break; + case CVariable variable: + _variables.Add(variable); + break; + case COpaqueType opaqueType: + _opaqueTypes.Add(opaqueType); + break; + case CFunction function: + ClearLocationForTypeInfo(function.ReturnTypeInfo); + foreach (var parameter in function.Parameters) + { + parameter.Location = null; + ClearLocationForTypeInfo(parameter.TypeInfo); + } + + _functions.Add(function); + break; + case CRecord record: + foreach (var field in record.Fields) + { + field.Location = null; + ClearLocationForTypeInfo(field.TypeInfo); + } + + _records.Add(record); + break; + case CFunctionPointer functionPointer: + ClearLocationForTypeInfo(functionPointer.ReturnTypeInfo); + foreach (var parameter in functionPointer.Parameters) + { + ClearLocationForTypeInfo(parameter.TypeInfo); + } + + _functionPointers.Add(functionPointer); + break; + case CMacroObject macroObject: + ClearLocationForTypeInfo(macroObject.TypeInfo); + _macroObjects.Add(macroObject); + break; + case CTypeAlias typeAlias: + ClearLocationForTypeInfo(typeAlias.UnderlyingTypeInfo); + _typeAliases.Add(typeAlias); + break; + case CEnumConstant enumConstant: + ClearLocationForTypeInfo(enumConstant.TypeInfo); + _enumConstants.Add(enumConstant); + break; + default: + throw new NotImplementedException($"Unknown node type '{node.GetType()}'"); + } + } + + private void ClearLocationForTypeInfo(CTypeInfo typeInfo) + { + var currentTypeInfo = typeInfo; + while (currentTypeInfo != null) + { + currentTypeInfo.Location = null; + currentTypeInfo = currentTypeInfo.InnerTypeInfo; + } + } + + private void BuildCrossPlatformNodes( + ImmutableArray platforms, + ImmutableArray nodes, + string nodeName) + { + if (nodes.Length != platforms.Length) + { + var nodePlatforms = nodes.Select(x => x.TargetPlatform); + var missingNodePlatforms = platforms.Except(nodePlatforms); + var missingNodePlatformsString = string.Join(", ", missingNodePlatforms); + LogNodeNotCrossPlatform(nodeName, missingNodePlatformsString); + return; + } + + if (nodes.Length == 1) + { + AddCrossPlatformNode(nodes[0]); + return; + } + + var areAllEqual = true; + var firstNode = nodes[0].Node; + for (var i = 1; i < nodes.Length; i++) + { + var node = nodes[i].Node; + + if (!node.Equals(firstNode)) + { + if (node is CMacroObject nodeMacroObject && firstNode is CMacroObject firstNodeMacroObject) + { + if (nodeMacroObject.EqualsWithoutValue(firstNodeMacroObject)) + { + areAllEqual = false; + break; + } + } + + LogNodeNotEqual(nodeName); + areAllEqual = false; + break; + } + } + + if (areAllEqual) + { + AddCrossPlatformNode(nodes[0]); + } + } + + private ImmutableSortedDictionary> GetPlatformNodesByName( + ImmutableArray platformFfis) + { + var platformNodesByName = new Dictionary>(); + + foreach (var ffi in platformFfis) + { + AddPlatformFfi(ffi, platformNodesByName); + } + + var result = platformNodesByName. + ToImmutableSortedDictionary( + x => x.Key, + y => y.Value.ToImmutableArray()); + return result; + } + + private void AddPlatformFfi( + CFfiTargetPlatform ffi, + Dictionary> platformNodesByName) + { + foreach (var (name, node) in ffi.Enums) + { + AddPlatformNode(name, node, ffi.PlatformRequested, platformNodesByName); + } + + foreach (var (name, node) in ffi.Functions) + { + AddPlatformNode(name, node, ffi.PlatformRequested, platformNodesByName); + } + + foreach (var (name, node) in ffi.Records) + { + AddPlatformNode(name, node, ffi.PlatformRequested, platformNodesByName); + } + + foreach (var (name, node) in ffi.Variables) + { + AddPlatformNode(name, node, ffi.PlatformRequested, platformNodesByName); + } + + foreach (var (name, node) in ffi.EnumConstants) + { + AddPlatformNode(name, node, ffi.PlatformRequested, platformNodesByName); + } + + foreach (var (name, node) in ffi.FunctionPointers) + { + AddPlatformNode(name, node, ffi.PlatformRequested, platformNodesByName); + } + + foreach (var (name, node) in ffi.MacroObjects) + { + AddPlatformNode(name, node, ffi.PlatformRequested, platformNodesByName); + } + + foreach (var (name, node) in ffi.OpaqueTypes) + { + AddPlatformNode(name, node, ffi.PlatformRequested, platformNodesByName); + } + + foreach (var (name, node) in ffi.TypeAliases) + { + AddPlatformNode(name, node, ffi.PlatformRequested, platformNodesByName); + } + } + + private void AddPlatformNode( + string name, + CNode node, + TargetPlatform targetPlatform, + Dictionary> platformNodesByName) + { + if (!platformNodesByName.TryGetValue(name, out var platformNodes)) + { + platformNodes = new List(); + platformNodesByName.Add(name, platformNodes); + } + + if (platformNodes.Count >= 1) + { + var previousNode = platformNodes[^1].Node; + if (node.NodeKind != previousNode.NodeKind) + { + var nodeActualKind = node.NodeKind.ToString(); + var nodePlatform = targetPlatform.ToString(); + var nodeExpectedKind = previousNode.NodeKind.ToString(); + var nodePlatformExpectedKind = platformNodes[^1].TargetPlatform.ToString(); + LogNodeNotSameKind(name, nodeActualKind, nodePlatform, nodeExpectedKind, nodePlatformExpectedKind); + return; + } + } + + var nodeWithTargetPlatform = new CNodeWithTargetPlatform(node, targetPlatform); + platformNodes.Add(nodeWithTargetPlatform); + } + + private ImmutableArray GetPlatformFfis(ImmutableArray inputFilePaths) + { + var builder = ImmutableArray.CreateBuilder(); + + foreach (var filePath in inputFilePaths) + { + CFfiTargetPlatform ffi; + try + { + ffi = Json.ReadFfiTargetPlatform(_fileSystem, filePath); + } +#pragma warning disable CA1031 + catch (Exception e) +#pragma warning restore CA1031 + { + LogFailedToLoadTargetPlatformAbstractSyntaxTree(e, filePath); + continue; + } + + if (ffi.PlatformRequested.Equals(ffi.PlatformActual) && ffi.PlatformRequested.Equals(TargetPlatform.Unknown)) + { + // Skip any FFI which is unknown platform. + // This can happen if the cross-platform FFI is placed in the same folder as the target-platform FFIs. + continue; + } + + builder.Add(ffi); + } + + return builder.ToImmutable(); + } + + [LoggerMessage(0, LogLevel.Warning, "The node '{NodeName}' is not cross-platform; there is no matching node for platforms: {MissingPlatformNames}")] + private partial void LogNodeNotCrossPlatform(string nodeName, string missingPlatformNames); + + [LoggerMessage(1, LogLevel.Error, "The node '{NodeName}' of kind '{NodeActualKind}' for platform '{NodePlatform}' does not match the kind '{nodeExpectedKind}' for platform {NodePlatformExpectedKind}.")] + private partial void LogNodeNotSameKind(string nodeName, string nodeActualKind, string nodePlatform, string nodeExpectedKind, string nodePlatformExpectedKind); + + [LoggerMessage(2, LogLevel.Error, "The node '{NodeName}' is not equal to all other platform nodes of the same name.")] + private partial void LogNodeNotEqual(string nodeName); + + [LoggerMessage(3, LogLevel.Information, "Success. Merged FFIs for the target platforms '{TargetPlatformsString}': {FilePath}")] + private partial void LogWriteAbstractSyntaxTreeSuccess( + string targetPlatformsString, + string filePath); + + [LoggerMessage(4, LogLevel.Error, "Failed to load platform FFI: {FilePath}")] + private partial void LogFailedToLoadTargetPlatformAbstractSyntaxTree(Exception e, string filePath); +} diff --git a/src/cs/production/c2ffi.Tool/Commands/Merge/Startup.cs b/src/cs/production/c2ffi.Tool/Commands/Merge/Startup.cs index c0c04f4..42d456c 100644 --- a/src/cs/production/c2ffi.Tool/Commands/Merge/Startup.cs +++ b/src/cs/production/c2ffi.Tool/Commands/Merge/Startup.cs @@ -1,16 +1,17 @@ // Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved. // Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information. -using JetBrains.Annotations; +using c2ffi.Tool.Commands.Merge.Input; using Microsoft.Extensions.DependencyInjection; namespace c2ffi.Tool.Commands.Merge; -[UsedImplicitly] public sealed class Startup : IDependencyInjectionStartup { public void ConfigureServices(IServiceCollection services) { services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); } } diff --git a/src/cs/production/c2ffi.Tool/Generated/Microsoft.Extensions.Logging.Generators/Microsoft.Extensions.Logging.Generators.LoggerMessageGenerator/LoggerMessage.g.cs b/src/cs/production/c2ffi.Tool/Generated/Microsoft.Extensions.Logging.Generators/Microsoft.Extensions.Logging.Generators.LoggerMessageGenerator/LoggerMessage.g.cs index 86bd41d..e71bfeb 100644 --- a/src/cs/production/c2ffi.Tool/Generated/Microsoft.Extensions.Logging.Generators/Microsoft.Extensions.Logging.Generators.LoggerMessageGenerator/LoggerMessage.g.cs +++ b/src/cs/production/c2ffi.Tool/Generated/Microsoft.Extensions.Logging.Generators/Microsoft.Extensions.Logging.Generators.LoggerMessageGenerator/LoggerMessage.g.cs @@ -420,4 +420,70 @@ private partial void LogFailure() } } } +} +namespace c2ffi.Tool.Commands.Merge +{ + partial class MergeFfisTool + { + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")] + private static readonly global::System.Action __LogNodeNotCrossPlatformCallback = + global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Warning, new global::Microsoft.Extensions.Logging.EventId(0, nameof(LogNodeNotCrossPlatform)), "The node '{NodeName}' is not cross-platform; there is no matching node for platforms: {MissingPlatformNames}", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true }); + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")] + private partial void LogNodeNotCrossPlatform(global::System.String nodeName, global::System.String missingPlatformNames) + { + if (_logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Warning)) + { + __LogNodeNotCrossPlatformCallback(_logger, nodeName, missingPlatformNames, null); + } + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")] + private static readonly global::System.Action __LogNodeNotSameKindCallback = + global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Error, new global::Microsoft.Extensions.Logging.EventId(1, nameof(LogNodeNotSameKind)), "The node '{NodeName}' of kind '{NodeActualKind}' for platform '{NodePlatform}' does not match the kind '{nodeExpectedKind}' for platform {NodePlatformExpectedKind}.", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true }); + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")] + private partial void LogNodeNotSameKind(global::System.String nodeName, global::System.String nodeActualKind, global::System.String nodePlatform, global::System.String nodeExpectedKind, global::System.String nodePlatformExpectedKind) + { + if (_logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Error)) + { + __LogNodeNotSameKindCallback(_logger, nodeName, nodeActualKind, nodePlatform, nodeExpectedKind, nodePlatformExpectedKind, null); + } + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")] + private static readonly global::System.Action __LogNodeNotEqualCallback = + global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Error, new global::Microsoft.Extensions.Logging.EventId(2, nameof(LogNodeNotEqual)), "The node '{NodeName}' is not equal to all other platform nodes of the same name.", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true }); + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")] + private partial void LogNodeNotEqual(global::System.String nodeName) + { + if (_logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Error)) + { + __LogNodeNotEqualCallback(_logger, nodeName, null); + } + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")] + private static readonly global::System.Action __LogWriteAbstractSyntaxTreeSuccessCallback = + global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Information, new global::Microsoft.Extensions.Logging.EventId(3, nameof(LogWriteAbstractSyntaxTreeSuccess)), "Success. Merged FFIs for the target platforms '{TargetPlatformsString}': {FilePath}", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true }); + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")] + private partial void LogWriteAbstractSyntaxTreeSuccess(global::System.String targetPlatformsString, global::System.String filePath) + { + if (_logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Information)) + { + __LogWriteAbstractSyntaxTreeSuccessCallback(_logger, targetPlatformsString, filePath, null); + } + } + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")] + private static readonly global::System.Action __LogFailedToLoadTargetPlatformAbstractSyntaxTreeCallback = + global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Error, new global::Microsoft.Extensions.Logging.EventId(4, nameof(LogFailedToLoadTargetPlatformAbstractSyntaxTree)), "Failed to load platform FFI: {FilePath}", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true }); + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")] + private partial void LogFailedToLoadTargetPlatformAbstractSyntaxTree(global::System.Exception e, global::System.String filePath) + { + if (_logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Error)) + { + __LogFailedToLoadTargetPlatformAbstractSyntaxTreeCallback(_logger, filePath, e); + } + } + } } \ No newline at end of file diff --git a/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/ExtractFfiTest.cs b/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/ExtractFfiTest.cs index 9ad5ec5..4a43a96 100644 --- a/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/ExtractFfiTest.cs +++ b/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/ExtractFfiTest.cs @@ -38,9 +38,9 @@ protected ExtractFfiTest() _tool = services.GetService()!; } - public ImmutableArray GetFfis(string relativeConfigurationFilePath) + public ImmutableArray GetFfis(string configurationFilePath) { - var fullConfigurationFilePath = GetFullConfigurationFilePath(relativeConfigurationFilePath); + var fullConfigurationFilePath = _fileSystemHelper.GetFullFilePath(configurationFilePath); var oldFilePaths = GetFfiFilePaths(fullConfigurationFilePath); DeleteFiles(oldFilePaths); RunTool(fullConfigurationFilePath); @@ -48,22 +48,10 @@ public ImmutableArray GetFfis(string relativeConfigurationFilePath) return ReadFfis(filePaths); } - private string GetFullConfigurationFilePath(string relativeConfigurationFilePath) + private ImmutableArray ReadFfis(IEnumerable filePaths) { - var rootDirectoryPath = _fileSystemHelper.GitRepositoryRootDirectoryPath; - var result = _path.Combine(rootDirectoryPath, relativeConfigurationFilePath); - if (!_file.Exists(result)) - { - throw new InvalidOperationException($"Could not find the configuration file path '{relativeConfigurationFilePath}'."); - } - - return result; - } - - private ImmutableArray ReadFfis(IEnumerable ffiFilePaths) - { - var builder = ImmutableArray.CreateBuilder(); - foreach (var filePath in ffiFilePaths) + var builder = ImmutableArray.CreateBuilder(); + foreach (var filePath in filePaths) { var ffi = ReadFfi(filePath); builder.Add(ffi); @@ -97,7 +85,7 @@ private void DeleteFiles(IEnumerable filePaths) } } - private CTestFfi ReadFfi(string filePath) + private CTestFfiTargetPlatform ReadFfi(string filePath) { var ffi = Json.ReadFfiTargetPlatform(_fileSystem, filePath); @@ -109,7 +97,7 @@ private CTestFfi ReadFfi(string filePath) var functionPointers = CreateTestFunctionPointers(ffi); var opaqueDataTypes = CreateTestOpaqueTypes(ffi); - var result = new CTestFfi( + var result = new CTestFfiTargetPlatform( ffi.PlatformRequested.ToString(), ffi.PlatformActual.ToString(), functions, @@ -122,11 +110,11 @@ private CTestFfi ReadFfi(string filePath) return result; } - private static ImmutableDictionary CreateTestFunctions(CFfiTargetPlatform ast) + private static ImmutableDictionary CreateTestFunctions(CFfiTargetPlatform ffi) { var builder = ImmutableDictionary.CreateBuilder(); - foreach (var function in ast.Functions.Values) + foreach (var function in ffi.Functions.Values) { var result = CreateTestFunction(function); builder.Add(result.Name, result); @@ -177,11 +165,11 @@ private static CTestFunctionParameter CreateTestFunctionParameter(CFunctionParam return result; } - private static ImmutableDictionary CreateTestEnums(CFfiTargetPlatform ast) + private static ImmutableDictionary CreateTestEnums(CFfiTargetPlatform ffi) { var builder = ImmutableDictionary.CreateBuilder(); - foreach (var @enum in ast.Enums.Values) + foreach (var @enum in ffi.Enums.Values) { var result = CreateTestEnum(@enum); builder.Add(result.Name, result); @@ -226,11 +214,11 @@ private static CTestEnumValue CreateTestEnumValue(CEnumValue value) return result; } - private static ImmutableDictionary CreateTestRecords(CFfiTargetPlatform ast) + private static ImmutableDictionary CreateTestRecords(CFfiTargetPlatform ffi) { var builder = ImmutableDictionary.CreateBuilder(); - foreach (var value in ast.Records.Values) + foreach (var value in ffi.Records.Values) { var result = CreateTestRecord(value); builder.Add(result.Name, result); @@ -282,11 +270,11 @@ private static CTestRecordField CreateTestRecordField(CRecordField value) return result; } - private ImmutableDictionary CreateTestMacroObjects(CFfiTargetPlatform ast) + private ImmutableDictionary CreateTestMacroObjects(CFfiTargetPlatform ffi) { var builder = ImmutableDictionary.CreateBuilder(); - foreach (var value in ast.MacroObjects.Values) + foreach (var value in ffi.MacroObjects.Values) { var result = CreateMacroObject(value); builder.Add(result.Name, result); @@ -307,11 +295,11 @@ private CTestMacroObject CreateMacroObject(CMacroObject value) return result; } - private ImmutableDictionary CreateTestTypeAliases(CFfiTargetPlatform ast) + private ImmutableDictionary CreateTestTypeAliases(CFfiTargetPlatform ffi) { var builder = ImmutableDictionary.CreateBuilder(); - foreach (var value in ast.TypeAliases.Values) + foreach (var value in ffi.TypeAliases.Values) { var result = CreateTestTypeAlias(value); builder.Add(result.Name, result); @@ -332,11 +320,11 @@ private CTestTypeAlias CreateTestTypeAlias(CTypeAlias value) return result; } - private ImmutableDictionary CreateTestFunctionPointers(CFfiTargetPlatform ast) + private ImmutableDictionary CreateTestFunctionPointers(CFfiTargetPlatform ffi) { var builder = ImmutableDictionary.CreateBuilder(); - foreach (var value in ast.FunctionPointers.Values) + foreach (var value in ffi.FunctionPointers.Values) { var result = CreateTestFunctionPointer(value); builder.Add(result.Name, result); @@ -386,11 +374,11 @@ private static CTestFunctionPointerParameter CreateTestFunctionPointerParameter( } private static ImmutableDictionary CreateTestOpaqueTypes( - CFfiTargetPlatform ast) + CFfiTargetPlatform ffi) { var builder = ImmutableDictionary.CreateBuilder(); - foreach (var value in ast.OpaqueTypes.Values) + foreach (var value in ffi.OpaqueTypes.Values) { var result = CreateTestOpaqueType(value); builder.Add(result.Name, result); diff --git a/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_int/Test.cs b/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_int/Test.cs index bfde7b6..8ae4338 100644 --- a/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_int/Test.cs +++ b/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_int/Test.cs @@ -18,13 +18,13 @@ public void FunctionExists() $"src/c/tests/functions/{FunctionName}/config.json"); Assert.True(ffis.Length > 0); - foreach (var ast in ffis) + foreach (var ffi in ffis) { - AstFunctionExists(ast); + FfiFunctionExists(ffi); } } - private void AstFunctionExists(CTestFfi ast) + private void FfiFunctionExists(CTestFfiTargetPlatform ast) { var function = ast.GetFunction(FunctionName); Assert.True(function.CallingConvention == "cdecl"); diff --git a/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_int_params_int/Test.cs b/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_int_params_int/Test.cs index 1c663cb..e918310 100644 --- a/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_int_params_int/Test.cs +++ b/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_int_params_int/Test.cs @@ -18,15 +18,15 @@ public void FunctionExists() $"src/c/tests/functions/{FunctionName}/config.json"); Assert.True(ffis.Length > 0); - foreach (var ast in ffis) + foreach (var ffi in ffis) { - AstFunctionExists(ast); + FfiFunctionExists(ffi); } } - private static void AstFunctionExists(CTestFfi ast) + private static void FfiFunctionExists(CTestFfiTargetPlatform ffi) { - var function = ast.GetFunction(FunctionName); + var function = ffi.GetFunction(FunctionName); Assert.True(function.CallingConvention == "cdecl"); Assert.True(function.ReturnTypeName == "int"); diff --git a/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_uint64_params_uint8_uint16_uint32/Test.cs b/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_uint64_params_uint8_uint16_uint32/Test.cs index 068f827..fa5fad3 100644 --- a/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_uint64_params_uint8_uint16_uint32/Test.cs +++ b/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_uint64_params_uint8_uint16_uint32/Test.cs @@ -18,15 +18,15 @@ public void FunctionExists() $"src/c/tests/functions/{FunctionName}/config.json"); Assert.True(ffis.Length > 0); - foreach (var ast in ffis) + foreach (var ffi in ffis) { - AstFunctionExists(ast); + FfiFunctionExists(ffi); } } - private static void AstFunctionExists(CTestFfi ast) + private static void FfiFunctionExists(CTestFfiTargetPlatform ffi) { - var function = ast.GetFunction(FunctionName); + var function = ffi.GetFunction(FunctionName); Assert.True(function.CallingConvention == "cdecl"); Assert.True(function.ReturnTypeName == "uint64_t"); diff --git a/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/MergeFfisTest.cs b/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/MergeFfisTest.cs new file mode 100644 index 0000000..bfd6d4b --- /dev/null +++ b/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/MergeFfisTest.cs @@ -0,0 +1,376 @@ +// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved. +// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information. + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.IO.Abstractions; +using c2ffi.Data; +using c2ffi.Data.Nodes; +using c2ffi.Data.Serialization; +using c2ffi.Tests.Library; +using c2ffi.Tests.Library.Helpers; +using c2ffi.Tests.Library.Models; +using c2ffi.Tool.Commands.Merge; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace c2ffi.Tests.EndToEnd.Merge; + +[PublicAPI] +[ExcludeFromCodeCoverage] +public abstract class MergeFfisTest +{ + private readonly IFileSystem _fileSystem; + private readonly IPath _path; + private readonly IDirectory _directory; + private readonly IFile _file; + private readonly FileSystemHelper _fileSystemHelper; + private readonly MergeFfisTool _tool; + + protected MergeFfisTest() + { + var services = TestHost.Services; + _fileSystem = services.GetService()!; + _path = _fileSystem.Path; + _directory = _fileSystem.Directory; + _file = _fileSystem.File; + _fileSystemHelper = services.GetService()!; + _tool = services.GetService()!; + } + + public CTestFfiCrossPlatform GetFfi(string relativeInputDirectoryPath) + { + var fullInputDirectoryPath = _fileSystemHelper.GetFullDirectoryPath(relativeInputDirectoryPath); + var fullOutputFilePath = _fileSystem.Path.Combine(fullInputDirectoryPath, "../ffi-x/cross-platform.json"); + RunTool(fullInputDirectoryPath, fullOutputFilePath); + return ReadFfi(fullOutputFilePath); + } + + private void RunTool(string inputDirectoryPath, string outputFilePath) + { + _tool.Run(inputDirectoryPath, outputFilePath); + } + + private IEnumerable GetFfiFilePaths(string configurationFilePath) + { + var directoryPath = _path.GetDirectoryName(configurationFilePath)!; + var ffiDirectoryPath = _path.Combine(directoryPath, "ffi"); + if (!_directory.Exists(ffiDirectoryPath)) + { + return Array.Empty(); + } + + return _directory.EnumerateFiles(ffiDirectoryPath); + } + + private CTestFfiCrossPlatform ReadFfi(string filePath) + { + var ffi = Json.ReadFfiCrossPlatform(_fileSystem, filePath); + + var functions = CreateTestFunctions(ffi); + var enums = CreateTestEnums(ffi); + var structs = CreateTestRecords(ffi); + var macroObjects = CreateTestMacroObjects(ffi); + var typeAliases = CreateTestTypeAliases(ffi); + var functionPointers = CreateTestFunctionPointers(ffi); + var opaqueDataTypes = CreateTestOpaqueTypes(ffi); + + var result = new CTestFfiCrossPlatform( + functions, + enums, + structs, + macroObjects, + typeAliases, + functionPointers, + opaqueDataTypes); + return result; + } + + private static ImmutableDictionary CreateTestFunctions(CFfiCrossPlatform ffi) + { + var builder = ImmutableDictionary.CreateBuilder(); + + foreach (var function in ffi.Functions.Values) + { + var result = CreateTestFunction(function); + builder.Add(result.Name, result); + } + + return builder.ToImmutable(); + } + + private static CTestFunction CreateTestFunction(CFunction value) + { + var parameters = CreateTestFunctionParameters(value.Parameters); + + var result = new CTestFunction + { + Name = value.Name, +#pragma warning disable CA1308 + CallingConvention = value.CallingConvention.ToString().ToLowerInvariant(), +#pragma warning restore CA1308 + ReturnTypeName = value.ReturnTypeInfo.Name, + Parameters = parameters, + Comment = value.Comment + }; + return result; + } + + private static ImmutableArray CreateTestFunctionParameters( + ImmutableArray values) + { + var builder = ImmutableArray.CreateBuilder(); + + foreach (var value in values) + { + var result = CreateTestFunctionParameter(value); + builder.Add(result); + } + + return builder.ToImmutable(); + } + + private static CTestFunctionParameter CreateTestFunctionParameter(CFunctionParameter value) + { + var result = new CTestFunctionParameter + { + Name = value.Name, + TypeName = value.TypeInfo.Name + }; + + return result; + } + + private static ImmutableDictionary CreateTestEnums(CFfiCrossPlatform ffi) + { + var builder = ImmutableDictionary.CreateBuilder(); + + foreach (var @enum in ffi.Enums.Values) + { + var result = CreateTestEnum(@enum); + builder.Add(result.Name, result); + } + + return builder.ToImmutable(); + } + + private static CTestEnum CreateTestEnum(CEnum value) + { + var values = CreateTestEnumValues(value.Values); + + var result = new CTestEnum + { + Name = value.Name, + IntegerType = value.IntegerTypeInfo.Name, + Values = values + }; + return result; + } + + private static ImmutableArray CreateTestEnumValues(ImmutableArray values) + { + var builder = ImmutableArray.CreateBuilder(); + + foreach (var value in values) + { + var result = CreateTestEnumValue(value); + builder.Add(result); + } + + return builder.ToImmutable(); + } + + private static CTestEnumValue CreateTestEnumValue(CEnumValue value) + { + var result = new CTestEnumValue + { + Name = value.Name, + Value = value.Value + }; + return result; + } + + private static ImmutableDictionary CreateTestRecords(CFfiCrossPlatform ffi) + { + var builder = ImmutableDictionary.CreateBuilder(); + + foreach (var value in ffi.Records.Values) + { + var result = CreateTestRecord(value); + builder.Add(result.Name, result); + } + + return builder.ToImmutable(); + } + + private static CTestRecord CreateTestRecord(CRecord value) + { + var name = value.Name; + var fields = CreateTestRecordFields(value.Fields); + + var result = new CTestRecord + { + Name = name, + SizeOf = value.SizeOf, + AlignOf = value.AlignOf, + Fields = fields, + IsUnion = false + }; + + return result; + } + + private static ImmutableArray CreateTestRecordFields(ImmutableArray values) + { + var builder = ImmutableArray.CreateBuilder(); + + foreach (var value in values) + { + var result = CreateTestRecordField(value); + builder.Add(result); + } + + return builder.ToImmutable(); + } + + private static CTestRecordField CreateTestRecordField(CRecordField value) + { + var result = new CTestRecordField + { + Name = value.Name, + TypeName = value.TypeInfo.Name, + OffsetOf = value.OffsetOf, + SizeOf = value.TypeInfo.SizeOf + }; + + return result; + } + + private ImmutableDictionary CreateTestMacroObjects(CFfiCrossPlatform ffi) + { + var builder = ImmutableDictionary.CreateBuilder(); + + foreach (var value in ffi.MacroObjects.Values) + { + var result = CreateMacroObject(value); + builder.Add(result.Name, result); + } + + return builder.ToImmutable(); + } + + private CTestMacroObject CreateMacroObject(CMacroObject value) + { + var result = new CTestMacroObject + { + Name = value.Name, + TypeName = value.TypeInfo.Name, + Value = value.Value + }; + + return result; + } + + private ImmutableDictionary CreateTestTypeAliases(CFfiCrossPlatform ffi) + { + var builder = ImmutableDictionary.CreateBuilder(); + + foreach (var value in ffi.TypeAliases.Values) + { + var result = CreateTestTypeAlias(value); + builder.Add(result.Name, result); + } + + return builder.ToImmutable(); + } + + private CTestTypeAlias CreateTestTypeAlias(CTypeAlias value) + { + var result = new CTestTypeAlias + { + Name = value.Name, + UnderlyingName = value.UnderlyingTypeInfo.Name, + UnderlyingKind = value.UnderlyingTypeInfo.NodeKind.ToString() + }; + + return result; + } + + private ImmutableDictionary CreateTestFunctionPointers(CFfiCrossPlatform ffi) + { + var builder = ImmutableDictionary.CreateBuilder(); + + foreach (var value in ffi.FunctionPointers.Values) + { + var result = CreateTestFunctionPointer(value); + builder.Add(result.Name, result); + } + + return builder.ToImmutable(); + } + + private CTestFunctionPointer CreateTestFunctionPointer(CFunctionPointer value) + { + var parameters = CreateTestFunctionPointerParameters(value.Parameters); + + var result = new CTestFunctionPointer + { + Name = value.Name, + CallingConvention = "todo", + ReturnTypeName = value.ReturnTypeInfo.Name, + Parameters = parameters + }; + + return result; + } + + private static ImmutableArray CreateTestFunctionPointerParameters( + ImmutableArray values) + { + var builder = ImmutableArray.CreateBuilder(); + + foreach (var value in values) + { + var result = CreateTestFunctionPointerParameter(value); + builder.Add(result); + } + + return builder.ToImmutable(); + } + + private static CTestFunctionPointerParameter CreateTestFunctionPointerParameter(CFunctionPointerParameter value) + { + var result = new CTestFunctionPointerParameter + { + Name = value.Name, + TypeName = value.TypeInfo.Name + }; + + return result; + } + + private static ImmutableDictionary CreateTestOpaqueTypes( + CFfiCrossPlatform ffi) + { + var builder = ImmutableDictionary.CreateBuilder(); + + foreach (var value in ffi.OpaqueTypes.Values) + { + var result = CreateTestOpaqueType(value); + builder.Add(result.Name, result); + } + + return builder.ToImmutable(); + } + + private static CTestOpaqueType CreateTestOpaqueType(COpaqueType value) + { + var result = new CTestOpaqueType + { + Name = value.Name, + SizeOf = value.SizeOf + }; + + return result; + } +} diff --git a/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/Test.cs b/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/Test.cs deleted file mode 100644 index b64e61c..0000000 --- a/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/Test.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved. -// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information. - -using System.IO.Abstractions; -using c2ffi.Tests.Library; -using c2ffi.Tests.Library.Helpers; -using Microsoft.Extensions.DependencyInjection; -using Xunit; -using Xunit.Abstractions; - -namespace c2ffi.Tests.EndToEnd.Merge; - -public class Test -{ - private readonly ITestOutputHelper _testOutputHelper; - private readonly IFileSystem _fileSystem; - private readonly FileSystemHelper _fileSystemHelper; - - public Test(ITestOutputHelper testOutputHelper) - { - _testOutputHelper = testOutputHelper; - var services = TestHost.Services; - _fileSystem = services.GetService()!; - _fileSystemHelper = services.GetService()!; - } - - [Fact] - public void Exists() - { - var rootDirectoryPath = _fileSystemHelper.GitRepositoryRootDirectoryPath; - var x = _fileSystem.Path.Combine(rootDirectoryPath, "src/c/tests/functions/function_int/ffi"); - var y = _fileSystem.DirectoryInfo.New(x); - Assert.True(y.Exists); - foreach (var z in y.EnumerateFiles()) - { - _testOutputHelper.WriteLine(z.FullName); - } - } -} diff --git a/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/function_int/Test.cs b/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/function_int/Test.cs new file mode 100644 index 0000000..6a73153 --- /dev/null +++ b/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/function_int/Test.cs @@ -0,0 +1,26 @@ +// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved. +// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information. + +using Xunit; + +#pragma warning disable CA1707 + +namespace c2ffi.Tests.EndToEnd.Merge.function_int; + +public class Test : MergeFfisTest +{ + private const string FunctionName = "function_int"; + + [Fact] + public void FunctionExists() + { + var ffi = GetFfi( + $"src/c/tests/functions/{FunctionName}/ffi"); + + var function = ffi.GetFunction(FunctionName); + Assert.True(function.CallingConvention == "cdecl"); + Assert.True(function.ReturnTypeName == "int"); + + Assert.True(function.Parameters.IsDefaultOrEmpty); + } +} diff --git a/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/function_int_params_int/Test.cs b/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/function_int_params_int/Test.cs new file mode 100644 index 0000000..c897509 --- /dev/null +++ b/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/function_int_params_int/Test.cs @@ -0,0 +1,30 @@ +// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved. +// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information. + +using c2ffi.Tests.Library.Models; +using Xunit; + +#pragma warning disable CA1707 + +namespace c2ffi.Tests.EndToEnd.Merge.function_int_params_int; + +public class Test : MergeFfisTest +{ + private const string FunctionName = "function_int_params_int"; + + [Fact] + public void FunctionExists() + { + var ffi = GetFfi( + $"src/c/tests/functions/{FunctionName}/ffi"); + + var function = ffi.GetFunction(FunctionName); + Assert.True(function.CallingConvention == "cdecl"); + Assert.True(function.ReturnTypeName == "int"); + + Assert.True(function.Parameters.Length == 1); + var parameter = function.Parameters[0]; + Assert.True(parameter.Name == "a"); + Assert.True(parameter.TypeName == "int"); + } +} diff --git a/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/function_uint64_params_uint8_uint16_uint32/Test.cs b/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/function_uint64_params_uint8_uint16_uint32/Test.cs new file mode 100644 index 0000000..2403287 --- /dev/null +++ b/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/function_uint64_params_uint8_uint16_uint32/Test.cs @@ -0,0 +1,39 @@ +// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved. +// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information. + +using c2ffi.Tests.Library.Models; +using Xunit; + +#pragma warning disable CA1707 + +namespace c2ffi.Tests.EndToEnd.Merge.function_uint64_params_uint8_uint16_uint32; + +public class Test : MergeFfisTest +{ + private const string FunctionName = "function_uint64_params_uint8_uint16_uint32"; + + [Fact] + public void FunctionExists() + { + var ffi = GetFfi( + $"src/c/tests/functions/{FunctionName}/ffi"); + + var function = ffi.GetFunction(FunctionName); + Assert.True(function.CallingConvention == "cdecl"); + Assert.True(function.ReturnTypeName == "uint64_t"); + + Assert.True(function.Parameters.Length == 3); + + var parameter1 = function.Parameters[0]; + Assert.True(parameter1.Name == "a"); + Assert.True(parameter1.TypeName == "uint8_t"); + + var parameter2 = function.Parameters[1]; + Assert.True(parameter2.Name == "b"); + Assert.True(parameter2.TypeName == "uint16_t"); + + var parameter3 = function.Parameters[2]; + Assert.True(parameter3.Name == "c"); + Assert.True(parameter3.TypeName == "uint32_t"); + } +} diff --git a/src/cs/tests/c2ffi.Tests.Library/Helpers/FileSystemHelper.cs b/src/cs/tests/c2ffi.Tests.Library/Helpers/FileSystemHelper.cs index 13bd457..9c02cd6 100644 --- a/src/cs/tests/c2ffi.Tests.Library/Helpers/FileSystemHelper.cs +++ b/src/cs/tests/c2ffi.Tests.Library/Helpers/FileSystemHelper.cs @@ -19,6 +19,31 @@ public FileSystemHelper(IFileSystem fileSystem) _fileSystem = fileSystem; } + public string GetFullFilePath(string relativeFilePath) + { + var rootDirectoryPath = GitRepositoryRootDirectoryPath; + var filePath = _fileSystem.Path.Combine(rootDirectoryPath, relativeFilePath); + + if (!_fileSystem.File.Exists(filePath)) + { + throw new InvalidOperationException($"Could not find file path: {filePath}"); + } + + return filePath; + } + + public string GetFullDirectoryPath(string relativeDirectoryPath) + { + var rootDirectoryPath = GitRepositoryRootDirectoryPath; + var directoryPath = _fileSystem.Path.Combine(rootDirectoryPath, relativeDirectoryPath); + if (!_fileSystem.Directory.Exists(directoryPath)) + { + throw new InvalidOperationException($"Could not find directory path: {relativeDirectoryPath}"); + } + + return directoryPath; + } + private string FindGitRepositoryRootDirectoryPath() { var baseDirectory = AppContext.BaseDirectory; diff --git a/src/cs/tests/c2ffi.Tests.Library/Models/CTestFfiCrossPlatform.cs b/src/cs/tests/c2ffi.Tests.Library/Models/CTestFfiCrossPlatform.cs new file mode 100644 index 0000000..47dd5bb --- /dev/null +++ b/src/cs/tests/c2ffi.Tests.Library/Models/CTestFfiCrossPlatform.cs @@ -0,0 +1,257 @@ +// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved. +// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information. + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; +using Xunit; + +namespace c2ffi.Tests.Library.Models; + +[PublicAPI] +[ExcludeFromCodeCoverage] +public sealed class CTestFfiCrossPlatform +{ + private readonly ImmutableDictionary _enums; + private readonly ImmutableDictionary _functions; + private readonly ImmutableDictionary _macroObjects; + private readonly ImmutableDictionary _records; + private readonly ImmutableDictionary _typeAliases; + private readonly ImmutableDictionary _functionPointers; + private readonly ImmutableDictionary _opaqueTypes; + + private readonly ImmutableHashSet.Builder _namesTested; + + public CTestFfiCrossPlatform( + ImmutableDictionary functions, + ImmutableDictionary enums, + ImmutableDictionary records, + ImmutableDictionary macroObjects, + ImmutableDictionary typeAliases, + ImmutableDictionary functionPointers, + ImmutableDictionary opaqueTypes) + { + _functions = functions; + _enums = enums; + _records = records; + _macroObjects = macroObjects; + _typeAliases = typeAliases; + _functionPointers = functionPointers; + _opaqueTypes = opaqueTypes; + _namesTested = ImmutableHashSet.CreateBuilder(); + + AssertPInvokePlatformNameFunction(); + } + + public void AssertNodesAreTested() + { + foreach (var value in _enums.Values) + { + Assert.True(_namesTested.Contains(value.Name), $"The C enum '{value.Name}' is not covered in a test!"); + } + + foreach (var value in _records.Values) + { + Assert.True(_namesTested.Contains(value.Name), $"The C record '{value.Name}' is not covered in a test!"); + } + + foreach (var value in _macroObjects.Values) + { + Assert.True(_namesTested.Contains(value.Name), $"The C macro object '{value.Name}' is not covered in a test!"); + } + + foreach (var value in _typeAliases.Values) + { + Assert.True(_namesTested.Contains(value.Name), $"The C type alias '{value.Name}' is not covered in a test!"); + } + + foreach (var value in _functionPointers.Values) + { + Assert.True(_namesTested.Contains(value.Name), $"The C function pointer '{value.Name}' is not covered in a test!"); + } + } + + public CTestFunction GetFunction(string name) + { + var exists = _functions.TryGetValue(name, out var value); + Assert.True(exists, $"The function '{name}' does not exist."); + _namesTested.Add(name); + return value!; + } + + public CTestFunction? TryGetFunction(string name) + { + var exists = _functions.TryGetValue(name, out var value); + if (!exists) + { + return null; + } + + _namesTested.Add(name); + return value; + } + + public CTestEnum GetEnum(string name) + { + var exists = _enums.TryGetValue(name, out var value); + Assert.True(exists, $"The enum '{name}' does not exist."); + _namesTested.Add(name); + return value!; + } + + public CTestEnum? TryGetEnum(string name) + { + var exists = _enums.TryGetValue(name, out var value); + if (!exists) + { + return null; + } + + _namesTested.Add(name); + return value; + } + + public CTestRecord GetRecord(string name) + { + var exists = _records.TryGetValue(name, out var value); + Assert.True(exists, $"The record '{name}' does not exist."); + _namesTested.Add(name); + AssertRecord(value!); + return value!; + } + + public CTestRecord? TryGetRecord(string name) + { + var exists = _records.TryGetValue(name, out var value); + if (!exists) + { + return null; + } + + _namesTested.Add(name); + AssertRecord(value!); + return value; + } + + public CTestMacroObject GetMacroObject(string name) + { + var exists = _macroObjects.TryGetValue(name, out var value); + Assert.True(exists, $"The macro object '{name}' does not exist."); + _namesTested.Add(name); + return value!; + } + + public CTestMacroObject? TryGetMacroObject(string name) + { + var exists = _macroObjects.TryGetValue(name, out var value); + if (!exists) + { + return null; + } + + _namesTested.Add(name); + return value; + } + + public CTestTypeAlias GetTypeAlias(string name) + { + var exists = _typeAliases.TryGetValue(name, out var value); + Assert.True(exists, $"The type alias '{name}' does not exist."); + _namesTested.Add(name); + return value!; + } + + public CTestTypeAlias? TryGetTypeAlias(string name) + { + var exists = _typeAliases.TryGetValue(name, out var value); + if (!exists) + { + return null; + } + + _namesTested.Add(name); + return value; + } + + public CTestFunctionPointer GetFunctionPointer(string name) + { + var exists = _functionPointers.TryGetValue(name, out var value); + Assert.True(exists, $"The function pointer '{name}' does not exist."); + _namesTested.Add(name); + return value!; + } + + public CTestFunctionPointer? TryGetFunctionPointer(string name) + { + var exists = _functionPointers.TryGetValue(name, out var value); + return exists ? value : null; + } + + public CTestOpaqueType GetOpaqueType(string name) + { + var exists = _opaqueTypes.TryGetValue(name, out var value); + Assert.True(exists, $"The opaque type '{name}' does not exist."); + _namesTested.Add(name); + return value!; + } + + public CTestOpaqueType? TryGetOpaqueType(string name) + { + var exists = _opaqueTypes.TryGetValue(name, out var value); + return exists ? value : null; + } + + private void AssertRecord(CTestRecord record) + { + var namesLookup = new List(); + + foreach (var field in record.Fields) + { + AssertRecordField(record, field, namesLookup); + } + + Assert.True( + record.AlignOf > 0, + $"C record '{record.Name}' does not have an alignment of which is positive."); + + Assert.True( + record.SizeOf >= 0, + $"C record '{record.Name}' does not have an size of of which is positive or zero."); + } + + private void AssertRecordField(CTestRecord record, CTestRecordField field, List namesLookup) + { + var recordKindName = record.IsUnion ? "union" : "struct"; + + Assert.False( + namesLookup.Contains(field.Name), + $"C {recordKindName} '{record.Name}' already has a field named `{field.Name}`."); + namesLookup.Add(field.Name); + + Assert.True( + field.OffsetOf >= 0, + $"C {recordKindName} '{record.Name}' field '{field.Name}' does not have an offset of which is positive or zero."); + Assert.True( + field.SizeOf > 0, + $"C {recordKindName} '{record.Name}' field '{field.Name}' does not have a size of which is positive."); + + if (record.IsUnion) + { + Assert.True( + field.OffsetOf == 0, + $"C union '{record.Name}' field '{field.Name}' does not have an offset of zero."); + Assert.True( + field.SizeOf == record.SizeOf, + $"C union '{record.Name}' field '{field.Name}' does not have a size that matches the union."); + } + } + + private void AssertPInvokePlatformNameFunction() + { + var function = GetFunction("ffi_get_platform_name"); + Assert.Equal("cdecl", function.CallingConvention); + Assert.Equal("const char *", function.ReturnTypeName); + Assert.Equal("// Returns the current platform name.", function.Comment); + Assert.True(function.Parameters.IsDefaultOrEmpty); + } +} diff --git a/src/cs/tests/c2ffi.Tests.Library/Models/CTestFfi.cs b/src/cs/tests/c2ffi.Tests.Library/Models/CTestFfiTargetPlatform.cs similarity index 99% rename from src/cs/tests/c2ffi.Tests.Library/Models/CTestFfi.cs rename to src/cs/tests/c2ffi.Tests.Library/Models/CTestFfiTargetPlatform.cs index 389d3d3..93d5c66 100644 --- a/src/cs/tests/c2ffi.Tests.Library/Models/CTestFfi.cs +++ b/src/cs/tests/c2ffi.Tests.Library/Models/CTestFfiTargetPlatform.cs @@ -10,7 +10,7 @@ namespace c2ffi.Tests.Library.Models; [PublicAPI] [ExcludeFromCodeCoverage] -public sealed class CTestFfi +public sealed class CTestFfiTargetPlatform { private readonly ImmutableDictionary _enums; private readonly ImmutableDictionary _functions; @@ -26,7 +26,7 @@ public sealed class CTestFfi public string TargetPlatformActual { get; } - public CTestFfi( + public CTestFfiTargetPlatform( string targetPlatformRequested, string targetPlatformActual, ImmutableDictionary functions,