Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ca8f385
Draft DAP protocol tests
lionel- Jan 30, 2026
27af77c
Move test fixtures to dev-only crate
lionel- Jan 30, 2026
99a6799
Add simple browser test
lionel- Jan 30, 2026
1e5e8e8
Don't send unnecessary `stop_debug` events
lionel- Jan 30, 2026
f10cd86
Test Threads and StackTrace
lionel- Jan 30, 2026
f48aa88
Use simple `unwrap()`s
lionel- Jan 30, 2026
f6bf470
Test all Stopped fields
lionel- Jan 30, 2026
a442c97
Test more stack trace fields
lionel- Jan 30, 2026
1e193a5
Test `source()` and stepping
lionel- Jan 30, 2026
314ae7d
Test execution of non-debug expression
lionel- Jan 30, 2026
6b40684
Extract `send_source()`
lionel- Jan 30, 2026
d717e85
Add more basic tests
lionel- Jan 30, 2026
5aca4ed
Add more variables tests
lionel- Jan 30, 2026
88d2ac9
Add basic breakpoint tests
lionel- Jan 30, 2026
3d0f4e6
Add state tests for breakpoints
lionel- Jan 30, 2026
3200388
More breakpoint state tests
lionel- Jan 31, 2026
4de84fb
Add breakpoint invalidation tests
lionel- Jan 31, 2026
3dfe7a0
Add session switching tests for breakpoints
lionel- Jan 31, 2026
9bf4ecc
More breakpoint tests
lionel- Feb 1, 2026
25ba86f
Add accumulator to fix flakiness due to message streams
lionel- Feb 1, 2026
ad96dde
Add more tests and message tracing
lionel- Feb 1, 2026
ba894b6
More breakpoints tests
lionel- Feb 1, 2026
e87a85b
Consume messages and check all are accounted for
lionel- Feb 3, 2026
6d1420d
Fix flakiness issues
lionel- Feb 3, 2026
436bb96
Update agents advice
lionel- Feb 3, 2026
b869cad
Split test files
lionel- Feb 3, 2026
03bd67e
Fix Windows failures
lionel- Feb 3, 2026
fb03d39
Allow trailing `Q` commands due to DAP disconnection on drop
lionel- Feb 3, 2026
c083a80
Fix flaky test
lionel- Feb 3, 2026
ed41d8b
Fix flaky test
lionel- Feb 3, 2026
884a2af
TMP: trace tests for arm win failure
lionel- Feb 3, 2026
a489ed7
WIP: Fix attempt for Windows ARM
lionel- Feb 3, 2026
9b3828b
WIP: Fix attempt for Windows ARM
lionel- Feb 3, 2026
f42fb38
TMP: More tracing
lionel- Feb 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,7 @@ jobs:
- name: Run Tests
env:
NEXTEST_PROFILE: "ci"
# Enable tracing to debug Windows ARM timeout issues with debug mode tests
ARK_TEST_TRACE: "all"
run: |
cargo nextest run
49 changes: 49 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,55 @@ just test <test_name>
just test -p ark
```

### Kernel and DAP Test Infrastructure

Integration tests for the kernel and DAP server live in `crates/ark/tests/` and use the test utilities from `crates/ark_test/`.

**Key components:**

- **`DummyArkFrontend`**: A mock Jupyter frontend that communicates with the kernel over ZMQ sockets. Use `DummyArkFrontend::lock()` to acquire it (only one per process).

- **`DapClient`**: A DAP client for testing the debugger. Obtained via `frontend.dap_client()` after starting the kernel.

- **`MessageAccumulator`**: Collects IOPub messages and coalesces stream fragments, making tests immune to batching variations.

**Common patterns:**

```rust
// Lock the frontend and send an execute request
let frontend = DummyArkFrontend::lock();
frontend.send_execute_request("1 + 1", ExecuteRequestOptions::default());
frontend.recv_iopub_busy();
frontend.recv_iopub_execute_input();
frontend.recv_iopub_execute_result();
frontend.recv_iopub_idle();
frontend.recv_shell_execute_reply();

// For complex async message flows, use recv_iopub_until with MessageAccumulator
frontend.recv_iopub_until(|acc| {
acc.has_comm_method("start_debug") &&
acc.streams_contain("debug at") &&
acc.saw_idle()
});

// Use in_order() to verify message ordering
frontend.recv_iopub_until(|acc| {
acc.in_order(&[is_execute_result(), is_idle()])
});
```

**Debugging tests:**

Log messages (from the `log` crate) are not shown in test output. Use `eprintln!` for printf-style debugging.

Enable message tracing to see timestamped DAP and IOPub message flows:

```bash
ARK_TEST_TRACE=1 just test test_name # All messages
ARK_TEST_TRACE=dap just test test_name # DAP events only
ARK_TEST_TRACE=iopub just test test_name # IOPub messages only
```

### Required R Packages for Testing

The following R packages are required for tests:
Expand Down
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/ark/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ rustc-hash = "2.1.1"
tracing-error = "0.2.0"

[dev-dependencies]
ark_test = { path = "../ark_test" }
insta = { version = "1.39.0" }
stdext = { path = "../stdext", features = ["testing"] }
tempfile = "3.13.0"
Expand Down
3 changes: 0 additions & 3 deletions crates/ark/src/console.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1569,9 +1569,6 @@ impl Console {
// For continue-like commands, we do not preserve focus,
// i.e. we let the cursor jump to the stopped position.
self.debug_preserve_focus = false;

// Let the DAP client know that execution is now continuing
self.debug_send_dap(DapBackendEvent::Continued);
}

// Forward the command to R's base REPL.
Expand Down
178 changes: 174 additions & 4 deletions crates/ark/src/dap/dap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ impl Dap {
self.stack = Some(stack);

log::trace!("DAP: Sending `start_debug` events");
eprintln!(
"DAP trace: start_debug: comm_tx={}, backend_events_tx={}",
self.comm_tx.is_some(),
self.backend_events_tx.is_some()
);

if let Some(comm_tx) = &self.comm_tx {
// Ask frontend to connect to the DAP
Expand All @@ -228,9 +233,17 @@ impl Dap {
.log_err();

if let Some(dap_tx) = &self.backend_events_tx {
dap_tx
.send(DapBackendEvent::Stopped(DapStoppedEvent { preserve_focus }))
.log_err();
eprintln!("DAP trace: start_debug: sending Stopped event");
match dap_tx.send(DapBackendEvent::Stopped(DapStoppedEvent { preserve_focus })) {
Ok(()) => eprintln!("DAP trace: start_debug: Stopped event sent"),
Err(err) => {
eprintln!("DAP trace: start_debug: failed to send Stopped event: {err:?}")
},
}
} else {
eprintln!(
"DAP trace: start_debug: backend_events_tx is None, cannot send Stopped event"
);
}
}
}
Expand All @@ -247,9 +260,11 @@ impl Dap {
self.fallback_sources.clear();
self.clear_variables_reference_maps();
self.reset_variables_reference_count();

let was_debugging = self.is_debugging;
self.is_debugging = false;

if self.is_connected {
if was_debugging && self.is_connected {
log::trace!("DAP: Sending `stop_debug` events");

if let Some(comm_tx) = &self.comm_tx {
Expand Down Expand Up @@ -507,3 +522,158 @@ impl ServerHandler for Dap {
return Ok(());
}
}

#[cfg(test)]
mod tests {
use crossbeam::channel::unbounded;

use super::*;

fn create_test_dap() -> (Dap, crossbeam::channel::Receiver<DapBackendEvent>) {
let (backend_events_tx, backend_events_rx) = unbounded();
let (r_request_tx, _r_request_rx) = unbounded();

let dap = Dap {
is_debugging: false,
is_connected: true,
backend_events_tx: Some(backend_events_tx),
stack: None,
breakpoints: HashMap::new(),
fallback_sources: HashMap::new(),
frame_id_to_variables_reference: HashMap::new(),
variables_reference_to_r_object: HashMap::new(),
current_variables_reference: 1,
current_breakpoint_id: 1,
comm_tx: None,
r_request_tx,
shared_self: None,
};

(dap, backend_events_rx)
}

#[test]
fn test_did_change_document_removes_breakpoints() {
let (mut dap, rx) = create_test_dap();

let uri = Url::parse("file:///test.R").unwrap();
let hash = blake3::hash(b"test content");

dap.breakpoints.insert(
uri.clone(),
(hash, vec![
Breakpoint::new(1, 10, BreakpointState::Verified),
Breakpoint::new(2, 20, BreakpointState::Verified),
]),
);

dap.did_change_document(&uri);

assert!(dap.breakpoints.get(&uri).is_none());

let event1 = rx.try_recv().unwrap();
let event2 = rx.try_recv().unwrap();

assert!(matches!(event1, DapBackendEvent::BreakpointState {
id: 1,
verified: false,
..
}));
assert!(matches!(event2, DapBackendEvent::BreakpointState {
id: 2,
verified: false,
..
}));

assert!(rx.try_recv().is_err());
}

#[test]
fn test_did_change_document_no_breakpoints_is_noop() {
let (mut dap, rx) = create_test_dap();

let uri = Url::parse("file:///test.R").unwrap();

dap.did_change_document(&uri);

assert!(rx.try_recv().is_err());
}

#[test]
fn test_did_change_document_only_affects_target_uri() {
let (mut dap, rx) = create_test_dap();

let uri1 = Url::parse("file:///test1.R").unwrap();
let uri2 = Url::parse("file:///test2.R").unwrap();
let hash1 = blake3::hash(b"content 1");
let hash2 = blake3::hash(b"content 2");

dap.breakpoints.insert(
uri1.clone(),
(hash1, vec![Breakpoint::new(
1,
10,
BreakpointState::Verified,
)]),
);
dap.breakpoints.insert(
uri2.clone(),
(hash2, vec![Breakpoint::new(
2,
20,
BreakpointState::Verified,
)]),
);

dap.did_change_document(&uri1);

assert!(dap.breakpoints.get(&uri1).is_none());
assert!(dap.breakpoints.get(&uri2).is_some());

let event = rx.try_recv().unwrap();
assert!(matches!(event, DapBackendEvent::BreakpointState {
id: 1,
..
}));
assert!(rx.try_recv().is_err());
}

#[test]
fn test_did_change_document_without_backend_tx_is_noop() {
let (r_request_tx, _r_request_rx) = unbounded();

let mut dap = Dap {
is_debugging: false,
is_connected: false,
backend_events_tx: None,
stack: None,
breakpoints: HashMap::new(),
fallback_sources: HashMap::new(),
frame_id_to_variables_reference: HashMap::new(),
variables_reference_to_r_object: HashMap::new(),
current_variables_reference: 1,
current_breakpoint_id: 1,
comm_tx: None,
r_request_tx,
shared_self: None,
};

let uri = Url::parse("file:///test.R").unwrap();
let hash = blake3::hash(b"test content");

dap.breakpoints.insert(
uri.clone(),
(hash, vec![Breakpoint::new(
1,
10,
BreakpointState::Verified,
)]),
);

// Should not panic even without `backend_events_tx`
dap.did_change_document(&uri);

// Breakpoints should still be removed
assert!(dap.breakpoints.get(&uri).is_none());
}
}
Loading
Loading