Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ source on disk. When a run skips a method it had patched before, `Warnings` name
| Private/internal access inside an async/iterator/closure body has no accessor-delegate shape | Conditional access (`?.`), `??=`, indexers, static field writes, initializer member assignments, compound writes whose receiver could be evaluated twice, assignments whose value is consumed, and calls with `ref`/`out`/`in`, named, optional, or `params` arguments (or to extension/generic/by-ref-returning methods) cannot be rewritten to accessor delegates. Neither can ref-returning properties. These limits apply to compiled members; a `ref`/`out` method added in the same reload is reached directly. A compiled private/internal static property can be read, assigned, and compound-assigned |
| An async/iterator/closure body references a private/internal type | Accessor delegates rescue member access, not type references; the body still cannot JIT-compile from the shim assembly |
| A declared return or parameter type cannot be resolved (a new type this reload could not introduce, a missing using, or a typo) | Skipped; a supported new type declared in an edited file of the same assembly is introduced by this reload, so check `Warnings` for the refusal reason (`introduced-types.md`); otherwise add the type or the `using`, or fix the typo, then run `uloop compile` |
| An added member's body cannot be fully bound in the hot-reload compilation | Hot reload cannot verify a member it cannot bind. A common cause: another file of the same reload, passed or pulled back in because it holds active patches, declares a compiled type from source, while a compiled API the body calls still names the compiled copy (for example, a lambda handed to a compiled `Register(Action<T>)`). The reason then names both types and the file declaring the compiled API; pass that file to the same reload as well so both bind to the same type, or run `uloop compile` |
| Edited setter, init, or indexer accessor of a *compiled* property | Accessor patching covers getters only; `uloop compile` applies these edits. Accessors of a property added in this edit are emitted instead |
| Constructor (instance or static), operator, conversion operator, or explicit event accessor (add/remove) | Skipped; `uloop compile` applies these edits |
| Method raises or reads a field-like event that has no reachable backing field | Custom `add`/`remove` accessors, an `abstract`/`extern`/interface event, a delegate type that is not visible outside the assembly, or an event added in this edit leave nothing for the shim's Harmony accessor to bind |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ source on disk. When a run skips a method it had patched before, `Warnings` name
| Private/internal access inside an async/iterator/closure body has no accessor-delegate shape | Conditional access (`?.`), `??=`, indexers, static field writes, initializer member assignments, compound writes whose receiver could be evaluated twice, assignments whose value is consumed, and calls with `ref`/`out`/`in`, named, optional, or `params` arguments (or to extension/generic/by-ref-returning methods) cannot be rewritten to accessor delegates. Neither can ref-returning properties. These limits apply to compiled members; a `ref`/`out` method added in the same reload is reached directly. A compiled private/internal static property can be read, assigned, and compound-assigned |
| An async/iterator/closure body references a private/internal type | Accessor delegates rescue member access, not type references; the body still cannot JIT-compile from the shim assembly |
| A declared return or parameter type cannot be resolved (a new type this reload could not introduce, a missing using, or a typo) | Skipped; a supported new type declared in an edited file of the same assembly is introduced by this reload, so check `Warnings` for the refusal reason (`introduced-types.md`); otherwise add the type or the `using`, or fix the typo, then run `uloop compile` |
| An added member's body cannot be fully bound in the hot-reload compilation | Hot reload cannot verify a member it cannot bind. A common cause: another file of the same reload, passed or pulled back in because it holds active patches, declares a compiled type from source, while a compiled API the body calls still names the compiled copy (for example, a lambda handed to a compiled `Register(Action<T>)`). The reason then names both types and the file declaring the compiled API; pass that file to the same reload as well so both bind to the same type, or run `uloop compile` |
| Edited setter, init, or indexer accessor of a *compiled* property | Accessor patching covers getters only; `uloop compile` applies these edits. Accessors of a property added in this edit are emitted instead |
| Constructor (instance or static), operator, conversion operator, or explicit event accessor (add/remove) | Skipped; `uloop compile` applies these edits |
| Method raises or reads a field-like event that has no reachable backing field | Custom `add`/`remove` accessors, an `abstract`/`extern`/interface event, a delegate type that is not visible outside the assembly, or an event added in this edit leave nothing for the shim's Harmony accessor to bind |
Expand Down
141 changes: 141 additions & 0 deletions Assets/Tests/Editor/HotReload/HotReloadBindingSplitE2ETests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

using NUnit.Framework;

using UnityEngine;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;
using io.github.hatayama.UnityCliLoop.ToolContracts;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload
{
/// <summary>
/// End-to-end EditMode coverage for the skipped row of an added method that cannot bind
/// because a file the reload pulls in on its own declares a compiled type from source.
/// </summary>
public class HotReloadBindingSplitE2ETests
{
private const string HostFileName = "HotReloadBindingSplitHost.cs";
private const string PayloadFileName = "HotReloadBindingSplitPayload.cs";
private const string RegistryFileName = "HotReloadBindingSplitRegistry.cs";
private const string HostInsertionAnchor = " public int Handled => _handled;";
private const string WireMethod =
"\n\n public void Wire()\n {\n _registry.Register(p => Handle(p));\n }";
private const string PayloadScaledBody = "return Value * 2;";

private HotReloadDomainTestScope _scope;

[SetUp]
public void SetUp()
{
_scope = new HotReloadDomainTestScope();
HotReloadAutoRefreshHold.SyncToActiveChanges();
}

[TearDown]
public void TearDown()
{
_scope.Dispose();
HotReloadAutoRefreshHold.SyncToActiveChanges();
VibeLogger.ClearMemoryLogs();
}

/// <summary>
/// What: when only the host is passed but the payload file comes back as an active sibling,
/// the added method that cannot bind is skipped with a reason naming the file declaring the
/// compiled API, because the reader never listed the payload file and has no other way to
/// learn which file to pass.
/// </summary>
[Test]
public async Task Run_PayloadReappliedAsSibling_SkippedRowNamesTheFileDeclaringTheCompiledSignature()
{
string hostPath = FixturePath(HostFileName);
string payloadPath = FixturePath(PayloadFileName);
string payloadEditPath = HotReloadTestSourceWriter.WriteEditedSource(
"BindingSplitE2EPayload.cs",
ReplaceOnce(File.ReadAllText(payloadPath), PayloadScaledBody, "return Value * 3;"));

HotReloadOrchestratorResult first = await HotReloadCompositionRoot.Services.Orchestrator.RunAsync(
new[] { payloadPath },
contentPathOverride: null,
CancellationToken.None,
new Dictionary<string, string> { [payloadPath] = payloadEditPath });
Assert.That(
new HotReloadBindingSplitPayload { Value = 2 }.Scaled(),
Is.EqualTo(6),
"Precondition: the payload body must be patched.\n" + FormatOutcomes(first));

HotReloadOrchestratorResult second = await HotReloadCompositionRoot.Services.Orchestrator.RunAsync(
new[] { hostPath },
contentPathOverride: null,
CancellationToken.None,
new Dictionary<string, string>
{
[hostPath] = HotReloadTestSourceWriter.WriteEditedSource(
"BindingSplitE2EHost.cs",
ReplaceOnce(File.ReadAllText(hostPath), HostInsertionAnchor, HostInsertionAnchor + WireMethod)),
[payloadPath] = payloadEditPath
});

Assert.That(
second.ReappliedSiblingPaths,
Is.EquivalentTo(new[] { ProjectRelativePath(PayloadFileName) }),
FormatOutcomes(second));
HotReloadMethodOutcome skipped = FindSkippedWire(second);
Assert.That(skipped, Is.Not.Null, "Missing skipped row for Wire.\n" + FormatOutcomes(second));
Assert.That(
skipped.Reason,
Does.Contain("Pass '" + ProjectRelativePath(RegistryFileName) + "'"),
skipped.Reason);
}

private static HotReloadMethodOutcome FindSkippedWire(HotReloadOrchestratorResult result)
{
foreach (HotReloadMethodOutcome outcome in result.Methods)
{
if (outcome.Kind == HotReloadMethodOutcomeKind.Skipped
&& outcome.Method != null
&& outcome.Method.Contains(".Wire("))
{
return outcome;
}
}

return null;
}

private static string ReplaceOnce(string source, string anchor, string replacement)
{
Assert.That(source, Does.Contain(anchor), "Precondition: anchor must exist.");
return source.Replace(anchor, replacement);
}

private static string FixturePath(string fileName)
{
string path = Path.GetFullPath(
Path.Combine(Application.dataPath, "Tests", "Editor", "HotReload", fileName));
Assert.That(File.Exists(path), Is.True, "Fixture missing: " + path);
return path;
}

private static string ProjectRelativePath(string fileName)
{
return "Assets/Tests/Editor/HotReload/" + fileName;
}

private static string FormatOutcomes(HotReloadOrchestratorResult result)
{
List<string> lines = new List<string>();
foreach (HotReloadMethodOutcome outcome in result.Methods)
{
lines.Add(outcome.Kind + " " + outcome.Method + " @" + outcome.FilePath + " :: " + outcome.Reason);
}

lines.AddRange(result.Warnings ?? new List<string>());
return string.Join("\n", lines);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload
{
/// <summary>
/// Holds a compiled API as a nested type, so a skipped row that names the type declaring a
/// compiled signature has to find the file of a nested type.
/// </summary>
public static class HotReloadBindingSplitNestedRegistry
{
/// <summary>
/// A compiled API that takes a handler of the payload type, like the top-level registry.
/// </summary>
public sealed class Inner
{
private Action<HotReloadBindingSplitPayload> _handler;

public void Register(Action<HotReloadBindingSplitPayload> handler)
{
_handler = handler;
}

public void Raise(int value)
{
_handler?.Invoke(new HotReloadBindingSplitPayload { Value = value });
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Assets/Tests/Editor/HotReload/HotReloadBindingSplitPayload.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,12 @@ namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload
public sealed class HotReloadBindingSplitPayload
{
public int Value;

// A body a reload can patch, so this file stays active and comes back into a later reload
// of the assembly as a sibling.
public int Scaled()
{
return Value * 2;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload
{
/// <summary>
/// A compiled extension on the payload type, so a split can hide in the receiver of a reduced
/// extension call, which is not one of the call's parameters.
/// </summary>
public static class HotReloadBindingSplitPayloadExtensions
{
public static int Doubled(this HotReloadBindingSplitPayload payload)
{
return payload.Value * 2;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public void Register(Action<HotReloadBindingSplitPayload> handler)
_handler = handler;
}

public HotReloadBindingSplitPayload this[int value] => new HotReloadBindingSplitPayload { Value = value };

public void Raise(int value)
{
_handler?.Invoke(new HotReloadBindingSplitPayload { Value = value });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,14 @@ private static IEnumerable<TestCaseData> RenderCases()
"The added member's body could not be fully bound in the hot-reload compilation "
+ "(CS1503: Argument 1: cannot convert); hot reload cannot verify a member it cannot bind, "
+ "so it is skipped. Run 'uloop compile'.");
yield return Case(
HotReloadWorkerReasonCode.AddedMethodBodyBindsCompiledSignature,
new[] { "CS1503: Argument 1: cannot convert", "'Example.Payload'", "'Example.Registry'", "'Assets/Registry.cs'" },
"The added member's body could not be fully bound in the hot-reload compilation "
+ "(CS1503: Argument 1: cannot convert): this reload declares 'Example.Payload' from source, "
+ "while the compiled signatures of 'Example.Registry' still name the compiled 'Example.Payload', "
+ "so it is skipped. Pass 'Assets/Registry.cs' to this reload as well so both bind to the same type. "
+ "Otherwise run 'uloop compile'.");
yield return Case(
HotReloadWorkerReasonCode.AddedFieldStructHost,
NoArgs,
Expand Down
Loading
Loading