Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public void Install_PausePointSide_CapturesThroughTheInstalledGateWithThePausePo
{
VibeLogger.ClearMemoryLogs();
int captureCount = 0;
HotReloadSourceSnapshotCapture capture = new HotReloadSourceSnapshotCapture(() =>
HotReloadSourceSnapshotCapture capture = HotReloadSourceSnapshotCaptureTestDoubles.CompletingSynchronously(() =>
{
captureCount++;
return true;
Expand Down Expand Up @@ -213,7 +213,7 @@ private static HotReloadServices CreateServicesSharing(HotReloadDomain domain)
TransformWorkerHost.Shared,
HotReloadGroupProcessorDependencies.CreateProduction,
new HotReloadApplicationPlayModeQuery(),
new HotReloadSourceSnapshotCapture(() => true),
HotReloadSourceSnapshotCaptureTestDoubles.CompletingSynchronously(() => true),
installed.WarmUp,
installed.CompiledCallers);
}
Expand All @@ -228,7 +228,7 @@ private static HotReloadServices CreateServicesOwning(HotReloadDomain domain)
TransformWorkerHost.Shared,
HotReloadGroupProcessorDependencies.CreateProduction,
new HotReloadApplicationPlayModeQuery(),
new HotReloadSourceSnapshotCapture(() => true),
HotReloadSourceSnapshotCaptureTestDoubles.CompletingSynchronously(() => true),
HotReloadWarmUpTestDoubles.CreateInert(),
HotReloadCompiledCallers.CreateProduction());
}
Expand Down
6 changes: 3 additions & 3 deletions Assets/Tests/Editor/HotReload/HotReloadDefaultFilesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ public async Task ExecuteAsync_WhenFilesAreOmitted_CapturesTheSourceSnapshotBefo
{
int captureCount = 0;
using IDisposable captureScope = HotReloadServicesTestScope.BeginWithSourceSnapshotCapture(
new HotReloadSourceSnapshotCapture(() => { captureCount++; return true; }));
HotReloadSourceSnapshotCaptureTestDoubles.CompletingSynchronously(() => { captureCount++; return true; }));
int captureCountAtDetection = -1;
using IDisposable detectorScope = HotReloadServicesTestScope.BeginWithChangeDetector(
new HotReloadStubChangeDetector(() =>
Expand Down Expand Up @@ -387,7 +387,7 @@ public async Task ExecuteAsync_WithFiles_CapturesTheSourceSnapshotBeforeTheRun()
{
int captureCount = 0;
using IDisposable captureScope = HotReloadServicesTestScope.BeginWithSourceSnapshotCapture(
new HotReloadSourceSnapshotCapture(() => { captureCount++; return true; }));
HotReloadSourceSnapshotCaptureTestDoubles.CompletingSynchronously(() => { captureCount++; return true; }));
int captureCountAtRun = -1;
using IDisposable orchestratorScope = HotReloadServicesTestScope.BeginWithOrchestrator(
new HotReloadStubOrchestrator((files, ignoredCt) =>
Expand All @@ -409,7 +409,7 @@ public async Task ExecuteAsync_ForStatus_DoesNotCaptureTheSourceSnapshot()
{
int captureCount = 0;
using IDisposable captureScope = HotReloadServicesTestScope.BeginWithSourceSnapshotCapture(
new HotReloadSourceSnapshotCapture(() => { captureCount++; return true; }));
HotReloadSourceSnapshotCaptureTestDoubles.CompletingSynchronously(() => { captureCount++; return true; }));

HotReloadResponse response = await ExecuteAsync(new JObject { ["Status"] = true });

Expand Down
2 changes: 1 addition & 1 deletion Assets/Tests/Editor/HotReload/HotReloadDomainTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1078,7 +1078,7 @@ public void GetVerifiedSnapshotSource_WhileCaptureRuns_ReturnsTheLiveFileWhenItM
/// makes the port wait for the capture once and read the snapshot instead.
/// </summary>
[Test]
public void GetVerifiedSnapshotSource_WhileCaptureRuns_WaitsWhenTheLiveFileDiffersFromThePdb()
public void GetVerifiedSnapshotSource_WhileCaptureRuns_WaitsWhenThePdbCannotConfirmTheLiveFile()
{
int ensureCount = 0;
HotReloadPausePointPort port = new HotReloadPausePointPort(
Expand Down
39 changes: 22 additions & 17 deletions Assets/Tests/Editor/HotReload/HotReloadEditorStartupTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ public class HotReloadEditorStartupTests
/// What: the capture before serving commands runs the installed capture once and records the domain-load trigger.
/// </summary>
[Test]
public void CaptureSourceSnapshotBeforeServingCommands_RunsTheInstalledCaptureOnceWithTheDomainLoadTrigger()
public void StartSourceSnapshotCaptureBeforeServingCommands_RunsTheInstalledCaptureOnceWithTheDomainLoadTrigger()
{
VibeLogger.ClearMemoryLogs();
int captureCount = 0;
HotReloadSourceSnapshotCapture capture = new HotReloadSourceSnapshotCapture(() =>
HotReloadSourceSnapshotCapture capture = HotReloadSourceSnapshotCaptureTestDoubles.CompletingSynchronously(() =>
{
captureCount++;
return true;
});

using (HotReloadServicesTestScope.BeginWithSourceSnapshotCapture(capture))
{
HotReloadEditorStartup.CaptureSourceSnapshotBeforeServingCommands(AllToolsEnabled);
HotReloadEditorStartup.StartSourceSnapshotCaptureBeforeServingCommands(AllToolsEnabled);
}

Assert.That(captureCount, Is.EqualTo(1), "captures");
Expand All @@ -41,15 +41,16 @@ public void CaptureSourceSnapshotBeforeServingCommands_RunsTheInstalledCaptureOn
}

/// <summary>
/// What: a capture that throws before serving commands passes the exception on and stays
/// undone, so the first update tick's capture runs it again.
/// What: a capture that fails before serving commands does not throw into the domain load,
/// which never waits for it; it logs one failed entry and stays undone, so the first update
/// tick's capture runs it again.
/// </summary>
[Test]
public void CaptureSourceSnapshotBeforeServingCommands_WhenTheCaptureThrows_ThrowsAndLeavesItForTheNextCall()
public void StartSourceSnapshotCaptureBeforeServingCommands_WhenTheCaptureFails_LogsFailedAndLeavesItForTheNextCall()
{
VibeLogger.ClearMemoryLogs();
int captureCount = 0;
HotReloadSourceSnapshotCapture capture = new HotReloadSourceSnapshotCapture(() =>
HotReloadSourceSnapshotCapture capture = HotReloadSourceSnapshotCaptureTestDoubles.CompletingSynchronously(() =>
{
captureCount++;
if (captureCount == 1)
Expand All @@ -62,14 +63,18 @@ public void CaptureSourceSnapshotBeforeServingCommands_WhenTheCaptureThrows_Thro

using (HotReloadServicesTestScope.BeginWithSourceSnapshotCapture(capture))
{
Assert.Throws<InvalidOperationException>(
() => HotReloadEditorStartup.CaptureSourceSnapshotBeforeServingCommands(AllToolsEnabled));
Assert.DoesNotThrow(
() => HotReloadEditorStartup.StartSourceSnapshotCaptureBeforeServingCommands(AllToolsEnabled));
JObject failed = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(
HotReloadConstants.VibeLogSourceSnapshotCaptureFailed);
Assert.That((string)failed["trigger"], Is.EqualTo(HotReloadConstants.SourceSnapshotCaptureTriggerDomainLoad));

HotReloadCompositionRoot.Services.SourceSnapshotCapture.EnsureCaptured(
HotReloadConstants.SourceSnapshotCaptureTriggerFirstUpdateTick);
HotReloadEditorStartup.CaptureAtStartupUnlessReadersAreDisabled(
HotReloadConstants.SourceSnapshotCaptureTriggerFirstUpdateTick, AllToolsEnabled);
}

Assert.That(captureCount, Is.EqualTo(2), "captures");
Assert.That(CountVibeEntries(HotReloadConstants.VibeLogSourceSnapshotCaptured), Is.EqualTo(1), "captured entries");
}

/// <summary>
Expand All @@ -78,23 +83,23 @@ public void CaptureSourceSnapshotBeforeServingCommands_WhenTheCaptureThrows_Thro
/// reader that runs later captures through the gate.
/// </summary>
[Test]
public void CaptureSourceSnapshotBeforeServingCommands_WhenNeitherReaderToolIsEnabled_SkipsAndLeavesTheCaptureForAReader()
public void StartSourceSnapshotCaptureBeforeServingCommands_WhenNeitherReaderToolIsEnabled_SkipsAndLeavesTheCaptureForAReader()
{
VibeLogger.ClearMemoryLogs();
int captureCount = 0;
HotReloadSourceSnapshotCapture capture = CountingCapture(() => captureCount++);

using (HotReloadServicesTestScope.BeginWithSourceSnapshotCapture(capture))
{
HotReloadEditorStartup.CaptureSourceSnapshotBeforeServingCommands(NoToolEnabled);
HotReloadEditorStartup.StartSourceSnapshotCaptureBeforeServingCommands(NoToolEnabled);

Assert.That(captureCount, Is.EqualTo(0), "captures while both readers are disabled");
JObject skipped = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogSourceSnapshotSkipped);
Assert.That((string)skipped["trigger"], Is.EqualTo(HotReloadConstants.SourceSnapshotCaptureTriggerDomainLoad));
Assert.That((string)skipped["reason"], Is.EqualTo(HotReloadConstants.SourceSnapshotSkipReasonToolsDisabled));
Assert.That(CountVibeEntries(HotReloadConstants.VibeLogSourceSnapshotCaptured), Is.EqualTo(0), "captured entries");

HotReloadCompositionRoot.Services.SourceSnapshotCapture.EnsureCaptured(
HotReloadCompositionRoot.Services.SourceSnapshotCapture.EnsureCapturedBlocking(
HotReloadConstants.SourceSnapshotCaptureTriggerApply);
}

Expand All @@ -107,15 +112,15 @@ public void CaptureSourceSnapshotBeforeServingCommands_WhenNeitherReaderToolIsEn
/// </summary>
[TestCase(UnityCliLoopConstants.TOOL_NAME_HOT_RELOAD)]
[TestCase(UnityCliLoopConstants.SETTINGS_TOOL_NAME_PAUSE_POINT)]
public void CaptureSourceSnapshotBeforeServingCommands_WhenOnlyOneReaderToolIsEnabled_Captures(string enabledTool)
public void StartSourceSnapshotCaptureBeforeServingCommands_WhenOnlyOneReaderToolIsEnabled_Captures(string enabledTool)
{
VibeLogger.ClearMemoryLogs();
int captureCount = 0;
HotReloadSourceSnapshotCapture capture = CountingCapture(() => captureCount++);

using (HotReloadServicesTestScope.BeginWithSourceSnapshotCapture(capture))
{
HotReloadEditorStartup.CaptureSourceSnapshotBeforeServingCommands(name => name == enabledTool);
HotReloadEditorStartup.StartSourceSnapshotCaptureBeforeServingCommands(name => name == enabledTool);
}

Assert.That(captureCount, Is.EqualTo(1), "captures");
Expand Down Expand Up @@ -215,7 +220,7 @@ public void StartWarmUpUnlessHotReloadIsDisabled_WhenHotReloadIsEnabled_StartsOn

private static HotReloadSourceSnapshotCapture CountingCapture(Action onCapture)
{
return new HotReloadSourceSnapshotCapture(() =>
return HotReloadSourceSnapshotCaptureTestDoubles.CompletingSynchronously(() =>
{
onCapture();
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ private static IDisposable BeginReplacementWith(IHotReloadHarmony harmony)
TransformWorkerHost.Shared,
HotReloadGroupProcessorDependencies.CreateProduction,
new HotReloadApplicationPlayModeQuery(),
new HotReloadSourceSnapshotCapture(() => true),
HotReloadSourceSnapshotCaptureTestDoubles.CompletingSynchronously(() => true),
HotReloadWarmUpTestDoubles.CreateInert(),
HotReloadCompiledCallers.CreateProduction()));
}
Expand Down
2 changes: 1 addition & 1 deletion Assets/Tests/Editor/HotReload/HotReloadPatcherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ private static IDisposable BeginReplacementWith(IHotReloadHarmony harmony)
TransformWorkerHost.Shared,
HotReloadGroupProcessorDependencies.CreateProduction,
new HotReloadApplicationPlayModeQuery(),
new HotReloadSourceSnapshotCapture(() => true),
HotReloadSourceSnapshotCaptureTestDoubles.CompletingSynchronously(() => true),
HotReloadWarmUpTestDoubles.CreateInert(),
HotReloadCompiledCallers.CreateProduction()));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using System.Threading.Tasks;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload
{
/// <summary>
/// Ways to build the source snapshot capture gate for tests, so no test runs a capture on a
/// pool thread.
/// </summary>
internal static class HotReloadSourceSnapshotCaptureTestDoubles
{
/// <summary>
/// A gate that runs <paramref name="capture"/> on the calling thread the moment it starts, so
/// the capture has finished when the call that started it returns.
/// </summary>
internal static HotReloadSourceSnapshotCapture CompletingSynchronously(Func<bool> capture)
{
return new HotReloadSourceSnapshotCapture(
FixedInputs,
(inputs, cancellationToken) => capture(),
RunNow,
() => 0);
}

/// <summary>Inputs that no test capture reads.</summary>
internal static HotReloadSnapshotCaptureInputs FixedInputs()
{
return new HotReloadSnapshotCaptureInputs(
"project-root",
HotReloadCompileStart.Unknown,
Array.Empty<HotReloadSnapshotPackageRoot>(),
StringComparison.Ordinal);
}

// Why the exception goes into the task: Task.Run does the same, and the gate must see a
// failed task rather than a throwing start.
private static Task<bool> RunNow(Func<bool> work)
{
try
{
return Task.FromResult(work());
}
catch (Exception exception)
{
return Task.FromException<bool>(exception);
}
}
}

/// <summary>
/// A stand-in for the pool thread that holds the started work until the test runs it, so a
/// test controls when a capture is in flight and when it finishes.
/// </summary>
internal sealed class HeldBackgroundRun
{
private Func<bool> _work;
private TaskCompletionSource<bool> _completion;

internal int StartCount { get; private set; }

internal Task<bool> Run(Func<bool> work)
{
StartCount++;
_work = work;
_completion = new TaskCompletionSource<bool>();
return _completion.Task;
}

/// <summary>
/// Runs the held work on the calling thread and finishes its task with the outcome, the way
/// Task.Run would: a thrown exception, cancellation included, faults the task.
/// </summary>
internal void RunHeldWork()
{
NUnit.Framework.Assert.That(_work, NUnit.Framework.Is.Not.Null, "no work was started");
Func<bool> work = _work;
TaskCompletionSource<bool> completion = _completion;
_work = null;
_completion = null;
bool result;
try
{
result = work();
}
catch (Exception exception)
{
completion.SetException(exception);
return;
}

completion.SetResult(result);
}
}
}

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

Loading
Loading