tap_py-3.2.1/docs/Makefile0000644000000000000000000001514613615410400012330 0ustar00# 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) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .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/tappy.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/tappy.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/tappy" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/tappy" @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." tap_py-3.2.1/docs/alternatives.rst0000644000000000000000000000333513615410400014120 0ustar00Alternatives ============ tappy is not the only project that can produce TAP output for Python. While tappy is a capable TAP producer and consumer, other projects might be a better fit for you. The following comparison lists some other Python TAP tools and lists some of the biggest differences compared to tappy. pycotap ------- pycotap is a good tool for when you want TAP output, but you don't want extra dependencies. pycotap is a zero dependency TAP producer. It is so small that you could even embed it into your project. `Check out the project homepage `_. catapult -------- catapult is a TAP producer. catapult is also capable of producing TAP-Y and TAP-J which are YAML and JSON test streams that are inspired by TAP. `You can find the catapult source on GitHub `_. pytap13 ------- pytap13 is a TAP consumer for TAP version 13. It parses a TAP stream and produces test instances that can be inspected. `pytap13's homepage is on Bitbucket `_. bayeux ------ bayeux is a TAP producer that is designed to work with unittest and unittest2. `bayeux is on GitLab. `_. taptaptap --------- taptaptap is a TAP producer with a procedural style similar to Perl. It also includes a ``TapWriter`` class as a TAP producer. `Visit the taptaptap homepage `_. unittest-tap-reporting ---------------------- unittest-tap-reporting is another zero dependency TAP producer. `Check it out on GitHub `_. If there are other relevant projects, please post an issue on GitHub so this comparison page can be updated accordingly. tap_py-3.2.1/docs/conf.py0000644000000000000000000002040513615410400012161 0ustar00# # tappy documentation build configuration file, created by # sphinx-quickstart on Tue Mar 11 20:21:22 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 os import sys sys.path.append(os.path.abspath("..")) from tap import __version__ # noqa # 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. # sys.path.insert(0, os.path.abspath('.')) # -- 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", ] # autodoc settings autodoc_member_order = "bysource" # 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 = "tappy" copyright = "Matt Layman and contributors" # 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 = __version__ # The full version, including alpha/beta/rc tags. release = __version__ # 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 = ["_build"] # 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" # 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 = True # 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 = "tappydoc" # -- 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", "tappy.tex", "tappy Documentation", "Matt Layman and contributors", "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 = [("tappy.1", "tappy", "a tap consumer for python", [], 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", "tappy", "tappy Documentation", "Matt Layman and contributors", "tappy", "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 tap_py-3.2.1/docs/consumers.rst0000644000000000000000000001112613615410400013432 0ustar00TAP Consumers ============= tappy Tool ---------- The ``tappy`` command line tool is a `TAP consumer `_. The tool accepts TAP files or directories containing TAP files and provides a standard Python ``unittest`` style summary report. Check out ``tappy -h`` for the complete list of options. You can also use the tool's shorter alias of ``tap``. .. code-block:: console $ tappy *.tap ................F.................................. ====================================================================== FAIL: - The parser extracts a bail out line. ---------------------------------------------------------------------- ---------------------------------------------------------------------- Ran 51 tests in 0.002s FAILED (failures=1) TAP Stream ~~~~~~~~~~ ``tappy`` can read a TAP stream directly STDIN. This permits any TAP producer to pipe its results to ``tappy`` without generating intermediate output files. ``tappy`` will read from STDIN when no arguments are provided or when a dash character is the only argument. Here is an example of ``nosetests`` piping to ``tappy``: .. code-block:: console $ nosetests --with-tap --tap-stream 2>&1 | tappy ...................................................................... ............................................... ---------------------------------------------------------------------- Ran 117 tests in 0.003s OK In this example, ``nosetests`` puts the TAP stream on STDERR so it must be redirected to STDOUT because the Unix pipe expects input on STDOUT. ``tappy`` can use redirected input from a shell. .. code-block:: console $ tappy < TestAdapter.tap ........ ---------------------------------------------------------------------- Ran 8 tests in 0.000s OK This final example shows ``tappy`` consuming TAP from Perl's test tool, ``prove``. The example includes the optional dash character. .. code-block:: console $ prove t/array.t -v | tappy - ............ ---------------------------------------------------------------------- Ran 12 tests in 0.001s OK API --- In addition to a command line interface, tappy enables programmatic access to TAP files for users to create their own TAP consumers. This access comes in two forms: 1. A ``Loader`` class which provides a ``load`` method to load a set of TAP files into a ``unittest.TestSuite``. The ``Loader`` can receive files or directories. .. code-block:: pycon >>> loader = Loader() >>> suite = loader.load(['foo.tap', 'bar.tap', 'baz.tap']) 2. A ``Parser`` class to provide a lower level interface. The ``Parser`` can parse a file via ``parse_file`` and return parsed lines that categorize the file contents. .. code-block:: pycon >>> parser = Parser() >>> for line in parser.parse_file('foo.tap'): ... # Do whatever you want with the processed line. ... pass The API specifics are listed below. .. autoclass:: tap.loader.Loader :members: .. autoclass:: tap.parser.Parser :members: .. _tap-version-13: TAP version 13 ~~~~~~~~~~~~~~ The specification for TAP version 13 adds support for `yaml blocks `_ to provide additional information about the preceding test. In order to consume yaml blocks, ``tappy`` requires `pyyaml `_ and `more-itertools `_ to be installed. These dependencies are optional. If they are not installed, TAP output will still be consumed, but any yaml blocks will be parsed as :class:`tap.line.Unknown`. If a :class:`tap.line.Result` object has an associated yaml block, :attr:`~tap.line.Result.yaml_block` will return the block converted to a ``dict``. Otherwise, it will return ``None``. ``tappy`` provides a strict interpretation of the specification. A yaml block will only be associated with a result if it immediately follows that result. Any :class:`diagnostic ` between a :class:`result ` and a yaml block will result in the block lines being parsed as :class:`tap.line.Unknown`. Line Categories ~~~~~~~~~~~~~~~ The parser returns ``Line`` instances. Each line contains different properties depending on its category. .. autoclass:: tap.line.Result :members: .. autoclass:: tap.line.Plan :members: .. autoclass:: tap.line.Diagnostic :members: .. autoclass:: tap.line.Bail :members: .. autoclass:: tap.line.Version :members: .. autoclass:: tap.line.Unknown :members: tap_py-3.2.1/docs/contributing.rst0000644000000000000000000000344113615410400014124 0ustar00Contributing ============ tappy should be easy to contribute to. If anything is unclear about how to contribute, please submit an issue on GitHub so that we can fix it! How --- Fork tappy on `GitHub `_ and `submit a Pull Request `_ when you're ready. The goal of tappy is to be a TAP-compliant producer and consumer. If you want to work on an issue that is outside of the TAP spec, please write up an issue first, so we can discuss the change. Setup ----- tappy uses [uv](https://docs.astral.sh/uv/) for development. .. code-block:: console $ git clone git@github.com:python-tap/tappy.git $ cd tappy $ # Edit some files and run the tests. $ make test The commands above show how to get a tappy clone configured. If you've executed those commands and the test suite passes, you should be ready to develop. Guidelines ---------- 1. Code uses Ruff for formatting and linting. If you have `pre-commit`, you can add ruff hooks via `pre-commit install`. These hooks will run as part of CI. Changes will not be accepted unless CI passes. 2. Make sure your change works with unit tests. 3. Document your change in the ``docs/releases.rst`` file. 4. For first time contributors, please add your name to ``AUTHORS`` so you get attribution for you effort. This is also to recognize your claim to the copyright in the project. Release checklist ----------------- These are notes for my release process, so I don't have to remember all the steps. Other contributors are free to ignore this. 1. Update ``docs/releases.rst``. 2. Update version in ``pyproject.toml`` and ``tap/__init__.py``. 3. ``rm -rf dist && uv build`` 4. ``uv publish`` 5. ``git tag -a vX.X -m "Version X.X"`` 6. ``git push --tags`` tap_py-3.2.1/docs/highlighter.rst0000644000000000000000000000115713615410400013715 0ustar00TAP Syntax Highlighter for Pygments =================================== `Pygments `_ contains an extension for syntax highlighting of TAP files. Any project that uses Pygments, like `Sphinx `_, can take advantage of this feature. This highlighter was initially implemented in tappy. Since the highlighter was merged into the upstream Pygments project, tappy is no longer a requirement to get TAP syntax highlighting. Below is an example usage for Sphinx. .. code-block:: rst .. code-block:: tap 1..2 ok 1 - A passing test. not ok 2 - A failing test. tap_py-3.2.1/docs/index.rst0000644000000000000000000000326413615410400012527 0ustar00tappy - TAP tools for Python ============================ .. image:: images/tap.png tappy provides tools for working with the `Test Anything Protocol (TAP) `_ in Python. tappy generates TAP output from your ``unittest`` test cases. You can use the TAP output files with a tool like the `Jenkins TAP plugin `_ or any other TAP consumer. tappy also provides a ``tappy`` command line tool as a TAP consumer. This tool can read TAP files and display the results like a normal Python test runner. tappy provides other TAP consumers via Python APIs for programmatic access to TAP files. For the curious: tappy sounds like "happy." Installation ------------ tappy is available for download from `PyPI `_. tappy is currently supported on Python 3.7, 3.8, 3.9, 3.10, and PyPy. It is continuously tested on Linux, OS X, and Windows. .. code-block:: console $ pip install tap.py TAP version 13 brings support for YAML blocks for `YAML blocks `_ associated with test results. To work with version 13, install the optional dependencies. Learn more about YAML support in the :ref:`tap-version-13` section. .. code-block:: console $ pip install tap.py[yaml] Quickstart ---------- tappy can run like the built-in ``unittest`` discovery runner. .. code-block:: console $ python -m tap This should be enough to run a unittest-based test suite and output TAP to the console. Documentation ------------- .. toctree:: :maxdepth: 2 producers consumers highlighter contributing alternatives releases tap_py-3.2.1/docs/make.bat0000644000000000000000000001505313615410400012272 0ustar00@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :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. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over 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 goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\tappy.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\tappy.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end tap_py-3.2.1/docs/producers.rst0000644000000000000000000001740113615410400013424 0ustar00TAP Producers ============= tappy integrates with ``unittest`` based test cases to produce TAP output. The producers come in three varieties: support with only the standard library, support for `nose `_, and support for `pytest `_. * ``TAPTestRunner`` - This subclass of ``unittest.TextTestRunner`` provides all the functionality of ``TextTestRunner`` and generates TAP files or streams. * tappy for **nose** - tappy provides a plugin (simply called ``TAP``) for the **nose** testing tool. * tappy for **pytest** - tappy provides a plugin called ``tap`` for the **pytest** testing tool. * tappy as the test runner - tappy can run like ``python -m unittest``. Run your test suite with ``python -m tap``. By default, the producers will create one TAP file for each ``TestCase`` executed by the test suite. The files will use the name of the test case class with a ``.tap`` extension. For example: .. code-block:: python class TestFoo(unittest.TestCase): def test_identity(self): """Test numeric equality as an example.""" self.assertTrue(1 == 1) The class will create a file named ``TestFoo.tap`` containing the following. .. code-block:: tap # TAP results for TestFoo ok 1 - Test numeric equality as an example. 1..1 The producers also have streaming modes which bypass the default runner output and write TAP to the output stream instead of files. This is useful for piping output directly to tools that read TAP natively. .. code-block:: tap $ nosetests --with-tap --tap-stream tap.tests.test_parser # TAP results for TestParser ok 1 - test_after_hash_is_not_description (tap.tests.test_parser.TestParser) ok 2 - The parser extracts a bail out line. ok 3 - The parser extracts a diagnostic line. ok 4 - The TAP spec dictates that anything less than 13 is an error. ok 5 - test_finds_description (tap.tests.test_parser.TestParser) ok 6 - The parser extracts a not ok line. ok 7 - The parser extracts a test number. ok 8 - The parser extracts an ok line. ok 9 - The parser extracts a plan line. ok 10 - The parser extracts a plan line containing a SKIP. 1..10 .. image:: images/stream.gif Examples -------- The ``TAPTestRunner`` works like the ``TextTestRunner``. To use the runner, load test cases using the ``TestLoader`` and pass the tests to the run method. The sample below is the test runner used with tappy's own tests. .. literalinclude:: ../tap/tests/run.py :lines: 3- Running tappy with **nose** is as straightforward as enabling the plugin when calling ``nosetests``. .. code-block:: console $ nosetests --with-tap ............... ---------------------------------------------------------------------- Ran 15 tests in 0.020s OK The **pytest** plugin is automatically activated for **pytest** when tappy is installed. Because it is automatically activated, **pytest** users should specify an output style. .. code-block:: console $ py.test --tap-files =========================== test session starts ============================ platform linux2 -- Python 2.7.6 -- py-1.4.30 -- pytest-2.7.2 rootdir: /home/matt/tappy, inifile: plugins: tap.py collected 94 items tests/test_adapter.py ..... tests/test_directive.py ...... tests/test_line.py ...... tests/test_loader.py ...... tests/test_main.py . tests/test_nose_plugin.py ...... tests/test_parser.py ................ tests/test_pytest_plugin.py ......... tests/test_result.py ....... tests/test_rules.py ........ tests/test_runner.py ....... tests/test_tracker.py ................. ======================== 94 passed in 0.24 seconds ========================= The configuration options for each TAP tool are listed in the following sections. TAPTestRunner ------------- You can configure the ``TAPTestRunner`` from a set of class or instance methods. * ``set_stream`` - Enable streaming mode to send TAP output directly to the output stream. Use the ``set_stream`` instance method. .. code-block:: python runner = TAPTestRunner() runner.set_stream(True) * ``set_outdir`` - The ``TAPTestRunner`` gives the user the ability to set the output directory. Use the ``set_outdir`` class method. .. code-block:: python TAPTestRunner.set_outdir('/my/output/directory') * ``set_combined`` - TAP results can be directed into a single output file. Use the ``set_combined`` class method to store the results in ``testresults.tap``. .. code-block:: python TAPTestRunner.set_combined(True) * ``set_format`` - Use the ``set_format`` class method to change the format of result lines. ``{method_name}`` and ``{short_description}`` are available options. .. code-block:: python TAPTestRunner.set_format('{method_name}: {short_description}') * ``set_header`` - Turn off or on the test case header output. The default is ``True`` (ie, the header is displayed.) Use the ``set_header`` instance method. .. code-block:: python runner = TAPTestRunner() runner.set_header(False) nose TAP Plugin --------------- .. note:: To use this plugin, install it with ``pip install nose-tap``. The **nose** TAP plugin is configured from command line flags. * ``--with-tap`` - This flag is required to enable the plugin. * ``--tap-stream`` - Enable streaming mode to send TAP output directly to the output stream. * ``--tap-combined`` - Store test results in a single output file in ``testresults.tap``. * ``--tap-outdir`` - The **nose** TAP plugin also supports an optional output directory when you don't want to store the ``.tap`` files wherever you executed ``nosetests``. Use ``--tap-outdir`` followed by a directory path to store the files in a different place. The directory will be created if it does not exist. * ``--tap-format`` - Provide a different format for the result lines. ``{method_name}`` and ``{short_description}`` are available options. For example, ``'{method_name}: {short_description}'``. pytest TAP Plugin ----------------- .. note:: To use this plugin, install it with ``pip install pytest-tap``. The **pytest** TAP plugin is configured from command line flags. Since **pytest** automatically activates the TAP plugin, the plugin does nothing by default. Users must enable a TAP output mode (via ``--tap-stream|files|combined``) or the plugin will take no action. * ``--tap-stream`` - Enable streaming mode to send TAP output directly to the output stream. * ``--tap-files`` - Store test results in individual test files. One test file is created for each test case. * ``--tap-combined`` - Store test results in a single output file in ``testresults.tap``. * ``--tap-outdir`` - The **pytest** TAP plugin also supports an optional output directory when you don't want to store the ``.tap`` files wherever you executed ``py.test``. Use ``--tap-outdir`` followed by a directory path to store the files in a different place. The directory will be created if it does not exist. Python and TAP -------------- The TAP specification is open-ended on certain topics. This section clarifies how tappy interprets these topics. The specification indicates that a test line represents a "test point" without explicitly defining "test point." tappy assumes that each test line is **per test method**. TAP producers in other languages may output test lines **per assertion**, but the unit of work in the Python ecosystem is the test method (i.e. ``unittest``, nose, and pytest all report per method by default). tappy does not permit setting the plan. Instead, the plan is a count of the number of test methods executed. Python test runners execute all test methods in a suite, regardless of any errors encountered. Thus, the test method count should be an accurate measure for the plan. tap_py-3.2.1/docs/sample_tap.txt0000644000000000000000000000074213615410400013552 0ustar00TAP version 13 1..7 # This is a full sample TAP file. It should try all the functionality of TAP. ok 1 A passing test not ok A failing test ok 3 An unexpected success # TODO That was unexpected. not ok 4 An expected failure # TODO Because it is not done yet. ok 5 A skipped test # SKIP Because. Just because. not ok 6 A skipped test # SKIP Failing or not does not matter. Bail out! Something blew up. ok 7 This should not have happened because the test supposedly bailed out. tap_py-3.2.1/docs/tappy.1.rst0000644000000000000000000000255113615410400012712 0ustar00:orphan: tappy manual page ================= Synopsis -------- **tappy** [*options*] <*pathname*> [<*pathname*> ...] Description ----------- The :program:`tappy` command consumes the list of tap files given as *pathname* s and produces an output similar to what the regular text test-runner from python's :py:mod:`unittest` module would. If *pathname* points to a directory, :program:`tappy` will look in that directory for ``*.tap`` files to consume. If you have a tool that consumes the `unittest` regular output, but wish to use the TAP protocol to better integrate with other tools, you may use tappy to *replay* tests from .tap files, without having to actually run the tests again (which is much faster). It is also an example of how to use the tap consumer API provided by the :py:mod:`tap` module. .. warning:: :program:`tappy`'s output will differ from the standard :py:mod:`unittest` output. Indeed it cannot reproduce error and failure messages (e.g. stack traces, ...) that are not recorded in tap files. Options ------- -h, --help show a short description and option list and exit. -v, --verbose produce verbose output Author ------ The :program:`tappy` and the :py:mod:`tap` modules were written by Matt LAYMAN (https://github.com/python-tap/tappy). This manual page was written Nicolas CANIART, for the Debian project. tap_py-3.2.1/docs/_static/.keep0000644000000000000000000000000013615410400013222 0ustar00tap_py-3.2.1/docs/images/python-tap.png0000644000000000000000000006366413615410400014756 0ustar00PNG  IHDR瘬9&s\\Lفq'<ǭ>`d] Pu} m\c3d/0-Cx^d?G!ԯ{2?캑R.~$hp<( ,cH4J"`0 Iwl2.}0vNx≫0b2۟.(JF(\ O@xM}8 dIf?yHƊ$ff3 Q`GgM#?W^aϞ=LjX,6lج,UWrܹ|t0BmE"9ºu[TO?E ~O|[̛;&{99Պi֯(rͬ#GD"2}:3f孷7J4 ׿uXd ---=Pc,]k׮e{エ$ZP~ /~#<_g޼y=FjjjXhр-[0ͬ_3j*$pk]΋/w;v\A.S[[… qw}~^txGG]]].ޡ:̾tvDX,6(@8w}TWW˗ 6RVV[o 6`Z~ f\z^z^Iy1۷rJb7nebR)I\oUE,JX,U~dÆ \r?KiuQ]]ݯWty1(--geÆ X,l2GT/rwiӦA5H_b/BAt]ąM&>ps>M < I$!gefmƒ%K8$s6mĪUz4׾ڗ鏤_,Xaݺudee>WloA0`4z;2ko6xmdD"A41%gΜ1%?я}c֭,X85h# &L EJ(;@u..V^{\AxGy <3=￟:;;y'dff^7<hEQ|X,syyy ҦȨ=kjjѣ466b4{Cz+JYY,YV~\@X,l޼KꪫU#\9\zj.֬Yy=yuW$ER/,Zwyt:{ .PPPpɈIEiwLnn.>oflق((B<!דoYYyyl|I^~/Ņ&33s撖(aύ7 { Rv/0^z%z n]ׇğ!_l6sqA`ժU=gbRn^cݺuTVVyfER)sx ü=d2p^&d2R!{n?Oz4w޽{{Ջ}i[- seN+%zym̱cQqw\\A/^fd2qI^/-E>p}Kmd⣏>BUU.\޽{ٻw/K,a֭1nܸ5Ƌ/HUUUqL1 ;vGvi?CdժUy}=رKGns)m6`ޤ/vqn0X,>z^DQH$|cܸq޽]/yV_|ddić~أ}F#ϿDc\ TBS0"Z997s I^ TBs3sLVZEEE[nE!sĉKL@u]l6SWWwsz Aa2.9.I3bMx!@nV~?F*WrW2NT>rϐѰ:t. [n_oɓl۶mHЎ6oc=l۶,˽F`Ǐ_2S3/رc>|O"?R21 . &ohTB1 `kG+Wp\A.F"֭[W\AEEŰq/Z(5b44痊bXKqv^ /F>CA?)?0N7ė,Y2\SV_y_8zjDQd޽u ۔|!sUU4 ='JCS/7X,F8 D"4M뵬,) : .رc<Fl’%Kشiӈy?8i!␃9) WpժU5sri^{>p1Լiv (.. 1 NCCttt{&!}?Ȳ'|,̟?R-[F4:dP\]ܿY,D|CV)p477( dڵ:848#_eVXqb^H$p8@SOWMT'r\L N"F<,[Pl%AMīiPLOBRk&_TSSS1rs:,fkbAYLMM z/S)9哽68|؏O~5,t @zzzhJSMxW?>/TUeƌ̟?B$_3rr,D)3Pt56B"Bvh$D}C'IБ%Ԍt  V>Y.3#4?=ʧ&?Y8n孷ޢ}EQ7ɞ={XjU+ 5yyYPB:P%xES[O=ԐȒ$Ix^qdYFKF^j``= &?xq2o0cdL.@x1#5(FgDc1Nhigtl;%C3ۤ3gP-:h;kq?ٳJrLF6?3^'`Ҥ)qڵ}!I%f5LzOzzYF* ;uTTm۠` Eé{kx_ϸX2tr뭷R\\L"?>Ͻk]CQc BsGB x Œ8:s2Z쯑Px)Kny9ɠdPw ;Yxf38 `{)+ZCM~9?`otRٹs'2w0-0R$#NUmM;uDLrss9w5k.ݯ?̬Ѯbžhta"$D ( MBk)*P$4y6 7Նh6rvG''i(Xv irLgԨē#<̞=ƶmP_0TYT M0LŚ5kO?WWt?u5xJAM̸[[1!M%cC4T]BxTAImfdv~p"o@AΠF'U&4A$+Dž`QMm ӿ߶m&N(lٲe@t:Yz5.nu5_y7/ _CRi SkFMb /M q2HtH$b$1 ӎP"w0c  E %5d&!"]%/݌esя?K ]$K 3f g?[n͛79jV+WĊV1oY9t!,ix;cD$Lx'(!A#(HQe#`L^](.p4zqX3cd$ktJ+c%#P(+/8A';(+.`Zq.M̎eT{/e׻8q"˖-7f)z 7PTTDmM57|W\{]QFv jD#LZ<>*[ttD1IAB b L> ÜEQi>6=@,B  O_-DfF6FQ HBASdPHab ,2]&ZcS}|;~oYfxسgcdܹL:P(-xM< gHapMi hl3IWT􄎠 4 :j8A[{'N2 t(+I"!F‘$=8hD":QUnjD* s&&.`M|ӟg444,&/ UUdfs̽*p ;'k mK~RαJ{[u^dLbL/3ŕW  de;2./ {H34 t`G %T85u]y4'e3*f `b\Y>]Ab8$芄̰ǑncۇDC+%/FRM92(+!^;q=/ ]-WB-06L RDȂ%>]D"aT, 390 nQţ%ZtAj6SD: 4 Fs9j0K:ݾ.͙mX iN;ͭnv;.VB1 _8٨sw_^*.kAӍ`Lzh:ݧC;TZ`(ԎT)0E[g4,999Þϑj|VV֐窪ά϶mO~f,gtDNV'ES!kT&mz%e4{h$== & ѧ޵Yq7A&,jj.NCG Wf6Yaf+9WFjf.'33?IɢJcd IDAT?z0m}0 XI,'f[;yWXudff9|>\_NRK g;y̩S3u#VMC)DQ˩EH[e@Q]ϸFB123w?Ƶڎ28A;dD ju-L4M ;/SP}*Ս͞/lhi!9\,?fō+Wի[pe*/ zC#c pl7:|Ú)>b|??*D݌IQRA҉%%bmnt]E0\ jj:9ӌAEwv!T99̘^¸Ă!E0tfO/{sҘ墮,txpɜNl3>9JbJnf-gY4e%:YtX}z{_1(1^|5}VjM {tɁn0p*3 +?AG1. Ma$ M%QƯ5:/橧bC|20OUZZʔ)SAJM]z_躯Ubc49s&$56kWqL(tvt8}1ndNl _NVA.7pn?$f2ϴYӐ ISKin{7lr>ȒL">޽f7{vp :b("i:ׁf PhiqwSXd & 98uj'Ų˘9s&v8+~ J9{*\&MΦX,ƫ[[2w9FGE & ;?}_#RUq#Oc[v"L:Qٜ=SKkS#4 QU`Ta>~gO\Ë Li0#xC"Ӓa%b0LZֿ=g!$Iޡ-JW&cD%&P*p])(+@Ls<Ƿ0Ip '*4dӧ@(wil6FGLy=~6\N ,A{k'^\Ɩ#[_ebb+YstuvP<6͎ @1qf\5,\a%%8Sust`kimk ]g8Q,\3$kǙn% "2 clr[?9HKc0RFg$$[oz3$P(t C@l8Y&𕕕=WVV^4ӱl˔鳨;IGGқD{GLaa.9ٙԞ:,dk:)SF;] Ph&,5GzW.X('O<'m覦QAKt{<̘F]ORh rYt>^f/&c2A5FyyGO5:6 ]LG#`taPU-#qߤA+{'K3a/>_<!<| 4UFtFz1&$$GRZt ,ɻnm&Ù@WKUGPT\H{-9?$P]-n7oahLV+Wp\NBᤂꨚ+D<*H"4 D,mV| I)))!--~5peW1 FZԖ޿ [FX_ڧرȶ<" d2dȡ.[½o"Kr1=r>`_ަ @OryHhtJ81UTR{u5 }QLy6:"xڛllb#X29QEyyݒ衇zӉ㨉&I5`%% Q߰Q~'i HЄudfhmjqt= X~.ltƨ45GUtA@UPu'1[,&:b4b(spiy))&W>?qYMqZ 8Xj"j&;7__(!26w,IsXzױ&# D~}]U%!d͚5?3~ƌjݎ `2[H HNϏbY$-+3mج(4D29_{ 1*r@1:V!9/TNz~!7O*0 J8 Ȃ];TTU#='(N*jHL{O)g_eΕӸ|&QEt6ɤ.꠫hq~o~e~S&>W̓oA(;7Q>5 Mȏ>ZǬY"v;"{yJ %`H n}GNNNQǕmCn"QASH$ⴶʼnFL$5qK#\ut6vL.o.O&8ZmYYp߷젩hU$;s(A( &4.MHqIUl4aDH$t 8AFg~w+l4%1(fP$jL?DT'Nȃ>L@gg'>h/| ;;{H4J&L)ҳ^E$8'dF~U?u;/~yDbHCӴ>a$ xɡD"A(ʦN6a3{T\.ZDvv:9 drıdesE4bj\>'erDZXD#$):N%G4ӝa񸑜 ^~ ֮ٳg=X6t4X$":* FUU07>d2^`~ǿVUDIr>>lqyYl)(LYImm㦔Q8c!=#&Qttdj5 fTM'UXOss'MW۝٭?/:MRS4IhDShDl\qQQ1<T`ea/ -Á$ݙ3F$g_yv79U%KCI j繍/D:Igȍh6rճ۸j4 i"7kbSOG[;H]1DJv0H v:~~WP8Hw#'XbqYTGdg! "ɤ !0'NM`s'VKDQv0c,JH`sML2 SPs4HJzy|6M;f@E$ӑTLf뼛,8]Dp}\wRҝ#ҳQ%IUʯу'}|/QTTVe 8SBFNƼ ow׃;ZDsh0զa5Ɍ. nFJǖ0 .x\g-N\xjww7D'AuA.ƴc t EIJFebR{.N>1B{s#zKh4`9zu̜3goל8h8@0҅$ I0L8f\1jE\}.b*seyĕc B$ASF$a7DTh4F0fڌf:;; c2FL)2@goź,n0L&3g̙31L6^EB3\|t;'"$Yرyy[.+Hn1Pkf/nQLRy$f%44 ֕$g2|-^BF>A4r n+*.zr352v`Eqc|N]=}{?f,dDb ܲnƕqhi+_`$NP8q0~ië:]{ov'-a (*ZEjgj8uθtqX6-jQ [XC  Yv,?n%Mɓޓ9,߽0f@éʓ*Ec^ %}kj4FR4v/V3QջQ]IBS4|Zfκ#Fvihh4y=?2iV!I466ͽû;0c8,Nr-ƺ݊A8+NspAsu>߶NtUa=y$3득̘?0LO@'{?/|%Z;9:SS:f$ *c1tV7.GS0zwS8f$)U'$++QHIKalH$r׳rH9!u 4oe۶ Q16T,/ +PQQESuB{Ĺ+L9. ̛;v.Rl$R{x|嫫:{Ƶq"e7"#-1ƐŇ>b""hiM Ν9sMSP8_b%5tlF1[v* +).) hjl'))Yxä&_˹+l.<R$XTl+Gc$ <9k3*KJJzҥԩXplN}7&2}n 7ϛĖv`K$0gkdN/^/BE<|~ͧOpXɄ|qyT:[|AӇȘMF ? I, سƓnTQDId;|=’QLZ8[UǕZaNLo}y6$ RVVm<ݎ Vxwɓg#w|vT>945BsS=01#; Xv)Յ`L–UH(|V7#e3g0e 8yyQW]Gm]3MM< fF (ـ˨ I0͗h9xILH@tθL{%|+ "k]E/Q2L/dnV33e <}s_?:(berl浗ɿ{gĥ+d).-|g9>>S6,ľ}gQew@XUٶe7g!d%#' ˏd>a__dL ͞LBRoԩSHMRB|>*+"eĀwfqݜkqLE&Vk =?NU5> 4MCpڧ0Il'A˜#5JM@?ZkԭKjii3fzL2ɂGlo#7FVNetrؾsΜfڌь,)"3;;MCY&oxrqn3Qc[0vZ0{q{|'e+F5vdndL>a]'$tR}!pc,p3/%NC[PD@clZ4)tB]^럞( =HAq(C~d?u+C9Qٶm >u;-V=H홣 {)N_yNGv+m, &E BdMĜoFd1CdioLYL>9Uxo粒 I$C8t Vk2)ii H$TNXm~vf#GPQQA֌%w SPcO4؜yQ3Wݖ((((5jkG(෿}'xIYprARJž#51=\Ŝ;o Q%# 4B!?.A?dB!w-MA"I^˰ƎŒH nœLHNhj_HExq($Pu6}H["|!%:\T$Iu.r=>G7Ə椾Μ:{?ӦDKK n uLu{)ͱ1( UyZ$ɺ_00t]Ge a2wX,"&ƌ)p~zZ[["m7D>B%G ֭vs]w|?Ey ߿ GG8~^-~ڜn?~&i4#}~~7Z(z:{]~m:: ;=4^iJ/)hibINlRWUǩCr}SJ]]k֬0")i]W   .]⭷bɒ%9v╗^ b4o,fp;ivn-TU&.j hj]WQ54T% 4Tt4B ANXёdVM4\J!_Y2>زeK3f%Qgƈ܎+n#ќsMStQ&LC(I㨛 "o6PQ5%+7C}NtkǕ+Wx7?>ӧO矟,g7HϜ)Q&3u2QB6(^"XhzQA#5U ua-EDt4|~ UP)*.ΞBs]gxh\w>E4$'ʌNp)2k6MS{=]6% ]7.k=4M% aݘ0YcZHPda0~:k%x ?fzo~m/^"\"JKKˋ}6^{Guay6o!&dB8#9@Ʉd$94DIF  Ua!F (]Zs`= 1o~0qrX%.DZ5] ڃHU~0iKt0BӰڒ0h@*m:kOQ-^' % 1MX3lIsL#ᡃ[o=Jyk6n 1 $Ib…=Ûo'>40d#KC݈,##C$tT5L0 hwpuiil΋-#`Lɸ>ׯSy&f# @nl^ fsMޙ{)D5WElr+24b49qk׮fpAt]sf͚AU !z Ȭ,ʘ1c3f̢֭[&*vA+91{j.YX-vlV+D `OGG.WOnNn.;yˍ8v. QT#ҡȈB4/CMA|h]Ը^cc#> / #CD]g::fs>$Yfa0?>-c03 ߿ӉСCɷc2~xrssYr+W"s Vɓ\+@E1$%&lO8;i?I))E/\9<'?2'":'Mӻm Bn]HnWf_5^. g#S+㩕ڮ]{ю@-[tRî]0L1{>G[[?y5k/ EF,>|8999&j4Y1MB$(M}}=\t Ӊ=ԇT"=1纙fi)pM0<(}v1I?']!(w9Obgu5.]֭[Yf +WHw@]ug?V}}=޽Jjj*Lbb"fY@ 륽6N'mmm|>4MCW,WhVE끼V` %4D_gMSQ./,B!kP\N Gk[>E˖-Cu/_֭[5kgn _!B z\..^؇93mܒ$)6^$D. dNjj^TU1@4 / z+$Y{*,}ǖ-[={67n$99~?ݻ1vȟvTz(Df,F"0{+ω#4UA4U#&`61i 7 ԌR!I+V //Kyxg~Fff&_H/zD7zeӫJdki0:ML~ԁaflƄ&,fep`D7=Yw9Cƃ>H~~>w_`<쳼i&233ԁj@AaF Kj#2"{w{-Zf؟+/brrrBUUF#7nAHIIaɒ%n6l)S$ȲH/1 H(0 qlV,bt;'ViDȯx!՗{Jo{ 0֭[ǪU 6,fΞ=ՑEff&bXtr=a8NL&yyy3gرvl6yyy3l6L4o<Ο?Ogg'yyyA~?yyyF#-c#96>*a $o&B)xWU9M/;+wrn\>[fӘZ`zmY-2$bSFéACs@݁i&&MyGXn=[oeܹ=Ļ(~6mD[[lٲO(q].@ذa۶m$^+>|8x^c cjhƤI8}4;w?TTTp 7`4 B];Ns'6Lcÿ=cĿ>^s3}[G;xǟ-c$ ~[v`WKd`w,SNeƍ>}zP`0;CBBgϦnFl6[LUUnSZZ$I1@FFFt_'|‚ EIPIK2 l4LMz B KˌnBD(i] NVNVjjzmұ%`%*0"_͂0cL_-cXdIL|^͛{!1-ZDcc#>,/aAQTTO?͙3gHJJ@Q._L[[[JUUE۷ɍ7ވkŋ)++]'=ʋD%Q(XA'/-) Z>O/3$?zU DsţQpFToIrrzhk/l^|իWseL&gϞ:V^%11JjjjzFZZZXz5NMXz5{/֔C1R,$a@`z4(6R~,tm }{A׍F2tt>Vfر}[Wc#jctwWx#Jyxٷoǎd2i~Ƙ5ӧc<餥%3V,pBÐe4S TWWх e$E"AG%i?) yHnF61y>U`&, )M0ٳX,,[Obij1_d޽|駱MhR@w5/m[1FI2UUci(#Ij[1& ߄UU1 $~Zuc6눪j(?ަ FZ]ډsBk^  9B*K.? u6FCCCƈ#q-9#3gn#ΝiӦ!2pcǎt:7oٜ8qĉcL2޽{1 ̙3Ξ=ɓ';v,eee8NCii)YYYڵtg8`t 7zݯen3i!I~0fc|qm~;_4"ӦMbr bJur"+܏;ۻlw2iwueǹ789#V$/2yd6m/%U%Qc͚5<}M2*((ot][oʘ-5xjw-j[DyRFA=A?z{W20 hI&6Eq\eSb݊{>6FwkժU=Ͽ60*>+W ݻÇ3vXdYѦw;M4LxnwsϊjԙhsqҮ& (zJX!C Z] Ӭ8,z#rpEM~ #3\0 2I*nvo=dddpM… hhhIHMMdŋIOO$$$0}t/^Lrr2h"2228y$ xqttq@aHkk: m6 V'ӦM㞻&~zΝW>LĶOo|Ap8iJ_:~~Cf+ޢ7%_e%޽;4vvISS~Z*++inn&;; lܹ:Xz5GCłV5f9x tvv"2~G Ja') =jp l:RC+WqߣxwcLsIDATs W]AV{}<ÿ*?}7=ÞŀEM2?C'#b$ sTU6S$я~t]`065;a0bjTQ# rMp,.}?kDD{.J+H8p ǠDM?V8JxMw]@ `HIIСC}lܸW^yg}MMCm2fBo;țoO<(1s߾},ZcvرccRe}aGDcqzHVSH ! ĉ7[oʂ ظq#/ذa*9~c AE*++y뭷馛 ?oIy駩iiѓ'cZ#Wǣ*J gs2cm +*ƜʈY\k^ m_*Z$/k&zG B `Y4y I2_o_sўurr&חV)$u|?>2hX4#<RT'nn@gTE)4zRyŒ;tݟ \|`=O\8E :Q=24V sE]G߁rt;)BJHm<͖sreOX'1B?rF-wӪlx]_=:9Bt^ڜMQpra0:䍟>i'蝤PF8Hh]6W-Z5T?4`橳[Aw7?ާF?E8O0QgaP^[ȓ_CYQ_L|'Z?~?j|M,v`zuGePF>mgq`rZ1~ߔo Ҥ݂k\C?Z=ٺn!+k5pǠAuݝn1o pm-={ubj'[Q}mW"78%8B_7|\yS0R%zg37Eɂ%y!<\_3Óꉺ&t?qZ"Tl(ƙ κӭ]ΘDnJb+c]鲧#l_w￀dZ? QQz+}T7  nt]E4Lf1o ڪ:ͩNkKs!LiQmk2cԙhLt ]+E0M'JO\Ew TWpW۹yܐU! C&ڲ`0!&}qPOB$("(   *`4 ^tKb\к;‘=$@ ILY~#'OqJW&b\ϰ;=n5p*ZBZBVZAh7, &3%/ # #) - ( '# 5 ;=5 )#9&'3#"#,--)()4+('-7+36/5;856:3/1-IQO q J:T-b$f* s. r#k9}>m5D4.Z>.O5,n:!c4 n-4S'&U$iFBE5^Kd(*`V7 !q@lv̭:YĪNlcL *C<;UA!IQl66N qĚ.nP`/뫛Mɣ~0ںWSow܀h TAR_ALIqOP C 2Ց|a6v+4ځPp<>%-A5C)ȼtA K<3~8 8I !Mb_TɲY6u\hB"Λ߈368\sZz 1z@yYsnhM7,!@,dZaҰAjM1|q}T wa+{6PBG Uk`.@Gf1A8@3$c3D 0B0@20Đ& (jL" e 2dp GXfv A8s %rM6w~shs7o*Ÿ/)s8$%^7HÍ4ӂ6@ 0qp lI @ c5+zX{*uTiDwxju6eGP;ֲ1ae؉7^R`B!P# (HP8IbԸ%rCA3\i.&1D1l(|!,(|"d]PapCģ7 mnZ\v\A zaDZ }'@h,*z0=L _`@jxÊliӊ6VdS"@ U6@ d 2k4$81"vH n JG7&f&<,]nń`J(RM/Fz 28L G`o ZQOv, 6Q![K'x y %A: ˍqCERy^h # XVx0^|N4ɠ!B`!N6HԆ3d#w2xE.|\47褐3 {s!*~`Uj</(²0DH ]A Z\yW囖`#-QZK@0!z`X!:PA`N2GC 蘃4|0!- +̭C 0F>~eC /W`fc)"^} śMHa.;'R;3r7FQ ϧ2N hc`ւ E#ȘZv؁p ?T( Vq_W 6 +_mo\C(T65T ,K 'ՀGw (c3{x,&a$ əDt IeHWk$YI3Ȑ!qAЋI]`7h>F{KQXmU+BaZz]ȭ}|SpA &dĶnI ;a= ܇rBFԁ*ɢ_5"AD]CSBlt Qxpt !G* ^)(>adΖtcX aR .6JWR d("mDLKGBeG&^]^ Z([ ^; >*'*O=cxnD I@nA|/I$p(OW0gN8Ϲw@ЇNHOҗ;PԧN[XϺַ{`NhOpNxϻOO;񐏼'O[ϼ7{GOқOWֻgOϽwOO;ЏO[Ͼ{OOOϿ8Xx ؀8Xx؁ "8$X&x(*,؂.0284X6x8:<؃>@B8DXFxHJL؄NPR8TXVxXZ\؅^`b8dXfxhjl؆npr8tXvxxz|؇~8Xx؈8Xx؉8Xx؊8Xx؋8XxȘʸ،8Xxؘڸ؍8Xx蘎긎؎8Xx؏9Yy ِ9Yyّ "9$Y&y(*,ْ.0294Y6y8:<ٓ>@B9DYFyHJLٔNPR9TYVyXZ\ٕ^`b9dYfyhjlٖnpr9tYvyxz|ٗ~9Yy٘9Ys  pəo )Inimɚ隱 l)Iikɛj )iIiɉhɜg  !.!matt@eden: ~/tappy,!!matt@eden: ~/tappy,o8p *DP!AhP@-b@č(;D 1v4lTy))L lϠ;)<*hM J q)ԠC1ZPV!!matt@eden: ~/tappy,!x8p *DP!AhP@-bgF ۗ GjE0w:PR"oI<(]nOAUAH&:7t0>珔I G!!matt@eden: ~/tappy,*x8p *DP!AhP@-bgF~OTZ$$PRTLэsHEؽO'?2OL(a"ͽ?O Pϟ?sNb*8aY!!matt@eden: ~/tappy,2 eH"paC.4@w1 Z욬XsR(`9LwCl`y8"`+_# .\X}h08ubD!*!matt@eden: ~/tappy,;-  L*D‡]xHqaĉ^F9RQ̼gI~ۗOT,y2̚`0ݐ ŮɊ5.H I^-z4Ԫ+xQ"B)|˓*X4{̓~}Nэ \?ĊI^h鮿*vGBS |:CTd A-j( 7PQbEy@r)e;CkU$Ex#9!ϧ_!a!matt@eden: ~/tappy,h !!matt@eden: ~/tappy,r EKˁPB9TGA7A.lpdÇ$A2O@ Pw !!matt@eden: ~/tappy,{D@o9$%Ev9I؁ ~oߠPIޠ%Pmf4鏄0{!"!matt@eden: ~/tappy, U Xo߾Q8KT,S y pETPG["(󈑙')y*sL@]k'xyF߾Oqt7(#$П 5?! !matt@eden: ~/tappy,~ Hp *‚ >QĊ'RĨ!G~ۗOT.ȊI X@/Sb'h"QN4ӡ4JVHsOUH3hI>ո"ƀ!!matt@eden: ~/tappy,{Hྃ ,X0Å :L1D⾅]xHqeKE<~` ! H=)1((RoG҄  ƠҠG(ǝ],@z;F' Zn); M(1hήBn ! !matt@eden: ~/tappy,Hྃ ,X0Å :L1D ^WF%n @w1!$-vMVGGR )'7G)q7^)Ɇ# 7Ny>R4> "ۛo7!!matt@eden: ~/tappy,Hྃ ,X0Å :L1D ^WF%n=DE[났hˍ2UF @kF"ާ;N'E(Y"ͽ?V'bUϟ?sN }r-ŶQn !!matt@eden: ~/tappy,{Hྃ ,X0Å :L1D⾅]xHqeKE<~` ! H=)1((RoG҄ 8,/{! !matt@eden: ~/tappy,  EIoߥ h" &Pv0!!^ &;>MB̓F5rc3! !matt@eden: ~/tappy, E`>sL ŮɊ5BχB<oG6ҠdR(_5ͅ=!>!matt@eden: ~/tappy, L޾|(pk].( שJ7Z;G7 Pa>a8rsN !!nosetests tap.tests.test_rules,!!nosetests tap.tests.test_rules,\ySg]!!nosetests tap.tests.test_rules, \ySg]!!nosetests tap.tests.test_rules,< H`AWT(( ! l`D1rxP8z) <@01r'C. JѣH*%SKf|JUUT^ݚV]~v԰FEv)۶I=*wnѺvt/ߖ~b , #!!matt@eden: ~/tappy,%uEH*\ȰÇ#JHŋ3jȱǏ CIɓ Dɲ˗0cʜI͛8_,ϟ@ Jя; ]ʴӧPJtԫXjʵ+̪KٳG]˶۷p!Kݻx˷߿50pNXx1B"l< @BȒRcRVT:T&Y$Qzb%Sc.eJ٥HaPKRM8*jUj(B*:jc@A.ʩ,\z*PJꨦv¦jBjZ 몱j믭Z+lꪲIJ):m2{+ km,K-ۖjKknvn`R% ߵWG,Q=LgqLo LQ"l,0,4l8<@C j4tE=P34# uA5vmuNMFȲ"]$3 Y/veq9CrJmݵ o;3QسO>PoA9^BZj,OA3l35ȟѿPP'zEq;|˝M&yDXJ2N,48!`14-ф- {C% )x4C~Q2B-0ٝd,k sc 8&'qKq B P!CH}<^Q0EҤ2I@2xKZItHZܢAv[(ڟ SDZd:">hnJcT.}Nm!`678M+8w3 %-!F0v+Ja;XhC ˈ2 $ikzc"@r DB @ U˰ `p} ¸'`U5m@ "!ddbLILAv@ FQѲFl@3gvle`N hB:Qa^V_EKN" gp7TA#!2Áf$ ),`jڥI s0*łQ| h\U J` Ҁ\#-n"1BtGϗ>!(C`zƙ4m1X@"wK]}9\.UGUBKjΘ> 7vB{ܻ_ 9枇k[? 0&R}料蹣=fÎ ZxL*[Xβ.{`L2hN6pL:xγ>πMBЈNF;ѐ'MJ[Ҙδ7N{ XC9YԩMv;Y3S-Sr~}E]I@ӷO}S`I#CcABHr8 86d8\sDW*Z1'bI8?`BԢ 7f @,DrCRG&4KuK7HX%fw{QGW%t% `| ^&lQgf~D0PdF;H߶#.ep#BLr,. cy^&9 XiT' "І4ʧ@=Hi w!H`G5D-G8^ĊI+HlP* XȆ8 B@7?87HʡB@;yS,шХ:Ԡ! @ tpt\ 6i؉sl' mѕ)zPU c>AbWQ1jpDB,Bq#q A<3Ct|7! '@YH@E%)dU"Liq1`% !}' v`IbtkPКD=HGA{ OA sQ2P!NaG^|h2i;nz%fc*0sx/a81GLA\8nȖUUNȌ 8T#H lYd#~ܜ %1u)C䦙, S Pv.ehNh!\Ca#AATjhmcW1#$id7@D!wUIE꠪E-dlE9:* - aOhc8 8 NfS|'?ة #mT I%4Q;@ (p!K)*yQQ9" SuH:ز-ῂF!!&kxA<TaH2ap@!֡2HVaC>a`5k, ]4!>FEJ9%bn35)=QZ$$:UESst#X?S F i>}!!matt@eden: ~/tappy,2m| Hp *‚ >QĊ'RĨ!G̼gIAbd~>PF' y̽x"ݐH8zypB@|bpsiF" u_Gf5=1 !;!matt@eden: ~/tappy,;m7H*\Ȱᾇ! ,Xŋ#jHq Ə 5FɌ"NLBœ  r.ryK4S̹OkPM7/:<ԟV^* tC48&+ֺ&)\$mܹ}h1؟R%8Qyy* ) q"=B.FFSM E+]ATOap "'t+\.Ls5'Vypw3@E w4$?OvCPDm5ͅh( i셙?g_8`H ;cTN>BCTjPUXv>2XC8FӍJDэ8fWG<¤T@!!matt@eden: ~/tappy,hmHHྃ ,X0Å :L1D ^WF%npdHIjR$˓Oɐ)my3ǀ!!matt@eden: ~/tappy,rmO8p *DP!AhP@-bgƎ5b(`G _;)) A)gN Fz0 !/!matt@eden: ~/tappy,{mO8p *DP!AhP@-bgƎ5b(`G _;)) A)gN Fz0 ! !matt@eden: ~/tappy,mHྃ ,X0Å :L1D ^WF%nS"(V(GTQxnu>}ʔ T"ȊA)P-!%XI}>mi /f 0Ƌ 1 ! !matt@eden: ~/tappy,mu8p *DP!AhPzR'P־E&R(RH `KI& sx3ˡ>XΆJikґ;՜BH4l̀!!matt@eden: ~/tappy,m{Hྃ ,X0Å :L1D⾅]xHqeKE<~` ! H=)1((RoG҄ QĊ'RĨ!F 3h H[cE_rx05A3# +^xt@ )ҟA ;TcQ3TB.$ aHjaSg/b !!matt@eden: ~/tappy,mHྃ ,X0Å :L1D ^WF%n'@W<Fy_ebhN^^ @C3O77S-UA/ H SѣI'URe@?N]F|?: @;x!_E<$qdٝu/!!matt@eden: ~/tappy,m_Hྃ ,X0Å :L1D ^WF%npcH 0e#˟?w:`: t&TIIjT(Ƣ#O~ !!matt@eden: ~/tappy, mx8p *DP!AhP@-bgF~OTZ$$PRTLэsHEؽO'?2OL(a"ͽ?O Pϟ?sNb*8aY!!matt@eden: ~/tappy,m{Hྃ ,X0Å :L1D⾅]xHqeKE<~` ! H=)1((RoG҄ 'P UX!A*9T:R2TBJM a3CՐd)}c@!&!matt@eden: ~/tappy,7mHྃ ,X0Å :L1D ^WF?*f\a'$ L@̺aY#cfeggUUtcϟ05sB֭V}^ȺB̌7vl-!!matt@eden: ~/tappy,Am H*\ȰÇ#& !!matt@eden: ~/tappy,Jq EKˁPB9TGA7A.lpdÇ$A2O@ Pw !!matt@eden: ~/tappy,StD@o9$%Ev9I؁ ~oߠPIޠ%Pmf4鏄0{!!matt@eden: ~/tappy,[t U Xo߾Q8KT,S y pETPG["(󈑙')y*sL@]k'xyF߾Oqt7(#$П 5?!!matt@eden: ~/tappy,t L޾|(pk].( שJ7Z;G7 Pa>a8rsN ! !matt@eden: ~/tappy,mw Hp *‚ >Q. @8 #v!j 1ϟc iJ=ȲeNrHΐU" +Þ!"!matt@eden: ~/tappy,mHྃ ,X0Å :L1D ^WF%n `/{#Y" eǜ@kI "';N'(Y"ͽ?VbUϟ?sN r-ƐQ~ !!matt@eden: ~/tappy,mWHྃ ,X0Å :L1D ^WF%npdHIjR$˓ ^ySa9 ƠҠG(ǝ],@z;F' Zn); M(1hήBn !!matt@eden: ~/tappy,mHྃ ,X0Å :L1D ^WF%n @w1!$-vMVGGR )'7G)q7^)Ɇ# 7Ny>R4> "ۛo7!!matt@eden: ~/tappy,mHྃ ,X0Å :L1D ^WF%n=DE[났hˍ2UF @kF"ާ;N'E(Y"ͽ?V'bUϟ?sN }r-ŶQn !!matt@eden: ~/tappy,m{Hྃ ,X0Å :L1D⾅]xHqeKE<~` ! H=)1((RoG҄ Q .H I (u@sF`SB@%,V53Db@!!6nosetests --with-tap --tap-stream tap.tests.test_rules,-!!6nosetests --with-tap --tap-stream tap.tests.test_rules,  H*\Ȑy- PP  _Q320EFj;8D8 P}j&ȥ/߾sOj\ZP(%]_~F_匸j. -b>Q4(ꥩLŬ Z 4F5ʀ@hM!_R*_1L 9 U^؅,l[c|0+v :a(Wc㷯] ̼gI {SADCL$TmUDx D1]STP AmwE$ OD!`}cq&p@70j [pi 8CHs Q1#@@ ;M? - IL!E"'?)%c3`:^c -40 $8See0g`Npa {>4ב^SNzOҙBtLˀ,(P,HfmQ,dh2KÚz=p*ô.NJ?Y^9by+ Rrw#GW(Ef܅2}4= ŌQ\r2JJ<>$$cmKEՌXC "uiI%W-AH2Qu(_>&긢GW&?ڍ訦Qxb 饏ZS I(qH*h=ƎΎb)c0<3<)kB!zkGczZ, `;ML;:p4K00:C4N+qxܲN+˯Y-|pITH *p@ #/.,F 3pv  ( ZX 8*0%B CX+D[6PV04K;J Yn`S0G /vy}057Lm@%<$RRxe>0: 77 ϒ\OK 8 O t4Q^C$hRCI}Ķz?FCLE=;,4D ))*0敄.#59IAD AlKk^q0 ٠ @I0?qL P}\_ gN|x_Nj2}Bz'+]'#ZP"Xgl 񇵊-=}9\ D-`@p UD hQ :"*h$טB҂F;q  S  @ GBFH_H0 #qB 58Q^~ÕKԐgLe"tyDX=>,H1lIʜ)ҕ!*؁V.u@ aAd "B RWTz8&hL,AMh JU}6Sx=m+Yҳ")'lI7ڠy_wu]\:z %FRʏ>S! UOΐ+Ff+[ۘ6/!MiK v{*KS*ʃo=Ѐ BHz[͕fG@kHOJKӠ&#{;57gN󚳼e:k6r>`kLgha5uݔbÙ!|L>-̃Hmю/Y#Ng$"XGѱlkp/{uuʀA[.bvw.`VH)*3VEmj7/шR MjH3X\HƓqaXIta 1F IM%9$ uqcE 9r>G{Ѯ]Ow)otsZ $h:#q<w Swk~ZɼA8GAkc;d.wehtO_%=║&Џ ' >O@ _}+wz></ʠ@"A?0$G!2"Qd+u~(025W|( 8H+!!6nosetests --with-tap --tap-stream tap.tests.test_rules,Z 8P.C*\ȰÇ#JHŋ3j6:HȎ(S\ɲ˗8 1E a^LBG>u (E޾}rgϡ!RPU4N[%נtؔcYRMt׍ERhU J0}phU]]P@2na](+FQt YJ1Gc[5!!' Cpr ؆6]?T`-Vebɋ,S}UF4!yʇ^b!ǘ p @F MdB̬3C6aESГ-=$LR\Qȶ" `uRRQB _SW҉CB@ I?l uP_ [6&CDN O92@)ې? `j(2SF/fXkG )#c>ںpO(L" wRrT% cA2c$aAӎ OzL^v+շɸ&k$N/ӎ+b!$l3D @g4G:AB"lor"H *p@Bǰ Gw/3q ]tӷ,s8:dS(1d;! &U s0) )JY;P8\)6 lx΃,Yð/TYU3P;-/mw~.:3-r)̌#샓r4z3&Lg5CI_34ޟ+%*DG+?\<;P fOT[,Cֵ+=EDBd!{B2j."'H> 9Sf!`a OԌ PB`(ÁEk?h̝Y@^ v({ 5 _?҂@y C p3ЃI!E P8PO<ᯩ<$*P mD6%" H(+! yCćӟ̆D`,491L # IGC1\')2-\d#ep m+-T^s#j(sؘBF;q ygy lB  R(4͌R8SEPF+fR: F-JR3t?М pZP!uCAIS(6IP5ܦM ӌ8M#xO"%%|455TAOjجVl#q>Z2pו 69E41(]ȣC&@ '0S]~mvÅ'ekH(.]Km#JC!P)T^smAٖBEjYkQ.#بbV !(҅SNMj[ǔ0lak6wCi2N'Ib?GV ppDO7zɻO.bOp҃j@.Pa }>H  (CF@¤ M%!HyQ (Cb˦x ,ʋPDScY.C # `F5p(rԠCbfv-Ih[`9"4K/&ϊmΡ7>ח}~|( ~! N |XzHʧ̇{ 療12S|W{$(v—'ZA}.G: (wG>9X*؃~d) 5(0(A5Q6C Xu<՛?nO\$WioޙOpc>`:$/`t;=BPU^zag>aBi(b AK%h g?yn@fgP tO9PJI)q%lI-a>T,##XTזY֘6敷 qѶ:4؎+&rg>#L⡏7ñmγXo[n"wm~z_ǘEnNq*X .jpL`Ur;!wq.3^4a0TfD" K׀e ¸'IZw$pHg g~fq<tkg.D 2Zl>FYLέe 8;wD{ϝ4 bϤqi,5}gaO1] _2 e5IlG3u+a vV`Qq1OCE]+!D2covI:=okrrŧIFT5MO$ *1ә`RH V>'G3h)ü55E!$Pht ԨK$}4ȩ*Q;AUvG\ ut>Ld?ǧd~K/-p#=c=Qb(7}EF`@wd׭GkՃH#IۍՂX>+$JdH]m(;+@;$E(Iڃܼ]JA@GB$F"IPA}ֽ2Գ[SJ@оWEA4K:VeVdcd5.G(@sBR!VaVW}C61mPUQW2!OV O z}$8%vP[OH:cU9&[{TX ^4 WL.Ťl!e=GzOėpRFQ c0MRb\S .L=exJ5EǜAY>z[dɃ-)+v&U<`eDB`[FA]&]mҽÂj6`S&;cBU,:&fN`α1 g dn[>R&'P].$5Ydŏ!g\R@&_Fjb:0-pݷfmiWl;˖\UˠoԒ^酶:7mql&GO. ~ a1SoF@:&iז8rpX@MB^q9qfmP+I8rtoz5Ur:kqٍqr,EgZƴ#KtU\y_Q^ lKx#F:9x{{ e!vx~@/tnMn_j̳]m|8uP(Na 29bJ9+(wje&˰vn .!U{^ ~OuGuʧ/tRk ߵاڽOtF!g Pa#DF1"g8DRJ-]3[hN0X]- e@>r#Zxᦂ?hX!=*dK;H$'*!)jծ- d$-`@nuEرEQFV‚&J0-wL\Ev=(P59o^5GCC#bRaӵs[o ffqΌTݻ~N^Tó}6E+UZz+?P{e!ԫ&6o1[ܬZzv(|@nFp ,ˆ6x@xgYl{|( "%88BTb3 %3L1$s5ه(JE(ҀU"p騤 tBBVF+rݩ*VfG!VRfc],pR5, a]Myxa:ƙJd Xy8uOJ`f})Tۍd;7e@yOfm߆;n箈Rr]# Û Ÿ´oG\JP11LA=; Q&] (hƛhG<걎X+2ǸRq!:#>؅Iy>! A}tJHS\ьdVB\i[%-HD| OB*C%ۂald'4` ]#G+u1ɸ%@  XR^FWG!jn*4/G' z\ :Ѥ&d\irMݒQd!0Q@RAØ.jZ&t*ibJhb+rC(lUSZ+>(&4j(SB!J|.՟jTDj5|Ԙ4DJ/aN V(fD`R:YŧRIdm:QȕGXZ#4cфմ({/⑏Z^Jfq:Gϧ(\% ">_<";,gZ:\{(շe8f.B9<8>T6n)NRA$:ؙvv]$^ϻحe:ܷSD8@-J`P}@7eJ:3B>as#))"ӏ3x/DZKԩӋ D'/tWS}bl~yP9d/>U`NX=Nzg~9G^]#3֣[h? ?$VT5Rڿ;@gJ@C?ӫeS3ۯDv88 |[^:;* pAy@3 C39Q AKS4 %4j[{.O @3ؘ+e=Ö(l70'Tp 5lC8L 9:4;B6\!ÏC6̐W878A>c+8$v?>#6M(Pk[\0S3RUSq=Y.*3@pKZB]@⧣%Mb!F_H1fXbȎu>o` &ɫM #Zw8x2/♋b؛91[VJ8Hēۻs 0cYF=D'^ӻ J'j9CE&S68ZQ:#M[:+M;"cA檨CVWUduB&`ֺCI;LfNU c֊`f3dk&D$V9:uP͕5YmbX[dC@wK=D~g >~ƐAoӈdh?e5{l4RQT^U:vb>[:>AJ @J=&KЛ`p.V@bg? 闦;w@sekUC<\dx$Da{)>E8K`+>PKQ̐Kc :H.n^,cllfBt܍y/܈:f3fQˆPCH mElʚlDמGlm@$씈C\lJL!{6XZ nR4 :dn_2HVJC*̖T~YE':.]@-f h|  YH(U4 oŀhGT(sliu t ieFN4ǟ܏wHlRKq iGL^I}TH~c ^ c%p ) +yr#|h5]QmBT siM܄\Q}1p Zt~3`hsLaIșFKMm+$D9'LOQļˠQtHW܌M=ߘJi,Y|mV~PM{z'[5b8ŁP9S %6'%Q[7GWgwy=gњzysj!rg2.M%WST0i$o,Ǔ3%z{) ih (l:3`eT^%ӧPOwk@_]VV-G#Ig$&LͤTB{2ɗ[޹X-O$ç废i{ῶ10ո_}Q|C}$Vԏև&!&@mȂvVv'\iOv 5'7JcrmׁYYU *+׭Q,TTvg[]9`E!T8+h`.,8B޾}+}((̻}PDhP-Sn"#E4xGh`˗cV)d '*?.U)ԟ9<ǐ#Zm4T%Mj.c@4KD04@>$x#dawVAYk&ؤKBvh7eܺw]$_=QֺƈgӮm6ܺw-;'/1W/ ˽{` !  &+ֺ +Z$1Pz[Ǯ{G*PBB0L;9T$>$3Prᄞz칇B0P2 -⎁ ̰CCm ,#G $L(,oᩰ=>4w]T_| 띘"1hZ1y_wAGy^ .$ږǎzfF|\z^*qD&{hƩl8c?xҐB9z9Dg,dKxY&~"c?饁hŨ2PD i$ũ>Q+ ;,:A 8IoƸ 0G:$=V bJ?qm]nA0[WՎ'cDC{~ov ͽ {m|bf ?*B‹P ?AbұnGzbnIbm뾬14A>&/B" HQ1±Y݆¬#Ԭ@./>*N+TL4B (du}7޿G"B2mU#.mbFT\aFFB48rCIFAH#/P?,z_ArZ-d%Ġ:/,p`)VdyAO?(mCyDSn9Es^P93Rg~̋$ڤ 8G?ץR?'^CitTB>` S¼[ދ$~sbB[ ¸'F _씁 @NpIFD" .! VXqEC @@~GO!jdO&qvH1AJ_Eڈd& -1]3´1caK3x'%ɨJnr@ TFY|GP @piV(iR37Sbgf-lHmXE!`'ޫ ;rʂKqcX!yHzgک)D_%Wag.qI2ϡp#:kEB rMD `p7G:s8yԥ) 7P!.WLNb2f檦yZB'DІnXC`Fs8Tw #G8P49u>9(`έD*r&\*׊"8Θ a7h\t J` Ҁ 5,`y>, 4xs }ld#!eLYo`NnecHVM -фT-h6X%ɨzm뤮nAD }Z)O\edE}ºec+ۄ7,lar[XZ6cW[%QL*R`7v9^E9($Ly"ep{;Xö+\d%@ LAfn&C&/Q4$5F$N֖qy^&gA#6=~&r@暜y6r3S1n^=; wpcmB ~0Hh:1 3)`D,g: 1G`qiT(GeAh΅;|}e9$HȜgO[ss"~4L >ܫ;ț:&$bf7<2rl4͚Aܪ bC> .83.Mi>/}:ֿ>k>ӯk?Ox˿& !!matt@eden: ~/tappy,3$$8П*TH‡6$"É-j40@Jyv#;<ǒ) 6dFXs Di B$΄xof>mݏMj(zsje pTU0ѩnb:6S7\yS!!}>Uc(-JqgJ"Y)mBrcsѥq!A^({ [fnݽnpŏ+O9sɏ?}t_מ}v݁'>y!!matt@eden: ~/tappy,E 8П*\ȰÇ#JHŋ3jGCnIɓ(S4eH+cʜI͇2P0(J [~$W7 @3ӧ<,55TDJSJ* P@o NR J0kۛ l@@U^ܐE`@&dZ!U' ˚ lC3UDIʫc*@(`7(&@,gI&In1@mn؆H l'ϬCHޟt Cf)Ju052'/ Q)tQgalDʅv(^YbuPMKh%4sZ K<3 T>3R54K(c$:t-$ԷC2Γ#68\sZ1Wf?xm@6t͞{Zby`L74w{Q`%TGtEh ǩQ?G}1\[thʇpHlPfb1$>h! $c3D @0/3Ġ0@ʜD2 3$7<dBq̓s %yr XOd鍔G8\Sf"MC  M5ЉsrL'4Ǭ&6?\H}TzwG+zI[zGз yjP,ɐa'x!+z55@"ZmЌ'ܭ| ivr%SI68 {&,1LNI & d|scoS A=W$O͎zdP 'AQ@'4T5H0X>Y'i"6B UZы54x)1 y+">JbxEx-=qAB@>c!-On駧P9P#~ @>JZ@X[  .NF|*!a4N ڠhDnȁM ( 1pAt8(&zz`h%N"M߰ \I^^-iӂ> #3mgw?0yJfL'NtuZpȽkLMڢE(*Ps&/6 9yUD>U*G)|bhimOٻ>:)e!do^B;-CU1O}]GFn/ vg|9`}=@>ș哱},vt1]eXr[u>Gj/{E/_'?Ɵ`'ƝC"⧬M飼|1Vn,eM'F?ףh? }σG 0@w|B6Jl?_DPUɄDJ",HD"q/Fc+"a1zP_-l~ڶΡ4PL(~096V5 5jbZ1 p$dyF/Mic_yvAر|[ =~??$6k2*+ǏqYowߔE(oի5|8;?I6h F~jۚ1{,jy'f4v;۾HUτO4kh' H#F4 ~%KѨ1]:tI&q饗#=Ʈm_<}g}z-[Pɻ[0sXdY͛Qe@#L&:;;o~eƑm_d"Ls,x}m6a_TUEN8J$I&I&>?!}:+̌1cm6U'sa-ZDSSk֬aL~>>nw}+G]Mzbq.ʪT xG$u;p׳yf/_ygpQ$bas6t]d29uNqq1>$DvCZV, &ikﲑ/*/^LSS<> ~/fl6?j*ʪ.OJlGvC/tRl²eX~=֭C֭[Ǽyx)..& UU6$Gfҥ 8r$ILԩS%myW ]ױl|+_ M=EIj*v]wExS ?{~ӟxx饗={v7c>(k׮SUU Y۹C`~0 FÇ&gjcƌnf]нil  ~ƍݱSO=τ vgdi'OfGO/^'Oj*233Q ˦]H5Ȳ̈#%(~2]Ì3.!(Bvv6Ç`ҥK{ڵky(--GA1KY'~߷/SLa֭=<zzz-{ѢEx^n.iDv@vwr.+~vCzcu\Q ;{}^]ߺu+cƌ;W_7-ZĆ =uz;}ńpuK$og})Syf x衇eϋ/ا毪b޼y?;g2]t 6l9٭FZ[[zTUNӞ{__wMUU7eaV+k֬9?chI2"W_~;[l9OM[nk֬ywޏtdee$o8U$" ??ロǏK%Kt˛kSZZʫMkbя ^YY&$I[ogAlO_͛Ylxu=]Ѐ@4 wIatk_k׮㭭~G.Ozj?gwaѢE݁칚kbذas!v-Fh/2UUnJAAyKx)-Z= EUUI$toii)yygJ9҃=P<ٵ7y 6h"n7n7Qh}ԣGw[{;+8\b$b A`Ŋlٲŋ7ɵtw=&ϟƍ),,# sN'NgGFAeмc2ԝInʪU(//gɒ%_Y`6l{$K} =E[nnndO_x}Q '7x'v! g? fdMKKcѢEtvv`Z[[{ٴi ,駟;L&ɲe)i",x1 [n0Xr倞^:[K/UVs}"-;;!777OƑH]vq׳fBPΝ;{%}II [l< =aXg$WV+B/_M޾dOxYz5_UU?Nh7ot+¼yzB^^>! hq\?Yo6vٝ<ʲ7OqnWTT>s&{}5JL}ZV<˭ފ vm=dٞ= p8X,9K/{}{{;{AnO8c rssϓ;ǎcw}MӘ7o;wdΝ,\7 ѣO?4}#d>:j۶?gvmq߲e -&>țoѣlڴ:ѵ ?}oz]E{EQl߾0@+wy{4̙3p]J꜏A璿+ߺuk6m˗/7" I8|y2a`Z9w*++{A(//޺,b9sI;Oy &M7=諾9;{3?פc<ͿqF{9z9¦MM.;f6lkM67G`2zxlY)//СCy㮝ٷo_I ]`޼y0vX^~nOU{s˖y>M~AΛF^.!lBIՠa6ukv"k:}x>l6; ?яqݬ[{キ a…Ce}[.?>@ +E+V}vV^=dydz-]uoGeZZZwEܮ!D h]{l Iy8x {/f 6pB} "=EE}-KEq1]đe;|r ƬY8v/bP~c0נ*t:)((Ç{A BP[[ӧimm%Ht/X>l`2x1L̙3甔xbbuuuA?]/@ϧ,&-/ЀٳUVgtwpo~nYaC"dbȑL83T;Fɓ74Q3"+3"0vXJJF3z?8SO_YYɱcǺWn 8޺ 0x' Uw*SUpJ)|xGşYПgTTT : qK{++yխl.'$8X8\ K68 P >HELJv)fkڹ=****سgMMMC?]߮_;3zo~;}to\T3XS>/S[;Hkuu$7Ș.ʃMYN<,BР-IHKK ?/q2.:Ҳ2E-cʄ<&epH%Pfb3 ">L}̂jIl<$БYXn1fGk{λVc{\[<0iZ⥗^$me|We!$>xWn/8Wu9s?1LqgGe,XнIӫ[__:nf]UN QD 5ȈlpݨD<>Q-"#6SDVø3,:~kEͧxoW54+cƔ0cz Y N1_'r뭷m6>$I:O%I ~>z ٿ;i$4]6hNE3;P~L&qtM$|gr˹RsdrKlļGQRg%bK$&5Dx8H83!ѤJBUQ<2ܲ2vjNq3def3rHZX[_/4M$ESM&bʕdffs-+~ ?5*AjAKԭ75glF, +ǁ.hhQU!b l}uv6B:-8*: )tA$+ǃEÜ-䦉8Mh&7c|nZɓsy 7QٰaC<#EsHvYb6t .w F 0׊hX$E^@N@i(qak_8%Z:krvU ct:&NB0"nE7 9Y (uHdImF2wl޼1cp7~7~qhngٲedgg{̞M*%]?.1I:8rd6C< r)FSkAI3:L @0nWD' 3GNLL.)*e8(-*`rQ.X]9~U[molgܸq,^_~)_\wu >𥛹껹R$$Sxr2ih3B4'Tq!+L,&Ú|FxuǛdfdc5dAb 4*6 áW+:EYdz,4Hįrͼ6OeǎN|3k,&MD8ƛQ42 'PHp$.h ũk-މ?P5 @0$ Qhni#=MV^NO:Ui*ٹ=0B^#Cҋ;݉@4j h@JTѐf}`\6s,wrd|Y͵O2w\jkk!禈KLΝ }XCk _YJ|6͠şOߠL$&KDђ"Ɇfeta*A_=L4,ݎ+? aS$9Y."A6oQ1d,fnCбZXdѨZ/Gqq}ȲH٤/'`WpWu]{mS=7ynoFKs5>bdNbJ?;WL kdeC8UKHt YZ[1ŠF/AUM -IL&+V`ati>m!$tD#df8hh Jw}DcaF撗 ) JCMR:W=9NLtXv}^o=.6Sw:HA4A[^EA~&>fXI `phqDJ ?2dA5U5X%NaIFcT׆5l@ICS;NLK]Ḏ?D4j6r%|뷲?_`J\lD7hIp( IDATf%`mG}[_q]yڸAG ''g]5MHOOg^U\ƈq9~`[oG ]a_L^ζV唖xy?p8w?d]lfқηeR6! YԶdf%8]VfϹS8ri32fxrmtUJ2ȏNjߢ9[;mC6%MvS6z_x,6##A, gtԓk?ׁ.MtI.ƻ6c ?N,x \_R[UM͏P6~$8ө>^ʩSM=Von;$Qx6Ii5c 1bTUeDa>ǙȑMA345%r`aC)AGD:idIjgyL4Gs8C1Ɩ0˃leX(_/K_5\EVX1䷸]5j-W׻CB̻[7 O'oc8Y2'%`6|eTUQuMtoArrr: cJŒ, 0u . fb+orí"{)QH{cRFcc;t x_FI jlCnYFpҋ"oD|~)z=?z϶u8fJ֔CY[w͎;z8a6O=ԠD"ԩSICC ^'::L>,?/@];Lzddp۾ ض˅ ģ1D*5PC&4y5|qדQl&.99zt+E6m۶mj>cb*<5Nvv6EEEq^ذUXAZ[p$h1wG'45cQv*ˏ1N;L4aٜmG96_q%ބJk1 X8U@M>t@%$DK"Ӗe'!$q+[wr4Vddl"dŌ7;w'uIw>xK3lp⫪Jqq1$曯S ^xq7'P45(HAi^"&Yxex2h9fr f\@GGRDN-' ) '"V^NS2fJ8C6HZ +pӆi!QgdzژxH&25ټ!N6B&mmm䐛K}}6|Jsk:1euUvk6KHII 6n91e(V:1ldXx_EKkb-6\i٨z6_;0  #x V$k!+kHFcx6,2=RI$Wņ_惽5ܼl7eM5_:EHc2Vg/u 96"f ŮIaDHϛE]cG*pqgP_UQTTDmmn>܋Fꈟ74HD$=ǎرc'Y:aÆo.]K FFySiij͇'Ũ|L6+{wU89lh:B\~Y+ﺉVFep8])+yQq8U]G6f \;՘LW*":qۉBD6qYUC !Dc]+2FdHIlV;2ꫯg#/&Ip<G" E|( ( x^^/d:8NVWcԜl5ͷa ׂa5a0L;1wL_ĉVĶH4'kj6Wz]h|D8A"!P]IUtzL8}`IM-Oa9 |;D݄bC5NFvee8ZGS{0p*de:(E$ W>M~Blܻ5.ZO;tO_LζX6vܭ)iZ徽{pyj&$`Ĉ<:˛b3!I0j8$)Քy=l~-DdBlj ÝBGc33W Wٽc7;$$7z kobw_94tRRRH(%T5M[Hv9&$Cq#h Æaǯ-mPWWGqq1iiij`4j?2ׁ,]@]:x&GQE#L`ٿKgsךPI>HegkTWc[;&;:,i,SVZrCG%&tĜdfz+pRO[]6Ɂ]U ٜǣyEcմzC8lDQE2dfbhatà IR4,'[eq%IEex<?QC433A"I/ݲ'O`yTZ%)pcø,al):q-&--_e``jfAOwJK/e<0zN׻^{8ԻdE$#I"f 4xmGNE&)!#FѨL(8U$5,qf4Uu$lba<Մ˴H$|NHJ}ڸ7Ͻ|.k컅[H|ďtL& 2e --gȯ(I&Oʕ+ebbI$hBn`@K@$EMPbaNn68|}aOwQqtA,Mʹ5s˵ù|NA bKi|q4#?5`d6Cg؈lkI4 A@ M $lf q0B3H2iy,."P<]K&pdƌEyIlf M"ۭ$g:kѣGim\TT4hQHǷcf ڻ:?VS%/~Q̃biDfxEQ z3]3@EQDdI% CS㴞n$d٘4-'/Yq$K"VdQGtVU>ͷSGtG+ v=v\bQXK"/76?ICj!"]$D܏``2cjB 'C+lsYvBet %Nfv:k8ЌE6f'U%]0gx63X\7R{/{&%p 558~hoO$rJA'^ۤaFnAbt.?f=MZV'q4Q=@"@5d#!r9>fbV.ȁDr瑓_FB$_]c6eDB#"K"&! 8]MggI$U4-A4.P_O~+&/0\yT>yAhnsՇ.IC4 uuu}lwH{ fse&fKg^q[xW8Bg>X"N'"Νk&?'Bgg'&O!l%/˂ϟ$F~Ai.4i!:^ze[9Uъ~x%a 't:++'=C$mvD+w'S KsiݘlfZZ26*,햑%k`yJ8O_zIHbdg! "ɤYDEXMƍ&?pB`go(0utjhΐ©zOI#:v4HNzW}U0ەRDQƌOv^0od&)S'M,#E)AnSJ9$x; 4a(>Bvf# f,㦛aTk?o~EZL\DlRTjde" ?EU2+ɥMbY,X4hT>AEdLtM%D:NI]3"L:J[[H_ƽD`೮ׁo/Bz´i`ڴiX,>;FE0MMM1+y\d9t'& $ 9Y,X[+'),NGE8'esƦVf_y/M{@!0v% 7o~w*s ƽN\1fgE,yT9׍Ak?UVQVvf_UVLUUN:EQQ7ݸ矿N/Ѹ( d%BNd3zLVsֺ,??}f_=_Ç"U%Ӯ@HgVI@k{[_z`Np11E^Y"X$>_z<n;Yn&HRTR@X;j"@\ME6V 9}t^4ǐm|^BP& ;EeɟN?^ԉݝJ$'O2g/XHF{G;ԯ$PAH``%z1Idgc7@n&;ql 2 1  vn3?䙕7}Aw{gOy7S'1wd'r D2|Tp&'sP1G~ p?zӌ%Rb´41ytz<ȸ ^Ν>M{{+"ݎGC@ :g4k:ip؝ԕ7r%j#(4ITX WRɄb3 fo'u1}?Fÿh4QWWG~;w[gwWӌg=HxD[;34\zKE8q9R['{IMȡ=}^LGp`~OOtNlßZ Q 2" OKpIllJR`=HA<8NJΗMll4('8N)p mn!o-Vk~aW^@6XZv˗館lVo}V%ՌΕ>NEKk+V3adH,rLZ(+)cD aH"bٶiRrzArDW/_j=-$%e+z 6LX>}͌b ;S_WKCU-vQAviNDL-6T#^$cEq(X-o.ϜqtKC!==^kwOKKRܓvZfƌL4Xm. n >8@`Xᘚ ռUVql$ ꏨIayY4`c8n:VM.Ɛ`DU%A(nJtFj4r Fdﶏdi~/durJ<ASmZWc ĉ\.F. IDATqgh4"2ggRk"udff/lذ+W ԩmC Lj:׆R]ĐX@M58`# Vk3 $O $C@+ˈOJƍJ"RN]M  YIHf!4">M}e5 \.͋O2PzAqTUT#"x"Fi5W]!y@"mm-ܸVÉE00چFe姻+^/u477zyog׽3#8w\*A?,Z)S2{L'3FgРDNZ'cm㳐׏)Gާ>~Xm6{|gOr ̉X$CS Yϩcbmm/}coBʩ(NŅGz[xqCy\Lk [ Z9~8[neɒ%444*,y7sO3;߳p"Q3ǰ^'-- ;wVR8E/Fx6.K3%WI>x~XĜSY|0eij4ac0h6 9ӟq{$LHlc9o~ 9Qx%-^%)11 FuNpPe8NDW&"MMCC15)ц'0SWux?G}DxxxvD9S|Owo?,LO@@sG(Ef(~HZ/x__x1Uin4Ȃi`',\ {op|ydi$ kdT رa 4ZBF:JsABXl2nNeiQ 5~4[^ gJ:k*UUTV5P_oڊ^@Jca8tODtֆxoiέ CۄwP](=Gt'_G3:kO?;wp`j[?M%k FI5Q ڟrx"bb"8r >ي#10- O W\&qPM!*.N=Ӳ{ͻJbr}'u{ 3ƃǘ1 u).FcӅFlB)~4,Kǿl=򤞮7s;(-?~<7ØQL_sv0bcyx΃_G 6>p|)cǧ2(?im%a@N;Ņ3x`X@|R Mưbucnbq;ȂD`h8--huLiC2n@NLH m`y~('/uǏZY}uk(̀{|3 ãs|nH%39y,CG/ֈB] 臤hv QL>AK`x_ F76s uMm}pU8q?YLCm= qNفN!44>}@Ԣ1T^Kh6n"`Ӝ9s)S/]vٺu+1㻼k<ďhdYDhJё43mxΜ*a'28mY͌m摅SA!ye mVV8FpHf+LG@H8ndv{}nmbGOۇjbqK %Wcu\'N'wߵkZZp' e -[xx'ZDx6{fD 6;H,z5>F :=LpIvZq ,6dY !1 .$ >2Gb6B%c2p9 M6bn`k2cks)l˼3FlقkĿa0(//g˖-,^_+Y|$V} D$hv 'b 6}`0.A$,<Fhz\0c(cs "vxp{]O+ihoQvA[˶0a$L&N׫ߪ n%II 7or?bԔ S@CqڪLfv6ǎ!P. KkA$ BdDc0D!E|.;NsۉBʐDގ݉W*kmXZ LuN'!Þ|&LDSS6m+Oo7mڄba#p1'?}}LÎ9w MZLVI1:=ewrXn;͌ւˉJ]uյ&vgGC*m̙?=v1dPذa---yK Ug*f'7ɕ|`ƍ̛7Aoּ"/D''>y"MH )1X}'d!,JA|dهO[>tH>d$N.+jih6SWFmu m\IDFI>dH^ 5f\U<3gV~X,DZcEFs˹ru{_w^M0օn̋_Nf[ /GUzl;;ìY8qDI$^/C%!!A޽8x,1 #;5C сNjc'<\GFEvmIF+{1<4X^kPtGd,))^_cǎ%??tgLfv=Ljx?#:ѣxR-Xn"}h4deeIG⯯Ote1DnEj$;AFrȂ,KX:Z-NZiAH ,[Ð>oz*|^ͳAөs|EbBbׇY 947nc\.dY&)))SaÆ| vׯ_feȑ?'lb׮vȧhk=Ea'<*`#a肂O`8hkklnR 9 di̜3L;477sYfS__ϻᆱYO|ߟJ,U3l /^=C|O& c#PY4qDt:ӦM#;;·~Nvɱc0L8N#36QԪ79w׮]jq0qDL&ǎ#33S;co~?;:uV`|>BCB[Mݽl#~:stUg&22K=dȐ!,\V^yW^y&d~QWVKN}|}J$ k.6l@^^ ;,w=u)APYY %22("""'((n!himtbhmmDKK vIj닂pQH?gO~/Lnn.frGE<vp1mgX "2-b׮]L8I&+oG/#љ6 LYY-9οCe$IǔFAů<몐~ҥ@GCZ-[(,YCr̙yh!}xj撐)((`ҤIl߾pz)F#?<1E͚Zan(a߾}(,]T'kɒ%ȲLqq_!p##s_^^ѐCBB ,`Ν]H__DGGbŊ[,B޻NB\nܸӉ܎ EQT;-)ˇw^s_w{M\?-_jk׮%..|)))B,lOLATߜ,e )7J)u3o7IIIW=YYY瓛 呓,cFM{{;$˗7n$!!`̙3={v;%d9d,Y֭[y7x뭷xh4wc֬Y޼<)裏"2gϞ7ngڋ_u꽦;wnSIi&/_$&&Fux'Mtۚ GUUDGGSSSCrr2c23gr2331L hllt~Z_%11D h$,, B\\aaaOBB׮]\.z=Cbb:}GLL ь;aÆq9222HIIm-$55Kشi̝;6֮]˳>Kmm-ٜ={6nc=ƻˢE].7>􊦿#ۙdddol26mDNNG'|6?u|RRR|f58N(--ƸqصkVNe477@bbjyUs ƎKFF.]bԐʉ'p83zhzǬQeT||?O9x ,`],Ԓ$P222ӧ7nӧh"BBBT2zU hpt:@cc#W^EpiZZGT$::dz"I:ɤIX`III]TQFO~F͓O>Ipp0[l!++C1{lZ[[Yf O?4deeQ\\Lnn.6l ''{ vk NN-(+1d:$HNN̝;&O1<쳪#ț~K"7n$''gϒECCVb̘1l߾K.}fGө-L4GOXX--|>FFQ$DEEM&ݶؽ{7ӧOW)x%zs.gggSWWǪUx饗$ m$SOqeBBBxGzTTTr%|$&&""{%::ݎ 1rH{B%J}ψ`1cƠh())a̜9f233),,d̙F^}UuΝ;3g۷og͚5*wye%K0bf$ [ϕ{I;ė$фu]߹sXz<쳼K* vKBI!?oٲV(X,0`$QYYF\t ݮf\+**X~= \ָ455~zl6AAAS^^fC1LHijjWt:V+555[orhhhd2>nGݲq[\\~k)))2c ֭[,˪_pjo&kl2{1A%2f7ٹ'`08{dYxjwءZzE+wn!"oxx9rgϞ`0vFeeQ%{ҥjtL&՘ 477ԄVtru5Fj)--UR`NRU"]~VlW>j)++Ssiiz#+Y!rohZ-ZĞ={1c-ٶm[,{!N<ɰ0|pOZ 4i*'O| MߝLUV/ravMCCRө/rݪ|])N#(v,9TNcDDDp.\a9r$&C1tPbcc9t}ZP'&<n:[,ٳU#k֬$Rl2Ο8OOJŹ߶m[wQI߬o&s;cl&33={؈hTQSLEqq DDDN0HEE'O $$#GR]] R^^ΤI9~^Oss3~p[;w.۷oW= .]%SmۙϿgΝL8eYZP􊼩c_(op t:F'ORVVFPPRXX ~z>#JKK޽&RRRHHH;wr9RSSh4޽F|EBc2x7۷/ |VUw2I9J[[)))]\+V ͛7wqx;G{jjjx꟟B/_o:wzq'%tޜzGeSo 8EllJ?ϬXKgT*IO.u{#0/+DQ+O<(ɩ#GM[[6lZۛlR*҂o2ؿRvr9t:βeصkZp8_{ŋ>}:۷oW^ah|WON|Hqq1o&ƍ_~eear8^cpFsfҺ:R3310 =׫1,n-Xdd$=LLG>$ǩ&s͖~PzDD20f A-CZ@_G+VP~"6QlJ,#WVVhquJ}츸nQS34XlOt8Jw^h:K) ]1:>כS7pC=w1U^أb9FkfvcܰiሯS! @w8hZ4 zfj?9ħOS%qCjķZ}=Dunu7 7<Γ.tEǴIф|s7wk7̐qwyf? A!x|_"ݜb`dO FW⽳+C]jӟD;G7޹Ce%C0վiy!|^;7!t*?H@J.Uǩy4a9eeʼ5Q9MQDun7O"& C$ 0 7$YH3JqOhpvsNN^eL|MaGz " tap_py-3.2.1/tap/directive.py0000644000000000000000000000337713615410400013057 0ustar00import re class Directive: """A representation of a result line directive.""" skip_pattern = re.compile( r"""^SKIP\S* (?P\s*) # Optional whitespace. (?P.*) # Slurp up the rest.""", re.IGNORECASE | re.VERBOSE, ) todo_pattern = re.compile( r"""^TODO\b # The directive name (?P\s*) # Immediately following must be whitespace. (?P.*) # Slurp up the rest.""", re.IGNORECASE | re.VERBOSE, ) def __init__(self, text): r"""Initialize the directive by parsing the text. The text is assumed to be everything after a '#\s*' on a result line. """ self._text = text self._skip = False self._todo = False self._reason = None match = self.skip_pattern.match(text) if match: self._skip = True self._reason = match.group("reason") match = self.todo_pattern.match(text) if match: if match.group("whitespace"): self._todo = True else: # Catch the case where the directive has no descriptive text. if match.group("reason") == "": self._todo = True self._reason = match.group("reason") @property def text(self): """Get the entire text.""" return self._text @property def skip(self): """Check if the directive is a SKIP type.""" return self._skip @property def todo(self): """Check if the directive is a TODO type.""" return self._todo @property def reason(self): """Get the reason for the directive.""" return self._reason tap_py-3.2.1/tap/formatter.py0000644000000000000000000000137213615410400013075 0ustar00import traceback def format_exception(exception): """Format an exception as diagnostics output. exception is the tuple as expected from sys.exc_info. """ exception_lines = traceback.format_exception(*exception) # The lines returned from format_exception do not strictly contain # one line per element in the list (i.e. some elements have new # line characters in the middle). Normalize that oddity. lines = "".join(exception_lines).splitlines(True) return format_as_diagnostics(lines) def format_as_diagnostics(lines): """Format the lines as diagnostics output by prepending the diagnostic #. This function makes no assumptions about the line endings. """ return "".join(["# " + line for line in lines]) tap_py-3.2.1/tap/line.py0000644000000000000000000001110113615410400012010 0ustar00try: import yaml LOAD_YAML = True except ImportError: # pragma: no cover LOAD_YAML = False class Line: """Base type for TAP data. TAP is a line based protocol. Thus, the most primitive type is a line. """ @property def category(self): raise NotImplementedError class Result(Line): """Information about an individual test line.""" def __init__( self, ok, number=None, description="", directive=None, diagnostics=None, raw_yaml_block=None, ): self._ok = ok if number: self._number = int(number) else: # The number may be an empty string so explicitly set to None. self._number = None self._description = description self.directive = directive self.diagnostics = diagnostics self._yaml_block = raw_yaml_block @property def category(self): """:returns: ``test``""" return "test" @property def ok(self): """Get the ok status. :rtype: bool """ return self._ok @property def number(self): """Get the test number. :rtype: int """ return self._number @property def description(self): """Get the description.""" return self._description @property def skip(self): """Check if this test was skipped. :rtype: bool """ return self.directive.skip @property def todo(self): """Check if this test was a TODO. :rtype: bool """ return self.directive.todo @property def yaml_block(self): """Lazy load a yaml_block. If yaml support is not available, there is an error in parsing the yaml block, or no yaml is associated with this result, ``None`` will be returned. :rtype: dict """ if LOAD_YAML and self._yaml_block is not None: try: yaml_dict = yaml.load(self._yaml_block, Loader=yaml.SafeLoader) return yaml_dict except yaml.error.YAMLError: print("Error parsing yaml block. Check formatting.") return None def __str__(self): is_not = "" if not self.ok: is_not = "not " directive = "" if self.directive is not None and self.directive.text: directive = f" # {self.directive.text}" diagnostics = "" if self.diagnostics is not None: diagnostics = "\n" + self.diagnostics.rstrip() return f"{is_not}ok {self.number} {self.description}{directive}{diagnostics}" class Plan(Line): """A plan line to indicate how many tests to expect.""" def __init__(self, expected_tests, directive=None): self._expected_tests = expected_tests self.directive = directive @property def category(self): """:returns: ``plan``""" return "plan" @property def expected_tests(self): """Get the number of expected tests. :rtype: int """ return self._expected_tests @property def skip(self): """Check if this plan should skip the file. :rtype: bool """ return self.directive.skip class Diagnostic(Line): """A diagnostic line (i.e. anything starting with a hash).""" def __init__(self, text): self._text = text @property def category(self): """:returns: ``diagnostic``""" return "diagnostic" @property def text(self): """Get the text.""" return self._text class Bail(Line): """A bail out line (i.e. anything starting with 'Bail out!').""" def __init__(self, reason): self._reason = reason @property def category(self): """:returns: ``bail``""" return "bail" @property def reason(self): """Get the reason.""" return self._reason class Version(Line): """A version line (i.e. of the form 'TAP version 13').""" def __init__(self, version): self._version = version @property def category(self): """:returns: ``version``""" return "version" @property def version(self): """Get the version number. :rtype: int """ return self._version class Unknown(Line): """A line that represents something that is not a known TAP line. This exists for the purpose of a Null Object pattern. """ @property def category(self): """:returns: ``unknown``""" return "unknown" tap_py-3.2.1/tap/loader.py0000644000000000000000000000557713615410400012353 0ustar00import os import unittest from tap.adapter import Adapter from tap.parser import Parser from tap.rules import Rules class Loader: """Load TAP lines into unittest-able objects.""" ignored_lines = set(["diagnostic", "unknown"]) def __init__(self): self._parser = Parser() def load(self, files): """Load any files found into a suite. Any directories are walked and their files are added as TAP files. :returns: A ``unittest.TestSuite`` instance """ suite = unittest.TestSuite() for filepath in files: if os.path.isdir(filepath): self._find_tests_in_directory(filepath, suite) else: suite.addTest(self.load_suite_from_file(filepath)) return suite def load_suite_from_file(self, filename): """Load a test suite with test lines from the provided TAP file. :returns: A ``unittest.TestSuite`` instance """ suite = unittest.TestSuite() rules = Rules(filename, suite) if not os.path.exists(filename): rules.handle_file_does_not_exist() return suite line_generator = self._parser.parse_file(filename) return self._load_lines(filename, line_generator, suite, rules) def load_suite_from_stdin(self): """Load a test suite with test lines from the TAP stream on STDIN. :returns: A ``unittest.TestSuite`` instance """ suite = unittest.TestSuite() rules = Rules("stream", suite) line_generator = self._parser.parse_stdin() return self._load_lines("stream", line_generator, suite, rules) def _find_tests_in_directory(self, directory, suite): """Find test files in the directory and add them to the suite.""" for dirpath, _dirnames, filenames in os.walk(directory): for filename in filenames: filepath = os.path.join(dirpath, filename) suite.addTest(self.load_suite_from_file(filepath)) def _load_lines(self, filename, line_generator, suite, rules): """Load a suite with lines produced by the line generator.""" line_counter = 0 for line in line_generator: line_counter += 1 if line.category in self.ignored_lines: continue if line.category == "test": suite.addTest(Adapter(filename, line)) rules.saw_test() elif line.category == "plan": if line.skip: rules.handle_skipping_plan(line) return suite rules.saw_plan(line, line_counter) elif line.category == "bail": rules.handle_bail(line) return suite elif line.category == "version": rules.saw_version_at(line_counter) rules.check(line_counter) return suite tap_py-3.2.1/tap/main.py0000644000000000000000000000411413615410400012013 0ustar00import argparse import sys import unittest from tap.loader import Loader from tap.runner import TAPTestRunner def main(argv=sys.argv, stream=sys.stderr): """Entry point for ``tappy`` command.""" args = parse_args(argv) suite = build_suite(args) runner = unittest.TextTestRunner(verbosity=args.verbose, stream=stream) result = runner.run(suite) return get_status(result) def build_suite(args): """Build a test suite by loading TAP files or a TAP stream.""" loader = Loader() if len(args.files) == 0 or args.files[0] == "-": suite = loader.load_suite_from_stdin() else: suite = loader.load(args.files) return suite def parse_args(argv): description = "A TAP consumer for Python" epilog = ( "When no files are given or a dash (-) is used for the file name, " "tappy will read a TAP stream from STDIN." ) parser = argparse.ArgumentParser(description=description, epilog=epilog) parser.add_argument( "files", metavar="FILE", nargs="*", help=( "A file containing TAP output. Any directories listed will be " "scanned for files to include as TAP files." ), ) parser.add_argument( "-v", "--verbose", action="store_const", default=1, const=2, help="use verbose messages", ) # argparse expects the executable to be removed from argv. args = parser.parse_args(argv[1:]) # When no files are provided, the user wants to use a TAP stream on STDIN. # But they probably didn't mean it if there is no pipe connected. # In that case, print the help and exit. if not args.files and sys.stdin.isatty(): sys.exit(parser.print_help()) return args def get_status(result): """Get a return status from the result.""" if result.wasSuccessful(): return 0 else: return 1 def main_module(): """Entry point for running as ``python -m tap``.""" runner = TAPTestRunner() runner.set_stream(True) unittest.main(module=None, testRunner=runner) tap_py-3.2.1/tap/parser.py0000644000000000000000000001520313615410400012364 0ustar00import itertools import re import sys from io import StringIO from tap.directive import Directive from tap.line import Bail, Diagnostic, Plan, Result, Unknown, Version try: import yaml # noqa from more_itertools import peekable ENABLE_VERSION_13 = True except ImportError: # pragma: no cover ENABLE_VERSION_13 = False class Parser: """A parser for TAP files and lines.""" # ok and not ok share most of the same characteristics. result_base = r""" \s* # Optional whitespace. (?P\d*) # Optional test number. \s* # Optional whitespace. (?P[^#]*) # Optional description before #. \#? # Optional directive marker. \s* # Optional whitespace. (?P.*) # Optional directive text. """ ok = re.compile(r"^ok" + result_base, re.VERBOSE) not_ok = re.compile(r"^not\ ok" + result_base, re.VERBOSE) plan = re.compile( r""" ^1..(?P\d+) # Match the plan details. [^#]* # Consume any non-hash character to confirm only # directives appear with the plan details. \#? # Optional directive marker. \s* # Optional whitespace. (?P.*) # Optional directive text. """, re.VERBOSE, ) diagnostic = re.compile(r"^#") bail = re.compile( r""" ^Bail\ out! \s* # Optional whitespace. (?P.*) # Optional reason. """, re.VERBOSE, ) version = re.compile(r"^TAP version (?P\d+)$") yaml_block_start = re.compile(r"^(?P\s+)-") yaml_block_end = re.compile(r"^\s+\.\.\.") TAP_MINIMUM_DECLARED_VERSION = 13 def parse_file(self, filename): """Parse a TAP file to an iterable of tap.line.Line objects. This is a generator method that will yield an object for each parsed line. The file given by `filename` is assumed to exist. """ return self.parse(open(filename)) def parse_stdin(self): """Parse a TAP stream from standard input. Note: this has the side effect of closing the standard input filehandle after parsing. """ return self.parse(sys.stdin) def parse_text(self, text): """Parse a string containing one or more lines of TAP output.""" return self.parse(StringIO(text)) def parse(self, fh): """Generate tap.line.Line objects, given a file-like object `fh`. `fh` may be any object that implements both the iterator and context management protocol (i.e. it can be used in both a "with" statement and a "for...in" statement.) Trailing whitespace and newline characters will be automatically stripped from the input lines. """ with fh: try: first_line = next(fh) except StopIteration: return first_parsed = self.parse_line(first_line.rstrip()) fh_new = itertools.chain([first_line], fh) if first_parsed.category == "version" and first_parsed.version >= 13: if ENABLE_VERSION_13: fh_new = peekable(itertools.chain([first_line], fh)) else: # pragma no cover print( """ WARNING: Optional imports not found, TAP 13 output will be ignored. To parse yaml, see requirements in docs: https://tappy.readthedocs.io/en/latest/consumers.html#tap-version-13""" ) for line in fh_new: yield self.parse_line(line.rstrip(), fh_new) def parse_line(self, text, fh=None): """Parse a line into whatever TAP category it belongs.""" match = self.ok.match(text) if match: return self._parse_result(True, match, fh) match = self.not_ok.match(text) if match: return self._parse_result(False, match, fh) if self.diagnostic.match(text): return Diagnostic(text) match = self.plan.match(text) if match: return self._parse_plan(match) match = self.bail.match(text) if match: return Bail(match.group("reason")) match = self.version.match(text) if match: return self._parse_version(match) return Unknown() def _parse_plan(self, match): """Parse a matching plan line.""" expected_tests = int(match.group("expected")) directive = Directive(match.group("directive")) # Only SKIP directives are allowed in the plan. if directive.text and not directive.skip: return Unknown() return Plan(expected_tests, directive) def _parse_result(self, ok, match, fh=None): """Parse a matching result line into a result instance.""" peek_match = None try: if fh is not None and ENABLE_VERSION_13 and isinstance(fh, peekable): peek_match = self.yaml_block_start.match(fh.peek()) except StopIteration: pass if peek_match is None: return Result( ok, number=match.group("number"), description=match.group("description").strip(), directive=Directive(match.group("directive")), ) indent = peek_match.group("indent") concat_yaml = self._extract_yaml_block(indent, fh) return Result( ok, number=match.group("number"), description=match.group("description").strip(), directive=Directive(match.group("directive")), raw_yaml_block=concat_yaml, ) def _extract_yaml_block(self, indent, fh): """Extract a raw yaml block from a file handler""" raw_yaml = [] indent_match = re.compile(rf"^{indent}") try: next(fh) while indent_match.match(fh.peek()): raw_yaml.append(next(fh).replace(indent, "", 1)) # check for the end and stop adding yaml if encountered if self.yaml_block_end.match(fh.peek()): next(fh) break except StopIteration: pass return "".join(raw_yaml) def _parse_version(self, match): version = int(match.group("version")) if version < self.TAP_MINIMUM_DECLARED_VERSION: raise ValueError( "It is an error to explicitly specify any version lower than 13." ) return Version(version) tap_py-3.2.1/tap/rules.py0000644000000000000000000000646213615410400012231 0ustar00from tap.adapter import Adapter from tap.directive import Directive from tap.line import Result class Rules: def __init__(self, filename, suite): self._filename = filename self._suite = suite self._lines_seen = {"plan": [], "test": 0, "version": []} def check(self, final_line_count): """Check the status of all provided data and update the suite.""" if self._lines_seen["version"]: self._process_version_lines() self._process_plan_lines(final_line_count) def _process_version_lines(self): """Process version line rules.""" if len(self._lines_seen["version"]) > 1: self._add_error("Multiple version lines appeared.") elif self._lines_seen["version"][0] != 1: self._add_error("The version must be on the first line.") def _process_plan_lines(self, final_line_count): """Process plan line rules.""" if not self._lines_seen["plan"]: self._add_error("Missing a plan.") return if len(self._lines_seen["plan"]) > 1: self._add_error("Only one plan line is permitted per file.") return plan, at_line = self._lines_seen["plan"][0] if not self._plan_on_valid_line(at_line, final_line_count): self._add_error("A plan must appear at the beginning or end of the file.") return if plan.expected_tests != self._lines_seen["test"]: self._add_error( "Expected {expected_count} tests but only {seen_count} ran.".format( expected_count=plan.expected_tests, seen_count=self._lines_seen["test"], ) ) def _plan_on_valid_line(self, at_line, final_line_count): """Check if a plan is on a valid line.""" # Put the common cases first. if at_line == 1 or at_line == final_line_count: return True # The plan may only appear on line 2 if the version is at line 1. after_version = ( self._lines_seen["version"] and self._lines_seen["version"][0] == 1 and at_line == 2 ) return bool(after_version) def handle_bail(self, bail): """Handle a bail line.""" self._add_error(f"Bailed: {bail.reason}") def handle_file_does_not_exist(self): """Handle a test file that does not exist.""" self._add_error(f"{self._filename} does not exist.") def handle_skipping_plan(self, skip_plan): """Handle a plan that contains a SKIP directive.""" skip_line = Result(True, None, skip_plan.directive.text, Directive("SKIP")) self._suite.addTest(Adapter(self._filename, skip_line)) def saw_plan(self, plan, at_line): """Record when a plan line was seen.""" self._lines_seen["plan"].append((plan, at_line)) def saw_test(self): """Record when a test line was seen.""" self._lines_seen["test"] += 1 def saw_version_at(self, line_counter): """Record when a version line was seen.""" self._lines_seen["version"].append(line_counter) def _add_error(self, message): """Add an error test to the suite.""" error_line = Result(False, None, message, Directive("")) self._suite.addTest(Adapter(self._filename, error_line)) tap_py-3.2.1/tap/runner.py0000644000000000000000000001171213615410400012402 0ustar00import os import sys from unittest import TextTestResult, TextTestRunner from unittest.runner import _WritelnDecorator from tap import formatter from tap.tracker import Tracker class TAPTestResult(TextTestResult): FORMAT = None def __init__(self, stream, descriptions, verbosity): super().__init__(stream, descriptions, verbosity) def addSubTest(self, test, subtest, err): super().addSubTest(test, subtest, err) if err is not None: diagnostics = formatter.format_exception(err) self.tracker.add_not_ok( self._cls_name(test), self._description(subtest), diagnostics=diagnostics, ) else: self.tracker.add_ok(self._cls_name(test), self._description(subtest)) def stopTestRun(self): # pragma: no cover """Once the test run is complete, generate each of the TAP files.""" super().stopTestRun() self.tracker.generate_tap_reports() def addError(self, test, err): super().addError(test, err) diagnostics = formatter.format_exception(err) self.tracker.add_not_ok( self._cls_name(test), self._description(test), diagnostics=diagnostics ) def addFailure(self, test, err): super().addFailure(test, err) diagnostics = formatter.format_exception(err) self.tracker.add_not_ok( self._cls_name(test), self._description(test), diagnostics=diagnostics ) def addSuccess(self, test): super().addSuccess(test) self.tracker.add_ok(self._cls_name(test), self._description(test)) def addSkip(self, test, reason): super().addSkip(test, reason) self.tracker.add_skip(self._cls_name(test), self._description(test), reason) def addExpectedFailure(self, test, err): super().addExpectedFailure(test, err) diagnostics = formatter.format_exception(err) self.tracker.add_not_ok( self._cls_name(test), self._description(test), "TODO {}".format("(expected failure)"), diagnostics=diagnostics, ) def addUnexpectedSuccess(self, test): super().addUnexpectedSuccess(test) self.tracker.add_ok( self._cls_name(test), self._description(test), "TODO {}".format("(unexpected success)"), ) def _cls_name(self, test): return test.__class__.__name__ def _description(self, test): if self.FORMAT: try: return self.FORMAT.format( method_name=str(test), short_description=test.shortDescription() or "", ) except KeyError: sys.exit( f"Bad format string: {self.FORMAT}\n" "Replacement options are: {short_description} and " "{method_name}" ) return test.shortDescription() or str(test) # TODO: 2016-7-30 mblayman - Since the 2.6 signature is no longer relevant, # check the possibility of removing the module level scope. # Module level state stinks, but this is the only way to keep compatibility # with Python 2.6. The best place for the tracker is as an instance variable # on the runner, but __init__ is so different that it is not easy to create # a runner that satisfies every supported Python version. _tracker = Tracker() class TAPTestRunner(TextTestRunner): """A test runner that will behave exactly like TextTestRunner and will additionally generate TAP files for each test case""" resultclass = TAPTestResult def set_stream(self, streaming): """Set the streaming boolean option to stream TAP directly to stdout. The test runner default output will be suppressed in favor of TAP. """ self.stream = _WritelnDecorator(open(os.devnull, "w")) # noqa: SIM115 _tracker.streaming = streaming _tracker.stream = sys.stdout def _makeResult(self): # pragma: no cover result = self.resultclass(self.stream, self.descriptions, self.verbosity) result.tracker = _tracker return result @classmethod def set_outdir(cls, outdir): """Set the output directory so that TAP files are written to the specified outdir location. """ # Blame the lack of unittest extensibility for this hacky method. _tracker.outdir = outdir @classmethod def set_combined(cls, combined): """Set the tracker to use a single output file.""" _tracker.combined = combined @classmethod def set_header(cls, header): """Set the header display flag.""" _tracker.header = header @classmethod def set_format(cls, fmt): """Set the format of each test line. The format string can use: * {method_name}: The test method name * {short_description}: The test's docstring short description """ TAPTestResult.FORMAT = fmt tap_py-3.2.1/tap/tracker.py0000644000000000000000000001663013615410400012530 0ustar00import os from tap.directive import Directive from tap.line import Result try: import more_itertools # noqa import yaml # noqa ENABLE_VERSION_13 = True except ImportError: # pragma: no cover ENABLE_VERSION_13 = False class Tracker: def __init__( self, outdir=None, combined=False, streaming=False, stream=None, header=True, plan=None, ): self.outdir = outdir # Combine all the test results into one file. self.combined = combined self.combined_line_number = 0 # Test case ordering is important for the combined results # because of how numbers are assigned. The test cases # must be tracked in order so that reporting can sequence # the line numbers properly. self.combined_test_cases_seen = [] # Stream output directly to a stream instead of file output. self.streaming = streaming self.stream = stream # The total number of tests we expect (or None if we don't know yet). self.plan = plan self._plan_written = False # Display the test case header unless told not to. self.header = header # Internal state for tracking each test case. self._test_cases = {} self._sanitized_table = str.maketrans(" \\/\n", "----") if self.streaming: self._write_tap_version(self.stream) if self.plan is not None: self._write_plan(self.stream) def _get_outdir(self): return self._outdir def _set_outdir(self, outdir): self._outdir = outdir if outdir and not os.path.exists(outdir): os.makedirs(outdir) outdir = property(_get_outdir, _set_outdir) def _track(self, class_name): """Keep track of which test cases have executed.""" if self._test_cases.get(class_name) is None: if self.streaming and self.header: self._write_test_case_header(class_name, self.stream) self._test_cases[class_name] = [] if self.combined: self.combined_test_cases_seen.append(class_name) def add_ok(self, class_name, description, directive="", diagnostics=None): result = Result( ok=True, number=self._get_next_line_number(class_name), description=description, diagnostics=diagnostics, directive=Directive(directive), ) self._add_line(class_name, result) def add_not_ok(self, class_name, description, directive="", diagnostics=None): result = Result( ok=False, number=self._get_next_line_number(class_name), description=description, diagnostics=diagnostics, directive=Directive(directive), ) self._add_line(class_name, result) def add_skip(self, class_name, description, reason): directive = f"SKIP {reason}" result = Result( ok=True, number=self._get_next_line_number(class_name), description=description, directive=Directive(directive), ) self._add_line(class_name, result) def _add_line(self, class_name, result): self._track(class_name) if self.streaming: print(result, file=self.stream) self._test_cases[class_name].append(result) def _get_next_line_number(self, class_name): if self.combined or self.streaming: # This has an obvious side effect. Oh well. self.combined_line_number += 1 return self.combined_line_number else: try: return len(self._test_cases[class_name]) + 1 except KeyError: # A result is created before the call to _track so the test # case may not be tracked yet. In that case, the line is 1. return 1 def set_plan(self, total): """Notify the tracker how many total tests there will be.""" self.plan = total if self.streaming: # This will only write the plan if we haven't written it # already but we want to check if we already wrote a # test out (in which case we can't just write the plan out # right here). if not self.combined_test_cases_seen: self._write_plan(self.stream) elif not self.combined: raise ValueError( "set_plan can only be used with combined or streaming output" ) def generate_tap_reports(self): """Generate TAP reports. The results are either combined into a single output file or the output file name is generated from the test case. """ if self.streaming: # We're streaming but set_plan wasn't called, so we can only # know the plan now (at the end). if not self._plan_written: print(f"1..{self.combined_line_number}", file=self.stream) self._plan_written = True return if self.combined: combined_file = "testresults.tap" if self.outdir: combined_file = os.path.join(self.outdir, combined_file) with open(combined_file, "w") as out_file: self._write_tap_version(out_file) if self.plan is not None: print(f"1..{self.plan}", file=out_file) for test_case in self.combined_test_cases_seen: self.generate_tap_report( test_case, self._test_cases[test_case], out_file ) if self.plan is None: print(f"1..{self.combined_line_number}", file=out_file) else: for test_case, tap_lines in self._test_cases.items(): with open(self._get_tap_file_path(test_case), "w") as out_file: self._write_tap_version(out_file) self.generate_tap_report(test_case, tap_lines, out_file) def generate_tap_report(self, test_case, tap_lines, out_file): self._write_test_case_header(test_case, out_file) for tap_line in tap_lines: print(tap_line, file=out_file) # For combined results, the plan is only output once after # all the test cases complete. if not self.combined: print(f"1..{len(tap_lines)}", file=out_file) def _write_tap_version(self, filename): """Write a Version 13 TAP row. ``filename`` can be a filename or a stream. """ if ENABLE_VERSION_13: print("TAP version 13", file=filename) def _write_plan(self, stream): """Write the plan line to the stream. If we have a plan and have not yet written it out, write it to the given stream. """ if self.plan is not None: if not self._plan_written: print(f"1..{self.plan}", file=stream) self._plan_written = True def _write_test_case_header(self, test_case, stream): print(f"# TAP results for {test_case}", file=stream) def _get_tap_file_path(self, test_case): """Get the TAP output file path for the test case.""" sanitized_test_case = test_case.translate(self._sanitized_table) tap_file = sanitized_test_case + ".tap" if self.outdir: return os.path.join(self.outdir, tap_file) return tap_file tap_py-3.2.1/tap/tests/__init__.py0000644000000000000000000000010713615410400013766 0ustar00"""Tests for tappy""" from tap.tests.testcase import TestCase # NOQA tap_py-3.2.1/tap/tests/factory.py0000644000000000000000000000206713615410400013705 0ustar00import sys import tempfile from unittest.runner import TextTestResult from tap.directive import Directive from tap.line import Bail, Plan, Result class Factory: """A factory to produce commonly needed objects""" def make_ok(self, directive_text=""): return Result(True, 1, "This is a description.", Directive(directive_text)) def make_not_ok(self, directive_text=""): return Result(False, 1, "This is a description.", Directive(directive_text)) def make_bail(self, reason="Because it is busted."): return Bail(reason) def make_plan(self, expected_tests=99, directive_text=""): return Plan(expected_tests, Directive(directive_text)) def make_test_result(self): stream = tempfile.TemporaryFile(mode="w") # noqa: SIM115 return TextTestResult(stream, None, 1) def make_exc(self): """Make a traceback tuple. Doing this intentionally is not straight forward. """ try: raise ValueError("boom") except ValueError: return sys.exc_info() tap_py-3.2.1/tap/tests/test_example.py0000644000000000000000000000027313615410400014725 0ustar00import unittest class TestCase(unittest.TestCase): def test_it(self): """This example exists for the test suite to have an in-package test.""" self.assertTrue(True) tap_py-3.2.1/tap/tests/testcase.py0000644000000000000000000000032013615410400014037 0ustar00import unittest from tap.tests.factory import Factory class TestCase(unittest.TestCase): def __init__(self, methodName="runTest"): super().__init__(methodName) self.factory = Factory() tap_py-3.2.1/tests/run.py0000644000000000000000000000071013615410400012247 0ustar00import os import sys import unittest from tap import TAPTestRunner if __name__ == "__main__": tests_dir = os.path.dirname(os.path.abspath(__file__)) loader = unittest.TestLoader() tests = loader.discover(tests_dir) runner = TAPTestRunner() runner.set_outdir("testout") runner.set_format("Hi: {method_name} - {short_description}") result = runner.run(tests) status = 0 if result.wasSuccessful() else 1 sys.exit(status) tap_py-3.2.1/tests/test_adapter.py0000644000000000000000000000353013615410400014125 0ustar00from unittest import mock from tap.adapter import Adapter from tap.tests import TestCase class TestAdapter(TestCase): """Tests for tap.adapter.Adapter""" def test_adapter_has_filename(self): """The adapter has a TAP filename.""" tap_filename = "fake.tap" adapter = Adapter(tap_filename, None) self.assertEqual(tap_filename, adapter._filename) def test_handles_ok_test_line(self): """Add a success for an ok test line.""" ok_line = self.factory.make_ok() adapter = Adapter("fake.tap", ok_line) result = mock.Mock() adapter(result) self.assertTrue(result.addSuccess.called) def test_handles_skip_test_line(self): """Add a skip when a test line contains a skip directive.""" skip_line = self.factory.make_ok(directive_text="SKIP This is the reason.") adapter = Adapter("fake.tap", skip_line) result = self.factory.make_test_result() adapter(result) self.assertEqual(1, len(result.skipped)) self.assertEqual("This is the reason.", result.skipped[0][1]) def test_handles_ok_todo_test_line(self): """Add an unexpected success for an ok todo test line.""" todo_line = self.factory.make_ok(directive_text="TODO An incomplete test") adapter = Adapter("fake.tap", todo_line) result = self.factory.make_test_result() adapter(result) self.assertEqual(1, len(result.unexpectedSuccesses)) def test_handles_not_ok_todo_test_line(self): """Add an expected failure for a not ok todo test line.""" todo_line = self.factory.make_not_ok(directive_text="TODO An incomplete test") adapter = Adapter("fake.tap", todo_line) result = self.factory.make_test_result() adapter(result) self.assertEqual(1, len(result.expectedFailures)) tap_py-3.2.1/tests/test_directive.py0000644000000000000000000000231113615410400014457 0ustar00import unittest from tap.directive import Directive class TestDirective(unittest.TestCase): """Tests for tap.directive.Directive""" def test_finds_todo(self): text = "ToDo This is something to do." directive = Directive(text) self.assertTrue(directive.todo) def test_finds_simplest_todo(self): text = "TODO" directive = Directive(text) self.assertTrue(directive.todo) def test_todo_has_boundary(self): """TAP spec indicates TODO directives must be on a boundary.""" text = "TODO: Not a TODO directive because of an immediate colon." directive = Directive(text) self.assertFalse(directive.todo) def test_finds_skip(self): text = "Skipping This is something to skip." directive = Directive(text) self.assertTrue(directive.skip) def test_finds_simplest_skip(self): text = "SKIP" directive = Directive(text) self.assertTrue(directive.skip) def test_skip_at_beginning(self): """Only match SKIP directives at the beginning.""" text = "This is not something to skip." directive = Directive(text) self.assertFalse(directive.skip) tap_py-3.2.1/tests/test_formatter.py0000644000000000000000000000113113615410400014503 0ustar00from tap.formatter import format_as_diagnostics, format_exception from tap.tests import TestCase class TestFormatter(TestCase): def test_formats_as_diagnostics(self): data = ["foo\n", "bar\n"] expected_diagnostics = "# foo\n# bar\n" diagnostics = format_as_diagnostics(data) self.assertEqual(expected_diagnostics, diagnostics) def test_format_exception_as_diagnostics(self): exc = self.factory.make_exc() diagnostics = format_exception(exc) self.assertTrue(diagnostics.startswith("# ")) self.assertTrue("boom" in diagnostics) tap_py-3.2.1/tests/test_line.py0000644000000000000000000000232513615410400013435 0ustar00import unittest from tap.directive import Directive from tap.line import Line, Result class TestLine(unittest.TestCase): """Tests for tap.line.Line""" def test_line_requires_category(self): line = Line() with self.assertRaises(NotImplementedError): _ = line.category class TestResult(unittest.TestCase): """Tests for tap.line.Result""" def test_category(self): result = Result(True) self.assertEqual("test", result.category) def test_ok(self): result = Result(True) self.assertTrue(result.ok) def test_str_ok(self): result = Result(True, 42, "passing") self.assertEqual("ok 42 passing", str(result)) def test_str_not_ok(self): result = Result(False, 43, "failing") self.assertEqual("not ok 43 failing", str(result)) def test_str_directive(self): directive = Directive("SKIP a reason") result = Result(True, 44, "passing", directive) self.assertEqual("ok 44 passing # SKIP a reason", str(result)) def test_str_diagnostics(self): result = Result(False, 43, "failing", diagnostics="# more info") self.assertEqual("not ok 43 failing\n# more info", str(result)) tap_py-3.2.1/tests/test_loader.py0000644000000000000000000000717113615410400013760 0ustar00import inspect import os import tempfile import unittest from io import StringIO from unittest import mock from tap.loader import Loader from tap.tests import TestCase class TestLoader(TestCase): """Tests for tap.loader.Loader""" def test_handles_file(self): """The loader handles a file.""" sample = inspect.cleandoc( """TAP version 13 1..2 # This is a diagnostic. ok 1 A passing test not ok 2 A failing test This is an unknown line. Bail out! This test would abort. """ ) with tempfile.NamedTemporaryFile(delete=False) as temp: temp.write(sample.encode("utf-8")) loader = Loader() suite = loader.load_suite_from_file(temp.name) # The bail line counts as a failed test. self.assertEqual(3, len(suite._tests)) def test_file_does_not_exist(self): """The loader records a failure when a file does not exist.""" loader = Loader() suite = loader.load_suite_from_file("phony.tap") self.assertEqual(1, len(suite._tests)) self.assertEqual( "{filename} does not exist.".format(filename="phony.tap"), suite._tests[0]._line.description, ) def test_handles_directory(self): directory = tempfile.mkdtemp() sub_directory = os.path.join(directory, "sub") os.mkdir(sub_directory) with open(os.path.join(directory, "a_file.tap"), "w") as f: f.write("ok A passing test") with open(os.path.join(sub_directory, "another_file.tap"), "w") as f: f.write("not ok A failing test") loader = Loader() suite = loader.load([directory]) self.assertEqual(2, len(suite._tests)) def test_errors_with_multiple_version_lines(self): sample = inspect.cleandoc( """TAP version 13 TAP version 13 1..0 """ ) with tempfile.NamedTemporaryFile(delete=False) as temp: temp.write(sample.encode("utf-8")) loader = Loader() suite = loader.load_suite_from_file(temp.name) self.assertEqual(1, len(suite._tests)) self.assertEqual( "Multiple version lines appeared.", suite._tests[0]._line.description ) def test_errors_with_version_not_on_first_line(self): sample = inspect.cleandoc( """# Something that doesn't belong. TAP version 13 1..0 """ ) with tempfile.NamedTemporaryFile(delete=False) as temp: temp.write(sample.encode("utf-8")) loader = Loader() suite = loader.load_suite_from_file(temp.name) self.assertEqual(1, len(suite._tests)) self.assertEqual( "The version must be on the first line.", suite._tests[0]._line.description, ) def test_skip_plan_aborts_loading(self): sample = inspect.cleandoc( """1..0 # Skipping this test file. ok This should not get processed. """ ) with tempfile.NamedTemporaryFile(delete=False) as temp: temp.write(sample.encode("utf-8")) loader = Loader() suite = loader.load_suite_from_file(temp.name) self.assertEqual(1, len(suite._tests)) self.assertEqual("Skipping this test file.", suite._tests[0]._line.description) @mock.patch("tap.parser.sys.stdin", StringIO("")) def test_loads_from_stream(self): loader = Loader() suite = loader.load_suite_from_stdin() self.assertTrue(isinstance(suite, unittest.TestSuite)) tap_py-3.2.1/tests/test_main.py0000644000000000000000000000376313615410400013441 0ustar00import argparse import os from unittest import mock from tap.loader import Loader from tap.main import build_suite, get_status, main, main_module, parse_args from tap.tests import TestCase class TestMain(TestCase): """Tests for tap.main""" def test_exits_with_error(self): """The main function returns an error status if there were failures.""" argv = ["/bin/fake", "fake.tap"] stream = open(os.devnull, "w") # noqa: SIM115 status = main(argv, stream=stream) self.assertEqual(1, status) def test_get_successful_status(self): result = mock.Mock() result.wasSuccessful.return_value = True self.assertEqual(0, get_status(result)) @mock.patch.object(Loader, "load_suite_from_stdin") def test_build_suite_from_stdin(self, load_suite_from_stdin): args = mock.Mock() args.files = [] expected_suite = mock.Mock() load_suite_from_stdin.return_value = expected_suite suite = build_suite(args) self.assertEqual(expected_suite, suite) @mock.patch.object(Loader, "load_suite_from_stdin") def test_build_suite_from_stdin_dash(self, load_suite_from_stdin): argv = ["/bin/fake", "-"] args = parse_args(argv) expected_suite = mock.Mock() load_suite_from_stdin.return_value = expected_suite suite = build_suite(args) self.assertEqual(expected_suite, suite) @mock.patch("tap.main.sys.stdin") @mock.patch("tap.main.sys.exit") @mock.patch.object(argparse.ArgumentParser, "print_help") def test_when_no_pipe_to_stdin(self, print_help, sys_exit, mock_stdin): argv = ["/bin/fake"] mock_stdin.isatty = mock.Mock(return_value=True) parse_args(argv) self.assertTrue(print_help.called) self.assertTrue(sys_exit.called) class TestMainModule(TestCase): @mock.patch("tap.main.unittest") def test_main_set_to_stream(self, mock_unittest): main_module() assert mock_unittest.main.called tap_py-3.2.1/tests/test_parser.py0000644000000000000000000004044713615410400014011 0ustar00import inspect import sys import tempfile import unittest from contextlib import contextmanager from io import StringIO from unittest import mock from tap.parser import Parser try: import yaml from more_itertools import peekable # noqa have_yaml = True except ImportError: have_yaml = False @contextmanager def captured_output(): new_out, new_err = StringIO(), StringIO() old_out, old_err = sys.stdout, sys.stderr try: sys.stdout, sys.stderr = new_out, new_err yield sys.stdout, sys.stderr finally: sys.stdout, sys.stderr = old_out, old_err class TestParser(unittest.TestCase): """Tests for tap.parser.Parser""" def test_finds_ok(self): """The parser extracts an ok line.""" parser = Parser() line = parser.parse_line("ok - This is a passing test line.") self.assertEqual("test", line.category) self.assertTrue(line.ok) self.assertTrue(line.number is None) def test_finds_number(self): """The parser extracts a test number.""" parser = Parser() line = parser.parse_line("ok 42 is the magic number.") self.assertEqual("test", line.category) self.assertEqual(42, line.number) def test_finds_description(self): parser = Parser() line = parser.parse_line("ok 42 A passing test.") self.assertEqual("test", line.category) self.assertEqual("A passing test.", line.description) def test_after_hash_is_not_description(self): parser = Parser() line = parser.parse_line("ok A description # Not part of description.") self.assertEqual("test", line.category) self.assertEqual("A description", line.description) def test_finds_todo(self): parser = Parser() line = parser.parse_line("ok A description # TODO Not done") self.assertEqual("test", line.category) self.assertTrue(line.todo) def test_finds_skip(self): parser = Parser() line = parser.parse_line("ok A description # SKIP for now") self.assertEqual("test", line.category) self.assertTrue(line.skip) def test_finds_not_ok(self): """The parser extracts a not ok line.""" parser = Parser() line = parser.parse_line("not ok - This is a failing test line.") self.assertEqual("test", line.category) self.assertFalse(line.ok) self.assertTrue(line.number is None) self.assertEqual("", line.directive.text) def test_finds_directive(self): """The parser extracts a directive""" parser = Parser() test_line = "not ok - This line fails # TODO not implemented" line = parser.parse_line(test_line) directive = line.directive self.assertEqual("test", line.category) self.assertEqual("TODO not implemented", directive.text) self.assertFalse(directive.skip) self.assertTrue(directive.todo) self.assertEqual("not implemented", directive.reason) def test_unrecognizable_line(self): """The parser returns an unrecognizable line.""" parser = Parser() line = parser.parse_line("This is not a valid TAP line. # srsly") self.assertEqual("unknown", line.category) def test_diagnostic_line(self): """The parser extracts a diagnostic line.""" text = "# An example diagnostic line" parser = Parser() line = parser.parse_line(text) self.assertEqual("diagnostic", line.category) self.assertEqual(text, line.text) def test_bail_out_line(self): """The parser extracts a bail out line.""" parser = Parser() line = parser.parse_line("Bail out! This is the reason to bail.") self.assertEqual("bail", line.category) self.assertEqual("This is the reason to bail.", line.reason) def test_finds_version(self): """The parser extracts a version line.""" parser = Parser() line = parser.parse_line("TAP version 13") self.assertEqual("version", line.category) self.assertEqual(13, line.version) def test_errors_on_old_version(self): """The TAP spec dictates that anything less than 13 is an error.""" parser = Parser() with self.assertRaises(ValueError): parser.parse_line("TAP version 12") def test_finds_plan(self): """The parser extracts a plan line.""" parser = Parser() line = parser.parse_line("1..42") self.assertEqual("plan", line.category) self.assertEqual(42, line.expected_tests) def test_finds_plan_with_skip(self): """The parser extracts a plan line containing a SKIP.""" parser = Parser() line = parser.parse_line("1..42 # Skipping this test file.") self.assertEqual("plan", line.category) self.assertTrue(line.skip) def test_ignores_plan_with_any_non_skip_directive(self): """The parser only recognizes SKIP directives in plans.""" parser = Parser() line = parser.parse_line("1..42 # TODO will not work.") self.assertEqual("unknown", line.category) def test_parses_text(self): sample = inspect.cleandoc( """1..2 ok 1 A passing test not ok 2 A failing test""" ) parser = Parser() lines = [] for line in parser.parse_text(sample): lines.append(line) self.assertEqual(3, len(lines)) self.assertEqual("plan", lines[0].category) self.assertEqual("test", lines[1].category) self.assertTrue(lines[1].ok) self.assertEqual("test", lines[2].category) self.assertFalse(lines[2].ok) def test_parses_file(self): sample = inspect.cleandoc( """1..2 ok 1 A passing test not ok 2 A failing test""" ) temp = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115 temp.write(sample.encode("utf-8")) temp.close() parser = Parser() lines = [] for line in parser.parse_file(temp.name): lines.append(line) self.assertEqual(3, len(lines)) self.assertEqual("plan", lines[0].category) self.assertEqual("test", lines[1].category) self.assertTrue(lines[1].ok) self.assertIsNone(lines[1].yaml_block) self.assertEqual("test", lines[2].category) self.assertFalse(lines[2].ok) def test_parses_yaml(self): sample = inspect.cleandoc( """TAP version 13 1..2 ok 1 A passing test --- test: sample yaml ... not ok 2 A failing test""" ) parser = Parser() lines = [] for line in parser.parse_text(sample): lines.append(line) if have_yaml: converted_yaml = yaml.safe_load("""test: sample yaml""") self.assertEqual(4, len(lines)) self.assertEqual(13, lines[0].version) self.assertEqual(converted_yaml, lines[2].yaml_block) self.assertEqual("test", lines[3].category) self.assertIsNone(lines[3].yaml_block) else: self.assertEqual(7, len(lines)) self.assertEqual(13, lines[0].version) for line_index in list(range(3, 6)): self.assertEqual("unknown", lines[line_index].category) self.assertEqual("test", lines[6].category) def test_parses_mixed(self): # Test that we can parse both a version 13 and earlier version files # using the same parser. Make sure that parsing works regardless of # the order of the incoming documents. sample_version_13 = inspect.cleandoc( """TAP version 13 1..2 ok 1 A passing version 13 test --- test: sample yaml ... not ok 2 A failing version 13 test""" ) sample_pre_13 = inspect.cleandoc( """1..2 ok 1 A passing pre-13 test not ok 2 A failing pre-13 test""" ) parser = Parser() lines = [] lines.extend(parser.parse_text(sample_version_13)) lines.extend(parser.parse_text(sample_pre_13)) if have_yaml: self.assertEqual(13, lines[0].version) self.assertEqual("A passing version 13 test", lines[2].description) self.assertEqual("A failing version 13 test", lines[3].description) self.assertEqual("A passing pre-13 test", lines[5].description) self.assertEqual("A failing pre-13 test", lines[6].description) else: self.assertEqual(13, lines[0].version) self.assertEqual("A passing version 13 test", lines[2].description) self.assertEqual("A failing version 13 test", lines[6].description) self.assertEqual("A passing pre-13 test", lines[8].description) self.assertEqual("A failing pre-13 test", lines[9].description) # Test parsing documents in reverse order parser = Parser() lines = [] lines.extend(parser.parse_text(sample_pre_13)) lines.extend(parser.parse_text(sample_version_13)) if have_yaml: self.assertEqual("A passing pre-13 test", lines[1].description) self.assertEqual("A failing pre-13 test", lines[2].description) self.assertEqual(13, lines[3].version) self.assertEqual("A passing version 13 test", lines[5].description) self.assertEqual("A failing version 13 test", lines[6].description) else: self.assertEqual("A passing pre-13 test", lines[1].description) self.assertEqual("A failing pre-13 test", lines[2].description) self.assertEqual(13, lines[3].version) self.assertEqual("A passing version 13 test", lines[5].description) self.assertEqual("A failing version 13 test", lines[9].description) def test_parses_yaml_no_end(self): sample = inspect.cleandoc( """TAP version 13 1..2 ok 1 A passing test --- test: sample yaml not ok 2 A failing test""" ) parser = Parser() lines = [] for line in parser.parse_text(sample): lines.append(line) if have_yaml: converted_yaml = yaml.safe_load("""test: sample yaml""") self.assertEqual(4, len(lines)) self.assertEqual(13, lines[0].version) self.assertEqual(converted_yaml, lines[2].yaml_block) self.assertEqual("test", lines[3].category) self.assertIsNone(lines[3].yaml_block) else: self.assertEqual(6, len(lines)) self.assertEqual(13, lines[0].version) for line_index in list(range(3, 5)): self.assertEqual("unknown", lines[line_index].category) self.assertEqual("test", lines[5].category) def test_parses_yaml_more_complex(self): sample = inspect.cleandoc( """TAP version 13 1..2 ok 1 A passing test --- message: test severity: fail data: got: - foo expect: - bar output: |- a multiline string must be handled properly even with | pipes | here > and: there last_nl: |+ there's a newline here -> """ ) parser = Parser() lines = [] for line in parser.parse_text(sample): lines.append(line) if have_yaml: converted_yaml = yaml.safe_load( r''' message: test severity: fail data: got: - foo expect: - bar output: "a multiline string\nmust be handled properly\neven with | pipes\n| here > and: there" last_nl: "there's a newline here ->\n"''' # noqa ) self.assertEqual(3, len(lines)) self.assertEqual(13, lines[0].version) self.assertEqual(converted_yaml, lines[2].yaml_block) else: self.assertEqual(19, len(lines)) self.assertEqual(13, lines[0].version) for line_index in list(range(3, 11)): self.assertEqual("unknown", lines[line_index].category) def test_parses_yaml_no_association(self): sample = inspect.cleandoc( """TAP version 13 1..2 ok 1 A passing test # Diagnostic line --- test: sample yaml ... not ok 2 A failing test""" ) parser = Parser() lines = [] for line in parser.parse_text(sample): lines.append(line) self.assertEqual(8, len(lines)) self.assertEqual(13, lines[0].version) self.assertIsNone(lines[2].yaml_block) self.assertEqual("diagnostic", lines[3].category) for line_index in list(range(4, 7)): self.assertEqual("unknown", lines[line_index].category) self.assertEqual("test", lines[7].category) def test_parses_yaml_no_start(self): sample = inspect.cleandoc( """TAP version 13 1..2 ok 1 A passing test test: sample yaml ... not ok 2 A failing test""" ) parser = Parser() lines = [] for line in parser.parse_text(sample): lines.append(line) self.assertEqual(6, len(lines)) self.assertEqual(13, lines[0].version) self.assertIsNone(lines[2].yaml_block) for line_index in list(range(3, 5)): self.assertEqual("unknown", lines[line_index].category) self.assertEqual("test", lines[5].category) def test_malformed_yaml(self): self.maxDiff = None sample = inspect.cleandoc( """TAP version 13 1..2 ok 1 A passing test --- test: sample yaml \tfail: tabs are not allowed! ... not ok 2 A failing test""" ) yaml_err = inspect.cleandoc( """ WARNING: Optional imports not found, TAP 13 output will be ignored. To parse yaml, see requirements in docs: https://tappy.readthedocs.io/en/latest/consumers.html#tap-version-13""" ) parser = Parser() lines = [] with captured_output() as (parse_out, _): for line in parser.parse_text(sample): lines.append(line) if have_yaml: self.assertEqual(4, len(lines)) self.assertEqual(13, lines[0].version) with captured_output() as (out, _): self.assertIsNone(lines[2].yaml_block) self.assertEqual( "Error parsing yaml block. Check formatting.", out.getvalue().strip() ) self.assertEqual("test", lines[3].category) self.assertIsNone(lines[3].yaml_block) else: self.assertEqual(8, len(lines)) self.assertEqual(13, lines[0].version) for line_index in list(range(3, 7)): self.assertEqual("unknown", lines[line_index].category) self.assertEqual("test", lines[7].category) self.assertEqual(yaml_err, parse_out.getvalue().strip()) def test_parse_empty_file(self): temp = tempfile.NamedTemporaryFile(delete=False) # noqa: SIM115 temp.close() parser = Parser() lines = [] for line in parser.parse_file(temp.name): lines.append(line) self.assertEqual(0, len(lines)) @mock.patch( "tap.parser.sys.stdin", StringIO( """1..2 ok 1 A passing test not ok 2 A failing test""" ), ) def test_parses_stdin(self): parser = Parser() lines = [] for line in parser.parse_stdin(): lines.append(line) self.assertEqual(3, len(lines)) self.assertEqual("plan", lines[0].category) self.assertEqual("test", lines[1].category) self.assertTrue(lines[1].ok) self.assertEqual("test", lines[2].category) self.assertFalse(lines[2].ok) tap_py-3.2.1/tests/test_result.py0000644000000000000000000000664613615410400014036 0ustar00import contextlib import os import unittest import unittest.case from tap.runner import TAPTestResult from tap.tests import TestCase from tap.tracker import Tracker class FakeTestCase(unittest.TestCase): def runTest(self): pass @contextlib.contextmanager def subTest(self, *args, **kwargs): try: self._subtest = unittest.case._SubTest(self, object(), {}) yield finally: self._subtest = None def __call__(self, result): pass class TestTAPTestResult(TestCase): @classmethod def _make_one(cls): # Yep, the stream is not being closed. stream = open(os.devnull, "w") # noqa: SIM115 result = TAPTestResult(stream, False, 0) result.tracker = Tracker() return result def test_adds_error(self): result = self._make_one() # Python 3 does some extra testing in unittest on exceptions so fake # the cause as if it were raised. ex = Exception() ex.__cause__ = None result.addError(FakeTestCase(), (None, ex, None)) self.assertEqual(len(result.tracker._test_cases["FakeTestCase"]), 1) def test_adds_failure(self): result = self._make_one() # Python 3 does some extra testing in unittest on exceptions so fake # the cause as if it were raised. ex = Exception() ex.__cause__ = None result.addFailure(FakeTestCase(), (None, ex, None)) self.assertEqual(len(result.tracker._test_cases["FakeTestCase"]), 1) def test_adds_success(self): result = self._make_one() result.addSuccess(FakeTestCase()) self.assertEqual(len(result.tracker._test_cases["FakeTestCase"]), 1) def test_adds_skip(self): result = self._make_one() result.addSkip(FakeTestCase(), "a reason") self.assertEqual(len(result.tracker._test_cases["FakeTestCase"]), 1) def test_adds_expected_failure(self): exc = self.factory.make_exc() result = self._make_one() result.addExpectedFailure(FakeTestCase(), exc) line = result.tracker._test_cases["FakeTestCase"][0] self.assertFalse(line.ok) self.assertEqual(line.directive.text, "TODO {}".format("(expected failure)")) def test_adds_unexpected_success(self): result = self._make_one() result.addUnexpectedSuccess(FakeTestCase()) line = result.tracker._test_cases["FakeTestCase"][0] self.assertTrue(line.ok) self.assertEqual(line.directive.text, "TODO {}".format("(unexpected success)")) def test_adds_subtest_success(self): """Test that the runner handles subtest success results.""" result = self._make_one() test = FakeTestCase() with test.subTest(): result.addSubTest(test, test._subtest, None) line = result.tracker._test_cases["FakeTestCase"][0] self.assertTrue(line.ok) def test_adds_subtest_failure(self): """Test that the runner handles subtest failure results.""" result = self._make_one() # Python 3 does some extra testing in unittest on exceptions so fake # the cause as if it were raised. ex = Exception() ex.__cause__ = None test = FakeTestCase() with test.subTest(): result.addSubTest(test, test._subtest, (ex.__class__, ex, None)) line = result.tracker._test_cases["FakeTestCase"][0] self.assertFalse(line.ok) tap_py-3.2.1/tests/test_rules.py0000644000000000000000000000534113615410400013641 0ustar00import unittest from tap.rules import Rules from tap.tests import TestCase class TestRules(TestCase): """Tests for tap.rules.Rules""" def _make_one(self): self.suite = unittest.TestSuite() return Rules("foobar.tap", self.suite) def test_handles_skipping_plan(self): skip_plan = self.factory.make_plan(directive_text="Skip on Mondays.") rules = self._make_one() rules.handle_skipping_plan(skip_plan) self.assertEqual(1, len(self.suite._tests)) self.assertEqual("Skip on Mondays.", self.suite._tests[0]._line.description) def test_tracks_plan_line(self): plan = self.factory.make_plan() rules = self._make_one() rules.saw_plan(plan, 28) self.assertEqual(rules._lines_seen["plan"][0][0], plan) self.assertEqual(rules._lines_seen["plan"][0][1], 28) def test_errors_plan_not_at_end(self): plan = self.factory.make_plan() rules = self._make_one() rules.saw_plan(plan, 41) rules.check(42) self.assertEqual( "A plan must appear at the beginning or end of the file.", self.suite._tests[0]._line.description, ) def test_requires_plan(self): rules = self._make_one() rules.check(42) self.assertEqual("Missing a plan.", self.suite._tests[0]._line.description) def test_only_one_plan(self): plan = self.factory.make_plan() rules = self._make_one() rules.saw_plan(plan, 41) rules.saw_plan(plan, 42) rules.check(42) self.assertEqual( "Only one plan line is permitted per file.", self.suite._tests[0]._line.description, ) def test_plan_line_two(self): """A plan may appear on line 2 when line 1 is a version line.""" rules = self._make_one() rules.saw_version_at(1) valid = rules._plan_on_valid_line(at_line=2, final_line_count=42) self.assertTrue(valid) def test_errors_when_expected_tests_differs_from_actual(self): plan = self.factory.make_plan(expected_tests=42) rules = self._make_one() rules.saw_plan(plan, 1) rules.saw_test() rules.check(2) self.assertEqual( f"Expected {42} tests but only {1} ran.", self.suite._tests[0]._line.description, ) def test_errors_on_bail(self): bail = self.factory.make_bail(reason="Missing something important.") rules = self._make_one() rules.handle_bail(bail) self.assertEqual(1, len(self.suite._tests)) self.assertEqual( "Bailed: {reason}".format(reason="Missing something important."), self.suite._tests[0]._line.description, ) tap_py-3.2.1/tests/test_runner.py0000644000000000000000000000650413615410400014022 0ustar00import os import sys import tempfile import unittest from unittest import mock from tap import TAPTestRunner from tap.runner import TAPTestResult, _tracker class TestTAPTestRunner(unittest.TestCase): def test_has_tap_test_result(self): runner = TAPTestRunner() self.assertEqual(runner.resultclass, TAPTestResult) def test_runner_uses_outdir(self): """Test that the test runner sets the outdir so that TAP files will be written to that location. Setting class attributes to get the right behavior is a dirty hack, but the unittest classes aren't very extensible. """ # Save the previous outdir in case **this** execution was using it. previous_outdir = _tracker.outdir outdir = tempfile.mkdtemp() TAPTestRunner.set_outdir(outdir) self.assertEqual(outdir, _tracker.outdir) _tracker.outdir = previous_outdir def test_runner_uses_format(self): """Test that format is set on TAPTestResult FORMAT.""" # Save the previous format in case **this** execution was using it. previous_format = TAPTestResult.FORMAT fmt = "{method_name}: {short_description}" TAPTestRunner.set_format(fmt) self.assertEqual(fmt, TAPTestResult.FORMAT) TAPTestResult.FORMAT = previous_format def test_runner_uses_combined(self): """Test that output is combined.""" # Save previous combined in case **this** execution was using it. previous_combined = _tracker.combined TAPTestRunner.set_combined(True) self.assertTrue(_tracker.combined) _tracker.combined = previous_combined @mock.patch("sys.exit") def test_bad_format_string(self, fake_exit): """A bad format string exits the runner.""" previous_format = TAPTestResult.FORMAT bad_format = "Not gonna work {sort_desc}" TAPTestRunner.set_format(bad_format) result = TAPTestResult(None, True, 1) test = mock.Mock() result._description(test) self.assertTrue(fake_exit.called) TAPTestResult.FORMAT = previous_format def test_runner_sets_tracker_for_streaming(self): """The tracker is set for streaming mode.""" previous_streaming = _tracker.streaming previous_stream = _tracker.stream runner = TAPTestRunner() runner.set_stream(True) self.assertTrue(_tracker.streaming) self.assertTrue(_tracker.stream, sys.stdout) _tracker.streaming = previous_streaming _tracker.stream = previous_stream def test_runner_stream_to_devnull_for_streaming(self): previous_streaming = _tracker.streaming previous_stream = _tracker.stream runner = TAPTestRunner() runner.set_stream(True) self.assertTrue(runner.stream.stream.name, os.devnull) _tracker.streaming = previous_streaming _tracker.stream = previous_stream def test_runner_uses_header(self): """Test that the case header can be turned off.""" # Save previous header in case **this** execution was using it. previous_header = _tracker.header TAPTestRunner.set_header(False) self.assertFalse(_tracker.header) TAPTestRunner.set_header(True) self.assertTrue(_tracker.header) _tracker.header = previous_header tap_py-3.2.1/tests/test_tracker.py0000644000000000000000000002606113615410400014144 0ustar00import inspect import os import tempfile from io import StringIO from unittest import mock from tap.tests import TestCase from tap.tracker import Tracker class TestTracker(TestCase): def _make_header(self, test_case): return f"# TAP results for {test_case}" def test_has_test_cases(self): tracker = Tracker() self.assertEqual(tracker._test_cases, {}) def test_tracks_class(self): tracker = Tracker() tracker._track("FakeTestClass") self.assertEqual(tracker._test_cases.get("FakeTestClass"), []) def test_adds_ok(self): tracker = Tracker() tracker.add_ok("FakeTestCase", "a description") line = tracker._test_cases["FakeTestCase"][0] self.assertTrue(line.ok) self.assertEqual(line.description, "a description") def test_adds_not_ok(self): tracker = Tracker() tracker.add_not_ok("FakeTestCase", "a description") line = tracker._test_cases["FakeTestCase"][0] self.assertFalse(line.ok) self.assertEqual(line.description, "a description") def test_adds_skip(self): tracker = Tracker() tracker.add_skip("FakeTestCase", "a description", "a reason") line = tracker._test_cases["FakeTestCase"][0] self.assertTrue(line.ok) self.assertEqual(line.description, "a description") self.assertEqual(line.directive.text, "SKIP a reason") def test_generates_tap_reports_in_new_outdir(self): tempdir = tempfile.mkdtemp() outdir = os.path.join(tempdir, "non", "existent", "path") tracker = Tracker(outdir=outdir) tracker.add_ok("FakeTestCase", "I should be in the specified dir.") tracker.generate_tap_reports() tap_file = os.path.join(outdir, "FakeTestCase.tap") self.assertTrue(os.path.exists(tap_file)) def test_generates_tap_reports_in_existing_outdir(self): outdir = tempfile.mkdtemp() tracker = Tracker(outdir=outdir) tracker.add_ok("FakeTestCase", "I should be in the specified dir.") tracker.generate_tap_reports() tap_file = os.path.join(outdir, "FakeTestCase.tap") self.assertTrue(os.path.exists(tap_file)) def test_results_not_combined_by_default(self): tracker = Tracker() self.assertFalse(tracker.combined) def test_individual_report_has_no_plan_when_combined(self): outdir = tempfile.mkdtemp() tracker = Tracker(outdir=outdir, combined=True) tracker.add_ok("FakeTestCase", "Look ma, no plan!") out_file = StringIO() tracker.generate_tap_report( "FakeTestCase", tracker._test_cases["FakeTestCase"], out_file ) report = out_file.getvalue() self.assertTrue("Look ma" in report) self.assertFalse("1.." in report) @mock.patch("tap.tracker.ENABLE_VERSION_13", False) def test_combined_results_in_one_file_tap_version_12(self): outdir = tempfile.mkdtemp() tracker = Tracker(outdir=outdir, combined=True) tracker.add_ok("FakeTestCase", "YESSS!") tracker.add_ok("DifferentFakeTestCase", "GOAAL!") tracker.generate_tap_reports() self.assertFalse(os.path.exists(os.path.join(outdir, "FakeTestCase.tap"))) self.assertFalse( os.path.exists(os.path.join(outdir, "DifferentFakeTestCase.tap")) ) with open(os.path.join(outdir, "testresults.tap")) as f: report = f.read() expected = inspect.cleandoc( """{header_1} ok 1 YESSS! {header_2} ok 2 GOAAL! 1..2 """.format( header_1=self._make_header("FakeTestCase"), header_2=self._make_header("DifferentFakeTestCase"), ) ) self.assertEqual(report.strip(), expected) @mock.patch("tap.tracker.ENABLE_VERSION_13", True) def test_combined_results_in_one_file_tap_version_13(self): outdir = tempfile.mkdtemp() tracker = Tracker(outdir=outdir, combined=True) tracker.add_ok("FakeTestCase", "YESSS!") tracker.add_ok("DifferentFakeTestCase", "GOAAL!") tracker.generate_tap_reports() self.assertFalse(os.path.exists(os.path.join(outdir, "FakeTestCase.tap"))) self.assertFalse( os.path.exists(os.path.join(outdir, "DifferentFakeTestCase.tap")) ) with open(os.path.join(outdir, "testresults.tap")) as f: report = f.read() expected = inspect.cleandoc( """ TAP version 13 {header_1} ok 1 YESSS! {header_2} ok 2 GOAAL! 1..2 """.format( header_1=self._make_header("FakeTestCase"), header_2=self._make_header("DifferentFakeTestCase"), ) ) self.assertEqual(report.strip(), expected) def test_tracker_does_not_stream_by_default(self): tracker = Tracker() self.assertFalse(tracker.streaming) def test_tracker_has_stream(self): tracker = Tracker() self.assertTrue(tracker.stream is None) @mock.patch("tap.tracker.ENABLE_VERSION_13", False) def test_add_ok_writes_to_stream_while_streaming(self): stream = StringIO() tracker = Tracker(streaming=True, stream=stream) tracker.add_ok("FakeTestCase", "YESSS!") tracker.add_ok("AnotherTestCase", "Sure.") expected = inspect.cleandoc( """{header_1} ok 1 YESSS! {header_2} ok 2 Sure. """.format( header_1=self._make_header("FakeTestCase"), header_2=self._make_header("AnotherTestCase"), ) ) self.assertEqual(stream.getvalue().strip(), expected) @mock.patch("tap.tracker.ENABLE_VERSION_13", False) def test_add_not_ok_writes_to_stream_while_streaming(self): stream = StringIO() tracker = Tracker(streaming=True, stream=stream) tracker.add_not_ok("FakeTestCase", "YESSS!") expected = inspect.cleandoc( """{header} not ok 1 YESSS! """.format(header=self._make_header("FakeTestCase")) ) self.assertEqual(stream.getvalue().strip(), expected) @mock.patch("tap.tracker.ENABLE_VERSION_13", False) def test_add_skip_writes_to_stream_while_streaming(self): stream = StringIO() tracker = Tracker(streaming=True, stream=stream) tracker.add_skip("FakeTestCase", "YESSS!", "a reason") expected = inspect.cleandoc( """{header} ok 1 YESSS! # SKIP a reason """.format(header=self._make_header("FakeTestCase")) ) self.assertEqual(stream.getvalue().strip(), expected) def test_streaming_does_not_write_files(self): outdir = tempfile.mkdtemp() stream = StringIO() tracker = Tracker(outdir=outdir, streaming=True, stream=stream) tracker.add_ok("FakeTestCase", "YESSS!") tracker.generate_tap_reports() self.assertFalse(os.path.exists(os.path.join(outdir, "FakeTestCase.tap"))) @mock.patch("tap.tracker.ENABLE_VERSION_13", False) def test_streaming_writes_plan(self): stream = StringIO() tracker = Tracker(streaming=True, stream=stream) tracker.combined_line_number = 42 tracker.generate_tap_reports() self.assertEqual(stream.getvalue(), "1..42\n") @mock.patch("tap.tracker.ENABLE_VERSION_13", False) def test_write_plan_first_streaming(self): outdir = tempfile.mkdtemp() stream = StringIO() tracker = Tracker(outdir=outdir, streaming=True, stream=stream) tracker.set_plan(123) tracker.add_ok("FakeTestCase", "YESSS!") tracker.generate_tap_reports() self.assertEqual( stream.getvalue(), "1..123\n{header}\nok 1 YESSS!\n".format( header=self._make_header("FakeTestCase") ), ) self.assertFalse(os.path.exists(os.path.join(outdir, "FakeTestCase.tap"))) @mock.patch("tap.tracker.ENABLE_VERSION_13", False) def test_write_plan_immediate_streaming(self): stream = StringIO() Tracker(streaming=True, stream=stream, plan=123) self.assertEqual(stream.getvalue(), "1..123\n") @mock.patch("tap.tracker.ENABLE_VERSION_13", False) def test_write_plan_first_combined(self): outdir = tempfile.mkdtemp() tracker = Tracker(streaming=False, outdir=outdir, combined=True) tracker.set_plan(123) tracker.generate_tap_reports() with open(os.path.join(outdir, "testresults.tap")) as f: lines = f.readlines() self.assertEqual(lines[0], "1..123\n") @mock.patch("tap.tracker.ENABLE_VERSION_13", False) def test_write_plan_first_not_combined(self): outdir = tempfile.mkdtemp() tracker = Tracker(streaming=False, outdir=outdir, combined=False) with self.assertRaises(ValueError): tracker.set_plan(123) @mock.patch("tap.tracker.ENABLE_VERSION_13", True) def test_streaming_writes_tap_version_13(self): stream = StringIO() tracker = Tracker(streaming=True, stream=stream) tracker.add_skip("FakeTestCase", "YESSS!", "a reason") expected = inspect.cleandoc( """ TAP version 13 {header} ok 1 YESSS! # SKIP a reason """.format(header=self._make_header("FakeTestCase")) ) self.assertEqual(stream.getvalue().strip(), expected) def test_get_default_tap_file_path(self): tracker = Tracker() file_path = tracker._get_tap_file_path("foo") self.assertEqual("foo.tap", file_path) def test_sanitizes_tap_file_path(self): tracker = Tracker() file_path = tracker._get_tap_file_path("an awful \\ testcase / name\n") self.assertEqual("an-awful---testcase---name-.tap", file_path) def test_adds_ok_with_diagnostics(self): tracker = Tracker() tracker.add_ok("FakeTestCase", "a description", diagnostics="# more info\n") line = tracker._test_cases["FakeTestCase"][0] self.assertEqual("# more info\n", line.diagnostics) def test_adds_not_ok_with_diagnostics(self): tracker = Tracker() tracker.add_not_ok("FakeTestCase", "a description", diagnostics="# more info\n") line = tracker._test_cases["FakeTestCase"][0] self.assertEqual("# more info\n", line.diagnostics) def test_header_displayed_by_default(self): tracker = Tracker() self.assertTrue(tracker.header) def test_header_set_by_init(self): tracker = Tracker(header=False) self.assertFalse(tracker.header) @mock.patch("tap.tracker.ENABLE_VERSION_13", False) def test_does_not_write_header(self): stream = StringIO() tracker = Tracker(streaming=True, stream=stream, header=False) tracker.add_skip("FakeTestCase", "YESSS!", "a reason") expected = inspect.cleandoc( """ ok 1 YESSS! # SKIP a reason """ ) self.assertEqual(stream.getvalue().strip(), expected) tap_py-3.2.1/.gitignore0000644000000000000000000000065213615410400011724 0ustar00*.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 __pycache__ # Installer logs pip-log.txt # Unit test / coverage reports .coverage coverage.xml .tox nosetests.xml htmlcov .cache .pytest_cache # Mr Developer .mr.developer.cfg .project .pydevproject # Dev *.swp .vscode # TAP *.tap # docs docs/_build # virtualenv include/ local/ venv/ tap_py-3.2.1/AUTHORS0000644000000000000000000000056213615410400011004 0ustar00tappy was originally created by Matt Layman. Contributors ------------ * Adeodato Simó * Allison Karlitskaya * Andrew McNamara * Chris Clarke * Cody D'Ambrosio * Erik Cederstrand * Marc Abramowitz * Mark E. Hamilton * Matt Layman * meejah (https://meejah.ca) * Michael F. Lamb (http://datagrok.org) * Nicolas Caniart * Richard Bosworth * Ross Burton * Simon McVittie tap_py-3.2.1/LICENSE0000644000000000000000000000247613615410400010747 0ustar00Copyright (c) 2019, Matt Layman and contributors. See AUTHORS for more details. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. tap_py-3.2.1/pyproject.toml0000644000000000000000000000347413615410400012655 0ustar00[project] name = "tap.py" version = "3.2.1" description = "Test Anything Protocol (TAP) tools" readme = "docs/releases.rst" license = {text = "BSD"} authors = [ { name = "Matt Layman", email = "matthewlayman@gmail.com" } ] homepage = "https://github.com/python-tap/tappy" requires-python = ">=3.9" dependencies = [] keywords = ["TAP", "unittest"] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Testing" ] [project.optional-dependencies] yaml = ["more-itertools", "PyYAML>=5.1"] [project.scripts] tappy = "tap.main:main" tap = "tap.main:main" [dependency-groups] dev = [ "coverage>=7.6.10", "pytest>=8.3.4", "ruff>=0.9.3", "sphinx>=7.4.7", "tox>=4.24.1", # These are the optional dependencies to enable TAP version 13 support. "more-itertools>=10.6.0", "pyyaml>=6.0.2", ] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.sdist] include = [ "/src", "/docs", "/tests", ] sources = ["src"] [tool.hatch.build.targets.wheel] sources = ["src"] packages = ["tap"] [tool.pytest.ini_options] pythonpath = [".", "src"] [tool.ruff.lint] select = [ # pycodestyle "E", "W", # Pyflakes "F", # pyupgrade "UP", # flake8-bandit "S", # flake8-bugbear "B", # flake8-simplify "SIM", # isort "I", ] ignore = [ # bandit: Use of `assert` detected "S101", ] tap_py-3.2.1/docs/releases.rst0000644000000000000000000001530013615410400013215 0ustar00tappy is a set of tools for working with the `Test Anything Protocol (TAP) `_, a line based test protocol for recording test data in a standard way. Follow tappy development on `GitHub `_. Developer documentation is on `Read the Docs `_. Releases ======== Version 3.2, Released January 25, 2025 -------------------------------------- * Drop support for Python 3.6 (it is end-of-life). * Drop support for Python 3.7 (it is end-of-life). * Drop support for Python 3.8 (it is end-of-life). * Add support for Python 3.11. * Add support for Python 3.12. * Add support for Python 3.13. * Add support for adding diagnostics to ok test. Version 3.1, Released December 29, 2021 --------------------------------------- * Add support for Python 3.10. * Add support for Python 3.9. * Add support for Python 3.8. * Drop support for Python 3.5 (it is end-of-life). * Fix parsing of multi-line strings in YAML blocks (#111) * Remove unmaintained i18n support. Version 3.0, Released January 10, 2020 -------------------------------------- * Drop support for Python 2 (it is end-of-life). * Add support for subtests. * Run a test suite with ``python -m tap``. * Discontinue use of Pipenv for managing development. Version 2.6.2, Released October 20, 2019 ---------------------------------------- * Fix bug in streaming mode that would generate tap files when the plan was already set (affected pytest). Version 2.6.1, Released September 17, 2019 ------------------------------------------ * Fix TAP version 13 support from more-itertools behavior change. Version 2.6, Released September 16, 2019 ---------------------------------------- * Add support for Python 3.7. * Drop support for Python 3.4 (it is end-of-life). Version 2.5, Released September 15, 2018 ---------------------------------------- * Add ``set_plan`` to ``Tracker`` which allows producing the ``1..N`` plan line before any tests. * Switch code style to use Black formatting. Version 2.4, Released May 29, 2018 ---------------------------------- * Add support for producing TAP version 13 output to streaming and file reports by including the ``TAP version 13`` line. Version 2.3, Released May 15, 2018 ---------------------------------- * Add optional method to install tappy for YAML support with ``pip install tap.py[yaml]``. * Make tappy version 13 compliant by adding support for parsing YAML blocks. * ``unittest.expectedFailure`` now uses a TODO directive to better align with the specification. Version 2.2, Released January 7, 2018 ------------------------------------- * Add support for Python 3.6. * Drop support for Python 3.3 (it is end-of-life). * Use Pipenv for managing development. * Switch to pytest as the development test runner. Version 2.1, Released September 23, 2016 ---------------------------------------- * Add ``Parser.parse_text`` to parse TAP provided as a string. Version 2.0, Released July 31, 2016 ----------------------------------- * Remove nose plugin. The plugin moved to the ``nose-tap`` distribution. * Remove pytest plugin. The plugin moved to the ``pytest-tap`` distribution. * Remove Pygments syntax highlighting plugin. The plugin was merged upstream directly into the Pygments project and is available without tappy. * Drop support for Python 2.6. Version 1.9, Released March 28, 2016 ------------------------------------ * ``TAPTestRunner`` has a ``set_header`` method to enable or disable test case header ouput in the TAP stream. * Add support for Python 3.5. * Perform continuous integration testing on OS X. * Drop support for Python 3.2. Version 1.8, Released November 30, 2015 --------------------------------------- * The ``tappy`` TAP consumer can read a TAP stream directly from STDIN. * Tracebacks are included as diagnostic output for failures and errors. * The ``tappy`` TAP consumer has an alternative, shorter name of ``tap``. * The pytest plugin now defaults to no output unless provided a flag. Users dependent on the old default behavior can use ``--tap-files`` to achieve the same results. * Translated into Arabic. * Translated into Chinese. * Translated into Japanese. * Translated into Russian. * Perform continuous integration testing on Windows with AppVeyor. * Improve unit test coverage to 100%. Version 1.7, Released August 19, 2015 ------------------------------------- * Provide a plugin to integrate with pytest. * Document some viable alternatives to tappy. * Translated into German. * Translated into Portuguese. Version 1.6, Released June 18, 2015 ----------------------------------- * ``TAPTestRunner`` has a ``set_stream`` method to stream all TAP output directly to an output stream instead of a file. results in a single output file. * The ``nosetests`` plugin has an optional ``--tap-stream`` flag to stream all TAP output directly to an output stream instead of a file. * tappy is now internationalized. It is translated into Dutch, French, Italian, and Spanish. * tappy is available as a Python wheel package, the new Python packaging standard. Version 1.5, Released May 18, 2015 ---------------------------------- * ``TAPTestRunner`` has a ``set_combined`` method to collect all results in a single output file. * The ``nosetests`` plugin has an optional ``--tap-combined`` flag to collect all results in a single output file. * ``TAPTestRunner`` has a ``set_format`` method to specify line format. * The ``nosetests`` plugin has an optional ``--tap-format`` flag to specify line format. Version 1.4, Released April 4, 2015 ----------------------------------- * Update ``setup.py`` to support Debian packaging. Include man page. Version 1.3, Released January 9, 2015 ------------------------------------- * The ``tappy`` command line tool is available as a TAP consumer. * The ``Parser`` and ``Loader`` are available as APIs for programmatic handling of TAP files and data. Version 1.2, Released December 21, 2014 --------------------------------------- * Provide a syntax highlighter for Pygments so any project using Pygments (e.g., Sphinx) can highlight TAP output. Version 1.1, Released October 23, 2014 -------------------------------------- * ``TAPTestRunner`` has a ``set_outdir`` method to specify where to store ``.tap`` files. * The ``nosetests`` plugin has an optional ``--tap-outdir`` flag to specify where to store ``.tap`` files. * tappy has backported support for Python 2.6. * tappy has support for Python 3.2, 3.3, and 3.4. * tappy has support for PyPy. Version 1.0, Released March 16, 2014 ------------------------------------ * Initial release of tappy * ``TAPTestRunner`` - A test runner for ``unittest`` modules that generates TAP files. * Provides a plugin for integrating with **nose**. tap_py-3.2.1/PKG-INFO0000644000000000000000000001715313615410400011035 0ustar00Metadata-Version: 2.4 Name: tap.py Version: 3.2.1 Summary: Test Anything Protocol (TAP) tools Author-email: Matt Layman License: BSD License-File: AUTHORS License-File: LICENSE Keywords: TAP,unittest Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Software Development :: Testing Requires-Python: >=3.9 Provides-Extra: yaml Requires-Dist: more-itertools; extra == 'yaml' Requires-Dist: pyyaml>=5.1; extra == 'yaml' Description-Content-Type: text/x-rst tappy is a set of tools for working with the `Test Anything Protocol (TAP) `_, a line based test protocol for recording test data in a standard way. Follow tappy development on `GitHub `_. Developer documentation is on `Read the Docs `_. Releases ======== Version 3.2, Released January 25, 2025 -------------------------------------- * Drop support for Python 3.6 (it is end-of-life). * Drop support for Python 3.7 (it is end-of-life). * Drop support for Python 3.8 (it is end-of-life). * Add support for Python 3.11. * Add support for Python 3.12. * Add support for Python 3.13. * Add support for adding diagnostics to ok test. Version 3.1, Released December 29, 2021 --------------------------------------- * Add support for Python 3.10. * Add support for Python 3.9. * Add support for Python 3.8. * Drop support for Python 3.5 (it is end-of-life). * Fix parsing of multi-line strings in YAML blocks (#111) * Remove unmaintained i18n support. Version 3.0, Released January 10, 2020 -------------------------------------- * Drop support for Python 2 (it is end-of-life). * Add support for subtests. * Run a test suite with ``python -m tap``. * Discontinue use of Pipenv for managing development. Version 2.6.2, Released October 20, 2019 ---------------------------------------- * Fix bug in streaming mode that would generate tap files when the plan was already set (affected pytest). Version 2.6.1, Released September 17, 2019 ------------------------------------------ * Fix TAP version 13 support from more-itertools behavior change. Version 2.6, Released September 16, 2019 ---------------------------------------- * Add support for Python 3.7. * Drop support for Python 3.4 (it is end-of-life). Version 2.5, Released September 15, 2018 ---------------------------------------- * Add ``set_plan`` to ``Tracker`` which allows producing the ``1..N`` plan line before any tests. * Switch code style to use Black formatting. Version 2.4, Released May 29, 2018 ---------------------------------- * Add support for producing TAP version 13 output to streaming and file reports by including the ``TAP version 13`` line. Version 2.3, Released May 15, 2018 ---------------------------------- * Add optional method to install tappy for YAML support with ``pip install tap.py[yaml]``. * Make tappy version 13 compliant by adding support for parsing YAML blocks. * ``unittest.expectedFailure`` now uses a TODO directive to better align with the specification. Version 2.2, Released January 7, 2018 ------------------------------------- * Add support for Python 3.6. * Drop support for Python 3.3 (it is end-of-life). * Use Pipenv for managing development. * Switch to pytest as the development test runner. Version 2.1, Released September 23, 2016 ---------------------------------------- * Add ``Parser.parse_text`` to parse TAP provided as a string. Version 2.0, Released July 31, 2016 ----------------------------------- * Remove nose plugin. The plugin moved to the ``nose-tap`` distribution. * Remove pytest plugin. The plugin moved to the ``pytest-tap`` distribution. * Remove Pygments syntax highlighting plugin. The plugin was merged upstream directly into the Pygments project and is available without tappy. * Drop support for Python 2.6. Version 1.9, Released March 28, 2016 ------------------------------------ * ``TAPTestRunner`` has a ``set_header`` method to enable or disable test case header ouput in the TAP stream. * Add support for Python 3.5. * Perform continuous integration testing on OS X. * Drop support for Python 3.2. Version 1.8, Released November 30, 2015 --------------------------------------- * The ``tappy`` TAP consumer can read a TAP stream directly from STDIN. * Tracebacks are included as diagnostic output for failures and errors. * The ``tappy`` TAP consumer has an alternative, shorter name of ``tap``. * The pytest plugin now defaults to no output unless provided a flag. Users dependent on the old default behavior can use ``--tap-files`` to achieve the same results. * Translated into Arabic. * Translated into Chinese. * Translated into Japanese. * Translated into Russian. * Perform continuous integration testing on Windows with AppVeyor. * Improve unit test coverage to 100%. Version 1.7, Released August 19, 2015 ------------------------------------- * Provide a plugin to integrate with pytest. * Document some viable alternatives to tappy. * Translated into German. * Translated into Portuguese. Version 1.6, Released June 18, 2015 ----------------------------------- * ``TAPTestRunner`` has a ``set_stream`` method to stream all TAP output directly to an output stream instead of a file. results in a single output file. * The ``nosetests`` plugin has an optional ``--tap-stream`` flag to stream all TAP output directly to an output stream instead of a file. * tappy is now internationalized. It is translated into Dutch, French, Italian, and Spanish. * tappy is available as a Python wheel package, the new Python packaging standard. Version 1.5, Released May 18, 2015 ---------------------------------- * ``TAPTestRunner`` has a ``set_combined`` method to collect all results in a single output file. * The ``nosetests`` plugin has an optional ``--tap-combined`` flag to collect all results in a single output file. * ``TAPTestRunner`` has a ``set_format`` method to specify line format. * The ``nosetests`` plugin has an optional ``--tap-format`` flag to specify line format. Version 1.4, Released April 4, 2015 ----------------------------------- * Update ``setup.py`` to support Debian packaging. Include man page. Version 1.3, Released January 9, 2015 ------------------------------------- * The ``tappy`` command line tool is available as a TAP consumer. * The ``Parser`` and ``Loader`` are available as APIs for programmatic handling of TAP files and data. Version 1.2, Released December 21, 2014 --------------------------------------- * Provide a syntax highlighter for Pygments so any project using Pygments (e.g., Sphinx) can highlight TAP output. Version 1.1, Released October 23, 2014 -------------------------------------- * ``TAPTestRunner`` has a ``set_outdir`` method to specify where to store ``.tap`` files. * The ``nosetests`` plugin has an optional ``--tap-outdir`` flag to specify where to store ``.tap`` files. * tappy has backported support for Python 2.6. * tappy has support for Python 3.2, 3.3, and 3.4. * tappy has support for PyPy. Version 1.0, Released March 16, 2014 ------------------------------------ * Initial release of tappy * ``TAPTestRunner`` - A test runner for ``unittest`` modules that generates TAP files. * Provides a plugin for integrating with **nose**.