all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Christoph Heiss <c.heiss@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH installer 7/7] tui: views: add some TUI component tests
Date: Wed,  4 Oct 2023 16:42:18 +0200	[thread overview]
Message-ID: <20231004144232.327071-8-c.heiss@proxmox.com> (raw)
In-Reply-To: <20231004144232.327071-1-c.heiss@proxmox.com>

Signed-off-by: Christoph Heiss <c.heiss@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





      parent reply	other threads:[~2023-10-04 14:43 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-10-04 14:42 [pve-devel] [PATCH installer 0/7] tui: add min/max constraints for bootdisk parameters Christoph Heiss
2023-10-04 14:42 ` [pve-devel] [PATCH installer 1/7] tui: fix setting content when using the `DiskSizeEditView` builder Christoph Heiss
2023-10-04 14:42 ` [pve-devel] [PATCH installer 2/7] tui: improve `FormView` error handling Christoph Heiss
2023-10-04 14:42 ` [pve-devel] [PATCH installer 3/7] tui: add optional min-value constraint to `NumericEditView` and `DiskSizeEditView` Christoph Heiss
2023-10-04 14:42 ` [pve-devel] [PATCH installer 4/7] tui: add min/max constraints for ZFS bootdisk parameters Christoph Heiss
2023-10-04 14:42 ` [pve-devel] [PATCH installer 5/7] tui: add min/max contraints for LVM " Christoph Heiss
2023-10-04 14:42 ` [pve-devel] [PATCH installer 6/7] tui: add min/max contraints for Btrfs " Christoph Heiss
2023-10-04 14:42 ` Christoph Heiss [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20231004144232.327071-8-c.heiss@proxmox.com \
    --to=c.heiss@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal