rast 0.15.1 → 0.18.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/Getting-Started.md +139 -0
- data/examples/logic_checker.rb +8 -0
- data/examples/lohika.rb +27 -0
- data/examples/positive.rb +0 -2
- data/examples/positive2.rb +10 -0
- data/examples/quiz_module.rb +0 -44
- data/lib/rast/converters/str_converter.rb +1 -1
- data/lib/rast/parameter_generator.rb +31 -17
- data/lib/rast/rast_spec.rb +8 -10
- data/lib/rast/rules/logic_helper.rb +17 -13
- data/lib/rast/rules/rule.rb +5 -30
- data/lib/rast/rules/rule_evaluator.rb +28 -36
- data/lib/rast/rules/rule_validator.rb +8 -16
- data/lib/rast/spec_dsl.rb +26 -27
- data/lib/rast.rb +4 -4
- data/lib/template_spec.yml +6 -8
- data/rast.gemspec +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 77e02b108231f3d36dd5e490e9a0ab9cf1334ebb53ae6ffce93cbec2c124423b
|
4
|
+
data.tar.gz: a738c7603f64ec75eff5a13c87fa472d147c1262efb0340d2b500d76fa590826
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 88fc71ee6789773bd21b743c9e8bafbabc07654fbc2494f65e7b855930f32f686222ffe0ada7ecafeab1a828b9f45f8a9d94debf5ca7c87603edfbf39e10e4b3
|
7
|
+
data.tar.gz: '01409ef86dc48e884d0a4afd8cfa6113c054a4bd70aa19e8d5ab34d0fac5f50119f4e26924634dea6c8e710ba131fc1cfaf79ae07a884a40a48e8e6be2a84346'
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
# Change log
|
2
2
|
|
3
|
+
- 0.18.0 - [feature] xspec to skip a test
|
4
|
+
- [feature] Added include rule to isolate scenarios.
|
5
|
+
- [feature] Asterisk can be used as token character.
|
6
|
+
- [bug] Duplicate outcome wasn't detected when there's default config.
|
7
|
+
- [enhancement] Early return for 'and' and 'or' operators.
|
8
|
+
|
9
|
+
- 0.17.0 - Allow default outcome for unmatched scenarios.
|
10
|
+
- 0.16.0 - Allow isolation of scenarios via include clause.
|
11
|
+
- 0.15.4 - Fix bug on logic checker for not operation.
|
12
|
+
- 0.15.3 - Fix use of boolean as key to the config.
|
13
|
+
- Fix allow factory to be called inside both execute and prepare block.
|
14
|
+
- 0.15.2 - Fix handling of nil token, and big integers.
|
3
15
|
- 0.15.1 - Fix converters when variable is multi typed.
|
4
16
|
- 0.14.0 - Introduced an else config as substitute for pair.
|
5
17
|
- 0.13.0 - Make pair in config optional for boolean result.
|
data/Getting-Started.md
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
# Getting Started
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
## Tutorials
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
### Writing a simple test.
|
10
|
+
|
11
|
+
Suppose we want to create a class that checks if a number is a positive number or not.
|
12
|
+
|
13
|
+
#### Step 1: Defining how to exercise the system under test.
|
14
|
+
|
15
|
+
Start by creating a spec file inside your `spec` folder, let's call it `positive_spec.rb`
|
16
|
+
|
17
|
+
- spec
|
18
|
+
- positive_spec.rb
|
19
|
+
|
20
|
+
|
21
|
+
`positive_spec.rb` would look like this:
|
22
|
+
|
23
|
+
```
|
24
|
+
require 'rast'
|
25
|
+
|
26
|
+
rast Positive do
|
27
|
+
spec 'Is Positive Example' do
|
28
|
+
execute { |number| subject.positive?(number) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
- On line: 1, the library is required:
|
34
|
+
|
35
|
+
```
|
36
|
+
require 'rast'
|
37
|
+
```
|
38
|
+
|
39
|
+
- On line: 3, invoke the DSL:
|
40
|
+
|
41
|
+
```
|
42
|
+
rast Positive do
|
43
|
+
```
|
44
|
+
`Positive` is the fully qualified name of the class or the module to be tested.
|
45
|
+
|
46
|
+
- On line: 4, define the first spec:
|
47
|
+
|
48
|
+
```
|
49
|
+
spec 'Is Positive Example' do
|
50
|
+
```
|
51
|
+
|
52
|
+
`'Is Positive Example'` is a descriptive identifier for the test. It can be the name of the method like `#positive?`.
|
53
|
+
|
54
|
+
- On line: 5, define the `execute` block..
|
55
|
+
|
56
|
+
```
|
57
|
+
execute { |number| subject.positive?(number) }
|
58
|
+
```
|
59
|
+
|
60
|
+
There is a lot going on here.<br>
|
61
|
+
- Inside the block, we define how our subject will be invoked. The `positive?` method is invoked on the `subject`
|
62
|
+
accepting a parameter.
|
63
|
+
- Block parameter `number` will contain the test scenario we want to execute. More on this later. Just know that whichever values are passed here, the `subject` has to be able to handle it correctly.
|
64
|
+
- `subject` is an instance of the module or class that we defined on line: 3.
|
65
|
+
- And lastly, `execute` block expects a return value that will be verified when the spec is run.
|
66
|
+
|
67
|
+
#### Step 2: Defining outcomes and the variables that affects the test.
|
68
|
+
|
69
|
+
For the sake of simplicity, we will define the variables and outcomes in a separate yaml file, mainly because the next
|
70
|
+
steps are purely configurations. Using yaml is completely optional.
|
71
|
+
|
72
|
+
Create a folder `rast` in the same level as the spec file:
|
73
|
+
|
74
|
+
- spec
|
75
|
+
- rast
|
76
|
+
|
77
|
+
Create a yaml file with the same name as the spec, but with `.yml` extension.
|
78
|
+
|
79
|
+
`spec/rast/positive_spec.yml` would contain:
|
80
|
+
|
81
|
+
```
|
82
|
+
specs:
|
83
|
+
Is Positive Exaple:
|
84
|
+
variables: {number: [-1, 0, 1]}
|
85
|
+
outcomes: {true: 1}
|
86
|
+
```
|
87
|
+
|
88
|
+
- On line: 1 is the root configuration element `specs` that will contain one or more specs.
|
89
|
+
- On line: 2 `Is Positive Example` is the spec identifier, this must match what we've defined on `positive_spec.rb:4`
|
90
|
+
- On line: 3, The variables that affect the SUT are defined, in this case there is only one variable called `number`,
|
91
|
+
which has 3 different possibilities `-1`, `0`, and `1`.
|
92
|
+
- On line: 4, This is where outcome to rule mapping is defined.
|
93
|
+
|
94
|
+
### The implementation file
|
95
|
+
|
96
|
+
positive.rb will have:
|
97
|
+
|
98
|
+
```
|
99
|
+
class Positive
|
100
|
+
def positive?(number)
|
101
|
+
number > 0
|
102
|
+
end
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
## How To's
|
107
|
+
|
108
|
+
### Use token subscript when there's a variable token clash.
|
109
|
+
|
110
|
+
In cases where multiple variables, `left` and `right`, has the same set of tokens `false` and `true`:
|
111
|
+
|
112
|
+
```
|
113
|
+
variables:
|
114
|
+
left: [false, true]
|
115
|
+
right: [false, true]
|
116
|
+
```
|
117
|
+
|
118
|
+
We need a way to uniquely identify the tokens in the `outcomes` configuration. We can do so by using a subscript in the format: `token[n]`
|
119
|
+
For Example:
|
120
|
+
|
121
|
+
```
|
122
|
+
outcomes: {true: 'true[0] & true[1]'}
|
123
|
+
```
|
124
|
+
|
125
|
+
The subscript `0` would refer to the `true` token in the `left` variable and subscript `1` would refer to the `true`
|
126
|
+
token in the `right` variable.
|
127
|
+
|
128
|
+
|
129
|
+
## References
|
130
|
+
|
131
|
+
### Outcomes
|
132
|
+
|
133
|
+
|
134
|
+
## Current Limitations.
|
135
|
+
|
136
|
+
|
137
|
+
|
138
|
+
|
139
|
+
|
data/examples/logic_checker.rb
CHANGED
@@ -20,4 +20,12 @@ class LogicChecker
|
|
20
20
|
def or(argument1, argument2)
|
21
21
|
argument1 || argument2
|
22
22
|
end
|
23
|
+
|
24
|
+
# Perform logical XOR operation on two arguments.
|
25
|
+
#
|
26
|
+
# @param argument1 first argument of Boolean type.
|
27
|
+
# @param argument2 second argument of Boolean type.
|
28
|
+
def xor(argument1, argument2)
|
29
|
+
argument1 && !argument2 || !argument1 && argument2
|
30
|
+
end
|
23
31
|
end
|
data/examples/lohika.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# LogicChecker ported from Java
|
4
|
+
#
|
5
|
+
# @author Royce Remulla
|
6
|
+
#
|
7
|
+
class Lohika
|
8
|
+
# Perform logical AND operation on two arguments.
|
9
|
+
#
|
10
|
+
# @param argument1 first argument of Boolean type.
|
11
|
+
# @param argument2 second argument of Boolean type.
|
12
|
+
def at(argument1, argument2)
|
13
|
+
return :oo if argument1 == 'oo' && argument2 == 'oo'
|
14
|
+
|
15
|
+
:hindi
|
16
|
+
end
|
17
|
+
|
18
|
+
# Perform logical OR operation on two arguments.
|
19
|
+
#
|
20
|
+
# @param argument1 first argument of Boolean type.
|
21
|
+
# @param argument2 second argument of Boolean type.
|
22
|
+
def o(argument1, argument2)
|
23
|
+
return :oo if argument1 == 'oo' || argument2 == 'oo'
|
24
|
+
|
25
|
+
:hindi
|
26
|
+
end
|
27
|
+
end
|
data/examples/positive.rb
CHANGED
data/examples/quiz_module.rb
CHANGED
@@ -16,21 +16,6 @@ module QuizModule
|
|
16
16
|
Regexp::IGNORECASE
|
17
17
|
)
|
18
18
|
|
19
|
-
def quiz?(front = nil, back = nil)
|
20
|
-
initialized = [true, false].include? @quiz
|
21
|
-
@quiz = quiz_choice?(back) || quiz_fill_blank?(front) unless initialized
|
22
|
-
@quiz
|
23
|
-
end
|
24
|
-
|
25
|
-
def quiz_choice?(back = nil)
|
26
|
-
initialized = [true, false].include? @choice
|
27
|
-
unless initialized
|
28
|
-
@choice = quiz_single_choice?(back) || quiz_multi_choice?(back)
|
29
|
-
end
|
30
|
-
|
31
|
-
@choice
|
32
|
-
end
|
33
|
-
|
34
19
|
def quiz_multi_choice?(back)
|
35
20
|
if back.is_a?(Array) && back.any?
|
36
21
|
back.each { |element| return false unless element[RE_QUIZ_OPTION_M] }
|
@@ -46,33 +31,4 @@ module QuizModule
|
|
46
31
|
end
|
47
32
|
false
|
48
33
|
end
|
49
|
-
|
50
|
-
def quiz_single_choice?(back)
|
51
|
-
if back.is_a?(Array) && back.any?
|
52
|
-
back.each { |element| return false unless element[RE_QUIZ_OPTION_S] }
|
53
|
-
|
54
|
-
# Find at least 1 selected answer
|
55
|
-
selected = back.filter do |element|
|
56
|
-
true if element[RE_SELECTED]
|
57
|
-
end
|
58
|
-
return false unless selected.size == 1
|
59
|
-
|
60
|
-
@quiz = true
|
61
|
-
@front_only = true
|
62
|
-
return true
|
63
|
-
end
|
64
|
-
false
|
65
|
-
end
|
66
|
-
|
67
|
-
def quiz_fill_blank?(front)
|
68
|
-
return true if (front.size == 1) && front.first[/_____/]
|
69
|
-
|
70
|
-
false
|
71
|
-
end
|
72
|
-
|
73
|
-
private
|
74
|
-
|
75
|
-
def choices_with_header(back)
|
76
|
-
|
77
|
-
end
|
78
34
|
end
|
@@ -6,8 +6,6 @@ require 'rast/rules/rule'
|
|
6
6
|
require 'rast/rules/rule_evaluator'
|
7
7
|
require 'rast/rules/rule_validator'
|
8
8
|
|
9
|
-
require 'rast/converters/default_converter'
|
10
|
-
|
11
9
|
# Generates the test parameters.
|
12
10
|
class ParameterGenerator
|
13
11
|
# Allow access so yaml-less can build the config via dsl.
|
@@ -24,7 +22,11 @@ class ParameterGenerator
|
|
24
22
|
spec_config = @specs_config[spec_id]
|
25
23
|
|
26
24
|
spec_config[:description] = spec_id
|
25
|
+
|
26
|
+
# Keep, for backwards compatibility
|
27
27
|
spec_config['rules'] ||= spec_config['outcomes']
|
28
|
+
spec_config['default'] ||= spec_config['else']
|
29
|
+
|
28
30
|
spec = instantiate_spec(spec_config)
|
29
31
|
|
30
32
|
list = []
|
@@ -43,13 +45,25 @@ class ParameterGenerator
|
|
43
45
|
private
|
44
46
|
|
45
47
|
def valid_case?(scenario, spec)
|
46
|
-
return true if spec.exclude_clause.nil?
|
48
|
+
return true if spec.exclude_clause.nil? && spec.include_clause.nil?
|
47
49
|
|
48
|
-
exclude_clause = Rule.sanitize(clause: spec.exclude_clause)
|
49
50
|
rule_evaluator = RuleEvaluator.new(converters: spec.converters)
|
50
|
-
rule_evaluator.parse(expression: exclude_clause)
|
51
51
|
|
52
|
-
|
52
|
+
include_result = true
|
53
|
+
unless spec.exclude_clause.nil?
|
54
|
+
exclude_clause = Rule.sanitize(clause: spec.exclude_clause)
|
55
|
+
rule_evaluator.parse(expression: exclude_clause)
|
56
|
+
evaluate_result = rule_evaluator.evaluate(scenario: scenario, rule_token_convert: spec.token_converter)
|
57
|
+
include_result = evaluate_result == 'false'
|
58
|
+
end
|
59
|
+
|
60
|
+
return include_result if spec.include_clause.nil? || !include_result
|
61
|
+
|
62
|
+
include_clause = Rule.sanitize(clause: spec.include_clause)
|
63
|
+
rule_evaluator.parse(expression: include_clause)
|
64
|
+
include_result = rule_evaluator.evaluate(scenario: scenario, rule_token_convert: spec.token_converter) == "true"
|
65
|
+
|
66
|
+
include_result
|
53
67
|
end
|
54
68
|
|
55
69
|
# add all fixtures to the list.
|
@@ -57,7 +71,10 @@ class ParameterGenerator
|
|
57
71
|
validator = RuleValidator.new
|
58
72
|
|
59
73
|
scenarios.each do |scenario|
|
60
|
-
|
74
|
+
good = valid_case?(scenario, spec)
|
75
|
+
# p "#{good} #{scenario}"
|
76
|
+
|
77
|
+
next unless good
|
61
78
|
|
62
79
|
list << build_param(validator, scenario, spec)
|
63
80
|
end
|
@@ -109,7 +126,8 @@ class ParameterGenerator
|
|
109
126
|
spec = RastSpec.new(
|
110
127
|
description: spec_config[:description],
|
111
128
|
variables: spec_config['variables'],
|
112
|
-
rule: Rule.new(rules: spec_config['rules'])
|
129
|
+
rule: Rule.new(rules: spec_config['rules']),
|
130
|
+
default_outcome: spec_config['default'] || spec_config['else']
|
113
131
|
)
|
114
132
|
|
115
133
|
pair_config = calculate_pair(spec_config)
|
@@ -119,6 +137,10 @@ class ParameterGenerator
|
|
119
137
|
spec.init_exclusion(spec_config['exclude'])
|
120
138
|
end
|
121
139
|
|
140
|
+
unless spec_config['include'].nil?
|
141
|
+
spec.init_inclusion(spec_config['include'])
|
142
|
+
end
|
143
|
+
|
122
144
|
converters_config = spec_config['converters']
|
123
145
|
converters = if converters_config.nil?
|
124
146
|
# when no converters defined, we detect if type is consistent, otherwise assume it's string.
|
@@ -130,14 +152,6 @@ class ParameterGenerator
|
|
130
152
|
default_converter
|
131
153
|
end
|
132
154
|
end
|
133
|
-
elsif converters_config.first.class == String
|
134
|
-
# when converters defined, determined by the converter name as String.
|
135
|
-
spec_config['converters'].map do |converter|
|
136
|
-
Object.const_get(converter).new
|
137
|
-
end
|
138
|
-
else
|
139
|
-
# converters defined, probably programmatically when yaml-less, just return it.
|
140
|
-
converters_config
|
141
155
|
end
|
142
156
|
|
143
157
|
spec.init_converters(converters: converters)
|
@@ -157,7 +171,7 @@ class ParameterGenerator
|
|
157
171
|
return { outcomes.first => outcomes.first == 'true' ? 'false' : 'true' }
|
158
172
|
end
|
159
173
|
|
160
|
-
return { outcomes.first => spec_config['
|
174
|
+
return { outcomes.first => spec_config['default'] } if spec_config['default']
|
161
175
|
end
|
162
176
|
|
163
177
|
{}
|
data/lib/rast/rast_spec.rb
CHANGED
@@ -7,17 +7,20 @@ class RastSpec
|
|
7
7
|
# token_converter is the mapping of a variable token to a converter
|
8
8
|
# converters is a list of converters used via positional tokens.
|
9
9
|
attr_reader :variables, :pair, :pair_reversed, :rule, :description,
|
10
|
-
:exclude_clause, :token_converter, :converters
|
10
|
+
:exclude_clause, :include_clause, :token_converter, :converters,
|
11
|
+
:default_outcome
|
11
12
|
|
12
13
|
attr_accessor :exclude
|
13
14
|
|
14
|
-
def initialize(description: '', variables: [][], rule: nil)
|
15
|
+
def initialize(description: '', variables: [][], rule: nil, default_outcome: '')
|
15
16
|
@description = description
|
16
17
|
@variables = variables
|
17
18
|
@pair = {}
|
18
19
|
@pair_reversed = {}
|
19
20
|
@rule = rule
|
20
21
|
@exclude_clause = nil
|
22
|
+
@include_clause = nil
|
23
|
+
@default_outcome = default_outcome
|
21
24
|
end
|
22
25
|
|
23
26
|
def init_pair(pair_config: {})
|
@@ -47,13 +50,8 @@ class RastSpec
|
|
47
50
|
self
|
48
51
|
end
|
49
52
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
Variables: #{@variables}
|
54
|
-
Rules: #{@rules}
|
55
|
-
Pair: #{@pair}
|
56
|
-
Converters: #{@converters}
|
57
|
-
"
|
53
|
+
def init_inclusion(include_clause)
|
54
|
+
@include_clause = include_clause
|
55
|
+
self
|
58
56
|
end
|
59
57
|
end
|
@@ -8,29 +8,29 @@ module LogicHelper
|
|
8
8
|
# * @author Royce
|
9
9
|
# */
|
10
10
|
|
11
|
-
TRUE = '
|
12
|
-
FALSE = '
|
11
|
+
TRUE = '*true'
|
12
|
+
FALSE = '*false'
|
13
13
|
|
14
14
|
# /**
|
15
15
|
# * @param scenario list of scenario tokens.
|
16
16
|
# * @param left_subscript left index.
|
17
17
|
# * @param right_subscript right index.
|
18
|
-
# * @param left left token.
|
19
|
-
# * @param right right token.
|
18
|
+
# * @param left left token, no subscript.
|
19
|
+
# * @param right right token, no subscript.
|
20
20
|
# */
|
21
21
|
def perform_logical_and(scenario: [], left_subscript: -1, right_subscript: -1,
|
22
22
|
left: nil, right: nil)
|
23
|
-
if FALSE == left || FALSE == right
|
23
|
+
if FALSE == left && left_subscript == -1 || FALSE == right && right_subscript == -1
|
24
24
|
FALSE
|
25
|
-
elsif TRUE == left && TRUE == right
|
25
|
+
elsif TRUE == left && left_subscript == -1 && TRUE == right && right_subscript == -1
|
26
26
|
TRUE
|
27
|
-
elsif TRUE == left
|
27
|
+
elsif TRUE == left && left_subscript == -1
|
28
28
|
if right_subscript < 0
|
29
29
|
scenario.include?(right).to_s
|
30
30
|
else
|
31
31
|
(scenario[right_subscript] == right).to_s
|
32
32
|
end
|
33
|
-
elsif TRUE == right
|
33
|
+
elsif TRUE == right && right_subscript == -1
|
34
34
|
if left_subscript < 0
|
35
35
|
scenario.include?(left).to_s
|
36
36
|
else
|
@@ -43,6 +43,8 @@ module LogicHelper
|
|
43
43
|
object: left
|
44
44
|
)
|
45
45
|
|
46
|
+
return 'false' unless left_eval
|
47
|
+
|
46
48
|
right_eval = pevaluate(
|
47
49
|
scenario: scenario,
|
48
50
|
subscript: right_subscript,
|
@@ -62,21 +64,21 @@ module LogicHelper
|
|
62
64
|
# */
|
63
65
|
def perform_logical_or(scenario: [], left_subscript: -1, right_subscript: -1,
|
64
66
|
left: nil, right: nil)
|
65
|
-
if TRUE == left || TRUE == right
|
67
|
+
if TRUE == left && left_subscript == -1 || TRUE == right && right_subscript == -1
|
66
68
|
TRUE
|
67
|
-
elsif FALSE == left && FALSE == right
|
69
|
+
elsif FALSE == left && left_subscript == -1 && FALSE == right && right_subscript == -1
|
68
70
|
FALSE
|
69
|
-
elsif FALSE == left
|
71
|
+
elsif FALSE == left && left_subscript == -1
|
70
72
|
if right_subscript < 0
|
71
73
|
scenario.include?(right).to_s
|
72
74
|
else
|
73
75
|
(scenario[right_subscript] == right).to_s
|
74
76
|
end
|
75
|
-
elsif FALSE == right
|
77
|
+
elsif FALSE == right && right_subscript == -1
|
76
78
|
if left_subscript < 0
|
77
79
|
scenario.include?(left).to_s
|
78
80
|
else
|
79
|
-
(scenario[left_subscript]
|
81
|
+
(scenario[left_subscript] == left).to_s
|
80
82
|
end
|
81
83
|
else
|
82
84
|
left_eval = pevaluate(
|
@@ -85,6 +87,8 @@ module LogicHelper
|
|
85
87
|
object: left
|
86
88
|
)
|
87
89
|
|
90
|
+
return 'true' if left_eval
|
91
|
+
|
88
92
|
right_eval = pevaluate(
|
89
93
|
scenario: scenario,
|
90
94
|
subscript: right_subscript,
|
data/lib/rast/rules/rule.rb
CHANGED
@@ -23,11 +23,11 @@ class Rule
|
|
23
23
|
duplicates = []
|
24
24
|
|
25
25
|
rules.each do |outcome, clause|
|
26
|
-
if duplicates.include?(outcome)
|
27
|
-
|
28
|
-
end
|
26
|
+
# if duplicates.include?(outcome)
|
27
|
+
# raise "#{outcome} matched multiple clauses"
|
28
|
+
# end
|
29
29
|
|
30
|
-
duplicates << outcome
|
30
|
+
# duplicates << outcome
|
31
31
|
|
32
32
|
@outcome_clause_hash[outcome.to_s] = Rule.sanitize(clause: clause)
|
33
33
|
end
|
@@ -35,7 +35,7 @@ class Rule
|
|
35
35
|
|
36
36
|
def self.sanitize(clause: '')
|
37
37
|
return clause if clause.is_a?(Array)
|
38
|
-
|
38
|
+
|
39
39
|
cleaner = Rule.remove_spaces(token: clause, separator: '(')
|
40
40
|
cleaner = Rule.remove_spaces(token: cleaner, separator: ')')
|
41
41
|
cleaner = Rule.remove_spaces(token: cleaner, separator: '&')
|
@@ -72,29 +72,4 @@ class Rule
|
|
72
72
|
def clause(outcome: '')
|
73
73
|
@outcome_clause_hash[outcome]
|
74
74
|
end
|
75
|
-
|
76
|
-
# /**
|
77
|
-
# * Get rule result give a fixed list of scenario tokens. Used for fixed
|
78
|
-
# * list.
|
79
|
-
# *
|
80
|
-
# * @param scenario of interest.
|
81
|
-
# * @return the actionToRuleClauses
|
82
|
-
# */
|
83
|
-
def rule_outcome(scenario: [])
|
84
|
-
scenario_string = scenario.to_s
|
85
|
-
anded_scenario = scenario_string[1..-2].gsub(/,\s/, '&')
|
86
|
-
|
87
|
-
@outcome_clause_hash.each do |key, clause|
|
88
|
-
or_list_clause = clause.split('\|').map(&:strip)
|
89
|
-
return key if or_list_clause.include?(anded_scenario)
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
# /**
|
94
|
-
# * @see {@link Object#toString()}
|
95
|
-
# * @return String representation of this instance.
|
96
|
-
# */
|
97
|
-
def to_s
|
98
|
-
@outcome_clause_hash.to_s
|
99
|
-
end
|
100
75
|
end
|
@@ -21,7 +21,7 @@ class RuleEvaluator
|
|
21
21
|
|
22
22
|
# the "false" part of the "false[1]"
|
23
23
|
RE_TOKEN_BODY = /^.+(?=\[)/.freeze
|
24
|
-
RE_TOKENS = /([!|)(&])|([a-zA-Z\s0-9-]+\[\d\])/.freeze
|
24
|
+
RE_TOKENS = /([!|)(&])|([*a-zA-Z\s0-9-]+\[\d\])/.freeze
|
25
25
|
|
26
26
|
def self.operator_from_symbol(symbol: nil)
|
27
27
|
OPERATORS.find { |operator| operator.symbol == symbol }
|
@@ -30,7 +30,7 @@ class RuleEvaluator
|
|
30
30
|
DEFAULT_CONVERT_HASH = {
|
31
31
|
Integer => IntConverter.new,
|
32
32
|
Float => FloatConverter.new,
|
33
|
-
Fixnum =>
|
33
|
+
Fixnum => IntConverter.new,
|
34
34
|
Array => DefaultConverter.new,
|
35
35
|
TrueClass => BoolConverter.new,
|
36
36
|
FalseClass => BoolConverter.new,
|
@@ -85,10 +85,7 @@ class RuleEvaluator
|
|
85
85
|
# * @return <code>String</code> representation of the result
|
86
86
|
# */
|
87
87
|
def evaluate(scenario: [], rule_token_convert: {})
|
88
|
-
|
89
|
-
if @stack_rpn.empty?
|
90
|
-
true
|
91
|
-
elsif @stack_rpn.size == 1
|
88
|
+
if @stack_rpn.size == 1
|
92
89
|
evaluate_one_rpn(scenario: scenario).to_s
|
93
90
|
else
|
94
91
|
evaluate_multi_rpn(
|
@@ -112,18 +109,14 @@ class RuleEvaluator
|
|
112
109
|
if TRUE != value && FALSE != value
|
113
110
|
subscript = extract_subscript(token: value.to_s)
|
114
111
|
value_str = value.to_s.strip
|
115
|
-
if subscript > -1
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
default_converter.convert(value_str)
|
122
|
-
else
|
123
|
-
rule_token_convert[value_str].convert(value_str)
|
124
|
-
end
|
125
|
-
end
|
112
|
+
value = if subscript > -1
|
113
|
+
value_token = value_str[/^.+(?=\[)/]
|
114
|
+
rule_token_convert[value_token].convert(value_token)
|
115
|
+
else
|
116
|
+
rule_token_convert[value_str].convert(value_str)
|
117
|
+
end
|
126
118
|
end
|
119
|
+
|
127
120
|
retval << subscript
|
128
121
|
retval << value
|
129
122
|
retval
|
@@ -161,6 +154,8 @@ class RuleEvaluator
|
|
161
154
|
# * @param string token to check for subscript.
|
162
155
|
# */
|
163
156
|
def extract_subscript(token: '')
|
157
|
+
return -1 if token.is_a? Array
|
158
|
+
|
164
159
|
subscript = token[/\[(\d+)\]$/, 1]
|
165
160
|
subscript.nil? ? -1 : subscript.to_i
|
166
161
|
end
|
@@ -177,9 +172,6 @@ class RuleEvaluator
|
|
177
172
|
stack_rpn_clone = Marshal.load(Marshal.dump(@stack_rpn))
|
178
173
|
|
179
174
|
# /* evaluating the RPN expression */
|
180
|
-
|
181
|
-
# binding.pry
|
182
|
-
|
183
175
|
while stack_rpn_clone.any?
|
184
176
|
token = stack_rpn_clone.pop
|
185
177
|
if operator?(token: token)
|
@@ -211,7 +203,8 @@ class RuleEvaluator
|
|
211
203
|
def evaluate_multi(scenario: [], rule_token_convert: {}, operator: nil)
|
212
204
|
default_converter = DEFAULT_CONVERT_HASH[scenario.first.class]
|
213
205
|
|
214
|
-
#
|
206
|
+
# Convert 'nil' to nil.
|
207
|
+
formatted_scenario = scenario.map { |token| token == 'nil' ? nil: token }
|
215
208
|
|
216
209
|
left_arr = next_value(
|
217
210
|
rule_token_convert: rule_token_convert,
|
@@ -225,17 +218,17 @@ class RuleEvaluator
|
|
225
218
|
|
226
219
|
answer = send(
|
227
220
|
"perform_logical_#{operator.name}",
|
228
|
-
scenario:
|
221
|
+
scenario: formatted_scenario,
|
229
222
|
left_subscript: left_arr[0],
|
230
223
|
right_subscript: right_arr[0],
|
231
224
|
left: left_arr[1],
|
232
225
|
right: right_arr[1]
|
233
226
|
)
|
234
227
|
|
235
|
-
@stack_answer << if answer[0] == '
|
228
|
+
@stack_answer << if answer[0] == '*'
|
236
229
|
answer
|
237
230
|
else
|
238
|
-
"
|
231
|
+
"*#{answer}"
|
239
232
|
end
|
240
233
|
end
|
241
234
|
|
@@ -243,29 +236,28 @@ class RuleEvaluator
|
|
243
236
|
# * @param scenario List of values to evaluate against the rule expression.
|
244
237
|
# */
|
245
238
|
def evaluate_multi_not(scenario: [])
|
246
|
-
|
247
|
-
|
248
|
-
# binding.pry
|
239
|
+
latest = @stack_answer.pop.strip
|
249
240
|
|
250
|
-
answer = if LogicHelper::TRUE ==
|
241
|
+
answer = if LogicHelper::TRUE == latest
|
251
242
|
LogicHelper::FALSE
|
252
|
-
elsif LogicHelper::FALSE ==
|
243
|
+
elsif LogicHelper::FALSE == latest
|
253
244
|
LogicHelper::TRUE
|
254
245
|
else
|
255
|
-
subscript = extract_subscript(token:
|
246
|
+
subscript = extract_subscript(token: latest)
|
247
|
+
converter = DEFAULT_CONVERT_HASH[scenario.first.class]
|
256
248
|
if subscript < 0
|
257
|
-
|
249
|
+
converted = converter.convert(latest)
|
250
|
+
(!scenario.include?(converted)).to_s
|
258
251
|
else
|
259
|
-
|
260
|
-
|
261
|
-
(scenario[subscript] == converted).to_s
|
252
|
+
converted = converter.convert(latest[RE_TOKEN_BODY])
|
253
|
+
(scenario[subscript] != converted).to_s
|
262
254
|
end
|
263
255
|
end
|
264
256
|
|
265
|
-
@stack_answer << if answer[0] == '
|
257
|
+
@stack_answer << if answer[0] == '*'
|
266
258
|
answer
|
267
259
|
else
|
268
|
-
"
|
260
|
+
"*#{answer}"
|
269
261
|
end
|
270
262
|
end
|
271
263
|
|
@@ -26,8 +26,6 @@ class RuleValidator
|
|
26
26
|
private
|
27
27
|
|
28
28
|
def validate_multi(scenario: [], spec: nil, rule_result: [])
|
29
|
-
# binding.pry
|
30
|
-
|
31
29
|
matched_outputs = []
|
32
30
|
match_count = 0
|
33
31
|
|
@@ -37,26 +35,20 @@ class RuleValidator
|
|
37
35
|
match_count += 1
|
38
36
|
matched_outputs << spec.rule.outcomes[i]
|
39
37
|
end
|
40
|
-
Rast.assert("Scenario must fall into a unique rule output/clause:
|
41
|
-
#{scenario} , matched: #{matched_outputs}") { match_count == 1 }
|
42
38
|
|
43
|
-
matched_outputs
|
39
|
+
Rast.assert("#{spec.description} #{scenario} must fall into a unique rule outcome/clause, matched: #{matched_outputs}") do
|
40
|
+
match_count == 1 || match_count == 0 && !spec.default_outcome.nil?
|
41
|
+
end
|
42
|
+
|
43
|
+
matched_outputs.first || spec.default_outcome
|
44
44
|
end
|
45
45
|
|
46
|
+
#
|
46
47
|
def binary_outcome(outcome: '', spec: nil, expected: false)
|
47
|
-
|
48
|
-
|
49
|
-
expected == 'true' ? outcome : opposite(outcome: outcome, spec: spec)
|
48
|
+
if expected == 'true'
|
49
|
+
outcome
|
50
50
|
else
|
51
|
-
expected == 'true' ? opposite(outcome: outcome, spec: spec) : outcome
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def opposite(outcome: '', spec: nil)
|
56
|
-
if spec.pair.keys.include? outcome
|
57
51
|
spec.pair[outcome]
|
58
|
-
else
|
59
|
-
spec.pair_reversed[outcome]
|
60
52
|
end
|
61
53
|
end
|
62
54
|
end
|
data/lib/rast/spec_dsl.rb
CHANGED
@@ -8,10 +8,10 @@ class SpecDSL
|
|
8
8
|
include FactoryGirl::Syntax::Methods
|
9
9
|
|
10
10
|
attr_accessor :subject, :rspec_methods, :execute_block,
|
11
|
-
:prepare_block, :transients, :outcomes, :fixtures
|
11
|
+
:prepare_block, :transients, :outcomes, :fixtures, :spec_id
|
12
12
|
|
13
13
|
# # yaml-less
|
14
|
-
attr_writer :variables, :exclude, :converters, :rules, :pair
|
14
|
+
attr_writer :variables, :exclude, :include, :converters, :rules, :pair, :default_outcome
|
15
15
|
|
16
16
|
# @subject the sut instance
|
17
17
|
# @name the sut name to be displayed with -fd
|
@@ -29,23 +29,6 @@ class SpecDSL
|
|
29
29
|
instance_eval(&block)
|
30
30
|
end
|
31
31
|
|
32
|
-
def respond_to_missing?(*several_variants)
|
33
|
-
super(several_variants)
|
34
|
-
end
|
35
|
-
|
36
|
-
def method_missing(method_name_symbol, *args, &block)
|
37
|
-
# p "method_missing: #{method_name_symbol}"
|
38
|
-
return super if method_name_symbol == :to_ary
|
39
|
-
|
40
|
-
@rspec_methods << {
|
41
|
-
name: method_name_symbol,
|
42
|
-
args: args.first,
|
43
|
-
block: block
|
44
|
-
}
|
45
|
-
|
46
|
-
self
|
47
|
-
end
|
48
|
-
|
49
32
|
# yaml-less start
|
50
33
|
def variables(vars)
|
51
34
|
@variables = vars
|
@@ -55,16 +38,18 @@ class SpecDSL
|
|
55
38
|
@exclude = clause
|
56
39
|
end
|
57
40
|
|
58
|
-
def converters(&block)
|
59
|
-
@converters = instance_eval(&block)
|
60
|
-
end
|
61
|
-
|
62
41
|
def rules(rules)
|
63
|
-
@rules =
|
42
|
+
@rules = {}
|
43
|
+
|
44
|
+
rules.each do |key, value|
|
45
|
+
calc_key = key
|
46
|
+
calc_key = key == :true if key == :true || key == :false
|
47
|
+
@rules[calc_key] = value
|
48
|
+
end
|
64
49
|
end
|
65
50
|
|
66
|
-
def
|
67
|
-
|
51
|
+
def outcomes(outcomes)
|
52
|
+
rules(outcomes)
|
68
53
|
end
|
69
54
|
|
70
55
|
# yaml-less end
|
@@ -91,9 +76,17 @@ class SpecDSL
|
|
91
76
|
end
|
92
77
|
|
93
78
|
@fixtures.sort_by! do |fixture|
|
79
|
+
|
80
|
+
if fixture[:expected_outcome].nil?
|
81
|
+
raise 'Broken initialization, check your single rule/else/default configuration'
|
82
|
+
end
|
83
|
+
|
94
84
|
fixture[:expected_outcome] + fixture[:scenario].to_s
|
85
|
+
# fixture[:scenario].to_s + fixture[:expected_outcome]
|
95
86
|
end
|
96
87
|
|
88
|
+
# @fixtures.reverse!
|
89
|
+
|
97
90
|
generate_rspecs
|
98
91
|
end
|
99
92
|
|
@@ -103,8 +96,13 @@ class SpecDSL
|
|
103
96
|
main_scope = self
|
104
97
|
|
105
98
|
title = "#{@subject_name}: #{@fixtures.first[:spec].description}"
|
99
|
+
|
106
100
|
exclusion = fixtures.first[:spec].exclude_clause
|
107
|
-
|
101
|
+
exclusion = exclusion.join if exclusion.is_a? Array
|
102
|
+
title += ", EXCLUDE: '#{exclusion}'" if exclusion
|
103
|
+
inclusion = fixtures.first[:spec].include_clause
|
104
|
+
inclusion = inclusion.join if inclusion.is_a? Array
|
105
|
+
title += ", ONLY: '#{inclusion}'" if inclusion
|
108
106
|
|
109
107
|
RSpec.describe title do
|
110
108
|
main_scope.fixtures.each do |fixture|
|
@@ -129,6 +127,7 @@ def generate_rspec(scope: nil, scenario: {}, expected: '')
|
|
129
127
|
block_params = scenario.values
|
130
128
|
|
131
129
|
@mysubject = scope.subject
|
130
|
+
|
132
131
|
class << self
|
133
132
|
define_method(:subject) { @mysubject }
|
134
133
|
end
|
data/lib/rast.rb
CHANGED
@@ -8,10 +8,6 @@ require 'rast/spec_dsl'
|
|
8
8
|
class Rast
|
9
9
|
# RSpec All scenario test
|
10
10
|
#
|
11
|
-
# Example:
|
12
|
-
# >> Hola.hi("spanish")
|
13
|
-
# => hola mundo
|
14
|
-
#
|
15
11
|
# Arguments:
|
16
12
|
# language: (String)
|
17
13
|
|
@@ -38,6 +34,10 @@ class Rast
|
|
38
34
|
raise message unless yield
|
39
35
|
end
|
40
36
|
|
37
|
+
def xspec(id, &block)
|
38
|
+
p "xspec skipped #{id}"
|
39
|
+
end
|
40
|
+
|
41
41
|
def spec(id, &block)
|
42
42
|
global_spec(
|
43
43
|
subject: @subject,
|
data/lib/template_spec.yml
CHANGED
@@ -5,14 +5,12 @@ specs:
|
|
5
5
|
|
6
6
|
variables:
|
7
7
|
param1:
|
8
|
-
-
|
9
|
-
-
|
8
|
+
- one
|
9
|
+
- two
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
outcomes: # required (dictionary)
|
12
|
+
true: true[0]
|
13
13
|
|
14
|
-
|
15
|
-
'true': 'true[0]'
|
14
|
+
pair: {true: one} # optional. (dictionary with 1 mapping)
|
16
15
|
|
17
|
-
pair
|
18
|
-
'true': 'false'
|
16
|
+
default: DEFAULT # optional (scalar) fall off value, or the else result of the `pair` config.
|
data/rast.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rast
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.18.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Royce Remulla
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-05-
|
11
|
+
date: 2020-05-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: factory_girl
|
@@ -38,6 +38,7 @@ files:
|
|
38
38
|
- CHANGELOG.md
|
39
39
|
- Gemfile
|
40
40
|
- Gemfile.lock
|
41
|
+
- Getting-Started.md
|
41
42
|
- README.md
|
42
43
|
- examples/arithmetic_module.rb
|
43
44
|
- examples/double_example.rb
|
@@ -45,8 +46,10 @@ files:
|
|
45
46
|
- examples/factory_example.rb
|
46
47
|
- examples/logic_checker.rb
|
47
48
|
- examples/logic_four.rb
|
49
|
+
- examples/lohika.rb
|
48
50
|
- examples/phone.rb
|
49
51
|
- examples/positive.rb
|
52
|
+
- examples/positive2.rb
|
50
53
|
- examples/prime_number.rb
|
51
54
|
- examples/quiz_module.rb
|
52
55
|
- examples/quoted.rb
|