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 @@ -64,7 +64,7 @@ public async Task ShutdownForCompile_StopsTheRunningBackfill()

private async Task StopWith(Action hook)
{
_backfill.Start(new[] { "a.dll", "b.dll" }, "corr");
_backfill.Start(new[] { "a.dll", "b.dll" }, HotReloadCallSiteBackfillTestDoubles.FindNothingRefusedAgain, "corr");
Assert.That(
_backfill.State,
Is.EqualTo(HotReloadCallSiteBackfillState.Running),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,42 @@ private TaskCompletionSource<bool> GetOrCreate(string dllPath)
return source;
}
}

/// <summary>
/// A stand-in for the backfill's recompute of the refused dlls: it counts its calls and
/// answers each from a function of the call number (1-based) and the backfill's token.
/// </summary>
internal sealed class RefusedAgainFinder
{
private readonly Func<int, CancellationToken, IReadOnlyList<string>> _answer;

internal RefusedAgainFinder(Func<int, CancellationToken, IReadOnlyList<string>> answer)
{
_answer = answer;
}

internal int Calls { get; private set; }

/// <summary>Answers the given lists in order, then empty lists.</summary>
internal static RefusedAgainFinder Answering(params string[][] answers)
{
return new RefusedAgainFinder(
(call, ct) => call <= answers.Length ? answers[call - 1] : Array.Empty<string>());
}

internal Task<IReadOnlyList<string>> Find(CancellationToken ct)
{
Calls++;
return Task.FromResult(_answer(Calls, ct));
}
}

internal static class HotReloadCallSiteBackfillTestDoubles
{
/// <summary>A recompute that finds no refused dll, for tests that do not exercise it.</summary>
internal static Task<IReadOnlyList<string>> FindNothingRefusedAgain(CancellationToken ct)
{
return Task.FromResult<IReadOnlyList<string>>(Array.Empty<string>());
}
}
}
190 changes: 180 additions & 10 deletions Assets/Tests/Editor/HotReload/HotReloadCallSiteBackfillTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ public sealed class HotReloadCallSiteBackfillTests
{
private PendingDllLoader _loader;
private HotReloadCallSiteBackfill _backfill;
private RefusedAgainFinder _finder;

[SetUp]
public void SetUp()
{
_loader = new PendingDllLoader();
_backfill = new HotReloadCallSiteBackfill(_loader.Load);
_finder = RefusedAgainFinder.Answering();
VibeLogger.ClearMemoryLogs();
}

Expand All @@ -47,7 +49,7 @@ public void TearDown()
[Test]
public async Task Start_LoadsEachPathInOrder_AndLogsOneCompleteEntry()
{
_backfill.Start(new[] { "a.dll", "b.dll" }, "corr");
_backfill.Start(new[] { "a.dll", "b.dll" }, _finder.Find, "corr");
await _backfill.Completion;

Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll", "b.dll" }), "paths read");
Expand All @@ -60,34 +62,202 @@ public async Task Start_LoadsEachPathInOrder_AndLogsOneCompleteEntry()
}

/// <summary>
/// What: an empty list starts nothing and leaves no entry behind.
/// What: an empty list starts nothing, recomputes nothing, and leaves no entry behind.
/// </summary>
[Test]
public void Start_WithNoPaths_LoadsNothingAndLogsNothing()
public void Start_WithNoDlls_DoesNotRunOrRecomputeAndLogsNothing()
{
_backfill.Start(Array.Empty<string>(), "corr");
_backfill.Start(Array.Empty<string>(), _finder.Find, "corr");

Assert.That(_backfill.Completion.IsCompleted, Is.True, "completion");
Assert.That(_loader.Ran, Is.Empty, "paths read");
Assert.That(_finder.Calls, Is.EqualTo(0), "recomputes");
Assert.That(CountCompleteEntries(), Is.EqualTo(0), "complete entries");
}

/// <summary>
/// What: after the refused dlls are read, the refused dlls are recomputed once; an empty
/// answer ends the backfill and the entry reports one round.
/// </summary>
[Test]
public async Task Start_WhenRecomputeFindsNothingNew_StopsAfterOneRound()
{
_backfill.Start(new[] { "a.dll" }, _finder.Find, "corr");
await _backfill.Completion;

Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll" }), "paths read");
Assert.That(_finder.Calls, Is.EqualTo(1), "recomputes");
JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogCallerNoteBackfillComplete);
Assert.That((int)context["rounds"], Is.EqualTo(1), "rounds");
}

