Skip to content
Merged
31 changes: 31 additions & 0 deletions Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTestRuns.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Threading.Tasks;

using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload
{
/// <summary>
/// Compiled-caller runs for tests that build an apply context by hand, outside any hot reload run.
/// </summary>
internal static class HotReloadCompiledCallersTestRuns
{
/// <summary>
/// Opens a run on compiled callers of its own: a fresh cache and a backfill that reads
/// nothing. Its hold is taken on that fresh cache, never on the shared one, so it keeps no
/// shared entry alive and needs no Dispose; the caller scans it does still read the shared
/// cache without a hold, as gate tests did before scans moved onto the run. The backfill is
/// idle, so the run begins synchronously and is read without blocking.
/// </summary>
internal static HotReloadCompiledCallersRun OpenIsolated()
{
HotReloadCompiledCallers compiledCallers = new HotReloadCompiledCallers(
new HotReloadCompiledCallSiteCache(HotReloadCompiledCallSiteCache.DefaultBudgetBytes),
new HotReloadCallSiteBackfill((dllPath, ct) => Task.CompletedTask));
Task<HotReloadCompiledCallersRun> begin = compiledCallers.BeginRunAsync();
Assert.That(begin.IsCompleted, Is.True, "Precondition: a run on an idle backfill begins synchronously.");
return begin.Result;
}
}
}

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

35 changes: 35 additions & 0 deletions Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

using NUnit.Framework;

using UnityEngine;
using UnityEngine.TestTools;

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

Expand Down Expand Up @@ -104,5 +109,35 @@ public async Task Dispose_WithoutEnd_ReleasesTheHoldAndStartsNoBackgroundRead()
Assert.That(_loader.Ran, Is.Empty, "no background read starts");
Assert.That(_backfill.State, Is.EqualTo(HotReloadCallSiteBackfillState.Idle));
}

/// <summary>
/// What: a caller scan asked of a run that already ended is reported, since its reads are
/// no longer held against eviction.
/// </summary>
[Test]
public async Task FindCallSites_AfterTheRunEnded_Asserts()
{
HotReloadCompiledCallersRun run = await _compiledCallers.BeginRunAsync();
run.End("corr");
LogAssert.Expect(LogType.Assert, "FindCallSites is called only while the run is open.");

run.FindCallSites(Path.GetFullPath(Path.Combine(Application.dataPath, "..")), Array.Empty<HotReloadCompiledMethodIdentity>());

run.Dispose();
}

/// <summary>
/// What: a caller scan asked of a run that was disposed without End (a run that threw) is
/// reported too, since its hold is released as well.
/// </summary>
[Test]
public async Task FindCallSites_AfterTheRunWasDisposedWithoutEnd_Asserts()
{
HotReloadCompiledCallersRun run = await _compiledCallers.BeginRunAsync();
run.Dispose();
LogAssert.Expect(LogType.Assert, "FindCallSites is called only while the run is open.");

run.FindCallSites(Path.GetFullPath(Path.Combine(Application.dataPath, "..")), Array.Empty<HotReloadCompiledMethodIdentity>());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,16 @@ private async Task<IReadOnlyList<HotReloadFileProcessResult>> RunGroupAsync(
collaborators, context, compileResult, entriesToPatch),
collaborators.EntryApplier.ApplyPreparedEntries)))
{
return await HotReloadCompositionRoot.Services.GroupProcessor.ProcessGroupAsync(
files,
"leave-out-test",
new HotReloadRunTiming(),
CancellationToken.None);
using (HotReloadCompiledCallersRun compiledCallersRun =
await HotReloadCompositionRoot.Services.CompiledCallers.BeginRunAsync())
{
return await HotReloadCompositionRoot.Services.GroupProcessor.ProcessGroupAsync(
files,
compiledCallersRun,
"leave-out-test",
new HotReloadRunTiming(),
CancellationToken.None);
}
}
}

