[pve-devel] [PATCH installer 7/7] tui: views: add some TUI component tests

Christoph Heiss c.heiss at proxmox.com
Wed Oct 4 16:42:18 CEST 2023


Signed-off-by: Christoph Heiss <c.heiss at proxmox.com>
---
 proxmox-tui-installer/src/main.rs           |  35 ++++++
 proxmox-tui-installer/src/options.rs        |   2 +-
 proxmox-tui-installer/src/views/mod.rs      | 127 +++++++++++++++++++-
 proxmox-tui-installer/src/views/timezone.rs | 109 +++++++++++++++++
 4 files changed, 267 insertions(+), 6 deletions(-)

diff --git a/proxmox-tui-installer/src/main.rs b/proxmox-tui-installer/src/main.rs
index ab990a8..4971071 100644
--- a/proxmox-tui-installer/src/main.rs
+++ b/proxmox-tui-installer/src/main.rs
@@ -937,3 +937,38 @@ impl FromStr for UiMessage {
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    pub mod utils {
+        use cursive::{
+            event::{Event, Key},
+            reexports::crossbeam_channel::Sender,
+            view::Nameable,
+            Cursive, CursiveRunner, Vec2, View,
+        };
+
+        pub fn puppet_siv(view: impl View) -> (CursiveRunner<Cursive>, Sender<Option<Event>>) {
+            let backend = cursive::backends::puppet::Backend::init(Some(Vec2::new(80, 24)));
+            let input = backend.input();
+            let mut siv = Cursive::new().into_runner(backend);
+
+            siv.add_layer(view.with_name("root"));
+            input.send(Some(Event::Refresh)).unwrap();
+            siv.step();
+
+            (siv, input)
+        }
+
+        pub fn send_keys(
+            siv: &mut CursiveRunner<Cursive>,
+            input: &Sender<Option<Event>>,
+            keys: Vec<Key>,
+        ) {
+            for key in keys {
+                input.send(Some(Event::Key(key))).unwrap()
+            }
+            siv.step();
+        }
+    }
+}
diff --git a/proxmox-tui-installer/src/options.rs b/proxmox-tui-installer/src/options.rs
index a0a81c9..d3f213d 100644
--- a/proxmox-tui-installer/src/options.rs
+++ b/proxmox-tui-installer/src/options.rs
@@ -275,7 +275,7 @@ impl BootdiskOptions {
     }
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub struct TimezoneOptions {
     pub country: String,
     pub timezone: String,
diff --git a/proxmox-tui-installer/src/views/mod.rs b/proxmox-tui-installer/src/views/mod.rs
index 0fe715e..b95bc20 100644
--- a/proxmox-tui-installer/src/views/mod.rs
+++ b/proxmox-tui-installer/src/views/mod.rs
@@ -18,9 +18,11 @@ pub use table_view::*;
 mod timezone;
 pub use timezone::*;
 
+#[derive(Debug, PartialEq)]
 pub enum NumericEditViewError<T>
 where
-    T: FromStr,
+    T: FromStr + fmt::Debug + PartialEq,
+    <T as FromStr>::Err: fmt::Debug + PartialEq,
 {
     ParseError(<T as FromStr>::Err),
     OutOfRange {
@@ -34,8 +36,8 @@ where
 
 impl<T> fmt::Display for NumericEditViewError<T>
 where
-    T: fmt::Display + FromStr,
-    <T as FromStr>::Err: fmt::Display,
+    T: fmt::Debug + fmt::Display + FromStr + PartialEq,
+    <T as FromStr>::Err: fmt::Debug + fmt::Display + PartialEq,
 {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use NumericEditViewError::*;
@@ -68,7 +70,8 @@ pub struct NumericEditView<T> {
 
 impl<T> NumericEditView<T>
 where
-    T: Copy + ToString + FromStr + PartialOrd,
+    T: fmt::Debug + Copy + ToString + FromStr + PartialOrd + PartialEq,
+    <T as FromStr>::Err: fmt::Debug + PartialEq,
 {
     pub fn new() -> Self {
         Self {
@@ -347,7 +350,8 @@ impl<T: 'static + Clone> FormViewGetValue<T> for SelectView<T> {
 
 impl<T> FormViewGetValue<T> for NumericEditView<T>
 where
-    T: Copy + ToString + FromStr + PartialOrd,
+    T: fmt::Debug + Copy + ToString + FromStr + PartialOrd + PartialEq,
+    <T as FromStr>::Err: fmt::Debug + PartialEq,
     NumericEditView<T>: ViewWrapper,
 {
     type Error = NumericEditViewError<T>;
@@ -405,6 +409,7 @@ impl FormViewGetValue<f64> for DiskSizeEditView {
     }
 }
 
+#[derive(PartialEq, Eq)]
 pub enum FormViewError<T> {
     ChildNotFound(usize),
     ValueError(T),
@@ -635,3 +640,115 @@ impl CidrAddressEditView {
 impl ViewWrapper for CidrAddressEditView {
     cursive::wrap_impl!(self.view: LinearLayout);
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn numeric_edit_view_setting_content_preserves_settings() {
+        let mut view = NumericEditView::with_range(2., 4.).content(2.);
+        assert_eq!(view.min_value, Some(2.));
+        assert_eq!(view.max_value, Some(4.));
+        assert_eq!(view.get_content(), Ok(2.));
+
+        view = view.content(4.);
+        assert_eq!(view.get_content(), Ok(4.));
+
+        view = view.content(3.);
+        assert_eq!(view.get_content(), Ok(3.));
+
+        view = view.content(0.);
+        let err = view.get_content();
+        match err {
+            Err(NumericEditViewError::OutOfRange { value, min, max }) => {
+                assert_eq!(value, 0.);
+                assert_eq!(min, Some(2.));
+                assert_eq!(max, Some(4.));
+            }
+            _ => panic!(),
+        }
+        assert_eq!(
+            err.unwrap_err().to_string(),
+            "out of range: 0, must be at least 2 and at most 4".to_owned(),
+        );
+    }
+
+    #[test]
+    fn disk_size_edit_view_setting_content_preserves_settings() {
+        let mut view = DiskSizeEditView::with_range(2., 4.).content(2.);
+        assert_eq!(view.get_inner_mut().unwrap().min_value, Some(2.));
+        assert_eq!(view.get_inner_mut().unwrap().max_value, Some(4.));
+        assert_eq!(view.get_content(), Ok(2.));
+
+        view = view.content(4.);
+        assert_eq!(view.get_content(), Ok(4.));
+
+        view = view.content(3.);
+        assert_eq!(view.get_content(), Ok(3.));
+
+        view = view.content(0.);
+        let err = view.get_content();
+        match err {
+            Err(NumericEditViewError::OutOfRange { value, min, max }) => {
+                assert_eq!(value, 0.);
+                assert_eq!(min, Some(2.));
+                assert_eq!(max, Some(4.));
+            }
+            _ => panic!(),
+        }
+        assert_eq!(
+            err.unwrap_err().to_string(),
+            "out of range: 0, must be at least 2 and at most 4".to_owned()
+        );
+    }
+
+    #[test]
+    fn numeric_edit_view_get_content() {
+        let mut view = NumericEditView::<usize>::new();
+
+        assert_eq!(view.get_content(), Ok(0));
+        view.set_min_value(2);
+        assert!(view.get_content().is_err());
+        view.set_max_value(4);
+        view = view.content(3);
+        assert_eq!(view.get_content(), Ok(3));
+        view = view.content(5);
+        assert!(view.get_content().is_err());
+
+        view.view.set_content("");
+        view.allow_empty = true;
+        assert_eq!(view.get_content(), Err(NumericEditViewError::Empty));
+    }
+
+    #[test]
+    fn form_view_get_value() {
+        let mut view = FormView::new()
+            .child("foo", EditView::new().content("foo"))
+            .child_conditional(false, "fake-bar", EditView::new().content("fake-bar"))
+            .child_conditional(true, "bar", EditView::new().content("bar"))
+            .child("numeric", NumericEditView::with_range(2, 4).content(2))
+            .child("disksize", DiskSizeEditView::with_range(2., 4.).content(2.))
+            .child(
+                "disksize-opt",
+                DiskSizeEditView::new_emptyable()
+                    .max_value(4.)
+                    .content_maybe(Some(2.)),
+            );
+
+        assert_eq!(view.len(), 5);
+        assert_eq!(view.get_value::<EditView, _>(0), Ok("foo".to_string()));
+        assert_eq!(view.get_value::<EditView, _>(1), Ok("bar".to_string()));
+        assert_eq!(view.get_value::<NumericEditView<usize>, _>(2), Ok(2));
+        assert_eq!(view.get_value::<DiskSizeEditView, _>(3), Ok(2.));
+        assert_eq!(view.get_opt_value::<DiskSizeEditView, _>(4), Ok(Some(2.)));
+
+        view.replace_child(
+            4,
+            DiskSizeEditView::new_emptyable()
+                .max_value(4.)
+                .content_maybe(None),
+        );
+        assert_eq!(view.get_opt_value::<DiskSizeEditView, _>(4), Ok(None));
+    }
+}
diff --git a/proxmox-tui-installer/src/views/timezone.rs b/proxmox-tui-installer/src/views/timezone.rs
index bd38a92..88055d0 100644
--- a/proxmox-tui-installer/src/views/timezone.rs
+++ b/proxmox-tui-installer/src/views/timezone.rs
@@ -132,3 +132,112 @@ impl TimezoneOptionsView {
 impl ViewWrapper for TimezoneOptionsView {
     cursive::wrap_impl!(self.view: FormView);
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{setup::CountryInfo, tests::utils::*};
+    use cursive::event::Key;
+    use std::collections::HashMap;
+
+    fn locale_info() -> LocaleInfo {
+        let mut cczones = HashMap::new();
+        let mut countries = HashMap::new();
+        let mut kmap = HashMap::new();
+
+        cczones.insert("at".to_owned(), vec!["Europe/Vienna".to_owned()]);
+        cczones.insert("jp".to_owned(), vec!["Asia/Tokyo".to_owned()]);
+
+        countries.insert(
+            "at".to_owned(),
+            CountryInfo {
+                name: "Austria".to_owned(),
+                zone: "Europe/Vienna".to_owned(),
+                kmap: "de".to_owned(),
+            },
+        );
+        countries.insert(
+            "jp".to_owned(),
+            CountryInfo {
+                name: "Japan".to_owned(),
+                zone: "Asia/Tokyo".to_owned(),
+                kmap: "jp".to_owned(),
+            },
+        );
+
+        kmap.insert(
+            "de".to_owned(),
+            KeyboardMapping {
+                name: "German".to_owned(),
+                id: "de".to_owned(),
+                xkb_layout: "de".to_owned(),
+                xkb_variant: "nodeadkeys".to_owned(),
+            },
+        );
+
+        LocaleInfo {
+            cczones,
+            countries,
+            kmap,
+        }
+    }
+
+    #[test]
+    fn timezone_view_sets_zones_correctly() {
+        let options = TimezoneOptions {
+            country: "at".to_owned(),
+            timezone: "Europe/Vienna".to_owned(),
+            kb_layout: "de".to_owned(),
+        };
+
+        let view = TimezoneOptionsView::new(&locale_info(), &options);
+        let (mut siv, input) = puppet_siv(view);
+
+        assert_eq!(
+            siv.find_name::<TimezoneOptionsView>("root")
+                .unwrap()
+                .get_values(),
+            Ok(TimezoneOptions {
+                country: "at".to_owned(),
+                timezone: "Europe/Vienna".to_owned(),
+                kb_layout: "de".to_owned(),
+            })
+        );
+
+        // Navigate to timezone and select the second element, aka. UTC
+        send_keys(
+            &mut siv,
+            &input,
+            vec![Key::Down, Key::Enter, Key::Down, Key::Enter],
+        );
+
+        assert_eq!(
+            siv.find_name::<TimezoneOptionsView>("root")
+                .unwrap()
+                .get_values(),
+            Ok(TimezoneOptions {
+                country: "at".to_owned(),
+                timezone: "UTC".to_owned(),
+                kb_layout: "de".to_owned(),
+            })
+        );
+
+        // Navigate up again to country and select the second one
+        send_keys(
+            &mut siv,
+            &input,
+            vec![Key::Up, Key::Enter, Key::Down, Key::Enter],
+        );
+
+        assert_eq!(
+            siv.find_name::<TimezoneOptionsView>("root")
+                .unwrap()
+                .get_values(),
+            Ok(TimezoneOptions {
+                country: "jp".to_owned(),
+                timezone: "Asia/Tokyo".to_owned(),
+                kb_layout: "de".to_owned(),
+            })
+        );
+    }
+}
-- 
2.42.0






More information about the pve-devel mailing list