rast 0.1.0.pre
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 +7 -0
- data/.gitignore +2 -0
- data/.rspec +1 -0
- data/.rubocop.yml +3 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +72 -0
- data/Guardfile +36 -0
- data/README.md +25 -0
- data/examples/logic_checker.rb +23 -0
- data/examples/prime_number.rb +19 -0
- data/examples/recruiter.rb +50 -0
- data/examples/worker.rb +12 -0
- data/lib/rast/assert.rb +7 -0
- data/lib/rast/converters/bool_converter.rb +10 -0
- data/lib/rast/converters/float_converter.rb +8 -0
- data/lib/rast/converters/int_converter.rb +8 -0
- data/lib/rast/converters/str_converter.rb +8 -0
- data/lib/rast/parameter_generator.rb +109 -0
- data/lib/rast/rast_spec.rb +44 -0
- data/lib/rast/rules/logic_helper.rb +132 -0
- data/lib/rast/rules/operator.rb +18 -0
- data/lib/rast/rules/rule.rb +93 -0
- data/lib/rast/rules/rule_evaluator.rb +273 -0
- data/lib/rast/rules/rule_processor.rb +41 -0
- data/lib/rast/rules/rule_validator.rb +63 -0
- data/lib/rast/spec_dsl.rb +96 -0
- data/lib/rast/version.rb +3 -0
- data/lib/rast.rb +46 -0
- data/lib/template_spec.yml +18 -0
- data/rast.gemspec +30 -0
- metadata +73 -0
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Represents a logical rule object.
|
4
|
+
class Rule
|
5
|
+
# /**
|
6
|
+
# * TODO: Validation of the output_clause_src parameter.
|
7
|
+
# *
|
8
|
+
# * Instantiate a rule with the given clause. <br/>
|
9
|
+
# * <br/>
|
10
|
+
# * <b>Parameter Example:</b><br/>
|
11
|
+
# * Visible:Proposed|Approved<br/>
|
12
|
+
# * <br/>
|
13
|
+
# * Peace:Friendly|Indifferent\<br/>
|
14
|
+
# * ~War:Angry\<br/>
|
15
|
+
# * ~Neutral:Play safe
|
16
|
+
# */
|
17
|
+
def initialize(rules: {})
|
18
|
+
raise 'Must not have empty rules' if rules.empty?
|
19
|
+
|
20
|
+
@outcome_clause_hash = {}
|
21
|
+
# should have array of the rule pairs
|
22
|
+
# rules = pActRuleSrc.split("~");
|
23
|
+
duplicates = []
|
24
|
+
|
25
|
+
rules.each do |outcome, clause|
|
26
|
+
if duplicates.include?(outcome)
|
27
|
+
raise "#{outcome} matched multiple clauses"
|
28
|
+
end
|
29
|
+
|
30
|
+
duplicates << outcome
|
31
|
+
clause = remove_spaces(string: clause, separator: '\(')
|
32
|
+
clause = remove_spaces(string: clause, separator: '\)')
|
33
|
+
clause = remove_spaces(string: clause, separator: '&')
|
34
|
+
clause = remove_spaces(string: clause, separator: '\|')
|
35
|
+
clause = remove_spaces(string: clause, separator: '!')
|
36
|
+
@outcome_clause_hash[outcome.to_s] = clause.strip
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def size
|
41
|
+
@outcome_clause_hash.size
|
42
|
+
end
|
43
|
+
|
44
|
+
# /**
|
45
|
+
# * Removes the leading and trailing spaces of rule tokens.
|
46
|
+
# *
|
47
|
+
# * @param string rule clause.
|
48
|
+
# * @param separator rule clause token.
|
49
|
+
# */
|
50
|
+
def remove_spaces(string: '', separator: '')
|
51
|
+
string.gsub(Regexp.new("\\s*#{separator} \\s*"), separator)
|
52
|
+
end
|
53
|
+
|
54
|
+
# /**
|
55
|
+
# * @return the actionList
|
56
|
+
# */
|
57
|
+
def outcomes
|
58
|
+
@outcome_clause_hash.keys
|
59
|
+
end
|
60
|
+
|
61
|
+
# /**
|
62
|
+
# * @param action action which rule we want to retrieve.
|
63
|
+
# * @return the actionToRuleClauses
|
64
|
+
# */
|
65
|
+
def clause(outcome: '')
|
66
|
+
@outcome_clause_hash[outcome]
|
67
|
+
end
|
68
|
+
|
69
|
+
# /**
|
70
|
+
# * Get rule result give a fixed list of scenario tokens. Used for fixed
|
71
|
+
# * list.
|
72
|
+
# *
|
73
|
+
# * @param scenario of interest.
|
74
|
+
# * @return the actionToRuleClauses
|
75
|
+
# */
|
76
|
+
def rule_outcome(scenario: [])
|
77
|
+
scenario_string = scenario.to_s
|
78
|
+
anded_scenario = scenario_string[1..-2].gsub(/,\s/, '&')
|
79
|
+
|
80
|
+
@outcome_clause_hash.each do |key, clause|
|
81
|
+
or_list_clause = clause.split('\|').map(&:strip)
|
82
|
+
return key if or_list_clause.include?(anded_scenario)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# /**
|
87
|
+
# * @see {@link Object#toString()}
|
88
|
+
# * @return String representation of this instance.
|
89
|
+
# */
|
90
|
+
def to_s
|
91
|
+
@outcome_clause_hash.to_s
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,273 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'operator'
|
4
|
+
require_relative 'logic_helper'
|
5
|
+
require 'rast/converters/int_converter'
|
6
|
+
require 'rast/converters/bool_converter'
|
7
|
+
require 'rast/converters/str_converter'
|
8
|
+
|
9
|
+
# Evaluates the rules.
|
10
|
+
class RuleEvaluator
|
11
|
+
include LogicHelper
|
12
|
+
|
13
|
+
NOT = Operator.new(name: 'not', symbol: '!', precedence: 100)
|
14
|
+
AND = Operator.new(name: 'and', symbol: '&', precedence: 2)
|
15
|
+
OR = Operator.new(name: 'or', symbol: '|', precedence: 1)
|
16
|
+
|
17
|
+
OPERATORS = [NOT, AND, OR].freeze
|
18
|
+
OPERATORS_CONCAT = OPERATORS.map(&:to_s).join
|
19
|
+
|
20
|
+
# the "false" part of the "false[1]"
|
21
|
+
RE_TOKEN_BODY = /^.+(?=\[)/.freeze
|
22
|
+
RE_TOKENS = /([!|)(&])|([a-zA-Z\s0-9]+\[\d\])/.freeze
|
23
|
+
|
24
|
+
def self.operator_from_symbol(symbol: nil)
|
25
|
+
OPERATORS.find { |operator| operator.symbol == symbol }
|
26
|
+
end
|
27
|
+
|
28
|
+
DEFAULT_CONVERT_HASH = {
|
29
|
+
Integer => IntConverter.new,
|
30
|
+
TrueClass => BoolConverter.new,
|
31
|
+
FalseClass => BoolConverter.new,
|
32
|
+
String => StrConverter.new
|
33
|
+
}.freeze
|
34
|
+
|
35
|
+
# /** @param pConverterList list of rule token converters. */
|
36
|
+
def initialize(converters: [])
|
37
|
+
@converters = converters
|
38
|
+
|
39
|
+
@stack_operations = []
|
40
|
+
@stack_rpn = []
|
41
|
+
@stack_answer = []
|
42
|
+
end
|
43
|
+
|
44
|
+
# /**
|
45
|
+
# * Parses the math expression (complicated formula) and stores the result.
|
46
|
+
# *
|
47
|
+
# * @param pExpression <code>String</code> input expression (logical
|
48
|
+
# * expression formula)
|
49
|
+
# * @since 0.3.0
|
50
|
+
# */
|
51
|
+
def parse(expression: '')
|
52
|
+
# /* cleaning stacks */
|
53
|
+
@stack_operations.clear
|
54
|
+
@stack_rpn.clear
|
55
|
+
|
56
|
+
# /* splitting input string into tokens */
|
57
|
+
tokens = expression.split(RE_TOKENS).reject(&:empty?)
|
58
|
+
|
59
|
+
# /* loop for handling each token - shunting-yard algorithm */
|
60
|
+
tokens.each { |token| shunt_internal(token: token.strip) }
|
61
|
+
|
62
|
+
@stack_rpn << @stack_operations.pop while @stack_operations.any?
|
63
|
+
@stack_rpn.reverse!
|
64
|
+
end
|
65
|
+
|
66
|
+
# /**
|
67
|
+
# * Evaluates once parsed math expression with "var" variable included.
|
68
|
+
# *
|
69
|
+
# * @param scenario List of values to evaluate against the rule expression.
|
70
|
+
# * @param rule_token_convert mapping of rule tokens to converter.
|
71
|
+
# * @return <code>String</code> representation of the result
|
72
|
+
# */
|
73
|
+
def evaluate(scenario: [], rule_token_convert: {})
|
74
|
+
# /* check if is there something to evaluate */
|
75
|
+
if @stack_rpn.empty?
|
76
|
+
true
|
77
|
+
elsif @stack_rpn.size == 1
|
78
|
+
evaluate_one_rpn(scenario: scenario)
|
79
|
+
else
|
80
|
+
evaluate_multi_rpn(
|
81
|
+
scenario: scenario,
|
82
|
+
rule_token_convert: rule_token_convert
|
83
|
+
)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# /**
|
88
|
+
# * @param rule_token_convert token to converter map.
|
89
|
+
# * @param default_converter default converter to use.
|
90
|
+
# */
|
91
|
+
def next_value(rule_token_convert: {}, default_converter: nil)
|
92
|
+
subscript = -1
|
93
|
+
retval = []
|
94
|
+
value = @stack_answer.pop&.strip
|
95
|
+
if TRUE != value && FALSE != value
|
96
|
+
subscript = extract_subscript(token: value.to_s)
|
97
|
+
value_str = value.to_s.strip
|
98
|
+
if subscript > -1
|
99
|
+
converter = @converters[subscript]
|
100
|
+
value = converter.convert(value_str[/^.+(?=\[)/])
|
101
|
+
else
|
102
|
+
value = if rule_token_convert.nil? ||
|
103
|
+
rule_token_convert[value_str].nil?
|
104
|
+
default_converter.convert(value_str)
|
105
|
+
else
|
106
|
+
rule_token_convert[value_str].convert(value_str)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
retval << subscript
|
111
|
+
retval << value
|
112
|
+
retval
|
113
|
+
end
|
114
|
+
|
115
|
+
# /** @param token token. */
|
116
|
+
def shunt_internal(token: '')
|
117
|
+
if open_bracket?(token: token)
|
118
|
+
@stack_operations << token
|
119
|
+
elsif close_bracket?(token: token)
|
120
|
+
while @stack_operations.any? &&
|
121
|
+
!open_bracket?(token: @stack_operations.last.strip)
|
122
|
+
@stack_rpn << @stack_operations.pop
|
123
|
+
end
|
124
|
+
@stack_operations.pop
|
125
|
+
elsif operator?(token: token)
|
126
|
+
while !@stack_operations.empty? &&
|
127
|
+
operator?(token: @stack_operations.last.strip) &&
|
128
|
+
precedence(symbol_char: token[0]) <=
|
129
|
+
precedence(symbol_char: @stack_operations.last.strip[0])
|
130
|
+
@stack_rpn << @stack_operations.pop
|
131
|
+
end
|
132
|
+
@stack_operations << token
|
133
|
+
else
|
134
|
+
@stack_rpn << token
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
# /**
|
141
|
+
# * Returns value of 'n' if rule token ends with '[n]'. where 'n' is the
|
142
|
+
# * variable group index.
|
143
|
+
# *
|
144
|
+
# * @param string token to check for subscript.
|
145
|
+
# */
|
146
|
+
def extract_subscript(token: '')
|
147
|
+
subscript = token[/\[(\d+)\]$/, 1]
|
148
|
+
subscript.nil? ? -1 : subscript.to_i
|
149
|
+
end
|
150
|
+
|
151
|
+
# /**
|
152
|
+
# * @param scenario List of values to evaluate against the rule expression.
|
153
|
+
# * @param rule_token_convert token to converter map.
|
154
|
+
# */
|
155
|
+
def evaluate_multi_rpn(scenario: [], rule_token_convert: {})
|
156
|
+
# /* clean answer stack */
|
157
|
+
@stack_answer.clear
|
158
|
+
|
159
|
+
# /* get the clone of the RPN stack for further evaluating */
|
160
|
+
stack_rpn_clone = Marshal.load(Marshal.dump(@stack_rpn))
|
161
|
+
|
162
|
+
# /* evaluating the RPN expression */
|
163
|
+
|
164
|
+
# binding.pry
|
165
|
+
|
166
|
+
while stack_rpn_clone.any?
|
167
|
+
token = stack_rpn_clone.pop.strip
|
168
|
+
if operator?(token: token)
|
169
|
+
if NOT.symbol == token
|
170
|
+
evaluate_multi_not(scenario: scenario)
|
171
|
+
else
|
172
|
+
evaluate_multi(
|
173
|
+
scenario: scenario,
|
174
|
+
rule_token_convert: rule_token_convert,
|
175
|
+
operator: RuleEvaluator.operator_from_symbol(symbol: token[0])
|
176
|
+
)
|
177
|
+
end
|
178
|
+
else
|
179
|
+
@stack_answer << token
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
raise 'Some operator is missing' if @stack_answer.size > 1
|
184
|
+
|
185
|
+
@stack_answer.pop[1..]
|
186
|
+
end
|
187
|
+
|
188
|
+
# /**
|
189
|
+
# * @param scenario List of values to evaluate against the rule expression.
|
190
|
+
# * @param rule_token_convert token to converter map.
|
191
|
+
# * @param operator OR/AND.
|
192
|
+
# */
|
193
|
+
def evaluate_multi(scenario: [], rule_token_convert: {}, operator: nil)
|
194
|
+
default_converter = DEFAULT_CONVERT_HASH[scenario.first.class]
|
195
|
+
|
196
|
+
# binding.pry
|
197
|
+
|
198
|
+
left_arr = next_value(
|
199
|
+
rule_token_convert: rule_token_convert,
|
200
|
+
default_converter: default_converter
|
201
|
+
)
|
202
|
+
|
203
|
+
right_arr = next_value(
|
204
|
+
rule_token_convert: rule_token_convert,
|
205
|
+
default_converter: default_converter
|
206
|
+
)
|
207
|
+
|
208
|
+
answer = send(
|
209
|
+
"perform_logical_#{operator.name}",
|
210
|
+
scenario: scenario,
|
211
|
+
left_subscript: left_arr[0],
|
212
|
+
right_subscript: right_arr[0],
|
213
|
+
left: left_arr[1],
|
214
|
+
right: right_arr[1]
|
215
|
+
)
|
216
|
+
|
217
|
+
@stack_answer << if answer[0] == '|'
|
218
|
+
answer
|
219
|
+
else
|
220
|
+
"|#{answer}"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# /**
|
225
|
+
# * @param scenario List of values to evaluate against the rule expression.
|
226
|
+
# */
|
227
|
+
def evaluate_multi_not(scenario: [])
|
228
|
+
left = @stack_answer.pop.strip
|
229
|
+
|
230
|
+
# binding.pry
|
231
|
+
|
232
|
+
answer = if LogicHelper::TRUE == left
|
233
|
+
LogicHelper::FALSE
|
234
|
+
elsif LogicHelper::FALSE == left
|
235
|
+
LogicHelper::TRUE
|
236
|
+
else
|
237
|
+
subscript = extract_subscript(token: left)
|
238
|
+
if subscript.negative?
|
239
|
+
(!scenario.include?(left)).to_s
|
240
|
+
else
|
241
|
+
default_converter = DEFAULT_CONVERT_HASH[scenario.first.class]
|
242
|
+
converted = default_converter.convert(left[RE_TOKEN_BODY])
|
243
|
+
(scenario[subscript] == converted).to_s
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
@stack_answer << if answer[0] == '|'
|
248
|
+
answer
|
249
|
+
else
|
250
|
+
"|#{answer}"
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# /** @param scenario to evaluate against the rule expression. */
|
255
|
+
def evaluate_one_rpn(scenario: [])
|
256
|
+
single = @stack_rpn.last
|
257
|
+
subscript = extract_subscript(token: single)
|
258
|
+
if subscript > -1
|
259
|
+
default_converter = DEFAULT_CONVERT_HASH[scenario.first.class]
|
260
|
+
scenario[subscript] == default_converter.convert(single[RE_TOKEN_BODY])
|
261
|
+
else
|
262
|
+
scenario.include? single
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def operator?(token: '')
|
267
|
+
!token.nil? && OPERATORS.map(&:symbol).include?(token)
|
268
|
+
end
|
269
|
+
|
270
|
+
def precedence(symbol_char: '')
|
271
|
+
RuleEvaluator.operator_from_symbol(symbol: symbol_char).precedence
|
272
|
+
end
|
273
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'rule_evaluator'
|
4
|
+
|
5
|
+
# undoc
|
6
|
+
class RuleProcessor
|
7
|
+
# /**
|
8
|
+
# * @param scenario current scenario.
|
9
|
+
# * @param caseFixture current test case fixture.
|
10
|
+
# */
|
11
|
+
def evaluate(scenario: [], fixture: nil)
|
12
|
+
# if scenario.empty? || fixture.nil?
|
13
|
+
# raise 'Scenario or fixture cannot be nil.'
|
14
|
+
# end
|
15
|
+
|
16
|
+
fixture[:spec].rule.outcomes.inject([]) do |retval, outcome|
|
17
|
+
process_outcome(
|
18
|
+
scenario: scenario,
|
19
|
+
fixture: fixture,
|
20
|
+
list: retval,
|
21
|
+
outcome: outcome
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def process_outcome(scenario: [], fixture: nil, list: [], outcome: '')
|
29
|
+
spec = fixture[:spec]
|
30
|
+
|
31
|
+
clause = spec.rule.clause(outcome: outcome)
|
32
|
+
rule_evaluator = RuleEvaluator.new(converters: spec.converters)
|
33
|
+
|
34
|
+
rule_evaluator.parse(expression: clause)
|
35
|
+
|
36
|
+
list << rule_evaluator.evaluate(
|
37
|
+
scenario: scenario,
|
38
|
+
rule_token_convert: fixture[:converter_hash]
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rast/assert'
|
4
|
+
require_relative 'rule_processor'
|
5
|
+
|
6
|
+
# Validates rules
|
7
|
+
class RuleValidator
|
8
|
+
def validate(scenario: [], fixture: {})
|
9
|
+
rule_result = RuleProcessor.new.evaluate(
|
10
|
+
scenario: scenario,
|
11
|
+
fixture: fixture
|
12
|
+
)
|
13
|
+
|
14
|
+
spec = fixture[:spec]
|
15
|
+
rule = spec.rule
|
16
|
+
|
17
|
+
single_result = rule.size == 1
|
18
|
+
if single_result
|
19
|
+
next_result = rule_result.first
|
20
|
+
outcome = rule.outcomes.first
|
21
|
+
binary_outcome(outcome: outcome, spec: spec, expected: next_result)
|
22
|
+
else
|
23
|
+
validate_multi(scenario: scenario, spec: spec, rule_result: rule_result)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def validate_multi(scenario: [], spec: nil, rule_result: [])
|
30
|
+
# binding.pry
|
31
|
+
|
32
|
+
matched_outputs = []
|
33
|
+
match_count = 0
|
34
|
+
|
35
|
+
rule_result.map { |result| result == 'true' }.each_with_index do |result, i|
|
36
|
+
next unless result
|
37
|
+
|
38
|
+
match_count += 1
|
39
|
+
matched_outputs << spec.rule.outcomes[i]
|
40
|
+
end
|
41
|
+
assert("Scenario must fall into a unique rule output/clause:
|
42
|
+
#{scenario} , matched: #{matched_outputs}") { match_count == 1 }
|
43
|
+
|
44
|
+
matched_outputs.first
|
45
|
+
end
|
46
|
+
|
47
|
+
def binary_outcome(outcome: '', spec: nil, expected: false)
|
48
|
+
is_positive = spec.pair.keys.include?(outcome)
|
49
|
+
if is_positive
|
50
|
+
expected == 'true' ? outcome : opposite(outcome: outcome, spec: spec)
|
51
|
+
else
|
52
|
+
expected == 'true' ? opposite(outcome: outcome, spec: spec) : outcome
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def opposite(outcome: '', spec: nil)
|
57
|
+
if spec.pair.keys.include? outcome
|
58
|
+
spec.pair[outcome]
|
59
|
+
else
|
60
|
+
spec.pair_reversed[outcome]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'parameter_generator'
|
4
|
+
|
5
|
+
# Main DSL. This is the entry point of the test when running a spec.
|
6
|
+
class SpecDSL
|
7
|
+
attr_accessor :subject, :rspec_methods, :execute_block, :prepare_block,
|
8
|
+
:transients, :outcomes, :fixtures
|
9
|
+
|
10
|
+
def initialize(subject: nil, fixtures: [], &block)
|
11
|
+
@subject = subject
|
12
|
+
@fixtures = fixtures
|
13
|
+
|
14
|
+
@transients = []
|
15
|
+
@result = nil
|
16
|
+
@rspec_methods = []
|
17
|
+
|
18
|
+
instance_eval(&block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def result(outcome)
|
22
|
+
@outcome = outcome.to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
def method_missing(method_name_symbol, *args, &block)
|
26
|
+
return super if method_name_symbol == :to_ary
|
27
|
+
|
28
|
+
@rspec_methods << {
|
29
|
+
name: method_name_symbol,
|
30
|
+
args: args.first,
|
31
|
+
block: block
|
32
|
+
}
|
33
|
+
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
def prepare(&block)
|
38
|
+
@prepare_block = block
|
39
|
+
@transients
|
40
|
+
end
|
41
|
+
|
42
|
+
def execute(&block)
|
43
|
+
@execute_block = block
|
44
|
+
|
45
|
+
@fixtures.sort_by! { |fixture| fixture[:expected_outcome] }
|
46
|
+
generate_rspecs
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def generate_rspecs
|
52
|
+
spec = @fixtures.first[:spec]
|
53
|
+
main_scope = self
|
54
|
+
|
55
|
+
RSpec.describe "#{@subject.class}: #{spec.description}" do
|
56
|
+
main_scope.fixtures.each do |fixture|
|
57
|
+
generate_rspec(
|
58
|
+
scope: main_scope,
|
59
|
+
scenario: fixture[:scenario],
|
60
|
+
expected: fixture[:expected_outcome]
|
61
|
+
)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def generate_rspec(scope: nil, scenario: {}, expected: '')
|
68
|
+
spec_params = scenario.keys.inject('') do |output, key|
|
69
|
+
output += ', ' unless output == ''
|
70
|
+
output + "#{key}: #{scenario[key]}"
|
71
|
+
end
|
72
|
+
|
73
|
+
it "[#{expected}]=[#{spec_params}]" do
|
74
|
+
block_params = scenario.values
|
75
|
+
scope.prepare_block&.call(*block_params) unless scope.rspec_methods.any?
|
76
|
+
|
77
|
+
while scope.rspec_methods.any?
|
78
|
+
first_meth = scope.rspec_methods.shift
|
79
|
+
second_meth = scope.rspec_methods.shift
|
80
|
+
if first_meth[:name] == :allow && second_meth[:name] == :receive
|
81
|
+
allow(scope.subject)
|
82
|
+
.to receive(second_meth[:args], &second_meth[:block])
|
83
|
+
end
|
84
|
+
scope.rspec_methods.shift
|
85
|
+
end
|
86
|
+
|
87
|
+
actual = scope.execute_block.call(*block_params).to_s
|
88
|
+
|
89
|
+
expect(actual).to eq(expected)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# DSL Entry Point
|
94
|
+
def spec(subject: nil, fixtures: [], &block)
|
95
|
+
SpecDSL.new(subject: subject, fixtures: fixtures, &block)
|
96
|
+
end
|
data/lib/rast/version.rb
ADDED
data/lib/rast.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspec'
|
4
|
+
require_relative 'rast/parameter_generator'
|
5
|
+
require_relative 'rast/spec_dsl'
|
6
|
+
|
7
|
+
# Main DSL. This is the entry point of the test when running a spec.
|
8
|
+
class Rast
|
9
|
+
# RSpec All scenario test
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
# >> Hola.hi("spanish")
|
13
|
+
# => hola mundo
|
14
|
+
#
|
15
|
+
# Arguments:
|
16
|
+
# language: (String)
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
alias global_spec spec
|
21
|
+
|
22
|
+
def initialize(rasted_class, &block)
|
23
|
+
@rasted_class = rasted_class
|
24
|
+
@subject = rasted_class.new
|
25
|
+
|
26
|
+
spec_path = caller[2][/spec.*?\.rb/]
|
27
|
+
yaml_path = spec_path.gsub(/(\w+).rb/, 'rast/\\1.yml')
|
28
|
+
|
29
|
+
@generator = ParameterGenerator.new(yaml_path: yaml_path)
|
30
|
+
|
31
|
+
instance_eval(&block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def spec(id, &block)
|
35
|
+
global_spec(
|
36
|
+
subject: @subject,
|
37
|
+
fixtures: @generator.generate_fixtures(spec_id: id),
|
38
|
+
&block
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# DSL Entry Point
|
44
|
+
def rast(rasted_class, &block)
|
45
|
+
Rast.new(rasted_class, &block)
|
46
|
+
end
|
data/rast.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/rast/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'rast'
|
7
|
+
spec.version = Rast::VERSION
|
8
|
+
spec.authors = ['Royce Remulla']
|
9
|
+
spec.email = ['royce.com@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = 'RSpec AST - All Scenario Testing'
|
12
|
+
spec.description = 'Extends RSpec functionality by using the catch-all-scenario testing (CAST) principle.'
|
13
|
+
spec.homepage = 'https://github.com/roycetech/rast'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
|
16
|
+
|
17
|
+
# spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
|
18
|
+
# spec.metadata['homepage_uri'] = spec.homepage
|
19
|
+
# spec.metadata['source_code_uri'] = "TODO: Put your gem's public repo URL here."
|
20
|
+
# spec.metadata['changelog_uri'] = "TODO: Put your gem's CHANGELOG.md URL here."
|
21
|
+
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
23
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
25
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
26
|
+
end
|
27
|
+
spec.bindir = 'exe'
|
28
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
29
|
+
spec.require_paths = %w[lib]
|
30
|
+
end
|