pax_global_header00006660000000000000000000000064133231711770014517gustar00rootroot0000000000000052 comment=b773ea8a585c95c6e21cce506871c39da845622b gabbi-1.44.0/000077500000000000000000000000001332317117700126515ustar00rootroot00000000000000gabbi-1.44.0/.coveragerc000066400000000000000000000000331332317117700147660ustar00rootroot00000000000000[run] omit = gabbi/tests/* gabbi-1.44.0/.gitignore000066400000000000000000000003021332317117700146340ustar00rootroot00000000000000*.pyc *.egg-info build .egg .eggs .tox dist # Generated by pbr AUTHORS ChangeLog # Generated by testrepository .testrepository .stestr .idea .cache/ # coverage related .coverage cover/ htmlcov/ gabbi-1.44.0/.pyup.yml000066400000000000000000000001731332317117700144500ustar00rootroot00000000000000# autogenerated pyup.io config file # see https://pyup.io/docs/configuration/ for all available options update: insecure gabbi-1.44.0/.stestr.conf000066400000000000000000000004221332317117700151200ustar00rootroot00000000000000[DEFAULT] test_path=gabbi/tests test_command=${PYTHON:-python} -m subunit.run discover gabbi $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list group_regex=(?:gabbi\.suitemaker\.(test_[^_]+_[^_]+)|tests\.test_(?:intercept|inner_fixture)\.([^_]+)) gabbi-1.44.0/.travis.yml000066400000000000000000000023631332317117700147660ustar00rootroot00000000000000sudo: false language: python services: # For Gnocchi - docker install: - | if [ "$TOXENV" == "gnocchi" ]; then docker pull gnocchixyz/ci-tools:latest else pip install tox fi script: - | 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=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 env: TOXENV=py35-pytest - python: 3.6 env: TOXENV=py36-failskip - python: 3.6 env: TOXENV=py36-limit - python: 3.6 env: TOXENV=py36-prefix - python: 3.6 env: TOXENV=py36 - python: 3.6 env: TOXENV=py36-pytest notifications: irc: "chat.freenode.net#gabbi" gabbi-1.44.0/CONTRIBUTING.md000066400000000000000000000044071332317117700151070ustar00rootroot00000000000000 Gabbi gets better because of the contributions from the people who use it. These contributions come in many forms: * Joining the #gabbi channel on Freenode IRC at freenode.net and having a chat. * Improvements to make the documentation more complete, more correct and typo free. * Reporting and reviewing bugs in the [issues](https://github.com/cdent/gabbi/issues) * Providing [pull requests](https://github.com/cdent/gabbi/pulls) containing fixes and new features. See [below](#pull-requests) for guidelines. If you have an idea for a new feature it is best to review the [Ideas](https://github.com/cdent/gabbi/wiki/Ideas) wiki page and the existing issues and pull requests to see if there is existing work you can contribute to. It's also worthwhile to ask around in IRC. In general the default stance with gabbi is to avoid adding new features if we can come up with some way to use the existing features to solve the requirements of your tests. This helps to keep the test format as clean and readable as possible. If you reach an impasse, create an issue and provide as much info as you can about your situation and together we can try to figure it out. # Pull Requests If you want to make a pull request, fork the gabbi repository and create 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. When you push your branch back to Github please never force push. If your pull request receives some comments and you need to make some changes, please do them as _an additional commit_ on the branch used for the pull request. Any code you submit should follow the rules of [pep8](https://www.python.org/dev/peps/pep-0008/). You can test that it does by running `tox -epep8` in your checkout. Note that when you run that the code will also be evaluated to be sure it follows some standards established in the OpenStack development community (mostly to do with import handling and line breaks). gabbi-1.44.0/LICENSE000066400000000000000000000011211332317117700136510ustar00rootroot00000000000000 Copyright 2015-2016 Chris Dent 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. gabbi-1.44.0/Makefile000066400000000000000000000016301332317117700143110ustar00rootroot00000000000000# simple Makefile for some common tasks .PHONY: clean test dist release pypi tagv docs clean: find . -name "*.pyc" |xargs rm || true rm -r dist || true rm -r build || true rm -rf .tox || true rm -r .testrepository || true rm -r cover .coverage || true rm -r .eggs || true rm -r gabbi.egg-info || true tagv: git tag -s \ -m `python -c 'import gabbi; print gabbi.__version__'` \ `python -c 'import gabbi; print gabbi.__version__'` git push origin master --tags cleanagain: find . -name "*.pyc" |xargs rm || true rm -r dist || true rm -r build || true rm -r .tox || true rm -r .testrepository || true rm -r cover .coverage || true rm -r .eggs || true rm -r gabbi.egg-info || true docs: cd docs ; $(MAKE) html test: tox --skip-missing-interpreters dist: test python3 setup.py sdist bdist_wheel release: clean test cleanagain tagv pypi pypi: python3 setup.py sdist bdist_wheel upload --sign gabbi-1.44.0/README.rst000066400000000000000000000070201332317117700143370ustar00rootroot00000000000000.. image:: https://travis-ci.org/cdent/gabbi.svg?branch=master :target: https://travis-ci.org/cdent/gabbi .. image:: https://readthedocs.org/projects/gabbi/badge/?version=latest :target: https://gabbi.readthedocs.io/en/latest/ :alt: Documentation Status Gabbi ===== `Release Notes`_ Gabbi is a tool for running HTTP tests where requests and responses are represented in a declarative YAML-based form. The simplest test looks like this:: tests: - name: A test GET: /api/resources/id See the docs_ for more details on the many features and formats for setting request headers and bodies and evaluating responses. Gabbi is tested with Python 2.7, 3.4, 3.5, 3.6 and pypy. Tests can be run using `unittest`_ style test runners, `pytest`_ or from the command line with a `gabbi-run`_ script. There is a `gabbi-demo`_ repository which provides a tutorial via its commit history. The demo builds a simple API using gabbi to facilitate test driven development. .. _Release Notes: https://gabbi.readthedocs.io/en/latest/release.html .. _docs: https://gabbi.readthedocs.io/ .. _gabbi-demo: https://github.com/cdent/gabbi-demo .. _unittest: https://gabbi.readthedocs.io/en/latest/example.html#loader .. _pytest: http://pytest.org/ .. _loader docs: https://gabbi.readthedocs.io/en/latest/example.html#pytest .. _gabbi-run: https://gabbi.readthedocs.io/en/latest/runner.html Purpose ------- Gabbi works to bridge the gap between human readable YAML files that represent HTTP requests and expected responses and the obscured realm of Python-based, object-oriented unit tests in the style of the unittest module and its derivatives. Each YAML file represents an ordered list of HTTP requests along with the expected responses. This allows a single file to represent a process in the API being tested. For example: * Create a resource. * Retrieve a resource. * Delete a resource. * Retrieve a resource again to confirm it is gone. At the same time it is still possible to ask gabbi to run just one request. If it is in a sequence of tests, those tests prior to it in the YAML file will be run (in order). In any single process any test will only be run once. Concurrency is handled such that one file runs in one process. These features mean that it is possible to create tests that are useful for both humans (as tools for improving and developing APIs) and automated CI systems. Testing and Developing Gabbi ---------------------------- 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 If you have the dependencies installed (or a warmed up virtualenv) you can run the tests by hand and exit on the first failure:: python -m subunit.run discover -f gabbi | subunit2pyunit Testing can be limited to individual modules by specifying them after the tox invocation:: tox -epep8,py27,py34 -- test_driver test_handlers 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 gabbi-1.44.0/docs/000077500000000000000000000000001332317117700136015ustar00rootroot00000000000000gabbi-1.44.0/docs/Makefile000066400000000000000000000151571332317117700152520ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Gabbi.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Gabbi.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Gabbi" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Gabbi" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." gabbi-1.44.0/docs/source/000077500000000000000000000000001332317117700151015ustar00rootroot00000000000000gabbi-1.44.0/docs/source/_static/000077500000000000000000000000001332317117700165275ustar00rootroot00000000000000gabbi-1.44.0/docs/source/_static/theme_override.css000066400000000000000000000002211332317117700222350ustar00rootroot00000000000000.wy-table-responsive table td, .wy-table-responsive table th { white-space: normal; } .wy-table-responsive td p { font-size: inherit; } gabbi-1.44.0/docs/source/conf.py000066400000000000000000000202071332317117700164010ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Gabbi documentation build configuration file, created by # sphinx-quickstart on Wed Dec 31 17:07:32 2014. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. docroot = os.path.abspath('../..') sys.path.insert(0, docroot) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Gabbi' copyright = u'' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '' # The full version, including alpha/beta/rc tags. release = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' html_context = {'css_files': [ 'https://media.readthedocs.org/css/sphinx_rtd_theme.css', 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', '_static/theme_override.css', ]} # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. html_show_copyright = False # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Gabbidoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'Gabbi.tex', u'Gabbi Documentation', u'Chris Dent', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'gabbi', u'Gabbi Documentation', [u'Chris Dent'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'Gabbi', u'Gabbi Documentation', u'Chris Dent', 'Gabbi', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False gabbi-1.44.0/docs/source/example.py000066400000000000000000000016101332317117700171040ustar00rootroot00000000000000"""A sample test module.""" # For pathname munging import os # The module that build_tests comes from. from gabbi import driver # We need access to the WSGI application that hosts our service from myapp import wsgiapp # We're using fixtures in the YAML files, we need to know where to # load them from. from myapp.test import fixtures # By convention the YAML files are put in a directory named # "gabbits" that is in the same directory as the Python test file. TESTS_DIR = 'gabbits' def load_tests(loader, tests, pattern): """Provide a TestSuite to the discovery process.""" test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) # Pass "require_ssl=True" as an argument to force all tests # to use SSL in requests. return driver.build_tests(test_dir, loader, intercept=wsgiapp.app, fixture_module=fixtures) gabbi-1.44.0/docs/source/example.rst000066400000000000000000000004441332317117700172700ustar00rootroot00000000000000Example Tests ============= .. _example: What follows is a commented example of some tests in a single file demonstrating many of the :doc:`format` features. See :doc:`loader` for the Python needed to integrate with a testing harness. .. literalinclude:: example.yaml :language: yaml gabbi-1.44.0/docs/source/example.yaml000066400000000000000000000163341332317117700174270ustar00rootroot00000000000000 # Fixtures can be used to set any necessary configuration, such as a # persistence layer, and establish sample data. They operate per # file. They are context managers, each one wrapping the next in the # sequence. fixtures: - ConfigFixture - SampleDataFixture # There is an included fixture named "SkipAllFixture" which can be # used to declare that all the tests in the given file are to be # skipped. # Each test file can specify a set of defaults that will be used for # every request. This is useful for always specifying a particular # header or always requiring SSL. These values will be used on every # test in the file unless overriden. Lists and dicts are merged one # level deep, except for "data" which is copied verbatim whether it # is a string, list or dict (it can be all three). defaults: ssl: True request_headers: x-my-token: zoom # The tests themselves are a list under a "tests" key. It's useful # to use plenty of whitespace to help readability. tests: # Each request *must* have a name which is unique to the file. When it # becomes a TestCase the name will be lowercased and spaces will # become "_". Use that generated name when limiting test runs. - name: a test for root desc: Some explanatory text that could be used by other tooling # The URL can either be relative to a host specified elsewhere or # be a fully qualified "http" or "https" URL. *You* are responsible # for url-encoding the URL. url: / method: GET # If no status or method are provided they default to "200" and # "GET". # Instead of explicitly stating "url" and "method" you can join # those two keys into one key representing the method. The method # *must* be uppercase. - name: another test for root desc: Same test as above but with GET key GET: / # A single test can override settings in defaults (set above). - name: root without ssl redirects ssl: False GET: / status: 302 # When evaluating response headers it is possible to use a regular # 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/ # By default redirects will not be followed. This can be changed. - name: follow root without ssl redirect ssl: False redirects: True GET: / status: 200 # This is the response code after the redirect. # URLs can express query parameters in two ways: either in the url # value directly, or as query_parameters. If both are used then # query_parameters are appended. In this example the resulting URL # will be equivalient to # /foo?section=news&article=1&article=2&date=yesterday # but not necessarily in that order. - name: create a url with parameters GET: /foo?section=news query_parameters: article: - 1 - 2 date: yesterday # Request headers can be used to declare media-type choices and # experiment with authorization handling (amongst other things). # Response headers allow evaluating headers in the response. These # two together form the core value of gabbi. - name: test accept GET: /resource request_headers: accept: application/json response_headers: content-type: /application/json/ # If a header must not be present in a response at all that can be # expressed in a test as follows. - name: test forbidden headers GET: /resource response_forbidden_headers: - x-special-header # All of the above requests have defaulted to a "GET" method. When # using "POST", "PUT" or "PATCH", the "data" key provides the # request body. - name: post some text POST: /text_repo request_headers: content-type: text/plain data: "I'm storing this" status: 201 # If the data is not a string, it will be transformed into JSON. # You must supply an appropriate content-type request header. - name: post some json POST: /json_repo request_headers: content-type: application/json data: name: smith abode: castle status: 201 # If the data is a string prepended with "<@" the value will be # treated as the name of a file in the same directory as the YAML # file. Again, you must supply an appropriate content-type. If the # content-type is one of several "text-like" types, the content will # be assumed to be UTF-8 encoded. - name: post an image POST: /image_repo request_headers: content-type: image/png data: <@kittens.png # A single request can be marked to be skipped. - name: patch an image skip: patching images not yet implemented PATCH: /image_repo/12d96fb8-e78c-11e4-8c03-685b35afa334 # Or a single request can be marked that it is expected to fail. - name: check allow headers desc: the framework doesn't do allow yet xfail: True PUT: /post_only_url status: 405 response_headers: allow: POST # The body of a response can be evaluated with response handlers. # The most simple checks for a set of strings anywhere in the # response. Note that the strings are members of a list. - name: check for css file GET: /blog/posts/12 response_strings: - normalize.css # For JSON responses, JSONPath rules can be used. - name: post some json get back json POST: /json_repo request_headers: content-type: application/json data: name: smith abode: castle status: 201 response_json_paths: $.name: smith $.abode: castle # Requests run in sequence. One test can make reference to the test # immediately prior using some special variables. # "$LOCATION" contains the "location" header in the previous # response. # "$HEADERS" is a pseudo dictionary containing all the headers of # the previous response. # "$ENVIRON" is a pseudo dictionary providing access to the current # environment. # "$RESPONSE" provides access to the JSON in the prior response, via # JSONPath. See http://jsonpath-rw.readthedocs.io/ for # jsonpath-rw formatting. # $SCHEME and $NETLOC provide access to the current protocol and # location (host and port). - name: get the thing we just posted GET: $LOCATION request_headers: x-magic-exchange: $HEADERS['x-magic-exchange'] x-token: $ENVIRON['OS_TOKEN'] response_json_paths: $.name: $RESPONSE['$.name'] $.abode: $RESPONSE['$.abode'] response_headers: content-location: /$SCHEME://$NETLOC/ # For APIs where resource creation is asynchronous it can be # necessary to poll for the resulting resource. First we create the # resource in one test. The next test uses the "poll" key to loop # with a delay for a set number of times. - name: create asynch POST: /async_creator request_headers: content-type: application/json data: name: jones abode: bungalow status: 202 - name: poll for created resource GET: $LOCATION poll: count: 10 # try up to ten times delay: .5 # wait .5 seconds between each try response_json_paths: $.name: $RESPONSE['$.name'] $.abode: $RESPONSE['$.abode'] gabbi-1.44.0/docs/source/faq.rst000066400000000000000000000072771332317117700164170ustar00rootroot00000000000000 Frequently Asked Questions ========================== .. note:: This section provides a collection of questions with answers that don't otherwise fit in the rest of the documentation. If something is missing, please create an issue_. As this document grows it will gain a more refined structure. .. highlight:: yaml General ~~~~~~~ Is gabbi only for testing Python-based APIs? -------------------------------------------- No, you can use :doc:`gabbi-run ` to test an HTTP service built in any programming language. How do I run just one test? --------------------------- Each YAML file contains a sequence of tests, each test within each file has a name. That name is translated to the name of the test by replacing spaces with an ``_``. When running tests that are :doc:`generated dynamically `, filtering based on the test name prior to the test being collected will not work in some test runners. Test runners that use a ``--load-list`` functionality can be convinced to filter after discovery. `pytest` does this directly with the ``-k`` keyword flag. When using testrepository with tox as used in gabbi's own tests it is possible to pass a filter in the tox command:: tox -epy27 -- get_the_widget When using ``testtools.run`` and similar test runners it's a bit more complicated. It is necessary to provide the full name of the test as a list to ``--load-list``:: 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 ~~~~~~~~~~~~~ Can I have variables in my YAML file? ------------------------------------- Gabbi provides the ``$ENVIRON`` :ref:`substitution ` which can operate a bit like variables that are set elsewhere and then used in the tests defined by the YAML. If you find it necessary to have variables within a single YAML file you take advantage of YAML `alias nodes`_ list this:: vars: - &uuid_1 5613AABF-BAED-4BBA-887A-252B2D3543F8 tests: - name: send a uuid to a post POST: /resource request_headers: content-type: application/json data: uuid: *uuid_1 You can alias all sorts of nodes, not just single items. Be aware that the replacement of an alias node happens while the YAML is being loaded, before gabbi does any processing. .. _alias nodes: http://www.yaml.org/spec/1.2/spec.html#id2786196 How many tests should be put in one YAML file? ---------------------------------------------- For the sake of readability it is best to keep each YAML file relatively short. Since each YAML file represents a sequence of requests, it usually makes sense to create a new file when a test is not dependent on any before it. It's tempting to put all the tests for any resource or URL in the same file, but this eventually leads to files that are too long and are thus difficult to read. .. _issue: https://github.com/cdent/gabbi/issues gabbi-1.44.0/docs/source/fixtures.rst000066400000000000000000000063721332317117700175140ustar00rootroot00000000000000Fixtures ======== Each suite of tests is represented by a single YAML file, and may optionally use one or more fixtures to provide the necessary environment required by the tests in that file. Fixtures are implemented as nested context managers. Subclasses of :class:`~gabbi.fixture.GabbiFixture` must implement ``start_fixture`` and ``stop_fixture`` methods for creating and destroying, respectively, any resources managed by the fixture. While the subclass may choose to implement ``__init__`` it is important that no exceptions are thrown in that method, otherwise the stack of context managers will fail in unexpected ways. Instead initialization of real resources should happen in ``start_fixture``. At this time there is no mechanism for the individual tests to have any direct awareness of the fixtures. The fixtures exist, conceptually, on the server side of the API being tested. Fixtures may do whatever is required by the testing environment, however there are two common scenarios: * Establishing (and then resetting when a test suite has finished) any baseline configuration settings and persistence systems required for the tests. * Creating sample data for use by the tests. If a fixture raises ``unittest.case.SkipTest`` during ``start_fixture`` all the tests in the current file will be skipped. This makes it possible to skip the tests if some optional configuration (such as a particular type of database) is not available. If an exception is raised while a fixture is being used, information about the exception will be stored on the fixture so that the ``stop_fixture`` method can decide if the exception should change how the fixture should clean up. The exception information can be found on ``exc_type``, ``exc_value`` and ``traceback`` method attributes. If an exception is raised when a fixture is started (in ``start_fixture``) the first test in the suite using the fixture will be marked with an error using the traceback from the exception and all the tests in the suite will be skipped. This ensures that fixture failure is adequately captured and reported by test runners. .. _inner-fixtures: Inner Fixtures ============== In some contexts (for example CI environments with a large number of tests being run in a broadly concurrent environment where output is logged to a single file) it can be important to capture and consolidate stray output that is produced during the tests and display it associated with an individual test. This can help debugging and avoids unusable output that is the result of multiple streams being interleaved. Inner fixtures have been added to support this. These are fixtures more in line with the tradtional ``unittest`` concept of fixtures: a class on which ``setUp`` and ``cleanUp`` is automatically called. :func:`~gabbi.driver.build_tests` accepts a named parameter arguments of ``inner_fixtures``. The value of that argument may be an ordered list of fixtures.Fixture_ classes that will be called when each individual test is set up. An example fixture that could be useful is the FakeLogger_. .. note:: At this time ``inner_fixtures`` are not supported when using the pytest :doc:`loader `. .. _fixtures.Fixture: https://pypi.python.org/pypi/fixtures .. _FakeLogger: https://pypi.python.org/pypi/fixtures#fakelogger gabbi-1.44.0/docs/source/format.rst000066400000000000000000000316141332317117700171300ustar00rootroot00000000000000.. highlight:: yaml Test Format =========== Gabbi tests are expressed in YAML as a series of HTTP requests with their expected response:: tests: - name: retrieve root GET: / status: 200 This will trigger a ``GET`` request to ``/`` on the configured :doc:`host`. The test will pass if the response's status code is ``200``. .. _test-structure: Test Structure -------------- The top-level ``tests`` category contains an ordered sequence of test declarations, each describing the expected response to a given request: .. _metadata: Metadata ******** .. 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 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 transformed to ``_``. In at least some test runners this will allow you to select and filter on test name. .. _request-parameters: Request Parameters ****************** .. table:: ==================== ======================================== ============ Key Description Notes ==================== ======================================== ============ any uppercase string Any such key is considered an HTTP method, with the corresponding value expressing the URL. This is a shortcut combining ``method`` and ``url`` into a single statement:: GET: /index corresponds to:: method: GET url: /index ``method`` The HTTP request method. defaults to ``GET`` ``url`` The URL to request. This can either be a Either this full path (e.g. "/index") or a fully or the qualified URL (i.e. including host and shortcut scheme, e.g. above is "http://example.org/index") — see **required** :doc:`host` for details. ``request_headers`` A dictionary of key-value pairs representing request header names and values. These will be added to the constructed request. ``query_parameters`` A dictionary of query parameters that will be added to the ``url`` as query string. If that URL already contains a set of query parameters, those wil be extended. See :doc:`example` for a demonstration of how the data is structured. ``data`` A representation to pass as the body of a request. Note that ``content-type`` in ``request_headers`` should also be set — see `Data`_ for details. ``redirects`` If ``True``, redirects will defaults to automatically be followed. ``False`` ``ssl`` Determines whether the request uses SSL defaults to (i.e. HTTPS). Note that the ``url``'s ``False`` scheme takes precedence if present — see :doc:`host` for details. ==================== ======================================== ============ .. _response-expectations: Response Expectations ********************* .. table:: ============================== ===================================== ============ Key Description Notes ============================== ===================================== ============ ``status`` The expected response status code. defaults to Multiple acceptable response codes ``200`` may be provided, separated by ``||`` (e.g. ``302 || 301`` — note, however, that this indicates ambiguity, which is generally undesirable). ``response_headers`` A dictionary of key-value pairs representing expected response header names and values. If a header's value is wrapped in ``/.../``, it will be treated as a regular expression to search for in the response header. ``response_forbidden_headers`` A list of headers which must `not` be present. ``response_strings`` A list of string fragments expected to be present in the response body. ``response_json_paths`` A dictionary of JSONPath rules paired with expected matches. Using this rule requires that the content being sent from the server is JSON (i.e. a content type of ``application/json`` or containing ``+json``) If the value is wrapped in ``/.../`` the result of the JSONPath query will be searched for the value as a regular expression. ``poll`` A dictionary of two keys: * ``count``: An integer stating the number of times to attempt this test before giving up. * ``delay``: A floating point number of seconds to delay between attempts. This makes it possible to poll for a resource created via an asynchronous request. Use with caution. ============================== ===================================== ============ Note that many of these items allow :ref:`substitutions `. Default values for a file's ``tests`` may be provided via the top-level ``defaults`` category. These take precedence over the global defaults (explained below). For examples see `the gabbi tests`_, :doc:`example` and the `gabbi-demo`_ tutorial. .. _fixtures: Fixtures -------- The top-level ``fixtures`` category contains a sequence of named :doc:`fixtures`. .. _response-handlers: Response Handlers ----------------- ``response_*`` keys are examples of Response Handlers. Custom handlers may be created by test authors for specific use cases. See :doc:`handlers` for more information. .. _state-substitution: Substitution ------------ There are a number of magical variables that can be used to make reference to the state of a current test, the one just prior or any test prior to the current one. The variables are replaced with real values during test processing. Global ****** * ``$ENVIRON['']``: The name of an environment variable. Its value will replace the magical variable. If the string value of the environment variable is ``"True"`` or ``"False"`` then the resulting value will be the corresponding boolean, not a string. Current Test ************ * ``$SCHEME``: The current scheme/protocol (usually ``http`` or ``https``). * ``$NETLOC``: The host and potentially port of the request. Immediately Prior Test ********************** * ``$COOKIE``: All the cookies set by any ``Set-Cookie`` headers in the prior response, including only the cookie key and value pairs and no metadata (e.g. ``expires`` or ``domain``). * ``$URL``: The URL defined in the prior request, after substitutions have been made. For backwards compatibility with earlier releases ``$LAST_URL`` may also be used, but if ``$HISTORY`` (see below) is being used, ``$URL`` must be used. * ``$LOCATION``: The location header returned in the prior response. * ``$HEADERS['
']``: The value of any header from the prior response. * ``$RESPONSE['']``: A JSONPath query into the prior response. See :doc:`jsonpath` for more on formatting. Any Previous Test ***************** * ``$HISTORY[''].``: Any variable which refers to a prior test may be used in an expression that refers to any earlier test in the same file by identifying the target test by its name in a ``$HISTORY`` dictionary. For example, to refer to a value in a JSON object in the response of a test named ``post json``:: $HISTORY['post json'].$RESPONSE['$.key'] This is a very powerful feature that could lead to test that are difficult for humans to read. Take care to optimize for the maintainers that will come after you, not yourself. .. note:: Where a single-quote character, ``'``, is shown in the variables above you may also use a double-quote character, ``"``, but in any given expression the same character must be used at both ends. All of these variables may be used in all of the following fields: * ``url`` * ``query_parameters`` * ``data`` * ``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`` (in both the key and value) * ``response_forbidden_headers`` * ``count`` and ``delay`` fields of ``poll`` With these variables it ought to be possible to traverse an API without any explicit statements about the URLs being used. If you need a replacement on a field that is not currently supported please raise an issue or provide a patch. As all of these features needed to be tested in the development of gabbi itself, `the gabbi tests`_ are a good source of examples on how to use the functionality. See also :doc:`example` for a collection of examples and the `gabbi-demo`_ tutorial. .. _data: Data ---- The ``data`` key has some special handing to allow for a bit more flexibility when doing a ``POST`` or ``PUT``: * If the value is not a string (that is, it is a sequence or structure) it is treated as a data structure that will be turned into a string by the ``dumps`` method on the relevant :doc:`content handler `. For example if the content-type of the body is ``application/json`` the data structure will be turned into a JSON string. * If the value is a string that begins with ``<@`` then the rest of the string is treated as a filepath to be loaded. The path is relative to the test directory and may not traverse up into parent directories. * If the value is an undecorated string, that's the value. .. note:: When reading from a file care should be taken to ensure that a reasonable content-type is set for the data as this will control if any encoding is done of the resulting string value. If it is text, json, xml or javascript it will be encoded to UTF-8. .. _the gabbi tests: https://github.com/cdent/gabbi/tree/master/gabbi/tests/gabbits_intercept .. _gabbi-demo: https://github.com/cdent/gabbi-demo gabbi-1.44.0/docs/source/gabbi.rst000066400000000000000000000044431332317117700167040ustar00rootroot00000000000000gabbi Package ============= :mod:`case` Module ------------------ .. automodule:: gabbi.case :members: :undoc-members: :show-inheritance: :mod:`driver` Module -------------------- .. automodule:: gabbi.driver :members: :undoc-members: :show-inheritance: :mod:`suitemaker` Module ------------------------ .. automodule:: gabbi.suitemaker :members: :undoc-members: :show-inheritance: :mod:`fixture` Module --------------------- .. automodule:: gabbi.fixture :members: :undoc-members: :show-inheritance: :mod:`handlers` Module ---------------------- .. automodule:: gabbi.handlers :members: :undoc-members: :show-inheritance: :mod:`handlers.base` Module ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. automodule:: gabbi.handlers.base :members: :undoc-members: :show-inheritance: :mod:`handlers.core` Module ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. automodule:: gabbi.handlers.core :members: :undoc-members: :show-inheritance: :mod:`handlers.jsonhandler` Module ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. automodule:: gabbi.handlers.jsonhandler :members: :undoc-members: :show-inheritance: :mod:`handlers.yaml_disk_loading_jsonhandler` Module ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. automodule:: gabbi.handlers.yaml_disk_loading_jsonhandler :members: :undoc-members: :show-inheritance: :mod:`suite` Module ------------------- .. automodule:: gabbi.suite :members: :undoc-members: :show-inheritance: :mod:`runner` Module -------------------- .. automodule:: gabbi.runner :members: :undoc-members: :show-inheritance: :mod:`reporter` Module ---------------------- .. automodule:: gabbi.reporter :members: :undoc-members: :show-inheritance: :mod:`utils` Module ------------------- .. automodule:: gabbi.utils :members: :undoc-members: :show-inheritance: :mod:`exception` Module ----------------------- .. automodule:: gabbi.exception :members: :undoc-members: :show-inheritance: :mod:`httpclient` Module ------------------------ .. automodule:: gabbi.httpclient :members: :undoc-members: :show-inheritance: :mod:`json_parser` Module ------------------------- .. automodule:: gabbi.json_parser :members: :undoc-members: :show-inheritance: gabbi-1.44.0/docs/source/handlers.rst000066400000000000000000000161341332317117700174400ustar00rootroot00000000000000 Content Handlers ================ Content handlers are responsible for preparing request data and evaluating response data based on the content-type of the request and response. A content handler operates as follows: * Structured YAML data provided via the ``data`` attribute is converted to a string or bytes sequence and used as request body. * The response body (a string or sequence of bytes) is transformed into a content-type dependent structure and stored in an internal attribute named ``response_data`` that is: * used when evaluating the response body * used in ``$RESPONSE[]`` :ref:`substitutions ` By default, gabbi provides content handlers for JSON. In that content handler the ``data`` test key is converted from structured YAML into a JSON string. Response bodies are converted from a JSON string into a data structure in ``response_data`` that is used when evaluating ``response_json_paths`` entries in a test or doing JSONPath-based ``$RESPONSE[]`` substitutions. A YAMLDiskLoadingJSONHandler has been added to extend the JSON handler. It works the same way as the JSON handler except for when evaluating the ``response_json_paths`` handle, data that is read from disk can be either in JSON or YAML format. The YAMLDiskLoadingJSONHandler is not enabled by default and must be added as shown in the :ref:`Extensions` section in order to be used in the tests. Further content handlers can be added as extensions. Test authors may need these extensions for their own suites, or enterprising developers may wish to create and distribute extensions for others to use. .. note:: One extension that is likely to be useful is a content handler that turns ``data`` into url-encoded form data suitable for POST and turns an HTML response into a DOM object. .. _Extensions: Extensions ---------- Content handlers are an evolution of the response handler concept in earlier versions gabbi. To preserve backwards compatibility with existing response handlers, old style response handlers are still allowed, but new handlers should implement the content handler interface (described below). .. highlight:: python Registering additional custom handlers is done by passing a subclass of :class:`~gabbi.handlers.base.ContentHandler` to :meth:`~gabbi.driver.build_tests`:: driver.build_tests(test_dir, loader, host=None, intercept=simple_wsgi.SimpleWsgi, content_handlers=[MyContentHandler]) If pytest is being used:: driver.py_test_generator(test_dir, intercept=simple_wsgi.SimpleWsgi, content_handlers=[MyContenHandler]) Gabbi provides an additional custom handler named YAMLDiskLoadingJSONHandler. This can be used for loading JSON and YAML files from disk when evaluating the ``response_json_paths`` handle. .. warning:: YAMLDiskLoadingJSONHandler shares the same content-type as the default JSONHandler. When there are multiple handlers listed that accept the same content-type, the one that is earliest in the list will be used. With ``gabbi-run``, custom handlers can be loaded via the ``--response-handler`` option -- see :meth:`~gabbi.runner.load_response_handlers` for details. .. note:: The use of the ``--response-handler`` argument is done to preserve backwards compatibility and avoid excessive arguments. Both types of handler may be passed to the argument. Implementation Details ~~~~~~~~~~~~~~~~~~~~~~ Creating a content handler requires subclassing :class:`~gabbi.handlers.base.ContentHandler` and implementing several methods. These methods are described below, but inspecting :class:`~gabbi.handlers.jsonhandler.JSONHandler` will be instructive in highlighting required arguments and techniques. To provide a ``response_`` response-body evaluator a subclass must define: * ``test_key_suffix``: This, along with the prefix ``response_``, forms the key used in the test structure. It is a class level string. * ``test_key_value``: The key's default value, either an empty list (``[]``) or empty dict (``{}``). It is a class level value. * ``action``: An instance method which tests the expected values against the HTTP response - it is invoked for each entry, with the parameters depending on the default value. The arguments to ``action`` are (in order): * ``self``: The current instance. * ``test``: The currently active ``HTTPTestCase`` * ``item``: The current entry if ``test_key_value`` is a list, otherwise the key half of the key/value pair at this entry. * ``value``: ``None`` if ``test_key_value`` is a list, otherwise the value half of the key/value pair at this entry. To translate request or response bodies to or from structured data a subclass must define an ``accepts`` method. This should return ``True`` if this class is willing to translate the provided content-type. During request processing it is given the value of the content-type header that will be sent in the request. During response processing it is given the value of the content-type header of the response. This makes it possible to handle different request and response bodies in the same handler, if desired. For example a handler might accept ``application/x-www-form-urlencoded`` and ``text/html``. If ``accepts`` is defined two additional static methods should be defined: * ``dumps``: Turn structured Python data from the ``data`` key in a test into a string or byte stream. The optional ``test`` param allows you to access the current test case which may help with manipulations for custom content handlers, e.g. ``multipart/form-data`` needs to add a ``boundary`` to the ``Content-Type`` header in order to mark the appropriate sections of the body. * ``loads``: Turn a string or byte stream in a response into a Python data structure. Gabbi will put this data on the ``response_data`` attribute on the test, where it can be used in the evaluations described above (in the ``action`` method) or in ``$RESPONSE`` handling. An example usage here would be to turn HTML into a DOM. * ``load_data_file``: Load data from disk into a Python data structure. Gabbi will call this method when ``response_`` contains an item where the right hand side value starts with ``<@``. The ``test`` param allows you to access the current test case and provides a load_data_file method which should be used because it verifies the data is loaded within the test diectory and returns the file source as a string. The ``load_data_file`` method was introduced to re-use the JSONHandler in order to support loading YAML files from disk through the implementation of an additional custom handler, see :class:`~gabbi.handlers.yaml_disk_loading_jsonhandler.YAMLDiskLoadingJSONHandler` for details. Finally if a ``replacer`` class method is defined, then when a ``$RESPONSE`` substitution is encountered, ``replacer`` will be passed the ``response_data`` of the prior test and the argument within the ``$RESPONSE``. Please see the `JSONHandler source`_ for additional detail. .. _JSONHandler source: https://github.com/cdent/gabbi/blob/master/gabbi/handlers/jsonhandler.py gabbi-1.44.0/docs/source/host.rst000066400000000000000000000031071332317117700166110ustar00rootroot00000000000000Target Host =========== The target host is the host on which the API to be tested can be found. Gabbi intends to preserve the flow and semantics of HTTP interactions as much as possible, and every HTTP request needs to be directed at a host of some form. Gabbi provides three ways to control this: * Using `wsgi-intercept`_ to provide a fake socket and ``WSGI`` environment on an arbitrary host and port attached to a ``WSGI`` application (see `intercept examples`_). * Using fully qualified ``url`` values in the YAML defined tests (see `full examples`_). * Using a host and (optionally) port defined at test build time (see `live examples`_). The intercept and live methods are mutually exclusive per test builder, but either kind of test can freely intermix fully qualified URLs into the sequence of tests in a YAML file. For test driven development and local tests the intercept style of testing lowers test requirements (no web server required) and is fast. Interception is performed as part of :doc:`fixtures` processing as the most deeply nested fixture. This allows any configuration or database setup to be performed prior to the WSGI application being created. For the implementation of the above see :meth:`~gabbi.driver.build_tests`. .. _wsgi-intercept: https://pypi.python.org/pypi/wsgi_intercept .. _intercept examples: https://github.com/cdent/gabbi/blob/master/gabbi/tests/test_intercept.py .. _full examples: https://github.com/cdent/gabbi/blob/master/gabbi/tests/gabbits_live/google.yaml .. _live examples: https://github.com/cdent/gabbi/blob/master/gabbi/tests/test_live.py gabbi-1.44.0/docs/source/index.rst000066400000000000000000000071011332317117700167410ustar00rootroot00000000000000.. Gabbi documentation master file, created by sphinx-quickstart on Wed Dec 31 17:07:32 2014. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. .. toctree:: :maxdepth: 1 :hidden: format loader example jsonpath host fixtures handlers runner release faq gabbi Gabbi ===== .. highlight:: yaml Gabbi is a tool for running HTTP tests where requests and responses are expressed as declarations in a collection of YAML files. The simplest test looks like this:: tests: - name: A test GET: /api/resources/id See the rest of these docs for more details on the many features and formats for setting request headers and bodies and evaluating responses. The name is derived from "gabby": excessively talkative. In a test environment having visibility of what a test is actually doing is a good thing. This is especially true when the goal of a test is to test the HTTP, not the testing infrastructure. Gabbi tries to put the HTTP interaction in the foreground of testing. Tests can be run using :ref:`unittest ` style test runners or py.test or from the command line with a :doc:`gabbi-run ` script. If you want to get straight to creating tests look at :doc:`example`, the test files in the `source distribution`_ and :doc:`format`. A `gabbi-demo`_ repository provides a tutorial of using gabbi to build an API, via the commit history of the repo. .. _source distribution: https://github.com/cdent/gabbi .. _gabbi-demo: https://github.com/cdent/gabbi-demo Purpose ------- .. highlight:: none Gabbi works to bridge the gap between human readable YAML files (see :doc:`format` for details) that represent HTTP requests and expected responses and the rather complex world of automated testing. Each YAML file represents an ordered list of HTTP requests along with the expected responses. This allows a single file to represent a process in the API being tested. For example: * Create a resource. * Retrieve a resource. * Delete a resource. * Retrieve a resource again to confirm it is gone. At the same time it is still possible to ask gabbi to run just one request. If it is in a sequence of tests, those tests prior to it in the YAML file will be run (in order). In any single process any test will only be run once. Concurrency is handled such that one file runs in one process. These features mean that it is possible to create tests that are useful for both humans (as tools for learning, improving and developing APIs) and automated CI systems. Significant flexibility and power is available in the :doc:`format` to make it relatively straightforward to test existing complex APIs. This extended functionality includes the use of `JSONPath`_ to query response bodies and templating of test data to allow access to the prior HTTP response in the current request. For APIs which do not use JSON additional :doc:`handlers` can be created. Care should be taken when using this functionality when you are creating a new API. If your API is so complex that it needs complex test files then you may wish to take that as a sign that your API itself too complex. One goal of gabbi is to encourage transparent and comprehensible APIs. Though gabbi is written in Python and under the covers uses ``unittest`` data structures and processes, there is no requirement that the :doc:`host` be a Python-based service. Anything talking HTTP can be tested. A :doc:`runner` makes it possible to simply create YAML files and point them at a running server. .. _JSONPath: http://goessner.net/articles/JsonPath/ gabbi-1.44.0/docs/source/jsonpath.rst000066400000000000000000000116431332317117700174660ustar00rootroot00000000000000JSONPath ======== Gabbi supports JSONPath both for validating JSON response bodies and within :ref:`substitutions `. JSONPath expressions are provided by `jsonpath_rw`_, with `jsonpath_rw_ext`_ custom extensions to address common requirements: #. Sorting via ``sorted`` and ``[/property]``. #. Filtering via ``[?property = value]``. #. Returning the respective length via ``len``. (These apply both to arrays and key-value pairs.) .. highlight:: json Here is a JSONPath example demonstrating some of these features. Given JSON data as follows:: { "pets": [ {"type": "cat", "sound": "meow"}, {"type": "dog", "sound": "woof"} ] } .. highlight:: yaml If the ordering of the list in ``pets`` is predictable and reliable it is relatively straightforward to test values:: response_json_paths: # length of list is two $.pets.`len`: 2 # sound of second item in list is woof $.pets[1].sound: woof If the ordering is *not* predictable additional effort is required:: response_json_paths: # sort by type $.pets[/type][0].sound: meow # sort by type, reversed $.pets[\type][0].sound: woof # all the sounds $.pets[/type]..sound: ['meow', 'woof'] # filter by type = dog $.pets[?type = "dog"].sound: woof If it is necessary to validate the entire JSON response use a JSONPath of ``$``:: response_json_paths: $: pets: - type: cat sound: meow - type: dog sound: woof This is not a technique that should be used frequently as it can lead to difficult to read tests and it also indicates that your gabbi tests are being used to test your serializers and data models, not just your API interactions. It is also possible to read raw JSON from disk for either all or some of a JSON response:: response_json_paths: $: @` can be made in both the left (query) and right (expected) hand sides of the json path expression. When subtitutions are used in the query, care must be taken to ensure proper quoting of the resulting value. For example if there is a uuid (with hyphens) at ``$RESPONSE['$.id']`` then this expression may fail:: $.nested.structure.$RESPONSE['$.id'].name: foobar as it will evaluate to something like:: $.nested.structure.ADC8AAFC-D564-40D1-9724-7680D3C010C2.name: foobar which may be treated as an arithemtic expression by the json path parser. The test author should write:: $.nested.structure["$RESPONSE['$.id']"].name: foobar to quote the result of the substitution. .. _jsonpath_rw: http://jsonpath-rw.readthedocs.io/en/latest/ .. _jsonpath_rw_ext: https://python-jsonpath-rw-ext.readthedocs.io/en/latest/ .. _own tests: https://github.com/cdent/gabbi/blob/master/gabbi/tests/gabbits_intercept/data.yaml .. _yaml-from-disk tests: https://github.com/cdent/gabbi/blob/master/gabbi/tests/gabbits_handlers/yaml-from-disk.yaml gabbi-1.44.0/docs/source/loader.rst000066400000000000000000000104121332317117700170770ustar00rootroot00000000000000 Loading and Running Tests ========================= .. _test_loaders: To run gabbi tests with a test harness they must be generated in some fashion and then run. This is accomplished by a test loader. Initially gabbi only supported those test harnesses that supported the ``load_tests`` protocol in UnitTest. It now possible to also build and run tests with pytest_ with some limitations described below. .. 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 tests into their respective suites. The usual way to do this is to use a regular expression that groups based on the name of the yaml files. For example, when using ``testrepository`` the ``.testr.conf`` file needs an entry similar to the following:: group_regex=gabbi\.suitemaker\.(test_[^_]+_[^_]+) UnitTest Style Loader ~~~~~~~~~~~~~~~~~~~~~ To run the tests with a ``load_tests`` style loader a test file containing a ``load_tests`` method is required. That will look a bit like: .. literalinclude:: example.py :language: python For details on the arguments available when building tests see :meth:`~gabbi.driver.build_tests`. Once the test loader has been created, it needs to be run. There are *many* options. Which is appropriate depends very much on your environment. Here are some examples using ``unittest`` or ``testtools`` that require minimal knowledge to get started. By file:: python -m testtools.run -v test/test_loader.py By module:: python -m testttols.run -v test.test_loader python -m unittest -v test.test_loader Using test discovery to locate all tests in a directory tree:: python -m testtools.run discover python -m unittest discover test See the `source distribution`_ and `the tutorial repo`_ for more advanced options, including using ``testrepository`` and ``subunit``. pytest ~~~~~~ .. _pytest_loader: Since pytest does not support the ``load_tests`` system, a different way of generating tests is required. Two techniques are supported. The original method (described below) used yield statements to generate tests which pytest would collect. This style of tests is deprecated as of ``pytest>=3.0`` so a new style using pytest fixtures has been developed. pytest >= 3.0 ------------- In the newer technique, a test file is created that uses the ``pytest_generate_tests`` hook. Special care must be taken to always import the ``test_pytest`` method which is the base test that the pytest hook parametrizes to generate the tests from the YAML files. Without the method, the hook will not be called and no tests generated. Here is a simple example file: .. literalinclude:: pytest3.0-example.py :language: python This can then be run with the usual pytest commands. For example:: py.test -svx pytest3.0-example.py pytest < 3.0 ------------ When using the older technique, test file must be created that calls :meth:`~gabbi.driver.py_test_generator` and yields the generated tests. That will look a bit like this: .. literalinclude:: pytest-example.py :language: python This can then be run with the usual pytest commands. For example:: py.test -svx pytest-example.py The older technique will continue to work with all versions of ``pytest<4.0`` but ``>=3.0`` will produce warnings. If you want to use the older technique but not see the warnings add ``--disable-pytest-warnings`` parameter to the invocation of ``py.test``. .. _source distribution: https://github.com/cdent/gabbi .. _the tutorial repo: https://github.com/cdent/gabbi-demo .. _pytest: http://pytest.org/ gabbi-1.44.0/docs/source/pytest-example.py000066400000000000000000000015161332317117700204370ustar00rootroot00000000000000"""A sample pytest module.""" # For pathname munging import os # The module that build_tests comes from. from gabbi import driver # We need access to the WSGI application that hosts our service from myapp import wsgiapp # We're using fixtures in the YAML files, we need to know where to # load them from. from myapp.test import fixtures # By convention the YAML files are put in a directory named # "gabbits" that is in the same directory as the Python test file. TESTS_DIR = 'gabbits' def test_gabbits(): test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) # Pass "require_ssl=True" as an argument to force all tests # to use SSL in requests. test_generator = driver.py_test_generator( test_dir, intercept=wsgiapp.app, fixture_module=fixtures) for test in test_generator: yield test gabbi-1.44.0/docs/source/pytest3.0-example.py000066400000000000000000000016471332317117700206650ustar00rootroot00000000000000"""A sample pytest module for pytest >= 3.0.""" # For pathname munging import os # The module that py_test_generator comes from. from gabbi import driver # We need test_pytest so that pytest test collection works properly. # Without this, the pytest_generate_tests method below will not be # called. from gabbi.driver import test_pytest # noqa # We need access to the WSGI application that hosts our service from myapp import wsgiapp # We're using fixtures in the YAML files, we need to know where to # load them from. from myapp.test import fixtures # By convention the YAML files are put in a directory named # "gabbits" that is in the same directory as the Python test file. TESTS_DIR = 'gabbits' def pytest_generate_tests(metafunc): test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) driver.py_test_generator( test_dir, intercept=wsgiapp.app, fixture_module=fixtures, metafunc=metafunc) gabbi-1.44.0/docs/source/release.rst000066400000000000000000000146341332317117700172630ustar00rootroot00000000000000Release Notes ============= These are informal release notes for gabbi since version 1.0.0, highlighting major features and changes. For more detail see the `commit logs`_ on GitHub. 1.44.0 ------ * Provide the :class:`~gabbi.handlers.yaml_disk_loading_jsonhandler.YAMLDiskLoadingJSONHandler` class that allows test result data for ``response_json_path`` checks to be loaded from YAML-on-disk. 1.43.0 ------ * Use :ref:`JSONPath` to select a portion of data-on-disk in ``response_json_path`` checks. * Restrict PyYAML to ``<4.0``. 1.42.0 ------ * Allow listing of tests with no host configured. When host is an empty string, tests can be listed (for discovery), but will be skipped on run. 1.41.0 ------ * JSON ``$RESPONSE`` :ref:`substitutions ` in the ``data`` field may be complex types (lists and dicts), not solely strings. 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 ------ :doc:`jsonpath` handling gets two improvements: * The value side of a ``response_json_paths`` entry can be loaded from a file using the ``<@file.json`` syntax also used in :ref:`data`. * The key side of a ``response_json_paths`` entry can use :ref:`substitutions `. This was already true for the value side. 1.34.0 ------ :ref:`Substitutions ` in ``$RESPONSE`` handling now preserve numeric types instead of casting to a string. This is useful when servers are expecting strong types and tests want to send response data back to the server. 1.33.0 ------ ``count`` and ``delay`` test keys allow :ref:`substitutions `. :meth:`gabbi.driver.build_tests` accepts a ``verbose`` parameter to set test :ref:`verbosity ` for an entire session. 1.32.0 ------ Better failure reporting when using :doc:`gabbi-run ` with multiple files. Test names are based on the files and a summary of failed files is provided at the end of the report. 1.31.0 ------ Effectively capture a failure in a :doc:`fixture ` and report the traceback. Without this some test runners swallow the error and discovering problems when developing fixtures can be quite challenging. 1.30.0 ------ Thanks to Samuel Fekete, tests can use the ``$HISTORY`` dictionary to refer to any prior test in the same file, not just the one immediately prior, when doing :ref:`substitutions `. 1.29.0 ------ Filenames used to read data into tests using the ``<@`` syntax may now use pathnames relative to the YAML file. See :ref:`data`. :doc:`gabbi-run ` gains a --verbose parameter to force all tests run in a session to run with :ref:`verbose ` set. When using :ref:`pytest ` to load tests, a new mechanism is available which avoids warnings produced in when using a version of pytest greater than ``3.0``. 1.28.0 ------ When verbosely displaying request and response bodies that are JSON, pretty print for improved readability. 1.27.0 ------ Allow :doc:`gabbi-run ` to accept multiple filenames as command line arguments instead of reading tests from stdin. 1.26.0 ------ Switch from response handlers to :doc:`handlers` to allow more flexible processing of both response _and_ request bodies. Add :ref:`inner fixtures ` for per test fixtures, useful for output capturing. 1.25.0 ------ Allow the ``test_loader_name`` arg to :meth:`gabbi.driver.build_tests` to override the prefix of the pretty printed name of generated tests. 1.24.0 ------ String values in JSONPath matches may be wrapped in ``/.../``` to be treated as regular expressions. 1.23.0 ------ Better :doc:`documentation ` of how to run gabbi in a concurrent environment. Improved handling of pytest fixtures and test counts. 1.22.0 ------ Add ``url`` to :meth:`gabbi.driver.build_tests` to use instead of ``host``, ``port`` and ``prefix``. 1.21.0 ------ Add ``require_ssl`` to :meth:`gabbi.driver.build_tests` to force use of SSL. 1.20.0 ------ Add ``$COOKIE`` :ref:`substitution `. 1.19.1 ------ Correctly support IPV6 hosts. 1.19.0 ------ Add ``$LAST_URL`` :ref:`substitution `. 1.17.0 ------ Introduce support for loading and running tests with pytest. 1.16.0 ------ Use urllib3 instead of httplib2 for driving HTTP requests. 1.13.0 ------ Add sorting and filtering to :doc:`jsonpath` handling. 1.11.0 ------ Add the ``response_forbidden_headers`` to :ref:`response expectations `. 1.7.0 ----- .. highlight:: yaml Instead of:: tests: - name: a simple get url: /some/path method: get 1.7.0 also makes it possible to:: tests: - name: a simple get GET: /some/path Any upper case key is treated as a method. 1.4.0 and 1.5.0 --------------- Enhanced flexibility and colorization when setting tests to be :ref:`verbose `. 1.3.0 ----- Adds the ``query_parameters`` key to :ref:`request parameters `. 1.2.0 ----- The start of improvements and extensions to :doc:`jsonpath` handling. In this case the addition of the ``len`` function. 1.1.0 ----- Vastly improved output and behavior in :doc:`gabbi-run `. 1.0.0 ----- Version 1 was the first release with a commitment to a stable :doc:`format`. Since then new fields have been added but have not been taken away. Contributors ============ The following people have contributed code to gabbi. Thanks to them. Thanks also to all the people who have made gabbi better by reporting issues_ and their successes and failures with using gabbi. * Chris Dent * FND * Mehdi Abaakouk * Tom Viner * Jason Myers * Josh Leeb-du Toit * Duc Truong * Zane Bitter * Ryan Spencer * Kim Raymoure * Travis Truman * Samuel Fekete * Michael McCune * Imran Hayder * Julien Danjou * Trevor McCasland * Danek Duvall * Marc Abramowitz .. _commit logs: https://github.com/cdent/gabbi/commits .. _issues: https://github.com/cdent/gabbi/issues gabbi-1.44.0/docs/source/runner.rst000066400000000000000000000041171332317117700171470ustar00rootroot00000000000000YAML Runner =========== If there is a running web service that needs to be tested and creating a test loader with :meth:`~gabbi.driver.build_tests` is either inconvenient or overkill it is possible to run YAML test files directly from the command line with the console-script ``gabbi-run``. It accepts YAML on ``stdin`` or as multiple file arguments, and generates and runs tests and outputs a summary of the results. The provided YAML may not use custom :doc:`fixtures` but otherwise uses the default :doc:`format`. :doc:`host` information is either expressed directly in the YAML file or provided on the command line:: gabbi-run [host[:port]] < /my/test.yaml or:: gabbi-run http://host:port < /my/test.yaml To test with one or more files the following command syntax may be used:: gabbi-run http://host:port -- /my/test.yaml /my/other.yaml .. note:: The filename arguments must come after a ``--`` and all other arguments (host, port, prefix, failfast) must come before the ``--``. .. note:: If files are provided, test output will use names including the name of the file. If any single file includes an error, the name of the file will be included in a summary of failed files at the end of the test report. To facilitate using the same tests against the same application mounted in different locations in a WSGI server, a ``prefix`` may be provided as a second argument:: gabbi-run host[:port] [prefix] < /my/test.yaml or in the target URL:: gabbi-run http://host:port/prefix < /my/test.yaml The value of prefix will be prepended to the path portion of URLs that are not fully qualified. Anywhere host is used, if it is a raw IPV6 address it should be wrapped in ``[`` and ``]``. If ``https`` is used in the target, then the tests in the provided YAML will default to ``ssl: True``. If a ``-x`` or ``--failfast`` argument is provided then ``gabbi-run`` will exit after the first test failure. Use ``-v`` or ``--verbose`` with a value of ``all``, ``headers`` or ``body`` to turn on :ref:`verbosity ` for all tests being run. gabbi-1.44.0/gabbi/000077500000000000000000000000001332317117700137155ustar00rootroot00000000000000gabbi-1.44.0/gabbi/__init__.py000066400000000000000000000011431332317117700160250ustar00rootroot00000000000000# # 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. """See gabbi.driver and gabbbi.case.""" __version__ = '1.44.0' gabbi-1.44.0/gabbi/case.py000066400000000000000000000570771332317117700152220ustar00rootroot00000000000000# # 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 single HTTP request represented as a subclass of ``testtools.TestCase`` 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 response. """ from collections import OrderedDict import copy import functools import os import re import sys import time 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__ from gabbi import exception from gabbi.handlers import base from gabbi import utils MAX_CHARS_OUTPUT = 2000 REPLACERS = [ 'SCHEME', 'NETLOC', 'ENVIRON', 'LOCATION', 'COOKIE', 'LAST_URL', 'URL', 'HEADERS', 'RESPONSE', ] # Basic test template determining both valid keys and default values BASE_TEST = { 'name': '', 'desc': '', 'verbose': False, 'ssl': False, 'redirects': False, 'method': 'GET', 'url': '', 'status': '200', 'request_headers': {}, 'query_parameters': {}, 'data': '', 'xfail': False, 'skip': '', 'poll': {}, 'use_prior_test': True, } def potentialFailure(func): """Decorate a test method that is expected to fail if 'xfail' is true.""" @functools.wraps(func) def wrapper(self): if self.test_data['xfail']: try: func(self) except Exception: if hasattr(testcase, '_ExpectedFailure'): raise testcase._ExpectedFailure(sys.exc_info()) else: self._addExpectedFailure(self.result, sys.exc_info()) else: if hasattr(self, '_addUnexpectedSuccess'): self._addUnexpectedSuccess(self.result) else: raise testcase._UnexpectedSuccess else: func(self) return wrapper def _is_complex_type(data): """If data is a list or dict return True.""" return isinstance(data, list) or isinstance(data, dict) class HTTPTestCase(testtools.TestCase): """Encapsulate a single HTTP request as a TestCase. If the test is a member of a sequence of requests, ensure that prior tests are run. To keep the test harness happy we need to make sure the setUp and tearDown are only run once. """ base_test = copy.copy(BASE_TEST) def setUp(self): if not self.has_run: super(HTTPTestCase, self).setUp() for fixture in self.inner_fixtures: self.useFixture(fixture()) def tearDown(self): if not self.has_run: super(HTTPTestCase, self).tearDown() self.has_run = True def run(self, result=None): """Store the current result handler on this test.""" self.result = result super(HTTPTestCase, self).run(result) @potentialFailure def test_request(self): """Run this request if it has not yet run. If there is a prior test in the sequence, run it first. """ if self.has_run: return if self.test_data['skip']: self.skipTest(self.test_data['skip']) 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(unitresult.TestResult()) self._run_test() def get_content_handler(self, content_type): """Determine the content handler for this media type.""" for handler in self.content_handlers: if handler.accepts(content_type): return handler return None 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], escape_regex=escape_regex) return message if isinstance(message, list): return [self.replace_template(line, escape_regex=escape_regex) for line in message] for replacer in REPLACERS: template = '$%s' % replacer method = '_%s_replace' % replacer.lower() try: if template in message: try: 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' % (template, message, exc)) except TypeError: # Message is not a string pass return message def load_data_file(self, filename): """Read a file from the current test directory.""" return self._load_data_file(filename) def _assert_response(self): """Compare the response with expected data.""" self._test_status(self.test_data['status'], self.response['status']) for handler in self.response_handlers: handler(self) def _clean_query_value(self, value): """Clean up a single query from query_parameters.""" value = self.replace_template(value) # stringify ints in Python version independent fashion value = '%s' % value value = value.encode('UTF-8') return value @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._regex_replacer(self._environ_replacer, escape_regex), message) try: if '.' in value: value = float(value) else: value = int(value) return value except ValueError: pass if value.lower() == "false": return False if value.lower() == "true": return True if value.lower() == "null": return None return value @staticmethod def _environ_replacer(match): """Replace a regex match with an environment value. Let KeyError raise if variable not present. """ environ_name = match.group('arg') return os.environ[environ_name] 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._regex_replacer(self._cookie_replacer, escape_regex), message) def _cookie_replacer(self, match): """Replace a regex match with the cookie of a previous response.""" case = match.group('case') if case: referred_case = self.history[case] else: referred_case = self.prior response_cookies = referred_case.response['set-cookie'] cookies = http_cookies.SimpleCookie() cookies.load(response_cookies) cookie_string = cookies.output(attrs=[], header='', sep=',').strip() return cookie_string def _headers_replace(self, message, escape_regex=False): """Replace a header indicator in a message. Replace it with the header's value from the prior request. """ return re.sub(self._replacer_regex('HEADERS'), 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.""" header_key = match.group('arg') case = match.group('case') if case: referred_case = self.history[case] else: referred_case = self.prior return referred_case.response[header_key.lower()] def _last_url_replace(self, message, escape_regex=False): """Replace $LAST_URL in a message. With the URL used in the prior request. """ 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, 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._regex_replacer(self._url_replacer, escape_regex), message) def _url_replacer(self, match): """Replace a regex match with the value of a previous url.""" case = match.group('case') if case: referred_case = self.history[case] else: referred_case = self.prior return referred_case.url 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._regex_replacer(self._location_replacer, escape_regex), message) def _location_replacer(self, match): """Replace a regex match with the value of a previous location.""" case = match.group('case') if case: referred_case = self.history[case] else: referred_case = self.prior return referred_case.location def _load_data_file(self, filename): """Read a file from the current test directory.""" path = os.path.join(self.test_directory, filename) has_dir_traversal = os.path.relpath( path, start=self.test_directory).startswith(os.pardir) if has_dir_traversal: raise ValueError( 'Attempted loading of data file outside test directory: %s' % filename) with open(path, mode='rb') as data_file: return data_file.read() 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): """Create a url from test data. If provided with a full URL, just return that. If SSL is requested set the scheme appropriately. Scheme and netloc are saved for later use in comparisons. """ query_params = self.test_data['query_parameters'] ssl = self.test_data['ssl'] parsed_url = urlparse.urlsplit(url) if not parsed_url.scheme: full_url = utils.create_url(url, self.host, port=self.port, prefix=self.prefix, ssl=ssl) # parse again to set updated netloc and scheme parsed_url = urlparse.urlsplit(full_url) self.scheme = parsed_url.scheme self.netloc = parsed_url.netloc if query_params: query_string = self._update_query_params(parsed_url.query, query_params) else: query_string = parsed_url.query return urlparse.urlunsplit((parsed_url.scheme, parsed_url.netloc, parsed_url.path, query_string, '')) _history_regex = ( r"(?:\$HISTORY\[(?P['\"])(?P.+?)(?P=quote1)\]\.)??" ) @staticmethod def _replacer_regex(key): """Compose a regular expression for test template variables.""" case = HTTPTestCase._history_regex return r"%s\$%s\[(?P['\"])(?P.+?)(?P=quote)\]" % ( case, key) @staticmethod def _simple_replacer_regex(key): """Compose a regular expression for simple variable replacement.""" case = HTTPTestCase._history_regex return r"%s\$%s" % (case, key) 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 to a string. """ regex = self._replacer_regex('RESPONSE') match = re.match('^%s$' % regex, message) if match: return self._response_replacer(match, preserve=True) 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.""" response_path = match.group('arg') case = match.group('case') if case: referred_case = self.history[case] else: referred_case = self.prior replacer_class = self.get_content_handler( referred_case.response.get('content-type')) # If no handler can be found use the null replacer, # which returns "foo" when "$RESPONSE['foo']". replacer_class = replacer_class or base.ContentHandler result = replacer_class.replacer( referred_case.response_data, response_path) if preserve: return result else: return six.text_type(result) def _run_request(self, url, method, headers, body, redirect=False): """Run the http request and decode output. The call to make the request will catch a WSGIAppError from wsgi_intercept so that the real traceback from a catastrophic error in the intercepted app can be examined. """ if 'user-agent' not in (key.lower() for key in headers): headers['user-agent'] = "gabbi/%s (Python urllib3)" % __version__ try: response, content = self.http.request( url, method=method, headers=headers, body=body, redirect=redirect ) except wsgi_intercept.WSGIAppError as exc: # Extract and re-raise the wrapped exception. six.reraise(exc.exception_type, exc.exception_value, exc.traceback) # Set headers and location attributes for follow on requests self.response = response if 'location' in response: self.location = response['location'] # Decode and store response decoded_output = utils.decode_response_content(response, content) self.content_type = response.get('content-type', '').lower() loader_class = self.get_content_handler(self.content_type) if decoded_output and loader_class: # save structured response data self.response_data = loader_class.loads(decoded_output) else: 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 base_url = self.replace_template(test['url']) # Save the URL after replacers but before query_parameters 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'] if test['data'] != '': body = self._test_data_to_string( test['data'], utils.extract_content_type(headers, default='')[0]) else: body = '' # ensure body is bytes, encoding as UTF-8 because that's # what we do here if isinstance(body, six.text_type): body = body.encode('UTF-8') if test['poll']: count = int(float(self.replace_template( test['poll'].get('count', 1)))) delay = float(self.replace_template(test['poll'].get('delay', 1))) failure = None while count: try: self._run_request(full_url, method, headers, body, redirect=test['redirects']) self._assert_response() failure = None break except (AssertionError, utils.ConnectionRefused) as exc: failure = exc count -= 1 time.sleep(delay) if failure: raise failure else: self._run_request(full_url, method, headers, body, redirect=test['redirects']) self._assert_response() def _scheme_replace(self, message, escape_regex=False): """Replace $SCHEME with the current protocol.""" 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. If the data is not binary, replace template strings. If the result of the template handling is not a string, run the result through the dumper. """ dumper_class = self.get_content_handler(content_type) if not _is_complex_type(data): if isinstance(data, six.string_types) and data.startswith('<@'): info = self.load_data_file(data.replace('<@', '', 1)) if utils.not_binary(content_type): data = six.text_type(info, 'UTF-8') else: # Return early we are binary content return info else: # We have a complex data structure, try to dump it. if dumper_class: data = self.replace_template(data) data = dumper_class.dumps(data, test=self) else: raise ValueError( 'unable to process data to %s' % content_type) data = self.replace_template(data) # If the result after template handling is not a string, dump # it if there is a suitable dumper. if dumper_class and not isinstance(data, six.string_types): # If there are errors dumping we want them to raise to the # test harness. data = dumper_class.dumps(data, test=self) return data def _test_status(self, expected_status, observed_status): """Confirm we got the expected status. If the status contains one or more || then it is treated as a list of acceptable statuses. """ expected_status = str(expected_status) if '||' in expected_status: statii = [stat.strip() for stat in expected_status.split('||')] else: statii = [expected_status.strip()] self.assert_in_or_print_output(observed_status, statii) def _update_query_params(self, original_query_string, query_params): """Update a query string from query_params dict. An OrderedDict is used to allow easier testing and greater predictability when doing query updates. """ encoded_query_params = OrderedDict() for param, value in query_params.items(): # isinstance used because we can iter a string if isinstance(value, list): encoded_query_params[param] = [ self._clean_query_value(subvalue) for subvalue in value] else: encoded_query_params[param] = ( self._clean_query_value(value)) query_string = urlparse.urlencode( encoded_query_params, doseq=True) if original_query_string: query_string = '&'.join([original_query_string, query_string]) return query_string def assert_in_or_print_output(self, expected, iterable): """Assert the iterable contains expected or print some output. If the output is long, it is limited by either GABBI_MAX_CHARS_OUTPUT in the environment or the MAX_CHARS_OUTPUT constant. """ if utils.not_binary(utils.parse_content_type(self.content_type)[0]): if expected in iterable: return if self.response_data: dumper_class = self.get_content_handler(self.content_type) if dumper_class: full_response = dumper_class.dumps(self.response_data, pretty=True, test=self) else: full_response = self.output else: full_response = self.output max_chars = os.getenv('GABBI_MAX_CHARS_OUTPUT', MAX_CHARS_OUTPUT) response = full_response[0:max_chars] is_truncated = (len(response) != len(full_response)) if iterable == self.output: msg = "'%s' not found in %s%s" % ( expected, response, '\n...truncated...' if is_truncated else '' ) else: msg = "'%s' not found in %s, %sresponse:\n%s" % ( expected, iterable, 'truncated ' if is_truncated else '', response) self.fail(msg) else: self.assertIn(expected, iterable) gabbi-1.44.0/gabbi/driver.py000066400000000000000000000223121332317117700155620ustar00rootroot00000000000000# # 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. """Generate HTTP tests from YAML files Each HTTP request is its own TestCase and can be requested to be run in isolation from other tests. If it is a member of a sequence of requests, prior requests will be run. A sequence is represented by an ordered list in a single YAML file. Each sequence becomes a TestSuite. An entire directory of YAML files is a TestSuite of TestSuites. """ import glob import inspect import os import unittest from unittest import suite import uuid import warnings from gabbi import exception from gabbi import handlers from gabbi import reporter from gabbi import suitemaker from gabbi import utils def build_tests(path, loader, host=None, port=8001, intercept=None, test_loader_name=None, fixture_module=None, response_handlers=None, content_handlers=None, prefix='', require_ssl=False, url=None, 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 a list of HTTP requests. :param path: The directory where yaml files are located. :param loader: The TestLoader. :param host: The host to test against. Do not use with ``intercept``. :param port: The port to test against. Used with ``host``. :param intercept: WSGI app factory for wsgi-intercept. :param test_loader_name: Base name for test classes. Use this to align the naming of the tests with other tests in a system. :param fixture_module: Python module containing fixture classes. :param response_handers: :class:`~gabbi.handlers.ResponseHandler` classes. :type response_handlers: List of ResponseHandler classes. :param content_handlers: ContentHandler classes. :type content_handlers: List of ContentHandler classes. :param prefix: A URL prefix for all URLs that are not fully qualified. :param url: A full URL to test against. Replaces host, port and prefix. :param require_ssl: If ``True``, make all tests default to using SSL. :param inner_fixtures: A list of ``Fixtures`` to use with each 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). """ # 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 ((host is not None) ^ 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: all_test_base_name = inspect.stack()[1] all_test_base_name = os.path.splitext( os.path.basename(all_test_base_name[1]))[0] else: all_test_base_name = None # Initialize response and content handlers. This is effectively # duplication of effort but not results. This allows for # backwards compatibility for existing callers. response_handlers = response_handlers or [] content_handlers = content_handlers or [] handler_objects = [] for handler in (content_handlers + response_handlers + handlers.RESPONSE_HANDLERS): handler_objects.append(handler()) top_suite = suite.TestSuite() for test_file in glob.iglob('%s/*.yaml' % path): if '_' in os.path.basename(test_file): warnings.warn(exception.GabbiSyntaxWarning( "'_' in test filename %s. This can break suite grouping." % test_file)) if intercept: host = str(uuid.uuid4()) 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) if require_ssl: if 'defaults' in suite_dict: suite_dict['defaults']['ssl'] = True else: suite_dict['defaults'] = {'ssl': True} if any((verbose == opt for opt in [True, 'all', 'headers', 'body'])): if 'defaults' in suite_dict: suite_dict['defaults']['verbose'] = verbose 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, test_loader_name=test_loader_name, handlers=handler_objects, inner_fixtures=inner_fixtures) top_suite.addTest(file_suite) return top_suite def py_test_generator(test_dir, host=None, port=8001, intercept=None, prefix=None, test_loader_name=None, fixture_module=None, response_handlers=None, content_handlers=None, require_ssl=False, url=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 a way that pytest can handle. """ import pytest pluginmanager = pytest.config.pluginmanager pluginmanager.import_plugin('gabbi.pytester') loader = unittest.TestLoader() result = reporter.PyTestResult() tests = build_tests(test_dir, loader, host=host, port=port, intercept=intercept, test_loader_name=test_loader_name, fixture_module=fixture_module, response_handlers=response_handlers, content_handlers=content_handlers, prefix=prefix, require_ssl=require_ssl, url=url, use_prior_test=use_prior_test, safe_yaml=safe_yaml) test_list = [] for test in tests: if hasattr(test, '_tests'): # Establish fixtures as if they were tests. These will # be cleaned up by the pytester plugin. test_list.append(('start_%s' % test._tests[0].__class__.__name__, test.start, result)) for subtest in test: test_list.append(('%s' % subtest.__class__.__name__, subtest, result)) test_list.append(('stop_%s' % test._tests[0].__class__.__name__, test.stop)) if metafunc: if metafunc.function == test_pytest: ids = [] args = [] for test in test_list: if len(test) >= 3: name, method, arg = test else: name, method = test arg = None ids.append(name) args.append((method, arg)) metafunc.parametrize("test, result", argvalues=args, ids=ids) else: # preserve backwards compatibility with old calling style return test_list def test_pytest(test, result): if result: test(result) else: test() def test_suite_from_yaml(loader, test_base_name, test_yaml, test_directory, host, port, fixture_module, intercept, prefix=''): """Legacy wrapper retained for backwards compatibility.""" with warnings.catch_warnings(): # ensures warnings filter is restored warnings.simplefilter('default', DeprecationWarning) warnings.warn('test_suite_from_yaml has been renamed to ' 'test_suite_from_dict', DeprecationWarning, stacklevel=2) return suitemaker.test_suite_from_dict( loader, test_base_name, test_yaml, test_directory, host, port, fixture_module, intercept, prefix) gabbi-1.44.0/gabbi/exception.py000066400000000000000000000014361332317117700162710ustar00rootroot00000000000000# # 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. """Gabbi specific exceptions.""" class GabbiFormatError(ValueError): """An exception to encapsulate poorly formed test data.""" pass class GabbiSyntaxWarning(SyntaxWarning): """A warning about syntax that is not desirable.""" pass gabbi-1.44.0/gabbi/fixture.py000066400000000000000000000060511332317117700157570ustar00rootroot00000000000000# # 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. """Manage fixtures for gabbi at the test suite level.""" import contextlib import sys from unittest import case import six class GabbiFixtureError(Exception): """Generic exception for GabbiFixture.""" pass class GabbiFixture(object): """A context manager that operates as a fixture. Subclasses must implement ``start_fixture`` and ``stop_fixture``, each of which contain the logic for stopping and starting whatever the fixture is. What a fixture is is left as an exercise for the implementor. These context managers will be nested so any actual work needs to happen in ``start_fixture`` and ``stop_fixture`` and not in ``__init__``. Otherwise exception handling will not work properly. """ def __init__(self): self.exc_type = None self.exc_value = None self.traceback = None def __enter__(self): self.start_fixture() def __exit__(self, exc_type, value, traceback): self.exc_type = exc_type self.exc_value = value self.traceback = traceback self.stop_fixture() def start_fixture(self): """Implement the actual workings of starting the fixture here.""" pass def stop_fixture(self): """Implement the actual workings of stopping the fixture here.""" pass class SkipAllFixture(GabbiFixture): """A fixture that skips all the tests in the current suite.""" def start_fixture(self): raise case.SkipTest('entire suite skipped') @contextlib.contextmanager def nest(fixtures): """Nest a series of fixtures. This is duplicated from ``nested`` in the stdlib, which has been deprecated because of issues with how exceptions are difficult to handle during ``__init__``. Gabbi needs to nest an unknown number of fixtures dynamically, so the ``with`` syntax that replaces ``nested`` will not work. """ contexts = [] exits = [] exc = (None, None, None) try: for fixture in fixtures: enter_func = fixture.__enter__ exit_func = fixture.__exit__ contexts.append(enter_func()) exits.append(exit_func) yield contexts except Exception: exc = sys.exc_info() finally: while exits: exit_func = exits.pop() try: if exit_func(*exc): exc = (None, None, None) except Exception: exc = sys.exc_info() if exc != (None, None, None): six.reraise(exc[0], exc[1], exc[2]) gabbi-1.44.0/gabbi/handlers/000077500000000000000000000000001332317117700155155ustar00rootroot00000000000000gabbi-1.44.0/gabbi/handlers/__init__.py000066400000000000000000000016231332317117700176300ustar00rootroot00000000000000# # 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. """Package for response and content handlers that process the body of a response in various ways. """ from gabbi.handlers import core from gabbi.handlers import jsonhandler # A list of the default handlers RESPONSE_HANDLERS = [ core.ForbiddenHeadersResponseHandler, core.HeadersResponseHandler, core.StringResponseHandler, jsonhandler.JSONHandler, ] gabbi-1.44.0/gabbi/handlers/base.py000066400000000000000000000077131332317117700170110ustar00rootroot00000000000000# # 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. """Base classes for response and content handlers.""" from gabbi.exception import GabbiFormatError class ResponseHandler(object): """Add functionality for making assertions about an HTTP response. A subclass may implement two methods: ``action`` and ``preprocess``. ``preprocess`` takes one argument, the ``TestCase``. It is called exactly once for each test before looping across the assertions. It is used, rarely, to copy the ``test.output`` into a useful form (such as a parsed DOM). ``action`` takes two or three arguments. If ``test_key_value`` is a list ``action`` is called with the test case and a single list item. If ``test_key_value`` is a dict then ``action`` is called with the test case and a key and value pair. """ test_key_suffix = '' test_key_value = [] def __init__(self): self._register() def __call__(self, test): if test.test_data[self._key]: self.preprocess(test) if not isinstance( test.test_data[self._key], type(self.test_key_value)): raise GabbiFormatError( "%s in '%s' has incorrect type, must be %s" % (self._key, test.test_data['name'], type(self.test_key_value))) for item in test.test_data[self._key]: try: value = test.test_data[self._key][item] except (TypeError, KeyError): value = None self.action(test, item, value=value) def preprocess(self, test): """Do any pre-single-test preprocessing.""" pass def action(self, test, item, value=None): """Test an individual entry for this response handler. If the entry is a key value pair the key is in item and the value in value. Otherwise the entry is considered a single item from a list. """ pass def _register(self): """Register this handler on the provided test class.""" self.response_handler = None self.content_handler = None if self.test_key_suffix: self._key = 'response_%s' % self.test_key_suffix self.test_base = {self._key: self.test_key_value} self.response_handler = self if hasattr(self, 'accepts'): self.content_handler = self def __eq__(self, other): if isinstance(other, ResponseHandler): return self.__class__ == other.__class__ return False def __ne__(self, other): return not self.__eq__(other) class ContentHandler(ResponseHandler): """A subclass of ResponseHandlers that adds content handling.""" @staticmethod def accepts(content_type): """Return True if this handler can handler this type.""" return False @classmethod def replacer(cls, response_data, path): """Return the string that is replacing RESPONSE.""" return path @staticmethod def dumps(data, pretty=False, test=None): """Return structured data as a string. If pretty is true, prettify. """ return data @staticmethod def loads(data): """Create structured (Python) data from a stream.""" return data @staticmethod def load_data_file(test, file_path): """Return the string content of the file specified by the file_path.""" return test.load_data_file(file_path) gabbi-1.44.0/gabbi/handlers/core.py000066400000000000000000000055271332317117700170300ustar00rootroot00000000000000# # 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. """Core response handlers.""" from gabbi.handlers import base class StringResponseHandler(base.ResponseHandler): """Test for matching strings in the the response body.""" test_key_suffix = 'strings' test_key_value = [] def action(self, test, expected, value=None): expected = test.replace_template(expected) test.assert_in_or_print_output(expected, test.output) class ForbiddenHeadersResponseHandler(base.ResponseHandler): """Test that listed headers are not in the response.""" test_key_suffix = 'forbidden_headers' test_key_value = [] def action(self, test, forbidden, value=None): # normalize forbidden header to lower case forbidden = test.replace_template(forbidden).lower() test.assertNotIn(forbidden, test.response, 'Forbidden header %s found in response' % forbidden) class HeadersResponseHandler(base.ResponseHandler): """Compare expected headers with actual headers. If a header value is wrapped in ``/`` it is treated as a raw regular expression. Headers values are always treated as strings. """ test_key_suffix = 'headers' test_key_value = {} def action(self, test, header, value=None): header = header.lower() # case-insensitive comparison response = test.response 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]) except KeyError: raise AssertionError( "'%s' header not present in response: %s" % ( header, response.keys())) if is_regex: header_value = header_value[1:-1] test.assertRegex( response_value, header_value, 'Expect header %s to match /%s/, got %s' % (header, header_value, response_value)) else: test.assertEqual(header_value, response_value, 'Expect header %s with value %s, got %s' % (header, header_value, response[header])) gabbi-1.44.0/gabbi/handlers/jsonhandler.py000066400000000000000000000116161332317117700204030ustar00rootroot00000000000000# # 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. """JSON-related content handling.""" import json import six from gabbi.handlers import base from gabbi import json_parser class JSONHandler(base.ContentHandler): """A ContentHandler for JSON * Structured test ``data`` is turned into JSON when request content-type is JSON. * Response bodies that are JSON strings are made into Python data on the test ``response_data`` attribute when the response content-type is JSON. * A ``response_json_paths`` response handler is added. * JSONPaths in $RESPONSE substitutions are supported. """ test_key_suffix = 'json_paths' test_key_value = {} @staticmethod def accepts(content_type): content_type = content_type.split(';', 1)[0].strip() return (content_type.endswith('+json') or content_type.startswith('application/json')) @classmethod def replacer(cls, response_data, match): return cls.extract_json_path_value(response_data, match) @staticmethod def dumps(data, pretty=False, test=None): if pretty: return json.dumps(data, indent=2, separators=(',', ': ')) else: return json.dumps(data) @staticmethod def loads(data): return json.loads(data) @staticmethod def load_data_file(test, file_path): info = test.load_data_file(file_path) info = six.text_type(info, 'UTF-8') return json.loads(info) @staticmethod def extract_json_path_value(data, path): """Extract the value at JSON Path path from the data. The input data is a Python datastructure, not a JSON string. """ path_expr = json_parser.parse(path) matches = [match.value for match in path_expr.find(data)] if matches: if len(matches) > 1: return matches else: return matches[0] else: raise ValueError( "JSONPath '%s' failed to match on data: '%s'" % (path, data)) def action(self, test, path, value=None): """Test json_paths against json data.""" # Do template expansion in the left hand side. lhs_path = test.replace_template(path) rhs_path = rhs_match = None try: lhs_match = self.extract_json_path_value( test.response_data, lhs_path) except AttributeError: raise AssertionError('unable to extract JSON from test results') except ValueError: raise AssertionError('left hand side json path %s cannot match ' '%s' % (path, test.response_data)) # read data from disk if the value starts with '<@' if isinstance(value, six.string_types) and value.startswith('<@'): # Do template expansion in the rhs if rhs_path is provided. if ':' in value: value, rhs_path = value.split(':$', 1) rhs_path = test.replace_template('$' + rhs_path) value = self.load_data_file(test, value.replace('<@', '', 1)) if rhs_path: try: rhs_match = self.extract_json_path_value(value, rhs_path) except AttributeError: raise AssertionError('unable to extract JSON from data on ' 'disk') except ValueError: raise AssertionError('right hand side json path %s cannot ' 'match %s' % (rhs_path, value)) # If expected is a string, check to see if it is a regex. is_regex = (isinstance(value, six.string_types) and value.startswith('/') and value.endswith('/') and len(value) > 1) expected = (rhs_match or test.replace_template(value, escape_regex=is_regex)) match = lhs_match if is_regex and not rhs_match: expected = expected[1:-1] # match may be a number so stringify match = six.text_type(match) test.assertRegex( match, expected, 'Expect jsonpath %s to match /%s/, got %s' % (path, expected, match)) else: test.assertEqual(expected, match, 'Unable to match %s as %s, got %s' % (path, expected, match)) gabbi-1.44.0/gabbi/handlers/yaml_disk_loading_jsonhandler.py000066400000000000000000000027641332317117700241400ustar00rootroot00000000000000# # 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. """JSON-related content handling with YAML data disk loading.""" import yaml import six from gabbi.handlers import jsonhandler class YAMLDiskLoadingJSONHandler(jsonhandler.JSONHandler): """A ContentHandler for JSON responses that loads YAML from disk * Structured test ``data`` is turned into JSON when request content-type is JSON. * Response bodies that are JSON strings are made into Python data on the test ``response_data`` attribute when the response content-type is JSON. * A ``response_json_paths`` response handler is added. Data read from disk during this handle will be loaded with the yaml.safe_load method to support both JSON and YAML data sources from disk. * JSONPaths in $RESPONSE substitutions are supported. """ @staticmethod def load_data_file(test, file_path): info = test.load_data_file(file_path) info = six.text_type(info, 'UTF-8') return yaml.safe_load(info) gabbi-1.44.0/gabbi/httpclient.py000066400000000000000000000153271332317117700164550ustar00rootroot00000000000000# # 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. from __future__ import print_function import os import sys import urllib3 from gabbi.handlers import jsonhandler from gabbi import utils # Disable SSL warnings otherwise tests which process stderr will get # extra information. urllib3.disable_warnings() class Http(urllib3.PoolManager): """A subclass of the ``urllib3.PoolManager`` to munge the data. This transforms the response to look more like what httplib2 provided when it was used as the HTTP client. """ def request(self, absolute_uri, method, body, headers, redirect): if redirect: retry = urllib3.util.Retry(raise_on_redirect=False, redirect=5) else: retry = urllib3.util.Retry(total=False, redirect=False) response = super(Http, self).request( method, absolute_uri, body=body, headers=headers, retries=retry) # Transform response into something akin to httplib2 # response object. content = response.data status = response.status reason = response.reason headers = response.headers headers['status'] = str(status) headers['reason'] = reason return headers, content class VerboseHttp(Http): """A subclass of ``Http`` that verbosely reports on activity. If the output is a tty or ``GABBI_FORCE_COLOR`` is set in the environment, then output will be colorized according to ``COLORMAP``. Output can include request and response headers, request and response body content (if of a printable content type), or both. The color of the output has reasonable defaults. These may be overridden by setting the following environment variables * ``GABBI_CAPTION_COLOR`` * ``GABBI_HEADER_COLOR`` * ``GABBI_REQUEST_COLOR`` * ``GABBI_STATUS_COLOR`` to any of: BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE """ # A list of request and response headers to never display. # Can include response object attributes that are not # technically headers. HEADER_BLACKLIST = [ 'status', 'reason', ] REQUEST_PREFIX = '>' RESPONSE_PREFIX = '<' COLORMAP = { 'caption': os.environ.get('GABBI_CAPTION_COLOR', 'BLUE').upper(), 'header': os.environ.get('GABBI_HEADER_COLOR', 'YELLOW').upper(), 'request': os.environ.get('GABBI_REQUEST_COLOR', 'CYAN').upper(), 'status': os.environ.get('GABBI_STATUS_COLOR', 'CYAN').upper(), } def __init__(self, **kwargs): self.caption = kwargs.pop('caption') self._show_body = kwargs.pop('body') self._show_headers = kwargs.pop('headers') self._use_color = kwargs.pop('colorize') self._stream = kwargs.pop('stream') if self._use_color: self.colorize = utils.get_colorizer(self._stream) super(VerboseHttp, self).__init__(**kwargs) def request(self, absolute_uri, method, body, headers, redirect): """Display request parameters before requesting.""" self._verbose_output('#### %s ####' % self.caption, color=self.COLORMAP['caption']) self._verbose_output('%s %s' % (method, absolute_uri), prefix=self.REQUEST_PREFIX, color=self.COLORMAP['request']) self._print_headers(headers, prefix=self.REQUEST_PREFIX) self._print_body(headers, body) response, content = super(VerboseHttp, self).request( absolute_uri, method, body, headers, redirect) # Blank line for division self._verbose_output('') self._verbose_output('%s %s' % (response['status'], response['reason']), prefix=self.RESPONSE_PREFIX, color=self.COLORMAP['status']) self._print_headers(response, prefix=self.RESPONSE_PREFIX) # response body self._print_body(response, content) self._verbose_output('') return (response, content) def _print_headers(self, headers, prefix=''): """Output request or response headers.""" if self._show_headers: for key in headers: if key not in self.HEADER_BLACKLIST: self._print_header(key, headers[key], prefix=prefix) def _print_body(self, headers, content): """Output body if not binary.""" content_type = utils.extract_content_type(headers)[0] if self._show_body and utils.not_binary(content_type): content = utils.decode_response_content(headers, content) # TODO(cdent): Using the JSONHandler here instead of # just the json module to make it clear that eventually # we could pretty print any printable output by using a # handler's loads() and dumps(). Not doing that now # because it would be pointless (no other interesting # handlers) and this approach may be entirely wrong. if content and jsonhandler.JSONHandler.accepts(content_type): data = jsonhandler.JSONHandler.loads(content) content = jsonhandler.JSONHandler.dumps(data, pretty=True) self._verbose_output('') self._verbose_output(content) def _print_header(self, name, value, prefix='', stream=None): """Output one single header.""" header = self.colorize(self.COLORMAP['header'], "%s:" % name) self._verbose_output("%s %s" % (header, value), prefix=prefix, stream=stream) def _verbose_output(self, message, prefix='', color=None, stream=None): """Output a message.""" stream = stream or self._stream if prefix and message: print(prefix, end=' ', file=stream) if color: message = self.colorize(color, message) print(message, file=stream) def get_http(verbose=False, caption=''): """Return an ``Http`` class for making requests.""" if not verbose: return Http(strict=True) headers = False if verbose == 'body' else True body = False if verbose == 'headers' else True return VerboseHttp(headers=headers, body=body, stream=sys.stdout, caption=caption, colorize=True, strict=True) gabbi-1.44.0/gabbi/json_parser.py000066400000000000000000000015111332317117700166120ustar00rootroot00000000000000# # 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. """Keep one single global jsonpath parser.""" from jsonpath_rw_ext import parser PARSER = None def parse(path): """Parse a JSONPath expression use the global parser.""" global PARSER if not PARSER: PARSER = parser.ExtentedJsonPathParser() return PARSER.parse(path) gabbi-1.44.0/gabbi/pytester.py000066400000000000000000000107171332317117700161540ustar00rootroot00000000000000# # 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 pytest plugin that runs under the covers with gabbi. This is a backwards compatible improvement to the way gabbi can work with pytest. Tests are loaded (and yielded in the same way) but they are filtered more correctly, and fixture start and stops are done more correctly. This allows the test count to be accurate, which is nice. """ import pytest # Globals storing the test-like functions to be used when starting # and stopping a suite. STARTS = {} STOPS = {} def get_cleanname(item): """Extract a test name from a pytest Function item.""" if '[' in item.name: cleanname = item.name.split('[', 1)[1] cleanname = cleanname.split(']', 1)[0] return cleanname return item.name def get_suitename(name): """Extract a test suite from a clean name. This is fragile. It assumes there are no underscores in suite names, which is not always true. """ if name.startswith('start_') or name.startswith('stop_'): _, name = name.split('_', 1) return name.split('_', 2)[1] def c_pytest_collection_modifyitems(items, config): """Set the starters and stoppers for a limited collection of tests.""" latest_suite = None latest_item = None for item in items: cleanname = get_cleanname(item) if not cleanname.startswith( ('stop_driver_', 'start_driver_', 'driver_')): continue prefix, testname = cleanname.split('_', 1) suitename, _ = testname.split('_', 1) if prefix == 'start' or prefix == 'stop': continue if latest_suite != suitename: item.starter = STARTS[suitename] if latest_item: latest_item.stopper = STOPS[ get_suitename(get_cleanname(latest_item))] latest_suite = suitename latest_item = item # Set the last stopper in the list if latest_item: latest_item.stopper = STOPS[get_suitename(get_cleanname(latest_item))] def a_pytest_collection_modifyitems(items, config): """Traverse collected tests to save START and STOPS. Remove those START and STOPS from the tests to run. """ remaining = [] deselected = [] for item in items: cleanname = get_cleanname(item) if not cleanname.startswith( ('stop_driver_', 'start_driver_', 'driver_')): remaining.append(item) continue suitename = get_suitename(cleanname) if cleanname.startswith('start_'): test = item.callspec.params['test'] result = item.callspec.params['result'] # TODO(cdent): Consider a named tuple here STARTS[suitename] = (test, result, []) deselected.append(item) elif cleanname.startswith('stop_'): test = item.callspec.params['test'] STOPS[suitename] = test 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 @pytest.hookimpl(hookwrapper=True) def pytest_collection_modifyitems(items, config): """Hook for processing collected tests. Discover start and stops, then use the default hook for filter for keywords and markers, then attach starter and stopper to the remaining tests. """ a_pytest_collection_modifyitems(items, config) yield c_pytest_collection_modifyitems(items, config) def pytest_runtest_setup(item): """Run a starter if a test has one. This is done before run, so it means that a single test will run its priors after running this. """ if hasattr(item, 'starter'): test, result, tests = item.starter test(result, tests) def pytest_runtest_teardown(item, nextitem): """Run a stopper if a test has one.""" if hasattr(item, 'stopper'): item.stopper() gabbi-1.44.0/gabbi/reporter.py000066400000000000000000000106041332317117700161320ustar00rootroot00000000000000# coding: utf-8 # 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. """TestRunner and TestResult for gabbi-run.""" from unittest import TestResult from unittest import TextTestResult from unittest import TextTestRunner import pytest from gabbi import utils class ConciseTestResult(TextTestResult): """A TextTestResult with simple but useful output. If the output is a tty or GABBI_FORCE_COLOR is set in the environment, output will be colorized. """ def __init__(self, stream, descriptions, verbosity): super(ConciseTestResult, self).__init__( stream, descriptions, verbosity) self.colorize = utils.get_colorizer(stream) def startTest(self, test): super(TextTestResult, self).startTest(test) if self.showAll: self.stream.write('... ') self.stream.flush() def addSuccess(self, test): super(TextTestResult, self).addSuccess(test) if self.showAll: self.stream.write(self.colorize('GREEN', '✓ ')) self.stream.writeln(self.getDescription(test)) def addFailure(self, test, err): super(TextTestResult, self).addFailure(test, err) if self.showAll: self.stream.write(self.colorize('RED', '✗ ')) self.stream.writeln(self.getDescription(test)) def addError(self, test, err): super(TextTestResult, self).addError(test, err) if self.showAll: self.stream.write(self.colorize('RED', 'E ')) self.stream.writeln(self.getDescription(test)) def addSkip(self, test, reason): super(TextTestResult, self).addSkip(test, reason) if self.showAll: self.stream.write('- ') self.stream.writeln(self.getDescription(test)) self.stream.writeln('\t[skipped] {0!r}'.format(reason)) def addExpectedFailure(self, test, err): super(TextTestResult, self).addExpectedFailure(test, err) if self.showAll: self.stream.write('o ') self.stream.writeln(self.getDescription(test)) self.stream.writeln('\t[expected failure]') def addUnexpectedSuccess(self, test): super(TextTestResult, self).addUnexpectedSuccess(test) if self.showAll: self.stream.write('! ') self.stream.writeln(self.getDescription(test)) self.stream.writeln('\t[unexpected success]') def getDescription(self, test): # Chop the test method ('test_request') off the test.id(). name = test.id().rsplit('.', 1)[0] desc = test.test_data.get('desc', None) return ': '.join((name, desc)) if desc else name def _exc_info_to_string(self, err, test): """Override exception to string handling The default does too much. We don't want doctoring. We want information! """ return err def printErrorList(self, flavor, errors): for test, err in errors: # err[0] is the type of exception # err[1] is the args of the exception # err[3] is the traceback, not currently used self.stream.writeln('%s: %s' % (flavor, self.getDescription(test))) message = str(err[1]) for line in message.splitlines(): self.stream.writeln('\t%s' % line) class PyTestResult(TestResult): """Wrap a test result to allow it to work with pytest. The main behaviors here are: * to turn what had been exceptions back into exceptions * use pytest's skip and xfail methods """ def addFailure(self, test, err): raise err[1] def addError(self, test, err): raise err[1] def addSkip(self, test, reason): pytest.skip(reason) def addExpectedFailure(self, test, err): pytest.xfail('%s' % err[1]) class ConciseTestRunner(TextTestRunner): """A TextTestRunner that uses ConciseTestResult for reporting results.""" resultclass = ConciseTestResult gabbi-1.44.0/gabbi/runner.py000066400000000000000000000203041332317117700155770ustar00rootroot00000000000000# # 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. """Implementation of a command-line runner for gabbi files (AKA suites).""" from __future__ import print_function import argparse from importlib import import_module import os import sys import unittest from gabbi import handlers from gabbi.reporter import ConciseTestRunner from gabbi import suitemaker from gabbi import utils def run(): """Run simple tests from STDIN. This command provides a way to run a set of tests encoded in YAML that is provided on STDIN. No fixtures are supported, so this is primarily designed for use with real running services. Host and port information may be provided in three different ways: * In the URL value of the tests. * In a `host` or `host:port` argument on the command line. * In a URL on the command line. An example run might looks like this:: gabbi-run example.com:9999 < mytest.yaml or:: gabbi-run http://example.com:999 < mytest.yaml It is also possible to provide a URL prefix which can be useful if the target application might be mounted in different locations. An example:: gabbi-run example.com:9999 /mountpoint < mytest.yaml or:: gabbi-run http://example.com:9999/mountpoint < mytest.yaml Use `-x` or `--failfast` to abort after the first error or failure:: gabbi-run -x example.com:9999 /mountpoint < mytest.yaml Use `-v` or `--verbose` with a value of `all`, `headers` or `body` to turn on verbosity for all tests being run. Multiple files may be named as arguments, separated from other arguments by a ``--``. Each file will be run as a separate test suite:: gabbi-run http://example.com -- /path/to/x.yaml /path/to/y.yaml Output is formatted as unittest summary information. """ parser = _make_argparser() argv, input_files = extract_file_paths(sys.argv) args = parser.parse_args(argv[1:]) host, port, prefix, force_ssl = utils.host_info_from_target( args.target, args.prefix) handler_objects = initialize_handlers(args.response_handlers) verbosity = args.verbosity failfast = args.failfast failure = False # Keep track of file names that have failures. failures = [] if not input_files: success = run_suite(sys.stdin, handler_objects, host, port, prefix, force_ssl, failfast, verbosity=verbosity, safe_yaml=args.safe_yaml) failure = not success else: for input_file in input_files: name = os.path.splitext(os.path.basename(input_file))[0] with open(input_file, 'r') as fh: data_dir = os.path.dirname(input_file) success = run_suite(fh, handler_objects, host, port, prefix, force_ssl, failfast, data_dir=data_dir, 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 failure = not success if failure and failfast: break if failures: print("There were failures in the following files:", file=sys.stderr) print('\n'.join(failures), file=sys.stderr) sys.exit(failure) def run_suite(handle, handler_objects, host, port, prefix, force_ssl=False, failfast=False, data_dir='.', verbosity=False, name='input', safe_yaml=True): """Run the tests from the YAML in handle.""" data = utils.load_yaml(handle, safe=safe_yaml) if force_ssl: if 'defaults' in data: data['defaults']['ssl'] = True else: data['defaults'] = {'ssl': True} if verbosity: if 'defaults' in data: data['defaults']['verbose'] = verbosity else: data['defaults'] = {'verbose': verbosity} loader = unittest.defaultTestLoader test_suite = suitemaker.test_suite_from_dict( loader, name, data, data_dir, host, port, None, None, prefix=prefix, handlers=handler_objects, test_loader_name='gabbi-runner') result = ConciseTestRunner( verbosity=2, failfast=failfast).run(test_suite) return result.wasSuccessful() def initialize_handlers(response_handlers): custom_response_handlers = [] handler_objects = [] for import_path in response_handlers or []: for handler in load_response_handlers(import_path): custom_response_handlers.append(handler) for handler in handlers.RESPONSE_HANDLERS + custom_response_handlers: handler_objects.append(handler()) return handler_objects def load_response_handlers(import_path): """Load and return custom response handlers from the import path. The import path references either a specific response handler class ("package.module:class") or a module that contains one or more response handler classes ("package.module"). For the latter, the module is expected to contain a ``gabbi_response_handlers`` object, which is either a list of response handler classes or a function returning such a list. """ if ":" in import_path: # package.module:class module_name, handler_name = import_path.rsplit(":", 1) module = import_module(module_name) handler = getattr(module, handler_name) custom_handlers = [handler] else: # package.module shorthand, expecting gabbi_response_handlers module = import_module(import_path) custom_handlers = module.gabbi_response_handlers if callable(custom_handlers): custom_handlers = custom_handlers() return custom_handlers def extract_file_paths(argv): """Extract file paths from the command-line. File path arguments follow a `--` end-of-options delimiter, if any. """ try: # extract file paths, separated by `--` i = argv.index("--") input_files = argv[i + 1:] argv = argv[:i] except ValueError: input_files = None return argv, input_files def _make_argparser(): """Set up the argparse.ArgumentParser.""" parser = argparse.ArgumentParser(description='Run gabbi tests from STDIN') parser.add_argument( 'target', nargs='?', default='stub', help='A fully qualified URL (with optional path as prefix) ' 'to the primary target or a host and port, : separated. ' 'If using an IPV6 address for the host in either form, ' 'wrap it in \'[\' and \']\'.' ) parser.add_argument( 'prefix', nargs='?', default=None, help='Path prefix where target app is mounted. Only used when ' 'target is of the form host[:port]' ) parser.add_argument( '-x', '--failfast', action='store_true', help='Exit on first failure' ) parser.add_argument( '-r', '--response-handler', nargs='?', default=None, dest='response_handlers', action='append', help='Custom response handler. Should be an import path of the ' 'form package.module or package.module:class.' ) parser.add_argument( '-v', '--verbose', dest='verbosity', 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 if __name__ == '__main__': run() gabbi-1.44.0/gabbi/suite.py000066400000000000000000000121451332317117700154230ustar00rootroot00000000000000# # 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 TestSuite for containing gabbi tests. This suite has two features: the contained tests are ordered and there are suite-level fixtures that operate as context managers. """ import sys import unittest from wsgi_intercept import interceptor from gabbi import fixture def noop(*args): """A noop method used to disable collected tests.""" pass class GabbiSuite(unittest.TestSuite): """A TestSuite with fixtures. The suite wraps the tests with a set of nested context managers that operate as fixtures. If a fixture raises unittest.case.SkipTest during setup, all the tests in this suite will be skipped. """ def run(self, result, debug=False): """Override TestSuite run to start suite-level fixtures. To avoid exception confusion, use a null Fixture when there are no fixtures. """ fixtures, intercept, host, port, prefix = self._get_intercept() try: with fixture.nest([fix() for fix in fixtures]): if intercept: with interceptor.Urllib3Interceptor( intercept, host, port, prefix): result = super(GabbiSuite, self).run(result, debug) else: result = super(GabbiSuite, self).run(result, debug) except unittest.SkipTest as exc: for test in self._tests: result.addSkip(test, str(exc)) # If we have an exception in the nested fixtures, that means # there's been an exception somewhere in the cycle other # than a specific test (as that would have been caught # already), thus from a fixture. If that exception were to # continue to raise here, then some test runners would # swallow it and the traceback of the failure would be # undiscoverable. To ensure the traceback is reported (via # the testrunner) to a human, the first test in the suite is # marked as having an error (it's fixture failed) and then # the entire suite is skipped, and the result stream told # we're done. If there are no tests (an empty suite) the # exception is re-raised. except Exception as exc: if self._tests: result.addError(self._tests[0], sys.exc_info()) for test in self._tests: result.addSkip(test, 'fixture failure') result.stop() else: raise return result def start(self, result, tests): """Start fixtures when using pytest.""" fixtures, intercept, host, port, prefix = self._get_intercept() self.used_fixtures = [] try: for fix in fixtures: fix_object = fix() fix_object.__enter__() self.used_fixtures.append(fix_object) except unittest.SkipTest as exc: # Disable the already collected tests that we now wish # to skip. for test in tests: test.run = noop test.add_marker('skip') result.addSkip(self, str(exc)) if intercept: intercept_fixture = interceptor.Urllib3Interceptor( intercept, host, port, prefix) intercept_fixture.__enter__() self.used_fixtures.append(intercept_fixture) def stop(self): """Stop fixtures when using pytest.""" for fix in reversed(self.used_fixtures): fix.__exit__(None, None, None) def _get_intercept(self): fixtures = [fixture.GabbiFixture] intercept = host = port = prefix = None try: first_test = self._find_first_full_test() fixtures = first_test.fixtures host = first_test.host port = first_test.port prefix = first_test.prefix intercept = first_test.intercept # Unbind a passed in WSGI application. During the # metaclass building process intercept becomes bound. try: intercept = intercept.__func__ except AttributeError: pass except AttributeError: pass return fixtures, intercept, host, port, prefix def _find_first_full_test(self): """Traverse a sparse test suite to find the first HTTPTestCase. When only some tests are requested empty TestSuites replace the unrequested tests. """ for test in self._tests: if hasattr(test, 'fixtures'): return test raise AttributeError('no fixtures found') gabbi-1.44.0/gabbi/suitemaker.py000066400000000000000000000252601332317117700164450ustar00rootroot00000000000000# # 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. """The code that creates a suite of tests. The key piece of code is :meth:`test_suite_from_dict`. It produces a :class:`gabbi.suite.GabbiSuite` containing one or more :class:`gabbi.case.HTTPTestCase`. """ import copy import functools from gabbi import case from gabbi.exception import GabbiFormatError from gabbi import httpclient from gabbi import suite class TestMaker(object): """A class for encapsulating test invariants. All of the tests in a single gabbi file have invariants which are provided when creating each HTTPTestCase. It is not useful to pass these around when making each test case. So they are wrapped in this class which then has make_one_test called multiple times to generate all the tests in the suite. """ def __init__(self, test_base_name, test_defaults, test_directory, fixture_classes, loader, host, port, intercept, prefix, response_handlers, content_handlers, test_loader_name=None, inner_fixtures=None): self.test_base_name = test_base_name self.test_defaults = test_defaults self.default_keys = set(test_defaults.keys()) self.test_directory = test_directory self.fixture_classes = fixture_classes self.host = host self.port = port self.loader = loader self.intercept = intercept self.prefix = prefix self.test_loader_name = test_loader_name self.inner_fixtures = inner_fixtures or [] self.content_handlers = content_handlers self.response_handlers = response_handlers def make_one_test(self, test_dict, prior_test): """Create one single HTTPTestCase. The returned HTTPTestCase is added to the TestSuite currently being built (one per YAML file). """ test = copy.deepcopy(self.test_defaults) try: test_update(test, test_dict) except KeyError as exc: raise GabbiFormatError('invalid key in test: %s' % exc) except AttributeError as exc: if not isinstance(test_dict, dict): raise GabbiFormatError( 'test chunk is not a dict at "%s"' % test_dict) else: # NOTE(cdent): Not clear this can happen but just in case. raise GabbiFormatError( 'malformed test chunk "%s": %s' % (test_dict, exc)) test_name = self._set_test_name(test) self._set_test_method_and_url(test, test_name) self._validate_keys(test, test_name) http_class = httpclient.get_http(verbose=test['verbose'], caption=test['name']) if prior_test: history = prior_test.history else: history = {} test_method_name = 'test_request' test_method = getattr(case.HTTPTestCase, test_method_name) @case.testcase.skipIf(self.host == '', 'No host configured') @functools.wraps(test_method) def do_test(*args, **kwargs): return test_method(*args, **kwargs) # Use metaclasses to build a class of the necessary type # and name with relevant arguments. klass = TestBuilder(test_name, (case.HTTPTestCase,), {'test_data': test, 'test_directory': self.test_directory, 'fixtures': self.fixture_classes, 'inner_fixtures': self.inner_fixtures, 'http': http_class, 'host': self.host, 'intercept': self.intercept, 'content_handlers': self.content_handlers, 'response_handlers': self.response_handlers, 'port': self.port, 'prefix': self.prefix, 'prior': prior_test, 'history': history, test_method_name: do_test, }) # We've been asked to, make this test class think it comes # from a different module. if self.test_loader_name: klass.__module__ = self.test_loader_name tests = self.loader.loadTestsFromTestCase(klass) history[test['name']] = tests._tests[0] # Return the first (and only) test in the klass. return tests._tests[0] def _set_test_name(self, test): """Set the name of the test The original name is lowercased and spaces are replaces with '_'. The result is appended to the test_base_name, which is based on the name of the input data file. """ if not test['name']: raise GabbiFormatError('Test name missing in a test in %s.' % self.test_base_name) return '%s_%s' % (self.test_base_name, test['name'].lower().replace(' ', '_')) @staticmethod def _set_test_method_and_url(test, test_name): """Extract the base URL and method for this test. If there is an upper case key in the test, that is used as the method and the value is used as the URL. If there is more than one uppercase that is a GabbiFormatError. If there is no upper case key then 'url' must be present. """ method_key = None for key, val in test.items(): if _is_method_shortcut(key): if method_key: raise GabbiFormatError( 'duplicate method/URL directive in "%s"' % test_name) test['method'] = key test['url'] = val method_key = key if method_key: del test[method_key] if not test['url']: raise GabbiFormatError('Test url missing in test %s.' % test_name) def _validate_keys(self, test, test_name): """Check for invalid keys. If there are any, raise a GabbiFormatError. """ test_keys = set(test.keys()) if test_keys != self.default_keys: raise GabbiFormatError( 'Invalid test keys used in test %s: %s' % (test_name, ', '.join(list(test_keys - self.default_keys)))) class TestBuilder(type): """Metaclass to munge a dynamically created test.""" required_attributes = {'has_run': False} def __new__(mcs, name, bases, attributes): attributes.update(mcs.required_attributes) return type.__new__(mcs, name, bases, attributes) def test_suite_from_dict(loader, test_base_name, suite_dict, test_directory, host, port, fixture_module, intercept, prefix='', handlers=None, test_loader_name=None, inner_fixtures=None): """Generate a GabbiSuite from a dict represent a list of tests. The dict takes the form: :param fixtures: An optional list of fixture classes that this suite can use. :param defaults: An optional dictionary of default values to be used in each test. :param tests: A list of individual tests, themselves each being a dictionary. See :data:`gabbi.case.BASE_TEST`. """ try: test_data = suite_dict['tests'] except KeyError: raise GabbiFormatError('malformed test file, "tests" key required') except TypeError: # `suite_dict` appears not to be a dictionary; we cannot infer # any details or suggestions on how to fix it, thus discarding # the original exception in favor of a generic error raise GabbiFormatError('malformed test file, invalid format') handlers = handlers or [] response_handlers = [] content_handlers = [] # Merge global with per-suite defaults default_test_dict = copy.deepcopy(case.HTTPTestCase.base_test) seen_keys = set() for handler in handlers: default_test_dict.update(handler.test_base) if handler.response_handler: if handler.test_key_suffix not in seen_keys: response_handlers.append(handler.response_handler) seen_keys.add(handler.test_key_suffix) if handler.content_handler: content_handlers.append(handler.content_handler) local_defaults = _validate_defaults(suite_dict.get('defaults', {})) test_update(default_test_dict, local_defaults) # Establish any fixture classes used in this file. fixtures = suite_dict.get('fixtures', None) fixture_classes = [] if fixtures and fixture_module: for fixture_class in fixtures: fixture_classes.append(getattr(fixture_module, fixture_class)) test_maker = TestMaker(test_base_name, default_test_dict, test_directory, fixture_classes, loader, host, port, intercept, prefix, response_handlers, content_handlers, test_loader_name=test_loader_name, inner_fixtures=inner_fixtures) file_suite = suite.GabbiSuite() prior_test = None for test_dict in test_data: this_test = test_maker.make_one_test(test_dict, prior_test) file_suite.addTest(this_test) prior_test = this_test return file_suite def test_update(orig_dict, new_dict): """Modify test in place to update with new data.""" for key, val in new_dict.items(): if key == 'data': orig_dict[key] = val elif isinstance(val, dict): orig_dict[key].update(val) elif isinstance(val, list): orig_dict[key] = orig_dict.get(key, []) + val else: orig_dict[key] = val def _is_method_shortcut(key): """Is this test key indicating a request method. It is a request method if it is all upper case. """ return key.isupper() def _validate_defaults(defaults): """Ensure default test settings are acceptable. Raises GabbiFormatError for invalid settings. """ if any(_is_method_shortcut(key) for key in defaults): raise GabbiFormatError('"METHOD: url" pairs not allowed in defaults') return defaults gabbi-1.44.0/gabbi/tests/000077500000000000000000000000001332317117700150575ustar00rootroot00000000000000gabbi-1.44.0/gabbi/tests/README000066400000000000000000000004531332317117700157410ustar00rootroot00000000000000Some of the tests in this collection will attempt to connect to google over the internet to validate some behaviors using real socket connections. If this is not desirable (for example behind firewalls or in packaging situations) set GABBI_SKIP_NETWORK to true in the environment running the tests. gabbi-1.44.0/gabbi/tests/__init__.py000066400000000000000000000011571332317117700171740ustar00rootroot00000000000000# # 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. import six six.add_move(six.MovedModule('mock', 'mock', 'unittest.mock')) gabbi-1.44.0/gabbi/tests/custom_response_handler.py000066400000000000000000000015211332317117700223550ustar00rootroot00000000000000# # 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. from gabbi.handlers import base def gabbi_response_handlers(): return [CustomResponseHandler] class CustomResponseHandler(base.ResponseHandler): test_key_suffix = 'custom' test_key_value = [] def action(self, test, item, value=None): test.assertTrue(item in test.output) gabbi-1.44.0/gabbi/tests/gabbits_handlers/000077500000000000000000000000001332317117700203525ustar00rootroot00000000000000gabbi-1.44.0/gabbi/tests/gabbits_handlers/cat.json000066400000000000000000000000531332317117700220120ustar00rootroot00000000000000{ "type": "cat", "sound": "meow" } gabbi-1.44.0/gabbi/tests/gabbits_handlers/data.json000066400000000000000000000000251332317117700221530ustar00rootroot00000000000000{"foo": {"bár": 1}} gabbi-1.44.0/gabbi/tests/gabbits_handlers/pets.json000066400000000000000000000001731332317117700222210ustar00rootroot00000000000000[ { "type": "cat", "sound": "meow" }, { "type": "dog", "sound": "woof" } ] gabbi-1.44.0/gabbi/tests/gabbits_handlers/subdir/000077500000000000000000000000001332317117700216425ustar00rootroot00000000000000gabbi-1.44.0/gabbi/tests/gabbits_handlers/subdir/data.yaml000066400000000000000000000000171332317117700234350ustar00rootroot00000000000000foo: bár: 1 gabbi-1.44.0/gabbi/tests/gabbits_handlers/subdir/pets.yaml000066400000000000000000000000641332317117700235010ustar00rootroot00000000000000- type: cat sound: meow - type: dog sound: woof gabbi-1.44.0/gabbi/tests/gabbits_handlers/subdir/values.yaml000066400000000000000000000002141332317117700240220ustar00rootroot00000000000000values: - pets: - type: cat sound: meow - type: dog sound: woof - people: - name: chris id: 1 - name: justin id: 2 gabbi-1.44.0/gabbi/tests/gabbits_handlers/values.json000066400000000000000000000005161332317117700225460ustar00rootroot00000000000000{ "values": [{ "pets": [{ "type": "cat", "sound": "meow" }, { "type": "dog", "sound": "woof" }] }, { "people": [{ "name": "chris", "id": 1 }, { "name": "justin", "id": 2 }] }] } gabbi-1.44.0/gabbi/tests/gabbits_handlers/yaml-from-disk.yaml000066400000000000000000000014341332317117700240730ustar00rootroot00000000000000# Test loading expected data from file on disk with JSONPath # defaults: method: POST url: /somewhere request_headers: content-type: application/json verbose: True tests: - name: yaml encoded value from disk data: <@data.json response_json_paths: $.foo['bár']: <@subdir/data.yaml:$.foo['bár'] - name: json encoded value from disk data: <@data.json response_json_paths: $.foo['bár']: <@data.json:$.foo['bár'] - name: yaml parital from disk data: <@cat.json response_json_paths: $: <@subdir/pets.yaml:$[?type = "cat"] - name: yaml partial both sides data: <@pets.json response_json_paths: $[?type = "cat"].sound: <@subdir/values.yaml:$.values[0].pets[?type = "cat"].sound gabbi-1.44.0/gabbi/tests/gabbits_inner/000077500000000000000000000000001332317117700176655ustar00rootroot00000000000000gabbi-1.44.0/gabbi/tests/gabbits_inner/inner.yaml000066400000000000000000000001661332317117700216670ustar00rootroot00000000000000 fixtures: - OuterFixture tests: - name: get one GET: / - name: get two GET: / - name: get three GET: / gabbi-1.44.0/gabbi/tests/gabbits_intercept/000077500000000000000000000000001332317117700205475ustar00rootroot00000000000000gabbi-1.44.0/gabbi/tests/gabbits_intercept/backref.yaml000066400000000000000000000047571332317117700230450ustar00rootroot00000000000000# Make reference to prior request response data via json path. # tests: - name: post some json url: /posterchild request_headers: content-type: application/json data: '{"a": 1, "b": 2, "link": "/v2"}' method: POST response_json_paths: a: 1 b: 2 link: "/v2" response_headers: location: $SCHEME://$NETLOC/posterchild - name: post some more json url: $RESPONSE["link"] request_headers: content-type: application/json method: POST data: a: $RESPONSE['a'] c: $RESPONSE['link'] d: z: $RESPONSE['b'] response_json_paths: a: $RESPONSE["a"] c: /v2 d: z: $RESPONSE['b'] response_headers: x-gabbi-url: $SCHEME://$NETLOC/v2 - name: post even more json url: $RESPONSE['c'] request_headers: content-type: application/json method: POST data: | {"a": "$RESPONSE['a']", "c": "$RESPONSE['c']"} response_strings: - '"a": "$RESPONSE[''a'']"' - '"c": "/v2"' response_headers: location: $SCHEME://$NETLOC$RESPONSE['c'] x-gabbi-url: $SCHEME://$NETLOC/v2 content-type: $HEADERS['content-type'] - name: post even more json quote different url: $RESPONSE["c"] request_headers: content-type: application/json method: POST data: | {"a": $RESPONSE["a"], "c": "$RESPONSE["c"]"} response_strings: - '"a": $RESPONSE["a"]' - '"c": "/v2"' response_headers: location: $SCHEME://$NETLOC$RESPONSE['c'] x-gabbi-url: $SCHEME://$NETLOC/v2 content-type: $HEADERS['content-type'] - name: use raw json from response POST: $LAST_URL request_headers: content-type: application/json # the value of '$' here should be {"c": "/v2", "a": 1} data: $RESPONSE['$'] response_json_paths: $.c: /v2 $.a: 1 - name: post a raw int as json POST: / request_headers: content-type: application/json data: 1 response_json_paths: $: 1 - name: repost that raw int POST: / request_headers: content-type: application/json data: $RESPONSE['$'] response_json_paths: $: 1 - name: backref json fail start url: / method: POST data: '' - name: backref json fail end xfail: true url: $RESPONSE['url'] - name: get a historical response GET: /$HISTORY['post some json'].$RESPONSE['a'] response_headers: x-gabbi-url: $SCHEME://$NETLOC/1 - name: get a historical response via jsonpath GET: /$HISTORY['post some json'].$RESPONSE['$.b'] response_headers: x-gabbi-url: $SCHEME://$NETLOC/2 gabbi-1.44.0/gabbi/tests/gabbits_intercept/cat.json000066400000000000000000000000531332317117700222070ustar00rootroot00000000000000{ "type": "cat", "sound": "meow" } gabbi-1.44.0/gabbi/tests/gabbits_intercept/coerce.yaml000066400000000000000000000111041332317117700226700ustar00rootroot00000000000000defaults: request_headers: content-type: application/json verbose: True tests: - name: post data POST: / data: one_string: "1" one_int: 1 one_float: 1.1 response_json_paths: $.one_string: "1" $.one_int: 1 $.one_float: 1.1 response_strings: - '"one_string": "1"' - '"one_int": 1' - '"one_float": 1.1' - name: use data desc: data will be coerced because templates in use POST: / data: one_string: !!str "$RESPONSE['$.one_string']" one_int: $RESPONSE['$.one_int'] one_float: $RESPONSE['$.one_float'] response_json_paths: $.one_string: "1" $.one_int: 1 $.one_float: 1.1 response_strings: - '"one_string": "1"' - '"one_int": 1' - '"one_float": 1.1' - name: from environ POST: / data: one_environ: $ENVIRON['ONE'] response_json_paths: $.one_environ: 1 response_strings: - '"one_environ": 1' - name: with list POST: / data: - $ENVIRON['ONE'] - 2 - "3" response_json_paths: $[0]: 1 $[1]: 2 $[2]: "3" response_strings: - '[1, 2, "3"]' - name: object with list desc: without recursive handling no coersion POST: / data: collection: - alpha: $ENVIRON['ONE'] beta: max - alpha: 2 beta: climb response_json_paths: $.collection[0].alpha: 1 $.collection[0].beta: max $.collection[1].alpha: 2 $.collection[1].beta: climb response_strings: - '"alpha": 1' - '"beta": "max"' - name: post extra data POST: / data: a: 1 b: 1.0 c: '[1,2,3]' d: true e: false f: key: val g: null h: key: less_key: [1, true, null] more_key: 1 response_json_paths: a: 1 b: 1.0 c: '[1,2,3]' d: true e: false f: key: val g: null h: key: less_key: [1, true, null] more_key: 1 - name: check posted data POST: / data: a: $RESPONSE['$.a'] b: $RESPONSE['$.b'] c: $RESPONSE['$.c'] d: $RESPONSE['$.d'] e: $RESPONSE['$.e'] f: $RESPONSE['$.f'] g: $RESPONSE['$.g'] h: $RESPONSE['$.h'] response_json_paths: a: 1 b: 1.0 c: '[1,2,3]' d: true e: false f: key: val g: null h: key: less_key: [1, true, null] more_key: 1 - name: Post again and check the results POST: / data: a: $HISTORY['post extra data'].$RESPONSE['$.a'] b: $HISTORY['post extra data'].$RESPONSE['$.b'] c: $HISTORY['post extra data'].$RESPONSE['$.c'] d: $HISTORY['post extra data'].$RESPONSE['$.d'] e: $HISTORY['post extra data'].$RESPONSE['$.e'] f: $HISTORY['post extra data'].$RESPONSE['$.f'] g: $HISTORY['post extra data'].$RESPONSE['$.g'] h: $HISTORY['post extra data'].$RESPONSE['$.h'] response_json_paths: a: $ENVIRON['ONE'] b: $ENVIRON['DECIMAL'] c: $ENVIRON['ARRAY_STRING'] d: $ENVIRON['TRUE'] e: $ENVIRON['FALSE'] f: key: $ENVIRON['STRING'] g: $ENVIRON['NULL'] h: key: less_key: - $ENVIRON['ONE'] - $ENVIRON['TRUE'] - $ENVIRON['NULL'] more_key: $ENVIRON['ONE'] - name: Post again and check the results (reversed) POST: / data: a: $ENVIRON['ONE'] b: $ENVIRON['DECIMAL'] c: $ENVIRON['ARRAY_STRING'] d: $ENVIRON['TRUE'] e: $ENVIRON['FALSE'] f: key: $ENVIRON['STRING'] g: $ENVIRON['NULL'] h: key: less_key: - $ENVIRON['ONE'] - $ENVIRON['TRUE'] - $ENVIRON['NULL'] more_key: $ENVIRON['ONE'] response_json_paths: a: $HISTORY['check posted data'].$RESPONSE['$.a'] b: $HISTORY['check posted data'].$RESPONSE['$.b'] c: $HISTORY['check posted data'].$RESPONSE['$.c'] d: $HISTORY['check posted data'].$RESPONSE['$.d'] e: $HISTORY['check posted data'].$RESPONSE['$.e'] f: $HISTORY['check posted data'].$RESPONSE['$.f'] g: $HISTORY['check posted data'].$RESPONSE['$.g'] h: key: less_key: - $HISTORY['check posted data'].$RESPONSE['$.h.key.less_key[0]'] - $HISTORY['check posted data'].$RESPONSE['$.h.key.less_key[1]'] - $HISTORY['check posted data'].$RESPONSE['$.h.key.less_key[2]'] more_key: $HISTORY['check posted data'].$RESPONSE['$.h.key.more_key'] - name: string internal replace POST: / data: endpoint_resp: /api/0.1/item/$HISTORY['check posted data'].$RESPONSE['$.a'] endpoint_var: /api/0.1/item/$ENVIRON['ONE'] response_json_paths: endpoint_resp: /api/0.1/item/1 endpoint_var: /api/0.1/item/1 gabbi-1.44.0/gabbi/tests/gabbits_intercept/contenttype.yaml000066400000000000000000000027571332317117700240220ustar00rootroot00000000000000# # Don't require content type on PUT, POST and PATCH requests # in the test case. Instead allow testing of how the WSGI app handles # it. # # https://github.com/cdent/gabbi/issues/16 # # This had been done initially to try to enforce some accuracy # in the created tests, but real-world use has shown we need to # be able to create tests that demonstrate that a server is behaving # poorly (and not requiring a content-type). # tests: - name: put no content-type desc: P methods should 400 when no content-type url: / data: '"moo"' method: PUT status: 400 - name: post no content-type desc: P methods should 400 when no content-type url: / data: '"moo"' method: POST status: 400 - name: patch no content-type desc: P methods should 400 when no content-type url: / data: '"moo"' method: PATCH status: 400 - name: put content-type desc: P methods should 400 when no content-type url: / data: '"moo"' request_headers: content-type: application/json method: PUT - name: post content-type desc: P methods should 400 when no content-type url: / data: '"moo"' request_headers: content-type: application/json method: POST - name: patch content-type desc: P methods should 400 when no content-type url: / data: '"moo"' request_headers: content-type: application/json method: PATCH gabbi-1.44.0/gabbi/tests/gabbits_intercept/cookie.yaml000066400000000000000000000013341332317117700227050ustar00rootroot00000000000000 tests: - name: get a cookie GET: /cookie response_headers: set-cookie: session=1234; domain=.example.com - name: use that cookie in a URL desc: to get it somewhere we can test it and confirm domain is dropped GET: /foobar?$COOKIE response_headers: x-gabbi-url: $SCHEME://$NETLOC/foobar?session=1234 - name: confirm no cookies causes error desc: "this will cause data unavailable: set-cookie" xfail: true GET: /foobar?$COOKIE response_headers: x-gabbi-url: $SCHEME://$NETLOC/foobar - name: use a historical cookie desc: Use a cookie from a test other than the last GET: /foobar?$HISTORY['get a cookie'].$COOKIE response_headers: x-gabbi-url: $SCHEME://$NETLOC/foobar?session=1234 gabbi-1.44.0/gabbi/tests/gabbits_intercept/data.json000066400000000000000000000000251332317117700223500ustar00rootroot00000000000000{"foo": {"bár": 1}} gabbi-1.44.0/gabbi/tests/gabbits_intercept/data.yaml000066400000000000000000000030651332317117700223500ustar00rootroot00000000000000# Test loading POST data via data structures and file # tests: - name: load data dictionary url: / method: POST request_headers: content-type: application/json data: foo: 1 bar: 2 response_json_paths: foo: 1 bar: 2 - name: load data list url: / method: POST request_headers: content-type: application/json data: - 1 - 2 response_json_paths: $[0]: 1 $[1]: 2 $.`len`: 2 - name: load json file url: / method: POST request_headers: content-type: application/json data: <@data.json response_json_paths: foo['bár']: 1 - name: load image file url: / method: POST request_headers: content-type: image/png data: <@kitten.png - name: load encoded text url: / method: POST request_headers: content-type: text/plain data: <@utf8.txt - name: json value from disk POST: / request_headers: content-type: application/json data: <@data.json response_json_paths: foo['bár']: 1 $: <@data.json - name: partial json from disk POST: / request_headers: content-type: application/json data: pets: - type: cat sound: meow - type: dog sound: woof response_json_paths: $.pets: <@pets.json $.pets[0]: <@cat.json gabbi-1.44.0/gabbi/tests/gabbits_intercept/failskip.yaml000066400000000000000000000005761332317117700232450ustar00rootroot00000000000000# # Tests to confirm that xfail and skip are working. # tests: - name: wrong status xfail: True url: / status: 404 - name: non existent header xfail: True url: / response_headers: unlikely_header: no way - name: skip me skip: Skipping for now because we can't do it url: http://nowhere.example.com/house gabbi-1.44.0/gabbi/tests/gabbits_intercept/fixture.yaml000066400000000000000000000003301332317117700231150ustar00rootroot00000000000000 fixtures: - FixtureOne - FixtureTwo tests: - name: just to see url: / - name: just to see one url: / - name: just to see two url: / - name: just to see three url: / gabbi-1.44.0/gabbi/tests/gabbits_intercept/forbiddenheaders.yaml000066400000000000000000000013641332317117700247270ustar00rootroot00000000000000# # Test that headers that should not be there are definitely not # there. These mostly test in a negative fashion. # tests: - name: header not there basic GET: /foobar response_headers: x-gabbi-url: $SCHEME://$NETLOC/foobar response_forbidden_headers: - no-there-one - no-there-two - name: header is there fail xfail: True GET: /foobar response_headers: x-gabbi-url: $SCHEME://$NETLOC/foobar response_forbidden_headers: - x-gabbi-url - name: header is there fail case insensitive xfail: True GET: /foobar response_headers: x-gabbi-url: $SCHEME://$NETLOC/foobar response_forbidden_headers: - x-gaBBi-uRl gabbi-1.44.0/gabbi/tests/gabbits_intercept/header-key.yaml000066400000000000000000000003601332317117700234500ustar00rootroot00000000000000# 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 gabbi-1.44.0/gabbi/tests/gabbits_intercept/horse000066400000000000000000000000001332317117700216000ustar00rootroot00000000000000gabbi-1.44.0/gabbi/tests/gabbits_intercept/json-extensions.yaml000066400000000000000000000030101332317117700245730ustar00rootroot00000000000000# # Gabbi has extensions to JSONPath, so far just `len`, # we need to test it. # tests: - name: test len url: /foobar method: POST request_headers: content-type: application/json data: alpha: - one - two beta: hello response_json_paths: # the dict has two keys $.`len`: 2 $.alpha[0]: one $.alpha.[1]: two # the list at alpha has two items $.alpha.`len`: 2 $.beta: hello # the string at beta has five chars $.beta.`len`: 5 - name: test sort url: /barfoo method: POST request_headers: content-type: application/json data: objects: - name: cow value: moo - name: cat value: meow response_json_paths: $.objects[/name][0].value: meow $.objects[/name][1].value: moo $.objects[\name][1].value: meow $.objects[\name][0].value: moo $.objects[/name]..value: ['meow', 'moo'] - name: test filtered url: /barfoo method: POST request_headers: content-type: application/json data: objects: - name: cow value: moo - name: cat value: meow response_json_paths: $.objects[?name = "cow"].value: moo $.objects[?name = "cat"].value: meow $.objects[?value = "meow"].name: cat gabbi-1.44.0/gabbi/tests/gabbits_intercept/json-left-side.yaml000066400000000000000000000013331332317117700242560ustar00rootroot00000000000000defaults: request_headers: content-type: application/json verbose: True tests: - name: left side json one desc: for reuse on the next test POST: / data: alpha: alpha1 beta: beta1 - name: expand left side POST: / data: alpha1: alpha beta1: beta response_json_paths: $["$RESPONSE['$.alpha']"]: alpha - name: expand environ left side POST: / data: alpha1: alpha beta1: beta 1: cow response_json_paths: $.['$ENVIRON['ONE']']: cow - name: set key and value GET: /jsonator?key=$ENVIRON['ONE']&value=10 - name: check key and value GET: /jsonator?key=$ENVIRON['ONE']&value=10 response_json_paths: $.["$ENVIRON['ONE']"]: $RESPONSE['$['1']'] gabbi-1.44.0/gabbi/tests/gabbits_intercept/json-right-side.yaml000066400000000000000000000012001332317117700244320ustar00rootroot00000000000000# Test loading expected data from file on disk with JSONPath # defaults: request_headers: content-type: application/json verbose: True tests: - name: json encoded value from disk POST: / data: <@data.json response_json_paths: $.foo['bár']: <@data.json:$.foo['bár'] - name: json parital from disk POST: / data: <@cat.json response_json_paths: $: <@pets.json:$[?type = "cat"] - name: json partial both sides POST: / data: <@pets.json response_json_paths: $[?type = "cat"].sound: <@values.json:$.values[0].pets[?type = "cat"].sound gabbi-1.44.0/gabbi/tests/gabbits_intercept/jsonbody.yaml000066400000000000000000000013731332317117700232660ustar00rootroot00000000000000# See if $ is the whole thing tests: - name: test fully body url: /foobar method: POST request_headers: content-type: application/json data: alpha: - one - two beta: hello response_json_paths: $: alpha: - one - two beta: hello - name: test empty dict url: /foobar method: POST request_headers: content-type: application/json data: {} response_json_paths: $: {} - name: test empty list url: /foobar method: POST request_headers: content-type: application/json data: [] response_json_paths: $: [] gabbi-1.44.0/gabbi/tests/gabbits_intercept/kitten.png000066400000000000000000003302171332317117700225610ustar00rootroot00000000000000PNG  IHDR@:0 IDATxieU{x1XUY$@PXm hЈiX6m1{ccڦ 2m -cAER Ys/x?PL/"wϻg߳w)r)r)r)r)r)r)r)vҿit-nQ onww)q xO]ן~Y.|z)b,) 0Eat]k5?y{ԟ 8Ϸ|;7< E ='ɧvv9GGG?=U=W|n9g/N_{/qM̓IR)#/:U=WE2gY,'{^k4E霨(%(/؆x4|Zn ?R])ߨ~R۶mq0𶷽(6(!jh `6i &S ´lCVY)a m)Ȫacpt$MS0as˥.S4+KRhX_#:u eO$chpoNi=??:-ќ;mu2ts,#r\ VLJ'nSOrx4!MS 0mK]Lw}_x=L.~ߏ?O7Oa뺛u]c۞{N˲sbm2eQR! I:^$I0 (q 4ȳ( O0咵i"t%=Jg4ڤ,J>epu(by"/0Agq7OY-2ty2 :e}i߸ZfZA֛4MP8A$/᢬ RKP(m_`WUsM;XʦnzJ)~UUE:eYyiS*zW* 'XXs._`DZh *H(Yp<ڴ5j ]CkNG+MkJW4iqmMMq2LL8:z,ײH (>YLEYH]õ۟{?ǠْVkO^kB(e3m-WF m_&5y퉽楾?N{jx" )5mt]BP5ߡ)\o3G ʣ){7y9Vᜣ(QJei>A 4&|i8[[oYMiMl ݶ ]VМ{Ӏu$"&r|#X0-,bGkz]"%k&1a`ZBȲ !È]sEmR2YEN4p8ڋ(0"N Ld5Īd\^CV54ueYt{],akk}{GETe:[[[ܹ~Mht]666ɳ0na(,öm⼢C7 !0 ׶H)%|+y?IB'~cNX{_/~{i=sNvSo0BJIY%a8`x/yhpx4awIfEQhA S.BH!J(8t%[Y~s7{`62 ]1$2ۣiwn^GJo̎xCU躆;={^oDAyV'FC^hR]eIhp0 @ᴲa.w`6f nϸӇꔏ=os8.쳩㠄*%vm k!ti B-iwpp/]֋1쇟fJ4HPjd0,iJ"ehn52I ) 4M8q]ewѴ|/ڎ|s{߯%5M_$^Ww;o<'c ӶmlۦYaT44MØhl1ű=LKQ%c평Ҁ8 f0Ak̖Wl i؎h4B6jsݽMUUmZԬs*LewzdM4M,g0_.IeB.euG:c 2IhH )(MmTM#.ވi4 $s,tD!ː tb2'/ .,+),ˀ%4!~tMymJ Jʠn@:?{ !iam?)mozͣt'?0{_w7xheZ6X?Xo1 ȩiHqLLB+:kIV3.\Ul6%b!.M(v:Teb2 (b<cY!(dkM`5Ԅ_]{WswM+r)c+0 <#0 (M8{뒗M]4 UU9RJRtb a<3"{2 Ð~M{/ߋ! GuH uX-'F{̳˲)pH=MvYMAӰLbӌ8Nȳ(7,4p6Տv8ˢ@u:. ۲^w&KS|c>#u{.eY)VBӨ0(Mu),!MWBpzqꪦq!|ڵk?oԷʳwK~ڌFk MoHM:4MRMGh+8Ga= ),FMV=9̏8q NhZR(PJa2˩ʒ#<ϽkR '>QNY]C\PL&^,9Nt:Du9::b8>O<ģRx+{?V<@^F#%q4))JAH)vsny];MeS筿W9gŇ!ʦ %E\t;$IyIµtʪIJ-:eYvmj*.\7nܠ) 'agL] lF8um~|eH"K,B)6qsttDjm8zk_ϾF=Ơikvu]:h6XKu}rhꒆEEe]GH]th*ʼB `NoYz.a)uҢhӕ$%Kedy| / q]8AuyF%8M&l[KL/WJ}_og }+㮥'=KqY4(`0dR Dm}'0ytz} eb6\ vm65АR'R Ni@S1Y2]ljyJa:th crЄdmcLQVEIGӴ66MJ&OL11!M{X';  bA]L&loo4 AH!tEI*Oɲ`pM,an R\~ qcUYSΝ}t]H}0JiDɒ0Jpa0e6;n²]2 ׯiI0doaۿǺ|Ǒ?J$isJǬonbM] )m#qmZʾ[n5J`)KhBvlQtQVnİL:>:볹Mg7xe+X.hFؖM#"-E+NDY'z8<<<E4uFJXئx<#/hZax%iԞBz=ND"b`FxGUU# b[CӛEA=NYu.\"#`m8 $"t)l^wF.u]SUKDS! Y.EA$H٦ܽ^lm,A*]Iì*Ο?2ϝG)6fL!uvqbLf1]pm,!+K:^sΡ:ł ΡM4mɃ1M8}W&aHv| 4" Vm Z%~RtxV ʗ7>r߅ܚw[2[qdu.YEQ(Z'wqxKqA4Z n("Aӡ MPJ!De`y pPa٬osnxn()ʒNǧkFy{~D,,B:ʲ2ML" Ct]<}(YA+ͨNo'EDQi:x*a"s4)Rq#d8+cu[}^C۞?Rd\[)CM1eY eY[n CTECyAkw~[{G@#R7[zwJ9{ FtzDb4IХ6]&K3n t$-M L["]CK2{tFc6v.p½ G]LSGJxDlnn!teۘT_tpTFtgwz]CQ}8iJ.Rʤ,34`}m올0cKQ)y FgV1],j Ӆ,SA]8$&ڮgQ}`]7<*( "W![;ݤ(K._0 8D0 I,VKly@]vɲpBEsSU*6Fe|jT%I!Ex9ͭB,k!^?[i U.%U]sZb /+$& :C-uitBõRCJ|9\CB3.YZ E]'dي"w6ɳӔ-iwzU':*s-*G58>SO}k_~o|jپIv7Gaܕ+Wpn4[HaSV~O%I}]oDTuq` @C)<BCMPiH lE)4BuMMg0^rߕ֢f3dY%""zMC IDATX,N6tm9P<˙/At2ׯ]%C7P%Y,oppBUtZrNhXܸqBS7$%%{kY)~,mZyLEyS i1ަA) \uq=)[ik$q2$ӴȳOޫME鶒dB]uNC0Z\ݻ˗)2xnv.W+ W( ïJF(Cqy71i:0^s;DJi" t]5u! AH/tb"FCD*F3ݐ8b>+e9Hea.Z;t@؞K#Zwt|l$s4`x{.?Lq|F+/j80|04i~.~僧#Q@2{}~f<\x]4;TLD:4(ݽ\.Hӂm+t]`&Cq Bnp\D" 4l" uh4e8MBP;DCPs2bBF^71 1%u:JBCW&*EI=%EITeJ^ܸuAh8a!YrvsBDI" FQ)˄ XQ%Qfݠi QwDDfە5-HD;t:s|G]̦؎XOs2Hހ(IFfIb>}ٙ܇9 /!u |B?%.yΜ:*M#\hӫmAR'0e`5ʔH)*j*pӲɋM؎K (V Iz2 *JiHG ܚ59eQ(1-|q2)F6ieYv:퐞48VPS%i@Ŕe2uQA*u8bZTUb1#VK_x"oOeD e*44Vm;I8ۓnqccrI, 6ﻏAOEXAfKz/]Akz>;P gȳ8PJqpOt>Ix'_˽^B!KcVUAʟl^xnp]ne)a`9&xN볦tF \O3}-(4tͯk,ee^`JoS0M/|}_{c쓏2=Wǔ*&22%`D[?n~ӯ|Q$O1mHQD Tln1Uk_R}@JAk?MwA~`^oiaxgE w)c(nx>SU%uVٌ Z/rRDAU%Y&u1Q7CN]lmm.gb`0y纭/ o37}y~ZV"uUS))$iGYO{4z?_Stg{g R[_pKͦ)u]z.kvhh4Zef4J$|ƶs,˾v{4M 'M3C<44LS|;.2F3Q28v :P&EQV:EkQ0x( y$ap8=)kkkey9iRV%h&*H]2H.0[׵|u'??n'~{Rϳm:VLdRyYiHF"+Фm4HF1aYwǪ~7|TƗL hcBHu[1 u]1q||DԘf{[0ЅB nPUiSwB,wn@ I5Tu|>ijt)&8&͔LEe:Mg,ov4lZ FFv_5?}s5_Ec?q۹?}|뗾۾O~fWh6FKz*0kMKU M˳o~[ ⇚/jݐ_L#f'Lg'*i$Rj@}qvvh4:T@}`,?~Ou&@۴G$DY\\#M@UC8U] PeM˖t]JYN6eYR BnXiFlV+B7|lz&Ͽ qGGtdi'3-A,WkP2PGoY.WXGs=ٽGt`]0quI$瓏/襤.Rs~v,FAM3 Xﶼ}?*k}IǦ( *MMEGHw;zEQYlp]oqm@QL,'$qK/ē'oxR(X\qs}M6elr oy]PT2rk0RWm8.OT}CjYzdo21 ]kهe1E^3,D7g!diBUe*?+?o|_~2~,#&S\Gy֛BkZJ^,moC~¶UcEٌSP݃7ҢmزTm) 9 iRB@8RKa47,'HMPh[6jeMSc0x g/kzjCOUyeYG 暾TUkJ8e1/0M(8;=;.]ױnNxGYض8dit:C˶iiDZU,nHxR՘}1{<]Wض{L\.߶uq 4ˑBlʒ$I u!cF=:>u~Ņ";ɘh.MSSJJNMx6s.BքL&q0tt:nj...i `gseʶU 'à4޾]]Ӿ~4?wwߕˏGkzwg}H75ey[MCYX"eIY\_]qyy풦' e0k6_k5Bmhۃe¶9= hY֑Kw՝Ni~ھo(-Nu,uIӴIv1m[ʲB7 ݠ( E'B|K$rx[].I-ٜ(HtJд8* VKhA]^}>LYV4um t6*s#c,d5\@-W7G\^]}/*0臁$N{5nipv~6){99;윪IVd:|_glǟmec ,N0AڄWf0lY0 :׈)m#)lNLz^zKJG j˳q1tDZ#"IYH;`(Il1F:䒷<B!5CCt5 *'ߒ4UmjxшԻRڦUPyK ӲF+*4]tmdxԺ"cOMڦk?iį?u_y_/5_z?aCQvY |}8AjʶN~#ġFuCOuv24TWY{6&IF,ӤkT˚1 Qs2K֫is~$iYQ5QE:z a|햾)89pñӔ')fy .Wym҈d NON'8K]MM#tv&˛j Y=0FxG8 axJ](EDmFYyx<ɉ;Z)myF!dZ[(Fqy'_e4M$"֜>Cڮ>Ŵآ* %jGtt"p߻w|A,Ujp8^Q#ޯѐtUPvtgM mͦ+#l6\Cunooqto$igǞ]۲r}_9.\޼NU C8H)8;;c\R7 prblυ~_{\>y ]C[G>M"`'v;H IDATuXeYb"" #8t1ƷtNN.W8{;d2!T=p,A)w\<~C׹!Ht]~g<a._mt}~tl6^ݧkCE~;d&#m{Piy9Bli۞4mh)ce.^}Uqs&(iq{AhǓ~52BQq-Yo9Y<ضvޫbaX]_з +^ôTelRXK2- R`Z @1ETU|BTE{Vluw3pMD]mC?>}~:`L!-V,M95_SJMUht%B4!5ʊ4MU**Mi4ߒ&;L}U6ImqvvF]}Z@PK~? ?_S5ix>0*l:e6#P  4;-< JiaE"6MKu8K}8zt=07?PҼ:I(eXAkdqFYU &Kws[^v0-xۛkʂ)(uLKx~%@pq`dss_{ϼHW/ij!8m:*C3$Athno˒(Xxk}iX|}b}ˉuaۼʇg$Ya6M!E;pz~b5.M ׵i ?tLۖ-7LHMcLMЄA+ݴ, |a6{]G\`ۈ:CLSW$&M3k?S?ox\{?׬0W;u.tC4Y>"~(i$i,+4Eg,KGqn4;:4)`x^ tZnno jLjJ) B,Q%-D<(2t&"' nWkETa|F cSUUb;nceTϺaP~۶Akػ1m>Z2' C^wD'3^y鮟zַsu.tVɎ?Ny%E._{ RLE)ӁXmvnyYӂ† o1 ӛ=E2z6tCQ7XNt ͌ѥޕB-,e>p[YW)a9Sw1hB=xmHɎl& !ZL,-$MR, &ԝ$IZ }elvئE'ZS]N{Fn}ծr)%Hzbt؎&;` 7ڊ𺒦nѥCMZ~F؎;UxD4:ƴխeNQE1wo/Ң(K'g㟌chOW~yӶN(|04)]0K B`:*@k6Mӑf)IUwH.Y уj~:DfZJA[#C dݰ^Zo-NOG8H)Y|_DKX."4Mcݑ)e]1| @WEힲQyEו Bl{D$'O.94P5A0i)hܐz0- ӰeY'>-e'?ئCg<\.Ug4n);hj; zNGt꺢nZLcI x;87c+0)#=YV0&j*m&u/dua7+@`e+*!E9 0G@uكgHK59Q{bZ.miYs3n8tƓYiK⃇,Mq]Z.Qܥ3f#6Kes{ƧN?ʃ{oW8;q#q <9OPm0DҶd9͊kxOP7I.(( I; XaK Fn`:લT9|Fdij 6D|(ZC@vj LcWׄ` oc>a7yJ쓜㏳$'1UӲn Qnӣ9q%/Eo>)fc4c2:aqqFcf"p2)i+âr%#u4ʞxw-t)Х9o:1M Cum[HǢ ob 3ͶH$3>~I,Z{6Ψx]eYAql{BB蘶f9 2n"+B hB"kiC ) 'T'mEc$:M uW)6X :ʖCڎI4LE<˒ +5vlZt%X*c;.ց`64(=*G5t]Y<ˀk>tx|yʿg~'ߺN}@󲬪Zacw:lW^@Uvjb2wB9ƻ&6-]cP(I=+DqMzIgSh(EY, 5cBZ9l8N= 8Y<7^&vu]nnnxYNON199UźfRnPęV( L GR`E.22M^\>~iY5/k%Kӣ>If)͆gy~C(bh1] C4YRVCcZk:4cuUZ""G1N?,z8Z RJG][ EQ`kB;d2W]&U.D >/%@Gy ػÿ! !gbȎ ^yv`oהEvWeNMWhKL;TU$MS\.M Yk>.膡= m04w{A[ l$+Y ;,r0vCFxn)m3hw;i(7Rװò Q۶ENe{tZudҝ :& AhVek }GmCCwheyi蚆cAקiZv d@PW 77X2n6Oyzs;^|vii&$MUqr2{ιf6?C <> ۍjmC')}ϡsa,s,CǏ%QϿ͊Q1mssu8'[n6;@7m$=<`Z!F<|gho-WחU}ܩf wST?渎:Yb:a&=Ym~c?~z}\ϣTta40]b;.idmi5,W+-{("NV ٧8c@$KĠwx @S7$qBUiFL0v,CT9W۶膁aZC&tM"q<]sOݛ^t 0)0麞"/YW^߰ZV,"%LSeZ]G35V+t `2M, dnwK5YԶL4l@}"L |D& .-EU|W~޽z~ܰ 4u4iY3 [g8e^MU͢weAUՔUz:E,iP {WiB5 p߰ '6}'ƈElZ&XnoAKPƮ7jAsUd8Tɞ~c/IoX$|bBUA'>2/]b.0uChh "Uxu!фDJ1K3M|u An±M4aהeBNԕW\t=oƬnT)al#uꆺhxcOǶ 99&E&[4MbiEiAixIҶ% a[6>A.M[1[!uCdsnvx' C+>y!FML2jZ\nwaǮG}`&ɚJ!nUS:VUjZYS0q17}}MxveYiۂ:ln QV) A2t]GյyH[F׵Oi4j򜲍?߽L(r#OM5T[}?n'xcI)T۶8x1LG-yRBM1U}K)=}SU[QWf?!j5UYEV4 "zldIZNNsz!?$s`O>eȋ, C1 \\\X,Ԕ/l4 w ܺoڊ`R0M me*ӴI.H0 NOOy ur35Zӥ;q{{Cs>d2%H)<ߏ0-S0IItuI+^; iPW&443iCQ5%!NYtE}!/*jjg'MKgUIvae-u>e>\g k}34msF+Nt!۝on-yNgA*lURU enK@tP!M2Mt%Tt?Tc>_xsl(࿒l ƴ=]5sUE & ONG١Ya#`Mc |voҀwWg8bZ o{$lF֚v$Bjsi pQϰ@ұ__>K?V|H_ L䗩]b/U' $e4z'`]%g'Geِ\2خT{_IE!MT)aS~9ӓ#LGt2Uu Wlfǯr> nۖ[fJsjGPU'gmWSv5:*׍6lko`β 8(I:WkX@]L kk-1ggX,N8 6Of%Ů0JGڶ$c?Xx`{u4N HlAw$tk|2\ @0 R*+g롳l糃MB*d2("4mM> ([XȾc:SUib*90$}eNv mJ)t{,(n;/X[޾}{ǗGw|[g݋W|gy(! i<Uqqqx<;nZWT h+UiR1It%#:hlJWyӧO裏Rp}}G+?x7omtY 'ާO>t:bptz/_RU\.h[Giۖ>X#0l6zնH!0Jku޵9#$o849Echz4&H8>9w|Ֆ9>>{c[ ĺ[Zh݆+sL&fa^1sk4I5Ǐޣ;\ZVIeR#Ya%Y&:k-O|I$ ϣFPWJF!-Zϣ#̿R|7;CwOD3|!F\A=aa7nr \o}h8::"V5m`fq8sܿwo&ĴzϐkWʯ3x)_yއ|$w/?h/=cs|}>| 'H]GY)HҌw~k<жJd "T(K}ѕ5$,E*h3F1Gl["eDdXk-Z.//9=95?S?v(JݻOg'H =C=hyt ;װLFc%5Y6f2E-"g޲Y2Np8 ԓ}FIW~LOȦs!AiEUw[o-?nH_*_*Kx)a4N@ NO &k9?/A2ӳ3=m:^~jږ0xp4%$c6ۖvVt#c6Q75tA %lw[ݞx䙑Rf))G^5Oy۴E¶+`OEI*6;O!W(NP: MײnhZK۴{QQKEƃ/v=.] Wn-HhPқ۶fP_CF %H\Ϻi(0L|ٗи!C_WhCoG?+0@U7e=*DmB 2헻a$#-Y_)z6_(i;ʢCBM]Ds-ǞP(%^ѴR8;?ݧz-!,c>]n@5mZ$|Iy Z6py,eЃ쯦whx}} w{>xސ_rGqr#R׶t]C$#wk0B[#^|AĈ11ɷKe/h u] *$˸"3 t< 2(ipu핿><,kul H4݆MM$ 8&,K-E]3?9B)3o 䨨*Kf4uO%*$-Xg]s  |9 c KRzXmhmב{Fc 0ZS[hm^.3;{tD/hd2hދoRLzRe[8n[$6}cu^!QR{^BȖh)@Q:$`b&B/!m| NP4Ph8(cp}OV|_̣ W~_l皦9Ol4;|K׵l֫[ZFIDU靷Su}*yX-/m=%(eǵ4_O&Ѷ-뵇aZ۲u09 l;klV+f(Mx1MӒ>o~ 햢WrI4|0ye̡D)$>dL `%BzgF0~ؐ4U6MscqrɃśrf4ThA5RSZד'dgl,!fۼ}4I%wXp{{{IʣGg~T'0͉Ro-Y/8Tzm;޾WmVkP޺QWbe;FYzB:CM:k~W pmvY6f:&޳(*ʪv-(h'bhprz>SO[َp@ Zc|2b xm[K&LS(w)4͞|_ Fk/V ݐe"WヒJ+)k5{־_maݦ5,f~{=(#L#K&kYo;GA0=GusA& }yfygmrB[e/9??;ڶb2>ـ4f< ݎߓ'FG8'y󆋋 /1|GmMC:DQx:vhh;Oxt>o߾[\k˒(8/PBx0kmEEt4[K*7HT0mi%~D;~('@K#p)+l)+A#[AH'R[D8J5mweQ`ٞ8Jph iDqR!Bke=oAl~CS (Ejn_38b6?"bo?=w Q$R"3(4c9;svzD:՚0(Ɩ\//Z{V+͛7|fEY/4'cl3Nt@#s; /$tg&#Ft<ÄQJU)G>@s8:^K?JXgi$Txʼnϴ] eU0ӣK#L9m47/ç~aj]R=5ϏHSQq|| ;S5Y`wӿyX<Y_1f)u]bbxGggؗB, y Qvso;눤`y}NbsCI1UY20wh[B<%^v;{Pv="Ib8Zz=bQ{$-/FJ,sz$ЁF(V )sa;KS[oxx<\-2 b̦c0&/?0ݻ\]^so/{Ёe9AiFw==6ܻwJ6JL&)N8C FX޲lPZڞ0t-`oj&ڦ~ H >vEaDg$o߼3az:7x}}f(d٘ iꖮIE7Koi"/Z$R߱>ԕnp0blB +)%;<zQUW? @F)Viг _ „l4=_c/M@i}N$,u_DQ@~~AckcMJ6u5UeI a4ީgUY!i|NٔG׷UV舘eqr is9I B*N'U9>[%y=F1Fl9&gR{.߽|'Lc)$q nуN~uX,8kqeISۖ sCO]IFYU;4ń!QFKNqrvk{v=* z[d'(#:: B"Fu!k- e#_/qm(M뷀7bWjZ0"C-=4PPzHҌt}O>#(0P` {JSh|@8|GvyUkk F$h8P5Pʝd;P0mþرZ/ Cyu`􄳳3P䖲,m GEŞb2nJ/˃ Q^ w~th%@89z o_|K[wCA~t{˶g0Y̻aˆlH}H`%}[TǷ?Y?q]mkڦi ]Oha[TOدVM`n ;|`< uI㔄aC0IFԷ9]ײڬ8<'/ktB*0ݰ+`v4U~k;o0JR57WW1[t I:ف |ru&MPV`: [atUANH`]FQLh#)]4uӠe?`k$˲AK CZK5tia^T Ehv /M8AZ<{tخ! "MX믮wѾ;w<3+81Sq;'7cKd m-wwy{BC :@O)nvhIv[Q?_o7_~Oh[הbS5( O[((-o[S5 ~G7R@c<ݢ,K5qjȋ=}o䏪XVhڻsB t-i 85ѡLTe̺d[tc}`飼leUxJ{q㧘($I})M}#]FEvQ^MڟԞ9Fj^=>ѵ5֒lw9EOS8c:Dw$z'O?677+^|~*lUӷ,I)Z0.s(dD&({L_"kb)1`%MSХ1%M]0Fz(-]'9n%ɲ8(w-U*FI9YRtַImj凑;(_x侷t yc[#r{ʳ߇/X1YFqBw][QA[^~aZv{+_>~~/_> c% IBbLHW4U0L#=Rk81n["߮q]CUh-e УU@z4+g| $H!$ymd?NY,a)b(,5|]b{<|@d m8}qPiJZ[1 єggYrrv8FifH3t]5-^3JSPfxmniJQTGKn쌼v[OB_;6v7!dNU7Đ(0Jj/SttuwO/~x2&IgG!]ջ00iJE.JP7Ե/6M#&)4vA@6@914e^S +:LJi6C##(8[[Ӵ5e;G!4 n6_fPMܱ4ɂ"/`MY:]RUwo+\%uݠM8ˈeV- ۡDH`Q;ڶcbLB֛5,H1wLg JGDYPüK-}ސ?~vf`> 2>{Żo mbv̦F)I?bߐg?Ǜw׌納-XzV!8 V$mE[dbASx]d9„g4_5&tE !]s.h@7*׀ɞ{GتbihlO/`w/oDȓCդ3cp<4OM~V\oY_Y\`(a=(6`1D ><<,ʢ`~*4y8ID? FH!GTՎpˢYQ]WYe]J 舶̱ hJ;/%3á]VȄC1dd$I4EA^m)CZLh!Rgtvkj}NYn6H%y WW̦3>)O>~dP[mY]7~_eY!* Fd1}9_‡9=YRbmL0wfXS&# ;H!}R"BHGgݐ5HEqx9?>dۖ;^|w|lja:zzh&O=! C&MRtLLJݗ*lS "Wz4U]1NI'S{h@CEG5H'9F52>tTZ Ryl}'Hghi}68!5AYhz_ x`i$ʹyzWa7;`9}:yw} <(sw94gXRez>{" ߸{Mo;EQ|}1 !v )݉('ɡx ) _Gspi\>/m6meZ+94h`ewkL" S(1CQu`ݰ^):R\]0e4xFUxs\C bqJaCDb* EsPוϬJ( UM}OJ`跭md<\O7=\__/{vW* N;S28pp;I<`ɡ㣏?gϞӃzg dw5X _Bo}9(KNaIY^6ر\61Jڪ1`"FQg_ VM"t vpÉm i9ň|cͱMA%Z[eʁ`M#N`[fDz~dY~؍F8w$ QdR5a;ܕYc[_?Q̞?0j<SkxgĻ8F8MB0bZ [RFl54NMZҴ CDQtQWaamON0& K3x>ߐ#єuC<GU]S x4~aڦ `q<[zhaeuLNnHmјԍ%H*^0cLg(0bb2yQW@w]#r`4e]AWmDM8F#lwu88"J%`8)*$*f& C!^Vad>]AA`VL;Ix$q@ Y' M]?v;4߹p(i di:X["Kn{Lr#j& tGL&(fIØ@Daz' }S#ueDBaږY6 Uډ.p&hخ6l iVA[U};~xUm{ڶBXBb:tB{A]5}I ZiTo,}1:u]IDt͞-m[szr%A,`;!+*zѳ~$?c2_럷֪}Cu(!=oa^F(qe4$MHF#tP=C5@*E/h(Qp2Q8 J((kcq4cW?̽YYz[kM{8sN`4c9q 2X@Z ()܁ $Šlc;nq:u=};~_UwDFeREhK{7>ﳞMcMg Dv!ΌbYTn uӁ y6aq$Q$J#mf&srrLx'h!mxx hmPHPTV41"#0%iDiLHkn7*b:+ h%EPo%Eȃ!|c@46@a'#O8;%NHNoß4Ut>gh0?6zdqJiI8J#8h˜rf L,CZ8;.tQ-[Q1P@T0gw$K3mIIx7dӔY>?d c-JEd*CgPF w ]m~NRz>/}?p% ~䌔$3z=pusNAna##ں#{V_(I8R HMquI&I9am:<:P$a.BUz:JI֛3n~k4tAQX"tc mtx#Q"jׇ9ӳm31H&)nĩ'Ba ;<;FefdIrJUqvq]lBI G=E8tak{?7a L2TKmXv;5GQT,H )7W@*E+3MH)"#%~*.fq0|F4:B6IX7T I0}OĨw2yU{%Ib4tvHiQ8&Hpq1?K3b$:"G0>h];3齬;ҷ?p?*ff:,n @׵Xo5-1FcT ЇOkMwXߓĊ$ wjc'V/F־3W釠"fmu!ǠNOΙG!X0O4@gLq[:# 2;F&b6xgf8e ]GYXaSwLS]xaY#u5Qsimɲxwy?vM7Lfy9e S (F1]Zg1] 9 I&SpaQ* hm;/伯 `u7ɧY~NJqo`=txL]]+AhGyZcx(v\YG nY9Bd>c]gE|y3NH Z풫Gڞކw0O0 ߤ8R͏|~![G8.cBfpGQn$~m9===̃"!iJa*VЙ()%y@#a,`O&Qqaf[R< >RDEph0M%Hs ]b ᆑfYНꦥY2:$PClv;Ĵ^\l­5eSl6ۃ,q6_zaq2q ͳ 4ծ K.iBW(Bf Ę'9HLeڷ믿ƽwI$h:dҔ4͙LO&sl@ BXDgFU.48dQ~^gw_O~¾W_Ip|R2l7[A!@{2&R)IB 14+QRnwHà0o6,EflW)E9E1wU eY'1]SOwxkV81R5&̋BE/? e<7$]Lc zN' 5 Fzt $h eL%TBж6TUE#n#+&L,,X]ׇnڒʔ겦ኮ%͆G|)/o:/Ķ8H$2u$Lx=}>'uzCOH(<;.ġ6# ^$ %Ԝ~e8)^Q '!--pL4M0JH}='D7+d("E R9^4ōSYpPyBG(DIJD6n+8l7WMR2u@Ek%''% 2<.=֡Tz df"PWJ0#CɄHa )1Q$H@:Nq>3s(kZcBP{nv,} !!G>ANRV":II&"J%.uYRO}l6kbASפYLVLH҂(MQ .AYD*v$Ye)/R(uiCGt^Q}W8}IJ1fKazGۮCnv20,)C3| ;A?l>fc] Kjl dġkII$IŚjɣoꫯ\xn0㱯Pq6fL1Ѭgo}_cBHamKU6$EyGaH4`BX T%ypN2ߍJ`n0iHFkz jo#.?fS5Xm: |Bޏ)0Ď/&HtS;݆?KOn~G {&LJ-^β/%!wC ;`<޺u k (q4$F?opϕsZ.%%mRm K 3 ha0bo_&H4,˂n EO?'g9DT0`㋢fɴ ǃ0VnsWweRnt!-|Y*bNhJ#!JSs68:uU& bEdqL=@J1X񮧪k xwXޒK!+sDDLg<ƅciDqRU]6 o&u]Od:("+J*mQ@OgS)% i_ik|nq-^6k|?|36?+1Yi\x/}o|K˼4Mک*6BLmRdGG yi>=q||P)was5mՐ1q1 ' ]MӴ-xxPԍ"B sQeI&`0Pk4 3SfSÀ<1ZCRK"pp0 ra_oH&H:f)dOrHJ uMqvv5!XɔlJ$8)H/˒jdIfGQ#(sb}pOEFGxnM}o=>}Yo_:ZVO?0t;PtG_]1N'L'A[WW7nھ%S"UEi$ ԣ4!BɔƅT &Aǒ(H0"M6q [6޼\r3<$;9 n8&b6-x7XJdSdGӠM|2ay_};>OʟuI)sxЧ8֨, |kG0֥)MB=+ !E}D"tkK$E F_ %Z̰͗orVDyB2!sfa_5ms>xێ Uc=l6-by_x};i:˧>ȲSn}Sjubخh2#JbmOOKiftIQv p[vH2 ~ul`^NzEu[D =X\RRN7e)&׮kv*&R-Z(9ya|fdӮ.I:]vyC'DfS0%=E NQ\2VxZ1 _G%ϼޗ'>\C ]0/xwF()H C;E>K Rg:FpiZl#Ռrnh9>>;v_z͝gg2d)zǻAm E>(XoݺEY!xߺ7_}o?o4GE-*D;8jWqzt:&;($I [ 3il͈Q C' Ɔ܋,ƞ;4{CZ׾?_~\m& ^G)h (8}{J0 rb:rsK_+VGk8Oqs_yO=ɭs90<o R(8lb2=af;:̢1֎GOP!%8wFEO~W;fvC_bn7&'`fqrvALo4zpCNS!ѿ,|w!6cD=V0 (}oԎ?}S/hVkh6K\prC~kGIZ0#ER8P2!MKdBzHRs!1VYhuyu;b j{O˯Iq|}w89f^l 䭷 Y&}GW+VzȤldDm-i^/Zxo_V|Dx.1Lsđvd^ Gpw/7ZKݱmzl>#()B#N51fc|B^Lhk4#Gx89qyn4m]ĭc.V02f㠽:~=D&q<&p,otNc-4ddd1_%qY+$NcTԙeW4vś)MUTƌV=CS YCAYN'B1Hӂlk:H){ ]4hz[Y.C" OAvquqsvӓS }t6~/szv(JjK7t]n;,B'e>3_Y#k;Jp=Nܳ/\oqs.c񕞣ۗ3}788yCֺ햛xoYVTUomУxްZ`4hc }.a\Ċg|1?gZHܾxӻLgQ|%"#놶 f޺z8qPH-F %NQBќrNΣRo3 "IAnqt 6X=x3<@d9y^&gCJ(dUE?c N3BnP Bn}-\?Ky^{> | 0_1hz}C޺W[k 낾o:6 ͚a,)ӄ*!+s,cWݽՒGoybsoMM۷$Cm+kHćYL+&8'N l,آOOScq zLBm[㜡ikon"Ff)tB$*O&(%ێ^X;C4]%P1t-':l%<#:.834}|g>?zntFuk"2ZjA&K\TxOLBlB~AQ)@b:(|{? ?ry'Gȴd>52m7Dyg\(QRqyyoqKan/P24DMdAwX IDAT4D%{/蕗xخW$" F!ĩD RYN\&KT[i6͆Q1z}u=tm{,+aRV-=t]_?+H1f.".f[K[|oQ־KK($/Q2DW됍\M$T2;937?a|S[/l1?aNϸ|z}T8#;jI$tUϠ:UEYXt$bdik,i!N;..ymj[Xm4mQ~#xu?~z8S>'/dńhr_#SO2tءkߦ:ha &1N% T!G0(ˌi1ؔ)UߢEn>-LB@Yp ^$c8wva )κ@\z }@!?)hf#E9ܓ.Qjf:=HHz|?7syRbuElۚ__ծ'/Ds3 :/9/r)b$ e45 O M#nD2`JЏ_r)TF5}oT b$=tFwJY0"Ztm`@/j/+/WmaguO#4MXprt:m;z6!,Kn4M"|F,K*G.׫gG>_@)^pabt>'IRhftI89fٰ(D²nXo:[:ǗV`. ٜtn;͎Y>|q̝OqS|%M9^ea)DCy~o~js6)4K\߱\ Q|[BNnD8.+}-(g1ᬧLf֐o>qX1 ya'ҵ串@fz@7-жhzM6YDLr泂ڐHH'/O`znxI|>*PSbUo_0[ο娼y-r[zP7L"Ab#b7ຖLI>\x|7_&Vs |~$E sG/:Ft$IR_SU՘8k_%o53}J1ᘨ$3̸_0sӆ.s%՚bN^Ivۚwujc\7^s^=?#_B*IqQW[wd9#($,t;ZYsmb =sm|SNJ0Csπ6xWR. {X]P Kr|8ʢQE$X_5Ev;m)I5"?^$:n̠ɳ -<*1`,l1HH1c zj `r(5yq?)0bĪI4EX?kI0^RJ$UUAa&jZs3 m%'cx!w_r2 ﳇb2ĘaNdFK3,K 3],v-nѶC?  ח O<ͳD)?Cx񥯒dSWvdnKAgRd5Έb|;7&ue92BG?hK8:K$#=l(sԎڠbX,, Ůn(9;;ij6-y>e:UvUm"M3(Ak G#@=nG/zQVL_^{o]yL,<^8ACޛ۾!#jV?zMzk,EsR9e*ɒ"NY,ܻ/Ι.nf7kӘbՆH*`ښ7T]C7]MS(ceMJx#T:İ-;E,9O𵗾u4Ra]6y#p NBHN1)`!~/G6wOJH8l"說$yȎȢw\9<׬nV[_:}M53vՊm^" W !c4gqv1 z@H|Co=m7&Y dw˚dBvcWo>tqB^N8:=r×_ Նnzr~~N;Uí[mhFwXs( ~ItN;ڦ HQաʂze8mH |=Q $Ckד%y>a FWdXghv[$ûr2GIXH zJb9y8}&l)_w>Ҋ [ =~9t :#l*8A"Zڵ/{ok[Ƙ}:]p1`1l "Yc'rɉE(%1B,l qܶSګs;3ܻ6A2U<*9|;ye ]gZ#p8 %m1tM$m3LO4;A%㨖*饉jKlC_mpk$+|gm:Rw2,ziЙאNa3?k//Ͻ9 eWzҴ11#KRPW9ݖ~Uu0|lSo^iۑ~pC&wMj0 o6,9C+z}mee:6 l%'n3 :aJ_|_^rF:GB<ˤ ӥjCVӡ%1m`<]?OƤ/ǰʗ2f䏙-Hi`YJM]L.aDezƋ=lWcE!+:8>>Av50Y5˛"e7EMxWUMnH7MCJ!$Ty:C,$@IA8#{p kJ"QM!v6|s [(?5kJ5)1IEww&K ]eLU 2??'Pb~~黂J)-ī(tPn7lmLCKYyr2IXmFuNyI7COk43`4[жMچYm}otrf;fåm-( 0McO0ʜCTYR隋l!LGx qc7q:, U{N_g)=w i tޡؔEmYx5Mkw!Rk=X:=v;:AÐcsJ y^dO~ _w#_c!Ruz4UN^v 1 -4g72f,*z]y]GVV/mm5\f&c =tL`2.De[ʔa6)ڂSHqx|o2DH3%j{x9ի9A4Ҁki`;׾*t!7]Q 6f@u]Sa$hz$ | E^_Gsl >||+ϹLk03l#[44԰Ѿd0l(aR-Eӧy~)$﮺eYb n!l3(ZfÏ`6Mئ8Yu1xIQh1q\^^jfLsr#P3шh(A8迯+4viqE CJFs| |\߻'-U]czZ~ޮ\.}JV+lfJU?鿰߶O8zf~HlǶݽUYMU JЂf9Q$pKJmCIٮt1$Zw8þWH”S5 Mћ&қCziMaxE@%ZZ(!tE¾+} uDǞ2FFZE+ [?_c ``&BX(ch4XY nizÞZǨ l Aj 4g0蕁4wNzik՚"ّ,YGOH,mqٜɨJ[D ,0z2>?o~ fh:Z~wQɄhil1 dSW`44C-`4MqBz(RҶZ h۶_zeYm黮e)Ӫ^Xw!Gii M)]l_}j| `g?ږ^k XE @_嚦aZ(:t'U /_\J1fN>OHz,Ks4|ˡjjVUBWXלX,fDz,!2z\0`6f؞C8Fax^>ٔ&tl{{K.c05mM [OB aUሖhH,A${7P.s?Z|۾bI,ψf4\f/YU!$MAJ|MSh?x:=v@4=(H̻D,ڎ:yR ):K)Ϲ~MTRa1p]W BI,ҋ :US&<#lvILWd ɔ$$ ]<#٭yT%mʮu=(7]F9a;*{аP۾p͑A̙U]nXgfZd]5*ыvٔ9we|>qhhvd:)6Q Xߤt;i9_eYJH A`Zqb_/C᭔ymF5y M$O[:8*Cvݖ TUUM~f8A_,:Jj kx;|)ꪦm$gv%Uvk7xhE?t>@xYYlvfDh Lۦ\ [Y1GTUbjG=& \0 ƤQG \,a;ȡS d(p?8`~pd~&Gne6rf|严-^EaHeC۴CD?/гNc[WK4?wEc97 o$b<0NosǸ(ˊ<˨irزt-6:u'O iklZ>CN¶=^!K^*:%X\PK.b7}m70[ tqܼ SH}ߑ1e`9i fc(\L0b:ꖮ)2|giN$ENdJb.])zrdQוF<hbAoX4MfqV; CFѝ4-mQ5 ? 0nR"ͫ?Ot۝OMFޢ*ʿ!zچ@n=EQ^$q5U6N( ?wy3iuS%qkU۲x:!pϴ 昮twAvY)tj2v\=>3"p#}=Q aהYj dcV_8g|ߖV*oM1d5yk0M0OijwڦKh u<$&^/1TK&MmZlH!,V: T]RwdmYH6,w<#K5jjN6<_ݯ"vGZ!%c؎w'X a>'AJɄdBR ΟhotW>Ɉ^j1_Q ͟iYo %u IDATu<$i p(J-„pc=Ch\Ő=]a-B8M4e/'~gEjj:Z"ĐZ[ك8r\9J1 (϶1Bu%JQ!MQ&U[zt'd5}r5kʺf4# ;t٥_'QR|R"% &ec^G}N?b6[A8(K˸~./|';ιf{{D)"L;lt;u7(,ٰ]+ZW\^^ж `,,&a#GDMGOMWf%/+./.X^]sIy"u!$0npiنiLfs ˢi;za&xAD;E)6MhUK{g_O)(Nڶmיe{-K3Q 7 ]z9BfSS.$gTM͆N3En9݁vxb4=rlL{=0߳- f[pyu;:Ƴ]p0]{I$FHxVgN$׏<8\ݯbWDc^aYQW-^4Uʽc=yeXc-@Ҽr6誂tuF^жa&4"Kd߱Y-W\WL{{|_&(LPE:hNN?󏾊!??B^f]Gv @kXkں,dZCaWU^M]]*_L9:<7xuq|oi|%N>%[|3oG1y볌GSnVk=l4M?d9зvcJlSpr n0"4$۶,m)nKP-uea;XSJOv+P`41EK/rvvFayvƳqL:q袛Sh1Y_?`2~O'(syC8e99ш8b$]s(xڦwa1Q4B㏞}^Ty!G[7$]ñ>b7n4v1vCX^^7-]^Д M<|?3\a mO8mL};<8-qST 4p=MK/L at,3mv]FшQjeẮ ))c8WUA=Zk(2.03;olYO _%/N&Sf fKPqr&YP٬eC^\V) ?>ՊMB]SW,0h\ #A ~JѣX,]mt 6n4%kd4FԝB40\{xnX^rs}GGOɚ w~+U|1gh4z[ieNg_;~ /CN^X-7Ȫt<ᑫqhcJRe2EA4yᗰ9of2Gx9J J PHiЖW5]]d)%EeQ5]S1O9::qLeM4()h(v;#)j{EUTmSđ$^HIt$ V:}BSѕ =V7+ҮIIOUlǹ֪4O~(lǖF'CYk[E/6qLYw"mS US-jcvgI3NҨuGY=^jcS5͐ eiUA]WUIeEH.r6>x96, <ߦ0m:K}) 8!R5%2Ctvkdcv77k}./6HXLF|ôRZ.uxfY,*4g6bZvzSv8ӟk;Bzd<,J4彳3Ś˱ݬ5ϋͮӤ+L˂CV5FMoI>c#hw@ky/8;++Fs'Fш g7L|fFq3̉{P UY] ba]c<"}O_ UIV /3 d:NiirsuE1$q;E4 <7:}P[98g2# rc2/.i:E;`'뺦(Kh;l!xwkeY {ETU R]#*l?x!ӽ ^sM,'FvjS@4( ڪ¢GЩeU5MVrD7 ^|>۫ &Cf{<|Ӈrtt)hw:Mjkp\-U5TYumy.1Jiq[6̦ӻ?ң,P{AeYgϞq\Lʻ}3TiJ$Q0Ftz#U]P9W^!ݏR)> 9qEsF?jT{iUoJz&Tq>h)S4}4 \W9Ɉﳿ]BIQjh&p||l>muAXִyx4f:٧,K`2P q|Azl7-c0S{i ~0Kaw 1(nR CD}6Els+xi*2mS8nA._z7w~Y8y|Ðl΃xT(errtw4(:Lioa %(Aݲ\^_җ98}%Mv S'CbĴUU}wȷ|88+D{{f3 ˤm; Ck㤔8E9=OGzy:??iem ƣq װu,[жI2aA e]ԕ~7k=f3MlNSUhF,;Y{ʺzʖgrzzz/z*i.$  ;r]zM;Jm"_7w}>_v5ef)Ֆ,v5Lw Q8ff<+Be>`yshq|r -ݥ;~` %e[PUu]X̹q,vyT/ffwD8CwB}/ӵu KJ4P] RDO]o}vGϹfE/j'|Gtmًg ML)F}Koi늪9yp!%$aslbq2$^a4[!TP)MAY8RnZXK, \Z+չ.)+y%zNSf^ěo|:䄽`iLҮB(ꪠRVXQ;xfE ~.4eXD#] xo9D#Ͷ|IQTՎMhNj4M"GEb#D?BvK$/X;ئ+Làn+,^}5fE#LS jIEc c6Wa^Ѕp2!+:Ͳz+0?moƐr])Y)(Z)tv ?xG2[qxp|qLi2)r]z|_]^{յN Cڶi+f.m[pj{=F w)eѰ3Ւ;)dTeWew]C](LKv&ak E^WkVW<}s*']FU8Ml[(p,tw ericnhi6tBaU'iF?{AhNu~G m[TMCVD( ӵ541E膔eEݹ3fs~}<JJFx"B'i Ftzґ[yNWdi$ #e*\.{p@s)+MRDǽ[ZخRҴ6USw5Q蒬hc9gmylHyze2^#d2y>BشYI[m|;Q#VSp,KR:SiJZB4vE4nGEuJ]"iu< i4EzHiI+&y|zyxx M5N4:0 Qcju\wأpѩCw'k!OO+BuMRf uKS5uQ]LWgDᄢ,R{!4t0-q)7͙m|X%I`z蹝Rw#:¶,<ɳ4\jBCӴ}n8UIg`C8r1گA߫dWN1oQv;<ضZ* vq=eᅁft:JeTŖ)y5Us| =ŷ%Ჽm|YS1P./YootJKdJln4 *~bm۔yaX80DOW|IǶiDMRX[*͆)4Gd\Ϟ]tBk4lm-@.! "8U2%mry,7@HqK^M7 Q[JFH+q!70 wuBYidBn)l"uX.tw kXeyY"0 plmXnꊶn6&s=E- qq] 3,&Ӏժ\{{{.3A}a[7)ښ4],J۟O \U{? ڦfrbW4wUQuuc A`;F#f9A65枃ٴj,'!iAli=e]d )$Bu iZDaDVTؖMUH-ضEgHC_du}I =GZSm]^t^V7aiˏ\?NYȲJ,q=&OSsrzx2b:SWE^!][S9%ii=/۱٥;luTWӶei]Z'Mhz ux2g4s{yE6ctm|NukO=(kj6#懧4uIwH45\heEBV{@F\SW57n*$kY]_ocs]zR%62m|lSeMb-{vO{h_̬*.$,L#A L 1Bbaf0BB!$0BUʪr{^܈7 $Gq;k}4(f:5+U-gvМ<`4;y\!Q04` f1NLfCK$qKQ=E]ơ`{\__!0_KfSF;ڦŋO,W_ϟݮږ27Nܦ,*v{ mPdt}JJ>T[C9>>f%˲x )kIpnɳ7o!ݎ4pi=ݞkm}et=CmynM-m{FMɋ̰mU3jeMM(ʂ;_uRϟ2jٞvi/~>۲@w|uM0X=,@xO/%}?Z\]]1Nˆvyѵ񔉵y5+xa$c8$LvS)GAt$MYR{lK7bh-qM_& mX^8eڒZ3OJTd7H b. h4&z"2Ɠ `ۓhOgT=R43wUFۙl6CE! ;M҄t%.I("NGn#s g8JQUYf6aG]$w_. G88m(4z-ޠ[޾yE42t",[A A fEYYNvn@c6a8NIrq&H38By]SEY 2t"߰oy,W>#Q͆tBS0ͱ%AqRlWQD4t}cMo{X}G/pDN2.k^Ыwop{b.U H"g2 r8 o:3Тn&G?VV[DS#4avzL74C4]G#%򰄍%J#b}e%os'FTUU.B@HQ蔲.2Pi t'O{>nY3dKU54eE&qDQ/1*W)lǣ :AX>G:UݢeQ휮G h Mz/R1i Zn,fBʘe{~ B72G3^&#>VTUjf2hx4gKv>#|ȏqi\+em'{V5_|SvEՕEFA@v|lCiM}ӓ#z{Vq]W-m݃m;; `ƌF#ƩGbWlss @O_Teemg&d7{Bxd:u9)ǑmeA״+65~AUl[nnn[qj:1Ii1EĉekjI'DQZћ>OIc8d]s||}D#ڮms65b[ |zզׯ)Ȣ'%]4=,(Ws" Por\aS5횦i aljrl=`vRytC &KVw=u5{8fx"4=P,͘'?F0+YYS>gY-cܦK@6DQ[mPM@W CT\6YNQ̰-;$NJJ3J $)ΑG}o~Cgr4"+*F#aKVtvE*f)Ϟueac>Q)duQ{׿/#g%KbCS,=9 P'Xe^rwpxXR art4"ˊ{A$ ^0?;g;,kF q 7o߰n /IG>Q豫K$,ʰ'( bkkVl)k{s̲(LCf)%ϟ g$ь$w5l• B[&-0:mZaҷ}3` [TeE, \WqpOԦ |+lb:uOYր>hJ 0<q[n#9ɘ$FjkM wXvo-?xY/A@2!6 l,%weAC/G$-6#M|H#1~)g1r|f)3|Q# E'q5yWaK bYn&WJq41tHG:xaH^$B$Ͼh[XfPkZkqlteu NNQ , F s^FJQdK,ˆNkَ'OOxq~r=zIҔd:Z^AX^oL̏Xk5jۯi \7u=D'p=5~޼1`artj.;ڮg9шjR,[1;r6:30|5DQlxX 2ZF<# !0[1@$H&c]qyyIeda* WQ:oSkTkz{r]o|Fe gkE'' =w/A{.g2/bI߳/ Nx =~̏9x)E9]ۡDv />/p\j k&t|uK]3Knz+VI|:f61%ӄtFClw( f2Gs4)1-ۖUxQ("M2#~*~cfR6]p3 ad-ՆKL &AXqf9z'1Ki";m-e V{ņ,qCUԕ F ^b B1h@Uy$isON XXu\@ж=i:f>+*V ߼kyIPUHIYהU~, -پFk}t{o<>ݛ_ȊK)CmOx" |\UP@lW6" ݒ%~=VUEwR2NI萋qT&/ϏIє$Qn@b]i-U_1MeBQQU-]G \'k=G=zxQz:H$e۰\. |I0lCF!Z/||8$ =m!i+J*ؑο Xvn{O~_I4U.q 8j4uNUXUl;?ixSfҙio'>100GߚS\$,R~(?ƭ47y&o5?:ǶNONx򄳧OKF^$8mC*J:+C~1^JʺŲk[\%wԇL*={w]V%_ӵ#'4Ay | vܣɄd>B0 KZ6\}-s6%-US5-z8a[DqDߵ, |g6 !Gq,c)A l[\jdk$M%E0JHjNlۼ6ugDAtOuHO 櫪+3{j4'X@tum(ԎCFx)q:"" mR)iچ8Il ۑֽ???E랶E*P~H&^mIҬ &hdt\B3F)I!Rp)0@klIUlvGlU44@N dN<(qQ>^m m[M~ `crZaɋONF|N< )"kPQoK0=-6k~mFVT,Vk" xaym[V%aM<}8qs~ʲ3E4f$3SaRBu=M5574eNꆇ1f<m˶mVL)z,p6A2B.}G:C:@)P~n5Yfu"4&RB~5 4WXƣ Cal}q ,mz5͂z-%#:]@c>۶,dqu ^#zsϸK޽yS}Nq Woߑox>g+ږ0z8ʬ)a+h0,^` tp=)Z. T:.+eyY q͉lꆪdłh0P2N v;Vcmk k<暫E{rKY,)&,KtS%}F7h9 uXbKn_QeUS9dFfU7knonK vsn Ώ#\2 wrLiKdx]׾Rl+STZ/~FA4(@}-5ert5Uc-uo}GπF0d< #$%JؖjjnyX,@[^c;DADfo2f2?ai2"MS Ār}: ea&ES:e嶴 guN6кg<;f~|RtmAvEMd6@.o^s5l- (D: keNw3 =EqPG)lmUUgusEY<ەFؒn&Yf/ؾady)z>%11IќL&G\2%0%CAە EX-%qtд}Aߴq<ĜhھEl!K:ACS5&ruE[JYU0h*{4}:t֘bEY7ְ7-MPx6a3u)TӶ e`ƓSNN^9^ `xJ\.hmEՔM~:6{ꮡk;4y\Av]tL'mCS6A&g/zB~ů=?=a`[q:UZ[ I5 F-~=]`Y1[#mSRu]D&<2nqW_lQ^@21{k;G?" |w1=!V6X`i[޾~~C1ьx9'O9 {ʥ,)9O8N৤1Ϟ?g42((!'!{ (G<)](9LLkusʮA6y^ж o//yԖkPtزg À-,eٰݘ]dB4MiJߛU9iK*h.<ˉ Wy p]#ǿ˜MUJsA#M|\{ʪmZlƳO9::,v@Qg/Gp+{o"EAQdm7Э>>4/v-M{OM9;}nBxBӚOx, mGTSG'!fMEߙYys"׿_ɘ,5 hѶr1;}ǩ m_:(oRk)eAP%m{m30 pb+v<}1~O~Sk[׷$iȣ#Z4h[C v]f)_®Zq.|?a'No&7b#B=6l-)˒^>}I?;9N膁m=v[S :@Jc޽# 86ZK˕,a=6A S-m+ESW6 }l6{tJiEaAɄY,툢ivđxu||)%gggRhꖧϞn Q2'IGU^n{`>\u79M[8sLY@(KOҦ(JBxeWFX@E&($GHW ,W 7ض}_ Oz?'h:;opq,p@-5%͒jwox5ּBEcꮢ r_жp;7QNHW-yn}[͒+q2?L3?IÊlۆkhۆ~m,6h3' LѶL~_nfpGmK_7XڮӧHң=ju~H%,6 -]1F>'/oد<o?C:{J2"+q, ;sjgu$~ b:=&Pa$1R:t G"mzi@(_C{(f(N}3i=ɑP5}NQDiDLкCnOt6}G:h6]۲lʒwor-lxBp{sCUEei'u/,R>A1X6'OPALQxmKV3!h@:h ${m$ІH$!b"k<͛KX.[rx 1LS8;0N5o5O?r'@?G?Ϟ )l9 r>߳YyX<^ogȐp$L{}lrx<&\^ YGn8~`{ 7IN%\zme%^Ht[Qs/AH]34Y,<|H89WMF {0U0'ǒ31 QJ 鈗/?B9}S2l H<}78a i:ាn S<3w?&?Mw'YRx78۞^7 }s}tmHS. @4 sE{bKFQL_)Rl9L[,[`=%ڰ&V`1ow4OWH[&# emZ3-q9e]3sϒNOH3$"%$Rq8 :8sX߳m,mF4EESضYAi\Ot%]2Bh͆a{2'a0k}oa"-e}eYxjB2&)O3Sgq잼؁owuc󇫳eُ+)''GL&S$>g:;f<'cќ4`ѵofX0u E6k{v=D,["F,6Q3MlIu`\]_RẦyzzzX?(h˜^}neY_G<>'@!2{:Тܳ]ܱ<+no N:Y|ZGr /o{Gs%L$qs'te2}3#*Z<پ{|y1IőfPȌz4XeR:v, PAy#|_{#-P͟麎7l6/ !M)qb>?a-Q t=q*ݶ8hOvġHﹾ}|I{( T^90?9*/4ֶ-=zƘ_?mnW׷*.؉ 6X()AHA$BQ9I"$ &8lqnj\ͽU?^11Z*e"䪺5stZk1}u=4&KB˥)isx)pyyeY!{{{kD0p@+_:4]jEJY,{Fde-l 0 ) ,$A(ڶMg[.Rfc .nDS_\p~vNkVcGp8L'|c n]U!`4[PQ}O?~` /m+?d"Uʡ,vL8zYz搓Ǽ_#JYu21ʆUrJg>`0C/HҔ4 lڶf2>^/2%m69˱M'XcۨFEڐlr6iva꺮NBwo HVZwފeYuM#P 0M8~ "#T]3 Y,(HӴb#k64J {S\y`>H~6_iv ݃o$ }۶iBٌS1nћLD (; ] lӉsڊ˪* <#K#]f 8e:TfFT,s0kʶ <70`40LiartXZ\E1iO}s C.P7ڿz.`HQhkU$im"QE*\eX1ɘ#ƓR%i! ɠ7(2-0 (Ǣg"ۿhOyAӚ8!wnLG+%E1hҌx+L\k"%M9Ug_D{ެv$^zopwX!t/}Ͼ^޻˯>__dt`Ps索ǭ%uvƖ1Mh.cƸe!ly'#Ѷ)g'Y#d88Hvg39>p6rsBBJZ[ZMà-/2ǣkf%'WDEbRh_m[I$d1,4=a4MmJQ&EcI$m;ŐA xhaGM:ڔ6YS5订9xR5%a:R;6 bh`cӂ)A.0 A6)tuMH,q|xH"NR ֝v nS:<&i?dT doG($.Ռ( vk9.uz->iV4.̲moPKEQ:;1Hyu([O,(h{aD/뚢(PMIHYSјxµNwp];YV״YDAn J5qr1U e>m+ꖽ#/Kʢxضx+)˂,ʒFi*Ƨym#fHe:(r=5V ZʊF:wشxpk"?ÏO4 :ogM@Vմao ϼo#l fMIVu6LQ ǜ&\5ڥk4TTR`uYY,K\O 7R6ꩶŴu~0@h7Mϰ6s|8I`;ý>1g!UU Ð0 MYAl1c1&1{.MіRgmgW !7ĒUH`gw/\I81}*j6bsDj\678k ϵ#"T1ZU,!1V0"ﲘ]P9{nkPUbu>,ȚtfmMTTuEED|zuMYAY<|| ꊢz?S`MrtiT{>?kRJF.X?Ljk^K,VjEos0ym_1ϑBPv[SU5,ϙ\x.f73/S.s?'#qy[?K4;%Zg$ dggCF)=|+B-L'ꈺjɲۖ4HQ3헸K(Uq5N 3L@UB18w&]tV) kL񞞆vn/:6`uCYz(@WX<#"$1IJ%X\^{Qu4jr 1ӝ)/2ӝ==vv&-c"fAat`f-m(ASWMz18Zp>E(IGOm Zܹmf'<,˴()!Mngn@Ae)Ui%Jݝ]ъxP^/@`ih[ay>҃xez&iFe*, =z(X.&e^jUݐp@SxBnܺqCZerpD7 Utؐ¢:#wF 4”5aeUSSPeX8N8WdɌҌr5;݆KCۿ{^Ic} 7|]}ZbZG8.E^3)jb4M>ϒe9 ( ip5u R w`G~ן'Kί26FLFШ4[R9J g:٣4R$yB[ĺv8W@lc(tAUU1MnÞVOQ[NKe0Zl['?1 'ګq=ʲbF(;Og㥏ѐ`@0zuE`y IDATccX&f7FXN( M_h y EASlEYQOigi=,dfSʲ.戻lM$inV`0QhɪȲ$Ip]8r-2["0`' )ӝ8&+Z\ >*|F`Ԝ_\.s -r PMQ0 u&Q8LF \@F js =Xˡqy1}f ?~j Xj4B|(UN f_}^a} K\`0a]5M=Q ,^A?q[oIUض͛7\Lp9i&Q>y o!Z=9SmMTHޔH+ܗKd"w<+m;dqmsG;&ums}?8@QSMCd\o?o'HՒ_oNm0>dz}#[S֔4l],ˤQfntgꊹԝ?6-auC[m]bhq:Rn/ԶmQ@Jck,"s,t\neYPV9J邜$֝c6+W õ$IST]#>ᔷxd~MZ``@Zya i$˲|L:tR@#TgtWiuPf)iy>e\kG#|w@77@5yEY,WK|ѵNEJ#NN%ejk%Ao۶5 {v,9i\,,?w~jeIgJm(`*+^y7( 5xSC#>ǹu#o{;ϲ;yi- SVimq,YGWшà A{>lw!m;dY,*mnuhԡmlu:!lSt]888ݻڼ^ܸqׯKJ81am.x@Ŋ^u፪R":tvug\;OWUBXxOJ)Y,ȮXVR ]ްMT4NJ嶗8 Ȳ›UAjÇY+ƻGܹ2\B:<8}ݝ.]m}ZͮSUVscրv躮soW4.iTN8utp8捧[A$.@bEhl8 \ʲ4}yeYSUlHLe)ӲhTCTJoVw}/~!*,g463ʲ8ƴBIw`8p-Ɠ{׮K'~g>oLGN8x}rFlY uFV+֫S9jyOp{$dwwOjiXTU4!kSyi PQX,mKI -qLEJa, edE18i'8<8R-~00 Au-AKYTuzFt=$ieMӨnYaHjt [#pF3-!$wS`w3voa$ I@`eF,g aئMKÄ'1mb94LX\^!Atmi%jyn@Y@UJW$ъvGN$!)eY^IFk%Qy{{ܹuGG65I$Xm$ Ә3%QcZ~F5 ;!I¶iS\Pia*pœ89~1Y´[\Ϥf޹{^<nV}qk7oq/(h1>e3씬H1 ɵ뻌>`>G=`>Vq4eoGxOw]]sFYd|@)=iݺCܽ{!au3>-E1iA]8eT7Np#Uh!Պck(S4B!# qqG{ezqřE7AuuPkE] eeIЃEU6zG:`'(SJFQuЈ A@q#Fޤmi2Y.DefaX0&oyʀ4N-7w(ԮVUqiz!7 (rGd4ԝ;IQ54yLQ1Cz}nW>aik0,  |UIi270X.T w߻pZgqrFgg>tPۺj6˲ιlF+ꊲN&daVmbL⤥,5*JUc:doJ(X\OXG'F1:|N@SOY-\2[\u+A:o1UmyWO0+ljvݳLӔ4Mi:&a43y]'J@ qquuVF})ntG\Ýyv~3Dzϴi Y%ܸ~^1>ӿ_7^xu Qo`ZUiss A^G2_8~:QBOGsTUwP9jN&Ŵ]-eh40d'Jȋ0Az,^O 4 YخMD IJ-"r,bquţy>UQi1C !)U #pd4鉴4(U]BШ׵zȷrG1miUeXTeL6vucɨZ;!$Y4uN]dqȰ $xm<hWh0²\l˥m P-E#Dac\g2,Sgy4E(5kd:96}\;y<4/ =";{ C|s\,.5, |#KS%Ŋ$NXV,V+/.y8JhTK݀f$J4Ϙ.ɳ09;=!JWlJi!LtG'9?;' WiD&W񷿏]?'?ܝ(^BLO| 5> xt ֫JaHk+} (JY l $C#^z3^euN*I%ՊUQwz y*{x^a¤z~APg%e'm)fjdE!zIDEXgEfHSъo[o# Ijcqp0&~I$g0h QVN( ,N5~b<ۤQhtDa6a"êRĚaZ! I'M=5uR0 l;t}ͮplquLr1vt \gFSף ,b y³ ںBH: iҤ$׎1IF k"*I'uN<ϩjEZJf8게|ŖвZ Ð ۆ8aKQc ;7oqusV ^ah[~~9#p mauH!yDъ:+YC$۲hʌ aJ 'mthjE[ JG$ %3fwjvg[})Eiy H*4emV54Z)edyB󦪰M0\)ea[GE/s4 BTMKԢ4鄿}>ihONrT0 4dqp=,#$!3 WnC1 fEYP5fkbX!;! 4,e*5 BJI\9.R ]y1ꊪrsQP*: 늠 wjݶH(Bhٵ"ںK=M/ٝbtS5 2]cH}xja$qi\]]eiqӶz8yQQUd;0ʒ8KImX^Wkzm2$B`4Y#(g$Daz\ǣc4R_;>ICެ?f7ns}Y&+h[Ed'Am61`w)ƃk:HD}&;`{% Z cqX9:.;=ލ=Vk-5g{][BFP_PV)u] 9==*2N˔>j!i\j=`(߿)J>|'' fK|<:?iҿ o{ApٽUY.p \hyI44 ,fgw`z>!Vw>͎4$CB[! A]V b4b1_R-sl+eooOSsmi[;Ew׾{ݹ+8h4bpQxz`ꂬs]xpBQ]|? 82 ~yz֬Yf"4%*c2QT 0$jiZ)Ґ.θ8;-\^\Y.,+ͯ[_<8ZRuw iZMVfVR ^Z"Am []6 Uִuj`v~I'K.9y]>Έ%u4h#,.tU/GBǏ MQhuL4+x_#3Ξ; IDATEZ&1!yc ({e{tB=izWA$2hFA6Wu C\;ؽv0e$+ШHHe&)smjrs z ҄p8iioǯtUDqLS7FC#AYL[yQ~+K_(~CZ^ޫ?O{'i+<5 nx[\\1yY;==z;&{YERlZn*z=l_!1 inq!Ռ<鏧p} خbuLӘx䝷͠3[\#կp4I҂4/ZArvrF&TeY|R"ږ21lEZV۠V* h1@VB`,pL2ϸ~GdRD'CZ%g0؂sl<ǐ8) i8{Bsw״ؾC{&?}~/~5G@" ir-ڃ:cYwv+҃*˲[Jiԗj:c۶?6eI]eIS5iAq!mMꅖ@49T0RP)$Z]!uJEz!I„(qWk/2ٙp\\>@),0L 0Lq>Ղx&+M&4ur )5 !iaH iV%yQR5n| ULb)5('(b^9I4n P9=? Oǧ,{Դqhm-&-4hZQRP5KJhPe !uHB‹B6TUx~3X3;?#p\f+j0D׾I%h<~ W績K|k_+:6?#_x˳ .k4MpBo8!\sl[R۱1LԅP MqSZLiClϳ뺸MYDQw4M%e^/`^`ױ@gsNNΩYW ?9.U-`0Zwdj(ݮ}m 0- #KSPY dx4Jf,Bqp?|uOIV\Y;Odz/}󜜿zYc.NK<&}ujkRۧXmopCo;RIVuV4ݻ} QaYY2J۪ʖbIq''x~@j"H"ZUc!wDl*cbJ\{6ג!$IKM2 umb [6,C0EV;K,""*a{<|)%OszviVaG_"EjN?Fjݻw~&>r>?O_~K4Uprt}O?gv}&;>)@Of2r BvCfmʛ3ybiFo: @k5:X{fYʷ&Q1NJEj"ô%MSqyu1Wg|[kh2e!rǶPB)_<<˻mg3skb!6$PqTA0J%8*$18 0Kmb$V$z3ww鞋Е-ͯjjtO9}]譗?]g+\c8ؤmtx [ٺ:=Ue$_ŗHƧ||pqʕ_wkO~­qg+d1bkgn>AM]xO<=L5Lu]zM*ֲnIVR ,? \Oy|>d|Bm yiΖk%d3;ZIꖭjL0@RJˣ;;DQ/$T- s=G6l c%TJ;`,|Q5 lu:ܥJ>1l]s|rw?^\G=ݾAPgiwڔex… G?Bg\z { S'1=$nc?K;pF Fh7 jZ4lV(H*T*e}ҳ^JI%kKJN+4GkE,K$CF,5B@(b)y S)o>t:q%HtzB/&ܸq7oqppk>o3Z(ZY;pI|O/~__ '7FL' !ۻwxի>gy"]-6n a ZH l|<n@wgDk#hm壥9 ߳yX[1h[(U. I1yNȵ0o^V ZQSy\$)%#Xdy*Y1ms/ s ڭZ9-[?jZٜvϥ { Zgk{ ?rn1.=N4`HG<@ 7pVWC EHμ=)$SiT41aaul+KǬfߣĢEm dIغprʵ$˗(e. "|,W GĢ'q>RlhyZ;a^w@1ELL /w ~H?m17˿X,FJ cj:-nV>xᔍVL&g*ZSOƳC5pJbE\pӨW1Oюˆ4 WH11>A/x~2Iksm, PVxc-v/_A;@ ]®D:/Qju)( fIUtgjzZ@؍PRPbZlD DIn <^2vH򜲬(˜& %$9NDa{ػxrT%ZakuVXnTiX,4UXR8 $O3G\ue, &#YUa2OHϾQ1/ Z,7ڰ11vI*}\eѡwh#n&yz. X!LI% Ãӎ~W7__6 wWa!_HWV3c'''~}QM"/OCHOSli;TesP”}?[#r*Ǒ%` (qS bAmIҌ"/J<2oKJH6s4G/G #]llp[-tut=Jk !5ᐼ*QA⸒0Z؊v;*s-FNdF:G'D."dBW xJ1Dajre+;|?JNF[[gnKH8 n8WOm7"Q4ܩuN$42uQָ,k~jtWLcx5/cD݌ 8&ɖ劻^`6?AK5uX.ND.t7vX 0]qezhG.>sHt:]\cuڝ^1^{eM'2> t.>Wn_k.GGexrdrfciZ۲*4iJ70,8Y{Cw(l0zq|>Z9~RSLxA*3c%smM3s4ZB% \$W?~W/tGX7 hZCt0?C-h@CGRKJ:ZU"a2h0mFek70'']* >v{@vV لEnOj0viH>#d(Ã{?~S ߹| ?eU$ zX"Dq2cuIwYYBF> pqXA]sqg# VJ2b. ɔ-Ӝ8=73-;zDVMl]F!~b U-{j1pO+S S7P)%1YmbiaEM6` א()7`G9}}l8iEL/IWL&cf7 @;-rE& ZlE[XkX,fk[MJpmfB[NcLn7!ISoo?sSL_dqy^v>pȝ{1iG}y:\eL-y&׮]#UL(W"VY$ͥQasT~#Pjm|˪s:W3_L(FDywsg||@/lS0 dI-c0YA^WE>ԴˆnuF24|,h"GH(?,Zx|ЊBGm})z3/S+MhN_ `YG>uUR&ɘ,c:e7 ZwPgt 6!ߥ%Tצ/YWH\+:KvHsw>" /<Te.[NKJY5fV+d(Q҂RՋX?e>/[|ڵ;5ϼ򰿏A~oµ"f)el(pk u-z %w:&y.(\܀ "':}¨Muh6{i๤Y@mpw"a{kĠs> N/j=7(Jӈ`0 f!fYŕRhEmoGwMg0B{-Mu; QMvO|N\r[7(!J)ɲ*{2oИ:H#kڡϣ?(s< l]69/v !9}3kCQfa -ڥ0Ʉ;c92GGw3 dq<]MIf)x~|z|q4Z9ԵnLɋRp9Nx2EB n{^ohc \?" F*.r H3ʣO|?_x2qiy'__ƗvIU8eEQfsl+O舓a19 ? Bu$RuuRnyRYr R"8Tk\˴1Gl w#fEFQWd6G*QNv-y 4V4uu]ڽ>;c9ws!^ Z> "8^@ Bw)%vƨX 7qGoF2's(MMdI4Kp*GGde;|W|5"92ez|Golآ?ƍ CƤ5Br,a>`peϼ{w?ޕWsgprVUs|2g%:(^H,q5[]\mmu^o l4djMu0 Osg|o~:x{~},͗d՟NOHl qҫ?KYI/2P%Q݆Vc1EC sٕ73ePRv@xC+:^E>-a~xZHh SW(aeɫ^ыh>ZY򪠨55Җ!Jؔ(ߥ=SKs.Tt; O"S(*|OaE#VWe֕h*'}) DmLI%kN&Sbe:[1YtF;\Ƿр\ݐ~oHhw]_kk\W1&p\6e"&O B(DZ$Di !UZI;֐}Y>wkr[$igA+EXf86`{1m6 5Gibc4B9!Aa08.ɘxL&6AAxܛÏ~9ux_oy[{jJ lXyҫ[ܽs7o)ZDa]jSs27ChxuVMUWE87д:Z!DؚFB!e̋ 8}qe5^۴eE0%|^`p5Uf;U55eYx>y"j g+muRaWK"4ۨ@ܓEo2 ||ETEqxxHFH%[-拘E>NIK&7htvkjk㔹q^:\oj4k$ŊFҪ.+<ǣ.+4%r,I,WK&4 q= W_rs|}erH]ApNT.uIҔ_W͘e ~@xa@?)y5jFJ3O$O~wu{nwx?.Os>J-r ӻXSjiE[;N18t58ϋRc-Z^Yq ~)#i"0 ĩ\2>9SU윿Vy34v/ϧEƛ15"2Vk;C.EY@mVO& 8U:i꺁}` y"]wi Tȵ3vѭ!pTEZN׊,Ð8˹?`~[]z-.]Fo=;G8G)j Hf8}Ov0OiReaҜ:/qs1Z޾s$]y z?};9>9$U W6`N2.eJDQDDl1F{YQX> >X ?m>ʒrY.˟'?up}01o=ZsH9\!lM'+nQID`;kښwf f IDATPRa-1jR"Ry XW+ҍQM;=66vX,YŒcZDKsEjQT5n a7`A`(X4u8P[?1R66F B ("MBe*ۆOebrz̭(+ڲ\.ɋ*q<'ҦnoECs>ABFxgZ3sS0t]gb1qr<֐9id<CUUS9-9#ك;g:jr5O7Am*Q{&ibj|qO-=]S| R-1e e.y5KkBm,]? v6O 7 D@ͮk:Ñ[ YLֳf)!)bܽ͝r3$x$yt6+aw'L|gsumoq~s/n+ sN}qB^S=.uצ F*aM& "V''f3^xy/x?tL&,s,c6PAM\8/2H;w_W^=ǝ{VMG193OlC̳8^2PUY\7Ƿ/ax_ꛋw=o{ݓ>ZwW)x@#XPWN9[n=vKh4Cu;^J?fI꺑Ed+P/%nPEfϿ3tBEEf(EX昲Ĕ%6_!QYXXۨS0=[9"xFYxADi3SfZ%.SV,Ke|rtZ(5Ufx#|(/fL)Y#Z[[[GR읿h2!t]:aRwN$I"_q|rG-d#s[8i53Fs;BZNM^ϳ4cjvsEV/a$[$ar>񔣣C )t;,ɻ65Ղ4M0uEyFMZ #,e"lu} kOq{3"mL<9Y~td&cԾ!2pz]EV"Fjݴou)K|ߐhg" b@Yc(2 #R )L\ԑKb YVVJRIJeDQ; wvon>#!2@V[+u]gXĬ1.eY5s8)Z5<tK Z=j!-ZT%:ܻ"MT-R>W CM1`JdD".R{T6~ [ qɓm% a,uYcu4US x0y9P6 r\qnNg̳2(rAloo.{ 81._a9v㪆~w쀳gT1-*raSqZ-1uEPdK,aynaGL3,2XCYhG.a 0tҕGG8~E頵G~vk\Ɋ%^i蘊>=S?5]}/ OxK_3ήI%7co1[3-JMv3(b +5OITm tBU/\@@R-4] BGd\97DkH*KcA(Phǣ 0[)15EeP(8n*Z8>E9y;.b^z22L7ҀM.o& e"( $fb,3Lmya! e<;Aѣ"ߥb%†,}Z-h?s|eQaQBdro}ߺ0 a3]x?2Wb1CH>{2#=aX9s m֚,+(bXbci"`8.3N&YtɰۂڢTiUc!emvų4\!Rz>b ۩ʼ*K0fm1&Kʡ*(ԦB8 /Q4K yƋ;Co{\Ҵhd-рAvۻ`th}NBb~4~`cDJ뺑R5&Iٖts|r|>f>99Gw0Y-xUO/dA@Ԋ;t-4#hu\c9~?~D] '3eI+L*P}770) (~oƿwF 6ܸqΨeAtTOoы+VZ9)0JYE5?p׋fUW {A;K{#_.v˘8MV25#2$+|NLH0쀒UA5Y+TQb뜼LrLm1u/BYnnO*T[dt;mP2 B]F/߿k-VL"ldKqVFʆ+ɗS֔YAZR&xNԵΝ{ݹI CΏ᳿yoGHWn\F^ D*A,W Պ|F;q¦qLR cQJIVDa|1e:~H۪0 ҞU1\bkEh!ILwp7p,kZ-,* L]"j*%C%2Q*K<.ȋ)j2^@QUXQS!_u>sJ)iQU -w>}_{&Զ?q<6ˑ,˨ʒ+nV5| [Q&jyE|Y0s9 V֍Ձ^S⅒JԔUF, ,Fkp^(p=^Ō~C?h0{vjlUMQ B*Qt=N=x<s?vGD?p ˪m7]KijD`1UE]U(,ԘhVHk) QUY"X mBܶLJC;Ej q^p~wP`q%6#6γw  6h-pi@9s q) !%6y9LT+x`6q \#ex5 R >Q ڕU*Y0_N-uﰭ(ʄlvdGǷ~VO&O`<~Ca7ltcQԊGJ0JV #_ϗɔNgHHH< >яjFY-3thVJ6K/?* Z I?8m$ܪ0L_?ȁoi{o*H8NiڤiMv@؜4M(}*u:z5t%% \wAU-fA@R._qڽj?767>ri_ '_O xp8*T<ϠsLӤnl58z{ V{JHPQ>g7*hRŘe9 o3'DQ'<<'(0 <Ћ\V>IYɒLLQAAkL^KQpѓԗ{u{?D+4ujZNs4d<$ƻc<tàjaZDQNfd2IILP95 Ai3xk~𵃻#fU"2o|Sg~[)a$Y8q#5Q~kfwkk[;^:[mhJYBzfr qǯzgc"灟g K0fqsQ HȚ@RulMwe+%CDa./ck} wG%hty[Jk#o…s0v`٢]o YbP&ZE(UcQowVfcKè8&vݩK2 p! MYrhSSZ9,k*"!F)P˲KXN0ZMx{߇><^:9&X?wu˾5?o>t4-ʲ,|R/ KWHU(ˈhwύrItW-,P ːIBغI=NVHEl E)Ӭ.-P2%J"?Ha3e9UY<[_|?"B+Ql6fs*bԺ4M$…7z@;n挿vfc0/r2HٳOzߎޚޓ$*TJBQ(PHsѵ*'+2zx\g iӬw8v$|o]V9yN]\ŲllSe2Űj ؎eL'STU{QsdLi#*6Hf_eG~kEӊk4,Hh`s}Si"t:E%TUôLd p\pf|sF`@~mCE3w>[=udV_<{Ǩ' )04]욃nO+˲P |?$|:eYycWޔfA$qF^uAGwa YVHS$I(U}rYaǙ'A9|0rׯ=`M/B߹h; ˀ}=G\jyaFu,ZFqeUc}7*D*H Z%LSt$ Cl*aeL#hJX`wURF5dI-_Y}}m=:%e{GR1]("O0Lxt:1M !QQCYf)JY`uQL4fdI˛9YҰF1뗸x jchh44N/ߕx3+/3.GR"p=nUT`മYN8CymW$A",IiUQ熨^1c[[dQ9}Itʍ7\WW/&}_x3f1{~{gd8i[h 4&!MS($G!aeue,ˤIFDȲ{d4s'm}v'VJXy8/|><5eL߿qWv|o!4TUa+<戢R' iu%|eamUU<,W77=N_;k1; ˘_yK+hwjC-j$N=. Q׈$ItXU6iq0r c. ?Ӄ3o ~_:шqtj BE65`:"McRòu-Lz2戓)mUnicode Demo

Taken from http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-demo.txt


$ENVIRON['HOME']

UTF-8 encoded sample plain-text file
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾

Markus Kuhn [ˈmaʳkʊs kuːn]  — 2002-07-25


The ASCII compatible UTF-8 encoding used in this plain-text file
is defined in Unicode, ISO 10646-1, and RFC 2279.


Using Unicode/UTF-8, you can write in emails and source code things such as

Mathematics and sciences:

  ∮ E⋅da = Q,  n → ∞, ∑ f(i) = ∏ g(i),      ⎧⎡⎛┌─────┐⎞⎤⎫
                                            ⎪⎢⎜│a²+b³ ⎟⎥⎪
  ∀x∈ℝ: ⌈x⌉ = −⌊−x⌋, α ∧ ¬β = ¬(¬α ∨ β),    ⎪⎢⎜│───── ⎟⎥⎪
                                            ⎪⎢⎜⎷ c₈   ⎟⎥⎪
  ℕ ⊆ ℕ₀ ⊂ ℤ ⊂ ℚ ⊂ ℝ ⊂ ℂ,                   ⎨⎢⎜       ⎟⎥⎬
                                            ⎪⎢⎜ ∞     ⎟⎥⎪
  ⊥ < a ≠ b ≡ c ≤ d ≪ ⊤ ⇒ (⟦A⟧ ⇔ ⟪B⟫),      ⎪⎢⎜ ⎲     ⎟⎥⎪
                                            ⎪⎢⎜ ⎳aⁱ-bⁱ⎟⎥⎪
  2H₂ + O₂ ⇌ 2H₂O, R = 4.7 kΩ, ⌀ 200 mm     ⎩⎣⎝i=1    ⎠⎦⎭

Linguistics and dictionaries:

  ði ıntəˈnæʃənəl fəˈnɛtık əsoʊsiˈeıʃn
  Y [ˈʏpsilɔn], Yen [jɛn], Yoga [ˈjoːgɑ]

APL:

  ((V⍳V)=⍳⍴V)/V←,V    ⌷←⍳→⍴∆∇⊃‾⍎⍕⌈

Nicer typography in plain text files:

  ╔══════════════════════════════════════════╗
  ║                                          ║
  ║   • ‘single’ and “double” quotes         ║
  ║                                          ║
  ║   • Curly apostrophes: “We’ve been here” ║
  ║                                          ║
  ║   • Latin-1 apostrophe and accents: '´`  ║
  ║                                          ║
  ║   • ‚deutsche‘ „Anführungszeichen“       ║
  ║                                          ║
  ║   • †, ‡, ‰, •, 3–4, —, −5/+5, ™, …      ║
  ║                                          ║
  ║   • ASCII safety test: 1lI|, 0OD, 8B     ║
  ║                      ╭─────────╮         ║
  ║   • the euro symbol: │ 14.95 € │         ║
  ║                      ╰─────────╯         ║
  ╚══════════════════════════════════════════╝

Combining characters:

  STARGΛ̊TE SG-1, a = v̇ = r̈, a⃑ ⊥ b⃑

Greek (in Polytonic):

  The Greek anthem:

  Σὲ γνωρίζω ἀπὸ τὴν κόψη
  τοῦ σπαθιοῦ τὴν τρομερή,
  σὲ γνωρίζω ἀπὸ τὴν ὄψη
  ποὺ μὲ βία μετράει τὴ γῆ.

  ᾿Απ᾿ τὰ κόκκαλα βγαλμένη
  τῶν ῾Ελλήνων τὰ ἱερά
  καὶ σὰν πρῶτα ἀνδρειωμένη
  χαῖρε, ὦ χαῖρε, ᾿Ελευθεριά!

  From a speech of Demosthenes in the 4th century BC:

  Οὐχὶ ταὐτὰ παρίσταταί μοι γιγνώσκειν, ὦ ἄνδρες ᾿Αθηναῖοι,
  ὅταν τ᾿ εἰς τὰ πράγματα ἀποβλέψω καὶ ὅταν πρὸς τοὺς
  λόγους οὓς ἀκούω· τοὺς μὲν γὰρ λόγους περὶ τοῦ
  τιμωρήσασθαι Φίλιππον ὁρῶ γιγνομένους, τὰ δὲ πράγματ᾿
  εἰς τοῦτο προήκοντα,  ὥσθ᾿ ὅπως μὴ πεισόμεθ᾿ αὐτοὶ
  πρότερον κακῶς σκέψασθαι δέον. οὐδέν οὖν ἄλλο μοι δοκοῦσιν
  οἱ τὰ τοιαῦτα λέγοντες ἢ τὴν ὑπόθεσιν, περὶ ἧς βουλεύεσθαι,
  οὐχὶ τὴν οὖσαν παριστάντες ὑμῖν ἁμαρτάνειν. ἐγὼ δέ, ὅτι μέν
  ποτ᾿ ἐξῆν τῇ πόλει καὶ τὰ αὑτῆς ἔχειν ἀσφαλῶς καὶ Φίλιππον
  τιμωρήσασθαι, καὶ μάλ᾿ ἀκριβῶς οἶδα· ἐπ᾿ ἐμοῦ γάρ, οὐ πάλαι
  γέγονεν ταῦτ᾿ ἀμφότερα· νῦν μέντοι πέπεισμαι τοῦθ᾿ ἱκανὸν
  προλαβεῖν ἡμῖν εἶναι τὴν πρώτην, ὅπως τοὺς συμμάχους
  σώσομεν. ἐὰν γὰρ τοῦτο βεβαίως ὑπάρξῃ, τότε καὶ περὶ τοῦ
  τίνα τιμωρήσεταί τις καὶ ὃν τρόπον ἐξέσται σκοπεῖν· πρὶν δὲ
  τὴν ἀρχὴν ὀρθῶς ὑποθέσθαι, μάταιον ἡγοῦμαι περὶ τῆς
  τελευτῆς ὁντινοῦν ποιεῖσθαι λόγον.

  Δημοσθένους, Γ´ ᾿Ολυνθιακὸς

Georgian:

  From a Unicode conference invitation:

  გთხოვთ ახლავე გაიაროთ რეგისტრაცია Unicode-ის მეათე საერთაშორისო
  კონფერენციაზე დასასწრებად, რომელიც გაიმართება 10-12 მარტს,
  ქ. მაინცში, გერმანიაში. კონფერენცია შეჰკრებს ერთად მსოფლიოს
  ექსპერტებს ისეთ დარგებში როგორიცაა ინტერნეტი და Unicode-ი,
  ინტერნაციონალიზაცია და ლოკალიზაცია, Unicode-ის გამოყენება
  ოპერაციულ სისტემებსა, და გამოყენებით პროგრამებში, შრიფტებში,
  ტექსტების დამუშავებასა და მრავალენოვან კომპიუტერულ სისტემებში.

Russian:

  From a Unicode conference invitation:

  Зарегистрируйтесь сейчас на Десятую Международную Конференцию по
  Unicode, которая состоится 10-12 марта 1997 года в Майнце в Германии.
  Конференция соберет широкий круг экспертов по  вопросам глобального
  Интернета и Unicode, локализации и интернационализации, воплощению и
  применению Unicode в различных операционных системах и программных
  приложениях, шрифтах, верстке и многоязычных компьютерных системах.

Thai (UCS Level 2):

  Excerpt from a poetry on The Romance of The Three Kingdoms (a Chinese
  classic 'San Gua'):

  [----------------------------|------------------------]
    ๏ แผ่นดินฮั่นเสื่อมโทรมแสนสังเวช  พระปกเกศกองบู๊กู้ขึ้นใหม่
  สิบสองกษัตริย์ก่อนหน้าแลถัดไป       สององค์ไซร้โง่เขลาเบาปัญญา
    ทรงนับถือขันทีเป็นที่พึ่ง           บ้านเมืองจึงวิปริตเป็นนักหนา
  โฮจิ๋นเรียกทัพทั่วหัวเมืองมา         หมายจะฆ่ามดชั่วตัวสำคัญ
    เหมือนขับไสไล่เสือจากเคหา      รับหมาป่าเข้ามาเลยอาสัญ
  ฝ่ายอ้องอุ้นยุแยกให้แตกกัน          ใช้สาวนั้นเป็นชนวนชื่นชวนใจ
    พลันลิฉุยกุยกีกลับก่อเหตุ          ช่างอาเพศจริงหนาฟ้าร้องไห้
  ต้องรบราฆ่าฟันจนบรรลัย           ฤๅหาใครค้ำชูกู้บรรลังก์ ฯ

  (The above is a two-column text. If combining characters are handled
  correctly, the lines of the second column should be aligned with the
  | character above.)

Ethiopian:

  Proverbs in the Amharic language:

  ሰማይ አይታረስ ንጉሥ አይከሰስ።
  ብላ ካለኝ እንደአባቴ በቆመጠኝ።
  ጌጥ ያለቤቱ ቁምጥና ነው።
  ደሀ በሕልሙ ቅቤ ባይጠጣ ንጣት በገደለው።
  የአፍ ወለምታ በቅቤ አይታሽም።
  አይጥ በበላ ዳዋ ተመታ።
  ሲተረጉሙ ይደረግሙ።
  ቀስ በቀስ፥ ዕንቁላል በእግሩ ይሄዳል።
  ድር ቢያብር አንበሳ ያስር።
  ሰው እንደቤቱ እንጅ እንደ ጉረቤቱ አይተዳደርም።
  እግዜር የከፈተውን ጉሮሮ ሳይዘጋው አይድርም።
  የጎረቤት ሌባ፥ ቢያዩት ይስቅ ባያዩት ያጠልቅ።
  ሥራ ከመፍታት ልጄን ላፋታት።
  ዓባይ ማደሪያ የለው፥ ግንድ ይዞ ይዞራል።
  የእስላም አገሩ መካ የአሞራ አገሩ ዋርካ።
  ተንጋሎ ቢተፉ ተመልሶ ባፉ።
  ወዳጅህ ማር ቢሆን ጨርስህ አትላሰው።
  እግርህን በፍራሽህ ልክ ዘርጋ።

Runes:

  ᚻᛖ ᚳᚹᚫᚦ ᚦᚫᛏ ᚻᛖ ᛒᚢᛞᛖ ᚩᚾ ᚦᚫᛗ ᛚᚪᚾᛞᛖ ᚾᚩᚱᚦᚹᛖᚪᚱᛞᚢᛗ ᚹᛁᚦ ᚦᚪ ᚹᛖᛥᚫ

  (Old English, which transcribed into Latin reads 'He cwaeth that he
  bude thaem lande northweardum with tha Westsae.' and means 'He said
  that he lived in the northern land near the Western Sea.')

Braille:

  ⡌⠁⠧⠑ ⠼⠁⠒  ⡍⠜⠇⠑⠹⠰⠎ ⡣⠕⠌

  ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠙⠑⠁⠙⠒ ⠞⠕ ⠃⠑⠛⠔ ⠺⠊⠹⠲ ⡹⠻⠑ ⠊⠎ ⠝⠕ ⠙⠳⠃⠞
  ⠱⠁⠞⠑⠧⠻ ⠁⠃⠳⠞ ⠹⠁⠞⠲ ⡹⠑ ⠗⠑⠛⠊⠌⠻ ⠕⠋ ⠙⠊⠎ ⠃⠥⠗⠊⠁⠇ ⠺⠁⠎
  ⠎⠊⠛⠝⠫ ⠃⠹ ⠹⠑ ⠊⠇⠻⠛⠹⠍⠁⠝⠂ ⠹⠑ ⠊⠇⠻⠅⠂ ⠹⠑ ⠥⠝⠙⠻⠞⠁⠅⠻⠂
  ⠁⠝⠙ ⠹⠑ ⠡⠊⠑⠋ ⠍⠳⠗⠝⠻⠲ ⡎⠊⠗⠕⠕⠛⠑ ⠎⠊⠛⠝⠫ ⠊⠞⠲ ⡁⠝⠙
  ⡎⠊⠗⠕⠕⠛⠑⠰⠎ ⠝⠁⠍⠑ ⠺⠁⠎ ⠛⠕⠕⠙ ⠥⠏⠕⠝ ⠰⡡⠁⠝⠛⠑⠂ ⠋⠕⠗ ⠁⠝⠹⠹⠔⠛ ⠙⠑
  ⠡⠕⠎⠑ ⠞⠕ ⠏⠥⠞ ⠙⠊⠎ ⠙⠁⠝⠙ ⠞⠕⠲

  ⡕⠇⠙ ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲

  ⡍⠔⠙⠖ ⡊ ⠙⠕⠝⠰⠞ ⠍⠑⠁⠝ ⠞⠕ ⠎⠁⠹ ⠹⠁⠞ ⡊ ⠅⠝⠪⠂ ⠕⠋ ⠍⠹
  ⠪⠝ ⠅⠝⠪⠇⠫⠛⠑⠂ ⠱⠁⠞ ⠹⠻⠑ ⠊⠎ ⠏⠜⠞⠊⠊⠥⠇⠜⠇⠹ ⠙⠑⠁⠙ ⠁⠃⠳⠞
  ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ ⡊ ⠍⠊⠣⠞ ⠙⠁⠧⠑ ⠃⠑⠲ ⠔⠊⠇⠔⠫⠂ ⠍⠹⠎⠑⠇⠋⠂ ⠞⠕
  ⠗⠑⠛⠜⠙ ⠁ ⠊⠕⠋⠋⠔⠤⠝⠁⠊⠇ ⠁⠎ ⠹⠑ ⠙⠑⠁⠙⠑⠌ ⠏⠊⠑⠊⠑ ⠕⠋ ⠊⠗⠕⠝⠍⠕⠝⠛⠻⠹
  ⠔ ⠹⠑ ⠞⠗⠁⠙⠑⠲ ⡃⠥⠞ ⠹⠑ ⠺⠊⠎⠙⠕⠍ ⠕⠋ ⠳⠗ ⠁⠝⠊⠑⠌⠕⠗⠎
  ⠊⠎ ⠔ ⠹⠑ ⠎⠊⠍⠊⠇⠑⠆ ⠁⠝⠙ ⠍⠹ ⠥⠝⠙⠁⠇⠇⠪⠫ ⠙⠁⠝⠙⠎
  ⠩⠁⠇⠇ ⠝⠕⠞ ⠙⠊⠌⠥⠗⠃ ⠊⠞⠂ ⠕⠗ ⠹⠑ ⡊⠳⠝⠞⠗⠹⠰⠎ ⠙⠕⠝⠑ ⠋⠕⠗⠲ ⡹⠳
  ⠺⠊⠇⠇ ⠹⠻⠑⠋⠕⠗⠑ ⠏⠻⠍⠊⠞ ⠍⠑ ⠞⠕ ⠗⠑⠏⠑⠁⠞⠂ ⠑⠍⠏⠙⠁⠞⠊⠊⠁⠇⠇⠹⠂ ⠹⠁⠞
  ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲

  (The first couple of paragraphs of "A Christmas Carol" by Dickens)

Compact font selection example text:

  ABCDEFGHIJKLMNOPQRSTUVWXYZ /0123456789
  abcdefghijklmnopqrstuvwxyz £©µÀÆÖÞßéöÿ
  –—‘“”„†•…‰™œŠŸž€ ΑΒΓΔΩαβγδω АБВГДабвгд
  ∀∂∈ℝ∧∪≡∞ ↑↗↨↻⇣ ┐┼╔╘░►☺♀ fi�⑀₂ἠḂӥẄɐː⍎אԱა

Greetings in various languages:

  Hello world, Καλημέρα κόσμε, コンニチハ

Box drawing alignment tests:                                          █
                                                                      ▉
  ╔══╦══╗  ┌──┬──┐  ╭──┬──╮  ╭──┬──╮  ┏━━┳━━┓  ┎┒┏┑   ╷  ╻ ┏┯┓ ┌┰┐    ▊ ╱╲╱╲╳╳╳
  ║┌─╨─┐║  │╔═╧═╗│  │╒═╪═╕│  │╓─╁─╖│  ┃┌─╂─┐┃  ┗╃╄┙  ╶┼╴╺╋╸┠┼┨ ┝╋┥    ▋ ╲╱╲╱╳╳╳
  ║│╲ ╱│║  │║   ║│  ││ │ ││  │║ ┃ ║│  ┃│ ╿ │┃  ┍╅╆┓   ╵  ╹ ┗┷┛ └┸┘    ▌ ╱╲╱╲╳╳╳
  ╠╡ ╳ ╞╣  ├╢   ╟┤  ├┼─┼─┼┤  ├╫─╂─╫┤  ┣┿╾┼╼┿┫  ┕┛┖┚     ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳
  ║│╱ ╲│║  │║   ║│  ││ │ ││  │║ ┃ ║│  ┃│ ╽ │┃  ░░▒▒▓▓██ ┊  ┆ ╎ ╏  ┇ ┋ ▎
  ║└─╥─┘║  │╚═╤═╝│  │╘═╪═╛│  │╙─╀─╜│  ┃└─╂─┘┃  ░░▒▒▓▓██ ┊  ┆ ╎ ╏  ┇ ┋ ▏
  ╚══╩══╝  └──┴──┘  ╰──┴──╯  ╰──┴──╯  ┗━━┻━━┛  ▗▄▖▛▀▜   └╌╌┘ ╎ ┗╍╍┛ ┋  ▁▂▃▄▅▆▇█
                                               ▝▀▘▙▄▟

gabbi-1.44.0/gabbi/tests/gabbits_intercept/values.json000066400000000000000000000005161332317117700227430ustar00rootroot00000000000000{ "values": [{ "pets": [{ "type": "cat", "sound": "meow" }, { "type": "dog", "sound": "woof" }] }, { "people": [{ "name": "chris", "id": 1 }, { "name": "justin", "id": 2 }] }] } gabbi-1.44.0/gabbi/tests/gabbits_live/000077500000000000000000000000001332317117700175115ustar00rootroot00000000000000gabbi-1.44.0/gabbi/tests/gabbits_live/google.yaml000066400000000000000000000010551332317117700216520ustar00rootroot00000000000000fixtures: - LiveSkipFixture defaults: ssl: True tests: - name: google url: / status: 302 || 301 - name: follow redirects desc: Confirm redirects are followed when we ask url: / redirects: True status: 200 # Explicit hosts - name: google full url url: https://google.com/ status: 302 || 301 - name: google russia desc: Test handling of non-utf-8 encoding url: https://www.google.ru/ - name: follow redirects full url desc: Confirm redirects are followed when we ask url: https://google.com redirects: True status: 200 gabbi-1.44.0/gabbi/tests/gabbits_runner/000077500000000000000000000000001332317117700200635ustar00rootroot00000000000000gabbi-1.44.0/gabbi/tests/gabbits_runner/failure.yaml000066400000000000000000000000711332317117700223740ustar00rootroot00000000000000tests: - name: expected failure GET: / status: 666 gabbi-1.44.0/gabbi/tests/gabbits_runner/nan.yaml000066400000000000000000000003131332317117700215200ustar00rootroot00000000000000tests: - name: test NAN url: /nan method: GET request_headers: content-type: application/json response_json_paths: $.nan: !!python/object:gabbi.tests.util.NanChecker {} gabbi-1.44.0/gabbi/tests/gabbits_runner/subdir/000077500000000000000000000000001332317117700213535ustar00rootroot00000000000000gabbi-1.44.0/gabbi/tests/gabbits_runner/subdir/sample.json000066400000000000000000000000351332317117700235250ustar00rootroot00000000000000{"items": {"house": "blue"}} gabbi-1.44.0/gabbi/tests/gabbits_runner/success.yaml000066400000000000000000000000741332317117700224200ustar00rootroot00000000000000tests: - name: expected success GET: /baz status: 200 gabbi-1.44.0/gabbi/tests/gabbits_runner/success_alt.yaml000066400000000000000000000000741332317117700232600ustar00rootroot00000000000000tests: - name: expected success GET: /baz status: 200 gabbi-1.44.0/gabbi/tests/gabbits_runner/test_data.yaml000066400000000000000000000002211332317117700227120ustar00rootroot00000000000000tests: - name: POST data from file verbose: true POST: / request_headers: content-type: application/json data: <@subdir/sample.json gabbi-1.44.0/gabbi/tests/gabbits_runner/test_verbose.yaml000066400000000000000000000004431332317117700234540ustar00rootroot00000000000000tests: - name: POST data with verbose true verbose: true POST: / request_headers: content-type: application/json data: - our text - name: structured data verbose: true POST: / request_headers: content-type: application/json data: cow: moo dog: bark gabbi-1.44.0/gabbi/tests/gabbits_runner/verbosity.yaml000066400000000000000000000001701332317117700227730ustar00rootroot00000000000000tests: - name: simple data post POST: / request_headers: content-type: application/json data: cat: poppy gabbi-1.44.0/gabbi/tests/gabbits_unsafe_yaml/000077500000000000000000000000001332317117700210555ustar00rootroot00000000000000gabbi-1.44.0/gabbi/tests/gabbits_unsafe_yaml/nan.yaml000066400000000000000000000003011332317117700225070ustar00rootroot00000000000000tests: - name: test NAN url: /nan method: GET request_headers: content-type: application/json response_json_paths: $.nan: !NanChecker {} $.nan: !IsNAN gabbi-1.44.0/gabbi/tests/simple_wsgi.py000066400000000000000000000131241332317117700177540ustar00rootroot00000000000000# # 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. """ SimpleWsgi provides a WSGI callable that can be used in tests to reflect posted data and otherwise confirm headers and queries. """ import json from six.moves.urllib import parse as urlparse CURRENT_POLL = 0 METHODS = ['GET', 'PUT', 'POST', 'DELETE', 'PATCH'] class SimpleWsgi(object): """A simple wsgi application to use in tests.""" def __call__(self, environ, start_response): global METHODS global CURRENT_POLL request_method = environ['REQUEST_METHOD'].upper() query_data = urlparse.parse_qs(environ.get('QUERY_STRING', '')) request_url = environ.get('REQUEST_URI', environ.get('RAW_URI', 'unknown')) path_info = environ.get('PATH_INFO', '') accept_header = environ.get('HTTP_ACCEPT') content_type_header = environ.get('CONTENT_TYPE', '') full_request_url = self._fully_qualify(environ, request_url) if accept_header: response_content_type = accept_header else: # JSON doesn't need a charset but we throw one in here # to exercise the decoding code response_content_type = ( 'application/json ; charset=utf-8 ; stop=no') headers = [ ('X-Gabbi-method', request_method), ('Content-Type', response_content_type), ('X-Gabbi-url', full_request_url), ] if request_method == 'DIE': raise Exception('because you asked me to') if request_method not in METHODS: headers.append( ('Allow', ', '.join(METHODS))) start_response('405 Method Not Allowed', headers) return [] if request_method.startswith('P'): body = environ['wsgi.input'].read() if body: if not content_type_header: start_response('400 Bad request', headers) return [] if content_type_header == 'application/json': body_data = json.loads(body.decode('utf-8')) if query_data: query_data.update(body_data) else: query_data = body_data headers.append(('Location', full_request_url)) if path_info == '/presenter': start_response('200 OK', [('Content-Type', 'text/html')]) return [b""" Hello World

Hello World

lorem ipsum dolor sit amet

"""] elif path_info.startswith('/poller'): if CURRENT_POLL == 0: CURRENT_POLL = int(query_data.get('count', [5])[0]) start_response('400 Bad Reqest', []) return [] else: CURRENT_POLL -= 1 if CURRENT_POLL > 0: start_response('400 Bad Reqest', []) return [] else: CURRENT_POLL = 0 # fall through if we've ended the loop elif path_info == '/cookie': headers.append(('Set-Cookie', 'session=1234; domain=.example.com')) elif path_info == '/jsonator': json_data = json.dumps({query_data['key'][0]: 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) query_output = json.dumps(query_data) return [query_output.encode('utf-8')] @staticmethod def _fully_qualify(environ, url): """Turn a URL path into a fully qualified URL.""" split_url = urlparse.urlsplit(url) server_name = environ.get('SERVER_NAME') server_port = str(environ.get('SERVER_PORT')) server_scheme = environ.get('wsgi.url_scheme') if server_port not in ['80', '443']: netloc = '%s:%s' % (server_name, server_port) else: netloc = server_name return urlparse.urlunsplit((server_scheme, netloc, split_url.path, split_url.query, split_url.fragment)) gabbi-1.44.0/gabbi/tests/test_driver.py000066400000000000000000000122001332317117700177560ustar00rootroot00000000000000# # 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 that the driver can build tests effectively.""" import os import unittest from gabbi import driver TESTS_DIR = 'test_gabbits' class DriverTest(unittest.TestCase): def setUp(self): super(DriverTest, self).setUp() self.loader = unittest.defaultTestLoader self.test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) 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(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__, 'test class name maps') self.assertEqual('one', the_one_test.test_data['name']) self.assertEqual('/', the_one_test.test_data['url']) def test_driver_prefix(self): suite = driver.build_tests(self.test_dir, self.loader, host='localhost', port=8001, prefix='/mountpoint') the_one_test = suite._tests[0]._tests[0] the_two_test = suite._tests[0]._tests[1] self.assertEqual('/mountpoint', the_one_test.prefix) self.assertEqual('/mountpoint', the_two_test.prefix) def test_build_requires_host_or_intercept(self): 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', require_ssl=True) first_test = suite._tests[0]._tests[0] full_url = first_test._parse_url(first_test.test_data['url']) self.assertEqual('https://localhost:8001/', full_url) suite = driver.build_tests(self.test_dir, self.loader, host='localhost', require_ssl=False) first_test = suite._tests[0]._tests[0] full_url = first_test._parse_url(first_test.test_data['url']) self.assertEqual('http://localhost:8001/', full_url) def test_build_url_target(self): suite = driver.build_tests(self.test_dir, self.loader, host='localhost', port='999', url='https://example.com:1024/theend') 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_target_forced_ssl(self): suite = driver.build_tests(self.test_dir, self.loader, host='localhost', port='999', url='http://example.com:1024/theend', require_ssl=True) 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']) gabbi-1.44.0/gabbi/tests/test_fixtures.py000066400000000000000000000034061332317117700203440ustar00rootroot00000000000000# # 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. """Use mocks to confirm that fixtures operate as context managers. """ import unittest from six.moves import mock from gabbi import fixture class FakeFixture(fixture.GabbiFixture): def __init__(self, _mock): super(FakeFixture, self).__init__() self.mock = _mock def start_fixture(self): self.mock.start() def stop_fixture(self): if self.exc_type: self.mock.handle(self.exc_type) self.mock.stop() class FixtureTest(unittest.TestCase): def setUp(self): super(FixtureTest, self).setUp() self.magic = mock.MagicMock(['start', 'stop', 'handle']) def test_fixture_starts_and_stop(self): with FakeFixture(self.magic): pass self.magic.start.assert_called_once_with() self.magic.stop.assert_called_once_with() def test_fixture_informs_on_exception(self): """Test that the stop fixture is passed exception info.""" try: with FakeFixture(self.magic): raise ValueError() except ValueError: pass self.magic.start.assert_called_once_with() self.magic.stop.assert_called_once_with() self.magic.handle.assert_called_once_with(ValueError) gabbi-1.44.0/gabbi/tests/test_gabbits/000077500000000000000000000000001332317117700175315ustar00rootroot00000000000000gabbi-1.44.0/gabbi/tests/test_gabbits/sample.yaml000066400000000000000000000003221332317117700216730ustar00rootroot00000000000000defaults: 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 gabbi-1.44.0/gabbi/tests/test_handlers.py000066400000000000000000000410521332317117700202720ustar00rootroot00000000000000# # 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 response handlers. """ import json import os import unittest from gabbi import case from gabbi.exception import GabbiFormatError from gabbi.handlers import core from gabbi.handlers import jsonhandler from gabbi.handlers import yaml_disk_loading_jsonhandler from gabbi import suitemaker class HandlersTest(unittest.TestCase): """Test the response handlers. Note that this does not test the magic template variables, that should be tested somewhere else. """ def setUp(self): super(HandlersTest, self).setUp() self.test_class = case.HTTPTestCase self.test = suitemaker.TestBuilder('mytest', (self.test_class,), {'test_data': {}, 'content_handlers': []}) def test_empty_response_handler(self): self.test.test_data = {'url': '$RESPONSE["barnabas"]'} self.test.response = {'content-type': 'unmatchable'} self.test.response_data = '' self.test.prior = self.test url = self.test('test_request').replace_template( self.test.test_data['url']) self.assertEqual('barnabas', url) self.test.response_data = None self.test.content_handlers = [jsonhandler.JSONHandler()] url = self.test('test_request').replace_template( self.test.test_data['url']) self.assertEqual('barnabas', url) def test_response_strings(self): handler = core.StringResponseHandler() self.test.content_type = "text/plain" self.test.response_data = None self.test.test_data = {'response_strings': ['alpha', 'beta']} self.test.output = 'alpha\nbeta\n' self._assert_handler(handler) def test_response_strings_fail(self): handler = core.StringResponseHandler() self.test.content_type = "text/plain" self.test.response_data = None self.test.test_data = {'response_strings': ['alpha', 'beta']} self.test.output = 'alpha\nbta\n' with self.assertRaises(AssertionError): self._assert_handler(handler) def test_response_strings_fail_big_output(self): handler = core.StringResponseHandler() self.test.content_type = "text/plain" self.test.response_data = None self.test.test_data = {'response_strings': ['alpha', 'beta']} self.test.output = 'alpha\nbta\n' * 1000 with self.assertRaises(AssertionError) as cm: self._assert_handler(handler) msg = str(cm.exception) self.assertEqual(2036, len(msg)) def test_response_strings_fail_big_payload(self): string_handler = core.StringResponseHandler() # Register the JSON handler so response_data is set. json_handler = jsonhandler.JSONHandler() self.test.response_handlers = [string_handler, json_handler] self.test.content_handlers = [json_handler] self.test.content_type = "application/json" self.test.test_data = {'response_strings': ['foobar']} self.test.response_data = { 'objects': [{'name': 'cw', 'location': 'barn'}, {'name': 'chris', 'location': 'house'}] * 100 } self.test.output = json.dumps(self.test.response_data) with self.assertRaises(AssertionError) as cm: self._assert_handler(string_handler) msg = str(cm.exception) self.assertEqual(2038, len(msg)) # Check the pprint of the json self.assertIn(' "location": "house"', msg) def test_response_string_list_type(self): handler = core.StringResponseHandler() self.test.test_data = { 'name': 'omega test', 'response_strings': 'omega' } self.test.output = 'omega\n' with self.assertRaises(GabbiFormatError) as exc: self._assert_handler(handler) self.assertIn('has incorrect type', str(exc)) self.assertIn("response_strings in 'omega test'", str(exc)) def test_response_json_paths(self): handler = jsonhandler.JSONHandler() self.test.content_type = "application/json" self.test.test_data = {'response_json_paths': { '$.objects[0].name': 'cow', '$.objects[1].location': 'house', }} self.test.response_data = { 'objects': [{'name': 'cow', 'location': 'barn'}, {'name': 'chris', 'location': 'house'}] } self._assert_handler(handler) def test_response_json_paths_fail_data(self): handler = jsonhandler.JSONHandler() self.test.content_type = "application/json" self.test.test_data = {'response_json_paths': { '$.objects[0].name': 'cow', '$.objects[1].location': 'house', }} self.test.response_data = { 'objects': [{'name': 'cw', 'location': 'barn'}, {'name': 'chris', 'location': 'house'}] } with self.assertRaises(AssertionError): self._assert_handler(handler) def test_response_json_paths_fail_path(self): handler = jsonhandler.JSONHandler() self.test.content_type = "application/json" self.test.test_data = {'response_json_paths': { '$.objects[1].name': 'cow', }} self.test.response_data = { 'objects': [{'name': 'cow', 'location': 'barn'}, {'name': 'chris', 'location': 'house'}] } with self.assertRaises(AssertionError): self._assert_handler(handler) def test_response_json_paths_regex(self): handler = jsonhandler.JSONHandler() self.test.content_type = "application/json" self.test.test_data = {'response_json_paths': { '$.objects[0].name': '/ow/', }} self.test.response_data = { '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" self.test.test_data = {'response_json_paths': { '$.objects[0].name': '/\d+/', }} self.test.response_data = { 'objects': [{'name': 99, 'location': 'barn'}, {'name': 'chris', 'location': 'house'}] } self._assert_handler(handler) def test_response_json_paths_dict_type(self): handler = jsonhandler.JSONHandler() self.test.test_data = { 'name': 'omega test', 'response_json_paths': ['alpha', 'beta'] } self.test.output = 'omega\n' with self.assertRaises(GabbiFormatError) as exc: self._assert_handler(handler) self.assertIn('has incorrect type', str(exc)) self.assertIn("response_json_paths in 'omega test'", str(exc)) def test_response_json_paths_from_disk_json_path(self): handler = jsonhandler.JSONHandler() lhs = '$.pets[?type = "cat"].sound' rhs = '$.values[0].pets[?type = "cat"].sound' self.test.test_directory = os.path.dirname(__file__) self.test.test_data = {'response_json_paths': { lhs: '<@gabbits_handlers/values.json:' + rhs, }} self.test.response_data = { "pets": [ {"type": "cat", "sound": "meow"}, {"type": "dog", "sound": "woof"} ] } self._assert_handler(handler) def test_response_json_paths_from_disk_json_path_fail(self): handler = jsonhandler.JSONHandler() lhs = '$.pets[?type = "cat"].sound' rhs = '$.values[0].pets[?type = "bad"].sound' self.test.test_directory = os.path.dirname(__file__) self.test.test_data = {'response_json_paths': { lhs: '<@gabbits_handlers/values.json:' + rhs, }} self.test.response_data = { "pets": [ {"type": "cat", "sound": "meow"}, {"type": "dog", "sound": "woof"} ] } with self.assertRaises(AssertionError): self._assert_handler(handler) def test_response_json_paths_yamlhandler(self): handler = yaml_disk_loading_jsonhandler.YAMLDiskLoadingJSONHandler() lhs = '$.pets[?type = "cat"].sound' rhs = '$.values[0].pets[?type = "cat"].sound' self.test.test_directory = os.path.dirname(__file__) self.test.test_data = {'response_json_paths': { lhs: '<@gabbits_handlers/subdir/values.yaml:' + rhs, }} self.test.response_data = { "pets": [ {"type": "cat", "sound": "meow"}, {"type": "dog", "sound": "woof"} ] } self._assert_handler(handler) def test_response_headers(self): handler = core.HeadersResponseHandler() self.test.response = {'content-type': 'text/plain'} self.test.test_data = {'response_headers': { 'content-type': 'text/plain', }} self._assert_handler(handler) self.test.test_data = {'response_headers': { 'Content-Type': 'text/plain', }} self._assert_handler(handler) def test_response_headers_regex(self): handler = core.HeadersResponseHandler() self.test.test_data = {'response_headers': { 'content-type': '/text/plain/', }} 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': { 'content-type': 'text/plain', }} self.test.response = {'content-type': 'application/json'} with self.assertRaises(AssertionError) as failure: self._assert_handler(handler) self.assertIn("Expect header content-type with value text/plain," " got application/json", str(failure.exception)) def test_response_headers_fail_header(self): handler = core.HeadersResponseHandler() self.test.test_data = {'response_headers': { 'location': '/somewhere', }} self.test.response = {'content-type': 'application/json'} with self.assertRaises(AssertionError) as failure: self._assert_handler(handler) self.assertIn("'location' header not present in response:", str(failure.exception)) def test_resonse_headers_stringify(self): handler = core.HeadersResponseHandler() self.test.test_data = {'response_headers': { 'x-alpha-beta': 2.0, }} self.test.response = {'x-alpha-beta': '2.0'} self._assert_handler(handler) self.test.response = {'x-alpha-beta': 2.0} self._assert_handler(handler) def _assert_handler(self, handler): # Instantiate our contained test class by naming its test # method and then run its tests to confirm. test = self.test('test_request') handler(test) gabbi-1.44.0/gabbi/tests/test_history.py000066400000000000000000000157651332317117700202070ustar00rootroot00000000000000# # 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 History Replacer. """ import unittest from gabbi import case from gabbi.handlers import jsonhandler from gabbi import suitemaker class HistoryTest(unittest.TestCase): """Test history variable.""" def setUp(self): super(HistoryTest, self).setUp() self.test_class = case.HTTPTestCase self.test = suitemaker.TestBuilder('mytest', (self.test_class,), {'test_data': {}, 'content_handlers': [], 'history': {}, }) def test_header_replace_prior(self): self.test.test_data = '$HEADERS["content-type"]' self.test.response = {'content-type': 'test_content'} self.test.prior = self.test header = self.test('test_request').replace_template( self.test.test_data) self.assertEqual('test_content', header) def test_header_replace_with_history(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) 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() 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) 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() self.test.content_type = "application/json" self.test.content_handlers = [json_handler] self.test.history["mytest"] = 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) self.assertEqual('test history', response) def test_cookie_replace_prior(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) 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'} self.test.history["mytest"] = self.test cookie = self.test('test_request').replace_template( self.test.test_data) self.assertEqual('test=cookie', cookie) def test_location_replace_prior(self): self.test.test_data = '$LOCATION' self.test.location = 'test_location' self.test.prior = self.test location = self.test('test_request').replace_template( 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' self.test.history["mytest"] = self.test location = self.test('test_request').replace_template( self.test.test_data) self.assertEqual('test_location', location) def test_url_replace_prior(self): self.test.test_data = '$URL' self.test.url = 'test_url' self.test.prior = self.test url = self.test('test_request').replace_template( 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' self.test.history["mytest"] = self.test url = self.test('test_request').replace_template( self.test.test_data) self.assertEqual('test_url', url) if __name__ == '__main__': unittest.main() gabbi-1.44.0/gabbi/tests/test_inner_fixture.py000066400000000000000000000043671332317117700213630ustar00rootroot00000000000000# # 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 the works of inner and outer fixtures. An "outer" fixture runs once per test suite. An "inner" is per test request. """ import os import sys 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 TESTS_DIR = 'gabbits_inner' COUNT_INNER = 0 COUNT_OUTER = 0 class OuterFixture(fixture.GabbiFixture): """Assert an outer fixture is only started once and is stopped.""" def start_fixture(self): global COUNT_OUTER COUNT_OUTER += 1 def stop_fixture(self): assert COUNT_OUTER == 1 class InnerFixture(fixtures.Fixture): """Test that setUp is called 3 times.""" def setUp(self): super(InnerFixture, self).setUp() global COUNT_INNER COUNT_INNER += 1 def cleanUp(self, raise_first=True): super(InnerFixture, self).cleanUp() 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, 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) gabbi-1.44.0/gabbi/tests/test_intercept.py000066400000000000000000000052551332317117700204740ustar00rootroot00000000000000# # 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 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 from gabbi.tests import util TESTS_DIR = 'gabbits_intercept' class FixtureOne(fixture.GabbiFixture): """Drive the fixture testing weakly.""" pass class FixtureTwo(fixture.GabbiFixture): """Drive the fixture testing weakly.""" pass class StubResponseHandler(base.ResponseHandler): """A sample response handler just to test.""" test_key_suffix = 'test' test_key_value = [] def preprocess(self, test): """Add some data if the data is a string.""" try: test.output = test.output + '\nAnother line' except TypeError: pass def action(self, test, item, value=None): item = item.replace('COW', '', 1) test.assertIn(item, test.output) # Incorporate the SkipAllFixture into this namespace so it can be used # by tests (cf. skipall.yaml). 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() 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): 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) gabbi-1.44.0/gabbi/tests/test_jsonpath.py000066400000000000000000000040621332317117700203200ustar00rootroot00000000000000# # 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 jsonpath handling """ import unittest from gabbi.handlers import jsonhandler extract = jsonhandler.JSONHandler.extract_json_path_value nested_data = { 'objects': [ {'name': 'one', 'value': 'alpha'}, {'name': 'two', 'value': 'beta'}, ] } simple_list = { 'objects': [ 'alpha', 'gamma', 'gabba', 'hey', 'carlton', ] } class JSONPathTest(unittest.TestCase): def test_basic_match(self): data = ['hi'] match = extract(data, '$[0]') self.assertEqual('hi', match) def test_list_handling(self): data = ['hi', 'bye'] match = extract(data, '$') self.assertEqual(data, match) def test_embedded_list_handling(self): match = extract(nested_data, '$.objects..name') self.assertEqual(['one', 'two'], match) def test_sorted_object_list(self): match = extract(nested_data, r'$.objects[\name][0].value') self.assertEqual('beta', match) def test_filtered_list(self): match = extract(nested_data, r'$.objects[?name = "one"].value') self.assertEqual('alpha', match) def test_sorted_simple_list(self): match = extract(simple_list, r'$.objects.`sorted`[-1]') self.assertEqual('hey', match) def test_len_simple_list(self): match = extract(simple_list, r'$.objects.`len`') self.assertEqual(5, match) def test_len_object_list(self): match = extract(nested_data, '$.objects.`len`') self.assertEqual(2, match) gabbi-1.44.0/gabbi/tests/test_live.py000066400000000000000000000033471332317117700174360ustar00rootroot00000000000000# # 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 test module to exercise the live requests functionality""" import os import sys 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 TESTS_DIR = 'gabbits_live' class LiveSkipFixture(fixture.GabbiFixture): """Skip a test file when we don't want to use the internet.""" def start_fixture(self): if os.environ.get('GABBI_SKIP_NETWORK', 'False').lower() == 'true': 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, **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) gabbi-1.44.0/gabbi/tests/test_load_data_file.py000066400000000000000000000052261332317117700214040ustar00rootroot00000000000000# # 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 loading data from files with <@. """ import unittest from six.moves import mock from gabbi import case @mock.patch( 'gabbi.case.open', new_callable=mock.mock_open, read_data='dummy content', create=True, ) class DataFileTest(unittest.TestCase): """Test reading files in tests. Reading from local files is only allowed at or below the test_directory level. """ def setUp(self): self.http_case = case.HTTPTestCase('test_request') def _assert_content_read(self, filepath): self.assertEqual( 'dummy content', self.http_case.load_data_file(filepath)) def test_load_file(self, m_open): self.http_case.test_directory = '.' self._assert_content_read('data.json') m_open.assert_called_with('./data.json', mode='rb') def test_load_file_in_directory(self, m_open): self.http_case.test_directory = '.' self._assert_content_read('a/b/c/data.json') m_open.assert_called_with('./a/b/c/data.json', mode='rb') def test_load_file_in_root(self, m_open): self.http_case.test_directory = '.' filepath = '/top-level.private' with self.assertRaises(ValueError): self.http_case.load_data_file(filepath) self.assertFalse(m_open.called) def test_load_file_in_parent_dir(self, m_open): self.http_case.test_directory = '.' filepath = '../file-in-parent-dir.txt' with self.assertRaises(ValueError): self.http_case.load_data_file(filepath) self.assertFalse(m_open.called) def test_load_file_within_test_directory(self, m_open): self.http_case.test_directory = '/a/b/c' self._assert_content_read('../../b/c/file-in-test-dir.txt') m_open.assert_called_with( '/a/b/c/../../b/c/file-in-test-dir.txt', mode='rb') def test_load_file_not_within_test_directory(self, m_open): self.http_case.test_directory = '/a/b/c' filepath = '../../b/E/file-in-test-dir.txt' with self.assertRaises(ValueError): self.http_case.load_data_file(filepath) self.assertFalse(m_open.called) gabbi-1.44.0/gabbi/tests/test_parse_url.py000066400000000000000000000134741332317117700204750ustar00rootroot00000000000000# # 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 place to put tests of URL parsing. These verbosely cover the _parse_url method to make sure it behaves. """ from collections import OrderedDict import copy import unittest import uuid from gabbi import case class UrlParseTest(unittest.TestCase): @staticmethod def make_test_case(host, port=8000, prefix='', ssl=False, params=None): # Attributes used are port, prefix and host and they must # be set manually here, due to metaclass magics elsewhere. # test_data must have a base value. http_case = case.HTTPTestCase('test_request') http_case.test_data = copy.copy(case.BASE_TEST) http_case.host = host http_case.port = port http_case.prefix = prefix http_case.test_data['ssl'] = ssl http_case.test_data['query_parameters'] = params or {} return http_case def test_parse_url(self): host = uuid.uuid4().hex http_case = self.make_test_case(host) parsed_url = http_case._parse_url('/foobar') self.assertEqual('http://%s:8000/foobar' % host, parsed_url) def test_parse_prefix(self): host = uuid.uuid4().hex http_case = self.make_test_case(host, prefix='/noise') parsed_url = http_case._parse_url('/foobar') self.assertEqual('http://%s:8000/noise/foobar' % host, parsed_url) def test_parse_full(self): host = uuid.uuid4().hex http_case = self.make_test_case(host) parsed_url = http_case._parse_url('http://example.com/house') self.assertEqual('http://example.com/house', parsed_url) def test_with_ssl(self): host = uuid.uuid4().hex http_case = self.make_test_case(host, ssl=True) parsed_url = http_case._parse_url('/foobar') self.assertEqual('https://%s:8000/foobar' % host, parsed_url) def test_default_port_http(self): host = uuid.uuid4().hex http_case = self.make_test_case(host, port='80') parsed_url = http_case._parse_url('/foobar') self.assertEqual('http://%s/foobar' % host, parsed_url) def test_default_port_int(self): host = uuid.uuid4().hex http_case = self.make_test_case(host, port=80) parsed_url = http_case._parse_url('/foobar') self.assertEqual('http://%s/foobar' % host, parsed_url) def test_default_port_https(self): host = uuid.uuid4().hex http_case = self.make_test_case(host, port='443', ssl=True) parsed_url = http_case._parse_url('/foobar') self.assertEqual('https://%s/foobar' % host, parsed_url) def test_default_port_https_no_ssl(self): host = uuid.uuid4().hex http_case = self.make_test_case(host, port='443') parsed_url = http_case._parse_url('/foobar') self.assertEqual('http://%s:443/foobar' % host, parsed_url) def test_https_port_80_ssl(self): host = uuid.uuid4().hex http_case = self.make_test_case(host, port='80', ssl=True) parsed_url = http_case._parse_url('/foobar') self.assertEqual('https://%s:80/foobar' % host, parsed_url) def test_ipv6_url(self): host = '::1' http_case = self.make_test_case(host, port='80', ssl=True) parsed_url = http_case._parse_url('/foobar') self.assertEqual('https://[%s]:80/foobar' % host, parsed_url) def test_ipv6_full_url(self): host = '::1' http_case = self.make_test_case(host, port='80', ssl=True) parsed_url = http_case._parse_url( 'http://[2001:4860:4860::8888]/foobar') self.assertEqual('http://[2001:4860:4860::8888]/foobar', parsed_url) def test_ipv6_no_double_colon_wacky_ssl(self): host = 'FEDC:BA98:7654:3210:FEDC:BA98:7654:3210' http_case = self.make_test_case(host, port='80', ssl=True) parsed_url = http_case._parse_url('/foobar') self.assertEqual( 'https://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/foobar', parsed_url) http_case = self.make_test_case(host, ssl=True) parsed_url = http_case._parse_url('/foobar') self.assertEqual( 'https://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:8000/foobar', parsed_url) def test_add_query_params(self): host = uuid.uuid4().hex # Use a sequence of tuples to ensure order. query = OrderedDict([('x', 1), ('y', 2)]) http_case = self.make_test_case(host, params=query) parsed_url = http_case._parse_url('/foobar') self.assertEqual('http://%s:8000/foobar?x=1&y=2' % host, parsed_url) def test_extend_query_params(self): host = uuid.uuid4().hex # Use a sequence of tuples to ensure order. query = OrderedDict([('x', 1), ('y', 2)]) http_case = self.make_test_case(host, params=query) parsed_url = http_case._parse_url('/foobar?alpha=beta') self.assertEqual('http://%s:8000/foobar?alpha=beta&x=1&y=2' % host, parsed_url) def test_extend_query_params_full_url(self): host = 'stub' query = OrderedDict([('x', 1), ('y', 2)]) http_case = self.make_test_case(host, params=query) parsed_url = http_case._parse_url( 'http://example.com/foobar?alpha=beta') self.assertEqual('http://example.com/foobar?alpha=beta&x=1&y=2', parsed_url) gabbi-1.44.0/gabbi/tests/test_replacers.py000066400000000000000000000042741332317117700204570ustar00rootroot00000000000000# # 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 place to put test of the replacers. """ import os import unittest from gabbi import case from gabbi import exception class EnvironReplaceTest(unittest.TestCase): def test_environ_boolean(self): """Environment variables are always strings That doesn't always suit our purposes, so test that "True" and "False" become booleans as a special case. """ http_case = case.HTTPTestCase('test_request') message = "$ENVIRON['moo']" os.environ['moo'] = "True" self.assertEqual(True, http_case._environ_replace(message)) os.environ['moo'] = "False" self.assertEqual(False, http_case._environ_replace(message)) os.environ['moo'] = "true" self.assertEqual(True, http_case._environ_replace(message)) os.environ['moo'] = "faLse" self.assertEqual(False, http_case._environ_replace(message)) os.environ['moo'] = "null" self.assertEqual(None, http_case._environ_replace(message)) os.environ['moo'] = "1" self.assertEqual(1, http_case._environ_replace(message)) os.environ['moo'] = "cow" self.assertEqual("cow", http_case._environ_replace(message)) message = '$ENVIRON["moo"]' 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) gabbi-1.44.0/gabbi/tests/test_runner.py000066400000000000000000000272661332317117700200160ustar00rootroot00000000000000# # 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 that the CLI works as expected """ import sys import unittest from uuid import uuid4 from six import StringIO from wsgi_intercept.interceptor import Urllib3Interceptor from gabbi import exception from gabbi.handlers import base from gabbi.handlers.jsonhandler import JSONHandler from gabbi import runner from gabbi.tests.simple_wsgi import SimpleWsgi class RunnerTest(unittest.TestCase): def setUp(self): super(RunnerTest, self).setUp() # NB: random host ensures that we're not accidentally connecting to an # actual server host, port = (str(uuid4()), 8000) self.host = host self.port = port self.server = lambda: Urllib3Interceptor( SimpleWsgi, host=host, port=port) self._stdin = sys.stdin self._stdout = sys.stdout sys.stdout = StringIO() # swallow output to avoid confusion self._stderr = sys.stderr sys.stderr = StringIO() # swallow output to avoid confusion self._argv = sys.argv sys.argv = ['gabbi-run', '%s:%s' % (host, port)] def tearDown(self): sys.stdin = self._stdin sys.stdout = self._stdout sys.stderr = self._stderr sys.argv = self._argv def test_input_files(self): sys.argv = ['gabbi-run', 'http://%s:%s/foo' % (self.host, self.port)] sys.argv.append('--') sys.argv.append('gabbi/tests/gabbits_runner/success.yaml') with self.server(): try: runner.run() except SystemExit as err: self.assertSuccess(err) sys.argv.append('gabbi/tests/gabbits_runner/failure.yaml') with self.server(): try: runner.run() except SystemExit as err: self.assertFailure(err) sys.argv.append('gabbi/tests/gabbits_runner/success_alt.yaml') with self.server(): try: runner.run() 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)] sys.stdin = StringIO(""" tests: - name: expected success GET: /baz status: 200 response_headers: x-gabbi-url: http://%s:%s/foo/baz """ % (self.host, self.port)) with self.server(): try: runner.run() except SystemExit as err: self.assertSuccess(err) def test_target_url_parsing_standard_port(self): # NOTE(cdent): For reasons unclear this regularly fails in # py.test and sometimes fails with testr. So there is # some state that is not being properly cleard somewhere. # Within SimpleWsgi, the environ thinks url_scheme is # 'https'. self.server = lambda: Urllib3Interceptor( SimpleWsgi, host=self.host, port=80) sys.argv = ['gabbi-run', 'http://%s/foo' % self.host] sys.stdin = StringIO(""" tests: - name: expected success GET: /baz status: 200 response_headers: x-gabbi-url: http://%s/foo/baz """ % self.host) with self.server(): try: runner.run() except SystemExit as err: self.assertSuccess(err) def test_custom_response_handler(self): sys.stdin = StringIO(""" tests: - name: unknown response handler GET: / response_html: ... """) with self.assertRaises(exception.GabbiFormatError): runner.run() sys.argv.insert(1, "--response-handler") sys.argv.insert(2, "gabbi.tests.test_runner:HTMLResponseHandler") sys.stdin = StringIO(""" tests: - name: custom response handler GET: /presenter response_html: h1: Hello World p: lorem ipsum dolor sit amet """) with self.server(): try: runner.run() except SystemExit as err: self.assertSuccess(err) sys.stdin = StringIO(""" tests: - name: custom response handler failure GET: /presenter response_html: h1: lipsum """) with self.server(): try: runner.run() except SystemExit as err: self.assertFailure(err) sys.argv.insert(3, "-r") sys.argv.insert(4, "gabbi.tests.test_intercept:StubResponseHandler") sys.stdin = StringIO(""" tests: - name: additional custom response handler GET: /presenter response_html: h1: Hello World response_test: - COWAnother line """) with self.server(): try: runner.run() except SystemExit as err: self.assertSuccess(err) sys.argv.insert(5, "-r") sys.argv.insert(6, "gabbi.tests.custom_response_handler") sys.stdin = StringIO(""" tests: - name: custom response handler shorthand GET: /presenter response_custom: - Hello World - lorem ipsum dolor sit amet """) with self.server(): try: runner.run() except SystemExit as err: self.assertSuccess(err) def test_exit_code(self): sys.stdin = StringIO() with self.assertRaises(exception.GabbiFormatError): runner.run() sys.stdin = StringIO(""" tests: - name: expected failure GET: / status: 666 """) try: runner.run() except SystemExit as err: self.assertFailure(err) sys.stdin = StringIO(""" tests: - name: expected success GET: / status: 200 """) with self.server(): try: runner.run() except SystemExit as err: self.assertSuccess(err) def test_verbose_output_formatting(self): """Confirm that a verbose test handles output properly.""" sys.argv = ['gabbi-run', 'http://%s:%s/foo' % (self.host, self.port)] sys.argv.append('--') sys.argv.append('gabbi/tests/gabbits_runner/test_verbose.yaml') with self.server(): try: runner.run() except SystemExit as err: self.assertSuccess(err) sys.stdout.seek(0) output = sys.stdout.read() self.assertIn('"our text"', output) self.assertIn('"cow": "moo"', output) self.assertIn('"dog": "bark"', output) # confirm pretty printing self.assertIn('{\n', output) self.assertIn('}\n', output) def test_data_dir_good(self): """Confirm that data dir is the test file's dir.""" sys.argv = ['gabbi-run', 'http://%s:%s/foo' % (self.host, self.port)] sys.argv.append('--') sys.argv.append('gabbi/tests/gabbits_runner/test_data.yaml') with self.server(): try: runner.run() except SystemExit as err: self.assertSuccess(err) # Compare the verbose output of tests with pretty printed # data. with open('gabbi/tests/gabbits_runner/subdir/sample.json') as data: data = JSONHandler.loads(data.read()) expected_string = JSONHandler.dumps(data, pretty=True) sys.stdout.seek(0) output = sys.stdout.read() self.assertIn(expected_string, output) def test_stdin_data_dir(self): """Confirm data dir as '.' when reading from stdin.""" sys.stdin = StringIO(""" tests: - name: expected success POST: / request_headers: content-type: application/json data: <@gabbi/tests/gabbits_runner/subdir/sample.json response_json_paths: $.items.house: blue """) with self.server(): try: runner.run() except SystemExit as err: self.assertSuccess(err) def _run_verbosity_arg(self): sys.argv.append('--') sys.argv.append('gabbi/tests/gabbits_runner/verbosity.yaml') with self.server(): try: runner.run() except SystemExit as err: self.assertSuccess(err) sys.stdout.seek(0) output = sys.stdout.read() return output def test_verbosity_arg_none(self): """Confirm --verbose handling.""" sys.argv = ['gabbi-run', 'http://%s:%s/foo' % (self.host, self.port)] output = self._run_verbosity_arg() self.assertEqual('', output) def test_verbosity_arg_body(self): """Confirm --verbose handling.""" sys.argv = ['gabbi-run', 'http://%s:%s/foo' % (self.host, self.port), '--verbose=body'] output = self._run_verbosity_arg() self.assertIn('{\n "cat": "poppy"\n}', output) self.assertNotIn('application/json', output) def test_verbosity_arg_headers(self): """Confirm --verbose handling.""" sys.argv = ['gabbi-run', 'http://%s:%s/foo' % (self.host, self.port), '--verbose=headers'] output = self._run_verbosity_arg() self.assertNotIn('{\n "cat": "poppy"\n}', output) self.assertIn('application/json', output) def test_verbosity_arg_all(self): """Confirm --verbose handling.""" sys.argv = ['gabbi-run', 'http://%s:%s/foo' % (self.host, self.port), '--verbose=all'] output = self._run_verbosity_arg() self.assertIn('{\n "cat": "poppy"\n}', output) self.assertIn('application/json', output) def assertSuccess(self, exitError): errors = exitError.args[0] if errors: self._dump_captured() self.assertEqual(errors, False) def assertFailure(self, exitError): errors = exitError.args[0] if not errors: self._dump_captured() self.assertEqual(errors, True) def _dump_captured(self): self._stderr.write('\n==> captured STDOUT <==\n') sys.stdout.flush() sys.stdout.seek(0) self._stderr.write(sys.stdout.read()) self._stderr.write('\n==> captured STDERR <==\n') sys.stderr.flush() sys.stderr.seek(0) self._stderr.write(sys.stderr.read()) class HTMLResponseHandler(base.ResponseHandler): test_key_suffix = 'html' test_key_value = {} def action(self, test, item, value=None): doc = test.output html = '<{tag}>{content}'.format(tag=item, content=value) test.assertTrue(html in doc, "no elements matching '%s'" % html) gabbi-1.44.0/gabbi/tests/test_suite.py000066400000000000000000000033361332317117700176260ustar00rootroot00000000000000# # 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. """Unit tests for the gabbi.suite. """ import sys import unittest from gabbi import fixture from gabbi import suitemaker VALUE_ERROR = 'value error sentinel' class FakeFixture(fixture.GabbiFixture): def start_fixture(self): raise ValueError(VALUE_ERROR) class SuiteTest(unittest.TestCase): def test_suite_catches_fixture_fail(self): """Verify fixture failure. When a fixture fails in start_fixture it should fail the first test in the suite and skip the others. """ loader = unittest.defaultTestLoader result = unittest.TestResult() test_data = {'fixtures': ['FakeFixture'], 'tests': [{'name': 'alpha', 'GET': '/'}, {'name': 'beta', 'GET': '/'}]} test_suite = suitemaker.test_suite_from_dict( loader, 'foo', test_data, '.', 'localhost', 80, sys.modules[__name__], None) test_suite.run(result) self.assertEqual(2, len(result.skipped)) self.assertEqual(1, len(result.errors)) errored_test, trace = result.errors[0] self.assertIn('foo_alpha', str(errored_test)) self.assertIn(VALUE_ERROR, trace) gabbi-1.44.0/gabbi/tests/test_suitemaker.py000066400000000000000000000151531332317117700206460ustar00rootroot00000000000000# # 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. import unittest from gabbi import exception from gabbi import handlers from gabbi.handlers import yaml_disk_loading_jsonhandler as ydlj_handler from gabbi import suitemaker class SuiteMakerTest(unittest.TestCase): def setUp(self): super(SuiteMakerTest, self).setUp() self.loader = unittest.defaultTestLoader def test_tests_key_required(self): test_yaml = {'name': 'house', 'url': '/'} with self.assertRaises(exception.GabbiFormatError) as failure: suitemaker.test_suite_from_dict(self.loader, 'foo', test_yaml, '.', 'localhost', 80, None, None) self.assertEqual('malformed test file, "tests" key required', str(failure.exception)) def test_upper_dict_required(self): test_yaml = [{'name': 'house', 'url': '/'}] with self.assertRaises(exception.GabbiFormatError) as failure: suitemaker.test_suite_from_dict(self.loader, 'foo', test_yaml, '.', 'localhost', 80, None, None) self.assertEqual('malformed test file, invalid format', str(failure.exception)) def test_inner_list_required(self): test_yaml = {'tests': {'name': 'house', 'url': '/'}} with self.assertRaises(exception.GabbiFormatError) as failure: suitemaker.test_suite_from_dict(self.loader, 'foo', test_yaml, '.', 'localhost', 80, None, None) self.assertIn('test chunk is not a dict at', str(failure.exception)) def test_name_key_required(self): test_yaml = {'tests': [{'url': '/'}]} with self.assertRaises(exception.GabbiFormatError) as failure: suitemaker.test_suite_from_dict(self.loader, 'foo', test_yaml, '.', 'localhost', 80, None, None) self.assertEqual('Test name missing in a test in foo.', str(failure.exception)) def test_url_key_required(self): test_yaml = {'tests': [{'name': 'missing url'}]} with self.assertRaises(exception.GabbiFormatError) as failure: suitemaker.test_suite_from_dict(self.loader, 'foo', test_yaml, '.', 'localhost', 80, None, None) self.assertEqual('Test url missing in test foo_missing_url.', str(failure.exception)) def test_unsupported_key_errors(self): test_yaml = {'tests': [{ 'url': '/', 'name': 'simple', 'bad_key': 'wow', }]} with self.assertRaises(exception.GabbiFormatError) as failure: suitemaker.test_suite_from_dict(self.loader, 'foo', test_yaml, '.', 'localhost', 80, None, None) self.assertIn("Invalid test keys used in test foo_simple:", str(failure.exception)) def test_method_url_pair_format_error(self): test_yaml = {'defaults': {'GET': '/foo'}, 'tests': []} with self.assertRaises(exception.GabbiFormatError) as failure: suitemaker.test_suite_from_dict(self.loader, 'foo', test_yaml, '.', 'localhost', 80, None, None) self.assertIn('"METHOD: url" pairs not allowed in defaults', str(failure.exception)) def test_method_url_pair_duplication_format_error(self): test_yaml = {'tests': [{ 'GET': '/', 'POST': '/', 'name': 'duplicate methods', }]} with self.assertRaises(exception.GabbiFormatError) as failure: suitemaker.test_suite_from_dict(self.loader, 'foo', test_yaml, '.', 'localhost', 80, None, None) self.assertIn( 'duplicate method/URL directive in "foo_duplicate_methods"', str(failure.exception) ) def test_dict_on_invalid_key(self): test_yaml = {'tests': [{ 'name': '...', 'GET': '/', 'response_html': { 'foo': 'hello', 'bar': 'world', } }]} with self.assertRaises(exception.GabbiFormatError) as failure: suitemaker.test_suite_from_dict(self.loader, 'foo', test_yaml, '.', 'localhost', 80, None, None) self.assertIn( "invalid key in test: 'response_html'", str(failure.exception) ) def test_response_handlers_same_test_key_yaml_last(self): test_yaml = {'tests': [{ 'name': '...', 'GET': '/', 'response_json_paths': { 'foo': 'hello', 'bar': 'world', } }]} handler_objects = [] ydlj_handler_object = ydlj_handler.YAMLDiskLoadingJSONHandler() for handler in handlers.RESPONSE_HANDLERS: handler_objects.append(handler()) handler_objects.append(ydlj_handler_object) file_suite = suitemaker.test_suite_from_dict( self.loader, 'foo', test_yaml, '.', 'localhost', 80, None, None, handlers=handler_objects) response_handlers = file_suite._tests[0].response_handlers self.assertNotIn(ydlj_handler_object, response_handlers) def test_response_handlers_same_test_key_yaml_first(self): test_yaml = {'tests': [{ 'name': '...', 'GET': '/', 'response_json_paths': { 'foo': 'hello', 'bar': 'world', } }]} ydlj_handler_object = ydlj_handler.YAMLDiskLoadingJSONHandler() handler_objects = [ydlj_handler_object] for handler in handlers.RESPONSE_HANDLERS: handler_objects.append(handler()) file_suite = suitemaker.test_suite_from_dict( self.loader, 'foo', test_yaml, '.', 'localhost', 80, None, None, handlers=handler_objects) response_handlers = file_suite._tests[0].response_handlers self.assertIn(ydlj_handler_object, response_handlers) gabbi-1.44.0/gabbi/tests/test_syntax_warning.py000066400000000000000000000026361332317117700215520ustar00rootroot00000000000000# # 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 that the driver warns on bad yaml name.""" import os import unittest import warnings from gabbi import driver from gabbi import exception TESTS_DIR = 'warning_gabbits' class DriverTest(unittest.TestCase): def setUp(self): super(DriverTest, self).setUp() self.loader = unittest.defaultTestLoader self.test_dir = os.path.join(os.path.dirname(__file__), TESTS_DIR) def test_driver_warnings_on_files(self): with warnings.catch_warnings(record=True) as the_warnings: driver.build_tests( self.test_dir, self.loader, host='localhost', port=8001) self.assertEqual(1, len(the_warnings)) the_warning = the_warnings[-1] self.assertEqual( the_warning.category, exception.GabbiSyntaxWarning) self.assertIn("'_' in test filename", str(the_warning.message)) gabbi-1.44.0/gabbi/tests/test_unsafe_yaml.py000066400000000000000000000032231332317117700207730ustar00rootroot00000000000000# # 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) gabbi-1.44.0/gabbi/tests/test_use_prior_test.py000066400000000000000000000043231332317117700215400ustar00rootroot00000000000000# # 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 import unittest from six.moves import mock 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() gabbi-1.44.0/gabbi/tests/test_utils.py000066400000000000000000000243341332317117700176360ustar00rootroot00000000000000# # 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 functions from the utils module. """ import unittest from gabbi import utils class BinaryTypesTest(unittest.TestCase): BINARY_TYPES = [ 'image/png', 'application/binary', ] NON_BINARY_TYPES = [ 'text/plain', 'application/atom+xml', 'application/vnd.custom+json', 'application/javascript', 'application/json', 'application/json-home', 'application/xml', ] def test_not_binary(self): for media_type in self.NON_BINARY_TYPES: self.assertTrue(utils.not_binary(media_type), '%s should not be binary' % media_type) def test_binary(self): for media_type in self.BINARY_TYPES: self.assertFalse(utils.not_binary(media_type), '%s should be binary' % media_type) class ParseContentTypeTest(unittest.TestCase): def test_parse_simple(self): self.assertEqual( ('text/plain', 'latin-1'), utils.parse_content_type('text/plain; charset=latin-1')) def test_parse_extra(self): self.assertEqual( ('text/plain', 'latin-1'), utils.parse_content_type( 'text/plain; charset=latin-1; version=1.2')) def test_parse_default(self): self.assertEqual( ('text/plain', 'utf-8'), utils.parse_content_type('text/plain')) def test_parse_error_default(self): self.assertEqual( ('text/plain', 'utf-8'), utils.parse_content_type( 'text/plain; face=ouch; charset=latin-1;')) def test_parse_nocharset_default(self): self.assertEqual( ('text/plain', 'utf-8'), utils.parse_content_type( 'text/plain; face=ouch')) def test_parse_override_default(self): self.assertEqual( ('text/plain', 'latin-1'), utils.parse_content_type( 'text/plain; face=ouch', default_charset='latin-1')) class ExtractContentTypeTest(unittest.TestCase): def test_extract_content_type_default_both(self): """Empty dicts returns default type and chartset.""" content_type, charset = utils.extract_content_type({}) self.assertEqual('application/binary', content_type) self.assertEqual('utf-8', charset) def test_extract_content_type_default_charset(self): """Empty dicts returns default type and chartset.""" content_type, charset = utils.extract_content_type({ 'content-type': 'text/colorful'}) self.assertEqual('text/colorful', content_type) self.assertEqual('utf-8', charset) def test_extract_content_type_with_charset(self): content_type, charset = utils.extract_content_type( {'content-type': 'text/colorful; charset=latin-10'}) self.assertEqual('text/colorful', content_type) self.assertEqual('latin-10', charset) def test_extract_content_type_multiple_params(self): content_type, charset = utils.extract_content_type( {'content-type': 'text/colorful; version=1.24; charset=latin-10'}) self.assertEqual('text/colorful', content_type) self.assertEqual('latin-10', charset) def test_extract_content_type_bad_params(self): content_type, charset = utils.extract_content_type( {'content-type': 'text/colorful; version=1.24; charset=latin-10;'}) self.assertEqual('text/colorful', content_type) self.assertEqual('utf-8', charset) class ColorizeTest(unittest.TestCase): def test_colorize_missing_color(self): """Make sure that choosing a non-existent color is safe.""" message = utils._colorize('CERULEAN', 'hello') self.assertEqual('hello', message) message = utils._colorize('BLUE', 'hello') self.assertNotEqual('hello', message) class CreateURLTest(unittest.TestCase): def test_create_url_simple(self): url = utils.create_url('/foo/bar', 'test.host.com') self.assertEqual('http://test.host.com/foo/bar', url) def test_create_url_ssl(self): url = utils.create_url('/foo/bar', 'test.host.com', ssl=True) self.assertEqual('https://test.host.com/foo/bar', url) def test_create_url_prefix(self): url = utils.create_url('/foo/bar', 'test.host.com', prefix='/zoom') self.assertEqual('http://test.host.com/zoom/foo/bar', url) def test_create_url_port(self): url = utils.create_url('/foo/bar', 'test.host.com', port=8000) self.assertEqual('http://test.host.com:8000/foo/bar', url) def test_create_url_port_and_ssl(self): url = utils.create_url('/foo/bar', 'test.host.com', ssl=True, port=8000) self.assertEqual('https://test.host.com:8000/foo/bar', url) def test_create_url_not_ssl_on_443(self): url = utils.create_url('/foo/bar', 'test.host.com', ssl=False, port=443) self.assertEqual('http://test.host.com:443/foo/bar', url) def test_create_url_ssl_on_80(self): url = utils.create_url('/foo/bar', 'test.host.com', ssl=True, port=80) self.assertEqual('https://test.host.com:80/foo/bar', url) def test_create_url_preserve_query(self): url = utils.create_url('/foo/bar?x=1&y=2', 'test.host.com', ssl=True, port=80) self.assertEqual('https://test.host.com:80/foo/bar?x=1&y=2', url) def test_create_url_ipv6_ssl(self): url = utils.create_url('/foo/bar?x=1&y=2', '::1', ssl=True) self.assertEqual('https://[::1]/foo/bar?x=1&y=2', url) def test_create_url_ipv6_ssl_weird_port(self): url = utils.create_url('/foo/bar?x=1&y=2', '::1', ssl=True, port=80) self.assertEqual('https://[::1]:80/foo/bar?x=1&y=2', url) def test_create_url_ipv6_full(self): url = utils.create_url('/foo/bar?x=1&y=2', '2607:f8b0:4000:801::200e', port=8080) self.assertEqual( 'http://[2607:f8b0:4000:801::200e]:8080/foo/bar?x=1&y=2', url) def test_create_url_ipv6_already_bracket(self): url = utils.create_url( '/foo/bar?x=1&y=2', '[2607:f8b0:4000:801::200e]', port=999) self.assertEqual( 'http://[2607:f8b0:4000:801::200e]:999/foo/bar?x=1&y=2', url) def test_create_url_no_double_colon(self): url = utils.create_url( '/foo', 'FEDC:BA98:7654:3210:FEDC:BA98:7654:3210', port=999) self.assertEqual( 'http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:999/foo', url) class UtilsHostInfoFromTarget(unittest.TestCase): def _test_hostport(self, url_or_host, expected_host, provided_prefix=None, expected_port=None, expected_prefix=None, expected_ssl=False): host, port, prefix, ssl = utils.host_info_from_target( url_or_host, provided_prefix) # normalize hosts, they are case insensitive self.assertEqual(expected_host.lower(), host.lower()) # port can be a string or int depending on the inputs self.assertEqual(expected_port, port) self.assertEqual(expected_prefix, prefix) self.assertEqual(expected_ssl, ssl) def test_plain_url_no_port(self): self._test_hostport('http://foobar.com/news', 'foobar.com', expected_port=None, expected_prefix='/news') def test_plain_url_with_port(self): self._test_hostport('http://foobar.com:80/news', 'foobar.com', expected_port=80, expected_prefix='/news') def test_ssl_url(self): self._test_hostport('https://foobar.com/news', 'foobar.com', expected_prefix='/news', expected_ssl=True) def test_ssl_port80_url(self): self._test_hostport('https://foobar.com:80/news', 'foobar.com', expected_prefix='/news', expected_port=80, expected_ssl=True) def test_ssl_port_url(self): self._test_hostport('https://foobar.com:999/news', 'foobar.com', expected_prefix='/news', expected_port=999, expected_ssl=True) def test_simple_hostport(self): self._test_hostport('foobar.com:999', 'foobar.com', expected_port='999') def test_simple_hostport_with_prefix(self): self._test_hostport('foobar.com:999', 'foobar.com', provided_prefix='/news', expected_port='999', expected_prefix='/news') def test_ipv6_url_long(self): self._test_hostport( 'http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:999/news', 'FEDC:BA98:7654:3210:FEDC:BA98:7654:3210', expected_port=999, expected_prefix='/news') def test_ipv6_url_localhost(self): self._test_hostport( 'http://[::1]:999/news', '::1', expected_port=999, expected_prefix='/news') def test_ipv6_host_localhost(self): # If a user wants to use the hostport form, then they need # to hack it with the brackets. self._test_hostport( '[::1]', '::1') def test_ipv6_hostport_localhost(self): self._test_hostport( '[::1]:999', '::1', expected_port='999') gabbi-1.44.0/gabbi/tests/test_yaml_disk_loading_jsonhandler.py000066400000000000000000000032201332317117700245250ustar00rootroot00000000000000# # 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 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.handlers import yaml_disk_loading_jsonhandler from gabbi.tests import simple_wsgi TESTS_DIR = 'gabbits_handlers' BUILD_TEST_ARGS = dict( intercept=simple_wsgi.SimpleWsgi, content_handlers=[yaml_disk_loading_jsonhandler.YAMLDiskLoadingJSONHandler] ) 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) gabbi-1.44.0/gabbi/tests/util.py000066400000000000000000000023361332317117700164120ustar00rootroot00000000000000# # 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. """Utility methods shared by some tests.""" import math import os import yaml def set_test_environ(): """Set some environment variables used in tests.""" os.environ['GABBI_TEST_URL'] = 'takingnames' # Setup environment variables for `coerce.yaml` os.environ['ONE'] = '1' os.environ['DECIMAL'] = '1.0' os.environ['ARRAY_STRING'] = '[1,2,3]' os.environ['TRUE'] = 'true' 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 ValueError: return False gabbi-1.44.0/gabbi/tests/warning_gabbits/000077500000000000000000000000001332317117700202175ustar00rootroot00000000000000gabbi-1.44.0/gabbi/tests/warning_gabbits/underscore_sample.yaml000066400000000000000000000001271332317117700246150ustar00rootroot00000000000000 tests: - name: one url: / - name: two url: http://example.com/moo gabbi-1.44.0/gabbi/utils.py000066400000000000000000000137261332317117700154400ustar00rootroot00000000000000# # 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. """Utility functions grab bag.""" import io import os import colorama import six from six.moves.urllib import parse as urlparse import yaml try: # Python 3 ConnectionRefused = ConnectionRefusedError except NameError: # Python 2 import socket ConnectionRefused = socket.error def create_url(base_url, host, port=None, prefix='', ssl=False): """Given pieces of a path-based url, return a fully qualified url.""" scheme = 'http' # A host with : in it at this stage is assumed to be an IPv6 # address of some kind (they come in many forms). Port should # already have been stripped off. if ':' in host and not (host.startswith('[') and host.endswith(']')): host = '[%s]' % host if port and not _port_follows_standard(port, ssl): netloc = '%s:%s' % (host, port) else: netloc = host if ssl: scheme = 'https' parsed_url = urlparse.urlsplit(base_url) query_string = parsed_url.query path = parsed_url.path # Guard against a prefix of None or the url already having the # prefix. Without the startswith check, the tests in prefix.yaml # fail. This is a pragmatic fix which does this for any URL in a # test request that does not have a scheme and does not # distinguish between URLs in a gabbi test file and those # generated by the server. Idealy we would not mutate nor need # to check URLs returned from the server. Doing that, however, # would require more complex data handling than we have now and # this covers most common cases and will be okay until someone # reports a bug. if prefix and not path.startswith(prefix): prefix = prefix.rstrip('/') path = path.lstrip('/') path = '%s/%s' % (prefix, path) return urlparse.urlunsplit((scheme, netloc, path, query_string, '')) def decode_response_content(header_dict, content): """Decode content to a proper string.""" content_type, charset = extract_content_type(header_dict) if not_binary(content_type) and isinstance(content, six.binary_type): return content.decode(charset) else: return content def extract_content_type(header_dict, default='application/binary'): """Extract parsed content-type from headers.""" content_type = header_dict.get('content-type', default).strip().lower() return parse_content_type(content_type) def get_colorizer(stream): """Return a function to colorize a string. Only if stream is a tty . """ if stream.isatty() or os.environ.get('GABBI_FORCE_COLOR', False): colorama.init() return _colorize else: return lambda x, y: y 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 load(source.read()) # This will intentionally raise AttributeError if handle is None. return load(handle.read()) def not_binary(content_type): """Decide if something is content we'd like to treat as a string.""" return (content_type.startswith('text/') or content_type.endswith('+xml') or content_type.endswith('+json') or content_type == 'application/javascript' or content_type.startswith('application/json') or content_type.startswith('application/xml')) def parse_content_type(content_type, default_charset='utf-8'): """Parse content type value for media type and charset.""" charset = default_charset if ';' in content_type: content_type, parameter_strings = (attr.strip() for attr in content_type.split(';', 1)) try: parameter_pairs = [atom.strip().split('=') for atom in parameter_strings.split(';')] parameters = {name: value for name, value in parameter_pairs} charset = parameters['charset'] except (ValueError, KeyError): # KeyError when no charset found. # ValueError when the parameter_strings are poorly # formed (for example trailing ;) pass return (content_type, charset) def host_info_from_target(target, prefix=None): """Turn url or host:port and target into test destination.""" force_ssl = False split_url = urlparse.urlparse(target) if split_url.scheme: if split_url.scheme == 'https': force_ssl = True return split_url.hostname, split_url.port, split_url.path, force_ssl else: target = target prefix = prefix if ':' in target and '[' not in target: host, port = target.rsplit(':', 1) elif ']:' in target: host, port = target.rsplit(':', 1) else: host = target port = None host = host.replace('[', '').replace(']', '') return host, port, prefix, force_ssl def _colorize(color, message): """Add a color to the message.""" try: return getattr(colorama.Fore, color) + message + colorama.Fore.RESET except AttributeError: return message def _port_follows_standard(port, ssl): """Return True if a standard port is using a non-standard ssl setting.""" port = int(port) return (port == 443 and ssl) or (port == 80 and not ssl) gabbi-1.44.0/requirements-dev.txt000066400000000000000000000000041332317117700167030ustar00rootroot00000000000000tox gabbi-1.44.0/requirements.txt000066400000000000000000000001521332317117700161330ustar00rootroot00000000000000pbr pytest six PyYAML<4.0 urllib3>=1.11.0 jsonpath-rw-ext>=1.0.0 wsgi-intercept>=1.2.2 colorama testtools gabbi-1.44.0/setup.cfg000066400000000000000000000017531332317117700145000ustar00rootroot00000000000000[metadata] name = gabbi author = Chris Dent author-email = cdent@anticdent.org summary = Declarative HTTP testing library description-file = README.rst license = Apache-2 home-page = https://github.com/cdent/gabbi classifier = Intended Audience :: Developers Intended Audience :: Information Technology Environment :: Web Environment License :: OSI Approved :: Apache Software License Operating System :: POSIX Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Topic :: Internet :: WWW/HTTP :: WSGI Topic :: Software Development :: Testing [files] packages = gabbi [build_sphinx] all_files = 1 build-dir = docs/build source-dir = docs/source [entry_points] console_scripts = gabbi-run = gabbi.runner:run [bdist_wheel] universal=1 gabbi-1.44.0/setup.py000066400000000000000000000012121332317117700143570ustar00rootroot00000000000000#!/usr/bin/env python # # 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. import setuptools setuptools.setup( setup_requires=['pbr'], pbr=True) gabbi-1.44.0/test-failskip.sh000077500000000000000000000014011332317117700157630ustar00rootroot00000000000000#!/bin/bash -e # Run the tests and confirm that the stuff we expect to skip or fail # does. # this would be somewhat less complex in bash4.. shopt -s nocasematch [[ "${GABBI_SKIP_NETWORK:-false}" == "true" ]] && SKIP=7 || SKIP=2 shopt -u nocasematch 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="$SKIP skipped, $FAILS xfailed" stestr run && \ for match in "${GREP_FAIL_MATCH}" "${GREP_UXSUC_MATCH}" "${GREP_SKIP_MATCH}"; do stestr last --subunit | subunit2pyunit 2>&1 | \ grep "${match}" done # Make sure pytest failskips too py.test gabbi | grep "$PYTEST_MATCH" gabbi-1.44.0/test-limit.sh000077500000000000000000000010441332317117700153020ustar00rootroot00000000000000#!/bin/bash # Run a test which is limited to just one request from a file that # contains many requests and confirm that only one was run and that # it did actually run. # # This covers a situation where the change of intercepts to fixtures # broke limiting tests and we never knew. GREP_TEST_MATCH='tests.test_intercept.self_checklimit.test_request ... ok' GREP_COUNT_MATCH='Ran: 1 ' stestr run "checklimit" && \ stestr last --subunit | subunit2pyunit 2>&1 | \ grep "${GREP_TEST_MATCH}" && \ stestr last | grep "${GREP_COUNT_MATCH}" gabbi-1.44.0/test-requirements.txt000066400000000000000000000001101332317117700171020ustar00rootroot00000000000000mock ; python_version < '3.3' stestr coverage pytest-cov hacking sphinx gabbi-1.44.0/tox.ini000066400000000000000000000050671332317117700141740ustar00rootroot00000000000000[tox] minversion = 1.6 skipsdist = True envlist = py27,py34,py35,py36,pypy,pep8,limit,failskip,docs,py36-prefix,py36-limit,py36-failskip,py27-pytest,py35-pytest,py36-pytest [testenv] deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt whitelist_externals = rm install_command = pip install -U {opts} {packages} commands = rm -f .testrepository/times.dbm stestr run {posargs} setenv = GABBI_PREFIX= passenv = GABBI_* HOME [testenv:venv] deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = {posargs} [testenv:py27-pytest] commands = py.test gabbi [testenv:py35-pytest] commands = py.test gabbi [testenv:py36-pytest] commands = py.test gabbi [testenv:py36-prefix] setenv = GABBI_PREFIX=/snoopy [testenv:pep8] basepython = python3 deps = hacking commands = flake8 [testenv:limit] commands = {toxinidir}/test-limit.sh [testenv:failskip] commands = {toxinidir}/test-failskip.sh [testenv:py36-limit] commands = {toxinidir}/test-limit.sh [testenv:py36-failskip] commands = {toxinidir}/test-failskip.sh [testenv:cover] setenv = PYTHON=coverage run --source gabbi --parallel-mode commands = coverage erase find . -type f -name "*.pyc" -delete stestr run {posargs} coverage combine coverage html -d cover coverage xml -o cover/coverage.xml coverage report [testenv:pytest-cov] commands = py.test --cov=gabbi gabbi/tests --cov-config .coveragerc --cov-report html [testenv:gnocchi] basepython = python2.7 deps = -egit+https://github.com/gnocchixyz/gnocchi#egg=gnocchi tox changedir = {envdir}/src/gnocchi 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 = rm -rf doc/build python setup.py build_sphinx whitelist_externals = rm [flake8] exclude=.venv,.git,.tox,dist,*egg,*.egg-info,build,examples,docs show-source = True