Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
67ffcba
Add regression tests for backtrace source mapping
JPDuchesne Jul 21, 2026
6ca140c
Add red acceptance tests for line-aligned emission through RSpock
JPDuchesne Jul 22, 2026
ebca577
Adopt line-aligned ast_transform 3.0; delete the mapping apparatus
JPDuchesne Jul 22, 2026
4c73414
Prepare 3.0.0: docs, skill, changelog
JPDuchesne Jul 22, 2026
bf97fa3
Pin unparser below 0.9 (requires Ruby >= 3.3; CI tests 3.2)
JPDuchesne Jul 22, 2026
8d39c11
Require Ruby >= 3.3; drop direct parser/unparser dependencies
JPDuchesne Jul 23, 2026
17f6332
CHANGELOG: state the Ruby 3.2 support drop explicitly
JPDuchesne Jul 23, 2026
f7aaf9b
Adopt dev + shadowenv for the development environment
JPDuchesne Jul 23, 2026
bf24270
Declare the Ruby toolchain in dependencies.rb
JPDuchesne Jul 23, 2026
127c097
Adopt proc-lowered deferral: When locals reach Then and Cleanup
JPDuchesne Jul 23, 2026
1c0df72
Adopt ast_transform's single-node Thunk API
JPDuchesne Jul 24, 2026
ceedc18
Adopt ast_transform's source-column indentation in expectations
JPDuchesne Jul 24, 2026
320aa90
Bump ast_transform to the rebased, style-clean branch revision
JPDuchesne Jul 24, 2026
dbd7db5
Bump ast_transform to the stateless emitter/lowering revision
JPDuchesne Jul 24, 2026
9d73b19
Bump ast_transform to the Thunk capture revision
JPDuchesne Jul 24, 2026
b5b57c1
Bump ast_transform to the producer-owned errors revision
JPDuchesne Jul 24, 2026
f761495
Bump ast_transform to the ArgumentError invariants revision
JPDuchesne Jul 24, 2026
c6bd244
Bump ast_transform to the testing namespace revision
JPDuchesne Jul 24, 2026
6521a85
Repoint ast_transform at git main now that ast-transform#12 is merged
JPDuchesne Jul 24, 2026
cdc3d24
Pin exact unparsed output in mocha mock transformation tests
JPDuchesne Jul 24, 2026
d826036
Depend on published ast_transform 3.0.0 instead of git main
JPDuchesne Jul 24, 2026
22c4666
Cover the raises-with-interactions branch
JPDuchesne Jul 24, 2026
ec8d4d3
Remove the Rails README section
JPDuchesne Jul 24, 2026
72ba104
Rename BacktraceSourceMappingTest to SourceTrueBacktraceTest
JPDuchesne Jul 24, 2026
d7a3bcc
Fit code within 120 columns; rewrap comments to match
JPDuchesne Jul 24, 2026
8565cc9
Cover the dangling-block validation
JPDuchesne Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ruby: ['3.2', '3.3', '4.0']
ruby: ['3.3', '3.4', '4.0']
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,7 @@ build-iPhoneSimulator/
.rvmrc

# RubyMine
/.idea
/.idea

# Generated per-machine by `dev up` (d3mlabs dev tool); never committed
/.shadowenv.d/
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4.0.6
28 changes: 27 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,33 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [3.0.0] - Unreleased

### Fixed

- Locals assigned in a When block are now readable in Then and Cleanup when the test declares interactions. The thunked When body lowers to a non-lambda proc with method-scope pre-declarations, so `result = subject.call` in When no longer raises `NameError` in a Then expectation or in an ensure-run Cleanup — a long-standing bug of the hoisted-interaction era.

### Changed

- Adopted ast_transform 3.0 line-aligned emission: transformed code is emitted with every statement on its original source line, so backtraces, failure messages, breakpoints, and debugger display are source-true natively — with zero runtime filtering.
- Interactions in Then blocks are emitted at their own source lines; the When body they must follow is wrapped in an ast_transform thunk instead of textually hoisting the interaction setups. A `return` inside the thunked body still returns from the test method (proc semantics).
- Where-driven test names now embed the row line via internal block parameters (`__rspock_row_index__` / `__rspock_row_line__`); the appended `<index> line <line>` name suffix is unchanged.
- RSpock node classes now register on `ASTTransform::Node` (the promoted registry) instead of RSpock's hand-rolled `Node::REGISTRY`/`NodeBuilder`.
- **Breaking:** dropped Ruby 3.2 support (EOL since March 2026); `required_ruby_version` is now `>= 3.3`.

### Removed

- **Breaking:** the `_test_index_` and `_line_number_` test-scope variables. Isolate a Where row by running its generated test by name (the name embeds the row line; the failure output prints the exact rerun command) and break normally — see the README's "Isolating a Where Block row".
- **Breaking:** the Rails generator (`rails g rspock:install`) and its backtrace-cleaner initializer template — there is no backtrace cleaning to configure anymore.
- `RSpock::BacktraceFilter`, `RSpock::Minitest::BacktraceFilter`, and the Minitest plugin (`minitest/rspock_plugin`) — line-aligned emission makes raw VM line numbers the source line numbers, so the whole mapping apparatus is gone.
- The rescue wrapper previously injected around transformed class bodies for backtrace mapping.
- `MethodCallToLVarTransformation` (only existed to service `_test_index_`/`_line_number_`).
- The direct `parser` and `unparser` runtime dependencies: rspock uses both only through ast_transform, which owns their version floors.

### Added

- Subprocess acceptance tests pinning line-aligned behavior end to end: Then assertion failures, failing Where rows, interaction setups, and Cleanup failures all cite their own source lines with zero filter machinery; When-assigned locals stay readable in Then and in ensure-run Cleanup when interactions thunk the When body.
- The rspock agent skill (`skills/rspock/SKILL.md`) ships in the gem: the RSpock dialect's mechanics, pitfalls, and debugging triage for coding agents.

## [2.5.0] - 2026-02-28

Expand Down
26 changes: 12 additions & 14 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
PATH
remote: .
specs:
rspock (2.5.0)
ast_transform (~> 2.0)
rspock (3.0.0)
ast_transform (~> 3.0)
minitest (~> 5.0)
mocha (>= 1.0)
parser (>= 3.0)
unparser (>= 0.6)

GEM
remote: https://rubygems.org/
specs:
ansi (1.5.0)
ansi (1.6.0)
ast (2.4.3)
ast_transform (2.1.4)
parser (>= 3.0)
ast_transform (3.0.0)
parser (>= 3.3)
prism (>= 1.5)
unparser (>= 0.6)
unparser (>= 0.8)
builder (3.3.0)
byebug (13.0.0)
reline (>= 0.6.0)
Expand All @@ -26,14 +24,14 @@ GEM
io-console (0.8.2)
method_source (1.1.0)
minitest (5.27.0)
minitest-reporters (1.7.1)
minitest-reporters (1.8.0)
ansi
builder
minitest (>= 5.0)
minitest (>= 5.0, < 7)
ruby-progressbar
mocha (3.0.2)
mocha (3.1.0)
ruby2_keywords (>= 0.0.5)
parser (3.3.10.2)
parser (3.3.12.0)
ast (~> 2.4.1)
racc
prism (1.9.0)
Expand All @@ -45,7 +43,7 @@ GEM
byebug (~> 13.0)
pry (>= 0.13, < 0.17)
racc (1.8.1)
rake (13.3.1)
rake (13.4.2)
reline (0.6.3)
io-console (~> 0.5)
ruby-progressbar (1.13.0)
Expand All @@ -56,7 +54,7 @@ GEM
simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
unparser (0.8.2)
unparser (0.9.0)
diff-lcs (>= 1.6, < 3)
parser (>= 3.3.0)
prism (>= 1.5.1)
Expand Down
56 changes: 12 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,6 @@ rakefile = "#{spec.gem_dir}/lib/Rakefile"
load rakefile
```

### Rails

If you are using Rails, it is necessary to add a filter to *Rails.backtrace_cleaner* for [source mapping](#backtraces) to work, so that you get proper line numbers in Minitest backtraces. For your convenience, we've built a Rails Generator just for that:

$ rails g rspock:install

Note: If you are not using Rails, you don't have anything to do, as RSpock includes a Minitest plugin that will set its own backtrace filter.

## Usage

Getting started using RSpock is extremely easy!
Expand Down Expand Up @@ -297,6 +289,8 @@ This effectively creates one version of the Feature Method for each data row. No

You might have noticed above that the test name contains string interpolations, that's one of the features of RSpock! You can interpolate test names and use Where block header variables to parameterize the test name using the test data.

RSpock also appends each data row's index and source line number to the generated test name (e.g. `... 1 line 15`). This keeps names unique for identical data rows and gives the `-n` selector a stable target for isolating a row (see [Isolating a Where Block row](#isolating-a-where-block-row)).

##### Truth Table

Formatting Where Blocks as a truth table is the recommended way to organize your Where Blocks in RSpock. It makes test cases more maintainable since it creates an order between the different test cases and makes it easier to spot missing or duplicated test cases. It also makes it easier to add a column:
Expand Down Expand Up @@ -586,47 +580,19 @@ This produces tests that are simpler, more explicit, and less coupled to impleme

## Debugging

### Pry

Let's be honest, at some point you will need to debug your tests. Because RSpock requires transforming the AST, the executed code is slightly different from the source code that you wrote. Although we have plans to add Source Mapping support in Pry so that you can see the exact source code you wrote, this is not currently available. This means that code shown in the Pry console will be slightly different. We think the value of using RSpock greatly outweighs this current limitation. We encourage you to try debugging a test to see what the transformed code looks like, or look through `tmp/rspock` for the transformed files.
Debugging just works. The transformed code is emitted line-aligned — every statement occupies its original source line — so backtraces, failure messages, `break file:line` breakpoints, and debugger display (Pry, byebug, debug.rb) all point at the file you wrote, with no filtering or mapping layer in between. The transformed files under `tmp/ast_transform` are a pure debugging artifact: their line numbers match your source exactly.

### Backtraces
One documented oddity: interaction setups in a `Then` block execute *before* the `When` body (see [Execution Order](#execution-order)), so stepping through a test with interactions jumps from the interaction lines back up to the `When` line once.

RSpock supports Source Mapping so that backtraces for the executed code point to the correct line numbers in your source code, and so that the correct files are referenced. This is achieved through wrapping the executed code in rescue blocks, processing the backtraces (source mapping) and re-raising the error.
### Isolating a Where Block row

### Tips and Tricks
Each `Where` data row generates a separate test whose name embeds the row's index and source line number (e.g. `... 1 line 15`), so isolating a row is standard Minitest:

#### _test_index_ and _line_number_
* **Newly failing row:** copy the rerun command printed with the failure (the test name embeds the row line), add a plain `binding.pry` (or `debugger`) in the test body, and run it. It fires exactly once, for that row, with the column values inspectable as locals.
* **Chosen row by position:** run with `-n` and a regex on the row's line number from your editor gutter, e.g. `-n /line_15/`.
* **Chosen row by meaning:** use a conditional break on the column locals themselves, e.g. `binding.pry if input == "not json"` — this survives row reordering.

The generated test name for each test case will contain the test index and the line number, corresponding to the Where Block data row for that case, which is available in the test scope as `_test_index_` and `_line_number_` respectively. This can be leveraged to conditionally break on certain test cases, so that you can have a more granular debugging session.

```ruby
test "Adding #{a} and #{b} results in #{c}" do
When "Adding two numbers"
actual = a + b

