rspock 2.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d6c10b52d342827b092af647c5bc144f4e381dff91167eedf93a0f99c9488ee2
4
- data.tar.gz: 20ea274b4fa0e3a79eefb88f33c53ddf84adfc582fd37fe3e2ee9688a8137747
3
+ metadata.gz: 78c080ee35d24e8f92601d74cef95ea7bd3e98bee05a47c2ab0a85b327efdf6a
4
+ data.tar.gz: dd3636b68aca10d31b3813183622ecd3dbef278194c522692bd3f542dd39c08e
5
5
  SHA512:
6
- metadata.gz: '019855311bf85f23d10fb75d3b1c0020b52fbf2ae7083205ca69a3f15b142ee7c2ba24e9be0506c690e65b42302b347ea7dc256c596edf1069ec8a0700aad666'
7
- data.tar.gz: a559d70b0f3509d361234019935a2105c4a2b96ddac5ce0df7198edff346fe83a5c7a9c889d5152fbf0a856a60edd069017c3b24f88b144eda651ab35a57b47b
6
+ metadata.gz: 6a671720b23a938a90a6d7ad598aa445b457cb60c9e98c77d8c88be16f0dd0070a49e343d778a4832c6738dc839fa414653118af3dd0d76e766b378b9d46803b
7
+ data.tar.gz: 32426daeb51bde329ba4c501c589be13d95c17ecaea9471cc397a3912621fa0336c616d08cb969bd5fee737f08658380d168ac7362a0da7170448e258928e5e8
@@ -0,0 +1 @@
1
+ * @JPDuchesne
@@ -12,7 +12,7 @@ jobs:
12
12
  strategy:
13
13
  fail-fast: false
14
14
  matrix:
15
- ruby: ['3.2', '3.3', '4.0']
15
+ ruby: ['3.3', '3.4', '4.0']
16
16
  steps:
17
17
  - uses: actions/checkout@v4
18
18
  - uses: ruby/setup-ruby@v1
data/.gitignore CHANGED
@@ -50,4 +50,7 @@ build-iPhoneSimulator/
50
50
  .rvmrc
51
51
 
52
52
  # RubyMine
53
- /.idea
53
+ /.idea
54
+
55
+ # Generated per-machine by `dev up` (d3mlabs dev tool); never committed
56
+ /.shadowenv.d/
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 4.0.6
data/CHANGELOG.md CHANGED
@@ -5,7 +5,33 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [3.0.0] - 2026-07-24
9
+
10
+ ### Fixed
11
+
12
+ - 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.
13
+
14
+ ### Changed
15
+
16
+ - 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.
17
+ - 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).
18
+ - 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.
19
+ - RSpock node classes now register on `ASTTransform::Node` (the promoted registry) instead of RSpock's hand-rolled `Node::REGISTRY`/`NodeBuilder`.
20
+ - **Breaking:** dropped Ruby 3.2 support (EOL since March 2026); `required_ruby_version` is now `>= 3.3`.
21
+
22
+ ### Removed
23
+
24
+ - **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".
25
+ - **Breaking:** the Rails generator (`rails g rspock:install`) and its backtrace-cleaner initializer template — there is no backtrace cleaning to configure anymore.
26
+ - `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.
27
+ - The rescue wrapper previously injected around transformed class bodies for backtrace mapping.
28
+ - `MethodCallToLVarTransformation` (only existed to service `_test_index_`/`_line_number_`).
29
+ - The direct `parser` and `unparser` runtime dependencies: rspock uses both only through ast_transform, which owns their version floors.
30
+
31
+ ### Added
32
+
33
+ - 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.
34
+ - The rspock agent skill (`skills/rspock/SKILL.md`) ships in the gem: the RSpock dialect's mechanics, pitfalls, and debugging triage for coding agents.
9
35
 
10
36
  ## [2.5.0] - 2026-02-28
11
37
 
@@ -152,7 +178,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
152
178
 
153
179
  - Initial release.
154
180
 
155
- [Unreleased]: https://github.com/rspockframework/rspock/compare/v2.5.0...HEAD
181
+ [Unreleased]: https://github.com/rspockframework/rspock/compare/v3.0.0...HEAD
182
+ [3.0.0]: https://github.com/rspockframework/rspock/compare/v2.5.0...v3.0.0
156
183
  [2.5.0]: https://github.com/rspockframework/rspock/compare/v2.4.0...v2.5.0
157
184
  [2.4.0]: https://github.com/rspockframework/rspock/compare/v2.3.1...v2.4.0
158
185
  [2.3.1]: https://github.com/rspockframework/rspock/compare/v2.3.0...v2.3.1
data/Gemfile.lock CHANGED
@@ -1,22 +1,20 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rspock (2.5.0)
5
- ast_transform (~> 2.0)
4
+ rspock (3.0.0)
5
+ ast_transform (~> 3.0)
6
6
  minitest (~> 5.0)
7
7
  mocha (>= 1.0)
8
- parser (>= 3.0)
9
- unparser (>= 0.6)
10
8
 
11
9
  GEM
12
10
  remote: https://rubygems.org/
13
11
  specs:
14
- ansi (1.5.0)
12
+ ansi (1.6.0)
15
13
  ast (2.4.3)
16
- ast_transform (2.1.4)
17
- parser (>= 3.0)
14
+ ast_transform (3.0.0)
15
+ parser (>= 3.3)
18
16
  prism (>= 1.5)
19
- unparser (>= 0.6)
17
+ unparser (>= 0.8)
20
18
  builder (3.3.0)
21
19
  byebug (13.0.0)
22
20
  reline (>= 0.6.0)
@@ -26,14 +24,14 @@ GEM
26
24
  io-console (0.8.2)
27
25
  method_source (1.1.0)
28
26
  minitest (5.27.0)
29
- minitest-reporters (1.7.1)
27
+ minitest-reporters (1.8.0)
30
28
  ansi
31
29
  builder
32
- minitest (>= 5.0)
30
+ minitest (>= 5.0, < 7)
33
31
  ruby-progressbar
34
- mocha (3.0.2)
32
+ mocha (3.1.0)
35
33
  ruby2_keywords (>= 0.0.5)
36
- parser (3.3.10.2)
34
+ parser (3.3.12.0)
37
35
  ast (~> 2.4.1)
38
36
  racc
39
37
  prism (1.9.0)
@@ -45,7 +43,7 @@ GEM
45
43
  byebug (~> 13.0)
46
44
  pry (>= 0.13, < 0.17)
47
45
  racc (1.8.1)
48
- rake (13.3.1)
46
+ rake (13.4.2)
49
47
  reline (0.6.3)
50
48
  io-console (~> 0.5)
51
49
  ruby-progressbar (1.13.0)
@@ -56,7 +54,7 @@ GEM
56
54
  simplecov_json_formatter (~> 0.1)
57
55
  simplecov-html (0.13.2)
58
56
  simplecov_json_formatter (0.1.4)
59
- unparser (0.8.2)
57
+ unparser (0.9.0)
60
58
  diff-lcs (>= 1.6, < 3)
61
59
  parser (>= 3.3.0)
62
60
  prism (>= 1.5.1)
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  RSpock is a testing and specification framework built on top of Minitest. It intends to give back productivity to its users with its incredibly simple yet highly expressive specification language.
7
7
 
8
- Note: RSpock is heavily inspired by Spock for the Groovy programming language.
8
+ Note: RSpock is heavily inspired by [Spock](https://github.com/spockframework/spock) for the Groovy programming language.
9
9
 
10
10
  ## Goals
11
11
 
@@ -57,14 +57,6 @@ rakefile = "#{spec.gem_dir}/lib/Rakefile"
57
57
  load rakefile
58
58
  ```
59
59
 
60
- ### Rails
61
-
62
- 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:
63
-
64
- $ rails g rspock:install
65
-
66
- 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.
67
-
68
60
  ## Usage
69
61
 
70
62
  Getting started using RSpock is extremely easy!
@@ -297,6 +289,8 @@ This effectively creates one version of the Feature Method for each data row. No
297
289
 
298
290
  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.
299
291
 
292
+ 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)).
293
+
300
294
  ##### Truth Table
301
295
 
302
296
  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:
@@ -537,49 +531,68 @@ end
537
531
 
538
532
  **Note**: Inline blocks (`do...end` or `{ }`) are not supported in interactions and will raise an `InteractionError`. Use a named proc or lambda variable with `&` instead, since block forwarding verification requires a reference to compare against.
539
533
 
540
- ## Debugging
534
+ #### Why Interactions Don't Support Block Yielding
535
+
536
+ In Groovy Spock, you can make a mocked method yield its block via closure-based interaction responses. RSpock intentionally omits this. The `>>` operator stubs a return value only — it does not invoke the block argument passed to the mocked method.
541
537
 
542
- ### Pry
538
+ This is a deliberate design choice. If your test requires a mock to yield a block, it typically indicates one of two design smells:
543
539
 
544
- 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.
540
+ 1. **The unit under test is doing too much.** A method that passes a block to a collaborator and then depends on that block's side effects to function correctly has too much coupling between the block body and the surrounding logic. Split the block's work into a separate, independently testable step.
545
541
 
546
- ### Backtraces
542
+ 2. **The interaction boundary is in the wrong place.** If the block reads from IO, mutates state, or produces a return value that the caller depends on, the mock boundary cuts through the middle of a single responsibility. Restructure so the mock boundary sits between responsibilities, not within one.
547
543
 
548
- 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.
544
+ **Example: the better pattern**
549
545
 
550
- ### Tips and Tricks
546
+ Suppose you have a method that wraps work in a block-based collaborator:
551
547
 
552
- #### _test_index_ and _line_number_
548
+ ```ruby
549
+ # Tightly coupled — block body depends on IO and collaborator
550
+ def handle_spin(label, io)
551
+ @ui.with_spinner(label) do
552
+ drain_lines(io) # block reads from IO; impossible to mock without yielding
553
+ end
554
+ end
555
+ ```
553
556
 
554
- 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.
557
+ Testing this requires the mock to yield the block, otherwise `drain_lines` never runs and the IO stream is left unread. The fix is to separate the two concerns:
555
558
 
556
559
  ```ruby
557
- test "Adding #{a} and #{b} results in #{c}" do
558
- When "Adding two numbers"
559
- actual = a + b
560
+ # Decoupled each step is independently testable
561
+ def handle_spin(label, io)
562
+ success = drain_lines(io)
563
+ success ? @ui.ok(label) : @ui.fail(label)
564
+ end
565
+ ```
560
566
 
561
- Then "We get the expected result"
562
- # Breaks on the first test case
563
- binding.pry if _test_index_ == 0
564
- # Breaks on the second test case
565
- binding.pry if _line_number_ == 15
566
- actual == c
567
+ Now `drain_lines` can be tested in isolation with a real IO (e.g. `StringIO`), and the UI interaction is a simple mock expectation with no block involved:
567
568
 
568
- Where
569
- a | b | c
570
- 1 | 2 | 3
571
- 4 | 5 | 9 # Line 15
569
+ ```ruby
570
+ test "reports ok after draining" do
571
+ When "handle_spin completes"
572
+ handle_spin("Fetching", io)
573
+
574
+ Then "ok is called"
575
+ 1 * @ui.ok("Fetching")
572
576
  end
573
577
  ```
574
578
 
575
- A few notes:
579
+ This produces tests that are simpler, more explicit, and less coupled to implementation details. The constraint is the feature.
576
580
 
577
- * Comparison with `_test_index_` and `_line_number_` is not transformed to assertions in Then and Expect Code Blocks
578
- * `_test_index_` is zero-based, meaning the index of the first test case is `0`
581
+ ## Debugging
582
+
583
+ 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.
584
+
585
+ 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.
579
586
 
580
- #### _line_number_
587
+ ### Isolating a Where Block row
581
588
 
582
- 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.
589
+ 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:
590
+
591
+ * **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.
592
+ * **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/`.
593
+ * **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.
594
+
595
+ 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.
583
596
 
584
597
  ## More info
585
598
 
@@ -620,6 +633,8 @@ end
620
633
 
621
634
  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.
622
635
 
636
+ 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.
637
+
623
638
  To install this gem onto your local machine, run `bundle exec rake install`.
624
639
 
625
640
  ## Releasing a New Version
data/dependencies.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Toolchain-only manifest for d3mlabs' dev tool: it provisions this exact
4
+ # Ruby (rbenv + shadowenv) for `dev` commands. Gems stay bundler-managed
5
+ # through the hand-written gemspec/Gemfile; contributors without dev can
6
+ # ignore this file and use .ruby-version.
7
+ require "dev/deps"
8
+
9
+ Dev::Deps.define do
10
+ ruby "4.0.6"
11
+ end
data/dev.yml ADDED
@@ -0,0 +1,8 @@
1
+ name: rspock
2
+ commands:
3
+ up:
4
+ desc: Install gems
5
+ run: bundle install
6
+ test:
7
+ desc: Run this repo's tests
8
+ run: bundle exec rake test
@@ -30,7 +30,9 @@ module RSpock
30
30
  result = chain_call(interaction.receiver, :expects, s(:sym, interaction.message))
31
31
  result = chain_call(result, :with, *interaction.args.children) if interaction.args
32
32
  result = build_cardinality(result, interaction.cardinality)
33
- result = chain_call(result, OUTCOME_METHODS.fetch(interaction.outcome.type), *interaction.outcome.children) if interaction.outcome
33
+ if interaction.outcome
34
+ result = chain_call(result, OUTCOME_METHODS.fetch(interaction.outcome.type), *interaction.outcome.children)
35
+ end
34
36
 
35
37
  if interaction.block_pass
36
38
  build_block_capture_setup(result, interaction.receiver, interaction.message)
@@ -1,22 +1,13 @@
1
1
  # frozen_string_literal: true
2
- require 'ast_transform/transformation_helper'
2
+ require 'ast_transform/node'
3
3
 
4
4
  module RSpock
5
5
  module AST
6
- class Node < ::Parser::AST::Node
7
- REGISTRY = {}
8
-
9
- def self.register(type)
10
- REGISTRY[type] = self
11
- end
12
-
13
- def self.build(type, *children)
14
- klass = REGISTRY[type] || self
15
- klass.new(type, children)
16
- end
17
- end
18
-
19
- class TestNode < Node
6
+ # RSpock's intermediate representation: custom node types registered on ASTTransform::Node, so
7
+ # +s(:rspock_*, ...)+ constructs these classes with their domain accessors. They exist only between the parser
8
+ # and TestMethodTransformation — the transformation lowers every one of them to plain Ruby nodes before
9
+ # emission.
10
+ class TestNode < ASTTransform::Node
20
11
  register :rspock_test
21
12
 
22
13
  def def_node = children[0]
@@ -24,38 +15,38 @@ module RSpock
24
15
  def where_node = children[2]
25
16
  end
26
17
 
27
- class BodyNode < Node
18
+ class BodyNode < ASTTransform::Node
28
19
  register :rspock_body
29
20
  end
30
21
 
31
- class DefNode < Node
22
+ class DefNode < ASTTransform::Node
32
23
  register :rspock_def
33
24
 
34
25
  def method_call = children[0]
35
26
  def args = children[1]
36
27
  end
37
28
 
38
- class GivenNode < Node
29
+ class GivenNode < ASTTransform::Node
39
30
  register :rspock_given
40
31
  end
41
32
 
42
- class WhenNode < Node
33
+ class WhenNode < ASTTransform::Node
43
34
  register :rspock_when
44
35
  end
45
36
 
46
- class ThenNode < Node
37
+ class ThenNode < ASTTransform::Node
47
38
  register :rspock_then
48
39
  end
49
40
 
50
- class ExpectNode < Node
41
+ class ExpectNode < ASTTransform::Node
51
42
  register :rspock_expect
52
43
  end
53
44
 
54
- class CleanupNode < Node
45
+ class CleanupNode < ASTTransform::Node
55
46
  register :rspock_cleanup
56
47
  end
57
48
 
58
- class WhereNode < Node
49
+ class WhereNode < ASTTransform::Node
59
50
  register :rspock_where
60
51
 
61
52
  def header
@@ -70,7 +61,7 @@ module RSpock
70
61
  end
71
62
  end
72
63
 
73
- class OutcomeNode < Node
64
+ class OutcomeNode < ASTTransform::Node
74
65
  end
75
66
 
76
67
  class StubReturnsNode < OutcomeNode
@@ -81,7 +72,7 @@ module RSpock
81
72
  register :rspock_stub_raises
82
73
  end
83
74
 
84
- class RaisesNode < Node
75
+ class RaisesNode < ASTTransform::Node
85
76
  register :rspock_raises
86
77
 
87
78
  def exception_class = children[0]
@@ -89,7 +80,7 @@ module RSpock
89
80
  def capture_name = capture_var&.children&.[](0)
90
81
  end
91
82
 
92
- class InteractionNode < Node
83
+ class InteractionNode < ASTTransform::Node
93
84
  register :rspock_interaction
94
85
 
95
86
  def cardinality = children[0]
@@ -101,7 +92,7 @@ module RSpock
101
92
  def block_pass = children[5]
102
93
  end
103
94
 
104
- class BinaryStatementNode < Node
95
+ class BinaryStatementNode < ASTTransform::Node
105
96
  register :rspock_binary_statement
106
97
 
107
98
  def lhs = children[0]
@@ -109,23 +100,11 @@ module RSpock
109
100
  def rhs = children[2]
110
101
  end
111
102
 
112
- class StatementNode < Node
103
+ class StatementNode < ASTTransform::Node
113
104
  register :rspock_statement
114
105
 
115
106
  def expression = children[0]
116
107
  def source = children[1]
117
108
  end
118
-
119
- module NodeBuilder
120
- include ASTTransform::TransformationHelper
121
-
122
- def s(type, *children)
123
- if type.to_s.start_with?('rspock_')
124
- Node.build(type, *children)
125
- else
126
- super
127
- end
128
- end
129
- end
130
109
  end
131
110
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'ast_transform/transformation_helper'
2
3
  require 'rspock/ast/node'
3
4
 
4
5
  module RSpock
@@ -7,7 +8,7 @@ module RSpock
7
8
  class BlockError < StandardError; end
8
9
 
9
10
  class Block
10
- include RSpock::AST::NodeBuilder
11
+ include ASTTransform::TransformationHelper
11
12
 
12
13
  # Constructs a new Block.
13
14
  #
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'ast_transform/transformation_helper'
2
3
  require 'rspock/ast/node'
3
4
 
4
5
  module RSpock
@@ -17,7 +18,7 @@ module RSpock
17
18
  # [4] outcome - nil if no >>, otherwise s(:rspock_stub_returns, value) or s(:rspock_stub_raises, *args)
18
19
  # [5] block_pass - nil if no &, otherwise s(:block_pass, ...)
19
20
  class InteractionParser
20
- include RSpock::AST::NodeBuilder
21
+ include ASTTransform::TransformationHelper
21
22
 
22
23
  class InteractionError < RuntimeError; end
23
24
 
@@ -34,6 +35,10 @@ module RSpock
34
35
  def parse(node)
35
36
  return node unless interaction_node?(node)
36
37
 
38
+ # Anchor at the full interaction expression (including >> outcome), so the Mocha setup it lowers into is
39
+ # emitted at the interaction's line.
40
+ anchor = node
41
+
37
42
  if return_value_node?(node)
38
43
  outcome = parse_outcome(node.children[2])
39
44
  node = node.children[0]
@@ -45,7 +50,7 @@ module RSpock
45
50
  rhs = node.children[2]
46
51
  receiver, message, args, block_pass = parse_rhs(rhs)
47
52
 
48
- s(:rspock_interaction,
53
+ interaction = s(:rspock_interaction,
49
54
  cardinality,
50
55
  receiver,
51
56
  s(:sym, message),
@@ -53,6 +58,8 @@ module RSpock
53
58
  outcome,
54
59
  block_pass
55
60
  )
61
+
62
+ anchor.loc&.expression ? s_at(anchor, interaction.type, *interaction.children) : interaction
56
63
  end
57
64
 
58
65
  private
@@ -101,8 +108,8 @@ module RSpock
101
108
 
102
109
  def parse_rhs(node)
103
110
  if node.type == :block
104
- raise InteractionError, "Inline blocks (do...end / { }) are not supported in interactions @ #{range(node)}. " \
105
- "Use &var for block forwarding verification, or << for method body override (future)."
111
+ raise InteractionError, "Inline blocks (do...end / { }) are not supported in interactions " \
112
+ "@ #{range(node)}. Use &var for block forwarding verification, or << for method body override (future)."
106
113
  end
107
114
 
108
115
  if node.type != :send
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'ast_transform/transformation_helper'
2
3
  require 'rspock/ast/node'
3
4
 
4
5
  module RSpock
@@ -10,7 +11,7 @@ module RSpock
10
11
  # - Binary operators (==, !=, =~, etc.) become :rspock_binary_statement nodes.
11
12
  # - Everything else becomes :rspock_statement nodes with the original source text captured.
12
13
  class StatementParser
13
- include RSpock::AST::NodeBuilder
14
+ include ASTTransform::TransformationHelper
14
15
 
15
16
  BINARY_OPERATORS = %i[== != =~ !~ > < >= <=].freeze
16
17
  ASSIGNMENT_TYPES = %i[lvasgn masgn op_asgn or_asgn and_asgn].freeze
@@ -44,10 +45,10 @@ module RSpock
44
45
  if node.type == :lvasgn
45
46
  variable = s(:sym, node.children[0])
46
47
  exception_class = node.children[1].children[2]
47
- s(:rspock_raises, exception_class, variable)
48
+ s_anchored(node, :rspock_raises, exception_class, variable)
48
49
  else
49
50
  exception_class = node.children[2]
50
- s(:rspock_raises, exception_class)
51
+ s_anchored(node, :rspock_raises, exception_class)
51
52
  end
52
53
  end
53
54
 
@@ -61,13 +62,20 @@ module RSpock
61
62
  BINARY_OPERATORS.include?(node.children[1])
62
63
  end
63
64
 
65
+ # RSpock IR nodes are anchored at the statement they classify, so the assertions they lower into are
66
+ # emitted at the statement's source line.
64
67
  def build_binary_statement(node)
65
- s(:rspock_binary_statement, node.children[0], s(:sym, node.children[1]), node.children[2])
68
+ s_anchored(node, :rspock_binary_statement, node.children[0], s(:sym, node.children[1]), node.children[2])
66
69
  end
67
70
 
68
71
  def build_statement(node)
69
72
  source = node.loc&.expression&.source || node.inspect
70
- s(:rspock_statement, node, s(:str, source))
73
+ s_anchored(node, :rspock_statement, node, s(:str, source))
74
+ end
75
+
76
+ # s_at that tolerates loc-less anchors (unit tests build synthetic ASTs).
77
+ def s_anchored(anchor, type, *children)
78
+ anchor.loc&.expression ? s_at(anchor, type, *children) : s(type, *children)
71
79
  end
72
80
  end
73
81
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'ast_transform/transformation_helper'
2
3
  require 'rspock/ast/node'
3
4
 
4
5
  module RSpock
@@ -12,7 +13,7 @@ module RSpock
12
13
  # s(:rspock_body, s(:rspock_given, ...), s(:rspock_when, ...), ...),
13
14
  # s(:rspock_where, ...)) # optional
14
15
  class TestMethodParser
15
- include RSpock::AST::NodeBuilder
16
+ include ASTTransform::TransformationHelper
16
17
 
17
18
  def initialize(block_registry, strict: true)
18
19
  @block_registry = block_registry
@@ -74,7 +75,8 @@ module RSpock
74
75
  end
75
76
 
76
77
  unless blocks.last.can_end?
77
- raise BlockError, "Block #{blocks.last.type} @ #{blocks.last.range} must be followed by one of these Blocks: #{blocks.last.successors}"
78
+ raise BlockError, "Block #{blocks.last.type} @ #{blocks.last.range} " \
79
+ "must be followed by one of these Blocks: #{blocks.last.successors}"
78
80
  end
79
81
  end
80
82
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'ast_transform/transformation_helper'
2
3
  require 'rspock/ast/node'
3
4
 
4
5
  module RSpock
@@ -8,7 +9,7 @@ module RSpock
8
9
  # Binary statements dispatch to specialized assertions (assert_equal, assert_match, assert_operator).
9
10
  # General statements use assert_equal(true/false, expr, source_message) with negation detection.
10
11
  class StatementToAssertionTransformation
11
- include RSpock::AST::NodeBuilder
12
+ include ASTTransform::TransformationHelper
12
13
 
13
14
  BINARY_DISPATCH = {
14
15
  :== => :assert_equal,
@@ -4,15 +4,16 @@ require 'rspock/ast/test_method_dstr_transformation'
4
4
 
5
5
  module RSpock
6
6
  module AST
7
+ # Appends the data row's index and source line to a Where-driven test's name. The values arrive through
8
+ # internal block parameters on the row iterator (see TestMethodTransformation#build_where_args) and surface in
9
+ # the test NAME only: the name is what makes identical data rows unique and what the -n selector matches to
10
+ # isolate a row. They are deliberately not exposed as test-scope variables.
7
11
  class TestMethodDefTransformation < ASTTransform::AbstractTransformation
8
- TEST_INDEX_AST = s(:begin,
9
- s(:lvar, :_test_index_))
10
-
11
- LINE_NUMBER_AST = s(:begin,
12
- s(:lvar, :_line_number_))
13
-
14
- SPACE_STR_AST = s(:str, " ")
12
+ ROW_INDEX_ARG = :__rspock_row_index__
13
+ ROW_LINE_ARG = :__rspock_row_line__
15
14
 
15
+ ROW_INDEX_AST = s(:begin, s(:lvar, ROW_INDEX_ARG))
16
+ ROW_LINE_AST = s(:begin, s(:lvar, ROW_LINE_ARG))
16
17
  LINE_NUMBER_STR_AST = s(:str, " line ")
17
18
 
18
19
  def run(node)
@@ -23,7 +24,7 @@ module RSpock
23
24
 
24
25
  def on_str(node)
25
26
  merged = s(:str, "#{node.children[0]} ")
26
- node.updated(:dstr, [merged, TEST_INDEX_AST, LINE_NUMBER_STR_AST, LINE_NUMBER_AST])
27
+ node.updated(:dstr, [merged, ROW_INDEX_AST, LINE_NUMBER_STR_AST, ROW_LINE_AST])
27
28
  end
28
29
 
29
30
  def on_dstr(node)