1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
|
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;
#[derive(Debug, Clone)]
/// Parsed options for generating a Game of Life simulation
pub(crate) struct Opts {
pub seed: usize,
pub input: Option<String>,
pub width: isize,
pub height: isize,
pub live_cell: char,
pub dead_cell: char,
pub delay: usize,
}
#[cfg_attr(test, mockable)]
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<Opts, Error> {
// 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::<usize>("seed").unwrap_or(rnd);
// Coerce the input to Options<String> 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().unwrap();
let max_width: isize = term_width as isize;
let max_height: isize = term_height as isize - 2;
let width = args.value_of_t::<isize>("width").unwrap_or(max_width);
let height = args.value_of_t::<isize>("height").unwrap_or(max_height);
// Unwrap the defaults of the other arguments and set them
let live_cell = args.value_of_t::<char>("live-cell")?;
let dead_cell = args.value_of_t::<char>("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::<usize>("delay")?;
Ok(Opts {
seed,
input,
width,
height,
live_cell,
dead_cell,
delay,
})
}
}
#[cfg(test)]
mod from {
use super::*;
use crate::game_of_life;
#[test]
fn no_options() {
let (Width(term_width), Height(term_height)) = terminal_size().unwrap();
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, term_width as isize);
assert_eq!(opts.height, term_height as isize - 2);
assert_eq!(opts.live_cell, '█');
assert_eq!(opts.dead_cell, ' ');
assert_eq!(opts.delay, 50);
}
#[test]
fn invalid_seed() {
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() {
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() {
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() {
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() {
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() {
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() {
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() {
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() {
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);
}
}
|