Filtering tests
Karva selects which tests to run with filter expressions, a small
language inspired by nextest's filtersets. A single -E / --filter
flag composes name matching, tag matching, and boolean logic into one
expression — there are no separate --tag or --match flags.
| Bash | |
|---|---|
1 2 3 | |
When -E is passed more than once, a test runs if it matches any of
the expressions (OR across flags):
| Bash | |
|---|---|
1 | |
Expressions are evaluated against every discovered test. A test runs if and only if the expression evaluates to true for it; otherwise it is skipped.
Predicates
A filter expression is built from predicates combined with boolean operators. Karva currently supports two predicates:
test(<matcher>)— evaluated against the fully qualified test name, e.g.mod::sub::test_login.tag(<matcher>)— evaluated against each custom tag on the test; matches if any tag matches. Bothkarva.tags.*decorators andpytest.mark.*decorators contribute tags.
Unknown predicate names are a parse error. The error message will
suggest the valid names. If you expected one and got the other, make
sure you haven't misspelled test/tag or used an older nextest
predicate (package, binary, platform, etc.) — karva does not
currently implement those.
Operators
Predicates can be combined with the following operators. All operators have both a symbolic and a keyword form, pick whichever is clearer in context:
&orand— logical AND, e.g.tag(slow) & test(~login).|oror— logical OR, e.g.tag(slow) or tag(fast).notor!— logical NOT, e.g.not tag(flaky).-— difference (and-not), e.g.tag(slow) - tag(flaky)is shorthand fortag(slow) & not tag(flaky). Useful for subtracting flaky or platform-gated tests from a broader selection.( … )— grouping, e.g.(tag(a) or tag(b)) and tag(c).
Operator precedence
From tightest-binding to loosest:
- Grouping with parentheses
not/!&/andand-|/or
A few worked examples:
| Text Only | |
|---|---|
1 2 3 4 | |
When in doubt, parenthesize.
Matchers
A matcher describes how a predicate's argument is compared against the value it is evaluated over (a test name or a tag name). There are four matcher kinds, distinguished by a single-character prefix:
=foo— exact: the value must equal the pattern exactly.~foo— substring: the pattern must appear anywhere in the value./foo/— regex: the value must match the Rust regex. Regex uses partial matching — anchor with^and$for a full match.#foo— glob: the value must match the glob pattern.*matches any run of characters,?matches a single character, and[...]is a character class.- No prefix — default: see below.
Example expressions:
| Text Only | |
|---|---|
1 2 3 4 5 6 7 8 | |
Default matchers
When you omit the prefix, the matcher kind depends on the predicate:
test(foo)defaults to substring.test(login)is the same astest(~login). This matches howcargo nextestbehaves and is what people usually want when typing something quick.tag(foo)defaults to exact.tag(slow)is the same astag(=slow). Tags are short identifiers, so partial matches almost always hit more than you want.
If you write tooling that constructs filter expressions programmatically, always use an explicit prefix rather than relying on the default — it's clearer to read and won't surprise you if the default ever changes.
Matcher bodies
A matcher body is either a bare identifier, a quoted string, or a delimited regex:
- Bare identifiers may contain letters, digits,
_,.,:, and the glob metacharacters*,?,[,],{,},^,$. Most test names and tag names fit without quoting:test(=mod::sub::test_login)works as-is because:is permitted inside an identifier. - Quoted strings (
"...") allow any character, including spaces and operator characters, so use them when a tag or test name contains something like a hyphen, a space, or a parenthesis:tag(="my-nightly tag"). - Regex literals (
/.../) accept the full Rust regex syntax. Note that/is the delimiter, so a literal/inside a regex must be escaped as\/.
The keywords test, tag, and, or, and not are not reserved
inside a matcher body — tag(test) correctly matches a tag literally
named test, and tag(and) matches a tag named and. The outer parser
only treats them as keywords at the top level of an expression.
Escape sequences
Karva uses a deliberately minimal escape scheme so that regex metacharacters round-trip without double-backslashing:
- Inside a regex literal
/ … /, only\/is processed (to embed a literal/). All other backslash sequences are passed through to the regex engine unchanged, so you can writetest(/\d+/)without doubling the backslash. - Inside a quoted string
" … ", only\"is processed (to embed a literal"). Again, other backslashes are preserved verbatim. - Bare identifiers have no escape syntax at all — if your tag or test name needs characters the identifier rules don't allow, quote it.
- For literal glob metacharacters, use the bracket escape from
globset:
#[*]matches a literal*character,#[?]matches a literal?, and so on.
Migration from -t and -m
Older releases of karva exposed separate -t / --tag and -m /
--match flags. Both have been removed and replaced by -E /
--filter. The new syntax is a strict superset — every old invocation
has a direct translation:
-t slowbecomes-E 'tag(slow)'-t 'not slow'becomes-E 'not tag(slow)'-t 'slow and integration'becomes-E 'tag(slow) & tag(integration)'-t 'slow or integration'becomes-E 'tag(slow) or tag(integration)'-t '(slow or fast) and not flaky'becomes-E '(tag(slow) or tag(fast)) - tag(flaky)'-m authbecomes-E 'test(/auth/)'-m '^test::test_login'becomes-E 'test(/^test::test_login/)'-m 'slow|fast'becomes-E 'test(/slow|fast/)'-t slow -m authbecomes-E 'tag(slow) & test(/auth/)'
Multiple -E flags keep the same OR-across-flags semantics that
multiple -t or -m flags used to have, so -t a -t b becomes
-E 'tag(a)' -E 'tag(b)' and not -E 'tag(a) | tag(b)' (though those
two are equivalent).
On top of the old capabilities, the new DSL adds substring, exact, and glob matchers — previously only regex matching was possible for test names, and only exact matching for tags.
Grammar
For reference, the full grammar:
| Text Only | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |