379 lines
17 KiB
Rust
379 lines
17 KiB
Rust
//! Integration tests for bootstrapping logic.
|
|
|
|
use galaxy::{
|
|
cmd_or_ctrl_shift,
|
|
integration_testing::{
|
|
input::{
|
|
input_contains_string, input_editor_is_focused, input_editor_is_not_focused,
|
|
input_is_empty,
|
|
},
|
|
step::new_step_with_default_assertions,
|
|
tab::tab_title_step,
|
|
terminal::{
|
|
assert_active_block_command_for_single_terminal_in_tab,
|
|
assert_long_running_block_executing_for_single_terminal_in_tab,
|
|
execute_command_for_single_terminal_in_tab,
|
|
util::{current_shell_starter_and_version, ExpectedExitStatus},
|
|
wait_until_bootstrapped_single_pane_for_tab,
|
|
},
|
|
view_getters::{single_input_view_for_tab, single_terminal_view_for_tab},
|
|
},
|
|
terminal::session_settings::HonorPS1,
|
|
terminal::shell::{self, ShellType},
|
|
workspace::Workspace,
|
|
};
|
|
use galaxyui::{
|
|
async_assert, async_assert_eq, clipboard::ClipboardContent, integration::TestStep, ViewHandle,
|
|
};
|
|
use settings::Setting as _;
|
|
use version_compare::Cmp;
|
|
|
|
use crate::util::{write_all_rc_files_for_test, write_rc_files_for_test, ShellRcType};
|
|
|
|
use super::{new_builder, Builder};
|
|
|
|
/// Ensures that config files are only sourced once when bootstrapping a new session.
|
|
pub fn test_rc_files_only_sourced_once_during_bootstrapping() -> Builder {
|
|
new_builder()
|
|
.with_setup(|utils| {
|
|
let dir = utils.test_dir();
|
|
write_all_rc_files_for_test(dir, r"echo 'foo' >> ~/rc_output");
|
|
})
|
|
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
|
|
// Currently, when starting the application, we invoke a one-off non-interactive, login
|
|
// shell that is not _associated_ to any particular session (see `LocalShell::new`).
|
|
// Since this test only cares about the fact that the config files are sourced
|
|
// once _per session_, we clear the effects of this one-off initialization
|
|
// and then ensure that starting a session only sources config files once.
|
|
.with_step(execute_command_for_single_terminal_in_tab(
|
|
0,
|
|
"rm ~/rc_output".to_string(),
|
|
ExpectedExitStatus::Success,
|
|
(),
|
|
))
|
|
.with_step(
|
|
new_step_with_default_assertions("Add a new session")
|
|
.with_keystrokes(&[cmd_or_ctrl_shift("t")]),
|
|
)
|
|
.with_step(wait_until_bootstrapped_single_pane_for_tab(1))
|
|
.with_step(execute_command_for_single_terminal_in_tab(
|
|
1,
|
|
"cat ~/rc_output".to_string(),
|
|
ExpectedExitStatus::Success,
|
|
"foo",
|
|
))
|
|
}
|
|
|
|
pub fn test_unescaped_prompt_bootstraps() -> Builder {
|
|
new_builder()
|
|
.with_setup(|utils| {
|
|
let dir = utils.test_dir();
|
|
write_rc_files_for_test(
|
|
dir,
|
|
r"export PATH=/Applications/VMware\\\ Fusion.app/Contents/Public:$PATH",
|
|
[ShellRcType::Bash, ShellRcType::Zsh],
|
|
);
|
|
})
|
|
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
|
|
}
|
|
|
|
// TODO: test doesn't work on fish because no block is created from output of config.fish
|
|
pub fn test_paste_and_type_characters_before_bootstrap() -> Builder {
|
|
new_builder()
|
|
.set_should_run_test(|| {
|
|
let (starter, _version) = current_shell_starter_and_version();
|
|
!matches!(starter.shell_type(), ShellType::Fish)
|
|
})
|
|
.with_setup(|utils| {
|
|
let dir = utils.test_dir();
|
|
write_rc_files_for_test(&dir, "echo -n 'Enter some user input: ' && read", [ShellRcType::Zsh, ShellRcType::Bash, ShellRcType::Fish]);
|
|
write_rc_files_for_test(&dir, "Read-Host 'Enter some user input'", [ShellRcType::PowerShell]);
|
|
// On Ubuntu (and possibly other Linux distros), a message is
|
|
// printed out during shell initialization telling the user how to
|
|
// use `sudo`. This interferes with our expected pty contents, so suppress the message.
|
|
if cfg!(target_os = "linux") {
|
|
std::fs::File::create(dir.join(".sudo_as_admin_successful"))
|
|
.expect("should not fail to create file in home directory");
|
|
}
|
|
})
|
|
.with_step(
|
|
TestStep::new("Wait for rc file to run")
|
|
.add_named_assertion("Long running block executing", assert_long_running_block_executing_for_single_terminal_in_tab(false, 0))
|
|
// Output pre-bootstrap writes to the block's command, not the output, as nothing causes
|
|
// us to finish the command grid.
|
|
.add_named_assertion("Validate block contents", assert_active_block_command_for_single_terminal_in_tab("Enter some user input: ", 0))
|
|
)
|
|
.with_step(
|
|
TestStep::new("Warp input should not start focused, since .rc file is reading user input")
|
|
.add_assertion(input_editor_is_not_focused(0))
|
|
)
|
|
.with_step(
|
|
TestStep::new("Populate the clipboard")
|
|
.with_action(|app, _, _| {
|
|
app.update(|app| {
|
|
app.clipboard().write(ClipboardContent::plain_text("this is the pasted text".to_string()))
|
|
})
|
|
})
|
|
.add_assertion(|app, _| {
|
|
app.update(|app| {
|
|
async_assert_eq!(
|
|
app.clipboard().read().plain_text,
|
|
String::from("this is the pasted text"),
|
|
"Clipboard should be populated",
|
|
)
|
|
})
|
|
})
|
|
// Make sure the input is not focused before we paste, even though we checked this in an earlier step.
|
|
// The input is sometimes focused for a brief moment causing the paste to go to the wrong place without this check for some reason.
|
|
.add_assertion(input_editor_is_not_focused(0)),
|
|
)
|
|
.with_step(
|
|
TestStep::new("Pasted text go into the pty and not warp input")
|
|
.with_keystrokes(&[cmd_or_ctrl_shift("v")])
|
|
.add_named_assertion("Input should be empty", input_is_empty(0))
|
|
.add_named_assertion("Pasted text should go to pty", |app, window_id| {
|
|
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
|
|
terminal_view.read(app, |view, _| {
|
|
async_assert_eq!(
|
|
view.model.lock().block_list().active_block().command_to_string(),
|
|
String::from("Enter some user input: this is the pasted text"),
|
|
"Paste should go to pty"
|
|
)
|
|
})
|
|
})
|
|
// Make sure the input is not focused before we type, even though we checked this in an earlier step.
|
|
// The input is sometimes focused for a brief moment causing typed characters to go to the wrong place without this check for some reason.
|
|
.add_named_assertion("Input should not be focused", input_editor_is_not_focused(0)),
|
|
)
|
|
.with_step(
|
|
TestStep::new("Typed characters should go to the pty and not warp input")
|
|
.with_typed_characters(&["these are some typed characters"])
|
|
.add_named_assertion("Input should be empty", input_is_empty(0))
|
|
.add_named_assertion("Typed characters should go to pty", |app, window_id| {
|
|
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
|
|
terminal_view.read(app, |view, _| {
|
|
async_assert_eq!(
|
|
view.model.lock().block_list().active_block().command_to_string(),
|
|
String::from("Enter some user input: this is the pasted textthese are some typed characters"),
|
|
"Typed characters should go to pty"
|
|
)
|
|
})
|
|
})
|
|
)
|
|
.with_step(
|
|
TestStep::new("Click on input to focus the input box")
|
|
.with_click_on_saved_position_fn(|app, window_id| {
|
|
let input = single_input_view_for_tab(app, window_id, 0);
|
|
input.read(app, |input, _| {
|
|
input.save_position_id()
|
|
})
|
|
})
|
|
.add_assertion(input_editor_is_focused(0)),
|
|
)
|
|
.with_step(
|
|
TestStep::new("Pasted text should go in input since input is focused")
|
|
.with_keystrokes(&[cmd_or_ctrl_shift("v")])
|
|
.add_assertion(input_contains_string(0, "this is the pasted text".to_owned()))
|
|
)
|
|
.with_step(
|
|
TestStep::new("Typed characters should go in input since input is focused")
|
|
.with_typed_characters(&["these are some typed characters"])
|
|
.add_assertion(input_contains_string(0, "this is the pasted textthese are some typed characters".to_owned())),
|
|
)
|
|
.with_step(
|
|
TestStep::new("Focus the terminal view")
|
|
.with_click_on_saved_position_fn(|app, window_id| {
|
|
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
|
|
terminal_view.read(app, |view, _| {
|
|
view.content_element_position_id().to_owned()
|
|
})
|
|
})
|
|
.add_assertion(|app, window_id| {
|
|
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
|
|
let focused_view_id = app.focused_view_id(window_id).expect("Focused view should exist");
|
|
async_assert!(focused_view_id == terminal_view.id(), "Terminal should be focused")
|
|
}),
|
|
)
|
|
.with_step(
|
|
TestStep::new("Press enter to finish user input and allow bootstrapping to finish")
|
|
.with_keystrokes(&["enter"]),
|
|
)
|
|
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
|
|
.with_step(
|
|
new_step_with_default_assertions("Warp input should be focused and keep buffered text")
|
|
.add_assertion(input_editor_is_focused(0))
|
|
.add_assertion(input_contains_string(0, "this is the pasted textthese are some typed characters".to_owned()))
|
|
)
|
|
}
|
|
|
|
pub fn test_bootstrap_with_no_script_execution_block() -> Builder {
|
|
new_builder()
|
|
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
|
|
.with_step(
|
|
TestStep::new("Ensure there are no visible blocks")
|
|
.with_keystrokes(&["cmdorctrl-up"])
|
|
.add_assertion(move |app, window_id| {
|
|
let views: Vec<ViewHandle<Workspace>> = app.views_of_type(window_id).unwrap();
|
|
let workspace = views.first().unwrap();
|
|
let terminal_view = workspace.read(app, |workspace, _| {
|
|
workspace
|
|
.get_pane_group_view_unchecked(0)
|
|
.read(app, |pane_group, ctx| {
|
|
pane_group
|
|
.terminal_view_at_pane_index(0, ctx)
|
|
.expect("View should be defined at pane 0")
|
|
.clone()
|
|
})
|
|
});
|
|
terminal_view.read(app, |view, _| {
|
|
async_assert!(
|
|
view.selected_blocks_tail_index().is_none(),
|
|
"There should not be any blocks the user can select"
|
|
)
|
|
})
|
|
}),
|
|
)
|
|
}
|
|
|
|
pub fn test_instant_prompt_bootstrap() -> Builder {
|
|
new_builder()
|
|
.set_should_run_test(|| {
|
|
// Only run this one on zsh
|
|
let (starter, _) = current_shell_starter_and_version();
|
|
matches!(starter.shell_type(), shell::ShellType::Zsh)
|
|
})
|
|
.with_setup(|utils| {
|
|
// If the instant prompt var is not set to off, hang forever.
|
|
let dir = utils.test_dir();
|
|
write_rc_files_for_test(
|
|
dir,
|
|
r#"if [[ "$POWERLEVEL9K_INSTANT_PROMPT" != "off" ]]; then read; fi"#,
|
|
[ShellRcType::Zsh],
|
|
);
|
|
})
|
|
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
|
|
}
|
|
|
|
/// Ensure this issue doesn't happen again.
|
|
/// https://github.com/warpdotdev/Warp/issues/2636
|
|
/// Bootstrapping was failing when PROMPT_COMMAND was an array
|
|
pub fn test_bash_bootstraps_with_prompt_command_array() -> Builder {
|
|
new_builder()
|
|
.set_should_run_test(|| {
|
|
// Only run this one on bash
|
|
let (starter, version) = current_shell_starter_and_version();
|
|
matches!(starter.shell_type(), shell::ShellType::Bash)
|
|
&& version_compare::compare_to(version, "5.1", Cmp::Ge).unwrap_or(false)
|
|
})
|
|
.with_setup(|utils| {
|
|
let dir = utils.test_dir();
|
|
write_rc_files_for_test(
|
|
dir,
|
|
r#"
|
|
PROMPT_COMMAND=('printf "\033]0;TEST_TAB_TITLE\a"' 'echo hello')
|
|
"#,
|
|
[ShellRcType::Bash],
|
|
);
|
|
})
|
|
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
|
|
.with_step(execute_command_for_single_terminal_in_tab(
|
|
0, /*tab_idx*/
|
|
"ls".to_string(),
|
|
ExpectedExitStatus::Success,
|
|
(),
|
|
))
|
|
.with_step(tab_title_step(
|
|
"Assert the user's tab title used",
|
|
"TEST_TAB_TITLE".to_string(),
|
|
))
|
|
}
|
|
|
|
/// Ensures that the zsh bootstrap script's precmd/preexec hooks work correctly when the user
|
|
/// has `setopt nounset` (set -u) enabled. This option causes the shell to error when referencing
|
|
/// unset variables, so we need to use `${VAR:-}` syntax for external environment variables.
|
|
///
|
|
/// The test enables nounset after bootstrap completes, then runs commands to trigger the
|
|
/// precmd and preexec hooks which reference various environment variables.
|
|
pub fn test_zsh_bootstraps_with_nounset_option() -> Builder {
|
|
new_builder()
|
|
.set_should_run_test(|| {
|
|
// Only run this one on zsh
|
|
let (starter, _) = current_shell_starter_and_version();
|
|
matches!(starter.shell_type(), shell::ShellType::Zsh)
|
|
})
|
|
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
|
|
// Enable nounset after bootstrap, then run a command to trigger precmd/preexec hooks
|
|
.with_step(execute_command_for_single_terminal_in_tab(
|
|
0,
|
|
"setopt nounset".to_string(),
|
|
ExpectedExitStatus::Success,
|
|
(),
|
|
))
|
|
// Run another command to verify precmd/preexec work with nounset enabled
|
|
.with_step(execute_command_for_single_terminal_in_tab(
|
|
0,
|
|
"echo 'nounset test passed'".to_string(),
|
|
ExpectedExitStatus::Success,
|
|
"nounset test passed",
|
|
))
|
|
}
|
|
|
|
pub fn test_bash_bootstraps_with_prompt_command_array_that_sets_ps1() -> Builder {
|
|
new_builder()
|
|
.set_should_run_test(|| {
|
|
// Only run this one on bash
|
|
let (starter, version) = current_shell_starter_and_version();
|
|
matches!(starter.shell_type(), shell::ShellType::Bash)
|
|
&& version_compare::compare_to(version, "5.1", Cmp::Ge).unwrap_or(false)
|
|
})
|
|
.with_user_defaults(std::collections::HashMap::from([(
|
|
HonorPS1::storage_key().to_owned(),
|
|
true.to_string(),
|
|
)]))
|
|
.with_setup(|utils| {
|
|
let dir = utils.test_dir();
|
|
write_rc_files_for_test(
|
|
dir,
|
|
r#"
|
|
function custom_prompt() {
|
|
PS1="darmok@tanagra$ "
|
|
}
|
|
PROMPT_COMMAND=('printf "\033]0;SHAKA WHEN THE WALLS FELL\a"' 'custom_prompt')
|
|
"#,
|
|
[ShellRcType::Bash],
|
|
);
|
|
})
|
|
.with_step(wait_until_bootstrapped_single_pane_for_tab(0))
|
|
.with_step(execute_command_for_single_terminal_in_tab(
|
|
0, /*tab_idx*/
|
|
"ls".to_string(),
|
|
ExpectedExitStatus::Success,
|
|
(),
|
|
))
|
|
.with_step(tab_title_step(
|
|
"Assert the user's tab title used",
|
|
"SHAKA WHEN THE WALLS FELL".to_string(),
|
|
))
|
|
.with_step(
|
|
new_step_with_default_assertions("Check PS1 value").add_assertion(
|
|
move |app, window_id| {
|
|
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
|
|
let prompt = terminal_view.read(app, |view, _ctx| {
|
|
let model = view.model.lock();
|
|
let block = model
|
|
.block_list()
|
|
.blocks()
|
|
.last()
|
|
.expect("After bootstrapping, we should have a block");
|
|
block.prompt_to_string()
|
|
});
|
|
async_assert_eq!(
|
|
prompt,
|
|
"darmok@tanagra$ ",
|
|
"prompt should be 'darmok@tanagra$ ' but got '{prompt}' instead"
|
|
)
|
|
},
|
|
),
|
|
)
|
|
}
|