2026-11-05 08:26:51 +00:00
|
|
|
use std::cell::RefCell;
|
|
|
|
|
use std::rc::Rc;
|
|
|
|
|
|
|
|
|
|
use runa::Server;
|
|
|
|
|
use runa_dap::{Adapter, Connection};
|
|
|
|
|
use runa_protocol::{Command, PROTOCOL_VERSION, Reply, Request, StepMode, Value};
|
|
|
|
|
use serde_json::{Value as Json, json};
|
|
|
|
|
|
|
|
|
|
const SOURCE: &str = r#"fn helper(value) {
|
|
|
|
|
value + 1
|
|
|
|
|
}
|
|
|
|
|
pub fn handle(ctx, state, sender, message) {
|
|
|
|
|
let next = helper(state);
|
|
|
|
|
next + message
|
|
|
|
|
}
|
|
|
|
|
"#;
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
struct Image {
|
|
|
|
|
server: Rc<RefCell<Server>>,
|
|
|
|
|
commands: Rc<RefCell<Vec<Command>>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Image {
|
|
|
|
|
fn new() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
server: Rc::new(RefCell::new(Server::new())),
|
|
|
|
|
commands: Rc::default(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn actor(&mut self, path: &str, source: &str) -> String {
|
|
|
|
|
self.request(Command::WriteSource {
|
|
|
|
|
path: path.to_owned(),
|
|
|
|
|
expected_source_id: None,
|
|
|
|
|
source: source.to_owned(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
let Reply::ActorSpawned { actor_ref } = self
|
|
|
|
|
.request(Command::Spawn {
|
|
|
|
|
behavior_name: path.to_owned(),
|
|
|
|
|
mailbox_capacity: 32,
|
|
|
|
|
state: Value::Integer(10),
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
else {
|
|
|
|
|
panic!("expected spawned actor")
|
|
|
|
|
};
|
|
|
|
|
actor_ref
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn inspect(&mut self, actor: &str) -> runa_protocol::ActorSnapshotDto {
|
|
|
|
|
let Reply::Actor(actor) = self
|
|
|
|
|
.request(Command::InspectActor {
|
|
|
|
|
actor_ref: actor.to_owned(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
else {
|
|
|
|
|
panic!()
|
|
|
|
|
};
|
|
|
|
|
actor
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Connection for Image {
|
|
|
|
|
fn request(&mut self, command: Command) -> Result<Reply, String> {
|
|
|
|
|
self.commands.borrow_mut().push(command.clone());
|
|
|
|
|
self.server
|
|
|
|
|
.borrow_mut()
|
|
|
|
|
.dispatch(Request {
|
|
|
|
|
version: PROTOCOL_VERSION,
|
|
|
|
|
request_id: "dap-test".to_owned(),
|
|
|
|
|
command,
|
|
|
|
|
})
|
|
|
|
|
.result
|
|
|
|
|
.map_err(|error| error.message)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn request(adapter: &mut Adapter<Image>, command: &str, args: Json) -> Vec<Json> {
|
|
|
|
|
adapter.handle(json!({"seq": 7, "type": "request", "command": command, "arguments": args}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn ok(adapter: &mut Adapter<Image>, command: &str, args: Json) -> Json {
|
|
|
|
|
let messages = request(adapter, command, args);
|
|
|
|
|
assert_eq!(messages[0]["success"], true, "{messages:?}");
|
|
|
|
|
messages[0]["body"].clone()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn attach(image: &Image, actor: &str) -> Adapter<Image> {
|
|
|
|
|
let mut adapter = Adapter::new(image.clone());
|
|
|
|
|
ok(&mut adapter, "initialize", json!({}));
|
|
|
|
|
let messages = request(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"attach",
|
|
|
|
|
json!({
|
|
|
|
|
"actorRef": actor, "behaviorSourceUri": format!("runa-actor://session/{actor}/behavior.rn"),
|
|
|
|
|
"workingSourceUri": "runa-image://localhost/source/tests/handler.rn"
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(messages[0]["success"], true, "{messages:?}");
|
|
|
|
|
assert!(
|
|
|
|
|
messages
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|message| message["event"] == "initialized")
|
|
|
|
|
);
|
|
|
|
|
ok(&mut adapter, "configurationDone", json!({}));
|
|
|
|
|
adapter
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn fixture() -> (Image, String) {
|
|
|
|
|
let mut image = Image::new();
|
|
|
|
|
let actor = image.actor("tests/handler", SOURCE);
|
|
|
|
|
(image, actor)
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-05 09:11:16 +00:00
|
|
|
fn attach_image(image: &Image) -> Adapter<Image> {
|
|
|
|
|
let mut adapter = Adapter::new(image.clone());
|
|
|
|
|
ok(&mut adapter, "initialize", json!({}));
|
|
|
|
|
let messages = request(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"attach",
|
|
|
|
|
json!({
|
|
|
|
|
"image": true,
|
|
|
|
|
"actorSourceBaseUri": "runa-actor://session",
|
|
|
|
|
"workingSourceBaseUri": "runa-image://localhost/source"
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(messages[0]["success"], true, "{messages:?}");
|
|
|
|
|
assert!(
|
|
|
|
|
messages
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|message| message["event"] == "initialized")
|
|
|
|
|
);
|
|
|
|
|
ok(&mut adapter, "configurationDone", json!({}));
|
|
|
|
|
adapter
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-05 08:26:51 +00:00
|
|
|
#[test]
|
|
|
|
|
fn attach_pause_source_and_resume_never_tick_or_clear_breakpoints() {
|
|
|
|
|
let (mut image, actor) = fixture();
|
|
|
|
|
let mut adapter = attach(&image, &actor);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
image.inspect(&actor).status,
|
|
|
|
|
runa_protocol::ActorStatusDto::Paused
|
|
|
|
|
);
|
|
|
|
|
let stopped = adapter.poll();
|
|
|
|
|
assert!(
|
|
|
|
|
stopped
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|message| message["event"] == "stopped" && message["body"]["reason"] == "pause")
|
|
|
|
|
);
|
|
|
|
|
assert!(adapter.poll().is_empty());
|
|
|
|
|
let sources = ok(&mut adapter, "loadedSources", json!({}));
|
|
|
|
|
assert_eq!(sources["sources"][0]["name"], "behavior.rn");
|
|
|
|
|
assert_eq!(sources["sources"][0]["sourceReference"], 1);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
sources["sources"][0]["path"],
|
|
|
|
|
format!("runa-actor://session/{actor}/behavior.rn")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(&mut adapter, "source", json!({"sourceReference": 1}))["content"],
|
|
|
|
|
SOURCE
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(&mut adapter, "threads", json!({}))["threads"][0]["id"],
|
|
|
|
|
1
|
|
|
|
|
);
|
|
|
|
|
let breakpoints = ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"setBreakpoints",
|
|
|
|
|
json!({"source": {"sourceReference": 1}, "breakpoints": [{"line": 5}]}),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(breakpoints["breakpoints"][0]["verified"], true);
|
|
|
|
|
ok(&mut adapter, "continue", json!({"threadId": 1}));
|
|
|
|
|
assert!(
|
|
|
|
|
!image
|
|
|
|
|
.commands
|
|
|
|
|
.borrow()
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|command| matches!(command, Command::Tick { .. }))
|
|
|
|
|
);
|
|
|
|
|
assert_ne!(
|
|
|
|
|
image.inspect(&actor).status,
|
|
|
|
|
runa_protocol::ActorStatusDto::Paused
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
request(&mut adapter, "variables", json!({"variablesReference": 1}))[0]["message"]
|
|
|
|
|
.as_str()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.contains("Stale")
|
|
|
|
|
);
|
|
|
|
|
ok(&mut adapter, "disconnect", json!({}));
|
|
|
|
|
let Reply::ExecutionInspected(inspection) = image
|
|
|
|
|
.request(Command::InspectExecution { actor_ref: actor })
|
|
|
|
|
.unwrap()
|
|
|
|
|
else {
|
|
|
|
|
panic!()
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(inspection.debugger.breakpoints.len(), 1);
|
|
|
|
|
assert!(adapter.is_closed());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn breakpoints_are_actor_specific_and_all_mutable_sources_are_guarded() {
|
|
|
|
|
let (mut image, actor) = fixture();
|
|
|
|
|
let second = image.actor("tests/other", SOURCE);
|
|
|
|
|
let mut adapter = attach(&image, &actor);
|
|
|
|
|
let source = json!({"path": format!("runa-actor://session/{actor}/behavior.rn")});
|
|
|
|
|
let set = |source: Json, dirty: bool| json!({"source": source, "sourceModified": dirty, "breakpoints": [{"line": 5}]});
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(&mut adapter, "setBreakpoints", set(source.clone(), false))["breakpoints"][0]["verified"],
|
|
|
|
|
true
|
|
|
|
|
);
|
|
|
|
|
let Reply::ExecutionInspected(other) = image
|
|
|
|
|
.request(Command::InspectExecution {
|
|
|
|
|
actor_ref: second.clone(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
else {
|
|
|
|
|
panic!()
|
|
|
|
|
};
|
|
|
|
|
assert!(other.debugger.breakpoints.is_empty());
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(&mut adapter, "setBreakpoints", set(source.clone(), true))["breakpoints"][0]["verified"],
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
let Reply::ActorBehavior { source_id, .. } = image
|
|
|
|
|
.request(Command::ActorBehavior {
|
|
|
|
|
actor_ref: actor.clone(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
else {
|
|
|
|
|
panic!()
|
|
|
|
|
};
|
|
|
|
|
image
|
|
|
|
|
.request(Command::WriteActorBehavior {
|
|
|
|
|
actor_ref: actor.clone(),
|
|
|
|
|
expected_source_id: source_id,
|
|
|
|
|
source: SOURCE.replace("value + 1", "value + 2"),
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(&mut adapter, "setBreakpoints", set(source, false))["breakpoints"][0]["verified"],
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"setBreakpoints",
|
|
|
|
|
set(json!({"sourceReference": 1}), true)
|
|
|
|
|
)["breakpoints"][0]["verified"],
|
|
|
|
|
true
|
|
|
|
|
);
|
|
|
|
|
let working = json!({"path": "runa-image://localhost/source/tests/handler.rn"});
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(&mut adapter, "setBreakpoints", set(working.clone(), false))["breakpoints"][0]["verified"],
|
|
|
|
|
true
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(&mut adapter, "setBreakpoints", set(working.clone(), true))["breakpoints"][0]["verified"],
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
let Reply::Source(file) = image
|
|
|
|
|
.request(Command::ReadSource {
|
|
|
|
|
path: "tests/handler".to_owned(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
else {
|
|
|
|
|
panic!()
|
|
|
|
|
};
|
|
|
|
|
image
|
|
|
|
|
.request(Command::WriteSource {
|
|
|
|
|
path: file.path,
|
|
|
|
|
expected_source_id: Some(file.source_id),
|
|
|
|
|
source: SOURCE.replace("value + 1", "value + 3"),
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(&mut adapter, "setBreakpoints", set(working, false))["breakpoints"][0]["verified"],
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
let before = image
|
|
|
|
|
.commands
|
|
|
|
|
.borrow()
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|command| matches!(command, Command::SetBreakpoints { .. }))
|
|
|
|
|
.count();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"setBreakpoints",
|
|
|
|
|
set(
|
|
|
|
|
json!({"path": format!("runa-actor://session/{second}/behavior.rn")}),
|
|
|
|
|
false
|
|
|
|
|
)
|
|
|
|
|
)["breakpoints"][0]["verified"],
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
let after = image
|
|
|
|
|
.commands
|
|
|
|
|
.borrow()
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|command| matches!(command, Command::SetBreakpoints { .. }))
|
|
|
|
|
.count();
|
|
|
|
|
assert_eq!(before, after);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn loaded_and_mutable_breakpoint_groups_merge_and_external_changes_emit_events() {
|
|
|
|
|
let (mut image, actor) = fixture();
|
|
|
|
|
let mut adapter = attach(&image, &actor);
|
|
|
|
|
ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"setBreakpoints",
|
|
|
|
|
json!({"source": {"sourceReference": 1}, "breakpoints": [{"line": 5}]}),
|
|
|
|
|
);
|
|
|
|
|
ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"setBreakpoints",
|
|
|
|
|
json!({"source": {"path": "runa-image://localhost/source/tests/handler.rn"}, "breakpoints": [{"line": 6}]}),
|
|
|
|
|
);
|
|
|
|
|
let Reply::ExecutionInspected(inspection) = image
|
|
|
|
|
.request(Command::InspectExecution {
|
|
|
|
|
actor_ref: actor.clone(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
else {
|
|
|
|
|
panic!()
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
inspection
|
|
|
|
|
.debugger
|
|
|
|
|
.breakpoints
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|bp| bp.line)
|
|
|
|
|
.collect::<Vec<_>>(),
|
|
|
|
|
[4, 5]
|
|
|
|
|
);
|
|
|
|
|
image
|
|
|
|
|
.request(Command::SetBreakpoints {
|
|
|
|
|
actor_ref: actor,
|
|
|
|
|
implementation_id: inspection.implementation_id,
|
|
|
|
|
lines: vec![],
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
let events = adapter.poll();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
events
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|event| event["event"] == "breakpoint"
|
|
|
|
|
&& event["body"]["breakpoint"]["verified"] == false)
|
|
|
|
|
.count(),
|
|
|
|
|
2
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn variable_expansion_and_evaluation_share_snapshot_handles() {
|
|
|
|
|
let (mut image, actor) = fixture();
|
|
|
|
|
image
|
|
|
|
|
.request(Command::Send {
|
|
|
|
|
actor_ref: actor.clone(),
|
|
|
|
|
message: Value::Record(std::collections::BTreeMap::from([(
|
|
|
|
|
"a/b~c".to_owned(),
|
|
|
|
|
Value::List(vec![Value::Integer(3)]),
|
|
|
|
|
)])),
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
let mut adapter = attach(&image, &actor);
|
|
|
|
|
adapter.poll();
|
|
|
|
|
let root =
|
|
|
|
|
ok(&mut adapter, "evaluate", json!({"expression": "actor"}))["variablesReference"].clone();
|
|
|
|
|
let evaluated_mailbox = ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"evaluate",
|
|
|
|
|
json!({"expression": "actor/mailbox"}),
|
|
|
|
|
)["variablesReference"]
|
|
|
|
|
.clone();
|
|
|
|
|
let root_variables = ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"variables",
|
|
|
|
|
json!({"variablesReference": root}),
|
|
|
|
|
);
|
|
|
|
|
let mailbox = root_variables["variables"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|variable| variable["name"] == "mailbox")
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(mailbox["variablesReference"], evaluated_mailbox);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"variables",
|
|
|
|
|
json!({"variablesReference": root})
|
|
|
|
|
),
|
|
|
|
|
root_variables
|
|
|
|
|
);
|
|
|
|
|
let queued = ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"variables",
|
|
|
|
|
json!({"variablesReference": evaluated_mailbox}),
|
|
|
|
|
)["variables"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|variable| variable["name"] == "queued")
|
|
|
|
|
.unwrap()
|
|
|
|
|
.clone();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"evaluate",
|
|
|
|
|
json!({"expression": "actor/mailbox/queued"})
|
|
|
|
|
)["variablesReference"],
|
|
|
|
|
queued["variablesReference"]
|
|
|
|
|
);
|
|
|
|
|
let message = ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"variables",
|
|
|
|
|
json!({"variablesReference": queued["variablesReference"]}),
|
|
|
|
|
)["variables"][0]
|
|
|
|
|
.clone();
|
|
|
|
|
let field = ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"variables",
|
|
|
|
|
json!({"variablesReference": message["variablesReference"]}),
|
|
|
|
|
)["variables"][0]
|
|
|
|
|
.clone();
|
|
|
|
|
assert_eq!(field["evaluateName"], "actor/mailbox/queued/0/a~1b~0c");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"evaluate",
|
|
|
|
|
json!({"expression": field["evaluateName"]})
|
|
|
|
|
)["variablesReference"],
|
|
|
|
|
field["variablesReference"]
|
|
|
|
|
);
|
|
|
|
|
ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"setBreakpoints",
|
|
|
|
|
json!({"source": {"sourceReference": 1}, "breakpoints": [{"line": 5}]}),
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
!adapter
|
|
|
|
|
.poll()
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|message| message["event"] == "stopped")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"evaluate",
|
|
|
|
|
json!({"expression": "actor/mailbox"})
|
|
|
|
|
)["variablesReference"],
|
|
|
|
|
evaluated_mailbox
|
|
|
|
|
);
|
|
|
|
|
image
|
|
|
|
|
.request(Command::EditActor {
|
|
|
|
|
actor_ref: actor,
|
|
|
|
|
state: Some(Value::Integer(99)),
|
|
|
|
|
messages: None,
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
adapter.poll();
|
|
|
|
|
let replacement = ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"evaluate",
|
|
|
|
|
json!({"expression": "actor/mailbox"}),
|
|
|
|
|
)["variablesReference"]
|
|
|
|
|
.clone();
|
|
|
|
|
assert_ne!(replacement, evaluated_mailbox);
|
|
|
|
|
let stale = request(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"variables",
|
|
|
|
|
json!({"variablesReference": evaluated_mailbox}),
|
|
|
|
|
);
|
|
|
|
|
assert!(stale[0]["message"].as_str().unwrap().contains("Stale"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn snapshot_paths_readonly_pagination_escaping_and_stale_references() {
|
|
|
|
|
let (mut image, actor) = fixture();
|
|
|
|
|
let mut adapter = attach(&image, &actor);
|
|
|
|
|
adapter.poll();
|
|
|
|
|
let state = ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"evaluate",
|
|
|
|
|
json!({"expression": "actor/committed_state", "context": "watch"}),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(state["result"], "10");
|
|
|
|
|
assert_eq!(state["evaluateName"], "actor/committed_state");
|
|
|
|
|
assert_eq!(state["presentationHint"]["attributes"], json!(["readOnly"]));
|
|
|
|
|
let root = ok(&mut adapter, "evaluate", json!({"expression": "actor"}))["variablesReference"]
|
|
|
|
|
.as_u64()
|
|
|
|
|
.unwrap();
|
|
|
|
|
let variables = ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"variables",
|
|
|
|
|
json!({"variablesReference": root, "start": 1, "count": 1}),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(variables["variables"].as_array().unwrap().len(), 1);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"variables",
|
|
|
|
|
json!({"variablesReference": root, "filter": "indexed"})
|
|
|
|
|
)["variables"],
|
|
|
|
|
json!([])
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"evaluate",
|
|
|
|
|
json!({"expression": "actor/mailbox/queued"})
|
|
|
|
|
)["result"],
|
|
|
|
|
"List(0)"
|
|
|
|
|
);
|
|
|
|
|
let mut fields = std::collections::BTreeMap::new();
|
|
|
|
|
fields.insert("a/b~c".to_owned(), Value::Integer(42));
|
|
|
|
|
image
|
|
|
|
|
.request(Command::EditActor {
|
|
|
|
|
actor_ref: actor.clone(),
|
|
|
|
|
state: Some(Value::Record(fields)),
|
|
|
|
|
messages: None,
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
adapter
|
|
|
|
|
.poll()
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|message| message["event"] == "stopped")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
request(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"variables",
|
|
|
|
|
json!({"variablesReference": root})
|
|
|
|
|
)[0]["success"],
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"evaluate",
|
|
|
|
|
json!({"expression": "actor/committed_state/a~1b~0c"})
|
|
|
|
|
)["result"],
|
|
|
|
|
"42"
|
|
|
|
|
);
|
|
|
|
|
for expression in [
|
|
|
|
|
"state + 1",
|
|
|
|
|
"actor/__proto__",
|
|
|
|
|
"actor/committed_state/a~2b",
|
|
|
|
|
"actor/mailbox/queued/01",
|
|
|
|
|
"actor/mailbox/queued/0",
|
|
|
|
|
] {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
request(&mut adapter, "evaluate", json!({"expression": expression}))[0]["success"],
|
|
|
|
|
false,
|
|
|
|
|
"{expression}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
ok(&mut adapter, "continue", json!({"threadId": 1}));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
request(&mut adapter, "evaluate", json!({"expression": "actor"}))[0]["success"],
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn source_steps_use_bounded_modes_and_real_frames_without_locals() {
|
|
|
|
|
let (mut image, actor) = fixture();
|
|
|
|
|
image
|
|
|
|
|
.request(Command::Send {
|
|
|
|
|
actor_ref: actor.clone(),
|
|
|
|
|
message: Value::Integer(2),
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
image
|
|
|
|
|
.request(Command::Tick {
|
|
|
|
|
ticks: 1,
|
|
|
|
|
instruction_budget: 1,
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
let mut adapter = attach(&image, &actor);
|
|
|
|
|
adapter.poll();
|
|
|
|
|
let old_root =
|
|
|
|
|
ok(&mut adapter, "evaluate", json!({"expression": "actor"}))["variablesReference"].clone();
|
|
|
|
|
for (command, mode) in [
|
|
|
|
|
("stepIn", StepMode::Into),
|
|
|
|
|
("next", StepMode::Over),
|
|
|
|
|
("stepOut", StepMode::Out),
|
|
|
|
|
] {
|
|
|
|
|
let messages = request(&mut adapter, command, json!({"threadId": 1}));
|
|
|
|
|
assert_eq!(messages[0]["success"], true, "{messages:?}");
|
|
|
|
|
assert!(
|
|
|
|
|
messages
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|message| message["event"] == "continued")
|
|
|
|
|
);
|
|
|
|
|
assert!(image.commands.borrow().iter().any(|command| matches!(command, Command::StepSource {instruction_budget: 1000, mode: actual, ..} if *actual == mode)));
|
|
|
|
|
let events = adapter.poll();
|
|
|
|
|
assert!(
|
|
|
|
|
events.iter().any(|event| event["event"] == "stopped"),
|
|
|
|
|
"{events:?}"
|
|
|
|
|
);
|
|
|
|
|
let trace = ok(&mut adapter, "stackTrace", json!({"threadId": 1}));
|
|
|
|
|
if command == "stepIn" {
|
|
|
|
|
assert!(trace["totalFrames"].as_u64().unwrap() > 0);
|
|
|
|
|
}
|
|
|
|
|
for frame in trace["stackFrames"].as_array().unwrap() {
|
|
|
|
|
let scopes = ok(&mut adapter, "scopes", json!({"frameId": frame["id"]}));
|
|
|
|
|
assert_eq!(scopes["scopes"].as_array().unwrap().len(), 2);
|
|
|
|
|
assert!(
|
|
|
|
|
scopes["scopes"][0]["name"]
|
|
|
|
|
.as_str()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.contains("not frame locals")
|
|
|
|
|
);
|
|
|
|
|
let metadata = ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"variables",
|
|
|
|
|
json!({"variablesReference": scopes["scopes"][1]["variablesReference"]}),
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
metadata["variables"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|variable| variable["name"] == "instruction_pointer")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(frame["source"]["name"], "behavior.rn");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
assert_eq!(
|
|
|
|
|
request(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"variables",
|
|
|
|
|
json!({"variablesReference": old_root})
|
|
|
|
|
)[0]["success"],
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn bounded_step_reports_budget_without_claiming_to_reach_a_source_line() {
|
|
|
|
|
let mut image = Image::new();
|
|
|
|
|
let actor = image.actor(
|
|
|
|
|
"tests/loop",
|
|
|
|
|
"pub fn handle(ctx, state, sender, message) { loop { let value = state + 1; } }",
|
|
|
|
|
);
|
|
|
|
|
image
|
|
|
|
|
.request(Command::Send {
|
|
|
|
|
actor_ref: actor.clone(),
|
|
|
|
|
message: Value::Null,
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
image
|
|
|
|
|
.request(Command::Tick {
|
|
|
|
|
ticks: 1,
|
|
|
|
|
instruction_budget: 1,
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
let mut adapter = attach(&image, &actor);
|
|
|
|
|
adapter.poll();
|
|
|
|
|
let response = request(&mut adapter, "stepOut", json!({"threadId": 1}));
|
|
|
|
|
assert_eq!(response[0]["success"], true, "{response:?}");
|
|
|
|
|
assert!(response.iter().any(|message| {
|
|
|
|
|
message["event"] == "output"
|
|
|
|
|
&& message["body"]["output"]
|
|
|
|
|
.as_str()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.contains("budget")
|
|
|
|
|
}));
|
|
|
|
|
let stopped = adapter.poll();
|
|
|
|
|
assert!(
|
|
|
|
|
stopped
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|message| message["event"] == "stopped" && message["body"]["reason"] == "pause")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(image.inspect(&actor).state, Value::Integer(10));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn failed_attach_is_nonmutating_and_controls_cannot_retry_invocation() {
|
|
|
|
|
let mut image = Image::new();
|
|
|
|
|
let actor = image.actor(
|
|
|
|
|
"tests/failure",
|
|
|
|
|
"pub fn handle(ctx, state, sender, message) { panic(\"bad invocation\") }",
|
|
|
|
|
);
|
|
|
|
|
image
|
|
|
|
|
.request(Command::Send {
|
|
|
|
|
actor_ref: actor.clone(),
|
|
|
|
|
message: Value::Integer(1),
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
image
|
|
|
|
|
.request(Command::Tick {
|
|
|
|
|
ticks: 1,
|
|
|
|
|
instruction_budget: 1000,
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
let before = image.inspect(&actor);
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
before.status,
|
|
|
|
|
runa_protocol::ActorStatusDto::Failed(_)
|
|
|
|
|
));
|
|
|
|
|
image.commands.borrow_mut().clear();
|
|
|
|
|
let mut adapter = attach(&image, &actor);
|
|
|
|
|
assert_eq!(image.inspect(&actor), before);
|
|
|
|
|
assert!(
|
|
|
|
|
!image
|
|
|
|
|
.commands
|
|
|
|
|
.borrow()
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|command| matches!(command, Command::System { .. }))
|
|
|
|
|
);
|
|
|
|
|
let events = adapter.poll();
|
|
|
|
|
assert!(
|
|
|
|
|
events.iter().any(
|
|
|
|
|
|message| message["event"] == "stopped" && message["body"]["reason"] == "exception"
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
let failure = ok(&mut adapter, "exceptionInfo", json!({"threadId": 1}));
|
|
|
|
|
assert_eq!(failure["breakMode"], "always");
|
|
|
|
|
assert!(
|
|
|
|
|
failure["description"]
|
|
|
|
|
.as_str()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.contains("bad invocation")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(&mut adapter, "stackTrace", json!({"threadId": 1}))["stackFrames"],
|
|
|
|
|
json!([])
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"evaluate",
|
|
|
|
|
json!({"expression": "actor/mailbox/reserved"})
|
|
|
|
|
)["result"],
|
|
|
|
|
"List(1)"
|
|
|
|
|
);
|
|
|
|
|
for command in ["pause", "continue", "next", "stepIn", "stepOut"] {
|
|
|
|
|
let response = request(&mut adapter, command, json!({"threadId": 1}));
|
|
|
|
|
assert_eq!(response[0]["success"], false);
|
|
|
|
|
assert!(
|
|
|
|
|
response[0]["message"]
|
|
|
|
|
.as_str()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.contains("repair/restart")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
assert_eq!(image.inspect(&actor), before);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn failed_actor_is_guarded_even_before_first_poll() {
|
|
|
|
|
let mut image = Image::new();
|
|
|
|
|
let actor = image.actor(
|
|
|
|
|
"tests/failure",
|
|
|
|
|
"pub fn handle(ctx, state, sender, message) { panic(\"failed\") }",
|
|
|
|
|
);
|
|
|
|
|
image
|
|
|
|
|
.request(Command::Send {
|
|
|
|
|
actor_ref: actor.clone(),
|
|
|
|
|
message: Value::Null,
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
image
|
|
|
|
|
.request(Command::Tick {
|
|
|
|
|
ticks: 1,
|
|
|
|
|
instruction_budget: 1000,
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
let mut adapter = attach(&image, &actor);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
request(&mut adapter, "continue", json!({"threadId": 1}))[0]["success"],
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn incarnation_change_ends_session_before_mutating_actor() {
|
|
|
|
|
let (mut image, actor) = fixture();
|
|
|
|
|
let mut adapter = attach(&image, &actor);
|
|
|
|
|
image
|
|
|
|
|
.request(Command::RestartActor {
|
|
|
|
|
actor_ref: actor.clone(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
image.commands.borrow_mut().clear();
|
|
|
|
|
let response = request(&mut adapter, "continue", json!({"threadId": 1}));
|
|
|
|
|
assert_eq!(response[0]["success"], false);
|
|
|
|
|
assert!(
|
|
|
|
|
response
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|message| message["event"] == "terminated")
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
!image
|
|
|
|
|
.commands
|
|
|
|
|
.borrow()
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|command| matches!(command, Command::System { .. }))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn external_stop_and_implementation_change_terminate_on_poll() {
|
|
|
|
|
for stop in [false, true] {
|
|
|
|
|
let (mut image, actor) = fixture();
|
|
|
|
|
let mut adapter = attach(&image, &actor);
|
|
|
|
|
if stop {
|
|
|
|
|
image
|
|
|
|
|
.request(Command::StopActor { actor_ref: actor })
|
|
|
|
|
.unwrap();
|
|
|
|
|
image
|
|
|
|
|
.request(Command::Tick {
|
|
|
|
|
ticks: 1,
|
|
|
|
|
instruction_budget: 1,
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
} else {
|
|
|
|
|
image
|
|
|
|
|
.request(Command::PublishBehavior {
|
|
|
|
|
name: "tests/replacement".to_owned(),
|
|
|
|
|
expected_revision: None,
|
|
|
|
|
source: "pub fn handle(ctx, state, sender, message) { state }".to_owned(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
image
|
|
|
|
|
.request(Command::ResumeActor {
|
|
|
|
|
actor_ref: actor.clone(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
image
|
|
|
|
|
.request(Command::InstallBehavior {
|
|
|
|
|
actor_ref: actor,
|
|
|
|
|
behavior_name: "tests/replacement".to_owned(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
}
|
|
|
|
|
let events = adapter.poll();
|
|
|
|
|
assert!(
|
|
|
|
|
events
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|message| message["event"] == "terminated"),
|
|
|
|
|
"stop={stop}, events={events:?}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn invalid_requests_and_unsupported_commands_never_silently_succeed() {
|
|
|
|
|
let (image, actor) = fixture();
|
|
|
|
|
let mut adapter = attach(&image, &actor);
|
|
|
|
|
for (command, args) in [
|
|
|
|
|
("launch", json!({})),
|
|
|
|
|
("terminate", json!({})),
|
|
|
|
|
("setVariable", json!({})),
|
|
|
|
|
("setExceptionBreakpoints", json!({"filters": ["all"]})),
|
|
|
|
|
("continue", json!({"threadId": 2})),
|
|
|
|
|
("source", json!({"sourceReference": 9})),
|
|
|
|
|
("next", json!({"threadId": 1, "granularity": "instruction"})),
|
|
|
|
|
("stackTrace", json!({"threadId": 1, "startFrame": -1})),
|
|
|
|
|
(
|
|
|
|
|
"evaluate",
|
|
|
|
|
json!({"expression": "actor", "context": "hover"}),
|
|
|
|
|
),
|
|
|
|
|
("disconnect", json!({"terminateDebuggee": true})),
|
|
|
|
|
] {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
request(&mut adapter, command, args)[0]["success"],
|
|
|
|
|
false,
|
|
|
|
|
"{command}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"setExceptionBreakpoints",
|
|
|
|
|
json!({"filters": []})
|
|
|
|
|
)["breakpoints"],
|
|
|
|
|
json!([])
|
|
|
|
|
);
|
|
|
|
|
let invalid = adapter.handle(json!({"type": "request", "command": "threads", "seq": -1}));
|
|
|
|
|
assert!(
|
|
|
|
|
invalid
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|message| message["event"] == "terminated")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn zero_based_locations_and_unsupported_breakpoint_features() {
|
|
|
|
|
let (image, actor) = fixture();
|
|
|
|
|
let mut adapter = Adapter::new(image);
|
|
|
|
|
ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"initialize",
|
|
|
|
|
json!({"linesStartAt1": false, "columnsStartAt1": false}),
|
|
|
|
|
);
|
|
|
|
|
ok(&mut adapter, "attach", json!({"actorRef": actor}));
|
|
|
|
|
let response = ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"setBreakpoints",
|
|
|
|
|
json!({"source": {"sourceReference": 1}, "breakpoints": [{"line": 4}, {"line": 5, "condition": "true"}, {"line": 9999}]}),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(response["breakpoints"][0]["line"], 4);
|
|
|
|
|
assert_eq!(response["breakpoints"][0]["verified"], true);
|
|
|
|
|
assert_eq!(response["breakpoints"][1]["verified"], false);
|
|
|
|
|
assert_eq!(response["breakpoints"][2]["verified"], false);
|
|
|
|
|
}
|
2026-11-05 09:11:16 +00:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn image_attach_discovers_threads_without_mutation_and_routes_actor_controls() {
|
|
|
|
|
let (mut image, first) = fixture();
|
|
|
|
|
let second = image.actor("tests/other", SOURCE);
|
|
|
|
|
let before_first = image.inspect(&first);
|
|
|
|
|
let before_second = image.inspect(&second);
|
|
|
|
|
let mut adapter = attach_image(&image);
|
|
|
|
|
|
|
|
|
|
assert_eq!(image.inspect(&first), before_first);
|
|
|
|
|
assert_eq!(image.inspect(&second), before_second);
|
|
|
|
|
assert!(!image.commands.borrow().iter().any(|command| {
|
|
|
|
|
matches!(
|
|
|
|
|
command,
|
|
|
|
|
Command::System { .. } | Command::SetBreakpoints { .. }
|
|
|
|
|
)
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
let threads = ok(&mut adapter, "threads", json!({}))["threads"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.clone();
|
|
|
|
|
assert_eq!(threads.len(), 2);
|
|
|
|
|
let first_thread = threads
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|thread| thread["name"] == format!("@{first}"))
|
|
|
|
|
.unwrap()["id"]
|
|
|
|
|
.as_u64()
|
|
|
|
|
.unwrap();
|
|
|
|
|
let second_thread = threads
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|thread| thread["name"] == format!("@{second}"))
|
|
|
|
|
.unwrap()["id"]
|
|
|
|
|
.as_u64()
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_ne!(first_thread, second_thread);
|
|
|
|
|
let sources = ok(&mut adapter, "loadedSources", json!({}))["sources"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.clone();
|
|
|
|
|
assert_eq!(sources.len(), 2);
|
|
|
|
|
assert_ne!(sources[0]["sourceReference"], sources[1]["sourceReference"]);
|
|
|
|
|
assert!(
|
|
|
|
|
sources.iter().any(|source| {
|
|
|
|
|
source["path"] == format!("runa-actor://session/{first}/behavior.rn")
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
let first_source_ref = sources
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|source| source["path"] == format!("runa-actor://session/{first}/behavior.rn"))
|
|
|
|
|
.unwrap()["sourceReference"]
|
|
|
|
|
.clone();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"source",
|
|
|
|
|
json!({"sourceReference":first_source_ref})
|
|
|
|
|
)["content"],
|
|
|
|
|
SOURCE
|
|
|
|
|
);
|
|
|
|
|
let first_source = json!({"path": format!("runa-actor://session/{first}/behavior.rn")});
|
|
|
|
|
let breakpoints = ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"setBreakpoints",
|
|
|
|
|
json!({"source": first_source, "breakpoints": [{"line": 5}]}),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(breakpoints["breakpoints"][0]["verified"], true);
|
|
|
|
|
let dirty = ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"setBreakpoints",
|
|
|
|
|
json!({
|
|
|
|
|
"source": {"path": format!("runa-actor://session/{first}/behavior.rn")},
|
|
|
|
|
"sourceModified": true,
|
|
|
|
|
"breakpoints": [{"line": 5}]
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(dirty["breakpoints"][0]["verified"], false);
|
|
|
|
|
let first_execution = image
|
|
|
|
|
.request(Command::InspectExecution {
|
|
|
|
|
actor_ref: first.clone(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
let Reply::ExecutionInspected(first_execution) = first_execution else {
|
|
|
|
|
panic!()
|
|
|
|
|
};
|
|
|
|
|
assert!(first_execution.debugger.breakpoints.is_empty());
|
|
|
|
|
let Reply::ExecutionInspected(other_execution) = image
|
|
|
|
|
.request(Command::InspectExecution {
|
|
|
|
|
actor_ref: second.clone(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
else {
|
|
|
|
|
panic!()
|
|
|
|
|
};
|
|
|
|
|
assert!(other_execution.debugger.breakpoints.is_empty());
|
|
|
|
|
|
|
|
|
|
ok(&mut adapter, "pause", json!({"threadId": first_thread}));
|
|
|
|
|
let stopped = adapter.poll();
|
|
|
|
|
assert!(stopped.iter().any(|message| {
|
|
|
|
|
message["event"] == "stopped"
|
|
|
|
|
&& message["body"]["threadId"] == first_thread
|
|
|
|
|
&& message["body"]["allThreadsStopped"] == false
|
|
|
|
|
}));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
image.inspect(&first).status,
|
|
|
|
|
runa_protocol::ActorStatusDto::Paused
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(image.inspect(&second).status, before_second.status);
|
|
|
|
|
|
|
|
|
|
let focused = request(&mut adapter, "focusActor", json!({"actorRef":second}));
|
|
|
|
|
assert_eq!(focused[0]["success"], true, "{focused:?}");
|
|
|
|
|
assert!(focused.iter().any(|message| {
|
|
|
|
|
message["event"] == "stopped"
|
|
|
|
|
&& message["body"]["threadId"] == second_thread
|
|
|
|
|
&& message["body"]["allThreadsStopped"] == false
|
|
|
|
|
}));
|
|
|
|
|
let first_root = ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"evaluate",
|
|
|
|
|
json!({"expression":"actor","threadId":first_thread}),
|
|
|
|
|
)["variablesReference"]
|
|
|
|
|
.clone();
|
|
|
|
|
let second_root = ok(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"evaluate",
|
|
|
|
|
json!({"expression":"actor","threadId":second_thread}),
|
|
|
|
|
)["variablesReference"]
|
|
|
|
|
.clone();
|
|
|
|
|
assert_ne!(first_root, second_root);
|
|
|
|
|
assert_ne!(first_root, first_source_ref);
|
|
|
|
|
assert_ne!(second_root, first_source_ref);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
request(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"variables",
|
|
|
|
|
json!({"variablesReference":first_root})
|
|
|
|
|
)[0]["success"],
|
|
|
|
|
true
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
request(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"variables",
|
|
|
|
|
json!({"variablesReference":second_root})
|
|
|
|
|
)[0]["success"],
|
|
|
|
|
true
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let continued = request(&mut adapter, "continue", json!({"threadId": first_thread}));
|
|
|
|
|
assert_eq!(continued[0]["success"], true);
|
|
|
|
|
assert_eq!(continued[0]["body"]["allThreadsContinued"], false);
|
|
|
|
|
assert!(continued.iter().any(|message| {
|
|
|
|
|
message["event"] == "continued"
|
|
|
|
|
&& message["body"]["threadId"] == first_thread
|
|
|
|
|
&& message["body"]["allThreadsContinued"] == false
|
|
|
|
|
}));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
image.inspect(&second).status,
|
|
|
|
|
runa_protocol::ActorStatusDto::Paused
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let focused = request(&mut adapter, "focusActor", json!({"actorRef":first}));
|
|
|
|
|
assert_eq!(focused[0]["success"], true, "{focused:?}");
|
|
|
|
|
assert!(focused.iter().any(|message| {
|
|
|
|
|
message["event"] == "stopped"
|
|
|
|
|
&& message["body"]["threadId"] == first_thread
|
|
|
|
|
&& message["body"]["allThreadsStopped"] == false
|
|
|
|
|
}));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
image.inspect(&first).status,
|
|
|
|
|
runa_protocol::ActorStatusDto::Paused
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
image.inspect(&second).status,
|
|
|
|
|
runa_protocol::ActorStatusDto::Paused
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
!image
|
|
|
|
|
.commands
|
|
|
|
|
.borrow()
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|command| matches!(command, Command::Tick { .. }))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn image_threads_track_spawn_stop_and_restart_incarnations() {
|
|
|
|
|
let (mut image, actor) = fixture();
|
|
|
|
|
let mut adapter = attach_image(&image);
|
|
|
|
|
let original = ok(&mut adapter, "threads", json!({}))["threads"][0]["id"]
|
|
|
|
|
.as_u64()
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let spawned = image.actor("tests/new", SOURCE);
|
|
|
|
|
let updated = request(&mut adapter, "threads", json!({}));
|
|
|
|
|
let spawned_thread = updated
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|message| {
|
|
|
|
|
message["event"] == "thread"
|
|
|
|
|
&& message["body"]["reason"] == "started"
|
|
|
|
|
&& message["body"]["threadId"] != original
|
|
|
|
|
})
|
|
|
|
|
.unwrap()["body"]["threadId"]
|
|
|
|
|
.as_u64()
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
updated[0]["body"]["threads"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|thread| thread["id"] == spawned_thread)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
image
|
|
|
|
|
.request(Command::RestartActor {
|
|
|
|
|
actor_ref: actor.clone(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
let restarted = adapter.poll();
|
|
|
|
|
assert!(restarted.iter().any(|message| {
|
|
|
|
|
message["event"] == "thread"
|
|
|
|
|
&& message["body"]["reason"] == "exited"
|
|
|
|
|
&& message["body"]["threadId"] == original
|
|
|
|
|
}));
|
|
|
|
|
let replacement_thread = ok(&mut adapter, "threads", json!({}))["threads"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|thread| thread["name"] == format!("@{actor}"))
|
|
|
|
|
.unwrap()["id"]
|
|
|
|
|
.as_u64()
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_ne!(replacement_thread, original);
|
|
|
|
|
|
|
|
|
|
image
|
|
|
|
|
.request(Command::StopActor {
|
|
|
|
|
actor_ref: spawned.clone(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
image
|
|
|
|
|
.request(Command::Tick {
|
|
|
|
|
ticks: 1,
|
|
|
|
|
instruction_budget: 1,
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
let removed = adapter.poll();
|
|
|
|
|
assert!(removed.iter().any(|message| {
|
|
|
|
|
message["event"] == "thread"
|
|
|
|
|
&& message["body"]["reason"] == "exited"
|
|
|
|
|
&& message["body"]["threadId"] == spawned_thread
|
|
|
|
|
}));
|
|
|
|
|
assert!(
|
|
|
|
|
!ok(&mut adapter, "threads", json!({}))["threads"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|thread| thread["id"] == spawned_thread)
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
ok(&mut adapter, "threads", json!({}))["threads"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|thread| thread["id"] == replacement_thread)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn image_behavior_rebind_retires_only_that_thread_and_its_source_reference() {
|
|
|
|
|
let (mut image, actor) = fixture();
|
|
|
|
|
let other = image.actor("tests/other", SOURCE);
|
|
|
|
|
let mut adapter = attach_image(&image);
|
|
|
|
|
let threads = ok(&mut adapter, "threads", json!({}))["threads"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.clone();
|
|
|
|
|
let actor_thread = threads
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|thread| thread["name"] == format!("@{actor}"))
|
|
|
|
|
.unwrap()["id"]
|
|
|
|
|
.as_u64()
|
|
|
|
|
.unwrap();
|
|
|
|
|
let other_thread = threads
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|thread| thread["name"] == format!("@{other}"))
|
|
|
|
|
.unwrap()["id"]
|
|
|
|
|
.as_u64()
|
|
|
|
|
.unwrap();
|
|
|
|
|
let old_source = ok(&mut adapter, "loadedSources", json!({}))["sources"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|source| source["path"] == format!("runa-actor://session/{actor}/behavior.rn"))
|
|
|
|
|
.unwrap()["sourceReference"]
|
|
|
|
|
.as_u64()
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
image
|
|
|
|
|
.request(Command::PublishBehavior {
|
|
|
|
|
name: "tests/rebound".to_owned(),
|
|
|
|
|
expected_revision: None,
|
|
|
|
|
source: SOURCE.replace("value + 1", "value + 2"),
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
image
|
|
|
|
|
.request(Command::InstallBehavior {
|
|
|
|
|
actor_ref: actor.clone(),
|
|
|
|
|
behavior_name: "tests/rebound".to_owned(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
let status_before_poll = image.inspect(&actor).status;
|
|
|
|
|
let events = adapter.poll();
|
|
|
|
|
assert_eq!(image.inspect(&actor).status, status_before_poll);
|
|
|
|
|
assert!(events.iter().any(|message| {
|
|
|
|
|
message["event"] == "thread"
|
|
|
|
|
&& message["body"]["reason"] == "exited"
|
|
|
|
|
&& message["body"]["threadId"] == actor_thread
|
|
|
|
|
}));
|
|
|
|
|
let replacement = ok(&mut adapter, "threads", json!({}))["threads"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|thread| thread["name"] == format!("@{actor}"))
|
|
|
|
|
.unwrap()["id"]
|
|
|
|
|
.as_u64()
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_ne!(replacement, actor_thread);
|
|
|
|
|
assert!(
|
|
|
|
|
ok(&mut adapter, "threads", json!({}))["threads"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|thread| thread["id"] == other_thread)
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
request(
|
|
|
|
|
&mut adapter,
|
|
|
|
|
"source",
|
|
|
|
|
json!({"sourceReference":old_source})
|
|
|
|
|
)[0]["success"],
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
let new_source = ok(&mut adapter, "loadedSources", json!({}))["sources"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|source| source["path"] == format!("runa-actor://session/{actor}/behavior.rn"))
|
|
|
|
|
.unwrap()["sourceReference"]
|
|
|
|
|
.clone();
|
|
|
|
|
assert_ne!(old_source, new_source.as_u64().unwrap());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn image_frame_ids_are_global_and_route_to_their_own_actor() {
|
|
|
|
|
let (mut image, first) = fixture();
|
|
|
|
|
let second = image.actor("tests/second", SOURCE);
|
|
|
|
|
for actor_ref in [&first, &second] {
|
|
|
|
|
image
|
|
|
|
|
.request(Command::Send {
|
|
|
|
|
actor_ref: actor_ref.clone(),
|
|
|
|
|
message: Value::Integer(3),
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
}
|
|
|
|
|
image
|
|
|
|
|
.request(Command::Tick {
|
|
|
|
|
ticks: 1,
|
|
|
|
|
instruction_budget: 1,
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
let mut adapter = attach_image(&image);
|
|
|
|
|
let source_references: Vec<_> = ok(&mut adapter, "loadedSources", json!({}))["sources"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|source| source["sourceReference"].as_u64().unwrap())
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
let mut frame_ids = Vec::new();
|
|
|
|
|
for actor_ref in [&first, &second] {
|
|
|
|
|
let focused = request(&mut adapter, "focusActor", json!({"actorRef":actor_ref}));
|
|
|
|
|
assert_eq!(focused[0]["success"], true, "{focused:?}");
|
|
|
|
|
let threads = ok(&mut adapter, "threads", json!({}))["threads"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.clone();
|
|
|
|
|
let thread_id = threads
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|thread| thread["name"] == format!("@{actor_ref}"))
|
|
|
|
|
.unwrap()["id"]
|
|
|
|
|
.clone();
|
|
|
|
|
let trace = ok(&mut adapter, "stackTrace", json!({"threadId":thread_id}));
|
|
|
|
|
assert!(trace["totalFrames"].as_u64().unwrap() > 0, "{trace:?}");
|
|
|
|
|
let frame_id = trace["stackFrames"][0]["id"].as_u64().unwrap();
|
|
|
|
|
assert!(!source_references.contains(&frame_id));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
ok(&mut adapter, "scopes", json!({"frameId":frame_id}))["scopes"]
|
|
|
|
|
.as_array()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.len(),
|
|
|
|
|
2
|
|
|
|
|
);
|
|
|
|
|
frame_ids.push(frame_id);
|
|
|
|
|
}
|
|
|
|
|
assert_ne!(frame_ids[0], frame_ids[1]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn image_focus_leaves_failed_actor_unchanged() {
|
|
|
|
|
let mut image = Image::new();
|
|
|
|
|
let actor = image.actor(
|
|
|
|
|
"tests/failure",
|
|
|
|
|
"pub fn handle(ctx, state, sender, message) { panic(\"failed\") }",
|
|
|
|
|
);
|
|
|
|
|
image
|
|
|
|
|
.request(Command::Send {
|
|
|
|
|
actor_ref: actor.clone(),
|
|
|
|
|
message: Value::Null,
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
image
|
|
|
|
|
.request(Command::Tick {
|
|
|
|
|
ticks: 1,
|
|
|
|
|
instruction_budget: 1000,
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
let before = image.inspect(&actor);
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
before.status,
|
|
|
|
|
runa_protocol::ActorStatusDto::Failed(_)
|
|
|
|
|
));
|
|
|
|
|
image.commands.borrow_mut().clear();
|
|
|
|
|
let mut adapter = attach_image(&image);
|
|
|
|
|
let messages = request(&mut adapter, "focusActor", json!({"actorRef":actor}));
|
|
|
|
|
assert_eq!(messages[0]["success"], true, "{messages:?}");
|
|
|
|
|
assert!(messages.iter().any(|message| message["event"] == "stopped"));
|
|
|
|
|
assert_eq!(image.inspect(&actor), before);
|
|
|
|
|
assert!(
|
|
|
|
|
!image
|
|
|
|
|
.commands
|
|
|
|
|
.borrow()
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|command| matches!(command, Command::System { .. }))
|
|
|
|
|
);
|
|
|
|
|
}
|