rast 0.1.0.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module Rast
2
+ VERSION = '0.1.0.pre'.freeze
3
+ end
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
@@ -0,0 +1,18 @@
1
+ ---
2
+ specs:
3
+ spec_key:
4
+ description: Spec Description
5
+
6
+ variables:
7
+ param1:
8
+ - false
9
+ - true
10
+
11
+ converters:
12
+ - BoolConverter
13
+
14
+ rules:
15
+ 'true': 'true[0]'
16
+
17
+ pair:
18
+ 'true': 'false'
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