add <range-in> spec DSL operator
Add a new spec DSL operator called `range-in` that allowes users of the spec_matcher to match values against numeric ranges. The surrounding brackets determines whether the limit should be inclusive or not. examples: <range-in> [ 10 20 ] : 10 <= x <= 20 <range-in> ( 10 20 ] : 10 < x <= 20 <range-in> [ 10 20 ) : 10 <= x < 20 <range-in> ( 10 20 ) : 10 < x < 20 Closes-Bug: #2052619 Change-Id: I444c01219d02ea7572d4b82117b89b8d3eb75e56 Signed-off-by: Adam Rozman <adam.rozman@est.tech> Co-authored-by: Takashi Kajinami <kajinamit@oss.nttdata.com>
This commit is contained in:
parent
093f20df8d
commit
72c80f6993
@ -27,6 +27,38 @@ def _all_in(x, *y):
|
||||
return all(val in x for val in y)
|
||||
|
||||
|
||||
def _range_in(x, *y):
|
||||
x = ast.literal_eval(x)
|
||||
if len(y) != 4:
|
||||
raise TypeError("<range-in> operator has to be followed by 2 "
|
||||
"space separated numeric value surrounded by "
|
||||
"brackets \"range_in [ 10 20 ] \"")
|
||||
num_x = float(x)
|
||||
num_y = float(y[1])
|
||||
num_z = float(y[2])
|
||||
if num_y > num_z:
|
||||
raise TypeError("<range-in> operator's first argument has to be "
|
||||
"smaller or equal to the second argument EG"
|
||||
"\"range_in ( 10 20 ] \"")
|
||||
|
||||
if y[0] == '[':
|
||||
lower = num_x >= num_y
|
||||
elif y[0] == '(':
|
||||
lower = num_x > num_y
|
||||
else:
|
||||
raise TypeError("The first element should be an opening bracket "
|
||||
"(\"(\" or \"[\")")
|
||||
|
||||
if y[3] == ']':
|
||||
upper = num_x <= num_z
|
||||
elif y[3] == ')':
|
||||
upper = num_x < num_z
|
||||
else:
|
||||
raise TypeError("The last element should be a closing bracket "
|
||||
"(\")\" or \"]\")")
|
||||
return lower and upper
|
||||
|
||||
|
||||
op_methods = {
|
||||
# This one is special/odd,
|
||||
# TODO(harlowja): fix it so that it's not greater than or
|
||||
@ -51,6 +83,7 @@ op_methods = {
|
||||
'<all-in>': _all_in,
|
||||
'<in>': lambda x, y: y in x,
|
||||
'<or>': lambda x, *y: any(x == a for a in y),
|
||||
'<range-in>': _range_in,
|
||||
}
|
||||
|
||||
|
||||
@ -79,9 +112,12 @@ String operations:
|
||||
* ``s>= :`` Greater than or equal
|
||||
|
||||
Other operations:
|
||||
* ``<all-in> :`` All items 'in' value
|
||||
* ``<in> :`` Item 'in' value, like a substring in a string.
|
||||
* ``<or> :`` Logical 'or'
|
||||
* ``<all-in> :`` All items 'in' value
|
||||
* ``<in> :`` Item 'in' value, like a substring in a string.
|
||||
* ``<or> :`` Logical 'or'
|
||||
* ``<range-in>:`` Range tester with customizable boundary conditions, tests
|
||||
whether value is in the range, boundary condition could be
|
||||
inclusve \'[\' or exclusive \'(\'.
|
||||
|
||||
If no operator is specified the default is ``s==`` (string equality comparison)
|
||||
|
||||
@ -91,6 +127,9 @@ Example operations:
|
||||
* ``"s== 2.1.0"`` Is the string value equal to ``2.1.0``
|
||||
* ``"<in> gcc"`` Is the string ``gcc`` contained in the value string
|
||||
* ``"<all-in> aes mmx"`` Are both ``aes`` and ``mmx`` in the value
|
||||
* ``"<range-in> [ 10 20 ]"`` float(value) >= 10 and float(value) <= 20
|
||||
* ``"<range-in> ( 10 20 ]"`` float(value) > 10 and float(value) <= 20
|
||||
* ``"<range-in> ( 10 20 )"`` float(value) > 10 and float(value) < 20
|
||||
|
||||
:returns: A pyparsing.MatchFirst object. See
|
||||
https://pythonhosted.org/pyparsing/ for details on pyparsing.
|
||||
@ -113,18 +152,21 @@ Example operations:
|
||||
|
||||
all_in_nary_op = pyparsing.Literal("<all-in>")
|
||||
or_ = pyparsing.Literal("<or>")
|
||||
range_in_binary_op = pyparsing.Literal("<range-in>")
|
||||
|
||||
# An atom is anything not an keyword followed by anything but whitespace
|
||||
atom = ~(unary_ops | all_in_nary_op | or_) + pyparsing.Regex(r"\S+")
|
||||
atom = ~(unary_ops | all_in_nary_op | or_ | range_in_binary_op) + \
|
||||
pyparsing.Regex(r"\S+")
|
||||
|
||||
unary = unary_ops + atom
|
||||
range_op = range_in_binary_op + atom + atom + atom + atom
|
||||
nary = all_in_nary_op + pyparsing.OneOrMore(atom)
|
||||
disjunction = pyparsing.OneOrMore(or_ + atom)
|
||||
|
||||
# Even-numbered tokens will be '<or>', so we drop them
|
||||
disjunction.setParseAction(lambda _s, _l, t: ["<or>"] + t[1::2])
|
||||
|
||||
expr = disjunction | nary | unary | atom
|
||||
expr = disjunction | nary | range_op | unary | atom
|
||||
return expr
|
||||
|
||||
|
||||
|
@ -435,3 +435,195 @@ class SpecsMatcherTestCase(test_base.BaseTestCase):
|
||||
specs_matcher.match,
|
||||
value="^&*($",
|
||||
req='<all-in> aes')
|
||||
|
||||
def test_specs_fails_not_enough_args_with_op_rangein(self):
|
||||
self.assertRaises(
|
||||
TypeError,
|
||||
specs_matcher.match,
|
||||
value="23",
|
||||
req='<range-in> [ 10 ]')
|
||||
|
||||
def test_specs_fails_no_brackets_with_op_rangein(self):
|
||||
self.assertRaises(
|
||||
TypeError,
|
||||
specs_matcher.match,
|
||||
value="23",
|
||||
req='<range-in> 10 20')
|
||||
|
||||
def test_specs_fails_no_opening_bracket_with_op_rangein(self):
|
||||
self.assertRaises(
|
||||
TypeError,
|
||||
specs_matcher.match,
|
||||
value="23",
|
||||
req='<range-in> 10 20 ]')
|
||||
|
||||
def test_specs_fails_no_closing_bracket_with_op_rangein(self):
|
||||
self.assertRaises(
|
||||
TypeError,
|
||||
specs_matcher.match,
|
||||
value="23",
|
||||
req='<range-in> [ 10 20')
|
||||
|
||||
def test_specs_fails_invalid_brackets_with_op_rangein(self):
|
||||
self.assertRaises(
|
||||
TypeError,
|
||||
specs_matcher.match,
|
||||
value="23",
|
||||
req='<range-in> { 10 20 }')
|
||||
|
||||
def test_specs_fails_not_opening_brackets_with_op_rangein(self):
|
||||
self.assertRaises(
|
||||
TypeError,
|
||||
specs_matcher.match,
|
||||
value="23",
|
||||
req='<range-in> ) 10 20 )')
|
||||
|
||||
def test_specs_fails_not_closing_brackets_with_op_rangein(self):
|
||||
self.assertRaises(
|
||||
TypeError,
|
||||
specs_matcher.match,
|
||||
value="23",
|
||||
req='<range-in> ( 10 20 (')
|
||||
|
||||
def test_specs_fails_reverse_brackets_with_op_rangein(self):
|
||||
self.assertRaises(
|
||||
TypeError,
|
||||
specs_matcher.match,
|
||||
value="23",
|
||||
req='<range-in> ) 10 20 (')
|
||||
|
||||
def test_specs_fails_too_many_args_with_op_rangein(self):
|
||||
self.assertRaises(
|
||||
TypeError,
|
||||
specs_matcher.match,
|
||||
value="23",
|
||||
req='<range-in> [ 10 20 30 ]')
|
||||
|
||||
def test_specs_fails_bad_limits_with_op_rangein(self):
|
||||
self.assertRaises(
|
||||
TypeError,
|
||||
specs_matcher.match,
|
||||
value="23",
|
||||
req='<range-in> [ 20 10 ]')
|
||||
|
||||
def test_specs_fails_match_beyond_scope_with_op_rangein_le(self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=False,
|
||||
value="23",
|
||||
req='<range-in> [ 10 20 ]')
|
||||
|
||||
def test_specs_fails_match_beyond_scope_with_op_rangein_lt(self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=False,
|
||||
value="23",
|
||||
req='<range-in> [ 10 20 )')
|
||||
|
||||
def test_specs_fails_match_under_scope_with_op_rangein_ge(self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=False,
|
||||
value="5",
|
||||
req='<range-in> [ 10 20 ]')
|
||||
|
||||
def test_specs_fails_match_under_scope_with_op_rangein_gt(self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=False,
|
||||
value="5",
|
||||
req='<range-in> ( 10 20 ]')
|
||||
|
||||
def test_specs_fails_match_float_beyond_scope_with_op_rangein_le(self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=False,
|
||||
value="20.3",
|
||||
req='<range-in> [ 10.1 20.2 ]')
|
||||
|
||||
def test_specs_fails_match_float_beyond_scope_with_op_rangein_lt(self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=False,
|
||||
value="20.3",
|
||||
req='<range-in> [ 10.1 20.2 )')
|
||||
|
||||
def test_specs_fails_match_float_under_scope_with_op_rangein_ge(self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=False,
|
||||
value="5.0",
|
||||
req='<range-in> [ 5.1 20.2 ]')
|
||||
|
||||
def test_specs_fails_match_float_under_scope_with_op_rangein_gt(self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=False,
|
||||
value="5.0",
|
||||
req='<range-in> ( 5.1 20.2 ]')
|
||||
|
||||
def test_specs_matches_int_lower_int_range_with_op_rangein_ge(self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=True,
|
||||
value="10",
|
||||
req='<range-in> [ 10 20 ]')
|
||||
|
||||
def test_specs_fails_matchesint_lower_int_range_with_op_rangein_gt(self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=False,
|
||||
value="10",
|
||||
req='<range-in> ( 10 20 ]')
|
||||
|
||||
def test_specs_matches_float_lower_float_range_with_op_rangein_ge(self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=True,
|
||||
value="10.1",
|
||||
req='<range-in> [ 10.1 20 ]')
|
||||
|
||||
def test_specs_fails_matche_float_lower_float_range_with_op_rangein_gt(
|
||||
self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=False,
|
||||
value="10.1",
|
||||
req='<range-in> ( 10.1 20 ]')
|
||||
|
||||
def test_specs_matches_int_with_int_range_with_op_rangein(self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=True,
|
||||
value="15",
|
||||
req='<range-in> [ 10 20 ]')
|
||||
|
||||
def test_specs_matches_float_with_int_limit_with_op_rangein(self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=True,
|
||||
value="15.5",
|
||||
req='<range-in> [ 10 20 ]')
|
||||
|
||||
def test_specs_matches_int_upper_int_range_with_op_rangein(self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=True,
|
||||
value="20",
|
||||
req='<range-in> [ 10 20 ]')
|
||||
|
||||
def test_specs_fails_matche_int_upper_int_range_with_op_rangein_lt(self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=False,
|
||||
value="20",
|
||||
req='<range-in> [ 10 20 )')
|
||||
|
||||
def test_specs_matches_float_upper_mixed_range_with_op_rangein(self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=True,
|
||||
value="20.5",
|
||||
req='<range-in> [ 10 20.5 ]')
|
||||
|
||||
def test_specs_fails_matche_float_upper_mixed_range_with_op_rangein_lt(
|
||||
self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=False,
|
||||
value="20.5",
|
||||
req='<range-in> [ 10 20.5 )')
|
||||
|
||||
def test_specs_matches_float_with_float_limit_with_op_rangein(self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=True,
|
||||
value="12.5",
|
||||
req='<range-in> [ 10.1 20.1 ]')
|
||||
|
||||
def test_specs_matches_only_one_with_op_rangein(self):
|
||||
self._do_specs_matcher_test(
|
||||
matches=True,
|
||||
value="10.1",
|
||||
req='<range-in> [ 10.1 10.1 ]')
|
||||
|
@ -0,0 +1,17 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Introducing a new spec DSL operator called ``<range-in>`` that allows users
|
||||
to match a numeric value against a range of numbers that are delimited with
|
||||
lower and upper limits. The new operator is a binary operator that accepts
|
||||
4 arguments.
|
||||
|
||||
- The first one and the last one are brackets. ``[`` and ``]`` defines
|
||||
inclusive limits while ``(`` and ``)`` defines exclusive limits.
|
||||
|
||||
- The second one is the lower limit while the third one is the upper
|
||||
limit.
|
||||
|
||||
Example: "<range-in> [ 10.4 20 )" will match a value against an range
|
||||
such as the lower limit of the range is 10.4 and the upper limit is 20.
|
||||
Note that 10.4 is included while 20 is excluded.
|
Loading…
Reference in New Issue
Block a user