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 { /// /// When used as a type in TestCompiler arguments signifies that the corresponding parameter is a pointer output. /// internal struct ReturnBox { } /// /// Interface used for initialize function pointers. /// internal interface IFunctionPointerProvider { object FromIntPtr(IntPtr ptr); } /// /// Used to implement custom testing behaviour /// internal interface TestCompilerBaseExtensions { (bool shouldSkip, string skipReason) SkipTest(MethodInfo method); Type FetchAlternateDelegate(out bool isInRegistry, out Func 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) { } /// /// 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. /// public bool CompileOnly { get; set; } /// /// The type of exception the test expects to be thrown. /// public Type ExpectedException { get; set; } /// /// Whether the test is expected to throw a compiler exception or not. /// 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; } /// /// Use this property when the JIT calculation is wrong (e.g when using float) /// public object OverrideResultOnMono { get; set; } /// /// 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) /// public object OverrideManagedResult { get; set; } /// /// 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. /// public object OverrideOn32BitNative { get; set; } /// /// Use this and specify a TargetPlatform (Host) to have the test ignored when running on that host. Mostly used by WASM at present. /// public object IgnoreOnPlatform { get; set; } public bool IgnoreOnNetFramework { get; set; } public bool IgnoreOnNetCore { get; set; } public bool? IsDeterministic { get; set; } public bool SkipForILInterpreter { get; set; } protected virtual bool SupportException => true; public bool IgnoreExceptionMessages { get; set; } public bool DisableStringInterpolationInExceptionMessages { get; set; } IEnumerable 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(this.Arguments); // Expand arguments with IntRangeAttributes if we have them foreach (var param in method.GetParameters()) { var attrs = param.GetCustomAttributes(false); if (attrs == null || attrs.Length != 1) continue; arguments.Add(attrs[0]); } IEnumerable 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 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; private readonly bool _ignoreExceptionMessages; private readonly bool _disableStringInterpolationInExceptionMessages; 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; _ignoreExceptionMessages = Attribute.IgnoreExceptionMessages; _disableStringInterpolationInExceptionMessages = Attribute.DisableStringInterpolationInExceptionMessages; _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 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 StaticDynamicDelegateCaller = new Func((del, arguments) => ((Delegate)del).DynamicInvoke(arguments)); private static readonly int MaxReturnBoxSize = 512; protected bool RunManagedBeforeNative { get; set; } protected static readonly Dictionary BailedTests = new Dictionary(); private string ExpectedExceptionMessage = null; 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 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, () => RunNativeCode(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 = RunNativeCode(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 = RunNativeCode(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 object RunNativeCode(Func nativeDelegateCaller, Delegate compiledFunction, object[] nativeArgs) { return nativeDelegateCaller(compiledFunction, nativeArgs); } 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)) { if (actualNativeArgType == typeof(int)) { nativeArgs[i] = new IntPtr((int)args[i]); args[i] = new IntPtr((int)args[i]); actualNativeArgType = typeof(IntPtr); actualArgType = typeof(IntPtr); } else if (actualNativeArgType == typeof(long)) { nativeArgs[i] = new IntPtr((long)args[i]); args[i] = new IntPtr((long)args[i]); actualNativeArgType = typeof(IntPtr); actualArgType = typeof(IntPtr); } } if (expectedArgType == typeof(UIntPtr)) { if (actualNativeArgType == typeof(uint)) { nativeArgs[i] = new UIntPtr((uint)args[i]); args[i] = new UIntPtr((uint)args[i]); actualNativeArgType = typeof(UIntPtr); actualArgType = typeof(UIntPtr); } else if (actualNativeArgType == typeof(ulong)) { nativeArgs[i] = new UIntPtr((ulong)args[i]); args[i] = new UIntPtr((ulong)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 ConvertToNativeArray(T[] array) where T : struct { var nativeArray = new NativeArray(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 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 (ExpectedExceptionMessage == null) ExpectedExceptionMessage = caughtException.Message; } } if (caughtException == null && !requireException) { return TryExpectedExceptionResult.DidNotThrowException; } if (caughtType != null && expectedException(caughtType)) { if (!CheckExpectedDiagnostics(context, contextName)) { return TryExpectedExceptionResult.ThrewUnexpectedException; } else if (caughtException.Message != ExpectedExceptionMessage && !_ignoreExceptionMessages && !_disableStringInterpolationInExceptionMessages) { context.CurrentResult.SetResult(ResultState.Failure, $"In {contextName} code, expected exception message {ExpectedExceptionMessage} but got {caughtException.Message}"); return TryExpectedExceptionResult.ThrewUnexpectedException; } else if (caughtException.Message == ExpectedExceptionMessage && _disableStringInterpolationInExceptionMessages && !_ignoreExceptionMessages && contextName == "Native") { context.CurrentResult.SetResult(ResultState.Failure, $"In {contextName} code, expected exception message {caughtException.Message} to not be interpolated."); 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 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 GetLoggedDiagnosticIds() => Array.Empty(); protected virtual IEnumerable 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; } } }