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
4 changes: 3 additions & 1 deletion .github/workflows/unity-compile-check-and-test-runner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ jobs:

- name: Run concurrency unit tests
if: steps.changes.outputs.csharp == 'true'
run: dotnet test tests/DynamicCodeExecutionScheduler.UnitTests/DynamicCodeExecutionScheduler.UnitTests.csproj --configuration Release
run: |
dotnet test tests/DynamicCodeExecutionScheduler.UnitTests/DynamicCodeExecutionScheduler.UnitTests.csproj --configuration Release
dotnet test tests/SharedRoslynCompilerWorker.UnitTests/SharedRoslynCompilerWorker.UnitTests.csproj --configuration Release

- name: Cache Library folder
if: env.HAS_UNITY_LICENSE == 'true'
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/unity-editmode-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ jobs:
dotnet-version: 10.0.x

- name: Run concurrency unit tests
run: dotnet test tests/DynamicCodeExecutionScheduler.UnitTests/DynamicCodeExecutionScheduler.UnitTests.csproj --configuration Release
run: |
dotnet test tests/DynamicCodeExecutionScheduler.UnitTests/DynamicCodeExecutionScheduler.UnitTests.csproj --configuration Release
dotnet test tests/SharedRoslynCompilerWorker.UnitTests/SharedRoslynCompilerWorker.UnitTests.csproj --configuration Release

- name: Cache Library folder
if: env.HAS_UNITY_LICENSE == 'true'
Expand Down
34 changes: 34 additions & 0 deletions Assets/Tests/Editor/DynamicCodeToolTests/WrapperTemplateTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Collections.Generic;

using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests
{
/// <summary>
/// String-level coverage for WrapperTemplate entry-point generation (no compile-and-run).
/// </summary>
[TestFixture]
public class WrapperTemplateTests
{
[Test]
public void Build_ShouldGenerateExecuteAsyncWithCancellationTokenAndOmitSyncExecute()
{
// Verifies wrapped snippets expose only ExecuteAsync with ct, not a sync Execute that
// would discard the runtime token via ExecuteAsync(parameters, default).
string wrappedSource = WrapperTemplate.Build(
new List<string>(),
System.Array.Empty<string>(),
"TestNs",
"TestClass",
"return 1;");

Assert.That(wrappedSource, Does.Contain("public async System.Threading.Tasks.Task<object> ExecuteAsync("));
Assert.That(wrappedSource, Does.Contain("System.Threading.CancellationToken ct = default)"));
Assert.That(wrappedSource, Does.Not.Contain("public object Execute("));
Assert.That(wrappedSource, Does.Not.Contain("ExecuteAsync(parameters, default)"));
Assert.That(wrappedSource, Does.Not.Contain("GetAwaiter().GetResult()"));
}
}
}

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 @@ -71,13 +71,13 @@ public static async Task<DynamicCompilationBackendResult> CompileAsync(
defineSymbols,
allowUnsafeCode);

