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 @@ -2,6 +2,7 @@
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;
Expand All @@ -27,6 +28,71 @@ public void ConfigureWorkerDotnetRuntimeEnvironment_WhenCalled_ShouldDisableMult
Is.EqualTo(SharedRoslynCompilerWorkerAssemblyBuilder.DotnetMultilevelLookupDisabledValue));
}

/// <summary>
/// Verifies a pending compiler stream does not keep the bounded drain waiting.
/// </summary>
[Test]
public void WaitForCompilerStreamDrain_WhenStreamIsPending_ShouldReturnFalse()
{
TaskCompletionSource<string> pendingStream = new();

bool completed = SharedRoslynCompilerWorkerAssemblyBuilder.WaitForCompilerStreamDrain(
Task.FromResult("stdout"),
pendingStream.Task,
0);
pendingStream.SetResult("stderr");

Assert.That(completed, Is.False);
}

/// <summary>
/// Verifies completed compiler streams satisfy the bounded drain immediately.
/// </summary>
[Test]
public void WaitForCompilerStreamDrain_WhenStreamsAreCompleted_ShouldReturnTrue()
{
bool completed = SharedRoslynCompilerWorkerAssemblyBuilder.WaitForCompilerStreamDrain(
Task.FromResult("stdout"),
Task.FromResult("stderr"),
0);

Assert.That(completed, Is.True);
}

/// <summary>
/// Verifies a faulted compiler stream is treated as drained without escaping timeout recovery.
/// </summary>
[Test]
public void WaitForCompilerStreamDrain_WhenStreamFaults_ShouldReturnTrue()
{
Task<string> faultedStream = Task.FromException<string>(new IOException("stream read failed"));

bool completed = SharedRoslynCompilerWorkerAssemblyBuilder.WaitForCompilerStreamDrain(
Task.FromResult("stdout"),
faultedStream,
0);

Assert.That(completed, Is.True);
}

/// <summary>
/// Verifies one faulted stream does not make a still-pending drain appear complete.
/// </summary>
[Test]
public void WaitForCompilerStreamDrain_WhenFaultedStreamHasPendingPeer_ShouldReturnFalse()
{
TaskCompletionSource<string> pendingStream = new();
Task<string> faultedStream = Task.FromException<string>(new IOException("stream read failed"));

bool completed = SharedRoslynCompilerWorkerAssemblyBuilder.WaitForCompilerStreamDrain(
faultedStream,
pendingStream.Task,
0);
pendingStream.SetResult("stderr");

Assert.That(completed, Is.False);
}

[Test]
public void CreateCompileRequestCommand_WhenPathIsWindowsAbsolutePath_ShouldEncodeAsciiPayload()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
internal static class SharedRoslynCompilerWorkerAssemblyBuilder
{
private const int WorkerAssemblyBuildTimeoutMilliseconds = 30000;
private const int WorkerCompilerTerminationTimeoutMilliseconds = 500;
internal const string DotnetMultilevelLookupEnvironmentVariableName = "DOTNET_MULTILEVEL_LOOKUP";
internal const string DotnetMultilevelLookupDisabledValue = "0";

Expand Down Expand Up @@ -107,10 +108,13 @@ internal static WorkerAssemblyBuildResult CompileWorkerAssembly(
if (!process.HasExited)
{
process.Kill();
process.WaitForExit(500);
process.WaitForExit(WorkerCompilerTerminationTimeoutMilliseconds);
}

Task.WaitAll(stdoutTask, stderrTask);
WaitForCompilerStreamDrain(
stdoutTask,
stderrTask,
WorkerCompilerTerminationTimeoutMilliseconds);
return WorkerAssemblyBuildResult.StartFailure(
"worker_compiler_timeout",
new
Expand All @@ -129,6 +133,33 @@ internal static WorkerAssemblyBuildResult CompileWorkerAssembly(
return WorkerAssemblyBuildResult.Started(compilerMessages);
}

internal static bool WaitForCompilerStreamDrain(
Task<string> stdoutTask,
Task<string> stderrTask,
int timeoutMilliseconds)
{
Debug.Assert(stdoutTask != null, "stdoutTask must not be null");
Debug.Assert(stderrTask != null, "stderrTask must not be null");
Debug.Assert(timeoutMilliseconds >= 0, "timeoutMilliseconds must not be negative");

Task streamDrainTask = Task.WhenAll(stdoutTask, stderrTask);
bool completed = Task.WaitAny(
new[] { streamDrainTask },
timeoutMilliseconds) == 0;
ObserveCompilerStreamFailure(stdoutTask);
ObserveCompilerStreamFailure(stderrTask);
ObserveCompilerStreamFailure(streamDrainTask);
Comment thread
hatayama marked this conversation as resolved.
return completed;
}

private static void ObserveCompilerStreamFailure(Task streamDrainTask)
{
// Why: disposing the process after a bounded drain can fault a pending read task later.
_ = streamDrainTask.ContinueWith(
task => { _ = task.Exception; },
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);
}

private static List<string> BuildWorkerReferenceSet(ExternalCompilerPaths externalCompilerPaths)
{
string sharedRuntimeDirectoryPath = externalCompilerPaths.NetCoreRuntimeSharedDirectoryPath;
Expand Down
Loading