Expand Down
40 changes: 26 additions & 14 deletions Assets/Tests/Editor/HotReload/HotReloadGroupProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -639,11 +639,16 @@ public async Task ProcessGroupAsync_WhenTheWorkerFails_BuildsUnappliedResultsWit
collaborators, context, compileResult, entriesToPatch),
collaborators.EntryApplier.ApplyPreparedEntries)))
{
results = await HotReloadCompositionRoot.Services.GroupProcessor.ProcessGroupAsync(
new[] { file },
"worker-failure-test",
new HotReloadRunTiming(),
CancellationToken.None);
using (HotReloadCompiledCallersRun compiledCallersRun =
await HotReloadCompositionRoot.Services.CompiledCallers.BeginRunAsync())
{
results = await HotReloadCompositionRoot.Services.GroupProcessor.ProcessGroupAsync(
new[] { file },
compiledCallersRun,
"worker-failure-test",
new HotReloadRunTiming(),
CancellationToken.None);
}
}

Assert.That(results, Has.Count.EqualTo(1));
Expand Down Expand Up @@ -749,13 +754,18 @@ private static async Task<HotReloadTimingBreakdown> RunGroupWithPhaseStubsAsync(
{
try
{
total.Start();
await HotReloadCompositionRoot.Services.GroupProcessor.ProcessGroupAsync(
new[] { stubs.GroupFile },
"phase-timing-test",
timing,
CancellationToken.None);
total.Stop();
using (HotReloadCompiledCallersRun compiledCallersRun =
await HotReloadCompositionRoot.Services.CompiledCallers.BeginRunAsync())
{
total.Start();
await HotReloadCompositionRoot.Services.GroupProcessor.ProcessGroupAsync(
new[] { stubs.GroupFile },
compiledCallersRun,
"phase-timing-test",
timing,
CancellationToken.None);
total.Stop();
}
}
catch (OperationCanceledException exception)
{
Expand Down Expand Up @@ -1821,7 +1831,8 @@ private static HotReloadApplyContext CreateContext(
workerOutput,
new[] { callerFile, targetFile },
null,
new HotReloadCompileFailureNoteSources(workerOutput.skipped, Array.Empty<HotReloadRefusedIntroducedType>()));
new HotReloadCompileFailureNoteSources(workerOutput.skipped, Array.Empty<HotReloadRefusedIntroducedType>()),
HotReloadCompiledCallersTestRuns.OpenIsolated());
}

private static HotReloadApplyContext CreateEmptyEntriesContext(
Expand All @@ -1848,7 +1859,8 @@ private static HotReloadApplyContext CreateEmptyEntriesContext(
emptyWorkerOutput,
context.Files,
null,
new HotReloadCompileFailureNoteSources(emptyWorkerOutput.skipped, Array.Empty<HotReloadRefusedIntroducedType>()));
new HotReloadCompileFailureNoteSources(emptyWorkerOutput.skipped, Array.Empty<HotReloadRefusedIntroducedType>()),
context.CompiledCallersRun);
}

private static Assembly FindCompilationAssembly()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,7 @@ private protected static HotReloadGroupStageCollaborators WithInternalAccessGran
collaborators.EditorStateSnapshotCapture,
collaborators.CommitPolicy,
collaborators.PlayMode,
internalAccessGrant,
collaborators.CompiledCallers);
internalAccessGrant);
}

private protected static Task<HotReloadOrchestratorResult> RunReloadAsync(
Expand Down
3 changes: 2 additions & 1 deletion Assets/Tests/Editor/HotReload/HotReloadOrchestratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3710,7 +3710,8 @@ private static HotReloadApplyContext CreateApplyContext(
workerOutput,
new[] { file },
null,
new HotReloadCompileFailureNoteSources(workerOutput.skipped, Array.Empty<HotReloadRefusedIntroducedType>()));
new HotReloadCompileFailureNoteSources(workerOutput.skipped, Array.Empty<HotReloadRefusedIntroducedType>()),
HotReloadCompiledCallersTestRuns.OpenIsolated());
}

private static HotReloadOrchestratorResult ToOrchestratorResult(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,6 @@ public static HotReloadCompiledCallers CreateProduction()
return new HotReloadCompiledCallers(cache, backfill);
}

/// <summary>
/// Finds the compiled call and ldftn sites that reference any of <paramref name="targets"/>,
/// reading every compiled assembly it needs. Call it only while a run begun with
/// <see cref="BeginRunAsync"/> is open: outside one, a read on another thread may evict and
/// dispose a cached assembly this is still reading.
/// </summary>
public List<HotReloadCallSiteHit> FindCallSites(string projectRoot, HotReloadCompiledMethodIdentity[] targets)
{
return HotReloadCallSiteScanner.FindCallSites(projectRoot, targets).Hits;
}

