From 8d8d389536d1f948f25a38c33f278a5e2f8d1b28 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sat, 30 Apr 2022 09:22:20 -0400 Subject: rename top level module to satisfy cargo fmt --- helix-term/tests/integration.rs | 2 +- helix-term/tests/integration/auto_indent.rs | 26 ----- helix-term/tests/integration/auto_pairs.rs | 26 ----- helix-term/tests/integration/commands.rs | 101 ----------------- helix-term/tests/integration/helpers.rs | 169 ---------------------------- helix-term/tests/integration/movement.rs | 135 ---------------------- helix-term/tests/integration/write.rs | 153 ------------------------- helix-term/tests/test/auto_indent.rs | 26 +++++ helix-term/tests/test/auto_pairs.rs | 26 +++++ helix-term/tests/test/commands.rs | 101 +++++++++++++++++ helix-term/tests/test/helpers.rs | 169 ++++++++++++++++++++++++++++ helix-term/tests/test/movement.rs | 135 ++++++++++++++++++++++ helix-term/tests/test/write.rs | 153 +++++++++++++++++++++++++ 13 files changed, 611 insertions(+), 611 deletions(-) delete mode 100644 helix-term/tests/integration/auto_indent.rs delete mode 100644 helix-term/tests/integration/auto_pairs.rs delete mode 100644 helix-term/tests/integration/commands.rs delete mode 100644 helix-term/tests/integration/helpers.rs delete mode 100644 helix-term/tests/integration/movement.rs delete mode 100644 helix-term/tests/integration/write.rs create mode 100644 helix-term/tests/test/auto_indent.rs create mode 100644 helix-term/tests/test/auto_pairs.rs create mode 100644 helix-term/tests/test/commands.rs create mode 100644 helix-term/tests/test/helpers.rs create mode 100644 helix-term/tests/test/movement.rs create mode 100644 helix-term/tests/test/write.rs (limited to 'helix-term/tests') diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs index 061bd8ff..376bc88b 100644 --- a/helix-term/tests/integration.rs +++ b/helix-term/tests/integration.rs @@ -1,5 +1,5 @@ #[cfg(feature = "integration")] -mod integration { +mod test { mod helpers; use std::path::PathBuf; diff --git a/helix-term/tests/integration/auto_indent.rs b/helix-term/tests/integration/auto_indent.rs deleted file mode 100644 index 8933cb6a..00000000 --- a/helix-term/tests/integration/auto_indent.rs +++ /dev/null @@ -1,26 +0,0 @@ -use super::*; - -#[tokio::test] -async fn auto_indent_c() -> anyhow::Result<()> { - test_key_sequence_text_result( - Args { - files: vec![(PathBuf::from("foo.c"), Position::default())], - ..Default::default() - }, - Config::default(), - // switches to append mode? - ( - helpers::platform_line("void foo() {#[|}]#").as_ref(), - "i", - helpers::platform_line(indoc! {"\ - void foo() { - #[|\n]#\ - } - "}) - .as_ref(), - ), - ) - .await?; - - Ok(()) -} diff --git a/helix-term/tests/integration/auto_pairs.rs b/helix-term/tests/integration/auto_pairs.rs deleted file mode 100644 index 52fee55e..00000000 --- a/helix-term/tests/integration/auto_pairs.rs +++ /dev/null @@ -1,26 +0,0 @@ -use super::*; - -#[tokio::test] -async fn auto_pairs_basic() -> anyhow::Result<()> { - test_key_sequence_text_result( - Args::default(), - Config::default(), - ("#[\n|]#", "i(", "(#[|)]#\n"), - ) - .await?; - - test_key_sequence_text_result( - Args::default(), - Config { - editor: helix_view::editor::Config { - auto_pairs: AutoPairConfig::Enable(false), - ..Default::default() - }, - ..Default::default() - }, - ("#[\n|]#", "i(", "(#[|\n]#"), - ) - .await?; - - Ok(()) -} diff --git a/helix-term/tests/integration/commands.rs b/helix-term/tests/integration/commands.rs deleted file mode 100644 index 1ff7cc90..00000000 --- a/helix-term/tests/integration/commands.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::{ - io::{Read, Write}, - ops::RangeInclusive, -}; - -use helix_core::diagnostic::Severity; -use helix_term::application::Application; - -use super::*; - -#[tokio::test] -async fn test_write_quit_fail() -> anyhow::Result<()> { - let file = helpers::new_readonly_tempfile()?; - - test_key_sequence( - &mut Application::new( - Args { - files: vec![(file.path().to_path_buf(), Position::default())], - ..Default::default() - }, - Config::default(), - )?, - Some("ihello:wq"), - Some(&|app| { - assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); - }), - ) - .await?; - - Ok(()) -} - -#[tokio::test] -async fn test_buffer_close() -> anyhow::Result<()> { - test_key_sequences( - &mut Application::new(Args::default(), Config::default())?, - vec![ - ( - None, - Some(&|app| { - assert_eq!(1, app.editor.documents().count()); - assert!(!app.editor.is_err()); - }), - ), - ( - Some("ihello:new"), - Some(&|app| { - assert_eq!(2, app.editor.documents().count()); - assert!(!app.editor.is_err()); - }), - ), - ( - Some(":bufferclose"), - Some(&|app| { - assert_eq!(1, app.editor.documents().count()); - assert!(!app.editor.is_err()); - }), - ), - ], - ) - .await?; - - // verify if writes are queued up, it finishes them before closing the buffer - let mut file = tempfile::NamedTempFile::new()?; - let mut command = String::new(); - const RANGE: RangeInclusive = 1..=10; - - for i in RANGE { - let cmd = format!("%c{}:w", i); - command.push_str(&cmd); - } - - command.push_str(":bufferclose"); - - test_key_sequence( - &mut Application::new( - Args { - files: vec![(file.path().to_path_buf(), Position::default())], - ..Default::default() - }, - Config::default(), - )?, - Some(&command), - Some(&|app| { - assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status()); - - let doc = app.editor.document_by_path(file.path()); - assert!(doc.is_none(), "found doc: {:?}", doc); - }), - ) - .await?; - - file.as_file_mut().flush()?; - file.as_file_mut().sync_all()?; - - let mut file_content = String::new(); - file.as_file_mut().read_to_string(&mut file_content)?; - assert_eq!(RANGE.end().to_string(), file_content); - - Ok(()) -} diff --git a/helix-term/tests/integration/helpers.rs b/helix-term/tests/integration/helpers.rs deleted file mode 100644 index 3fe1934f..00000000 --- a/helix-term/tests/integration/helpers.rs +++ /dev/null @@ -1,169 +0,0 @@ -use std::{io::Write, time::Duration}; - -use anyhow::bail; -use crossterm::event::{Event, KeyEvent}; -use helix_core::{test, Selection, Transaction}; -use helix_term::{application::Application, args::Args, config::Config}; -use helix_view::{doc, input::parse_macro}; -use tempfile::NamedTempFile; -use tokio_stream::wrappers::UnboundedReceiverStream; - -#[derive(Clone, Debug)] -pub struct TestCase { - pub in_text: String, - pub in_selection: Selection, - pub in_keys: String, - pub out_text: String, - pub out_selection: Selection, -} - -impl> From<(S, S, S)> for TestCase { - fn from((input, keys, output): (S, S, S)) -> Self { - let (in_text, in_selection) = test::print(&input.into()); - let (out_text, out_selection) = test::print(&output.into()); - - TestCase { - in_text, - in_selection, - in_keys: keys.into(), - out_text, - out_selection, - } - } -} - -#[inline] -pub async fn test_key_sequence( - app: &mut Application, - in_keys: Option<&str>, - test_fn: Option<&dyn Fn(&Application)>, -) -> anyhow::Result<()> { - test_key_sequences(app, vec![(in_keys, test_fn)]).await -} - -pub async fn test_key_sequences( - app: &mut Application, - inputs: Vec<(Option<&str>, Option<&dyn Fn(&Application)>)>, -) -> anyhow::Result<()> { - const TIMEOUT: Duration = Duration::from_millis(500); - let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); - let mut rx_stream = UnboundedReceiverStream::new(rx); - - for (in_keys, test_fn) in inputs { - if let Some(in_keys) = in_keys { - for key_event in parse_macro(&in_keys)?.into_iter() { - tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?; - } - } - - if !app.event_loop_until_idle(&mut rx_stream).await { - bail!("application exited before test function could run"); - } - - if let Some(test) = test_fn { - test(app); - }; - } - - for key_event in parse_macro(":q!")?.into_iter() { - tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?; - } - - let event_loop = app.event_loop(&mut rx_stream); - tokio::time::timeout(TIMEOUT, event_loop).await?; - app.close().await?; - - Ok(()) -} - -pub async fn test_key_sequence_with_input_text>( - app: Option, - test_case: T, - test_fn: &dyn Fn(&Application), -) -> anyhow::Result<()> { - let test_case = test_case.into(); - let mut app = match app { - Some(app) => app, - None => Application::new(Args::default(), Config::default())?, - }; - - let (view, doc) = helix_view::current!(app.editor); - let sel = doc.selection(view.id).clone(); - - // replace the initial text with the input text - doc.apply( - &Transaction::change_by_selection(&doc.text(), &sel, |_| { - (0, doc.text().len_chars(), Some((&test_case.in_text).into())) - }) - .with_selection(test_case.in_selection.clone()), - view.id, - ); - - test_key_sequence(&mut app, Some(&test_case.in_keys), Some(test_fn)).await -} - -/// Use this for very simple test cases where there is one input -/// document, selection, and sequence of key presses, and you just -/// want to verify the resulting document and selection. -pub async fn test_key_sequence_text_result>( - args: Args, - config: Config, - test_case: T, -) -> anyhow::Result<()> { - let test_case = test_case.into(); - let app = Application::new(args, config)?; - - test_key_sequence_with_input_text(Some(app), test_case.clone(), &|app| { - let doc = doc!(app.editor); - assert_eq!(&test_case.out_text, doc.text()); - - let mut selections: Vec<_> = doc.selections().values().cloned().collect(); - assert_eq!(1, selections.len()); - - let sel = selections.pop().unwrap(); - assert_eq!(test_case.out_selection, sel); - }) - .await -} - -pub fn temp_file_with_contents>( - content: S, -) -> anyhow::Result { - let mut temp_file = tempfile::NamedTempFile::new()?; - - temp_file - .as_file_mut() - .write_all(content.as_ref().as_bytes())?; - - temp_file.flush()?; - temp_file.as_file_mut().sync_all()?; - Ok(temp_file) -} - -/// Replaces all LF chars with the system's appropriate line feed -/// character, and if one doesn't exist already, appends the system's -/// appropriate line ending to the end of a string. -pub fn platform_line(input: &str) -> String { - let line_end = helix_core::DEFAULT_LINE_ENDING.as_str(); - - // we can assume that the source files in this code base will always - // be LF, so indoc strings will always insert LF - let mut output = input.replace("\n", line_end); - - if !output.ends_with(line_end) { - output.push_str(line_end); - } - - output -} - -/// Creates a new temporary file that is set to read only. Useful for -/// testing write failures. -pub fn new_readonly_tempfile() -> anyhow::Result { - let mut file = tempfile::NamedTempFile::new()?; - let metadata = file.as_file().metadata()?; - let mut perms = metadata.permissions(); - perms.set_readonly(true); - file.as_file_mut().set_permissions(perms)?; - Ok(file) -} diff --git a/helix-term/tests/integration/movement.rs b/helix-term/tests/integration/movement.rs deleted file mode 100644 index e0bfc3bf..00000000 --- a/helix-term/tests/integration/movement.rs +++ /dev/null @@ -1,135 +0,0 @@ -use helix_term::application::Application; - -use super::*; - -#[tokio::test] -async fn insert_mode_cursor_position() -> anyhow::Result<()> { - test_key_sequence_text_result( - Args::default(), - Config::default(), - TestCase { - in_text: String::new(), - in_selection: Selection::single(0, 0), - in_keys: "i".into(), - out_text: String::new(), - out_selection: Selection::single(0, 0), - }, - ) - .await?; - - test_key_sequence_text_result( - Args::default(), - Config::default(), - ("#[\n|]#", "i", "#[|\n]#"), - ) - .await?; - - test_key_sequence_text_result( - Args::default(), - Config::default(), - ("#[\n|]#", "i", "#[|\n]#"), - ) - .await?; - - test_key_sequence_text_result( - Args::default(), - Config::default(), - ("#[\n|]#", "ii", "#[|\n]#"), - ) - .await?; - - Ok(()) -} - -/// Range direction is preserved when escaping insert mode to normal -#[tokio::test] -async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { - test_key_sequence_text_result( - Args::default(), - Config::default(), - ("#[f|]#oo\n", "vll", "#[|foo]#\n"), - ) - .await?; - - test_key_sequence_text_result( - Args::default(), - Config::default(), - ( - indoc! {"\ - #[f|]#oo - #(b|)#ar" - }, - "vll", - indoc! {"\ - #[|foo]# - #(|bar)#" - }, - ), - ) - .await?; - - test_key_sequence_text_result( - Args::default(), - Config::default(), - ( - indoc! {"\ - #[f|]#oo - #(b|)#ar" - }, - "a", - indoc! {"\ - #[fo|]#o - #(ba|)#r" - }, - ), - ) - .await?; - - test_key_sequence_text_result( - Args::default(), - Config::default(), - ( - indoc! {"\ - #[f|]#oo - #(b|)#ar" - }, - "a", - indoc! {"\ - #[f|]#oo - #(b|)#ar" - }, - ), - ) - .await?; - - Ok(()) -} - -/// Ensure the very initial cursor in an opened file is the width of -/// the first grapheme -#[tokio::test] -async fn cursor_position_newly_opened_file() -> anyhow::Result<()> { - let test = |content: &str, expected_sel: Selection| -> anyhow::Result<()> { - let file = helpers::temp_file_with_contents(content)?; - - let mut app = Application::new( - Args { - files: vec![(file.path().to_path_buf(), Position::default())], - ..Default::default() - }, - Config::default(), - )?; - - let (view, doc) = helix_view::current!(app.editor); - let sel = doc.selection(view.id).clone(); - assert_eq!(expected_sel, sel); - - Ok(()) - }; - - test("foo", Selection::single(0, 1))?; - test("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ foo", Selection::single(0, 7))?; - test("", Selection::single(0, 0))?; - - Ok(()) -} diff --git a/helix-term/tests/integration/write.rs b/helix-term/tests/integration/write.rs deleted file mode 100644 index f3abbd91..00000000 --- a/helix-term/tests/integration/write.rs +++ /dev/null @@ -1,153 +0,0 @@ -use std::{ - io::{Read, Write}, - ops::RangeInclusive, -}; - -use helix_core::diagnostic::Severity; -use helix_term::application::Application; -use helix_view::doc; - -use super::*; - -#[tokio::test] -async fn test_write() -> anyhow::Result<()> { - let mut file = tempfile::NamedTempFile::new()?; - - test_key_sequence( - &mut Application::new( - Args { - files: vec![(file.path().to_path_buf(), Position::default())], - ..Default::default() - }, - Config::default(), - )?, - Some("ii can eat glass, it will not hurt me:w"), - None, - ) - .await?; - - file.as_file_mut().flush()?; - file.as_file_mut().sync_all()?; - - let mut file_content = String::new(); - file.as_file_mut().read_to_string(&mut file_content)?; - assert_eq!( - helpers::platform_line("i can eat glass, it will not hurt me"), - file_content - ); - - Ok(()) -} - -#[tokio::test] -async fn test_write_concurrent() -> anyhow::Result<()> { - let mut file = tempfile::NamedTempFile::new()?; - let mut command = String::new(); - const RANGE: RangeInclusive = 1..=5000; - - for i in RANGE { - let cmd = format!("%c{}:w", i); - command.push_str(&cmd); - } - - test_key_sequence( - &mut Application::new( - Args { - files: vec![(file.path().to_path_buf(), Position::default())], - ..Default::default() - }, - Config::default(), - )?, - Some(&command), - None, - ) - .await?; - - file.as_file_mut().flush()?; - file.as_file_mut().sync_all()?; - - let mut file_content = String::new(); - file.as_file_mut().read_to_string(&mut file_content)?; - assert_eq!(RANGE.end().to_string(), file_content); - - Ok(()) -} - -#[tokio::test] -async fn test_write_fail_mod_flag() -> anyhow::Result<()> { - let file = helpers::new_readonly_tempfile()?; - - test_key_sequences( - &mut Application::new( - Args { - files: vec![(file.path().to_path_buf(), Position::default())], - ..Default::default() - }, - Config::default(), - )?, - vec![ - ( - None, - Some(&|app| { - let doc = doc!(app.editor); - assert!(!doc.is_modified()); - }), - ), - ( - Some("ihello"), - Some(&|app| { - let doc = doc!(app.editor); - assert!(doc.is_modified()); - }), - ), - ( - Some(":w"), - Some(&|app| { - assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); - - let doc = doc!(app.editor); - assert!(doc.is_modified()); - }), - ), - ], - ) - .await?; - - Ok(()) -} - -#[tokio::test] -async fn test_write_fail_new_path() -> anyhow::Result<()> { - let file = helpers::new_readonly_tempfile()?; - - test_key_sequences( - &mut Application::new(Args::default(), Config::default())?, - vec![ - ( - None, - Some(&|app| { - let doc = doc!(app.editor); - assert_ne!( - Some(&Severity::Error), - app.editor.get_status().map(|status| status.1) - ); - assert_eq!(None, doc.path()); - }), - ), - ( - Some(&format!(":w {}", file.path().to_string_lossy())), - Some(&|app| { - let doc = doc!(app.editor); - assert_eq!( - Some(&Severity::Error), - app.editor.get_status().map(|status| status.1) - ); - assert_eq!(None, doc.path()); - }), - ), - ], - ) - .await?; - - Ok(()) -} diff --git a/helix-term/tests/test/auto_indent.rs b/helix-term/tests/test/auto_indent.rs new file mode 100644 index 00000000..8933cb6a --- /dev/null +++ b/helix-term/tests/test/auto_indent.rs @@ -0,0 +1,26 @@ +use super::*; + +#[tokio::test] +async fn auto_indent_c() -> anyhow::Result<()> { + test_key_sequence_text_result( + Args { + files: vec![(PathBuf::from("foo.c"), Position::default())], + ..Default::default() + }, + Config::default(), + // switches to append mode? + ( + helpers::platform_line("void foo() {#[|}]#").as_ref(), + "i", + helpers::platform_line(indoc! {"\ + void foo() { + #[|\n]#\ + } + "}) + .as_ref(), + ), + ) + .await?; + + Ok(()) +} diff --git a/helix-term/tests/test/auto_pairs.rs b/helix-term/tests/test/auto_pairs.rs new file mode 100644 index 00000000..52fee55e --- /dev/null +++ b/helix-term/tests/test/auto_pairs.rs @@ -0,0 +1,26 @@ +use super::*; + +#[tokio::test] +async fn auto_pairs_basic() -> anyhow::Result<()> { + test_key_sequence_text_result( + Args::default(), + Config::default(), + ("#[\n|]#", "i(", "(#[|)]#\n"), + ) + .await?; + + test_key_sequence_text_result( + Args::default(), + Config { + editor: helix_view::editor::Config { + auto_pairs: AutoPairConfig::Enable(false), + ..Default::default() + }, + ..Default::default() + }, + ("#[\n|]#", "i(", "(#[|\n]#"), + ) + .await?; + + Ok(()) +} diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs new file mode 100644 index 00000000..1ff7cc90 --- /dev/null +++ b/helix-term/tests/test/commands.rs @@ -0,0 +1,101 @@ +use std::{ + io::{Read, Write}, + ops::RangeInclusive, +}; + +use helix_core::diagnostic::Severity; +use helix_term::application::Application; + +use super::*; + +#[tokio::test] +async fn test_write_quit_fail() -> anyhow::Result<()> { + let file = helpers::new_readonly_tempfile()?; + + test_key_sequence( + &mut Application::new( + Args { + files: vec![(file.path().to_path_buf(), Position::default())], + ..Default::default() + }, + Config::default(), + )?, + Some("ihello:wq"), + Some(&|app| { + assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); + }), + ) + .await?; + + Ok(()) +} + +#[tokio::test] +async fn test_buffer_close() -> anyhow::Result<()> { + test_key_sequences( + &mut Application::new(Args::default(), Config::default())?, + vec![ + ( + None, + Some(&|app| { + assert_eq!(1, app.editor.documents().count()); + assert!(!app.editor.is_err()); + }), + ), + ( + Some("ihello:new"), + Some(&|app| { + assert_eq!(2, app.editor.documents().count()); + assert!(!app.editor.is_err()); + }), + ), + ( + Some(":bufferclose"), + Some(&|app| { + assert_eq!(1, app.editor.documents().count()); + assert!(!app.editor.is_err()); + }), + ), + ], + ) + .await?; + + // verify if writes are queued up, it finishes them before closing the buffer + let mut file = tempfile::NamedTempFile::new()?; + let mut command = String::new(); + const RANGE: RangeInclusive = 1..=10; + + for i in RANGE { + let cmd = format!("%c{}:w", i); + command.push_str(&cmd); + } + + command.push_str(":bufferclose"); + + test_key_sequence( + &mut Application::new( + Args { + files: vec![(file.path().to_path_buf(), Position::default())], + ..Default::default() + }, + Config::default(), + )?, + Some(&command), + Some(&|app| { + assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status()); + + let doc = app.editor.document_by_path(file.path()); + assert!(doc.is_none(), "found doc: {:?}", doc); + }), + ) + .await?; + + file.as_file_mut().flush()?; + file.as_file_mut().sync_all()?; + + let mut file_content = String::new(); + file.as_file_mut().read_to_string(&mut file_content)?; + assert_eq!(RANGE.end().to_string(), file_content); + + Ok(()) +} diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs new file mode 100644 index 00000000..3fe1934f --- /dev/null +++ b/helix-term/tests/test/helpers.rs @@ -0,0 +1,169 @@ +use std::{io::Write, time::Duration}; + +use anyhow::bail; +use crossterm::event::{Event, KeyEvent}; +use helix_core::{test, Selection, Transaction}; +use helix_term::{application::Application, args::Args, config::Config}; +use helix_view::{doc, input::parse_macro}; +use tempfile::NamedTempFile; +use tokio_stream::wrappers::UnboundedReceiverStream; + +#[derive(Clone, Debug)] +pub struct TestCase { + pub in_text: String, + pub in_selection: Selection, + pub in_keys: String, + pub out_text: String, + pub out_selection: Selection, +} + +impl> From<(S, S, S)> for TestCase { + fn from((input, keys, output): (S, S, S)) -> Self { + let (in_text, in_selection) = test::print(&input.into()); + let (out_text, out_selection) = test::print(&output.into()); + + TestCase { + in_text, + in_selection, + in_keys: keys.into(), + out_text, + out_selection, + } + } +} + +#[inline] +pub async fn test_key_sequence( + app: &mut Application, + in_keys: Option<&str>, + test_fn: Option<&dyn Fn(&Application)>, +) -> anyhow::Result<()> { + test_key_sequences(app, vec![(in_keys, test_fn)]).await +} + +pub async fn test_key_sequences( + app: &mut Application, + inputs: Vec<(Option<&str>, Option<&dyn Fn(&Application)>)>, +) -> anyhow::Result<()> { + const TIMEOUT: Duration = Duration::from_millis(500); + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + let mut rx_stream = UnboundedReceiverStream::new(rx); + + for (in_keys, test_fn) in inputs { + if let Some(in_keys) = in_keys { + for key_event in parse_macro(&in_keys)?.into_iter() { + tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?; + } + } + + if !app.event_loop_until_idle(&mut rx_stream).await { + bail!("application exited before test function could run"); + } + + if let Some(test) = test_fn { + test(app); + }; + } + + for key_event in parse_macro(":q!")?.into_iter() { + tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?; + } + + let event_loop = app.event_loop(&mut rx_stream); + tokio::time::timeout(TIMEOUT, event_loop).await?; + app.close().await?; + + Ok(()) +} + +pub async fn test_key_sequence_with_input_text>( + app: Option, + test_case: T, + test_fn: &dyn Fn(&Application), +) -> anyhow::Result<()> { + let test_case = test_case.into(); + let mut app = match app { + Some(app) => app, + None => Application::new(Args::default(), Config::default())?, + }; + + let (view, doc) = helix_view::current!(app.editor); + let sel = doc.selection(view.id).clone(); + + // replace the initial text with the input text + doc.apply( + &Transaction::change_by_selection(&doc.text(), &sel, |_| { + (0, doc.text().len_chars(), Some((&test_case.in_text).into())) + }) + .with_selection(test_case.in_selection.clone()), + view.id, + ); + + test_key_sequence(&mut app, Some(&test_case.in_keys), Some(test_fn)).await +} + +/// Use this for very simple test cases where there is one input +/// document, selection, and sequence of key presses, and you just +/// want to verify the resulting document and selection. +pub async fn test_key_sequence_text_result>( + args: Args, + config: Config, + test_case: T, +) -> anyhow::Result<()> { + let test_case = test_case.into(); + let app = Application::new(args, config)?; + + test_key_sequence_with_input_text(Some(app), test_case.clone(), &|app| { + let doc = doc!(app.editor); + assert_eq!(&test_case.out_text, doc.text()); + + let mut selections: Vec<_> = doc.selections().values().cloned().collect(); + assert_eq!(1, selections.len()); + + let sel = selections.pop().unwrap(); + assert_eq!(test_case.out_selection, sel); + }) + .await +} + +pub fn temp_file_with_contents>( + content: S, +) -> anyhow::Result { + let mut temp_file = tempfile::NamedTempFile::new()?; + + temp_file + .as_file_mut() + .write_all(content.as_ref().as_bytes())?; + + temp_file.flush()?; + temp_file.as_file_mut().sync_all()?; + Ok(temp_file) +} + +/// Replaces all LF chars with the system's appropriate line feed +/// character, and if one doesn't exist already, appends the system's +/// appropriate line ending to the end of a string. +pub fn platform_line(input: &str) -> String { + let line_end = helix_core::DEFAULT_LINE_ENDING.as_str(); + + // we can assume that the source files in this code base will always + // be LF, so indoc strings will always insert LF + let mut output = input.replace("\n", line_end); + + if !output.ends_with(line_end) { + output.push_str(line_end); + } + + output +} + +/// Creates a new temporary file that is set to read only. Useful for +/// testing write failures. +pub fn new_readonly_tempfile() -> anyhow::Result { + let mut file = tempfile::NamedTempFile::new()?; + let metadata = file.as_file().metadata()?; + let mut perms = metadata.permissions(); + perms.set_readonly(true); + file.as_file_mut().set_permissions(perms)?; + Ok(file) +} diff --git a/helix-term/tests/test/movement.rs b/helix-term/tests/test/movement.rs new file mode 100644 index 00000000..e0bfc3bf --- /dev/null +++ b/helix-term/tests/test/movement.rs @@ -0,0 +1,135 @@ +use helix_term::application::Application; + +use super::*; + +#[tokio::test] +async fn insert_mode_cursor_position() -> anyhow::Result<()> { + test_key_sequence_text_result( + Args::default(), + Config::default(), + TestCase { + in_text: String::new(), + in_selection: Selection::single(0, 0), + in_keys: "i".into(), + out_text: String::new(), + out_selection: Selection::single(0, 0), + }, + ) + .await?; + + test_key_sequence_text_result( + Args::default(), + Config::default(), + ("#[\n|]#", "i", "#[|\n]#"), + ) + .await?; + + test_key_sequence_text_result( + Args::default(), + Config::default(), + ("#[\n|]#", "i", "#[|\n]#"), + ) + .await?; + + test_key_sequence_text_result( + Args::default(), + Config::default(), + ("#[\n|]#", "ii", "#[|\n]#"), + ) + .await?; + + Ok(()) +} + +/// Range direction is preserved when escaping insert mode to normal +#[tokio::test] +async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { + test_key_sequence_text_result( + Args::default(), + Config::default(), + ("#[f|]#oo\n", "vll", "#[|foo]#\n"), + ) + .await?; + + test_key_sequence_text_result( + Args::default(), + Config::default(), + ( + indoc! {"\ + #[f|]#oo + #(b|)#ar" + }, + "vll", + indoc! {"\ + #[|foo]# + #(|bar)#" + }, + ), + ) + .await?; + + test_key_sequence_text_result( + Args::default(), + Config::default(), + ( + indoc! {"\ + #[f|]#oo + #(b|)#ar" + }, + "a", + indoc! {"\ + #[fo|]#o + #(ba|)#r" + }, + ), + ) + .await?; + + test_key_sequence_text_result( + Args::default(), + Config::default(), + ( + indoc! {"\ + #[f|]#oo + #(b|)#ar" + }, + "a", + indoc! {"\ + #[f|]#oo + #(b|)#ar" + }, + ), + ) + .await?; + + Ok(()) +} + +/// Ensure the very initial cursor in an opened file is the width of +/// the first grapheme +#[tokio::test] +async fn cursor_position_newly_opened_file() -> anyhow::Result<()> { + let test = |content: &str, expected_sel: Selection| -> anyhow::Result<()> { + let file = helpers::temp_file_with_contents(content)?; + + let mut app = Application::new( + Args { + files: vec![(file.path().to_path_buf(), Position::default())], + ..Default::default() + }, + Config::default(), + )?; + + let (view, doc) = helix_view::current!(app.editor); + let sel = doc.selection(view.id).clone(); + assert_eq!(expected_sel, sel); + + Ok(()) + }; + + test("foo", Selection::single(0, 1))?; + test("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ foo", Selection::single(0, 7))?; + test("", Selection::single(0, 0))?; + + Ok(()) +} diff --git a/helix-term/tests/test/write.rs b/helix-term/tests/test/write.rs new file mode 100644 index 00000000..f3abbd91 --- /dev/null +++ b/helix-term/tests/test/write.rs @@ -0,0 +1,153 @@ +use std::{ + io::{Read, Write}, + ops::RangeInclusive, +}; + +use helix_core::diagnostic::Severity; +use helix_term::application::Application; +use helix_view::doc; + +use super::*; + +#[tokio::test] +async fn test_write() -> anyhow::Result<()> { + let mut file = tempfile::NamedTempFile::new()?; + + test_key_sequence( + &mut Application::new( + Args { + files: vec![(file.path().to_path_buf(), Position::default())], + ..Default::default() + }, + Config::default(), + )?, + Some("ii can eat glass, it will not hurt me:w"), + None, + ) + .await?; + + file.as_file_mut().flush()?; + file.as_file_mut().sync_all()?; + + let mut file_content = String::new(); + file.as_file_mut().read_to_string(&mut file_content)?; + assert_eq!( + helpers::platform_line("i can eat glass, it will not hurt me"), + file_content + ); + + Ok(()) +} + +#[tokio::test] +async fn test_write_concurrent() -> anyhow::Result<()> { + let mut file = tempfile::NamedTempFile::new()?; + let mut command = String::new(); + const RANGE: RangeInclusive = 1..=5000; + + for i in RANGE { + let cmd = format!("%c{}:w", i); + command.push_str(&cmd); + } + + test_key_sequence( + &mut Application::new( + Args { + files: vec![(file.path().to_path_buf(), Position::default())], + ..Default::default() + }, + Config::default(), + )?, + Some(&command), + None, + ) + .await?; + + file.as_file_mut().flush()?; + file.as_file_mut().sync_all()?; + + let mut file_content = String::new(); + file.as_file_mut().read_to_string(&mut file_content)?; + assert_eq!(RANGE.end().to_string(), file_content); + + Ok(()) +} + +#[tokio::test] +async fn test_write_fail_mod_flag() -> anyhow::Result<()> { + let file = helpers::new_readonly_tempfile()?; + + test_key_sequences( + &mut Application::new( + Args { + files: vec![(file.path().to_path_buf(), Position::default())], + ..Default::default() + }, + Config::default(), + )?, + vec![ + ( + None, + Some(&|app| { + let doc = doc!(app.editor); + assert!(!doc.is_modified()); + }), + ), + ( + Some("ihello"), + Some(&|app| { + let doc = doc!(app.editor); + assert!(doc.is_modified()); + }), + ), + ( + Some(":w"), + Some(&|app| { + assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); + + let doc = doc!(app.editor); + assert!(doc.is_modified()); + }), + ), + ], + ) + .await?; + + Ok(()) +} + +#[tokio::test] +async fn test_write_fail_new_path() -> anyhow::Result<()> { + let file = helpers::new_readonly_tempfile()?; + + test_key_sequences( + &mut Application::new(Args::default(), Config::default())?, + vec![ + ( + None, + Some(&|app| { + let doc = doc!(app.editor); + assert_ne!( + Some(&Severity::Error), + app.editor.get_status().map(|status| status.1) + ); + assert_eq!(None, doc.path()); + }), + ), + ( + Some(&format!(":w {}", file.path().to_string_lossy())), + Some(&|app| { + let doc = doc!(app.editor); + assert_eq!( + Some(&Severity::Error), + app.editor.get_status().map(|status| status.1) + ); + assert_eq!(None, doc.path()); + }), + ), + ], + ) + .await?; + + Ok(()) +} -- cgit v1.2.3-70-g09d2