diff -Nru python-gabbi-1.35.0/CONTRIBUTING.md python-gabbi-1.40.0/CONTRIBUTING.md --- python-gabbi-1.35.0/CONTRIBUTING.md 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/CONTRIBUTING.md 2018-01-07 10:31:10.000000000 +0000 @@ -31,6 +31,9 @@ a new branch that will contain your changes. Name the branch something meaningful and related to your change. +See the "Testing and Developing Gabbi" section of the the `README` for +information on setting up a reasonable working environment. + You should provide verbose commit messages on each of your commits. You should not feel obliged to squash your commits into one commit. We want to the see the full expression of your process and thinking. diff -Nru python-gabbi-1.35.0/debian/changelog python-gabbi-1.40.0/debian/changelog --- python-gabbi-1.35.0/debian/changelog 2017-07-31 19:08:21.000000000 +0000 +++ python-gabbi-1.40.0/debian/changelog 2018-02-20 20:45:22.000000000 +0000 @@ -1,3 +1,13 @@ +python-gabbi (1.40.0-0ubuntu1) bionic; urgency=medium + + * New upstream release. + * d/*: wrap-and-sort -bast. + * d/control: Update Standards-Version to 4.1.2. + * d/control: Bump debhelper compat to 10. + * d/control: Align (Build-)Depends with upstream. + + -- Corey Bryant Tue, 20 Feb 2018 15:45:22 -0500 + python-gabbi (1.35.0-0ubuntu1) artful; urgency=medium * New upstream release. diff -Nru python-gabbi-1.35.0/debian/compat python-gabbi-1.40.0/debian/compat --- python-gabbi-1.35.0/debian/compat 2017-07-31 19:08:21.000000000 +0000 +++ python-gabbi-1.40.0/debian/compat 2018-02-20 20:45:22.000000000 +0000 @@ -1 +1 @@ -9 +10 diff -Nru python-gabbi-1.35.0/debian/control python-gabbi-1.40.0/debian/control --- python-gabbi-1.35.0/debian/control 2017-07-31 19:08:21.000000000 +0000 +++ python-gabbi-1.40.0/debian/control 2018-02-20 20:45:22.000000000 +0000 @@ -3,55 +3,71 @@ Priority: optional Maintainer: Ubuntu Developers XSBC-Original-Maintainer: PKG OpenStack -Uploaders: Thomas Goirand , -Build-Depends: debhelper (>= 9), - dh-python, - openstack-pkg-tools (>= 52~), - python-all, - python-pbr, - python-setuptools, - python-sphinx, - python3-all, - python3-pbr, - python3-setuptools, -Build-Depends-Indep: python-colorama, - python-coverage, - python-hacking, - python-httplib2, - python-jsonpath-rw, - python-mock, - python-six, - python-testtools, - python-wsgi-intercept (>= 1.0.0), - python-yaml, - python3-colorama, - python3-httplib2, - python3-jsonpath-rw, - python3-mock, - python3-six, - python3-testtools, - python3-wsgi-intercept (>= 1.0.0), - python3-yaml, - subunit, - testrepository, -Standards-Version: 3.9.8 +Uploaders: + Thomas Goirand , +Build-Depends: + debhelper (>= 10~), + dh-python, + openstack-pkg-tools (>= 52~), + python-all, + python-pbr, + python-setuptools, + python-sphinx, + python3-all, + python3-pbr, + python3-setuptools, + python3-sphinx, +Build-Depends-Indep: + python-colorama, + python-coverage, + python-hacking, + python-httplib2, + python-jsonpath-rw, + python-jsonpath-rw-ext (>= 1.0.0), + python-mock, + python-six, + python-testrepository, + python-testtools, + python-urllib3 (>= 1.11.0), + python-wsgi-intercept (>= 1.2.2), + python-yaml, + python3-colorama, + python3-coverage, + python3-hacking, + python3-httplib2, + python3-jsonpath-rw, + python3-jsonpath-rw-ext (>= 1.0.0), + python3-mock, + python3-six, + python3-testrepository, + python3-testtools, + python3-urllib3 (>= 1.11.0), + python3-wsgi-intercept (>= 1.2.2), + python3-yaml, + subunit, + testrepository, +Standards-Version: 4.1.2 Vcs-Browser: https://git.launchpad.net/~ubuntu-server-dev/ubuntu/+source/python-gabbi Vcs-Git: git://git.launchpad.net/~ubuntu-server-dev/ubuntu/+source/python-gabbi Homepage: https://github.com/cdent/gabbi Package: python-gabbi Architecture: all -Depends: python-colorama, - python-httplib2, - python-jsonpath-rw, - python-pbr, - python-six, - python-testtools, - python-wsgi-intercept (>= 1.0.0), - python-yaml, - ${misc:Depends}, - ${python:Depends}, -Suggests: python-gabbi-doc, +Depends: + python-colorama, + python-httplib2, + python-jsonpath-rw, + python-jsonpath-rw-ext (>= 1.0.0), + python-pbr, + python-six, + python-testtools, + python-urllib3 (>= 1.11.0), + python-wsgi-intercept (>= 1.2.2), + python-yaml, + ${misc:Depends}, + ${python:Depends}, +Suggests: + python-gabbi-doc, Description: declarative HTTP testing library - Python 2.x Gabbi is a tool for running HTTP tests where requests and responses are represented in a declarative form. @@ -81,20 +97,13 @@ . This package contains the Python 2.x module. -Package: python3-gabbi +Package: python-gabbi-doc +Section: doc Architecture: all -Depends: python3-colorama, - python3-httplib2, - python3-jsonpath-rw, - python3-pbr, - python3-six (>= 1.7.0), - python3-testtools, - python3-wsgi-intercept (>= 1.0.0), - python3-yaml, - ${misc:Depends}, - ${python3:Depends}, -Suggests: python-gabbi-doc, -Description: declarative HTTP testing library - Python 3.x +Depends: + ${misc:Depends}, + ${sphinxdoc:Depends}, +Description: declarative HTTP testing library - doc Gabbi is a tool for running HTTP tests where requests and responses are represented in a declarative form. . @@ -121,14 +130,26 @@ both humans (as tools for improving and developing APIs) and automated CI systems. . - This package contains the Python 3.x module. + This package contains the documentation. -Package: python-gabbi-doc -Section: doc +Package: python3-gabbi Architecture: all -Depends: ${misc:Depends}, - ${sphinxdoc:Depends}, -Description: declarative HTTP testing library - doc +Depends: + python3-colorama, + python3-httplib2, + python3-jsonpath-rw, + python3-jsonpath-rw-ext (>= 1.0.0), + python3-pbr, + python3-six, + python3-testtools, + python3-urllib3 (>= 1.11.0), + python3-wsgi-intercept (>= 1.2.2), + python3-yaml, + ${misc:Depends}, + ${python3:Depends}, +Suggests: + python-gabbi-doc, +Description: declarative HTTP testing library - Python 3.x Gabbi is a tool for running HTTP tests where requests and responses are represented in a declarative form. . @@ -155,4 +176,4 @@ both humans (as tools for improving and developing APIs) and automated CI systems. . - This package contains the documentation. + This package contains the Python 3.x module. diff -Nru python-gabbi-1.35.0/docs/source/example.yaml python-gabbi-1.40.0/docs/source/example.yaml --- python-gabbi-1.35.0/docs/source/example.yaml 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/docs/source/example.yaml 2018-01-07 10:31:10.000000000 +0000 @@ -62,10 +62,11 @@ status: 302 # When evaluating response headers it is possible to use a regular -# expression to not have to test the whole value. +# expression to not have to test the whole value. Regular expressions match +# anywhere in the output, not just at the beginning. response_headers: - location: /https/ + location: /^https/ # By default redirects will not be followed. This can be changed. diff -Nru python-gabbi-1.35.0/docs/source/faq.rst python-gabbi-1.40.0/docs/source/faq.rst --- python-gabbi-1.35.0/docs/source/faq.rst 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/docs/source/faq.rst 2018-01-07 10:31:10.000000000 +0000 @@ -47,6 +47,22 @@ python -m testtools.run --load-list \ <(echo package.tests.test_api.yamlfile_get_the_widge.test_request) +How do I run just one test, without running prior tests in a sequence? +---------------------------------------------------------------------- + +By default, when you select a single test to run, all tests prior to that one +in a file will be run as well: the file is treated as as sequence of dependent +tests. If you do not want this you can adjust the ``use_prior_test`` test +:ref:`metadata ` in one of three ways: + +* Set it in the YAML file for the one test you are concerned with. +* Set the ``defaults`` for all tests in that file. +* set ``use_prior_test`` to false when calling :func:`~gabbi.driver.build_tests` + +Be aware that doing this breaks a fundamental assumption that gabbi +makes about how tests work. Any :ref:`substitutions ` +will fail. + Testing Style ~~~~~~~~~~~~~ diff -Nru python-gabbi-1.35.0/docs/source/format.rst python-gabbi-1.40.0/docs/source/format.rst --- python-gabbi-1.35.0/docs/source/format.rst 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/docs/source/format.rst 2018-01-07 10:31:10.000000000 +0000 @@ -29,33 +29,43 @@ Metadata ******** -.. table:: +.. list-table:: + :header-rows: 1 - =========== ================================================= ============ - Key Description Notes - =========== ================================================= ============ - ``name`` The test's name. Must be unique within a file. **required** - - ``desc`` An arbitrary string describing the test. - - ``verbose`` If ``True`` or ``all`` (synonymous), prints a defaults to - representation of the current request and ``False`` - response to ``stdout``, including both headers - and body. If set to ``headers`` or ``body``, only - the corresponding part of the request and - response will be printed. If the output is a TTY, - colors will be used. If the body content-type is - JSON it will be formatted for improved - readability. See - :class:`~gabbi.httpclient.VerboseHttp` for - details. - - ``skip`` A string message which if set will cause the test defaults to - to be skipped with the provided message. ``False`` - - ``xfail`` Determines whether to expect this test to fail. - Note that the test will be run anyway. - =========== ================================================= ============ + * - Key + - Description + - Notes + * - ``name`` + - The test's name. Must be unique within a file. + - **required** + * - ``desc`` + - An arbitrary string describing the test. + - + * - ``verbose`` + - If ``True`` or ``all`` (synonymous), prints a representation of the + current request and response to ``stdout``, including both headers and + body. If set to ``headers`` or ``body``, only the corresponding part of + the request and response will be printed. If the output is a TTY, colors + will be used. If the body content-type is JSON it will be formatted for + improved readability. See :class:`~gabbi.httpclient.VerboseHttp` for + details. + - defaults to ``False`` + * - ``skip`` + - A string message which if set will cause the test to be skipped with the + provided message. + - defaults to ``False`` + * - ``xfail`` + - Determines whether to expect this test to fail. Note that the test will + be run anyway. + - defaults to ``False`` + * - ``use_prior_test`` + - Determines if this test will be run in sequence (after) the test prior + to it in the list of tests within a file. To be concrete, when this is + ``True`` the test is dependent on the prior test and if that prior + has not yet run, it wil be run, even if only the current test has been + selected. Set this to ``False`` to allow selecting a test without + dependencies. + - defaults to ``True`` .. note:: When tests are generated dynamically, the ``TestCase`` name will include the respective test's ``name``, lowercased with spaces @@ -144,7 +154,8 @@ representing expected response header names and values. If a header's value is wrapped in ``/.../``, it will be - treated as a regular expression. + treated as a regular expression to + search for in the response header. ``response_forbidden_headers`` A list of headers which must `not` be present. @@ -161,7 +172,7 @@ If the value is wrapped in ``/.../`` the result of the JSONPath query - will be compared against the + will be searched for the value as a regular expression. ``poll`` A dictionary of two keys: @@ -272,11 +283,11 @@ * ``url`` * ``query_parameters`` * ``data`` -* ``request_headers`` +* ``request_headers`` (in both the key and value) * ``response_strings`` * ``response_json_paths`` (in both the key and value, see :ref:`json path substitution ` for more info) -* ``response_headers`` (on the value side of the key value pair) +* ``response_headers`` (in both the key and value) * ``response_forbidden_headers`` * ``count`` and ``delay`` fields of ``poll`` diff -Nru python-gabbi-1.35.0/docs/source/loader.rst python-gabbi-1.40.0/docs/source/loader.rst --- python-gabbi-1.35.0/docs/source/loader.rst 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/docs/source/loader.rst 2018-01-07 10:31:10.000000000 +0000 @@ -13,6 +13,14 @@ .. note:: It is also possible to run gabbi tests from the command line. See :doc:`runner`. +.. note:: By default gabbi will load YAML files using the ``safe_load`` + function. This means only basic YAML types are allowed in the + file. For most use cases this is fine. If you need custom types + (for example, to match NaN) it is possible to set the ``safe_yaml`` + parameter of :meth:`~gabbi.driver.build_tests` to ``False``. + If custom types are used, please keep in mind that this can limit + the portability of the YAML files to other contexts. + .. warning:: If test are being run with a runner that supports concurrency (such as ``testrepository``) it is critical that the test runner is informed of how to group the diff -Nru python-gabbi-1.35.0/docs/source/release.rst python-gabbi-1.40.0/docs/source/release.rst --- python-gabbi-1.35.0/docs/source/release.rst 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/docs/source/release.rst 2018-01-07 10:31:10.000000000 +0000 @@ -5,6 +5,36 @@ highlighting major features and changes. For more detail see the `commit logs`_ on GitHub. +1.40.0 +----- + +* When the HTTP response begins with a bad status line, have + BadStatusLine be raised from urllib3. + +1.39.0 +------ + +* Allow :ref:`substitutions ` in the key portion + of request and response headers, not just the value. + +1.38.0 +------ + +* Remove support for Python 3.3. +* Make handling of fixture-level skips in pytest actually work. + +1.37.0 +------ + +* Add ``safe_yaml`` parameter to :meth:`~gabbi.driver.build_tests`. + +1.36.0 +------ + +* ``use_prior_test`` is added to test :ref:`metadata`. +* Extensive cleanups in regular expression handling when constructing + tests from YAML. + 1.35.0 ------ @@ -221,6 +251,9 @@ * Mehdi Abaakouk * Tom Viner * Jason Myers +* Josh Leeb-du Toit +* Duc Truong +* Zane Bitter * Ryan Spencer * Kim Raymoure * Travis Truman diff -Nru python-gabbi-1.35.0/gabbi/case.py python-gabbi-1.40.0/gabbi/case.py --- python-gabbi-1.35.0/gabbi/case.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/case.py 2018-01-07 10:31:10.000000000 +0000 @@ -14,7 +14,7 @@ The test case encapsulates the request headers and body and expected response headers and body. When the test is run an HTTP request is -made using urllib3. Assertions are made against the reponse. +made using urllib3. Assertions are made against the response. """ from collections import OrderedDict @@ -23,14 +23,14 @@ import os import re import sys -from testtools import testcase import time -from unittest import result +from unittest import result as unitresult import six from six.moves import http_cookies from six.moves.urllib import parse as urlparse import testtools +from testtools import testcase import wsgi_intercept from gabbi import __version__ @@ -69,6 +69,7 @@ 'xfail': False, 'skip': '', 'poll': {}, + 'use_prior_test': True, } @@ -134,10 +135,11 @@ if self.test_data['skip']: self.skipTest(self.test_data['skip']) - if self.prior and not self.prior.has_run: + if (self.prior and not self.prior.has_run and + self.test_data['use_prior_test']): # Use a different result so we don't count this test # in the results. - self.prior.run(result.TestResult()) + self.prior.run(unitresult.TestResult()) self._run_test() def get_content_handler(self, content_type): @@ -147,15 +149,17 @@ return handler return None - def replace_template(self, message): + def replace_template(self, message, escape_regex=False): """Replace magic strings in message.""" if isinstance(message, dict): for k in message: - message[k] = self.replace_template(message[k]) + message[k] = self.replace_template(message[k], + escape_regex=escape_regex) return message if isinstance(message, list): - return [self.replace_template(line) for line in message] + return [self.replace_template(line, escape_regex=escape_regex) + for line in message] for replacer in REPLACERS: template = '$%s' % replacer @@ -163,7 +167,8 @@ try: if template in message: try: - message = getattr(self, method)(message) + replace = getattr(self, method) + message = replace(message, escape_regex=escape_regex) except (KeyError, AttributeError, ValueError) as exc: raise AssertionError( 'unable to replace %s in %s, data unavailable: %s' @@ -193,14 +198,28 @@ value = value.encode('UTF-8') return value - def _environ_replace(self, message): + @staticmethod + def _regex_replacer(replacer, escape_regex): + """Wrap a replacer function to escape return values in a regex.""" + if escape_regex: + @functools.wraps(replacer) + def replace(match): + return re.escape(replacer(match)) + + return replace + else: + return replacer + + def _environ_replace(self, message, escape_regex=False): """Replace an indicator in a message with the environment value. If value can be a number, cast it as such. If value is a form of "null", "true", or "false" cast it to None, True, False. """ value = re.sub(self._replacer_regex('ENVIRON'), - self._environ_replacer, message) + self._regex_replacer(self._environ_replacer, + escape_regex), + message) try: if '.' in value: value = float(value) @@ -226,13 +245,15 @@ environ_name = match.group('arg') return os.environ[environ_name] - def _cookie_replace(self, message): + def _cookie_replace(self, message, escape_regex=False): """Replace $COOKIE in a message. With cookie data from set-cookie in the prior request. """ return re.sub(self._simple_replacer_regex('COOKIE'), - self._cookie_replacer, message) + self._regex_replacer(self._cookie_replacer, + escape_regex), + message) def _cookie_replacer(self, match): """Replace a regex match with the cookie of a previous response.""" @@ -247,12 +268,14 @@ cookie_string = cookies.output(attrs=[], header='', sep=',').strip() return cookie_string - def _headers_replace(self, message): + def _headers_replace(self, message, escape_regex=False): """Replace a header indicator in a message with that headers value from the prior request. """ return re.sub(self._replacer_regex('HEADERS'), - self._header_replacer, message) + self._regex_replacer(self._header_replacer, + escape_regex), + message) def _header_replacer(self, match): """Replace a regex match with the value of a prior header.""" @@ -264,20 +287,25 @@ referred_case = self.prior return referred_case.response[header_key.lower()] - def _last_url_replace(self, message): + def _last_url_replace(self, message, escape_regex=False): """Replace $LAST_URL in a message. With the URL used in the prior request. """ - return message.replace('$LAST_URL', self.prior.url) + last_url = self.prior.url + if escape_regex: + last_url = re.escape(last_url) + return message.replace('$LAST_URL', last_url) - def _url_replace(self, message): + def _url_replace(self, message, escape_regex=False): """Replace $URL in a message. With the URL used in a previous request. """ return re.sub(self._simple_replacer_regex('URL'), - self._url_replacer, message) + self._regex_replacer(self._url_replacer, + escape_regex), + message) def _url_replacer(self, match): """Replace a regex match with the value of a previous url.""" @@ -288,13 +316,15 @@ referred_case = self.prior return referred_case.url - def _location_replace(self, message): + def _location_replace(self, message, escape_regex=False): """Replace $LOCATION in a message. With the location header from a previous request. """ return re.sub(self._simple_replacer_regex('LOCATION'), - self._location_replacer, message) + self._regex_replacer(self._location_replacer, + escape_regex), + message) def _location_replacer(self, match): """Replace a regex match with the value of a previous location.""" @@ -317,11 +347,13 @@ with open(path, mode='rb') as data_file: return data_file.read() - def _netloc_replace(self, message): + def _netloc_replace(self, message, escape_regex=False): """Replace $NETLOC with the current host and port.""" netloc = self.netloc if self.prefix: netloc = '%s%s' % (netloc, self.prefix) + if escape_regex: + netloc = re.escape(netloc) return message.replace('$NETLOC', netloc) def _parse_url(self, url): @@ -371,7 +403,7 @@ case = HTTPTestCase._history_regex return r"%s\$%s" % (case, key) - def _response_replace(self, message): + def _response_replace(self, message, escape_regex=False): """Replace a content path with the value from a previous response. If the match would replace the entire message, then don't cast it @@ -381,7 +413,10 @@ match = re.match('^%s$' % regex, message) if match: return self._response_replacer(match, preserve=True) - return re.sub(regex, self._response_replacer, message) + return re.sub(regex, + self._regex_replacer(self._response_replacer, + escape_regex), + message) def _response_replacer(self, match, preserve=False): """Replace a regex match with the value from a previous response.""" @@ -443,6 +478,21 @@ self.response_data = None self.output = decoded_output + def _replace_headers_template(self, test_name, headers): + replaced_headers = {} + + try: + for name in headers: + replaced_name = self.replace_template(name) + replaced_headers[replaced_name] = self.replace_template( + headers[name] + ) + except TypeError as exc: + raise exception.GabbiFormatError( + 'malformed headers in test %s: %s' % (test_name, exc)) + + return replaced_headers + def _run_test(self): """Make an HTTP request and compare the response with expectations.""" test = self.test_data @@ -452,14 +502,15 @@ self.url = base_url full_url = self._parse_url(base_url) + # Replace variables in headers with variable values. This includes both + # in the header key and the header value. + test['request_headers'] = self._replace_headers_template( + test['name'], test['request_headers']) + test['response_headers'] = self._replace_headers_template( + test['name'], test['response_headers']) + method = test['method'].upper() headers = test['request_headers'] - for name in headers: - try: - headers[name] = self.replace_template(headers[name]) - except TypeError as exc: - raise exception.GabbiFormatError( - 'malformed headers in test %s: %s' % (test['name'], exc)) if test['data'] != '': body = self._test_data_to_string( @@ -498,9 +549,10 @@ redirect=test['redirects']) self._assert_response() - def _scheme_replace(self, message): + def _scheme_replace(self, message, escape_regex=False): """Replace $SCHEME with the current protocol.""" - return message.replace('$SCHEME', self.scheme) + scheme = re.escape(self.scheme) if escape_regex else self.scheme + return message.replace('$SCHEME', scheme) def _test_data_to_string(self, data, content_type): """Turn the request data into a string. diff -Nru python-gabbi-1.35.0/gabbi/driver.py python-gabbi-1.40.0/gabbi/driver.py --- python-gabbi-1.35.0/gabbi/driver.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/driver.py 2018-01-07 10:31:10.000000000 +0000 @@ -42,10 +42,11 @@ test_loader_name=None, fixture_module=None, response_handlers=None, content_handlers=None, prefix='', require_ssl=False, url=None, - inner_fixtures=None, verbose=False): + inner_fixtures=None, verbose=False, + use_prior_test=True, safe_yaml=True): """Read YAML files from a directory to create tests. - Each YAML file represents an ordered sequence of HTTP requests. + Each YAML file represents a list of HTTP requests. :param path: The directory where yaml files are located. :param loader: The TestLoader. @@ -66,21 +67,26 @@ individual test request. :param verbose: If ``True`` or ``'all'``, make tests verbose by default ``'headers'`` and ``'body'`` are also accepted. + :param use_prior_test: If ``True``, uses prior test to create ordered + sequence of tests + :param safe_yaml: If ``True``, recognizes only standard YAML tags and not + Python object :type inner_fixtures: List of fixtures.Fixture classes. :rtype: TestSuite containing multiple TestSuites (one for each YAML file). """ - # Exit immediately if we have no host to access, either via a real host - # or an intercept. - if not bool(host) ^ bool(intercept): - raise AssertionError('must specify exactly one of host or intercept') - # If url is being used, reset host, port and prefix. if url: host, port, prefix, force_ssl = utils.host_info_from_target(url) if force_ssl and not require_ssl: require_ssl = force_ssl + # Exit immediately if we have no host to access, either via a real host + # or an intercept. + if not bool(host) ^ bool(intercept): + raise AssertionError( + 'must specify exactly one of host or url, or intercept') + # If the client has not provided a name to use as our base, # create one so that tests are effectively namespaced. if test_loader_name is None: @@ -96,8 +102,8 @@ response_handlers = response_handlers or [] content_handlers = content_handlers or [] handler_objects = [] - for handler in (content_handlers + response_handlers - + handlers.RESPONSE_HANDLERS): + for handler in (content_handlers + response_handlers + + handlers.RESPONSE_HANDLERS): handler_objects.append(handler()) top_suite = suite.TestSuite() @@ -108,7 +114,8 @@ % test_file)) if intercept: host = str(uuid.uuid4()) - suite_dict = utils.load_yaml(yaml_file=test_file) + suite_dict = utils.load_yaml(yaml_file=test_file, + safe=safe_yaml) test_base_name = os.path.splitext(os.path.basename(test_file))[0] if all_test_base_name: test_base_name = '%s_%s' % (all_test_base_name, test_base_name) @@ -125,6 +132,12 @@ else: suite_dict['defaults'] = {'verbose': verbose} + if not use_prior_test: + if 'defaults' in suite_dict: + suite_dict['defaults']['use_prior_test'] = use_prior_test + else: + suite_dict['defaults'] = {'use_prior_test': use_prior_test} + file_suite = suitemaker.test_suite_from_dict( loader, test_base_name, suite_dict, path, host, port, fixture_module, intercept, prefix=prefix, @@ -138,7 +151,8 @@ prefix=None, test_loader_name=None, fixture_module=None, response_handlers=None, content_handlers=None, require_ssl=False, url=None, - metafunc=None): + metafunc=None, use_prior_test=True, + inner_fixtures=None, safe_yaml=True): """Generate tests cases for py.test This uses build_tests to create TestCases and then yields them in @@ -158,7 +172,8 @@ response_handlers=response_handlers, content_handlers=content_handlers, prefix=prefix, require_ssl=require_ssl, - url=url) + url=url, use_prior_test=use_prior_test, + safe_yaml=safe_yaml) test_list = [] for test in tests: diff -Nru python-gabbi-1.35.0/gabbi/handlers/core.py python-gabbi-1.40.0/gabbi/handlers/core.py --- python-gabbi-1.35.0/gabbi/handlers/core.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/handlers/core.py 2018-01-07 10:31:10.000000000 +0000 @@ -53,9 +53,14 @@ def action(self, test, header, value=None): header = header.lower() # case-insensitive comparison - response = test.response - header_value = test.replace_template(str(value)) + + header_value = str(value) + is_regex = (header_value.startswith('/') and + header_value.endswith('/') and + len(header_value) > 1) + header_value = test.replace_template(header_value, + escape_regex=is_regex) try: response_value = str(response[header]) @@ -64,8 +69,8 @@ "'%s' header not present in response: %s" % ( header, response.keys())) - if header_value.startswith('/') and header_value.endswith('/'): - header_value = header_value.strip('/').rstrip('/') + if is_regex: + header_value = header_value[1:-1] test.assertRegex( response_value, header_value, 'Expect header %s to match /%s/, got %s' % diff -Nru python-gabbi-1.35.0/gabbi/handlers/jsonhandler.py python-gabbi-1.40.0/gabbi/handlers/jsonhandler.py --- python-gabbi-1.35.0/gabbi/handlers/jsonhandler.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/handlers/jsonhandler.py 2018-01-07 10:31:10.000000000 +0000 @@ -92,13 +92,16 @@ info = six.text_type(info, 'UTF-8') value = self.loads(info) - expected = test.replace_template(value) # If expected is a string, check to see if it is a regex. - if (hasattr(expected, 'startswith') and expected.startswith('/') - and expected.endswith('/')): - expected = expected.strip('/').rstrip('/') + is_regex = (isinstance(value, six.string_types) and + value.startswith('/') and + value.endswith('/') and + len(value) > 1) + expected = test.replace_template(value, escape_regex=is_regex) + if is_regex: + expected = expected[1:-1] # match may be a number so stringify - match = str(match) + match = six.text_type(match) test.assertRegexpMatches( match, expected, 'Expect jsonpath %s to match /%s/, got %s' % diff -Nru python-gabbi-1.35.0/gabbi/httpclient.py python-gabbi-1.40.0/gabbi/httpclient.py --- python-gabbi-1.35.0/gabbi/httpclient.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/httpclient.py 2018-01-07 10:31:10.000000000 +0000 @@ -182,5 +182,5 @@ if verbose == 'headers': body = False return VerboseHttp(body=body, headers=headers, colorize=colorize, - stream=stream, caption=caption) - return Http() + stream=stream, caption=caption, strict=True) + return Http(strict=True) diff -Nru python-gabbi-1.35.0/gabbi/__init__.py python-gabbi-1.40.0/gabbi/__init__.py --- python-gabbi-1.35.0/gabbi/__init__.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/__init__.py 2018-01-07 10:31:10.000000000 +0000 @@ -12,4 +12,4 @@ # under the License. """See gabbi.driver and gabbbi.case.""" -__version__ = '1.35.0' +__version__ = '1.40.0' diff -Nru python-gabbi-1.35.0/gabbi/pytester.py python-gabbi-1.40.0/gabbi/pytester.py --- python-gabbi-1.35.0/gabbi/pytester.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/pytester.py 2018-01-07 10:31:10.000000000 +0000 @@ -90,7 +90,8 @@ if cleanname.startswith('start_'): test = item.callspec.params['test'] result = item.callspec.params['result'] - STARTS[suitename] = (test, result) + # TODO(cdent): Consider a named tuple here + STARTS[suitename] = (test, result, []) deselected.append(item) elif cleanname.startswith('stop_'): test = item.callspec.params['test'] @@ -98,6 +99,9 @@ deselected.append(item) else: remaining.append(item) + # Add each kept test to the start fixture + # in case we need to skip all the tests. + STARTS[suitename][2].append(item) if deselected: items[:] = remaining @@ -123,8 +127,8 @@ run its priors after running this. """ if hasattr(item, 'starter'): - test, result = item.starter - test(result) + test, result, tests = item.starter + test(result, tests) def pytest_runtest_teardown(item, nextitem): diff -Nru python-gabbi-1.35.0/gabbi/runner.py python-gabbi-1.40.0/gabbi/runner.py --- python-gabbi-1.35.0/gabbi/runner.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/runner.py 2018-01-07 10:31:10.000000000 +0000 @@ -89,7 +89,8 @@ if not input_files: success = run_suite(sys.stdin, handler_objects, host, port, prefix, force_ssl, failfast, - verbosity=verbosity) + verbosity=verbosity, + safe_yaml=args.safe_yaml) failure = not success else: for input_file in input_files: @@ -99,7 +100,8 @@ success = run_suite(fh, handler_objects, host, port, prefix, force_ssl, failfast, data_dir=data_dir, - verbosity=verbosity, name=name) + verbosity=verbosity, name=name, + safe_yaml=args.safe_yaml) if not success: failures.append(input_file) if not failure: # once failed, this is considered immutable @@ -114,9 +116,10 @@ def run_suite(handle, handler_objects, host, port, prefix, force_ssl=False, - failfast=False, data_dir='.', verbosity=False, name='input'): + failfast=False, data_dir='.', verbosity=False, name='input', + safe_yaml=True): """Run the tests from the YAML in handle.""" - data = utils.load_yaml(handle) + data = utils.load_yaml(handle, safe=safe_yaml) if force_ssl: if 'defaults' in data: data['defaults']['ssl'] = True @@ -224,6 +227,14 @@ choices=['all', 'body', 'headers'], help='Turn on test verbosity for all tests run in this session.' ) + parser.add_argument( + '--unsafe-yaml', + dest='safe_yaml', + action='store_false', + default=True, + help='Turn on recognition of Python objects in addition to ' + 'standard YAML tags.' + ) return parser diff -Nru python-gabbi-1.35.0/gabbi/suite.py python-gabbi-1.40.0/gabbi/suite.py --- python-gabbi-1.35.0/gabbi/suite.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/suite.py 2018-01-07 10:31:10.000000000 +0000 @@ -82,7 +82,7 @@ return result - def start(self, result): + def start(self, result, tests): """Start fixtures when using pytest.""" fixtures, intercept, host, port, prefix = self._get_intercept() @@ -95,9 +95,9 @@ except unittest.SkipTest as exc: # Disable the already collected tests that we now wish # to skip. - for test in self: + for test in tests: test.run = noop - result.addSkip(test, str(exc)) + test.add_marker('skip') result.addSkip(self, str(exc)) if intercept: intercept_fixture = interceptor.Urllib3Interceptor( diff -Nru python-gabbi-1.35.0/gabbi/tests/gabbits_intercept/fixture.yaml python-gabbi-1.40.0/gabbi/tests/gabbits_intercept/fixture.yaml --- python-gabbi-1.35.0/gabbi/tests/gabbits_intercept/fixture.yaml 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/gabbits_intercept/fixture.yaml 2018-01-07 10:31:10.000000000 +0000 @@ -1,7 +1,7 @@ fixtures: - - TestFixtureOne - - TestFixtureTwo + - FixtureOne + - FixtureTwo tests: - name: just to see diff -Nru python-gabbi-1.35.0/gabbi/tests/gabbits_intercept/header-key.yaml python-gabbi-1.40.0/gabbi/tests/gabbits_intercept/header-key.yaml --- python-gabbi-1.35.0/gabbi/tests/gabbits_intercept/header-key.yaml 1970-01-01 00:00:00.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/gabbits_intercept/header-key.yaml 2018-01-07 10:31:10.000000000 +0000 @@ -0,0 +1,12 @@ +# Test that header keys run the template handling. + +tests: + + - name: header named http + verbose: True + GET: /header_key + request_headers: + $SCHEME: some-scheme + status: 200 + response_headers: + $SCHEME: some-scheme diff -Nru python-gabbi-1.35.0/gabbi/tests/gabbits_runner/nan.yaml python-gabbi-1.40.0/gabbi/tests/gabbits_runner/nan.yaml --- python-gabbi-1.35.0/gabbi/tests/gabbits_runner/nan.yaml 1970-01-01 00:00:00.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/gabbits_runner/nan.yaml 2018-01-07 10:31:10.000000000 +0000 @@ -0,0 +1,8 @@ +tests: + - name: test NAN + url: /nan + method: GET + request_headers: + content-type: application/json + response_json_paths: + $.nan: !!python/object:gabbi.tests.util.NanChecker {} diff -Nru python-gabbi-1.35.0/gabbi/tests/gabbits_unsafe_yaml/nan.yaml python-gabbi-1.40.0/gabbi/tests/gabbits_unsafe_yaml/nan.yaml --- python-gabbi-1.35.0/gabbi/tests/gabbits_unsafe_yaml/nan.yaml 1970-01-01 00:00:00.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/gabbits_unsafe_yaml/nan.yaml 2018-01-07 10:31:10.000000000 +0000 @@ -0,0 +1,9 @@ +tests: + - name: test NAN + url: /nan + method: GET + request_headers: + content-type: application/json + response_json_paths: + $.nan: !NanChecker {} + $.nan: !IsNAN diff -Nru python-gabbi-1.35.0/gabbi/tests/simple_wsgi.py python-gabbi-1.40.0/gabbi/tests/simple_wsgi.py --- python-gabbi-1.35.0/gabbi/tests/simple_wsgi.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/simple_wsgi.py 2018-01-07 10:31:10.000000000 +0000 @@ -112,6 +112,22 @@ query_data['value'][0]}) start_response('200 OK', [('Content-Type', 'application/json')]) return [json_data.encode('utf-8')] + elif path_info == '/nan': + start_response('200 OK', [('Content-Type', 'application/json')]) + return [json.dumps({ + "nan": float('nan') + }).encode('utf-8')] + elif path_info == '/header_key': + scheme_header = environ.get('HTTP_HTTP', False) + + if scheme_header: + headers.append(('HTTP', scheme_header)) + start_response('200 OK', headers) + else: + start_response('500 SERVER ERROR', headers) + + query_output = json.dumps(query_data) + return [query_output.encode('utf-8')] start_response('200 OK', headers) diff -Nru python-gabbi-1.35.0/gabbi/tests/test_driver.py python-gabbi-1.40.0/gabbi/tests/test_driver.py --- python-gabbi-1.35.0/gabbi/tests/test_driver.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/test_driver.py 2018-01-07 10:31:10.000000000 +0000 @@ -28,13 +28,13 @@ self.loader = unittest.defaultTestLoader self.test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) - def test_driver_loads_two_tests(self): + def test_driver_loads_three_tests(self): suite = driver.build_tests(self.test_dir, self.loader, host='localhost', port=8001) self.assertEqual(1, len(suite._tests), 'top level suite contains one suite') - self.assertEqual(2, len(suite._tests[0]._tests), - 'contained suite contains two tests') + self.assertEqual(3, len(suite._tests[0]._tests), + 'contained suite contains three tests') the_one_test = suite._tests[0]._tests[0] self.assertEqual('test_driver_sample_one', the_one_test.__class__.__name__, @@ -56,6 +56,16 @@ with self.assertRaises(AssertionError): driver.build_tests(self.test_dir, self.loader) + def test_build_with_url_provides_host(self): + """This confirms that url provides the required host.""" + suite = driver.build_tests(self.test_dir, self.loader, + url='https://foo.example.com') + first_test = suite._tests[0]._tests[0] + full_url = first_test._parse_url(first_test.test_data['url']) + ssl = first_test.test_data['ssl'] + self.assertEqual('https://foo.example.com/', full_url) + self.assertTrue(ssl) + def test_build_require_ssl(self): suite = driver.build_tests(self.test_dir, self.loader, host='localhost', @@ -87,3 +97,22 @@ first_test = suite._tests[0]._tests[0] full_url = first_test._parse_url(first_test.test_data['url']) self.assertEqual('https://example.com:1024/theend/', full_url) + + def test_build_url_use_prior_test(self): + suite = driver.build_tests(self.test_dir, self.loader, + host='localhost', + use_prior_test=True) + for test in suite._tests[0]._tests: + if test.test_data['name'] != 'use_prior_false': + expected_use_prior = True + else: + expected_use_prior = False + + self.assertEqual(expected_use_prior, + test.test_data['use_prior_test']) + + suite = driver.build_tests(self.test_dir, self.loader, + host='localhost', + use_prior_test=False) + for test in suite._tests[0]._tests: + self.assertEqual(False, test.test_data['use_prior_test']) diff -Nru python-gabbi-1.35.0/gabbi/tests/test_fixtures.py python-gabbi-1.40.0/gabbi/tests/test_fixtures.py --- python-gabbi-1.35.0/gabbi/tests/test_fixtures.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/test_fixtures.py 2018-01-07 10:31:10.000000000 +0000 @@ -21,9 +21,9 @@ class FakeFixture(fixture.GabbiFixture): - def __init__(self, mock): + def __init__(self, _mock): super(FakeFixture, self).__init__() - self.mock = mock + self.mock = _mock def start_fixture(self): self.mock.start() diff -Nru python-gabbi-1.35.0/gabbi/tests/test_gabbits/sample.yaml python-gabbi-1.40.0/gabbi/tests/test_gabbits/sample.yaml --- python-gabbi-1.35.0/gabbi/tests/test_gabbits/sample.yaml 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/test_gabbits/sample.yaml 2018-01-07 10:31:10.000000000 +0000 @@ -1,6 +1,11 @@ +defaults: + use_prior_test: True tests: - name: one url: / - name: two url: http://example.com/moo + - name: use_prior_false + url: http://example.com/foo + use_prior_test: False diff -Nru python-gabbi-1.35.0/gabbi/tests/test_gabbits_pytest.py python-gabbi-1.40.0/gabbi/tests/test_gabbits_pytest.py --- python-gabbi-1.35.0/gabbi/tests/test_gabbits_pytest.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/test_gabbits_pytest.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,39 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -"""Test pytest driving of tests. - -Unittest loaders don't see this file and pytest doesn't see load_tests, -so we manage to get coverage across both types of drivers, from tox, -without duplication. -""" - -import os - -from gabbi import driver -# TODO(cdent): this test_* needs to be imported bare or things do not work -from gabbi.driver import test_pytest # noqa -from gabbi.tests import simple_wsgi -from gabbi.tests import test_intercept -from gabbi.tests import util - -TESTS_DIR = 'gabbits_intercept' - - -def pytest_generate_tests(metafunc): - util.set_test_environ() - test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) - driver.py_test_generator( - test_dir, intercept=simple_wsgi.SimpleWsgi, - fixture_module=test_intercept, - response_handlers=[test_intercept.TestResponseHandler], - metafunc=metafunc) diff -Nru python-gabbi-1.35.0/gabbi/tests/test_handlers.py python-gabbi-1.40.0/gabbi/tests/test_handlers.py --- python-gabbi-1.35.0/gabbi/tests/test_handlers.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/test_handlers.py 2018-01-07 10:31:10.000000000 +0000 @@ -35,7 +35,7 @@ self.test_class = case.HTTPTestCase self.test = suitemaker.TestBuilder('mytest', (self.test_class,), {'test_data': {}, - 'content_handlers': []}) + 'content_handlers': []}) def test_empty_response_handler(self): self.test.test_data = {'url': '$RESPONSE["barnabas"]'} @@ -171,13 +171,76 @@ '$.objects[0].name': '/ow/', }} self.test.response_data = { - 'objects': [{'name': 'cow', + 'objects': [{'name': u'cow\U0001F404', 'location': 'barn'}, {'name': 'chris', 'location': 'house'}] } self._assert_handler(handler) + def test_response_json_paths_regex_path_match(self): + handler = jsonhandler.JSONHandler() + self.test.content_type = "application/json" + self.test.test_data = {'response_json_paths': { + '$.pathtest': '//bar//', + }} + self.test.response_data = { + 'pathtest': '/foo/bar/baz' + } + self._assert_handler(handler) + + def test_response_json_paths_regex_path_nomatch(self): + handler = jsonhandler.JSONHandler() + self.test.content_type = "application/json" + self.test.test_data = {'response_json_paths': { + '$.pathtest': '//bar//', + }} + self.test.response_data = { + 'pathtest': '/foo/foobar/baz' + } + with self.assertRaises(AssertionError): + self._assert_handler(handler) + + def test_response_json_paths_substitution_regex(self): + handler = jsonhandler.JSONHandler() + self.test.location = '/foo/bar' + self.test.prior = self.test + self.test.content_type = "application/json" + self.test.test_data = {'response_json_paths': { + '$.pathtest': '/$LOCATION/', + }} + self.test.response_data = { + 'pathtest': '/foo/bar/baz' + } + self._assert_handler(handler) + + def test_response_json_paths_substitution_noregex(self): + handler = jsonhandler.JSONHandler() + self.test.location = '/foo/bar/' + self.test.prior = self.test + self.test.content_type = "application/json" + self.test.test_data = {'response_json_paths': { + '$.pathtest': '$LOCATION', + }} + self.test.response_data = { + 'pathtest': '/foo/bar/baz' + } + with self.assertRaises(AssertionError): + self._assert_handler(handler) + + def test_response_json_paths_substitution_esc_regex(self): + handler = jsonhandler.JSONHandler() + self.test.location = '/foo/bar?query' + self.test.prior = self.test + self.test.content_type = "application/json" + self.test.test_data = {'response_json_paths': { + '$.pathtest': '/$LOCATION/', + }} + self.test.response_data = { + 'pathtest': '/foo/bar?query=value' + } + self._assert_handler(handler) + def test_response_json_paths_regex_number(self): handler = jsonhandler.JSONHandler() self.test.content_type = "application/json" @@ -227,6 +290,70 @@ self.test.response = {'content-type': 'text/plain; charset=UTF-8'} self._assert_handler(handler) + def test_response_headers_substitute_noregex(self): + handler = core.HeadersResponseHandler() + self.test.location = '/foo/bar/' + self.test.prior = self.test + self.test.test_data = {'response_headers': { + 'location': '$LOCATION', + }} + self.test.response = {'location': '/foo/bar/baz'} + with self.assertRaises(AssertionError): + self._assert_handler(handler) + + def test_response_headers_substitute_regex(self): + handler = core.HeadersResponseHandler() + self.test.location = '/foo/bar/' + self.test.prior = self.test + self.test.test_data = {'response_headers': { + 'location': '/^$LOCATION/', + }} + self.test.response = {'location': '/foo/bar/baz'} + self._assert_handler(handler) + + def test_response_headers_substitute_esc_regex(self): + handler = core.HeadersResponseHandler() + self.test.scheme = 'git+ssh' + self.test.test_data = {'response_headers': { + 'location': '/^$SCHEME://.*/', + }} + self.test.response = {'location': 'git+ssh://example.test'} + self._assert_handler(handler) + + def test_response_headers_noregex_path_match(self): + handler = core.HeadersResponseHandler() + self.test.test_data = {'response_headers': { + 'location': '/', + }} + self.test.response = {'location': '/'} + self._assert_handler(handler) + + def test_response_headers_noregex_path_nomatch(self): + handler = core.HeadersResponseHandler() + self.test.test_data = {'response_headers': { + 'location': '/', + }} + self.test.response = {'location': '/foo'} + with self.assertRaises(AssertionError): + self._assert_handler(handler) + + def test_response_headers_regex_path_match(self): + handler = core.HeadersResponseHandler() + self.test.test_data = {'response_headers': { + 'location': '//bar//', + }} + self.test.response = {'location': '/foo/bar/baz'} + self._assert_handler(handler) + + def test_response_headers_regex_path_nomatch(self): + handler = core.HeadersResponseHandler() + self.test.test_data = {'response_headers': { + 'location': '//bar//', + }} + self.test.response = {'location': '/foo/foobar/baz'} + with self.assertRaises(AssertionError): + self._assert_handler(handler) + def test_response_headers_fail_data(self): handler = core.HeadersResponseHandler() self.test.test_data = {'response_headers': { diff -Nru python-gabbi-1.35.0/gabbi/tests/test_history.py python-gabbi-1.40.0/gabbi/tests/test_history.py --- python-gabbi-1.35.0/gabbi/tests/test_history.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/test_history.py 2018-01-07 10:31:10.000000000 +0000 @@ -50,6 +50,15 @@ self.test.test_data) self.assertEqual('test_content', header) + def test_header_replace_with_history_regex(self): + self.test.test_data = '/$HISTORY["mytest"].$HEADERS["content-type"]/' + self.test.response = {'content-type': 'test+content'} + self.test.history["mytest"] = self.test + + header = self.test('test_request').replace_template( + self.test.test_data, escape_regex=True) + self.assertEqual(r'/test\+content/', header) + def test_response_replace_prior(self): self.test.test_data = '$RESPONSE["$.object.name"]' json_handler = jsonhandler.JSONHandler() @@ -65,6 +74,21 @@ self.test.test_data) self.assertEqual('test history', response) + def test_response_replace_prior_regex(self): + self.test.test_data = '/$RESPONSE["$.object.name"]/' + json_handler = jsonhandler.JSONHandler() + self.test.content_type = "application/json" + self.test.content_handlers = [json_handler] + self.test.prior = self.test + self.test.response = {'content-type': 'application/json'} + self.test.response_data = { + 'object': {'name': 'test history.'} + } + + response = self.test('test_request').replace_template( + self.test.test_data, escape_regex=True) + self.assertEqual(r'/test\ history\./', response) + def test_response_replace_with_history(self): self.test.test_data = '$HISTORY["mytest"].$RESPONSE["$.object.name"]' json_handler = jsonhandler.JSONHandler() @@ -89,6 +113,15 @@ self.test.test_data) self.assertEqual('test=cookie', cookie) + def test_cookie_replace_prior_regex(self): + self.test.test_data = '/$COOKIE/' + self.test.response = {'set-cookie': 'test=cookie?'} + self.test.prior = self.test + + cookie = self.test('test_request').replace_template( + self.test.test_data, escape_regex=True) + self.assertEqual(r'/test\=cookie\?/', cookie) + def test_cookie_replace_history(self): self.test.test_data = '$HISTORY["mytest"].$COOKIE' self.test.response = {'set-cookie': 'test=cookie'} @@ -107,6 +140,15 @@ self.test.test_data) self.assertEqual('test_location', location) + def test_location_replace_prior_regex(self): + self.test.test_data = '/$LOCATION/' + self.test.location = '..' + self.test.prior = self.test + + location = self.test('test_request').replace_template( + self.test.test_data, escape_regex=True) + self.assertEqual(r'/\.\./', location) + def test_location_replace_history(self): self.test.test_data = '$HISTORY["mytest"].$LOCATION' self.test.location = 'test_location' @@ -125,6 +167,15 @@ self.test.test_data) self.assertEqual('test_url', url) + def test_url_replace_prior_regex(self): + self.test.test_data = '/$URL/' + self.test.url = 'testurl?query' + self.test.prior = self.test + + url = self.test('test_request').replace_template( + self.test.test_data, escape_regex=True) + self.assertEqual(r'/testurl\?query/', url) + def test_url_replace_history(self): self.test.test_data = '$HISTORY["mytest"].$URL' self.test.url = 'test_url' diff -Nru python-gabbi-1.35.0/gabbi/tests/test_inner_fixture.py python-gabbi-1.40.0/gabbi/tests/test_inner_fixture.py --- python-gabbi-1.35.0/gabbi/tests/test_inner_fixture.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/test_inner_fixture.py 2018-01-07 10:31:10.000000000 +0000 @@ -21,6 +21,10 @@ import fixtures from gabbi import driver +# TODO(cdent): test_pytest allows pytest to see the tests this module +# produces. Without it, the generator will not run. It is a todo because +# needing to do this is annoying and gross. +from gabbi.driver import test_pytest # noqa from gabbi import fixture from gabbi.tests import simple_wsgi @@ -54,10 +58,21 @@ assert 1 <= COUNT_INNER <= 3 +BUILD_TEST_ARGS = dict( + intercept=simple_wsgi.SimpleWsgi, + fixture_module=sys.modules[__name__], + inner_fixtures=[InnerFixture], +) + + def load_tests(loader, tests, pattern): test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) - return driver.build_tests(test_dir, loader, host=None, - intercept=simple_wsgi.SimpleWsgi, - fixture_module=sys.modules[__name__], - inner_fixtures=[InnerFixture], - test_loader_name=__name__) + return driver.build_tests(test_dir, loader, + test_loader_name=__name__, + **BUILD_TEST_ARGS) + + +def pytest_generate_tests(metafunc): + test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) + driver.py_test_generator(test_dir, metafunc=metafunc, + **BUILD_TEST_ARGS) diff -Nru python-gabbi-1.35.0/gabbi/tests/test_intercept.py python-gabbi-1.40.0/gabbi/tests/test_intercept.py --- python-gabbi-1.35.0/gabbi/tests/test_intercept.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/test_intercept.py 2018-01-07 10:31:10.000000000 +0000 @@ -20,6 +20,10 @@ import sys from gabbi import driver +# TODO(cdent): test_pytest allows pytest to see the tests this module +# produces. Without it, the generator will not run. It is a todo because +# needing to do this is annoying and gross. +from gabbi.driver import test_pytest # noqa from gabbi import fixture from gabbi.handlers import base from gabbi.tests import simple_wsgi @@ -29,17 +33,17 @@ TESTS_DIR = 'gabbits_intercept' -class TestFixtureOne(fixture.GabbiFixture): +class FixtureOne(fixture.GabbiFixture): """Drive the fixture testing weakly.""" pass -class TestFixtureTwo(fixture.GabbiFixture): +class FixtureTwo(fixture.GabbiFixture): """Drive the fixture testing weakly.""" pass -class TestResponseHandler(base.ResponseHandler): +class StubResponseHandler(base.ResponseHandler): """A sample response handler just to test.""" test_key_suffix = 'test' @@ -62,16 +66,27 @@ SkipAllFixture = fixture.SkipAllFixture +BUILD_TEST_ARGS = dict( + intercept=simple_wsgi.SimpleWsgi, + fixture_module=sys.modules[__name__], + prefix=os.environ.get('GABBI_PREFIX'), + response_handlers=[StubResponseHandler] +) + + def load_tests(loader, tests, pattern): """Provide a TestSuite to the discovery process.""" # Set and environment variable for one of the tests. util.set_test_environ() - prefix = os.environ.get('GABBI_PREFIX') test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) - return driver.build_tests(test_dir, loader, host=None, - intercept=simple_wsgi.SimpleWsgi, + return driver.build_tests(test_dir, loader, test_loader_name=__name__, - prefix=prefix, - fixture_module=sys.modules[__name__], - response_handlers=[TestResponseHandler]) + **BUILD_TEST_ARGS) + + +def pytest_generate_tests(metafunc): + util.set_test_environ() + test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) + driver.py_test_generator(test_dir, metafunc=metafunc, + **BUILD_TEST_ARGS) diff -Nru python-gabbi-1.35.0/gabbi/tests/test_live.py python-gabbi-1.40.0/gabbi/tests/test_live.py --- python-gabbi-1.35.0/gabbi/tests/test_live.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/test_live.py 2018-01-07 10:31:10.000000000 +0000 @@ -19,6 +19,10 @@ from unittest import case from gabbi import driver +# TODO(cdent): test_pytest allows pytest to see the tests this module +# produces. Without it, the generator will not run. It is a todo because +# needing to do this is annoying and gross. +from gabbi.driver import test_pytest # noqa from gabbi import fixture @@ -33,9 +37,19 @@ raise case.SkipTest('live tests skipped') +BUILD_TEST_ARGS = dict( + host='google.com', + fixture_module=sys.modules[__name__], + port=443 +) + + def load_tests(loader, tests, pattern): """Provide a TestSuite to the discovery process.""" test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) - return driver.build_tests(test_dir, loader, host='google.com', - fixture_module=sys.modules[__name__], - port=443) + return driver.build_tests(test_dir, loader, **BUILD_TEST_ARGS) + + +def pytest_generate_tests(metafunc): + test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) + driver.py_test_generator(test_dir, metafunc=metafunc, **BUILD_TEST_ARGS) diff -Nru python-gabbi-1.35.0/gabbi/tests/test_replacers.py python-gabbi-1.40.0/gabbi/tests/test_replacers.py --- python-gabbi-1.35.0/gabbi/tests/test_replacers.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/test_replacers.py 2018-01-07 10:31:10.000000000 +0000 @@ -18,6 +18,7 @@ import unittest from gabbi import case +from gabbi import exception class EnvironReplaceTest(unittest.TestCase): @@ -56,3 +57,13 @@ os.environ['moo'] = "True" self.assertEqual(True, http_case._environ_replace(message)) + + +class TestReplaceHeaders(unittest.TestCase): + + def test_empty_headers(self): + """A None value in headers should cause a GabbiFormatError.""" + http_case = case.HTTPTestCase('test_request') + self.assertRaises( + exception.GabbiFormatError, + http_case._replace_headers_template, 'foo', None) diff -Nru python-gabbi-1.35.0/gabbi/tests/test_runner.py python-gabbi-1.40.0/gabbi/tests/test_runner.py --- python-gabbi-1.35.0/gabbi/tests/test_runner.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/test_runner.py 2018-01-07 10:31:10.000000000 +0000 @@ -85,6 +85,19 @@ except SystemExit as err: self.assertFailure(err) + def test_unsafe_yaml(self): + sys.argv = ['gabbi-run', 'http://%s:%s/nan' % (self.host, self.port)] + + sys.argv.append('--unsafe-yaml') + sys.argv.append('--') + sys.argv.append('gabbi/tests/gabbits_runner/nan.yaml') + + with self.server(): + try: + runner.run() + except SystemExit as err: + self.assertSuccess(err) + def test_target_url_parsing(self): sys.argv = ['gabbi-run', 'http://%s:%s/foo' % (self.host, self.port)] @@ -167,7 +180,7 @@ self.assertFailure(err) sys.argv.insert(3, "-r") - sys.argv.insert(4, "gabbi.tests.test_intercept:TestResponseHandler") + sys.argv.insert(4, "gabbi.tests.test_intercept:StubResponseHandler") sys.stdin = StringIO(""" tests: diff -Nru python-gabbi-1.35.0/gabbi/tests/test_unsafe_yaml.py python-gabbi-1.40.0/gabbi/tests/test_unsafe_yaml.py --- python-gabbi-1.35.0/gabbi/tests/test_unsafe_yaml.py 1970-01-01 00:00:00.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/test_unsafe_yaml.py 2018-01-07 10:31:10.000000000 +0000 @@ -0,0 +1,54 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""A sample test module to exercise the code. + +For the sake of exploratory development. +""" + +import os + +import yaml + +from gabbi import driver +# TODO(cdent): test_pytest allows pytest to see the tests this module +# produces. Without it, the generator will not run. It is a todo because +# needing to do this is annoying and gross. +from gabbi.driver import test_pytest # noqa +from gabbi.tests import simple_wsgi +from gabbi.tests import util + + +TESTS_DIR = 'gabbits_unsafe_yaml' + + +yaml.add_constructor(u'!IsNAN', lambda loader, node: util.NanChecker()) + + +BUILD_TEST_ARGS = dict( + intercept=simple_wsgi.SimpleWsgi, + safe_yaml=False +) + + +def load_tests(loader, tests, pattern): + """Provide a TestSuite to the discovery process.""" + test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) + return driver.build_tests(test_dir, loader, + test_loader_name=__name__, + **BUILD_TEST_ARGS) + + +def pytest_generate_tests(metafunc): + test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) + driver.py_test_generator(test_dir, metafunc=metafunc, **BUILD_TEST_ARGS) diff -Nru python-gabbi-1.35.0/gabbi/tests/test_use_prior_test.py python-gabbi-1.40.0/gabbi/tests/test_use_prior_test.py --- python-gabbi-1.35.0/gabbi/tests/test_use_prior_test.py 1970-01-01 00:00:00.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/test_use_prior_test.py 2018-01-07 10:31:10.000000000 +0000 @@ -0,0 +1,64 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Test use_prior_test directive. +""" + +import copy +from six.moves import mock +import unittest + +from gabbi import case + + +class UsePriorTest(unittest.TestCase): + + @staticmethod + def make_test_case(use_prior_test=None): + http_case = case.HTTPTestCase('test_request') + http_case.test_data = copy.copy(case.BASE_TEST) + if use_prior_test is not None: + http_case.test_data['use_prior_test'] = use_prior_test + return http_case + + @mock.patch('gabbi.case.HTTPTestCase._run_test') + def test_use_prior_true(self, m_run_test): + http_case = self.make_test_case(True) + http_case.has_run = False + http_case.prior = self.make_test_case(True) + http_case.prior.run = mock.MagicMock(unsafe=True) + http_case.prior.has_run = False + + http_case.test_request() + http_case.prior.run.assert_called_once() + + @mock.patch('gabbi.case.HTTPTestCase._run_test') + def test_use_prior_false(self, m_run_test): + http_case = self.make_test_case(False) + http_case.has_run = False + http_case.prior = self.make_test_case(True) + http_case.prior.run = mock.MagicMock(unsafe=True) + http_case.prior.has_run = False + + http_case.test_request() + http_case.prior.run.assert_not_called() + + @mock.patch('gabbi.case.HTTPTestCase._run_test') + def test_use_prior_default_true(self, m_run_test): + http_case = self.make_test_case() + http_case.has_run = False + http_case.prior = self.make_test_case(True) + http_case.prior.run = mock.MagicMock(unsafe=True) + http_case.prior.has_run = False + + http_case.test_request() + http_case.prior.run.assert_called_once() diff -Nru python-gabbi-1.35.0/gabbi/tests/util.py python-gabbi-1.40.0/gabbi/tests/util.py --- python-gabbi-1.35.0/gabbi/tests/util.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/tests/util.py 2018-01-07 10:31:10.000000000 +0000 @@ -12,8 +12,11 @@ # under the License. """Utility methods shared by some tests.""" +import math import os +import yaml + def set_test_environ(): """Set some environment variables used in tests.""" @@ -27,3 +30,13 @@ os.environ['FALSE'] = 'false' os.environ['STRING'] = 'val' os.environ['NULL'] = 'null' + + +class NanChecker(yaml.YAMLObject): + yaml_tag = u'!NanChecker' + + def __eq__(self, other): + try: + return math.isnan(other) + except TypeError: + return False diff -Nru python-gabbi-1.35.0/gabbi/utils.py python-gabbi-1.40.0/gabbi/utils.py --- python-gabbi-1.35.0/gabbi/utils.py 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/gabbi/utils.py 2018-01-07 10:31:10.000000000 +0000 @@ -99,19 +99,21 @@ return lambda x, y: y -def load_yaml(handle=None, yaml_file=None): +def load_yaml(handle=None, yaml_file=None, safe=True): """Read and parse any YAML file or filehandle. Let exceptions flow where they may. If no file or handle is provided, read from STDIN. """ + load = yaml.safe_load if safe else yaml.load + if yaml_file: with io.open(yaml_file, encoding='utf-8') as source: - return yaml.safe_load(source.read()) + return load(source.read()) # This will intentionally raise AttributeError if handle is None. - return yaml.safe_load(handle.read()) + return load(handle.read()) def not_binary(content_type): diff -Nru python-gabbi-1.35.0/.pyup.yml python-gabbi-1.40.0/.pyup.yml --- python-gabbi-1.35.0/.pyup.yml 1970-01-01 00:00:00.000000000 +0000 +++ python-gabbi-1.40.0/.pyup.yml 2018-01-07 10:31:10.000000000 +0000 @@ -0,0 +1,4 @@ +# autogenerated pyup.io config file +# see https://pyup.io/docs/configuration/ for all available options + +update: insecure diff -Nru python-gabbi-1.35.0/README.rst python-gabbi-1.40.0/README.rst --- python-gabbi-1.35.0/README.rst 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/README.rst 2018-01-07 10:31:10.000000000 +0000 @@ -64,16 +64,29 @@ useful for both humans (as tools for improving and developing APIs) and automated CI systems. -Testing -------- +Testing and Developing Gabbi +---------------------------- -To run the built in tests (the YAML files are in the directories -``gabbi/gabbits_*`` and loaded by the file ``gabbi/test_*.py``), -you can use ``tox``:: +To get started, after cloning the `repository`_, you should install the +development dependencies:: + + $ pip install -r requirements-dev.txt + +If you prefer to keep things isolated you can create a virtual +environment:: + + $ virtualenv gabbi-venv + $ . gabbi-venv/bin/activate + $ pip install -r requirements-dev.txt + +Gabbi is set up to be developed and tested using `tox`_ (installed via +``requirements-dev.txt``). To run the built-in tests (the YAML files +are in the directories ``gabbi/tests/gabbits_*`` and loaded by the file +``gabbi/test_*.py``), you call ``tox``:: tox -epep8,py27,py34 -Or if you have the dependencies installed (or a warmed up +If you have the dependencies installed (or a warmed up virtualenv) you can run the tests by hand and exit on the first failure:: @@ -86,3 +99,6 @@ If you wish to avoid running tests that connect to internet hosts, set ``GABBI_SKIP_NETWORK`` to ``True``. + +.. _tox: https://tox.readthedocs.io/ +.. _repository: https://github.com/cdent/gabbi diff -Nru python-gabbi-1.35.0/requirements-dev.txt python-gabbi-1.40.0/requirements-dev.txt --- python-gabbi-1.35.0/requirements-dev.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-gabbi-1.40.0/requirements-dev.txt 2018-01-07 10:31:10.000000000 +0000 @@ -0,0 +1 @@ +tox diff -Nru python-gabbi-1.35.0/setup.cfg python-gabbi-1.40.0/setup.cfg --- python-gabbi-1.35.0/setup.cfg 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/setup.cfg 2018-01-07 10:31:10.000000000 +0000 @@ -16,7 +16,6 @@ Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.3 Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 diff -Nru python-gabbi-1.35.0/test-failskip.sh python-gabbi-1.40.0/test-failskip.sh --- python-gabbi-1.35.0/test-failskip.sh 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/test-failskip.sh 2018-01-07 10:31:10.000000000 +0000 @@ -7,12 +7,14 @@ [[ "${GABBI_SKIP_NETWORK:-false}" == "true" ]] && SKIP=7 || SKIP=2 shopt -u nocasematch -GREP_FAIL_MATCH='expected failures=12,' +FAILS=12 + +GREP_FAIL_MATCH="expected failures=$FAILS," GREP_SKIP_MATCH="skipped=$SKIP," GREP_UXSUC_MATCH='unexpected successes=1' # This skip is always 2 because the pytest tests don't # run the live tests. -PYTEST_MATCH='2 skipped, 12 xfailed' +PYTEST_MATCH="$SKIP skipped, $FAILS xfailed" python setup.py testr && \ for match in "${GREP_FAIL_MATCH}" "${GREP_UXSUC_MATCH}" "${GREP_SKIP_MATCH}"; do diff -Nru python-gabbi-1.35.0/tox.ini python-gabbi-1.40.0/tox.ini --- python-gabbi-1.35.0/tox.ini 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/tox.ini 2018-01-07 10:31:10.000000000 +0000 @@ -1,7 +1,7 @@ [tox] minversion = 1.6 skipsdist = True -envlist = py27,py33,py34,py35,py36,pypy,pep8,limit,failskip,docs,py35-prefix,py35-limit,py35-failskip,py27-pytest,py35-pytest,py36-pytest +envlist = py27,py34,py35,py36,pypy,pep8,limit,failskip,docs,py35-prefix,py35-limit,py35-failskip,py27-pytest,py35-pytest,py36-pytest [testenv] deps = -r{toxinidir}/requirements.txt @@ -59,9 +59,25 @@ deps = -egit+https://github.com/gnocchixyz/gnocchi#egg=gnocchi tox changedir = {envdir}/src/gnocchi -commands = tox -e py27-gabbi --notest # ensure a virtualenv is built - {envdir}/src/gnocchi/.tox/py27-gabbi/bin/pip install -U {toxinidir} # install gabbi - tox -e py27-gabbi +commands = tox -e py27-postgresql-file --notest # ensure a virtualenv is built + {envdir}/src/gnocchi/.tox/py27-postgresql-file/bin/pip install -U {toxinidir} # install gabbi + tox -e py27-postgresql-file gabbi + +[testenv:placement] +basepython = python2.7 +deps = tox +commands = -mkdir {envdir}/src + -rm -r {envdir}/src/* + bash -c "curl https://tarballs.openstack.org/nova/nova-master.tar.gz | tar -C {envdir}/src -zxv --strip-components 1 -f - " + tox -c {envdir}/src -e functional --notest # ensure a virtualenv is built + {envdir}/src/.tox/functional/bin/pip install -U {toxinidir} # install gabbi + tox -c {envdir}/src -e functional test_placement_api +whitelist_externals = + mkdir + curl + tar + rm + bash [testenv:docs] commands = diff -Nru python-gabbi-1.35.0/.travis.yml python-gabbi-1.40.0/.travis.yml --- python-gabbi-1.35.0/.travis.yml 2017-07-07 13:23:25.000000000 +0000 +++ python-gabbi-1.40.0/.travis.yml 2018-01-07 10:31:10.000000000 +0000 @@ -1,18 +1,38 @@ sudo: false language: python +services: + # For Gnocchi + - docker install: - - pip install tox + - | + if [ "$TOXENV" == "gnocchi" ]; then + docker pull gnocchixyz/ci-tools:latest + else + pip install tox + fi script: - - tox + - | + case "$TOXENV" in + gnocchi) + docker run -v ~/.cache/pip:/home/tester/.cache/pip -v $(pwd):/home/tester/src gnocchixyz/ci-tools:latest tox -e ${TOXENV} + ;; + *) + tox + ;; + esac matrix: include: - env: TOXENV=py27 - - env: TOXENV=py33 - - env: TOXENV=py34 - - env: TOXENV=pypy - env: TOXENV=pep8 - env: TOXENV=py27-pytest - env: TOXENV=gnocchi + - env: TOXENV=placement + - python: 3.4 + env: TOXENV=py34 + - python: pypy + env: TOXENV=pypy + - python: pypy3 + env: TOXENV=pypy3 - python: 3.5 env: TOXENV=py35 - python: 3.5 @@ -27,8 +47,6 @@ env: TOXENV=py36 - python: 3.6 env: TOXENV=py36-pytest - allow_failures: - - env: TOXENV=gnocchi notifications: irc: "chat.freenode.net#gabbi"