Description
Hey @mackwic,
I'd like to hear you views on the prospect adding type-safe tagging to rspec (assuming that we decide that we want to support tagging to begin with).
Pretty much all BDD frameworks out there support some form of through tagging:
Generalized Tags:
- Ruby's Rspec does it through symbols, what would be interned strings in Rust, as used in rustc itself.
- Swift's Quick does it through
[String : Bool]
, what would be a…Map<String, bool>
in Rust. - C++'s Catch does it through
std::set<std::string>
, what would be a…Set<String>
in Rust. - C++'s bandit does it through
std::vector<struct { std::string, bool }>
, what would be aVec<(String, bool)>
in Rust.
The benefit us making tags string-based is the obvious one of extensibility. With string-based extensibility comes a cost however: it's hard to catch typos.
Specialized Functions:
Apart from Catch they also all provide API method variants along the lines of fdescribe
("focus describe") or idescribe
("ignore describe").
I'd prefer to not go this route as it simply doesn't scale and can easily be generalized through proper tagging.
Rust Rspec
How I envision rspec's tagging is by providing a carefully chosen set of batteries-included tags, like this:
bitflags! {
#[derive(Default)]
struct Tag: usize {
const FOCUS = 1 << 0;
const IGNORE = 1 << 1;
const SMOKE = 1 << 2;
}
}
impl TagsTrait for Tag {}
Which then, assuming an existing test scenario defined like this …
scenario!("scenario", env: usize, || {
suite!("suite", env: 42, || {
context!("context A", || {
// ...
});
context!("context B", || {
// ...
});
});
});
… would be used like this …
scenario!("scenario", tags: Tag, env: usize, || {
// ...
context!("context A", tags: Tag::IGNORE, || {
// ...
});
// ...
// ...
});
… causing only scenario : suite : context B
to be executed, when run with --skip="ignore"
.
If the default set of tags is not enough for one's needs then by simply specifying a custom type T: TagsTrait
…
scenario!("scenario", tags: CustomTag, env: usize, || {
// ...
context!("context A", tags: (CustomTag::LOREM | CustomTag::IPSUM), || {
// ...
});
// ...
// ...
});
… which then could be filtered for through --filter="lorem, ipsum"
The big benefit here is two-fold:
- Typing
tags: Tag::INGORE
instead oftags: Tag::IGNORE
would trigger a compile-time error. - Typing
--filter="ingore"
instead of--filter="ignore"
would trigger a run-time error.
Both is possible by having type Tag
define a closed set of allowed tags, which still remains open for extension through custom types.
Caveat:
To make tagging ergonomic we would basically be forced to migrate to a use of macros à la suite!(…)
instead of ctx.suite(…)
. I would however see this as a feature, not a bug, as it also greatly improves extensibility through support for optional arguments. I don't expect optional function/method arguments to lang in stable Rust (or even nightly) any time soon, given the current focus on async/await, NLL, RLS, etc.