rspec-tracer

CI Gem Version codecov Docs

RSpec Tracer is a specs dependency analyzer, flaky-test detector, test accelerator, and coverage reporter for RSpec. It records the inputs every example consumes โ€” Ruby files (via Coverage), file I/O (via prepended File / IO / YAML / JSON hooks), Rails template + AR notifications, and user-declared globs โ€” then re-runs only the examples whose inputs changed since the last run.

It never skips:

  • Failed, flaky, or pending examples.
  • Examples whose dependent inputs changed (the whole point).

๐Ÿ“š Full documentation site: avmnu-sng.github.io/rspec-tracer โ€” YARD API reference, sample HTML report, cookbook, and per-version coverage report.

For a complete account of the input taxonomy and how the engine fits together, read ARCHITECTURE.md.

Table of contents

Quick start

Requires Ruby 3.1+ and rspec-core 3.12+. Rails 7.0+ when using Rails; Rails 8.0 needs Ruby 3.2+. JRuby 9.4 is supported.

  1. Add the gem:
   # 2.0 is in pre-release. Pin to the pre-release version explicitly;
   # switch to '~> 2.0' once 2.0.0 final ships.
   gem 'rspec-tracer', '= 2.0.0.pre.1', group: :test, require: false

bundle install will resolve the pre-release version. You can also install ad-hoc with gem install rspec-tracer --pre.

  1. Add the canonical directories to your .gitignore:
   rspec_tracer.lock
   rspec_tracer_cache/
   rspec_tracer_coverage/
   rspec_tracer_report/
  1. Load and start at the very top of spec_helper.rb / rails_helper.rb, before any application code:
   require 'rspec_tracer'
   RSpecTracer.start

With SimpleCov, start SimpleCov first (load order is part of the contract):

   require 'simplecov'
   SimpleCov.start
   require 'rspec_tracer'
   RSpecTracer.start
  1. Run your suite. After the run, open rspec_tracer_report/index.html for the HTML report, and rspec_tracer_report/report.json for machine-readable output.

The terminal prints a one-line summary with cache size and the reasons examples re-ran:

   rspec-tracer: 1,820 examples ยท 42 re-run ยท 1,778 skipped (97% cached)
   by reason: 38 Files changed ยท 4 Failed previously
   cache: rspec_tracer_cache (14.4 MiB; +6.7 KiB vs prev run)

How it works

Every test is a pure function of its inputs; the tracker's job is to identify every input and hash it. Inputs come from six buckets:

  1. Ruby-executed source โ€” observed via Ruby's Coverage module.
  2. File I/O (File.read, YAML.load_file, etc.) โ€” observed via Module#prepend hooks.
  3. Framework events โ€” Rails template renders + (opt-in) AR schema-touching queries via ActiveSupport::Notifications.
  4. Declared globs โ€” what you tell the tracker to track via track_files / tracks: / track_rails_defaults.
  5. Whole-suite invalidators โ€” Gemfile.lock, .ruby-version, .rspec-tracer, the rspec-tracer gem version itself.
  6. Truly unobservable inputs โ€” env-var branches, refinements in unexecuted files. Use track_env / tracks: { env: ... } to declare them; the tracker can't auto-detect them.

A digest of every observed input is stored per-example. The next run loads the cached digest set, recomputes per-input digests, and skips examples whose inputs are unchanged.

Read ARCHITECTURE.md for the full layer structure (Tracker / Storage / RemoteCache / Reporters), data flows, and extension protocols.

Per-example precision under Rails config.eager_load

A precision tradeoff worth knowing before you run a suite that uses Rails:

  • config.eager_load = false in test env โ€” per-example precision works as designed. Files autoloaded during an example land in per-example deps; editing one re-runs only the examples that touched it.
  • config.eager_load = true (Rails default for CI tests to mirror prod) โ€” all app/ files load at boot and land in the whole-suite invalidator set. Editing any app/ file re-runs every example. Safe (never misses a dep) but coarser than per-example attribution.

