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
1 change: 1 addition & 0 deletions .agents/skills/uloop-hot-reload/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ Returns JSON with:
- `AddedFields` (array): source-level names ("Type.field") of fields this reload added; their values live outside the compiled type until 'uloop compile'. Every run that adds fields also carries one warning stating that the values live outside the compiled assembly and last only until the next 'uloop compile' or domain reload; the warning names exactly the fields listed in AddedFields. Pause-point `CapturedVariables` never includes these fields; `enable-pause-point` warns when the resolved type has any.
- `UnchangedTotal` (number): Methods left untouched because their bodies match the source baseline from the last compile; `0` when no baseline was available
- `ActivePatchTotal` (number): Active changes after this run — patched methods plus added members. `--revert-all` clears both and reports the combined count in `ClearedCount`
- `DroppedByPlayModeEntryCount` (number): Remaining method identities discarded by the Play-entry domain reload that have not been recovered by a later apply (`Patched` / `Added`), `--revert-all`, or a successful compile. Omitted when the count is 0. Re-apply `uloop hot-reload`, or edit the files and run `uloop compile`
- `ClearedCount` (number): Patches removed by `--revert-all` (0 on apply)
- `Message` (string): Short summary. On `--status`, when any `Active` row has `InvocationCount` 0, Message also appends how many such rows there are and points at `Methods[].Reason`; added-member rows are not included in that count
- `RecommendedNextAction` (string): Present when any method outcome is `Failed`. A partial apply (some methods patched or added) says to fix and rerun, run `uloop compile`, or `uloop hot-reload --revert-all`; a failure with nothing applied says to fix and rerun or compile. Omitted on success.
1 change: 1 addition & 0 deletions .claude/skills/uloop-hot-reload/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ Returns JSON with:
- `AddedFields` (array): source-level names ("Type.field") of fields this reload added; their values live outside the compiled type until 'uloop compile'. Every run that adds fields also carries one warning stating that the values live outside the compiled assembly and last only until the next 'uloop compile' or domain reload; the warning names exactly the fields listed in AddedFields. Pause-point `CapturedVariables` never includes these fields; `enable-pause-point` warns when the resolved type has any.
- `UnchangedTotal` (number): Methods left untouched because their bodies match the source baseline from the last compile; `0` when no baseline was available
- `ActivePatchTotal` (number): Active changes after this run — patched methods plus added members. `--revert-all` clears both and reports the combined count in `ClearedCount`
- `DroppedByPlayModeEntryCount` (number): Remaining method identities discarded by the Play-entry domain reload that have not been recovered by a later apply (`Patched` / `Added`), `--revert-all`, or a successful compile. Omitted when the count is 0. Re-apply `uloop hot-reload`, or edit the files and run `uloop compile`
- `ClearedCount` (number): Patches removed by `--revert-all` (0 on apply)
- `Message` (string): Short summary. On `--status`, when any `Active` row has `InvocationCount` 0, Message also appends how many such rows there are and points at `Methods[].Reason`; added-member rows are not included in that count
- `RecommendedNextAction` (string): Present when any method outcome is `Failed`. A partial apply (some methods patched or added) says to fix and rerun, run `uloop compile`, or `uloop hot-reload --revert-all`; a failure with nothing applied says to fix and rerun or compile. Omitted on success.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using UnityEditor;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload
{
/// <summary>
/// Snapshots the live Editor Play-entry drop ledger so tests can restore it after
/// mutating the production SessionState key.
/// </summary>
internal sealed class HotReloadPlayModeEntryDropLedgerSessionScope
{
private readonly string _capturedRaw;

public HotReloadPlayModeEntryDropLedgerSessionScope()
{
_capturedRaw = SessionState.GetString(
HotReloadConstants.PlayModeEntryDropSessionStateKey,
string.Empty);
HotReloadPlayModeEntryDropLedger.Clear();
HotReloadPlayModeEntryDropRecorder.ResetPendingForTesting();
}

public void Restore()
{
HotReloadPlayModeEntryDropRecorder.ResetPendingForTesting();
SessionState.SetString(
HotReloadConstants.PlayModeEntryDropSessionStateKey,
_capturedRaw);
}
}
}

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,80 @@
using System.Collections.Generic;
using NUnit.Framework;

