From 18d709f1d9689df069fddd5c7347f694a55c5be1 Mon Sep 17 00:00:00 2001 From: rhysd Date: Wed, 25 Oct 2023 21:14:50 +0900 Subject: [PATCH] add unit tests for converting backend input events into `Input` --- .cargo/config.toml | 4 +- .github/workflows/ci.yml | 31 +++++---- CONTRIBUTING.md | 6 +- src/input/crossterm.rs | 120 +++++++++++++++++++++++++++++++++- src/input/mod.rs | 28 +++++++- src/input/termion.rs | 100 +++++++++++++++++++++++------ src/input/termwiz.rs | 135 +++++++++++++++++++++++++++++++++++++-- 7 files changed, 376 insertions(+), 48 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 2fb9621..9553df9 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,3 @@ [alias] -watch-check = ["watch", "-x", "clippy --features search --examples --tests", "-x", "clippy --features tuirs-crossterm,search --no-default-features --examples --tests"] -watch-test = ["watch", "-x", "test --features=search", "-w", "src", "-w", "tests"] +watch-check = ["watch", "-x", "clippy --features=search,termwiz --examples --tests", "-x", "clippy --features tuirs-crossterm,search --no-default-features --examples --tests"] +watch-test = ["watch", "-x", "test --features=search,termwiz", "-w", "src", "-w", "tests"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6130912..6ef8d1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,10 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - run: cargo test --features=search + - run: cargo test --features=search,termwiz,termion,arbitrary + if: ${{ matrix.os != 'windows-latest' }} + - run: cargo test --features=search,termwiz,arbitrary + if: ${{ matrix.os == 'windows-latest' }} - run: cargo test --no-default-features --features=tuirs-crossterm,search -- --skip .rs - run: cargo test --no-default-features --features=tuirs-termion,search -- --skip .rs if: ${{ matrix.os != 'windows-latest' }} @@ -29,16 +32,18 @@ jobs: components: clippy,rustfmt - uses: Swatinem/rust-cache@v2 - run: cargo fmt -- --check - - run: cargo clippy --examples -- -D warnings - - run: cargo clippy --examples --features search -- -D warnings - - run: cargo clippy --examples --no-default-features --features termion -- -D warnings - - run: cargo clippy --examples --no-default-features --features termion,search -- -D warnings - - run: cargo clippy --examples --no-default-features --features no-backend -- -D warnings - - run: cargo clippy --examples --no-default-features --features no-backend,search -- -D warnings - - run: cargo clippy --examples --no-default-features --features tuirs-crossterm -- -D warnings - - run: cargo clippy --examples --no-default-features --features tuirs-crossterm,search -- -D warnings - - run: cargo clippy --examples --no-default-features --features tuirs-termion -- -D warnings - - run: cargo clippy --examples --no-default-features --features tuirs-termion,search -- -D warnings - - run: cargo clippy --examples --no-default-features --features tuirs-no-backend -- -D warnings - - run: cargo clippy --examples --no-default-features --features tuirs-no-backend,search -- -D warnings + - run: cargo clippy --examples --tests -- -D warnings + - run: cargo clippy --examples --tests --features search -- -D warnings + - run: cargo clippy --examples --tests --no-default-features --features termion -- -D warnings + - run: cargo clippy --examples --tests --no-default-features --features termion,search -- -D warnings + - run: cargo clippy --examples --tests --no-default-features --features termwiz -- -D warnings + - run: cargo clippy --examples --tests --no-default-features --features termwiz,search -- -D warnings + - run: cargo clippy --examples --tests --no-default-features --features no-backend -- -D warnings + - run: cargo clippy --examples --tests --no-default-features --features no-backend,search -- -D warnings + - run: cargo clippy --examples --tests --no-default-features --features tuirs-crossterm -- -D warnings + - run: cargo clippy --examples --tests --no-default-features --features tuirs-crossterm,search -- -D warnings + - run: cargo clippy --examples --tests --no-default-features --features tuirs-termion -- -D warnings + - run: cargo clippy --examples --tests --no-default-features --features tuirs-termion,search -- -D warnings + - run: cargo clippy --examples --tests --no-default-features --features tuirs-no-backend -- -D warnings + - run: cargo clippy --examples --tests --no-default-features --features tuirs-no-backend,search -- -D warnings - run: cargo rustdoc --features=search,termwiz,termion -p tui-textarea -- -D warnings diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 94d3783..8d5beda 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,11 +28,13 @@ cargo test --features=search To run linters: ```sh -cargo clippy --features=search --tests --examples -cargo clippy --features=tuirs-crossterm,search --no-default-features --tests --examples +cargo clippy --features=search,termwiz,termion --tests --examples +cargo clippy --features=tuirs-crossterm,tuirs-termion,search --no-default-features --tests --examples cargo fmt -- --check ``` +Note: On Windows, remove `termion` and `tuirs-termion` features from `--features` argument since termion doesn't support Windows. + If you use [cargo-watch][], `cargo watch-check` and `cargo watch-test` aliases are useful to run checks/tests automatically on files being changed. diff --git a/src/input/crossterm.rs b/src/input/crossterm.rs index eac7d9d..509aa21 100644 --- a/src/input/crossterm.rs +++ b/src/input/crossterm.rs @@ -54,10 +54,128 @@ impl From for Input { let key = match mouse.kind { MouseEventKind::ScrollDown => Key::MouseScrollDown, MouseEventKind::ScrollUp => Key::MouseScrollUp, - _ => return Self::default(), + _ => Key::Null, }; let ctrl = mouse.modifiers.contains(KeyModifiers::CONTROL); let alt = mouse.modifiers.contains(KeyModifiers::ALT); Self { key, ctrl, alt } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::crossterm::event::{KeyEventKind, KeyEventState}; + use crate::input::tests::input; + + fn key_event(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent { + KeyEvent { + code, + modifiers, + kind: KeyEventKind::Press, + state: KeyEventState::empty(), + } + } + + fn mouse_event(kind: MouseEventKind, modifiers: KeyModifiers) -> MouseEvent { + MouseEvent { + kind, + column: 1, + row: 1, + modifiers, + } + } + + #[test] + fn key_to_input() { + for (from, to) in [ + ( + key_event(KeyCode::Char('a'), KeyModifiers::empty()), + input(Key::Char('a'), false, false), + ), + ( + key_event(KeyCode::Enter, KeyModifiers::empty()), + input(Key::Enter, false, false), + ), + ( + key_event(KeyCode::Left, KeyModifiers::CONTROL), + input(Key::Left, true, false), + ), + ( + key_event(KeyCode::Home, KeyModifiers::ALT), + input(Key::Home, false, true), + ), + ( + key_event(KeyCode::F(1), KeyModifiers::ALT | KeyModifiers::CONTROL), + input(Key::F(1), true, true), + ), + ( + key_event(KeyCode::NumLock, KeyModifiers::CONTROL), + input(Key::Null, true, false), + ), + ] { + assert_eq!(Input::from(from), to, "{:?} -> {:?}", from, to); + } + } + + #[test] + fn mouse_to_input() { + for (from, to) in [ + ( + mouse_event(MouseEventKind::ScrollDown, KeyModifiers::empty()), + input(Key::MouseScrollDown, false, false), + ), + ( + mouse_event(MouseEventKind::ScrollUp, KeyModifiers::CONTROL), + input(Key::MouseScrollUp, true, false), + ), + ( + mouse_event(MouseEventKind::ScrollDown, KeyModifiers::ALT), + input(Key::MouseScrollDown, false, true), + ), + ( + mouse_event( + MouseEventKind::ScrollUp, + KeyModifiers::CONTROL | KeyModifiers::ALT, + ), + input(Key::MouseScrollUp, true, true), + ), + ( + mouse_event(MouseEventKind::Moved, KeyModifiers::CONTROL), + input(Key::Null, true, false), + ), + ] { + assert_eq!(Input::from(from), to, "{:?} -> {:?}", from, to); + } + } + + #[test] + fn event_to_input() { + for (from, to) in [ + ( + Event::Key(key_event(KeyCode::Char('a'), KeyModifiers::empty())), + input(Key::Char('a'), false, false), + ), + ( + Event::Mouse(mouse_event( + MouseEventKind::ScrollDown, + KeyModifiers::empty(), + )), + input(Key::MouseScrollDown, false, false), + ), + (Event::FocusGained, input(Key::Null, false, false)), + ] { + assert_eq!(Input::from(from.clone()), to, "{:?} -> {:?}", from, to); + } + } + + // Regression for https://github.com/rhysd/tui-textarea/issues/14 + #[test] + #[cfg(target_os = "windows")] + fn press_ignore_on_windows() { + let mut k = key_event(KeyCode::Char('a'), KeyModifiers::empty()); + k.kind = KeyEventKind::Release; + let want = input(Key::Null, false, false); + assert_eq!(Input::from(k.clone()), want, "{:?} -> {:?}", k, want); + } +} diff --git a/src/input/mod.rs b/src/input/mod.rs index c9d879c..4feff85 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -12,7 +12,7 @@ use arbitrary::Arbitrary; /// /// This type is marked as `#[non_exhaustive]` since more keys may be supported in the future. #[non_exhaustive] -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Hash)] #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] pub enum Key { /// Normal letter key input @@ -93,7 +93,7 @@ impl Default for Key { /// alt: false, /// }); /// ``` -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, PartialEq, Hash)] #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] pub struct Input { /// Typed key. @@ -103,3 +103,27 @@ pub struct Input { /// Alt modifier key. `true` means Alt key was pressed. pub alt: bool, } + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(dead_code)] + pub(crate) fn input(key: Key, ctrl: bool, alt: bool) -> Input { + Input { key, ctrl, alt } + } + + #[test] + #[cfg(feature = "arbitrary")] + fn arbitrary_input() { + let mut u = arbitrary::Unstructured::new(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + Input::arbitrary(&mut u).unwrap(); + } + + #[test] + #[cfg(feature = "arbitrary")] + fn arbitrary_key() { + let mut u = arbitrary::Unstructured::new(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + Key::arbitrary(&mut u).unwrap(); + } +} diff --git a/src/input/termion.rs b/src/input/termion.rs index 9a95a42..12a23d7 100644 --- a/src/input/termion.rs +++ b/src/input/termion.rs @@ -1,5 +1,5 @@ use super::{Input, Key}; -use termion::event::{Event, Key as KeyEvent, MouseEvent}; +use termion::event::{Event, Key as KeyEvent, MouseButton, MouseEvent}; impl From for Input { /// Convert [`termion::event::Event`] to [`Input`]. @@ -15,34 +15,32 @@ impl From for Input { impl From for Input { /// Convert [`termion::event::Key`] to [`Input`]. fn from(key: KeyEvent) -> Self { - use KeyEvent::*; - let mut ctrl = false; let mut alt = false; let key = match key { - Char('\n' | '\r') => Key::Enter, - Char(c) => Key::Char(c), - Ctrl(c) => { + KeyEvent::Char('\n' | '\r') => Key::Enter, + KeyEvent::Char(c) => Key::Char(c), + KeyEvent::Ctrl(c) => { ctrl = true; Key::Char(c) } - Alt(c) => { + KeyEvent::Alt(c) => { alt = true; Key::Char(c) } - Backspace => Key::Backspace, - Left => Key::Left, - Right => Key::Right, - Up => Key::Up, - Down => Key::Down, - Home => Key::Home, - End => Key::End, - PageUp => Key::PageUp, - PageDown => Key::PageDown, - BackTab => Key::Tab, - Delete => Key::Delete, - Esc => Key::Esc, - F(x) => Key::F(x), + KeyEvent::Backspace => Key::Backspace, + KeyEvent::Left => Key::Left, + KeyEvent::Right => Key::Right, + KeyEvent::Up => Key::Up, + KeyEvent::Down => Key::Down, + KeyEvent::Home => Key::Home, + KeyEvent::End => Key::End, + KeyEvent::PageUp => Key::PageUp, + KeyEvent::PageDown => Key::PageDown, + KeyEvent::BackTab => Key::Tab, + KeyEvent::Delete => Key::Delete, + KeyEvent::Esc => Key::Esc, + KeyEvent::F(x) => Key::F(x), _ => Key::Null, }; @@ -53,7 +51,6 @@ impl From for Input { impl From for Input { /// Convert [`termion::event::MouseEvent`] to [`Input`]. fn from(mouse: MouseEvent) -> Self { - use termion::event::MouseButton; let key = match mouse { MouseEvent::Press(MouseButton::WheelUp, ..) => Key::MouseScrollUp, MouseEvent::Press(MouseButton::WheelDown, ..) => Key::MouseScrollDown, @@ -66,3 +63,64 @@ impl From for Input { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::input::tests::input; + + #[test] + fn key_to_input() { + for (from, to) in [ + (KeyEvent::Char('a'), input(Key::Char('a'), false, false)), + (KeyEvent::Ctrl('a'), input(Key::Char('a'), true, false)), + (KeyEvent::Alt('a'), input(Key::Char('a'), false, true)), + (KeyEvent::Char('\n'), input(Key::Enter, false, false)), + (KeyEvent::Char('\r'), input(Key::Enter, false, false)), + (KeyEvent::F(1), input(Key::F(1), false, false)), + (KeyEvent::BackTab, input(Key::Tab, false, false)), + (KeyEvent::Null, input(Key::Null, false, false)), + ] { + assert_eq!(Input::from(from), to, "{:?} -> {:?}", from, to); + } + } + + #[test] + fn mouse_to_input() { + for (from, to) in [ + ( + MouseEvent::Press(MouseButton::WheelDown, 1, 1), + input(Key::MouseScrollDown, false, false), + ), + ( + MouseEvent::Press(MouseButton::WheelUp, 1, 1), + input(Key::MouseScrollUp, false, false), + ), + ( + MouseEvent::Press(MouseButton::Left, 1, 1), + input(Key::Null, false, false), + ), + (MouseEvent::Release(1, 1), input(Key::Null, false, false)), + (MouseEvent::Hold(1, 1), input(Key::Null, false, false)), + ] { + assert_eq!(Input::from(from), to, "{:?} -> {:?}", from, to); + } + } + + #[test] + fn event_to_input() { + for (from, to) in [ + ( + Event::Key(KeyEvent::Char('a')), + input(Key::Char('a'), false, false), + ), + ( + Event::Mouse(MouseEvent::Press(MouseButton::WheelDown, 1, 1)), + input(Key::MouseScrollDown, false, false), + ), + (Event::Unsupported(vec![]), input(Key::Null, false, false)), + ] { + assert_eq!(Input::from(from.clone()), to, "{:?} -> {:?}", from, to); + } + } +} diff --git a/src/input/termwiz.rs b/src/input/termwiz.rs index 9e31e15..ab5ff1a 100644 --- a/src/input/termwiz.rs +++ b/src/input/termwiz.rs @@ -1,5 +1,7 @@ use super::{Input, Key}; -use termwiz::input::{InputEvent, KeyEvent, MouseButtons, MouseEvent, PixelMouseEvent}; +use termwiz::input::{ + InputEvent, KeyCode, KeyEvent, Modifiers, MouseButtons, MouseEvent, PixelMouseEvent, +}; impl From for Input { /// Convert [`termwiz::input::InputEvent`] to [`Input`]. @@ -16,8 +18,6 @@ impl From for Input { impl From for Input { /// Convert [`termwiz::input::KeyEvent`] to [`Input`]. fn from(key: KeyEvent) -> Self { - use termwiz::input::{KeyCode, Modifiers}; - let KeyEvent { key, modifiers } = key; let key = match key { KeyCode::Char(c) => Key::Char(c), @@ -62,8 +62,6 @@ impl From for Key { impl From for Input { /// Convert [`termwiz::input::MouseEvent`] to [`Input`]. fn from(mouse: MouseEvent) -> Self { - use termwiz::input::Modifiers; - let MouseEvent { mouse_buttons, modifiers, @@ -80,8 +78,6 @@ impl From for Input { impl From for Input { /// Convert [`termwiz::input::PixelMouseEvent`] to [`Input`]. fn from(mouse: PixelMouseEvent) -> Self { - use termwiz::input::Modifiers; - let PixelMouseEvent { mouse_buttons, modifiers, @@ -94,3 +90,128 @@ impl From for Input { Self { key, ctrl, alt } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::input::tests::input; + + fn key_event(key: KeyCode, modifiers: Modifiers) -> KeyEvent { + KeyEvent { key, modifiers } + } + + fn mouse_event(mouse_buttons: MouseButtons, modifiers: Modifiers) -> MouseEvent { + MouseEvent { + mouse_buttons, + modifiers, + x: 1, + y: 1, + } + } + + fn pixel_mouse_event(mouse_buttons: MouseButtons, modifiers: Modifiers) -> PixelMouseEvent { + PixelMouseEvent { + mouse_buttons, + modifiers, + x_pixels: 1, + y_pixels: 1, + } + } + + #[test] + fn key_to_input() { + for (from, to) in [ + ( + key_event(KeyCode::Char('a'), Modifiers::empty()), + input(Key::Char('a'), false, false), + ), + ( + key_event(KeyCode::Enter, Modifiers::empty()), + input(Key::Enter, false, false), + ), + ( + key_event(KeyCode::LeftArrow, Modifiers::CTRL), + input(Key::Left, true, false), + ), + ( + key_event(KeyCode::Home, Modifiers::ALT), + input(Key::Home, false, true), + ), + ( + key_event(KeyCode::Function(1), Modifiers::ALT | Modifiers::CTRL), + input(Key::F(1), true, true), + ), + ( + key_event(KeyCode::NumLock, Modifiers::CTRL), + input(Key::Null, true, false), + ), + ] { + assert_eq!(Input::from(from.clone()), to, "{:?} -> {:?}", from, to); + } + } + + #[test] + fn mouse_to_input() { + for (from, to) in [ + ( + mouse_event(MouseButtons::VERT_WHEEL, Modifiers::empty()), + input(Key::MouseScrollDown, false, false), + ), + ( + mouse_event( + MouseButtons::VERT_WHEEL | MouseButtons::WHEEL_POSITIVE, + Modifiers::empty(), + ), + input(Key::MouseScrollUp, false, false), + ), + ( + mouse_event(MouseButtons::VERT_WHEEL, Modifiers::CTRL), + input(Key::MouseScrollDown, true, false), + ), + ( + mouse_event(MouseButtons::VERT_WHEEL, Modifiers::ALT), + input(Key::MouseScrollDown, false, true), + ), + ( + mouse_event(MouseButtons::VERT_WHEEL, Modifiers::CTRL | Modifiers::ALT), + input(Key::MouseScrollDown, true, true), + ), + ( + mouse_event(MouseButtons::LEFT, Modifiers::empty()), + input(Key::Null, false, false), + ), + ] { + assert_eq!(Input::from(from.clone()), to, "{:?} -> {:?}", from, to); + + let from = pixel_mouse_event(from.mouse_buttons, from.modifiers); + assert_eq!(Input::from(from.clone()), to, "{:?} -> {:?}", from, to); + } + } + + #[test] + fn event_to_input() { + for (from, to) in [ + ( + InputEvent::Key(key_event(KeyCode::Char('a'), Modifiers::empty())), + input(Key::Char('a'), false, false), + ), + ( + InputEvent::Mouse(mouse_event(MouseButtons::VERT_WHEEL, Modifiers::empty())), + input(Key::MouseScrollDown, false, false), + ), + ( + InputEvent::PixelMouse(pixel_mouse_event( + MouseButtons::VERT_WHEEL, + Modifiers::empty(), + )), + input(Key::MouseScrollDown, false, false), + ), + ( + InputEvent::Paste("x".into()), + input(Key::Null, false, false), + ), + ] { + assert_eq!(Input::from(from.clone()), to, "{:?} -> {:?}", from, to); + } + } +}