diff --git a/src/Testura.Code.Tests/Compilation/CompilerTests.cs b/src/Testura.Code.Tests/Compilation/CompilerTests.cs index 7d89092..236b4a9 100644 --- a/src/Testura.Code.Tests/Compilation/CompilerTests.cs +++ b/src/Testura.Code.Tests/Compilation/CompilerTests.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Reflection; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using NUnit.Framework; @@ -21,16 +22,17 @@ public void SetUp() [Test] public async Task CompileSourceAsync_WhenCompilingSource_ShouldGetADll() { - var result = await _compiler.CompileSourceAsync(Path.Combine(TestContext.CurrentContext.TestDirectory, "test.dll"), new ClassBuilder("TestClass", "Test").Build().NormalizeWhitespace().ToString()); - Assert.IsNotNull(result.PathToDll); + var outputPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "test01.dll"); + var result = await _compiler.CompileSourceAsync(outputPath, new ClassBuilder("TestClass", "Test").Build().NormalizeWhitespace().ToString()); Assert.AreEqual(0, result.OutputRows.Count); Assert.IsTrue(result.Success); + Assembly.LoadFrom(outputPath); } [Test] public async Task CompileSourceAsync_WhenCompilingSourceWithError_ShouldGetListContainingErrors() { - var result = await _compiler.CompileSourceAsync(Path.Combine(TestContext.CurrentContext.TestDirectory, "test.dll"), "gfdgdfgfdg"); + var result = await _compiler.CompileSourceAsync(Path.Combine(TestContext.CurrentContext.TestDirectory, "test02.dll"), "gfdgdfgfdg"); Assert.AreEqual(1, result.OutputRows.Count); Assert.IsFalse(result.Success); } @@ -50,5 +52,18 @@ public async Task CompileSourceInMemoryAsync_WhenCompilingSourceWithError_Should Assert.AreEqual(1, result.OutputRows.Count); Assert.IsFalse(result.Success); } + + [Test] + public async Task CompileSourceToStreamAsync_WhenCompilingSource_ShouldHaveAProperlyLoadingAssembly() + { + using (var ms = new MemoryStream()) + { + var result = await _compiler.CompileSourceToStreamAsync("test", ms, new ClassBuilder("TestClass", "Test").Build().NormalizeWhitespace().ToString()); + var assemblyBytes = ms.ToArray(); + Assert.IsTrue(result.Success); + Assert.NotZero(assemblyBytes.Length); + Assembly.Load(assemblyBytes); + } + } } } diff --git a/src/Testura.Code/Compilations/AdvancedCompiler.cs b/src/Testura.Code/Compilations/AdvancedCompiler.cs new file mode 100644 index 0000000..e47db88 --- /dev/null +++ b/src/Testura.Code/Compilations/AdvancedCompiler.cs @@ -0,0 +1,73 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Testura.Code.Compilations +{ + public static class AdvancedCompiler + { + public static CompileResult CompileSourceStrings(Stream outputStream, CompilerSettings settings = default, params string[] sources) + { + if (settings == null) + { + settings = new CompilerSettings(); + } + + // add references + var metaDataRef = new List(); + foreach (var s in settings.ReferenceAssemblyStreams) + { + metaDataRef.Add(MetadataReference.CreateFromStream(s)); + } + + foreach (var s in settings.ReferenceAssemblyFilePaths) + { + metaDataRef.Add(MetadataReference.CreateFromFile(s)); + } + + var parseOptions = new CSharpParseOptions(settings.LanguageVersion); + var parsedSyntaxTrees = new SyntaxTree[sources.Length]; + for (int i = 0; i < sources.Length; i++) + { + var stringText = SourceText.From(sources[i], Encoding.UTF8); + parsedSyntaxTrees[i] = SyntaxFactory.ParseSyntaxTree(stringText, parseOptions); + } + + var defaultCompilationOptions = new CSharpCompilationOptions(settings.OutputKind) + .WithPlatform(settings.Platform) + .WithOverflowChecks(settings.EnableOverflowChecks) + .WithOptimizationLevel(settings.OptimizationLevel) + .WithUsings(settings.Usings); + + var compilation = CSharpCompilation.Create( + settings.AssemblyName, + parsedSyntaxTrees, + metaDataRef, + defaultCompilationOptions); + + var result = compilation.Emit(outputStream); + var outputRows = ConvertDiagnosticsToOutputRows(result.Diagnostics); + return new CompileResult(result.Success, outputRows); + } + + private static IList ConvertDiagnosticsToOutputRows(IEnumerable diagnostics) + { + var outputRows = new List(); + foreach (var diagnostic in diagnostics) + { + if (diagnostic.Severity < DiagnosticSeverity.Error) + { + continue; + } + + outputRows.Add(new OutputRow { Description = diagnostic.GetMessage(), Severity = diagnostic.Severity.ToString(), ClassName = diagnostic.Id }); + } + + return outputRows; + } + } +} diff --git a/src/Testura.Code/Compilations/CompileResult.cs b/src/Testura.Code/Compilations/CompileResult.cs index 5f9528a..1f181eb 100644 --- a/src/Testura.Code/Compilations/CompileResult.cs +++ b/src/Testura.Code/Compilations/CompileResult.cs @@ -15,18 +15,12 @@ public class CompileResult /// Path to the dll. /// If the compilation was successful or not. /// Output from the compilation. - public CompileResult(string pathToDll, bool success, IList outputRows) + public CompileResult(bool success, IList outputRows) { - PathToDll = pathToDll; Success = success; OutputRows = outputRows; } - /// - /// Gets or sets path to the generated dlls. - /// - public string PathToDll { get; set; } - /// /// Gets or sets a value indicating whether the test are successful. /// diff --git a/src/Testura.Code/Compilations/Compiler.cs b/src/Testura.Code/Compilations/Compiler.cs index 4fe39c2..1ca6403 100644 --- a/src/Testura.Code/Compilations/Compiler.cs +++ b/src/Testura.Code/Compilations/Compiler.cs @@ -79,7 +79,10 @@ public async Task CompileFilesAsync(string outputPath, params str source[n] = File.ReadAllText(pathsToCsFiles[n]); } - return await CompileSourceAsync(outputPath, source); + using (var fs = File.OpenWrite(outputPath)) + { + return await CompileSourceToStreamAsync("compilationResult", fs, source); + } } /// @@ -89,6 +92,22 @@ public async Task CompileFilesAsync(string outputPath, params str /// Source string to compile. /// The result of the compilation. public async Task CompileSourceAsync(string outputPath, params string[] source) + { + var stream = outputPath != null ? (Stream)File.OpenWrite(outputPath) : new MemoryStream(); + using (stream) + { + return await CompileSourceToStreamAsync("temporary", stream, source); + } + } + + /// + /// Compile code from a string source into a dll. + /// + /// Name for the assembly. + /// Stream to write the assembly to. + /// Source string to compile. + /// The result of the compilation. + public async Task CompileSourceToStreamAsync(string assemblyName, Stream stream, params string[] source) { if (source.Length == 0) { @@ -111,26 +130,16 @@ public async Task CompileSourceAsync(string outputPath, params st .WithUsings(_defaultNamespaces); var compilation = CSharpCompilation.Create( - string.IsNullOrEmpty(outputPath) ? "temporary" : Path.GetFileName(outputPath), + assemblyName, parsedSyntaxTrees, ConvertReferenceToMetaDataReferfence(), defaultCompilationOptions); EmitResult result; - if (string.IsNullOrEmpty(outputPath)) - { - using (var memoryStream = new MemoryStream()) - { - result = compilation.Emit(memoryStream); - } - } - else - { - result = compilation.Emit(outputPath); - } + result = compilation.Emit(stream); var outputRows = ConvertDiagnosticsToOutputRows(result.Diagnostics); - return new CompileResult(outputPath, result.Success, outputRows); + return new CompileResult(result.Success, outputRows); }); } diff --git a/src/Testura.Code/Compilations/CompilerSettings.cs b/src/Testura.Code/Compilations/CompilerSettings.cs new file mode 100644 index 0000000..3721669 --- /dev/null +++ b/src/Testura.Code/Compilations/CompilerSettings.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Testura.Code.Compilations +{ + public class CompilerSettings + { + public CompilerSettings() + { + Platform = Platform.AnyCpu; + OutputKind = OutputKind.DynamicallyLinkedLibrary; + EnableOverflowChecks = false; + OptimizationLevel = OptimizationLevel.Release; + AssemblyName = "Temporary"; + LanguageVersion = LanguageVersion.Latest; + var loadadAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + var defaultAssemblyNames = new[] { "mscorlib", "System", "System.Core" }; + + // make sure above default assemblies are available to load from file, if they aren't (e.g. we run on WASM) don't load them + var defaultAssemblies = loadadAssemblies.Where(x => + defaultAssemblyNames.Contains(x.GetName().Name) && + !string.IsNullOrEmpty(x.Location) && + File.Exists(x.Location)).Select(x => x.Location); + ReferenceAssemblyFilePaths = new List(defaultAssemblies); + + ReferenceAssemblyStreams = new List(); + Usings = new List + { + "System", + "System.IO", + "System.Net", + "System.Linq", + "System.Text", + "System.Text.RegularExpressions", + "System.Collections.Generic", + }; + } + + public Platform Platform { get; set; } + + public OutputKind OutputKind { get; set; } + + public bool EnableOverflowChecks { get; set; } + + public OptimizationLevel OptimizationLevel { get; set; } + + public string AssemblyName { get; set; } + + public LanguageVersion LanguageVersion { get; set; } + + public List ReferenceAssemblyFilePaths { get; set; } + + public List ReferenceAssemblyStreams { get; set; } + + public List Usings { get; set; } + } +}