[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