rast 0.1.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- 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
|