CompilerMessage[] workerMessages = SharedRoslynCompilerWorkerHost.TryCompile(
CompilerMessage[] workerMessages = await SharedRoslynCompilerWorkerHost.TryCompileAsync(
workerRequestFilePath,
externalCompilerPaths,
ct,
markBuildStarted,
markBuildFinished,
incrementBuildCount);
incrementBuildCount).ConfigureAwait(false);
if (workerMessages != null)
{
return new DynamicCompilationBackendResult(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditor.Compilation;

Expand Down Expand Up @@ -123,25 +124,26 @@ internal static void RegisterLifecycleForEditorStartup()
EditorApplication.quitting += ShutdownForQuit;
}

public static CompilerMessage[] TryCompile(
public static Task<CompilerMessage[]> TryCompileAsync(
string requestFilePath,
ExternalCompilerPaths externalCompilerPaths,
CancellationToken ct,
Action markBuildStarted,
Action markBuildFinished,
Action incrementBuildCount)
{
return ServiceValue.ExecuteLocked(
() => TryCompileWithRetries(
return ServiceValue.RunSerializedCompileAsync(
operationCt => TryCompileWithRetriesAsync(
requestFilePath,
externalCompilerPaths,
ct,
operationCt,
markBuildStarted,
markBuildFinished,
incrementBuildCount));
incrementBuildCount),
ct);
}

private static CompilerMessage[] TryCompileWithRetries(
private static async Task<CompilerMessage[]> TryCompileWithRetriesAsync(
string requestFilePath,
ExternalCompilerPaths externalCompilerPaths,
CancellationToken ct,
Expand All @@ -151,20 +153,20 @@ private static CompilerMessage[] TryCompileWithRetries(
{
for (int attempt = 1; attempt <= SharedCompilerWorkerMaxAttempts; attempt++)
{
WorkerAttemptResult attemptResult = TryCompileOnce(
WorkerAttemptResult attemptResult = await TryCompileOnceAsync(
requestFilePath,
externalCompilerPaths,
ct,
markBuildStarted,
markBuildFinished,
incrementBuildCount);
incrementBuildCount).ConfigureAwait(false);

if (attemptResult.Succeeded)
{
return attemptResult.Messages;
}

ServiceValue.ShutdownProcessLocked();
ServiceValue.ExecuteWithStateLock(ServiceValue.ShutdownProcessLocked);

if (attemptResult.ShouldRetry && attempt < SharedCompilerWorkerMaxAttempts)
{
Expand All @@ -180,7 +182,7 @@ private static CompilerMessage[] TryCompileWithRetries(
return null;
}

private static WorkerAttemptResult TryCompileOnce(
private static async Task<WorkerAttemptResult> TryCompileOnceAsync(
string requestFilePath,
ExternalCompilerPaths externalCompilerPaths,
CancellationToken ct,
Expand All @@ -196,17 +198,17 @@ private static WorkerAttemptResult TryCompileOnce(
startupResult.FailureContext);
}

return InvokeWorkerOnce(
return await InvokeWorkerOnceAsync(
requestFilePath,
ct,
markBuildStarted,
markBuildFinished,
incrementBuildCount);
incrementBuildCount).ConfigureAwait(false);
}

private static WorkerStartupResult EnsureWorkerReady(ExternalCompilerPaths externalCompilerPaths)
{
if (ServiceValue.HasLiveProcessLocked())
if (ServiceValue.ExecuteWithStateLock(ServiceValue.HasLiveProcessLocked))
{
return WorkerStartupResult.Ready();
}
Expand Down Expand Up @@ -234,8 +236,10 @@ private static WorkerStartupResult EnsureWorkerAssemblyBuilt(
return WorkerStartupResult.Ready();
}

// Why outside state lock: worker DLL compile can take seconds; shutdown must still kill
// an already-running shared worker without waiting on this build.
SharedRoslynCompilerWorkerAssemblyBuilder.WorkerAssemblyBuildResult buildResult =
ServiceValue.CompileWorkerAssemblyLocked(
ServiceValue.CompileWorkerAssembly(
externalCompilerPaths,
workerPaths.SourcePath,
workerPaths.AssemblyPath,
Expand Down Expand Up @@ -267,7 +271,9 @@ private static WorkerStartupResult StartWorkerProcess(
WorkerPaths workerPaths)
{
ProcessStartInfo startInfo = CreateWorkerStartInfo(externalCompilerPaths, workerPaths);
if (!ServiceValue.StartProcessLocked(startInfo))
bool started = ServiceValue.ExecuteWithStateLock(
() => ServiceValue.StartProcessLocked(startInfo));
if (!started)
{
return WorkerStartupResult.Failure(
"worker_start_failed",
Expand All @@ -281,14 +287,28 @@ private static WorkerStartupResult StartWorkerProcess(
return WorkerStartupResult.Ready();
}

private static WorkerAttemptResult InvokeWorkerOnce(
private static async Task<WorkerAttemptResult> InvokeWorkerOnceAsync(
string requestFilePath,
CancellationToken ct,
Action markBuildStarted,
Action markBuildFinished,
Action incrementBuildCount)
{
if (!ServiceValue.HasLiveProcessLocked())
StreamReader reader = null;
// Why not send here: stdin WriteLine/Flush can throw IOException if the worker dies
// after HasLiveProcessLocked; keep send inside the retryable try below.
bool prepared = ServiceValue.ExecuteWithStateLock(() =>
{
if (!ServiceValue.HasLiveProcessLocked())
{
return false;
}

reader = ServiceValue.GetOutputReaderLocked();
return true;
});

if (!prepared)
{
return WorkerAttemptResult.RetryableFailure(
"worker_process_missing",
Expand All @@ -301,8 +321,8 @@ private static WorkerAttemptResult InvokeWorkerOnce(

try
{
SendCompileRequest(requestFilePath);
return ReadWorkerResponse(requestFilePath, ct);
ServiceValue.ExecuteWithStateLock(() => SendCompileRequestLocked(requestFilePath));
return await ReadWorkerResponseAsync(requestFilePath, reader, ct).ConfigureAwait(false);
}
catch (IOException ex)
{
Expand All @@ -314,7 +334,7 @@ private static WorkerAttemptResult InvokeWorkerOnce(
}
catch (OperationCanceledException)
{
ServiceValue.ShutdownProcessLocked();
ServiceValue.ExecuteWithStateLock(ServiceValue.ShutdownProcessLocked);
throw;
}
finally
Expand All @@ -323,52 +343,65 @@ private static WorkerAttemptResult InvokeWorkerOnce(
}
}

private static void SendCompileRequest(string requestFilePath)
private static void SendCompileRequestLocked(string requestFilePath)
{
string absoluteRequestFilePath = Path.GetFullPath(requestFilePath);
ServiceValue.SendCompileRequestLocked(absoluteRequestFilePath);
}

private static WorkerAttemptResult ReadWorkerResponse(
private static async Task<WorkerAttemptResult> ReadWorkerResponseAsync(
string requestFilePath,
StreamReader reader,
CancellationToken ct)
{
StreamReader reader = ServiceValue.GetOutputReaderLocked();
string responseHeader = SharedRoslynCompilerWorkerProtocol.ReadProtocolLine(
int timeoutMilliseconds = ServiceValue.ResponseTimeoutMilliseconds;
string responseHeader = await SharedRoslynCompilerWorkerProtocol.ReadProtocolLineAsync(
reader,
ct);
ct,
timeoutMilliseconds).ConfigureAwait(false);
if (string.IsNullOrEmpty(responseHeader))
{
// Why kill immediately: abandoned ReadLine tasks unblock only after the pipe closes.
ServiceValue.ExecuteWithStateLock(ServiceValue.ShutdownProcessLocked);
return WorkerAttemptResult.RetryableFailure(
"worker_empty_header",
new { request_file_path = requestFilePath });
}

if (!SharedRoslynCompilerWorkerProtocol.TryParseResponseHeader(responseHeader, out int exitCode))
{
ServiceValue.ExecuteWithStateLock(ServiceValue.ShutdownProcessLocked);
return WorkerAttemptResult.RetryableFailure(
SharedRoslynCompilerWorkerProtocol.GetResponseHeaderFailureReason(responseHeader),
new { header = responseHeader });
}

List<string> outputLines = SharedRoslynCompilerWorkerProtocol.ReadDiagnosticLines(reader, ct);
List<string> outputLines = await SharedRoslynCompilerWorkerProtocol.ReadDiagnosticLinesAsync(
reader,
ct,
timeoutMilliseconds).ConfigureAwait(false);
if (outputLines == null)
{
ServiceValue.ExecuteWithStateLock(ServiceValue.ShutdownProcessLocked);
return WorkerAttemptResult.RetryableFailure(
"worker_missing_end_marker",
new { request_file_path = requestFilePath });
}

string combinedOutput = string.Join("\n", outputLines);
CompilerMessage[] compilerMessages = ExternalCompilerMessageParser.Parse(combinedOutput, string.Empty, exitCode);
CompilerMessage[] compilerMessages = ExternalCompilerMessageParser.Parse(
combinedOutput,
string.Empty,
exitCode);
return WorkerAttemptResult.Successful(compilerMessages);
}

private static WorkerPaths CreateWorkerPaths()
{
string workerDirectoryPath = GetWorkerDirectoryPath();
Directory.CreateDirectory(workerDirectoryPath);
ServiceValue.RecordWorkerDirectoryLocked(workerDirectoryPath);
ServiceValue.ExecuteWithStateLock(
() => ServiceValue.RecordWorkerDirectoryLocked(workerDirectoryPath));
return new WorkerPaths(
workerDirectoryPath,
Path.Combine(workerDirectoryPath, RoslynWorkerSourceFileName),
Expand Down Expand Up @@ -403,7 +436,8 @@ private static ProcessStartInfo CreateWorkerStartInfo(
ExternalCompilerPaths externalCompilerPaths,
WorkerPaths workerPaths)
{
ProcessStartInfo startInfo = new() {
ProcessStartInfo startInfo = new()
{
FileName = externalCompilerPaths.DotnetHostPath,
Arguments = "exec"
+ " --runtimeconfig " + SharedRoslynCompilerWorkerAssemblyBuilder.QuoteCommandLineArgument(externalCompilerPaths.CompilerRuntimeConfigPath)
Expand Down Expand Up @@ -479,6 +513,11 @@ internal static Func<ExternalCompilerPaths, string, string, string, CompilerMess
return ServiceValue.SwapWorkerAssemblyCompilerForTests(compiler);
}

internal static void SetResponseTimeoutMillisecondsForTests(int timeoutMilliseconds)
{
ServiceValue.SetResponseTimeoutMillisecondsForTests(timeoutMilliseconds);
}

private static void Shutdown()
{
ServiceValue.Shutdown(GetWorkerDirectoryPath());
Expand Down Expand Up @@ -506,6 +545,5 @@ private static object AppendAttempt(object failureContext, int attempt)
details = failureContext
};
}

}
}
Loading
Loading