Skip to content

Commit 810994f

Browse files
committed
improve(error-handling): return Result instead of panic
Instead of panicking return proper Results with custom error type. This allows for better error messages and also increases testability, allowing for more comprehensive testing of validation errors. Bring in test_case crate, which makes data-table driven tests possible and rewrite tests to take advantage of this. Also fix a bug, where upper bound of the value range wasn't treated as valid speed option.
1 parent 73380a1 commit 810994f

File tree

4 files changed

+164
-50
lines changed

4 files changed

+164
-50
lines changed

Cargo.lock

+61
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ edition = "2018"
77
[dependencies]
88
getopts = "0.2"
99
rusb = "0.8"
10+
11+
[dev-dependencies]
12+
test-case = "1.1"

src/main.rs

+60-24
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ extern crate rusb;
2020
use std::fmt::Display;
2121
use std::fmt::Formatter;
2222
use std::num::ParseIntError;
23-
use std::ops::Range;
23+
use std::ops::RangeInclusive;
2424
use std::time::Duration;
2525

2626
use getopts::{Matches, Options};
@@ -29,18 +29,22 @@ use rusb::{Device, GlobalContext};
2929
use crate::DeviceParam::{FanSpeed, PumpSpeed};
3030

3131
fn main() {
32+
if let Err(e) = run() {
33+
eprintln!("Error: {}", e);
34+
std::process::exit(1);
35+
}
36+
}
37+
38+
fn run<'e>() -> Result<(), CliValidationError<'e>> {
3239
let args: Vec<String> = std::env::args().collect();
3340
let program = args[0].clone();
3441
let opts = build_options();
3542
let opt_matches = match_opts(args, &opts);
3643

3744
if opt_matches.opt_present("h") || no_options_present(&opt_matches) {
38-
print_usage(program, opts)
45+
Ok(print_usage(program, opts))
3946
} else {
40-
match validate(opt_matches) {
41-
Ok(params) => set_params(params),
42-
Err(f) => eprintln!("Invalid options: {}", f),
43-
}
47+
validate(opt_matches).map(set_params)
4448
}
4549
}
4650

@@ -54,30 +58,61 @@ fn set_params(params: Vec<DeviceParam>) {
5458
kraken.reattach();
5559
}
5660

57-
fn validate(opt_matches: Matches) -> Result<Vec<DeviceParam>, ParseIntError> {
58-
let range: Range<u8> = 10..100;
59-
60-
let mut params: Vec<DeviceParam> = Vec::new();
61+
#[derive(Debug)]
62+
struct CliValidationError<'e> {
63+
value: String,
64+
param: &'e str,
65+
reason: String,
66+
}
6167

62-
if let Some(fan_speed) = opt_matches.opt_get("f")? {
63-
assert!(
64-
range.contains(&fan_speed),
65-
"FANSPEED should be within range 10-100"
66-
);
67-
params.push(FanSpeed(fan_speed))
68+
impl Display for CliValidationError<'_> {
69+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
70+
write!(
71+
f,
72+
"Value '{}' is not valid for parameter '{}': {}. Try --help.",
73+
self.value, self.param, self.reason
74+
)
6875
}
76+
}
6977

70-
if let Some(pump_speed) = opt_matches.opt_get("p")? {
71-
assert!(
72-
range.contains(&pump_speed),
73-
"PUMPSPEED should be within range 10-100"
74-
);
75-
params.push(PumpSpeed(pump_speed))
76-
}
78+
fn validate<'e>(opt_matches: Matches) -> Result<Vec<DeviceParam>, CliValidationError<'e>> {
79+
let range = 10..=100;
7780

81+
let mut params: Vec<DeviceParam> = Vec::new();
82+
parse_param(&opt_matches, &range, "fan-speed", FanSpeed)?.map(|dp| params.push(dp));
83+
parse_param(&opt_matches, &range, "pump-speed", PumpSpeed)?.map(|dp| params.push(dp));
7884
Ok(params)
7985
}
8086

87+
fn parse_param<'e>(
88+
opt_matches: &Matches,
89+
range: &RangeInclusive<u8>,
90+
param: &'e str,
91+
creator: fn(u8) -> DeviceParam,
92+
) -> Result<Option<DeviceParam>, CliValidationError<'e>> {
93+
opt_matches
94+
.opt_get(param)
95+
.or_else(|f: ParseIntError| {
96+
Err(CliValidationError {
97+
param,
98+
value: opt_matches.opt_str(param).unwrap(),
99+
reason: f.to_string(),
100+
})
101+
})?
102+
.map(|fan_speed| {
103+
if range.contains(&fan_speed) {
104+
Ok(Some(creator(fan_speed)))
105+
} else {
106+
Err(CliValidationError {
107+
param,
108+
value: fan_speed.to_string(),
109+
reason: "out of range".to_string(),
110+
})
111+
}
112+
})
113+
.unwrap_or_else(|| Ok(None))
114+
}
115+
81116
fn no_options_present(opt_matches: &Matches) -> bool {
82117
!opt_matches.opt_present("f") && !opt_matches.opt_present("p")
83118
}
@@ -104,7 +139,7 @@ fn build_options() -> Options {
104139
"p",
105140
"pump-speed",
106141
"Pump speed in 10 - 100 range.",
107-
"PUMPSEED",
142+
"PUMPSPEED",
108143
);
109144
opts
110145
}
@@ -176,6 +211,7 @@ impl KrakenControl for Kraken {
176211
}
177212
}
178213

