2023-03-28 13:24:16 -04:00
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 ) ;
2023-05-07 18:43:11 -04:00
if ( TestInterpreter )
{
compiledFunction = interpretDelegate ;
}
2023-03-28 13:24:16 -04:00
if ( compiledFunction = = null )
2023-05-07 18:43:11 -04:00
{
2023-03-28 13:24:16 -04:00
return context . CurrentResult ;
2023-05-07 18:43:11 -04:00
}
2023-03-28 13:24:16 -04:00
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 ; }
}
}