/// <summary>
/// What: a dll the recompute newly refuses (a caller one level further) is read too, after
/// the first ones, and the backfill recomputes again until nothing new is refused.
/// </summary>
[Test]
public async Task Start_WhenRecomputeRefusesANewDll_ReadsItToo()
{
_finder = RefusedAgainFinder.Answering(new[] { "b.dll" });

_backfill.Start(new[] { "a.dll" }, _finder.Find, "corr");
await _backfill.Completion;

Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll", "b.dll" }), "paths read");
JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogCallerNoteBackfillComplete);
Assert.That((int)context["rounds"], Is.EqualTo(2), "rounds");
Assert.That((int)context["requested"], Is.EqualTo(2), "requested");
Assert.That((int)context["loaded"], Is.EqualTo(2), "loaded");
}

/// <summary>
/// What: a dll that failed to read is refused again by the recompute but is not tried a
/// second time, so the backfill ends after one round.
/// </summary>
[Test]
public async Task Start_WhenRecomputeRefusesOnlyTriedDlls_DoesNotRetryThem()
{
_loader.ThrowFor.Add("a.dll");
LogAssert.Expect(LogType.Warning, new Regex("caller-note backfill"));
_finder = new RefusedAgainFinder((call, ct) => new[] { "a.dll" });

_backfill.Start(new[] { "a.dll" }, _finder.Find, "corr");
await _backfill.Completion;

Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll" }), "paths read");
Assert.That(_finder.Calls, Is.EqualTo(1), "recomputes");
JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogCallerNoteBackfillComplete);
Assert.That((int)context["requested"], Is.EqualTo(1), "requested");
Assert.That((int)context["failed"], Is.EqualTo(1), "failed");
}

/// <summary>
/// What: a recompute that keeps refusing new dlls is stopped after the caller-depth limit
/// of rounds. The stand-in throws on the round past the limit, so a missing limit fails
/// the test instead of looping forever.
/// </summary>
[Test]
public async Task Start_WhenRecomputeKeepsFindingNewDlls_StopsAtTheRoundLimit()
{
int limit = HotReloadOneShotCallerClosure.MaxCallerDepth;
_finder = new RefusedAgainFinder((call, ct) =>
{
if (call > limit)
{
throw new InvalidOperationException("test: recomputed past the round limit");
}

return new[] { "level" + call + ".dll" };
});

_backfill.Start(new[] { "a.dll" }, _finder.Find, "corr");
await _backfill.Completion;

Assert.That(_finder.Calls, Is.EqualTo(limit), "recomputes");
Assert.That(_loader.Ran.Count, Is.EqualTo(limit + 1), "paths read");
JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogCallerNoteBackfillComplete);
Assert.That((int)context["rounds"], Is.EqualTo(limit), "rounds");
}

/// <summary>
/// What: a cancel that lands while the last dll is read stops the backfill before the
/// recompute, so the run that cancelled waits for that dll only.
/// </summary>
[Test]
public async Task Start_WhenCancelledBeforeRecompute_DoesNotRecompute()
{
_loader.PendingFor.Add("a.dll");
_backfill.Start(new[] { "a.dll" }, _finder.Find, "corr");

Task yielded = _backfill.YieldToRunAsync();
_loader.Release("a.dll");
await yielded;

Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll" }), "paths read");
Assert.That(_finder.Calls, Is.EqualTo(0), "recomputes");
JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogCallerNoteBackfillComplete);
Assert.That((string)context["cancelledBy"], Is.EqualTo(HotReloadConstants.WarmUpCancelledByRun), "cancelledBy");
}

/// <summary>
/// What: a recompute cancelled from inside (it throws OperationCanceledException on the
/// backfill's token) ends the backfill quietly: the completion does not fault, an entry names
/// what stopped it, and the backfill is idle again.
/// </summary>
[Test]
public async Task Start_WhenRecomputeIsCancelled_StopsQuietly()
{
_finder = new RefusedAgainFinder((call, ct) =>
{
_backfill.Shutdown(HotReloadConstants.WarmUpShutdownTriggerCompilationStarted);
ct.ThrowIfCancellationRequested();
return new[] { "b.dll" };
});

_backfill.Start(new[] { "a.dll" }, _finder.Find, "corr");
await _backfill.Completion;

Assert.That(_finder.Calls, Is.EqualTo(1), "recomputes");
Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll" }), "paths read");
Assert.That(_backfill.Completion.IsFaulted, Is.False, "completion faulted");
Assert.That(_backfill.State, Is.EqualTo(HotReloadCallSiteBackfillState.Idle), "state");
JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogCallerNoteBackfillComplete);
Assert.That((string)context["cancelledBy"], Is.EqualTo("compilationStarted"), "cancelledBy");
}

/// <summary>
/// What: a recompute answer that repeats a dll or names one already read reads each new
/// dll once.
/// </summary>
[Test]
public async Task Start_WhenRecomputeReturnsDuplicates_ReadsEachNewDllOnce()
{
_finder = RefusedAgainFinder.Answering(new[] { "b.dll", "a.dll", "b.dll" });

_backfill.Start(new[] { "a.dll" }, _finder.Find, "corr");
await _backfill.Completion;

Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll", "b.dll" }), "paths read");
JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogCallerNoteBackfillComplete);
Assert.That((int)context["requested"], Is.EqualTo(2), "requested");
}