If your CI suite runs with eager_load = true and your app/-edit cycle is bottlenecked by whole-suite re-runs, set config.eager_load = false in test env to recover per-example precision. A future 2.1 enhancement (working name: track_class_attribution) will install class-dispatch tracking to trim the invalidator scope under eager-loaded test environments โ€” see CHANGELOG.md "Deferred to 2.1" for the planned contract.

Per-example tracks: DSL

Annotate any describe / context / example with extra inputs the tracker can't auto-observe โ€” config files baked at boot, env-var branches, non-Ruby deps:

RSpec.describe AdminController,
               tracks: { files: 'app/policies/**/*.rb',
                         env: 'ROLE_CONFIG' } do
  it 'gates on the feature flag' do
    # ...
  end
end
Key Value shape Semantics
:files String glob OR Array of strings Each matched file is attached as a :declared-kind dep on the example.
:env String name OR Array OR wildcard Each named env var is digested at finalize. Missing env reads as empty.

Wildcards ('RAILS_*', '*_TOKEN', '*') expand against the live ENV at register time; the persisted snapshot only carries concrete keys.

Cascade. Nested groups contribute additively. A parent's tracks: { files: 'a/*' } and a child's tracks: { env: 'B' } both attach to the child's examples; the child does not clobber the parent on any shared key.

Use track_files / track_env for the global case. Files / env vars every test depends on (Gemfile.lock, AUTH_TOKEN, RAILS_ENV) belong in .rspec-tracer:

RSpecTracer.configure do
  track_files 'config/locales/**/*.yml', 'db/schema.rb', 'Gemfile.lock'
  track_env   'AUTH_TOKEN', 'DATABASE_URL', 'RAILS_*'
end

Rails quick start

# spec/rails_helper.rb
require 'simplecov'      # if used; load BEFORE rspec_tracer
SimpleCov.start

require 'rspec_tracer'
RSpecTracer.start

require File.expand_path('../config/environment', __dir__)
require 'rspec/rails'
# .rspec-tracer
RSpecTracer.configure do
  track_rails_defaults
end

track_rails_defaults attaches the common Rails-side declared globs: views, locales, fixtures, factories, helpers, config. Drop a specific glob to hand attribution off to the per-example subscribers instead:

RSpecTracer.configure do
  # Templates โ†’ render_template.action_view subscriber attributes
  # them per-example. Schema โ†’ opt-in sql.active_record observer
  # attributes db/schema.rb + db/structure.sql per AR-touching
  # example (read the Narrow AR-schema attribution section before
  # enabling).
  track_rails_defaults except: [:views, :schema]
  track_ar_schema_notifications
end

Supported Rails matrix: 7.0 / 7.1 / 7.2 / 8.0 (8.0 requires Ruby 3.2+). On JRuby use JRUBY_OPTS="--debug -X+O" and the JDBC adapter (gem 'activerecord-jdbcsqlite3-adapter', '~> 71.0', platforms: :jruby for Rails 7.1; track the major to your Rails line).

Narrow AR-schema attribution

track_ar_schema_notifications promises per-example attribution of db/schema.rb via the sql.active_record subscriber. The narrow promise only holds when no per-example AR cleanup mechanism fires queries inside the per-example bucket. Common Rails setups trip this:

  • use_transactional_fixtures = true (Rails default) โ€” per-example BEGIN/COMMIT fires sql.active_record.
  • DatabaseCleaner :truncation / :deletion / :transaction in around hooks โ€” cleanup queries fire inside the bucket.

Either case attributes db/schema.rb to every AR-touching example (safe, but widens invalidation). A boot-time warn fires when the precondition is unmet so you don't discover this from a confused cache-hit-rate chart later.

