204 lines
7.1 KiB
C#
204 lines
7.1 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.Debugger.Protocol;
|
|
using SharpEmu.Debugger.Session;
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.Debugger.Tests;
|
|
|
|
public sealed class DebugCommandDispatcherTests
|
|
{
|
|
private const int MaxMemoryChunk = 64 * 1024;
|
|
|
|
[Fact]
|
|
public void Dispatch_UnknownCommand_Fails()
|
|
{
|
|
var dispatcher = CreateDispatcher();
|
|
|
|
var response = dispatcher.Dispatch(Parse("""{"command":"nope"}"""));
|
|
|
|
Assert.False(response.Ok);
|
|
Assert.Equal("nope", response.Command);
|
|
Assert.Contains("Unknown command", response.Error, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dispatch_Registers_WhenNotPaused_Fails()
|
|
{
|
|
var session = new FakeDebuggerSession(DebuggerRunState.Running);
|
|
var dispatcher = new DebugCommandDispatcher(session);
|
|
|
|
var response = dispatcher.Dispatch(Parse("""{"command":"registers"}"""));
|
|
|
|
Assert.False(response.Ok);
|
|
Assert.Equal("Target is not paused.", response.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dispatch_ContinueAndStep_WhenNotPaused_Fail()
|
|
{
|
|
var session = new FakeDebuggerSession(DebuggerRunState.Running);
|
|
var dispatcher = new DebugCommandDispatcher(session);
|
|
|
|
var cont = dispatcher.Dispatch(Parse("""{"command":"continue"}"""));
|
|
var step = dispatcher.Dispatch(Parse("""{"command":"step"}"""));
|
|
|
|
Assert.False(cont.Ok);
|
|
Assert.Equal("Target is not paused.", cont.Error);
|
|
Assert.False(step.Ok);
|
|
Assert.Equal("Target is not paused.", step.Error);
|
|
Assert.Equal(1, session.ContinueCallCount);
|
|
Assert.Equal(1, session.StepFrameCallCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dispatch_ReadMemory_RejectsLengthAboveCap()
|
|
{
|
|
var session = new FakeDebuggerSession();
|
|
session.SeedMemory(0x1000, new byte[16]);
|
|
var dispatcher = new DebugCommandDispatcher(session);
|
|
|
|
var oversize = dispatcher.Dispatch(Parse(
|
|
$$"""{"command":"read-memory","address":"0x1000","length":{{MaxMemoryChunk + 1}}}"""));
|
|
var zero = dispatcher.Dispatch(Parse(
|
|
"""{"command":"read-memory","address":"0x1000","length":0}"""));
|
|
|
|
Assert.False(oversize.Ok);
|
|
Assert.Contains(MaxMemoryChunk.ToString(), oversize.Error);
|
|
Assert.False(zero.Ok);
|
|
Assert.Contains(MaxMemoryChunk.ToString(), zero.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dispatch_WriteMemory_RejectsPayloadAboveCap()
|
|
{
|
|
var session = new FakeDebuggerSession();
|
|
var dispatcher = new DebugCommandDispatcher(session);
|
|
var hex = new string('A', (MaxMemoryChunk + 1) * 2);
|
|
|
|
var response = dispatcher.Dispatch(Parse(
|
|
$$"""{"command":"write-memory","address":"0x1000","bytes":"{{hex}}"}"""));
|
|
|
|
Assert.False(response.Ok);
|
|
Assert.Contains(MaxMemoryChunk.ToString(), response.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dispatch_ReadMemory_WhenPaused_ReturnsHexBytes()
|
|
{
|
|
var session = new FakeDebuggerSession();
|
|
session.SeedMemory(0x2000, [0xDE, 0xAD, 0xBE, 0xEF]);
|
|
var dispatcher = new DebugCommandDispatcher(session);
|
|
|
|
var response = dispatcher.Dispatch(Parse(
|
|
"""{"command":"read-memory","address":"0x2000","length":4}"""));
|
|
|
|
Assert.True(response.Ok);
|
|
Assert.NotNull(response.Data);
|
|
Assert.Equal("DEADBEEF", GetString(response.Data!, "bytes"));
|
|
Assert.Equal("0x0000000000002000", GetString(response.Data!, "address"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Dispatch_BreakpointCrud_HappyPath()
|
|
{
|
|
var session = new FakeDebuggerSession();
|
|
var dispatcher = new DebugCommandDispatcher(session);
|
|
|
|
var added = dispatcher.Dispatch(Parse(
|
|
"""{"command":"add-breakpoint","address":"0x401000","kind":"Execute","length":8}"""));
|
|
Assert.True(added.Ok);
|
|
Assert.NotNull(added.Data);
|
|
|
|
var breakpoint = GetDict(added.Data!, "breakpoint");
|
|
Assert.Equal(1, Convert.ToInt32(breakpoint["id"]));
|
|
Assert.Equal("Execute", Assert.IsType<string>(breakpoint["kind"]));
|
|
Assert.Equal(1UL, Convert.ToUInt64(breakpoint["length"]));
|
|
Assert.True(Assert.IsType<bool>(breakpoint["enabled"]));
|
|
|
|
var listed = dispatcher.Dispatch(Parse("""{"command":"list-breakpoints"}"""));
|
|
Assert.True(listed.Ok);
|
|
var list = Assert.IsAssignableFrom<System.Collections.IEnumerable>(listed.Data!["breakpoints"]);
|
|
Assert.Single(list.Cast<object?>());
|
|
|
|
var disabled = dispatcher.Dispatch(Parse(
|
|
"""{"command":"enable-breakpoint","id":1,"enabled":false}"""));
|
|
Assert.True(disabled.Ok);
|
|
Assert.False(session.Breakpoints.Snapshot().Single().Enabled);
|
|
|
|
var removed = dispatcher.Dispatch(Parse(
|
|
"""{"command":"remove-breakpoint","id":1}"""));
|
|
Assert.True(removed.Ok);
|
|
Assert.Empty(session.Breakpoints.Snapshot());
|
|
|
|
var missing = dispatcher.Dispatch(Parse(
|
|
"""{"command":"remove-breakpoint","id":1}"""));
|
|
Assert.False(missing.Ok);
|
|
Assert.Contains("No breakpoint", missing.Error, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dispatch_PingAndState_Succeed()
|
|
{
|
|
var session = new FakeDebuggerSession(DebuggerRunState.Running);
|
|
var dispatcher = new DebugCommandDispatcher(session);
|
|
|
|
var ping = dispatcher.Dispatch(Parse("""{"command":"ping"}"""));
|
|
var state = dispatcher.Dispatch(Parse("""{"command":"state"}"""));
|
|
|
|
Assert.True(ping.Ok);
|
|
Assert.True(state.Ok);
|
|
Assert.Equal("Running", GetString(state.Data!, "state"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Dispatch_Pause_RequestsPause()
|
|
{
|
|
var session = new FakeDebuggerSession(DebuggerRunState.Running);
|
|
var dispatcher = new DebugCommandDispatcher(session);
|
|
|
|
var response = dispatcher.Dispatch(Parse("""{"command":"pause"}"""));
|
|
|
|
Assert.True(response.Ok);
|
|
Assert.True(session.PauseRequested);
|
|
}
|
|
|
|
[Fact]
|
|
public void Dispatch_ParseErrorCommand_SurfacesMessage()
|
|
{
|
|
var dispatcher = CreateDispatcher();
|
|
|
|
var response = dispatcher.Dispatch(Parse(
|
|
$$"""{"command":"{{JsonLineDebugProtocol.ParseErrorCommand}}","message":"bad line"}"""));
|
|
|
|
Assert.False(response.Ok);
|
|
Assert.Equal("bad line", response.Error);
|
|
}
|
|
|
|
private static DebugCommandDispatcher CreateDispatcher()
|
|
=> new(new FakeDebuggerSession());
|
|
|
|
private static DebugRequest Parse(string json)
|
|
{
|
|
Assert.True(DebugRequest.TryParse(json, out var request, out var error), error);
|
|
return request;
|
|
}
|
|
|
|
private static string GetString(IReadOnlyDictionary<string, object?> data, string key)
|
|
{
|
|
Assert.True(data.TryGetValue(key, out var value));
|
|
Assert.NotNull(value);
|
|
return Assert.IsType<string>(value);
|
|
}
|
|
|
|
private static IReadOnlyDictionary<string, object?> GetDict(
|
|
IReadOnlyDictionary<string, object?> data,
|
|
string key)
|
|
{
|
|
Assert.True(data.TryGetValue(key, out var value));
|
|
Assert.NotNull(value);
|
|
return Assert.IsAssignableFrom<IReadOnlyDictionary<string, object?>>(value);
|
|
}
|
|
}
|