Skip to content

Commit 8a80e3a

Browse files
committed
Pass Console reference to handlers directly
Better reflects the borrowing model
1 parent 78ae528 commit 8a80e3a

11 files changed

Lines changed: 113 additions & 34 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ark/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ edition.workspace = true
1010
license.workspace = true
1111
rust-version.workspace = true
1212

13+
[features]
14+
testing = []
15+
1316
[lints]
1417
workspace = true
1518

@@ -73,6 +76,7 @@ winsafe.workspace = true
7376
yaml-rust.workspace = true
7477

7578
[dev-dependencies]
79+
ark = { path = ".", features = ["testing"] }
7680
ark_test.workspace = true
7781
assert_matches.workspace = true
7882
insta.workspace = true

crates/ark/src/comm_handler.rs

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ use stdext::result::ResultExt;
2121
use crate::console::Console;
2222

2323
/// Context provided to `CommHandler` methods, giving access to the outgoing
24-
/// channel, close-request mechanism, and the Console singleton (via
25-
/// `console()`). Through the Console, handlers can reach runtime state such
26-
/// as the graphics device context.
24+
/// channel and close-request mechanism.
2725
#[derive(Debug)]
2826
pub struct CommHandlerContext {
2927
pub outgoing_tx: CommOutgoingTx,
@@ -59,11 +57,6 @@ impl CommHandlerContext {
5957
};
6058
self.outgoing_tx.send(CommMsg::Data(json)).log_err();
6159
}
62-
63-
/// Access the Console singleton (the R runtime).
64-
pub(crate) fn console(&self) -> &Console {
65-
Console::get()
66-
}
6760
}
6861

6962
/// Trait for comm handlers that run synchronously on the R thread.
@@ -79,18 +72,24 @@ pub trait CommHandler: Debug {
7972

8073
/// Initialise handler state on the R thread (initial scan, first event,
8174
/// etc.). Default is no-op.
82-
fn handle_open(&mut self, _ctx: &CommHandlerContext) {}
75+
fn handle_open(&mut self, _ctx: &CommHandlerContext, _console: &Console) {}
8376

8477
/// Handle an incoming message (RPC or data).
85-
fn handle_msg(&mut self, msg: CommMsg, ctx: &CommHandlerContext);
78+
fn handle_msg(&mut self, msg: CommMsg, ctx: &CommHandlerContext, console: &Console);
8679

8780
/// Handle comm close. Default is no-op.
88-
fn handle_close(&mut self, _ctx: &CommHandlerContext) {}
81+
fn handle_close(&mut self, _ctx: &CommHandlerContext, _console: &Console) {}
8982

9083
/// Called when the environment changes. The `event` indicates what
9184
/// triggered the change so handlers can decide whether to react.
9285
/// Default is no-op.
93-
fn handle_environment(&mut self, _event: &EnvironmentChanged, _ctx: &CommHandlerContext) {}
86+
fn handle_environment(
87+
&mut self,
88+
_event: &EnvironmentChanged,
89+
_ctx: &CommHandlerContext,
90+
_console: &Console,
91+
) {
92+
}
9493
}
9594

9695
/// Why the environment changed.

crates/ark/src/console.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ thread_local! {
177177
pub static CONSOLE: RefCell<UnsafeCell<Console>> = panic!("Must access `CONSOLE` from the R thread");
178178
}
179179

180-
pub(crate) struct Console {
180+
pub struct Console {
181181
pub(crate) positron_ns: Option<RObject>,
182182

183183
kernel_request_rx: Receiver<KernelRequest>,

crates/ark/src/console/console_comm.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ impl Console {
2525
log::warn!("Received message for unknown registered comm {comm_id}");
2626
return;
2727
};
28-
comm.handler.handle_msg(msg, &comm.ctx);
28+
comm.handler.handle_msg(msg, &comm.ctx, Console::get());
2929
self.drain_closed();
3030
}
3131

@@ -34,7 +34,7 @@ impl Console {
3434
log::warn!("Received close for unknown registered comm {comm_id}");
3535
return;
3636
};
37-
comm.handler.handle_close(&comm.ctx);
37+
comm.handler.handle_close(&comm.ctx, Console::get());
3838
}
3939

4040
/// Register a backend-initiated comm on the R thread.
@@ -57,7 +57,7 @@ impl Console {
5757
);
5858

5959
let ctx = CommHandlerContext::new(comm.outgoing_tx.clone(), self.comm_event_tx.clone());
60-
handler.handle_open(&ctx);
60+
handler.handle_open(&ctx, Console::get());
6161

