Last active
June 10, 2021 08:13
-
-
Save GeorgDangl/4a9982a3b520f056a9e890635b3695e0 to your computer and use it in GitHub Desktop.
Compile and test code in-memory with Roslyn, see https://blog.dangl.me/archive/integration-testing-in-memory-compiled-code-with-roslyn/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace InMemoreCompilation | |
{ | |
public static class CodeGenerator | |
{ | |
public static string GenerateCalculator() | |
{ | |
var calculator = @"namespace Calculator | |
{ | |
public class Calculator | |
{ | |
public int AddIntegers(int x, int y) | |
{ | |
return x + y; | |
} | |
} | |
}"; | |
return calculator; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Reflection; | |
using Microsoft.CodeAnalysis; | |
using Microsoft.CodeAnalysis.CSharp; | |
using Microsoft.CodeAnalysis.Emit; | |
using InMemoreCompilation; | |
using Xunit; | |
namespace InMemoryCompilation.Tests | |
{ | |
public class CodeGeneratorTests | |
{ | |
public CodeGeneratorTests() | |
{ | |
GenerateCode(); | |
CreateCompilation(); | |
CompileAndLoadAssembly(); | |
} | |
private string _generatedCode; | |
private CSharpCompilation _compilation; | |
private Assembly _generatedAssembly; | |
[Theory] | |
[InlineData(1,1,2)] | |
[InlineData(5,5,10)] | |
[InlineData(9,9,18)] | |
public void GenerateCompileAndTestCode(int x, int y, int expectedResult) | |
{ | |
var calculatedResult = CallCalculatorMethod(x, y); | |
Assert.Equal(expectedResult, calculatedResult); | |
} | |
private void GenerateCode() | |
{ | |
_generatedCode = CodeGenerator.GenerateCalculator(); | |
} | |
private int CallCalculatorMethod(int x, int y) | |
{ | |
var calculatorType = _generatedAssembly.GetType("Calculator.Calculator"); | |
var calculatorInstance = Activator.CreateInstance(calculatorType); | |
var calculateMethod = calculatorType.GetTypeInfo().GetDeclaredMethod("AddIntegers"); | |
var calculationResult = calculateMethod.Invoke(calculatorInstance, new object[] { x, y }); | |
Assert.IsType(typeof(int), calculationResult); | |
return (int)calculationResult; | |
} | |
private void CreateCompilation() | |
{ | |
var syntaxTree = CSharpSyntaxTree.ParseText(_generatedCode); | |
string assemblyName = Guid.NewGuid().ToString(); | |
var references = GetAssemblyReferences(); | |
var compilation = CSharpCompilation.Create( | |
assemblyName, | |
new[] { syntaxTree }, | |
references, | |
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); | |
_compilation = compilation; | |
} | |
private static IEnumerable<MetadataReference> GetAssemblyReferences() | |
{ | |
var references = new MetadataReference[] | |
{ | |
MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location) | |
// A bit hacky, if you need it | |
//MetadataReference.CreateFromFile(Path.Combine(typeof(object).GetTypeInfo().Assembly.Location, "..", "mscorlib.dll")), | |
}; | |
return references; | |
} | |
private void CompileAndLoadAssembly() | |
{ | |
using (var ms = new MemoryStream()) | |
{ | |
var result = _compilation.Emit(ms); | |
ThrowExceptionIfCompilationFailure(result); | |
ms.Seek(0, SeekOrigin.Begin); | |
var assembly = System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromStream(ms); | |
#if NET46 | |
// Different in full .Net framework | |
var assembly = Assembly.Load(ms.ToArray()); | |
#endif | |
_generatedAssembly = assembly; | |
} | |
} | |
private void ThrowExceptionIfCompilationFailure(EmitResult result) | |
{ | |
if (!result.Success) | |
{ | |
var compilationErrors = result.Diagnostics.Where(diagnostic => | |
diagnostic.IsWarningAsError || | |
diagnostic.Severity == DiagnosticSeverity.Error) | |
.ToList(); | |
if (compilationErrors.Any()) | |
{ | |
var firstError = compilationErrors.First(); | |
var errorNumber = firstError.Id; | |
var errorDescription = firstError.GetMessage(); | |
var firstErrorMessage = $"{errorNumber}: {errorDescription};"; | |
throw new Exception($"Compilation failed, first error is: {firstErrorMessage}"); | |
} | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"version": "1.0.0", | |
"dependencies": { | |
"InMemoreCompilation": "*", | |
"xunit": "2.2.0", | |
"dotnet-test-xunit": "2.2.0-preview2-build1029", | |
"Microsoft.CodeAnalysis.CSharp": "2.0.0" | |
}, | |
"testRunner": "xunit", | |
"frameworks": { | |
"netcoreapp1.1": { | |
"dependencies": { | |
"Microsoft.NETCore.App": { | |
"version": "1.1.0", | |
"type": "platform" | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment