summaryrefslogtreecommitdiffstats
path: root/zellij-server
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2022-12-19 12:48:43 +0100
committerGitHub <noreply@github.com>2022-12-19 12:48:43 +0100
commit1b5f3c52a48feb584228f326c20829c83c21cc47 (patch)
treef4a76f6ecb2e3fca72c83899ba2c1cd83acbfe02 /zellij-server
parentd1f50150f6f7525f93ccb9ed94f75ce6bfb5c60b (diff)
fix(panes): show visual error when failing to resize panes (#2036)
* fix(panes): show visual error when failing to resize pane vertically/horizontally * fix(resize): retry pane resize on rounding errors * fix(resize): proper error when resizing other panes into fixed panes * style(fmt): rustfmt
Diffstat (limited to 'zellij-server')
-rw-r--r--zellij-server/src/background_jobs.rs11
-rw-r--r--zellij-server/src/panes/plugin_pane.rs10
-rw-r--r--zellij-server/src/panes/terminal_pane.rs10
-rw-r--r--zellij-server/src/panes/tiled_panes/mod.rs33
-rw-r--r--zellij-server/src/panes/tiled_panes/pane_resizer.rs21
-rw-r--r--zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs146
-rw-r--r--zellij-server/src/screen.rs28
-rw-r--r--zellij-server/src/tab/mod.rs30
-rw-r--r--zellij-server/src/tab/unit/tab_tests.rs195
9 files changed, 415 insertions, 69 deletions
diff --git a/zellij-server/src/background_jobs.rs b/zellij-server/src/background_jobs.rs
index 3c89ed064..c612c9d57 100644
--- a/zellij-server/src/background_jobs.rs
+++ b/zellij-server/src/background_jobs.rs
@@ -10,7 +10,7 @@ use crate::thread_bus::Bus;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum BackgroundJob {
- DisplayPaneError(PaneId, String),
+ DisplayPaneError(Vec<PaneId>, String),
Exit,
}
@@ -34,7 +34,7 @@ pub(crate) fn background_jobs_main(bus: Bus<BackgroundJob>) -> Result<()> {
err_ctx.add_call(ContextType::BackgroundJob((&event).into()));
let job = event.clone();
match event {
- BackgroundJob::DisplayPaneError(pane_id, text) => {
+ BackgroundJob::DisplayPaneError(pane_ids, text) => {
if job_already_running(job, &mut running_jobs) {
continue;
}
@@ -42,11 +42,14 @@ pub(crate) fn background_jobs_main(bus: Bus<BackgroundJob>) -> Result<()> {
let senders = bus.senders.clone();
async move {
let _ = senders.send_to_screen(
- ScreenInstruction::AddRedPaneFrameColorOverride(pane_id, Some(text)),
+ ScreenInstruction::AddRedPaneFrameColorOverride(
+ pane_ids.clone(),
+ Some(text),
+ ),
);
task::sleep(std::time::Duration::from_millis(FLASH_DURATION_MS)).await;
let _ = senders.send_to_screen(
- ScreenInstruction::ClearPaneFrameColorOverride(pane_id),
+ ScreenInstruction::ClearPaneFrameColorOverride(pane_ids),
);
}
});
diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs
index 3a0ec9c83..babb66b67 100644
--- a/zellij-server/src/panes/plugin_pane.rs
+++ b/zellij-server/src/panes/plugin_pane.rs
@@ -16,7 +16,7 @@ use zellij_utils::{
channels::SenderWithContext,
data::{Event, InputMode, Mouse, Palette, PaletteColor, Style},
errors::prelude::*,
- pane_size::{Dimension, PaneGeom},
+ pane_size::PaneGeom,
shared::make_terminal_title,
vte,
};
@@ -341,28 +341,28 @@ impl Pane for PluginPane {
}
fn reduce_height(&mut self, percent: f64) {
if let Some(p) = self.geom.rows.as_percent() {
- self.geom.rows = Dimension::percent(p - percent);
+ self.geom.rows.set_percent(p - percent);
self.resize_grids();
self.set_should_render(true);
}
}
fn increase_height(&mut self, percent: f64) {
if let Some(p) = self.geom.rows.as_percent() {
- self.geom.rows = Dimension::percent(p + percent);
+ self.geom.rows.set_percent(p + percent);
self.resize_grids();
self.set_should_render(true);
}
}
fn reduce_width(&mut self, percent: f64) {
if let Some(p) = self.geom.cols.as_percent() {
- self.geom.cols = Dimension::percent(p - percent);
+ self.geom.cols.set_percent(p - percent);
self.resize_grids();
self.set_should_render(true);
}
}
fn increase_width(&mut self, percent: f64) {
if let Some(p) = self.geom.cols.as_percent() {
- self.geom.cols = Dimension::percent(p + percent);
+ self.geom.cols.set_percent(p + percent);
self.resize_grids();
self.set_should_render(true);
}
diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs
index 4d5ed840c..d206245ad 100644
--- a/zellij-server/src/panes/terminal_pane.rs
+++ b/zellij-server/src/panes/terminal_pane.rs
@@ -18,8 +18,8 @@ use zellij_utils::pane_size::Offset;
use zellij_utils::{
data::{InputMode, Palette, PaletteColor, Style},
errors::prelude::*,
+ pane_size::PaneGeom,
pane_size::SizeInPixels,
- pane_size::{Dimension, PaneGeom},
position::Position,
shared::make_terminal_title,
vte,
@@ -446,25 +446,25 @@ impl Pane for TerminalPane {
}
fn reduce_height(&mut self, percent: f64) {
if let Some(p) = self.geom.rows.as_percent() {
- self.geom.rows = Dimension::percent(p - percent);
+ self.geom.rows.set_percent(p - percent);
self.set_should_render(true);
}
}
fn increase_height(&mut self, percent: f64) {
if let Some(p) = self.geom.rows.as_percent() {
- self.geom.rows = Dimension::percent(p + percent);
+ self.geom.rows.set_percent(p + percent);
self.set_should_render(true);
}
}
fn reduce_width(&mut self, percent: f64) {
if let Some(p) = self.geom.cols.as_percent() {
- self.geom.cols = Dimension::percent(p - percent);
+ self.geom.cols.set_percent(p - percent);
self.set_should_render(true);
}
}
fn increase_width(&mut self, percent: f64) {
if let Some(p) = self.geom.cols.as_percent() {
- self.geom.cols = Dimension::percent(p + percent);
+ self.geom.cols.set_percent(p + percent);
self.set_should_render(true);
}
}
diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs
index ee752ea15..726627f4b 100644
--- a/zellij-server/src/panes/tiled_panes/mod.rs
+++ b/zellij-server/src/panes/tiled_panes/mod.rs
@@ -577,9 +577,38 @@ impl TiledPanes {
*self.viewport.borrow(),
);
- pane_grid
+ match pane_grid
.change_pane_size(&active_pane_id, strategy, (RESIZE_PERCENT, RESIZE_PERCENT))
- .with_context(err_context)?;
+ .with_context(err_context)
+ {
+ Ok(_) => {},
+ Err(err) => match err.downcast_ref::<ZellijError>() {
+ Some(ZellijError::PaneSizeUnchanged) => {
+ // try once more with double the resize percent, but let's keep it at that
+ match pane_grid
+ .change_pane_size(
+ &active_pane_id,
+ strategy,
+ (RESIZE_PERCENT * 2.0, RESIZE_PERCENT * 2.0),
+ )
+ .with_context(err_context)
+ {
+ Ok(_) => {},
+ Err(err) => match err.downcast_ref::<ZellijError>() {
+ Some(ZellijError::PaneSizeUnchanged) => {
+ Err::<(), _>(err).non_fatal()
+ },
+ _ => {
+ return Err(err);
+ },
+ },
+ }
+ },
+ _ => {
+ return Err(err);
+ },
+ },
+ }
for pane in self.panes.values_mut() {
resize_pty!(pane, self.os_api, self.senders).unwrap();
diff --git a/zellij-server/src/panes/tiled_panes/pane_resizer.rs b/zellij-server/src/panes/tiled_panes/pane_resizer.rs
index 37eb6c4c6..c01e42e4b 100644
--- a/zellij-server/src/panes/tiled_panes/pane_resizer.rs
+++ b/zellij-server/src/panes/tiled_panes/pane_resizer.rs
@@ -53,7 +53,7 @@ impl<'a> PaneResizer<'a> {
let spans = self
.discretize_spans(grid, space)
.map_err(|err| anyhow!("{}", err))?;
- self.apply_spans(spans);
+ self.apply_spans(spans)?;
Ok(())
}
@@ -127,10 +127,13 @@ impl<'a> PaneResizer<'a> {
Ok(grid.into_iter().flatten().collect())
}
- fn apply_spans(&mut self, spans: Vec<Span>) {
+ fn apply_spans(&mut self, spans: Vec<Span>) -> Result<()> {
+ let err_context = || format!("Failed to apply spans");
let mut panes = self.panes.borrow_mut();
+ let mut geoms_changed = false;
for span in spans {
let pane = panes.get_mut(&span.pid).unwrap();
+ let current_geom = pane.position_and_size();
let new_geom = match span.direction {
SplitDirection::Horizontal => PaneGeom {
x: span.pos,
@@ -143,12 +146,26 @@ impl<'a> PaneResizer<'a> {
..pane.current_geom()
},
};
+ if new_geom.rows.as_usize() != current_geom.rows.as_usize()
+ || new_geom.cols.as_usize() != current_geom.cols.as_usize()
+ {
+ geoms_changed = true;
+ }
if pane.geom_override().is_some() {
pane.set_geom_override(new_geom);
} else {
pane.set_geom(new_geom);
}
}
+ if geoms_changed {
+ Ok(())
+ } else {
+ // probably a rounding issue - this might be considered an error depending on who
+ // called us - if it's an explicit resize operation, it's clearly an error (the user
+ // wanted to resize and doesn't care about percentage rounding), if it's resizing the
+ // terminal window as a whole, it might not be
+ Err(ZellijError::PaneSizeUnchanged).with_context(err_context)
+ }
}
// FIXME: Functions like this should have unit tests!
diff --git a/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs b/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs
index fdb2f74ad..e34164f9e 100644
--- a/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs
+++ b/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs
@@ -88,6 +88,56 @@ impl<'a> TiledPaneGrid<'a> {
}
.is_fixed())
}
+ fn neighbor_pane_ids(&self, pane_id: &PaneId, direction: Direction) -> Result<Vec<PaneId>> {
+ let err_context = || format!("Failed to get neighboring panes");
+ // Shorthand
+ use Direction as Dir;
+ let mut neighbor_terminals = self
+ .pane_ids_directly_next_to(pane_id, &direction)
+ .with_context(err_context)?;
+
+ let neighbor_terminal_borders: HashSet<_> = if direction.is_horizontal() {
+ neighbor_terminals
+ .iter()
+ .map(|t| self.panes.borrow().get(t).unwrap().y())
+ .collect()
+ } else {
+ neighbor_terminals
+ .iter()
+ .map(|t| self.panes.borrow().get(t).unwrap().x())
+ .collect()
+ };
+
+ // Only return those neighbors that are aligned and between pane borders
+ let (some_direction, other_direction) = match direction {
+ Dir::Left | Dir::Right => (Dir::Up, Dir::Down),
+ Dir::Down | Dir::Up => (Dir::Left, Dir::Right),
+ };
+ let (some_borders, _some_terminals) = self
+ .contiguous_panes_with_alignment(
+ pane_id,
+ &neighbor_terminal_borders,
+ &direction,
+ &some_direction,
+ )
+ .with_context(err_context)?;
+ let (other_borders, _other_terminals) = self
+ .contiguous_panes_with_alignment(
+ pane_id,
+ &neighbor_terminal_borders,
+ &direction,
+ &other_direction,
+ )
+ .with_context(err_context)?;
+ neighbor_terminals.retain(|t| {
+ if direction.is_horizontal() {
+ self.pane_is_between_horizontal_borders(t, some_borders, other_borders)
+ } else {
+ self.pane_is_between_vertical_borders(t, some_borders, other_borders)
+ }
+ });
+ Ok(neighbor_terminals)
+ }
// Check if panes in the desired direction can be resized. Returns the maximum resize that's
// possible (at most `change_by`).
@@ -100,12 +150,39 @@ impl<'a> TiledPaneGrid<'a> {
let err_context = || format!("failed to determine if pane {pane_id:?} can {strategy}");
if let Some(direction) = strategy.direction {
- let mut pane_ids = self
- .pane_ids_directly_next_to(pane_id, &direction)
+ if !self
+ .pane_is_flexible(direction.into(), pane_id)
+ .unwrap_or(false)
+ {
+ let pane_ids = match pane_id {
+ PaneId::Terminal(id) => vec![(*id, true)],
+ PaneId::Plugin(id) => vec![(*id, false)],
+ };
+ return Err(ZellijError::CantResizeFixedPanes { pane_ids })
+ .with_context(err_context);
+ }
+ let pane_ids = self
+ .neighbor_pane_ids(pane_id, direction)
.with_context(err_context)?;
- pane_ids.retain(|id| self.pane_is_flexible(direction.into(), id).unwrap_or(false));
+ let fixed_panes: Vec<PaneId> = pane_ids
+ .iter()
+ .filter(|p| !self.pane_is_flexible(direction.into(), p).unwrap_or(false))
+ .copied()
+ .collect();
+ if !fixed_panes.is_empty() {
+ let mut pane_ids = vec![];
+ for fixed_pane in fixed_panes {
+ match fixed_pane {
+ PaneId::Terminal(id) => pane_ids.push((id, true)),
+ PaneId::Plugin(id) => pane_ids.push((id, false)),
+ };
+ }
+ return Err(ZellijError::CantResizeFixedPanes { pane_ids })
+ .with_context(err_context);
+ }
if pane_ids.is_empty() {
+ // TODO: proper error
return Ok(false);
}
@@ -160,37 +237,56 @@ impl<'a> TiledPaneGrid<'a> {
change_by: (f64, f64),
) -> Result<bool> {
let err_context = || format!("failed to {strategy} by {change_by:?} for pane {pane_id:?}");
-
// Shorthand
use Direction as Dir;
+ let mut fixed_panes_blocking_resize = vec![];
+
// Default behavior is to only increase pane size, unless the direction being resized to is
// a boundary. In this case, decrease size from the other side (invert strategy)!
- let strategy = if strategy.resize_increase() // Only invert when increasing
+ let can_invert_strategy_if_needed = strategy.resize_increase() // Only invert when increasing
&& strategy.invert_on_boundaries // Only invert if configured to do so
- && strategy.direction.is_some() // Only invert if there's a direction
- && strategy
- .direction
- .and_then(|direction| {
- // Only invert if there are no neighbor IDs in the given direction
- let mut neighbors = self.pane_ids_directly_next_to(pane_id, &direction)
- .unwrap_or_default();
- neighbors.retain(|pane_id| self.pane_is_flexible(direction.into(), pane_id).unwrap_or(false));
- neighbors.is_empty().then_some(true)
+ && strategy.direction.is_some(); // Only invert if there's a direction
+
+ let can_change_pane_size_in_main_direction = self
+ .can_change_pane_size(pane_id, &strategy, change_by)
+ .unwrap_or_else(|err| {
+ if let Some(ZellijError::CantResizeFixedPanes { pane_ids }) =
+ err.downcast_ref::<ZellijError>()
+ {
+ fixed_panes_blocking_resize.append(&mut pane_ids.clone());
+ }
+ false
+ });
+ let can_change_pane_size_in_inverted_direction = if can_invert_strategy_if_needed {
+ let strategy = strategy.invert();
+ self.can_change_pane_size(pane_id, &strategy, change_by)
+ .unwrap_or_else(|err| {
+ if let Some(ZellijError::CantResizeFixedPanes { pane_ids }) =
+ err.downcast_ref::<ZellijError>()
+ {
+ fixed_panes_blocking_resize.append(&mut pane_ids.clone());
+ }
+ false
})
- .unwrap_or(false)
- {
- strategy.invert()
} else {
- *strategy
+ false
};
-
- if !self
- .can_change_pane_size(pane_id, &strategy, change_by)
- .with_context(err_context)?
+ if strategy.direction.is_some()
+ && !can_change_pane_size_in_main_direction
+ && !can_change_pane_size_in_inverted_direction
{
- // Resize not possible, quit
- return Ok(false);
+ // we can't resize in any direction, not playing the blame game, but I'm looking at
+ // you: fixed_panes_blocking_resize
+ return Err(ZellijError::CantResizeFixedPanes {
+ pane_ids: fixed_panes_blocking_resize,
+ })
+ .with_context(err_context);
}
+ let strategy = if can_change_pane_size_in_main_direction {
+ *strategy
+ } else {
+ strategy.invert()
+ };
if let Some(direction) = strategy.direction {
let mut neighbor_terminals = self
@@ -407,7 +503,7 @@ impl<'a> TiledPaneGrid<'a> {
};
if self
.change_pane_size(pane_id, &new_strategy, change_by)
- .with_context(err_context)?
+ .unwrap_or(false)
{
return Ok(true);
}
diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs
index 9ed803bbd..a862cb405 100644
--- a/zellij-server/src/screen.rs
+++ b/zellij-server/src/screen.rs
@@ -224,8 +224,8 @@ pub enum ScreenInstruction {
SearchToggleCaseSensitivity(ClientId),
SearchToggleWholeWord(ClientId),
SearchToggleWrap(ClientId),
- AddRedPaneFrameColorOverride(PaneId, Option<String>), // Option<String> => optional error text
- ClearPaneFrameColorOverride(PaneId),
+ AddRedPaneFrameColorOverride(Vec<PaneId>, Option<String>), // Option<String> => optional error text
+ ClearPaneFrameColorOverride(Vec<PaneId>),
}
impl From<&ScreenInstruction> for ScreenContext {
@@ -2120,22 +2120,26 @@ pub(crate) fn screen_thread_main(
screen.render()?;
screen.unblock_input()?;
},
- ScreenInstruction::AddRedPaneFrameColorOverride(pane_id, error_text) => {
+ ScreenInstruction::AddRedPaneFrameColorOverride(pane_ids, error_text) => {
let all_tabs = screen.get_tabs_mut();
- for tab in all_tabs.values_mut() {
- if tab.has_pane_with_pid(&pane_id) {
- tab.add_red_pane_frame_color_override(pane_id, error_text);
- break;
+ for pane_id in pane_ids {
+ for tab in all_tabs.values_mut() {
+ if tab.has_pane_with_pid(&pane_id) {
+ tab.add_red_pane_frame_color_override(pane_id, error_text.clone());
+ break;
+ }
}
}
screen.render()?;
},
- ScreenInstruction::ClearPaneFrameColorOverride(pane_id) => {
+ ScreenInstruction::ClearPaneFrameColorOverride(pane_ids) => {
let all_tabs = screen.get_tabs_mut();
- for tab in all_tabs.values_mut() {
- if tab.has_pane_with_pid(&pane_id) {
- tab.clear_pane_frame_color_override(pane_id);
- break;
+ for pane_id in pane_ids {
+ for tab in all_tabs.values_mut() {
+ if tab.has_pane_with_pid(&pane_id) {
+ tab.clear_pane_frame_color_override(pane_id);
+ break;
+ }
}
}
screen.render()?;
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs
index a841b39ae..c2b26ecf8 100644
--- a/zellij-server/src/tab/mod.rs
+++ b/zellij-server/src/tab/mod.rs
@@ -1048,7 +1048,7 @@ impl Tab {
if let Some(active_pane_id) = self.tiled_panes.get_active_pane_id(client_id) {
self.senders
.send_to_background_jobs(BackgroundJob::DisplayPaneError(
- active_pane_id,
+ vec![active_pane_id],
"TOO SMALL!".into(),
))
.with_context(err_context)?;
@@ -1102,7 +1102,7 @@ impl Tab {
if let Some(active_pane_id) = self.tiled_panes.get_active_pane_id(client_id) {
self.senders
.send_to_background_jobs(BackgroundJob::DisplayPaneError(
- active_pane_id,
+ vec![active_pane_id],
"TOO SMALL!".into(),
))
.with_context(err_context)?;
@@ -1630,6 +1630,7 @@ impl Tab {
self.should_clear_display_before_rendering = true;
}
pub fn resize(&mut self, client_id: ClientId, strategy: ResizeStrategy) -> Result<()> {
+ let err_context = || format!("unable to resize pane");
if self.floating_panes.panes_are_visible() {
let successfully_resized = self
.floating_panes
@@ -1639,9 +1640,28 @@ impl Tab {
self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" in case of a decrease
}
} else {
- self.tiled_panes
- .resize_active_pane(client_id, &strategy)
- .unwrap();
+ match self.tiled_panes.resize_active_pane(client_id, &strategy) {
+ Ok(_) => {},
+ Err(err) => match err.downcast_ref::<ZellijError>() {
+ Some(ZellijError::CantResizeFixedPanes { pane_ids }) => {
+ let mut pane_ids_to_error = vec![];
+ for (id, is_terminal) in pane_ids {
+ if *is_terminal {
+ pane_ids_to_error.push(PaneId::Terminal(*id));
+ } else {
+ pane_ids_to_error.push(PaneId::Plugin(*id));
+ };
+ }
+ self.senders
+ .send_to_background_jobs(BackgroundJob::DisplayPaneError(
+ pane_ids_to_error,
+ "FIXED!".into(),
+ ))
+ .with_context(err_context)?;
+ },
+ _ => Err::<(), _>(err).fatal(),
+ },
+ }
}
Ok(())
}
diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs
index 192c55e3e..86848045a 100644
--- a/zellij-server/src/tab/unit/tab_tests.rs
+++ b/zellij-server/src/tab/unit/tab_tests.rs
@@ -231,15 +231,8 @@ fn create_new_tab_with_layout(size: Size, layout: PaneLayout) -> Tab {
for i in 0..layout.extract_run_instructions().len() {
new_terminal_ids.push((i as u32, None));
}
- tab.apply_layout(
- layout,
- // vec![(1, None), (2, None)],
- new_terminal_ids,
- HashMap::new(),
- index,
- client_id,
- )
- .unwrap();
+ tab.apply_layout(layout, new_terminal_ids, HashMap::new(), index, client_id)
+ .unwrap();
tab
}
@@ -5933,6 +5926,144 @@ pub fn cannot_resize_down_when_pane_below_is_at_minimum_height() {
}
#[test]
+pub fn cannot_resize_down_when_pane_has_fixed_rows() {
+ // ┌───────────┐ ┌───────────┐
+ // │███████████│ │███████████│
+ // ├───────────┤ ==resize=down==> ├───────────┤
+ // │ │ │ │
+ // └───────────┘ └───────────┘
+ // █ == focused pane
+
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+
+ let mut initial_layout = PaneLayout::default();
+ initial_layout.children_split_direction = SplitDirection::Horizontal;
+ let mut fixed_child = PaneLayout::default();
+ fixed_child.split_size = Some(SplitSize::Fixed(10));
+ initial_layout.children = vec![fixed_child, PaneLayout::default()];
+ let mut tab = create_new_tab_with_layout(size, initial_layout);
+ tab_resize_down(&mut tab, 1);
+
+ assert_eq!(
+ tab.tiled_panes
+ .panes
+ .get(&PaneId::Terminal(0))
+ .unwrap()
+ .position_and_size()
+ .rows
+ .as_usize(),
+ 10,
+ "pane 1 height stayed the same"
+ );
+ assert_eq!(
+ tab.tiled_panes
+ .panes
+ .get(&PaneId::Terminal(1))
+ .unwrap()
+ .position_and_size()
+ .rows
+ .as_usize(),
+ 10,
+ "pane 2 height stayed the same"
+ );
+}
+
+#[test]