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
|
@@ -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,12 +11,13 @@ 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
|
|
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
|
|
17
18
|
|
|
18
19
|
def parse(node)
|
|
20
|
+
return build_raises(node) if raises_condition?(node)
|
|
19
21
|
return node if assignment?(node)
|
|
20
22
|
return build_binary_statement(node) if binary_statement?(node)
|
|
21
23
|
|
|
@@ -24,6 +26,32 @@ module RSpock
|
|
|
24
26
|
|
|
25
27
|
private
|
|
26
28
|
|
|
29
|
+
def raises_condition?(node)
|
|
30
|
+
direct_raises?(node) || assigned_raises?(node)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def direct_raises?(node)
|
|
34
|
+
node.type == :send && node.children[0].nil? && node.children[1] == :raises
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def assigned_raises?(node)
|
|
38
|
+
node.type == :lvasgn &&
|
|
39
|
+
node.children[1]&.type == :send &&
|
|
40
|
+
node.children[1].children[0].nil? &&
|
|
41
|
+
node.children[1].children[1] == :raises
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build_raises(node)
|
|
45
|
+
if node.type == :lvasgn
|
|
46
|
+
variable = s(:sym, node.children[0])
|
|
47
|
+
exception_class = node.children[1].children[2]
|
|
48
|
+
s_anchored(node, :rspock_raises, exception_class, variable)
|
|
49
|
+
else
|
|
50
|
+
exception_class = node.children[2]
|
|
51
|
+
s_anchored(node, :rspock_raises, exception_class)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
27
55
|
def assignment?(node)
|
|
28
56
|
ASSIGNMENT_TYPES.include?(node.type)
|
|
29
57
|
end
|
|
@@ -34,13 +62,20 @@ module RSpock
|
|
|
34
62
|
BINARY_OPERATORS.include?(node.children[1])
|
|
35
63
|
end
|
|
36
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.
|
|
37
67
|
def build_binary_statement(node)
|
|
38
|
-
|
|
68
|
+
s_anchored(node, :rspock_binary_statement, node.children[0], s(:sym, node.children[1]), node.children[2])
|
|
39
69
|
end
|
|
40
70
|
|
|
41
71
|
def build_statement(node)
|
|
42
72
|
source = node.loc&.expression&.source || node.inspect
|
|
43
|
-
|
|
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)
|
|
44
79
|
end
|
|
45
80
|
end
|
|
46
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
|
|
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}
|
|
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
|
|
|
@@ -30,6 +30,11 @@ module RSpock
|
|
|
30
30
|
statement_parser.parse(child)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
raises_count = spock_children.count { |c| c.type == :rspock_raises }
|
|
34
|
+
if raises_count > 1
|
|
35
|
+
raise BlockError, "Then block @ #{range} may contain at most one raises() condition"
|
|
36
|
+
end
|
|
37
|
+
|
|
33
38
|
s(:rspock_then, *spock_children)
|
|
34
39
|
end
|
|
35
40
|
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
|
|
@@ -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
|
|
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
|
-
|
|
9
|
-
|
|
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,
|
|
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)
|
|
@@ -3,15 +3,13 @@ require 'ast_transform/abstract_transformation'
|
|
|
3
3
|
|
|
4
4
|
module RSpock
|
|
5
5
|
module AST
|
|
6
|
+
# dstr counterpart of TestMethodDefTransformation: appends the row index and source line interpolations to an
|
|
7
|
+
# already-interpolated test name.
|
|
6
8
|
class TestMethodDstrTransformation < ASTTransform::AbstractTransformation
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
LINE_NUMBER_AST = s(:begin,
|
|
11
|
-
s(:lvar, :_line_number_))
|
|
9
|
+
ROW_INDEX_AST = s(:begin, s(:lvar, :__rspock_row_index__))
|
|
10
|
+
ROW_LINE_AST = s(:begin, s(:lvar, :__rspock_row_line__))
|
|
12
11
|
|
|
13
12
|
SPACE_STR_AST = s(:str, " ")
|
|
14
|
-
|
|
15
13
|
LINE_NUMBER_STR_AST = s(:str, " line ")
|
|
16
14
|
|
|
17
15
|
def on_dstr(node)
|
|
@@ -24,7 +22,7 @@ module RSpock
|
|
|
24
22
|
children << SPACE_STR_AST
|
|
25
23
|
end
|
|
26
24
|
|
|
27
|
-
children.push(
|
|
25
|
+
children.push(ROW_INDEX_AST, LINE_NUMBER_STR_AST, ROW_LINE_AST)
|
|
28
26
|
node.updated(nil, children)
|
|
29
27
|
end
|
|
30
28
|
end
|
|
@@ -5,7 +5,6 @@ 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'
|
|
8
|
-
require 'rspock/ast/method_call_to_lvar_transformation'
|
|
9
8
|
require 'rspock/ast/test_method_def_transformation'
|
|
10
9
|
require 'rspock/ast/parser/test_method_parser'
|
|
11
10
|
|
|
@@ -26,79 +25,187 @@ module RSpock
|
|
|
26
25
|
private
|
|
27
26
|
|
|
28
27
|
def transform(rspock_ast)
|
|
29
|
-
hoisted_setups = []
|
|
30
|
-
|
|
31
28
|
method_call = rspock_ast.def_node.method_call
|
|
32
29
|
method_args = rspock_ast.def_node.args
|
|
33
30
|
where = rspock_ast.where_node
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
body = build_test_body(rspock_ast.body_node)
|
|
33
|
+
build_ruby_ast(method_call, method_args, body, where)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# --- Test body assembly ---
|
|
37
|
+
#
|
|
38
|
+
# Statements are assembled in SOURCE order so line-aligned emission keeps each one on its own line.
|
|
39
|
+
# Execution-order requirements that source order cannot express (interaction setups in Then must run before the
|
|
40
|
+
# When body they observe) are carried by ast-transform's thunk facility (run_after / thunk) instead of by
|
|
41
|
+
# textual hoisting.
|
|
42
|
+
def build_test_body(body_node)
|
|
43
|
+
blocks = body_node.children
|
|
44
|
+
sections = blocks.map { |block_node| transform_block(block_node) }
|
|
45
|
+
|
|
46
|
+
when_statements = statements_of_type(blocks, sections, :rspock_when)
|
|
47
|
+
cleanup_statements = statements_of_type(blocks, sections, :rspock_cleanup)
|
|
48
|
+
interaction_setups = sections.flat_map(&:interaction_setups)
|
|
49
|
+
raises_node = blocks.filter_map { |block_node| find_raises(block_node) }.first
|
|
50
|
+
|
|
51
|
+
source_order = blocks.zip(sections).flat_map do |block_node, section|
|
|
52
|
+
next [] if block_node.type == :rspock_cleanup
|
|
53
|
+
|
|
54
|
+
section.statements.reject { |statement| statement.type == :rspock_raises }
|
|
44
55
|
end
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
body_children = order_execution(source_order, when_statements, interaction_setups, raises_node)
|
|
58
|
+
|
|
59
|
+
ast = s(:begin, *body_children)
|
|
60
|
+
cleanup_statements.empty? ? ast : s(:kwbegin, s(:ensure, ast, s(:begin, *cleanup_statements)))
|
|
48
61
|
end
|
|
49
62
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
63
|
+
Section = Data.define(:statements, :interaction_setups)
|
|
64
|
+
|
|
65
|
+
def transform_block(block_node)
|
|
66
|
+
case block_node.type
|
|
67
|
+
when :rspock_then, :rspock_expect
|
|
68
|
+
transform_assertion_block(block_node)
|
|
69
|
+
else
|
|
70
|
+
Section.new(statements: block_node.children, interaction_setups: [])
|
|
71
|
+
end
|
|
72
|
+
end
|
|
53
73
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
# Then/Expect children become plain Ruby in place: interactions lower to Mocha setups anchored at the
|
|
75
|
+
# interaction's own source line (plus an identity assertion for &block forwarding), statements become
|
|
76
|
+
# assertions at their own lines.
|
|
77
|
+
#
|
|
78
|
+
# Within the section, ALL setups come before all assertions: the thunked When body executes right after the
|
|
79
|
+
# last setup, and every assertion (identity or otherwise) observes the When body's effects, so none may
|
|
80
|
+
# precede that point. Setups keep source order and their anchors, so alignment holds; assertions after them
|
|
81
|
+
# are either synthetic (identity assertions, loc-less, pack anywhere) or textually below the interactions in
|
|
82
|
+
# the common case.
|
|
83
|
+
def transform_assertion_block(block_node)
|
|
84
|
+
setups = []
|
|
85
|
+
assertions = []
|
|
86
|
+
interaction_index = 0
|
|
58
87
|
|
|
59
|
-
|
|
60
|
-
|
|
88
|
+
block_node.children.each do |child|
|
|
89
|
+
case child.type
|
|
90
|
+
when :rspock_interaction
|
|
91
|
+
interaction_setups, identity_assertions = lower_interaction(child, interaction_index)
|
|
92
|
+
interaction_index += 1
|
|
93
|
+
setups.concat(interaction_setups)
|
|
94
|
+
assertions.concat(identity_assertions)
|
|
95
|
+
when :rspock_binary_statement, :rspock_statement
|
|
96
|
+
assertions << anchored_at(child, @statement_transformation.run(child))
|
|
61
97
|
else
|
|
62
|
-
|
|
98
|
+
assertions << child
|
|
63
99
|
end
|
|
64
100
|
end
|
|
65
101
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
102
|
+
Section.new(statements: setups + assertions, interaction_setups: setups)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @return [Array(Array, Array)] Mocha setup statements, identity assertions
|
|
106
|
+
def lower_interaction(interaction, index)
|
|
107
|
+
setup = InteractionToMochaMockTransformation.new(index).run(interaction)
|
|
108
|
+
assertion = InteractionToBlockIdentityAssertionTransformation.new(index).run(interaction)
|
|
109
|
+
|
|
110
|
+
setups = setup.type == :begin ? setup.children.dup : [setup]
|
|
111
|
+
setups[0] = anchored_at(interaction, setups[0])
|
|
112
|
+
|
|
113
|
+
[setups, assertion.equal?(interaction) ? [] : [assertion]]
|
|
114
|
+
end
|
|
75
115
|
|
|
76
|
-
|
|
116
|
+
# Reorders execution (not text) where required:
|
|
117
|
+
# - interactions without raises: run the When body after the last interaction setup (run_after — the paved
|
|
118
|
+
# road).
|
|
119
|
+
# - raises without interactions: the When body inlines directly into assert_raises; no thunk needed.
|
|
120
|
+
# - raises with interactions: the When body is thunked into the assert_raises block inserted after the last
|
|
121
|
+
# setup; the lowering re-emits the body at its own source lines.
|
|
122
|
+
def order_execution(source_order, when_statements, interaction_setups, raises_node)
|
|
123
|
+
if raises_node
|
|
124
|
+
build_raises_body(source_order, when_statements, interaction_setups, raises_node)
|
|
125
|
+
elsif interaction_setups.any? && when_statements.any?
|
|
126
|
+
run_after(source_order, run: when_statements, after: interaction_setups.last)
|
|
127
|
+
else
|
|
128
|
+
source_order
|
|
129
|
+
end
|
|
77
130
|
end
|
|
78
131
|
|
|
79
|
-
def
|
|
80
|
-
|
|
81
|
-
|
|
132
|
+
def build_raises_body(source_order, when_statements, interaction_setups, raises_node)
|
|
133
|
+
if interaction_setups.any?
|
|
134
|
+
assertion = build_assert_raises(raises_node, thunk(*when_statements))
|
|
135
|
+
|
|
136
|
+
reordered = replace_run(source_order, when_statements, [])
|
|
137
|
+
insert_after(reordered, interaction_setups.last, assertion)
|
|
138
|
+
else
|
|
139
|
+
when_body = when_statements.length == 1 ? when_statements[0] : s(:begin, *when_statements)
|
|
140
|
+
assertion = build_assert_raises(raises_node, when_body)
|
|
141
|
+
|
|
142
|
+
replace_run(source_order, when_statements, [assertion])
|
|
143
|
+
end
|
|
82
144
|
end
|
|
83
145
|
|
|
84
|
-
def
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
146
|
+
def build_assert_raises(raises_node, body)
|
|
147
|
+
assert_raises_call = s(:block,
|
|
148
|
+
s(:send, nil, :assert_raises, raises_node.exception_class),
|
|
149
|
+
s(:args),
|
|
150
|
+
body
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if raises_node.capture_name
|
|
154
|
+
s(:lvasgn, raises_node.capture_name, assert_raises_call)
|
|
88
155
|
else
|
|
89
|
-
|
|
156
|
+
assert_raises_call
|
|
90
157
|
end
|
|
91
158
|
end
|
|
92
159
|
|
|
160
|
+
def find_raises(block_node)
|
|
161
|
+
return nil unless block_node.type == :rspock_then
|
|
162
|
+
|
|
163
|
+
block_node.children.find { |child| child.type == :rspock_raises }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def statements_of_type(blocks, sections, type)
|
|
167
|
+
blocks.zip(sections)
|
|
168
|
+
.select { |block_node, _section| block_node.type == type }
|
|
169
|
+
.flat_map { |_block_node, section| section.statements }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# --- Identity-based sequence edits (non-thunk counterparts of run_after) ---
|
|
173
|
+
|
|
174
|
+
def replace_run(statements, run, replacement)
|
|
175
|
+
start = statements.index { |statement| statement.equal?(run.first) }
|
|
176
|
+
raise ArgumentError, "run is not part of statements" unless start
|
|
177
|
+
|
|
178
|
+
result = statements.dup
|
|
179
|
+
result[start, run.length] = replacement
|
|
180
|
+
result
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def insert_after(statements, anchor, insertion)
|
|
184
|
+
index = statements.index { |statement| statement.equal?(anchor) }
|
|
185
|
+
raise ArgumentError, "anchor is not part of statements" unless index
|
|
186
|
+
|
|
187
|
+
result = statements.dup
|
|
188
|
+
result.insert(index + 1, insertion)
|
|
189
|
+
result
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Re-anchors +node+ at +anchor+'s source location so emission places it on the anchor's line.
|
|
193
|
+
# No-op for anchors without locations.
|
|
194
|
+
def anchored_at(anchor, node)
|
|
195
|
+
return node unless anchor.loc&.expression
|
|
196
|
+
|
|
197
|
+
s_at(anchor, node.type, *node.children)
|
|
198
|
+
end
|
|
199
|
+
|
|
93
200
|
# --- Build final Ruby AST ---
|
|
94
201
|
|
|
95
|
-
def build_ruby_ast(method_call, method_args, body_node, where
|
|
202
|
+
def build_ruby_ast(method_call, method_args, body_node, where)
|
|
96
203
|
if where
|
|
97
|
-
test_def = s(:block,
|
|
204
|
+
test_def = anchored_at(method_call, s(:block,
|
|
98
205
|
TestMethodDefTransformation.new.run(method_call),
|
|
99
206
|
method_args,
|
|
100
|
-
|
|
101
|
-
)
|
|
207
|
+
body_node
|
|
208
|
+
))
|
|
102
209
|
test_def = HeaderNodesTransformation.new(where.header).run(test_def)
|
|
103
210
|
|
|
104
211
|
s(:block,
|
|
@@ -107,43 +214,19 @@ module RSpock
|
|
|
107
214
|
test_def
|
|
108
215
|
)
|
|
109
216
|
else
|
|
110
|
-
s(:block,
|
|
217
|
+
anchored_at(method_call, s(:block,
|
|
111
218
|
method_call,
|
|
112
219
|
method_args,
|
|
113
|
-
|
|
114
|
-
)
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def build_test_body(body_node, hoisted_setups)
|
|
119
|
-
body_children = []
|
|
120
|
-
|
|
121
|
-
body_node.children.each do |block_node|
|
|
122
|
-
case block_node.type
|
|
123
|
-
when :rspock_given
|
|
124
|
-
body_children.concat(block_node.children)
|
|
125
|
-
when :rspock_when
|
|
126
|
-
body_children.concat(hoisted_setups)
|
|
127
|
-
body_children.concat(block_node.children)
|
|
128
|
-
when :rspock_then, :rspock_expect
|
|
129
|
-
body_children.concat(block_node.children)
|
|
130
|
-
when :rspock_cleanup
|
|
131
|
-
# handled below as ensure
|
|
132
|
-
end
|
|
220
|
+
body_node
|
|
221
|
+
))
|
|
133
222
|
end
|
|
134
|
-
|
|
135
|
-
ast = s(:begin, *body_children)
|
|
136
|
-
|
|
137
|
-
cleanup = body_node.children.find { |n| n.type == :rspock_cleanup }
|
|
138
|
-
if cleanup && !cleanup.children.empty?
|
|
139
|
-
ensure_node = s(:begin, *cleanup.children)
|
|
140
|
-
ast = s(:kwbegin, s(:ensure, ast, ensure_node))
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
MethodCallToLVarTransformation.new(:_test_index_, :_line_number_).run(ast)
|
|
144
223
|
end
|
|
145
224
|
|
|
146
225
|
# --- Where block helpers ---
|
|
226
|
+
#
|
|
227
|
+
# Each data row carries its source line as a trailing element, surfaced in the generated test NAME only
|
|
228
|
+
# (uniqueness for identical rows + the -n selector target) through internal block parameters. There is no
|
|
229
|
+
# user-facing runtime variable: isolate a row by running its generated test by name, then break normally.
|
|
147
230
|
|
|
148
231
|
def build_where_iterator(data_rows)
|
|
149
232
|
s(:send,
|
|
@@ -158,15 +241,16 @@ module RSpock
|
|
|
158
241
|
def build_where_data_row(row)
|
|
159
242
|
children = row.dup
|
|
160
243
|
children << s(:int, row.first&.loc&.expression&.line)
|
|
161
|
-
|
|
244
|
+
anchor = row.first
|
|
245
|
+
anchor&.loc&.expression ? s_at(anchor, :array, *children) : s(:array, *children)
|
|
162
246
|
end
|
|
163
247
|
|
|
164
248
|
def build_where_args(header)
|
|
165
249
|
injected_args = header.map { |column| s(:arg, column) }
|
|
166
|
-
injected_args << s(:arg,
|
|
250
|
+
injected_args << s(:arg, TestMethodDefTransformation::ROW_LINE_ARG)
|
|
167
251
|
s(:args,
|
|
168
252
|
s(:mlhs, *injected_args),
|
|
169
|
-
s(:arg,
|
|
253
|
+
s(:arg, TestMethodDefTransformation::ROW_INDEX_ARG),
|
|
170
254
|
)
|
|
171
255
|
end
|
|
172
256
|
end
|
|
@@ -77,8 +77,7 @@ module RSpock
|
|
|
77
77
|
|
|
78
78
|
def process_rspock(node)
|
|
79
79
|
processed = process_all(node).compact
|
|
80
|
-
|
|
81
|
-
node.updated(nil, children)
|
|
80
|
+
node.updated(nil, [EXTEND_RSPOCK_DECLARATIVE, *processed])
|
|
82
81
|
end
|
|
83
82
|
|
|
84
83
|
def on_block(node)
|
|
@@ -91,31 +90,6 @@ module RSpock
|
|
|
91
90
|
strict: @strict
|
|
92
91
|
).run(node)
|
|
93
92
|
end
|
|
94
|
-
|
|
95
|
-
def source_map_rescue_wrapper(node)
|
|
96
|
-
s(:kwbegin,
|
|
97
|
-
s(:rescue,
|
|
98
|
-
node,
|
|
99
|
-
s(:resbody,
|
|
100
|
-
s(:array,
|
|
101
|
-
s(:const, nil, :StandardError)
|
|
102
|
-
),
|
|
103
|
-
s(:lvasgn, :e),
|
|
104
|
-
s(:begin,
|
|
105
|
-
s(:send,
|
|
106
|
-
s(:send,
|
|
107
|
-
s(:const,
|
|
108
|
-
s(:const,
|
|
109
|
-
s(:cbase), :RSpock), :BacktraceFilter), :new), :filter_exception,
|
|
110
|
-
s(:lvar, :e)
|
|
111
|
-
),
|
|
112
|
-
s(:send, nil, :raise)
|
|
113
|
-
)
|
|
114
|
-
),
|
|
115
|
-
nil
|
|
116
|
-
)
|
|
117
|
-
)
|
|
118
|
-
end
|
|
119
93
|
end
|
|
120
94
|
end
|
|
121
95
|
end
|
data/lib/rspock/version.rb
CHANGED
data/lib/rspock.rb
CHANGED
data/rspock.gemspec
CHANGED
|
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
|
|
|
19
19
|
spec.bindir = "exe"
|
|
20
20
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
21
21
|
spec.require_paths = ["lib"]
|
|
22
|
-
spec.required_ruby_version = '>= 3.
|
|
22
|
+
spec.required_ruby_version = '>= 3.3'
|
|
23
23
|
|
|
24
24
|
# Development dependencies
|
|
25
25
|
spec.add_development_dependency "bundler", ">= 2.1"
|
|
@@ -31,9 +31,8 @@ Gem::Specification.new do |spec|
|
|
|
31
31
|
spec.add_development_dependency "simplecov", "~> 0.22"
|
|
32
32
|
|
|
33
33
|
# Runtime dependencies
|
|
34
|
-
|
|
34
|
+
# parser and unparser are used only through ast_transform, which owns their floors.
|
|
35
|
+
spec.add_runtime_dependency "ast_transform", "~> 3.0"
|
|
35
36
|
spec.add_runtime_dependency "minitest", "~> 5.0"
|
|
36
37
|
spec.add_runtime_dependency "mocha", ">= 1.0"
|
|
37
|
-
spec.add_runtime_dependency "parser", ">= 3.0"
|
|
38
|
-
spec.add_runtime_dependency "unparser", ">= 0.6"
|
|
39
38
|
end
|