rspock 2.3.0 → 2.4.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/CHANGELOG.md +130 -19
- data/Gemfile.lock +1 -1
- data/README.md +74 -14
- data/lib/rspock/ast/interaction_to_mocha_mock_transformation.rb +11 -2
- data/lib/rspock/ast/node.rb +27 -1
- data/lib/rspock/ast/parser/expect_block.rb +7 -0
- data/lib/rspock/ast/parser/interaction_parser.rb +12 -4
- data/lib/rspock/ast/parser/statement_parser.rb +48 -0
- data/lib/rspock/ast/parser/then_block.rb +11 -2
- data/lib/rspock/ast/statement_to_assertion_transformation.rb +68 -0
- data/lib/rspock/ast/test_method_transformation.rb +13 -4
- data/lib/rspock/version.rb +1 -1
- data/lib/rspock.rb +1 -0
- metadata +4 -3
- data/lib/rspock/ast/comparison_to_assertion_transformation.rb +0 -40
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e4d05a65bb9d040615d65ceee976bc3b0fef696d54921b9fb47160c07c18dc2
|
|
4
|
+
data.tar.gz: 8d2b4a392f6ea3cff0133278a7994b2934fea9d2a3d695da4a9da66d2727ae25
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c59e0798461d56be30b9579cd09b72b373a2b70039139295cc42bf610031e73be438e93f0645963cfb29d6617cab0096b0de978a954950ec6f9c21c13e1d510a
|
|
7
|
+
data.tar.gz: 2d36558c145a2684e169331d7f277f2aa7b09b39c642b331dcf4ec36757228a0ccf6c8ab1ce5b8be7408b157061ae30ef0b7a45108ea012a1462742cbb480b87
|
data/CHANGELOG.md
CHANGED
|
@@ -1,46 +1,157 @@
|
|
|
1
1
|
# Changelog
|
|
2
|
+
|
|
2
3
|
All notable changes to this project will be documented in this file.
|
|
3
4
|
|
|
4
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
7
|
|
|
7
|
-
## [
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [2.4.0] - 2026-02-28
|
|
11
|
+
|
|
8
12
|
### Added
|
|
9
|
-
|
|
13
|
+
|
|
14
|
+
- Spock-style implicit assertions: every non-assignment statement in Then/Expect blocks is now an assertion — no assertion API needed.
|
|
15
|
+
- Binary operator assertions: `=~`, `!~`, `>`, `<`, `>=`, `<=` with clear error messages.
|
|
16
|
+
- General statement assertions: bare boolean expressions (e.g. `obj.valid?`) with the original source text in the error message.
|
|
17
|
+
- Negation support: `!expr` is detected automatically and produces a clear error message.
|
|
18
|
+
- `>> raises(...)` syntax for exception stubbing in interactions.
|
|
10
19
|
|
|
11
20
|
### Changed
|
|
21
|
+
|
|
22
|
+
- Renamed `ConditionParser` to `StatementParser` and `ConditionToAssertionTransformation` to `StatementToAssertionTransformation` for consistency with Spock's model.
|
|
23
|
+
- Then and Expect block parsers now use `StatementParser` for statement classification.
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
|
|
27
|
+
- `ComparisonToAssertionTransformation` — replaced by `StatementToAssertionTransformation`.
|
|
28
|
+
|
|
29
|
+
## [2.3.1] - 2026-02-27
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Require `block_capture` so it is available at runtime.
|
|
34
|
+
|
|
35
|
+
## [2.3.0] - 2026-02-27
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- Interaction transformations and block identity verification via `&` operator.
|
|
40
|
+
- RSpock AST node hierarchy (`Node`, `InteractionNode`, `BodyNode`, etc.) for type-safe AST handling.
|
|
41
|
+
- `TestMethodParser` extracted from `TestMethodTransformation` for separation of parsing and transformation.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- Restructured block classes into `Parser` namespace and converted `InteractionParser` to a class.
|
|
46
|
+
- Introduced `BodyNode` and removed legacy interaction transformations.
|
|
47
|
+
|
|
48
|
+
## [2.2.0] - 2026-02-25
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- Interaction stubbing with `>>` for return value stubbing in Then block interactions.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- Pry and pry-byebug compatibility.
|
|
57
|
+
- Failing test on Ruby 3+.
|
|
58
|
+
- `filter_string` for `ast_transform` 2.1.4 source mapping change.
|
|
59
|
+
|
|
60
|
+
## [2.1.0] - 2026-02-21
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- Ruby 4.0 support.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- Codecov badge URL to use master branch.
|
|
69
|
+
|
|
70
|
+
## [2.0.0] - 2026-02-21
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- Minimum Ruby version bumped to 3.2.
|
|
75
|
+
- Upgraded to Ruby 3.x compatibility.
|
|
76
|
+
- Use `ast_transform` 2.0.0 from RubyGems.
|
|
77
|
+
- CI modernization and release workflow improvements.
|
|
78
|
+
|
|
79
|
+
## [1.0.0] - 2020-07-09
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- Interaction-based testing: mock with expectations in the Then block.
|
|
84
|
+
- Travis CI and code coverage.
|
|
85
|
+
|
|
86
|
+
### Changed
|
|
87
|
+
|
|
12
88
|
- Test names now have the test index and line number as suffix instead of prefix.
|
|
13
|
-
- Cleanup
|
|
89
|
+
- Removed unnecessary ensure block when Cleanup block is empty; moved source map wrapper to class scope.
|
|
90
|
+
- Bump `ast_transform` to release 1.0.0.
|
|
14
91
|
|
|
15
92
|
### Fixed
|
|
16
|
-
- Fixed source mapping for transformed assertion nodes.
|
|
17
93
|
|
|
18
|
-
|
|
94
|
+
- Source mapping for transformed assertion nodes.
|
|
95
|
+
- Truth table generator command with proper escaping.
|
|
96
|
+
|
|
97
|
+
## [0.2.5] - 2019-05-28
|
|
98
|
+
|
|
19
99
|
### Fixed
|
|
20
|
-
- Fixed BacktraceFilter so that source mapping works again
|
|
21
100
|
|
|
22
|
-
|
|
101
|
+
- BacktraceFilter so that source mapping works again.
|
|
102
|
+
|
|
103
|
+
## [0.2.4] - 2019-05-27
|
|
104
|
+
|
|
23
105
|
### Changed
|
|
24
|
-
- Bump Unparser dependency from ~> 0.2.8 to ~> 0.4
|
|
25
106
|
|
|
26
|
-
|
|
107
|
+
- Bump Unparser dependency from `~> 0.2.8` to `~> 0.4`.
|
|
108
|
+
|
|
109
|
+
## [0.2.3] - 2018-11-09
|
|
110
|
+
|
|
27
111
|
### Fixed
|
|
28
|
-
- Cleanup block can now contain more than one node
|
|
29
112
|
|
|
30
|
-
|
|
113
|
+
- Cleanup block can now contain more than one node.
|
|
114
|
+
|
|
115
|
+
## [0.2.2] - 2018-11-08
|
|
116
|
+
|
|
31
117
|
### Changed
|
|
32
|
-
- Extracted ASTTransform to its own gem: `ast_transform`
|
|
33
118
|
|
|
34
|
-
|
|
119
|
+
- Extracted ASTTransform to its own gem: `ast_transform`.
|
|
120
|
+
|
|
121
|
+
## [0.2.1] - 2018-10-09
|
|
122
|
+
|
|
35
123
|
### Added
|
|
36
|
-
|
|
124
|
+
|
|
125
|
+
- `_line_number_` is now displayed in the test name and available in test scope for debugging.
|
|
37
126
|
|
|
38
127
|
### Changed
|
|
39
|
-
- Renamed test_index to _test_index_
|
|
40
128
|
|
|
41
|
-
|
|
129
|
+
- Renamed `test_index` to `_test_index_`.
|
|
130
|
+
|
|
131
|
+
## [0.2.0] - 2018-09-21
|
|
132
|
+
|
|
42
133
|
### Added
|
|
134
|
+
|
|
43
135
|
- Truth table generator Rake task.
|
|
44
136
|
|
|
45
|
-
## [0.1.1] 2018-09-
|
|
46
|
-
|
|
137
|
+
## [0.1.1] - 2018-09-18
|
|
138
|
+
|
|
139
|
+
### Added
|
|
140
|
+
|
|
141
|
+
- Initial release.
|
|
142
|
+
|
|
143
|
+
[Unreleased]: https://github.com/rspockframework/rspock/compare/v2.4.0...HEAD
|
|
144
|
+
[2.4.0]: https://github.com/rspockframework/rspock/compare/v2.3.1...v2.4.0
|
|
145
|
+
[2.3.1]: https://github.com/rspockframework/rspock/compare/v2.3.0...v2.3.1
|
|
146
|
+
[2.3.0]: https://github.com/rspockframework/rspock/compare/v2.2.0...v2.3.0
|
|
147
|
+
[2.2.0]: https://github.com/rspockframework/rspock/compare/v2.1.0...v2.2.0
|
|
148
|
+
[2.1.0]: https://github.com/rspockframework/rspock/compare/v2.0.0...v2.1.0
|
|
149
|
+
[2.0.0]: https://github.com/rspockframework/rspock/compare/1.0.0...v2.0.0
|
|
150
|
+
[1.0.0]: https://github.com/rspockframework/rspock/compare/0.2.5...1.0.0
|
|
151
|
+
[0.2.5]: https://github.com/rspockframework/rspock/compare/0.2.4...0.2.5
|
|
152
|
+
[0.2.4]: https://github.com/rspockframework/rspock/compare/0.2.3...0.2.4
|
|
153
|
+
[0.2.3]: https://github.com/rspockframework/rspock/compare/0.2.2...0.2.3
|
|
154
|
+
[0.2.2]: https://github.com/rspockframework/rspock/compare/0.2.1...0.2.2
|
|
155
|
+
[0.2.1]: https://github.com/rspockframework/rspock/compare/0.2.0...0.2.1
|
|
156
|
+
[0.2.0]: https://github.com/rspockframework/rspock/compare/0.1.1...0.2.0
|
|
157
|
+
[0.1.1]: https://github.com/rspockframework/rspock/releases/tag/0.1.1
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -16,8 +16,10 @@ Note: RSpock is heavily inspired by Spock for the Groovy programming language.
|
|
|
16
16
|
|
|
17
17
|
* BDD-style code blocks: Given, When, Then, Expect, Cleanup, Where
|
|
18
18
|
* Data-driven testing with incredibly expressive table-based Where blocks
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* Spock-style implicit assertions: every statement in Then/Expect blocks is an assertion (no assertion API needed)
|
|
20
|
+
* Binary operator assertions: `==`, `!=`, `=~`, `!~`, `>`, `<`, `>=`, `<=` with clear error messages
|
|
21
|
+
* General statement assertions: bare boolean expressions (e.g. `obj.valid?`, `list.include?(x)`) and negation (`!obj.empty?`) with source-text error messages
|
|
22
|
+
* [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`
|
|
21
23
|
* (Planned) BDD-style custom reporter that outputs information from Code Blocks
|
|
22
24
|
* (Planned) Capture all Then block violations
|
|
23
25
|
|
|
@@ -150,15 +152,25 @@ The When block describes the stimulus to be applied to the system under test. It
|
|
|
150
152
|
|
|
151
153
|
```ruby
|
|
152
154
|
Then "The product is added to the cart"
|
|
155
|
+
!cart.products.empty?
|
|
153
156
|
cart.products.size == 1
|
|
154
157
|
cart.products.first == product
|
|
158
|
+
cart.products.size > 0
|
|
155
159
|
```
|
|
156
160
|
|
|
157
|
-
The Then block describes the response from the stimulus.
|
|
161
|
+
The Then block describes the response from the stimulus. Following Spock's core model, **every statement is an assertion** unless it's a variable assignment. No assertion API is needed.
|
|
162
|
+
|
|
163
|
+
**Binary operators** (`==`, `!=`, `=~`, `!~`, `>`, `<`, `>=`, `<=`) produce clear error messages on failure. By convention, the **LHS** operand is the **actual** value and the **RHS** is the **expected** value.
|
|
164
|
+
|
|
165
|
+
**General statements** (bare boolean expressions like `obj.valid?`, `list.include?(x)`) include the original source text in the error message, so you see exactly which expression failed. **Negation** (`!expr`) is detected automatically.
|
|
166
|
+
|
|
167
|
+
**Variable assignments** pass through unchanged and execute in source order after the stimulus.
|
|
158
168
|
|
|
159
169
|
#### Expect Block
|
|
160
170
|
|
|
161
|
-
The Expect block is useful when expressing the stimulus and the response in one statement is more natural.
|
|
171
|
+
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.
|
|
172
|
+
|
|
173
|
+
For example, let's compare two equivalent ways of describing some behaviour:
|
|
162
174
|
|
|
163
175
|
##### When + Then
|
|
164
176
|
```ruby
|
|
@@ -175,6 +187,17 @@ Expect "absolute of -2 is 2"
|
|
|
175
187
|
-2.abs == 2
|
|
176
188
|
```
|
|
177
189
|
|
|
190
|
+
The Expect block supports the full range of assertion expressions:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
Expect "string matching and predicates"
|
|
194
|
+
str =~ /potato/
|
|
195
|
+
str.include?("pot")
|
|
196
|
+
!str.empty?
|
|
197
|
+
str.length > 3
|
|
198
|
+
str.length == 6
|
|
199
|
+
```
|
|
200
|
+
|
|
178
201
|
A good rule of thumb is using When + Then blocks to describe methods with side-effects and Expect blocks to describe purely functional methods.
|
|
179
202
|
|
|
180
203
|
#### Cleanup Block
|
|
@@ -306,12 +329,12 @@ test "#publish sends a message to all subscribers" do
|
|
|
306
329
|
end
|
|
307
330
|
```
|
|
308
331
|
|
|
309
|
-
The above ___Then___ block contains 2 interactions, each of which has 4 parts: the _cardinality_, the _receiver_, the _message_ and its _arguments_. Optionally,
|
|
332
|
+
The above ___Then___ block contains 2 interactions, each of which has 4 parts: the _cardinality_, the _receiver_, the _message_ and its _arguments_. Optionally, an _outcome_ can be specified using the `>>` operator (either a return value or `raises(...)` for exceptions), and _block forwarding_ can be verified using the `&` operator.
|
|
310
333
|
|
|
311
334
|
```
|
|
312
335
|
1 * receiver.message('hello', &blk) >> "result"
|
|
313
336
|
| | | | | |
|
|
314
|
-
| | | | |
|
|
337
|
+
| | | | | outcome (optional): value or raises(...)
|
|
315
338
|
| | | | block forwarding (optional)
|
|
316
339
|
| | | argument(s) (optional)
|
|
317
340
|
| | message
|
|
@@ -421,6 +444,39 @@ _ * cache.fetch("key") >> expensive_result # a variable
|
|
|
421
444
|
|
|
422
445
|
**Note**: Without `>>`, an interaction sets up an expectation only (the method will return `nil` by default). Use `>>` when the code under test depends on the return value.
|
|
423
446
|
|
|
447
|
+
#### Stubbing Exceptions
|
|
448
|
+
|
|
449
|
+
When the code under test needs a collaborator to raise an exception, use `>> raises(...)` instead of a return value. This sets up the mock to raise the given exception when called.
|
|
450
|
+
|
|
451
|
+
```ruby
|
|
452
|
+
test "#fetch raises when the record is not found" do
|
|
453
|
+
Given
|
|
454
|
+
repository = mock
|
|
455
|
+
service = Service.new(repository)
|
|
456
|
+
|
|
457
|
+
When
|
|
458
|
+
service.fetch(42)
|
|
459
|
+
|
|
460
|
+
Then
|
|
461
|
+
1 * repository.find(42) >> raises(RecordNotFound)
|
|
462
|
+
end
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
You can also pass a message or an exception instance:
|
|
466
|
+
|
|
467
|
+
```ruby
|
|
468
|
+
1 * repository.find(42) >> raises(RecordNotFound, "not found") # class + message
|
|
469
|
+
1 * repository.find(42) >> raises(RecordNotFound.new("not found")) # instance
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
This works with all interaction features — cardinality, arguments, and ranges:
|
|
473
|
+
|
|
474
|
+
```ruby
|
|
475
|
+
(1..3) * service.call(anything) >> raises(TimeoutError)
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
**Note**: Without `raises(...)`, the `>>` operator stubs a return value. With `raises(...)`, it stubs an exception instead.
|
|
479
|
+
|
|
424
480
|
#### Block Forwarding Verification
|
|
425
481
|
|
|
426
482
|
When the code under test forwards a block (or proc) to a collaborator, you may want to verify that the _exact_ block was passed through. RSpock supports this with the `&` operator in interactions, performing an identity check on the block reference.
|
|
@@ -527,20 +583,24 @@ To install this gem onto your local machine, run `bundle exec rake install`.
|
|
|
527
583
|
|
|
528
584
|
## Releasing a New Version
|
|
529
585
|
|
|
530
|
-
There are two ways to create a release. Both require that `version.rb` has already been updated and merged to main.
|
|
586
|
+
There are two ways to create a release. Both require that `version.rb` has already been updated, `CHANGELOG.md` has been updated, and changes have been merged to main.
|
|
531
587
|
|
|
532
588
|
### Via GitHub UI
|
|
533
589
|
|
|
534
|
-
1. Update `VERSION` in `lib/rspock/version.rb` and run `bundle install` to regenerate `Gemfile.lock
|
|
535
|
-
2.
|
|
536
|
-
3.
|
|
537
|
-
4.
|
|
538
|
-
5.
|
|
590
|
+
1. Update `VERSION` in `lib/rspock/version.rb` and run `bundle install` to regenerate `Gemfile.lock`
|
|
591
|
+
2. Move the `[Unreleased]` section in `CHANGELOG.md` to a new version heading with today's date (e.g. `## [2.4.0] - 2026-03-01`) and add a fresh empty `[Unreleased]` section above it. Update the comparison links at the bottom of the file.
|
|
592
|
+
3. Commit, open a PR, and merge to main
|
|
593
|
+
4. Go to the repo on GitHub → **Releases** → **Draft a new release**
|
|
594
|
+
5. Enter a new tag (e.g. `v2.4.0`), select `main` as the target branch
|
|
595
|
+
6. Add a title and release notes (GitHub can auto-generate these from merged PRs)
|
|
596
|
+
7. Click **Publish release**
|
|
539
597
|
|
|
540
598
|
### Via CLI
|
|
541
599
|
|
|
542
|
-
1. Update `VERSION` in `lib/rspock/version.rb` and run `bundle install` to regenerate `Gemfile.lock
|
|
543
|
-
2.
|
|
600
|
+
1. Update `VERSION` in `lib/rspock/version.rb` and run `bundle install` to regenerate `Gemfile.lock`
|
|
601
|
+
2. Move the `[Unreleased]` section in `CHANGELOG.md` to a new version heading with today's date and add a fresh empty `[Unreleased]` section above it. Update the comparison links at the bottom of the file.
|
|
602
|
+
3. Commit, open a PR, and merge to main
|
|
603
|
+
4. Tag and push:
|
|
544
604
|
```
|
|
545
605
|
git checkout main && git pull
|
|
546
606
|
git tag v2.0.0
|
|
@@ -6,11 +6,20 @@ module RSpock
|
|
|
6
6
|
module AST
|
|
7
7
|
# Transforms an :rspock_interaction node into Mocha mock setup code.
|
|
8
8
|
#
|
|
9
|
-
# Input: s(:rspock_interaction, cardinality, receiver, sym, args,
|
|
9
|
+
# Input: s(:rspock_interaction, cardinality, receiver, sym, args, outcome, block_pass)
|
|
10
10
|
# Output: receiver.expects(:message).with(*args).times(n).returns(value)
|
|
11
11
|
#
|
|
12
|
+
# The outcome node type maps directly to the Mocha chain method:
|
|
13
|
+
# :rspock_returns -> .returns(value)
|
|
14
|
+
# :rspock_raises -> .raises(exception_class, ...)
|
|
15
|
+
#
|
|
12
16
|
# When block_pass is present, wraps the expects chain with a BlockCapture.capture call.
|
|
13
17
|
class InteractionToMochaMockTransformation < ASTTransform::AbstractTransformation
|
|
18
|
+
OUTCOME_METHODS = {
|
|
19
|
+
rspock_returns: :returns,
|
|
20
|
+
rspock_raises: :raises,
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
14
23
|
def initialize(index = 0)
|
|
15
24
|
@index = index
|
|
16
25
|
end
|
|
@@ -21,7 +30,7 @@ module RSpock
|
|
|
21
30
|
result = chain_call(interaction.receiver, :expects, s(:sym, interaction.message))
|
|
22
31
|
result = chain_call(result, :with, *interaction.args.children) if interaction.args
|
|
23
32
|
result = build_cardinality(result, interaction.cardinality)
|
|
24
|
-
result = chain_call(result,
|
|
33
|
+
result = chain_call(result, OUTCOME_METHODS.fetch(interaction.outcome.type), *interaction.outcome.children) if interaction.outcome
|
|
25
34
|
|
|
26
35
|
if interaction.block_pass
|
|
27
36
|
build_block_capture_setup(result, interaction.receiver, interaction.message)
|
data/lib/rspock/ast/node.rb
CHANGED
|
@@ -70,6 +70,17 @@ module RSpock
|
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
class OutcomeNode < Node
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class ReturnsNode < OutcomeNode
|
|
77
|
+
register :rspock_returns
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class RaisesNode < OutcomeNode
|
|
81
|
+
register :rspock_raises
|
|
82
|
+
end
|
|
83
|
+
|
|
73
84
|
class InteractionNode < Node
|
|
74
85
|
register :rspock_interaction
|
|
75
86
|
|
|
@@ -78,10 +89,25 @@ module RSpock
|
|
|
78
89
|
def message_sym = children[2]
|
|
79
90
|
def message = message_sym.children[0]
|
|
80
91
|
def args = children[3]
|
|
81
|
-
def
|
|
92
|
+
def outcome = children[4]
|
|
82
93
|
def block_pass = children[5]
|
|
83
94
|
end
|
|
84
95
|
|
|
96
|
+
class BinaryStatementNode < Node
|
|
97
|
+
register :rspock_binary_statement
|
|
98
|
+
|
|
99
|
+
def lhs = children[0]
|
|
100
|
+
def operator = children[1]
|
|
101
|
+
def rhs = children[2]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class StatementNode < Node
|
|
105
|
+
register :rspock_statement
|
|
106
|
+
|
|
107
|
+
def expression = children[0]
|
|
108
|
+
def source = children[1]
|
|
109
|
+
end
|
|
110
|
+
|
|
85
111
|
module NodeBuilder
|
|
86
112
|
include ASTTransform::TransformationHelper
|
|
87
113
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require 'rspock/ast/parser/block'
|
|
3
|
+
require 'rspock/ast/parser/statement_parser'
|
|
3
4
|
|
|
4
5
|
module RSpock
|
|
5
6
|
module AST
|
|
@@ -20,6 +21,12 @@ module RSpock
|
|
|
20
21
|
def successors
|
|
21
22
|
@successors ||= [:Cleanup, :Where].freeze
|
|
22
23
|
end
|
|
24
|
+
|
|
25
|
+
def to_rspock_node
|
|
26
|
+
statement_parser = StatementParser.new
|
|
27
|
+
spock_children = @children.map { |child| statement_parser.parse(child) }
|
|
28
|
+
s(:rspock_expect, *spock_children)
|
|
29
|
+
end
|
|
23
30
|
end
|
|
24
31
|
end
|
|
25
32
|
end
|
|
@@ -7,14 +7,14 @@ module RSpock
|
|
|
7
7
|
# Parses raw Ruby AST interaction nodes into structured :rspock_interaction nodes.
|
|
8
8
|
#
|
|
9
9
|
# Input: 1 * receiver.message("arg", &blk) >> "result"
|
|
10
|
-
# Output: s(:rspock_interaction, cardinality, receiver, sym, args,
|
|
10
|
+
# Output: s(:rspock_interaction, cardinality, receiver, sym, args, outcome, block_pass)
|
|
11
11
|
#
|
|
12
12
|
# :rspock_interaction children:
|
|
13
13
|
# [0] cardinality - e.g. s(:int, 1), s(:begin, s(:irange, ...)), s(:send, nil, :_)
|
|
14
14
|
# [1] receiver - e.g. s(:send, nil, :subscriber)
|
|
15
15
|
# [2] message - e.g. s(:sym, :receive)
|
|
16
16
|
# [3] args - nil if no args, s(:array, *arg_nodes) otherwise
|
|
17
|
-
# [4]
|
|
17
|
+
# [4] outcome - nil if no >>, otherwise s(:rspock_returns, value) or s(:rspock_raises, *args)
|
|
18
18
|
# [5] block_pass - nil if no &, otherwise s(:block_pass, ...)
|
|
19
19
|
class InteractionParser
|
|
20
20
|
include RSpock::AST::NodeBuilder
|
|
@@ -35,7 +35,7 @@ module RSpock
|
|
|
35
35
|
return node unless interaction_node?(node)
|
|
36
36
|
|
|
37
37
|
if return_value_node?(node)
|
|
38
|
-
|
|
38
|
+
outcome = parse_outcome(node.children[2])
|
|
39
39
|
node = node.children[0]
|
|
40
40
|
end
|
|
41
41
|
|
|
@@ -50,7 +50,7 @@ module RSpock
|
|
|
50
50
|
receiver,
|
|
51
51
|
s(:sym, message),
|
|
52
52
|
args,
|
|
53
|
-
|
|
53
|
+
outcome,
|
|
54
54
|
block_pass
|
|
55
55
|
)
|
|
56
56
|
end
|
|
@@ -61,6 +61,14 @@ module RSpock
|
|
|
61
61
|
node.type == :send && node.children[1] == :>> && interaction_node?(node.children[0])
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
def parse_outcome(node)
|
|
65
|
+
if node.type == :send && node.children[0].nil? && node.children[1] == :raises
|
|
66
|
+
s(:rspock_raises, *node.children[2..])
|
|
67
|
+
else
|
|
68
|
+
s(:rspock_returns, node)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
64
72
|
def validate_cardinality(node)
|
|
65
73
|
case node.type
|
|
66
74
|
when *ALLOWED_CARDINALITY_NODES
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'rspock/ast/node'
|
|
3
|
+
|
|
4
|
+
module RSpock
|
|
5
|
+
module AST
|
|
6
|
+
module Parser
|
|
7
|
+
# Classifies raw Ruby AST statements into RSpock node types for Then/Expect blocks.
|
|
8
|
+
#
|
|
9
|
+
# - Assignments pass through as raw AST (no wrapping).
|
|
10
|
+
# - Binary operators (==, !=, =~, etc.) become :rspock_binary_statement nodes.
|
|
11
|
+
# - Everything else becomes :rspock_statement nodes with the original source text captured.
|
|
12
|
+
class StatementParser
|
|
13
|
+
include RSpock::AST::NodeBuilder
|
|
14
|
+
|
|
15
|
+
BINARY_OPERATORS = %i[== != =~ !~ > < >= <=].freeze
|
|
16
|
+
ASSIGNMENT_TYPES = %i[lvasgn masgn op_asgn or_asgn and_asgn].freeze
|
|
17
|
+
|
|
18
|
+
def parse(node)
|
|
19
|
+
return node if assignment?(node)
|
|
20
|
+
return build_binary_statement(node) if binary_statement?(node)
|
|
21
|
+
|
|
22
|
+
build_statement(node)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def assignment?(node)
|
|
28
|
+
ASSIGNMENT_TYPES.include?(node.type)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def binary_statement?(node)
|
|
32
|
+
node.type == :send &&
|
|
33
|
+
node.children.length == 3 &&
|
|
34
|
+
BINARY_OPERATORS.include?(node.children[1])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def build_binary_statement(node)
|
|
38
|
+
s(:rspock_binary_statement, node.children[0], s(:sym, node.children[1]), node.children[2])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_statement(node)
|
|
42
|
+
source = node.loc&.expression&.source || node.inspect
|
|
43
|
+
s(:rspock_statement, node, s(:str, source))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require 'rspock/ast/parser/block'
|
|
3
3
|
require 'rspock/ast/parser/interaction_parser'
|
|
4
|
+
require 'rspock/ast/parser/statement_parser'
|
|
4
5
|
|
|
5
6
|
module RSpock
|
|
6
7
|
module AST
|
|
@@ -19,8 +20,16 @@ module RSpock
|
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def to_rspock_node
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
interaction_parser = InteractionParser.new
|
|
24
|
+
statement_parser = StatementParser.new
|
|
25
|
+
|
|
26
|
+
spock_children = @children.map do |child|
|
|
27
|
+
parsed = interaction_parser.parse(child)
|
|
28
|
+
next parsed unless parsed.equal?(child)
|
|
29
|
+
|
|
30
|
+
statement_parser.parse(child)
|
|
31
|
+
end
|
|
32
|
+
|
|
24
33
|
s(:rspock_then, *spock_children)
|
|
25
34
|
end
|
|
26
35
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'rspock/ast/node'
|
|
3
|
+
|
|
4
|
+
module RSpock
|
|
5
|
+
module AST
|
|
6
|
+
# Transforms :rspock_binary_statement and :rspock_statement nodes into Minitest assertion calls.
|
|
7
|
+
#
|
|
8
|
+
# Binary statements dispatch to specialized assertions (assert_equal, assert_match, assert_operator).
|
|
9
|
+
# General statements use assert_equal(true/false, expr, source_message) with negation detection.
|
|
10
|
+
class StatementToAssertionTransformation
|
|
11
|
+
include RSpock::AST::NodeBuilder
|
|
12
|
+
|
|
13
|
+
BINARY_DISPATCH = {
|
|
14
|
+
:== => :assert_equal,
|
|
15
|
+
:!= => :refute_equal,
|
|
16
|
+
:=~ => :assert_match,
|
|
17
|
+
:'!~' => :refute_match,
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
OPERATOR_ASSERTIONS = %i[> < >= <=].freeze
|
|
21
|
+
|
|
22
|
+
def run(node)
|
|
23
|
+
case node.type
|
|
24
|
+
when :rspock_binary_statement
|
|
25
|
+
transform_binary_statement(node)
|
|
26
|
+
when :rspock_statement
|
|
27
|
+
transform_statement(node)
|
|
28
|
+
else
|
|
29
|
+
node
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def transform_binary_statement(node)
|
|
36
|
+
lhs = node.lhs
|
|
37
|
+
op = node.operator.children[0]
|
|
38
|
+
rhs = node.rhs
|
|
39
|
+
|
|
40
|
+
if (assertion = BINARY_DISPATCH[op])
|
|
41
|
+
s(:send, nil, assertion, rhs, lhs)
|
|
42
|
+
elsif OPERATOR_ASSERTIONS.include?(op)
|
|
43
|
+
s(:send, nil, :assert_operator, lhs, s(:sym, op), rhs)
|
|
44
|
+
else
|
|
45
|
+
s(:send, nil, :assert_operator, lhs, s(:sym, op), rhs)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def transform_statement(node)
|
|
50
|
+
expr = node.expression
|
|
51
|
+
source_text = node.source.children[0]
|
|
52
|
+
|
|
53
|
+
if negated?(expr)
|
|
54
|
+
inner = expr.children[0]
|
|
55
|
+
message = "Expected \"#{source_text}\" to be false"
|
|
56
|
+
s(:send, nil, :assert_equal, s(:false), inner, s(:str, message))
|
|
57
|
+
else
|
|
58
|
+
message = "Expected \"#{source_text}\" to be true"
|
|
59
|
+
s(:send, nil, :assert_equal, s(:true), expr, s(:str, message))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def negated?(node)
|
|
64
|
+
node.type == :send && node.children[1] == :! && node.children.length == 2
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require 'ast_transform/abstract_transformation'
|
|
3
3
|
require 'rspock/ast/node'
|
|
4
|
-
require 'rspock/ast/
|
|
4
|
+
require 'rspock/ast/statement_to_assertion_transformation'
|
|
5
5
|
require 'rspock/ast/header_nodes_transformation'
|
|
6
6
|
require 'rspock/ast/interaction_to_mocha_mock_transformation'
|
|
7
7
|
require 'rspock/ast/interaction_to_block_identity_assertion_transformation'
|
|
@@ -14,7 +14,7 @@ module RSpock
|
|
|
14
14
|
class TestMethodTransformation < ASTTransform::AbstractTransformation
|
|
15
15
|
def initialize(block_registry, strict: true)
|
|
16
16
|
@parser = Parser::TestMethodParser.new(block_registry, strict: strict)
|
|
17
|
-
@
|
|
17
|
+
@statement_transformation = StatementToAssertionTransformation.new
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def run(node)
|
|
@@ -59,7 +59,7 @@ module RSpock
|
|
|
59
59
|
interaction_setups << setup
|
|
60
60
|
then_children << assertion unless assertion.equal?(child)
|
|
61
61
|
else
|
|
62
|
-
then_children <<
|
|
62
|
+
then_children << transform_statement_or_passthrough(child)
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
@@ -77,10 +77,19 @@ module RSpock
|
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
def transform_expect_block(expect_node)
|
|
80
|
-
new_children = expect_node.children.map { |child|
|
|
80
|
+
new_children = expect_node.children.map { |child| transform_statement_or_passthrough(child) }
|
|
81
81
|
expect_node.updated(nil, new_children)
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
def transform_statement_or_passthrough(child)
|
|
85
|
+
case child.type
|
|
86
|
+
when :rspock_binary_statement, :rspock_statement
|
|
87
|
+
@statement_transformation.run(child)
|
|
88
|
+
else
|
|
89
|
+
child
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
84
93
|
# --- Build final Ruby AST ---
|
|
85
94
|
|
|
86
95
|
def build_ruby_ast(method_call, method_args, body_node, where, hoisted_setups)
|
data/lib/rspock/version.rb
CHANGED
data/lib/rspock.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rspock
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jean-Philippe Duchesne
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -207,7 +207,6 @@ files:
|
|
|
207
207
|
- lib/generators/templates/rspock_initializer.rb
|
|
208
208
|
- lib/minitest/rspock_plugin.rb
|
|
209
209
|
- lib/rspock.rb
|
|
210
|
-
- lib/rspock/ast/comparison_to_assertion_transformation.rb
|
|
211
210
|
- lib/rspock/ast/header_nodes_transformation.rb
|
|
212
211
|
- lib/rspock/ast/interaction_to_block_identity_assertion_transformation.rb
|
|
213
212
|
- lib/rspock/ast/interaction_to_mocha_mock_transformation.rb
|
|
@@ -218,10 +217,12 @@ files:
|
|
|
218
217
|
- lib/rspock/ast/parser/expect_block.rb
|
|
219
218
|
- lib/rspock/ast/parser/given_block.rb
|
|
220
219
|
- lib/rspock/ast/parser/interaction_parser.rb
|
|
220
|
+
- lib/rspock/ast/parser/statement_parser.rb
|
|
221
221
|
- lib/rspock/ast/parser/test_method_parser.rb
|
|
222
222
|
- lib/rspock/ast/parser/then_block.rb
|
|
223
223
|
- lib/rspock/ast/parser/when_block.rb
|
|
224
224
|
- lib/rspock/ast/parser/where_block.rb
|
|
225
|
+
- lib/rspock/ast/statement_to_assertion_transformation.rb
|
|
225
226
|
- lib/rspock/ast/test_method_def_transformation.rb
|
|
226
227
|
- lib/rspock/ast/test_method_dstr_transformation.rb
|
|
227
228
|
- lib/rspock/ast/test_method_transformation.rb
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require 'ast_transform/abstract_transformation'
|
|
3
|
-
require 'rspock/ast/method_call_to_lvar_transformation'
|
|
4
|
-
|
|
5
|
-
module RSpock
|
|
6
|
-
module AST
|
|
7
|
-
class ComparisonToAssertionTransformation < ASTTransform::AbstractTransformation
|
|
8
|
-
def initialize(*ignored_method_call_symbols)
|
|
9
|
-
@method_call_transformation = RSpock::AST::MethodCallToLVarTransformation.new(*ignored_method_call_symbols)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def on_send(node)
|
|
13
|
-
if node.children.count == 3 && node.children[1] == :== && ignored_method_call_node?(node)
|
|
14
|
-
transform_to_assert_equal(node)
|
|
15
|
-
elsif node.children.count == 3 && node.children[1] == :!= && ignored_method_call_node?(node)
|
|
16
|
-
transform_to_refute_equal(node)
|
|
17
|
-
else
|
|
18
|
-
node.updated(nil, process_all(node))
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
def ignored_method_call_node?(node)
|
|
25
|
-
return false unless node.is_a?(::Parser::AST::Node)
|
|
26
|
-
|
|
27
|
-
!@method_call_transformation.method_call_node?(node.children[0]) &&
|
|
28
|
-
!@method_call_transformation.method_call_node?(node.children[2])
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def transform_to_assert_equal(node)
|
|
32
|
-
node.updated(nil, [nil, :assert_equal, node.children[2], node.children[0]])
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def transform_to_refute_equal(node)
|
|
36
|
-
node.updated(nil, [nil, :refute_equal, node.children[2], node.children[0]])
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|