repl: Test outputs and ExecutionView (#48892)

More tests for outputs and the `ExecutionView`. Wanting to get more of
these in before we progress on Notebooks and Widgets.

Release Notes:

- N/A
This commit is contained in:
Kyle Kelley 2026-02-11 08:22:26 -08:00 committed by GitHub
parent 5c6aa4481c
commit de5fc22335
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 319 additions and 0 deletions

View file

@ -72,4 +72,5 @@ theme = { workspace = true, features = ["test-support"] }
tree-sitter-md.workspace = true
tree-sitter-typescript.workspace = true
tree-sitter-python.workspace = true
workspace = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }

View file

@ -646,6 +646,19 @@ impl ExecutionView {
}
}
impl ExecutionView {
#[cfg(test)]
fn output_as_stream_text(&self, cx: &App) -> Option<String> {
self.outputs.iter().find_map(|output| {
if let Output::Stream { content } = output {
Some(content.read(cx).full_text())
} else {
None
}
})
}
}
impl Render for ExecutionView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let status = match &self.status {
@ -708,3 +721,308 @@ impl Render for ExecutionView {
.into_any_element()
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;
use runtimelib::{
ClearOutput, ErrorOutput, ExecutionState, JupyterMessageContent, MimeType, Status, Stdio,
StreamContent,
};
use settings::SettingsStore;
use std::path::Path;
use std::sync::Arc;
#[test]
fn test_rank_mime_type_ordering() {
let data_table = MimeType::DataTable(Box::default());
let json = MimeType::Json(serde_json::json!({}));
let png = MimeType::Png(String::new());
let jpeg = MimeType::Jpeg(String::new());
let markdown = MimeType::Markdown(String::new());
let plain = MimeType::Plain(String::new());
assert_eq!(rank_mime_type(&data_table), 6);
assert_eq!(rank_mime_type(&json), 5);
assert_eq!(rank_mime_type(&png), 4);
assert_eq!(rank_mime_type(&jpeg), 3);
assert_eq!(rank_mime_type(&markdown), 2);
assert_eq!(rank_mime_type(&plain), 1);
assert!(rank_mime_type(&data_table) > rank_mime_type(&json));
assert!(rank_mime_type(&json) > rank_mime_type(&png));
assert!(rank_mime_type(&png) > rank_mime_type(&jpeg));
assert!(rank_mime_type(&jpeg) > rank_mime_type(&markdown));
assert!(rank_mime_type(&markdown) > rank_mime_type(&plain));
}
#[test]
fn test_rank_mime_type_unsupported_returns_zero() {
let html = MimeType::Html(String::new());
let svg = MimeType::Svg(String::new());
let latex = MimeType::Latex(String::new());
assert_eq!(rank_mime_type(&html), 0);
assert_eq!(rank_mime_type(&svg), 0);
assert_eq!(rank_mime_type(&latex), 0);
}
async fn init_test(
cx: &mut TestAppContext,
) -> (gpui::VisualTestContext, WeakEntity<workspace::Workspace>) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
theme::init(theme::LoadThemes::JustBase, cx);
});
let fs = project::FakeFs::new(cx.background_executor.clone());
let project = project::Project::test(fs, [] as [&Path; 0], cx).await;
let window =
cx.add_window(|window, cx| workspace::Workspace::test_new(project, window, cx));
let workspace = window.root(cx).expect("workspace should exist");
let weak_workspace = workspace.downgrade();
let visual_cx = gpui::VisualTestContext::from_window(window.into(), cx);
(visual_cx, weak_workspace)
}
fn create_execution_view(
cx: &mut gpui::VisualTestContext,
weak_workspace: WeakEntity<workspace::Workspace>,
) -> Entity<ExecutionView> {
cx.update(|_window, cx| {
cx.new(|cx| ExecutionView::new(ExecutionStatus::Queued, weak_workspace, cx))
})
}
#[gpui::test]
async fn test_push_message_stream_content(cx: &mut TestAppContext) {
let (mut cx, workspace) = init_test(cx).await;
let execution_view = create_execution_view(&mut cx, workspace);
cx.update(|window, cx| {
execution_view.update(cx, |view, cx| {
let message = JupyterMessageContent::StreamContent(StreamContent {
name: Stdio::Stdout,
text: "hello world\n".to_string(),
});
view.push_message(&message, window, cx);
});
});
cx.update(|_, cx| {
let view = execution_view.read(cx);
assert_eq!(view.outputs.len(), 1);
assert!(matches!(view.outputs[0], Output::Stream { .. }));
let text = view.output_as_stream_text(cx);
assert!(text.is_some());
assert!(text.as_ref().is_some_and(|t| t.contains("hello world")));
});
}
#[gpui::test]
async fn test_push_message_stream_appends(cx: &mut TestAppContext) {
let (mut cx, workspace) = init_test(cx).await;
let execution_view = create_execution_view(&mut cx, workspace);
cx.update(|window, cx| {
execution_view.update(cx, |view, cx| {
let message1 = JupyterMessageContent::StreamContent(StreamContent {
name: Stdio::Stdout,
text: "first ".to_string(),
});
let message2 = JupyterMessageContent::StreamContent(StreamContent {
name: Stdio::Stdout,
text: "second".to_string(),
});
view.push_message(&message1, window, cx);
view.push_message(&message2, window, cx);
});
});
cx.update(|_, cx| {
let view = execution_view.read(cx);
assert_eq!(
view.outputs.len(),
1,
"consecutive streams should merge into one output"
);
let text = view.output_as_stream_text(cx);
assert!(text.as_ref().is_some_and(|t| t.contains("first ")));
assert!(text.as_ref().is_some_and(|t| t.contains("second")));
});
}
#[gpui::test]
async fn test_push_message_error_output(cx: &mut TestAppContext) {
let (mut cx, workspace) = init_test(cx).await;
let execution_view = create_execution_view(&mut cx, workspace);
cx.update(|window, cx| {
execution_view.update(cx, |view, cx| {
let message = JupyterMessageContent::ErrorOutput(ErrorOutput {
ename: "NameError".to_string(),
evalue: "name 'x' is not defined".to_string(),
traceback: vec![
"Traceback (most recent call last):".to_string(),
"NameError: name 'x' is not defined".to_string(),
],
});
view.push_message(&message, window, cx);
});
});
cx.update(|_, cx| {
let view = execution_view.read(cx);
assert_eq!(view.outputs.len(), 1);
match &view.outputs[0] {
Output::ErrorOutput(error_view) => {
assert_eq!(error_view.ename, "NameError");
assert_eq!(error_view.evalue, "name 'x' is not defined");
}
other => panic!(
"expected ErrorOutput, got {:?}",
std::mem::discriminant(other)
),
}
});
}
#[gpui::test]
async fn test_push_message_clear_output_immediate(cx: &mut TestAppContext) {
let (mut cx, workspace) = init_test(cx).await;
let execution_view = create_execution_view(&mut cx, workspace);
cx.update(|window, cx| {
execution_view.update(cx, |view, cx| {
let stream = JupyterMessageContent::StreamContent(StreamContent {
name: Stdio::Stdout,
text: "some output\n".to_string(),
});
view.push_message(&stream, window, cx);
assert_eq!(view.outputs.len(), 1);
let clear = JupyterMessageContent::ClearOutput(ClearOutput { wait: false });
view.push_message(&clear, window, cx);
assert_eq!(
view.outputs.len(),
0,
"immediate clear should remove all outputs"
);
});
});
}
#[gpui::test]
async fn test_push_message_clear_output_deferred(cx: &mut TestAppContext) {
let (mut cx, workspace) = init_test(cx).await;
let execution_view = create_execution_view(&mut cx, workspace);
cx.update(|window, cx| {
execution_view.update(cx, |view, cx| {
let stream = JupyterMessageContent::StreamContent(StreamContent {
name: Stdio::Stdout,
text: "old output\n".to_string(),
});
view.push_message(&stream, window, cx);
assert_eq!(view.outputs.len(), 1);
let clear = JupyterMessageContent::ClearOutput(ClearOutput { wait: true });
view.push_message(&clear, window, cx);
assert_eq!(view.outputs.len(), 2, "deferred clear adds a wait marker");
assert!(matches!(view.outputs[1], Output::ClearOutputWaitMarker));
let new_stream = JupyterMessageContent::StreamContent(StreamContent {
name: Stdio::Stdout,
text: "new output\n".to_string(),
});
view.push_message(&new_stream, window, cx);
assert_eq!(
view.outputs.len(),
1,
"next output after wait marker should clear previous outputs"
);
});
});
}
#[gpui::test]
async fn test_push_message_status_transitions(cx: &mut TestAppContext) {
let (mut cx, workspace) = init_test(cx).await;
let execution_view = create_execution_view(&mut cx, workspace);
cx.update(|window, cx| {
execution_view.update(cx, |view, cx| {
let busy = JupyterMessageContent::Status(Status {
execution_state: ExecutionState::Busy,
});
view.push_message(&busy, window, cx);
assert!(matches!(view.status, ExecutionStatus::Executing));
let idle = JupyterMessageContent::Status(Status {
execution_state: ExecutionState::Idle,
});
view.push_message(&idle, window, cx);
assert!(matches!(view.status, ExecutionStatus::Finished));
let starting = JupyterMessageContent::Status(Status {
execution_state: ExecutionState::Starting,
});
view.push_message(&starting, window, cx);
assert!(matches!(view.status, ExecutionStatus::ConnectingToKernel));
let dead = JupyterMessageContent::Status(Status {
execution_state: ExecutionState::Dead,
});
view.push_message(&dead, window, cx);
assert!(matches!(view.status, ExecutionStatus::Shutdown));
let restarting = JupyterMessageContent::Status(Status {
execution_state: ExecutionState::Restarting,
});
view.push_message(&restarting, window, cx);
assert!(matches!(view.status, ExecutionStatus::Restarting));
let terminating = JupyterMessageContent::Status(Status {
execution_state: ExecutionState::Terminating,
});
view.push_message(&terminating, window, cx);
assert!(matches!(view.status, ExecutionStatus::ShuttingDown));
});
});
}
#[gpui::test]
async fn test_push_message_status_idle_emits_finished_empty(cx: &mut TestAppContext) {
let (mut cx, workspace) = init_test(cx).await;
let execution_view = create_execution_view(&mut cx, workspace);
let emitted = Arc::new(std::sync::atomic::AtomicBool::new(false));
let emitted_clone = emitted.clone();
cx.update(|_, cx| {
cx.subscribe(
&execution_view,
move |_, _event: &ExecutionViewFinishedEmpty, _cx| {
emitted_clone.store(true, std::sync::atomic::Ordering::SeqCst);
},
)
.detach();
});
cx.update(|window, cx| {
execution_view.update(cx, |view, cx| {
assert!(view.outputs.is_empty());
let idle = JupyterMessageContent::Status(Status {
execution_state: ExecutionState::Idle,
});
view.push_message(&idle, window, cx);
});
});
assert!(
emitted.load(std::sync::atomic::Ordering::SeqCst),
"should emit ExecutionViewFinishedEmpty when idle with no outputs"
);
}
}