using System; using NUnit.Framework; using Unity.Pipeline.Compilation; using UnityEngine; namespace Unity.Pipeline.Tests.Editor { /// /// Native-object interop through the interpreter host binding (IlInterpreterHostBindings): real /// UnityEngine structs ( via the flat-frame AllowTypeStruct path), /// native-backed objects (/), and fake-null /// semantics on a destroyed — none of which the dotnet layer can /// exercise. /// class IlInterpreterHostInteropTests { // Compile a statement body and run it through the interpreter directly, against the shared // host binding (IlInterpreterHostBindings.CreateStandard) — the same surface code reload uses. No // eval command, no Assembly.Load, so the interop coverage holds under IL2CPP. static object RunViaInterpreter(string body) { var source = $@"using UnityEngine; public static class Probe {{ public static object Run() {{ {body} }} }}"; var compile = RoslynCompilationService.Compile(new CompilationRequest { SourceCode = source, AssemblyName = "IlInterpreterInteropProbe", SkipLoad = true, // interpreter walks the bytes; no Assembly.Load }); Assert.IsTrue(compile.Success, "probe source should compile"); using var interp = new IlInterpreter.Interpreter.ScriptInterpreter(IlInterpreterHostBindings.CreateStandard()); interp.Load(new RawScript(compile.AssemblyBytes)); return interp.Invoke("Run"); } [Test] public void Vector3_Struct_RoundTrips_ThroughFlatFrame() { // AllowTypeStruct: ctor + x/y/z field reads through the unmanaged Value frame. var result = RunViaInterpreter("var v = new Vector3(1f, 2f, 3f); return v.x + v.y + v.z;"); Assert.AreEqual(6f, (float)result, 1e-4f); } [Test] public void Transform_Position_SetAndGet() { var result = RunViaInterpreter( "var go = new GameObject(\"t\"); go.transform.position = new Vector3(1f, 2f, 3f); return go.transform.position.x;"); Assert.AreEqual(1f, (float)result, 1e-4f); } [Test] public void Mathf_StaticCall() { var result = RunViaInterpreter("return Mathf.Max(3f, 7f);"); Assert.AreEqual(7f, (float)result, 1e-4f); } // Regression for the gameplay pattern `a.GetType() == b.GetType()`: `==` on System.Type // operands compiles to Type.op_Equality, a BCL MemberRef the auto-bind policy skips — it // must be registered explicitly in AllowBcl (was: "Host method 'Type.op_Equality' is not // registered in the binding" from reloaded CharacterInputController.UseConsumable). [Test] public void TypeEquality_GetTypeComparison_BindsThroughStandardSurface() { var result = RunViaInterpreter( "GameObject a = new GameObject(\"type-eq-a\"); GameObject b = new GameObject(\"type-eq-b\"); " + "return a.GetType() == b.GetType();"); Assert.AreEqual(1, Convert.ToInt32(result), "same runtime types should compare equal"); } [Test] public void TypeInequality_DifferentTypes_BindsThroughStandardSurface() { var result = RunViaInterpreter("return typeof(GameObject) != typeof(Transform);"); Assert.AreEqual(1, Convert.ToInt32(result), "different types should compare not-equal"); } // `.Name` on a Type receiver binds through MemberInfo.get_Name (the declaring property), // not Type.get_Name — found by the push-time surface validation on real gameplay code. [Test] public void TypeName_ViaMemberInfo_BindsThroughStandardSurface() { var result = RunViaInterpreter("GameObject g = new GameObject(\"type-name\"); return g.GetType().Name;"); Assert.AreEqual("GameObject", (string)result); } // A destroyed UnityEngine.Object compares == null via Unity's overloaded operator, not // reference equality. Driven directly (not via eval) so we can register op_Equality and pass // the destroyed instance in as an argument — proving the interpreter honours fake-null. static object InvokeIsNull(UnityEngine.Object arg) { const string source = @" public class S { public static bool Run(UnityEngine.Object o) { return o == null; } }"; var compile = RoslynCompilationService.Compile(new CompilationRequest { SourceCode = source, AssemblyName = "IlInterpreterFakeNullProbe", SkipLoad = true, // interpreter walks the bytes; no Assembly.Load }); Assert.IsTrue(compile.Success, "probe source should compile"); var binding = new IlInterpreter.Interpreter.HostBinding() .AllowBcl() .AllowStatic("Object", "op_Equality", (_, a) => (object)((UnityEngine.Object)a[0] == (UnityEngine.Object)a[1]), 2); using var interp = new IlInterpreter.Interpreter.ScriptInterpreter(binding); interp.Load(new RawScript(compile.AssemblyBytes)); return interp.Invoke("Run", arg); } [Test] public void DestroyedObject_ComparesEqualToNull() { var go = new GameObject("to-destroy"); UnityEngine.Object.DestroyImmediate(go); // The host op_Equality returns bool, so the interpreter hands it back boxed as bool // (unlike interpreter-internal bools, which come back as boxed int). Assert.IsTrue((bool)InvokeIsNull(go), "destroyed object should be fake-null"); } [Test] public void LiveObject_DoesNotCompareEqualToNull() { var go = new GameObject("alive"); try { Assert.IsFalse((bool)InvokeIsNull(go), "live object should not be null"); } finally { UnityEngine.Object.DestroyImmediate(go); } } // Roslyn emits `call Array.Empty()` when a params T[] parameter receives no arguments. // The interpreter must bind that MethodSpec or the call throws at load time. [Test] public void ParamsArray_OmittedArgument_BindsArrayEmpty() { // Sum() with no args → Roslyn emits Array.Empty() for the params int[]. var result = RunViaInterpreter( "int Sum(params int[] ns) { int s = 0; foreach (var n in ns) s += n; return s; } return Sum();"); Assert.AreEqual(0, Convert.ToInt32(result)); } [Test] public void ParamsArray_OmittedStringArgument_BindsArrayEmpty() { var result = RunViaInterpreter( "int Count(params string[] ss) { return ss.Length; } return Count();"); Assert.AreEqual(0, Convert.ToInt32(result)); } static object RunViaArrayArgHost(string body) { var source = $@"using Unity.Pipeline.Tests.Editor; public static class Probe {{ public static object Run() {{ {body} }} }}"; var compile = RoslynCompilationService.Compile(new CompilationRequest { SourceCode = source, AssemblyName = "IlInterpreterArrayArgProbe", SkipLoad = true, // interpreter walks the bytes; no Assembly.Load }); Assert.IsTrue(compile.Success, "probe source should compile"); var binding = new IlInterpreter.Interpreter.HostBinding() .AllowBcl() .AllowType(); using var interp = new IlInterpreter.Interpreter.ScriptInterpreter(binding); interp.Load(new RawScript(compile.AssemblyBytes)); return interp.Invoke("Run"); } [Test] public void StringArrayLiteral_PassedToHostMethodTakingStringArray_DoesNotThrow() { var result = RunViaArrayArgHost( "return ArrayArgHost.Count(new string[] { \"a\", \"b\", \"c\" });"); Assert.AreEqual(3, Convert.ToInt32(result)); } /// Minimal IScript over raw Roslyn-emitted PE bytes. sealed class RawScript : IlInterpreter.IScript { public RawScript(byte[] bytes) { Il = bytes; } public string Name => "IlInterpreterProbe"; public ReadOnlyMemory Il { get; } } } /// /// Minimal host method taking a reference-type array parameter, registered through /// HostBinding.AllowType the same reflection-based way any Unity API method would be. /// public sealed class ArrayArgHost { public static int Count(string[] items) => items.Length; } }