#![allow(dead_code)]
use std::collections::VecDeque;
use std::sync::Arc;
use serde_json::{Value, json};
use crate::debug::SourceMode;
use crate::debug::source_map::SourceMap;
use crate::debug::types::{
ResolvedBreakpoint, Scope, SessionCommand, SessionEvent, SourceRef, StopReason, ThreadInfo,
Variable,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedSource {
pub source: Value,
pub line: u32,
}
pub type SourceResolver = Box<dyn Fn(&str, u32) -> ResolvedSource + Send>;
pub fn default_source_resolver() -> SourceResolver {
Box::new(|lua_source: &str, lua_line: u32| ResolvedSource {
source: json!({ "path": lua_source }),
line: lua_line,
})
}
pub fn pasta_source_resolver(map: Arc<SourceMap>) -> SourceResolver {
Box::new(move |lua_source: &str, lua_line: u32| {
match map.resolve_lua_to_pasta(lua_source, lua_line) {
Some(pos) => ResolvedSource {
source: json!({ "path": pos.file }),
line: pos.line,
},
None => default_source_resolver()(lua_source, lua_line),
}
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum PendingKind {
SetBreakpoints,
Threads,
StackTrace,
Variables,
}
#[derive(Debug, Default)]
struct PendingTable {
set_breakpoints: VecDeque<u64>,
threads: VecDeque<u64>,
stack_trace: VecDeque<u64>,
variables: VecDeque<u64>,
}
impl PendingTable {
fn push(&mut self, kind: PendingKind, request_seq: u64) {
match kind {
PendingKind::SetBreakpoints => self.set_breakpoints.push_back(request_seq),
PendingKind::Threads => self.threads.push_back(request_seq),
PendingKind::StackTrace => self.stack_trace.push_back(request_seq),
PendingKind::Variables => self.variables.push_back(request_seq),
}
}
fn pop(&mut self, kind: PendingKind) -> Option<u64> {
match kind {
PendingKind::SetBreakpoints => self.set_breakpoints.pop_front(),
PendingKind::Threads => self.threads.pop_front(),
PendingKind::StackTrace => self.stack_trace.pop_front(),
PendingKind::Variables => self.variables.pop_front(),
}
}
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Decoded {
pub command: Option<SessionCommand>,
pub response: Option<Value>,
pub events: Vec<Value>,
pub attach_source_mode: Option<SourceMode>,
}
pub struct DapAdapter {
out_seq: u64,
pending: PendingTable,
source_resolver: SourceResolver,
}
impl std::fmt::Debug for DapAdapter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DapAdapter")
.field("out_seq", &self.out_seq)
.field("pending", &self.pending)
.field("source_resolver", &"<SourceResolver>")
.finish()
}
}
impl Default for DapAdapter {
fn default() -> Self {
Self {
out_seq: 0,
pending: PendingTable::default(),
source_resolver: default_source_resolver(),
}
}
}
impl DapAdapter {
pub fn new() -> Self {
Self::default()
}
pub fn set_source_resolver(&mut self, resolver: SourceResolver) {
self.source_resolver = resolver;
}
fn next_seq(&mut self) -> u64 {
self.out_seq += 1;
self.out_seq
}
fn response(&mut self, request_seq: u64, command: &str, body: Value) -> Value {
let seq = self.next_seq();
let mut msg = json!({
"seq": seq,
"type": "response",
"request_seq": request_seq,
"success": true,
"command": command,
});
if !body.is_null() {
msg["body"] = body;
}
msg
}
fn event(&mut self, event: &str, body: Value) -> Value {
let seq = self.next_seq();
json!({
"seq": seq,
"type": "event",
"event": event,
"body": body,
})
}
pub fn decode_request(&mut self, req: &Value) -> Decoded {
let request_seq = req.get("seq").and_then(Value::as_u64).unwrap_or(0);
let command = req.get("command").and_then(Value::as_str).unwrap_or("");
let args = req.get("arguments");
match command {
"initialize" => {
let response = self.response(
request_seq,
"initialize",
json!({
"supportsConfigurationDoneRequest": true,
}),
);
let initialized = self.event("initialized", json!({}));
Decoded {
command: None,
response: Some(response),
events: vec![initialized],
attach_source_mode: None,
}
}
"setBreakpoints" => {
let (source, lines) = parse_set_breakpoints(args);
self.pending.push(PendingKind::SetBreakpoints, request_seq);
Decoded {
command: Some(SessionCommand::SetBreakpoints { source, lines }),
response: None,
events: Vec::new(),
attach_source_mode: None,
}
}
"configurationDone" => {
let response = self.response(request_seq, "configurationDone", Value::Null);
Decoded {
command: None,
response: Some(response),
events: Vec::new(),
attach_source_mode: None,
}
}
"threads" => {
self.pending.push(PendingKind::Threads, request_seq);
Decoded {
command: Some(SessionCommand::Threads),
response: None,
events: Vec::new(),
attach_source_mode: None,
}
}
"stackTrace" => {
self.pending.push(PendingKind::StackTrace, request_seq);
Decoded {
command: Some(SessionCommand::StackTrace),
response: None,
events: Vec::new(),
attach_source_mode: None,
}
}
"scopes" => {
let frame_id = args
.and_then(|a| a.get("frameId"))
.and_then(Value::as_u64)
.unwrap_or(0) as u32;
let var_ref = frame_id + 1;
let response = self.response(
request_seq,
"scopes",
json!({
"scopes": [{
"name": "Locals",
"variablesReference": var_ref,
"expensive": false,
}],
}),
);
Decoded {
command: Some(SessionCommand::Scopes { frame_id }),
response: Some(response),
events: Vec::new(),
attach_source_mode: None,
}
}
"variables" => {
let var_ref = args
.and_then(|a| a.get("variablesReference"))
.and_then(Value::as_u64)
.unwrap_or(0) as u32;
self.pending.push(PendingKind::Variables, request_seq);
Decoded {
command: Some(SessionCommand::Variables { var_ref }),
response: None,
events: Vec::new(),
attach_source_mode: None,
}
}
"continue" => {
let response = self.response(
request_seq,
"continue",
json!({ "allThreadsContinued": true }),
);
Decoded {
command: Some(SessionCommand::Continue),
response: Some(response),
events: Vec::new(),
attach_source_mode: None,
}
}
"next" => self.step_ack(request_seq, "next", SessionCommand::Next),
"stepIn" => self.step_ack(request_seq, "stepIn", SessionCommand::StepIn),
"stepOut" => self.step_ack(request_seq, "stepOut", SessionCommand::StepOut),
"attach" => {
let attach_source_mode = args
.and_then(|a| a.get("sourcePresentation"))
.and_then(Value::as_str)
.map(SourceMode::parse);
let response = self.response(request_seq, "attach", Value::Null);
Decoded {
command: None,
response: Some(response),
events: Vec::new(),
attach_source_mode,
}
}
"disconnect" => {
let response = self.response(request_seq, "disconnect", Value::Null);
Decoded {
command: Some(SessionCommand::Disconnect),
response: Some(response),
events: Vec::new(),
attach_source_mode: None,
}
}
_ => Decoded::default(),
}
}
fn step_ack(&mut self, request_seq: u64, command: &str, cmd: SessionCommand) -> Decoded {
let response = self.response(request_seq, command, Value::Null);
Decoded {
command: Some(cmd),
response: Some(response),
events: Vec::new(),
attach_source_mode: None,
}
}
pub fn encode_event(&mut self, event: SessionEvent) -> Vec<Value> {
match event {
SessionEvent::Stopped { reason, thread_id } => {
let body = json!({
"reason": stop_reason_str(reason),
"threadId": thread_id,
"allThreadsStopped": true,
});
vec![self.event("stopped", body)]
}
SessionEvent::Terminated => vec![self.event("terminated", json!({}))],
SessionEvent::Breakpoints(bps) => {
let request_seq = self.pending.pop(PendingKind::SetBreakpoints).unwrap_or(0);
let body = json!({ "breakpoints": encode_breakpoints(&bps) });
vec![self.response(request_seq, "setBreakpoints", body)]
}
SessionEvent::Threads(threads) => {
let request_seq = self.pending.pop(PendingKind::Threads).unwrap_or(0);
let body = json!({ "threads": encode_threads(&threads) });
vec![self.response(request_seq, "threads", body)]
}
SessionEvent::Stack(frames) => {
let request_seq = self.pending.pop(PendingKind::StackTrace).unwrap_or(0);
let total = frames.len();
let body = json!({
"stackFrames": encode_frames(&frames, self.source_resolver.as_ref()),
"totalFrames": total,
});
vec![self.response(request_seq, "stackTrace", body)]
}
SessionEvent::Scopes(scopes) => {
let _ = scopes;
Vec::new()
}
SessionEvent::Variables(vars) => {
let request_seq = self.pending.pop(PendingKind::Variables).unwrap_or(0);
let body = json!({ "variables": encode_variables(&vars) });
vec![self.response(request_seq, "variables", body)]
}
SessionEvent::Error(msg) => {
let body = json!({
"category": "stderr",
"output": format!("{msg}\n"),
});
vec![self.event("output", body)]
}
}
}
}
fn stop_reason_str(reason: StopReason) -> &'static str {
match reason {
StopReason::Breakpoint => "breakpoint",
StopReason::Step => "step",
StopReason::Entry => "entry",
StopReason::Pause => "pause",
}
}
fn parse_set_breakpoints(args: Option<&Value>) -> (SourceRef, Vec<u32>) {
let path = args
.and_then(|a| a.get("source"))
.and_then(|s| s.get("path"))
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let lines = args
.and_then(|a| a.get("breakpoints"))
.and_then(Value::as_array)
.map(|arr| {
arr.iter()
.filter_map(|bp| bp.get("line").and_then(Value::as_u64).map(|l| l as u32))
.collect()
})
.unwrap_or_default();
(SourceRef { path }, lines)
}
fn encode_breakpoints(bps: &[ResolvedBreakpoint]) -> Vec<Value> {
bps.iter()
.map(|bp| {
json!({
"verified": bp.verified,
"line": bp.line,
})
})
.collect()
}
fn encode_threads(threads: &[ThreadInfo]) -> Vec<Value> {
threads
.iter()
.map(|t| json!({ "id": t.id, "name": t.name }))
.collect()
}
fn encode_frames(
frames: &[crate::debug::types::FrameInfo],
resolver: &(dyn Fn(&str, u32) -> ResolvedSource + Send),
) -> Vec<Value> {
frames
.iter()
.enumerate()
.map(|(idx, f)| {
let resolved = resolver(&f.source, f.line);
json!({
"id": idx as u32,
"name": f.func_name.clone().unwrap_or_else(|| "?".to_string()),
"source": resolved.source,
"line": resolved.line,
"column": 1,
})
})
.collect()
}
fn encode_variables(vars: &[Variable]) -> Vec<Value> {
vars.iter()
.map(|v| {
json!({
"name": v.name,
"value": v.repr,
"type": v.type_name,
"variablesReference": 0,
})
})
.collect()
}
fn encode_scope(scope: &Scope) -> Value {
json!({
"name": scope.name,
"variablesReference": scope.variables_reference,
"expensive": false,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::debug::types::FrameInfo;
fn request(seq: u64, command: &str, arguments: Value) -> Value {
json!({
"seq": seq,
"type": "request",
"command": command,
"arguments": arguments,
})
}
#[test]
fn initialize_advertises_capabilities_and_emits_initialized() {
let mut dap = DapAdapter::new();
let decoded = dap.decode_request(&request(1, "initialize", json!({ "adapterID": "pasta" })));
assert_eq!(decoded.command, None);
let resp = decoded.response.expect("initialize must produce a response");
assert_eq!(resp["type"], "response");
assert_eq!(resp["command"], "initialize");
assert_eq!(resp["request_seq"], 1);
assert_eq!(resp["success"], true);
assert_eq!(
resp["body"]["supportsConfigurationDoneRequest"], true,
"R3.2: initialize must advertise supportsConfigurationDoneRequest"
);
assert_eq!(decoded.events.len(), 1, "initialize emits one event");
let ev = &decoded.events[0];
assert_eq!(ev["type"], "event");
assert_eq!(ev["event"], "initialized");
assert_eq!(resp["seq"], 1);
assert_eq!(ev["seq"], 2);
}
#[test]
fn set_breakpoints_decodes_command_and_correlates_response() {
let mut dap = DapAdapter::new();
let decoded = dap.decode_request(&request(
5,
"setBreakpoints",
json!({
"source": { "path": "@scene.lua" },
"breakpoints": [{ "line": 3 }, { "line": 7 }],
}),
));
assert_eq!(
decoded.command,
Some(SessionCommand::SetBreakpoints {
source: SourceRef::new("@scene.lua"),
lines: vec![3, 7],
}),
"R3.3: setBreakpoints → SetBreakpoints{{source,lines}}"
);
assert!(decoded.response.is_none());
let out = dap.encode_event(SessionEvent::Breakpoints(vec![
ResolvedBreakpoint {
source: SourceRef::new("@scene.lua"),
line: 3,
verified: true,
},
ResolvedBreakpoint {
source: SourceRef::new("@scene.lua"),
line: 7,
verified: false,
},
]));
assert_eq!(out.len(), 1);
let resp = &out[0];
assert_eq!(resp["type"], "response");
assert_eq!(resp["command"], "setBreakpoints");
assert_eq!(resp["request_seq"], 5, "deferred response carries originating seq");
assert_eq!(resp["success"], true);
let bps = resp["body"]["breakpoints"].as_array().expect("breakpoints array");
assert_eq!(bps.len(), 2);
assert_eq!(bps[0]["verified"], true);
assert_eq!(bps[0]["line"], 3);
assert_eq!(bps[1]["verified"], false);
assert_eq!(bps[1]["line"], 7);
}
#[test]
fn configuration_done_acks_without_command() {
let mut dap = DapAdapter::new();
let decoded = dap.decode_request(&request(2, "configurationDone", json!({})));
assert_eq!(decoded.command, None);
let resp = decoded.response.expect("ack response");
assert_eq!(resp["type"], "response");
assert_eq!(resp["command"], "configurationDone");
assert_eq!(resp["request_seq"], 2);
assert_eq!(resp["success"], true);
assert!(resp.get("body").is_none(), "ack has no body");
}
#[test]
fn threads_decodes_command_and_correlates_response() {
let mut dap = DapAdapter::new();
let decoded = dap.decode_request(&request(8, "threads", json!({})));
assert_eq!(decoded.command, Some(SessionCommand::Threads));
assert!(decoded.response.is_none());
let out = dap.encode_event(SessionEvent::Threads(vec![ThreadInfo {
id: 1,
name: "main".to_string(),
}]));
assert_eq!(out.len(), 1);
let resp = &out[0];
assert_eq!(resp["command"], "threads");
assert_eq!(resp["request_seq"], 8);
let threads = resp["body"]["threads"].as_array().expect("threads array");
assert_eq!(threads.len(), 1);
assert_eq!(threads[0]["id"], 1);
assert_eq!(threads[0]["name"], "main");
}
#[test]
fn stack_trace_decodes_command_and_correlates_response() {
let mut dap = DapAdapter::new();
let decoded = dap.decode_request(&request(11, "stackTrace", json!({ "threadId": 1 })));
assert_eq!(decoded.command, Some(SessionCommand::StackTrace));
assert!(decoded.response.is_none());
let out = dap.encode_event(SessionEvent::Stack(vec![
FrameInfo {
source: "@scene.lua".to_string(),
line: 7,
func_name: Some("talk".to_string()),
},
FrameInfo {
source: "@scene.lua".to_string(),
line: 2,
func_name: None,
},
]));
assert_eq!(out.len(), 1);
let resp = &out[0];
assert_eq!(resp["command"], "stackTrace");
assert_eq!(resp["request_seq"], 11);
assert_eq!(resp["body"]["totalFrames"], 2);
let frames = resp["body"]["stackFrames"].as_array().expect("stackFrames array");
assert_eq!(frames.len(), 2);
assert_eq!(frames[0]["id"], 0, "frame id = stack index");
assert_eq!(frames[0]["name"], "talk");
assert_eq!(frames[0]["source"]["path"], "@scene.lua");
assert_eq!(frames[0]["line"], 7);
assert_eq!(frames[0]["column"], 1);
assert_eq!(frames[1]["id"], 1);
assert_eq!(frames[1]["name"], "?", "missing func name → placeholder");
}
#[test]
fn scopes_immediately_returns_locals_scope_with_decodable_ref() {
let mut dap = DapAdapter::new();
let decoded = dap.decode_request(&request(13, "scopes", json!({ "frameId": 2 })));
assert_eq!(
decoded.command,
Some(SessionCommand::Scopes { frame_id: 2 }),
"scopes → Scopes{{frame_id}}"
);
let resp = decoded.response.expect("scopes answered immediately");
assert_eq!(resp["command"], "scopes");
assert_eq!(resp["request_seq"], 13);
let scopes = resp["body"]["scopes"].as_array().expect("scopes array");
assert_eq!(scopes.len(), 1);
assert_eq!(scopes[0]["name"], "Locals");
assert_eq!(scopes[0]["variablesReference"], 3);
}
#[test]
fn variables_decodes_command_and_maps_fields() {
let mut dap = DapAdapter::new();
let decoded = dap.decode_request(&request(15, "variables", json!({ "variablesReference": 3 })));
assert_eq!(
decoded.command,
Some(SessionCommand::Variables { var_ref: 3 }),
"variables → Variables{{var_ref}}"
);
assert!(decoded.response.is_none());
let out = dap.encode_event(SessionEvent::Variables(vec![
Variable {
name: "x".to_string(),
type_name: "number".to_string(),
repr: "42".to_string(),
},
Variable {
name: "s".to_string(),
type_name: "string".to_string(),
repr: "\"hi\"".to_string(),
},
]));
assert_eq!(out.len(), 1);
let resp = &out[0];
assert_eq!(resp["command"], "variables");
assert_eq!(resp["request_seq"], 15);
let vars = resp["body"]["variables"].as_array().expect("variables array");
assert_eq!(vars.len(), 2);
assert_eq!(vars[0]["name"], "x");
assert_eq!(vars[0]["value"], "42");
assert_eq!(vars[0]["type"], "number");
assert_eq!(vars[0]["variablesReference"], 0);
assert_eq!(vars[1]["name"], "s");
assert_eq!(vars[1]["value"], "\"hi\"");
assert_eq!(vars[1]["type"], "string");
}
#[test]
fn continue_acks_and_forwards_command() {
let mut dap = DapAdapter::new();
let decoded = dap.decode_request(&request(20, "continue", json!({ "threadId": 1 })));
assert_eq!(decoded.command, Some(SessionCommand::Continue));
let resp = decoded.response.expect("continue acks");
assert_eq!(resp["command"], "continue");
assert_eq!(resp["request_seq"], 20);
assert_eq!(resp["body"]["allThreadsContinued"], true);
}
#[test]
fn step_commands_ack_and_forward() {
for (command, expected) in [
("next", SessionCommand::Next),
("stepIn", SessionCommand::StepIn),
("stepOut", SessionCommand::StepOut),
] {
let mut dap = DapAdapter::new();
let decoded = dap.decode_request(&request(30, command, json!({ "threadId": 1 })));
assert_eq!(decoded.command, Some(expected), "{command} → step command");
let resp = decoded.response.expect("step acks");
assert_eq!(resp["command"], command);
assert_eq!(resp["request_seq"], 30);
assert_eq!(resp["success"], true);
assert!(resp.get("body").is_none(), "step ack has no body");
}
}
#[test]
fn disconnect_acks_forwards_and_later_terminates() {
let mut dap = DapAdapter::new();
let decoded = dap.decode_request(&request(40, "disconnect", json!({})));
assert_eq!(decoded.command, Some(SessionCommand::Disconnect));
let resp = decoded.response.expect("disconnect acks");
assert_eq!(resp["command"], "disconnect");
assert_eq!(resp["request_seq"], 40);
let out = dap.encode_event(SessionEvent::Terminated);
assert_eq!(out.len(), 1);
assert_eq!(out[0]["type"], "event");
assert_eq!(out[0]["event"], "terminated");
}
#[test]
fn stopped_event_maps_each_reason_and_thread() {
for (reason, expected) in [
(StopReason::Breakpoint, "breakpoint"),
(StopReason::Step, "step"),
(StopReason::Entry, "entry"),
(StopReason::Pause, "pause"),
] {
let mut dap = DapAdapter::new();
let out = dap.encode_event(SessionEvent::Stopped {
reason,
thread_id: 1,
});
assert_eq!(out.len(), 1);
let ev = &out[0];
assert_eq!(ev["type"], "event");
assert_eq!(ev["event"], "stopped");
assert_eq!(ev["body"]["reason"], expected, "R3.4: reason mapping");
assert_eq!(ev["body"]["threadId"], 1);
}
}
#[test]
fn terminated_event_encoded() {
let mut dap = DapAdapter::new();
let out = dap.encode_event(SessionEvent::Terminated);
assert_eq!(out.len(), 1);
assert_eq!(out[0]["type"], "event");
assert_eq!(out[0]["event"], "terminated");
}
#[test]
fn error_maps_to_output_event() {
let mut dap = DapAdapter::new();
let out = dap.encode_event(SessionEvent::Error("lua boom".to_string()));
assert_eq!(out.len(), 1);
let ev = &out[0];
assert_eq!(ev["type"], "event");
assert_eq!(ev["event"], "output");
assert_eq!(ev["body"]["category"], "stderr");
assert!(
ev["body"]["output"].as_str().unwrap().contains("lua boom"),
"error message surfaced in output event"
);
}
#[test]
fn outgoing_seq_is_monotonic_across_responses_and_events() {
let mut dap = DapAdapter::new();
let init = dap.decode_request(&request(1, "initialize", json!({})));
assert_eq!(init.response.unwrap()["seq"], 1);
assert_eq!(init.events[0]["seq"], 2);
let stopped = dap.encode_event(SessionEvent::Stopped {
reason: StopReason::Breakpoint,
thread_id: 1,
});
assert_eq!(stopped[0]["seq"], 3);
let cfg = dap.decode_request(&request(2, "configurationDone", json!({})));
assert_eq!(cfg.response.unwrap()["seq"], 4);
}
#[test]
fn deferred_responses_correlate_in_fifo_order_per_kind() {
let mut dap = DapAdapter::new();
dap.decode_request(&request(100, "stackTrace", json!({})));
dap.decode_request(&request(101, "stackTrace", json!({})));
let first = dap.encode_event(SessionEvent::Stack(vec![]));
assert_eq!(first[0]["request_seq"], 100, "first event pairs to first request");
let second = dap.encode_event(SessionEvent::Stack(vec![]));
assert_eq!(second[0]["request_seq"], 101, "second event pairs to second request");
}
#[test]
fn unknown_request_is_ignored() {
let mut dap = DapAdapter::new();
let decoded = dap.decode_request(&request(99, "evaluate", json!({})));
assert_eq!(decoded, Decoded::default(), "unknown command yields empty decode");
}
#[test]
fn attach_parses_explicit_source_presentation() {
for (raw, expected) in [("lua", SourceMode::Lua), ("pasta", SourceMode::Pasta)] {
let mut dap = DapAdapter::new();
let decoded = dap.decode_request(&request(
3,
"attach",
json!({ "sourcePresentation": raw }),
));
assert_eq!(
decoded.attach_source_mode,
Some(expected),
"explicit sourcePresentation={raw:?} must parse to {expected:?} (R6.3)"
);
assert_eq!(decoded.command, None);
let resp = decoded.response.expect("attach must ack");
assert_eq!(resp["command"], "attach");
assert_eq!(resp["request_seq"], 3);
assert_eq!(resp["success"], true);
}
}
#[test]
fn attach_invalid_source_presentation_falls_back_to_pasta() {
let mut dap = DapAdapter::new();
let decoded = dap.decode_request(&request(
3,
"attach",
json!({ "sourcePresentation": "garbage" }),
));
assert_eq!(
decoded.attach_source_mode,
Some(SourceMode::Pasta),
"invalid sourcePresentation → default pasta (design 615)"
);
}
#[test]
fn attach_without_source_presentation_is_none() {
let mut dap = DapAdapter::new();
let decoded = dap.decode_request(&request(3, "attach", json!({ "host": "127.0.0.1" })));
assert_eq!(
decoded.attach_source_mode, None,
"absent sourcePresentation must NOT override the resolved mode (design 581)"
);
let resp = decoded.response.expect("attach must ack even without the arg");
assert_eq!(resp["command"], "attach");
}
#[test]
fn stack_trace_default_resolver_presents_generated_lua() {
let mut dap = DapAdapter::new();
dap.decode_request(&request(11, "stackTrace", json!({ "threadId": 1 })));
let out = dap.encode_event(SessionEvent::Stack(vec![
FrameInfo {
source: "@scene.lua".to_string(),
line: 7,
func_name: Some("talk".to_string()),
},
FrameInfo {
source: "@scene.lua".to_string(),
line: 2,
func_name: None,
},
]));
let resp = &out[0];
let frames = resp["body"]["stackFrames"].as_array().expect("stackFrames array");
assert_eq!(frames[0]["source"], json!({ "path": "@scene.lua" }));
assert_eq!(frames[0]["line"], 7);
assert_eq!(frames[1]["source"], json!({ "path": "@scene.lua" }));
assert_eq!(frames[1]["line"], 2);
}
#[test]
fn stack_trace_alternate_resolver_presents_pasta() {
let mut dap = DapAdapter::new();
dap.set_source_resolver(Box::new(|_lua_source: &str, lua_line: u32| {
ResolvedSource {
source: json!({ "path": "foo.pasta" }),
line: lua_line + 100,
}
}));
dap.decode_request(&request(11, "stackTrace", json!({ "threadId": 1 })));
let out = dap.encode_event(SessionEvent::Stack(vec![FrameInfo {
source: "@scene.lua".to_string(),
line: 7,
func_name: Some("talk".to_string()),
}]));
let resp = &out[0];
let frames = resp["body"]["stackFrames"].as_array().expect("stackFrames array");
assert_eq!(frames[0]["source"], json!({ "path": "foo.pasta" }));
assert_eq!(frames[0]["line"], 107);
assert_eq!(frames[0]["id"], 0);
assert_eq!(frames[0]["name"], "talk");
}
#[test]
fn set_breakpoints_with_no_breakpoints_clears_lines() {
let mut dap = DapAdapter::new();
let decoded = dap.decode_request(&request(
7,
"setBreakpoints",
json!({ "source": { "path": "@s.lua" } }),
));
assert_eq!(
decoded.command,
Some(SessionCommand::SetBreakpoints {
source: SourceRef::new("@s.lua"),
lines: vec![],
}),
"missing breakpoints array → empty (clears) line set"
);
}
use std::sync::Arc;
use crate::debug::source_map::{ChunkSourceMap, PastaPos, SourceMap};
fn map_with(chunk_name: &str, lua_line: u32, file: &str, pasta_line: u32) -> SourceMap {
let mut forward = std::collections::BTreeMap::new();
forward.insert(
lua_line,
PastaPos {
file: file.to_string(),
line: pasta_line,
},
);
let mut sm = SourceMap::new();
sm.insert_chunk(
chunk_name.to_string(),
file.to_string(),
ChunkSourceMap::from_forward(forward),
);
sm
}
#[test]
fn pasta_resolver_maps_frame_to_pasta_source_and_line() {
let raw_hook_source = r"@C:\proj\cache\scene.lua";
let map = map_with("C:/proj/cache/scene.lua", 12, "C:/proj/scene.pasta", 7);
let resolver = pasta_source_resolver(Arc::new(map));
let resolved = resolver(raw_hook_source, 12);
assert_eq!(
resolved.source,
json!({ "path": "C:/proj/scene.pasta" }),
"R5.1/R5.2: 対応ありフレームは `.pasta` パスを提示する"
);
assert_eq!(resolved.line, 7, "R5.1/R5.2: 提示行は `.pasta` 行 (pos.line)");
}
#[test]
fn pasta_resolver_falls_back_to_lua_for_unmapped() {
let map = map_with("C:/proj/cache/scene.lua", 12, "C:/proj/scene.pasta", 7);
let resolver = pasta_source_resolver(Arc::new(map));
let resolved = resolver(r"@C:\proj\cache\scene.lua", 99);
let expected = default_source_resolver()(r"@C:\proj\cache\scene.lua", 99);
assert_eq!(
resolved, expected,
"R5.3: 未対応行は既定 `.lua` resolver と同一の提示へフォールバックする"
);
assert_eq!(resolved.source, json!({ "path": r"@C:\proj\cache\scene.lua" }));
assert_eq!(resolved.line, 99);
}
#[test]
fn pasta_resolver_falls_back_to_lua_for_unknown_chunk() {
let map = map_with("C:/proj/cache/scene.lua", 12, "C:/proj/scene.pasta", 7);
let resolver = pasta_source_resolver(Arc::new(map));
let resolved = resolver("@C:/proj/cache/other.lua", 12);
let expected = default_source_resolver()("@C:/proj/cache/other.lua", 12);
assert_eq!(
resolved, expected,
"R5.3: 未知 chunk は `.lua` フォールバック(誤マッピング禁止)"
);
}
#[test]
fn stack_trace_with_pasta_resolver_presents_each_frame() {
let map = map_with("C:/proj/cache/scene.lua", 7, "C:/proj/scene.pasta", 3);
let mut dap = DapAdapter::new();
dap.set_source_resolver(pasta_source_resolver(Arc::new(map)));
dap.decode_request(&request(11, "stackTrace", json!({ "threadId": 1 })));
let out = dap.encode_event(SessionEvent::Stack(vec![
FrameInfo {
source: r"@C:\proj\cache\scene.lua".to_string(),
line: 7,
func_name: Some("talk".to_string()),
},
FrameInfo {
source: r"@C:\proj\cache\scene.lua".to_string(),
line: 2,
func_name: None,
},
]));
let resp = &out[0];
let frames = resp["body"]["stackFrames"].as_array().expect("stackFrames array");
assert_eq!(frames[0]["source"], json!({ "path": "C:/proj/scene.pasta" }));
assert_eq!(frames[0]["line"], 3);
assert_eq!(frames[1]["source"], json!({ "path": r"@C:\proj\cache\scene.lua" }));
assert_eq!(frames[1]["line"], 2);
}
}