Skip to content

Commit 2aa3ca2

Browse files
committed
Integrate hypothesis testing into jmespath
These new tests found 2 bugs in the lexer/parser, so this commit also contains the fixes for these bugs. I've added support for bumping up the max_examples run so I can have travis run these tests for a little longer. These don't really need to run every python version, so for now I've added longer hypothesis runs on py27 on travis. hypothesis isn't supported on python2.6, so these tests will be skipped when running on py26. I also had to add a setting option that disables a healthcheck that fails on pypy.
1 parent 4eaa65b commit 2aa3ca2

File tree

6 files changed

+114
-2
lines changed

6 files changed

+114
-2
lines changed

.travis.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ install:
1414
- pip install -r requirements.txt
1515
- python setup.py bdist_wheel
1616
- pip install dist/*.whl
17-
script: cd tests/ && nosetests --with-coverage --cover-package jmespath .
17+
script:
18+
- cd tests/ && nosetests --with-coverage --cover-package jmespath .
19+
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then JP_MAX_EXAMPLES=10000 nosetests test_hypothesis.py; fi
1820
after_success:
1921
- codecov

jmespath/lexer.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,15 @@ def tokenize(self, expression):
8787
elif self._current == '!':
8888
yield self._match_or_else('=', 'ne', 'not')
8989
elif self._current == '=':
90-
yield self._match_or_else('=', 'eq', 'unknown')
90+
if self._next() == '=':
91+
yield {'type': 'eq', 'value': '==',
92+
'start': self._position - 1, 'end': self._position}
93+
self._next()
94+
else:
95+
raise LexerError(
96+
lexer_position=self._position - 1,
97+
lexer_value='=',
98+
message="Unknown token =")
9199
else:
92100
raise LexerError(lexer_position=self._position,
93101
lexer_value=self._current,

jmespath/parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class Parser(object):
3939
'eof': 0,
4040
'unquoted_identifier': 0,
4141
'quoted_identifier': 0,
42+
'literal': 0,
4243
'rbracket': 0,
4344
'rparen': 0,
4445
'comma': 0,

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ py==1.4.12
33
tox==1.4.2
44
wheel==0.24.0
55
coverage==3.7.1
6+
hypothesis==3.1.0

tests/compliance/syntax.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@
9595
{
9696
"expression": "!",
9797
"error": "syntax"
98+
},
99+
{
100+
"expression": "@=",
101+
"error": "syntax"
102+
},
103+
{
104+
"expression": "@``",
105+
"error": "syntax"
98106
}
99107
]
100108
},

tests/test_hypothesis.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Test suite using hypothesis to generate test cases.
2+
# This is in a standalone module so that these tests
3+
# can a) be run separately and b) allow for customization
4+
# via env var for longer runs in travis.
5+
import os
6+
import sys
7+
8+
from nose.plugins.skip import SkipTest
9+
from hypothesis import given, settings, assume, HealthCheck
10+
import hypothesis.strategies as st
11+
12+
from jmespath import lexer
13+
from jmespath import parser
14+
from jmespath import exceptions
15+
16+
17+
MAX_EXAMPLES = int(os.environ.get('JP_MAX_EXAMPLES', 100))
18+
if sys.version_info[:2] != (2, 6):
19+
RANDOM_JSON = st.recursive(
20+
st.floats() | st.booleans() | st.text() | st.none(),
21+
lambda children: st.lists(children) | st.dictionaries(st.text(), children)
22+
)
23+
else:
24+
# hypothesis doesn't work on py26 and the prevous definition of
25+
# RANDOM_JSON would throw an error. We set RANDOM_JSON to some
26+
# arbitrary value that we know will work. This isn't going to get
27+
# called anyways.
28+
# XXX: Is there a better way to do this?
29+
RANDOM_JSON = st.none()
30+
31+
32+
BASE_SETTINGS = {
33+
'max_examples': MAX_EXAMPLES,
34+
'suppress_health_check': [HealthCheck.too_slow],
35+
}
36+
37+
38+
def setup_module():
39+
if sys.version_info[:2] == (2, 6):
40+
raise SkipTest("Hypothesis test not supported on py26")
41+
42+
43+
# For all of these tests they verify these proprties:
44+
# either the operation succeeds or it raises a JMESPathError.
45+
# If any other exception is raised then we error out.
46+
@settings(**BASE_SETTINGS)
47+
@given(st.text())
48+
def test_lexer_api(expr):
49+
try:
50+
tokens = list(lexer.Lexer().tokenize(expr))
51+
except exceptions.JMESPathError as e:
52+
return
53+
except Exception as e:
54+
raise AssertionError("Non JMESPathError raised: %s" % e)
55+
assert isinstance(tokens, list)
56+
57+
58+
@settings(**BASE_SETTINGS)
59+
@given(st.text())
60+
def test_parser_api_from_str(expr):
61+
# Same a lexer above with the assumption that we're parsing
62+
# a valid sequence of tokens.
63+
try:
64+
list(lexer.Lexer().tokenize(expr))
65+
except exceptions.JMESPathError as e:
66+
# We want to try to parse things that tokenize
67+
# properly.
68+
assume(False)
69+
try:
70+
ast = parser.Parser().parse(expr)
71+
except exceptions.JMESPathError as e:
72+
return
73+
except Exception as e:
74+
raise AssertionError("Non JMESPathError raised: %s" % e)
75+
assert isinstance(ast.parsed, dict)
76+
77+
78+
@settings(**BASE_SETTINGS)
79+
@given(expr=st.text(), data=RANDOM_JSON)
80+
def test_search_api(expr, data):
81+
try:
82+
ast = parser.Parser().parse(expr)
83+
except exceptions.JMESPathError as e:
84+
# We want to try to parse things that tokenize
85+
# properly.
86+
assume(False)
87+
try:
88+
ast.search(data)
89+
except exceptions.JMESPathError as e:
90+
return
91+
except Exception as e:
92+
raise AssertionError("Non JMESPathError raised: %s" % e)

0 commit comments

Comments
 (0)