rspock 2.2.0 → 2.3.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/Gemfile.lock +1 -1
- data/README.md +36 -6
- data/lib/rspock/ast/comparison_to_assertion_transformation.rb +1 -1
- data/lib/rspock/ast/interaction_to_block_identity_assertion_transformation.rb +30 -0
- data/lib/rspock/ast/interaction_to_mocha_mock_transformation.rb +104 -0
- data/lib/rspock/ast/node.rb +97 -0
- data/lib/rspock/ast/parser/block.rb +88 -0
- data/lib/rspock/ast/parser/cleanup_block.rb +22 -0
- data/lib/rspock/ast/parser/expect_block.rb +26 -0
- data/lib/rspock/ast/parser/given_block.rb +22 -0
- data/lib/rspock/ast/parser/interaction_parser.rb +131 -0
- data/lib/rspock/ast/parser/test_method_parser.rb +103 -0
- data/lib/rspock/ast/parser/then_block.rb +29 -0
- data/lib/rspock/ast/parser/when_block.rb +22 -0
- data/lib/rspock/ast/parser/where_block.rb +94 -0
- data/lib/rspock/ast/test_method_transformation.rb +114 -115
- data/lib/rspock/ast/transformation.rb +16 -24
- data/lib/rspock/helpers/block_capture.rb +41 -0
- data/lib/rspock/version.rb +1 -1
- metadata +15 -12
- data/lib/rspock/ast/block.rb +0 -95
- data/lib/rspock/ast/cleanup_block.rb +0 -16
- data/lib/rspock/ast/end_block.rb +0 -17
- data/lib/rspock/ast/expect_block.rb +0 -21
- data/lib/rspock/ast/given_block.rb +0 -16
- data/lib/rspock/ast/interaction_transformation.rb +0 -165
- data/lib/rspock/ast/start_block.rb +0 -28
- data/lib/rspock/ast/then_block.rb +0 -34
- data/lib/rspock/ast/when_block.rb +0 -16
- data/lib/rspock/ast/where_block.rb +0 -86
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0fd03a245608aa5e7e8923fdee5a0cf31c3b553a7e58bbee8a8b57e04c3f8035
|
|
4
|
+
data.tar.gz: 55a5d98ae2855d5e1e1dfde07adba69d7b21c63eed99b3cc1bc951a7d74d62e6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 10ae8fd089fa44e920f6b0f781e6099971aa2474c2aacc6a355c1c3e1a7952bbf779a8170125d66be1c1eb6ea9f21d731ceefb62490e212b4a5c3cd04b615b48
|
|
7
|
+
data.tar.gz: 848890cb9cb6cf79ef15f7c6d607b8a480f07ce500e627ab1bb246c1ed40d6b18f5f6e10ef28a3aba41cd9197b4276d8e6ce3787089740eaea69ae24a20c0e54
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -17,7 +17,7 @@ Note: RSpock is heavily inspired by Spock for the Groovy programming language.
|
|
|
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
19
|
* Expressive assertions: Use familiar comparison operators `==` and `!=` for assertions!
|
|
20
|
-
* [Interaction-based testing](#mocking-with-interactions), i.e. `1 * object.receive("message")` in Then blocks, with optional [return value stubbing](#stubbing-return-values) via `>>`
|
|
20
|
+
* [Interaction-based testing](#mocking-with-interactions), i.e. `1 * object.receive("message")` in Then blocks, with optional [return value stubbing](#stubbing-return-values) via `>>` and [block forwarding verification](#block-forwarding-verification) via `&block`
|
|
21
21
|
* (Planned) BDD-style custom reporter that outputs information from Code Blocks
|
|
22
22
|
* (Planned) Capture all Then block violations
|
|
23
23
|
|
|
@@ -306,13 +306,14 @@ test "#publish sends a message to all subscribers" do
|
|
|
306
306
|
end
|
|
307
307
|
```
|
|
308
308
|
|
|
309
|
-
The above ___Then___ block contains 2 interactions, each of which has 4 parts: the _cardinality_, the _receiver_, the _message_ and its _arguments_. Optionally, a _return value_ can be specified using the `>>` operator.
|
|
309
|
+
The above ___Then___ block contains 2 interactions, each of which has 4 parts: the _cardinality_, the _receiver_, the _message_ and its _arguments_. Optionally, a _return value_ can be specified using the `>>` operator, and _block forwarding_ can be verified using the `&` operator.
|
|
310
310
|
|
|
311
311
|
```
|
|
312
|
-
1 * receiver.message('hello') >> "result"
|
|
313
|
-
| | | |
|
|
314
|
-
| | | |
|
|
315
|
-
| | |
|
|
312
|
+
1 * receiver.message('hello', &blk) >> "result"
|
|
313
|
+
| | | | | |
|
|
314
|
+
| | | | | return value (optional)
|
|
315
|
+
| | | | block forwarding (optional)
|
|
316
|
+
| | | argument(s) (optional)
|
|
316
317
|
| | message
|
|
317
318
|
| receiver
|
|
318
319
|
cardinality
|
|
@@ -320,6 +321,16 @@ cardinality
|
|
|
320
321
|
|
|
321
322
|
Note: Interactions are supported in the ___Then___ block only.
|
|
322
323
|
|
|
324
|
+
#### Execution Order
|
|
325
|
+
|
|
326
|
+
Although interactions are _declared_ in the ___Then___ block, they are effectively active _before_ the ___When___ block executes. RSpock ensures the following order:
|
|
327
|
+
|
|
328
|
+
1. **Before the stimulus** — mock expectations and block captures are installed on the receiver, so they are ready to intercept calls.
|
|
329
|
+
2. **Stimulus** — the ___When___ block runs.
|
|
330
|
+
3. **After the stimulus** — assertions such as block identity checks run alongside other ___Then___ assertions. Cardinality is verified at teardown.
|
|
331
|
+
|
|
332
|
+
Simply declare _what_ should happen in a natural order — RSpock handles the _when_.
|
|
333
|
+
|
|
323
334
|
#### Cardinality
|
|
324
335
|
|
|
325
336
|
The cardinality of an interaction describes how often a method is expected to be called. It can be a fixed number of times, or a range.
|
|
@@ -410,6 +421,25 @@ _ * cache.fetch("key") >> expensive_result # a variable
|
|
|
410
421
|
|
|
411
422
|
**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.
|
|
412
423
|
|
|
424
|
+
#### Block Forwarding Verification
|
|
425
|
+
|
|
426
|
+
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.
|
|
427
|
+
|
|
428
|
+
```ruby
|
|
429
|
+
test "#frame forwards the block to CLI::UI.frame" do
|
|
430
|
+
Given "a block to forward"
|
|
431
|
+
my_block = proc { puts "hello" }
|
|
432
|
+
|
|
433
|
+
When "we call frame with that block"
|
|
434
|
+
@ui.frame("Build", &my_block)
|
|
435
|
+
|
|
436
|
+
Then "the block was forwarded to cli_ui.frame"
|
|
437
|
+
1 * @cli_ui.frame("Build", to: @out, &my_block)
|
|
438
|
+
end
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
**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.
|
|
442
|
+
|
|
413
443
|
## Debugging
|
|
414
444
|
|
|
415
445
|
### Pry
|
|
@@ -22,7 +22,7 @@ module RSpock
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
24
|
def ignored_method_call_node?(node)
|
|
25
|
-
return false unless node.is_a?(Parser::AST::Node)
|
|
25
|
+
return false unless node.is_a?(::Parser::AST::Node)
|
|
26
26
|
|
|
27
27
|
!@method_call_transformation.method_call_node?(node.children[0]) &&
|
|
28
28
|
!@method_call_transformation.method_call_node?(node.children[2])
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'ast_transform/abstract_transformation'
|
|
3
|
+
require 'rspock/ast/node'
|
|
4
|
+
|
|
5
|
+
module RSpock
|
|
6
|
+
module AST
|
|
7
|
+
# Transforms an :rspock_interaction node into an assert_same assertion
|
|
8
|
+
# when a block_pass (&var) is present.
|
|
9
|
+
#
|
|
10
|
+
# Returns the node unchanged (passthrough) when no block_pass is present.
|
|
11
|
+
class InteractionToBlockIdentityAssertionTransformation < ASTTransform::AbstractTransformation
|
|
12
|
+
def initialize(index = 0)
|
|
13
|
+
@index = index
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run(interaction)
|
|
17
|
+
return interaction unless interaction.type == :rspock_interaction
|
|
18
|
+
return interaction unless interaction.block_pass
|
|
19
|
+
|
|
20
|
+
capture_var = :"__rspock_blk_#{@index}"
|
|
21
|
+
block_var = interaction.block_pass.children[0]
|
|
22
|
+
|
|
23
|
+
s(:send, nil, :assert_same,
|
|
24
|
+
block_var,
|
|
25
|
+
s(:send, s(:lvar, capture_var), :call)
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'ast_transform/abstract_transformation'
|
|
3
|
+
require 'rspock/ast/node'
|
|
4
|
+
|
|
5
|
+
module RSpock
|
|
6
|
+
module AST
|
|
7
|
+
# Transforms an :rspock_interaction node into Mocha mock setup code.
|
|
8
|
+
#
|
|
9
|
+
# Input: s(:rspock_interaction, cardinality, receiver, sym, args, return_value, block_pass)
|
|
10
|
+
# Output: receiver.expects(:message).with(*args).times(n).returns(value)
|
|
11
|
+
#
|
|
12
|
+
# When block_pass is present, wraps the expects chain with a BlockCapture.capture call.
|
|
13
|
+
class InteractionToMochaMockTransformation < ASTTransform::AbstractTransformation
|
|
14
|
+
def initialize(index = 0)
|
|
15
|
+
@index = index
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run(interaction)
|
|
19
|
+
return interaction unless interaction.type == :rspock_interaction
|
|
20
|
+
|
|
21
|
+
result = chain_call(interaction.receiver, :expects, s(:sym, interaction.message))
|
|
22
|
+
result = chain_call(result, :with, *interaction.args.children) if interaction.args
|
|
23
|
+
result = build_cardinality(result, interaction.cardinality)
|
|
24
|
+
result = chain_call(result, :returns, interaction.return_value) if interaction.return_value
|
|
25
|
+
|
|
26
|
+
if interaction.block_pass
|
|
27
|
+
build_block_capture_setup(result, interaction.receiver, interaction.message)
|
|
28
|
+
else
|
|
29
|
+
result
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def build_cardinality(result, cardinality)
|
|
36
|
+
if any_matcher_node?(cardinality)
|
|
37
|
+
chain_call(result, :at_least, s(:int, 0))
|
|
38
|
+
elsif [:send, :lvar, :int].include?(cardinality.type)
|
|
39
|
+
chain_call(result, :times, cardinality)
|
|
40
|
+
elsif cardinality.type == :begin && cardinality.children[0]&.type == :irange
|
|
41
|
+
min_node, max_node = cardinality.children[0].children
|
|
42
|
+
build_irange(result, min_node, max_node)
|
|
43
|
+
elsif cardinality.type == :begin && cardinality.children[0]&.type == :erange
|
|
44
|
+
min_node, max_node = cardinality.children[0].children
|
|
45
|
+
max_node = chain_call(max_node, :-, s(:int, 1))
|
|
46
|
+
build_erange(result, min_node, max_node)
|
|
47
|
+
else
|
|
48
|
+
raise ArgumentError, "Unrecognized cardinality in :rspock_interaction: #{cardinality.type}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def build_irange(result, min_node, max_node)
|
|
53
|
+
if any_matcher_node?(min_node) && any_matcher_node?(max_node)
|
|
54
|
+
chain_call(result, :at_least, s(:int, 0))
|
|
55
|
+
elsif !any_matcher_node?(min_node) && any_matcher_node?(max_node)
|
|
56
|
+
chain_call(result, :at_least, min_node)
|
|
57
|
+
elsif any_matcher_node?(min_node) && !any_matcher_node?(max_node)
|
|
58
|
+
result = chain_call(result, :at_least, s(:int, 0))
|
|
59
|
+
chain_call(result, :at_most, max_node)
|
|
60
|
+
else
|
|
61
|
+
result = chain_call(result, :at_least, min_node)
|
|
62
|
+
chain_call(result, :at_most, max_node)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def build_erange(result, min_node, max_node)
|
|
67
|
+
if any_matcher_node?(min_node) && any_matcher_node?(max_node.children[0])
|
|
68
|
+
chain_call(result, :at_least, s(:int, 0))
|
|
69
|
+
elsif !any_matcher_node?(min_node) && any_matcher_node?(max_node.children[0])
|
|
70
|
+
chain_call(result, :at_least, min_node)
|
|
71
|
+
elsif any_matcher_node?(min_node) && !any_matcher_node?(max_node.children[0])
|
|
72
|
+
result = chain_call(result, :at_least, s(:int, 0))
|
|
73
|
+
chain_call(result, :at_most, max_node)
|
|
74
|
+
else
|
|
75
|
+
result = chain_call(result, :at_least, min_node)
|
|
76
|
+
chain_call(result, :at_most, max_node)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def build_block_capture_setup(expects_node, receiver, message)
|
|
81
|
+
capture_var = :"__rspock_blk_#{@index}"
|
|
82
|
+
|
|
83
|
+
capture_call = s(:lvasgn, capture_var,
|
|
84
|
+
s(:send,
|
|
85
|
+
s(:const, s(:const, s(:const, nil, :RSpock), :Helpers), :BlockCapture),
|
|
86
|
+
:capture,
|
|
87
|
+
receiver,
|
|
88
|
+
s(:sym, message)
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
s(:begin, expects_node, capture_call)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def chain_call(receiver_node, method_name, *arg_nodes)
|
|
96
|
+
s(:send, receiver_node, method_name, *arg_nodes)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def any_matcher_node?(node)
|
|
100
|
+
node.type == :send && node.children[0].nil? && node.children[1] == :_
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'ast_transform/transformation_helper'
|
|
3
|
+
|
|
4
|
+
module RSpock
|
|
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
|
|
20
|
+
register :rspock_test
|
|
21
|
+
|
|
22
|
+
def def_node = children[0]
|
|
23
|
+
def body_node = children[1]
|
|
24
|
+
def where_node = children[2]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class BodyNode < Node
|
|
28
|
+
register :rspock_body
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class DefNode < Node
|
|
32
|
+
register :rspock_def
|
|
33
|
+
|
|
34
|
+
def method_call = children[0]
|
|
35
|
+
def args = children[1]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class GivenNode < Node
|
|
39
|
+
register :rspock_given
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class WhenNode < Node
|
|
43
|
+
register :rspock_when
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class ThenNode < Node
|
|
47
|
+
register :rspock_then
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class ExpectNode < Node
|
|
51
|
+
register :rspock_expect
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class CleanupNode < Node
|
|
55
|
+
register :rspock_cleanup
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class WhereNode < Node
|
|
59
|
+
register :rspock_where
|
|
60
|
+
|
|
61
|
+
def header
|
|
62
|
+
header_node = children.find { |n| n.type == :rspock_where_header }
|
|
63
|
+
header_node.children.map { |sym_node| sym_node.children[0] }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def data_rows
|
|
67
|
+
children
|
|
68
|
+
.select { |n| n.type == :array }
|
|
69
|
+
.map(&:children)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class InteractionNode < Node
|
|
74
|
+
register :rspock_interaction
|
|
75
|
+
|
|
76
|
+
def cardinality = children[0]
|
|
77
|
+
def receiver = children[1]
|
|
78
|
+
def message_sym = children[2]
|
|
79
|
+
def message = message_sym.children[0]
|
|
80
|
+
def args = children[3]
|
|
81
|
+
def return_value = children[4]
|
|
82
|
+
def block_pass = children[5]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
module NodeBuilder
|
|
86
|
+
include ASTTransform::TransformationHelper
|
|
87
|
+
|
|
88
|
+
def s(type, *children)
|
|
89
|
+
if type.to_s.start_with?('rspock_')
|
|
90
|
+
Node.build(type, *children)
|
|
91
|
+
else
|
|
92
|
+
super
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'rspock/ast/node'
|
|
3
|
+
|
|
4
|
+
module RSpock
|
|
5
|
+
module AST
|
|
6
|
+
module Parser
|
|
7
|
+
class BlockError < StandardError; end
|
|
8
|
+
|
|
9
|
+
class Block
|
|
10
|
+
include RSpock::AST::NodeBuilder
|
|
11
|
+
|
|
12
|
+
# Constructs a new Block.
|
|
13
|
+
#
|
|
14
|
+
# @param type [Symbol] The Block type.
|
|
15
|
+
# @param node [Parser::AST::Node] The node associated to this Block.
|
|
16
|
+
def initialize(type, node)
|
|
17
|
+
@type = type
|
|
18
|
+
@node = node
|
|
19
|
+
@children = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :type, :node
|
|
23
|
+
|
|
24
|
+
# Adds the given +child_node+ to this Block.
|
|
25
|
+
#
|
|
26
|
+
# @param child_node [Parser::AST::Node] The node to be added.
|
|
27
|
+
def <<(child_node)
|
|
28
|
+
@children << child_node
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Retrieves the Parser::Source::Range for this Block.
|
|
32
|
+
#
|
|
33
|
+
# @return [Parser::Source::Range] The range.
|
|
34
|
+
def range
|
|
35
|
+
node&.loc&.expression || "?"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Whether this block can be the first block in a test method.
|
|
39
|
+
def can_start?
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Whether this block can be the last block in a test method.
|
|
44
|
+
def can_end?
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Retrieves the valid successors for this Block.
|
|
49
|
+
#
|
|
50
|
+
# @return [Array<Symbol>] This Block's successors.
|
|
51
|
+
def successors
|
|
52
|
+
@successors ||= [].freeze
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Retrieves the duped array of children AST nodes for this Block.
|
|
56
|
+
#
|
|
57
|
+
# @return [Array<Parser::AST::Node>] The children nodes.
|
|
58
|
+
def children
|
|
59
|
+
@children.dup
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Converts this Block into an RSpock node.
|
|
63
|
+
#
|
|
64
|
+
# @return [Parser::AST::Node] A node with type :rspock_<block_type>.
|
|
65
|
+
def to_rspock_node
|
|
66
|
+
rspock_type = :"rspock_#{type.downcase}"
|
|
67
|
+
s(rspock_type, *@children)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Checks whether or not the given +block+ is a valid successor for this Block.
|
|
71
|
+
#
|
|
72
|
+
# @param block [Block] The candidate successor.
|
|
73
|
+
#
|
|
74
|
+
# @return [Boolean] True if the given block is a valid successor, false otherwise.
|
|
75
|
+
def valid_successor?(block)
|
|
76
|
+
successors.include?(block.type)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Retrieves the error message for succession errors.
|
|
80
|
+
#
|
|
81
|
+
# @return [String] The error message.
|
|
82
|
+
def succession_error_msg
|
|
83
|
+
"Block #{type} @ #{range} must be followed by one of these Blocks: #{successors}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'rspock/ast/parser/block'
|
|
3
|
+
|
|
4
|
+
module RSpock
|
|
5
|
+
module AST
|
|
6
|
+
module Parser
|
|
7
|
+
class CleanupBlock < Block
|
|
8
|
+
def initialize(node)
|
|
9
|
+
super(:Cleanup, node)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def can_end?
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def successors
|
|
17
|
+
@successors ||= [:Where].freeze
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'rspock/ast/parser/block'
|
|
3
|
+
|
|
4
|
+
module RSpock
|
|
5
|
+
module AST
|
|
6
|
+
module Parser
|
|
7
|
+
class ExpectBlock < Block
|
|
8
|
+
def initialize(node)
|
|
9
|
+
super(:Expect, node)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def can_start?
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def can_end?
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def successors
|
|
21
|
+
@successors ||= [:Cleanup, :Where].freeze
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'rspock/ast/parser/block'
|
|
3
|
+
|
|
4
|
+
module RSpock
|
|
5
|
+
module AST
|
|
6
|
+
module Parser
|
|
7
|
+
class GivenBlock < Block
|
|
8
|
+
def initialize(node)
|
|
9
|
+
super(:Given, node)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def can_start?
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def successors
|
|
17
|
+
@successors ||= [:When, :Expect].freeze
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'rspock/ast/node'
|
|
3
|
+
|
|
4
|
+
module RSpock
|
|
5
|
+
module AST
|
|
6
|
+
module Parser
|
|
7
|
+
# Parses raw Ruby AST interaction nodes into structured :rspock_interaction nodes.
|
|
8
|
+
#
|
|
9
|
+
# Input: 1 * receiver.message("arg", &blk) >> "result"
|
|
10
|
+
# Output: s(:rspock_interaction, cardinality, receiver, sym, args, return_value, block_pass)
|
|
11
|
+
#
|
|
12
|
+
# :rspock_interaction children:
|
|
13
|
+
# [0] cardinality - e.g. s(:int, 1), s(:begin, s(:irange, ...)), s(:send, nil, :_)
|
|
14
|
+
# [1] receiver - e.g. s(:send, nil, :subscriber)
|
|
15
|
+
# [2] message - e.g. s(:sym, :receive)
|
|
16
|
+
# [3] args - nil if no args, s(:array, *arg_nodes) otherwise
|
|
17
|
+
# [4] return_value - nil if no >>, otherwise the value node
|
|
18
|
+
# [5] block_pass - nil if no &, otherwise s(:block_pass, ...)
|
|
19
|
+
class InteractionParser
|
|
20
|
+
include RSpock::AST::NodeBuilder
|
|
21
|
+
|
|
22
|
+
class InteractionError < RuntimeError; end
|
|
23
|
+
|
|
24
|
+
ALLOWED_CARDINALITY_NODES = [:send, :lvar, :int].freeze
|
|
25
|
+
|
|
26
|
+
def interaction_node?(node)
|
|
27
|
+
return false if node.nil?
|
|
28
|
+
return true if node.type == :send && node.children[1] == :*
|
|
29
|
+
return true if return_value_node?(node)
|
|
30
|
+
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def parse(node)
|
|
35
|
+
return node unless interaction_node?(node)
|
|
36
|
+
|
|
37
|
+
if return_value_node?(node)
|
|
38
|
+
return_value = node.children[2]
|
|
39
|
+
node = node.children[0]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
cardinality = node.children[0]
|
|
43
|
+
validate_cardinality(cardinality)
|
|
44
|
+
|
|
45
|
+
rhs = node.children[2]
|
|
46
|
+
receiver, message, args, block_pass = parse_rhs(rhs)
|
|
47
|
+
|
|
48
|
+
s(:rspock_interaction,
|
|
49
|
+
cardinality,
|
|
50
|
+
receiver,
|
|
51
|
+
s(:sym, message),
|
|
52
|
+
args,
|
|
53
|
+
return_value,
|
|
54
|
+
block_pass
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def return_value_node?(node)
|
|
61
|
+
node.type == :send && node.children[1] == :>> && interaction_node?(node.children[0])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def validate_cardinality(node)
|
|
65
|
+
case node.type
|
|
66
|
+
when *ALLOWED_CARDINALITY_NODES
|
|
67
|
+
# OK
|
|
68
|
+
when :begin
|
|
69
|
+
if node.children.count > 1
|
|
70
|
+
raise_cardinality_error(node,
|
|
71
|
+
msg_prefix: "Left-hand side of ",
|
|
72
|
+
msg_suffix: " or a range in parentheses")
|
|
73
|
+
end
|
|
74
|
+
case node.children[0].type
|
|
75
|
+
when :irange, :erange
|
|
76
|
+
unless ALLOWED_CARDINALITY_NODES.include?(node.children[0].children[0].type)
|
|
77
|
+
raise_cardinality_error(node.children[0].children[0], msg_prefix: "Minimum range of ")
|
|
78
|
+
end
|
|
79
|
+
unless ALLOWED_CARDINALITY_NODES.include?(node.children[0].children[1].type)
|
|
80
|
+
raise_cardinality_error(node.children[0].children[1], msg_prefix: "Maximum range of ")
|
|
81
|
+
end
|
|
82
|
+
else
|
|
83
|
+
raise_cardinality_error(node,
|
|
84
|
+
msg_prefix: "Left-hand side of ",
|
|
85
|
+
msg_suffix: " or a range in parentheses")
|
|
86
|
+
end
|
|
87
|
+
else
|
|
88
|
+
raise_cardinality_error(node,
|
|
89
|
+
msg_prefix: "Left-hand side of ",
|
|
90
|
+
msg_suffix: " or a range in parentheses")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def parse_rhs(node)
|
|
95
|
+
if node.type == :block
|
|
96
|
+
raise InteractionError, "Inline blocks (do...end / { }) are not supported in interactions @ #{range(node)}. " \
|
|
97
|
+
"Use &var for block forwarding verification, or << for method body override (future)."
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if node.type != :send
|
|
101
|
+
raise InteractionError, "Right-hand side of Interaction @ #{range(node)} must be a :send node."
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
receiver, message, *arg_nodes = node.children
|
|
105
|
+
|
|
106
|
+
if receiver.nil?
|
|
107
|
+
raise InteractionError, "Right-hand side of Interaction @ #{range(node)} must have a receiver."
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
block_pass = arg_nodes.find { |n| n.type == :block_pass }
|
|
111
|
+
if block_pass
|
|
112
|
+
arg_nodes = arg_nodes.reject { |n| n.equal?(block_pass) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
args = arg_nodes.empty? ? nil : s(:array, *arg_nodes)
|
|
116
|
+
|
|
117
|
+
[receiver, message, args, block_pass]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def range(node)
|
|
121
|
+
node&.loc&.expression || "?"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def raise_cardinality_error(node, msg_prefix: "", msg_suffix: "")
|
|
125
|
+
raise InteractionError, "#{msg_prefix}Interaction @ #{range(node)} must be one of " \
|
|
126
|
+
"#{ALLOWED_CARDINALITY_NODES}#{msg_suffix}."
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|