Skip to main content

Test tags

Bleep can filter the set of test suites it runs at the suite-dispatch boundary, before a single test class is loaded by a framework. You declare tag → suite-pattern mappings in bleep.yaml and pass --only-tag / --exclude-tag on the command line. Because filtering happens in bleep's own dispatcher — not inside ScalaTest, JUnit, MUnit, utest, Specs2 or anything else — the tag mechanism is framework-agnostic and works uniformly across every test runner bleep supports.

Class-level only. Tags match suite FQDNs, never individual test methods. If a framework offers per-method tagging (ScalaTest Tag, JUnit 5 @Tag) you can still use that inside a class; bleep's tags choose which classes to dispatch in the first place.

Quick start: mark integration tests as slow, run only fast tests​

projects:
mytests:
isTestProject: true
dependencies: org.scalatest::scalatest:3.2.19
testTags:
slow:
- "**IT"

Then on the command line:

bleep test mytests # runs everything
bleep test mytests --only-tag slow # runs only **IT
bleep test mytests --exclude-tag slow # runs everything except **IT

The bleep.yaml declaration above is what bleep itself uses to keep its own native-image CI jobs fast: see bleep.yaml and the --exclude-tag slow step in build.yml.

Why suite-dispatch, not framework-native?​

Most test frameworks ship their own tagging surface (ScalaTest tags, JUnit 4 categories, JUnit 5 @Tag, MUnit, Specs2). They each work — until your build has projects on three different frameworks, or you want to ask one question across them ("don't run anything that hits the network"). Bleep tags solve that:

  1. Same syntax for every framework. You never write @Slow in Scala, @Category(Slow.class) in Java 4, and @Tag("slow") in Java 5. The marker lives in bleep.yaml.
  2. Build-level, not source-level. A test class doesn't need to know it's slow. Reclassifying a slow test as fast is one line in bleep.yaml, no recompile.
  3. Saves compile work too (see project pre-filter below). Framework-native tags can't, because the framework hasn't loaded yet when bleep decides what to compile.

Declaration syntax​

testTags is a map of tag name to one-or-more suite FQDN glob patterns. Both forms are valid:

projects:
mytests:
testTags:
slow: "**IT" # single string
flaky:
- "com.example.NetworkTest" # array of strings
- "**FlakyIT"
e2e:
- "com.example.e2e.**"

Glob semantics​

PatternMatches
Literal charactersThemselves (Foo, Test, ., …)
*Any sequence within a single FQDN segment — does not cross dots
**Any sequence including dots

Examples:

Patterncom.example.FooITcom.example.foo.FooITcom.example.FastTest
**IT✓✓✗
com.example.*IT✓✗ (dot blocks *)✗
com.example.**Test✗✗✓
**✓✓✓

Regex metacharacters in the literal portions (., $, +, (, ), [, ], {, }, ^, |, ?, \) are escaped automatically. Foo$Bar matches only Foo$Bar, not FooXBar.

CLI surface: --only-tag and --exclude-tag​

The flags mirror the existing --only / --exclude (which filter on suite-name substrings, not tags):

bleep test --only-tag slow # union: any suite tagged slow
bleep test --only-tag slow --only-tag e2e # OR: tagged slow OR tagged e2e
bleep test --exclude-tag flaky # subtractive: drop suites tagged flaky
bleep test --only-tag slow --exclude-tag flaky # slow-and-not-flaky
bleep test --only-tag slow --only FooIT # AND with --only: must match both

Rules:

  • --only-tag is union. Multiple --only-tag flags OR together (suite needs to match any requested tag).
  • --exclude-tag is subtractive. It always drops; never adds. A suite tagged with both slow and flaky is dropped by --exclude-tag flaky even if --only-tag slow was also given.
  • No --only-tags active = all suites considered. Untagged suites are kept by default — they are only excluded when at least one --only-tag is specified.
  • Mixing with --only / --exclude = AND. First the FQDN regex filter runs (the existing --only/--exclude substring match), then the tag filter narrows further. Both must pass.

Strict tag validation​

Bleep's CLI uses decline with Argument.fromMap, fed by the set of tag names actually declared in your build. That gives you two things for free:

  1. Tab-completion. bleep test --only-tag <TAB> lists the tags your projects declare.
  2. Typos fail loud at parse time. bleep test --only-tag slo exits before any work runs with:
    Unknown value: slo. Expected slow.