/// <summary>The compiled dlls of the assemblies that reference <paramref name="assemblyName"/>.</summary>
public IReadOnlyList<string> CollectReferencingDllPaths(string projectRoot, string assemblyName)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public sealed class HotReloadCompiledCallersRun : IDisposable
private IReadOnlyList<HotReloadOneShotCallerNoteRequest> _requests;
private bool _notesBuilt;
private bool _ended;
private bool _disposed;

internal HotReloadCompiledCallersRun(
IDisposable hold,
Expand All @@ -38,6 +39,18 @@ internal HotReloadCompiledCallersRun(
_backfill = backfill;
}

/// <summary>
/// Finds the compiled call and ldftn sites that reference any of <paramref name="targets"/>,
/// reading every compiled assembly it needs. Call before the run ends: the assemblies it
/// reads stay cached only while the run is open, so another thread cannot evict and
/// dispose one this is still reading.
/// </summary>
public List<HotReloadCallSiteHit> FindCallSites(string projectRoot, HotReloadCompiledMethodIdentity[] targets)
{
Debug.Assert(!_ended && !_disposed, "FindCallSites is called only while the run is open.");
return HotReloadCallSiteScanner.FindCallSites(projectRoot, targets).Hits;
}

/// <summary>
/// Builds the one-shot lifecycle note of each request: one entry per request, in the same
/// order, null where no note can be proven. Reads at most the configured number of compiled
Expand Down Expand Up @@ -105,6 +118,7 @@ public void End(string correlationId)
/// </summary>
public void Dispose()
{
_disposed = true;
_hold.Dispose();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ internal HotReloadApplyContext(
TransformWorkerOutputDto workerOutput,
IReadOnlyList<HotReloadGroupFile> files,
HotReloadPreparedIntroducedTypes preparedIntroducedTypes,
HotReloadCompileFailureNoteSources compileFailureNoteSources)
HotReloadCompileFailureNoteSources compileFailureNoteSources,
HotReloadCompiledCallersRun compiledCallersRun)
{
Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be empty.");
Debug.Assert(!string.IsNullOrEmpty(assemblyName), "assemblyName must not be empty.");
Expand All @@ -39,6 +40,7 @@ internal HotReloadApplyContext(
Debug.Assert(workerOutput != null, "workerOutput must not be null.");
Debug.Assert(files != null && files.Count > 0, "A group must hold a file.");
Debug.Assert(compileFailureNoteSources != null, "compileFailureNoteSources must not be null.");
Debug.Assert(compiledCallersRun != null, "compiledCallersRun must not be null.");

ProjectRoot = projectRoot;
AssemblyName = assemblyName;
Expand All @@ -51,6 +53,7 @@ internal HotReloadApplyContext(
Files = files;
PreparedIntroducedTypes = preparedIntroducedTypes;
CompileFailureNoteSources = compileFailureNoteSources;
CompiledCallersRun = compiledCallersRun;

List<(string ProjectRelativePath, string AssemblyResolvePath)> filePaths =
new List<(string ProjectRelativePath, string AssemblyResolvePath)>(files.Count);
Expand Down Expand Up @@ -101,6 +104,10 @@ internal HotReloadApplyContext(
// What a shim compile failure of this group can be explained with.
internal HotReloadCompileFailureNoteSources CompileFailureNoteSources { get; }

// The compiled-caller analysis of the run this group belongs to; caller scans go through it
// so they read only while the run keeps the cached assemblies alive.
internal HotReloadCompiledCallersRun CompiledCallersRun { get; }

// The edited files of this group, in the order they were sent to the worker.
internal IReadOnlyList<HotReloadGroupFile> Files { get; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,7 @@ internal static HotReloadServices CreateServices(
editorStateSnapshotCapture,
new HotReloadGroupCommitPolicy(domain, fileEntryApplier),
playMode,
HotReloadMonoInternalAccessGrant.ForThisProcess(),
compiledCallers);
HotReloadMonoInternalAccessGrant.ForThisProcess());
// A factory, not a built value: the stages are bound to the collaborators built here,
// and a caller that built them from the installed services would bind a replacement's
// group run back to the domain that was installed when it called.
Expand Down
Loading
Loading