diff --git a/.github/workflows/dotnet-integration.yml b/.github/workflows/dotnet-integration.yml new file mode 100644 index 00000000..5bd5a8f9 --- /dev/null +++ b/.github/workflows/dotnet-integration.yml @@ -0,0 +1,32 @@ +name: Integration Tests + +on: + workflow_dispatch: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # depth is needed for nbgv + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 6.0.x + 8.0.x + include-prerelease: true + + - name: Build + run: dotnet build Dapper.AOT.sln -c Debug + + - name: E2E Tests + run: dotnet test test/Dapper.AOT.Test.Integration/Dapper.AOT.Test.Integration.csproj --no-build --verbosity normal -c Debug \ No newline at end of file diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index d3140df0..a45009ae 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -13,40 +13,40 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # depth is needed for nbgv - - - name: Setup .NET - uses: actions/setup-dotnet@v1 - with: - dotnet-version: | - 6.0.x - 8.0.x - include-prerelease: true - - - uses: dotnet/nbgv@master - with: - setAllVars: true - - - name: Restore dependencies - run: dotnet restore Build.csproj - - - name: Purge - run: del src/Dapper.*/bin/Release/Dapper.*.nupkg - - - name: Build - run: dotnet build Build.csproj --no-restore -c Release - - - name: Test - run: dotnet test Build.csproj --no-build --verbosity normal -c Release -f net6.0 --filter FullyQualifiedName!~Integration - - - name: Pack - if: ${{ success() && !github.base_ref }} - run: dotnet pack src/Dapper.AOT/Dapper.AOT.csproj --no-build --verbosity normal -c Release - - - name: Push to MyGet - if: ${{ success() && !github.base_ref }} - run: dotnet nuget push src/Dapper.*/bin/Release/Dapper.*.nupkg --source https://www.myget.org/F/dapper/api/v2/package --api-key "$env:MYGETAPIKEY" - env: - MYGETAPIKEY: ${{ secrets.MYGETAPIKEY }} + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # depth is needed for nbgv + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 6.0.x + 8.0.x + include-prerelease: true + + - uses: dotnet/nbgv@master + with: + setAllVars: true + + - name: Restore dependencies + run: dotnet restore Build.csproj + + - name: Purge + run: del src/Dapper.*/bin/Release/Dapper.*.nupkg + + - name: Build + run: dotnet build Build.csproj --no-restore -c Release + + - name: Test + run: dotnet test Build.csproj --no-build --verbosity normal -c Release -f net6.0 --filter FullyQualifiedName!~Integration + + - name: Pack + if: ${{ success() && !github.base_ref }} + run: dotnet pack src/Dapper.AOT/Dapper.AOT.csproj --no-build --verbosity normal -c Release + + - name: Push to MyGet + if: ${{ success() && !github.base_ref }} + run: dotnet nuget push src/Dapper.*/bin/Release/Dapper.*.nupkg --source https://www.myget.org/F/dapper/api/v2/package --api-key "$env:MYGETAPIKEY" + env: + MYGETAPIKEY: ${{ secrets.MYGETAPIKEY }} \ No newline at end of file diff --git a/Dapper.AOT.sln b/Dapper.AOT.sln index 15a5e283..0942f68d 100644 --- a/Dapper.AOT.sln +++ b/Dapper.AOT.sln @@ -14,6 +14,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props .github\workflows\dotnet.yml = .github\workflows\dotnet.yml + .github\workflows\dotnet-integration.yml = .github\workflows\dotnet-integration.yml global.json = global.json nuget.config = nuget.config README.md = README.md @@ -40,6 +41,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "docs", "docs\docs.csproj", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UsageVanilla", "test\UsageVanilla\UsageVanilla.csproj", "{840EA1CA-62FF-409E-89F5-CD3BB269BAE3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapper.AOT.Test.Integration.Executables", "test\Dapper.AOT.Test.Integration.Executables\Dapper.AOT.Test.Integration.Executables.csproj", "{9FA6A3B4-5722-40B2-8B58-F418F5D8C9A8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapper.AOT.Test.Integration", "test\Dapper.AOT.Test.Integration\Dapper.AOT.Test.Integration.csproj", "{CAB51448-4F0D-497B-A035-2B7ACB2DD9EB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -78,6 +83,14 @@ Global {840EA1CA-62FF-409E-89F5-CD3BB269BAE3}.Debug|Any CPU.Build.0 = Debug|Any CPU {840EA1CA-62FF-409E-89F5-CD3BB269BAE3}.Release|Any CPU.ActiveCfg = Release|Any CPU {840EA1CA-62FF-409E-89F5-CD3BB269BAE3}.Release|Any CPU.Build.0 = Release|Any CPU + {9FA6A3B4-5722-40B2-8B58-F418F5D8C9A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FA6A3B4-5722-40B2-8B58-F418F5D8C9A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FA6A3B4-5722-40B2-8B58-F418F5D8C9A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FA6A3B4-5722-40B2-8B58-F418F5D8C9A8}.Release|Any CPU.Build.0 = Release|Any CPU + {CAB51448-4F0D-497B-A035-2B7ACB2DD9EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CAB51448-4F0D-497B-A035-2B7ACB2DD9EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CAB51448-4F0D-497B-A035-2B7ACB2DD9EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CAB51448-4F0D-497B-A035-2B7ACB2DD9EB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -91,6 +104,8 @@ Global {A77B633C-573E-43CD-85A4-8063B33143B4} = {FE215D4B-811B-47BB-9F05-6382DD1C6729} {C6527566-38F4-43CC-9E0E-91C4B8854774} = {1135D4FD-770E-41DF-920B-A8F75E42A832} {840EA1CA-62FF-409E-89F5-CD3BB269BAE3} = {9A846B95-90CE-4335-9043-48C5B8EA4FB8} + {9FA6A3B4-5722-40B2-8B58-F418F5D8C9A8} = {9A846B95-90CE-4335-9043-48C5B8EA4FB8} + {CAB51448-4F0D-497B-A035-2B7ACB2DD9EB} = {9A846B95-90CE-4335-9043-48C5B8EA4FB8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A89CDAFA-494F-4168-9648-1138BA738D43} diff --git a/Directory.Packages.props b/Directory.Packages.props index e7f3774c..37851bc2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,6 +13,7 @@ + diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs index 702feb08..951a1060 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs @@ -16,7 +16,6 @@ using System.Diagnostics; using System.Globalization; using System.Linq; -using System.Runtime.InteropServices.ComTypes; using System.Text; using System.Threading; using static Dapper.Internal.Inspection; @@ -26,12 +25,33 @@ namespace Dapper.CodeAnalysis; [Generator(LanguageNames.CSharp), DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed partial class DapperInterceptorGenerator : InterceptorGeneratorBase { + private readonly bool _withInterceptionRecording = false; + public override ImmutableArray SupportedDiagnostics => DiagnosticsBase.All(); #pragma warning disable CS0067 // unused; retaining for now public event Action? Log; #pragma warning restore CS0067 + /// + /// Creates an interceptor generator for Dapper + /// + public DapperInterceptorGenerator() + { + } + + /// + /// Creates an interceptor generator for Dapper used for Tests. + /// + /// + /// It will insert very specific call with known method name. + /// Users will not have a reference to inserted assembly code, therefore: don't make it public + /// + internal DapperInterceptorGenerator(bool withInterceptionRecording) + { + _withInterceptionRecording = withInterceptionRecording; + } + public override void Initialize(IncrementalGeneratorInitializationContext context) { var nodes = context.SyntaxProvider.CreateSyntaxProvider(PreFilter, Parse) @@ -360,6 +380,13 @@ internal void Generate(in GenerateState ctx) sb.NewLine(); + if (_withInterceptionRecording) + { + sb.Append("// record interception for tests assertions").NewLine(); + sb.Append("global::Dapper.AOT.Test.Integration.Executables.Recording.InterceptorRecorderResolver.Resolve().Record();").NewLine(); + sb.NewLine(); + } + if (flags.HasAny(OperationFlags.GetRowParser)) { WriteGetRowParser(sb, resultType, readers); diff --git a/src/Dapper.AOT.Analyzers/Dapper.AOT.Analyzers.csproj b/src/Dapper.AOT.Analyzers/Dapper.AOT.Analyzers.csproj index 47f8010a..0a113eb8 100644 --- a/src/Dapper.AOT.Analyzers/Dapper.AOT.Analyzers.csproj +++ b/src/Dapper.AOT.Analyzers/Dapper.AOT.Analyzers.csproj @@ -59,4 +59,8 @@ DapperAnalyzer.cs + + + + diff --git a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs index 00c4d359..4e172c87 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs @@ -505,7 +505,7 @@ public DapperSpecialType DapperSpecialType ContainingNamespace: { Name: "Dapper", - IsGlobalNamespace: true + ContainingNamespace.IsGlobalNamespace: true } }) return DapperSpecialType.DbString; diff --git a/test/Dapper.AOT.Test.Integration.Executables/Dapper.AOT.Test.Integration.Executables.csproj b/test/Dapper.AOT.Test.Integration.Executables/Dapper.AOT.Test.Integration.Executables.csproj new file mode 100644 index 00000000..828f4929 --- /dev/null +++ b/test/Dapper.AOT.Test.Integration.Executables/Dapper.AOT.Test.Integration.Executables.csproj @@ -0,0 +1,16 @@ + + + net8.0;net6.0;net48 + Dapper.AOT.Test.Integration.Executables + + + + + + + + + + + + diff --git a/test/Dapper.AOT.Test.Integration.Executables/ExceptedCodeInterceptorRecorder.cs b/test/Dapper.AOT.Test.Integration.Executables/ExceptedCodeInterceptorRecorder.cs new file mode 100644 index 00000000..76f6c489 --- /dev/null +++ b/test/Dapper.AOT.Test.Integration.Executables/ExceptedCodeInterceptorRecorder.cs @@ -0,0 +1,51 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Text; +using Dapper.AOT.Test.Integration.Executables.Recording; +using Dapper.CodeAnalysis; + +namespace Dapper.AOT.Test.Integration.Executables; + +public class ExceptedCodeInterceptorRecorder : IInterceptorRecorder +{ + readonly string expectedFileName; + + public ExceptedCodeInterceptorRecorder(string expectedFileName) + { + this.expectedFileName = expectedFileName ?? throw new ArgumentNullException(nameof(expectedFileName)); + } + + public bool WasCalled { get; private set; } + public string? Diagnostics { get; private set; } + + public void Record() + { + WasCalled = true; + var diagnostics = new StringBuilder(); + + var stackTrace = new StackTrace(fNeedFileInfo: true); + var recentFrames = stackTrace.GetFrames().Take(15).ToList(); // we dont need everything + + var userCodeFrameIndex = recentFrames.FindIndex(frame => + frame.GetFileName()?.Contains(expectedFileName) == true && frame.GetMethod()?.Name.Equals(nameof(IExecutable.Execute)) == true); + if (userCodeFrameIndex == -1) + { + diagnostics.AppendLine("- User code execution is not found"); + } + + var dapperInterceptionFrameIndex = recentFrames.FindIndex(frame => + frame.GetFileName()?.Contains(".generated.cs") == true && frame.GetFileName()?.Contains(nameof(DapperInterceptorGenerator)) == true); + if (dapperInterceptionFrameIndex == -1) + { + diagnostics.AppendLine("- User code execution is not found"); + } + + if (userCodeFrameIndex < dapperInterceptionFrameIndex) + { + diagnostics.AppendLine("- User code call should be higher (executed before) on the stack trace than intercepted code"); + } + + Diagnostics = diagnostics.ToString(); + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration.Executables/IExecutable.cs b/test/Dapper.AOT.Test.Integration.Executables/IExecutable.cs new file mode 100644 index 00000000..98a062d4 --- /dev/null +++ b/test/Dapper.AOT.Test.Integration.Executables/IExecutable.cs @@ -0,0 +1,8 @@ +using System.Data; + +namespace Dapper.AOT.Test.Integration.Executables; + +public interface IExecutable +{ + public T Execute(IDbConnection connection); +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration.Executables/Models/DbStringPoco.cs b/test/Dapper.AOT.Test.Integration.Executables/Models/DbStringPoco.cs new file mode 100644 index 00000000..ed41851c --- /dev/null +++ b/test/Dapper.AOT.Test.Integration.Executables/Models/DbStringPoco.cs @@ -0,0 +1,9 @@ +namespace Dapper.AOT.Test.Integration.Executables.Models; + +public class DbStringPoco +{ + public const string TableName = "dbStringPoco"; + + public int Id { get; set; } + public string Name { get; set; } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration.Executables/Recording/IInterceptorRecorder.cs b/test/Dapper.AOT.Test.Integration.Executables/Recording/IInterceptorRecorder.cs new file mode 100644 index 00000000..02ef13df --- /dev/null +++ b/test/Dapper.AOT.Test.Integration.Executables/Recording/IInterceptorRecorder.cs @@ -0,0 +1,19 @@ +namespace Dapper.AOT.Test.Integration.Executables.Recording; + +public interface IInterceptorRecorder +{ + /// + /// returns true if expected interception was called + /// + public bool WasCalled { get; } + + /// + /// Returns diagnostics of recording + /// + public string? Diagnostics { get; } + + /// + /// Is executed in the interception + /// + public void Record(); +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration.Executables/Recording/InterceptorRecorderResolver.cs b/test/Dapper.AOT.Test.Integration.Executables/Recording/InterceptorRecorderResolver.cs new file mode 100644 index 00000000..c6119701 --- /dev/null +++ b/test/Dapper.AOT.Test.Integration.Executables/Recording/InterceptorRecorderResolver.cs @@ -0,0 +1,9 @@ +namespace Dapper.AOT.Test.Integration.Executables.Recording; + +public static class InterceptorRecorderResolver +{ + static IInterceptorRecorder? _recorder; + + public static IInterceptorRecorder? Resolve() => _recorder; + public static void Register(IInterceptorRecorder? recorder) => _recorder = recorder; +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration.Executables/UserCode/DbStringUsage.cs b/test/Dapper.AOT.Test.Integration.Executables/UserCode/DbStringUsage.cs new file mode 100644 index 00000000..a3181881 --- /dev/null +++ b/test/Dapper.AOT.Test.Integration.Executables/UserCode/DbStringUsage.cs @@ -0,0 +1,23 @@ +using System.Data; +using System.Linq; +using Dapper.AOT.Test.Integration.Executables.Models; + +namespace Dapper.AOT.Test.Integration.Executables.UserCode; + +[DapperAot] +public class DbStringUsage : IExecutable +{ + public DbStringPoco Execute(IDbConnection connection) + { + var results = connection.Query( + $"select * from {DbStringPoco.TableName} where name = @name", + new + { + name = new DbString + { + Value = "my-dbString" + } + }); + return results.First(); + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration/Dapper.AOT.Test.Integration.csproj b/test/Dapper.AOT.Test.Integration/Dapper.AOT.Test.Integration.csproj new file mode 100644 index 00000000..e69aaed0 --- /dev/null +++ b/test/Dapper.AOT.Test.Integration/Dapper.AOT.Test.Integration.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + Dapper.AOT.Test.Integration + enable + false + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Dapper.AOT.Test.Integration/DbStringTests.cs b/test/Dapper.AOT.Test.Integration/DbStringTests.cs new file mode 100644 index 00000000..9822803e --- /dev/null +++ b/test/Dapper.AOT.Test.Integration/DbStringTests.cs @@ -0,0 +1,43 @@ +using System; +using System.Data; +using Dapper.AOT.Test.Integration.Executables.Models; +using Dapper.AOT.Test.Integration.Executables.UserCode; +using Dapper.AOT.Test.Integration.Setup; + +namespace Dapper.AOT.Test.Integration; + +[Collection(SharedPostgresqlClient.Collection)] +public class DbStringTests : IntegrationTestsBase +{ + public DbStringTests(PostgresqlFixture fixture) : base(fixture) + { + } + + protected override void SetupDatabase(IDbConnection dbConnection) + { + base.SetupDatabase(dbConnection); + + dbConnection.Execute($""" + CREATE TABLE IF NOT EXISTS {DbStringPoco.TableName}( + id integer PRIMARY KEY, + name varchar(40) + ); + + TRUNCATE {DbStringPoco.TableName}; + + INSERT INTO {DbStringPoco.TableName} (id, name) + VALUES (1, 'my-dbString'), + (2, 'my-poco'), + (3, 'your-data'); + """); + } + + [Fact] + public void DbString_BasicUsage_InterceptsAndReturnsExpectedData() + { + var result = ExecuteInterceptedUserCode(DbConnection); + + Assert.True(result.Id.Equals(1)); + Assert.True(result.Name.Equals("my-dbString", StringComparison.InvariantCultureIgnoreCase)); + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration/Helpers/CompilationExtensions.cs b/test/Dapper.AOT.Test.Integration/Helpers/CompilationExtensions.cs new file mode 100644 index 00000000..d004eaf0 --- /dev/null +++ b/test/Dapper.AOT.Test.Integration/Helpers/CompilationExtensions.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using Dapper.AOT.Test.Integration.Setup; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Emit; + +namespace Dapper.AOT.Test.Integration.Helpers; + +internal static class CompilationExtensions +{ + public static Assembly CompileToAssembly(this Compilation compilation) + { + using var peStream = new MemoryStream(); + using var pdbstream = new MemoryStream(); + var dbg = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? DebugInformationFormat.Pdb : DebugInformationFormat.PortablePdb; + + var emitResult = compilation.Emit(peStream, pdbstream, null, null, null, new EmitOptions(false, dbg)); + if (!emitResult.Success) + { + TryThrowErrors(emitResult.Diagnostics); + } + + peStream.Position = pdbstream.Position = 0; + return Assembly.Load(peStream.ToArray(), pdbstream.ToArray()); + } + + static void TryThrowErrors(IEnumerable items) + { + var errors = new List(); + foreach (var item in items) + { + if (item.Severity == DiagnosticSeverity.Error) + { + errors.Add(item.GetMessage(CultureInfo.InvariantCulture)); + } + } + + if (errors.Count > 0) + { + throw new ExpressionParsingException(errors); + } + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration/Setup/ExpressionParsingException.cs b/test/Dapper.AOT.Test.Integration/Setup/ExpressionParsingException.cs new file mode 100644 index 00000000..46ef7fc2 --- /dev/null +++ b/test/Dapper.AOT.Test.Integration/Setup/ExpressionParsingException.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; + +namespace Dapper.AOT.Test.Integration.Setup; + +class ExpressionParsingException(IEnumerable errors) + : Exception(string.Join(Environment.NewLine, errors)) +{ +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs b/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs new file mode 100644 index 00000000..762259de --- /dev/null +++ b/test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using Dapper.AOT.Test.Integration.Executables; +using Dapper.AOT.Test.Integration.Executables.Recording; +using Dapper.AOT.Test.Integration.Helpers; +using Dapper.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; + +namespace Dapper.AOT.Test.Integration.Setup; + +public abstract class IntegrationTestsBase +{ + private const string UserCodeFileName = "UserCode.cs"; + static readonly CSharpParseOptions InterceptorSupportedParseOptions = new CSharpParseOptions(LanguageVersion.Preview) + .WithFeatures(new[] + { + new KeyValuePair("InterceptorsPreviewNamespaces", "$(InterceptorsPreviewNamespaces);Dapper.AOT"), + }); + + protected readonly IDbConnection DbConnection; + + protected IntegrationTestsBase(PostgresqlFixture fixture) + { + DbConnection = fixture.NpgsqlConnection; + SetupDatabase(DbConnection); + } + + protected virtual void SetupDatabase(IDbConnection dbConnection) + { + } + + protected TResult ExecuteInterceptedUserCode(IDbConnection dbConnection) + { + var userSourceCode = ReadUserSourceCode(); + + var inputCompilation = CSharpCompilation.Create( + assemblyName: "Test.dll", + syntaxTrees: [ Parse(UserCodeFileName, userSourceCode) ], + references: [ + // dotnet System.Runtime + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location), + + // Dapper and Dapper.AOT + MetadataReference.CreateFromFile(typeof(Dapper.SqlMapper).Assembly.Location), // Dapper + MetadataReference.CreateFromFile(typeof(Dapper.CommandFactory).Assembly.Location), // Dapper.AOT + + // user code + MetadataReference.CreateFromFile(typeof(Dapper.AOT.Test.Integration.Executables.UserCode.DbStringUsage).Assembly.Location), // Dapper.AOT.Test.Integration.Executables + + // dependencies + MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Data.Common.DbConnection).Assembly.Location), // System.Data.Common + + // Additional stuff required by Dapper.AOT generators + MetadataReference.CreateFromFile(Assembly.Load("System.Collections").Location), // System.Collections + ], + options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var interceptorRecorder = new ExceptedCodeInterceptorRecorder(UserCodeFileName); + InterceptorRecorderResolver.Register(interceptorRecorder); + + var generator = new DapperInterceptorGenerator(withInterceptionRecording: true); + GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator.AsSourceGenerator() }, parseOptions: InterceptorSupportedParseOptions); + _ = driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var outputCompilation, out var diagnostics); + + var assembly = outputCompilation.CompileToAssembly(); + var type = assembly.GetTypes().Single(t => t.FullName == typeof(TExecutable).FullName); + var executableInstance = Activator.CreateInstance(type); + var mainMethod = type.GetMethod(nameof(IExecutable.Execute), BindingFlags.Public | BindingFlags.Instance); + var result = mainMethod!.Invoke(obj: executableInstance, [ dbConnection ]); + + Assert.True(interceptorRecorder.WasCalled); + Assert.True(string.IsNullOrEmpty(interceptorRecorder.Diagnostics), userMessage: interceptorRecorder.Diagnostics); + + return (TResult)result!; + } + + private static SyntaxTree Parse(string filename, string text) + { + var stringText = SourceText.From(text, Encoding.UTF8); + return SyntaxFactory.ParseSyntaxTree(stringText, InterceptorSupportedParseOptions, filename); + } + + private static string ReadUserSourceCode() + { + var userTypeName = typeof(TExecutable).Name; + + // it's very fragile to get user code cs files into output directory (btw we can't remove them from compilation, because we will use them for assertions) + // so let's simply get back to test\ dir, and try to find Executables.UserCode from there + var testDir = Directory.GetParent(Directory.GetParent(Directory.GetParent(Directory.GetParent(Directory.GetCurrentDirectory())!.FullName)!.FullName)!.FullName); + return File.ReadAllText(Path.Combine(testDir!.FullName, "Dapper.AOT.Test.Integration.Executables", "UserCode", $"{userTypeName}.cs")); + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test.Integration/Setup/PostgresqlFixture.cs b/test/Dapper.AOT.Test.Integration/Setup/PostgresqlFixture.cs new file mode 100644 index 00000000..0d85fff2 --- /dev/null +++ b/test/Dapper.AOT.Test.Integration/Setup/PostgresqlFixture.cs @@ -0,0 +1,51 @@ +using Npgsql; +using System.Threading.Tasks; +using Testcontainers.PostgreSql; + +namespace Dapper.AOT.Test.Integration.Setup; + +[CollectionDefinition(Collection)] +public class SharedPostgresqlClient : ICollectionFixture +{ + public const string Collection = nameof(SharedPostgresqlClient); +} + +public sealed class PostgresqlFixture : IAsyncLifetime +{ + private readonly PostgreSqlContainer _postgresContainer = new PostgreSqlBuilder() + .WithImage("postgres:15-alpine") + .Build(); + + public string ConnectionString { get; private set; } = null!; + + private NpgsqlConnection? _sharedConnection; + + public NpgsqlConnection NpgsqlConnection + => _sharedConnection ??= CreateOpenConnection(); + + public NpgsqlConnection CreateOpenConnection() + { + var conn = new NpgsqlConnection(ConnectionString); + conn.Open(); + return conn; + } + + async Task IAsyncLifetime.InitializeAsync() + { + await _postgresContainer.StartAsync(); + ConnectionString = _postgresContainer.GetConnectionString(); + } + + async Task IAsyncLifetime.DisposeAsync() + { + await using (_postgresContainer) + { + var tmp = _sharedConnection; + _sharedConnection = null; + if (tmp is not null) + { + await tmp.DisposeAsync(); + } + } + } +} diff --git a/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj b/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj index 67a758e5..663a4053 100644 --- a/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj +++ b/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj @@ -33,7 +33,7 @@ - + @@ -48,8 +48,6 @@ runtime; build; native; contentfiles; analyzers - - @@ -59,4 +57,9 @@ + + + + + diff --git a/test/Dapper.AOT.Test/Integration/PostgresqlFixture.cs b/test/Dapper.AOT.Test/Integration/PostgresqlFixture.cs index 59329192..b2b5e506 100644 --- a/test/Dapper.AOT.Test/Integration/PostgresqlFixture.cs +++ b/test/Dapper.AOT.Test/Integration/PostgresqlFixture.cs @@ -1,5 +1,5 @@ -using Npgsql; -using System.Threading.Tasks; +using System.Threading.Tasks; +using Npgsql; using Testcontainers.PostgreSql; using Xunit;