Skip to content

Commit cbb50af

Browse files
authored
[generator-Tests] Use Roslyn for .NET Core Support (dotnet#638)
How do we test the C# output of `generator`? *A* way is to check the output against "known good" files, which we do. *Another* way is to use a compiler to ensure that the output compiles, which we *also* do. Unfortunately, the "just compile it!" approach has a major flaw, as the easiest accessible C# compiler is `Microsoft.CSharp.CSharpCodeProvider`, for use with System.CodeDom, but *on Windows* `CSharpCodeProvider` only supports up to C# 5. `generator`, meanwhile, may currently produce C# 8 output. This conundrum was addressed in commit 968b474 by using the [Microsoft.CodeDom.Providers.DotNetCompilerPlatform][0] NuGet package when running on Windows, as that supported C#6+. Unfortunately, `DotNetCompilerPlatform` does *not* run under .NET Core, because it uses the MSBuild `CodeTaskFactory` which is not available on .NET Core. ([This issue has been fixed][1]; the fix is unreleased.) Now that we support multitargeting net471 and netcoreapp3.1 (95f698b), we would like to be able to build *and run* our unit tests under .NET Core as well as Mono (macOS) or .NET Framework (Windows). Migrate away from the `DotNetCompilerPlatform` NuGet package and instead use the [Microsoft.CodeAnalysis.CSharp][2] NuGet package, which contains an up-to-date C#8 Roslyn compiler. This new package supports .NET Core; we just need to update `Compiler.Compile()` to work in terms of Roslyn SyntaxTrees instead of CodeDom objects. This allows us to run the `generator` unit tests under .NET Core: dotnet test bin\TestDebug\generator-tests.dll [0]: https://www.nuget.org/packages/Microsoft.CodeDom.Providers.DotNetCompilerPlatform/ [1]: aspnet/RoslynCodeDomProvider#51 [2]: https://www.nuget.org/packages/Microsoft.CodeAnalysis.CSharp/
1 parent 56955d9 commit cbb50af

File tree

2 files changed

+63
-54
lines changed

2 files changed

+63
-54
lines changed

tests/generator-Tests/Integration-Tests/Compiler.cs

Lines changed: 62 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,29 @@
11
using System;
2-
using System.Reflection;
3-
using System.CodeDom.Compiler;
2+
using System.Collections.Generic;
43
using System.IO;
54
using System.Linq;
6-
using System.Collections.Generic;
5+
using System.Reflection;
6+
using System.Text;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CSharp;
79
using NUnit.Framework;
810

911
namespace generatortests
1012
{
1113
public static class Compiler
1214
{
13-
const string RoslynEnvironmentVariable = "ROSLYN_COMPILER_LOCATION";
14-
private static string unitTestFrameworkAssemblyPath = typeof(Assert).Assembly.Location;
15-
private static string supportFilePath = typeof(Compiler).Assembly.Location;
16-
17-
static CodeDomProvider GetCodeDomProvider ()
18-
{
19-
if (Environment.OSVersion.Platform == PlatformID.Win32NT) {
20-
//NOTE: there is an issue where Roslyn's csc.exe isn't copied to output for non-ASP.NET projects
21-
// Comments on this here: https://stackoverflow.com/a/40311406/132442
22-
// They added an environment variable as a workaround: https://github.com/aspnet/RoslynCodeDomProvider/pull/12
23-
if (string.IsNullOrEmpty (Environment.GetEnvironmentVariable (RoslynEnvironmentVariable, EnvironmentVariableTarget.Process))) {
24-
string roslynPath = Path.GetFullPath (Path.Combine (unitTestFrameworkAssemblyPath, "..", "..", "..", "packages", "microsoft.codedom.providers.dotnetcompilerplatform", "2.0.1", "tools", "RoslynLatest"));
25-
Environment.SetEnvironmentVariable (RoslynEnvironmentVariable, roslynPath, EnvironmentVariableTarget.Process);
26-
}
27-
28-
return new Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider ();
29-
} else {
30-
return new Microsoft.CSharp.CSharpCodeProvider ();
31-
}
32-
}
15+
private static string supportFilePath = typeof (Compiler).Assembly.Location;
16+
private static string unitTestFrameworkAssemblyPath = typeof (Assert).Assembly.Location;
3317

3418
public static Assembly Compile (Xamarin.Android.Binder.CodeGeneratorOptions options,
3519
string assemblyFileName, IEnumerable<string> AdditionalSourceDirectories,
3620
out bool hasErrors, out string output, bool allowWarnings)
3721
{
22+
// Gather all the files we need to compile
3823
var generatedCodePath = options.ManagedCallableWrapperSourceOutputDirectory;
3924
var sourceFiles = Directory.EnumerateFiles (generatedCodePath, "*.cs",
4025
SearchOption.AllDirectories).ToList ();
41-
sourceFiles = sourceFiles.Select (x => Path.GetFullPath(x)).ToList ();
26+
sourceFiles = sourceFiles.Select (x => Path.GetFullPath (x)).ToList ();
4227

4328
var supportFiles = Directory.EnumerateFiles (Path.Combine (Path.GetDirectoryName (supportFilePath), "SupportFiles"),
4429
"*.cs", SearchOption.AllDirectories);
@@ -49,36 +34,51 @@ public static Assembly Compile (Xamarin.Android.Binder.CodeGeneratorOptions opti
4934
sourceFiles.AddRange (additonal);
5035
}
5136

52-
CompilerParameters parameters = new CompilerParameters ();
53-
parameters.GenerateExecutable = false;
54-
parameters.GenerateInMemory = true;
55-
parameters.CompilerOptions = "/unsafe";
56-
parameters.OutputAssembly = assemblyFileName;
57-
parameters.ReferencedAssemblies.Add (unitTestFrameworkAssemblyPath);
58-
parameters.ReferencedAssemblies.Add (typeof (Enumerable).Assembly.Location);
59-
60-
var binDir = Path.GetDirectoryName (typeof (BaseGeneratorTest).Assembly.Location);
61-
var facDir = GetFacadesPath ();
62-
parameters.ReferencedAssemblies.Add (Path.Combine (binDir, "Java.Interop.dll"));
63-
parameters.ReferencedAssemblies.Add (Path.Combine (facDir, "netstandard.dll"));
64-
#if DEBUG
65-
parameters.IncludeDebugInformation = true;
66-
#else
67-
parameters.IncludeDebugInformation = false;
68-
#endif
69-
70-
using (var codeProvider = GetCodeDomProvider ()) {
71-
CompilerResults results = codeProvider.CompileAssemblyFromFile (parameters, sourceFiles.ToArray ());
72-
73-
hasErrors = false;
74-
75-
foreach (CompilerError message in results.Errors) {
76-
hasErrors |= !message.IsWarning || !allowWarnings;
37+
// Parse the source files
38+
var syntax_trees = sourceFiles.Distinct ().Select (s => CSharpSyntaxTree.ParseText (File.ReadAllText (s))).ToArray ();
39+
40+
// Set up the assemblies we need to reference
41+
var binDir = Path.GetDirectoryName (typeof (BaseGeneratorTest).Assembly.Location);
42+
var facDir = GetFacadesPath ();
43+
44+
var references = new [] {
45+
MetadataReference.CreateFromFile (unitTestFrameworkAssemblyPath),
46+
MetadataReference.CreateFromFile (typeof(object).Assembly.Location),
47+
MetadataReference.CreateFromFile (typeof(Enumerable).Assembly.Location),
48+
MetadataReference.CreateFromFile (Path.Combine (binDir, "Java.Interop.dll")),
49+
MetadataReference.CreateFromFile (Path.Combine (facDir, "netstandard.dll"))
50+
};
51+
52+
// Compile!
53+
var compilation = CSharpCompilation.Create (
54+
Path.GetFileName (assemblyFileName),
55+
syntaxTrees: syntax_trees,
56+
references: references,
57+
options: new CSharpCompilationOptions (OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true));
58+
59+
// Save assembly to a memory stream and load it with reflection
60+
using (var ms = new MemoryStream ()) {
61+
var result = compilation.Emit (ms);
62+
var success = result.Success && (allowWarnings || !result.Diagnostics.Any (d => d.Severity == DiagnosticSeverity.Warning));
63+
64+
if (!success) {
65+
var failures = result.Diagnostics.Where (diagnostic =>
66+
diagnostic.Severity == DiagnosticSeverity.Warning ||
67+
diagnostic.Severity == DiagnosticSeverity.Error);
68+
69+
hasErrors = true;
70+
output = OutputDiagnostics (failures);
71+
} else {
72+
ms.Seek (0, SeekOrigin.Begin);
73+
74+
hasErrors = false;
75+
output = null;
76+
77+
return Assembly.Load (ms.ToArray ());
7778
}
78-
output = string.Join (Environment.NewLine, results.Output.Cast<string> ());
79-
80-
return results.CompiledAssembly;
8179
}
80+
81+
return null;
8282
}
8383

8484
static string GetFacadesPath ()
@@ -94,6 +94,15 @@ static string GetFacadesPath ()
9494

9595
return dir;
9696
}
97+
98+
static string OutputDiagnostics (IEnumerable<Diagnostic> diagnostics)
99+
{
100+
var sb = new StringBuilder ();
101+
102+
foreach (var d in diagnostics)
103+
sb.AppendLine ($"{d.Id}: {d.GetMessage ()} ({d.Location})");
104+
105+
return sb.ToString ();
106+
}
97107
}
98108
}
99-

tests/generator-Tests/generator-Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
<Import Project="..\..\build-tools\scripts\cecil.projitems" />
1414

1515
<ItemGroup>
16+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.5.0" />
1617
<PackageReference Include="nunit" Version="3.11.0" />
1718
<PackageReference Include="NUnit.ConsoleRunner" Version="3.9.0" />
1819
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
1920
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
20-
<PackageReference Include="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" Version="2.0.1" />
2121
</ItemGroup>
2222

2323
<ItemGroup>

0 commit comments

Comments
 (0)