Integrate DAP debugger into JobRunner and StepsRunner

This commit is contained in:
Francesco Renzi
2026-03-11 08:56:08 -07:00
committed by GitHub
parent 17b05ddaa4
commit 915e13c842
5 changed files with 1144 additions and 0 deletions

View File

@@ -13,6 +13,7 @@ using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker.Dap;
using GitHub.Services.Common;
using GitHub.Services.WebApi;
using Sdk.RSWebApi.Contracts;
@@ -112,6 +113,9 @@ namespace GitHub.Runner.Worker
IExecutionContext jobContext = null;
CancellationTokenRegistration? runnerShutdownRegistration = null;
IDapServer dapServer = null;
IDapDebugSession debugSession = null;
CancellationTokenRegistration? dapCancellationRegistration = null;
try
{
// Create the job execution context.
@@ -124,6 +128,31 @@ namespace GitHub.Runner.Worker
if (jobContext.Global.EnableDebugger)
{
Trace.Info("Debugger enabled for this job run");
try
{
var port = 4711;
var portEnv = Environment.GetEnvironmentVariable("ACTIONS_DAP_PORT");
if (!string.IsNullOrEmpty(portEnv) && int.TryParse(portEnv, out var customPort))
{
port = customPort;
}
dapServer = HostContext.GetService<IDapServer>();
debugSession = HostContext.GetService<IDapDebugSession>();
dapServer.SetSession(debugSession);
debugSession.SetDapServer(dapServer);
await dapServer.StartAsync(port, jobRequestCancellationToken);
Trace.Info($"DAP server started on port {port}, listening for debugger client");
}
catch (Exception ex)
{
Trace.Warning($"Failed to start DAP server: {ex.Message}. Job will continue without debugging.");
dapServer = null;
debugSession = null;
}
}
runnerShutdownRegistration = HostContext.RunnerShutdownToken.Register(() =>
@@ -224,6 +253,39 @@ namespace GitHub.Runner.Worker
await Task.WhenAny(_jobServerQueue.JobRecordUpdated.Task, Task.Delay(1000));
}
// Wait for DAP debugger client connection and handshake after "Set up job"
// so the job page shows the setup step before we block on the debugger
if (dapServer != null && debugSession != null)
{
try
{
Trace.Info("Waiting for debugger client connection...");
await dapServer.WaitForConnectionAsync(jobRequestCancellationToken);
Trace.Info("Debugger client connected.");
await debugSession.WaitForHandshakeAsync(jobRequestCancellationToken);
Trace.Info("DAP handshake complete.");
dapCancellationRegistration = jobRequestCancellationToken.Register(() =>
{
Trace.Info("Job cancellation requested, cancelling debug session.");
debugSession.CancelSession();
});
}
catch (OperationCanceledException) when (jobRequestCancellationToken.IsCancellationRequested)
{
Trace.Info("Job was cancelled before debugger client connected. Continuing without debugger.");
dapServer = null;
debugSession = null;
}
catch (Exception ex)
{
Trace.Warning($"Failed to complete DAP handshake: {ex.Message}. Job will continue without debugging.");
dapServer = null;
debugSession = null;
}
}
// Run all job steps
Trace.Info("Run all job steps.");
var stepsRunner = HostContext.GetService<IStepsRunner>();
@@ -264,6 +326,25 @@ namespace GitHub.Runner.Worker
runnerShutdownRegistration = null;
}
if (dapCancellationRegistration.HasValue)
{
dapCancellationRegistration.Value.Dispose();
dapCancellationRegistration = null;
}
if (dapServer != null)
{
try
{
Trace.Info("Stopping DAP server");
await dapServer.StopAsync();
}
catch (Exception ex)
{
Trace.Warning($"Error stopping DAP server: {ex.Message}");
}
}
await ShutdownQueue(throwOnFailure: false);
}
}

View File

