diff --git a/python-jmespath.changes b/python-jmespath.changes index 9c6e17f..32d2957 100644 --- a/python-jmespath.changes +++ b/python-jmespath.changes @@ -5,6 +5,15 @@ Tue Mar 26 14:43:27 UTC 2019 - Tomáš Chvátal * Fix min_by/max_by with empty lists (`issue 151) * Fix reverse type for null type (issue 145) +------------------------------------------------------------------- +Mon Mar 18 06:06:26 UTC 2019 - John Vandenberg + +- Add extra testing with hypothesis +- Remove unnecessary dependency on python-base +- Update to v0.9.4 + * Fix min_by/max_by with empty lists + * Fix reverse type for null type + ------------------------------------------------------------------- Tue Dec 4 12:49:31 UTC 2018 - Matej Cepl diff --git a/python-jmespath.spec b/python-jmespath.spec index 1742032..05a5ed6 100644 --- a/python-jmespath.spec +++ b/python-jmespath.spec @@ -23,16 +23,17 @@ Release: 0 Summary: Python module for declarative JSON document element extraction License: MIT Group: Development/Languages/Python -URL: https://github.com/boto/jmespath +URL: https://github.com/jmespath/jmespath.py Source: https://files.pythonhosted.org/packages/source/j/jmespath/jmespath-%{version}.tar.gz +Source1: https://raw.githubusercontent.com/jmespath/jmespath.py/develop/extra/test_hypothesis.py # Testing +BuildRequires: %{python_module hypothesis} BuildRequires: %{python_module nose} BuildRequires: %{python_module ply >= 3.4} BuildRequires: %{python_module setuptools} BuildRequires: %{python_module simplejson} BuildRequires: fdupes BuildRequires: python-rpm-macros -Requires: python-base Requires: python-ply >= 3.4 Requires: python-simplejson Requires(post): update-alternatives @@ -73,6 +74,7 @@ The expression: foo.*.name will return ["one", "two"]. %prep %setup -q -n jmespath-%{version} +cp %{SOURCE1} . %build %python_build @@ -84,7 +86,9 @@ mv %{buildroot}%{_bindir}/jp.py %{buildroot}%{_bindir}/jp %python_expand %fdupes %{buildroot}%{$python_sitelib} %check -%python_expand nosetests-%{$python_bin_suffix} tests +%{python_expand $python setup.py test +$python test_hypothesis.py +} %post %python_install_alternative jp diff --git a/test_hypothesis.py b/test_hypothesis.py new file mode 100644 index 0000000..5533eef --- /dev/null +++ b/test_hypothesis.py @@ -0,0 +1,140 @@ +# Test suite using hypothesis to generate test cases. +# This is in a standalone module so that these tests +# can a) be run separately and b) allow for customization +# via env var for longer runs in travis. +import os +import sys +import numbers + +from nose.plugins.skip import SkipTest +from hypothesis import given, settings, assume, HealthCheck +import hypothesis.strategies as st + +from jmespath import lexer +from jmespath import parser +from jmespath import exceptions +from jmespath.functions import Functions + + +if sys.version_info[:2] == (2, 6): + raise RuntimeError("Hypothesis tests are not supported on python2.6. " + "Use python2.7, or python3.3 and greater.") + + +JSON_NUMBERS = (st.integers() | st.floats(allow_nan=False, + allow_infinity=False)) + +RANDOM_JSON = st.recursive( + JSON_NUMBERS | st.booleans() | st.text() | st.none(), + lambda children: st.lists(children) | st.dictionaries(st.text(), children) +) + +MAX_EXAMPLES = int(os.environ.get('JP_MAX_EXAMPLES', 1000)) +BASE_SETTINGS = { + 'max_examples': MAX_EXAMPLES, + 'suppress_health_check': [HealthCheck.too_slow], +} + + +# For all of these tests they verify these properties: +# either the operation succeeds or it raises a JMESPathError. +# If any other exception is raised then we error out. +@settings(**BASE_SETTINGS) +@given(st.text()) +def test_lexer_api(expr): + try: + tokens = list(lexer.Lexer().tokenize(expr)) + except exceptions.EmptyExpressionError: + return + except exceptions.LexerError as e: + assert e.lex_position >= 0, e.lex_position + assert e.lex_position < len(expr), e.lex_position + if expr: + assert expr[e.lex_position] == e.token_value[0], ( + "Lex position does not match first token char.\n" + "Expression: %s\n%s != %s" % (expr, expr[e.lex_position], + e.token_value[0]) + ) + return + except Exception as e: + raise AssertionError("Non JMESPathError raised: %s" % e) + assert isinstance(tokens, list) + # Token starting positions must be unique, can't have two + # tokens with the same start position. + start_locations = [t['start'] for t in tokens] + assert len(set(start_locations)) == len(start_locations), ( + "Tokens must have unique starting locations.") + # Starting positions must be increasing (i.e sorted). + assert sorted(start_locations) == start_locations, ( + "Tokens must have increasing start locations.") + # Last token is always EOF. + assert tokens[-1]['type'] == 'eof' + + +@settings(**BASE_SETTINGS) +@given(st.text()) +def test_parser_api_from_str(expr): + # Same a lexer above with the assumption that we're parsing + # a valid sequence of tokens. + try: + list(lexer.Lexer().tokenize(expr)) + except exceptions.JMESPathError as e: + # We want to try to parse things that tokenize + # properly. + assume(False) + try: + ast = parser.Parser().parse(expr) + except exceptions.JMESPathError as e: + return + except Exception as e: + raise AssertionError("Non JMESPathError raised: %s" % e) + assert isinstance(ast.parsed, dict) + assert 'type' in ast.parsed + assert 'children' in ast.parsed + assert isinstance(ast.parsed['children'], list) + + +@settings(**BASE_SETTINGS) +@given(expr=st.text(), data=RANDOM_JSON) +def test_search_api(expr, data): + try: + ast = parser.Parser().parse(expr) + except exceptions.JMESPathError as e: + # We want to try to parse things that tokenize + # properly. + assume(False) + try: + ast.search(data) + except exceptions.JMESPathError as e: + return + except Exception as e: + raise AssertionError("Non JMESPathError raised: %s" % e) + + +# Additional property tests for functions. + +@given(arg=JSON_NUMBERS) +def test_abs(arg): + assert Functions().call_function('abs', [arg]) >= 0 + + +@given(arg=st.lists(JSON_NUMBERS)) +def test_avg(arg): + result = Functions().call_function('avg', [arg]) + if result is not None: + assert isinstance(result, numbers.Number) + + +@given(arg=st.lists(st.floats() | st.booleans() | st.text() | st.none(), + min_size=1)) +def test_not_null(arg): + result = Functions().call_function('not_null', arg) + if result is not None: + assert result in arg + + +@given(arg=RANDOM_JSON) +def test_to_number(arg): + result = Functions().call_function('to_number', [arg]) + if result is not None: + assert isinstance(result, numbers.Number)