Project pre-filter​

--only-tag X narrows the project set before BSP dispatch, not just the suite set inside each project. The narrowing rule:

If a project's testTags declare none of the tags requested by --only-tag, drop the project entirely. Don't compile it. Don't discover its suites.

In practice this matters when you have, say, bleep-tests (which declares slow) and bleep-bsp-tests (which doesn't):

$ bleep test bleep-tests bleep-bsp-tests --only-tag slow
[info] --only-tag slow: pre-filtered 1 project(s) with no matching tags: bleep-bsp-tests
...
Projects: 1/2 selected (1 pre-filtered by --only-tag slow: bleep-bsp-tests)
Filters active: --only-tag slow

bleep-bsp-tests never compiled. That's the saved work.

The mirror flag --exclude-tag does not prune projects, because a project can have a mix of tagged and untagged suites — the untagged ones still need to run.

Inspecting tags: bleep list-tests​

Lists every discovered suite alphabetically, with matching tags in parens, and warns about manifest patterns that match nothing:

$ bleep list-tests bleep-tests
bleep-tests:
bleep.AnnotationProcessingIT (slow)
bleep.BleepDevDepsTest
bleep.BuildRewriteTest
bleep.CrossBuildingKotlinIT (slow)
bleep.FastTest
...

[warn] [bleep-tests] testTags.slow pattern '**LegacyIT' matches no discovered suite

The drift warning is informational, not fatal. It tells you when a tag pattern has fallen out of sync with reality — a class was renamed, deleted, or never existed.

Summary diagnostics​

When any filter is active OR --only-tag pre-filtered any project, the test summary appends:

✓ Build Summary

Tests: 47 passed, 0 failed
Suites: 12 total
Projects: 1/2 selected (1 pre-filtered by --only-tag slow: bleep-bsp-tests)
Filters active: --only NoSuchTest · --only-tag slow
Duration: 9.8s

The Filters active line is verbatim what you'd type to reproduce the run.

Error messages​

When --only or --only-tag matches no suites in a project, the error walks the filter pipeline so you can tell which stage emptied the set:

--only-tag matched no test suites in mytest (--only-tag slow):
1 discovered → 0 after tag filter.
Tags declared in mytest: slow
Suites that survived --only/--exclude (none matched the tag filter): example.FastTest

The breakdown — N discovered → M after --only/--exclude → K after tag filter — appears whenever the resulting set is empty and an include-style filter (--only or --only-tag) was requested. Pure --exclude / --exclude-tag emptying the set stays silent because that's the user explicitly skipping, not a misconfiguration.

Special-cased hints:

  • If the project declares no testTags at all: "Project X declares no testTags; --only-tag will never match here. (Did you mean to declare tags in bleep.yaml?)"
  • If the regex filter emptied things first: lists the suites available before the regex filter ran.

CI recipe: fast per-arch validation, full coverage in one canonical job​

This is exactly how bleep itself uses tags. The full test suite runs in the build job; per-arch native-image jobs skip the slow bracket since their job is to validate the produced binary boots, not to re-run the integration surface on every architecture.

# bleep.yaml
projects:
bleep-tests:
testTags:
slow:
- "**IT"
# .github/workflows/build.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: bleep test # full suite — canonical gate

build-native-image:
strategy:
matrix:
os: [ubuntu-22.04, ubuntu-22.04-arm, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- run: ./bleep --dev test --no-color jvm3 --exclude-tag slow

The trade: native-image jobs no longer block on the integration-test wall-time, but the integration tests remain a release-blocker via the build job. No architecture is silently skipped — every job runs some tests, and the slow bracket is gated by exactly one canonical run.

Comparison to framework-native tagging​

You can still use framework-native tagging alongside bleep tags. They occupy different layers:

Concernbleep testTagsFramework-native (ScalaTest tags, JUnit @Tag, …)
LayerBuild / suite-dispatchFramework runtime
GranularityClass FQDNTest method (usually)
Cross-framework✓ One mechanism for everything✗ Each framework's own surface
Saves compile work✓ (via project pre-filter)✗
Source change to retag✗ Edit bleep.yaml✓ Edit annotation in source
Per-method filtering✗✓

Use bleep tags for class-level structural categories — "integration", "slow", "flaky-on-CI", "needs-network". Reach for framework-native tags when you genuinely want per-method filtering within a class.