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:
Adam Rozman 2024-02-07 12:55:30 +02:00
parent 093f20df8d
commit 72c80f6993
3 changed files with 256 additions and 5 deletions

View File

@ -27,6 +27,38 @@ def _all_in(x, *y):
return all(val in x for val in 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 = { op_methods = {
# This one is special/odd, # This one is special/odd,
# TODO(harlowja): fix it so that it's not greater than or # TODO(harlowja): fix it so that it's not greater than or
@ -51,6 +83,7 @@ op_methods = {
'<all-in>': _all_in, '<all-in>': _all_in,
'<in>': lambda x, y: y in x, '<in>': lambda x, y: y in x,
'<or>': lambda x, *y: any(x == a for a in y), '<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 * ``s>= :`` Greater than or equal
Other operations: Other operations:
* ``<all-in> :`` All items 'in' value * ``<all-in> :`` All items 'in' value
* ``<in> :`` Item 'in' value, like a substring in a string. * ``<in> :`` Item 'in' value, like a substring in a string.
* ``<or> :`` Logical 'or' * ``<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) 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`` * ``"s== 2.1.0"`` Is the string value equal to ``2.1.0``
* ``"<in> gcc"`` Is the string ``gcc`` contained in the value string * ``"<in> gcc"`` Is the string ``gcc`` contained in the value string
* ``"<all-in> aes mmx"`` Are both ``aes`` and ``mmx`` in the value * ``"<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 :returns: A pyparsing.MatchFirst object. See
https://pythonhosted.org/pyparsing/ for details on pyparsing. https://pythonhosted.org/pyparsing/ for details on pyparsing.
@ -113,18 +152,21 @@ Example operations:
all_in_nary_op = pyparsing.Literal("<all-in>") all_in_nary_op = pyparsing.Literal("<all-in>")
or_ = pyparsing.Literal("<or>") or_ = pyparsing.Literal("<or>")
range_in_binary_op = pyparsing.Literal("<range-in>")
# An atom is anything not an keyword followed by anything but whitespace # 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 unary = unary_ops + atom
range_op = range_in_binary_op + atom + atom + atom + atom
nary = all_in_nary_op + pyparsing.OneOrMore(atom) nary = all_in_nary_op + pyparsing.OneOrMore(atom)
disjunction = pyparsing.OneOrMore(or_ + atom) disjunction = pyparsing.OneOrMore(or_ + atom)
# Even-numbered tokens will be '<or>', so we drop them # Even-numbered tokens will be '<or>', so we drop them
disjunction.setParseAction(lambda _s, _l, t: ["<or>"] + t[1::2]) disjunction.setParseAction(lambda _s, _l, t: ["<or>"] + t[1::2])
expr = disjunction | nary | unary | atom expr = disjunction | nary | range_op | unary | atom
return expr return expr

View File

@ -435,3 +435,195 @@ class SpecsMatcherTestCase(test_base.BaseTestCase):
specs_matcher.match, specs_matcher.match,
value="^&*($", value="^&*($",
req='<all-in> aes') 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 ]')

View File

@ -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.