6262
self.comms
6363
.insert(comm_id.clone(), ConsoleComm { handler, ctx });
@@ -82,13 +82,13 @@ impl Console {
8282
mut handler: Box<dyn CommHandler>,
8383
) {
8484
let ctx = CommHandlerContext::new(outgoing_tx, self.comm_event_tx.clone());
85-
handler.handle_open(&ctx);
85+
handler.handle_open(&ctx, Console::get());
8686

8787
if comm_name == UI_COMM_NAME {
8888
if let Some(old_id) = self.ui_comm_id.take() {
8989
log::info!("Replacing an existing UI comm.");
9090
if let Some(mut old) = self.comm_remove(&old_id) {
91-
old.handler.handle_close(&old.ctx);
91+
old.handler.handle_close(&old.ctx, Console::get());
9292
}
9393
}
9494
self.ui_comm_id = Some(comm_id.clone());
@@ -99,7 +99,8 @@ impl Console {
9999

100100
pub(super) fn comm_notify_environment_changed(&mut self, event: &EnvironmentChanged) {
101101
for (_, comm) in self.comms.iter_mut() {
102-
comm.handler.handle_environment(event, &comm.ctx);
102+
comm.handler
103+
.handle_environment(event, &comm.ctx, Console::get());
103104
}
104105
self.drain_closed();
105106
}

crates/ark/src/console/console_repl.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -663,10 +663,66 @@ impl Console {
663663
/// Access a reference to the singleton instance of this struct
664664
///
665665
/// SAFETY: Accesses must occur after `Console::start()` initializes it.
666-
pub(crate) fn get() -> &'static Self {
666+
pub fn get() -> &'static Self {
667667
Console::get_mut()
668668
}
669669

670+
/// Install a minimal stopgap `Console` in the thread-local for unit tests
671+
/// that need a `&Console` (e.g. to pass to `CommHandler` methods) but
672+
/// don't go through the full `Console::start()` path.
673+
///
674+
/// All internal channels are created with their counterpart immediately
675+
/// dropped, so the Console is inert: it never processes requests, tasks,
676+
/// or kernel messages. Any attempt to send to IOPub or other channels
677+
/// will return a disconnected error, which surfaces problems early
678+
/// rather than silently hanging.
679+
///
680+
/// For tests that exercise real Console behaviour (execution, comms
681+
/// lifecycle, IOPub output), prefer full integration tests with
682+
/// `DummyArkFrontend` which spins up a real Console.
683+
///
684+
/// Idempotent per thread. Does not set `R_INIT`, so the `r_task()` escape
685+
/// hatch for unit tests continues to work.
686+
#[cfg(feature = "testing")]
687+
pub fn test_init() {
688+
use std::cell::Cell;
689+
thread_local! {
690+
static INITIALIZED: Cell<bool> = const { Cell::new(false) };
691+
}
692+
693+
INITIALIZED.with(|init| {
694+
if init.get() {
695+
return;
696+
}
697+
init.set(true);
698+
699+
let (_, tasks_interrupt_rx) = crossbeam::channel::unbounded();
700+
let (_, tasks_idle_rx) = crossbeam::channel::unbounded();
701+
let (_, tasks_idle_any_rx) = crossbeam::channel::unbounded();
702+
let (comm_event_tx, _) = crossbeam::channel::unbounded();
703+
let (r_request_tx, r_request_rx) = crossbeam::channel::unbounded();
704+
let (stdin_request_tx, _) = crossbeam::channel::unbounded();
705+
let (_, stdin_reply_rx) = crossbeam::channel::unbounded();
706+
let (iopub_tx, _) = crossbeam::channel::unbounded();
707+
let (_, kernel_request_rx) = crossbeam::channel::unbounded();
708+
let dap = Dap::new_shared(r_request_tx);
709+
710+
CONSOLE.set(UnsafeCell::new(Console::new(
711+
tasks_interrupt_rx,
712+
tasks_idle_rx,
713+
tasks_idle_any_rx,
714+
comm_event_tx,
715+
r_request_rx,
716+
stdin_request_tx,
717+
stdin_reply_rx,
718+
iopub_tx,
719+
kernel_request_rx,
720+
dap,
721+
SessionMode::Console,
722+
)));
723+
});
724+
}
725+
670726
/// Access a mutable reference to the singleton instance of this struct
671727
///
672728
/// SAFETY: Accesses must occur after `Console::start()` initializes it.

crates/ark/src/data_explorer/r_data_explorer.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -481,13 +481,18 @@ impl CommHandler for RDataExplorer {
481481
serde_json::json!({ "title": self.title, "inline_only": inline_only })
482482
}
483483

484-
fn handle_msg(&mut self, msg: CommMsg, ctx: &CommHandlerContext) {
484+
fn handle_msg(&mut self, msg: CommMsg, ctx: &CommHandlerContext, _console: &Console) {
485485
handle_rpc_request(&ctx.outgoing_tx, DATA_EXPLORER_COMM_NAME, msg, |req| {
486486
self.handle_rpc(req, ctx)
487487
});
488488
}
489489

490-
fn handle_environment(&mut self, event: &EnvironmentChanged, ctx: &CommHandlerContext) {
490+
fn handle_environment(
491+
&mut self,
492+
event: &EnvironmentChanged,
493+
ctx: &CommHandlerContext,
494+
_console: &Console,
495+
) {
491496
let EnvironmentChanged::Execution { .. } = event else {
492497
return;
493498
};

crates/ark/src/fixtures/utils.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use std::sync::Once;
99

1010
use tree_sitter::Point;
1111

12+
#[cfg(feature = "testing")]
13+
use crate::console::Console;
1214
use crate::modules;
1315
use crate::modules::ARK_ENVS;
1416

@@ -20,6 +22,10 @@ pub fn r_test_init() {
2022
// Initialize the positron module so tests can use them.
2123
modules::initialize().unwrap();
2224
});
25+
// Per-thread: install a minimal Console singleton so that unit tests
26+
// can pass `&Console` to comm handlers via `Console::get()`.
27+
#[cfg(feature = "testing")]
28+
Console::test_init();
2329
}
2430

2531
pub fn point_from_cursor(x: &str) -> (String, Point) {

crates/ark/src/plots/graphics_device.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -900,15 +900,15 @@ impl CommHandler for PlotComm {
900900
self.open_data.clone()
901901
}
902902

903-
fn handle_msg(&mut self, msg: CommMsg, ctx: &CommHandlerContext) {
904-
let dc = ctx.console().device_context();
903+
fn handle_msg(&mut self, msg: CommMsg, ctx: &CommHandlerContext, console: &Console) {
904+
let dc = console.device_context();
905905
handle_rpc_request(&ctx.outgoing_tx, PLOT_COMM_NAME, msg, |req| {
906906
dc.handle_rpc(req, &self.id)
907907
});
908908
}
909909

910-
fn handle_close(&mut self, ctx: &CommHandlerContext) {
911-
ctx.console().device_context().on_plot_closed(&self.id);
910+
fn handle_close(&mut self, _ctx: &CommHandlerContext, console: &Console) {
911+
console.device_context().on_plot_closed(&self.id);
912912
}
913913
}
914914

crates/ark/src/ui/ui_comm.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ pub struct UiComm {
5050
}
5151

5252
impl CommHandler for UiComm {
53-
fn handle_open(&mut self, ctx: &CommHandlerContext) {
53+
fn handle_open(&mut self, ctx: &CommHandlerContext, _console: &Console) {
5454
// Set initial console width from the comm_open data, if provided.
5555
if let Some(width) = self.comm_open_data.console_width {
5656
if let Err(err) = RFunction::from(".ps.rpc.setConsoleWidth")
@@ -69,13 +69,18 @@ impl CommHandler for UiComm {
6969
self.refresh(&input_prompt, &continuation_prompt, ctx);
7070
}
7171

72-
fn handle_msg(&mut self, msg: CommMsg, ctx: &CommHandlerContext) {
72+
fn handle_msg(&mut self, msg: CommMsg, ctx: &CommHandlerContext, _console: &Console) {
7373
handle_rpc_request(&ctx.outgoing_tx, UI_COMM_NAME, msg, |req| {
7474
self.handle_rpc(req)
7575
});
7676
}
7777

78-
fn handle_environment(&mut self, event: &EnvironmentChanged, ctx: &CommHandlerContext) {
78+
fn handle_environment(
79+
&mut self,
80+
event: &EnvironmentChanged,
81+
ctx: &CommHandlerContext,
82+
_console: &Console,
83+
) {
7984
let EnvironmentChanged::Execution {
8085
input_prompt,
8186
continuation_prompt,
@@ -307,7 +312,7 @@ mod tests {
307312
}))
308313
.unwrap(),
309314
};
310-
handler.handle_msg(msg, &ctx);
315+
handler.handle_msg(msg, &ctx, Console::get());
311316

312317
// Assert that the console width changed
313318
let new_width: i32 = harp::get_option("width").try_into().unwrap();
@@ -323,7 +328,7 @@ mod tests {
323328
}))
324329
.unwrap(),
325330
};
326-
handler.handle_msg(msg, &ctx);
331+
handler.handle_msg(msg, &ctx, Console::get());
327332

328333
old_width
329334
});
@@ -369,7 +374,7 @@ mod tests {
369374
}))
370375
.unwrap(),
371376
};
372-
handler.handle_msg(msg, &ctx);
377+
handler.handle_msg(msg, &ctx, Console::get());
373378
});
374379

375380
let response = iopub_rx.recv_comm_msg();

0 commit comments

Comments
 (0)