/// <summary>
/// What: an unexpected exception from the recompute ends the backfill, is logged, and
/// leaves the completion task unfaulted, the backfill idle, and no complete entry behind.
/// </summary>
[Test]
public async Task Start_WhenRecomputeThrowsAnUnexpectedException_LogsItAndCompletionDoesNotFault()
{
_finder = new RefusedAgainFinder((call, ct) => throw new IOException("test: recompute failed"));
LogAssert.Expect(LogType.Exception, new Regex("IOException: test: recompute failed"));

_backfill.Start(new[] { "a.dll" }, _finder.Find, "corr");
await _backfill.Completion;

Assert.That(_finder.Calls, Is.EqualTo(1), "recomputes");
Assert.That(_backfill.Completion.IsFaulted, Is.False, "completion faulted");
Assert.That(_backfill.State, Is.EqualTo(HotReloadCallSiteBackfillState.Idle), "state");
Assert.That(CountCompleteEntries(), Is.EqualTo(0), "complete entries");
}

/// <summary>
/// What: a run that yields while a dll is being read waits for that dll, and no later dll
/// is read; the entry names the run as what stopped the backfill.
/// is read nor recompute run; the entry names the run as what stopped the backfill.
/// </summary>
[Test]
public async Task YieldToRunAsync_WhileADllIsInFlight_WaitsForItAndLoadsNoLaterOne()
{
_loader.PendingFor.Add("a.dll");
_backfill.Start(new[] { "a.dll", "b.dll" }, "corr");
_backfill.Start(new[] { "a.dll", "b.dll" }, _finder.Find, "corr");

Task yielded = _backfill.YieldToRunAsync();
Assert.That(yielded.IsCompleted, Is.False, "the yield must wait while a.dll is being read");
_loader.Release("a.dll");
await yielded;

Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll" }), "paths read");
Assert.That(_finder.Calls, Is.EqualTo(0), "recomputes");
JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogCallerNoteBackfillComplete);
Assert.That((int)context["loaded"], Is.EqualTo(1), "loaded");
Assert.That((string)context["cancelledBy"], Is.EqualTo(HotReloadConstants.WarmUpCancelledByRun), "cancelledBy");
Expand Down Expand Up @@ -125,7 +295,7 @@ public async Task YieldToRunAsync_WhileTheInFlightDllFails_CompletesAndCountsTha
{
_loader.PendingFor.Add("a.dll");
LogAssert.Expect(LogType.Warning, new Regex("caller-note backfill"));
_backfill.Start(new[] { "a.dll", "b.dll" }, "corr");
_backfill.Start(new[] { "a.dll", "b.dll" }, _finder.Find, "corr");

Task yielded = _backfill.YieldToRunAsync();
_loader.Fail("a.dll", new IOException("test: cannot read a.dll"));
Expand All @@ -146,7 +316,7 @@ public async Task YieldToRunAsync_WhileTheInFlightDllFails_CompletesAndCountsTha
public async Task Shutdown_WhileADllIsInFlight_StopsBeforeTheNextOneAndRecordsTheTrigger()
{
_loader.PendingFor.Add("a.dll");
_backfill.Start(new[] { "a.dll", "b.dll" }, "corr");
_backfill.Start(new[] { "a.dll", "b.dll" }, _finder.Find, "corr");

Task stopped = _backfill.Shutdown(HotReloadConstants.WarmUpShutdownTriggerCompilationStarted);
_loader.Release("a.dll");
Expand All @@ -167,7 +337,7 @@ public async Task Start_WhenALoadThrowsAnIoException_CountsItFailedAndLoadsTheNe
_loader.ThrowFor.Add("a.dll");
LogAssert.Expect(LogType.Warning, new Regex("caller-note backfill"));

_backfill.Start(new[] { "a.dll", "b.dll" }, "corr");
_backfill.Start(new[] { "a.dll", "b.dll" }, _finder.Find, "corr");
await _backfill.Completion;

Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll", "b.dll" }), "paths read");
Expand All @@ -186,7 +356,7 @@ public async Task Start_WhenALoadThrowsAnUnexpectedException_LogsItAndCompletion
_loader.ThrowUnexpectedFor.Add("a.dll");
LogAssert.Expect(LogType.Exception, new Regex("InvalidOperationException: unexpected"));

_backfill.Start(new[] { "a.dll", "b.dll" }, "corr");
_backfill.Start(new[] { "a.dll", "b.dll" }, _finder.Find, "corr");
await _backfill.Completion;

Assert.That(_backfill.Completion.IsFaulted, Is.False, "completion faulted");
Expand Down
Loading
Loading