You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
916 lines
38 KiB
916 lines
38 KiB
using System; |
|
using System.Collections.Generic; |
|
using System.IO; |
|
using System.Linq; |
|
using System.Reflection; |
|
using System.Runtime.InteropServices; |
|
using Burst.Compiler.IL.Tests.Helpers; |
|
using NUnit.Framework; |
|
using NUnit.Framework.Interfaces; |
|
using NUnit.Framework.Internal; |
|
using NUnit.Framework.Internal.Builders; |
|
using NUnit.Framework.Internal.Commands; |
|
using Unity.Burst; |
|
using Unity.Collections; |
|
using Unity.Mathematics; |
|
using UnityBenchShared; |
|
#if !BURST_TESTS_ONLY |
|
using ExecutionContext = NUnit.Framework.Internal.ITestExecutionContext; |
|
#else |
|
using ExecutionContext = NUnit.Framework.Internal.TestExecutionContext; |
|
#endif |
|
|
|
namespace Burst.Compiler.IL.Tests |
|
{ |
|
/// <summary> |
|
/// When used as a type in TestCompiler arguments signifies that the corresponding parameter is a pointer output. |
|
/// </summary> |
|
internal struct ReturnBox { } |
|
|
|
|
|
/// <summary> |
|
/// Interface used for initialize function pointers. |
|
/// </summary> |
|
internal interface IFunctionPointerProvider |
|
{ |
|
object FromIntPtr(IntPtr ptr); |
|
} |
|
|
|
/// <summary> |
|
/// Used to implement custom testing behaviour |
|
/// </summary> |
|
internal interface TestCompilerBaseExtensions |
|
{ |
|
(bool shouldSkip, string skipReason) SkipTest(MethodInfo method); |
|
Type FetchAlternateDelegate(out bool isInRegistry, out Func<object, object[], object> caller); |
|
object[] ProcessNativeArgsForDelegateCaller(object[] nativeArgs, MethodInfo methodInfo); |
|
} |
|
|
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] |
|
internal abstract class TestCompilerAttributeBase : TestCaseAttribute, ITestBuilder, IWrapTestMethod |
|
{ |
|
private readonly NUnitTestCaseBuilder _builder = new NUnitTestCaseBuilder(); |
|
|
|
public const string GoldFolder = "gold/x86"; |
|
public const string GoldFolderArm = "gold/arm"; |
|
public const string GeneratedFolder = "generated/x86"; |
|
public const string GeneratedFolderArm = "generated/arm"; |
|
|
|
public TestCompilerAttributeBase(params object[] arguments) : base(arguments) |
|
{ |
|
} |
|
|
|
/// <summary> |
|
/// Whether the test should only be compiled and not run. Useful for having tests that would produce infinitely running code which could ICE the compiler. |
|
/// </summary> |
|
public bool CompileOnly { get; set; } |
|
|
|
/// <summary> |
|
/// The type of exception the test expects to be thrown. |
|
/// </summary> |
|
public Type ExpectedException { get; set; } |
|
|
|
/// <summary> |
|
/// Whether the test is expected to throw a compiler exception or not. |
|
/// </summary> |
|
public bool ExpectCompilerException { get; set; } |
|
|
|
public bool DisableGold { get; set; } |
|
|
|
public DiagnosticId ExpectedDiagnosticId |
|
{ |
|
get => throw new InvalidOperationException(); |
|
set => ExpectedDiagnosticIds = new DiagnosticId[] { value }; |
|
} |
|
|
|
public DiagnosticId[] ExpectedDiagnosticIds { get; set; } = new DiagnosticId[0]; |
|
|
|
public bool FastMath { get; set; } |
|
|
|
/// <summary> |
|
/// Use this property when the JIT calculation is wrong (e.g when using float) |
|
/// </summary> |
|
public object OverrideResultOnMono { get; set; } |
|
|
|
/// <summary> |
|
/// Use this property to set the result of the managed method and skip running it completely (for example when there is no reference managed implementation) |
|
/// </summary> |
|
public object OverrideManagedResult { get; set; } |
|
|
|
/// <summary> |
|
/// Use this when a pointer is used in a sizeof computation, since on a 32bit target the result will differ versus our 64bit managed results. |
|
/// </summary> |
|
public object OverrideOn32BitNative { get; set; } |
|
|
|
/// <summary> |
|
/// Use this and specify a TargetPlatform (Host) to have the test ignored when running on that host. Mostly used by WASM at present. |
|
/// </summary> |
|
public object IgnoreOnPlatform { get; set; } |
|
|
|
public bool? IsDeterministic { get; set; } |
|
|
|
protected virtual bool SupportException => true; |
|
|
|
IEnumerable<TestMethod> ITestBuilder.BuildFrom(IMethodInfo method, Test suite) |
|
{ |
|
// If the system doesn't support exceptions (Unity editor for delegates) we should not test with exceptions |
|
bool skipTest = (ExpectCompilerException || ExpectedException != null) && !SupportException; |
|
var expectResult = !method.ReturnType.IsType(typeof(void)); |
|
var arguments = new List<object>(this.Arguments); |
|
|
|
// Expand arguments with IntRangeAttributes if we have them |
|
foreach (var param in method.GetParameters()) |
|
{ |
|
var attrs = param.GetCustomAttributes<IntRangeAttribute>(false); |
|
if (attrs == null || attrs.Length != 1) |
|
continue; |
|
|
|
arguments.Add(attrs[0]); |
|
} |
|
|
|
IEnumerable<object[]> permutations = CreatePermutation(0, arguments.ToArray(), method.GetParameters()); |
|
|
|
// TODO: Workaround for a scalability bug with R# or Rider |
|
// Run only one testcase if not running from the commandline |
|
if (!IsCommandLine()) |
|
{ |
|
permutations = permutations.Take(1); |
|
} |
|
|
|
foreach (var newArguments in permutations) |
|
{ |
|
var caseParameters = new TestCaseParameters(newArguments); |
|
if (expectResult) |
|
{ |
|
caseParameters.ExpectedResult = true; |
|
if (OverrideResultOnMono != null && IsMono()) |
|
{ |
|
caseParameters.Properties.Set(nameof(OverrideResultOnMono), OverrideResultOnMono); |
|
} |
|
if (OverrideManagedResult != null) |
|
{ |
|
caseParameters.Properties.Set(nameof(OverrideManagedResult), OverrideManagedResult); |
|
} |
|
if (OverrideOn32BitNative != null) |
|
{ |
|
caseParameters.Properties.Set(nameof(OverrideOn32BitNative), OverrideOn32BitNative); |
|
} |
|
} |
|
|
|
// Transfer FastMath parameter to the compiler |
|
caseParameters.Properties.Set(nameof(FastMath), FastMath); |
|
|
|
var test = _builder.BuildTestMethod(method, suite, caseParameters); |
|
if (skipTest) |
|
{ |
|
test.RunState = RunState.Skipped; |
|
test.Properties.Add(PropertyNames.SkipReason, "Exceptions are not supported"); |
|
} |
|
|
|
yield return test; |
|
} |
|
} |
|
|
|
private static IEnumerable<object[]> CreatePermutation(int index, object[] args, IParameterInfo[] parameters) |
|
{ |
|
if (index >= args.Length) |
|
{ |
|
yield return args; |
|
yield break; |
|
} |
|
var copyArgs = (object[])args.Clone(); |
|
bool hasRange = false; |
|
for (; index < args.Length; index++) |
|
{ |
|
var arg = copyArgs[index]; |
|
if (arg is DataRange) |
|
{ |
|
var range = (DataRange)arg; |
|
// TEMP: Disable NaN test for now |
|
//range = range & ~(DataRange.NaN); |
|
foreach (var value in range.ExpandRange(parameters[index].ParameterType, index)) |
|
{ |
|
copyArgs[index] = value; |
|
foreach (var subPermutation in CreatePermutation(index + 1, copyArgs, parameters)) |
|
{ |
|
hasRange = true; |
|
yield return subPermutation; |
|
} |
|
} |
|
} |
|
else if (arg is IntRangeAttribute) |
|
{ |
|
var ir = (IntRangeAttribute)arg; |
|
if (ir != null) |
|
{ |
|
for (int x = ir.Lo; x <= ir.Hi; ++x) |
|
{ |
|
copyArgs[index] = x; |
|
|
|
foreach (var subPermutation in CreatePermutation(index + 1, copyArgs, parameters)) |
|
{ |
|
hasRange = true; |
|
yield return subPermutation; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
if (!hasRange) |
|
{ |
|
yield return copyArgs; |
|
} |
|
} |
|
|
|
TestCommand ICommandWrapper.Wrap(TestCommand command) |
|
{ |
|
var testMethod = (TestMethod)command.Test; |
|
return GetTestCommand(this, testMethod, testMethod); |
|
} |
|
|
|
protected abstract bool IsCommandLine(); |
|
|
|
protected abstract bool IsMono(); |
|
|
|
protected abstract TestCompilerCommandBase GetTestCommand(TestCompilerAttributeBase attribute, Test test, TestMethod originalMethod); |
|
} |
|
|
|
internal abstract class TestCompilerCommandBase : TestCommand |
|
{ |
|
protected readonly TestMethod _originalMethod; |
|
private readonly bool _compileOnly; |
|
private readonly Type _expectedException; |
|
protected readonly bool _expectCompilerException; |
|
private readonly DiagnosticId[] _expectedDiagnosticIds; |
|
|
|
protected virtual bool TestInterpreter => false; |
|
|
|
protected TestCompilerCommandBase(TestCompilerAttributeBase attribute, Test test, TestMethod originalMethod) : base(test) |
|
{ |
|
_originalMethod = originalMethod; |
|
Attribute = attribute; |
|
_compileOnly = Attribute.CompileOnly; |
|
_expectedException = Attribute.ExpectedException; |
|
_expectCompilerException = Attribute.ExpectCompilerException; |
|
_expectedDiagnosticIds = Attribute.ExpectedDiagnosticIds; |
|
} |
|
|
|
public TestCompilerAttributeBase Attribute { get; } |
|
|
|
public override TestResult Execute(ExecutionContext context) |
|
{ |
|
TestResult lastResult = null; |
|
for (int i = 0; i < GetRunCount(); i++) |
|
{ |
|
lastResult = ExecuteMethod(context); |
|
} |
|
|
|
return lastResult; |
|
} |
|
|
|
protected virtual Type CreateNativeDelegateType(Type returnType, Type[] arguments, out bool isInRegistry, out Func<object, object[], object> caller) |
|
{ |
|
if (GetExtension() != null) |
|
{ |
|
Type type = GetExtension().FetchAlternateDelegate(out isInRegistry, out caller); |
|
if (type != null) |
|
{ |
|
return type; |
|
} |
|
} |
|
|
|
isInRegistry = false; |
|
StaticDelegateCallback staticDelegate; |
|
if (StaticDelegateRegistry.TryFind(returnType, arguments, out staticDelegate)) |
|
{ |
|
isInRegistry = true; |
|
caller = staticDelegate.Caller; |
|
return staticDelegate.DelegateType; |
|
} |
|
else |
|
{ |
|
#if BURST_TESTS_ONLY |
|
// Else we try to do it with a dynamic call |
|
var type = DelegateHelper.NewDelegateType(returnType, arguments); |
|
caller = StaticDynamicDelegateCaller; |
|
return type; |
|
#else |
|
throw new Exception("Couldn't find delegate in static registry and not able to use a dynamic call."); |
|
#endif |
|
} |
|
} |
|
|
|
private static Func<object, object[], object> StaticDynamicDelegateCaller = new Func<object, object[], object>((del, arguments) => ((Delegate)del).DynamicInvoke(arguments)); |
|
|
|
private static readonly int MaxReturnBoxSize = 512; |
|
|
|
protected bool RunManagedBeforeNative { get; set; } |
|
|
|
protected static readonly Dictionary<string, string> BailedTests = new Dictionary<string, string>(); |
|
|
|
private unsafe void ZeroMemory(byte* ptr, int size) |
|
{ |
|
for (int i = 0; i < size; i++) |
|
{ |
|
*(ptr + i) = 0; |
|
} |
|
} |
|
|
|
private unsafe TestResult ExecuteMethod(ExecutionContext context) |
|
{ |
|
byte* returnBox = stackalloc byte[MaxReturnBoxSize]; |
|
Setup(); |
|
var methodInfo = _originalMethod.Method.MethodInfo; |
|
|
|
var runTest = TestOnCurrentHostEnvironment(methodInfo); |
|
|
|
if (runTest) |
|
{ |
|
var arguments = GetArgumentsArray(_originalMethod); |
|
|
|
// We can't skip tests during BuildFrom that rely on specific options (e.g. current platform) |
|
// So we handle the remaining cases here via extensions |
|
if (GetExtension() != null) |
|
{ |
|
var skip = GetExtension().SkipTest(methodInfo); |
|
if (skip.shouldSkip) |
|
{ |
|
// For now, mark the tests as passed rather than skipped, to avoid the log spam |
|
//On wasm this log spam accounts for 33minutes of test execution time!! |
|
//context.CurrentResult.SetResult(ResultState.Skipped, skip.skipReason); |
|
context.CurrentResult.SetResult(ResultState.Success); |
|
return context.CurrentResult; |
|
} |
|
} |
|
|
|
// If we expect a compiler exception, then we need to allow argument transformation to fail, |
|
// because this may be the actual thing that we're trying to test. |
|
object[] nativeArgs = null; |
|
Type[] nativeArgTypes = null; |
|
Type returnBoxType = null; |
|
var transformedArguments = false; |
|
if (_expectCompilerException) |
|
{ |
|
var expectedExceptionResult = TryExpectedException( |
|
context, |
|
() => TransformArguments(_originalMethod.Method.MethodInfo, arguments, out nativeArgs, out nativeArgTypes, returnBox, out returnBoxType), |
|
"Transforming arguments", |
|
type => true, |
|
"Any exception", |
|
false, |
|
false); |
|
if (expectedExceptionResult != TryExpectedExceptionResult.DidNotThrowException) |
|
{ |
|
return context.CurrentResult; |
|
} |
|
transformedArguments = true; |
|
} |
|
|
|
if (!transformedArguments) |
|
{ |
|
TransformArguments(_originalMethod.Method.MethodInfo, arguments, out nativeArgs, out nativeArgTypes, returnBox, out returnBoxType); |
|
} |
|
|
|
bool isInRegistry = false; |
|
Func<object, object[], object> nativeDelegateCaller; |
|
var delegateType = CreateNativeDelegateType(_originalMethod.Method.MethodInfo.ReturnType, nativeArgTypes, out isInRegistry, out nativeDelegateCaller); |
|
if (!isInRegistry) |
|
{ |
|
TestContext.Out.WriteLine($"Warning, the delegate for the method `{_originalMethod.Method}` has not been generated"); |
|
} |
|
|
|
Delegate compiledFunction; |
|
Delegate interpretDelegate; |
|
try |
|
{ |
|
compiledFunction = CompileDelegate(context, methodInfo, delegateType, returnBox, out _, out interpretDelegate); |
|
} |
|
catch (Exception ex) when (_expectedException != null && ex.GetType() == _expectedException) |
|
{ |
|
context.CurrentResult.SetResult(ResultState.Success); |
|
return context.CurrentResult; |
|
} |
|
|
|
Assert.IsTrue(returnBoxType == null || Marshal.SizeOf(returnBoxType) <= MaxReturnBoxSize); |
|
|
|
if (TestInterpreter) |
|
{ |
|
compiledFunction = interpretDelegate; |
|
} |
|
|
|
if (compiledFunction == null) |
|
{ |
|
return context.CurrentResult; |
|
} |
|
|
|
if (_compileOnly) // If the test only wants to compile the code, bail now. |
|
{ |
|
context.CurrentResult.SetResult(ResultState.Success); |
|
return context.CurrentResult; |
|
} |
|
else if (_expectedException != null) // Special case if we have an expected exception |
|
{ |
|
if (TryExpectedException(context, () => _originalMethod.Method.Invoke(context.TestObject, arguments), ".NET", type => type == _expectedException, _expectedException.FullName, false) != TryExpectedExceptionResult.ThrewExpectedException) |
|
{ |
|
return context.CurrentResult; |
|
} |
|
|
|
if (TryExpectedException(context, () => nativeDelegateCaller(compiledFunction, nativeArgs), "Native", type => type == _expectedException, _expectedException.FullName, true) != TryExpectedExceptionResult.ThrewExpectedException) |
|
{ |
|
return context.CurrentResult; |
|
} |
|
} |
|
else |
|
{ |
|
object resultNative = null; |
|
|
|
// We are forced to run native before managed, because on IL2CPP, if a parameter |
|
// is a ref, it will keep the same memory location for both managed and burst |
|
// while in .NET CLR we have a different behavior |
|
// The result is that on functions expecting the same input value through the ref |
|
// it won't be anymore true because the managed could have modified the value before |
|
// burst |
|
|
|
// ------------------------------------------------------------------ |
|
// Run Native (Before) |
|
// ------------------------------------------------------------------ |
|
if (!RunManagedBeforeNative && !TestInterpreter) |
|
{ |
|
if (GetExtension() != null) |
|
{ |
|
nativeArgs = GetExtension().ProcessNativeArgsForDelegateCaller(nativeArgs, methodInfo); |
|
} |
|
resultNative = nativeDelegateCaller(compiledFunction, nativeArgs); |
|
if (returnBoxType != null) |
|
{ |
|
resultNative = Marshal.PtrToStructure((IntPtr)returnBox, returnBoxType); |
|
} |
|
} |
|
|
|
// ------------------------------------------------------------------ |
|
// Run Interpreter |
|
// ------------------------------------------------------------------ |
|
object resultInterpreter = null; |
|
|
|
if (TestInterpreter) |
|
{ |
|
ZeroMemory(returnBox, MaxReturnBoxSize); |
|
var name = methodInfo.DeclaringType.FullName + "." + methodInfo.Name; |
|
if (!InterpretMethod(interpretDelegate, methodInfo, nativeArgs, methodInfo.ReturnType, |
|
out var reason, out resultInterpreter)) |
|
{ |
|
lock (BailedTests) |
|
{ |
|
BailedTests[name] = reason; |
|
} |
|
} |
|
else |
|
{ |
|
if (returnBoxType != null) |
|
{ |
|
resultInterpreter = Marshal.PtrToStructure((IntPtr)returnBox, returnBoxType); |
|
} |
|
} |
|
} |
|
|
|
|
|
// ------------------------------------------------------------------ |
|
// Run Managed |
|
// ------------------------------------------------------------------ |
|
object resultClr; |
|
// This option skips running the managed version completely |
|
var overrideManagedResult = _originalMethod.Properties.Get("OverrideManagedResult"); |
|
if (overrideManagedResult != null) |
|
{ |
|
TestContext.Out.WriteLine($"Using OverrideManagedResult: `{overrideManagedResult}` to compare to burst `{resultNative}`, managed version not run"); |
|
resultClr = overrideManagedResult; |
|
} |
|
else |
|
{ |
|
ZeroMemory(returnBox, MaxReturnBoxSize); |
|
resultClr = _originalMethod.Method.Invoke(context.TestObject, arguments); |
|
|
|
if (returnBoxType != null) |
|
{ |
|
resultClr = Marshal.PtrToStructure((IntPtr)returnBox, returnBoxType); |
|
} |
|
} |
|
|
|
var overrideResultOnMono = _originalMethod.Properties.Get("OverrideResultOnMono"); |
|
if (overrideResultOnMono != null) |
|
{ |
|
TestContext.Out.WriteLine($"Using OverrideResultOnMono: `{overrideResultOnMono}` instead of `{resultClr}` compare to burst `{resultNative}`"); |
|
resultClr = overrideResultOnMono; |
|
} |
|
|
|
var overrideOn32BitNative = _originalMethod.Properties.Get("OverrideOn32BitNative"); |
|
if (overrideOn32BitNative != null && TargetIs32Bit()) |
|
{ |
|
TestContext.Out.WriteLine($"Using OverrideOn32BitNative: '{overrideOn32BitNative}' instead of '{resultClr}' compare to burst '{resultNative}' due to 32bit native runtime"); |
|
resultClr = overrideOn32BitNative; |
|
} |
|
|
|
// ------------------------------------------------------------------ |
|
// Run Native (After) |
|
// ------------------------------------------------------------------ |
|
if (RunManagedBeforeNative && !TestInterpreter) |
|
{ |
|
ZeroMemory(returnBox, MaxReturnBoxSize); |
|
resultNative = nativeDelegateCaller(compiledFunction, nativeArgs); |
|
if (returnBoxType != null) |
|
{ |
|
resultNative = Marshal.PtrToStructure((IntPtr)returnBox, returnBoxType); |
|
} |
|
} |
|
|
|
if (!TestInterpreter) |
|
{ |
|
// Use our own version (for handling correctly float precision) |
|
AssertHelper.AreEqual(resultClr, resultNative, GetULP()); |
|
} |
|
else if (resultInterpreter != null) |
|
{ |
|
AssertHelper.AreEqual(resultClr, resultInterpreter, GetULP()); |
|
} |
|
|
|
// Validate deterministic outputs - Disabled for now |
|
//RunDeterminismValidation(_originalMethod, resultNative); |
|
|
|
// Allow to process native result |
|
ProcessNativeResult(_originalMethod, resultNative); |
|
|
|
context.CurrentResult.SetResult(ResultState.Success); |
|
|
|
PostAssert(context); |
|
} |
|
|
|
// Check that the method is actually in the registry |
|
Assert.True(isInRegistry, "The test method is not in the registry, recompile the project with the updated StaticDelegateRegistry.generated.cs"); |
|
|
|
// Make an attempt to clean up arguments (to reduce wasted native heap memory) |
|
DisposeObjects(arguments); |
|
DisposeObjects(nativeArgs); |
|
} |
|
|
|
// Compile the method once again, this time for Arm CPU to check against gold asm images |
|
GoldFileTestForOtherPlatforms(methodInfo, runTest); |
|
|
|
CompleteTest(context); |
|
|
|
return context.CurrentResult; |
|
} |
|
|
|
protected virtual void PostAssert(ExecutionContext context) |
|
{ |
|
} |
|
|
|
protected virtual void ProcessNativeResult(TestMethod method, object result) |
|
{ |
|
} |
|
protected virtual string GetLinesFromDeterminismLog() |
|
{ |
|
return ""; |
|
} |
|
protected virtual bool IsDeterministicTest(TestMethod method) |
|
{ |
|
return false; |
|
} |
|
|
|
private static void DisposeObjects(object[] arguments) |
|
{ |
|
foreach (object o in arguments) |
|
{ |
|
IDisposable disp = o as IDisposable; |
|
disp?.Dispose(); |
|
} |
|
} |
|
|
|
private object[] CloneArguments(object[] arguments) |
|
{ |
|
var newArguments = new object[arguments.Length]; |
|
for (int i = 0; i < arguments.Length; i++) |
|
{ |
|
newArguments[i] = arguments[i]; |
|
} |
|
return newArguments; |
|
} |
|
|
|
protected unsafe void TransformArguments(MethodInfo method, object[] args, out object[] nativeArgs, out Type[] nativeArgTypes, byte* returnBox, out Type returnBoxType) |
|
{ |
|
returnBoxType = null; |
|
|
|
// Transform Arguments if necessary |
|
nativeArgs = (object[])args.Clone(); |
|
|
|
for (var i = 0; i < nativeArgs.Length; i++) |
|
{ |
|
var arg = args[i]; |
|
if (arg == null) |
|
{ |
|
throw new AssertionException($"Argument number `{i}` for method `{method}` cannot be null"); |
|
} |
|
|
|
if (arg.GetType() == typeof(float[])) |
|
{ |
|
args[i] = ConvertToNativeArray((float[])arg); |
|
} |
|
else if (arg.GetType() == typeof(int[])) |
|
{ |
|
args[i] = ConvertToNativeArray((int[])arg); |
|
} |
|
else if (arg.GetType() == typeof(float3[])) |
|
{ |
|
args[i] = ConvertToNativeArray((float3[])arg); |
|
} |
|
else if (arg is Type) |
|
{ |
|
var attrType = (Type)arg; |
|
if (typeof(IArgumentProvider).IsAssignableFrom(attrType)) |
|
{ |
|
var argumentProvider = (IArgumentProvider)Activator.CreateInstance(attrType); |
|
// Duplicate the input for C#/Burst in case the code is modifying the data |
|
args[i] = argumentProvider.Value; |
|
nativeArgs[i] = argumentProvider.Value; |
|
} |
|
else if (typeof(ReturnBox).IsAssignableFrom(attrType)) |
|
{ |
|
args[i] = (IntPtr)returnBox; |
|
nativeArgs[i] = (IntPtr)returnBox; |
|
} |
|
} |
|
} |
|
|
|
var parameters = method.GetParameters(); |
|
nativeArgTypes = new Type[nativeArgs.Length]; |
|
for (var i = 0; i < parameters.Length; i++) |
|
{ |
|
var expectedArgType = parameters[i].ParameterType; |
|
var actualArgType = args[i].GetType(); |
|
var actualNativeArgType = nativeArgs[i].GetType(); |
|
|
|
if (typeof(IFunctionPointerProvider).IsAssignableFrom(expectedArgType) || (expectedArgType.IsByRef && typeof(IFunctionPointerProvider).IsAssignableFrom(expectedArgType.GetElementType())) && actualNativeArgType == typeof(string)) |
|
{ |
|
var methodName = (string)args[i]; |
|
var candidates = |
|
_originalMethod.Method.MethodInfo.DeclaringType? |
|
.GetMethods() |
|
.Where(x => x.IsStatic && x.Name.Equals(methodName)) |
|
.ToArray(); |
|
|
|
if (candidates == null || candidates.Length != 1) |
|
{ |
|
throw new ArgumentException($"Could not resolve an unambigoues static method from name {methodName}."); |
|
} |
|
|
|
var functionPointer = CompileFunctionPointer(candidates[0], expectedArgType.IsByRef ? expectedArgType.GetElementType() : expectedArgType); |
|
nativeArgs[i] = functionPointer; |
|
args[i] = functionPointer; |
|
actualNativeArgType = expectedArgType; |
|
actualArgType = expectedArgType; |
|
} |
|
else |
|
{ |
|
// If the expected parameter for the native is a reference, we need to specify it here |
|
if (expectedArgType.IsByRef) |
|
{ |
|
actualArgType = actualArgType.MakeByRefType(); |
|
actualNativeArgType = actualNativeArgType.MakeByRefType(); |
|
} |
|
if (expectedArgType == typeof(IntPtr) && actualNativeArgType == typeof(int)) |
|
{ |
|
nativeArgs[i] = new IntPtr((int)args[i]); |
|
args[i] = new IntPtr((int)args[i]); |
|
actualNativeArgType = typeof(IntPtr); |
|
actualArgType = typeof(IntPtr); |
|
} |
|
if (expectedArgType == typeof(UIntPtr) && actualNativeArgType == typeof(uint)) |
|
{ |
|
nativeArgs[i] = new UIntPtr((uint)args[i]); |
|
args[i] = new UIntPtr((uint)args[i]); |
|
actualNativeArgType = typeof(UIntPtr); |
|
actualArgType = typeof(UIntPtr); |
|
} |
|
if (expectedArgType.IsPointer && actualNativeArgType == typeof(IntPtr)) |
|
{ |
|
if ((IntPtr)args[i] == (IntPtr)returnBox) |
|
{ |
|
if (returnBoxType != null) |
|
{ |
|
throw new ArgumentException($"Only one ReturnBox allowed"); |
|
} |
|
returnBoxType = expectedArgType.GetElementType(); |
|
} |
|
|
|
nativeArgs[i] = args[i]; |
|
actualNativeArgType = expectedArgType; |
|
actualArgType = expectedArgType; |
|
} |
|
} |
|
|
|
nativeArgTypes[i] = actualNativeArgType; |
|
|
|
if (expectedArgType != actualArgType) |
|
{ |
|
throw new ArgumentException($"Type mismatch in parameter {i} passed to {method.Name}: expected {expectedArgType}, got {actualArgType}."); |
|
} |
|
} |
|
} |
|
|
|
private static NativeArray<T> ConvertToNativeArray<T>(T[] array) where T : struct |
|
{ |
|
var nativeArray = new NativeArray<T>(array.Length, Allocator.Persistent); |
|
for (var j = 0; j < array.Length; j++) |
|
nativeArray[j] = array[j]; |
|
return nativeArray; |
|
} |
|
|
|
protected enum TryExpectedExceptionResult |
|
{ |
|
ThrewExpectedException, |
|
ThrewUnexpectedException, |
|
DidNotThrowException, |
|
} |
|
|
|
protected TryExpectedExceptionResult TryExpectedException(ExecutionContext context, Action action, string contextName, Func<Type, bool> expectedException, string expectedExceptionName, bool isTargetException, bool requireException = true) |
|
{ |
|
Type caughtType = null; |
|
|
|
Exception caughtException = null; |
|
try |
|
{ |
|
action(); |
|
} |
|
catch (Exception ex) |
|
{ |
|
if (isTargetException && ex is TargetInvocationException) |
|
{ |
|
ex = ((TargetInvocationException)ex).InnerException; |
|
} |
|
if (ex is NUnitException) |
|
ex = ex.InnerException; |
|
caughtException = ex; |
|
if (caughtException != null) |
|
{ |
|
caughtType = caughtException.GetType(); |
|
} |
|
} |
|
|
|
if (caughtException == null && !requireException) |
|
{ |
|
return TryExpectedExceptionResult.DidNotThrowException; |
|
} |
|
|
|
if (caughtType != null && expectedException(caughtType)) |
|
{ |
|
if (!CheckExpectedDiagnostics(context, contextName)) |
|
{ |
|
return TryExpectedExceptionResult.ThrewUnexpectedException; |
|
} |
|
else |
|
{ |
|
context.CurrentResult.SetResult(ResultState.Success); |
|
return TryExpectedExceptionResult.ThrewExpectedException; |
|
} |
|
} |
|
else if (caughtType != null) |
|
{ |
|
context.CurrentResult.SetResult(ResultState.Failure, $"In {contextName} code, expected {expectedExceptionName} but got {caughtType.Name}. Exception: {caughtException}"); |
|
return TryExpectedExceptionResult.ThrewUnexpectedException; |
|
} |
|
else |
|
{ |
|
context.CurrentResult.SetResult(ResultState.Failure, $"In {contextName} code, expected {expectedExceptionName} but no exception was thrown"); |
|
return TryExpectedExceptionResult.ThrewUnexpectedException; |
|
} |
|
} |
|
|
|
private static string GetDiagnosticIds(IEnumerable<DiagnosticId> diagnosticIds) |
|
{ |
|
if (diagnosticIds.Count() == 0) |
|
{ |
|
return "None"; |
|
} |
|
else |
|
{ |
|
return string.Join(",", diagnosticIds); |
|
} |
|
} |
|
|
|
public static void ReportBailedTests(TextWriter writer = null) |
|
{ |
|
writer = writer ?? Console.Out; |
|
lock (BailedTests) |
|
{ |
|
foreach (var bailedTest in BailedTests.OrderBy(kv => kv.Key)) |
|
{ |
|
writer.WriteLine($"{bailedTest.Key}: {bailedTest.Value}"); |
|
} |
|
} |
|
} |
|
|
|
protected bool CheckExpectedDiagnostics(ExecutionContext context, string contextName) |
|
{ |
|
var loggedDiagnosticIds = GetLoggedDiagnosticIds().OrderBy(x => x); |
|
var expectedDiagnosticIds = _expectedDiagnosticIds.OrderBy(x => x); |
|
|
|
if (!loggedDiagnosticIds.SequenceEqual(expectedDiagnosticIds)) |
|
{ |
|
context.CurrentResult.SetResult(ResultState.Failure, $"In {contextName} code, expecting diagnostic(s) to be logged with IDs {GetDiagnosticIds(_expectedDiagnosticIds)} but instead the following diagnostic(s) were logged: {GetDiagnosticIds(loggedDiagnosticIds)}"); |
|
return false; |
|
} |
|
return true; |
|
} |
|
|
|
protected virtual IEnumerable<DiagnosticId> GetLoggedDiagnosticIds() => Array.Empty<DiagnosticId>(); |
|
protected virtual IEnumerable<DiagnosticId> GetExpectedDiagnosticIds() => _expectedDiagnosticIds; |
|
|
|
protected void RunDeterminismValidation(TestMethod method, object resultNative) |
|
{ |
|
// GetLines first as this will allow us to ignore these tests if the log file is missing |
|
//which occurs when running the "trunk" package tests, since they use their own project file |
|
//a possible workaround for this is to embed the log into a .cs file and stick that in the tests |
|
//folder, then we don't need the resource folder version. |
|
var lines = GetLinesFromDeterminismLog(); |
|
|
|
// If the log is not found, this will also return false |
|
if (!IsDeterministicTest(method)) |
|
return; |
|
|
|
var allLines = lines.Split(new char[] { '\r', '\n' }); |
|
string matchName = $"{method.FullName}:"; |
|
foreach (var line in allLines) |
|
{ |
|
if (line.StartsWith(matchName)) |
|
{ |
|
if (resultNative.GetType() == typeof(Single)) |
|
{ |
|
unsafe |
|
{ |
|
var val = (float)resultNative; |
|
int intvalue = *((int*)&val); |
|
var resStr = $"0x{intvalue:X4}"; |
|
|
|
if (!line.EndsWith(resStr)) |
|
{ |
|
Assert.Fail($"Deterministic mismatch '{method.FullName}: {resStr}' but expected '{line}'"); |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
Assert.That(resultNative.GetType() == typeof(Double)); |
|
unsafe |
|
{ |
|
var val = (double)resultNative; |
|
long longvalue = *((long*)&val); |
|
var resStr = $"0x{longvalue:X8}"; |
|
|
|
if (!line.EndsWith(resStr)) |
|
{ |
|
Assert.Fail($"Deterministic mismatch '{method.FullName}: {resStr}' but expected '{line}'"); |
|
} |
|
} |
|
} |
|
return; |
|
} |
|
} |
|
Assert.Fail($"Deterministic mismatch test not present : '{method.FullName}'"); |
|
} |
|
|
|
protected abstract int GetRunCount(); |
|
|
|
protected abstract void CompleteTest(ExecutionContext context); |
|
|
|
protected abstract int GetULP(); |
|
|
|
protected abstract object[] GetArgumentsArray(TestMethod method); |
|
|
|
protected abstract unsafe Delegate CompileDelegate(ExecutionContext context, MethodInfo methodInfo, Type delegateType, byte* returnBox, out Type returnBoxType, out Delegate interpretDelegate); |
|
|
|
protected abstract bool InterpretMethod(Delegate interpretDelegate, MethodInfo methodInfo, object[] args, Type returnType, out string reason, out object result); |
|
|
|
protected abstract void GoldFileTestForOtherPlatforms(MethodInfo methodInfo, bool testWasRun); |
|
|
|
protected abstract bool TestOnCurrentHostEnvironment(MethodInfo methodInfo); |
|
|
|
protected abstract object CompileFunctionPointer(MethodInfo methodInfo, Type functionType); |
|
|
|
protected abstract void Setup(); |
|
|
|
protected abstract TestResult HandleCompilerException(ExecutionContext context, MethodInfo methodInfo); |
|
|
|
protected abstract TestCompilerBaseExtensions GetExtension(); |
|
|
|
protected abstract bool TargetIs32Bit(); |
|
} |
|
|
|
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] |
|
internal sealed class IntRangeAttribute : Attribute |
|
{ |
|
public readonly int Lo; |
|
public readonly int Hi; |
|
|
|
public IntRangeAttribute(int hi) { Hi = hi; } |
|
public IntRangeAttribute(int lo, int hi) { Lo = lo; Hi = hi; } |
|
} |
|
}
|
|
|