Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
@@ -0,0 +1,826 @@
use pathfinder_geometry::rect::RectF;
use regex::Regex;
use settings::Setting as _;
use warp_util::path::user_friendly_path;
use warpui::{
async_assert, async_assert_eq,
integration::{AssertionCallback, AssertionOutcome},
units::Lines,
windowing::WindowManager,
App, SingletonEntity, ViewHandle, WindowId,
};
use crate::{
ai::blocklist::agent_view::AgentViewState,
integration_testing::view_getters::{
single_input_view_for_tab, single_terminal_view, single_terminal_view_for_tab,
terminal_view,
},
settings::InputModeSettings,
terminal::{
block_list_viewport::InputMode,
block_list_viewport::ScrollPosition,
model::block::BlockState,
model::bootstrap::BootstrapStage,
model::grid::grid_handler::TermMode,
model::{blocks::BlockFilter, terminal_model::BlockIndex},
view::TerminalViewState,
History,
},
workspace::{ActiveSession, Workspace},
};
use super::util::ExpectedOutput;
lazy_static::lazy_static! {
/// When a python interpreter is ready for user input,
/// the '>>>' prompt is displayed at the end of the REPL.
pub static ref PYTHON_PROMPT_READY: Regex = Regex::new(">>> $").expect("python prompt regex should not fail to compile");
}
pub fn validate_block_output<T>(
expected_output: &T,
tab_idx: usize,
pane_idx: usize,
window_id: WindowId,
app: &App,
) -> AssertionOutcome
where
T: ExpectedOutput + ?Sized,
{
let terminal_view = terminal_view(app, window_id, tab_idx, pane_idx);
terminal_view.read(app, |view, _ctx| {
let model = view.model.lock();
let last_index = model
.block_list()
.last_matching_block_by_index(BlockFilter::commands());
// After the last test step, there should always be a block here, but for
// some reason, it sometimes doesn't exist.
match last_index {
Some(last_index) => {
let block = model
.block_list()
.block_at(last_index)
.expect("Block should exist");
let last_output = block
.output_grid()
.contents_to_string_with_secrets_unobfuscated(
false, /*include_escape_sequences*/
None, /*max_rows*/
);
async_assert!(
expected_output.matches(&last_output),
"The output should be {:?}, but got \"{}\"",
expected_output,
last_output
)
}
None => AssertionOutcome::failure("No block yet".to_string()),
}
})
}
/// Assumes that the block is finished and its contents are now immutable.
/// Fails fast if the block contents don't match the expected output.
pub fn validate_block_output_on_finished_block<T>(
expected_output: &T,
tab_idx: usize,
pane_idx: usize,
window_id: WindowId,
app: &App,
) -> AssertionOutcome
where
T: ExpectedOutput + ?Sized,
{
let terminal_view = terminal_view(app, window_id, tab_idx, pane_idx);
terminal_view.read(app, |view, _ctx| {
let model = view.model.lock();
let last_index = model
.block_list()
.last_matching_block_by_index(BlockFilter::commands());
// After the last test step, there should always be a block here, but for
// some reason, it sometimes doesn't exist.
match last_index {
Some(last_index) => {
let block = model
.block_list()
.block_at(last_index)
.expect("Block should exist");
let last_output = block
.output_grid()
.contents_to_string_with_secrets_unobfuscated(
false, /*include_escape_sequences*/
None, /*max_rows*/
);
if expected_output.matches(&last_output) {
AssertionOutcome::Success
} else {
AssertionOutcome::immediate_failure(format!(
"The output should be {expected_output:?}, but got \"{last_output}\""
))
}
}
None => AssertionOutcome::failure("No block yet".to_string()),
}
})
}
pub fn assert_input_mode(expected_input_mode: InputMode) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = single_terminal_view(app, window_id);
terminal_view.read(app, |_, ctx| {
let input_mode = *InputModeSettings::as_ref(ctx).input_mode.value();
async_assert_eq!(input_mode, expected_input_mode, "input mode doesn't match")
})
})
}
pub fn assert_gap_exists(gap_exists: bool) -> AssertionCallback {
Box::new(move |app, window_id| {
app.update(|ctx| {
assert!(ctx
.presenter(window_id)
.expect("should exist")
.borrow()
.scene()
.is_some());
});
let terminal_view = single_terminal_view(app, window_id);
terminal_view.read(app, |view, _ctx| {
let model = view.model.lock();
let has_gap = model.block_list().active_gap().is_some();
async_assert_eq!(
gap_exists,
has_gap,
"Expected gap {} but was gap {}",
gap_exists,
has_gap
)
})
})
}
#[derive(Debug)]
pub enum InputPosition {
TopOfTerminal,
BottomOfTerminal,
NotAtEitherEdge,
}
const ROUNDING_ERROR_PX: f32 = 0.1;
impl InputPosition {
fn assert_position(&self, terminal_rect: RectF, input_rect: RectF) -> bool {
match *self {
InputPosition::TopOfTerminal => {
(terminal_rect.origin_y() - input_rect.origin_y()).abs() < ROUNDING_ERROR_PX
}
InputPosition::BottomOfTerminal => {
(terminal_rect.max_y() - input_rect.max_y()).abs() < ROUNDING_ERROR_PX
}
InputPosition::NotAtEitherEdge => {
terminal_rect.contains_rect(input_rect)
&& !InputPosition::TopOfTerminal.assert_position(terminal_rect, input_rect)
&& !InputPosition::BottomOfTerminal.assert_position(terminal_rect, input_rect)
}
}
}
}
pub fn assert_input_position(input_position: InputPosition) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = single_terminal_view(app, window_id);
terminal_view.read(app, |view, ctx| {
let terminal_rect = ctx
.element_position_by_id_at_last_frame(window_id, view.terminal_position_id())
.expect("terminal position should be set");
let input_id = view.input().as_ref(ctx).save_position_id();
let input_rect = ctx
.element_position_by_id_at_last_frame(window_id, input_id)
.expect("input position should be set");
async_assert!(
input_position.assert_position(terminal_rect, input_rect),
"Input should be {:?} but it isn't. Terminal rect {:?} and input rect {:?}",
input_position,
terminal_rect,
input_rect
)
})
})
}
pub fn assert_input_at_top_of_terminal() -> AssertionCallback {
assert_input_position(InputPosition::TopOfTerminal)
}
pub fn assert_input_at_bottom_of_terminal() -> AssertionCallback {
assert_input_position(InputPosition::BottomOfTerminal)
}
pub fn assert_input_not_at_either_edge_of_terminal() -> AssertionCallback {
assert_input_position(InputPosition::NotAtEitherEdge)
}
pub fn assert_view_has_text_selection(has_text_selection: bool) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = single_terminal_view(app, window_id);
terminal_view.read(app, |view, _ctx| {
let view_is_selecting = view.is_selecting();
async_assert_eq!(
view_is_selecting,
has_text_selection,
"Expected view to have text selection {} but it was {}",
has_text_selection,
view_is_selecting
)
})
})
}
/// Asserts whether the waterfall gap empty state element is rendered or not
pub fn assert_waterfall_gap_empty_background_rendered(is_showing: bool) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = single_terminal_view(app, window_id);
terminal_view.read(app, |view, ctx| {
let element_showing = ctx
.element_position_by_id_at_last_frame(
window_id,
view.waterfall_background_position_id(),
)
.is_some();
async_assert_eq!(
element_showing,
is_showing,
"Expected gap element to be showing {} but it was {}",
is_showing,
element_showing
)
})
})
}
pub fn assert_model_term_mode(mode: TermMode, expected_value: bool) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = single_terminal_view(app, window_id);
terminal_view.read(app, |view, _| {
let model = view.model.lock();
async_assert_eq!(model.is_term_mode_set(mode), expected_value)
})
})
}
pub fn assert_no_block_executing(tab_index: usize, pane_index: usize) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = terminal_view(app, window_id, tab_index, pane_index);
terminal_view.read(app, |view, _ctx| {
let model = view.model.lock();
// Note: When the user presses enter, we "start" the block and send the newline to the
// shell, however we don't update the state of the block until the shell responds with
// a preexec message. As a result, we need to check _both_ the state and whether the
// block has started to ensure that we don't think a recently executed block is
// actually waiting for a command.
let block = model.block_list().active_block();
let block_is_ready =
!block.started() && matches!(block.state(), BlockState::BeforeExecution);
async_assert!(
block_is_ready,
"Should not be a command active. Block output is:\n{}\n",
block.output_with_secrets_unobfuscated()
)
})
})
}
pub fn assert_alt_grid_active(
tab_index: usize,
pane_index: usize,
should_be_active: bool,
) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = terminal_view(app, window_id, tab_index, pane_index);
terminal_view.read(app, |view, _ctx| {
let model = view.model.lock();
let is_alt_grid_active = model.is_alt_screen_active();
async_assert_eq!(
should_be_active,
is_alt_grid_active,
"Expected alt grid active to be {} but it was {}",
should_be_active,
is_alt_grid_active
)
})
})
}
/// Asserts that a long running block is currently executing.
pub fn assert_long_running_block_executing_for_single_terminal_in_tab(
assert_output_grid_active: bool,
tab_index: usize,
) -> AssertionCallback {
assert_long_running_block_executing(assert_output_grid_active, tab_index, 0)
}
pub fn assert_long_running_block_executing(
assert_output_grid_active: bool,
tab_index: usize,
pane_index: usize,
) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = terminal_view(app, window_id, tab_index, pane_index);
terminal_view.read(app, |view, _ctx| {
let model = view.model.lock();
let is_editor_focused = view
.input()
.read(app, |input, ctx| input.editor().is_focused(ctx));
let active_block = model.block_list().active_block();
// Note that we check the output grid is active to ensure the
// command has actually started executing.
async_assert!(
!is_editor_focused
&& (!assert_output_grid_active || active_block.is_executing())
&& active_block.is_active_and_long_running(),
"Check that it's a long running process/command"
)
})
})
}
pub fn assert_single_terminal_in_tab_bootstrapped(
app: &App,
window_id: WindowId,
tab_index: usize,
) -> AssertionOutcome {
assert_bootstrapping_result(
app, window_id, tab_index, 0, true, /* expect_bootstrapped */
)
}
pub fn assert_terminal_bootstrapped(tab_index: usize, pane_index: usize) -> AssertionCallback {
Box::new(move |app, window_id| {
assert_bootstrapping_result(app, window_id, tab_index, pane_index, true)
})
}
pub fn assert_terminal_bootstrapping(tab_index: usize, pane_index: usize) -> AssertionCallback {
Box::new(move |app, window_id| {
assert_bootstrapping_result(app, window_id, tab_index, pane_index, false)
})
}
pub fn assert_bootstrapping_result(
app: &App,
window_id: WindowId,
tab_index: usize,
pane_index: usize,
expect_bootstrapped: bool,
) -> AssertionOutcome {
let terminal_view = terminal_view(app, window_id, tab_index, pane_index);
let bootstrapped = terminal_view.read(app, |view, ctx| {
let model = view.model.lock();
let input_visible = view.is_input_box_visible(&model, ctx);
let history_bootstrapped = model
.block_list()
.active_block()
.session_id()
.is_some_and(|session_id| History::as_ref(ctx).is_session_initialized(&session_id));
input_visible
&& history_bootstrapped
// Note that we check whether the precmd that follows bootstrapping is done rather than
// just checking bootstrapping is done. In tests it can cause indeterminancy to have
// this precmd come in later (it increases the number of blocks), whereas in the actual
// running of the app we don't care about these blocks and it's a slight performance hit
// to wait for the precmd so we can just check is_bootstrapped.
&& model.block_list().is_bootstrapping_precmd_done()
});
async_assert_eq!(
expect_bootstrapped,
bootstrapped,
"terminal should be bootstrapped ({})",
expect_bootstrapped
)
}
pub fn assert_selected_block_index_is_first_renderable() -> AssertionCallback {
Box::new(|app, window_id| {
let terminal_view = single_terminal_view(app, window_id);
terminal_view.read(app, |view, _| {
let selected_block_index = view
.selected_blocks_tail_index()
.expect("Selection should not be none");
let model = view.model.lock();
let block = model
.block_list()
.block_at(selected_block_index)
.expect("Block should exist");
assert!(
block.height(&AgentViewState::Inactive) != Lines::zero(),
"The selected block should be rendered"
);
// Previous index either doesn't exist or isn't renderable
if selected_block_index > BlockIndex::zero() {
let prev_block = model.block_list().block_at(selected_block_index - 1.into());
if let Some(prev_block) = prev_block {
assert!(
prev_block.is_empty(&AgentViewState::Inactive),
"Prev index should be hidden"
);
}
}
AssertionOutcome::Success
})
})
}
pub fn assert_selected_block_index_is_last_renderable() -> AssertionCallback {
Box::new(|app, window_id| {
let terminal_view = single_terminal_view(app, window_id);
terminal_view.read(app, |view, _| {
let selected_block_index = view
.selected_blocks_tail_index()
.expect("Selection should not be none");
let model = view.model.lock();
let block = model
.block_list()
.block_at(selected_block_index)
.expect("Block should exist");
assert!(
block.height(&AgentViewState::Inactive) != Lines::zero(),
"The selected block should be rendered"
);
assert_eq!(
model.block_list().last_non_hidden_block_by_index(),
Some(selected_block_index)
);
AssertionOutcome::Success
})
})
}
pub fn assert_focused_editor_in_tab(tab_index: usize) -> AssertionCallback {
Box::new(move |app, window_id| {
let input_view = single_input_view_for_tab(app, window_id, tab_index);
input_view.read(app, |view, ctx| {
async_assert!(view.editor().is_focused(ctx), "Editor should be focused")
})
})
}
pub fn assert_command_executed_for_single_terminal_in_tab(
tab_index: usize,
command: String,
) -> AssertionCallback {
assert_command_executed(tab_index, 0, command)
}
pub fn assert_command_executed(
tab_index: usize,
pane_index: usize,
command: String,
) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = terminal_view(app, window_id, tab_index, pane_index);
terminal_view.read(app, |view, _ctx| {
let model = view.model.lock();
let last_index = model
.block_list()
.last_matching_block_by_index(BlockFilter::commands());
if let Some(last_index) = last_index {
let block = model
.block_list()
.block_at(last_index)
.expect("block should exist");
let block_is_done = matches!(
block.state(),
BlockState::DoneWithExecution | BlockState::DoneWithNoExecution
);
let last_command = block
.command_with_secrets_unobfuscated(false /*include_escape_sequences*/);
// We send an escape sequence once the line editor is active to fetch
// typeahead. Currently, this is racy in integration tests because
// they send the queued command more quickly than a real user could
// type. For the time being, we handle this by cleaning up the command,
// but ongoing work to consolidate PTY writes should be a more robust
// solution.
let cleaned_last_command = last_command.trim_end_matches("^[i").trim_end();
let cleaned_command = command.trim_end();
async_assert!(
block_is_done && cleaned_last_command == cleaned_command,
"Previous command should be {}, instead got {}",
command,
last_command,
)
} else {
AssertionOutcome::failure("No block yet".to_string())
}
})
})
}
pub fn assert_active_block_received_precmd(
tab_index: usize,
pane_index: usize,
) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = terminal_view(app, window_id, tab_index, pane_index);
terminal_view.read(app, |view, _ctx| {
let model = view.model.lock();
let active_block = model.block_list().active_block();
if active_block.has_received_precmd() {
AssertionOutcome::Success
} else {
AssertionOutcome::failure("Precmd has not been received yet".to_string())
}
})
})
}
pub fn assert_active_block_input_is_empty(
tab_index: usize,
pane_index: usize,
) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = terminal_view(app, window_id, tab_index, pane_index);
terminal_view.read(app, |view, ctx| {
view.input().read(ctx, |input, ctx| {
let text = input.buffer_text(ctx);
async_assert!(
text.is_empty(),
"Input buffer is not empty after block finished. Input buffer contents: {}",
text
)
})
})
})
}
pub fn assert_bootstrapping_stage(
tab_index: usize,
pane_index: usize,
stage: BootstrapStage,
) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = terminal_view(app, window_id, tab_index, pane_index);
terminal_view.read(app, |view, _ctx| {
let model = view.model.lock();
let active_block = model.block_list().active_block();
async_assert_eq!(active_block.bootstrap_stage(), stage)
})
})
}
pub fn assert_context_menu_is_open(should_be_open: bool) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = single_terminal_view(app, window_id);
terminal_view.read(app, |view, _ctx| {
let open_or_closed_str = if should_be_open { "open" } else { "closed" };
async_assert_eq!(
view.is_context_menu_open(),
should_be_open,
"The context menu should be {open_or_closed_str}"
)
})
})
}
pub fn assert_active_block_command_for_single_terminal_in_tab(
expected_command: impl ExpectedOutput + 'static,
tab_index: usize,
) -> AssertionCallback {
assert_active_block_command(expected_command, tab_index, 0)
}
pub fn assert_active_block_command(
expected_command: impl ExpectedOutput + 'static,
tab_index: usize,
pane_index: usize,
) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = terminal_view(app, window_id, tab_index, pane_index);
terminal_view.read(app, |view, _| {
let model = view.model.lock();
let command = model.block_list().active_block().command_to_string();
async_assert!(
expected_command.matches(&command),
"The command should be {:?}, but got \"{}\"",
expected_command,
command
)
})
})
}
pub fn assert_active_block_output_for_single_terminal_in_tab(
expected_output: impl ExpectedOutput + 'static,
tab_index: usize,
) -> AssertionCallback {
assert_active_block_output(expected_output, tab_index, 0)
}
pub fn assert_active_block_output(
expected_output: impl ExpectedOutput + 'static,
tab_index: usize,
pane_index: usize,
) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = terminal_view(app, window_id, tab_index, pane_index);
terminal_view.read(app, |view, _| {
let model = view.model.lock();
let output = model.block_list().active_block().output_to_string();
async_assert!(
expected_output.matches(&output),
"The output should be {:?}, but got \"{}\"",
expected_output,
output
)
})
})
}
pub fn assert_no_visible_background_blocks(
tab_index: usize,
pane_index: usize,
) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = terminal_view(app, window_id, tab_index, pane_index);
terminal_view.read(app, |view, _| {
let model = view.model.lock();
let count_nonempty_background_blocks = model
.block_list()
.blocks()
.iter()
.filter(|block| {
block.is_background() && block.is_visible(&AgentViewState::Inactive)
})
.count();
async_assert_eq!(
count_nonempty_background_blocks,
0,
"BlockList should have no non-empty background blocks."
)
})
})
}
/// Asserts that the output of the alt screen matches `expected_output`.
pub fn assert_alt_screen_output(
expected_output: impl ExpectedOutput + 'static,
tab_index: usize,
pane_index: usize,
) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = terminal_view(app, window_id, tab_index, pane_index);
terminal_view.read(app, |view, _| {
let model = view.model.lock();
let output = model.alt_screen().output_to_string();
async_assert!(
expected_output.matches(&output),
"The output should be {:?}, but got \"{}\"",
expected_output,
output
)
})
})
}
/// Builds an assertion that the input box for the given tab will contain the
/// expected text.
pub fn assert_input_editor_contents(
tab_index: usize,
expected_contents: impl AsRef<str> + 'static,
) -> AssertionCallback {
Box::new(move |app, window_id| {
let expected_contents = expected_contents.as_ref();
let input_view = single_input_view_for_tab(app, window_id, tab_index);
input_view.read(app, |view, ctx| {
let contents = view.buffer_text(ctx);
async_assert_eq!(&contents, expected_contents, "Incorrect input box contents:\nExpected {expected_contents:?}\nActual: {contents:?}")
})
})
}
pub fn assert_pane_group_has_state(
tab_index: usize,
expected_state: TerminalViewState,
) -> AssertionCallback {
Box::new(move |app, _| {
let active_window_id = app.read(|ctx| {
WindowManager::as_ref(ctx)
.active_window()
.expect("should have active window")
});
let views = app
.views_of_type(active_window_id)
.expect("Active window lacks a Workspace.");
let workspace: &ViewHandle<Workspace> =
views.first().expect("Window is missing Workspace view.");
workspace.read(app, |workspace, ctx| {
workspace
.get_pane_group_view(tab_index)
.expect("Workspace has no tab view.")
.read(ctx, |pane_group, ctx| {
async_assert_eq!(pane_group.most_recent_pane_state(ctx), expected_state)
})
})
})
}
fn assert_snackbar_visibility(tab_index: usize, is_visible: bool) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = single_terminal_view_for_tab(app, window_id, tab_index);
terminal_view.update(app, |view, ctx| {
let presenter = ctx.presenter(window_id).expect("window should exist");
let snackbar_position = presenter
.borrow()
.position_cache()
.get_position(format!("block_list_snackbar:{}", view.id()));
async_assert_eq!(snackbar_position.is_some(), is_visible)
})
})
}
/// Asserts that the snackbar is visible.
pub fn assert_snackbar_is_visible(tab_index: usize) -> AssertionCallback {
assert_snackbar_visibility(tab_index, true /* is_visible */)
}
/// Asserts that the snackbar is _not_ visible.
pub fn assert_snackbar_is_not_visible(tab_index: usize) -> AssertionCallback {
assert_snackbar_visibility(tab_index, false /* is_visible */)
}
/// Asserts that the current scroll position is equal to `ScrollPosition`.
pub fn assert_scroll_position(
tab_index: usize,
scroll_position: ScrollPosition,
) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = single_terminal_view_for_tab(app, window_id, tab_index);
terminal_view.read(app, |view, _ctx| {
let actual_scroll_position = view.scroll_position();
async_assert_eq!(actual_scroll_position, scroll_position)
})
})
}
pub fn validate_git_branch(
expected_git_branch: Option<String>,
tab_idx: usize,
window_id: warpui::WindowId,
app: &warpui::App,
) -> AssertionOutcome {
let terminal_view = single_terminal_view_for_tab(app, window_id, tab_idx);
terminal_view.read(app, |view, _ctx| {
let model = view.model.lock();
let block = model.block_list().active_block();
let actual_branch = block.git_branch();
if actual_branch.map(Into::into) == expected_git_branch {
AssertionOutcome::Success
} else {
AssertionOutcome::failure(format!(
"Expected {expected_git_branch:?} as git branch but got {actual_branch:?}"
))
}
})
}
/// Asserts that the active session of the current window's workspace has the expected local path.
/// For convenience, the local path is converted to a user-friendly path, since it will generally
/// be a temporary directory.
pub fn assert_active_session_local_path(expected_path: &'static str) -> AssertionCallback {
Box::new(move |app, window_id| {
ActiveSession::handle(app).read(app, |active_session, _| {
let session = active_session.session(window_id);
let pwd = active_session.path_if_local(window_id);
match session.zip(pwd) {
Some((session, pwd)) => {
let relative_path = user_friendly_path(
pwd.to_str().expect("Non-UTF8 path"),
session.home_dir(),
);
async_assert_eq!(expected_path, relative_path)
}
None => {
AssertionOutcome::failure("Expected a local active session path".to_string())
}
}
})
})
}
pub fn assert_input_is_focused() -> AssertionCallback {
Box::new(|app, window_id| {
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
terminal_view.read(app, |view, ctx| {
let is_input_focused = view.input().as_ref(ctx).editor().as_ref(ctx).is_focused();
async_assert!(is_input_focused)
})
})
}
@@ -0,0 +1,6 @@
mod assertion;
mod step;
pub mod util;
pub use assertion::*;
pub use step::*;
@@ -0,0 +1,474 @@
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use warpui::{
async_assert,
integration::{AssertionOutcome, TestStep},
Event, SingletonEntity,
};
use crate::integration_testing::terminal::{
assert_context_menu_is_open, assert_long_running_block_executing,
};
use crate::integration_testing::view_getters::single_terminal_view_for_tab;
use crate::integration_testing::{
block::assert_num_blocks_in_model, terminal::assert_active_block_input_is_empty,
};
use crate::terminal::model::terminal_model::BlockIndex;
use crate::terminal::shell::ShellType;
use crate::{
cmd_or_ctrl_shift, integration_testing::terminal::validate_block_output_on_finished_block,
};
use crate::{
integration_testing::command_palette::open_command_palette_and_run_action,
settings::PrivacySettings,
};
use crate::{
integration_testing::{
step::{
assert_no_pending_model_events, new_step_with_default_assertions,
new_step_with_default_assertions_for_pane,
},
view_getters::{single_input_view_for_tab, terminal_view},
},
terminal::input::InputSuggestionsMode,
};
use super::{
assert_active_block_output_for_single_terminal_in_tab, assert_active_block_received_precmd,
assert_alt_grid_active, assert_command_executed,
assert_long_running_block_executing_for_single_terminal_in_tab, assert_terminal_bootstrapped,
util::{current_shell_starter_and_version, nonce, ExpectedExitStatus, ExpectedOutput},
validate_block_output, PYTHON_PROMPT_READY,
};
pub fn wait_until_bootstrapped_single_pane_for_tab(tab_index: usize) -> TestStep {
wait_until_bootstrapped_pane(tab_index, 0)
}
pub fn initialize_secret_regexes() -> TestStep {
new_step_with_default_assertions("Initialize default secret regexes").with_action(
move |app, _, _| {
let privacy_settings = PrivacySettings::handle(app);
privacy_settings.update(app, |me, ctx| {
me.initialize_default_regexes_once(ctx);
});
},
)
}
pub fn wait_until_bootstrapped_pane(tab_index: usize, pane_index: usize) -> TestStep {
new_step_with_default_assertions("Wait for bootstrapping")
.add_named_assertion(
"waiting for bootstrapping",
assert_terminal_bootstrapped(tab_index, pane_index),
)
.set_timeout(Duration::from_secs(20))
.set_on_failure_handler("bootstrapping failed, bail on the test", move |_, _| {
let (starter, version) = current_shell_starter_and_version();
if matches!(&starter.shell_type(), &ShellType::Bash) && version.starts_with('3') {
// There's a bug in older versions of bash that causes bootstrapping
// to occasionally fail.
AssertionOutcome::PreconditionFailed("bash flaked on startup".to_owned())
} else {
AssertionOutcome::failure("failed to bootstrap".to_owned())
}
})
}
pub fn open_context_menu_for_selected_block() -> Vec<TestStep> {
let mut steps = open_command_palette_and_run_action("Open Block Context Menu");
let last = steps.pop().expect("steps should not be empty");
steps.push(last.add_assertion(assert_context_menu_is_open(true)));
steps
}
/// Runs the completer with the given input text, waiting up to 2 seconds for the completer to return
/// with results.
pub fn run_completer(tab_index: usize, input_text: impl Into<String>) -> TestStep {
let input_text = input_text.into();
new_step_with_default_assertions(&format!("Type {} and hit tab", &input_text))
.with_typed_characters(&[&input_text])
.with_keystrokes(&["tab"])
.set_timeout(Duration::from_secs(2))
.add_assertion(move |app, window_id| {
let input_view = single_input_view_for_tab(app, window_id, tab_index);
input_view.read(app, |input, ctx| {
let buffer_text = input.buffer_text(ctx);
// There are 2 possible outcomes that can signify the completer has finished:
// 1: TabCompletion mode is now active.
// 2: InputSuggestionsMode is `Closed`, but the buffer text has changed. This is the
// case when there is a single completion result that we insert directly into the
// buffer.
async_assert!(
matches!(
input.suggestions_mode_model().as_ref(ctx).mode(),
InputSuggestionsMode::CompletionSuggestions { .. }
) || (buffer_text != input_text
&& matches!(
input.suggestions_mode_model().as_ref(ctx).mode(),
InputSuggestionsMode::Closed
)),
"Completions did not finish"
)
})
})
}
/// Executes a given command and verifies it is executing.
pub fn execute_long_running_command(tab_idx: usize, command: String) -> TestStep {
execute_long_running_command_for_pane(tab_idx, 0 /* pane_idx */, command)
}
/// Executes a given command for a specific pane and tab and verifies it is executing.
pub fn execute_long_running_command_for_pane(
tab_idx: usize,
pane_idx: usize,
command: impl AsRef<str>,
) -> TestStep {
let command = command.as_ref();
TestStep::new(&format!("Run '{command}' and verify block is running"))
.add_named_assertion("no pending model events", assert_no_pending_model_events())
.with_typed_characters(&[command])
.with_keystrokes(&["enter"])
.set_timeout(Duration::from_secs(10))
.add_named_assertion(
format!("assert '{command}' is running"),
assert_long_running_block_executing(
true, /* output_grid_active */
tab_idx, pane_idx,
),
)
}
/// Executes a python3 interpreter and leaves it running in the active block.
pub fn execute_python_interpreter_in_tab(tab_idx: usize) -> TestStep {
TestStep::new("Run python3 interpreter")
.add_named_assertion("no pending model events", assert_no_pending_model_events())
.with_typed_characters(&["python3"])
.with_keystrokes(&["enter"])
.add_assertion(assert_active_block_output_for_single_terminal_in_tab(
&*PYTHON_PROMPT_READY,
0,
))
.add_named_assertion(
"assert python3 is running",
assert_long_running_block_executing_for_single_terminal_in_tab(
true, /* output_grid_active */
tab_idx,
),
)
}
/// Runs an alt-grid program followed by a series of steps and then
/// runs a command to exit the alt grid and asserts it's no longer active.
///
/// The terminal view at tab_index, pane_index is expected to be focused to run the program (this
/// step asserts the alt screen is active on the corresponding TerminalView).
pub fn run_alt_grid_program(
command: &str,
tab_index: usize,
pane_index: usize,
exit_step: TestStep,
steps_before_exiting: Vec<TestStep>,
) -> Vec<TestStep> {
let mut steps = vec![];
let run_step = TestStep::new(&format!("Run '{command}' and then exit with exit step"))
.add_named_assertion("no pending model events", assert_no_pending_model_events())
.with_typed_characters(&[command])
.with_keystrokes(&["enter"])
.set_timeout(Duration::from_secs(10))
.add_named_assertion(
"alt grid should be active",
assert_alt_grid_active(tab_index, pane_index, true),
);
steps.push(run_step);
steps.extend(steps_before_exiting);
steps.push(exit_step);
steps.push(
new_step_with_default_assertions("return to block list").add_named_assertion(
"alt grid should not be active",
assert_alt_grid_active(tab_index, pane_index, false),
),
);
steps
}
/// Executes a given command and verifies it's executed.
/// Asserts the exit code of the command is the same as the expected exit code.
/// #Panics if the execution failed for some reason.
pub fn execute_command_for_single_terminal_in_tab(
tab_idx: usize,
command: String,
expected_exit_code: ExpectedExitStatus,
expected_output: impl ExpectedOutput + 'static,
) -> TestStep {
execute_command(tab_idx, 0, command, expected_exit_code, expected_output)
}
pub fn execute_command_successfully(command: &str) -> TestStep {
execute_command(0, 0, command.to_owned(), ExpectedExitStatus::Success, ())
}
pub fn assert_execute_command_successfully(
command: &str,
expected_output: impl ExpectedOutput + 'static,
) -> TestStep {
execute_command(
0,
0,
command.to_owned(),
ExpectedExitStatus::Success,
expected_output,
)
}
/// Creates an event function that saves whether AI mode is active, switches to terminal
/// input mode if needed, and returns a `TypedCharacters` event for the given command.
fn switch_to_terminal_mode_and_type_command(
tab_idx: usize,
pane_idx: usize,
was_ai_mode: Arc<AtomicBool>,
command: String,
) -> impl Fn(&mut warpui::App, warpui::WindowId) -> Event + 'static {
move |app, window_id| {
let tv = terminal_view(app, window_id, tab_idx, pane_idx);
let is_ai = tv.read(app, |view, ctx| {
view.input()
.read(ctx, |input, ctx| input.input_type(ctx).is_ai())
});
was_ai_mode.store(is_ai, Ordering::SeqCst);
if is_ai {
tv.update(app, |view, ctx| {
view.input().update(ctx, |input, ctx| {
input.set_input_mode_terminal(false, ctx);
});
});
}
Event::TypedCharacters {
chars: command.clone(),
}
}
}
/// Restores AI input mode if it was previously active (as recorded in `was_ai_mode`).
///
/// This is an action (not an assertion) so it always runs before assertions,
/// ensuring the input mode is restored even if a subsequent assertion fails.
fn restore_ai_mode_if_needed(
tab_idx: usize,
pane_idx: usize,
was_ai_mode: Arc<AtomicBool>,
) -> impl Fn(&mut warpui::App, warpui::WindowId) + 'static {
move |app, window_id| {
if was_ai_mode.load(Ordering::SeqCst) {
let tv = terminal_view(app, window_id, tab_idx, pane_idx);
tv.update(app, |view, ctx| {
view.input().update(ctx, |input, ctx| {
input.set_input_mode_agent(false, ctx);
});
});
}
}
}
/// Shared implementation for executing a shell command in the terminal.
///
/// If the input is currently in AI mode, this automatically switches to terminal input
/// mode before typing the command, and restores AI mode after the command completes.
/// This allows callers to run shell commands without manually toggling input mode.
fn execute_command_step(
tab_idx: usize,
pane_idx: usize,
command: String,
validate_output_fn: impl FnMut(&mut warpui::App, warpui::WindowId) -> AssertionOutcome + 'static,
) -> TestStep {
let was_ai_mode = Arc::new(AtomicBool::new(false));
let was_ai_mode_for_restore = was_ai_mode.clone();
let command_for_event = command.clone();
new_step_with_default_assertions_for_pane(
&format!("Run '{command}' and verify block exists"),
tab_idx,
pane_idx,
)
.with_event_fn(switch_to_terminal_mode_and_type_command(
tab_idx,
pane_idx,
was_ai_mode,
command_for_event,
))
.with_keystrokes(&["enter"])
.set_timeout(Duration::from_secs(10))
.with_action({
let restore = restore_ai_mode_if_needed(tab_idx, pane_idx, was_ai_mode_for_restore);
move |app, window_id, _| restore(app, window_id)
})
.add_named_assertion(
format!("assert '{command}' ran"),
assert_command_executed(tab_idx, pane_idx, command),
)
.add_named_assertion("assert command output", validate_output_fn)
.add_named_assertion(
"wait for precmd so we have metadata for the next block",
assert_active_block_received_precmd(tab_idx, pane_idx),
)
}
/// Executes a given command and verifies it ran successfully.
/// Asserts the exit code matches `expected_exit_code` and validates the output.
///
/// If the input is in AI mode, this automatically switches to terminal input mode
/// before running the command and restores AI mode afterward.
pub fn execute_command(
tab_idx: usize,
pane_idx: usize,
command: String,
expected_exit_code: ExpectedExitStatus,
expected_output: impl ExpectedOutput + 'static,
) -> TestStep {
execute_command_step(tab_idx, pane_idx, command, move |app, window_id| {
validate_block_output_on_finished_block(&expected_output, tab_idx, pane_idx, window_id, app)
})
.add_named_assertion("assert exit code", move |app, window_id| {
let terminal_view = terminal_view(app, window_id, tab_idx, pane_idx);
terminal_view.read(app, |view, _ctx| {
let model = view.model.lock();
// After the last test step, there should always be a block here, but for
// some reason, it sometimes doesn't exist.
let last_block = model
.block_list()
.last_non_hidden_block()
.expect("Block should exist");
match expected_exit_code {
ExpectedExitStatus::Success => {
if last_block.exit_code().value() != 0 {
return AssertionOutcome::immediate_failure(format!(
"Expected exit code 0, but got {}. Block output:\n{}\n",
last_block.exit_code().value(),
last_block
.output_grid()
.contents_to_string_with_secrets_unobfuscated(
false, /*include_escape_sequences*/
None, /*max_rows*/
)
));
}
}
ExpectedExitStatus::Failure => {
if last_block.exit_code().value() == 0 {
return AssertionOutcome::immediate_failure(format!(
"Expected non-zero exit code, but got 0. Block output:\n{}\n",
last_block
.output_grid()
.contents_to_string_with_secrets_unobfuscated(
false, /*include_escape_sequences*/
None, /*max_rows*/
)
));
}
}
ExpectedExitStatus::ExactCode(code) => {
if last_block.exit_code() != code {
return AssertionOutcome::immediate_failure(format!(
"Expected exit code {}, but got {}",
code.value(),
last_block.exit_code().value()
));
}
}
ExpectedExitStatus::Any => (),
};
AssertionOutcome::Success
})
})
.add_named_assertion(
"check that input is empty",
assert_active_block_input_is_empty(tab_idx, pane_idx),
)
}
/// Executes a given command and validates its output, without asserting the exit code.
///
/// If the input is in AI mode, this automatically switches to terminal input mode
/// before running the command and restores AI mode afterward.
pub fn execute_command_without_expected_exit_code(
tab_idx: usize,
pane_idx: usize,
command: String,
expected_output: impl ExpectedOutput + 'static,
) -> TestStep {
execute_command_step(tab_idx, pane_idx, command, move |app, window_id| {
validate_block_output(&expected_output, tab_idx, pane_idx, window_id, app)
})
}
// Executes an echo with a random nonce and verifies it's executed.
// The purpose of the nonce is to distinguish between distinct command executions.
pub fn execute_echo(tab_idx: usize) -> TestStep {
let rand = nonce();
let command = format!("echo {rand}");
execute_command_for_single_terminal_in_tab(tab_idx, command, ExpectedExitStatus::Success, rand)
}
// Executes an echo with the specified string.
pub fn execute_echo_str(tab_idx: usize, str: &str) -> TestStep {
let command = format!("echo \"{str}\"");
execute_command_for_single_terminal_in_tab(
tab_idx,
command,
ExpectedExitStatus::Success,
str.to_owned(),
)
}
/// Runs a performance test on a given tab idx.
/// # Arguments
/// * `tab_idx` - id number of the tab the step should be executed on;
/// * `test_file` is a path to the bash file that will execute the test, and needs to be available
/// for the test itself (use MockUserData structure to ensure it);
/// * `repetitions` denotes how many times a test should be repeated;
/// #Panics if the execution failed for some reason.
pub fn performance_test(tab_idx: usize, test_file: &str, repetitions: usize) -> TestStep {
execute_command_for_single_terminal_in_tab(
tab_idx,
format!("multitime -n {repetitions} bash {test_file}"),
ExpectedExitStatus::Success,
(),
)
}
/// Clears the blocklist so that when we create a new block, its block index is 0.
/// Otherwise, its index within the blocklist will be dependent on the shell bootstrapped.
///
/// NOTE: Call this step after bootstrapping and before running any commands
/// to ensure that the next block created has `BlockIndex::zero()`. Also, this function
/// assumes that there is only one terminal view in tab 0.
pub fn clear_blocklist_to_remove_bootstrapped_blocks() -> TestStep {
new_step_with_default_assertions("Clear blocklist")
.with_keystrokes(&[cmd_or_ctrl_shift("k")])
.set_timeout(Duration::from_secs(10))
.add_assertion(assert_num_blocks_in_model(1))
}
pub fn hover_over_block_zero() -> TestStep {
new_step_with_default_assertions("Hover over the recently created block")
.with_hover_over_saved_position("block_index:0")
.add_assertion(|app, window_id| {
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
terminal_view.read(app, |view, _ctx| {
assert_eq!(
Some(BlockIndex::from(0)),
view.hovered_block_index(),
"Expected first block to be hovered over, but got block index {:?}",
view.hovered_block_index()
);
});
AssertionOutcome::Success
})
}
@@ -0,0 +1,201 @@
use async_io::block_on;
use command::blocking::Command;
use std::borrow::Cow;
use std::iter;
use std::path::{Path, PathBuf};
use warp_core::command::ExitCode;
#[cfg(windows)]
use warp_core::paths::base_config_dir;
use rand::Rng;
use rand::{distributions::Alphanumeric, thread_rng};
use regex::Regex;
use crate::terminal::shell::ShellType;
use crate::terminal::{
local_tty::shell::{DirectShellStarter, ShellStarter, ShellStarterSource},
shell,
};
/// Returns the shell starter along with the version of the shell about to be run.
pub fn current_shell_starter_and_version() -> (DirectShellStarter, String) {
let shell_starter_or_wsl_name = ShellStarter::init(Default::default())
.expect("Could not create a shell starter or wsl name");
let shell_starter_source =
block_on(async { shell_starter_or_wsl_name.to_shell_starter_source().await })
.expect("Could not create a shell starter source");
let starter = match shell_starter_source {
ShellStarterSource::Override(starter) => match starter {
ShellStarter::Direct(direct_shell_starter) => direct_shell_starter,
ShellStarter::Wsl(_) => {
// TODO(CORE-2302): Support integration tests on Windows (including WSL).
todo!("We don't yet support integration tests for WSL shells")
}
// TODO(CORE-2302): Support integration tests on Windows (including WSL).
ShellStarter::MSYS2(_) => {
todo!("We don't yet support integration tests for MSYS2")
}
ShellStarter::DockerSandbox(_) => {
todo!("We don't yet support integration tests for Docker sandbox shells")
}
},
ShellStarterSource::Environment(starter)
| ShellStarterSource::UserDefault(starter)
| ShellStarterSource::Fallback { starter, .. } => starter,
};
let version = match starter.shell_type() {
shell::ShellType::Zsh => {
let stdout = Command::new(starter.logical_shell_path())
.args(["-c", "echo $ZSH_VERSION"])
.output()
.expect("version command should run")
.stdout;
String::from_utf8_lossy(&stdout).into_owned()
}
shell::ShellType::Bash => {
let stdout = Command::new(starter.logical_shell_path())
.args(["-c", "echo $BASH_VERSION"])
.output()
.expect("version command should run")
.stdout;
String::from_utf8_lossy(&stdout).into_owned()
}
shell::ShellType::Fish => {
let stdout = Command::new(starter.logical_shell_path())
.args(["-c", "echo $FISH_VERSION"])
.output()
.expect("version command should run")
.stdout;
String::from_utf8_lossy(&stdout).into_owned()
}
shell::ShellType::PowerShell => {
let stdout = Command::new(starter.logical_shell_path())
.args(["-Version"])
.output()
.expect("version command should run")
.stdout;
String::from_utf8_lossy(&stdout).into_owned()
}
};
assert!(!version.is_empty());
(starter, version)
}
/// Returns the directory for the default histfile location for the ShellType in this
/// ShellStarter based on the given user `home_dir`.
pub fn default_histfile_directory(shell: &ShellType, home_dir: &Path) -> PathBuf {
match shell {
ShellType::Fish => home_dir.join(".local/share/fish"),
#[cfg(not(windows))]
ShellType::PowerShell => home_dir.join(".local/share/powershell/PSReadLine"),
#[cfg(windows)]
ShellType::PowerShell => base_config_dir().join("Microsoft/Windows/PowerShell/PSReadLine"),
_ => home_dir.to_owned(),
}
}
/// Generates a random nonce to distinguish between commands.
pub fn nonce() -> String {
let mut rng = thread_rng();
iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.map(char::from)
.take(7)
.collect()
}
/// Different options for asserting the value of the exit code.
pub enum ExpectedExitStatus {
/// Checks code == 0
Success,
/// Checks code != 0
Failure,
/// Checks code == expected
ExactCode(ExitCode),
/// Any exit status is considered valid.
Any,
}
/// A representation of the expected output from running a command.
pub trait ExpectedOutput: std::fmt::Debug {
/// Returns whether the given result matches the expected output.
fn matches(&self, result: &str) -> bool;
}
#[derive(Debug)]
pub struct ExactLine<'a>(Cow<'a, str>);
impl<'a, T: Into<Cow<'a, str>>> From<T> for ExactLine<'a> {
fn from(value: T) -> Self {
ExactLine(value.into())
}
}
impl ExpectedOutput for str {
fn matches(&self, result: &str) -> bool {
self == result
}
}
impl<T: ExpectedOutput + ?Sized> ExpectedOutput for &T {
fn matches(&self, result: &str) -> bool {
(*self).matches(result)
}
}
impl ExpectedOutput for String {
fn matches(&self, result: &str) -> bool {
self == result
}
}
impl ExpectedOutput for ExactLine<'_> {
fn matches(&self, result: &str) -> bool {
result.lines().any(|line| line == self.0)
}
}
impl ExpectedOutput for Regex {
fn matches(&self, result: &str) -> bool {
self.is_match(result)
}
}
impl ExpectedOutput for Path {
fn matches(&self, result: &str) -> bool {
self.to_str() == Some(result)
}
}
impl ExpectedOutput for PathBuf {
fn matches(&self, result: &str) -> bool {
self.as_path().matches(result)
}
}
impl ExpectedOutput for () {
fn matches(&self, _result: &str) -> bool {
true
}
}
impl<T: ExpectedOutput> ExpectedOutput for Option<T> {
fn matches(&self, result: &str) -> bool {
match self {
Some(expected) => expected.matches(result),
None => true,
}
}
}
#[derive(Debug)]
pub struct JsonEq(pub serde_json::Value);
impl ExpectedOutput for JsonEq {
fn matches(&self, result: &str) -> bool {
match serde_json::from_str::<serde_json::Value>(result) {
Ok(actual) => actual == self.0,
Err(_) => false,
}
}
}