Skip to content

Commit

Permalink
setup the infra for test
Browse files Browse the repository at this point in the history
  • Loading branch information
DeagleGross committed Jun 30, 2024
1 parent e804648 commit 63fd1a9
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 32 deletions.
2 changes: 1 addition & 1 deletion src/Dapper.AOT.Analyzers/InGeneration/DapperHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#if !DAPPERAOT_INTERNAL
file
#endif
static class DbStringHelpers
static partial class DbStringHelpers
{
public static void ConfigureDbStringDbParameter(
global::System.Data.Common.DbParameter dbParameter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@
</PropertyGroup>

<ItemGroup>
<Compile Remove="Accessors/Data/**/*.*.cs" />
<None Include="Accessors/Data/**/*.*" />
<None Update="Accessors/Data/**/*.*" CopyToOutputDirectory="PreserveNewest" />

<Compile Remove="Interceptors/**/*.*.cs" />
<None Include="Interceptors/**/*.*" />
<None Update="Interceptors/**/*.*" CopyToOutputDirectory="PreserveNewest" />

<Compile Include="..\..\src\Dapper.AOT.Analyzers\AotGridReader.cs" Link="AotGridReader.cs" />
<Compile Include="..\..\src\Dapper.AOT.Analyzers\InGeneration\DapperHelpers.cs" Link="DapperHelpers.cs" />

<Compile Remove="InterceptionExecutables/**/*.*.cs" />
<None Include="InterceptionExecutables/**/*.*" />
<None Update="InterceptionExecutables/**/*.*" CopyToOutputDirectory="PreserveNewest" />

<!-- <Compile Remove="InterceptionExecutables\Template.cs" />-->
<!-- <None Include="InterceptionExecutables\_Template.cs" />-->
<!-- <Compile Update="InterceptionExecutables\**\*.cs">-->
<!-- <CopyToOutputDirectory>Always</CopyToOutputDirectory>-->
<!-- </Compile>-->
</ItemGroup>

<ItemGroup>
Expand Down Expand Up @@ -49,6 +50,7 @@
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.Analyzer.Testing.XUnit" />
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.CodeFix.Testing.XUnit" />
<PackageReference Include="Testcontainers.PostgreSql" />
<ProjectReference Include="..\Dapper.AOT.Test\Dapper.AOT.Test.csproj" />
</ItemGroup>

</Project>
24 changes: 13 additions & 11 deletions test/Dapper.AOT.Test.Integration/DbStringTests.cs
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
using System.Linq;
using System.Data;
using System.Threading.Tasks;
using Dapper.AOT.Test.Integration.Setup;
using Xunit;
using Xunit.Abstractions;

namespace Dapper.AOT.Test.Integration;

[Collection(SharedPostgresqlClient.Collection)]
public class DbStringTests
public class DbStringTests : InterceptedCodeExecutionTestsBase
{
private PostgresqlFixture _fixture;

public DbStringTests(PostgresqlFixture fixture)
public DbStringTests(PostgresqlFixture fixture, ITestOutputHelper log) : base(fixture, log)
{
_fixture = fixture;
fixture.NpgsqlConnection.Execute("""
Fixture.NpgsqlConnection.Execute("""
CREATE TABLE IF NOT EXISTS dbStringTable(
id integer PRIMARY KEY,
name varchar(40) NOT NULL CHECK (name <> '')
);
TRUNCATE dbStringTable;
"""
);
""");
}

[Fact]
public void ExecuteMulti()
[DapperAot]
public async Task Test()
{

var sourceCode = PrepareSourceCodeFromFile("DbString");
var executionResults = BuildAndExecuteInterceptedUserCode<int>(sourceCode, methodName: "ExecuteAsync");

// TODO DO THE CHECK HERE
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Data;
using System.Data.SqlClient;
using System.Linq;

namespace InterceptionExecutables
{
using System;
using System.IO;
using Dapper;
using System.Threading.Tasks;

public static class Program
{
public static async Task<int> ExecuteAsync(IDbConnection dbConnection)
{
var res = await dbConnection.QueryAsync<int>("SELECT count(*) FROM dbStringTable");
return res.First();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace InterceptionExecutables
{
using System;
using System.IO;
using Dapper;
using System.Threading.Tasks;

// this is just a sample for easy test-writing
public static class Program
{
public static async Task<object> ExecuteAsync(IDbConnection dbConnection)
{
<your user code goes here>
}
}

<additional code goes here>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Dapper.CodeAnalysis;
using Dapper.TestCommon;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Text;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;

namespace Dapper.AOT.Test.Integration.Setup;

public abstract class InterceptedCodeExecutionTestsBase : GeneratorTestBase
{
protected readonly PostgresqlFixture Fixture;

protected InterceptedCodeExecutionTestsBase(PostgresqlFixture fixture, ITestOutputHelper? log) : base(log)
{
Fixture = fixture;
}

protected static string PrepareSourceCodeFromFile(string inputFileName, string extension = ".cs")
{
var fullPath = Path.Combine("InterceptionExecutables", inputFileName + extension);
if (!File.Exists(fullPath))
{
throw new FileNotFoundException(fullPath);
}

using var sr = new StreamReader(fullPath);
return sr.ReadToEnd();
}

protected T BuildAndExecuteInterceptedUserCode<T>(
string userSourceCode,
string className = "Program",
string methodName = "ExecuteAsync")
{
var inputCompilation = RoslynTestHelpers.CreateCompilation("Assembly", syntaxTrees: [
BuildInterceptorSupportedSyntaxTree(filename: "Program.cs", userSourceCode)
]);

var diagnosticsOutputStringBuilder = new StringBuilder();
var (compilation, generatorDriverRunResult, diagnostics, errorCount) = Execute<DapperInterceptorGenerator>(inputCompilation, diagnosticsOutputStringBuilder, initializer: g =>
{
g.Log += message => Log(message);
});

var results = Assert.Single(generatorDriverRunResult.Results);
Assert.NotNull(compilation);
Assert.True(errorCount == 0, "User code should not report errors");

var assembly = Compile(compilation!);
var type = assembly.GetTypes().Single(t => t.FullName == $"InterceptionExecutables.{className}");
var mainMethod = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static);
Assert.NotNull(mainMethod);

var result = mainMethod!.Invoke(obj: null, [ Fixture.NpgsqlConnection ]);
Assert.NotNull(result);
if (result is not Task<T> taskResult)
{
throw new XunitException($"expected execution result is '{typeof(Task<T>)}' but got {result!.GetType()}");
}

return taskResult.GetAwaiter().GetResult();
}

SyntaxTree BuildInterceptorSupportedSyntaxTree(string filename, string text)
{
var options = new CSharpParseOptions(LanguageVersion.Preview)
.WithFeatures(new []
{
new KeyValuePair<string, string>("InterceptorsPreviewNamespaces", "$(InterceptorsPreviewNamespaces);ProgramNamespace;Dapper.AOT"),
new KeyValuePair<string, string>("Features", "InterceptorsPreview"),
new KeyValuePair<string, string>("LangVersion", "preview"),
});

var stringText = SourceText.From(text, Encoding.UTF8);
return SyntaxFactory.ParseSyntaxTree(stringText, options, filename);
}

static Assembly Compile(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<Diagnostic> items)
{
var errors = new List<string>();
foreach (var item in items)
{
if (item.Severity == DiagnosticSeverity.Error)
{
errors.Add(item.GetMessage(CultureInfo.InvariantCulture));
}
}

if (errors.Count > 0)
{
throw new CompilationException(errors);
}
}

class CompilationException : Exception
{
public IEnumerable<string> Errors { get; private set; }

public CompilationException(IEnumerable<string> errors)
: base(string.Join(Environment.NewLine, errors))
{
this.Errors = errors;
}

public CompilationException(params string[] errors)
: base(string.Join(Environment.NewLine, errors))
{
this.Errors = errors;
}
}
}
25 changes: 17 additions & 8 deletions test/Dapper.AOT.Test/GeneratorTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,27 @@ protected GeneratorTestBase(ITestOutputHelper? log)
protected static string? GetOriginCodeLocation([CallerFilePath] string? path = null) => path;

// input from https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md#unit-testing-of-generators

protected (Compilation? Compilation, GeneratorDriverRunResult Result, ImmutableArray<Diagnostic> Diagnostics, int ErrorCount) Execute<T>(string source,
StringBuilder? diagnosticsTo = null,
[CallerMemberName] string? name = null,
string? fileName = null,
Action<T>? initializer = null
) where T : class, IIncrementalGenerator, new()
{
// Create the 'input' compilation that the generator will act on
if (string.IsNullOrWhiteSpace(name)) name = "compilation";
if (string.IsNullOrWhiteSpace(fileName)) fileName = "input.cs";
var inputCompilation = RoslynTestHelpers.CreateCompilation(source, name!, fileName!);

return Execute(inputCompilation, diagnosticsTo, initializer);
}

protected (Compilation? Compilation, GeneratorDriverRunResult Result, ImmutableArray<Diagnostic> Diagnostics, int ErrorCount) Execute<T>(
Compilation inputCompilation,
StringBuilder? diagnosticsTo = null,
Action<T>? initializer = null
) where T : class, IIncrementalGenerator, new()
{
void OutputDiagnostic(Diagnostic d)
{
Expand All @@ -54,26 +68,21 @@ void Output(string message, bool force = false)
diagnosticsTo?.AppendLine(message.Replace('\\', '/')); // need to normalize paths
}
}
// Create the 'input' compilation that the generator will act on
if (string.IsNullOrWhiteSpace(name)) name = "compilation";
if (string.IsNullOrWhiteSpace(fileName)) fileName = "input.cs";
Compilation inputCompilation = RoslynTestHelpers.CreateCompilation(source, name!, fileName!);

// directly create an instance of the generator
// (Note: in the compiler this is loaded from an assembly, and created via reflection at runtime)
T generator = new();
initializer?.Invoke(generator);

ShowDiagnostics("Input code", inputCompilation, diagnosticsTo, "CS8795", "CS1701", "CS1702");

// Create the driver that will control the generation, passing in our generator
GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator.AsSourceGenerator() }, parseOptions: RoslynTestHelpers.ParseOptionsLatestLangVer);

// Run the generation pass
// (Note: the generator driver itself is immutable, and all calls return an updated version of the driver that you should use for subsequent calls)
driver = driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var outputCompilation, out var diagnostics);
var runResult = driver.GetRunResult();

foreach (var result in runResult.Results)
{
if (result.Exception is not null) throw result.Exception;
Expand Down
39 changes: 36 additions & 3 deletions test/Dapper.AOT.Test/TestCommon/RoslynTestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

namespace Dapper.TestCommon;

internal static class RoslynTestHelpers
public static class RoslynTestHelpers
{
internal static readonly CSharpParseOptions ParseOptionsLatestLangVer = CSharpParseOptions.Default
.WithLanguageVersion(LanguageVersion.Latest)
Expand Down Expand Up @@ -45,8 +45,41 @@ internal static class RoslynTestHelpers
})
.WithFeatures(new[] { DapperInterceptorGenerator.FeatureKeys.InterceptorsPreviewNamespacePair });

public static Compilation CreateCompilation(string source, string name, string fileName)
=> CSharpCompilation.Create(name,
public static Compilation CreateCompilation(string assemblyName, SyntaxTree[] syntaxTrees, OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary)
=> CSharpCompilation.Create(assemblyName,
syntaxTrees: syntaxTrees,
references: new[] {
MetadataReference.CreateFromFile(typeof(Binder).Assembly.Location),
#if !NET48
MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location),
MetadataReference.CreateFromFile(Assembly.Load("System.Data").Location),
MetadataReference.CreateFromFile(Assembly.Load("netstandard").Location),
MetadataReference.CreateFromFile(Assembly.Load("System.Collections").Location),
MetadataReference.CreateFromFile(typeof(System.ComponentModel.DataAnnotations.Schema.ColumnAttribute).Assembly.Location),
#endif
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
MetadataReference.CreateFromFile(typeof(DbConnection).Assembly.Location),
MetadataReference.CreateFromFile(typeof(System.Data.SqlClient.SqlConnection).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Microsoft.Data.SqlClient.SqlConnection).Assembly.Location),
MetadataReference.CreateFromFile(typeof(OracleConnection).Assembly.Location),
MetadataReference.CreateFromFile(typeof(ValueTask<int>).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Component).Assembly.Location),
MetadataReference.CreateFromFile(typeof(DapperAotExtensions).Assembly.Location),
MetadataReference.CreateFromFile(typeof(SqlMapper).Assembly.Location),
MetadataReference.CreateFromFile(typeof(ImmutableList<int>).Assembly.Location),
MetadataReference.CreateFromFile(typeof(ImmutableArray<int>).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location),
MetadataReference.CreateFromFile(typeof(IAsyncEnumerable<int>).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Span<int>).Assembly.Location),
MetadataReference.CreateFromFile(typeof(IgnoreDataMemberAttribute).Assembly.Location),
MetadataReference.CreateFromFile(typeof(SqlMapper).Assembly.Location),
MetadataReference.CreateFromFile(typeof(DynamicAttribute).Assembly.Location),
MetadataReference.CreateFromFile(typeof(IValidatableObject).Assembly.Location),
},
options: new CSharpCompilationOptions(outputKind, allowUnsafe: true));

public static Compilation CreateCompilation(string source, string assemblyName, string fileName)
=> CSharpCompilation.Create(assemblyName,
syntaxTrees: new[] { CSharpSyntaxTree.ParseText(source, ParseOptionsLatestLangVer).WithFilePath(fileName) },
references: new[] {
MetadataReference.CreateFromFile(typeof(Binder).Assembly.Location),
Expand Down

0 comments on commit 63fd1a9

Please sign in to comment.