Replies: 1 comment
-
I wouldn't quite phrase that as "don't unit test parsing". You are separating the integration logic from the unit and testing the unit. This is the approach I would recommend.
If you are doing end-to-end tests, how can you inject the current time? If you are using an env variable, can't you have your field parser read that variable as well?
This is quite an invasive change with limited application. This is also adding a lot more "magic" in the derive, rather than delegating to the builder API, which we've been moving more towards.
I assume there isn't an intermediate where it gets parsed as a relative time and then gets turned into an absolute time later? |
Beta Was this translation helpful? Give feedback.
-
Motivating use case: I wanted to use
clap_derive
to parse option strings like "5 days" or "tomorrow" intochrono::DateTime<Utc>
instances. I wrote a function that can take such a string and return aResult<DateTime<Utc>, Box<dyn Error>>
, which would work as avalue_parser
for the fields in my struct derivingParser
:Fundamentally, the
DateTime
representation of an option string like "tomorrow" is context-dependent, and will be different for different values of the "current time". No worries, I can useUtc::now()
in the body ofparse_due_date()
to get the current time.The problem is that I want to write unit tests for parsing, but if I use e.g.
Utc::now()
to get the current time, the "current time" will be different every time I run the tests and I can't write assertions that I get a particular output without controlling the input. To write deterministic tests that calltry_parse_from()
on example argument vectors, I need to somehow find a way to hardcode the "current" time in the value parser for thedue
field.Here are the options I've found:
parse_due_date
by extracting aparse_due_date_impl(now: DateTime<Utc>, s: &str)
function that takes the current timeDue::parse()
method is static, I can't inject anything at parse time.lazy_static
of aMutex<Option<DateTime<Utc>>>
that defaults toNone
, and have a public staticcurrent_time()
function that returns the result ofUtc::now()
when the static variable isNone
and return the wrapped value if it'sSome
. Incfg(test)
, there can be a test utility function likeoverride_current_time_for_testing(now: DateTime<Utc>)
that tests can call to inject a hard-coded time, and then any value parsers in myclap_derive
structs that use my mockablecurrent_time()
function will be able to be deterministic as long as the override function is called before parsing.override_current_time_for_testing()
function return a handle that resets the override onDrop
so the state of tests don't leak into each other. But there's still potential issues around overriding twice to be careful of; for my purposes it might be enough to just panic on a double override and print a readable error message since it is only available in tests.clap_derive
and manually createCommand
parsers.TypedValueParser
implementors that carry context like the current time into a function that uses theclap
builder API.clap_derive
and express the command line options so elegantly with Rust structs and enumsfrom_arg_matches()
, we could have a way to additionally pass some context object of a user-supplied type that can be forwarded toTypedValueParser
s used by any args,from_arg_matches_with_context<C>(context: C, matches: &ArgMatches)
.TypedValueParserWithContext<C>
trait to live alongsideTypedValueParser
andCommandFactoryWithContext<C>
trait to live alongsideCommandFactory
. Then we'd also need aParserWithContext<C>
trait to provide atry_parse_from_with_context<I, T>(context: C, itr: I) where I: IntoIterator<Item = T>, T: Into<OsString> + Clone
and other equivalents fromParser
's API.clap
andclap_derive
.TypedValueParser
instances without a problem in the clap builder API, this functionality would only be necessary inclap_derive
.With the last option, I'd be able to rewrite my example as something like:
(I actually don't know if it's syntactically valid to
derive(ParserWithContext<C>)
with the angle brackets and everything... if not, maybe something could be added to the#[command]
attribute, like#[command(context = DateTime<Utc>)]
)Right now, I'm simply parsing the due dates as strings and doing validation in a later step, but it would be nice to move errors to the help messages that are created by
clap
, so I'm contemplating using the lazy static overridable "current time" and biting the bullet on the brittleness that introduces between hidden requirements, potentially leaky global mutable state, test/prod behavior divergence, etc. If I could be convinced that usingclap
's builder API instead ofclap_derive
would be beneficial for other reasons, I may choose to do that instead, in which case I could easily supply aTypedValueParser
that captures the runtime context I need to preserve.But in the mean time, having done all this investigation, I wanted to solicit discussion on whether adding some kind of
WithContext
variants toclap
, at least when theclap_derive
feature is enabled, might be sufficiently useful to others. I can think of other use cases, like having values from config files or environment variables determine how command line arguments are parsed, or testing localization. What do you all think?Beta Was this translation helpful? Give feedback.
All reactions