-
Notifications
You must be signed in to change notification settings - Fork 26
Restore output capture of R tasks correctly #1065
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| // | ||
| // kernel-captured-output.rs | ||
| // | ||
| // Copyright (C) 2026 Posit Software, PBC. All rights reserved. | ||
| // | ||
| // Integration tests verifying that pending async idle tasks don't swallow | ||
| // console output. See the `captured_output` save/restore mechanism in | ||
| // `poll_task` and the test helpers in `r_task.rs` for the full flow. | ||
|
|
||
| use std::time::Duration; | ||
| use std::time::Instant; | ||
|
|
||
| use amalthea::fixtures::dummy_frontend::ExecuteRequestOptions; | ||
| use ark_test::comm::RECV_TIMEOUT; | ||
| use ark_test::DummyArkFrontend; | ||
|
|
||
| /// Poll until the kernel confirms the pending idle task has been polled | ||
| /// by checking the `ark.test.task_polled` R option. | ||
| /// | ||
| /// We sleep between iterations so the kernel's event loop gets actual | ||
| /// idle time. Without the sleep, execute requests win the `try_recv` | ||
| /// priority check every time and completely starve the idle-task | ||
| /// channel. The kernel never reaches `select` where it could pick up | ||
| /// the idle task. | ||
| fn wait_until_task_polled(frontend: &DummyArkFrontend) { | ||
| let deadline = Instant::now() + RECV_TIMEOUT; | ||
| let polled = std::cell::Cell::new(false); | ||
| loop { | ||
| std::thread::sleep(Duration::from_millis(50)); | ||
| frontend.execute_request("getOption('ark.test.task_polled', FALSE)", |result| { | ||
| polled.set(result.contains("TRUE")); | ||
| }); | ||
| if polled.get() { | ||
| return; | ||
| } | ||
| if Instant::now() >= deadline { | ||
| panic!("Timed out waiting for idle task to be polled"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_pending_idle_task_does_not_swallow_stdout() { | ||
| let frontend = DummyArkFrontend::lock(); | ||
|
|
||
| // Spawn an idle task that holds a ConsoleOutputCapture and stays pending. | ||
| frontend.execute_request_invisibly(r#"invisible(.Call("ps_test_spawn_pending_task"))"#); | ||
|
|
||
| // Wait until the idle task has been polled (capture is active then saved). | ||
| wait_until_task_polled(&frontend); | ||
|
|
||
| // This output must reach IOPub as a stream message, not be captured. | ||
| frontend.send_execute_request( | ||
| r#"cat("hello from stdout\n")"#, | ||
| ExecuteRequestOptions::default(), | ||
| ); | ||
| frontend.recv_iopub_busy(); | ||
| frontend.recv_iopub_execute_input(); | ||
| frontend.assert_stream_stdout_contains("hello from stdout"); | ||
| frontend.recv_iopub_idle(); | ||
| frontend.recv_shell_execute_reply(); | ||
|
|
||
| // Clean up: unblock the pending task so it finishes. | ||
| frontend.execute_request_invisibly(r#"invisible(.Call("ps_test_complete_pending_task"))"#); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_pending_idle_task_does_not_swallow_autoprint() { | ||
| let frontend = DummyArkFrontend::lock(); | ||
|
|
||
| // Spawn the pending idle task. | ||
| frontend.execute_request_invisibly(r#"invisible(.Call("ps_test_spawn_pending_task"))"#); | ||
|
|
||
| // Wait until the idle task has been polled. | ||
| wait_until_task_polled(&frontend); | ||
|
|
||
| // Autoprint output must still arrive as execute_result. | ||
| frontend.execute_request("42", |result| { | ||
| assert_eq!(result, "[1] 42"); | ||
| }); | ||
|
|
||
| // Clean up. | ||
| frontend.execute_request_invisibly(r#"invisible(.Call("ps_test_complete_pending_task"))"#); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_pending_idle_task_does_not_swallow_stderr() { | ||
| let frontend = DummyArkFrontend::lock(); | ||
|
|
||
| // Spawn the pending idle task. | ||
| frontend.execute_request_invisibly(r#"invisible(.Call("ps_test_spawn_pending_task"))"#); | ||
|
|
||
| // Wait until the idle task has been polled. | ||
| wait_until_task_polled(&frontend); | ||
|
|
||
| // Stderr must still reach IOPub. | ||
| frontend.send_execute_request( | ||
| r#"cat("hello from stderr\n", file = stderr())"#, | ||
| ExecuteRequestOptions::default(), | ||
| ); | ||
| frontend.recv_iopub_busy(); | ||
| frontend.recv_iopub_execute_input(); | ||
| frontend.assert_stream_stderr_contains("hello from stderr"); | ||
| frontend.recv_iopub_idle(); | ||
| frontend.recv_shell_execute_reply(); | ||
|
|
||
| // Clean up. | ||
| frontend.execute_request_invisibly(r#"invisible(.Call("ps_test_complete_pending_task"))"#); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_multiple_pending_tasks_do_not_swallow_output() { | ||
| let frontend = DummyArkFrontend::lock(); | ||
|
|
||
| // Spawn two pending idle tasks. The second spawn overwrites the oneshot | ||
| // sender so the first task will be orphaned (its oneshot is dropped), | ||
| // which is fine - the future resolves with Err(Canceled). The R option | ||
| // is reset by each spawn and set by each poll, so we only need to wait | ||
| // for the last one. | ||
| frontend.execute_request_invisibly(r#"invisible(.Call("ps_test_spawn_pending_task"))"#); | ||
| frontend.execute_request_invisibly(r#"invisible(.Call("ps_test_spawn_pending_task"))"#); | ||
|
|
||
| // Wait until the idle task has been polled. | ||
| wait_until_task_polled(&frontend); | ||
|
|
||
| // Output must still reach IOPub. | ||
| frontend.send_execute_request(r#"cat("still works\n")"#, ExecuteRequestOptions::default()); | ||
| frontend.recv_iopub_busy(); | ||
| frontend.recv_iopub_execute_input(); | ||
| frontend.assert_stream_stdout_contains("still works"); | ||
| frontend.recv_iopub_idle(); | ||
| frontend.recv_shell_execute_reply(); | ||
|
|
||
| // Clean up. | ||
| frontend.execute_request_invisibly(r#"invisible(.Call("ps_test_complete_pending_task"))"#); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,20 @@ macro_rules! debug_panic { | |
| }; | ||
| } | ||
|
|
||
| #[macro_export] | ||
| macro_rules! soft_assert { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not they both have the same implication:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's the name I initially picked, but it conflicts with a macro in the standard library. I thought it's fine to have |
||
| ( $cond:expr $(,)? ) => { | ||
| if !$cond { | ||
| stdext::debug_panic!("assertion failed: {}", stringify!($cond)); | ||
| } | ||
| }; | ||
| ( $cond:expr, $($fmt_arg:tt)+ ) => { | ||
| if !$cond { | ||
| stdext::debug_panic!( $($fmt_arg)+ ); | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| // From https://github.com/zed-industries/zed/blob/a910c594/crates/util/src/util.rs#L554 | ||
| pub trait ResultExt<E> { | ||
| type Ok; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.