use serde_json::{json, Value};
use super::error::BridgeError;
use super::servo_backend::ServoBackend;
fn get_str(params: &Value, key: &str) -> Result<String, BridgeError> {
params
.get(key)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| BridgeError::InvalidParams(format!("missing string field: {key}")))
}
fn get_opt_str(params: &Value, key: &str) -> Option<String> {
params
.get(key)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
fn get_opt_u32(params: &Value, key: &str, default: u32) -> u32 {
params
.get(key)
.and_then(|v| v.as_u64())
.map(|v| v as u32)
.unwrap_or(default)
}
pub fn debugger_enable(
backend: &dyn ServoBackend,
target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
backend.debugger_enable(target_id)?;
Ok(json!({ "debuggerId": 1 }))
}
pub fn debugger_disable(
backend: &dyn ServoBackend,
target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
backend.debugger_disable(target_id)?;
Ok(Value::Object(Default::default()))
}
pub fn debugger_set_breakpoint_by_url(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let line_number = get_opt_u32(params, "lineNumber", 0);
let column_number = get_opt_u32(params, "columnNumber", 0);
let url = get_opt_str(params, "url")
.or_else(|| get_opt_str(params, "urlRegex"))
.ok_or_else(|| {
BridgeError::InvalidParams(
"Debugger.setBreakpointByUrl requires `url` or `urlRegex`".into(),
)
})?;
let bp = backend.debugger_set_breakpoint_by_url(target_id, &url, line_number, column_number)?;
Ok(json!({
"breakpointId": bp.breakpoint_id,
"locations": [{
"scriptId": bp.script_id.to_string(),
"lineNumber": bp.actual_line,
"columnNumber": bp.actual_column,
}],
}))
}
pub fn debugger_set_breakpoint(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let location = params.get("location").ok_or_else(|| {
BridgeError::InvalidParams("Debugger.setBreakpoint requires `location`".into())
})?;
let script_id_str = location
.get("scriptId")
.and_then(|v| v.as_str())
.ok_or_else(|| BridgeError::InvalidParams("location.scriptId required".into()))?;
let line_number = get_opt_u32(location, "lineNumber", 0);
let column_number = get_opt_u32(location, "columnNumber", 0);
let script_id: u32 = script_id_str.parse().unwrap_or_else(|_| {
let mut h: u32 = 2166136261;
for b in script_id_str.bytes() {
h ^= b as u32;
h = h.wrapping_mul(16777619);
}
h
});
let bp = backend.debugger_set_breakpoint_by_url(
target_id,
&format!("inline:{script_id}"),
line_number,
column_number,
)?;
Ok(json!({
"breakpointId": bp.breakpoint_id,
"actualLocation": {
"scriptId": script_id_str,
"lineNumber": bp.actual_line,
"columnNumber": bp.actual_column,
},
}))
}
pub fn debugger_remove_breakpoint(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let bp_id = get_str(params, "breakpointId")?;
let parts: Vec<&str> = bp_id.rsplitn(3, ':').collect();
if parts.len() != 3 {
return Err(BridgeError::InvalidParams(format!(
"malformed breakpointId: {bp_id}"
)));
}
let column: u32 = parts[0].parse().map_err(|_| {
BridgeError::InvalidParams(format!("invalid column in breakpointId: {}", parts[0]))
})?;
let line: u32 = parts[1].parse().map_err(|_| {
BridgeError::InvalidParams(format!("invalid line in breakpointId: {}", parts[1]))
})?;
let script_id: u32 = parts[2]
.strip_prefix("bp")
.unwrap_or(parts[2])
.parse()
.unwrap_or(1);
backend.debugger_remove_breakpoint(target_id, script_id, line, column)?;
Ok(Value::Object(Default::default()))
}
pub fn debugger_pause(
backend: &dyn ServoBackend,
target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
backend.debugger_pause(target_id)?;
Ok(Value::Object(Default::default()))
}
pub fn debugger_resume(
backend: &dyn ServoBackend,
target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
backend.debugger_resume(target_id, None)?;
Ok(Value::Object(Default::default()))
}
pub fn debugger_step_over(
backend: &dyn ServoBackend,
target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
backend.debugger_resume(target_id, Some(super::servo_backend::DebugStepAction::Next))?;
Ok(Value::Object(Default::default()))
}
pub fn debugger_step_into(
backend: &dyn ServoBackend,
target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
backend.debugger_resume(target_id, Some(super::servo_backend::DebugStepAction::Into))?;
Ok(Value::Object(Default::default()))
}
pub fn debugger_step_out(
backend: &dyn ServoBackend,
target_id: &str,
_params: &Value,
) -> Result<Value, BridgeError> {
backend.debugger_resume(target_id, Some(super::servo_backend::DebugStepAction::Out))?;
Ok(Value::Object(Default::default()))
}
pub fn debugger_evaluate_on_call_frame(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let call_frame_id = get_str(params, "callFrameId")?;
let expression = get_str(params, "expression")?;
let return_by_value = params
.get("returnByValue")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let result = backend.debugger_evaluate_on_call_frame(target_id, &call_frame_id, &expression)?;
let mut remote = json!({
"type": result.result.type_,
});
if let Some(oid) = &result.result.object_id {
remote["objectId"] = json!(oid);
}
if let Some(cls) = &result.result.class_name {
remote["className"] = json!(cls);
}
if let Some(desc) = &result.result.description {
remote["description"] = json!(desc);
}
if return_by_value {
if let Some(v) = &result.result.value {
remote["value"] = v.clone();
}
} else if result.result.type_ == "string" {
if let Some(v) = &result.result.value {
remote["value"] = v.clone();
}
}
let response = if result.has_exception {
json!({
"result": remote,
"exceptionDetails": {
"exceptionId": 0,
"text": "Evaluation threw exception",
"exception": remote,
},
})
} else {
json!({ "result": remote })
};
Ok(response)
}
pub fn debugger_get_possible_breakpoints(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let start = params
.get("start")
.ok_or_else(|| BridgeError::InvalidParams("start location required".into()))?;
let script_id_str = start
.get("scriptId")
.and_then(|v| v.as_str())
.ok_or_else(|| BridgeError::InvalidParams("start.scriptId required".into()))?;
let script_id: u32 = script_id_str.parse().unwrap_or(1);
let pbps = backend.debugger_get_possible_breakpoints(target_id, script_id)?;
let locations: Vec<Value> = pbps
.iter()
.map(|p| {
json!({
"scriptId": p.script_id.to_string(),
"lineNumber": p.line_number,
"columnNumber": p.column_number,
})
})
.collect();
Ok(json!({ "locations": locations }))
}
pub fn debugger_get_script_source(
backend: &dyn ServoBackend,
target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let script_id_str = get_str(params, "scriptId")?;
let script_id: u32 = script_id_str.parse().unwrap_or(1);
let source = backend.debugger_get_script_source(target_id, script_id)?;
Ok(json!({ "scriptSource": source, "bytecode:": Value::Null }))
}
pub fn debugger_set_breakpoints_active(
_backend: &dyn ServoBackend,
_target_id: &str,
params: &Value,
) -> Result<Value, BridgeError> {
let _active = params
.get("active")
.and_then(|v| v.as_bool())
.ok_or_else(|| BridgeError::InvalidParams("active (bool) required".into()))?;
Ok(Value::Object(Default::default()))
}
#[cfg(test)]
mod tests {
use super::super::servo_backend::MockServoBackend;
use super::*;
use serde_json::json;
fn backend() -> MockServoBackend {
MockServoBackend::new()
}
#[test]
fn enable_routes_to_backend() {
let b = backend();
let r = debugger_enable(&b, "1", &json!({})).unwrap();
assert_eq!(r["debuggerId"], 1);
let log = b.call_log.lock().unwrap();
assert!(log.iter().any(|(_, m, _)| m == "debugger_enable"));
}
#[test]
fn set_breakpoint_by_url_returns_breakpoint_id_and_location() {
let b = backend();
let r = debugger_set_breakpoint_by_url(
&b,
"1",
&json!({
"lineNumber": 10,
"url": "https://example.com/app.js",
"columnNumber": 5,
}),
)
.unwrap();
assert!(r["breakpointId"].is_string());
assert!(r["breakpointId"].as_str().unwrap().contains("10"));
assert!(r["breakpointId"].as_str().unwrap().contains("5"));
let locs = r["locations"].as_array().unwrap();
assert_eq!(locs.len(), 1);
assert!(locs[0]["scriptId"].is_string());
assert_eq!(locs[0]["lineNumber"], 10);
assert_eq!(locs[0]["columnNumber"], 5);
}
#[test]
fn set_breakpoint_by_url_missing_url_returns_invalid_params() {
let b = backend();
let err = debugger_set_breakpoint_by_url(&b, "1", &json!({"lineNumber": 1})).unwrap_err();
assert!(matches!(err, BridgeError::InvalidParams(_)));
}
#[test]
fn set_breakpoint_with_location_object_routes() {
let b = backend();
let r = debugger_set_breakpoint(
&b,
"1",
&json!({
"location": { "scriptId": "42", "lineNumber": 3, "columnNumber": 0 }
}),
)
.unwrap();
assert!(r["breakpointId"].is_string());
assert_eq!(r["actualLocation"]["scriptId"], "42");
assert_eq!(r["actualLocation"]["lineNumber"], 3);
}
#[test]
fn remove_breakpoint_routes_to_clear() {
let b = backend();
let r = debugger_remove_breakpoint(
&b,
"1",
&json!({
"breakpointId": "bp:1:10:5"
}),
)
.unwrap();
assert!(r.as_object().unwrap().is_empty());
let log = b.call_log.lock().unwrap();
assert!(log
.iter()
.any(|(_, m, p)| m == "debugger_remove_breakpoint" && p.contains("1:10:5")));
}
#[test]
fn remove_breakpoint_malformed_id_returns_invalid_params() {
let b = backend();
let err =
debugger_remove_breakpoint(&b, "1", &json!({"breakpointId": "garbage"})).unwrap_err();
assert!(matches!(err, BridgeError::InvalidParams(_)));
}
#[test]
fn pause_routes_to_interrupt() {
let b = backend();
debugger_pause(&b, "1", &json!({})).unwrap();
let log = b.call_log.lock().unwrap();
assert!(log
.iter()
.any(|(_, m, p)| m == "debugger_pause" && p == "Interrupt"));
}
#[test]
fn resume_routes_to_resume_no_limit() {
let b = backend();
debugger_resume(&b, "1", &json!({})).unwrap();
let log = b.call_log.lock().unwrap();
assert!(log
.iter()
.any(|(_, m, p)| m == "debugger_resume" && p == "Resume()"));
}
#[test]
fn step_over_routes_to_resume_next() {
let b = backend();
debugger_step_over(&b, "1", &json!({})).unwrap();
let log = b.call_log.lock().unwrap();
assert!(log
.iter()
.any(|(_, m, p)| m == "debugger_resume" && p == "Resume(next)"));
}
#[test]
fn step_into_routes_to_resume_step() {
let b = backend();
debugger_step_into(&b, "1", &json!({})).unwrap();
let log = b.call_log.lock().unwrap();
assert!(log
.iter()
.any(|(_, m, p)| m == "debugger_resume" && p == "Resume(step)"));
}
#[test]
fn step_out_routes_to_resume_finish() {
let b = backend();
debugger_step_out(&b, "1", &json!({})).unwrap();
let log = b.call_log.lock().unwrap();
assert!(log
.iter()
.any(|(_, m, p)| m == "debugger_resume" && p == "Resume(finish)"));
}
#[test]
fn evaluate_on_call_frame_returns_remote_object() {
let b = backend();
let r = debugger_evaluate_on_call_frame(
&b,
"1",
&json!({
"callFrameId": "frame-0",
"expression": "x + 1",
}),
)
.unwrap();
assert_eq!(r["result"]["type"], "string");
assert_eq!(r["result"]["value"], "x + 1");
assert!(r.get("exceptionDetails").is_none());
}
#[test]
fn evaluate_on_call_frame_missing_callframe_returns_invalid_params() {
let b = backend();
let err =
debugger_evaluate_on_call_frame(&b, "1", &json!({"expression": "x"})).unwrap_err();
assert!(matches!(err, BridgeError::InvalidParams(_)));
}
#[test]
fn evaluate_on_call_frame_return_by_value_inlines_value() {
let b = backend();
let r = debugger_evaluate_on_call_frame(
&b,
"1",
&json!({
"callFrameId": "frame-0",
"expression": "1 + 1",
"returnByValue": true,
}),
)
.unwrap();
assert_eq!(r["result"]["value"], "1 + 1");
}
#[test]
fn get_possible_breakpoints_returns_locations_array() {
let b = backend();
let r = debugger_get_possible_breakpoints(
&b,
"1",
&json!({
"start": { "scriptId": "1", "lineNumber": 0 }
}),
)
.unwrap();
let arr = r["locations"].as_array().unwrap();
assert!(!arr.is_empty());
assert!(arr[0]["scriptId"].is_string());
}
#[test]
fn get_script_source_returns_string() {
let b = backend();
let r = debugger_get_script_source(&b, "1", &json!({"scriptId": "1"})).unwrap();
assert!(r["scriptSource"].is_string());
}
#[test]
fn set_breakpoints_active_validates_bool_param() {
let b = backend();
let r = debugger_set_breakpoints_active(&b, "1", &json!({"active": false})).unwrap();
assert!(r.as_object().unwrap().is_empty());
let err = debugger_set_breakpoints_active(&b, "1", &json!({})).unwrap_err();
assert!(matches!(err, BridgeError::InvalidParams(_)));
}
#[test]
fn enable_unknown_target_returns_page_not_found() {
let b = backend();
let err = debugger_enable(&b, "999", &json!({})).unwrap_err();
assert!(matches!(err, BridgeError::PageNotFound(_)));
}
#[test]
fn debug_step_action_from_cdp_parses_known_values() {
use super::super::servo_backend::DebugStepAction;
assert_eq!(
DebugStepAction::from_cdp("over"),
Some(DebugStepAction::Next)
);
assert_eq!(
DebugStepAction::from_cdp("into"),
Some(DebugStepAction::Into)
);
assert_eq!(DebugStepAction::from_cdp("out"), Some(DebugStepAction::Out));
assert_eq!(DebugStepAction::from_cdp("unknown"), None);
assert_eq!(DebugStepAction::Next.servo_resume_limit(), "next");
assert_eq!(DebugStepAction::Into.servo_resume_limit(), "step");
assert_eq!(DebugStepAction::Out.servo_resume_limit(), "finish");
}
}