using System.IO; using System.Text.RegularExpressions; using NUnit.Framework; using Unity.Pipeline.Compilation; using Unity.Pipeline.CodeReload; using Unity.Pipeline.Runtime.Commands; using UnityEngine; using UnityEngine.TestTools; namespace Unity.Pipeline.Tests.Editor { /// /// Top-level probe with a woven [CodeReload] method, reloaded through the interpreter backend. /// Top-level (not nested) so the generated static override can name it from a separate assembly. /// public class IlInterpreterReloadProbe { public int baseline; public int hotValue; [CodeReload] public void Compute() { baseline = 1; // original body } } /// /// Probe with a PRIVATE field, reloaded through the interpreter. Verifies that a [CodeReload] body can /// read/write private state now that the override is compiled with access checks disabled and the host /// binding registers non-public instance members. A public getter exposes the private field for asserts. /// public class IlInterpreterPrivateReloadProbe { private int m_Secret; public int Secret => m_Secret; [CodeReload] public void Compute() { m_Secret = 1; // original body } } /// /// Probe whose reloaded body CALLS a private helper method (not just reads a private field). Mirrors /// TankAI, whose reloaded SeekUpdate calls the private GetPathLength. The interpreter must resolve the /// private method token to its registered binding (it looked methods up with BindingFlags.Public only, /// so private calls hit "Host method not registered" and the whole override fell back). /// public class IlInterpreterPrivateMethodProbe { public int result; private int Helper(int x) => x * 3; [CodeReload] public void Compute() { result = 0; // original body } } /// /// Probe with a PRIVATE nested enum field switched on in a [CodeReload] body — the TankAI pattern /// (Update switches on a private enum State). The interpreter classified enum host fields as O /// (boxed), so the switch matched no case and the method silently no-op'd; enum fields must read as /// their underlying int. /// public class IlInterpreterEnumSwitchProbe { private enum Mode { Idle, Run, Stop } #pragma warning disable CS0414 // read only by the reloaded body, which the compiler cannot see private Mode m_Mode = Mode.Run; #pragma warning restore CS0414 public int picked = -1; [CodeReload] public void Compute() { picked = -1; } // original body } /// /// Probe whose reloaded Compute calls another reloaded method (Inner) on the /// same instance — a host call that woven-dispatches back into the SAME interpreter VM (reentrant). /// The VM ran every Invoke at frame base 0, so the nested call clobbered Compute's live frame; verify /// Compute's locals survive the nested call. Mirrors TankAI.Update calling TankAI.SeekUpdate. /// public class IlInterpreterReentrancyProbe { public int result; public int innerRan; [CodeReload] public void Inner() { innerRan = -1; } // original body [CodeReload] public void Compute() { result = -1; } // original body } /// /// Probe whose reloaded body constructs a UnityEngine reference type (new NavMeshPath()) — a /// newobj over a type NOT in the standard binding. Verifies the interpreter registers referenced /// engine types so the ctor token resolves (otherwise lowering fails "opcode 0x0073 (newobj) not supported"). /// public class IlInterpreterNewobjReloadProbe { public int made = -1; [CodeReload] public void Compute() { made = -1; // original body } } /// /// Probe whose reloaded body raises IMGUI's control-flow exception (GUIUtility.ExitGUI). The /// dispatch must PROPAGATE the original exception object — not log-and-fall-back — so a /// reloaded method behaves like its compiled equivalent (IMGUI catches ExitGUIException /// by type). The VM wraps it in ScriptRuntimeException with the original as InnerException; /// the registry unwraps and rethrows it. /// public class IlInterpreterExitGuiProbe { public int marker = -1; [CodeReload] public void Compute() { marker = 0; // original body } } /// /// Probe whose reloaded body throws at runtime. Verifies interpreter errors carry a source /// location ("at line N (File.cs)") resolved from the embedded PDB + the transform's #line /// directives — not a raw IL offset ("at IL+0x000D"), which users can't act on. /// public class IlInterpreterLineInfoProbe { public int made = -1; [CodeReload] public void Compute() { made = 0; // original body } } /// /// Probe with Nullable<T> fields — the TankMovement pattern (its reloaded methods read /// InputUser.controlScheme.Value.name, an InputControlScheme?). Nullable members /// are interpreter intrinsics over the boxed-T-or-null representation; before that, /// Nullable`1.get_Value failed as "not registered in the binding". /// public class IlInterpreterNullableProbe { public float? maybeFloat = 2.5f; public float? emptyFloat = null; public int? maybeInt = 7; public int? emptyInt = null; public float sum; public int result = -1; [CodeReload] public void Compute() { result = 0; // original body } } /// /// Probe whose reloaded body reads UnityEngine.Random statics — `value` (a float) and /// `rotationUniform` (a STATIC property returning a flat struct, Quaternion). Repro for /// "Random.rotationUniform does not code reload". /// public class IlInterpreterRandomProbe { public int made = -1; [CodeReload] public void Compute() { made = 0; // original body } } /// /// Probe whose compiled body mutates state; the empty-body reload test replaces it with a no-op. /// public class IlInterpreterEmptyBodyProbe { public int computedValue; [CodeReload] public void Compute() { computedValue = 42; // original body } } /// /// Probe for reloads that DECLARE a new method and call it: CompiledHelper is the compiled /// twin (same body text as the LiveHelper the edited source adds) — its output is the oracle /// the interpreted, live-added helper must match. /// public class IlInterpreterNewMethodProbe { public int result; public int CompiledHelper(int x) { int acc = 0; for (int i = 1; i <= x; i++) acc += i * x - 1; return acc + x / 2; } [CodeReload] public void Compute() { result = -1; // original body } } /// /// Probe for a reload that adds a new Click method and wires it to a UITK-style callback — /// a method group over a live-added method (the DebugUIController Button case). The event /// and Fire() stand in for Button.clicked, which needs a live panel to raise. /// public class IlInterpreterClickProbe { public int clicks; public event System.Action Clicked; public void Fire() => Clicked?.Invoke(); [CodeReload] public void Compute() { clicks = -1; // original body } } /// /// Probe for a reload that adds a NEW [CodeReload]-tagged method: the new method can't register /// (no woven prologue exists for it) but must not poison the rest of the file's overrides. /// public class IlInterpreterNewTaggedProbe { public int result; [CodeReload] public void Compute() { result = -1; // original body } } /// /// End-to-end interpreter code reload: the same reload_file pipeline as the Assembly.Load path but /// via reload_file_editor_interpreter, so the edited [CodeReload] body is dispatched through the /// IlInterpreter VM (InterpreterCodeReloadExecutor) rather than reflection. Mirrors /// 's end-to-end test with the interpreter backend selected. /// class IlInterpreterCodeReloadInterpreterTests { [SetUp] public void Setup() => CodeReloadRegistry.ClearAllForTesting(); [TearDown] public void TearDown() => CodeReloadRegistry.ClearAllForTesting(); [Test] public void ReloadFileInterpreter_EditedBody_AppliesViaInterpreterDispatch() { // Auto-discovery does this at play start; register the target explicitly for the test. CodeReloadRegistry.RegisterReloadableMethod( typeof(IlInterpreterReloadProbe).GetMethod(nameof(IlInterpreterReloadProbe.Compute)), new CodeReloadAttribute()); var editedSource = @" using Unity.Pipeline.CodeReload; namespace Unity.Pipeline.Tests.Editor { public class IlInterpreterReloadProbe { public int baseline; public int hotValue; [CodeReload] public void Compute() { hotValue = 99; } } }"; var path = Path.Combine(Application.temporaryCachePath, "IlInterpreterReloadProbe_edit.cs"); File.WriteAllText(path, editedSource); try { var response = CodeReloadCommands.ReloadFileEditorInterpreter(path); Assert.IsTrue(response.Success, $"reload_file_editor_interpreter should succeed. Error: {response.ErrorDetails}"); var probe = new IlInterpreterReloadProbe(); probe.Compute(); Assert.AreEqual(99, probe.hotValue, "Edited [CodeReload] body should run via the interpreter dispatch"); Assert.AreEqual(0, probe.baseline, "Original compiled body should be skipped when the interpreter override runs"); } finally { if (File.Exists(path)) File.Delete(path); } } [Test] public void ReloadFileInterpreter_RandomStatics_DispatchViaInterpreter() { CodeReloadRegistry.RegisterReloadableMethod( typeof(IlInterpreterRandomProbe).GetMethod(nameof(IlInterpreterRandomProbe.Compute)), new CodeReloadAttribute()); // rotationUniform is a static property returning a flat struct (Quaternion); value is // a static float property. A unit quaternion's squared magnitude is 1, so made == 2 // proves both statics dispatched with real values (a fallback leaves made == 0). var editedSource = @" using Unity.Pipeline.CodeReload; namespace Unity.Pipeline.Tests.Editor { public class IlInterpreterRandomProbe { public int made = -1; [CodeReload] public void Compute() { made = 0; var v = UnityEngine.Random.value; if (v >= 0f && v <= 1f) made = made + 1; var q = UnityEngine.Random.rotationUniform; float mag = q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w; if (mag > 0.99f && mag < 1.01f) made = made + 1; } } }"; var path = Path.Combine(Application.temporaryCachePath, "IlInterpreterRandomProbe_edit.cs"); File.WriteAllText(path, editedSource); try { var response = CodeReloadCommands.ReloadFileEditorInterpreter(path); Assert.IsTrue(response.Success, $"reload should succeed. Error: {response.ErrorDetails}"); var probe = new IlInterpreterRandomProbe(); probe.Compute(); Assert.AreEqual(2, probe.made, "Random.value and Random.rotationUniform should both dispatch via the interpreter"); } finally { if (File.Exists(path)) File.Delete(path); } } [Test] public void ReloadFileInterpreter_EmptyBody_AppliesNoOpOverride() { CodeReloadRegistry.RegisterReloadableMethod( typeof(IlInterpreterEmptyBodyProbe).GetMethod(nameof(IlInterpreterEmptyBodyProbe.Compute)), new CodeReloadAttribute()); // Emptying a [CodeReload] body is a legitimate edit (e.g. deleting a Debug.Log): the // reload must bind a no-op override, not fail with "No [CodeReload] methods found" // (which would leave the previous override live). var editedSource = @" using Unity.Pipeline.CodeReload; namespace Unity.Pipeline.Tests.Editor { public class IlInterpreterEmptyBodyProbe { public int computedValue; [CodeReload] public void Compute() { } } }"; var path = Path.Combine(Application.temporaryCachePath, "IlInterpreterEmptyBodyProbe_edit.cs"); File.WriteAllText(path, editedSource); try { var response = CodeReloadCommands.ReloadFileEditorInterpreter(path); Assert.IsTrue(response.Success, $"reload of an emptied [CodeReload] body should succeed. Error: {response.ErrorDetails}"); var probe = new IlInterpreterEmptyBodyProbe(); probe.Compute(); Assert.AreEqual(0, probe.computedValue, "Empty override should replace the original body (which sets 42)"); } finally { if (File.Exists(path)) File.Delete(path); } } [Test] public void ReloadFileInterpreter_PrivateFieldAccess_AppliesViaInterpreterDispatch() { CodeReloadRegistry.RegisterReloadableMethod( typeof(IlInterpreterPrivateReloadProbe).GetMethod(nameof(IlInterpreterPrivateReloadProbe.Compute)), new CodeReloadAttribute()); var editedSource = @" using Unity.Pipeline.CodeReload; namespace Unity.Pipeline.Tests.Editor { public class IlInterpreterPrivateReloadProbe { private int m_Secret; public int Secret => m_Secret; [CodeReload] public void Compute() { m_Secret = 77; } } }"; var path = Path.Combine(Application.temporaryCachePath, "IlInterpreterPrivateReloadProbe_edit.cs"); File.WriteAllText(path, editedSource); try { var response = CodeReloadCommands.ReloadFileEditorInterpreter(path); Assert.IsTrue(response.Success, $"reload_file_editor_interpreter should succeed for private field access. Error: {response.ErrorDetails}"); var probe = new IlInterpreterPrivateReloadProbe(); probe.Compute(); Assert.AreEqual(77, probe.Secret, "Edited [CodeReload] body should write the private field via the interpreter dispatch"); } finally { if (File.Exists(path)) File.Delete(path); } } [Test] public void ReloadFileInterpreter_PrivateMethodCall_AppliesViaInterpreterDispatch() { CodeReloadRegistry.RegisterReloadableMethod( typeof(IlInterpreterPrivateMethodProbe).GetMethod(nameof(IlInterpreterPrivateMethodProbe.Compute)), new CodeReloadAttribute()); // If the interpreter can't resolve the private Helper it throws "Host method not registered" // and falls back (result stays 0); result==39 proves the private call dispatched. var editedSource = @" using Unity.Pipeline.CodeReload; namespace Unity.Pipeline.Tests.Editor { public class IlInterpreterPrivateMethodProbe { public int result; private int Helper(int x) => x * 3; [CodeReload] public void Compute() { result = Helper(13); } } }"; var path = Path.Combine(Application.temporaryCachePath, "IlInterpreterPrivateMethodProbe_edit.cs"); File.WriteAllText(path, editedSource); try { var response = CodeReloadCommands.ReloadFileEditorInterpreter(path); Assert.IsTrue(response.Success, $"reload_file_editor_interpreter should succeed for a private method call. Error: {response.ErrorDetails}"); var probe = new IlInterpreterPrivateMethodProbe(); probe.Compute(); Assert.AreEqual(39, probe.result, "Edited body should call the private Helper via the interpreter dispatch (13*3)"); } finally { if (File.Exists(path)) File.Delete(path); } } [Test] public void ReloadFileInterpreter_EnumFieldSwitch_DispatchesViaInterpreter() { CodeReloadRegistry.RegisterReloadableMethod( typeof(IlInterpreterEnumSwitchProbe).GetMethod(nameof(IlInterpreterEnumSwitchProbe.Compute)), new CodeReloadAttribute()); // m_Mode==Run, so case Run must set picked=20. If the enum field reads as a boxed // enum (the bug), no case matches and picked stays -1. var editedSource = @" using Unity.Pipeline.CodeReload; namespace Unity.Pipeline.Tests.Editor { public class IlInterpreterEnumSwitchProbe { private enum Mode { Idle, Run, Stop } private Mode m_Mode = Mode.Run; public int picked = -1; [CodeReload] public void Compute() { switch (m_Mode) { case Mode.Idle: picked = 10; break; case Mode.Run: picked = 20; break; case Mode.Stop: picked = 30; break; } } } }"; var path = Path.Combine(Application.temporaryCachePath, "IlInterpreterEnumSwitchProbe_edit.cs"); File.WriteAllText(path, editedSource); try { var response = CodeReloadCommands.ReloadFileEditorInterpreter(path); Assert.IsTrue(response.Success, $"reload should succeed. Error: {response.ErrorDetails}"); var probe = new IlInterpreterEnumSwitchProbe(); probe.Compute(); Assert.AreEqual(20, probe.picked, "switch on the private enum field should hit case Run via the interpreter"); } finally { if (File.Exists(path)) File.Delete(path); } } [Test] public void ReloadFileInterpreter_ReentrantCall_PreservesCallerFrame() { CodeReloadRegistry.RegisterReloadableMethod( typeof(IlInterpreterReentrancyProbe).GetMethod(nameof(IlInterpreterReentrancyProbe.Compute)), new CodeReloadAttribute()); CodeReloadRegistry.RegisterReloadableMethod( typeof(IlInterpreterReentrancyProbe).GetMethod(nameof(IlInterpreterReentrancyProbe.Inner)), new CodeReloadAttribute()); // Compute keeps locals a/b live across a call to Inner (itself an interpreter override, invoked // reentrantly via the host/woven dispatch). Inner uses its own locals; if the reentrant call ran // at frame base 0 it would clobber a/b and result != 11. var editedSource = @" using Unity.Pipeline.CodeReload; namespace Unity.Pipeline.Tests.Editor { public class IlInterpreterReentrancyProbe { public int result; public int innerRan; [Unity.Pipeline.CodeReload.CodeReload] public void Inner() { int x = 99; int y = x + 1; innerRan = y; } [Unity.Pipeline.CodeReload.CodeReload] public void Compute() { int a = 5; int b = 6; Inner(); result = a + b; } } }"; var path = Path.Combine(Application.temporaryCachePath, "IlInterpreterReentrancyProbe_edit.cs"); File.WriteAllText(path, editedSource); try { var response = CodeReloadCommands.ReloadFileEditorInterpreter(path); Assert.IsTrue(response.Success, $"reload should succeed. Error: {response.ErrorDetails}"); var probe = new IlInterpreterReentrancyProbe(); probe.Compute(); Assert.AreEqual(100, probe.innerRan, "reentrant Inner override should have run"); Assert.AreEqual(11, probe.result, "caller frame (locals a,b) must survive the reentrant Inner call"); } finally { if (File.Exists(path)) File.Delete(path); } } [Test] public void ReloadFileInterpreter_NewobjHostType_AppliesViaInterpreterDispatch() { CodeReloadRegistry.RegisterReloadableMethod( typeof(IlInterpreterNewobjReloadProbe).GetMethod(nameof(IlInterpreterNewobjReloadProbe.Compute)), new CodeReloadAttribute()); // NavMeshPath is not in the standard binding, so the newobj token only resolves if the // interpreter registers referenced engine types (AllowReferencedTypes). If newobj or the // instance call fails, the override falls back and `made` stays -1; made==1 proves the // host-type construction dispatched. var editedSource = @" using Unity.Pipeline.CodeReload; namespace Unity.Pipeline.Tests.Editor { public class IlInterpreterNewobjReloadProbe { public int made = -1; [CodeReload] public void Compute() { var p = new UnityEngine.AI.NavMeshPath(); p.ClearCorners(); made = 1; } } }"; var path = Path.Combine(Application.temporaryCachePath, "IlInterpreterNewobjReloadProbe_edit.cs"); File.WriteAllText(path, editedSource); try { var response = CodeReloadCommands.ReloadFileEditorInterpreter(path); Assert.IsTrue(response.Success, $"reload_file_editor_interpreter should succeed for `new NavMeshPath()`. Error: {response.ErrorDetails}"); var probe = new IlInterpreterNewobjReloadProbe(); probe.Compute(); Assert.AreEqual(1, probe.made, "Edited body should construct NavMeshPath (newobj over a host type) and run via the interpreter"); } finally { if (File.Exists(path)) File.Delete(path); } } [Test] public void ReloadFileInterpreter_NullableMembers_DispatchViaIntrinsics() { CodeReloadRegistry.RegisterReloadableMethod( typeof(IlInterpreterNullableProbe).GetMethod(nameof(IlInterpreterNullableProbe.Compute)), new CodeReloadAttribute()); // Exercises every Nullable shape the intrinsics model: HasValue, Value (local and // field receivers), `??` (GetValueOrDefault), a lifted null comparison, GetValueOrDefault // with and without a fallback, and a T→T? conversion (Nullable ctor via ldloca+call). var editedSource = @" using Unity.Pipeline.CodeReload; namespace Unity.Pipeline.Tests.Editor { public class IlInterpreterNullableProbe { public float? maybeFloat = 2.5f; public float? emptyFloat = null; public int? maybeInt = 7; public int? emptyInt = null; public float sum; public int result = -1; [CodeReload] public void Compute() { float total = 0f; var f = maybeFloat; if (f.HasValue) total += f.Value; // +2.5 total += emptyFloat ?? 3.0f; // +3.0 (null branch) float? made = 4.0f; // Nullable ctor if (made != null) // lifted comparison → HasValue total += made.Value; // +4.0 sum = total; // 9.5 result = maybeInt.GetValueOrDefault() + emptyInt.GetValueOrDefault(2); // 7 + 2 } } }"; var path = Path.Combine(Application.temporaryCachePath, "IlInterpreterNullableProbe_edit.cs"); File.WriteAllText(path, editedSource); try { var response = CodeReloadCommands.ReloadFileEditorInterpreter(path); Assert.IsTrue(response.Success, $"reload should succeed. Error: {response.ErrorDetails}"); var probe = new IlInterpreterNullableProbe(); probe.Compute(); Assert.AreEqual(9.5f, probe.sum, 0.001f, "HasValue/Value/??/ctor over float? should all dispatch via the interpreter"); Assert.AreEqual(9, probe.result, "GetValueOrDefault (0- and 1-arg) over int? should dispatch via the interpreter"); } finally { if (File.Exists(path)) File.Delete(path); } } [Test] public void ReloadFileInterpreter_RuntimeError_ReportsSourceLineAndFile() { CodeReloadRegistry.RegisterReloadableMethod( typeof(IlInterpreterLineInfoProbe).GetMethod(nameof(IlInterpreterLineInfoProbe.Compute)), new CodeReloadAttribute()); // The edited body dereferences a null host object on LINE 13 of this source (the // `made = p.corners.Length;` statement — line 1 is the empty line after the opening // quote). The interpreter's error must carry that line and the file name, resolved // from the embedded PDB whose #line directives map the body to the pushed file. var editedSource = @" using Unity.Pipeline.CodeReload; namespace Unity.Pipeline.Tests.Editor { public class IlInterpreterLineInfoProbe { public int made = -1; [CodeReload] public void Compute() { UnityEngine.AI.NavMeshPath p = null; made = p.corners.Length; } } }"; var path = Path.Combine(Application.temporaryCachePath, "IlInterpreterLineInfoProbe_edit.cs"); File.WriteAllText(path, editedSource); try { var response = CodeReloadCommands.ReloadFileEditorInterpreter(path); Assert.IsTrue(response.Success, $"reload should succeed. Error: {response.ErrorDetails}"); // The dispatch logs the interpreter error and falls back to the original body. LogAssert.Expect(LogType.Error, new Regex( @"CodeReload: Error invoking override for 'IlInterpreterLineInfoProbe\.Compute':.*" + @"NullReferenceException.* at line 13 \(IlInterpreterLineInfoProbe_edit\.cs\)")); LogAssert.Expect(LogType.Error, new Regex("CodeReload: Stack trace:")); var probe = new IlInterpreterLineInfoProbe(); probe.Compute(); Assert.AreEqual(0, probe.made, "After the override throws, the original body should run as the fallback"); } finally { if (File.Exists(path)) File.Delete(path); } } [Test] public void ReloadFileInterpreter_RuntimeError_WithLineInfoDisabled_ReportsIlOffset() { CodeReloadRegistry.RegisterReloadableMethod( typeof(IlInterpreterLineInfoProbe).GetMethod(nameof(IlInterpreterLineInfoProbe.Compute)), new CodeReloadAttribute()); // Same failing body as the line-info test, but with the "Error Line Info" setting off: // no embedded PDB is shipped, so the error falls back to the raw IL offset. var editedSource = @" using Unity.Pipeline.CodeReload; namespace Unity.Pipeline.Tests.Editor { public class IlInterpreterLineInfoProbe { public int made = -1; [CodeReload] public void Compute() { UnityEngine.AI.NavMeshPath p = null; made = p.corners.Length; } } }"; var path = Path.Combine(Application.temporaryCachePath, "IlInterpreterLineInfoProbe_noinfo_edit.cs"); File.WriteAllText(path, editedSource); var previous = CodeReloadCompiler.EmitSourceLineInfo; CodeReloadCompiler.EmitSourceLineInfo = false; try { var response = CodeReloadCommands.ReloadFileEditorInterpreter(path); Assert.IsTrue(response.Success, $"reload should succeed. Error: {response.ErrorDetails}"); LogAssert.Expect(LogType.Error, new Regex( @"CodeReload: Error invoking override for 'IlInterpreterLineInfoProbe\.Compute':.*" + @"NullReferenceException.* at IL\+0x")); LogAssert.Expect(LogType.Error, new Regex("CodeReload: Stack trace:")); var probe = new IlInterpreterLineInfoProbe(); probe.Compute(); Assert.AreEqual(0, probe.made, "After the override throws, the original body should run as the fallback"); } finally { CodeReloadCompiler.EmitSourceLineInfo = previous; if (File.Exists(path)) File.Delete(path); } } [Test] public void ReloadFileInterpreter_OverrideThrows_PropagatesOriginalException() { CodeReloadRegistry.RegisterReloadableMethod( typeof(IlInterpreterExitGuiProbe).GetMethod(nameof(IlInterpreterExitGuiProbe.Compute)), new CodeReloadAttribute()); var editedSource = @" using Unity.Pipeline.CodeReload; namespace Unity.Pipeline.Tests.Editor { public class IlInterpreterExitGuiProbe { public int marker = -1; [CodeReload] public void Compute() { marker = 42; UnityEngine.GUIUtility.ExitGUI(); } } }"; var path = UnityEditor.FileUtil.GetUniqueTempPathInProject() + ".cs"; File.WriteAllText(path, editedSource); try { var response = CodeReloadCommands.ReloadFileEditorInterpreter(path); Assert.IsTrue(response.Success, $"reload should succeed. Error: {response.ErrorDetails}"); var probe = new IlInterpreterExitGuiProbe(); Assert.Throws(() => probe.Compute(), "The override body's exception must propagate with its type intact (IMGUI " + "catches ExitGUIException by type), not be swallowed into a fallback"); Assert.AreEqual(42, probe.marker, "The override ran up to the throw, and the original body (marker = 0) must " + "NOT run as a fallback"); } finally { if (File.Exists(path)) File.Delete(path); } } [Test] public void ReloadFileInterpreter_NewHelperMethod_MatchesCompiledTwin() { CodeReloadRegistry.RegisterReloadableMethod( typeof(IlInterpreterNewMethodProbe).GetMethod(nameof(IlInterpreterNewMethodProbe.Compute)), new CodeReloadAttribute()); // LiveHelper does not exist on the compiled type: the transform must co-emit it into // the override class so the changed Compute reaches it via call_script. Same body text // as CompiledHelper, so the compiled twin's output is the oracle. var editedSource = @" using Unity.Pipeline.CodeReload; namespace Unity.Pipeline.Tests.Editor { public class IlInterpreterNewMethodProbe { public int result; public int CompiledHelper(int x) { int acc = 0; for (int i = 1; i <= x; i++) acc += i * x - 1; return acc + x / 2; } [CodeReload] public void Compute() { result = LiveHelper(9); } int LiveHelper(int x) { int acc = 0; for (int i = 1; i <= x; i++) acc += i * x - 1; return acc + x / 2; } } }"; var path = Path.Combine(Application.temporaryCachePath, "IlInterpreterNewMethodProbe_edit.cs"); File.WriteAllText(path, editedSource); try { var response = CodeReloadCommands.ReloadFileEditorInterpreter(path); Assert.IsTrue(response.Success, $"reload with a live-added helper should succeed. Error: {response.ErrorDetails}"); var probe = new IlInterpreterNewMethodProbe(); probe.Compute(); Assert.AreEqual(probe.CompiledHelper(9), probe.result, "The live-added LiveHelper must produce the same output as its compiled twin"); } finally { if (File.Exists(path)) File.Delete(path); } } [Test] public void ReloadFileInterpreter_NewClickMethodWiredToCallback_Fires() { CodeReloadRegistry.RegisterReloadableMethod( typeof(IlInterpreterClickProbe).GetMethod(nameof(IlInterpreterClickProbe.Compute)), new CodeReloadAttribute()); // Click does not exist compiled; the reloaded body subscribes it as a method group. // The transform wraps it in a lambda over the co-emitted static, and the interpreter // creates a real Action that re-enters the VM when the host fires it. var editedSource = @" using Unity.Pipeline.CodeReload; namespace Unity.Pipeline.Tests.Editor { public class IlInterpreterClickProbe { public int clicks; public event System.Action Clicked; public void Fire() => Clicked?.Invoke(); [CodeReload] public void Compute() { clicks = 0; Clicked += Click; } void Click() { clicks = clicks + 1; } } }"; var path = Path.Combine(Application.temporaryCachePath, "IlInterpreterClickProbe_edit.cs"); File.WriteAllText(path, editedSource); try { var response = CodeReloadCommands.ReloadFileEditorInterpreter(path); Assert.IsTrue(response.Success, $"reload wiring a live-added Click method should succeed. Error: {response.ErrorDetails}"); var probe = new IlInterpreterClickProbe(); probe.Compute(); probe.Fire(); probe.Fire(); Assert.AreEqual(2, probe.clicks, "The live-added Click must run interpreted each time the host raises the event"); } finally { if (File.Exists(path)) File.Delete(path); } } [Test] public void ReloadFileInterpreter_NewTaggedMethod_DoesNotPoisonFile() { CodeReloadRegistry.RegisterReloadableMethod( typeof(IlInterpreterNewTaggedProbe).GetMethod(nameof(IlInterpreterNewTaggedProbe.Compute)), new CodeReloadAttribute()); // `Added` is a NEW [CodeReload] method, declared FIRST so it is names[0] at registration. // It has no woven prologue and can't register — but Compute's override must still apply. var editedSource = @" using Unity.Pipeline.CodeReload; namespace Unity.Pipeline.Tests.Editor { public class IlInterpreterNewTaggedProbe { public int result; [CodeReload] public void Added() { result = 1000; } [CodeReload] public void Compute() { result = 7; } } }"; var path = Path.Combine(Application.temporaryCachePath, "IlInterpreterNewTaggedProbe_edit.cs"); File.WriteAllText(path, editedSource); try { var response = CodeReloadCommands.ReloadFileEditorInterpreter(path); Assert.IsTrue(response.Success, $"reload should succeed with the new method skipped, not poison the file. Error: {response.ErrorDetails}"); var probe = new IlInterpreterNewTaggedProbe(); probe.Compute(); Assert.AreEqual(7, probe.result, "Compute's override must register even though the new 'Added' method can't"); } finally { if (File.Exists(path)) File.Delete(path); } } [Test] public void ReloadFileInterpreter_HostValueTypeArray_AppliesViaInterpreterDispatch() { CodeReloadRegistry.RegisterReloadableMethod( typeof(IlInterpreterNewobjReloadProbe).GetMethod(nameof(IlInterpreterNewobjReloadProbe.Compute)), new CodeReloadAttribute()); // NavMeshPath.corners is a Vector3[] — a host-returned VALUE-TYPE array (not the interpreter's // object?[]). Reading .Length exercises ldlen over a real typed array, which used to throw // InvalidCastException (the (object?[]) cast fails for value-type arrays). An empty path → 0. var editedSource = @" using Unity.Pipeline.CodeReload; namespace Unity.Pipeline.Tests.Editor { public class IlInterpreterNewobjReloadProbe { public int made = -1; [CodeReload] public void Compute() { var p = new UnityEngine.AI.NavMeshPath(); made = p.corners.Length; } } }"; var path = Path.Combine(Application.temporaryCachePath, "IlInterpreterVtArrayProbe_edit.cs"); File.WriteAllText(path, editedSource); try { var response = CodeReloadCommands.ReloadFileEditorInterpreter(path); Assert.IsTrue(response.Success, $"reload_file_editor_interpreter should succeed for a host Vector3[] read. Error: {response.ErrorDetails}"); var probe = new IlInterpreterNewobjReloadProbe(); probe.Compute(); Assert.AreEqual(0, probe.made, "ldlen over a host Vector3[] should run via the interpreter (empty NavMeshPath → 0 corners)"); } finally { if (File.Exists(path)) File.Delete(path); } } } }