on master help sign in
tests for serialized writes
07fc80aSkyler Hawthorne
helix-term/tests/integration.rs+2
@@ -17,6 +17,7 @@ mod integration {
17 Args::default(),
17 Args::default(),
18 Config::default(),
18 Config::default(),
19 ("#[\n|]#", "ihello world<esc>", "hello world#[|\n]#"),
19 ("#[\n|]#", "ihello world<esc>", "hello world#[|\n]#"),
20+ None,
20 )
21 )
21 .await?;
22 .await?;
22
23
@@ -25,6 +26,7 @@ mod integration {
25
26
26 mod auto_indent;
27 mod auto_indent;
27 mod auto_pairs;
28 mod auto_pairs;
29+ mod commands;
28 mod movement;
30 mod movement;
29 mod write;
31 mod write;
30}
32}
helix-term/tests/integration/auto_indent.rs+1
@@ -18,6 +18,7 @@ async fn auto_indent_c() -> anyhow::Result<()> {
18 }
18 }
19 "},
19 "},
20 ),
20 ),
21+ None,
21 )
22 )
22 .await?;
23 .await?;
23
24
helix-term/tests/integration/auto_pairs.rs+2
@@ -6,6 +6,7 @@ async fn auto_pairs_basic() -> anyhow::Result<()> {
6 Args::default(),
6 Args::default(),
7 Config::default(),
7 Config::default(),
8 ("#[\n|]#", "i(<esc>", "(#[|)]#\n"),
8 ("#[\n|]#", "i(<esc>", "(#[|)]#\n"),
9+ None,
9 )
10 )
10 .await?;
11 .await?;
11
12
@@ -19,6 +20,7 @@ async fn auto_pairs_basic() -> anyhow::Result<()> {
19 ..Default::default()
20 ..Default::default()
20 },
21 },
21 ("#[\n|]#", "i(<esc>", "(#[|\n]#"),
22 ("#[\n|]#", "i(<esc>", "(#[|\n]#"),
23+ None,
22 )
24 )
23 .await?;
25 .await?;
24
26
helix-term/tests/integration/commands.rsnew file+25
@@ -0,0 +1,25 @@
1+use helix_core::diagnostic::Severity;
2+use helix_term::application::Application;
3+
4+use super::*;
5+
6+#[tokio::test]
7+async fn test_write_quit_fail() -> anyhow::Result<()> {
8+ test_key_sequence(
9+ &mut Application::new(
10+ Args {
11+ files: vec![(PathBuf::from("/foo"), Position::default())],
12+ ..Default::default()
13+ },
14+ Config::default(),
15+ )?,
16+ "ihello<esc>:wq<ret>",
17+ Some(&|app| {
18+ assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
19+ }),
20+ None,
21+ )
22+ .await?;
23+
24+ Ok(())
25+}
helix-term/tests/integration/helpers.rs+44−25
@@ -5,7 +5,6 @@ use crossterm::event::{Event, KeyEvent};
5use helix_core::{test, Selection, Transaction};
5use helix_core::{test, Selection, Transaction};
6use helix_term::{application::Application, args::Args, config::Config};
6use helix_term::{application::Application, args::Args, config::Config};
7use helix_view::{doc, input::parse_macro};
7use helix_view::{doc, input::parse_macro};
8−use tokio::time::timeout;
9use tokio_stream::wrappers::UnboundedReceiverStream;
8use tokio_stream::wrappers::UnboundedReceiverStream;
10
9
11#[derive(Clone, Debug)]
10#[derive(Clone, Debug)]
@@ -32,35 +31,48 @@ impl<S: Into<String>> From<(S, S, S)> for TestCase {
32 }
31 }
33}
32}
34
33
34+#[inline]
35pub async fn test_key_sequence(
35pub async fn test_key_sequence(
36 app: &mut Application,
36 app: &mut Application,
37 in_keys: &str,
37 in_keys: &str,
38 test_fn: Option<&dyn Fn(&Application)>,
38 test_fn: Option<&dyn Fn(&Application)>,
39+ timeout: Option<Duration>,
39) -> anyhow::Result<()> {
40) -> anyhow::Result<()> {
41+ test_key_sequences(app, vec![(in_keys, test_fn)], timeout).await
42+}
43+
44+pub async fn test_key_sequences(
45+ app: &mut Application,
46+ inputs: Vec<(&str, Option<&dyn Fn(&Application)>)>,
47+ timeout: Option<Duration>,
48+) -> anyhow::Result<()> {
49+ let timeout = timeout.unwrap_or(Duration::from_millis(500));
40 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
50 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
51+ let mut rx_stream = UnboundedReceiverStream::new(rx);
41
52
53+ for (in_keys, test_fn) in inputs {
42− for key_event in parse_macro(&in_keys)?.into_iter() {
54+ for key_event in parse_macro(&in_keys)?.into_iter() {
43− tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?;
55+ tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?;
44− }
56+ }
45
57
46− let mut rx_stream = UnboundedReceiverStream::new(rx);
47− let event_loop = app.event_loop(&mut rx_stream);
58+ let event_loop = app.event_loop(&mut rx_stream);
48− let result = timeout(Duration::from_millis(500), event_loop).await;
59+ let result = tokio::time::timeout(timeout, event_loop).await;
49
60
50− if result.is_ok() {
61+ if result.is_ok() {
51− bail!("application exited before test function could run");
62+ bail!("application exited before test function could run");
52− }
63+ }
53
64
54− if let Some(test) = test_fn {
65+ if let Some(test) = test_fn {
55− test(app);
66+ test(app);
56− };
67+ };
68+ }
57
69
58 for key_event in parse_macro("<esc>:q!<ret>")?.into_iter() {
70 for key_event in parse_macro("<esc>:q!<ret>")?.into_iter() {
59 tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?;
71 tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?;
60 }
72 }
61
73
62 let event_loop = app.event_loop(&mut rx_stream);
74 let event_loop = app.event_loop(&mut rx_stream);
63− timeout(Duration::from_millis(5000), event_loop).await?;
75+ tokio::time::timeout(timeout, event_loop).await?;
64 app.close().await?;
76 app.close().await?;
65
77
66 Ok(())
78 Ok(())
@@ -70,6 +82,7 @@ pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
70 app: Option<Application>,
82 app: Option<Application>,
71 test_case: T,
83 test_case: T,
72 test_fn: &dyn Fn(&Application),
84 test_fn: &dyn Fn(&Application),
85+ timeout: Option<Duration>,
73) -> anyhow::Result<()> {
86) -> anyhow::Result<()> {
74 let test_case = test_case.into();
87 let test_case = test_case.into();
75 let mut app =
88 let mut app =
@@ -87,7 +100,7 @@ pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
87 view.id,
100 view.id,
88 );
101 );
89
102
90− test_key_sequence(&mut app, &test_case.in_keys, Some(test_fn)).await
103+ test_key_sequence(&mut app, &test_case.in_keys, Some(test_fn), timeout).await
91}
104}
92
105
93/// Use this for very simple test cases where there is one input
106/// Use this for very simple test cases where there is one input
@@ -97,20 +110,26 @@ pub async fn test_key_sequence_text_result<T: Into<TestCase>>(
97 args: Args,
110 args: Args,
98 config: Config,
111 config: Config,
99 test_case: T,
112 test_case: T,
113+ timeout: Option<Duration>,
100) -> anyhow::Result<()> {
114) -> anyhow::Result<()> {
101 let test_case = test_case.into();
115 let test_case = test_case.into();
102 let app = Application::new(args, config).unwrap();
116 let app = Application::new(args, config).unwrap();
103
117
104− test_key_sequence_with_input_text(Some(app), test_case.clone(), &|app| {
118+ test_key_sequence_with_input_text(
119+ Some(app),
120+ test_case.clone(),
121+ &|app| {
105− let doc = doc!(app.editor);
122+ let doc = doc!(app.editor);
106− assert_eq!(&test_case.out_text, doc.text());
123+ assert_eq!(&test_case.out_text, doc.text());
107−
124+
108− let mut selections: Vec<_> = doc.selections().values().cloned().collect();
125+ let mut selections: Vec<_> = doc.selections().values().cloned().collect();
109− assert_eq!(1, selections.len());
126+ assert_eq!(1, selections.len());
110−
127+
111− let sel = selections.pop().unwrap();
128+ let sel = selections.pop().unwrap();
112− assert_eq!(test_case.out_selection, sel);
129+ assert_eq!(test_case.out_selection, sel);
130+ },
131+ timeout,
113− })
132+ )
114 .await
133 .await
115}
134}
116
135
helix-term/tests/integration/movement.rs+8
@@ -14,6 +14,7 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> {
14 out_text: String::new(),
14 out_text: String::new(),
15 out_selection: Selection::single(0, 0),
15 out_selection: Selection::single(0, 0),
16 },
16 },
17+ None,
17 )
18 )
18 .await?;
19 .await?;
19
20
@@ -21,6 +22,7 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> {
21 Args::default(),
22 Args::default(),
22 Config::default(),
23 Config::default(),
23 ("#[\n|]#", "i", "#[|\n]#"),
24 ("#[\n|]#", "i", "#[|\n]#"),
25+ None,
24 )
26 )
25 .await?;
27 .await?;
26
28
@@ -28,6 +30,7 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> {
28 Args::default(),
30 Args::default(),
29 Config::default(),
31 Config::default(),
30 ("#[\n|]#", "i<esc>", "#[|\n]#"),
32 ("#[\n|]#", "i<esc>", "#[|\n]#"),
33+ None,
31 )
34 )
32 .await?;
35 .await?;
33
36
@@ -35,6 +38,7 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> {
35 Args::default(),
38 Args::default(),
36 Config::default(),
39 Config::default(),
37 ("#[\n|]#", "i<esc>i", "#[|\n]#"),
40 ("#[\n|]#", "i<esc>i", "#[|\n]#"),
41+ None,
38 )
42 )
39 .await?;
43 .await?;
40
44
@@ -48,6 +52,7 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
48 Args::default(),
52 Args::default(),
49 Config::default(),
53 Config::default(),
50 ("#[f|]#oo\n", "vll<A-;><esc>", "#[|foo]#\n"),
54 ("#[f|]#oo\n", "vll<A-;><esc>", "#[|foo]#\n"),
55+ None,
51 )
56 )
52 .await?;
57 .await?;
53
58
@@ -65,6 +70,7 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
65 #(|bar)#"
70 #(|bar)#"
66 },
71 },
67 ),
72 ),
73+ None,
68 )
74 )
69 .await?;
75 .await?;
70
76
@@ -82,6 +88,7 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
82 #(ba|)#r"
88 #(ba|)#r"
83 },
89 },
84 ),
90 ),
91+ None,
85 )
92 )
86 .await?;
93 .await?;
87
94
@@ -99,6 +106,7 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
99 #(b|)#ar"
106 #(b|)#ar"
100 },
107 },
101 ),
108 ),
109+ None,
102 )
110 )
103 .await?;
111 .await?;
104
112
helix-term/tests/integration/write.rs+60−19
@@ -1,9 +1,11 @@
1use std::{
1use std::{
2 io::{Read, Write},
2 io::{Read, Write},
3− ops::RangeInclusive,
3+ time::Duration,
4};
4};
5
5
6+use helix_core::diagnostic::Severity;
6use helix_term::application::Application;
7use helix_term::application::Application;
8+use helix_view::doc;
7
9
8use super::*;
10use super::*;
9
11
@@ -21,6 +23,7 @@ async fn test_write() -> anyhow::Result<()> {
21 )?,
23 )?,
22 "ii can eat glass, it will not hurt me<ret><esc>:w<ret>",
24 "ii can eat glass, it will not hurt me<ret><esc>:w<ret>",
23 None,
25 None,
26+ Some(Duration::from_millis(1000)),
24 )
27 )
25 .await?;
28 .await?;
26
29
@@ -35,35 +38,73 @@ async fn test_write() -> anyhow::Result<()> {
35}
38}
36
39
37#[tokio::test]
40#[tokio::test]
38−async fn test_write_concurrent() -> anyhow::Result<()> {
41+async fn test_write_fail_mod_flag() -> anyhow::Result<()> {
39− let mut file = tempfile::NamedTempFile::new().unwrap();
42+ test_key_sequences(
40− let mut command = String::new();
41− const RANGE: RangeInclusive<i32> = 1..=1000;
42−
43− for i in RANGE {
44− let cmd = format!("%c{}<esc>:w<ret>", i);
45− command.push_str(&cmd);
46− }
47−
48− test_key_sequence(
49 &mut Application::new(
43 &mut Application::new(
50 Args {
44 Args {
51− files: vec![(file.path().to_path_buf(), Position::default())],
45+ files: vec![(PathBuf::from("/foo"), Position::default())],
52 ..Default::default()
46 ..Default::default()
53 },
47 },
54 Config::default(),
48 Config::default(),
55 )?,
49 )?,
56− &command,
50+ vec![
51+ (
52+ "",
53+ Some(&|app| {
54+ let doc = doc!(app.editor);
55+ assert!(!doc.is_modified());
56+ }),
57+ ),
58+ (
59+ "ihello<esc>",
60+ Some(&|app| {
61+ let doc = doc!(app.editor);
62+ assert!(doc.is_modified());
63+ }),
64+ ),
65+ (
66+ ":w<ret>",
67+ Some(&|app| {
68+ assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
69+
70+ let doc = doc!(app.editor);
71+ assert!(doc.is_modified());
72+ }),
73+ ),
74+ ],
57 None,
75 None,
58 )
76 )
59 .await?;
77 .await?;
60
78
61− file.as_file_mut().flush()?;
79+ Ok(())
62− file.as_file_mut().sync_all()?;
80+}
63
81
64− let mut file_content = String::new();
82+#[tokio::test]
65− file.as_file_mut().read_to_string(&mut file_content)?;
83+#[ignore]
66− assert_eq!(RANGE.end().to_string(), file_content);
84+async fn test_write_fail_new_path() -> anyhow::Result<()> {
85+ test_key_sequences(
86+ &mut Application::new(Args::default(), Config::default())?,
87+ vec![
88+ (
89+ "",
90+ Some(&|app| {
91+ let doc = doc!(app.editor);
92+ assert_eq!(None, app.editor.get_status());
93+ assert_eq!(None, doc.path());
94+ }),
95+ ),
96+ (
97+ ":w /foo<ret>",
98+ Some(&|app| {
99+ let doc = doc!(app.editor);
100+ assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
101+ assert_eq!(None, doc.path());
102+ }),
103+ ),
104+ ],
105+ Some(Duration::from_millis(1000)),
106+ )
107+ .await?;
67
108
68 Ok(())
109 Ok(())
69}
110}
helix-view/src/editor.rs+5
@@ -571,6 +571,11 @@ impl Editor {
571 self.status_msg = Some((error.into(), Severity::Error));
571 self.status_msg = Some((error.into(), Severity::Error));
572 }
572 }
573
573
574+ #[inline]
575+ pub fn get_status(&self) -> Option<(&Cow<'static, str>, &Severity)> {
576+ self.status_msg.as_ref().map(|(status, sev)| (status, sev))
577+ }
578+
574 pub fn set_theme(&mut self, theme: Theme) {
579 pub fn set_theme(&mut self, theme: Theme) {
575 // `ui.selection` is the only scope required to be able to render a theme.
580 // `ui.selection` is the only scope required to be able to render a theme.
576 if theme.find_scope_index("ui.selection").is_none() {
581 if theme.find_scope_index("ui.selection").is_none() {