aboutsummaryrefslogtreecommitdiff
path: root/helix-term/tests
diff options
context:
space:
mode:
authorBlaΕΎ Hrastnik2022-06-21 16:59:02 +0000
committerGitHub2022-06-21 16:59:02 +0000
commit19dccade7c44619bfa414a711fe72a612e4ca358 (patch)
treed0a502bc095c1619cfb94236782f42d595af0197 /helix-term/tests
parenta17626a822b36d4de3146c2d410f976e19dd189c (diff)
parent458b89e21dcf76bbf9ca6ba237bd334f4922722d (diff)
Merge pull request #2359 from dead10ck/test-harness
Integration testing harness
Diffstat (limited to 'helix-term/tests')
-rw-r--r--helix-term/tests/integration.rs25
-rw-r--r--helix-term/tests/test/auto_indent.rs26
-rw-r--r--helix-term/tests/test/auto_pairs.rs21
-rw-r--r--helix-term/tests/test/commands.rs93
-rw-r--r--helix-term/tests/test/helpers.rs213
-rw-r--r--helix-term/tests/test/movement.rs87
-rw-r--r--helix-term/tests/test/write.rs169
7 files changed, 634 insertions, 0 deletions
diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs
new file mode 100644
index 00000000..11bc4e4c
--- /dev/null
+++ b/helix-term/tests/integration.rs
@@ -0,0 +1,25 @@
+#[cfg(feature = "integration")]
+mod test {
+ mod helpers;
+
+ use std::path::PathBuf;
+
+ use helix_core::{syntax::AutoPairConfig, Position, Selection};
+ use helix_term::{args::Args, config::Config};
+
+ use indoc::indoc;
+
+ use self::helpers::*;
+
+ #[tokio::test]
+ async fn hello_world() -> anyhow::Result<()> {
+ test(("#[\n|]#", "ihello world<esc>", "hello world#[|\n]#")).await?;
+ Ok(())
+ }
+
+ mod auto_indent;
+ mod auto_pairs;
+ mod commands;
+ mod movement;
+ mod write;
+}
diff --git a/helix-term/tests/test/auto_indent.rs b/helix-term/tests/test/auto_indent.rs
new file mode 100644
index 00000000..2f638893
--- /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_with_config(
+ 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<ret><esc>",
+ 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..ec47a5b4
--- /dev/null
+++ b/helix-term/tests/test/auto_pairs.rs
@@ -0,0 +1,21 @@
+use super::*;
+
+#[tokio::test]
+async fn auto_pairs_basic() -> anyhow::Result<()> {
+ test(("#[\n|]#", "i(<esc>", "(#[|)]#\n")).await?;
+
+ test_with_config(
+ Args::default(),
+ Config {
+ editor: helix_view::editor::Config {
+ auto_pairs: AutoPairConfig::Enable(false),
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ ("#[\n|]#", "i(<esc>", "(#[|\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..0cd79bc7
--- /dev/null
+++ b/helix-term/tests/test/commands.rs
@@ -0,0 +1,93 @@
+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 helpers::app_with_file(file.path())?,
+ Some("ihello<esc>:wq<ret>"),
+ Some(&|app| {
+ assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
+ }),
+ false,
+ )
+ .await?;
+
+ Ok(())
+}
+
+#[tokio::test]
+#[ignore]
+async fn test_buffer_close_concurrent() -> 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<esc>:new<ret>"),
+ Some(&|app| {
+ assert_eq!(2, app.editor.documents().count());
+ assert!(!app.editor.is_err());
+ }),
+ ),
+ (
+ Some(":buffer<minus>close<ret>"),
+ Some(&|app| {
+ assert_eq!(1, app.editor.documents().count());
+ assert!(!app.editor.is_err());
+ }),
+ ),
+ ],
+ false,
+ )
+ .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<i32> = 1..=1000;
+
+ for i in RANGE {
+ let cmd = format!("%c{}<esc>:w<ret>", i);
+ command.push_str(&cmd);
+ }
+
+ command.push_str(":buffer<minus>close<ret>");
+
+ test_key_sequence(
+ &mut helpers::app_with_file(file.path())?,
+ 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);
+ }),
+ false,
+ )
+ .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..8f2501e6
--- /dev/null
+++ b/helix-term/tests/test/helpers.rs
@@ -0,0 +1,213 @@
+use std::{io::Write, path::PathBuf, 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<S: Into<String>> 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)>,
+ should_exit: bool,
+) -> anyhow::Result<()> {
+ test_key_sequences(app, vec![(in_keys, test_fn)], should_exit).await
+}
+
+#[allow(clippy::type_complexity)]
+pub async fn test_key_sequences(
+ app: &mut Application,
+ inputs: Vec<(Option<&str>, Option<&dyn Fn(&Application)>)>,
+ should_exit: bool,
+) -> anyhow::Result<()> {
+ const TIMEOUT: Duration = Duration::from_millis(500);
+ let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
+ let mut rx_stream = UnboundedReceiverStream::new(rx);
+ let num_inputs = inputs.len();
+
+ for (i, (in_keys, test_fn)) in inputs.into_iter().enumerate() {
+ 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))))?;
+ }
+ }
+
+ let app_exited = !app.event_loop_until_idle(&mut rx_stream).await;
+
+ // the app should not exit from any test until the last one
+ if i < num_inputs - 1 && app_exited {
+ bail!("application exited before test function could run");
+ }
+
+ // verify if it exited on the last iteration if it should have and
+ // the inverse
+ if i == num_inputs - 1 && app_exited != should_exit {
+ bail!("expected app to exit: {} != {}", app_exited, should_exit);
+ }
+
+ if let Some(test) = test_fn {
+ test(app);
+ };
+ }
+
+ if !should_exit {
+ for key_event in parse_macro("<esc>:q!<ret>")?.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<T: Into<TestCase>>(
+ app: Option<Application>,
+ test_case: T,
+ test_fn: &dyn Fn(&Application),
+ should_exit: bool,
+) -> 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),
+ should_exit,
+ )
+ .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_with_config<T: Into<TestCase>>(
+ 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);
+ },
+ false,
+ )
+ .await
+}
+
+pub async fn test<T: Into<TestCase>>(test_case: T) -> anyhow::Result<()> {
+ test_with_config(Args::default(), Config::default(), test_case).await
+}
+
+pub fn temp_file_with_contents<S: AsRef<str>>(
+ content: S,
+) -> anyhow::Result<tempfile::NamedTempFile> {
+ 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<NamedTempFile> {
+ 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)
+}
+
+/// Creates a new Application with default config that opens the given file
+/// path
+pub fn app_with_file<P: Into<PathBuf>>(path: P) -> anyhow::Result<Application> {
+ Application::new(
+ Args {
+ files: vec![(path.into(), helix_core::Position::default())],
+ ..Default::default()
+ },
+ Config::default(),
+ )
+}
diff --git a/helix-term/tests/test/movement.rs b/helix-term/tests/test/movement.rs
new file mode 100644
index 00000000..088685df
--- /dev/null
+++ b/helix-term/tests/test/movement.rs
@@ -0,0 +1,87 @@
+use super::*;
+
+#[tokio::test]
+async fn insert_mode_cursor_position() -> anyhow::Result<()> {
+ test(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(("#[\n|]#", "i", "#[|\n]#")).await?;
+ test(("#[\n|]#", "i<esc>", "#[|\n]#")).await?;
+ test(("#[\n|]#", "i<esc>i", "#[|\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(("#[f|]#oo\n", "vll<A-;><esc>", "#[|foo]#\n")).await?;
+ test((
+ indoc! {"\
+ #[f|]#oo
+ #(b|)#ar"
+ },
+ "vll<A-;><esc>",
+ indoc! {"\
+ #[|foo]#
+ #(|bar)#"
+ },
+ ))
+ .await?;
+
+ test((
+ indoc! {"\
+ #[f|]#oo
+ #(b|)#ar"
+ },
+ "a",
+ indoc! {"\
+ #[fo|]#o
+ #(ba|)#r"
+ },
+ ))
+ .await?;
+
+ test((
+ indoc! {"\
+ #[f|]#oo
+ #(b|)#ar"
+ },
+ "a<esc>",
+ 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 = helpers::app_with_file(file.path())?;
+
+ 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..8869d881
--- /dev/null
+++ b/helix-term/tests/test/write.rs
@@ -0,0 +1,169 @@
+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 helpers::app_with_file(file.path())?,
+ Some("ithe gostak distims the doshes<ret><esc>:w<ret>"),
+ None,
+ false,
+ )
+ .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("the gostak distims the doshes"),
+ file_content
+ );
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn test_write_quit() -> anyhow::Result<()> {
+ let mut file = tempfile::NamedTempFile::new()?;
+
+ test_key_sequence(
+ &mut helpers::app_with_file(file.path())?,
+ Some("ithe gostak distims the doshes<ret><esc>:wq<ret>"),
+ None,
+ true,
+ )
+ .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("the gostak distims the doshes"),
+ file_content
+ );
+
+ Ok(())
+}
+
+#[tokio::test]
+#[ignore]
+async fn test_write_concurrent() -> anyhow::Result<()> {
+ let mut file = tempfile::NamedTempFile::new()?;
+ let mut command = String::new();
+ const RANGE: RangeInclusive<i32> = 1..=5000;
+
+ for i in RANGE {
+ let cmd = format!("%c{}<esc>:w<ret>", i);
+ command.push_str(&cmd);
+ }
+
+ test_key_sequence(
+ &mut helpers::app_with_file(file.path())?,
+ Some(&command),
+ None,
+ false,
+ )
+ .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]
+#[ignore]
+async fn test_write_fail_mod_flag() -> anyhow::Result<()> {
+ let file = helpers::new_readonly_tempfile()?;
+
+ test_key_sequences(
+ &mut helpers::app_with_file(file.path())?,
+ vec![
+ (
+ None,
+ Some(&|app| {
+ let doc = doc!(app.editor);
+ assert!(!doc.is_modified());
+ }),
+ ),
+ (
+ Some("ihello<esc>"),
+ Some(&|app| {
+ let doc = doc!(app.editor);
+ assert!(doc.is_modified());
+ }),
+ ),
+ (
+ Some(":w<ret>"),
+ Some(&|app| {
+ assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
+
+ let doc = doc!(app.editor);
+ assert!(doc.is_modified());
+ }),
+ ),
+ ],
+ false,
+ )
+ .await?;
+
+ Ok(())
+}
+
+#[tokio::test]
+#[ignore]
+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 {}<ret>", 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());
+ }),
+ ),
+ ],
+ false,
+ )
+ .await?;
+
+ Ok(())
+}