@@ -10,6 +10,7 @@ using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker.Dap;
using GitHub.Runner.Worker.Expressions;
namespace GitHub.Runner.Worker
@@ -50,6 +51,16 @@ namespace GitHub.Runner.Worker
jobContext.JobContext.Status = (jobContext.Result ?? TaskResult.Succeeded).ToActionResult();
var scopeInputs = new Dictionary<string, PipelineContextData>(StringComparer.OrdinalIgnoreCase);
bool checkPostJobActions = false;
IDapDebugSession debugSession = null;
try
{
debugSession = HostContext.GetService<IDapDebugSession>();
}
catch
{
// Debug session not available — continue without debugging
}
bool isFirstStep = true;
while (jobContext.JobSteps.Count > 0 || !checkPostJobActions)
{
if (jobContext.JobSteps.Count == 0 && !checkPostJobActions)
@@ -226,9 +237,35 @@ namespace GitHub.Runner.Worker
}
else
{
// Pause for DAP debugger before step execution
if (debugSession?.IsActive == true)
{
try
{
await debugSession.OnStepStartingAsync(step, jobContext, isFirstStep, jobContext.CancellationToken);
isFirstStep = false;
}
catch (Exception ex)
{
Trace.Warning($"DAP OnStepStarting error: {ex.Message}");
}
}
// Run the step
await RunStepAsync(step, jobContext.CancellationToken);
CompleteStep(step);
if (debugSession?.IsActive == true)
{
try
{
debugSession.OnStepCompleted(step);
}
catch (Exception ex)
{
Trace.Warning($"DAP OnStepCompleted error: {ex.Message}");
}
}
}
}
finally
@@ -255,6 +292,18 @@ namespace GitHub.Runner.Worker
Trace.Info($"Current state: job state = '{jobContext.Result}'");
}
if (debugSession?.IsActive == true)
{
try
{
debugSession.OnJobCompleted();
}
catch (Exception ex)
{
Trace.Warning($"DAP OnJobCompleted error: {ex.Message}");
}
}
}
private async Task RunStepAsync(IStep step, CancellationToken jobCancellationToken)

View File

@@ -0,0 +1,611 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using GitHub.DistributedTask.ObjectTemplating.Tokens;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Dap;
using Moq;
using Newtonsoft.Json;
using Xunit;
namespace GitHub.Runner.Common.Tests.Worker
{
public sealed class DapDebugSessionL0
{
private DapDebugSession _session;
private Mock<IDapServer> _mockServer;
private List<Event> _sentEvents;
private List<Response> _sentResponses;
private TestHostContext CreateTestContext([CallerMemberName] string testName = "")
{
var hc = new TestHostContext(this, testName);
_session = new DapDebugSession();
_session.Initialize(hc);
_sentEvents = new List<Event>();
_sentResponses = new List<Response>();
_mockServer = new Mock<IDapServer>();
_mockServer.Setup(x => x.SendEvent(It.IsAny<Event>()))
.Callback<Event>(e => _sentEvents.Add(e));
_mockServer.Setup(x => x.SendResponse(It.IsAny<Response>()))
.Callback<Response>(r => _sentResponses.Add(r));
_session.SetDapServer(_mockServer.Object);
return hc;
}
private Mock<IStep> CreateMockStep(string displayName, TaskResult? result = null)
{
var mockEc = new Mock<IExecutionContext>();
mockEc.SetupAllProperties();
mockEc.Object.Result = result;
var mockStep = new Mock<IStep>();
mockStep.Setup(x => x.DisplayName).Returns(displayName);
mockStep.Setup(x => x.ExecutionContext).Returns(mockEc.Object);
return mockStep;
}
private Mock<IExecutionContext> CreateMockJobContext()
{
var mockJobContext = new Mock<IExecutionContext>();
mockJobContext.Setup(x => x.GetGitHubContext("job")).Returns("test-job");
return mockJobContext;
}
private async Task InitializeSessionAsync()
{
var initJson = JsonConvert.SerializeObject(new Request
{
Seq = 1,
Type = "request",
Command = "initialize"
});
await _session.HandleMessageAsync(initJson, CancellationToken.None);
var attachJson = JsonConvert.SerializeObject(new Request
{
Seq = 2,
Type = "request",
Command = "attach"
});
await _session.HandleMessageAsync(attachJson, CancellationToken.None);
var configJson = JsonConvert.SerializeObject(new Request
{
Seq = 3,
Type = "request",
Command = "configurationDone"
});
await _session.HandleMessageAsync(configJson, CancellationToken.None);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void InitialStateIsWaitingForConnection()
{
using (CreateTestContext())
{
Assert.Equal(DapSessionState.WaitingForConnection, _session.State);
Assert.False(_session.IsActive);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task InitializeHandlerSetsInitializingState()
{
using (CreateTestContext())
{
var json = JsonConvert.SerializeObject(new Request
{
Seq = 1,
Type = "request",
Command = "initialize"
});
await _session.HandleMessageAsync(json, CancellationToken.None);
Assert.Equal(DapSessionState.Initializing, _session.State);
Assert.Single(_sentResponses);
Assert.True(_sentResponses[0].Success);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task ConfigurationDoneSetsReadyState()
{
using (CreateTestContext())
{
await InitializeSessionAsync();
Assert.Equal(DapSessionState.Ready, _session.State);
Assert.True(_session.IsActive);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task OnStepStartingPausesAndSendsStoppedEvent()
{
using (CreateTestContext())
{
await InitializeSessionAsync();
_session.HandleClientConnected();
// Wait for the async initialized event to arrive, then clear
await Task.Delay(200);
_sentEvents.Clear();
var step = CreateMockStep("Checkout code");
var jobContext = CreateMockJobContext();
var cts = new CancellationTokenSource();
var stepTask = _session.OnStepStartingAsync(step.Object, jobContext.Object, isFirstStep: true, cts.Token);
await Task.Delay(100);
Assert.False(stepTask.IsCompleted);
Assert.Equal(DapSessionState.Paused, _session.State);
var stoppedEvents = _sentEvents.FindAll(e => e.EventType == "stopped");
Assert.Single(stoppedEvents);
var continueJson = JsonConvert.SerializeObject(new Request
{
Seq = 10,
Type = "request",
Command = "continue"
});
await _session.HandleMessageAsync(continueJson, CancellationToken.None);
await Task.WhenAny(stepTask, Task.Delay(5000));
Assert.True(stepTask.IsCompleted);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task NextCommandPausesOnFollowingStep()
{
using (CreateTestContext())
{
await InitializeSessionAsync();
_session.HandleClientConnected();
_sentEvents.Clear();
var step1 = CreateMockStep("Step 1");
var jobContext = CreateMockJobContext();
var step1Task = _session.OnStepStartingAsync(step1.Object, jobContext.Object, isFirstStep: true, CancellationToken.None);
var nextJson = JsonConvert.SerializeObject(new Request
{
Seq = 10,
Type = "request",
Command = "next"
});
await _session.HandleMessageAsync(nextJson, CancellationToken.None);
await Task.WhenAny(step1Task, Task.Delay(5000));
Assert.True(step1Task.IsCompleted);
_session.OnStepCompleted(step1.Object);
_sentEvents.Clear();
var step2 = CreateMockStep("Step 2");
var step2Task = _session.OnStepStartingAsync(step2.Object, jobContext.Object, isFirstStep: false, CancellationToken.None);
await Task.Delay(100);
Assert.False(step2Task.IsCompleted);
Assert.Equal(DapSessionState.Paused, _session.State);
var stoppedEvents = _sentEvents.FindAll(e => e.EventType == "stopped");
Assert.Single(stoppedEvents);
var continueJson = JsonConvert.SerializeObject(new Request
{
Seq = 11,
Type = "request",
Command = "continue"
});
await _session.HandleMessageAsync(continueJson, CancellationToken.None);
await Task.WhenAny(step2Task, Task.Delay(5000));
Assert.True(step2Task.IsCompleted);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task ContinueCommandSkipsNextPause()
{
using (CreateTestContext())
{
await InitializeSessionAsync();
_session.HandleClientConnected();
_sentEvents.Clear();
var step1 = CreateMockStep("Step 1");
var jobContext = CreateMockJobContext();
var step1Task = _session.OnStepStartingAsync(step1.Object, jobContext.Object, isFirstStep: true, CancellationToken.None);
var continueJson = JsonConvert.SerializeObject(new Request
{
Seq = 10,
Type = "request",
Command = "continue"
});
await _session.HandleMessageAsync(continueJson, CancellationToken.None);
await Task.WhenAny(step1Task, Task.Delay(5000));
Assert.True(step1Task.IsCompleted);
_session.OnStepCompleted(step1.Object);
_sentEvents.Clear();
var step2 = CreateMockStep("Step 2");
var step2Task = _session.OnStepStartingAsync(step2.Object, jobContext.Object, isFirstStep: false, CancellationToken.None);
await Task.WhenAny(step2Task, Task.Delay(5000));
Assert.True(step2Task.IsCompleted);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task CancellationUnblocksPausedStep()
{
using (CreateTestContext())
{
await InitializeSessionAsync();
_session.HandleClientConnected();
_sentEvents.Clear();
var step = CreateMockStep("Step 1");
var jobContext = CreateMockJobContext();
var cts = new CancellationTokenSource();
var stepTask = _session.OnStepStartingAsync(step.Object, jobContext.Object, isFirstStep: true, cts.Token);
await Task.Delay(100);
Assert.False(stepTask.IsCompleted);
Assert.Equal(DapSessionState.Paused, _session.State);
cts.Cancel();
await Task.WhenAny(stepTask, Task.Delay(5000));
Assert.True(stepTask.IsCompleted);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task CancelSessionSendsTerminatedAndExitedEvents()
{
using (CreateTestContext())
{
await InitializeSessionAsync();
_sentEvents.Clear();
_session.CancelSession();
Assert.Equal(DapSessionState.Terminated, _session.State);
Assert.False(_session.IsActive);
var terminatedEvents = _sentEvents.FindAll(e => e.EventType == "terminated");
var exitedEvents = _sentEvents.FindAll(e => e.EventType == "exited");
Assert.Single(terminatedEvents);
Assert.Single(exitedEvents);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task CancelSessionReleasesBlockedStep()
{
using (CreateTestContext())
{
await InitializeSessionAsync();
_session.HandleClientConnected();
_sentEvents.Clear();
var step = CreateMockStep("Blocked Step");
var jobContext = CreateMockJobContext();
var stepTask = _session.OnStepStartingAsync(step.Object, jobContext.Object, isFirstStep: true, CancellationToken.None);
await Task.Delay(100);
Assert.False(stepTask.IsCompleted);
_session.CancelSession();
await Task.WhenAny(stepTask, Task.Delay(5000));
Assert.True(stepTask.IsCompleted);
Assert.Equal(DapSessionState.Terminated, _session.State);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task ReconnectionResendStoppedEvent()
{
using (CreateTestContext())
{
await InitializeSessionAsync();
_session.HandleClientConnected();
// Wait for the async initialized event to arrive, then clear
await Task.Delay(200);
_sentEvents.Clear();
var step = CreateMockStep("Step 1");
var jobContext = CreateMockJobContext();
var stepTask = _session.OnStepStartingAsync(step.Object, jobContext.Object, isFirstStep: true, CancellationToken.None);
await Task.Delay(100);
Assert.Equal(DapSessionState.Paused, _session.State);
var stoppedEvents = _sentEvents.FindAll(e => e.EventType == "stopped");
Assert.Single(stoppedEvents);
_session.HandleClientDisconnected();
Assert.Equal(DapSessionState.Paused, _session.State);
_sentEvents.Clear();
_session.HandleClientConnected();
Assert.Single(_sentEvents);
Assert.Equal("stopped", _sentEvents[0].EventType);
var continueJson = JsonConvert.SerializeObject(new Request
{
Seq = 20,
Type = "request",
Command = "continue"
});
await _session.HandleMessageAsync(continueJson, CancellationToken.None);
await Task.WhenAny(stepTask, Task.Delay(5000));
Assert.True(stepTask.IsCompleted);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task DisconnectCommandTerminatesSession()
{
using (CreateTestContext())
{
await InitializeSessionAsync();
var disconnectJson = JsonConvert.SerializeObject(new Request
{
Seq = 10,
Type = "request",
Command = "disconnect"
});
await _session.HandleMessageAsync(disconnectJson, CancellationToken.None);
Assert.Equal(DapSessionState.Terminated, _session.State);
Assert.False(_session.IsActive);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task OnStepCompletedTracksCompletedSteps()
{
using (CreateTestContext())
{
await InitializeSessionAsync();
_session.HandleClientConnected();
var step1 = CreateMockStep("Step 1");
step1.Object.ExecutionContext.Result = TaskResult.Succeeded;
var jobContext = CreateMockJobContext();
var step1Task = _session.OnStepStartingAsync(step1.Object, jobContext.Object, isFirstStep: true, CancellationToken.None);
var continueJson = JsonConvert.SerializeObject(new Request
{
Seq = 10,
Type = "request",
Command = "continue"
});
await _session.HandleMessageAsync(continueJson, CancellationToken.None);
await Task.WhenAny(step1Task, Task.Delay(5000));
_session.OnStepCompleted(step1.Object);
var stackTraceJson = JsonConvert.SerializeObject(new Request
{
Seq = 11,
Type = "request",
Command = "stackTrace"
});
_sentResponses.Clear();
await _session.HandleMessageAsync(stackTraceJson, CancellationToken.None);
Assert.Single(_sentResponses);
Assert.True(_sentResponses[0].Success);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task OnJobCompletedSendsTerminatedAndExitedEvents()
{
using (CreateTestContext())
{
await InitializeSessionAsync();
_sentEvents.Clear();
_session.OnJobCompleted();
Assert.Equal(DapSessionState.Terminated, _session.State);
var terminatedEvents = _sentEvents.FindAll(e => e.EventType == "terminated");
var exitedEvents = _sentEvents.FindAll(e => e.EventType == "exited");
Assert.Single(terminatedEvents);
Assert.Single(exitedEvents);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task OnStepStartingNoOpWhenNotActive()
{
using (CreateTestContext())
{
var step = CreateMockStep("Step 1");
var jobContext = CreateMockJobContext();
var task = _session.OnStepStartingAsync(step.Object, jobContext.Object, isFirstStep: true, CancellationToken.None);
await Task.WhenAny(task, Task.Delay(5000));
Assert.True(task.IsCompleted);
_mockServer.Verify(x => x.SendEvent(It.IsAny<Event>()), Times.Never);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task ThreadsCommandReturnsJobThread()
{
using (CreateTestContext())
{
await InitializeSessionAsync();
var threadsJson = JsonConvert.SerializeObject(new Request
{
Seq = 10,
Type = "request",
Command = "threads"
});
_sentResponses.Clear();
await _session.HandleMessageAsync(threadsJson, CancellationToken.None);
Assert.Single(_sentResponses);
Assert.True(_sentResponses[0].Success);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task UnsupportedCommandReturnsErrorResponse()
{
using (CreateTestContext())
{
await InitializeSessionAsync();
var json = JsonConvert.SerializeObject(new Request
{
Seq = 99,
Type = "request",
Command = "stepIn"
});
_sentResponses.Clear();
await _session.HandleMessageAsync(json, CancellationToken.None);
Assert.Single(_sentResponses);
Assert.False(_sentResponses[0].Success);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task FullFlowInitAttachConfigStepContinueComplete()
{
using (CreateTestContext())
{
await InitializeSessionAsync();
_session.HandleClientConnected();
_sentEvents.Clear();
_sentResponses.Clear();
Assert.Equal(DapSessionState.Ready, _session.State);
var step = CreateMockStep("Run tests");
var jobContext = CreateMockJobContext();
var stepTask = _session.OnStepStartingAsync(step.Object, jobContext.Object, isFirstStep: true, CancellationToken.None);
await Task.Delay(100);
Assert.Equal(DapSessionState.Paused, _session.State);
var stoppedEvents = _sentEvents.FindAll(e => e.EventType == "stopped");
Assert.Single(stoppedEvents);
var continueJson = JsonConvert.SerializeObject(new Request
{
Seq = 10,
Type = "request",
Command = "continue"
});
await _session.HandleMessageAsync(continueJson, CancellationToken.None);
await Task.WhenAny(stepTask, Task.Delay(5000));
Assert.True(stepTask.IsCompleted);
var continuedEvents = _sentEvents.FindAll(e => e.EventType == "continued");
Assert.Single(continuedEvents);
step.Object.ExecutionContext.Result = TaskResult.Succeeded;
_session.OnStepCompleted(step.Object);
_sentEvents.Clear();
_session.OnJobCompleted();
Assert.Equal(DapSessionState.Terminated, _session.State);
var terminatedEvents = _sentEvents.FindAll(e => e.EventType == "terminated");
var exitedEvents = _sentEvents.FindAll(e => e.EventType == "exited");
Assert.Single(terminatedEvents);
Assert.Single(exitedEvents);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task DoubleCancelSessionIsIdempotent()
{
using (CreateTestContext())
{
await InitializeSessionAsync();
_sentEvents.Clear();
_session.CancelSession();
_session.CancelSession();
Assert.Equal(DapSessionState.Terminated, _session.State);
var terminatedEvents = _sentEvents.FindAll(e => e.EventType == "terminated");
Assert.Single(terminatedEvents);
}
}
}
}

View File

@@ -0,0 +1,233 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;
using GitHub.Runner.Worker.Dap;
namespace GitHub.Runner.Common.Tests.Worker
{
public sealed class DapMessagesL0
{
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void RequestSerializesCorrectly()
{
var request = new Request
{
Seq = 1,
Type = "request",
Command = "initialize",
Arguments = JObject.FromObject(new { clientID = "test-client" })
};
var json = JsonConvert.SerializeObject(request);
var deserialized = JsonConvert.DeserializeObject<Request>(json);
Assert.Equal(1, deserialized.Seq);
Assert.Equal("request", deserialized.Type);
Assert.Equal("initialize", deserialized.Command);
Assert.Equal("test-client", deserialized.Arguments["clientID"].ToString());
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ResponseSerializesCorrectly()
{
var response = new Response
{
Seq = 2,
Type = "response",
RequestSeq = 1,
Success = true,
Command = "initialize",
Body = new Capabilities { SupportsConfigurationDoneRequest = true }
};
var json = JsonConvert.SerializeObject(response);
var deserialized = JsonConvert.DeserializeObject<Response>(json);
Assert.Equal(2, deserialized.Seq);
Assert.Equal("response", deserialized.Type);
Assert.Equal(1, deserialized.RequestSeq);
Assert.True(deserialized.Success);
Assert.Equal("initialize", deserialized.Command);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void EventSerializesWithCorrectType()
{
var evt = new Event
{
EventType = "stopped",
Body = new StoppedEventBody
{
Reason = "entry",
Description = "Stopped at entry",
ThreadId = 1,
AllThreadsStopped = true
}
};
Assert.Equal("event", evt.Type);
var json = JsonConvert.SerializeObject(evt);
Assert.Contains("\"type\":\"event\"", json);
Assert.Contains("\"event\":\"stopped\"", json);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void StoppedEventBodyOmitsNullFields()
{
var body = new StoppedEventBody
{
Reason = "step"
};
var json = JsonConvert.SerializeObject(body);
Assert.Contains("\"reason\":\"step\"", json);
Assert.DoesNotContain("\"threadId\"", json);
Assert.DoesNotContain("\"allThreadsStopped\"", json);
Assert.DoesNotContain("\"description\"", json);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CapabilitiesMvpDefaults()
{
var caps = new Capabilities
{
SupportsConfigurationDoneRequest = true,
SupportsFunctionBreakpoints = false,
SupportsStepBack = false
};
var json = JsonConvert.SerializeObject(caps);
var deserialized = JsonConvert.DeserializeObject<Capabilities>(json);
Assert.True(deserialized.SupportsConfigurationDoneRequest);
Assert.False(deserialized.SupportsFunctionBreakpoints);
Assert.False(deserialized.SupportsStepBack);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ContinueResponseBodySerialization()
{
var body = new ContinueResponseBody { AllThreadsContinued = true };
var json = JsonConvert.SerializeObject(body);
var deserialized = JsonConvert.DeserializeObject<ContinueResponseBody>(json);
Assert.True(deserialized.AllThreadsContinued);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ThreadsResponseBodySerialization()
{
var body = new ThreadsResponseBody
{
Threads = new List<Thread>
{
new Thread { Id = 1, Name = "Job Thread" }
}
};
var json = JsonConvert.SerializeObject(body);
var deserialized = JsonConvert.DeserializeObject<ThreadsResponseBody>(json);
Assert.Single(deserialized.Threads);
Assert.Equal(1, deserialized.Threads[0].Id);
Assert.Equal("Job Thread", deserialized.Threads[0].Name);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void StackFrameSerialization()
{
var frame = new StackFrame
{
Id = 1,
Name = "Step: Checkout",
Line = 1,
Column = 1,
PresentationHint = "normal"
};
var json = JsonConvert.SerializeObject(frame);
var deserialized = JsonConvert.DeserializeObject<StackFrame>(json);
Assert.Equal(1, deserialized.Id);
Assert.Equal("Step: Checkout", deserialized.Name);
Assert.Equal("normal", deserialized.PresentationHint);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ExitedEventBodySerialization()
{
var body = new ExitedEventBody { ExitCode = 130 };
var json = JsonConvert.SerializeObject(body);
var deserialized = JsonConvert.DeserializeObject<ExitedEventBody>(json);
Assert.Equal(130, deserialized.ExitCode);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void DapCommandEnumValues()
{
Assert.Equal(0, (int)DapCommand.Continue);
Assert.Equal(1, (int)DapCommand.Next);
Assert.Equal(4, (int)DapCommand.Disconnect);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void RequestDeserializesFromRawJson()
{
var json = @"{""seq"":5,""type"":""request"",""command"":""continue"",""arguments"":{""threadId"":1}}";
var request = JsonConvert.DeserializeObject<Request>(json);
Assert.Equal(5, request.Seq);
Assert.Equal("request", request.Type);
Assert.Equal("continue", request.Command);
Assert.Equal(1, request.Arguments["threadId"].Value<int>());
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void ErrorResponseBodySerialization()
{
var body = new ErrorResponseBody
{
Error = new Message
{
Id = 1,
Format = "Something went wrong",
ShowUser = true
}
};
var json = JsonConvert.SerializeObject(body);
var deserialized = JsonConvert.DeserializeObject<ErrorResponseBody>(json);
Assert.Equal(1, deserialized.Error.Id);
Assert.Equal("Something went wrong", deserialized.Error.Format);
Assert.True(deserialized.Error.ShowUser);
}
}
}

View File

@@ -0,0 +1,170 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Runner.Worker.Dap;
using Moq;
using Xunit;
namespace GitHub.Runner.Common.Tests.Worker
{
public sealed class DapServerL0
{
private DapServer _server;
private TestHostContext CreateTestContext([CallerMemberName] string testName = "")
{
var hc = new TestHostContext(this, testName);
_server = new DapServer();
_server.Initialize(hc);
return hc;
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void InitializeSucceeds()
{
using (CreateTestContext())
{
Assert.NotNull(_server);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SetSessionAcceptsMock()
{
using (CreateTestContext())
{
var mockSession = new Mock<IDapDebugSession>();
_server.SetSession(mockSession.Object);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SendEventNoClientDoesNotThrow()
{
using (CreateTestContext())
{
var evt = new Event
{
EventType = "stopped",
Body = new StoppedEventBody
{
Reason = "entry",
ThreadId = 1,
AllThreadsStopped = true
}
};
_server.SendEvent(evt);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SendResponseNoClientDoesNotThrow()
{
using (CreateTestContext())
{
var response = new Response
{
Type = "response",
RequestSeq = 1,
Command = "initialize",
Success = true
};
_server.SendResponse(response);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SendMessageNoClientDoesNotThrow()
{
using (CreateTestContext())
{
var msg = new ProtocolMessage
{
Type = "response",
Seq = 1
};
_server.SendMessage(msg);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task StopWithoutStartDoesNotThrow()
{
using (CreateTestContext())
{
await _server.StopAsync();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task StartAndStopOnAvailablePort()
{
using (CreateTestContext())
{
var cts = new CancellationTokenSource();
await _server.StartAsync(0, cts.Token);
await _server.StopAsync();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task WaitForConnectionCancelledByCancellationToken()
{
using (CreateTestContext())
{
var cts = new CancellationTokenSource();
await _server.StartAsync(0, cts.Token);
var waitTask = _server.WaitForConnectionAsync(cts.Token);
cts.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
{
await waitTask;
});
await _server.StopAsync();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task StartAndStopMultipleTimesDoesNotThrow()
{
using (CreateTestContext())
{
var cts1 = new CancellationTokenSource();
await _server.StartAsync(0, cts1.Token);
await _server.StopAsync();
_server = new DapServer();
_server.Initialize(CreateTestContext());
var cts2 = new CancellationTokenSource();
await _server.StartAsync(0, cts2.Token);
await _server.StopAsync();
}
}
}
}