From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 76DD891BC5 for ; Wed, 4 Oct 2023 16:43:15 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 577B0F456 for ; Wed, 4 Oct 2023 16:42:45 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Wed, 4 Oct 2023 16:42:44 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id B6B944486E for ; Wed, 4 Oct 2023 16:42:42 +0200 (CEST) From: Christoph Heiss To: pve-devel@lists.proxmox.com Date: Wed, 4 Oct 2023 16:42:18 +0200 Message-ID: <20231004144232.327071-8-c.heiss@proxmox.com> X-Mailer: git-send-email 2.42.0 In-Reply-To: <20231004144232.327071-1-c.heiss@proxmox.com> References: <20231004144232.327071-1-c.heiss@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.027 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pve-devel] [PATCH installer 7/7] tui: views: add some TUI component tests X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Wed, 04 Oct 2023 14:43:15 -0000 Signed-off-by: Christoph Heiss --- 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, Sender>) { + 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, + input: &Sender>, + keys: Vec, + ) { + 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 where - T: FromStr, + T: FromStr + fmt::Debug + PartialEq, + ::Err: fmt::Debug + PartialEq, { ParseError(::Err), OutOfRange { @@ -34,8 +36,8 @@ where impl fmt::Display for NumericEditViewError where - T: fmt::Display + FromStr, - ::Err: fmt::Display, + T: fmt::Debug + fmt::Display + FromStr + PartialEq, + ::Err: fmt::Debug + fmt::Display + PartialEq, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use NumericEditViewError::*; @@ -68,7 +70,8 @@ pub struct NumericEditView { impl NumericEditView where - T: Copy + ToString + FromStr + PartialOrd, + T: fmt::Debug + Copy + ToString + FromStr + PartialOrd + PartialEq, + ::Err: fmt::Debug + PartialEq, { pub fn new() -> Self { Self { @@ -347,7 +350,8 @@ impl FormViewGetValue for SelectView { impl FormViewGetValue for NumericEditView where - T: Copy + ToString + FromStr + PartialOrd, + T: fmt::Debug + Copy + ToString + FromStr + PartialOrd + PartialEq, + ::Err: fmt::Debug + PartialEq, NumericEditView: ViewWrapper, { type Error = NumericEditViewError; @@ -405,6 +409,7 @@ impl FormViewGetValue for DiskSizeEditView { } } +#[derive(PartialEq, Eq)] pub enum FormViewError { 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::::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::(0), Ok("foo".to_string())); + assert_eq!(view.get_value::(1), Ok("bar".to_string())); + assert_eq!(view.get_value::, _>(2), Ok(2)); + assert_eq!(view.get_value::(3), Ok(2.)); + assert_eq!(view.get_opt_value::(4), Ok(Some(2.))); + + view.replace_child( + 4, + DiskSizeEditView::new_emptyable() + .max_value(4.) + .content_maybe(None), + ); + assert_eq!(view.get_opt_value::(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::("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::("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::("root") + .unwrap() + .get_values(), + Ok(TimezoneOptions { + country: "jp".to_owned(), + timezone: "Asia/Tokyo".to_owned(), + kb_layout: "de".to_owned(), + }) + ); + } +} -- 2.42.0