//! Game of Life module for generating the CLI app, validating the options //! and starting the simulation use clap::{crate_authors, crate_version, App, Arg, Error}; /// Options module for validations and Opts struct pub(crate) mod opts; use opts::{validators, Opts}; /// Generators Module to provide interface for generating universes /// using Seed or File input pub(crate) mod generators; /// The different building blocks used in Universe simulations pub(crate) mod simulation; use simulation::universe::Universe; /// Define the Clap CLI App for running Game of Life simulation /// /// Using Clap to define the available options that can be used with game of life pub(crate) fn app() -> App<'static> { App::new("Game of Life") .author(crate_authors!()) .version(crate_version!()) .arg( Arg::new("seed") .about("Specify the seed number to use as an initial state [default: random]") .takes_value(true) .long("seed") .short('s') .conflicts_with("input") .display_order(0) ) .arg( Arg::new("input") .about("Specify the path/URL for the file to use as an initial state. (used instead of seed)") .takes_value(true) .long("input") .short('i') .display_order(1) ) .arg( Arg::new("width") .about("Specify the width of generated universe. [default: terminal width]") .takes_value(true) .long("width") .validator(validators::is_fitting_term_width) .display_order(2) ) .arg( Arg::new("height") .about("Specify the width of generated universe. [default: terminal height]") .takes_value(true) .long("height") .validator(validators::is_fitting_term_height) .display_order(3) ) .arg( Arg::new("live-cell") .about("Specify the live-cell representation") .takes_value(true) .long("live-cell") .default_value("█") .display_order(4) ) .arg( Arg::new("dead-cell") .about("Specify the dead-cell representation") .takes_value(true) .long("dead-cell") .default_value(" ") .display_order(5) ) .arg( Arg::new("delay") .about("Specify the introduced delay between each generation") .takes_value(true) .short('d') .long("delay") .default_value("50") .validator(validators::is_positive) .display_order(6) ) } #[cfg(test)] mod app { use super::*; #[test] fn no_options() { let matches = app().get_matches_from(vec![""]); assert_eq!(matches.value_of("seed"), None); assert_eq!(matches.value_of("input"), None); assert_eq!(matches.value_of("width"), None); assert_eq!(matches.value_of("height"), None); assert_eq!(matches.value_of("live-cell"), Some("█")); assert_eq!(matches.value_of("dead-cell"), Some(" ")); assert_eq!(matches.value_of("delay"), Some("50")); } #[test] fn seed() { let matches = app().get_matches_from(vec!["", "--seed=1337"]); assert_eq!(matches.value_of("seed"), Some("1337")); } #[test] fn input() { let matches = app().get_matches_from(vec!["", "--input=file"]); assert_eq!(matches.value_of("input"), Some("file")); } #[test] fn seed_and_input() { let matches = app().try_get_matches_from(vec!["", "--seed=1337", "--input=file"]); assert!(matches.is_err()); } #[test] fn valid_width() { let matches = app().get_matches_from(vec!["", "--width=13"]); assert_eq!(matches.value_of("width"), Some("13")); } #[test] fn valid_height() { let matches = app().get_matches_from(vec!["", "--height=13"]); assert_eq!(matches.value_of("height"), Some("13")); } #[test] fn invalid_width() { let matches = app().try_get_matches_from(vec!["", "--width=1337"]); assert!(matches.is_err()); } #[test] fn invalid_height() { let matches = app().try_get_matches_from(vec!["", "--height=1337"]); assert!(matches.is_err()); } #[test] fn valid_delay() { let matches = app().get_matches_from(vec!["", "--delay=13"]); assert_eq!(matches.value_of("delay"), Some("13")); } #[test] fn invalid_delay() { let matches = app().try_get_matches_from(vec!["", "--delay=-13"]); assert!(matches.is_err()); } #[test] fn live_and_dead_cells() { let matches = app().get_matches_from(vec!["", "--live-cell=x", "--dead-cell=x"]); assert_eq!(matches.value_of("live-cell"), Some("x")); assert_eq!(matches.value_of("dead-cell"), Some("x")); } } /// Generate a universe based on the options /// /// If an input is provided, the input will be used (taking precedence over seed) /// Else use the seed option (defaults to random when not provided). pub(crate) fn generate(opts: Opts) -> Result { match opts.input { Some(_) => generators::input::new(opts), None => generators::seed::new(opts), } } #[cfg(test)] mod generate { use super::*; use crate::game_of_life::simulation::plane::Plane; use mocktopus::mocking::*; fn universe_mock(opts: Opts) -> Universe { Universe { opts, current: Plane(vec![Plane(vec![])]), future: Plane(vec![Plane(vec![])]), } } #[test] fn without_input() { let opts = Opts { seed: 1337, input: None, width: 13, height: 13, live_cell: 'L', dead_cell: 'D', delay: 13, }; generators::seed::new.mock_safe(|opts| MockResult::Return(Ok(universe_mock(opts)))); generators::input::new.mock_safe(|_| panic!()); let universe = generate(opts.clone()).unwrap(); assert_eq!(universe, universe_mock(opts)); } #[test] fn with_input() { let opts = Opts { seed: 1337, input: Some(String::from("input")), width: 13, height: 13, live_cell: 'L', dead_cell: 'D', delay: 13, }; generators::seed::new.mock_safe(|_| panic!()); generators::input::new.mock_safe(|opts| MockResult::Return(Ok(universe_mock(opts)))); let universe = generate(opts.clone()).unwrap(); assert_eq!(universe, universe_mock(opts)); } }