214+
#[derive(Debug, PartialOrd, PartialEq)]
179215
enum DeviceParam {
180216
FanSpeed(u8),
181217
PumpSpeed(u8),

src/tests.rs

+40-26
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,51 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
#[cfg(test)]
18-
use super::*;
17+
use test_case::test_case;
1918

20-
#[test]
21-
#[should_panic(expected = "FANSPEED should be within range 10-100")]
22-
fn fan_speed_should_fail_validation_if_value_less_than_range() {
23-
let opts = build_options();
24-
let command_line = vec!["krak", "-f9"];
25-
opts.parse(command_line).map(|r| validate(r));
26-
}
19+
use super::*;
2720

28-
#[test]
29-
#[should_panic(expected = "FANSPEED should be within range 10-100")]
30-
fn fan_speed_should_fail_validation_if_value_more_than_range() {
21+
#[test_case("--fan-speed", 9; "FANSPEED less than range min")]
22+
#[test_case("--fan-speed", 101; "FANSPEED more than range max")]
23+
#[test_case("--pump-speed", 9; "PUMPSPEED less than range min")]
24+
#[test_case("--pump-speed", 101; "PUMPSPEED more than range max")]
25+
fn should_fail_validation_if_speed_is_outside_of_range(test_param: &str, test_value: u8) {
3126
let opts = build_options();
32-
let command_line = vec!["krak", "-f101"];
33-
opts.parse(command_line).map(|r| validate(r));
27+
let fanspeed = format!("{}={}", test_param, test_value);
28+
let command_line = vec!["krak", fanspeed.as_str()];
29+
let result = opts.parse(command_line).map(|r| validate(r)).unwrap();
30+
assert!(result.is_err());
31+
if let Err(CliValidationError {
32+
value,
33+
param,
34+
reason,
35+
}) = result
36+
{
37+
assert_eq!(test_value.to_string(), value);
38+
assert_eq!("out of range", reason);
39+
assert_eq!(param, param)
40+
}
3441
}
3542

36-
#[test]
37-
#[should_panic(expected = "PUMPSPEED should be within range 10-100")]
38-
fn pump_speed_should_fail_validation_if_value_less_than_range() {
43+
#[test_case("--fan-speed", 10, FanSpeed(10); "FANSPEED is range.min")]
44+
#[test_case("--fan-speed", 42, FanSpeed(42); "FANSPEED is inside range")]
45+
#[test_case("--fan-speed", 100, FanSpeed(100); "FANSPEED is range.max")]
46+
#[test_case("--pump-speed", 10, PumpSpeed(10); "PUMPSPEED is range.min")]
47+
#[test_case("--pump-speed", 42, PumpSpeed(42); "PUMPSPEED is inside range")]
48+
#[test_case("--pump-speed", 100, PumpSpeed(100); "PUMPSPEED is range.max")]
49+
fn should_pass_validation_if_speed_is_in_range(
50+
test_param: &str,
51+
test_value: u8,
52+
test_opt: DeviceParam,
53+
) {
3954
let opts = build_options();
40-
let command_line = vec!["krak", "-p9"];
41-
opts.parse(command_line).map(|r| validate(r));
42-
}
55+
let fanspeed = format!("{}={}", test_param, test_value);
56+
let command_line = vec!["krak", fanspeed.as_str()];
57+
let result = opts.parse(command_line).map(|r| validate(r)).unwrap();
4358

44-
#[test]
45-
#[should_panic(expected = "PUMPSPEED should be within range 10-100")]
46-
fn pump_speed_should_fail_validation_if_value_more_than_range() {
47-
let opts = build_options();
48-
let command_line = vec!["krak", "-p101"];
49-
opts.parse(command_line).map(|r| validate(r));
59+
assert!(result.is_ok());
60+
if let Ok(vec) = result {
61+
assert_eq!(vec.len(), 1);
62+
assert_eq!(vec.first().unwrap(), &test_opt);
63+
}
5064
}

0 commit comments

Comments
 (0)