Then "We get the expected result"
# Breaks on the first test case
binding.pry if _test_index_ == 0
# Breaks on the second test case
binding.pry if _line_number_ == 15
actual == c

Where
a | b | c
1 | 2 | 3
4 | 5 | 9 # Line 15
end
```

A few notes:

* Comparison with `_test_index_` and `_line_number_` is not transformed to assertions in Then and Expect Code Blocks
* `_test_index_` is zero-based, meaning the index of the first test case is `0`

#### _line_number_

The Line number is extremely useful for figuring out exactly which test case failed in your Where Block, especially if you have many rows in your Where Block data table.
Note: a source-line *breakpoint* on a data row cannot isolate that row's run — the Where table is evaluated once, in class scope, when tests are defined. Name-selection is the row-isolation mechanism.

## More info

Expand Down Expand Up @@ -667,6 +633,8 @@ end

After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.

If you use the [d3mlabs dev tool](https://github.com/d3mlabs/dev), `dev up` provisions the pinned Ruby (see `.ruby-version`) with a per-project shadowenv, and `dev test` runs the suite — plain Bundler as above works just as well.

To install this gem onto your local machine, run `bundle exec rake install`.

## Releasing a New Version
Expand Down
11 changes: 11 additions & 0 deletions dependencies.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

# Toolchain-only manifest for d3mlabs' dev tool: it provisions this exact
# Ruby (rbenv + shadowenv) for `dev` commands. Gems stay bundler-managed
# through the hand-written gemspec/Gemfile; contributors without dev can
# ignore this file and use .ruby-version.
require "dev/deps"

Dev::Deps.define do
ruby "4.0.6"
end
8 changes: 8 additions & 0 deletions dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: rspock
commands:
up:
desc: Install gems
run: bundle install
test:
desc: Run this repo's tests
run: bundle exec rake test
17 changes: 0 additions & 17 deletions lib/generators/rspock/install_generator.rb

This file was deleted.

2 changes: 0 additions & 2 deletions lib/generators/templates/rspock_initializer.rb

This file was deleted.

10 changes: 0 additions & 10 deletions lib/minitest/rspock_plugin.rb

This file was deleted.

1 change: 0 additions & 1 deletion lib/rspock.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'rspock/version'

require 'rspock/backtrace_filter'
require 'rspock/declarative'

require 'ast_transform'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ def run(interaction)
result = chain_call(interaction.receiver, :expects, s(:sym, interaction.message))
result = chain_call(result, :with, *interaction.args.children) if interaction.args
result = build_cardinality(result, interaction.cardinality)
result = chain_call(result, OUTCOME_METHODS.fetch(interaction.outcome.type), *interaction.outcome.children) if interaction.outcome
if interaction.outcome
result = chain_call(result, OUTCOME_METHODS.fetch(interaction.outcome.type), *interaction.outcome.children)
end

if interaction.block_pass
build_block_capture_setup(result, interaction.receiver, interaction.message)
Expand Down
24 changes: 0 additions & 24 deletions lib/rspock/ast/method_call_to_lvar_transformation.rb

This file was deleted.

Loading
Loading