317 lines
14 KiB
C#
317 lines
14 KiB
C#
using System.Collections.Generic;
|
|
using System.IO;
|
|
using NUnit.Framework;
|
|
using Unity.Pipeline.CodeReload;
|
|
using Unity.Pipeline.Runtime.Commands;
|
|
using UnityEngine;
|
|
|
|
namespace Unity.Pipeline.Tests.Editor
|
|
{
|
|
/// <summary>
|
|
/// Probe for the baseline-aware in-place reload tests: two independent [CodeReload] methods so
|
|
/// a reload of one can be asserted to leave the other running compiled.
|
|
/// </summary>
|
|
public class BaselineReloadProbe
|
|
{
|
|
public int first;
|
|
public int second;
|
|
|
|
[CodeReload]
|
|
public void First()
|
|
{
|
|
first = 1; // original body
|
|
}
|
|
|
|
[CodeReload]
|
|
public void Second()
|
|
{
|
|
second = 1; // original body
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests for <see cref="CodeReloadBaseline"/> — the per-method snapshot that lets a reload skip
|
|
/// methods still matching the compiled code — and for its use by InPlaceReloadProcessor
|
|
/// (skip unchanged, unregister reverted).
|
|
/// </summary>
|
|
class CodeReloadBaselineTests
|
|
{
|
|
private const string FakePath = "Assets/FakeBaselineProbe.cs";
|
|
|
|
private const string BaselineSource = @"
|
|
using Unity.Pipeline.CodeReload;
|
|
namespace Unity.Pipeline.Tests.Editor
|
|
{
|
|
public class BaselineReloadProbe
|
|
{
|
|
public int first;
|
|
public int second;
|
|
|
|
[CodeReload]
|
|
public void First()
|
|
{
|
|
first = 1; // original body
|
|
}
|
|
|
|
[CodeReload]
|
|
public void Second()
|
|
{
|
|
second = 1; // original body
|
|
}
|
|
}
|
|
}";
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
CodeReloadBaseline.Clear();
|
|
CodeReloadRegistry.UnregisterMethodOverride("BaselineReloadProbe.First");
|
|
CodeReloadRegistry.UnregisterMethodOverride("BaselineReloadProbe.Second");
|
|
}
|
|
|
|
// -------- classification --------
|
|
|
|
[Test]
|
|
public void NoBaseline_ClassifiesNothing()
|
|
{
|
|
Assert.IsNull(CodeReloadBaseline.GetUnchangedMethods(FakePath, BaselineSource),
|
|
"Without a captured baseline every method must be treated as changed.");
|
|
Assert.IsFalse(CodeReloadBaseline.IsFileUpToDate(FakePath, BaselineSource));
|
|
}
|
|
|
|
[Test]
|
|
public void IdenticalSource_AllMethodsUnchanged()
|
|
{
|
|
Assert.IsTrue(CodeReloadBaseline.CaptureFromSource(FakePath, BaselineSource));
|
|
|
|
var unchanged = CodeReloadBaseline.GetUnchangedMethods(FakePath, BaselineSource);
|
|
Assert.IsNotNull(unchanged);
|
|
CollectionAssert.AreEquivalent(new[] { "First", "Second" }, unchanged);
|
|
Assert.IsTrue(CodeReloadBaseline.IsFileUpToDate(FakePath, BaselineSource));
|
|
}
|
|
|
|
[Test]
|
|
public void WhitespaceAndCommentEdits_DoNotCountAsChanges()
|
|
{
|
|
CodeReloadBaseline.CaptureFromSource(FakePath, BaselineSource);
|
|
|
|
var reformatted = BaselineSource
|
|
.Replace("first = 1; // original body", "first =\n 1; /* reflowed, comment changed */")
|
|
.Replace("public void Second()", "public void Second()");
|
|
|
|
var unchanged = CodeReloadBaseline.GetUnchangedMethods(FakePath, reformatted);
|
|
CollectionAssert.AreEquivalent(new[] { "First", "Second" }, unchanged);
|
|
Assert.IsTrue(CodeReloadBaseline.IsFileUpToDate(FakePath, reformatted));
|
|
}
|
|
|
|
[Test]
|
|
public void BodyEdit_MarksOnlyThatMethodChanged()
|
|
{
|
|
CodeReloadBaseline.CaptureFromSource(FakePath, BaselineSource);
|
|
|
|
var edited = BaselineSource.Replace("second = 1;", "second = 99;");
|
|
var unchanged = CodeReloadBaseline.GetUnchangedMethods(FakePath, edited);
|
|
|
|
CollectionAssert.AreEquivalent(new[] { "First" }, unchanged);
|
|
Assert.IsFalse(CodeReloadBaseline.IsFileUpToDate(FakePath, edited));
|
|
}
|
|
|
|
[Test]
|
|
public void SignatureEdit_MarksMethodChanged()
|
|
{
|
|
CodeReloadBaseline.CaptureFromSource(FakePath, BaselineSource);
|
|
|
|
// Renaming a parameterless method's parameter list is not possible; add a default arg.
|
|
var edited = BaselineSource.Replace("public void Second()", "public void Second(int times = 1)");
|
|
var unchanged = CodeReloadBaseline.GetUnchangedMethods(FakePath, edited);
|
|
|
|
// The signature change also changes the class context (the signature is context), so
|
|
// conservatively nothing is skippable.
|
|
CollectionAssert.DoesNotContain(unchanged, "Second");
|
|
}
|
|
|
|
[Test]
|
|
public void FieldEdit_ChangesContext_NothingSkippable()
|
|
{
|
|
CodeReloadBaseline.CaptureFromSource(FakePath, BaselineSource);
|
|
|
|
// An untouched body can read this field; the override compiles against the current
|
|
// file, so a declaration change conservatively invalidates every method.
|
|
var edited = BaselineSource.Replace("public int second;", "public int second = 5;");
|
|
var unchanged = CodeReloadBaseline.GetUnchangedMethods(FakePath, edited);
|
|
|
|
Assert.IsNotNull(unchanged);
|
|
Assert.AreEqual(0, unchanged.Count);
|
|
}
|
|
|
|
[Test]
|
|
public void HelperBodyEdit_DoesNotChangeContext()
|
|
{
|
|
var withHelper = BaselineSource.Replace(
|
|
"public int first;",
|
|
"public int first;\n private int Helper() { return 3; }\n private int Arrow => 4;");
|
|
CodeReloadBaseline.CaptureFromSource(FakePath, withHelper);
|
|
|
|
// Overrides call helpers through the compiled host either way, so a helper body edit
|
|
// can't change what an override does — it must not mark reloadable methods changed.
|
|
var edited = withHelper
|
|
.Replace("return 3;", "return 30;")
|
|
.Replace("Arrow => 4;", "Arrow => 40;");
|
|
var unchanged = CodeReloadBaseline.GetUnchangedMethods(FakePath, edited);
|
|
|
|
CollectionAssert.AreEquivalent(new[] { "First", "Second" }, unchanged);
|
|
}
|
|
|
|
[Test]
|
|
public void MustReloadMethods_BlockUpToDateShortCircuit()
|
|
{
|
|
CodeReloadBaseline.CaptureFromSource(FakePath, BaselineSource);
|
|
|
|
// A device already running an override for Second can't unregister it remotely — the
|
|
// file must not be reported up to date even though the source matches the baseline.
|
|
var pushed = new HashSet<string> { "Second" };
|
|
Assert.IsFalse(CodeReloadBaseline.IsFileUpToDate(FakePath, BaselineSource, null, pushed));
|
|
}
|
|
|
|
[Test]
|
|
public void SerializeRestore_RoundTripsClassification()
|
|
{
|
|
CodeReloadBaseline.CaptureFromSource(FakePath, BaselineSource);
|
|
var serialized = CodeReloadBaseline.Serialize();
|
|
|
|
CodeReloadBaseline.Clear();
|
|
Assert.IsNull(CodeReloadBaseline.GetUnchangedMethods(FakePath, BaselineSource));
|
|
|
|
CodeReloadBaseline.Restore(serialized);
|
|
var edited = BaselineSource.Replace("second = 1;", "second = 99;");
|
|
CollectionAssert.AreEquivalent(new[] { "First" },
|
|
CodeReloadBaseline.GetUnchangedMethods(FakePath, edited));
|
|
}
|
|
|
|
// -------- processor integration --------
|
|
|
|
[Test]
|
|
public void Reload_SkipsUnchangedMethods_AndUnregistersReverted()
|
|
{
|
|
CodeReloadRegistry.RegisterReloadableMethod(
|
|
typeof(BaselineReloadProbe).GetMethod(nameof(BaselineReloadProbe.First)),
|
|
new CodeReloadAttribute());
|
|
CodeReloadRegistry.RegisterReloadableMethod(
|
|
typeof(BaselineReloadProbe).GetMethod(nameof(BaselineReloadProbe.Second)),
|
|
new CodeReloadAttribute());
|
|
|
|
var path = Path.Combine(Application.temporaryCachePath, "BaselineReloadProbe_edit.cs");
|
|
try
|
|
{
|
|
// Watch start: disk matches the compiled probe.
|
|
CodeReloadBaseline.CaptureFromSource(path, BaselineSource);
|
|
|
|
// Save 1: untouched file → nothing compiles, nothing registers.
|
|
File.WriteAllText(path, BaselineSource);
|
|
var result = InPlaceReloadProcessor.ProcessSourceFileOnMainThread(path);
|
|
Assert.IsTrue(result.Success, result.ErrorMessage);
|
|
Assert.IsTrue(result.AllUpToDate);
|
|
CollectionAssert.IsEmpty(result.RegisteredMethods);
|
|
CollectionAssert.AreEquivalent(new[] { "First", "Second" }, result.UpToDateMethods);
|
|
Assert.IsFalse(CodeReloadRegistry.HasOverride("BaselineReloadProbe.Second"));
|
|
|
|
// Save 2: only Second edited → First stays compiled, Second gets an override.
|
|
File.WriteAllText(path, BaselineSource.Replace("second = 1;", "second = 99;"));
|
|
result = InPlaceReloadProcessor.ProcessSourceFileOnMainThread(path);
|
|
Assert.IsTrue(result.Success, result.ErrorMessage);
|
|
CollectionAssert.AreEquivalent(new[] { "BaselineReloadProbe.Second" }, result.RegisteredMethods);
|
|
CollectionAssert.AreEquivalent(new[] { "First" }, result.UpToDateMethods);
|
|
Assert.IsFalse(CodeReloadRegistry.HasOverride("BaselineReloadProbe.First"));
|
|
|
|
var probe = new BaselineReloadProbe();
|
|
probe.Second();
|
|
Assert.AreEqual(99, probe.second, "Edited body should run via the woven dispatch");
|
|
probe.First();
|
|
Assert.AreEqual(1, probe.first, "Unchanged body should run compiled");
|
|
|
|
// Save 3: the edit is undone → the stale override is removed, compiled body runs.
|
|
File.WriteAllText(path, BaselineSource);
|
|
result = InPlaceReloadProcessor.ProcessSourceFileOnMainThread(path);
|
|
Assert.IsTrue(result.Success, result.ErrorMessage);
|
|
Assert.IsTrue(result.AllUpToDate);
|
|
CollectionAssert.AreEquivalent(new[] { "BaselineReloadProbe.Second" }, result.RevertedMethods);
|
|
Assert.IsFalse(CodeReloadRegistry.HasOverride("BaselineReloadProbe.Second"));
|
|
|
|
var reverted = new BaselineReloadProbe();
|
|
reverted.Second();
|
|
Assert.AreEqual(1, reverted.second, "Reverted method should run the original compiled body");
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(path)) File.Delete(path);
|
|
}
|
|
}
|
|
|
|
[Test] // The reload_file command's contract for the baseline paths: an up-to-date file is
|
|
// an explicit "up to date" success (never "successful with 0 methods"), and stale
|
|
// overrides it reverts are reported, not silently unregistered.
|
|
public void ReloadFileCommand_UpToDate_ReportsUpToDateAndReverted()
|
|
{
|
|
CodeReloadRegistry.RegisterReloadableMethod(
|
|
typeof(BaselineReloadProbe).GetMethod(nameof(BaselineReloadProbe.First)),
|
|
new CodeReloadAttribute());
|
|
CodeReloadRegistry.RegisterReloadableMethod(
|
|
typeof(BaselineReloadProbe).GetMethod(nameof(BaselineReloadProbe.Second)),
|
|
new CodeReloadAttribute());
|
|
|
|
var path = Path.Combine(Application.temporaryCachePath, "BaselineReloadProbe_cmd.cs");
|
|
try
|
|
{
|
|
CodeReloadBaseline.CaptureFromSource(path, BaselineSource);
|
|
|
|
// Edit Second → a normal reload that registers one override.
|
|
File.WriteAllText(path, BaselineSource.Replace("second = 1;", "second = 99;"));
|
|
var edited = CodeReloadCommands.ReloadFile(path);
|
|
Assert.IsTrue(edited.Success, edited.Message);
|
|
StringAssert.Contains("1 methods", edited.Message);
|
|
|
|
// Undo the edit → explicit up-to-date success that names the reverted override.
|
|
File.WriteAllText(path, BaselineSource);
|
|
var upToDate = CodeReloadCommands.ReloadFile(path);
|
|
Assert.IsTrue(upToDate.Success, upToDate.Message);
|
|
StringAssert.Contains("Up to date", upToDate.Message);
|
|
StringAssert.Contains("reverted 1 stale override(s): BaselineReloadProbe.Second", upToDate.Message);
|
|
StringAssert.DoesNotContain("0 methods", upToDate.Message);
|
|
Assert.IsFalse(CodeReloadRegistry.HasOverride("BaselineReloadProbe.Second"));
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(path)) File.Delete(path);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Reload_WithoutBaseline_ReloadsEverything()
|
|
{
|
|
CodeReloadRegistry.RegisterReloadableMethod(
|
|
typeof(BaselineReloadProbe).GetMethod(nameof(BaselineReloadProbe.First)),
|
|
new CodeReloadAttribute());
|
|
CodeReloadRegistry.RegisterReloadableMethod(
|
|
typeof(BaselineReloadProbe).GetMethod(nameof(BaselineReloadProbe.Second)),
|
|
new CodeReloadAttribute());
|
|
|
|
var path = Path.Combine(Application.temporaryCachePath, "BaselineReloadProbe_nobaseline.cs");
|
|
try
|
|
{
|
|
// No baseline captured (no watch): pre-baseline behavior — every method reloads.
|
|
File.WriteAllText(path, BaselineSource);
|
|
var result = InPlaceReloadProcessor.ProcessSourceFileOnMainThread(path);
|
|
Assert.IsTrue(result.Success, result.ErrorMessage);
|
|
Assert.IsFalse(result.AllUpToDate);
|
|
CollectionAssert.AreEquivalent(
|
|
new[] { "BaselineReloadProbe.First", "BaselineReloadProbe.Second" },
|
|
result.RegisteredMethods);
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(path)) File.Delete(path);
|
|
}
|
|
}
|
|
}
|
|
}
|