Switch to a command-line API closer to TaskWarrior

* Use a parser (rather than clap) to process the command line
* Outline some generic support for filtering, reporting, modifying, etc.
* Break argument parsing strictly from invocation, to allow independent testing
This commit is contained in:
Dustin J. Mitchell
2020-12-03 06:58:10 +00:00
parent 87bb829634
commit 2c579b9f01
45 changed files with 1720 additions and 1072 deletions

232
cli/src/argparse/args.rs Normal file
View File

@@ -0,0 +1,232 @@
//! Parsers for argument lists -- arrays of strings
use super::ArgList;
use nom::bytes::complete::tag as nomtag;
use nom::{
branch::*,
character::complete::*,
combinator::*,
error::{Error, ErrorKind},
multi::*,
sequence::*,
Err, IResult,
};
/// Recognizes any argument
pub(super) fn any(input: &str) -> IResult<&str, &str> {
rest(input)
}
/// Recognizes a literal string
pub(super) fn literal(literal: &'static str) -> impl Fn(&str) -> IResult<&str, &str> {
move |input: &str| all_consuming(nomtag(literal))(input)
}
/// Recognizes a comma-separated list of ID's (integers or UUID prefixes)
pub(super) fn id_list(input: &str) -> IResult<&str, Vec<&str>> {
fn hex_n(n: usize) -> impl Fn(&str) -> IResult<&str, &str> {
move |input: &str| recognize(many_m_n(n, n, one_of(&b"0123456789abcdefABCDEF"[..])))(input)
}
all_consuming(separated_list1(
char(','),
alt((
recognize(tuple((
hex_n(8),
char('-'),
hex_n(4),
char('-'),
hex_n(4),
char('-'),
hex_n(4),
char('-'),
hex_n(12),
))),
recognize(tuple((
hex_n(8),
char('-'),
hex_n(4),
char('-'),
hex_n(4),
char('-'),
hex_n(4),
))),
recognize(tuple((hex_n(8), char('-'), hex_n(4), char('-'), hex_n(4)))),
recognize(tuple((hex_n(8), char('-'), hex_n(4)))),
hex_n(8),
digit1,
)),
))(input)
}
/// Recognizes a tag prefixed with `+` and returns the tag value
#[allow(dead_code)] // tags not implemented yet
pub(super) fn plus_tag(input: &str) -> IResult<&str, &str> {
fn to_tag(input: (char, &str)) -> Result<&str, ()> {
Ok(input.1)
}
map_res(
all_consuming(tuple((char('+'), recognize(pair(alpha1, alphanumeric0))))),
to_tag,
)(input)
}
/// Recognizes a tag prefixed with `-` and returns the tag value
#[allow(dead_code)] // tags not implemented yet
pub(super) fn minus_tag(input: &str) -> IResult<&str, &str> {
fn to_tag(input: (char, &str)) -> Result<&str, ()> {
Ok(input.1)
}
map_res(
all_consuming(tuple((char('-'), recognize(pair(alpha1, alphanumeric0))))),
to_tag,
)(input)
}
/// Recognizes a tag prefixed with either `-` or `+`, returning true for + and false for -
#[allow(dead_code)] // tags not implemented yet
pub(super) fn tag(input: &str) -> IResult<&str, (bool, &str)> {
fn to_plus(input: &str) -> Result<(bool, &str), ()> {
Ok((true, input))
}
fn to_minus(input: &str) -> Result<(bool, &str), ()> {
Ok((false, input))
}
alt((map_res(plus_tag, to_plus), map_res(minus_tag, to_minus)))(input)
}
/// Consume a single argument from an argument list that matches the given string parser (one
/// of the other functions in this module). The given parser must consume the entire input.
pub(super) fn arg_matching<'a, O, F>(f: F) -> impl Fn(ArgList<'a>) -> IResult<ArgList, O>
where
F: Fn(&'a str) -> IResult<&'a str, O>,
{
move |input: ArgList<'a>| {
if let Some(arg) = input.get(0) {
return match f(arg) {
Ok(("", rv)) => Ok((&input[1..], rv)),
// single-arg parsers must consume the entire arg
Ok((unconsumed, _)) => panic!("unconsumed argument input {}", unconsumed),
// single-arg parsers are all complete parsers
Err(Err::Incomplete(_)) => unreachable!(),
// for error and failure, rewrite to an error at this position in the arugment list
Err(Err::Error(Error { input: _, code })) => Err(Err::Error(Error { input, code })),
Err(Err::Failure(Error { input: _, code })) => {
Err(Err::Failure(Error { input, code }))
}
};
}
Err(Err::Error(Error {
input,
// since we're using nom's built-in Error, our choices here are limited, but tihs
// occurs when there's no argument where one is expected, so Eof seems appropriate
code: ErrorKind::Eof,
}))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_arg_matching() {
assert_eq!(
arg_matching(tag)(argv!["+foo", "bar"]).unwrap(),
(argv!["bar"], (true, "foo"))
);
assert_eq!(
arg_matching(tag)(argv!["-foo", "bar"]).unwrap(),
(argv!["bar"], (false, "foo"))
);
assert!(arg_matching(tag)(argv!["foo", "bar"]).is_err());
}
#[test]
fn test_plus_tag() {
assert_eq!(plus_tag("+abc").unwrap().1, "abc");
assert_eq!(plus_tag("+abc123").unwrap().1, "abc123");
assert!(plus_tag("-abc123").is_err());
assert!(plus_tag("+abc123 ").is_err());
assert!(plus_tag(" +abc123").is_err());
assert!(plus_tag("+1abc").is_err());
}
#[test]
fn test_minus_tag() {
assert_eq!(minus_tag("-abc").unwrap().1, "abc");
assert_eq!(minus_tag("-abc123").unwrap().1, "abc123");
assert!(minus_tag("+abc123").is_err());
assert!(minus_tag("-abc123 ").is_err());
assert!(minus_tag(" -abc123").is_err());
assert!(minus_tag("-1abc").is_err());
}
#[test]
fn test_tag() {
assert_eq!(tag("-abc").unwrap().1, (false, "abc"));
assert_eq!(tag("+abc123").unwrap().1, (true, "abc123"));
assert!(tag("+abc123 --").is_err());
assert!(tag("-abc123 ").is_err());
assert!(tag(" -abc123").is_err());
assert!(tag("-1abc").is_err());
}
#[test]
fn test_literal() {
assert_eq!(literal("list")("list").unwrap().1, "list");
assert!(literal("list")("listicle").is_err());
assert!(literal("list")(" list ").is_err());
assert!(literal("list")("LiSt").is_err());
assert!(literal("list")("denylist").is_err());
}
#[test]
fn test_id_list_single() {
assert_eq!(id_list("123").unwrap().1, vec!["123".to_owned()]);
}
#[test]
fn test_id_list_uuids() {
assert_eq!(id_list("12341234").unwrap().1, vec!["12341234".to_owned()]);
assert_eq!(id_list("1234abcd").unwrap().1, vec!["1234abcd".to_owned()]);
assert_eq!(id_list("abcd1234").unwrap().1, vec!["abcd1234".to_owned()]);
assert_eq!(
id_list("abcd1234-1234").unwrap().1,
vec!["abcd1234-1234".to_owned()]
);
assert_eq!(
id_list("abcd1234-1234-2345").unwrap().1,
vec!["abcd1234-1234-2345".to_owned()]
);
assert_eq!(
id_list("abcd1234-1234-2345-3456").unwrap().1,
vec!["abcd1234-1234-2345-3456".to_owned()]
);
assert_eq!(
id_list("abcd1234-1234-2345-3456-0123456789ab").unwrap().1,
vec!["abcd1234-1234-2345-3456-0123456789ab".to_owned()]
);
}
#[test]
fn test_id_list_invalid_partial_uuids() {
assert!(id_list("abcd123").is_err());
assert!(id_list("abcd12345").is_err());
assert!(id_list("abcd1234-").is_err());
assert!(id_list("abcd1234-123").is_err());
assert!(id_list("abcd1234-1234-").is_err());
assert!(id_list("abcd1234-12345-").is_err());
assert!(id_list("abcd1234-1234-2345-3456-0123456789ab-").is_err());
}
#[test]
fn test_id_list_uuids_mixed() {
assert_eq!(id_list("abcd1234,abcd1234-1234,abcd1234-1234-2345,abcd1234-1234-2345-3456,abcd1234-1234-2345-3456-0123456789ab").unwrap().1,
vec!["abcd1234".to_owned(),
"abcd1234-1234".to_owned(),
"abcd1234-1234-2345".to_owned(),
"abcd1234-1234-2345-3456".to_owned(),
"abcd1234-1234-2345-3456-0123456789ab".to_owned(),
]);
}
}

View File

@@ -0,0 +1,62 @@
use super::args::*;
use super::{ArgList, Subcommand};
use failure::{format_err, Fallible};
use nom::{combinator::*, sequence::*, Err, IResult};
/// A command is the overall command that the CLI should execute.
///
/// It consists of some information common to all commands and a `Subcommand` identifying the
/// particular kind of behavior desired.
#[derive(Debug, PartialEq)]
pub(crate) struct Command {
pub(crate) command_name: String,
pub(crate) subcommand: Subcommand,
}
impl Command {
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Command> {
fn to_command(input: (&str, Subcommand)) -> Result<Command, ()> {
let command = Command {
command_name: input.0.to_owned(),
subcommand: input.1,
};
Ok(command)
}
map_res(
all_consuming(tuple((arg_matching(any), Subcommand::parse))),
to_command,
)(input)
}
/// Parse a command from the given list of strings.
pub fn from_argv(argv: &[&str]) -> Fallible<Command> {
match Command::parse(argv) {
Ok((&[], cmd)) => Ok(cmd),
Ok((trailing, _)) => Err(format_err!(
"command line has trailing arguments: {:?}",
trailing
)),
Err(Err::Incomplete(_)) => unreachable!(),
Err(Err::Error(e)) => Err(format_err!("command line not recognized: {:?}", e)),
Err(Err::Failure(e)) => Err(format_err!("command line not recognized: {:?}", e)),
}
}
}
#[cfg(test)]
mod test {
use super::*;
// NOTE: most testing of specific subcommands is handled in `subcommand.rs`.
#[test]
fn test_version() {
assert_eq!(
Command::from_argv(argv!["task", "version"]).unwrap(),
Command {
subcommand: Subcommand::Version,
command_name: "task".to_owned(),
}
);
}
}

105
cli/src/argparse/filter.rs Normal file
View File

@@ -0,0 +1,105 @@
use super::args::{arg_matching, id_list};
use super::ArgList;
use nom::{combinator::*, multi::fold_many0, IResult};
/// A filter represents a selection of a particular set of tasks.
#[derive(Debug, PartialEq, Default, Clone)]
pub(crate) struct Filter {
/// A list of numeric IDs or prefixes of UUIDs
pub(crate) id_list: Option<Vec<String>>,
}
enum FilterArg {
IdList(Vec<String>),
}
impl Filter {
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Filter> {
fn fold(mut acc: Filter, mod_arg: FilterArg) -> Filter {
match mod_arg {
FilterArg::IdList(mut id_list) => {
if let Some(ref mut existing) = acc.id_list {
// given multiple ID lists, concatenate them to represent
// an "OR" between them.
existing.append(&mut id_list);
} else {
acc.id_list = Some(id_list);
}
}
}
acc
}
fold_many0(
Self::id_list,
Filter {
..Default::default()
},
fold,
)(input)
}
fn id_list(input: ArgList) -> IResult<ArgList, FilterArg> {
fn to_filterarg(mut input: Vec<&str>) -> Result<FilterArg, ()> {
Ok(FilterArg::IdList(
input.drain(..).map(str::to_owned).collect(),
))
}
map_res(arg_matching(id_list), to_filterarg)(input)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_empty() {
let (input, filter) = Filter::parse(argv![]).unwrap();
assert_eq!(input.len(), 0);
assert_eq!(
filter,
Filter {
..Default::default()
}
);
}
#[test]
fn test_id_list_single() {
let (input, filter) = Filter::parse(argv!["1"]).unwrap();
assert_eq!(input.len(), 0);
assert_eq!(
filter,
Filter {
id_list: Some(vec!["1".to_owned()]),
..Default::default()
}
);
}
#[test]
fn test_id_list_commas() {
let (input, filter) = Filter::parse(argv!["1,2,3"]).unwrap();
assert_eq!(input.len(), 0);
assert_eq!(
filter,
Filter {
id_list: Some(vec!["1".to_owned(), "2".to_owned(), "3".to_owned()]),
..Default::default()
}
);
}
#[test]
fn test_id_list_uuids() {
let (input, filter) = Filter::parse(argv!["1,abcd1234"]).unwrap();
assert_eq!(input.len(), 0);
assert_eq!(
filter,
Filter {
id_list: Some(vec!["1".to_owned(), "abcd1234".to_owned()]),
..Default::default()
}
);
}
}

27
cli/src/argparse/mod.rs Normal file
View File

@@ -0,0 +1,27 @@
/*!
This module is responsible for parsing command lines (`Arglist`, an alias for `&[&str]`) into `Command` instances.
It removes some redundancy from the command line, for example combining the multiple ways to modify a task into a single `Modification` struct.
The module is organized as a nom parser over ArgList, and each struct has a `parse` method to parse such a list.
The exception to this rule is the `args` sub-module, which contains string parsers that are applied to indivdual command-line elements.
All of the structs produced by this module are fully-owned, data-only structs.
That is, they contain no references, and have no methods to aid in their execution -- that is the `invocation` module's job.
*/
mod args;
mod command;
mod filter;
mod modification;
mod report;
mod subcommand;
pub(crate) use command::Command;
pub(crate) use filter::Filter;
pub(crate) use modification::{DescriptionMod, Modification};
pub(crate) use report::Report;
pub(crate) use subcommand::Subcommand;
type ArgList<'a> = &'a [&'a str];

View File

@@ -0,0 +1,119 @@
use super::args::{any, arg_matching};
use super::ArgList;
use nom::{combinator::*, multi::fold_many0, IResult};
use taskchampion::Status;
#[derive(Debug, PartialEq, Clone)]
pub enum DescriptionMod {
/// do not change the description
None,
/// Prepend the given value to the description, with a space separator
Prepend(String),
/// Append the given value to the description, with a space separator
Append(String),
/// Set the description
Set(String),
}
impl Default for DescriptionMod {
fn default() -> Self {
Self::None
}
}
/// A modification represents a change to a task: adding or removing tags, setting the
/// description, and so on.
#[derive(Debug, PartialEq, Clone, Default)]
pub struct Modification {
/// Change the description
pub description: DescriptionMod,
/// Set the status
pub status: Option<Status>,
/// Set the "active" status, that is, start (true) or stop (false) the task.
pub active: Option<bool>,
}
/// A single argument that is part of a modification, used internally to this module
enum ModArg<'a> {
Description(&'a str),
}
impl Modification {
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Modification> {
fn fold(mut acc: Modification, mod_arg: ModArg) -> Modification {
match mod_arg {
ModArg::Description(description) => {
if let DescriptionMod::Set(existing) = acc.description {
acc.description =
DescriptionMod::Set(format!("{} {}", existing, description));
} else {
acc.description = DescriptionMod::Set(description.to_string());
}
}
}
acc
}
fold_many0(
Self::description,
Modification {
..Default::default()
},
fold,
)(input)
}
fn description(input: ArgList) -> IResult<ArgList, ModArg> {
fn to_modarg(input: &str) -> Result<ModArg, ()> {
Ok(ModArg::Description(input))
}
map_res(arg_matching(any), to_modarg)(input)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_empty() {
let (input, modification) = Modification::parse(argv![]).unwrap();
assert_eq!(input.len(), 0);
assert_eq!(
modification,
Modification {
..Default::default()
}
);
}
#[test]
fn test_single_arg_description() {
let (input, modification) = Modification::parse(argv!["newdesc"]).unwrap();
assert_eq!(input.len(), 0);
assert_eq!(
modification,
Modification {
description: DescriptionMod::Set("newdesc".to_owned()),
..Default::default()
}
);
}
#[test]
fn test_multi_arg_description() {
let (input, modification) = Modification::parse(argv!["new", "desc", "fun"]).unwrap();
assert_eq!(input.len(), 0);
assert_eq!(
modification,
Modification {
description: DescriptionMod::Set("new desc fun".to_owned()),
..Default::default()
}
);
}
}

View File

@@ -0,0 +1,33 @@
use super::{ArgList, Filter};
use nom::IResult;
/// A report specifies a filter as well as a sort order and information about which
/// task attributes to display
#[derive(Debug, PartialEq, Default)]
pub(crate) struct Report {
pub filter: Filter,
}
impl Report {
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Report> {
let (input, filter) = Filter::parse(input)?;
Ok((input, Report { filter }))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_empty() {
let (input, report) = Report::parse(argv![]).unwrap();
assert_eq!(input.len(), 0);
assert_eq!(
report,
Report {
..Default::default()
}
);
}
}

View File

@@ -0,0 +1,526 @@
use super::args::*;
use super::{ArgList, DescriptionMod, Filter, Modification, Report};
use nom::{branch::alt, combinator::*, sequence::*, IResult};
use taskchampion::Status;
/// A subcommand is the specific operation that the CLI should execute.
#[derive(Debug, PartialEq)]
pub(crate) enum Subcommand {
/// Display the tool version
Version,
/// Display the help output
Help {
/// Give the summary help (fitting on a few lines)
summary: bool,
},
/// Add a new task
Add {
modification: Modification,
},
/// Modify existing tasks
Modify {
filter: Filter,
modification: Modification,
},
/// Lists (reports)
List {
report: Report,
},
/// Per-task information (typically one task)
Info {
filter: Filter,
debug: bool,
},
/// Basic operations without args
Gc,
Sync,
}
impl Subcommand {
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Subcommand> {
alt((
Self::version,
Self::help,
Self::add,
Self::modify_prepend_append,
Self::start_stop_done,
Self::list,
Self::info,
Self::gc,
Self::sync,
))(input)
}
fn version(input: ArgList) -> IResult<ArgList, Subcommand> {
fn to_subcommand(_: &str) -> Result<Subcommand, ()> {
Ok(Subcommand::Version)
}
map_res(
alt((
arg_matching(literal("version")),
arg_matching(literal("--version")),
)),
to_subcommand,
)(input)
}
fn help(input: ArgList) -> IResult<ArgList, Subcommand> {
fn to_subcommand(input: &str) -> Result<Subcommand, ()> {
Ok(Subcommand::Help {
summary: input == "-h",
})
}
map_res(
alt((
arg_matching(literal("help")),
arg_matching(literal("--help")),
arg_matching(literal("-h")),
)),
to_subcommand,
)(input)
}
fn add(input: ArgList) -> IResult<ArgList, Subcommand> {
fn to_subcommand(input: (&str, Modification)) -> Result<Subcommand, ()> {
Ok(Subcommand::Add {
modification: input.1,
})
}
map_res(
pair(arg_matching(literal("add")), Modification::parse),
to_subcommand,
)(input)
}
fn modify_prepend_append(input: ArgList) -> IResult<ArgList, Subcommand> {
fn to_subcommand(input: (Filter, &str, Modification)) -> Result<Subcommand, ()> {
let filter = input.0;
let mut modification = input.2;
match input.1 {
"prepend" => {
if let DescriptionMod::Set(s) = modification.description {
modification.description = DescriptionMod::Prepend(s)
}
}
"append" => {
if let DescriptionMod::Set(s) = modification.description {
modification.description = DescriptionMod::Append(s)
}
}
_ => {}
}
Ok(Subcommand::Modify {
filter,
modification,
})
}
map_res(
tuple((
Filter::parse,
alt((
arg_matching(literal("modify")),
arg_matching(literal("prepend")),
arg_matching(literal("append")),
)),
Modification::parse,
)),
to_subcommand,
)(input)
}
fn start_stop_done(input: ArgList) -> IResult<ArgList, Subcommand> {
// start, stop, and done are special cases of modify
fn to_subcommand(input: (Filter, &str, Modification)) -> Result<Subcommand, ()> {
let filter = input.0;
let mut modification = input.2;
match input.1 {
"start" => modification.active = Some(true),
"stop" => modification.active = Some(false),
"done" => modification.status = Some(Status::Completed),
_ => unreachable!(),
}
Ok(Subcommand::Modify {
filter,
modification,
})
}
map_res(
tuple((
Filter::parse,
alt((
arg_matching(literal("start")),
arg_matching(literal("stop")),
arg_matching(literal("done")),
)),
Modification::parse,
)),
to_subcommand,
)(input)
}
fn list(input: ArgList) -> IResult<ArgList, Subcommand> {
fn to_subcommand(input: (Report, &str)) -> Result<Subcommand, ()> {
Ok(Subcommand::List { report: input.0 })
}
map_res(
pair(Report::parse, arg_matching(literal("list"))),
to_subcommand,
)(input)
}
fn info(input: ArgList) -> IResult<ArgList, Subcommand> {
fn to_subcommand(input: (Filter, &str)) -> Result<Subcommand, ()> {
let debug = input.1 == "debug";
Ok(Subcommand::Info {
filter: input.0,
debug,
})
}
map_res(
pair(
Filter::parse,
alt((
arg_matching(literal("info")),
arg_matching(literal("debug")),
)),
),
to_subcommand,
)(input)
}
fn gc(input: ArgList) -> IResult<ArgList, Subcommand> {
fn to_subcommand(_: &str) -> Result<Subcommand, ()> {
Ok(Subcommand::Gc)
}
map_res(arg_matching(literal("gc")), to_subcommand)(input)
}
fn sync(input: ArgList) -> IResult<ArgList, Subcommand> {
fn to_subcommand(_: &str) -> Result<Subcommand, ()> {
Ok(Subcommand::Sync)
}
map_res(arg_matching(literal("sync")), to_subcommand)(input)
}
}
#[cfg(test)]
mod test {
use super::*;
const EMPTY: Vec<&str> = vec![];
#[test]
fn test_version() {
assert_eq!(
Subcommand::parse(argv!["version"]).unwrap(),
(&EMPTY[..], Subcommand::Version)
);
}
#[test]
fn test_dd_version() {
assert_eq!(
Subcommand::parse(argv!["--version"]).unwrap(),
(&EMPTY[..], Subcommand::Version)
);
}
#[test]
fn test_d_h() {
assert_eq!(
Subcommand::parse(argv!["-h"]).unwrap(),
(&EMPTY[..], Subcommand::Help { summary: true })
);
}
#[test]
fn test_help() {
assert_eq!(
Subcommand::parse(argv!["help"]).unwrap(),
(&EMPTY[..], Subcommand::Help { summary: false })
);
}
#[test]
fn test_dd_help() {
assert_eq!(
Subcommand::parse(argv!["--help"]).unwrap(),
(&EMPTY[..], Subcommand::Help { summary: false })
);
}
#[test]
fn test_add_description() {
let subcommand = Subcommand::Add {
modification: Modification {
description: DescriptionMod::Set("foo".to_owned()),
..Default::default()
},
};
assert_eq!(
Subcommand::parse(argv!["add", "foo"]).unwrap(),
(&EMPTY[..], subcommand)
);
}
#[test]
fn test_add_description_multi() {
let subcommand = Subcommand::Add {
modification: Modification {
description: DescriptionMod::Set("foo bar".to_owned()),
..Default::default()
},
};
assert_eq!(
Subcommand::parse(argv!["add", "foo", "bar"]).unwrap(),
(&EMPTY[..], subcommand)
);
}
#[test]
fn test_modify_description_multi() {
let subcommand = Subcommand::Modify {
filter: Filter {
id_list: Some(vec!["123".to_owned()]),
},
modification: Modification {
description: DescriptionMod::Set("foo bar".to_owned()),
..Default::default()
},
};
assert_eq!(
Subcommand::parse(argv!["123", "modify", "foo", "bar"]).unwrap(),
(&EMPTY[..], subcommand)
);
}
#[test]
fn test_append() {
let subcommand = Subcommand::Modify {
filter: Filter {
id_list: Some(vec!["123".to_owned()]),
},
modification: Modification {
description: DescriptionMod::Append("foo bar".to_owned()),
..Default::default()
},
};
assert_eq!(
Subcommand::parse(argv!["123", "append", "foo", "bar"]).unwrap(),
(&EMPTY[..], subcommand)
);
}
#[test]
fn test_prepend() {
let subcommand = Subcommand::Modify {
filter: Filter {
id_list: Some(vec!["123".to_owned()]),
},
modification: Modification {
description: DescriptionMod::Prepend("foo bar".to_owned()),
..Default::default()
},
};
assert_eq!(
Subcommand::parse(argv!["123", "prepend", "foo", "bar"]).unwrap(),
(&EMPTY[..], subcommand)
);
}
#[test]
fn test_done() {
let subcommand = Subcommand::Modify {
filter: Filter {
id_list: Some(vec!["123".to_owned()]),
},
modification: Modification {
status: Some(Status::Completed),
..Default::default()
},
};
assert_eq!(
Subcommand::parse(argv!["123", "done"]).unwrap(),
(&EMPTY[..], subcommand)
);
}
#[test]
fn test_done_modify() {
let subcommand = Subcommand::Modify {
filter: Filter {
id_list: Some(vec!["123".to_owned()]),
},
modification: Modification {
description: DescriptionMod::Set("now-finished".to_owned()),
status: Some(Status::Completed),
..Default::default()
},
};
assert_eq!(
Subcommand::parse(argv!["123", "done", "now-finished"]).unwrap(),
(&EMPTY[..], subcommand)
);
}
#[test]
fn test_start() {
let subcommand = Subcommand::Modify {
filter: Filter {
id_list: Some(vec!["123".to_owned()]),
},
modification: Modification {
active: Some(true),
..Default::default()
},
};
assert_eq!(
Subcommand::parse(argv!["123", "start"]).unwrap(),
(&EMPTY[..], subcommand)
);
}
#[test]
fn test_start_modify() {
let subcommand = Subcommand::Modify {
filter: Filter {
id_list: Some(vec!["123".to_owned()]),
},
modification: Modification {
active: Some(true),
description: DescriptionMod::Set("mod".to_owned()),
..Default::default()
},
};
assert_eq!(
Subcommand::parse(argv!["123", "start", "mod"]).unwrap(),
(&EMPTY[..], subcommand)
);
}
#[test]
fn test_stop() {
let subcommand = Subcommand::Modify {
filter: Filter {
id_list: Some(vec!["123".to_owned()]),
},
modification: Modification {
active: Some(false),
..Default::default()
},
};
assert_eq!(
Subcommand::parse(argv!["123", "stop"]).unwrap(),
(&EMPTY[..], subcommand)
);
}
#[test]
fn test_stop_modify() {
let subcommand = Subcommand::Modify {
filter: Filter {
id_list: Some(vec!["123".to_owned()]),
},
modification: Modification {
description: DescriptionMod::Set("mod".to_owned()),
active: Some(false),
..Default::default()
},
};
assert_eq!(
Subcommand::parse(argv!["123", "stop", "mod"]).unwrap(),
(&EMPTY[..], subcommand)
);
}
#[test]
fn test_list() {
let subcommand = Subcommand::List {
report: Report {
..Default::default()
},
};
assert_eq!(
Subcommand::parse(argv!["list"]).unwrap(),
(&EMPTY[..], subcommand)
);
}
#[test]
fn test_list_filter() {
let subcommand = Subcommand::List {
report: Report {
filter: Filter {
id_list: Some(vec!["12".to_owned(), "13".to_owned()]),
},
},
};
assert_eq!(
Subcommand::parse(argv!["12,13", "list"]).unwrap(),
(&EMPTY[..], subcommand)
);
}
#[test]
fn test_info_filter() {
let subcommand = Subcommand::Info {
debug: false,
filter: Filter {
id_list: Some(vec!["12".to_owned(), "13".to_owned()]),
},
};
assert_eq!(
Subcommand::parse(argv!["12,13", "info"]).unwrap(),
(&EMPTY[..], subcommand)
);
}
#[test]
fn test_debug_filter() {
let subcommand = Subcommand::Info {
debug: true,
filter: Filter {
id_list: Some(vec!["12".to_owned()]),
},
};
assert_eq!(
Subcommand::parse(argv!["12", "debug"]).unwrap(),
(&EMPTY[..], subcommand)
);
}
#[test]
fn test_gc() {
let subcommand = Subcommand::Gc;
assert_eq!(
Subcommand::parse(argv!["gc"]).unwrap(),
(&EMPTY[..], subcommand)
);
}
#[test]
fn test_gc_extra_args() {
let subcommand = Subcommand::Gc;
assert_eq!(
Subcommand::parse(argv!["gc", "foo"]).unwrap(),
(&vec!["foo"][..], subcommand)
);
}
#[test]
fn test_sync() {
let subcommand = Subcommand::Sync;
assert_eq!(
Subcommand::parse(argv!["sync"]).unwrap(),
(&EMPTY[..], subcommand)
);
}
}