Skip to content

Discussion: 'tags' experiment #1

Open
@regexident

Description

@regexident

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 a Vec<(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:

  1. Typing tags: Tag::INGORE instead of tags: Tag::IGNORE would trigger a compile-time error.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions