use clap::{ArgMatches, Error}; use rand::{thread_rng, Rng}; use terminal_size::{terminal_size, Height, Width}; #[cfg(test)] use mocktopus::macros::mockable; pub(crate) mod validators; /// An internal method used to allow mocking terminal size in tests #[cfg_attr(test, mockable)] pub(crate) fn terminal_size_wrapper() -> (Width, Height) { terminal_size().unwrap() } #[derive(Debug, Clone, PartialEq)] /// Parsed options for generating a Game of Life simulation pub(crate) struct Opts { pub seed: usize, pub input: Option, pub width: isize, pub height: isize, pub live_cell: char, pub dead_cell: char, pub delay: usize, } impl Opts { /// Parse args, set the defaults for the dynamic options (like `seed`, `width`, `height`) /// and validate the conflicting options (same live/dead cell configured). /// /// The dynamic options that can be set are: /// - `seed` is set to random if it's not provided. /// - `width` is set to the current terminal screen width when not provided. /// - `height` is set to the current terminal screen height -2 when not provided. /// /// The parsed options are typed into the correct types used (from strings) /// and validated to make sure that they work together (live/dead cells must be different) /// and the delay between renders must be a positive integer (milliseconds). pub fn from(args: ArgMatches) -> Result { // Set the seed to a random generated number as default if not provided let rnd = thread_rng().gen_range(0..1_000_000); let seed = args.value_of_t::("seed").unwrap_or(rnd); // Coerce the input to Options from args let input = match args.value_of("input") { Some(val) => Some(String::from(val)), None => None, }; // Set the width and height to the maximum terminal size possible when not provided let (Width(term_width), Height(term_height)) = terminal_size_wrapper(); let max_width: isize = term_width as isize; let max_height: isize = term_height as isize - 2; let width = args.value_of_t::("width").unwrap_or(max_width); let height = args.value_of_t::("height").unwrap_or(max_height); // Unwrap the defaults of the other arguments and set them let live_cell = args.value_of_t::("live-cell")?; let dead_cell = args.value_of_t::("dead-cell")?; if live_cell == dead_cell { return Err(Error::with_description( String::from( "The arguments '--live-cell', '--dead-cell' must have different values\n", ), clap::ErrorKind::InvalidValue, )); } // unwrap the delay let delay = args.value_of_t::("delay")?; Ok(Opts { seed, input, width, height, live_cell, dead_cell, delay, }) } /// Print out the Banner info about used input or fallback to show the seed. pub fn banner(&self) -> String { match &self.input { Some(i) => format!("Input: {}", &i), None => format!("Seed: {}", &self.seed), } } } #[cfg(test)] mod from { use super::*; use crate::game_of_life; use mocktopus::mocking::*; #[test] fn no_options() { terminal_size_wrapper.mock_safe(|| MockResult::Return((Width(42), Height(42)))); let matches = game_of_life::app().try_get_matches_from(vec![""]).unwrap(); let opts = Opts::from(matches).unwrap(); assert!(matches!(opts.seed, 0..=1_000_000)); assert_eq!(opts.input, None); assert_eq!(opts.width, 42); assert_eq!(opts.height, 40); assert_eq!(opts.live_cell, '█'); assert_eq!(opts.dead_cell, ' '); assert_eq!(opts.delay, 50); } #[test] fn invalid_seed() { terminal_size_wrapper.mock_safe(|| MockResult::Return((Width(42), Height(42)))); let matches = game_of_life::app() .try_get_matches_from(vec!["", "--seed=leet"]) .unwrap(); let opts = Opts::from(matches).unwrap(); assert!(matches!(opts.seed, 0..=1_000_000)); } #[test] fn seed() { terminal_size_wrapper.mock_safe(|| MockResult::Return((Width(42), Height(42)))); let matches = game_of_life::app() .try_get_matches_from(vec!["", "--seed=1337"]) .unwrap(); let opts = Opts::from(matches).unwrap(); assert_eq!(opts.seed, 1337); } #[test] fn input() { terminal_size_wrapper.mock_safe(|| MockResult::Return((Width(42), Height(42)))); let matches = game_of_life::app() .try_get_matches_from(vec!["", "--input=file"]) .unwrap(); let opts = Opts::from(matches).unwrap(); assert_eq!(opts.input, Some(String::from("file"))); } #[test] fn width() { terminal_size_wrapper.mock_safe(|| MockResult::Return((Width(42), Height(42)))); let matches = game_of_life::app() .try_get_matches_from(vec!["", "--width=13"]) .unwrap(); let opts = Opts::from(matches).unwrap(); assert_eq!(opts.width, 13); } #[test] fn height() { terminal_size_wrapper.mock_safe(|| MockResult::Return((Width(42), Height(42)))); let matches = game_of_life::app() .try_get_matches_from(vec!["", "--height=13"]) .unwrap(); let opts = Opts::from(matches).unwrap(); assert_eq!(opts.height, 13); } #[test] fn live_cell() { terminal_size_wrapper.mock_safe(|| MockResult::Return((Width(42), Height(42)))); let matches = game_of_life::app() .try_get_matches_from(vec!["", "--live-cell=l"]) .unwrap(); let opts = Opts::from(matches).unwrap(); assert_eq!(opts.live_cell, 'l'); } #[test] fn dead_cell() { terminal_size_wrapper.mock_safe(|| MockResult::Return((Width(42), Height(42)))); let matches = game_of_life::app() .try_get_matches_from(vec!["", "--dead-cell=d"]) .unwrap(); let opts = Opts::from(matches).unwrap(); assert_eq!(opts.dead_cell, 'd'); } #[test] fn same_live_and_dead_cells() { terminal_size_wrapper.mock_safe(|| MockResult::Return((Width(42), Height(42)))); let matches = game_of_life::app() .try_get_matches_from(vec!["", "--live-cell=x", "--dead-cell=x"]) .unwrap(); let opts = Opts::from(matches); assert!(opts.is_err()); } #[test] fn delay() { terminal_size_wrapper.mock_safe(|| MockResult::Return((Width(42), Height(42)))); let matches = game_of_life::app() .try_get_matches_from(vec!["", "--delay=13"]) .unwrap(); let opts = Opts::from(matches).unwrap(); assert_eq!(opts.delay, 13); } } #[cfg(test)] mod banner { use super::*; #[test] fn with_input() { let opts = Opts { input: Some(String::from("./file")), seed: 1337, width: 4, height: 3, live_cell: 'L', dead_cell: 'D', delay: 13, }; assert_eq!(opts.banner(), "Input: ./file"); } #[test] fn without_input() { let opts = Opts { input: None, seed: 1337, width: 4, height: 3, live_cell: 'L', dead_cell: 'D', delay: 13, }; assert_eq!(opts.banner(), "Seed: 1337"); } }