mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
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:
parent
5c6aa4481c
commit
de5fc22335
2 changed files with 319 additions and 0 deletions
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue