rspock 2.4.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 +4 -4
- data/.github/CODEOWNERS +1 -0
- data/.github/workflows/ci.yml +1 -1
- data/.gitignore +4 -1
- data/.ruby-version +1 -0
- data/CHANGELOG.md +42 -2
- data/Gemfile.lock +12 -14
- data/README.md +91 -35
- data/dependencies.rb +11 -0
- data/dev.yml +8 -0
- data/lib/rspock/ast/interaction_to_mocha_mock_transformation.rb +7 -5
- data/lib/rspock/ast/node.rb +29 -42
- data/lib/rspock/ast/parser/block.rb +2 -1
- data/lib/rspock/ast/parser/expect_block.rb +5 -0
- data/lib/rspock/ast/parser/interaction_parser.rb +14 -7
- data/lib/rspock/ast/parser/statement_parser.rb +38 -3
- data/lib/rspock/ast/parser/test_method_parser.rb +4 -2
- data/lib/rspock/ast/parser/then_block.rb +5 -0
- data/lib/rspock/ast/statement_to_assertion_transformation.rb +2 -1
- data/lib/rspock/ast/test_method_def_transformation.rb +9 -8
- data/lib/rspock/ast/test_method_dstr_transformation.rb +5 -7
- data/lib/rspock/ast/test_method_transformation.rb +164 -80
- data/lib/rspock/ast/transformation.rb +1 -27
- data/lib/rspock/version.rb +1 -1
- data/lib/rspock.rb +0 -1
- data/rspock.gemspec +3 -4
- data/skills/rspock/SKILL.md +198 -0
- metadata +10 -39
- data/lib/generators/rspock/install_generator.rb +0 -17
- data/lib/generators/templates/rspock_initializer.rb +0 -2
- data/lib/minitest/rspock_plugin.rb +0 -10
- data/lib/rspock/ast/method_call_to_lvar_transformation.rb +0 -24
- data/lib/rspock/backtrace_filter.rb +0 -53
- data/lib/rspock/minitest/backtrace_filter.rb +0 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78c080ee35d24e8f92601d74cef95ea7bd3e98bee05a47c2ab0a85b327efdf6a
|
|
4
|
+
data.tar.gz: dd3636b68aca10d31b3813183622ecd3dbef278194c522692bd3f542dd39c08e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6a671720b23a938a90a6d7ad598aa445b457cb60c9e98c77d8c88be16f0dd0070a49e343d778a4832c6738dc839fa414653118af3dd0d76e766b378b9d46803b
|
|
7
|
+
data.tar.gz: 32426daeb51bde329ba4c501c589be13d95c17ecaea9471cc397a3912621fa0336c616d08cb969bd5fee737f08658380d168ac7362a0da7170448e258928e5e8
|
data/.github/CODEOWNERS
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* @JPDuchesne
|
data/.github/workflows/ci.yml
CHANGED
data/.gitignore
CHANGED
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.6
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,45 @@ 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
|
-
## [
|
|
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.
|
|
35
|
+
|
|
36
|
+
## [2.5.0] - 2026-02-28
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- Exception conditions: `raises ExceptionClass` in Then blocks wraps the preceding When block in an exception assertion.
|
|
41
|
+
- Exception capture: `e = raises ExceptionClass` captures the exception for further assertions in the same Then block.
|
|
42
|
+
- Exception conditions work with data-driven `Where` blocks.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- Renamed interaction outcome nodes from `rspock_returns` / `rspock_raises` to `rspock_stub_returns` / `rspock_stub_raises` to distinguish them from the new exception condition `rspock_raises`.
|
|
9
47
|
|
|
10
48
|
## [2.4.0] - 2026-02-28
|
|
11
49
|
|
|
@@ -140,7 +178,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
140
178
|
|
|
141
179
|
- Initial release.
|
|
142
180
|
|
|
143
|
-
[Unreleased]: https://github.com/rspockframework/rspock/compare/
|
|
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
|
|
183
|
+
[2.5.0]: https://github.com/rspockframework/rspock/compare/v2.4.0...v2.5.0
|
|
144
184
|
[2.4.0]: https://github.com/rspockframework/rspock/compare/v2.3.1...v2.4.0
|
|
145
185
|
[2.3.1]: https://github.com/rspockframework/rspock/compare/v2.3.0...v2.3.1
|
|
146
186
|
[2.3.0]: https://github.com/rspockframework/rspock/compare/v2.2.0...v2.3.0
|
data/Gemfile.lock
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
rspock (
|
|
5
|
-
ast_transform (~>
|
|
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.
|
|
12
|
+
ansi (1.6.0)
|
|
15
13
|
ast (2.4.3)
|
|
16
|
-
ast_transform (
|
|
17
|
-
parser (>= 3.
|
|
14
|
+
ast_transform (3.0.0)
|
|
15
|
+
parser (>= 3.3)
|
|
18
16
|
prism (>= 1.5)
|
|
19
|
-
unparser (>= 0.
|
|
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.
|
|
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
|
|
32
|
+
mocha (3.1.0)
|
|
35
33
|
ruby2_keywords (>= 0.0.5)
|
|
36
|
-
parser (3.3.
|
|
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.
|
|
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.
|
|
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
|
|
|
@@ -19,6 +19,7 @@ Note: RSpock is heavily inspired by Spock for the Groovy programming language.
|
|
|
19
19
|
* Spock-style implicit assertions: every statement in Then/Expect blocks is an assertion (no assertion API needed)
|
|
20
20
|
* Binary operator assertions: `==`, `!=`, `=~`, `!~`, `>`, `<`, `>=`, `<=` with clear error messages
|
|
21
21
|
* General statement assertions: bare boolean expressions (e.g. `obj.valid?`, `list.include?(x)`) and negation (`!obj.empty?`) with source-text error messages
|
|
22
|
+
* [Exception conditions](#exception-conditions): `raises ExceptionClass` in Then blocks wraps the When block in an exception assertion, with optional capture for property assertions
|
|
22
23
|
* [Interaction-based testing](#mocking-with-interactions), i.e. `1 * object.receive("message")` in Then blocks, with optional [return value stubbing](#stubbing-return-values) via `>>`, [exception stubbing](#stubbing-exceptions) via `>> raises(...)`, and [block forwarding verification](#block-forwarding-verification) via `&block`
|
|
23
24
|
* (Planned) BDD-style custom reporter that outputs information from Code Blocks
|
|
24
25
|
* (Planned) Capture all Then block violations
|
|
@@ -56,14 +57,6 @@ rakefile = "#{spec.gem_dir}/lib/Rakefile"
|
|
|
56
57
|
load rakefile
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
### Rails
|
|
60
|
-
|
|
61
|
-
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:
|
|
62
|
-
|
|
63
|
-
$ rails g rspock:install
|
|
64
|
-
|
|
65
|
-
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.
|
|
66
|
-
|
|
67
60
|
## Usage
|
|
68
61
|
|
|
69
62
|
Getting started using RSpock is extremely easy!
|
|
@@ -166,6 +159,46 @@ The Then block describes the response from the stimulus. Following Spock's core
|
|
|
166
159
|
|
|
167
160
|
**Variable assignments** pass through unchanged and execute in source order after the stimulus.
|
|
168
161
|
|
|
162
|
+
##### Exception Conditions
|
|
163
|
+
|
|
164
|
+
Use `raises` in a Then block to assert that the preceding When block raises a specific exception:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
When "Dividing by zero"
|
|
168
|
+
1 / 0
|
|
169
|
+
|
|
170
|
+
Then "An error is raised"
|
|
171
|
+
raises ZeroDivisionError
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
To inspect the exception, capture it into a variable:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
When "Parsing bad input"
|
|
178
|
+
JSON.parse("not json")
|
|
179
|
+
|
|
180
|
+
Then "A parse error is raised with a message"
|
|
181
|
+
e = raises JSON::ParserError
|
|
182
|
+
e.message.include?("unexpected token")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The captured variable is available for further assertions in the same Then block. Exception conditions work with data-driven `Where` blocks as well:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
When "Parsing invalid input"
|
|
189
|
+
JSON.parse(input)
|
|
190
|
+
|
|
191
|
+
Then "The expected error is raised"
|
|
192
|
+
raises expected_error
|
|
193
|
+
|
|
194
|
+
Where
|
|
195
|
+
input | expected_error
|
|
196
|
+
"not json" | JSON::ParserError
|
|
197
|
+
"{invalid" | JSON::ParserError
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Only **one** `raises` condition is allowed per Then block, and `raises` is **not supported** in Expect blocks (use a When + Then block instead).
|
|
201
|
+
|
|
169
202
|
#### Expect Block
|
|
170
203
|
|
|
171
204
|
The Expect block is useful when expressing the stimulus and the response in one statement is more natural. The same assertion rules apply as in Then blocks — every statement is an assertion unless it's a variable assignment.
|
|
@@ -256,6 +289,8 @@ This effectively creates one version of the Feature Method for each data row. No
|
|
|
256
289
|
|
|
257
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.
|
|
258
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
|
+
|
|
259
294
|
##### Truth Table
|
|
260
295
|
|
|
261
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:
|
|
@@ -496,49 +531,68 @@ end
|
|
|
496
531
|
|
|
497
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.
|
|
498
533
|
|
|
499
|
-
|
|
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.
|
|
500
537
|
|
|
501
|
-
|
|
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:
|
|
502
539
|
|
|
503
|
-
|
|
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.
|
|
504
541
|
|
|
505
|
-
|
|
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.
|
|
506
543
|
|
|
507
|
-
|
|
544
|
+
**Example: the better pattern**
|
|
508
545
|
|
|
509
|
-
|
|
546
|
+
Suppose you have a method that wraps work in a block-based collaborator:
|
|
510
547
|
|
|
511
|
-
|
|
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
|
+
```
|
|
512
556
|
|
|
513
|
-
|
|
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:
|
|
514
558
|
|
|
515
559
|
```ruby
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
+
```
|
|
519
566
|
|
|
520
|
-
|
|
521
|
-
# Breaks on the first test case
|
|
522
|
-
binding.pry if _test_index_ == 0
|
|
523
|
-
# Breaks on the second test case
|
|
524
|
-
binding.pry if _line_number_ == 15
|
|
525
|
-
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:
|
|
526
568
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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")
|
|
531
576
|
end
|
|
532
577
|
```
|
|
533
578
|
|
|
534
|
-
|
|
579
|
+
This produces tests that are simpler, more explicit, and less coupled to implementation details. The constraint is the feature.
|
|
535
580
|
|
|
536
|
-
|
|
537
|
-
* `_test_index_` is zero-based, meaning the index of the first test case is `0`
|
|
581
|
+
## Debugging
|
|
538
582
|
|
|
539
|
-
|
|
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.
|
|
540
584
|
|
|
541
|
-
|
|
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.
|
|
586
|
+
|
|
587
|
+
### Isolating a Where Block row
|
|
588
|
+
|
|
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.
|
|
542
596
|
|
|
543
597
|
## More info
|
|
544
598
|
|
|
@@ -579,6 +633,8 @@ end
|
|
|
579
633
|
|
|
580
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.
|
|
581
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
|
+
|
|
582
638
|
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
583
639
|
|
|
584
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
|
@@ -10,14 +10,14 @@ module RSpock
|
|
|
10
10
|
# Output: receiver.expects(:message).with(*args).times(n).returns(value)
|
|
11
11
|
#
|
|
12
12
|
# The outcome node type maps directly to the Mocha chain method:
|
|
13
|
-
# :
|
|
14
|
-
# :
|
|
13
|
+
# :rspock_stub_returns -> .returns(value)
|
|
14
|
+
# :rspock_stub_raises -> .raises(exception_class, ...)
|
|
15
15
|
#
|
|
16
16
|
# When block_pass is present, wraps the expects chain with a BlockCapture.capture call.
|
|
17
17
|
class InteractionToMochaMockTransformation < ASTTransform::AbstractTransformation
|
|
18
18
|
OUTCOME_METHODS = {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
rspock_stub_returns: :returns,
|
|
20
|
+
rspock_stub_raises: :raises,
|
|
21
21
|
}.freeze
|
|
22
22
|
|
|
23
23
|
def initialize(index = 0)
|
|
@@ -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
|
-
|
|
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)
|
data/lib/rspock/ast/node.rb
CHANGED
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
require 'ast_transform/
|
|
2
|
+
require 'ast_transform/node'
|
|
3
3
|
|
|
4
4
|
module RSpock
|
|
5
5
|
module AST
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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,18 +61,26 @@ 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
|
-
class
|
|
77
|
-
register :
|
|
67
|
+
class StubReturnsNode < OutcomeNode
|
|
68
|
+
register :rspock_stub_returns
|
|
78
69
|
end
|
|
79
70
|
|
|
80
|
-
class
|
|
71
|
+
class StubRaisesNode < OutcomeNode
|
|
72
|
+
register :rspock_stub_raises
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class RaisesNode < ASTTransform::Node
|
|
81
76
|
register :rspock_raises
|
|
77
|
+
|
|
78
|
+
def exception_class = children[0]
|
|
79
|
+
def capture_var = children[1]
|
|
80
|
+
def capture_name = capture_var&.children&.[](0)
|
|
82
81
|
end
|
|
83
82
|
|
|
84
|
-
class InteractionNode < Node
|
|
83
|
+
class InteractionNode < ASTTransform::Node
|
|
85
84
|
register :rspock_interaction
|
|
86
85
|
|
|
87
86
|
def cardinality = children[0]
|
|
@@ -93,7 +92,7 @@ module RSpock
|
|
|
93
92
|
def block_pass = children[5]
|
|
94
93
|
end
|
|
95
94
|
|
|
96
|
-
class BinaryStatementNode < Node
|
|
95
|
+
class BinaryStatementNode < ASTTransform::Node
|
|
97
96
|
register :rspock_binary_statement
|
|
98
97
|
|
|
99
98
|
def lhs = children[0]
|
|
@@ -101,23 +100,11 @@ module RSpock
|
|
|
101
100
|
def rhs = children[2]
|
|
102
101
|
end
|
|
103
102
|
|
|
104
|
-
class StatementNode < Node
|
|
103
|
+
class StatementNode < ASTTransform::Node
|
|
105
104
|
register :rspock_statement
|
|
106
105
|
|
|
107
106
|
def expression = children[0]
|
|
108
107
|
def source = children[1]
|
|
109
108
|
end
|
|
110
|
-
|
|
111
|
-
module NodeBuilder
|
|
112
|
-
include ASTTransform::TransformationHelper
|
|
113
|
-
|
|
114
|
-
def s(type, *children)
|
|
115
|
-
if type.to_s.start_with?('rspock_')
|
|
116
|
-
Node.build(type, *children)
|
|
117
|
-
else
|
|
118
|
-
super
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
109
|
end
|
|
123
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
|
|
11
|
+
include ASTTransform::TransformationHelper
|
|
11
12
|
|
|
12
13
|
# Constructs a new Block.
|
|
13
14
|
#
|
|
@@ -25,6 +25,11 @@ module RSpock
|
|
|
25
25
|
def to_rspock_node
|
|
26
26
|
statement_parser = StatementParser.new
|
|
27
27
|
spock_children = @children.map { |child| statement_parser.parse(child) }
|
|
28
|
+
|
|
29
|
+
if spock_children.any? { |c| c.type == :rspock_raises }
|
|
30
|
+
raise BlockError, "raises() is not supported in Expect blocks @ #{range}. Use a When + Then block instead."
|
|
31
|
+
end
|
|
32
|
+
|
|
28
33
|
s(:rspock_expect, *spock_children)
|
|
29
34
|
end
|
|
30
35
|
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
|
|
@@ -14,10 +15,10 @@ module RSpock
|
|
|
14
15
|
# [1] receiver - e.g. s(:send, nil, :subscriber)
|
|
15
16
|
# [2] message - e.g. s(:sym, :receive)
|
|
16
17
|
# [3] args - nil if no args, s(:array, *arg_nodes) otherwise
|
|
17
|
-
# [4] outcome - nil if no >>, otherwise s(:
|
|
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
|
|
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
|
|
@@ -63,9 +70,9 @@ module RSpock
|
|
|
63
70
|
|
|
64
71
|
def parse_outcome(node)
|
|
65
72
|
if node.type == :send && node.children[0].nil? && node.children[1] == :raises
|
|
66
|
-
s(:
|
|
73
|
+
s(:rspock_stub_raises, *node.children[2..])
|
|
67
74
|
else
|
|
68
|
-
s(:
|
|
75
|
+
s(:rspock_stub_returns, node)
|
|
69
76
|
end
|
|
70
77
|
end
|
|
71
78
|
|
|
@@ -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
|
|
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
|