368 lines
11 KiB
Rust
368 lines
11 KiB
Rust
//! Utilities to help construct [`TerminalModel`]s and its
|
|
//! constituents, like [`Block`]s and [`BlockList`]s for use
|
|
//! in unit tests.
|
|
//!
|
|
//! Note that the example code in the documentation of this module
|
|
//! is marked as no_run only because it's currently not possible
|
|
//! to reference `#[cfg(test)]` symbols from doctests.
|
|
|
|
use std::{io::sink, sync::Arc};
|
|
|
|
use warp_core::command::ExitCode;
|
|
use warpui::r#async::executor::Background;
|
|
|
|
use crate::ai::blocklist::SerializedBlockListItem;
|
|
use crate::terminal::{
|
|
color::{self, Colors},
|
|
event_listener::ChannelEventListener,
|
|
BlockPadding, SizeInfo,
|
|
};
|
|
|
|
use super::{
|
|
ansi::{CommandFinishedValue, Handler, PrecmdValue, PreexecValue, Processor},
|
|
block::{Block, BlockId, BlockSize},
|
|
blocks::BlockList,
|
|
bootstrap::BootstrapStage,
|
|
terminal_model::BlockIndex,
|
|
ObfuscateSecrets, TerminalModel,
|
|
};
|
|
|
|
pub fn block_size() -> BlockSize {
|
|
BlockSize {
|
|
size: SizeInfo::new_without_font_metrics(10, 7),
|
|
block_padding: block_padding(),
|
|
max_block_scroll_limit: 1000,
|
|
warp_prompt_height_lines: 0.6,
|
|
}
|
|
}
|
|
|
|
fn block_padding() -> BlockPadding {
|
|
BlockPadding {
|
|
padding_top: 0.2,
|
|
command_padding_top: 0.2,
|
|
middle: 0.5,
|
|
bottom: 1.0,
|
|
}
|
|
}
|
|
|
|
/// A helper struct for creating a [`BlockList`] for use in tests.
|
|
///
|
|
/// For example, to create a [`BlockList`] that respects the user's custom
|
|
/// prompt:
|
|
///
|
|
/// ```no_run
|
|
/// # use warp::terminal::model::test_utils::TestBlockListBuilder;
|
|
/// let block_list = TestBlockListBuilder::new()
|
|
/// .with_honor_ps1(true)
|
|
/// .build();
|
|
/// ```
|
|
/// For tests that want to observe the events produced through interactions
|
|
/// with the block list, a custom [`ChannelEventListener`] can be registered.
|
|
///
|
|
/// This example restores a block, and asserts that an event was sent over
|
|
/// the channel event proxy:
|
|
///
|
|
/// ```no_run
|
|
/// # use warp::terminal::event::{BlockType, Event};
|
|
/// # use warp::terminal::event_listener::ChannelEventListener;
|
|
/// # use warp::terminal::model::block::SerializedBlock;
|
|
/// # use warp::terminal::model::test_utils::TestBlockListBuilder;
|
|
///
|
|
/// let (events_tx, events_rx) = async_channel::unbounded();
|
|
/// let channel_event_proxy = ChannelEventListener::builder_for_test()
|
|
/// .with_terminal_events_tx(events_tx)
|
|
/// .build();
|
|
///
|
|
/// let block = SerializedBlock::new_for_test("test".into(), "test".into());
|
|
///
|
|
/// let block_list = TestBlockListBuilder::new()
|
|
/// .with_channel_event_proxy(channel_event_proxy)
|
|
/// .with_restored_blocks(&[block])
|
|
/// .build();
|
|
///
|
|
/// let Ok(Event::BlockCompleted(data)) = events_rx.try_recv() else {
|
|
/// panic!("Expected a BlockCompleted event to have been generated!");
|
|
/// };
|
|
///
|
|
/// assert!(matches!(data.block_type, BlockType::Restored));
|
|
/// ```
|
|
pub struct TestBlockListBuilder<'a> {
|
|
restored_blocks: Option<&'a [SerializedBlockListItem]>,
|
|
honor_ps1: bool,
|
|
block_sizes: BlockSize,
|
|
channel_event_proxy: ChannelEventListener,
|
|
}
|
|
|
|
impl<'a> TestBlockListBuilder<'a> {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
restored_blocks: None,
|
|
honor_ps1: false,
|
|
block_sizes: block_size(),
|
|
channel_event_proxy: ChannelEventListener::new_for_test(),
|
|
}
|
|
}
|
|
|
|
pub fn with_restored_blocks(mut self, restored_blocks: &'a [SerializedBlockListItem]) -> Self {
|
|
self.restored_blocks = Some(restored_blocks);
|
|
self
|
|
}
|
|
|
|
pub fn with_honor_ps1(mut self, honor_ps1: bool) -> Self {
|
|
self.honor_ps1 = honor_ps1;
|
|
self
|
|
}
|
|
|
|
pub fn with_block_sizes(mut self, block_sizes: BlockSize) -> Self {
|
|
self.block_sizes = block_sizes;
|
|
self
|
|
}
|
|
|
|
pub fn with_channel_event_proxy(mut self, channel_event_proxy: ChannelEventListener) -> Self {
|
|
self.channel_event_proxy = channel_event_proxy;
|
|
self
|
|
}
|
|
|
|
pub fn build(self) -> BlockList {
|
|
let mut block_list = BlockList::new(
|
|
self.restored_blocks,
|
|
self.block_sizes,
|
|
self.channel_event_proxy,
|
|
Arc::new(Background::default()),
|
|
false, /* show_warp_bootstrap_input */
|
|
false, /* show_warp_bootstrap_input */
|
|
false, /* show_memory_stats */
|
|
self.honor_ps1,
|
|
false, /* is_inverted */
|
|
ObfuscateSecrets::No,
|
|
false, /* is_telemetry_enabled */
|
|
);
|
|
// This is usually done by the terminal manager after constructing the blocklist,
|
|
// but we have tests assuming the separator exists.
|
|
if self.restored_blocks.is_some() {
|
|
block_list.append_session_restoration_separator_to_block_list(false);
|
|
}
|
|
block_list
|
|
}
|
|
}
|
|
|
|
impl Default for TestBlockListBuilder<'_> {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// A helper struct for creating a [`Block`] for use in tests.
|
|
///
|
|
/// For example, to create a [`Block`] that respects the user's custom prompt:
|
|
///
|
|
/// ```no_run
|
|
/// # use warp::terminal::model::test_utils::TestBlockBuilder;
|
|
/// let block = TestBlockBuilder::new()
|
|
/// .with_honor_ps1(true)
|
|
/// .build();
|
|
/// ```
|
|
pub struct TestBlockBuilder {
|
|
block_index: BlockIndex,
|
|
honor_ps1: bool,
|
|
event_proxy: ChannelEventListener,
|
|
size: BlockSize,
|
|
bootstrap_stage: BootstrapStage,
|
|
}
|
|
|
|
/// A helper struct for creating a [`Block`] for use in tests.
|
|
impl TestBlockBuilder {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
block_index: BlockIndex::zero(),
|
|
honor_ps1: false,
|
|
event_proxy: ChannelEventListener::new_for_test(),
|
|
size: block_size(),
|
|
bootstrap_stage: BootstrapStage::PostBootstrapPrecmd,
|
|
}
|
|
}
|
|
|
|
pub fn with_block_index(mut self, block_index: BlockIndex) -> Self {
|
|
self.block_index = block_index;
|
|
self
|
|
}
|
|
|
|
pub fn with_honor_ps1(mut self, honor_ps1: bool) -> Self {
|
|
self.honor_ps1 = honor_ps1;
|
|
self
|
|
}
|
|
|
|
pub fn with_event_proxy(mut self, event_proxy: ChannelEventListener) -> Self {
|
|
self.event_proxy = event_proxy;
|
|
self
|
|
}
|
|
|
|
pub fn with_size_info(mut self, size: SizeInfo) -> Self {
|
|
self.size.size = size;
|
|
self
|
|
}
|
|
|
|
pub fn with_bootstrap_stage(mut self, bootstrap_stage: BootstrapStage) -> Self {
|
|
self.bootstrap_stage = bootstrap_stage;
|
|
self
|
|
}
|
|
|
|
pub fn build(self) -> Block {
|
|
Block::new(
|
|
BlockId::new(),
|
|
self.size,
|
|
self.event_proxy,
|
|
Arc::new(Background::default()),
|
|
self.bootstrap_stage,
|
|
false, /* show_warp_bootstrap_input */
|
|
false, /* show_in_band_command_blocks */
|
|
false, /* show_memory_stats */
|
|
self.block_index,
|
|
self.honor_ps1,
|
|
ObfuscateSecrets::No,
|
|
false, /* is_telemetry_enabled */
|
|
None,
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Default for TestBlockBuilder {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
pub fn create_test_block_with_grids(
|
|
block_index: BlockIndex,
|
|
prompt_and_command_grid: super::blockgrid::BlockGrid,
|
|
rprompt_grid: super::blockgrid::BlockGrid,
|
|
output_grid: super::blockgrid::BlockGrid,
|
|
honor_ps1: bool,
|
|
) -> super::block::Block {
|
|
let mut block = TestBlockBuilder::new()
|
|
.with_block_index(block_index)
|
|
.with_honor_ps1(honor_ps1)
|
|
.build();
|
|
block.set_prompt_and_command_grid(prompt_and_command_grid);
|
|
block.set_rprompt_grid(rprompt_grid);
|
|
block.set_output_grid(output_grid);
|
|
block
|
|
}
|
|
|
|
impl TerminalModel {
|
|
/// Creates a simple, default [`TerminalModel`] with an optional
|
|
/// set of restored blocks and an optional [`ChannelEventListener`]
|
|
/// to subscribe to terminal events.
|
|
///
|
|
/// See [`TerminalModel::new_for_test`] for a more configurable
|
|
/// test constructor.
|
|
pub fn mock(
|
|
restored_blocks: Option<&[SerializedBlockListItem]>,
|
|
event_proxy: Option<ChannelEventListener>,
|
|
) -> TerminalModel {
|
|
TerminalModel::new_for_test(
|
|
block_size(),
|
|
color::List::from(&Colors::default()),
|
|
event_proxy.unwrap_or_else(ChannelEventListener::new_for_test),
|
|
Arc::new(Background::default()),
|
|
false,
|
|
restored_blocks,
|
|
false,
|
|
false, /* is_inverted */
|
|
None,
|
|
)
|
|
}
|
|
|
|
/// Simulates the creation of a block as if the `input` command
|
|
/// was run and it produced `output` bytes.
|
|
///
|
|
/// This includes invoking all of the relevant hooks that
|
|
/// would be invoked by running the command against a real PTY
|
|
/// (e.g. pre-exec, pre-cmd).
|
|
pub fn simulate_block<B: AsBytes>(&mut self, input: B, output: B) {
|
|
self.simulate_long_running_block(input, output);
|
|
self.block_list_mut()
|
|
.active_block_mut()
|
|
.set_was_long_running(false.into());
|
|
self.finish_block();
|
|
}
|
|
|
|
/// Simulates the creation of a long-running block as if the `input` command
|
|
/// was run and it produced `output_so_far` bytes.
|
|
pub fn simulate_long_running_block<B: AsBytes>(&mut self, input: B, output_so_far: B) {
|
|
self.block_list_mut().active_block_mut().start();
|
|
self.simulate_cmd(input);
|
|
self.process_bytes(output_so_far);
|
|
self.block_list_mut()
|
|
.active_block_mut()
|
|
.set_was_long_running(true.into());
|
|
}
|
|
|
|
/// Simulates a command being run by writing the `input`
|
|
/// bytes as input and subsequently calling pre-exec.
|
|
///
|
|
/// We assume that `input` forms a valid UTF-8 string.
|
|
pub fn simulate_cmd<B: AsBytes>(&mut self, input: B) {
|
|
self.process_bytes(input.as_bytes());
|
|
self.preexec(PreexecValue {
|
|
command: std::str::from_utf8(input.as_bytes()).unwrap().to_owned(),
|
|
});
|
|
}
|
|
|
|
/// Simulates the completion of a block.
|
|
/// Assumes that a block was running to begin with.
|
|
pub fn finish_block(&mut self) {
|
|
self.command_finished(CommandFinishedValue {
|
|
exit_code: ExitCode::from(0),
|
|
next_block_id: BlockId::new(),
|
|
});
|
|
self.precmd(PrecmdValue {
|
|
pwd: None,
|
|
git_head: None,
|
|
git_branch: None,
|
|
virtual_env: None,
|
|
conda_env: None,
|
|
node_version: None,
|
|
session_id: Some(0),
|
|
kube_config: None,
|
|
ps1: None,
|
|
honor_ps1: None,
|
|
rprompt: None,
|
|
ps1_is_encoded: Some(true),
|
|
is_after_in_band_command: false,
|
|
});
|
|
}
|
|
|
|
/// Processes a set of `bytes` and applies them to the model,
|
|
/// akin to what happens when reading bytes from a real PTY.
|
|
pub fn process_bytes<B: AsBytes>(&mut self, bytes: B) {
|
|
let bytes = bytes.as_bytes();
|
|
let mut processor = Processor::new();
|
|
processor.parse_bytes(
|
|
self,
|
|
bytes,
|
|
// For unit tests, there's no shell to write back to
|
|
// so the writes should no-op.
|
|
&mut sink(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A helper trait to make it more ergonomic
|
|
/// to use types that can be converted to a byte slice.
|
|
pub trait AsBytes {
|
|
fn as_bytes(&self) -> &[u8];
|
|
}
|
|
|
|
impl AsBytes for &str {
|
|
fn as_bytes(&self) -> &[u8] {
|
|
str::as_bytes(self)
|
|
}
|
|
}
|
|
|
|
impl AsBytes for &[u8] {
|
|
fn as_bytes(&self) -> &[u8] {
|
|
self
|
|
}
|
|
}
|