Skip to content

Filtering tests

Use -E / --filter to select tests by name or tag. Combine predicates with boolean operators, using syntax inspired by nextest's filtersets:

Bash
1
2
3
uv run karva test -E 'tag(slow)'
uv run karva test -E 'test(/^auth::/) & not tag(flaky)'
uv run karva test -E '(tag(fast) | tag(unit)) - tag(flaky)'

Adding custom tags

Any attribute under karva.tags that is not a built-in tag creates a custom tag. Tags are permissive by default:

test_checkout.py
1
2
3
4
5
6
import karva


@karva.tags.integration
def test_checkout():
    assert checkout()

Pytest marks are also recognized as custom tags, so @pytest.mark.integration matches the same tag(integration) predicate. Arguments attached to custom tags do not affect filtering; filters match the tag name.

Set karva_tag at module scope to apply one Karva tag or several tags to every test in that module:

test_checkout.py
1
2
3
import karva

karva_tag = [karva.tags.integration, karva.tags.slow]

Module tags support built-in tags, custom tags, and parametrization. Karva composes tags in this order: function decorators, karva_tag, then pytestmark. Scalar policies such as timeout use the first applicable value; skip tags accumulate, and parametrization dimensions compose. An empty list applies no tags.

Projects can register supported names once and enable strict validation in any profile:

karva.toml
1
2
3
4
5
6
[tags]
integration = "Uses an external service"
slow = ""

[profile.ci.test]
strict-tags = true

With strict validation, unregistered karva.tags.* decorators, karva_tag values, and pytest.mark.* marks fail collection. Karva validates tags on tests, fixtures, and individual parameter sets. Clear misspellings include a suggested registered name. Built-in tags and marks do not need registration.

When -E is passed more than once, a test runs if it matches any of the expressions (OR across flags):

Bash
1
uv run karva test -E 'tag(slow)' -E 'tag(integration)'

Tests that do not match are 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 tag on the test; matches if any tag matches. Built-in and custom karva.tags.* decorators and pytest.mark.* decorators all contribute tags.

Unknown predicates produce a parse error listing the supported names.

Operators

Combine predicates with these operators:

  • & or and — logical AND, e.g. tag(slow) & test(~login).
  • | or or — logical OR, e.g. tag(slow) or tag(fast).
  • not or ! — logical NOT, e.g. not tag(flaky).
  • - — difference (and-not), e.g. tag(slow) - tag(flaky) is shorthand for tag(slow) & not tag(flaky).
  • ( … ) — grouping, e.g. (tag(a) or tag(b)) and tag(c).

Operator precedence

From tightest-binding to loosest:

  1. Grouping with parentheses
  2. not / !
  3. & / and and -
  4. | / or

A few worked examples:

Text Only
1
2
3
4
tag(a) | tag(b) & tag(c)    ≡  tag(a) | (tag(b) & tag(c))
not tag(a) & tag(b)         ≡  (not tag(a)) & tag(b)
tag(a) - tag(b) | tag(c)    ≡  (tag(a) - tag(b)) | tag(c)
tag(a) & tag(b) - tag(c)    ≡  tag(a) & (tag(b) - tag(c))

When in doubt, parenthesize.

Matchers

Prefixes control how test and tag names are matched:

  • =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
test(=mod::test_login)        # exact test name
test(~login)                  # any test whose name contains "login"
test(/^mod::test_log/)        # regex — tests in mod:: starting with test_log
test(/test_add\(x=1\)/)       # regex — a parametrized test case
test(#*_login_*)              # glob — names with _login_ somewhere in them
tag(slow)                     # tag exactly named "slow"
tag(~slo)                     # any tag containing "slo"
tag(#py3*)                    # any tag matching the glob "py3*"

Default matchers

When you omit the prefix, the matcher kind depends on the predicate:

  • test(foo) defaults to substring. test(login) is the same as test(~login).
  • tag(foo) defaults to exact. tag(slow) is the same as tag(=slow).

Use explicit prefixes when generating expressions in tooling.

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 write test(/\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 slow becomes -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 auth becomes -E 'test(/auth/)'
  • -m '^test::test_login' becomes -E 'test(/^test::test_login/)'
  • -m 'slow|fast' becomes -E 'test(/slow|fast/)'
  • -t slow -m auth becomes -E 'tag(slow) & test(/auth/)'

Multiple -E flags are combined with OR, as multiple -t or -m flags were. For example, -t a -t b becomes -E 'tag(a)' -E 'tag(b)'.

When nothing matches

With the default --no-tests=auto, collecting no tests fails the run when no filter was supplied, but a valid filter that matches nothing exits successfully. This makes optional CI shards and local filters easy to compose without hiding an empty suite.

Use --no-tests=pass, --no-tests=warn, or --no-tests=fail to choose the behavior explicitly. The same setting is available as no-tests in the profile's test configuration.

Grammar

For reference, the full grammar:

Text Only
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
filterset   ::= or_expr
or_expr     ::= and_expr (('|' | 'or') and_expr)*
and_expr    ::= unary_expr (('&' | 'and' | '-') unary_expr)*
unary_expr  ::= ('!' | 'not') unary_expr | atom
atom        ::= '(' or_expr ')' | predicate
predicate   ::= ('test' | 'tag') '(' matcher ')'
matcher     ::= '=' body      # exact
              | '~' body      # substring
              | '#' body      # glob
              | regex
              | body          # default (substring for test, exact for tag)
body        ::= identifier | string
regex       ::= '/' … '/'       # `\/` escapes a literal '/'
string      ::= '"' … '"'       # `\"` escapes a literal '"'
identifier  ::= [A-Za-z0-9_.:*?\[\]{}^$]+