using UnityEditor;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload
{
/// <summary>
/// Verifies Play-entry drop identities are stored, unioned, removed, and cleared.
/// </summary>
[TestFixture]
public sealed class HotReloadPlayModeEntryDropLedgerTests
{
private HotReloadPlayModeEntryDropLedgerSessionScope _ledgerSessionScope;

[SetUp]
public void SetUp()
{
_ledgerSessionScope = new HotReloadPlayModeEntryDropLedgerSessionScope();
}

[TearDown]
public void TearDown()
{
_ledgerSessionScope.Restore();
}

/// <summary>
/// What: Record unions identities and GetIdentities returns them in ordinal order.
/// </summary>
[Test]
public void Record_WhenCalledTwice_UnionsIdentitiesWithoutDuplicates()
{
HotReloadPlayModeEntryDropLedger.Record(new[] { "Type.B()", "Type.A()" });
HotReloadPlayModeEntryDropLedger.Record(new[] { "Type.A()", "Type.C()" });

IReadOnlyList<string> identities = HotReloadPlayModeEntryDropLedger.GetIdentities();

Assert.That(identities, Is.EqualTo(new[] { "Type.A()", "Type.B()", "Type.C()" }));
Assert.That(HotReloadPlayModeEntryDropLedger.Count, Is.EqualTo(3));
Assert.That(
SessionState.GetString(HotReloadConstants.PlayModeEntryDropSessionStateKey, string.Empty),
Is.EqualTo("Type.A()\nType.B()\nType.C()"));
}

/// <summary>
/// What: Remove deletes only the named identities and leaves the rest.
/// </summary>
[Test]
public void Remove_WhenSomeIdentitiesMatch_LeavesTheRest()
{
HotReloadPlayModeEntryDropLedger.Record(new[] { "Type.A()", "Type.B()", "Type.C()" });

HotReloadPlayModeEntryDropLedger.Remove(new[] { "Type.B()", "Type.Missing()" });

Assert.That(
HotReloadPlayModeEntryDropLedger.GetIdentities(),
Is.EqualTo(new[] { "Type.A()", "Type.C()" }));
}

/// <summary>
/// What: Clear empties the SessionState record.
/// </summary>
[Test]
public void Clear_WhenIdentitiesExist_RemovesAll()
{
HotReloadPlayModeEntryDropLedger.Record(new[] { "Type.A()" });

HotReloadPlayModeEntryDropLedger.Clear();

Assert.That(HotReloadPlayModeEntryDropLedger.Count, Is.EqualTo(0));
Assert.That(HotReloadPlayModeEntryDropLedger.GetIdentities(), Is.Empty);
Assert.That(
SessionState.GetString(HotReloadConstants.PlayModeEntryDropSessionStateKey, string.Empty),
Is.EqualTo(string.Empty));
}
}
}

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,155 @@
using System.Collections.Generic;
using NUnit.Framework;

using UnityEditor;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload
{
/// <summary>
/// Verifies Play-entry drop record/clear decisions without driving Editor events.
/// </summary>
[TestFixture]
public sealed class HotReloadPlayModeEntryDropRecorderTests
{
private HotReloadPlayModeEntryDropLedgerSessionScope _ledgerSessionScope;

[SetUp]
public void SetUp()
{
_ledgerSessionScope = new HotReloadPlayModeEntryDropLedgerSessionScope();
}

[TearDown]
public void TearDown()
{
_ledgerSessionScope.Restore();
}

/// <summary>
/// What: only ExitingEditMode with domain reload and at least one identity records.
/// </summary>
[Test]
public void ShouldRecord_RequiresExitingEditModeEnabledReloadAndIdentities()
{
Assert.That(
HotReloadPlayModeEntryDropRecorder.ShouldRecord(
PlayModeStateChange.ExitingEditMode,
isDomainReloadDisabledOnEnterPlayMode: false,
activeIdentityCount: 2),
Is.True);
Assert.That(
HotReloadPlayModeEntryDropRecorder.ShouldRecord(
PlayModeStateChange.EnteredPlayMode,
isDomainReloadDisabledOnEnterPlayMode: false,
activeIdentityCount: 2),
Is.False);
Assert.That(
HotReloadPlayModeEntryDropRecorder.ShouldRecord(
PlayModeStateChange.ExitingEditMode,
isDomainReloadDisabledOnEnterPlayMode: true,
activeIdentityCount: 2),
Is.False);
Assert.That(
HotReloadPlayModeEntryDropRecorder.ShouldRecord(
PlayModeStateChange.ExitingEditMode,
isDomainReloadDisabledOnEnterPlayMode: false,
activeIdentityCount: 0),
Is.False);
}

/// <summary>
/// What: a failed compile keeps the leftover identities.
/// </summary>
[Test]
public void NotifyCompilationFinished_WhenErrorCountIsPositive_KeepsIdentities()
{
HotReloadPlayModeEntryDropLedger.Record(new[] { "Type.A()", "Type.B()" });

HotReloadPlayModeEntryDropRecorder.NotifyCompilationFinished(1);

Assert.That(
HotReloadPlayModeEntryDropLedger.GetIdentities(),
Is.EqualTo(new[] { "Type.A()", "Type.B()" }));
}

/// <summary>
/// What: a successful compile clears every leftover identity.
/// </summary>
[Test]
public void NotifyCompilationFinished_WhenErrorCountIsZero_ClearsIdentities()
{
HotReloadPlayModeEntryDropLedger.Record(new[] { "Type.A()" });

HotReloadPlayModeEntryDropRecorder.NotifyCompilationFinished(0);

Assert.That(HotReloadPlayModeEntryDropLedger.Count, Is.EqualTo(0));
}

/// <summary>
/// What: apply removes only Patched and Added identities from the leftover set.
/// </summary>
[Test]
public void NotifyApplyRecovered_RemovesOnlyPatchedAndAddedIdentities()
{
HotReloadPlayModeEntryDropLedger.Record(
new[] { "Type.Patched()", "Type.Added()", "Type.Failed()", "Type.Skipped()" });

HotReloadPlayModeEntryDropRecorder.NotifyApplyRecovered(
new List<HotReloadMethodOutcome>
{
HotReloadMethodOutcome.Patched("Type.Patched()", "Assets/A.cs"),
HotReloadMethodOutcome.Added("Type.Added()", "Assets/A.cs"),
HotReloadMethodOutcome.Failed("Type.Failed()", "reason", "Assets/A.cs"),
HotReloadMethodOutcome.Skipped("Type.Skipped()", "reason", "Assets/A.cs")
});

Assert.That(
HotReloadPlayModeEntryDropLedger.GetIdentities(),
Is.EqualTo(new[] { "Type.Failed()", "Type.Skipped()" }));
}

/// <summary>
/// What: revert-all clears every leftover identity.
/// </summary>
[Test]
public void NotifyRevertAll_ClearsIdentities()
{
HotReloadPlayModeEntryDropLedger.Record(new[] { "Type.A()" });

HotReloadPlayModeEntryDropRecorder.NotifyRevertAll();

Assert.That(HotReloadPlayModeEntryDropLedger.Count, Is.EqualTo(0));
}

/// <summary>
/// What: a same-domain EnteredEditMode after ExitingEditMode means Play entry
/// was cancelled, so the just-recorded identities leave the ledger and older
/// leftovers stay.
/// </summary>
[Test]
public void NotifyPlayModeStateChanged_WhenEnteredEditModeFollowsExitingEditModeInSameDomain_RemovesOnlyPendingIdentities()
{
HotReloadPlayModeEntryDropLedger.Record(new[] { "Type.Old()" });

HotReloadPlayModeEntryDropRecorder.NotifyPlayModeStateChanged(
PlayModeStateChange.ExitingEditMode,
new[] { "Type.Active()" },
isDomainReloadDisabledOnEnterPlayMode: false);

Assert.That(
HotReloadPlayModeEntryDropLedger.GetIdentities(),
Is.EqualTo(new[] { "Type.Active()", "Type.Old()" }));

HotReloadPlayModeEntryDropRecorder.NotifyPlayModeStateChanged(
PlayModeStateChange.EnteredEditMode,
new[] { "Type.Active()" },
isDomainReloadDisabledOnEnterPlayMode: false);

Assert.That(
HotReloadPlayModeEntryDropLedger.GetIdentities(),
Is.EqualTo(new[] { "Type.Old()" }));
}
}
}

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,50 @@
using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload
{
/// <summary>
/// Verifies the --status drop Message is emitted only when no changes are active
/// and discarded identities remain.
/// </summary>
[TestFixture]
public sealed class HotReloadPlayModeEntryDropStatusMessageBuilderTests
{
/// <summary>
/// What: zero active changes with leftover identities uses the Play-entry discard wording.
/// </summary>
[Test]
public void Build_WhenActiveCountIsZeroAndDroppedCountIsPositive_ReturnsExactDropMessage()
{
string message = HotReloadPlayModeEntryDropStatusMessageBuilder.Build(0, 2);

Assert.That(
message,
Is.EqualTo(
"0 change(s) currently active. 2 change(s) were discarded by the domain reload when Play Mode was entered — hot-reloaded edits that were never compiled are not in effect. Re-apply 'uloop hot-reload', or edit the files and run 'uloop compile'."));
}

/// <summary>
/// What: active changes keep the existing --status Message path (no drop sentence).
/// </summary>
[Test]
public void Build_WhenActiveCountIsPositive_ReturnsNull()
{
string message = HotReloadPlayModeEntryDropStatusMessageBuilder.Build(1, 2);

Assert.That(message, Is.Null);
}

/// <summary>
/// What: no leftover identities keep the existing --status Message path.
/// </summary>
[Test]
public void Build_WhenDroppedCountIsZero_ReturnsNull()
{
string message = HotReloadPlayModeEntryDropStatusMessageBuilder.Build(0, 0);

Assert.That(message, Is.Null);
}
}
}

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

Loading
Loading