1use std::path::PathBuf;
4use std::time::Duration;
5
6use clap::{
7 builder::{
8 IntoResettable, OsStr, PossibleValue,
9 Resettable::{self, *},
10 },
11 ArgGroup, Parser, ValueEnum, ValueHint,
12};
13use clap_complete::Shell;
14
15fn is_number(s: &str) -> bool {
16 s.parse::<u64>().is_ok()
17}
18
19fn parse_duration(arg: &str) -> Result<Duration, std::num::ParseIntError> {
20 if is_number(arg) {
21 return Ok(Duration::from_secs(arg.parse()?));
22 }
23
24 let mut input = arg;
25 if input.ends_with("s") {
26 input = &arg[..arg.len() - 1]
27 }
28
29 let seconds = input.parse()?;
30 Ok(Duration::from_secs(seconds))
31}
32
33fn parse_percentiles(arg: &str) -> anyhow::Result<f32> {
34 let value = arg.parse::<f32>()?;
35 if value <= 0f32 || value >= 1f32 {
36 anyhow::bail!("{} must be limited to the range (0 to 1)", value);
37 }
38 Ok(value)
39}
40
41fn parse_filename_and_path(s: &str) -> anyhow::Result<(String, PathBuf)> {
42 let pos = s.find(':').ok_or(anyhow::anyhow!(
43 "invalid filename:filepath,: no `:` found in `{s}`"
44 ))?;
45 Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
46}
47
48#[derive(Debug, Clone, Copy)]
50pub enum OutputFormat {
51 Text,
53 Json,
55}
56
57impl IntoResettable<OsStr> for OutputFormat {
58 fn into_resettable(self) -> Resettable<OsStr> {
59 match self {
60 OutputFormat::Text => Value(OsStr::from("TEXT")),
61 OutputFormat::Json => Value(OsStr::from("JSON")),
62 }
63 }
64}
65
66impl ValueEnum for OutputFormat {
67 fn value_variants<'a>() -> &'a [Self] {
68 &[OutputFormat::Text, OutputFormat::Json]
69 }
70
71 fn to_possible_value<'a>(&self) -> Option<PossibleValue> {
72 Some(match self {
73 OutputFormat::Text => PossibleValue::new("TEXT"),
74 OutputFormat::Json => PossibleValue::new("JSON"),
75 })
76 }
77}
78
79#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
81pub enum Method {
82 Get,
84 Post,
86 Put,
88 Delete,
90 Head,
92 Patch,
94}
95
96impl Method {
97 pub(crate) fn to_reqwest_method(self) -> reqwest::Method {
99 match self {
100 Method::Get => reqwest::Method::GET,
101 Method::Post => reqwest::Method::POST,
102 Method::Put => reqwest::Method::PUT,
103 Method::Patch => reqwest::Method::PATCH,
104 Method::Delete => reqwest::Method::DELETE,
105 Method::Head => reqwest::Method::HEAD,
106 }
107 }
108}
109
110impl IntoResettable<OsStr> for Method {
111 fn into_resettable(self) -> Resettable<OsStr> {
112 match self {
113 Method::Get => Value(OsStr::from("GET")),
114 Method::Post => Value(OsStr::from("POST")),
115 Method::Put => Value(OsStr::from("PUT")),
116 Method::Delete => Value(OsStr::from("DELETE")),
117 Method::Head => Value(OsStr::from("HEAD")),
118 Method::Patch => Value(OsStr::from("PATCH")),
119 }
120 }
121}
122
123impl ValueEnum for Method {
124 fn value_variants<'a>() -> &'a [Self] {
125 &[
126 Method::Get,
127 Method::Put,
128 Method::Post,
129 Method::Delete,
130 Method::Head,
131 Method::Patch,
132 ]
133 }
134
135 fn to_possible_value<'a>(&self) -> Option<PossibleValue> {
136 Some(match self {
137 Method::Get => PossibleValue::new("GET"),
138 Method::Put => PossibleValue::new("PUT"),
139 Method::Post => PossibleValue::new("POST"),
140 Method::Delete => PossibleValue::new("DELETE"),
141 Method::Head => PossibleValue::new("HEAD"),
142 Method::Patch => PossibleValue::new("PATCH"),
143 })
144 }
145}
146
147#[derive(Debug, Parser)]
149#[clap(color = concolor_clap::color_choice())]
150#[command(author, version, about, allow_missing_positional(true))]
151#[command(group(ArgGroup::new("json").args(["json_body", "json_file"])))]
152#[command(group(ArgGroup::new("text").args(["text_body", "text_file"])))]
153#[command(group(ArgGroup::new("multipart").args(["mp", "mp_file"]).multiple(true)))]
154#[command(group(ArgGroup::new("mode").args(["duration", "requests"])))]
155#[command(help_template(
156 "\
157{before-help}{name}({version}){tab}{about-with-newline}
158{usage-heading} {usage}
159
160{all-args}{after-help}\
161"
162))]
163pub struct Arg {
164 #[arg(
166 long,
167 short,
168 default_value_t = 50,
169 help = "Maximum number of concurrent connections"
170 )]
171 pub connections: u16,
172
173 #[arg(
175 long,
176 short,
177 value_parser = parse_duration,
178 default_value = "30s",
179 help = "Socket/request timeout"
180 )]
181 pub(crate) timeout: Duration,
182
183 #[arg(long, short, help = "Print latency statistics")]
185 pub(crate) latencies: bool,
186
187 #[arg(
189 long,
190 num_args = 0..,
191 value_parser = parse_percentiles,
192 value_delimiter = ',',
193 default_value = "0.5,0.75,0.9,0.99",
194 help = "Custom latency percentiles"
195 )]
196 pub(crate) percentiles: Vec<f32>,
197
198 #[arg(
200 long,
201 short,
202 default_value = Method::Get,
203 value_enum,
204 help = "Request method"
205 )]
206 pub method: Method,
207
208 #[arg(long, short = 'a', help = "Disable HTTP keep-alive")]
210 pub(crate) disable_keep_alive: bool,
211
212 #[arg(
213 long,
214 short = 'H',
215 num_args = 0..,
216 value_delimiter = ',',
217 help = "HTTP headers to use, example: -H=k:v,k1:v1 -H=k2:v2"
218 )]
219 pub(crate) headers: Vec<String>,
220
221 #[arg(
223 long,
224 short = 'n',
225 help = "Number of requests",
226 required_unless_present_any(["duration", "completions"])
227 )]
228 pub requests: Option<u64>,
229
230 #[arg(
232 long,
233 short = 'd',
234 value_parser = parse_duration,
235 help = "Duration of test",
236 required_unless_present_any(["requests", "completions"])
237 )]
238 pub duration: Option<Duration>,
239
240 #[arg(long, short = 'r', help = "Rate limit in requests per second")]
242 pub(crate) rate: Option<u16>,
243
244 #[arg(
246 long,
247 value_hint = ValueHint::FilePath,
248 requires("key"),
249 help = "Path to the client's TLS Certificate"
250 )]
251 pub(crate) cert: Option<PathBuf>,
252
253 #[arg(
255 long,
256 requires("cert"),
257 value_hint = ValueHint::FilePath,
258 help = "Path to the client's TLS Certificate Private Key"
259 )]
260 pub(crate) key: Option<PathBuf>,
261
262 #[arg(
265 long,
266 short = 'k',
267 help = "Controls whether a client verifies the server's certificate chain and host name"
268 )]
269 pub(crate) insecure: bool,
270
271 #[arg(
273 long,
274 value_hint = ValueHint::FilePath,
275 conflicts_with_all(["mp_file", "mp", "form", "text_body", "text_file", "json_body", "json_command"]),
276 help = "File to use as Request body for ContentType: application/json"
277 )]
278 pub(crate) json_file: Option<PathBuf>,
279
280 #[arg(
282 long,
283 conflicts_with_all(["mp_file", "mp", "form", "text_body", "text_file", "json_file", "json_command"]),
284 help = "Request body for ContentType: application/json")]
285 pub(crate) json_body: Option<String>,
286
287 #[arg(
289 long,
290 conflicts_with_all(["mp_file", "mp", "form", "text_body", "text_file", "json_file", "json_body"]),
291 help = "Build request body from external command for ContentType: application/json")]
292 pub(crate) json_command: Option<String>,
293
294 #[arg(
296 long,
297 value_hint = ValueHint::FilePath,
298 conflicts_with_all(["mp_file", "mp", "form", "text_body", "json_file", "json_body"]),
299 help = "File to use as Request body for ContentType: text/plain"
300 )]
301 pub(crate) text_file: Option<PathBuf>,
302
303 #[arg(
305 long,
306 conflicts_with_all(["mp_file", "mp", "form", "text_file", "json_file", "json_body"]),
307 help = "Request body for ContentType: text/plain"
308 )]
309 pub(crate) text_body: Option<String>,
310
311 #[arg(
313 long,
314 num_args = 0..,
315 value_delimiter = ',',
316 conflicts_with_all(["form", "text_body", "text_file", "json_file", "json_body"]),
317 help = "Multipart body parameters, Content-Type: multipart/form-data, example: --mp=k1:v1,k2:v2"
318 )]
319 pub(crate) mp: Vec<String>,
320
321 #[arg(
323 long,
324 num_args = 0..,
325 value_delimiter = ',',
326 value_parser = parse_filename_and_path,
327 conflicts_with_all(["form", "text_body", "text_file", "json_file", "json_body"]),
328 help = "Multipart body files, Content-Type: multipart/form-data, example: --mp-file=t1:t1.txt,filename2:t2.txt"
329 )]
330 pub(crate) mp_file: Vec<(String, PathBuf)>,
331
332 #[arg(
334 long,
335 num_args = 0..,
336 value_delimiter = ',',
337 conflicts_with_all(["mp_file", "mp", "text_body", "text_file", "json_file", "json_file"]),
338 help = "Form request parameters, Content-Type: application/x-www-form-urlencoded, example: --form=k:v,k1:v1"
339 )]
340 pub(crate) form: Vec<String>,
341
342 #[arg(
344 long,
345 default_value = OutputFormat::Text,
346 value_enum,
347 help = "Output format"
348 )]
349 pub output_format: OutputFormat,
350
351 #[arg(long, value_enum)]
354 pub completions: Option<Shell>,
355
356 #[arg(
358 required_unless_present("completions"),
359 value_hint = ValueHint::Url,
360 help = "Target Url"
361 )]
362 pub url: Option<String>,
363}
364
365#[cfg(test)]
366mod tests {
367 use clap::{Command, CommandFactory};
368
369 use super::*;
370 const URI: &str = "https://localhost/test";
371 const BINARY: &str = "rsb";
372
373 #[test]
374 fn test_is_number() {
375 assert!(is_number("12234"));
376 assert!(!is_number("12234s"));
377 }
378
379 #[test]
380 fn test_parse_duration() {
381 assert!(parse_duration("123").is_ok());
382 assert!(parse_duration("123s").is_ok());
383 assert!(parse_duration("123x").is_err());
384 }
385
386 #[test]
387 fn test_method_choices() {
388 let mut cmd = Arg::command();
389 let methods = vec!["GET", "PUT", "POST", "DELETE", "HEAD", "PATCH"];
390 for method in methods {
391 let args = vec![BINARY, "-n", "20", "-m", method, URI];
392 let result = cmd.try_get_matches_from_mut(args);
393 assert!(result.is_ok());
394 }
395
396 let result = cmd.try_get_matches_from_mut(vec![
397 BINARY, "-n", "20", "-m", "get", URI,
398 ]);
399 assert!(result.as_ref().is_err());
400 let err_msg = result.err().unwrap().to_string();
401 assert!(err_msg
402 .contains("possible values: GET, PUT, POST, DELETE, HEAD, PATCH"));
403 }
404
405 #[test]
406 fn test_required_parameters() {
407 let mut cmd = Arg::command();
408 let result = cmd.try_get_matches_from_mut(vec!["rsb"]);
409 assert!(result.as_ref().is_err());
410 let err_msg = result.err().unwrap().to_string();
411 assert!(err_msg.contains(
412 "error: the following required arguments were not provided:
413 --requests <REQUESTS>
414 --duration <DURATION>
415 <URL>"
416 ))
417 }
418
419 #[test]
420 fn test_must_provide_requests_or_duration_parameters() {
421 let mut cmd = Arg::command();
422
423 let result = cmd.try_get_matches_from_mut(vec![BINARY, URI]);
425 assert!(result.as_ref().is_err());
426 let err_msg = result.err().unwrap().to_string();
427 assert!(err_msg.contains(
428 "error: the following required arguments were not provided:
429 --requests <REQUESTS>
430 --duration <DURATION>"
431 ));
432
433 let result = cmd.try_get_matches_from_mut(vec![
435 BINARY, "-n", "20", "-d", "300", URI,
436 ]);
437 assert!(result.as_ref().is_err());
438 let err_msg = result.err().unwrap().to_string();
439 assert!(err_msg.contains(
440 "error: the argument '--requests <REQUESTS>' \
441 cannot be used with '--duration <DURATION>'"
442 ));
443 }
444
445 #[test]
446 fn test_require_key_and_cert_at_the_same_time() {
447 let mut cmd = Arg::command();
448
449 let result = cmd.try_get_matches_from_mut(vec![
451 BINARY, "-n", "20", "--key", "key.crt", URI,
452 ]);
453 assert!(result.as_ref().is_err());
454 let err_msg = result.err().unwrap().to_string();
455 assert!(err_msg.contains(
456 "error: the following required arguments were not provided:
457 --cert <CERT>"
458 ));
459
460 let result = cmd.try_get_matches_from_mut(vec![
462 BINARY, "-n", "20", "--cert", "cert.crt", URI,
463 ]);
464 assert!(result.as_ref().is_err());
465 let err_msg = result.err().unwrap().to_string();
466 assert!(err_msg.contains(
467 "error: the following required arguments were not provided:
468 --key <KEY>"
469 ));
470
471 let result = cmd.try_get_matches_from_mut(vec![
473 BINARY, "-n", "20", "--cert", "cert.crt", "--key", "key.crt", URI,
474 ]);
475 assert!(result.as_ref().is_ok());
476 }
477
478 #[test]
479 fn test_json_body_or_json_file() {
480 let mut cmd = Arg::command();
481
482 let result = cmd.try_get_matches_from_mut(vec![
484 BINARY,
485 "-n",
486 "20",
487 "--json-body",
488 "jsonBody",
489 "--json-file",
490 "jsonBody.txt",
491 URI,
492 ]);
493 assert!(result.as_ref().is_err());
494
495 let result = cmd.try_get_matches_from_mut(vec![
497 BINARY,
498 "-n",
499 "20",
500 "--json-body",
501 "jsonBody",
502 URI,
503 ]);
504 assert!(result.as_ref().is_ok());
505
506 let result = cmd.try_get_matches_from_mut(vec![
508 BINARY,
509 "-n",
510 "20",
511 "--json-file",
512 "jsonBody.txt",
513 URI,
514 ]);
515 assert!(result.as_ref().is_ok());
516 }
517
518 #[test]
519 fn test_json_body_conflicts_with_other_body() {
520 let cmd = Arg::command();
521 let args =
522 vec![BINARY, "-n", "20", "--json-body", "jb", "xx", "xx", URI];
523 let conflicts_params = vec![
524 "--mp-file",
525 "--mp",
526 "--form",
527 "--text-body",
528 "--text-file",
529 "--json-file",
530 "--json-command",
531 ];
532 validate_args_conflict(conflicts_params, args, cmd);
533 }
534
535 #[test]
536 fn test_json_command_conflicts_with_other_body() {
537 let cmd = Arg::command();
538 let args = vec![
539 BINARY,
540 "-n",
541 "20",
542 "--json-command",
543 "/usr/bin/command generate",
544 "xx",
545 "xx",
546 URI,
547 ];
548 let conflicts_params = vec![
549 "--mp-file",
550 "--mp",
551 "--form",
552 "--text-body",
553 "--text-file",
554 "--json-file",
555 "--json-body",
556 ];
557 validate_args_conflict(conflicts_params, args, cmd);
558 }
559
560 #[test]
561 fn test_json_file_conflicts_with_other_body() {
562 let cmd = Arg::command();
563 let args =
564 vec![BINARY, "-n", "20", "--json-file", "jb", "xx", "xx", URI];
565 let conflicts_params = vec![
566 "--mp-file",
567 "--mp",
568 "--form",
569 "--text-body",
570 "--text-file",
571 "--json-body",
572 "--json-command",
573 ];
574 validate_args_conflict(conflicts_params, args, cmd);
575 }
576
577 #[test]
578 fn test_text_body_or_text_file() {
579 let mut cmd = Arg::command();
580
581 let result = cmd.try_get_matches_from_mut(vec![
583 BINARY,
584 "-n",
585 "20",
586 "--text-body",
587 "text",
588 "--text-file",
589 "ts.txt",
590 URI,
591 ]);
592 assert!(result.as_ref().is_err());
593
594 let result = cmd.try_get_matches_from_mut(vec![
596 BINARY,
597 "-n",
598 "20",
599 "--text-body",
600 "text",
601 URI,
602 ]);
603 assert!(result.as_ref().is_ok());
604
605 let result = cmd.try_get_matches_from_mut(vec![
607 BINARY,
608 "-n",
609 "20",
610 "--text-file",
611 "jsonBody.txt",
612 URI,
613 ]);
614 assert!(result.as_ref().is_ok());
615 }
616
617 #[test]
618 fn test_text_body_conflicts_with_other_body() {
619 let cmd = Arg::command();
620 let args =
621 vec![BINARY, "-n", "20", "--text-body", "jb", "xx", "xx", URI];
622 let conflicts_params = vec![
623 "--mp-file",
624 "--mp",
625 "--form",
626 "--json-body",
627 "--text-file",
628 "--json-file",
629 ];
630 validate_args_conflict(conflicts_params, args, cmd);
631 }
632
633 fn validate_args_conflict<'a>(
634 conflicts_params: Vec<&'a str>,
635 mut args: Vec<&'a str>,
636 mut cmd: Command,
637 ) {
638 for cp in conflicts_params {
639 args[5] = cp;
640 if cp == "--mp-file" {
641 args[6] = "filename1:file2.txt";
642 }
643 let result = cmd.try_get_matches_from_mut(args.clone());
644 assert!(result.as_ref().is_err());
645 let err_msg = result.err().unwrap().to_string();
646 let fragment = format!("cannot be used with '{cp}");
647 assert!(err_msg.contains(&fragment));
648 }
649 }
650
651 #[test]
652 fn test_text_file_conflicts_with_other_body() {
653 let cmd = Arg::command();
654 let args =
655 vec![BINARY, "-n", "20", "--text-file", "jb", "xx", "xx", URI];
656 let conflicts_params = vec![
657 "--mp-file",
658 "--mp",
659 "--form",
660 "--text-body",
661 "--json-file",
662 "--json-body",
663 "--json-command",
664 ];
665 validate_args_conflict(conflicts_params, args, cmd);
666 }
667
668 #[test]
669 fn test_multipart_param_exist_with_multipart_file() {
670 let mut cmd = Arg::command();
671 let args = vec![
672 BINARY,
673 "-n",
674 "20",
675 "--mp=k1:v1,k2:v2",
676 "--mp-file=t1:text1.txt,t2:text2.txt",
677 URI,
678 ];
679 let result = cmd.try_get_matches_from_mut(args);
680 assert!(result.as_ref().is_ok());
681 let mps = result
682 .as_ref()
683 .unwrap()
684 .get_many::<String>("mp")
685 .unwrap()
686 .collect::<Vec<_>>();
687 assert_eq!(mps, ["k1:v1", "k2:v2"]);
688
689 let mp_files = result
690 .as_ref()
691 .unwrap()
692 .get_many::<(String, PathBuf)>("mp_file")
693 .unwrap()
694 .collect::<Vec<_>>();
695 let mp_files = mp_files
696 .iter()
697 .map(|x| (x.0.as_str(), x.1.to_str().unwrap()))
698 .collect::<Vec<_>>();
699 assert_eq!(mp_files, [("t1", "text1.txt"), ("t2", "text2.txt")]);
700 }
701
702 #[test]
703 fn test_mp_conflicts_with_other_body() {
704 let cmd = Arg::command();
705 let args = vec![
706 BINARY,
707 "-n",
708 "20",
709 "--mp=k1:v1,k2:v2",
710 "--mp-file=text1:text1.txt",
711 "xx",
712 "xx",
713 URI,
714 ];
715 let conflicts_params = vec![
716 "--text-file",
717 "--form",
718 "--text-body",
719 "--json-file",
720 "--json-body",
721 "--json-command",
722 ];
723 validate_args_conflict(conflicts_params, args, cmd);
724 }
725
726 #[test]
727 fn test_parse_form_params() {
728 let mut cmd = Arg::command();
729
730 let args = vec![BINARY, "-n", "20", "--form=k1:v1,k2:v2", URI];
732 let result = cmd.try_get_matches_from_mut(args);
733 assert!(result.as_ref().is_ok());
734 let forms = result
735 .as_ref()
736 .unwrap()
737 .get_many::<String>("form")
738 .unwrap()
739 .collect::<Vec<_>>();
740 assert_eq!(forms, ["k1:v1", "k2:v2"]);
741
742 let args =
744 vec![BINARY, "-n", "20", "--form=k1:v1", "--form=k2:v2", URI];
745 let result = cmd.try_get_matches_from_mut(args);
746 assert!(result.as_ref().is_ok());
747 let forms = result
748 .as_ref()
749 .unwrap()
750 .get_many::<String>("form")
751 .unwrap()
752 .collect::<Vec<_>>();
753 assert_eq!(forms, ["k1:v1", "k2:v2"]);
754 }
755
756 #[test]
757 fn test_form_conflicts_with_other_body() {
758 let cmd = Arg::command();
759 let args = vec![
760 BINARY,
761 "-n",
762 "20",
763 "--form=k1:v1,k2:v2",
764 "--form=k3:v3,k4:v4",
765 "xx",
766 "xx",
767 URI,
768 ];
769 let conflicts_params = vec![
770 "--text-file",
771 "--mp-file",
772 "--mp",
773 "--text-body",
774 "--json-file",
775 "--json-body",
776 "--json-command",
777 ];
778 validate_args_conflict(conflicts_params, args, cmd);
779 }
780
781 #[test]
782 fn test_parse_headers_params() {
783 let mut cmd = Arg::command();
784
785 let args =
787 vec![BINARY, "-n", "20", "-H=k1:v1,k2:v2", "--headers=k3:v3", URI];
788 let result = cmd.try_get_matches_from_mut(args);
789 assert!(result.as_ref().is_ok());
790 let headers = result
791 .as_ref()
792 .unwrap()
793 .get_many::<String>("headers")
794 .unwrap()
795 .collect::<Vec<_>>();
796 assert_eq!(headers, ["k1:v1", "k2:v2", "k3:v3"]);
797 }
798
799 #[test]
800 fn test_parse_percentiles_params() {
801 let mut cmd = Arg::command();
802
803 let args = vec![BINARY, "-n", "20", URI];
805 let result = cmd.try_get_matches_from_mut(args);
806 assert!(result.as_ref().is_ok());
807 let percentiles = result
808 .as_ref()
809 .unwrap()
810 .get_many::<f32>("percentiles")
811 .unwrap()
812 .collect::<Vec<_>>();
813 assert_eq!(format!("{percentiles:?}"), "[0.5, 0.75, 0.9, 0.99]");
814
815 let args =
817 vec![BINARY, "-n", "20", "--percentiles=0.60,0.80,0.90,0.95", URI];
818 let result = cmd.try_get_matches_from_mut(args);
819 assert!(result.as_ref().is_ok());
820 let percentiles = result
821 .as_ref()
822 .unwrap()
823 .get_many::<f32>("percentiles")
824 .unwrap()
825 .collect::<Vec<_>>();
826 assert_eq!(format!("{percentiles:?}"), "[0.6, 0.8, 0.9, 0.95]");
827
828 let args = vec![BINARY, "-n", "20", "--percentiles=-0.1", URI];
830 let result = cmd.try_get_matches_from_mut(args);
831 assert!(result
832 .err()
833 .unwrap()
834 .to_string()
835 .contains("-0.1 must be limited to the range (0 to 1)"));
836
837 let args = vec![BINARY, "-n", "20", "--percentiles=1", URI];
839 let result = cmd.try_get_matches_from_mut(args);
840 assert!(result
841 .err()
842 .unwrap()
843 .to_string()
844 .contains("1 must be limited to the range (0 to 1)"));
845 }
846
847 #[test]
848 fn test_parse_output_format() {
849 let mut cmd = Arg::command();
850
851 let args = vec![BINARY, "-n", "20", "--output-format", "TEXT", URI];
853 let result = cmd.try_get_matches_from_mut(args);
854 assert!(result.as_ref().is_ok());
855
856 let args = vec![BINARY, "-n", "20", "--output-format", "JSON", URI];
858 let result = cmd.try_get_matches_from_mut(args);
859 assert!(result.as_ref().is_ok());
860
861 let args = vec![BINARY, "-n", "20", "--output-format", "OTHER", URI];
863 let result = cmd.try_get_matches_from_mut(args);
864 assert!(result.as_ref().is_err());
865 }
866}