blob: 0ce6602071763f394863e1cb9d55686407afab1f [file]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using NUnit.Framework;
using Avro.Specific;
namespace Avro.Test.AvroGen
{
class AvroGenToolResult
{
public int ExitCode { get; set; }
public string[] StdOut { get; set; }
public string[] StdErr { get; set; }
}
class AvroGenHelper
{
public static AvroGenToolResult RunAvroGenTool(params string[] args)
{
// Save stdout and stderr
TextWriter conOut = Console.Out;
TextWriter conErr = Console.Error;
try
{
AvroGenToolResult result = new AvroGenToolResult();
StringBuilder strBuilderOut = new StringBuilder();
StringBuilder strBuilderErr = new StringBuilder();
using (StringWriter writerOut = new StringWriter(strBuilderOut))
using (StringWriter writerErr = new StringWriter(strBuilderErr))
{
writerOut.NewLine = "\n";
writerErr.NewLine = "\n";
// Overwrite stdout and stderr to be able to capture console output
Console.SetOut(writerOut);
Console.SetError(writerErr);
result.ExitCode = AvroGenTool.Main(args.ToArray());
writerOut.Flush();
writerErr.Flush();
result.StdOut = strBuilderOut.Length == 0 ? Array.Empty<string>() : strBuilderOut.ToString().Split(writerOut.NewLine);
result.StdErr = strBuilderErr.Length == 0 ? Array.Empty<string>() : strBuilderErr.ToString().Split(writerErr.NewLine);
}
return result;
}
finally
{
// Restore console
Console.SetOut(conOut);
Console.SetError(conErr);
}
}
public static Assembly CompileCSharpFilesIntoLibrary(IEnumerable<string> sourceFiles, string assemblyName = null, bool loadAssembly = true)
{
// Create random assembly name if not specified
if (assemblyName == null)
assemblyName = Path.GetRandomFileName();
// Base path to assemblies .NET assemblies
var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location);
using (var compilerStream = new MemoryStream())
{
List<string> assemblies = new List<string>()
{
typeof(object).Assembly.Location,
typeof(Schema).Assembly.Location,
typeof(System.CodeDom.Compiler.GeneratedCodeAttribute).Assembly.Location,
Path.Combine(assemblyPath, "System.Runtime.dll"),
Path.Combine(assemblyPath, "netstandard.dll")
};
// Create compiler
CSharpCompilation compilation = CSharpCompilation
.Create(assemblyName)
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.AddReferences(assemblies.Select(path => MetadataReference.CreateFromFile(path)))
.AddSyntaxTrees(sourceFiles.Select(sourceFile =>
{
string sourceText = System.IO.File.ReadAllText(sourceFile);
return CSharpSyntaxTree.ParseText(sourceText);
}));
// Compile
EmitResult compilationResult = compilation.Emit(compilerStream);
#if DEBUG
if (!compilationResult.Success)
{
foreach (Diagnostic diagnostic in compilationResult.Diagnostics)
{
if (diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error)
{
TestContext.WriteLine($"{diagnostic.Id} - {diagnostic.GetMessage()} - {diagnostic.Location}");
}
}
}
#endif
Assert.That(compilationResult.Success, Is.True);
if (!loadAssembly)
{
return null;
}
// Load assembly from stream
compilerStream.Seek(0, SeekOrigin.Begin);
return Assembly.Load(compilerStream.ToArray());
}
}
public static string CreateEmptyTemporaryFolder(out string uniqueId, string path = null)
{
// Create unique id
uniqueId = Guid.NewGuid().ToString();
// Temporary folder name in working folder or the specified path
string tempFolder = Path.Combine(path ?? TestContext.CurrentContext.WorkDirectory, uniqueId);
// Create folder
Directory.CreateDirectory(tempFolder);
// Make sure it is empty
Assert.That(new DirectoryInfo(tempFolder), Is.Empty);
return tempFolder;
}
public static Assembly CompileCSharpFilesAndCheckTypes(
string outputDir,
string assemblyName,
IEnumerable<string> typeNamesToCheck = null,
IEnumerable<string> generatedFilesToCheck = null)
{
// Check if all generated files exist
if (generatedFilesToCheck != null)
{
foreach (string generatedFile in generatedFilesToCheck)
{
Assert.That(new FileInfo(Path.Combine(outputDir, generatedFile)), Does.Exist);
}
}
// Compile into netstandard library and load assembly
Assembly assembly = CompileCSharpFilesIntoLibrary(
new DirectoryInfo(outputDir)
.EnumerateFiles("*.cs", SearchOption.AllDirectories)
.Select(fi => fi.FullName),
assemblyName);
if (typeNamesToCheck != null)
{
// Check if the compiled code has the same number of types defined as the check list
// Note: Ignore types which are injected by the compiler (System.* and Microsoft.*), e.g. Microsoft.CodeAnalysis.EmbeddedAttribute
Assert.That(
typeNamesToCheck.Count(),
Is.EqualTo(
assembly
.DefinedTypes
.Where(t =>
{
return !t.Namespace.StartsWith("Microsoft.", StringComparison.OrdinalIgnoreCase) &&
!t.Namespace.StartsWith("System.", StringComparison.OrdinalIgnoreCase);
})
.Count()));
// Check if types available in compiled assembly
foreach (string typeName in typeNamesToCheck)
{
Type type = assembly.GetType(typeName);
Assert.That(type, Is.Not.Null);
// Protocols are abstract and cannot be instantiated
if (typeof(ISpecificProtocol).IsAssignableFrom(type))
{
Assert.That(type.IsAbstract, Is.True);
// If directly inherited from ISpecificProtocol, use reflection to read static private field
// holding the protocol. Callback objects are not directly inherited from ISpecificProtocol,
// so private fields in the base class cannot be accessed
if (type.BaseType.Equals(typeof(ISpecificProtocol)))
{
// Use reflection to read static field, holding the protocol
FieldInfo protocolField = type.GetField("protocol", BindingFlags.NonPublic | BindingFlags.Static);
Protocol protocol = protocolField.GetValue(null) as Protocol;
Assert.That(protocol, Is.Not.Null);
}
}
else
{
Assert.That(type.IsClass || type.IsEnum, Is.True);
// Instantiate object
object obj = Activator.CreateInstance(type);
Assert.That(obj, Is.Not.Null);
// If ISpecificRecord, call its member for sanity check
if (obj is ISpecificRecord record)
{
// Read record's schema object
Assert.That(record.Schema, Is.Not.Null);
// Force exception by reading/writing invalid field
Assert.Throws<AvroRuntimeException>(() => record.Get(-1));
Assert.Throws<AvroRuntimeException>(() => record.Put(-1, null));
}
}
}
}
return assembly;
}
public static Assembly TestSchema(
string schema,
IEnumerable<string> typeNamesToCheck = null,
IEnumerable<KeyValuePair<string, string>> namespaceMapping = null,
IEnumerable<string> generatedFilesToCheck = null,
bool skipDirectories = false)
{
// Create temp folder
string outputDir = CreateEmptyTemporaryFolder(out string uniqueId);
try
{
// Save schema
string schemaFileName = Path.Combine(outputDir, $"{uniqueId}.avsc");
System.IO.File.WriteAllText(schemaFileName, schema);
// Generate from schema file
Assert.That(AvroGenTool.GenSchema(schemaFileName, outputDir, namespaceMapping ?? new Dictionary<string, string>(), skipDirectories), Is.EqualTo(0));
return CompileCSharpFilesAndCheckTypes(outputDir, uniqueId, typeNamesToCheck, generatedFilesToCheck);
}
finally
{
Directory.Delete(outputDir, true);
}
}
public static Assembly TestProtocol(
string protocol,
IEnumerable<string> typeNamesToCheck = null,
IEnumerable<KeyValuePair<string, string>> namespaceMapping = null,
IEnumerable<string> generatedFilesToCheck = null)
{
// Create temp folder
string outputDir = CreateEmptyTemporaryFolder(out string uniqueId);
try
{
// Save protocol
string schemaFileName = Path.Combine(outputDir, $"{uniqueId}.avpr");
System.IO.File.WriteAllText(schemaFileName, protocol);
// Generate from protocol file
Assert.That(AvroGenTool.GenProtocol(schemaFileName, outputDir, namespaceMapping ?? new Dictionary<string, string>()), Is.EqualTo(0));
return CompileCSharpFilesAndCheckTypes(outputDir, uniqueId, typeNamesToCheck, generatedFilesToCheck);
}
finally
{
Directory.Delete(outputDir, true);
}
}
}
}