For genuinely narrow attribution: set use_transactional_fixtures = false and use sequence-based factories (or another non-AR cleanup mechanism that doesn't fire sql.active_record inside the example window).

Working with parallel_tests

Supported out of the box. The tracker writes to per-worker directories and merges at finalize on the elected last worker. If you interrupt a run mid-flight, delete the lock file:

rm -f rspec_tracer.lock && bundle exec parallel_rspec

Configuring CI

The 1.x flow is preserved bit-for-bit. In your project's Rakefile:

spec = Gem::Specification.find_by_name('rspec-tracer')
load "#{spec.gem_dir}/lib/rspec_tracer/remote_cache/Rakefile"

Then in your CI pipeline:

bundle exec rake rspec_tracer:remote_cache:download
bundle exec rspec
bundle exec rake rspec_tracer:remote_cache:upload

With these env vars:

  • GIT_DEFAULT_BRANCH โ€” your repo's default branch (main / master).
  • GIT_BRANCH โ€” the branch the build is on.
  • TEST_SUITES โ€” total number of suite shards (when sharding).
  • TEST_SUITE_ID โ€” current shard identifier.
  • RSPEC_TRACER_REMOTE_CACHE_URI โ€” the remote-cache URI (s3://bucket/prefix, file:///tmp/cache, redis://host).

Caching rspec-tracer in CI

If you don't have S3 โ€” or only need cache between runs of a single workflow โ€” GitHub Actions' built-in cache is a drop-in storage substrate. Restore rspec_tracer_cache/ before specs, run the suite, let the post-step save it back. A reference workflow lives at .github/workflows/example-tracer-cache.yml. The cache step:

- name: Restore rspec-tracer cache
  uses: actions/cache@v5
  with:
    path: rspec_tracer_cache
    key: >-
      ${{ runner.os
      }}-${{ hashFiles('.ruby-version')
      }}-${{ hashFiles('Gemfile.lock')
      }}-rspec-tracer-cache
    restore-keys: |
      ${{ runner.os }}-${{ hashFiles('.ruby-version') }}-
      ${{ runner.os }}-

The 4-component cache key (runner.os + Ruby version + the rspec-tracer gem's own version + your project's Gemfile lock) invalidates when something would make the previous run's decisions incorrect: native gem binaries differ across runner OSes, Ruby ABI changes invalidate native extensions, a tracer upgrade can change the cache schema, and gem-set drift is the most common cache-staleness trigger.

The pattern translates 1:1 to CircleCI, GitLab CI, Buildkite, and Heroku CI; only the YAML envelope is GHA-specific. See docs/CI_RECIPES.md for per-provider recipes.

Remote cache backends

Three backends ship in 2.0. Pick one in .rspec-tracer:

RSpecTracer.configure do
  # S3 (preserves 1.x layout; supports awslocal / LocalStack).
  remote_cache_backend :s3, local: false

  # Filesystem-backed (no S3 needed).
  # remote_cache_backend :local_fs, root: '/tmp/rspec-tracer-cache'

  # Redis (with optional per-key TTL + PR-branch tracking sidecar).
  # remote_cache_backend :redis, url: ENV['REDIS_URL'], ttl: 7 * 86_400
end

The rake rspec_tracer:remote_cache:* task surface is unchanged โ€” backend selection happens in config; the rake tasks dispatch identically.

Pluggable storage

Two on-disk storage formats:

RSpecTracer.configure do
  # JSON (default) โ€” preserves the 1.x 10-file layout per run.
  storage_backend :json

  # SQLite โ€” single-file database. Faster cold reads above ~5,000
  # examples. JRuby auto-falls-back to :json with a one-time warn.
  # storage_backend :sqlite
end

Or override per-run via env: RSPEC_TRACER_STORAGE=sqlite.

Command-line tools

bin/rspec-tracer exposes five sub-commands:

bin/rspec-tracer doctor         # diagnose config + environment
bin/rspec-tracer cache:info     # size, last run, invalidation stats
bin/rspec-tracer cache:clear    # rm cache dirs
bin/rspec-tracer report:open    # open the HTML report
bin/rspec-tracer explain <id>   # why is <example_id> scheduled to (re-)run?

The CLI is opt-in for local-dev convenience. The rake rspec_tracer:remote_cache:* tasks remain first-class for CI integration โ€” nothing in the CLI replaces them.

SimpleCov interop

Load order is part of the contract. SimpleCov first, then rspec-tracer:

require 'simplecov'
SimpleCov.start

require 'rspec_tracer'
RSpecTracer.start

If you call require 'simplecov' but skip SimpleCov.start before RSpecTracer.start, a boot-time warn fires pointing this out (silent-degradation breaks coverage output).

Branch coverage works alongside rspec-tracer in 2.0. The 1.x caveat ("SimpleCov would not report branch coverage results even when enabled") is no longer applicable โ€” the coverage emission decoupled from SimpleCov's branch-tracking. Re-enable enable_coverage :branch in your SimpleCov.start block and you get both.

Filter chains compose: SimpleCov's add_filter / coverage_filters control SimpleCov's HTML output; rspec-tracer's add_filter / add_coverage_filter control which files contribute to the dependency graph.

FAQ + comparison

Why not just SimpleCov filtering? SimpleCov tells you which files are covered. rspec-tracer tells you which examples depend on which inputs, so it can skip the ones whose inputs didn't change. The two solve adjacent problems; you typically want both.

Knapsack / Knapsack Pro / Test Boosters? Those are test-splitting tools โ€” they shard your suite across CI workers. rspec-tracer is orthogonal: it skips already-passing examples on a single worker. Compose them: shard with Knapsack, then skip with rspec-tracer. A composition smoke spec (spec/regressions/knapsack_coexistence_spec.rb) proves they coexist.

RSpec::Retry / RSpec::Rerun? Retries flaky failures. rspec-tracer detects flaky examples (same inputs, different outcomes) and refuses to skip them on the next run. Compose: retry to make a flaky suite green, let rspec-tracer flag the flakes for you to actually fix.

Monorepo with N apps? Set per-app cache_dir in each app's .rspec-tracer (the default rspec_tracer_cache/ would otherwise collide across apps).

tracks: vs track_rails_defaults overlap? When both attribute the same file to an example, the declared-glob attribution wins. Deterministic; see ARCHITECTURE.md "Input taxonomy" for the rule.

Reports

After the run, rspec_tracer_report/index.html opens five HTML reports:

  • All Examples โ€” basic test info (id, status, duration, the inputs the example consumed).
  • Duplicate Examples โ€” pairs RSpec couldn't uniquely identify (file:line collisions; only appears when duplicates are present).
  • Flaky Examples โ€” examples that passed after previously failing without any dependency change. The tracker refuses to skip these on subsequent runs.
  • Examples Dependency โ€” per-example file dependency lists; the "what does this test depend on?" view.
  • Files Dependency โ€” the inverse: "what tests run if I change this file?" The unique-to-rspec-tracer report that makes refactors safer.

Plus a machine-readable rspec_tracer_report/report.json for CI dashboards and the terminal one-liner shown in Quick start.

Documentation + coverage

  • Docs site: avmnu-sng.github.io/rspec-tracer publishes a YARD API reference, the cookbook, internals deep-dives, and the sample HTML report for every rolling main + tagged release. The landing page lets you switch versions; per-version subpaths look like /main/yard/, /main/demo/, /v2.0.0.pre.1/yard/, etc.

  • Coverage: tracked by Codecov (per-PR delta + diff-coverage gate โ‰ฅ 90%; project history). Multi- suite resultsets (unit, edge-cases, regressions-plain, fuzz) merge via SimpleCov.collate in the CI coverage job; the same merger runs locally via task coverage:merge after task test:* runs (open coverage/index.html for the local HTML report).

Help and community

Released under the MIT License. Everyone interacting in the project is expected to follow the Code of Conduct.

Section anchor map (1.x โ†’ 2.0)

The README was restructured in 2.0. If you bookmarked a 1.x section, here's where its content lives now:

1.x anchor 2.0 anchor
#getting-started Quick start
#working-with-jruby Quick start (JRuby noted under floors in UPGRADING.md)
#working-with-parallel-tests Working with parallel_tests under Rails quick start
#configuring-ci Configuring CI
#caching-rspec-tracer-in-ci Caching rspec-tracer in CI
#advanced-configuration Split between Per-example tracks: DSL, Pluggable storage, and UPGRADING.md
#available-settings Inline across the new sections; see also ARCHITECTURE.md
#filters Inline under SimpleCov interop and the configuration DSL examples
#duplicate-examples Reports under "Duplicate Examples"
#demo Reports