use crate::debug::source_map::ChunkName;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Variable {
pub name: String,
pub type_name: String,
pub repr: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FrameInfo {
pub source: String,
pub line: u32,
pub func_name: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StopReason {
Breakpoint,
Step,
Entry,
Pause,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SourceRef {
pub path: String,
}
impl SourceRef {
pub fn new(path: impl Into<String>) -> Self {
Self { path: path.into() }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Breakpoint {
pub present_source: String,
pub chunk: ChunkName,
pub lua_line: u32,
}
impl Breakpoint {
pub fn new(present_source: impl Into<String>, chunk: impl Into<ChunkName>, lua_line: u32) -> Self {
Self {
present_source: present_source.into(),
chunk: chunk.into(),
lua_line,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedBreakpoint {
pub source: SourceRef,
pub line: u32,
pub verified: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Scope {
pub name: String,
pub variables_reference: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ThreadInfo {
pub id: u32,
pub name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ThreadId(pub usize);
impl ThreadId {
pub fn from_state(p: *mut mlua::ffi::lua_State) -> Self {
Self(p as usize)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LineEvent {
pub source: String,
pub line: u32,
pub thread_ptr: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionCommand {
SetBreakpoints {
source: SourceRef,
lines: Vec<u32>,
},
Continue,
Next,
StepIn,
StepOut,
StackTrace,
Scopes {
frame_id: u32,
},
Variables {
var_ref: u32,
},
Threads,
RefreshPresentation,
Disconnect,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionEvent {
Stopped {
reason: StopReason,
thread_id: u32,
},
Terminated,
Breakpoints(Vec<ResolvedBreakpoint>),
Stack(Vec<FrameInfo>),
Scopes(Vec<Scope>),
Variables(Vec<Variable>),
Threads(Vec<ThreadInfo>),
Error(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn variable_round_trip() {
let v = Variable {
name: "x".to_string(),
type_name: "number".to_string(),
repr: "42".to_string(),
};
let cloned = v.clone();
assert_eq!(v, cloned);
assert_eq!(cloned.name, "x");
assert_eq!(cloned.type_name, "number");
assert_eq!(cloned.repr, "42");
}
#[test]
fn frame_info_round_trip() {
let f = FrameInfo {
source: "@scene.lua".to_string(),
line: 7,
func_name: Some("talk".to_string()),
};
let cloned = f.clone();
assert_eq!(f, cloned);
assert_eq!(cloned.source, "@scene.lua");
assert_eq!(cloned.line, 7);
assert_eq!(cloned.func_name.as_deref(), Some("talk"));
}
#[test]
fn session_event_error_carries_stringified_message() {
let ev = SessionEvent::Error("lua boom".to_string());
match ev {
SessionEvent::Error(msg) => assert_eq!(msg, "lua boom"),
other => panic!("expected Error, got {other:?}"),
}
}
#[test]
fn source_ref_round_trip() {
let s = SourceRef::new("@scene.lua");
assert_eq!(s, SourceRef { path: "@scene.lua".to_string() });
assert_eq!(s.clone().path, "@scene.lua");
}
#[test]
fn resolved_breakpoint_round_trip() {
let bp = ResolvedBreakpoint {
source: SourceRef::new("@scene.lua"),
line: 12,
verified: true,
};
let cloned = bp.clone();
assert_eq!(bp, cloned);
assert_eq!(cloned.line, 12);
assert!(cloned.verified);
assert_eq!(cloned.source.path, "@scene.lua");
}
#[test]
fn breakpoint_two_tier_is_hashable_and_keyed_by_all_fields() {
use std::collections::HashSet;
let bp = Breakpoint::new("x.pasta", "@scene.lua", 3);
let mut set: HashSet<Breakpoint> = HashSet::new();
set.insert(bp.clone());
assert!(set.contains(&bp));
let other_present = Breakpoint::new("scene.lua", "@scene.lua", 3);
assert_ne!(bp, other_present, "present_source participates in identity");
set.insert(other_present.clone());
assert_eq!(set.len(), 2, "distinct present sources are distinct entries");
assert!(set.contains(&other_present));
assert_eq!(bp.present_source, "x.pasta");
assert_eq!(bp.chunk, "@scene.lua");
assert_eq!(bp.lua_line, 3);
}
#[test]
fn scope_and_thread_info_round_trip() {
let scope = Scope {
name: "Locals".to_string(),
variables_reference: 1001,
};
assert_eq!(scope.clone(), scope);
assert_eq!(scope.variables_reference, 1001);
let t = ThreadInfo {
id: 1,
name: "main".to_string(),
};
assert_eq!(t.clone(), t);
assert_eq!(t.id, 1);
assert_eq!(t.name, "main");
}
#[test]
fn thread_id_identity_and_from_state() {
let a = ThreadId(0xDEAD);
let b = ThreadId(0xDEAD);
let c = ThreadId(0xBEEF);
assert_eq!(a, b);
assert_ne!(a, c);
let p: *mut mlua::ffi::lua_State = std::ptr::null_mut();
assert_eq!(ThreadId::from_state(p), ThreadId(0));
}
#[test]
fn line_event_round_trip() {
let ev = LineEvent {
source: "@scene.lua".to_string(),
line: 5,
thread_ptr: 0x1234,
};
assert_eq!(ev.clone(), ev);
assert_eq!(ev.line, 5);
assert_eq!(ev.thread_ptr, 0x1234);
}
#[test]
fn session_command_variants_construct() {
let set = SessionCommand::SetBreakpoints {
source: SourceRef::new("@scene.lua"),
lines: vec![1, 2, 3],
};
assert_eq!(set.clone(), set);
assert_eq!(SessionCommand::Continue, SessionCommand::Continue);
assert_ne!(SessionCommand::Next, SessionCommand::StepIn);
let scopes = SessionCommand::Scopes { frame_id: 0 };
let vars = SessionCommand::Variables { var_ref: 1001 };
assert_ne!(scopes, vars);
}
#[test]
fn session_event_stopped_carries_reason_and_thread() {
let ev = SessionEvent::Stopped {
reason: StopReason::Breakpoint,
thread_id: 2,
};
match ev {
SessionEvent::Stopped { reason, thread_id } => {
assert_eq!(reason, StopReason::Breakpoint);
assert_eq!(thread_id, 2);
}
other => panic!("expected Stopped, got {other:?}"),
}
}
fn _assert_send<T: Send>() {}
#[test]
fn command_and_event_are_send() {
_assert_send::<SessionCommand>();
_assert_send::<SessionEvent>();
_assert_send::<ThreadId>();
_assert_send::<LineEvent>();
}
}