Retire repo
This repo was created by accident, use deb-python-os-testr instead. Needed-By: I1ac1a06931c8b6dd7c2e73620a0302c29e605f03 Change-Id: I81894aea69b9d09b0977039623c26781093a397a
This commit is contained in:
parent
083faa0741
commit
946b07d23b
@ -1,7 +0,0 @@
|
||||
[run]
|
||||
branch = True
|
||||
source = os_testr
|
||||
omit = os_testr/tests/*,os_testr/openstack/*
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
54
.gitignore
vendored
54
.gitignore
vendored
@ -1,54 +0,0 @@
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Packages
|
||||
*.egg*
|
||||
dist
|
||||
build
|
||||
eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
.installed.cfg
|
||||
lib
|
||||
lib64
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
cover/
|
||||
.coverage*
|
||||
!.coveragerc
|
||||
.tox
|
||||
nosetests.xml
|
||||
.testrepository
|
||||
.venv
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
|
||||
# Complexity
|
||||
output/*.html
|
||||
output/*/index.html
|
||||
|
||||
# Sphinx
|
||||
doc/build
|
||||
|
||||
# pbr generates these
|
||||
AUTHORS
|
||||
ChangeLog
|
||||
|
||||
# Editors
|
||||
*~
|
||||
.*.swp
|
||||
.*sw?
|
@ -1,4 +0,0 @@
|
||||
[gerrit]
|
||||
host=review.openstack.org
|
||||
port=29418
|
||||
project=openstack/os-testr.git
|
3
.mailmap
3
.mailmap
@ -1,3 +0,0 @@
|
||||
# Format is:
|
||||
# <preferred e-mail> <other e-mail 1>
|
||||
# <preferred e-mail> <other e-mail 2>
|
@ -1,7 +0,0 @@
|
||||
[DEFAULT]
|
||||
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
|
||||
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
|
||||
OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
|
||||
${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
@ -1,16 +0,0 @@
|
||||
If you would like to contribute to the development of OpenStack,
|
||||
you must follow the steps in this page:
|
||||
|
||||
http://docs.openstack.org/infra/manual/developers.html
|
||||
|
||||
Once those steps have been completed, changes to OpenStack
|
||||
should be submitted for review via the Gerrit tool, following
|
||||
the workflow documented at:
|
||||
|
||||
http://docs.openstack.org/infra/manual/developers.html#development-workflow
|
||||
|
||||
Pull requests submitted through GitHub will be ignored.
|
||||
|
||||
Bugs should be filed on Launchpad, not GitHub:
|
||||
|
||||
https://bugs.launchpad.net/os-testr
|
@ -1,4 +0,0 @@
|
||||
os-testr Style Commandments
|
||||
===============================================
|
||||
|
||||
Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/
|
176
LICENSE
176
LICENSE
@ -1,176 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
@ -1,6 +0,0 @@
|
||||
include AUTHORS
|
||||
include ChangeLog
|
||||
exclude .gitignore
|
||||
exclude .gitreview
|
||||
|
||||
global-exclude *.pyc
|
28
README.rst
28
README.rst
@ -1,28 +0,0 @@
|
||||
========
|
||||
os-testr
|
||||
========
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/os-testr.svg
|
||||
:target: https://pypi.python.org/pypi/os-testr/
|
||||
:alt: Latest Version
|
||||
|
||||
.. image:: https://img.shields.io/pypi/dm/os-testr.svg
|
||||
:target: https://pypi.python.org/pypi/os-testr/
|
||||
:alt: Downloads
|
||||
|
||||
A testr wrapper to provide functionality for OpenStack projects.
|
||||
|
||||
* Free software: Apache license
|
||||
* Documentation: http://docs.openstack.org/developer/os-testr
|
||||
* Source: http://git.openstack.org/cgit/openstack/os-testr
|
||||
* Bugs: http://bugs.launchpad.net/os-testr
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
* ``ostestr``: a testr wrapper that uses subunit-trace for output and builds
|
||||
some helpful extra functionality around testr
|
||||
* ``subunit-trace``: an output filter for a subunit stream which provides
|
||||
useful information about the run
|
||||
* ``subunit2html``: generates a test results html page from a subunit stream
|
||||
* ``generate-subunit``: generate a subunit stream for a single test
|
13
README.txt
Normal file
13
README.txt
Normal file
@ -0,0 +1,13 @@
|
||||
This project is no longer maintained.
|
||||
|
||||
The contents of this repository are still available in the Git
|
||||
source code management system. To see the contents of this
|
||||
repository before it reached its end of life, please check out the
|
||||
previous commit with "git checkout HEAD^1".
|
||||
|
||||
Use instead the project deb-python-os-testr at
|
||||
http://git.openstack.org/cgit/openstack/deb-python-os-testr .
|
||||
|
||||
For any further questions, please email
|
||||
openstack-dev@lists.openstack.org or join #openstack-dev on
|
||||
Freenode.
|
@ -1,86 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath('../..'))
|
||||
# -- General configuration ----------------------------------------------------
|
||||
|
||||
# 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',
|
||||
# 'sphinx.ext.intersphinx',
|
||||
'oslosphinx'
|
||||
]
|
||||
|
||||
# autodoc generation is a bit aggressive and a nuisance when doing heavy
|
||||
# text edit cycles.
|
||||
# execute "export SPHINX_DEBUG=1" in your terminal to disable
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'os-testr'
|
||||
copyright = u'2015, Matthew Treinish'
|
||||
|
||||
# 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
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# -- Options for HTML output --------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
||||
# Sphinx are currently 'default' and 'sphinxdoc'.
|
||||
# html_theme_path = ["."]
|
||||
# html_theme = '_theme'
|
||||
# html_static_path = ['static']
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
html_use_smartypants = False
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = '%sdoc' % project
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass
|
||||
# [howto/manual]).
|
||||
latex_documents = [
|
||||
('index',
|
||||
'%s.tex' % project,
|
||||
u'%s Documentation' % project,
|
||||
u'Matthew Treinish', 'manual'),
|
||||
]
|
||||
|
||||
man_pages = [('ostestr', 'ostestr', 'tooling to run OpenStack tests',
|
||||
['Matthew Treinish'], 1),
|
||||
('subunit_trace', 'subunit-trace', 'pretty output filter for '
|
||||
'subunit streams', ['Matthew Treinish'], 1),
|
||||
('subunit2html', 'subunit2html', 'generate a html results page '
|
||||
'from a subunit stream', ['Matthew Treinish'], 1)]
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
# intersphinx_mapping = {'http://docs.python.org/': None}
|
@ -1,4 +0,0 @@
|
||||
============
|
||||
Contributing
|
||||
============
|
||||
.. include:: ../../CONTRIBUTING.rst
|
@ -1,51 +0,0 @@
|
||||
.. generate_subunit:
|
||||
|
||||
generate-subunit
|
||||
================
|
||||
|
||||
generate-subunit is a simple tool to, as its name implies, generate a subunit
|
||||
stream. It will generate a stream with a single test result to STDOUT. The
|
||||
subunit protocol lets you concatenate multiple streams together so if you want
|
||||
to generate a stream with multiple just append the output of multiple executions
|
||||
of generate-subunit.
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
generate-subunit timestamp secs [status] [test_id]
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
generate-subunit has 2 mandatory arguments. These are needed to specify when
|
||||
the "test" started running and how long it took. The first argument is a POSIX
|
||||
timestamp (which can returned by the date util using ``date +%s``) for when it
|
||||
started running. The second argument is the number of seconds it took for the
|
||||
execution to finish. For example::
|
||||
|
||||
$ generate-subunit $(date +%s) 42
|
||||
|
||||
will generate a stream with the test_id 'devstack' successfully running for 42
|
||||
secs starting when the command was executed. This leads into the 2 optional
|
||||
arguments. The first optional argument is for specifying the status. This must
|
||||
be the 3rd argument when calling generate-subunit. Valid status options can
|
||||
be found in the `testtools documentation`_. If status is not specified it will
|
||||
default to success. For example::
|
||||
|
||||
$ generate-subunit $(date +%s) 42 fail
|
||||
|
||||
will be the same as the previous example except that it marks the test as
|
||||
failing.
|
||||
|
||||
.. _testtools documentation: http://testtools.readthedocs.io/en/latest/api.html#testtools.StreamResult.status
|
||||
|
||||
The other optional argument is the test_id (aka test name) and is used to
|
||||
identify the "test" being run. For better or worse this defaults to *devstack*.
|
||||
(which is an artifact of why this tool was originally created) Note, this must
|
||||
be the 4th argument when calling generate-subunit. This means you also must
|
||||
specify a status if you want to set your own test_id. For example::
|
||||
|
||||
$ generate-subunit %(date +%s) 42 fail my_little_test
|
||||
|
||||
will generate a subunit stream as before except instead the test will be named
|
||||
my_little_test.
|
@ -1 +0,0 @@
|
||||
.. include:: ../../ChangeLog
|
@ -1,26 +0,0 @@
|
||||
.. os-testr documentation master file, created by
|
||||
sphinx-quickstart on Tue Jul 9 22:26:36 2013.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to os-testr's documentation!
|
||||
========================================================
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
readme
|
||||
installation
|
||||
usage
|
||||
contributing
|
||||
history
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
@ -1,12 +0,0 @@
|
||||
============
|
||||
Installation
|
||||
============
|
||||
|
||||
At the command line::
|
||||
|
||||
$ pip install os-testr
|
||||
|
||||
Or, if you have virtualenvwrapper installed::
|
||||
|
||||
$ mkvirtualenv os-testr
|
||||
$ pip install os-testr
|
@ -1,238 +0,0 @@
|
||||
.. _ostestr:
|
||||
|
||||
ostestr
|
||||
=======
|
||||
|
||||
The ostestr command provides a wrapper around the testr command included in
|
||||
the testrepository package. It's designed to build on the functionality
|
||||
included in testr and workaround several UI bugs in the short term. By default
|
||||
it also has output that is much more useful for OpenStack's test suites which
|
||||
are lengthy in both runtime and number of tests. Please note that the CLI
|
||||
semantics are still a work in progress as the project is quite young, so
|
||||
default behavior might change in future version.
|
||||
|
||||
Summary
|
||||
-------
|
||||
ostestr [-b|--blacklist_file <blacklist_file>] [-r|--regex REGEX]
|
||||
[-w|--whitelist_file <whitelist_file>]
|
||||
[-p|--pretty] [--no-pretty] [-s|--subunit] [-l|--list]
|
||||
[-n|--no-discover <test_id>] [--slowest] [--no-slowest]
|
||||
[--pdb <test_id>] [--parallel] [--serial]
|
||||
[-c|--concurrency <workers>] [--until-failure] [--print-exclude]
|
||||
|
||||
Options
|
||||
-------
|
||||
|
||||
--blacklist_file BLACKLIST_FILE, -b BLACKLIST_FILE
|
||||
Path to a blacklist file, this file contains a
|
||||
separate regex exclude on each newline
|
||||
--whitelist_file WHITELIST_FILE, -w WHITELIST_FILE
|
||||
Path to a whitelist file, this file contains a
|
||||
separate regex on each newline
|
||||
--regex REGEX, -r REGEX
|
||||
A normal testr selection regex. If a blacklist file is
|
||||
specified, the regex will be appended to the end of
|
||||
the generated regex from that file
|
||||
--pretty, -p
|
||||
Print pretty output from subunit-trace. This is
|
||||
mutually exclusive with --subunit
|
||||
--no-pretty
|
||||
Disable the pretty output with subunit-trace
|
||||
--subunit, -s
|
||||
output the raw subunit v2 from the test run this is
|
||||
mutually exclusive with --pretty
|
||||
--list, -l
|
||||
List all the tests which will be run.
|
||||
--no-discover TEST_ID, -n TEST_ID
|
||||
Takes in a single test to bypasses test discover and
|
||||
just execute the test specified
|
||||
--slowest
|
||||
After the test run print the slowest tests
|
||||
--no-slowest
|
||||
After the test run don't print the slowest tests
|
||||
--pdb TEST_ID
|
||||
Run a single test that has pdb traces added
|
||||
--parallel
|
||||
Run tests in parallel (this is the default)
|
||||
--serial
|
||||
Run tests serially
|
||||
--concurrency WORKERS, -c WORKERS
|
||||
The number of workers to use when running in parallel.
|
||||
By default this is the number of cpus
|
||||
--until-failure
|
||||
Run the tests in a loop until a failure is
|
||||
encountered. Running with subunit or prettyoutput
|
||||
enable will force the loop to run testsserially
|
||||
--print-exclude
|
||||
If an exclude file is used this option will prints the
|
||||
comment from the same line and all skipped tests
|
||||
before the test run
|
||||
|
||||
Running Tests
|
||||
-------------
|
||||
|
||||
os-testr is primarily for running tests at it's basic level you just invoke
|
||||
ostestr to run a test suite for a project. (assuming it's setup to run tests
|
||||
using testr already) For example::
|
||||
|
||||
$ ostestr
|
||||
|
||||
This will run tests in parallel (with the number of workers matching the number
|
||||
of CPUs) and with subunit-trace output. If you need to run tests in serial you
|
||||
can use the serial option::
|
||||
|
||||
$ ostestr --serial
|
||||
|
||||
Or if you need to adjust the concurrency but still run in parallel you can use
|
||||
-c/--concurrency::
|
||||
|
||||
$ ostestr --concurrency 2
|
||||
|
||||
If you only want to run an individual test module or more specific (a single
|
||||
class, or test) and parallel execution doesn't matter, you can use the
|
||||
-n/--no-discover to skip test discovery and just directly calls subunit.run on
|
||||
the tests under the covers. Bypassing discovery is desirable when running a
|
||||
small subset of tests in a larger test suite because the discovery time can
|
||||
often far exceed the total run time of the tests.
|
||||
|
||||
For example::
|
||||
|
||||
$ ostestr --no-discover test.test_thing.TestThing.test_thing_method
|
||||
|
||||
Additionally, if you need to run a single test module, class, or single test
|
||||
with pdb enabled you can use --pdb to directly call testtools.run under the
|
||||
covers which works with pdb. For example::
|
||||
|
||||
$ ostestr --pdb tests.test_thing.TestThing.test_thing_method
|
||||
|
||||
|
||||
Test Selection
|
||||
--------------
|
||||
|
||||
ostestr is designed to build on top of the test selection in testr. testr only
|
||||
exposed a regex option to select tests. This equivalent is functionality is
|
||||
exposed via the --regex option. For example::
|
||||
|
||||
$ ostestr --regex 'magic\.regex'
|
||||
|
||||
This will do a straight passthrough of the provided regex to testr.
|
||||
Additionally, ostestr allows you to specify a blacklist file to define a set
|
||||
of regexes to exclude. You can specify a blacklist file with the
|
||||
--blacklist_file/-b option, for example::
|
||||
|
||||
$ ostestr --blacklist_file $path_to_file
|
||||
|
||||
The format for the file is line separated regex, with '#' used to signify the
|
||||
start of a comment on a line. For example::
|
||||
|
||||
# Blacklist File
|
||||
^regex1 # Excludes these tests
|
||||
.*regex2 # exclude those tests
|
||||
|
||||
Will generate a regex to pass to testr which will exclude both any tests
|
||||
matching '^regex1' and '.*regex2'. If a blacklist file is used in conjunction
|
||||
with the --regex option the regex specified with --regex will be appended to
|
||||
the generated output from the --blacklist_file. Also it's worth noting that the
|
||||
regex test selection options can not be used in conjunction with the
|
||||
--no-discover or --pdb options described in the previous section. This is
|
||||
because the regex selection requires using testr under the covers to actually
|
||||
do the filtering, and those 2 options do not use testr.
|
||||
|
||||
The dual of the blacklist file is the whitelist file which works in the exact
|
||||
same manner, except that instead of excluding regex matches it includes them.
|
||||
You can specify the path to the file with --whitelist_file/-w, for example::
|
||||
|
||||
$ ostestr --whitelist_file $path_to_file
|
||||
|
||||
The format for the file is more or less identical to the blacklist file::
|
||||
|
||||
# Whitelist File
|
||||
^regex1 # Include these tests
|
||||
.*regex2 # include those tests
|
||||
|
||||
However, instead of excluding the matches it will include them. Note that a
|
||||
blacklist file can not be used at the same time as a whitelist file, they
|
||||
are mutually exclusive.
|
||||
|
||||
It's also worth noting that you can use the test list option to dry run any
|
||||
selection arguments you are using. You just need to use --list/-l with your
|
||||
selection options to do this, for example::
|
||||
|
||||
$ ostestr --regex 'regex3.*' --blacklist_file blacklist.txt --list
|
||||
|
||||
This will list all the tests which will be run by ostestr using that combination
|
||||
of arguments.
|
||||
|
||||
Please not that all of this selection functionality will be expanded on in the
|
||||
future and a default grammar for selecting multiple tests will be chosen in a
|
||||
future release. However as of right now all current arguments (which have
|
||||
guarantees on always remaining in place) are still required to perform any
|
||||
selection logic while this functionality is still under development.
|
||||
|
||||
|
||||
Output Options
|
||||
--------------
|
||||
|
||||
By default ostestr will use subunit-trace as the output filter on the test
|
||||
run. It will also print the slowest tests from the run after the run is
|
||||
concluded. You can disable the printing the slowest tests with the --no-slowest
|
||||
flag, for example::
|
||||
|
||||
$ ostestr --no-slowest
|
||||
|
||||
If you'd like to disable the subunit-trace output you can do this using
|
||||
--no-pretty::
|
||||
|
||||
$ ostestr --no-pretty
|
||||
|
||||
ostestr also provides the option to just output the raw subunit stream on
|
||||
STDOUT with --subunit/-s. Note if you want to use this you also have to
|
||||
specify --no-pretty as the subunit-trace output and the raw subunit output
|
||||
are mutually exclusive. For example, to get raw subunit output the arguments
|
||||
would be::
|
||||
|
||||
$ ostestr --no-pretty --subunit
|
||||
|
||||
An additional option on top of the blacklist file is --print-exclude option.
|
||||
When this option is specified when using a blacklist file before the tests are
|
||||
run ostestr will print all the tests it will be excluding from the blacklist
|
||||
file. If a line in the blacklist file has a comment that will be printed before
|
||||
listing the tests which will be excluded by that line's regex. If no comment is
|
||||
present on a line the regex from that line will be used instead. For example,
|
||||
if you were using the example blacklist file from the previous section the
|
||||
output before the regular test run output would be::
|
||||
|
||||
$ ostestr -b blacklist-file blacklist.txt --print-exclude
|
||||
Excludes these tests
|
||||
regex1_match
|
||||
regex1_exclude
|
||||
|
||||
exclude those tests
|
||||
regex2_match
|
||||
regex2_exclude
|
||||
|
||||
...
|
||||
|
||||
Notes for running with tox
|
||||
--------------------------
|
||||
|
||||
If you use `tox`_ for running your tests and call ostestr as the test command
|
||||
it's recommended that you set a posargs following ostestr on the commands
|
||||
stanza. For example::
|
||||
|
||||
[testenv]
|
||||
commands = ostestr {posargs}
|
||||
|
||||
.. _tox: https://tox.readthedocs.org/en/latest/
|
||||
|
||||
this will enable end users to pass args to configure the output, use the
|
||||
selection logic, or any other options directly from the tox cli. This will let
|
||||
tox take care of the venv management and the environment separation but enable
|
||||
direct access to all of the ostestr options to easily customize your test run.
|
||||
For example, assuming the above posargs usage you would be to do::
|
||||
|
||||
$ tox -epy34 -- --regex ^regex1
|
||||
|
||||
or to skip discovery::
|
||||
|
||||
$ tox -epy34 -- -n test.test_thing.TestThing.test_thing_method
|
@ -1 +0,0 @@
|
||||
.. include:: ../../README.rst
|
@ -1,33 +0,0 @@
|
||||
.. _subunit2html:
|
||||
|
||||
subunit2html
|
||||
============
|
||||
|
||||
subunit2html is a tool that takes in a subunit stream file and will output an
|
||||
html page
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
subunit2html subunit_stream [output]
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
subunit2html takes in 1 mandatory argument. This is used to specify the location
|
||||
of the subunit stream file. For example::
|
||||
|
||||
$ subunit2html subunit_stream
|
||||
|
||||
By default subunit2html will store the generated html results file at
|
||||
results.html file in the current working directory.
|
||||
|
||||
An optional second argument can be provided to set the output path of the html
|
||||
results file that is generated. If it is provided this will be the output path
|
||||
for saving the generated file, otherwise results.html in the current working
|
||||
directory will be used. For example::
|
||||
|
||||
$ subunit2html subunit_stream test_results.html
|
||||
|
||||
will write the generated html results file to test_results.html in the current
|
||||
working directory
|
@ -1,112 +0,0 @@
|
||||
.. _subunit_trace:
|
||||
|
||||
subunit-trace
|
||||
=============
|
||||
|
||||
subunit-trace is an output filter for subunit streams. It is often used in
|
||||
conjunction with test runners that emit subunit to enable a consistent and
|
||||
useful realtime output from a test run.
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
subunit-trace [--fails|-f] [--failonly] [--perc-diff|-d] [--no-summary]
|
||||
[--diff-threshold|-t <threshold>] [--color]
|
||||
|
||||
Options
|
||||
-------
|
||||
|
||||
--no-failure-debug, -n
|
||||
Disable printing failure debug information in realtime
|
||||
--fails, -f
|
||||
Print failure debug information after the stream is
|
||||
processed
|
||||
--failonly
|
||||
Don't print success items
|
||||
--perc-diff, -d
|
||||
Print percent change in run time on each test
|
||||
--diff-threshold THRESHOLD, -t THRESHOLD
|
||||
Threshold to use for displaying percent change from the
|
||||
avg run time. If one is not specified the percent
|
||||
change will always be displayed.
|
||||
--no-summary
|
||||
Don't print the summary of the test run after completes
|
||||
--color
|
||||
Print result with colors
|
||||
|
||||
Usage
|
||||
-----
|
||||
subunit-trace will take a subunit stream in via STDIN. This is the only input
|
||||
into the tool. It will then print on STDOUT the formatted test result output
|
||||
for the test run information contained in the stream.
|
||||
|
||||
A subunit v2 stream must be passed into subunit-trace. If only a subunit v1
|
||||
stream is available you must use the subunit-1to2 utility to convert it before
|
||||
passing the stream into subunit-trace. For example this can be done by chaining
|
||||
pipes::
|
||||
|
||||
$ cat subunit_v1 | subunit-1to2 | subunit-trace
|
||||
|
||||
Adjusting per test output
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
subunit-trace provides several options to customize it's output. This allows
|
||||
users to customize the output from subunit-trace to suit their needs. The output
|
||||
from subunit-trace basically comes in 2 parts, the per test output, and the
|
||||
summary at the end. By default subunit-trace will print failure messages during
|
||||
the per test output, meaning when a test fails it will also print the message
|
||||
and any traceback and other attachments at that time. However this can be
|
||||
disabled by using --no-failure-debug, -n. For example::
|
||||
|
||||
$ testr run --subunit | subunit-trace --no-failure-debug
|
||||
|
||||
Here is also the option to print all failures together at the end of the test
|
||||
run before the summary view. This is done using the --fails/-f option. For
|
||||
example::
|
||||
|
||||
$ testr run --subunit | subunit-trace --fails
|
||||
|
||||
Often the --fails and --no-failure-debug options are used in conjunction to
|
||||
only print failures at the end of a test run. This is useful for large test
|
||||
suites where an error message might be lost in the noise. To do this ::
|
||||
|
||||
$ testr run --subunit | subunit-trace --fails --no-failure-debug
|
||||
|
||||
By default subunit-trace will print a line for each test after it completes with
|
||||
the test status. However, if you only want to see the run time output for
|
||||
failures and not any other test status you can use the --failonly option. For
|
||||
example::
|
||||
|
||||
$ testr run --subunit | subunit-trace --failonly
|
||||
|
||||
The last output option provided by subunit-trace is to disable the summary view
|
||||
of the test run which is normally displayed at the end of a run. You can do
|
||||
this using the --no-summary option. For example::
|
||||
|
||||
$ testr run --subunit | subunit-trace --no-summary
|
||||
|
||||
|
||||
Show per test run time percent change
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
subunit-trace provides an option to display the percent change in run time
|
||||
from the previous run. To do this subunit-trace leverages the testr internals
|
||||
a bit. It uses the times.dbm database which, the file repository type in
|
||||
testrepository will create, to get the previous run time for a test. If testr
|
||||
hasn't ever been used before or for whatever reason subunit-trace is unable to
|
||||
find the times.dbm file from testr no percentages will be displayed even if it's
|
||||
enabled. Additionally, if a test is run which does not have an entry in the
|
||||
times.dbm file will not have a percentage printed for it.
|
||||
|
||||
To enable this feature you use --perc-diff/-d, for example::
|
||||
|
||||
$ testr run --subunit | subunit-trace --perc-diff
|
||||
|
||||
There is also the option to set a threshold value for this option. If used it
|
||||
acts as an absolute value and only percentage changes that exceed it will be
|
||||
printed. Use the --diff-threshold/-t option to set a threshold, for example::
|
||||
|
||||
$ testr run --subunit | subunit-trace --perc-diff --threshold 45
|
||||
|
||||
This will only display percent differences when the change in run time is either
|
||||
>=45% faster or <=45% slower.
|
@ -1,13 +0,0 @@
|
||||
=====
|
||||
Usage
|
||||
=====
|
||||
|
||||
This section contains the documentation for each of tools packaged in os-testr
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
ostestr
|
||||
subunit_trace
|
||||
subunit2html
|
||||
generate_subunit
|
@ -1,19 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import pbr.version
|
||||
|
||||
|
||||
__version__ = pbr.version.VersionInfo(
|
||||
'os_testr').version_string()
|
@ -1,58 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import datetime
|
||||
import sys
|
||||
|
||||
import pbr.version
|
||||
import subunit
|
||||
from subunit import iso8601
|
||||
|
||||
|
||||
__version__ = pbr.version.VersionInfo('os_testr').version_string()
|
||||
|
||||
|
||||
def main():
|
||||
if '--version' in sys.argv:
|
||||
print(__version__)
|
||||
exit(0)
|
||||
|
||||
start_time = datetime.datetime.fromtimestamp(float(sys.argv[1])).replace(
|
||||
tzinfo=iso8601.UTC)
|
||||
elapsed_time = datetime.timedelta(seconds=int(sys.argv[2]))
|
||||
stop_time = start_time + elapsed_time
|
||||
|
||||
if len(sys.argv) > 3:
|
||||
status = sys.argv[3]
|
||||
else:
|
||||
status = 'success'
|
||||
|
||||
if len(sys.argv) > 4:
|
||||
test_id = sys.argv[4]
|
||||
else:
|
||||
test_id = 'devstack'
|
||||
|
||||
# Write the subunit test
|
||||
output = subunit.v2.StreamResultToBytes(sys.stdout)
|
||||
output.startTestRun()
|
||||
output.status(timestamp=start_time, test_id=test_id)
|
||||
# Write the end of the test
|
||||
output.status(test_status=status, timestamp=stop_time, test_id=test_id)
|
||||
output.stopTestRun()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -1,248 +0,0 @@
|
||||
#!/usr/bin/env python2
|
||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pbr.version
|
||||
from subunit import run as subunit_run
|
||||
from testtools import run as testtools_run
|
||||
|
||||
from os_testr import regex_builder as rb
|
||||
|
||||
|
||||
__version__ = pbr.version.VersionInfo('os_testr').version_string()
|
||||
|
||||
|
||||
def get_parser(args):
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Tool to run openstack tests')
|
||||
parser.add_argument('--version', action='version',
|
||||
version='%s' % __version__)
|
||||
list_files = parser.add_mutually_exclusive_group()
|
||||
list_files.add_argument('--blacklist_file', '-b',
|
||||
help='Path to a blacklist file, this file '
|
||||
'contains a separate regex exclude on each '
|
||||
'newline')
|
||||
list_files.add_argument('--whitelist_file', '-w',
|
||||
help='Path to a whitelist file, this file '
|
||||
'contains a separate regex on each newline.')
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument('--regex', '-r',
|
||||
help='A normal testr selection regex. If a blacklist '
|
||||
'file is specified, the regex will be appended '
|
||||
'to the end of the generated regex from that '
|
||||
'file.')
|
||||
group.add_argument('--path', metavar='FILE_OR_DIRECTORY',
|
||||
help='A file name or directory of tests to run.')
|
||||
group.add_argument('--no-discover', '-n', metavar='TEST_ID',
|
||||
help="Takes in a single test to bypasses test "
|
||||
"discover and just execute the test specified. "
|
||||
"A file name may be used in place of a test "
|
||||
"name.")
|
||||
pretty = parser.add_mutually_exclusive_group()
|
||||
pretty.add_argument('--pretty', '-p', dest='pretty', action='store_true',
|
||||
help='Print pretty output from subunit-trace. This is '
|
||||
'mutually exclusive with --subunit')
|
||||
pretty.add_argument('--no-pretty', dest='pretty', action='store_false',
|
||||
help='Disable the pretty output with subunit-trace')
|
||||
parser.add_argument('--subunit', '-s', action='store_true',
|
||||
help='output the raw subunit v2 from the test run '
|
||||
'this is mutually exclusive with --pretty')
|
||||
parser.add_argument('--list', '-l', action='store_true',
|
||||
help='List all the tests which will be run.')
|
||||
parser.add_argument('--color', action='store_true',
|
||||
help='Use color in the pretty output')
|
||||
slowest = parser.add_mutually_exclusive_group()
|
||||
slowest.add_argument('--slowest', dest='slowest', action='store_true',
|
||||
help="after the test run print the slowest tests")
|
||||
slowest.add_argument('--no-slowest', dest='slowest', action='store_false',
|
||||
help="after the test run don't print the slowest "
|
||||
"tests")
|
||||
parser.add_argument('--pdb', metavar='TEST_ID',
|
||||
help='Run a single test that has pdb traces added')
|
||||
parallel = parser.add_mutually_exclusive_group()
|
||||
parallel.add_argument('--parallel', dest='parallel', action='store_true',
|
||||
help='Run tests in parallel (this is the default)')
|
||||
parallel.add_argument('--serial', dest='parallel', action='store_false',
|
||||
help='Run tests serially')
|
||||
parser.add_argument('--concurrency', '-c', type=int, metavar='WORKERS',
|
||||
help='The number of workers to use when running in '
|
||||
'parallel. By default this is the number of cpus')
|
||||
parser.add_argument('--until-failure', action='store_true',
|
||||
help='Run the tests in a loop until a failure is '
|
||||
'encountered. Running with subunit or pretty'
|
||||
'output enable will force the loop to run tests'
|
||||
'serially')
|
||||
parser.add_argument('--print-exclude', action='store_true',
|
||||
help='If an exclude file is used this option will '
|
||||
'prints the comment from the same line and all '
|
||||
'skipped tests before the test run')
|
||||
parser.set_defaults(pretty=True, slowest=True, parallel=True)
|
||||
return parser.parse_known_args(args)
|
||||
|
||||
|
||||
def call_testr(regex, subunit, pretty, list_tests, slowest, parallel, concur,
|
||||
until_failure, color, others=None):
|
||||
others = others or []
|
||||
if parallel:
|
||||
cmd = ['testr', 'run', '--parallel']
|
||||
if concur:
|
||||
cmd.append('--concurrency=%s' % concur)
|
||||
else:
|
||||
cmd = ['testr', 'run']
|
||||
if list_tests:
|
||||
cmd = ['testr', 'list-tests']
|
||||
elif (subunit or pretty) and not until_failure:
|
||||
cmd.append('--subunit')
|
||||
elif not (subunit or pretty) and until_failure:
|
||||
cmd.append('--until-failure')
|
||||
cmd.append(regex)
|
||||
env = copy.deepcopy(os.environ)
|
||||
|
||||
if pretty:
|
||||
subunit_trace_cmd = ['subunit-trace', '--no-failure-debug', '-f']
|
||||
if color:
|
||||
subunit_trace_cmd.append('--color')
|
||||
|
||||
# This workaround is necessary because of lp bug 1411804 it's super hacky
|
||||
# and makes tons of unfounded assumptions, but it works for the most part
|
||||
if (subunit or pretty) and until_failure:
|
||||
test_list = rb._get_test_list(regex, env)
|
||||
count = 0
|
||||
failed = False
|
||||
if not test_list:
|
||||
print("No tests to run")
|
||||
exit(1)
|
||||
# If pretty or subunit output is desired manually loop forever over
|
||||
# test individually and generate the desired output in a linear series
|
||||
# this avoids 1411804 while retaining most of the desired behavior
|
||||
while True:
|
||||
for test in test_list:
|
||||
if pretty:
|
||||
cmd = ['python', '-m', 'subunit.run', test]
|
||||
ps = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE)
|
||||
subunit_trace_cmd.append('--no-summary')
|
||||
proc = subprocess.Popen(subunit_trace_cmd,
|
||||
env=env,
|
||||
stdin=ps.stdout)
|
||||
ps.stdout.close()
|
||||
proc.communicate()
|
||||
if proc.returncode > 0:
|
||||
failed = True
|
||||
break
|
||||
else:
|
||||
try:
|
||||
subunit_run.main([sys.argv[0], test], sys.stdout)
|
||||
except SystemExit as e:
|
||||
if e > 0:
|
||||
print("Ran %s tests without failure" % count)
|
||||
exit(1)
|
||||
else:
|
||||
raise
|
||||
count = count + 1
|
||||
if failed:
|
||||
print("Ran %s tests without failure" % count)
|
||||
exit(0)
|
||||
# If not until-failure special case call testr like normal
|
||||
elif pretty and not list_tests:
|
||||
cmd.extend(others)
|
||||
ps = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE)
|
||||
proc = subprocess.Popen(subunit_trace_cmd,
|
||||
env=env, stdin=ps.stdout)
|
||||
ps.stdout.close()
|
||||
else:
|
||||
cmd.extend(others)
|
||||
proc = subprocess.Popen(cmd, env=env)
|
||||
proc.communicate()
|
||||
return_code = proc.returncode
|
||||
if slowest and not list_tests:
|
||||
print("\nSlowest Tests:\n")
|
||||
slow_proc = subprocess.Popen(['testr', 'slowest'], env=env)
|
||||
slow_proc.communicate()
|
||||
return return_code
|
||||
|
||||
|
||||
def call_subunit_run(test_id, pretty, subunit):
|
||||
if pretty:
|
||||
env = copy.deepcopy(os.environ)
|
||||
cmd = ['python', '-m', 'subunit.run', test_id]
|
||||
ps = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE)
|
||||
proc = subprocess.Popen(['subunit-trace', '--no-failure-debug', '-f'],
|
||||
env=env, stdin=ps.stdout)
|
||||
ps.stdout.close()
|
||||
proc.communicate()
|
||||
return proc.returncode
|
||||
elif subunit:
|
||||
subunit_run.main([sys.argv[0], test_id], sys.stdout)
|
||||
else:
|
||||
testtools_run.main([sys.argv[0], test_id], sys.stdout)
|
||||
|
||||
|
||||
def _select_and_call_runner(opts, exclude_regex, others):
|
||||
ec = 1
|
||||
if not os.path.isdir('.testrepository'):
|
||||
subprocess.call(['testr', 'init'])
|
||||
|
||||
if not opts.no_discover and not opts.pdb:
|
||||
ec = call_testr(exclude_regex, opts.subunit, opts.pretty, opts.list,
|
||||
opts.slowest, opts.parallel, opts.concurrency,
|
||||
opts.until_failure, opts.color, others)
|
||||
else:
|
||||
if others:
|
||||
print('Unexpected arguments: ' + ' '.join(others))
|
||||
return 2
|
||||
test_to_run = opts.no_discover or opts.pdb
|
||||
if test_to_run.find('/') != -1:
|
||||
test_to_run = rb.path_to_regex(test_to_run)
|
||||
ec = call_subunit_run(test_to_run, opts.pretty, opts.subunit)
|
||||
return ec
|
||||
|
||||
|
||||
def main():
|
||||
opts, others = get_parser(sys.argv[1:])
|
||||
if opts.pretty and opts.subunit:
|
||||
msg = ('Subunit output and pretty output cannot be specified at the '
|
||||
'same time')
|
||||
print(msg)
|
||||
exit(2)
|
||||
if opts.list and opts.no_discover:
|
||||
msg = ('you can not list tests when you are bypassing discovery to '
|
||||
'run a single test')
|
||||
print(msg)
|
||||
exit(3)
|
||||
if not opts.parallel and opts.concurrency:
|
||||
msg = "You can't specify a concurrency to use when running serially"
|
||||
print(msg)
|
||||
exit(4)
|
||||
if (opts.pdb or opts.no_discover) and opts.until_failure:
|
||||
msg = "You can not use until_failure mode with pdb or no-discover"
|
||||
print(msg)
|
||||
exit(5)
|
||||
if opts.path:
|
||||
regex = rb.path_to_regex(opts.path)
|
||||
else:
|
||||
regex = opts.regex
|
||||
exclude_regex = rb.construct_regex(opts.blacklist_file,
|
||||
opts.whitelist_file,
|
||||
regex,
|
||||
opts.print_exclude)
|
||||
exit(_select_and_call_runner(opts, exclude_regex, others))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -1,99 +0,0 @@
|
||||
# Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
||||
def _get_test_list(regex, env=None):
|
||||
env = env or copy.deepcopy(os.environ)
|
||||
proc = subprocess.Popen(['testr', 'list-tests', regex], env=env,
|
||||
stdout=subprocess.PIPE)
|
||||
out = proc.communicate()[0]
|
||||
raw_test_list = out.split('\n')
|
||||
bad = False
|
||||
test_list = []
|
||||
exclude_list = ['OS_', 'CAPTURE', 'TEST_TIMEOUT', 'PYTHON',
|
||||
'subunit.run discover']
|
||||
for line in raw_test_list:
|
||||
for exclude in exclude_list:
|
||||
if exclude in line or not line:
|
||||
bad = True
|
||||
break
|
||||
if not bad:
|
||||
test_list.append(line)
|
||||
bad = False
|
||||
return test_list
|
||||
|
||||
|
||||
def print_skips(regex, message):
|
||||
test_list = _get_test_list(regex)
|
||||
if test_list:
|
||||
if message:
|
||||
print(message)
|
||||
else:
|
||||
print('Skipped because of regex %s:' % regex)
|
||||
for test in test_list:
|
||||
print(test)
|
||||
# Extra whitespace to separate
|
||||
print('\n')
|
||||
|
||||
|
||||
def path_to_regex(path):
|
||||
root, _ = os.path.splitext(path)
|
||||
return root.replace('/', '.')
|
||||
|
||||
|
||||
def get_regex_from_whitelist_file(file_path):
|
||||
lines = []
|
||||
for line in open(file_path).read().splitlines():
|
||||
split_line = line.strip().split('#')
|
||||
# Before the # is the regex
|
||||
line_regex = split_line[0].strip()
|
||||
if line_regex:
|
||||
lines.append(line_regex)
|
||||
return '|'.join(lines)
|
||||
|
||||
|
||||
def construct_regex(blacklist_file, whitelist_file, regex, print_exclude):
|
||||
if not blacklist_file:
|
||||
exclude_regex = ''
|
||||
else:
|
||||
black_file = open(blacklist_file, 'r')
|
||||
exclude_regex = ''
|
||||
for line in black_file:
|
||||
raw_line = line.strip()
|
||||
split_line = raw_line.split('#')
|
||||
# Before the # is the regex
|
||||
line_regex = split_line[0].strip()
|
||||
if len(split_line) > 1:
|
||||
# After the # is a comment
|
||||
comment = split_line[1].strip()
|
||||
else:
|
||||
comment = ''
|
||||
if line_regex:
|
||||
if print_exclude:
|
||||
print_skips(line_regex, comment)
|
||||
if exclude_regex:
|
||||
exclude_regex = '|'.join([line_regex, exclude_regex])
|
||||
else:
|
||||
exclude_regex = line_regex
|
||||
if exclude_regex:
|
||||
exclude_regex = "^((?!" + exclude_regex + ").)*$"
|
||||
if regex:
|
||||
exclude_regex += regex
|
||||
if whitelist_file:
|
||||
exclude_regex += '%s' % get_regex_from_whitelist_file(whitelist_file)
|
||||
return exclude_regex
|
@ -1,751 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright 2012-2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""
|
||||
Utility to convert a subunit stream to an html results file.
|
||||
Code is adapted from the pyunit Html test runner at
|
||||
http://tungwaiyip.info/software/HTMLTestRunner.html
|
||||
|
||||
Takes two arguments. First argument is path to subunit log file, second
|
||||
argument is path of desired output file. Second argument is optional,
|
||||
defaults to 'results.html'.
|
||||
|
||||
Original HTMLTestRunner License:
|
||||
------------------------------------------------------------------------
|
||||
Copyright (c) 2004-2007, Wai Yip Tung
|
||||
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.
|
||||
* Neither the name Wai Yip Tung nor the names of its contributors may be
|
||||
used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
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 OWNER
|
||||
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.
|
||||
"""
|
||||
|
||||
import collections
|
||||
import datetime
|
||||
import io
|
||||
import sys
|
||||
import traceback
|
||||
from xml.sax import saxutils
|
||||
|
||||
import pbr.version
|
||||
import subunit
|
||||
import testtools
|
||||
|
||||
|
||||
__version__ = pbr.version.VersionInfo('os_testr').version_string()
|
||||
|
||||
|
||||
class TemplateData(object):
|
||||
"""Define a HTML template for report customerization and generation.
|
||||
|
||||
Overall structure of an HTML report
|
||||
|
||||
HTML
|
||||
+------------------------+
|
||||
|<html> |
|
||||
| <head> |
|
||||
| |
|
||||
| STYLESHEET |
|
||||
| +----------------+ |
|
||||
| | | |
|
||||
| +----------------+ |
|
||||
| |
|
||||
| </head> |
|
||||
| |
|
||||
| <body> |
|
||||
| |
|
||||
| HEADING |
|
||||
| +----------------+ |
|
||||
| | | |
|
||||
| +----------------+ |
|
||||
| |
|
||||
| REPORT |
|
||||
| +----------------+ |
|
||||
| | | |
|
||||
| +----------------+ |
|
||||
| |
|
||||
| ENDING |
|
||||
| +----------------+ |
|
||||
| | | |
|
||||
| +----------------+ |
|
||||
| |
|
||||
| </body> |
|
||||
|</html> |
|
||||
+------------------------+
|
||||
"""
|
||||
|
||||
STATUS = {
|
||||
0: 'pass',
|
||||
1: 'fail',
|
||||
2: 'error',
|
||||
3: 'skip',
|
||||
}
|
||||
|
||||
DEFAULT_TITLE = 'Unit Test Report'
|
||||
DEFAULT_DESCRIPTION = ''
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# HTML Template
|
||||
|
||||
HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>%(title)s</title>
|
||||
<meta name="generator" content="%(generator)s"/>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
%(stylesheet)s
|
||||
</head>
|
||||
<body>
|
||||
<script language="javascript" type="text/javascript"><!--
|
||||
output_list = Array();
|
||||
|
||||
/* level - 0:Summary; 1:Failed; 2:All */
|
||||
function showCase(level) {
|
||||
trs = document.getElementsByTagName("tr");
|
||||
for (var i = 0; i < trs.length; i++) {
|
||||
tr = trs[i];
|
||||
id = tr.id;
|
||||
if (id.substr(0,2) == 'ft') {
|
||||
if (level < 1) {
|
||||
tr.className = 'hiddenRow';
|
||||
}
|
||||
else {
|
||||
tr.className = '';
|
||||
}
|
||||
}
|
||||
if (id.substr(0,2) == 'pt') {
|
||||
if (level > 1) {
|
||||
tr.className = '';
|
||||
}
|
||||
else {
|
||||
tr.className = 'hiddenRow';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showClassDetail(cid, count) {
|
||||
var id_list = Array(count);
|
||||
var toHide = 1;
|
||||
for (var i = 0; i < count; i++) {
|
||||
tid0 = 't' + cid.substr(1) + '.' + (i+1);
|
||||
tid = 'f' + tid0;
|
||||
tr = document.getElementById(tid);
|
||||
if (!tr) {
|
||||
tid = 'p' + tid0;
|
||||
tr = document.getElementById(tid);
|
||||
}
|
||||
id_list[i] = tid;
|
||||
if (tr.className) {
|
||||
toHide = 0;
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < count; i++) {
|
||||
tid = id_list[i];
|
||||
if (toHide) {
|
||||
document.getElementById('div_'+tid).style.display = 'none'
|
||||
document.getElementById(tid).className = 'hiddenRow';
|
||||
}
|
||||
else {
|
||||
document.getElementById(tid).className = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showTestDetail(div_id){
|
||||
var details_div = document.getElementById(div_id)
|
||||
var displayState = details_div.style.display
|
||||
// alert(displayState)
|
||||
if (displayState != 'block' ) {
|
||||
displayState = 'block'
|
||||
details_div.style.display = 'block'
|
||||
}
|
||||
else {
|
||||
details_div.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function html_escape(s) {
|
||||
s = s.replace(/&/g,'&');
|
||||
s = s.replace(/</g,'<');
|
||||
s = s.replace(/>/g,'>');
|
||||
return s;
|
||||
}
|
||||
|
||||
/* obsoleted by detail in <div>
|
||||
function showOutput(id, name) {
|
||||
var w = window.open("", //url
|
||||
name,
|
||||
"resizable,scrollbars,status,width=800,height=450");
|
||||
d = w.document;
|
||||
d.write("<pre>");
|
||||
d.write(html_escape(output_list[id]));
|
||||
d.write("\n");
|
||||
d.write("<a href='javascript:window.close()'>close</a>\n");
|
||||
d.write("</pre>\n");
|
||||
d.close();
|
||||
}
|
||||
*/
|
||||
--></script>
|
||||
|
||||
%(heading)s
|
||||
%(report)s
|
||||
%(ending)s
|
||||
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
# variables: (title, generator, stylesheet, heading, report, ending)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Stylesheet
|
||||
#
|
||||
# alternatively use a <link> for external style sheet, e.g.
|
||||
# <link rel="stylesheet" href="$url" type="text/css">
|
||||
|
||||
STYLESHEET_TMPL = """
|
||||
<style type="text/css" media="screen">
|
||||
body { font-family: verdana, arial, helvetica, sans-serif;
|
||||
font-size: 80%; }
|
||||
table { font-size: 100%; width: 100%;}
|
||||
pre { font-size: 80%; }
|
||||
|
||||
/* -- heading -------------------------------------------------------------- */
|
||||
h1 {
|
||||
font-size: 16pt;
|
||||
color: gray;
|
||||
}
|
||||
.heading {
|
||||
margin-top: 0ex;
|
||||
margin-bottom: 1ex;
|
||||
}
|
||||
|
||||
.heading .attribute {
|
||||
margin-top: 1ex;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.heading .description {
|
||||
margin-top: 4ex;
|
||||
margin-bottom: 6ex;
|
||||
}
|
||||
|
||||
/* -- css div popup -------------------------------------------------------- */
|
||||
a.popup_link {
|
||||
}
|
||||
|
||||
a.popup_link:hover {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.popup_window {
|
||||
display: none;
|
||||
overflow-x: scroll;
|
||||
/*border: solid #627173 1px; */
|
||||
padding: 10px;
|
||||
background-color: #E6E6D6;
|
||||
font-family: "Ubuntu Mono", "Lucida Console", "Courier New", monospace;
|
||||
text-align: left;
|
||||
font-size: 8pt;
|
||||
}
|
||||
|
||||
}
|
||||
/* -- report --------------------------------------------------------------- */
|
||||
#show_detail_line {
|
||||
margin-top: 3ex;
|
||||
margin-bottom: 1ex;
|
||||
}
|
||||
#result_table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid #777;
|
||||
}
|
||||
#header_row {
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
background-color: #777;
|
||||
}
|
||||
#result_table td {
|
||||
border: 1px solid #777;
|
||||
padding: 2px;
|
||||
}
|
||||
#total_row { font-weight: bold; }
|
||||
.passClass { background-color: #6c6; }
|
||||
.failClass { background-color: #c60; }
|
||||
.errorClass { background-color: #c00; }
|
||||
.passCase { color: #6c6; }
|
||||
.failCase { color: #c60; font-weight: bold; }
|
||||
.errorCase { color: #c00; font-weight: bold; }
|
||||
.hiddenRow { display: none; }
|
||||
.testcase { margin-left: 2em; }
|
||||
td.testname {width: 40%}
|
||||
td.small {width: 40px}
|
||||
|
||||
/* -- ending --------------------------------------------------------------- */
|
||||
#ending {
|
||||
}
|
||||
|
||||
</style>
|
||||
"""
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Heading
|
||||
#
|
||||
|
||||
HEADING_TMPL = """<div class='heading'>
|
||||
<h1>%(title)s</h1>
|
||||
%(parameters)s
|
||||
<p class='description'>%(description)s</p>
|
||||
</div>
|
||||
|
||||
""" # variables: (title, parameters, description)
|
||||
|
||||
HEADING_ATTRIBUTE_TMPL = """
|
||||
<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
|
||||
""" # variables: (name, value)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Report
|
||||
#
|
||||
|
||||
REPORT_TMPL = """
|
||||
<p id='show_detail_line'>Show
|
||||
<a href='javascript:showCase(0)'>Summary</a>
|
||||
<a href='javascript:showCase(1)'>Failed</a>
|
||||
<a href='javascript:showCase(2)'>All</a>
|
||||
</p>
|
||||
<table id='result_table'>
|
||||
<colgroup>
|
||||
<col align='left' />
|
||||
<col align='right' />
|
||||
<col align='right' />
|
||||
<col align='right' />
|
||||
<col align='right' />
|
||||
<col align='right' />
|
||||
<col align='right' />
|
||||
<col align='right' />
|
||||
</colgroup>
|
||||
<tr id='header_row'>
|
||||
<td>Test Group/Test case</td>
|
||||
<td>Count</td>
|
||||
<td>Pass</td>
|
||||
<td>Fail</td>
|
||||
<td>Error</td>
|
||||
<td>Skip</td>
|
||||
<td>View</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
%(test_list)s
|
||||
<tr id='total_row'>
|
||||
<td>Total</td>
|
||||
<td>%(count)s</td>
|
||||
<td>%(Pass)s</td>
|
||||
<td>%(fail)s</td>
|
||||
<td>%(error)s</td>
|
||||
<td>%(skip)s</td>
|
||||
<td> </td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
""" # variables: (test_list, count, Pass, fail, error)
|
||||
|
||||
REPORT_CLASS_TMPL = r"""
|
||||
<tr class='%(style)s'>
|
||||
<td class="testname">%(desc)s</td>
|
||||
<td class="small">%(count)s</td>
|
||||
<td class="small">%(Pass)s</td>
|
||||
<td class="small">%(fail)s</td>
|
||||
<td class="small">%(error)s</td>
|
||||
<td class="small">%(skip)s</td>
|
||||
<td class="small"><a href="javascript:showClassDetail('%(cid)s',%(count)s)"
|
||||
>Detail</a></td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
""" # variables: (style, desc, count, Pass, fail, error, cid)
|
||||
|
||||
REPORT_TEST_WITH_OUTPUT_TMPL = r"""
|
||||
<tr id='%(tid)s' class='%(Class)s'>
|
||||
<td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
|
||||
<td colspan='7' align='left'>
|
||||
|
||||
<!--css div popup start-->
|
||||
<a class="popup_link" onfocus='this.blur();'
|
||||
href="javascript:showTestDetail('div_%(tid)s')" >
|
||||
%(status)s</a>
|
||||
|
||||
<div id='div_%(tid)s' class="popup_window">
|
||||
<div style='text-align: right; color:red;cursor:pointer'>
|
||||
<a onfocus='this.blur();'
|
||||
onclick="document.getElementById('div_%(tid)s').style.display = 'none' " >
|
||||
[x]</a>
|
||||
</div>
|
||||
<pre>
|
||||
%(script)s
|
||||
</pre>
|
||||
</div>
|
||||
<!--css div popup end-->
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
""" # variables: (tid, Class, style, desc, status)
|
||||
|
||||
REPORT_TEST_NO_OUTPUT_TMPL = r"""
|
||||
<tr id='%(tid)s' class='%(Class)s'>
|
||||
<td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
|
||||
<td colspan='6' align='center'>%(status)s</td>
|
||||
</tr>
|
||||
""" # variables: (tid, Class, style, desc, status)
|
||||
|
||||
REPORT_TEST_OUTPUT_TMPL = r"""
|
||||
%(id)s: %(output)s
|
||||
""" # variables: (id, output)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# ENDING
|
||||
#
|
||||
|
||||
ENDING_TMPL = """<div id='ending'> </div>"""
|
||||
|
||||
# -------------------- The end of the Template class -------------------
|
||||
|
||||
|
||||
class ClassInfoWrapper(object):
|
||||
def __init__(self, name, mod):
|
||||
self.name = name
|
||||
self.mod = mod
|
||||
|
||||
def __repr__(self):
|
||||
return "%s" % (self.name)
|
||||
|
||||
|
||||
class HtmlOutput(testtools.TestResult):
|
||||
"""Output test results in html."""
|
||||
|
||||
def __init__(self, html_file='result.html'):
|
||||
super(HtmlOutput, self).__init__()
|
||||
self.success_count = 0
|
||||
self.failure_count = 0
|
||||
self.error_count = 0
|
||||
self.skip_count = 0
|
||||
self.result = []
|
||||
self.html_file = html_file
|
||||
|
||||
def addSuccess(self, test):
|
||||
self.success_count += 1
|
||||
output = test.shortDescription()
|
||||
if output is None:
|
||||
output = test.id()
|
||||
self.result.append((0, test, output, ''))
|
||||
|
||||
def addSkip(self, test, err):
|
||||
output = test.shortDescription()
|
||||
if output is None:
|
||||
output = test.id()
|
||||
self.skip_count += 1
|
||||
self.result.append((3, test, output, ''))
|
||||
|
||||
def addError(self, test, err):
|
||||
output = test.shortDescription()
|
||||
if output is None:
|
||||
output = test.id()
|
||||
# Skipped tests are handled by SkipTest Exceptions.
|
||||
# if err[0] == SkipTest:
|
||||
# self.skip_count += 1
|
||||
# self.result.append((3, test, output, ''))
|
||||
else:
|
||||
self.error_count += 1
|
||||
_exc_str = self.formatErr(err)
|
||||
self.result.append((2, test, output, _exc_str))
|
||||
|
||||
def addFailure(self, test, err):
|
||||
print(test)
|
||||
self.failure_count += 1
|
||||
_exc_str = self.formatErr(err)
|
||||
output = test.shortDescription()
|
||||
if output is None:
|
||||
output = test.id()
|
||||
self.result.append((1, test, output, _exc_str))
|
||||
|
||||
def formatErr(self, err):
|
||||
exctype, value, tb = err
|
||||
return ''.join(traceback.format_exception(exctype, value, tb))
|
||||
|
||||
def stopTestRun(self):
|
||||
super(HtmlOutput, self).stopTestRun()
|
||||
self.stopTime = datetime.datetime.now()
|
||||
report_attrs = self._getReportAttributes()
|
||||
generator = 'subunit2html %s' % __version__
|
||||
heading = self._generate_heading(report_attrs)
|
||||
report = self._generate_report()
|
||||
ending = self._generate_ending()
|
||||
output = TemplateData.HTML_TMPL % dict(
|
||||
title=saxutils.escape(TemplateData.DEFAULT_TITLE),
|
||||
generator=generator,
|
||||
stylesheet=TemplateData.STYLESHEET_TMPL,
|
||||
heading=heading,
|
||||
report=report,
|
||||
ending=ending,
|
||||
)
|
||||
if self.html_file:
|
||||
with open(self.html_file, 'wb') as html_file:
|
||||
html_file.write(output.encode('utf8'))
|
||||
|
||||
def _getReportAttributes(self):
|
||||
"""Return report attributes as a list of (name, value)."""
|
||||
status = []
|
||||
if self.success_count:
|
||||
status.append('Pass %s' % self.success_count)
|
||||
if self.failure_count:
|
||||
status.append('Failure %s' % self.failure_count)
|
||||
if self.error_count:
|
||||
status.append('Error %s' % self.error_count)
|
||||
if self.skip_count:
|
||||
status.append('Skip %s' % self.skip_count)
|
||||
if status:
|
||||
status = ' '.join(status)
|
||||
else:
|
||||
status = 'none'
|
||||
return [
|
||||
('Status', status),
|
||||
]
|
||||
|
||||
def _generate_heading(self, report_attrs):
|
||||
a_lines = []
|
||||
for name, value in report_attrs:
|
||||
line = TemplateData.HEADING_ATTRIBUTE_TMPL % dict(
|
||||
name=saxutils.escape(name),
|
||||
value=saxutils.escape(value),
|
||||
)
|
||||
a_lines.append(line)
|
||||
heading = TemplateData.HEADING_TMPL % dict(
|
||||
title=saxutils.escape(TemplateData.DEFAULT_TITLE),
|
||||
parameters=''.join(a_lines),
|
||||
description=saxutils.escape(TemplateData.DEFAULT_DESCRIPTION),
|
||||
)
|
||||
return heading
|
||||
|
||||
def _generate_report(self):
|
||||
rows = []
|
||||
sortedResult = self._sortResult(self.result)
|
||||
for cid, (cls, cls_results) in enumerate(sortedResult):
|
||||
# subtotal for a class
|
||||
np = nf = ne = ns = 0
|
||||
for n, t, o, e in cls_results:
|
||||
if n == 0:
|
||||
np += 1
|
||||
elif n == 1:
|
||||
nf += 1
|
||||
elif n == 2:
|
||||
ne += 1
|
||||
else:
|
||||
ns += 1
|
||||
|
||||
# format class description
|
||||
if cls.mod == "__main__":
|
||||
name = cls.name
|
||||
else:
|
||||
name = "%s" % (cls.name)
|
||||
doc = cls.__doc__ and cls.__doc__.split("\n")[0] or ""
|
||||
desc = doc and '%s: %s' % (name, doc) or name
|
||||
|
||||
row = TemplateData.REPORT_CLASS_TMPL % dict(
|
||||
style=(ne > 0 and 'errorClass' or nf > 0
|
||||
and 'failClass' or 'passClass'),
|
||||
desc = desc,
|
||||
count = np + nf + ne + ns,
|
||||
Pass = np,
|
||||
fail = nf,
|
||||
error = ne,
|
||||
skip = ns,
|
||||
cid = 'c%s' % (cid + 1),
|
||||
)
|
||||
rows.append(row)
|
||||
|
||||
for tid, (n, t, o, e) in enumerate(cls_results):
|
||||
self._generate_report_test(rows, cid, tid, n, t, o, e)
|
||||
|
||||
report = TemplateData.REPORT_TMPL % dict(
|
||||
test_list=''.join(rows),
|
||||
count=str(self.success_count + self.failure_count +
|
||||
self.error_count + self.skip_count),
|
||||
Pass=str(self.success_count),
|
||||
fail=str(self.failure_count),
|
||||
error=str(self.error_count),
|
||||
skip=str(self.skip_count),
|
||||
)
|
||||
return report
|
||||
|
||||
def _sortResult(self, result_list):
|
||||
# unittest does not seems to run in any particular order.
|
||||
# Here at least we want to group them together by class.
|
||||
rmap = {}
|
||||
# Differentiate between classes that have test failures so we can sort
|
||||
# them at the top of the html page for easier troubleshooting
|
||||
failclasses = []
|
||||
passclasses = []
|
||||
for n, t, o, e in result_list:
|
||||
classes = failclasses if n == 1 or n == 2 else passclasses
|
||||
if hasattr(t, '_tests'):
|
||||
for inner_test in t._tests:
|
||||
self._add_cls(rmap, classes, inner_test,
|
||||
(n, inner_test, o, e))
|
||||
else:
|
||||
self._add_cls(rmap, classes, t, (n, t, o, e))
|
||||
classort = lambda s: str(s)
|
||||
sortedfailclasses = sorted(failclasses, key=classort)
|
||||
sortedpassclasses = sorted(passclasses, key=classort)
|
||||
sortedclasses = sortedfailclasses + sortedpassclasses
|
||||
r = [(cls, rmap[str(cls)]) for cls in sortedclasses]
|
||||
return r
|
||||
|
||||
def _add_cls(self, rmap, classes, test, data_tuple):
|
||||
if hasattr(test, 'test'):
|
||||
test = test.test
|
||||
if test.__class__ == subunit.RemotedTestCase:
|
||||
cl = test._RemotedTestCase__description.rsplit('.', 1)[0]
|
||||
else:
|
||||
cl = test.id().rsplit('.', 1)[0]
|
||||
mod = cl.rsplit('.', 1)[0]
|
||||
cls = ClassInfoWrapper(cl, mod)
|
||||
if not str(cls) in rmap:
|
||||
rmap[str(cls)] = []
|
||||
classes.append(cls)
|
||||
rmap[str(cls)].append(data_tuple)
|
||||
|
||||
def _generate_report_test(self, rows, cid, tid, n, t, o, e):
|
||||
# e.g. 'pt1.1', 'ft1.1', etc
|
||||
# ptx.x for passed/skipped tests and ftx.x for failed/errored tests.
|
||||
has_output = bool(o or e)
|
||||
tid = ((n == 0 or n == 3) and
|
||||
'p' or 'f') + 't%s.%s' % (cid + 1, tid + 1)
|
||||
name = t.id().split('.')[-1]
|
||||
# if shortDescription is not the function name, use it
|
||||
if t.shortDescription().find(name) == -1:
|
||||
doc = t.shortDescription()
|
||||
else:
|
||||
doc = None
|
||||
desc = doc and ('%s: %s' % (name, doc)) or name
|
||||
tmpl = (has_output and TemplateData.REPORT_TEST_WITH_OUTPUT_TMPL
|
||||
or TemplateData.REPORT_TEST_NO_OUTPUT_TMPL)
|
||||
|
||||
script = TemplateData.REPORT_TEST_OUTPUT_TMPL % dict(
|
||||
id=tid,
|
||||
output=saxutils.escape(o + e),
|
||||
)
|
||||
|
||||
row = tmpl % dict(
|
||||
tid=tid,
|
||||
Class=((n == 0 or n == 3) and 'hiddenRow' or 'none'),
|
||||
style=(n == 2 and 'errorCase' or
|
||||
(n == 1 and 'failCase' or 'none')),
|
||||
desc=desc,
|
||||
script=script,
|
||||
status=TemplateData.STATUS[n],
|
||||
)
|
||||
rows.append(row)
|
||||
if not has_output:
|
||||
return
|
||||
|
||||
def _generate_ending(self):
|
||||
return TemplateData.ENDING_TMPL
|
||||
|
||||
def startTestRun(self):
|
||||
super(HtmlOutput, self).startTestRun()
|
||||
|
||||
|
||||
class FileAccumulator(testtools.StreamResult):
|
||||
|
||||
def __init__(self):
|
||||
super(FileAccumulator, self).__init__()
|
||||
self.route_codes = collections.defaultdict(io.BytesIO)
|
||||
|
||||
def status(self, **kwargs):
|
||||
if kwargs.get('file_name') != 'stdout':
|
||||
return
|
||||
file_bytes = kwargs.get('file_bytes')
|
||||
if not file_bytes:
|
||||
return
|
||||
route_code = kwargs.get('route_code')
|
||||
stream = self.route_codes[route_code]
|
||||
stream.write(file_bytes)
|
||||
|
||||
|
||||
def main():
|
||||
if '--version' in sys.argv:
|
||||
print(__version__)
|
||||
exit(0)
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Need at least one argument: path to subunit log.")
|
||||
exit(1)
|
||||
subunit_file = sys.argv[1]
|
||||
if len(sys.argv) > 2:
|
||||
html_file = sys.argv[2]
|
||||
else:
|
||||
html_file = 'results.html'
|
||||
|
||||
html_result = HtmlOutput(html_file)
|
||||
stream = open(subunit_file, 'rb')
|
||||
|
||||
# Feed the subunit stream through both a V1 and V2 parser.
|
||||
# Depends on having the v2 capable libraries installed.
|
||||
# First V2.
|
||||
# Non-v2 content and captured non-test output will be presented as file
|
||||
# segments called stdout.
|
||||
suite = subunit.ByteStreamToStreamResult(stream, non_subunit_name='stdout')
|
||||
# The HTML output code is in legacy mode.
|
||||
result = testtools.StreamToExtendedDecorator(html_result)
|
||||
# Divert non-test output
|
||||
accumulator = FileAccumulator()
|
||||
result = testtools.StreamResultRouter(result)
|
||||
result.add_rule(accumulator, 'test_id', test_id=None)
|
||||
result.startTestRun()
|
||||
suite.run(result)
|
||||
# Now reprocess any found stdout content as V1 subunit
|
||||
for bytes_io in accumulator.route_codes.values():
|
||||
bytes_io.seek(0)
|
||||
suite = subunit.ProtocolTestCase(bytes_io)
|
||||
suite.run(html_result)
|
||||
result.stopTestRun()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -1,403 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright 2014 Samsung Electronics
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Trace a subunit stream in reasonable detail and high accuracy."""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import pbr.version
|
||||
import subunit
|
||||
import testtools
|
||||
|
||||
from os_testr.utils import colorizer
|
||||
|
||||
# NOTE(mtreinish) on python3 anydbm was renamed dbm and the python2 dbm module
|
||||
# was renamed to dbm.ndbm, this block takes that into account
|
||||
try:
|
||||
import anydbm as dbm
|
||||
except ImportError:
|
||||
import dbm
|
||||
|
||||
DAY_SECONDS = 60 * 60 * 24
|
||||
FAILS = []
|
||||
RESULTS = {}
|
||||
|
||||
|
||||
def total_seconds(timedelta):
|
||||
# NOTE(mtreinish): This method is built-in to the timedelta class in
|
||||
# python >= 2.7 it is here to enable it's use on older versions
|
||||
return ((timedelta.days * DAY_SECONDS + timedelta.seconds) * 10 ** 6 +
|
||||
timedelta.microseconds) / 10 ** 6
|
||||
|
||||
|
||||
def cleanup_test_name(name, strip_tags=True, strip_scenarios=False):
|
||||
"""Clean up the test name for display.
|
||||
|
||||
By default we strip out the tags in the test because they don't help us
|
||||
in identifying the test that is run to it's result.
|
||||
|
||||
Make it possible to strip out the testscenarios information (not to
|
||||
be confused with tempest scenarios) however that's often needed to
|
||||
indentify generated negative tests.
|
||||
"""
|
||||
if strip_tags:
|
||||
tags_start = name.find('[')
|
||||
tags_end = name.find(']')
|
||||
if tags_start > 0 and tags_end > tags_start:
|
||||
newname = name[:tags_start]
|
||||
newname += name[tags_end + 1:]
|
||||
name = newname
|
||||
|
||||
if strip_scenarios:
|
||||
tags_start = name.find('(')
|
||||
tags_end = name.find(')')
|
||||
if tags_start > 0 and tags_end > tags_start:
|
||||
newname = name[:tags_start]
|
||||
newname += name[tags_end + 1:]
|
||||
name = newname
|
||||
|
||||
return name
|
||||
|
||||
|
||||
def get_duration(timestamps):
|
||||
start, end = timestamps
|
||||
if not start or not end:
|
||||
duration = ''
|
||||
else:
|
||||
delta = end - start
|
||||
duration = '%d.%06ds' % (
|
||||
delta.days * DAY_SECONDS + delta.seconds, delta.microseconds)
|
||||
return duration
|
||||
|
||||
|
||||
def find_worker(test):
|
||||
"""Get the worker number.
|
||||
|
||||
If there are no workers because we aren't in a concurrent environment,
|
||||
assume the worker number is 0.
|
||||
"""
|
||||
for tag in test['tags']:
|
||||
if tag.startswith('worker-'):
|
||||
return int(tag[7:])
|
||||
return 0
|
||||
|
||||
|
||||
# Print out stdout/stderr if it exists, always
|
||||
def print_attachments(stream, test, all_channels=False):
|
||||
"""Print out subunit attachments.
|
||||
|
||||
Print out subunit attachments that contain content. This
|
||||
runs in 2 modes, one for successes where we print out just stdout
|
||||
and stderr, and an override that dumps all the attachments.
|
||||
"""
|
||||
channels = ('stdout', 'stderr')
|
||||
for name, detail in test['details'].items():
|
||||
# NOTE(sdague): the subunit names are a little crazy, and actually
|
||||
# are in the form pythonlogging:'' (with the colon and quotes)
|
||||
name = name.split(':')[0]
|
||||
if detail.content_type.type == 'test':
|
||||
detail.content_type.type = 'text'
|
||||
if (all_channels or name in channels) and detail.as_text():
|
||||
title = "Captured %s:" % name
|
||||
stream.write("\n%s\n%s\n" % (title, ('~' * len(title))))
|
||||
# indent attachment lines 4 spaces to make them visually
|
||||
# offset
|
||||
for line in detail.as_text().split('\n'):
|
||||
line = line.encode('utf8')
|
||||
stream.write(" %s\n" % line)
|
||||
|
||||
|
||||
def find_test_run_time_diff(test_id, run_time):
|
||||
times_db_path = os.path.join(os.path.join(os.getcwd(), '.testrepository'),
|
||||
'times.dbm')
|
||||
if os.path.isfile(times_db_path):
|
||||
try:
|
||||
test_times = dbm.open(times_db_path)
|
||||
except Exception:
|
||||
return False
|
||||
try:
|
||||
avg_runtime = float(test_times.get(str(test_id), False))
|
||||
except Exception:
|
||||
try:
|
||||
avg_runtime = float(test_times[str(test_id)])
|
||||
except Exception:
|
||||
avg_runtime = False
|
||||
|
||||
if avg_runtime and avg_runtime > 0:
|
||||
run_time = float(run_time.rstrip('s'))
|
||||
perc_diff = ((run_time - avg_runtime) / avg_runtime) * 100
|
||||
return perc_diff
|
||||
return False
|
||||
|
||||
|
||||
def show_outcome(stream, test, print_failures=False, failonly=False,
|
||||
enable_diff=False, threshold='0', abbreviate=False,
|
||||
enable_color=False):
|
||||
global RESULTS
|
||||
status = test['status']
|
||||
# TODO(sdague): ask lifeless why on this?
|
||||
if status == 'exists':
|
||||
return
|
||||
|
||||
worker = find_worker(test)
|
||||
name = cleanup_test_name(test['id'])
|
||||
duration = get_duration(test['timestamps'])
|
||||
|
||||
if worker not in RESULTS:
|
||||
RESULTS[worker] = []
|
||||
RESULTS[worker].append(test)
|
||||
|
||||
# don't count the end of the return code as a fail
|
||||
if name == 'process-returncode':
|
||||
return
|
||||
|
||||
for color in [colorizer.AnsiColorizer, colorizer.NullColorizer]:
|
||||
if not enable_color:
|
||||
color = colorizer.NullColorizer(stream)
|
||||
break
|
||||
if color.supported():
|
||||
color = color(stream)
|
||||
break
|
||||
|
||||
if status == 'fail' or status == 'uxsuccess':
|
||||
FAILS.append(test)
|
||||
if abbreviate:
|
||||
color.write('F', 'red')
|
||||
else:
|
||||
stream.write('{%s} %s [%s] ... ' % (
|
||||
worker, name, duration))
|
||||
color.write('FAILED', 'red')
|
||||
stream.write('\n')
|
||||
if not print_failures:
|
||||
print_attachments(stream, test, all_channels=True)
|
||||
elif not failonly:
|
||||
if status == 'success' or status == 'xfail':
|
||||
if abbreviate:
|
||||
color.write('.', 'green')
|
||||
else:
|
||||
out_string = '{%s} %s [%s' % (worker, name, duration)
|
||||
perc_diff = find_test_run_time_diff(test['id'], duration)
|
||||
if enable_diff:
|
||||
if perc_diff and abs(perc_diff) >= abs(float(threshold)):
|
||||
if perc_diff > 0:
|
||||
out_string = out_string + ' +%.2f%%' % perc_diff
|
||||
else:
|
||||
out_string = out_string + ' %.2f%%' % perc_diff
|
||||
stream.write(out_string + '] ... ')
|
||||
color.write('ok', 'green')
|
||||
stream.write('\n')
|
||||
print_attachments(stream, test)
|
||||
elif status == 'skip':
|
||||
if abbreviate:
|
||||
color.write('S', 'blue')
|
||||
else:
|
||||
reason = test['details'].get('reason', '')
|
||||
if reason:
|
||||
reason = ': ' + reason.as_text()
|
||||
stream.write('{%s} %s ... ' % (
|
||||
worker, name))
|
||||
color.write('SKIPPED', 'blue')
|
||||
stream.write('%s' % (reason))
|
||||
stream.write('\n')
|
||||
else:
|
||||
if abbreviate:
|
||||
stream.write('%s' % test['status'][0])
|
||||
else:
|
||||
stream.write('{%s} %s [%s] ... %s\n' % (
|
||||
worker, name, duration, test['status']))
|
||||
if not print_failures:
|
||||
print_attachments(stream, test, all_channels=True)
|
||||
|
||||
stream.flush()
|
||||
|
||||
|
||||
def print_fails(stream):
|
||||
"""Print summary failure report.
|
||||
|
||||
Currently unused, however there remains debate on inline vs. at end
|
||||
reporting, so leave the utility function for later use.
|
||||
"""
|
||||
if not FAILS:
|
||||
return
|
||||
stream.write("\n==============================\n")
|
||||
stream.write("Failed %s tests - output below:" % len(FAILS))
|
||||
stream.write("\n==============================\n")
|
||||
for f in FAILS:
|
||||
stream.write("\n%s\n" % f['id'])
|
||||
stream.write("%s\n" % ('-' * len(f['id'])))
|
||||
print_attachments(stream, f, all_channels=True)
|
||||
stream.write('\n')
|
||||
|
||||
|
||||
def count_tests(key, value):
|
||||
count = 0
|
||||
for k, v in RESULTS.items():
|
||||
for item in v:
|
||||
if key in item:
|
||||
if re.search(value, item[key]):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def run_time():
|
||||
runtime = 0.0
|
||||
for k, v in RESULTS.items():
|
||||
for test in v:
|
||||
test_dur = get_duration(test['timestamps']).strip('s')
|
||||
# NOTE(toabctl): get_duration() can return an empty string
|
||||
# which leads to a ValueError when casting to float
|
||||
if test_dur:
|
||||
runtime += float(test_dur)
|
||||
return runtime
|
||||
|
||||
|
||||
def worker_stats(worker):
|
||||
tests = RESULTS[worker]
|
||||
num_tests = len(tests)
|
||||
stop_time = tests[-1]['timestamps'][1]
|
||||
start_time = tests[0]['timestamps'][0]
|
||||
if not start_time or not stop_time:
|
||||
delta = 'N/A'
|
||||
else:
|
||||
delta = stop_time - start_time
|
||||
return num_tests, str(delta)
|
||||
|
||||
|
||||
def print_summary(stream, elapsed_time):
|
||||
stream.write("\n======\nTotals\n======\n")
|
||||
stream.write("Ran: %s tests in %.4f sec.\n" % (
|
||||
count_tests('status', '.*'), total_seconds(elapsed_time)))
|
||||
stream.write(" - Passed: %s\n" % count_tests('status', '^success$'))
|
||||
stream.write(" - Skipped: %s\n" % count_tests('status', '^skip$'))
|
||||
stream.write(" - Expected Fail: %s\n" % count_tests('status', '^xfail$'))
|
||||
stream.write(" - Unexpected Success: %s\n" % count_tests('status',
|
||||
'^uxsuccess$'))
|
||||
stream.write(" - Failed: %s\n" % count_tests('status', '^fail$'))
|
||||
stream.write("Sum of execute time for each test: %.4f sec.\n" % run_time())
|
||||
|
||||
# we could have no results, especially as we filter out the process-codes
|
||||
if RESULTS:
|
||||
stream.write("\n==============\nWorker Balance\n==============\n")
|
||||
|
||||
for w in range(max(RESULTS.keys()) + 1):
|
||||
if w not in RESULTS:
|
||||
stream.write(
|
||||
" - WARNING: missing Worker %s! "
|
||||
"Race in testr accounting.\n" % w)
|
||||
else:
|
||||
num, time = worker_stats(w)
|
||||
out_str = " - Worker %s (%s tests) => %s" % (w, num, time)
|
||||
if time.isdigit():
|
||||
out_str += 's'
|
||||
out_str += '\n'
|
||||
stream.write(out_str)
|
||||
|
||||
|
||||
__version__ = pbr.version.VersionInfo('os_testr').version_string()
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--version', action='version',
|
||||
version='%s' % __version__)
|
||||
parser.add_argument('--no-failure-debug', '-n', action='store_true',
|
||||
dest='print_failures', help='Disable printing failure '
|
||||
'debug information in realtime')
|
||||
parser.add_argument('--fails', '-f', action='store_true',
|
||||
dest='post_fails', help='Print failure debug '
|
||||
'information after the stream is proccesed')
|
||||
parser.add_argument('--failonly', action='store_true',
|
||||
dest='failonly', help="Don't print success items",
|
||||
default=(
|
||||
os.environ.get('TRACE_FAILONLY', False)
|
||||
is not False))
|
||||
parser.add_argument('--abbreviate', '-a', action='store_true',
|
||||
dest='abbreviate', help='Print one character status'
|
||||
'for each test')
|
||||
parser.add_argument('--perc-diff', '-d', action='store_true',
|
||||
dest='enable_diff',
|
||||
help="Print percent change in run time on each test ")
|
||||
parser.add_argument('--diff-threshold', '-t', dest='threshold',
|
||||
help="Threshold to use for displaying percent change "
|
||||
"from the avg run time. If one is not specified "
|
||||
"the percent change will always be displayed")
|
||||
parser.add_argument('--no-summary', action='store_true',
|
||||
help="Don't print the summary of the test run after "
|
||||
" completes")
|
||||
parser.add_argument('--color', action='store_true',
|
||||
help="Print results with colors")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def trace(stdin, stdout, print_failures=False, failonly=False,
|
||||
enable_diff=False, abbreviate=False, color=False, post_fails=False,
|
||||
no_summary=False):
|
||||
stream = subunit.ByteStreamToStreamResult(
|
||||
stdin, non_subunit_name='stdout')
|
||||
outcomes = testtools.StreamToDict(
|
||||
functools.partial(show_outcome, stdout,
|
||||
print_failures=print_failures,
|
||||
failonly=failonly,
|
||||
enable_diff=enable_diff,
|
||||
abbreviate=abbreviate,
|
||||
enable_color=color))
|
||||
summary = testtools.StreamSummary()
|
||||
result = testtools.CopyStreamResult([outcomes, summary])
|
||||
result = testtools.StreamResultRouter(result)
|
||||
cat = subunit.test_results.CatFiles(stdout)
|
||||
result.add_rule(cat, 'test_id', test_id=None)
|
||||
start_time = datetime.datetime.utcnow()
|
||||
result.startTestRun()
|
||||
try:
|
||||
stream.run(result)
|
||||
finally:
|
||||
result.stopTestRun()
|
||||
stop_time = datetime.datetime.utcnow()
|
||||
elapsed_time = stop_time - start_time
|
||||
|
||||
if count_tests('status', '.*') == 0:
|
||||
print("The test run didn't actually run any tests")
|
||||
return 1
|
||||
if post_fails:
|
||||
print_fails(stdout)
|
||||
if not no_summary:
|
||||
print_summary(stdout, elapsed_time)
|
||||
|
||||
# NOTE(mtreinish): Ideally this should live in testtools streamSummary
|
||||
# this is just in place until the behavior lands there (if it ever does)
|
||||
if count_tests('status', '^success$') == 0:
|
||||
print("\nNo tests were successful during the run")
|
||||
return 1
|
||||
return 0 if summary.wasSuccessful() else 1
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
exit(trace(sys.stdin, sys.stdout, args.print_failures, args.failonly,
|
||||
args.enable_diff, args.abbreviate, args.color, args.post_fails,
|
||||
args.no_summary))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -1,23 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2010-2011 OpenStack Foundation
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslotest import base
|
||||
|
||||
|
||||
class TestCase(base.BaseTestCase):
|
||||
|
||||
"""Test case base class for all unit tests."""
|
@ -1,23 +0,0 @@
|
||||
# Copyright 2013 IBM Corp.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import testtools
|
||||
|
||||
class FakeTestClass(testtools.TestCase):
|
||||
def test_pass(self):
|
||||
self.assertTrue(False)
|
||||
|
||||
def test_pass_list(self):
|
||||
test_list = ['test', 'a', 'b']
|
||||
self.assertIn('fail', test_list)
|
@ -1,23 +0,0 @@
|
||||
# Copyright 2013 IBM Corp.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import testtools
|
||||
|
||||
class FakeTestClass(testtools.TestCase):
|
||||
def test_pass(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_pass_list(self):
|
||||
test_list = ['test', 'a', 'b']
|
||||
self.assertIn('test', test_list)
|
@ -1,20 +0,0 @@
|
||||
[metadata]
|
||||
name = tempest_unit_tests
|
||||
version = 1
|
||||
summary = Fake Project for testing wrapper scripts
|
||||
author = OpenStack
|
||||
author-email = openstack-dev@lists.openstack.org
|
||||
home-page = http://www.openstack.org/
|
||||
classifier =
|
||||
Intended Audience :: Information Technology
|
||||
Intended Audience :: System Administrators
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: POSIX :: Linux
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 2
|
||||
Programming Language :: Python :: 2.7
|
||||
|
||||
[global]
|
||||
setup-hooks =
|
||||
pbr.hooks.setup_hook
|
@ -1,5 +0,0 @@
|
||||
[DEFAULT]
|
||||
test_command=${PYTHON:-python} -m subunit.run discover -t ./ ./tests $LISTOPT $IDOPTION
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
||||
group_regex=([^\.]*\.)*
|
Binary file not shown.
Binary file not shown.
@ -1,117 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
test_os_testr
|
||||
----------------------------------
|
||||
|
||||
Tests for `os_testr` module.
|
||||
"""
|
||||
|
||||
import mock
|
||||
|
||||
from os_testr import ostestr as os_testr
|
||||
from os_testr.tests import base
|
||||
|
||||
|
||||
class TestGetParser(base.TestCase):
|
||||
def test_pretty(self):
|
||||
namespace = os_testr.get_parser(['--pretty'])
|
||||
self.assertEqual(True, namespace[0].pretty)
|
||||
namespace = os_testr.get_parser(['--no-pretty'])
|
||||
self.assertEqual(False, namespace[0].pretty)
|
||||
self.assertRaises(SystemExit, os_testr.get_parser,
|
||||
['--no-pretty', '--pretty'])
|
||||
|
||||
def test_slowest(self):
|
||||
namespace = os_testr.get_parser(['--slowest'])
|
||||
self.assertEqual(True, namespace[0].slowest)
|
||||
namespace = os_testr.get_parser(['--no-slowest'])
|
||||
self.assertEqual(False, namespace[0].slowest)
|
||||
self.assertRaises(SystemExit, os_testr.get_parser,
|
||||
['--no-slowest', '--slowest'])
|
||||
|
||||
def test_parallel(self):
|
||||
namespace = os_testr.get_parser(['--parallel'])
|
||||
self.assertEqual(True, namespace[0].parallel)
|
||||
namespace = os_testr.get_parser(['--serial'])
|
||||
self.assertEqual(False, namespace[0].parallel)
|
||||
self.assertRaises(SystemExit, os_testr.get_parser,
|
||||
['--parallel', '--serial'])
|
||||
|
||||
|
||||
class TestCallers(base.TestCase):
|
||||
def test_no_discover(self):
|
||||
namespace = os_testr.get_parser(['-n', 'project.tests.foo'])
|
||||
|
||||
def _fake_exit(arg):
|
||||
self.assertTrue(arg)
|
||||
|
||||
def _fake_run(*args, **kwargs):
|
||||
return 'project.tests.foo' in args
|
||||
|
||||
with mock.patch.object(os_testr, 'exit', side_effect=_fake_exit), \
|
||||
mock.patch.object(os_testr, 'get_parser', return_value=namespace), \
|
||||
mock.patch.object(os_testr,
|
||||
'call_subunit_run',
|
||||
side_effect=_fake_run):
|
||||
os_testr.main()
|
||||
|
||||
def test_no_discover_path(self):
|
||||
namespace = os_testr.get_parser(['-n', 'project/tests/foo'])
|
||||
|
||||
def _fake_exit(arg):
|
||||
self.assertTrue(arg)
|
||||
|
||||
def _fake_run(*args, **kwargs):
|
||||
return 'project.tests.foo' in args
|
||||
|
||||
with mock.patch.object(os_testr, 'exit', side_effect=_fake_exit), \
|
||||
mock.patch.object(os_testr, 'get_parser', return_value=namespace), \
|
||||
mock.patch.object(os_testr,
|
||||
'call_subunit_run',
|
||||
side_effect=_fake_run):
|
||||
os_testr.main()
|
||||
|
||||
def test_pdb(self):
|
||||
namespace = os_testr.get_parser(['--pdb', 'project.tests.foo'])
|
||||
|
||||
def _fake_exit(arg):
|
||||
self.assertTrue(arg)
|
||||
|
||||
def _fake_run(*args, **kwargs):
|
||||
return 'project.tests.foo' in args
|
||||
|
||||
with mock.patch.object(os_testr, 'exit', side_effect=_fake_exit), \
|
||||
mock.patch.object(os_testr, 'get_parser', return_value=namespace), \
|
||||
mock.patch.object(os_testr,
|
||||
'call_subunit_run',
|
||||
side_effect=_fake_run):
|
||||
os_testr.main()
|
||||
|
||||
def test_pdb_path(self):
|
||||
namespace = os_testr.get_parser(['--pdb', 'project/tests/foo'])
|
||||
|
||||
def _fake_exit(arg):
|
||||
self.assertTrue(arg)
|
||||
|
||||
def _fake_run(*args, **kwargs):
|
||||
return 'project.tests.foo' in args
|
||||
|
||||
with mock.patch.object(os_testr, 'exit', side_effect=_fake_exit), \
|
||||
mock.patch.object(os_testr, 'get_parser', return_value=namespace), \
|
||||
mock.patch.object(os_testr,
|
||||
'call_subunit_run',
|
||||
side_effect=_fake_run):
|
||||
os_testr.main()
|
@ -1,170 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
|
||||
import six
|
||||
|
||||
from os_testr import regex_builder as os_testr
|
||||
from os_testr.tests import base
|
||||
|
||||
|
||||
class TestPathToRegex(base.TestCase):
|
||||
|
||||
def test_file_name(self):
|
||||
result = os_testr.path_to_regex("tests/network/v2/test_net.py")
|
||||
self.assertEqual("tests.network.v2.test_net", result)
|
||||
result = os_testr.path_to_regex("openstack/tests/network/v2")
|
||||
self.assertEqual("openstack.tests.network.v2", result)
|
||||
|
||||
|
||||
class TestConstructRegex(base.TestCase):
|
||||
def test_regex_passthrough(self):
|
||||
result = os_testr.construct_regex(None, None, 'fake_regex', False)
|
||||
self.assertEqual(result, 'fake_regex')
|
||||
|
||||
def test_blacklist_regex_with_comments(self):
|
||||
blacklist_file = six.StringIO()
|
||||
for i in range(4):
|
||||
blacklist_file.write('fake_regex_%s # A Comment\n' % i)
|
||||
blacklist_file.seek(0)
|
||||
with mock.patch('six.moves.builtins.open',
|
||||
return_value=blacklist_file):
|
||||
result = os_testr.construct_regex('fake_path', None, None, False)
|
||||
self.assertEqual(
|
||||
result,
|
||||
"^((?!fake_regex_3|fake_regex_2|fake_regex_1|fake_regex_0).)*$")
|
||||
|
||||
def test_whitelist_regex_with_comments(self):
|
||||
whitelist_file = six.StringIO()
|
||||
for i in range(4):
|
||||
whitelist_file.write('fake_regex_%s # A Comment\n' % i)
|
||||
whitelist_file.seek(0)
|
||||
with mock.patch('six.moves.builtins.open',
|
||||
return_value=whitelist_file):
|
||||
result = os_testr.construct_regex(None, 'fake_path', None, False)
|
||||
self.assertEqual(
|
||||
result,
|
||||
"fake_regex_0|fake_regex_1|fake_regex_2|fake_regex_3")
|
||||
|
||||
def test_blacklist_regex_without_comments(self):
|
||||
blacklist_file = six.StringIO()
|
||||
for i in range(4):
|
||||
blacklist_file.write('fake_regex_%s\n' % i)
|
||||
blacklist_file.seek(0)
|
||||
with mock.patch('six.moves.builtins.open',
|
||||
return_value=blacklist_file):
|
||||
result = os_testr.construct_regex('fake_path', None, None, False)
|
||||
self.assertEqual(
|
||||
result,
|
||||
"^((?!fake_regex_3|fake_regex_2|fake_regex_1|fake_regex_0).)*$")
|
||||
|
||||
def test_blacklist_regex_with_comments_and_regex(self):
|
||||
blacklist_file = six.StringIO()
|
||||
for i in range(4):
|
||||
blacklist_file.write('fake_regex_%s # Comments\n' % i)
|
||||
blacklist_file.seek(0)
|
||||
with mock.patch('six.moves.builtins.open',
|
||||
return_value=blacklist_file):
|
||||
result = os_testr.construct_regex('fake_path', None,
|
||||
'fake_regex', False)
|
||||
|
||||
expected_regex = ("^((?!fake_regex_3|fake_regex_2|fake_regex_1|"
|
||||
"fake_regex_0).)*$fake_regex")
|
||||
self.assertEqual(result, expected_regex)
|
||||
|
||||
def test_blacklist_regex_without_comments_and_regex(self):
|
||||
blacklist_file = six.StringIO()
|
||||
for i in range(4):
|
||||
blacklist_file.write('fake_regex_%s\n' % i)
|
||||
blacklist_file.seek(0)
|
||||
with mock.patch('six.moves.builtins.open',
|
||||
return_value=blacklist_file):
|
||||
result = os_testr.construct_regex('fake_path', None,
|
||||
'fake_regex', False)
|
||||
|
||||
expected_regex = ("^((?!fake_regex_3|fake_regex_2|fake_regex_1|"
|
||||
"fake_regex_0).)*$fake_regex")
|
||||
self.assertEqual(result, expected_regex)
|
||||
|
||||
@mock.patch.object(os_testr, 'print_skips')
|
||||
def test_blacklist_regex_with_comment_print_skips(self, print_mock):
|
||||
blacklist_file = six.StringIO()
|
||||
for i in range(4):
|
||||
blacklist_file.write('fake_regex_%s # Comment\n' % i)
|
||||
blacklist_file.seek(0)
|
||||
with mock.patch('six.moves.builtins.open',
|
||||
return_value=blacklist_file):
|
||||
result = os_testr.construct_regex('fake_path', None,
|
||||
None, True)
|
||||
|
||||
expected_regex = ("^((?!fake_regex_3|fake_regex_2|fake_regex_1|"
|
||||
"fake_regex_0).)*$")
|
||||
self.assertEqual(result, expected_regex)
|
||||
calls = print_mock.mock_calls
|
||||
self.assertEqual(len(calls), 4)
|
||||
args = list(map(lambda x: x[1], calls))
|
||||
self.assertIn(('fake_regex_0', 'Comment'), args)
|
||||
self.assertIn(('fake_regex_1', 'Comment'), args)
|
||||
self.assertIn(('fake_regex_2', 'Comment'), args)
|
||||
self.assertIn(('fake_regex_3', 'Comment'), args)
|
||||
|
||||
@mock.patch.object(os_testr, 'print_skips')
|
||||
def test_blacklist_regex_without_comment_print_skips(self, print_mock):
|
||||
blacklist_file = six.StringIO()
|
||||
for i in range(4):
|
||||
blacklist_file.write('fake_regex_%s\n' % i)
|
||||
blacklist_file.seek(0)
|
||||
with mock.patch('six.moves.builtins.open',
|
||||
return_value=blacklist_file):
|
||||
result = os_testr.construct_regex('fake_path', None,
|
||||
None, True)
|
||||
|
||||
expected_regex = ("^((?!fake_regex_3|fake_regex_2|"
|
||||
"fake_regex_1|fake_regex_0).)*$")
|
||||
self.assertEqual(result, expected_regex)
|
||||
calls = print_mock.mock_calls
|
||||
self.assertEqual(len(calls), 4)
|
||||
args = list(map(lambda x: x[1], calls))
|
||||
self.assertIn(('fake_regex_0', ''), args)
|
||||
self.assertIn(('fake_regex_1', ''), args)
|
||||
self.assertIn(('fake_regex_2', ''), args)
|
||||
self.assertIn(('fake_regex_3', ''), args)
|
||||
|
||||
|
||||
class TestWhitelistFile(base.TestCase):
|
||||
def test_read_whitelist_file(self):
|
||||
file_contents = """regex_a
|
||||
regex_b"""
|
||||
whitelist_file = six.StringIO()
|
||||
whitelist_file.write(file_contents)
|
||||
whitelist_file.seek(0)
|
||||
with mock.patch('six.moves.builtins.open',
|
||||
return_value=whitelist_file):
|
||||
regex = os_testr.get_regex_from_whitelist_file('/path/to/not_used')
|
||||
self.assertEqual('regex_a|regex_b', regex)
|
||||
|
||||
def test_whitelist_regex_without_comments_and_regex(self):
|
||||
file_contents = """regex_a
|
||||
regex_b"""
|
||||
whitelist_file = six.StringIO()
|
||||
whitelist_file.write(file_contents)
|
||||
whitelist_file.seek(0)
|
||||
with mock.patch('six.moves.builtins.open',
|
||||
return_value=whitelist_file):
|
||||
result = os_testr.construct_regex(None, 'fake_path',
|
||||
None, False)
|
||||
|
||||
expected_regex = 'regex_a|regex_b'
|
||||
self.assertEqual(result, expected_regex)
|
@ -1,104 +0,0 @@
|
||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
import testtools
|
||||
|
||||
from os_testr.tests import base
|
||||
from six import StringIO
|
||||
|
||||
DEVNULL = open(os.devnull, 'wb')
|
||||
|
||||
|
||||
class TestReturnCodes(base.TestCase):
|
||||
def setUp(self):
|
||||
super(TestReturnCodes, self).setUp()
|
||||
# Setup test dirs
|
||||
self.directory = tempfile.mkdtemp(prefix='ostestr-unit')
|
||||
self.addCleanup(shutil.rmtree, self.directory)
|
||||
self.test_dir = os.path.join(self.directory, 'tests')
|
||||
os.mkdir(self.test_dir)
|
||||
# Setup Test files
|
||||
self.testr_conf_file = os.path.join(self.directory, '.testr.conf')
|
||||
self.setup_cfg_file = os.path.join(self.directory, 'setup.cfg')
|
||||
self.passing_file = os.path.join(self.test_dir, 'test_passing.py')
|
||||
self.failing_file = os.path.join(self.test_dir, 'test_failing.py')
|
||||
self.init_file = os.path.join(self.test_dir, '__init__.py')
|
||||
self.setup_py = os.path.join(self.directory, 'setup.py')
|
||||
shutil.copy('os_testr/tests/files/testr-conf', self.testr_conf_file)
|
||||
shutil.copy('os_testr/tests/files/passing-tests', self.passing_file)
|
||||
shutil.copy('os_testr/tests/files/failing-tests', self.failing_file)
|
||||
shutil.copy('setup.py', self.setup_py)
|
||||
shutil.copy('os_testr/tests/files/setup.cfg', self.setup_cfg_file)
|
||||
shutil.copy('os_testr/tests/files/__init__.py', self.init_file)
|
||||
|
||||
self.stdout = StringIO()
|
||||
self.stderr = StringIO()
|
||||
# Change directory, run wrapper and check result
|
||||
self.addCleanup(os.chdir, os.path.abspath(os.curdir))
|
||||
os.chdir(self.directory)
|
||||
|
||||
def assertRunExit(self, cmd, expected, subunit=False):
|
||||
p = subprocess.Popen(
|
||||
"%s" % cmd, shell=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = p.communicate()
|
||||
|
||||
if not subunit:
|
||||
self.assertEqual(
|
||||
p.returncode, expected,
|
||||
"Stdout: %s; Stderr: %s" % (out, err))
|
||||
else:
|
||||
self.assertEqual(p.returncode, expected,
|
||||
"Expected return code: %s doesn't match actual "
|
||||
"return code of: %s" % (expected, p.returncode))
|
||||
|
||||
def test_default_passing(self):
|
||||
self.assertRunExit('ostestr --regex passing', 0)
|
||||
|
||||
def test_default_fails(self):
|
||||
self.assertRunExit('ostestr', 1)
|
||||
|
||||
def test_default_passing_no_slowest(self):
|
||||
self.assertRunExit('ostestr --no-slowest --regex passing', 0)
|
||||
|
||||
def test_default_fails_no_slowest(self):
|
||||
self.assertRunExit('ostestr --no-slowest', 1)
|
||||
|
||||
def test_default_serial_passing(self):
|
||||
self.assertRunExit('ostestr --serial --regex passing', 0)
|
||||
|
||||
def test_default_serial_fails(self):
|
||||
self.assertRunExit('ostestr --serial', 1)
|
||||
|
||||
def test_testr_subunit_passing(self):
|
||||
self.assertRunExit('ostestr --no-pretty --subunit --regex passing', 0,
|
||||
subunit=True)
|
||||
|
||||
@testtools.skip('Skipped because of testrepository lp bug #1411804')
|
||||
def test_testr_subunit_fails(self):
|
||||
self.assertRunExit('ostestr --no-pretty --subunit', 1, subunit=True)
|
||||
|
||||
def test_testr_no_pretty_passing(self):
|
||||
self.assertRunExit('ostestr --no-pretty --regex passing', 0)
|
||||
|
||||
def test_testr_no_pretty_fails(self):
|
||||
self.assertRunExit('ostestr --no-pretty', 1)
|
||||
|
||||
def test_list(self):
|
||||
self.assertRunExit('ostestr --list', 0)
|
@ -1,76 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import sys
|
||||
|
||||
from ddt import data
|
||||
from ddt import ddt
|
||||
from subunit import RemotedTestCase
|
||||
from testtools import PlaceHolder
|
||||
|
||||
from os_testr import subunit2html
|
||||
from os_testr.tests import base
|
||||
|
||||
|
||||
@ddt
|
||||
class TestSubunit2html(base.TestCase):
|
||||
@data(RemotedTestCase, PlaceHolder)
|
||||
def test_class_parsing(self, test_cls):
|
||||
"""Tests that the class paths are parsed for v1 & v2 tests"""
|
||||
test_ = test_cls("example.path.to.test.method")
|
||||
obj_ = subunit2html.HtmlOutput()
|
||||
cls_ = []
|
||||
obj_._add_cls({}, cls_, test_, ())
|
||||
self.assertEqual("example.path.to.test", cls_[0].name)
|
||||
|
||||
@data(RemotedTestCase, PlaceHolder)
|
||||
def test_result_sorting(self, test_cls):
|
||||
tests = []
|
||||
for i in range(9):
|
||||
tests.append(test_cls('example.path.to.test%d.method' % i))
|
||||
# addFailure, addError, and addSkip need the real exc_info
|
||||
try:
|
||||
raise Exception('fake')
|
||||
except Exception:
|
||||
err = sys.exc_info()
|
||||
obj = subunit2html.HtmlOutput()
|
||||
obj.addSuccess(tests[3])
|
||||
obj.addSuccess(tests[1])
|
||||
# example.path.to.test2 has a failure
|
||||
obj.addFailure(tests[2], err)
|
||||
obj.addSkip(tests[0], err)
|
||||
obj.addSuccess(tests[8])
|
||||
# example.path.to.test5 has a failure (error)
|
||||
obj.addError(tests[5], err)
|
||||
# example.path.to.test4 has a failure
|
||||
obj.addFailure(tests[4], err)
|
||||
obj.addSuccess(tests[7])
|
||||
# example.path.to.test6 has a failure
|
||||
obj.addFailure(tests[6], err)
|
||||
sorted_result = obj._sortResult(obj.result)
|
||||
# _sortResult returns a list of results of format:
|
||||
# [(class, [test_result_tuple, ...]), ...]
|
||||
# sorted by str(class)
|
||||
#
|
||||
# Classes with failures (2, 4, 5, and 6) should be sorted separately
|
||||
# at the top. The rest of the classes should be in sorted order after.
|
||||
expected_class_order = ['example.path.to.test2',
|
||||
'example.path.to.test4',
|
||||
'example.path.to.test5',
|
||||
'example.path.to.test6',
|
||||
'example.path.to.test0',
|
||||
'example.path.to.test1',
|
||||
'example.path.to.test3',
|
||||
'example.path.to.test7',
|
||||
'example.path.to.test8']
|
||||
for i, r in enumerate(sorted_result):
|
||||
self.assertEqual(expected_class_order[i], str(r[0]))
|
@ -1,96 +0,0 @@
|
||||
# Copyright 2015 SUSE Linux GmbH
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from datetime import datetime as dt
|
||||
import io
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from ddt import data
|
||||
from ddt import ddt
|
||||
from ddt import unpack
|
||||
from mock import patch
|
||||
import six
|
||||
|
||||
from os_testr import subunit_trace
|
||||
from os_testr.tests import base
|
||||
|
||||
|
||||
@ddt
|
||||
class TestSubunitTrace(base.TestCase):
|
||||
|
||||
@data(([dt(2015, 4, 17, 22, 23, 14, 111111),
|
||||
dt(2015, 4, 17, 22, 23, 14, 111111)],
|
||||
"0.000000s"),
|
||||
([dt(2015, 4, 17, 22, 23, 14, 111111),
|
||||
dt(2015, 4, 17, 22, 23, 15, 111111)],
|
||||
"1.000000s"),
|
||||
([dt(2015, 4, 17, 22, 23, 14, 111111),
|
||||
None],
|
||||
""))
|
||||
@unpack
|
||||
def test_get_durating(self, timestamps, expected_result):
|
||||
self.assertEqual(subunit_trace.get_duration(timestamps),
|
||||
expected_result)
|
||||
|
||||
@data(([dt(2015, 4, 17, 22, 23, 14, 111111),
|
||||
dt(2015, 4, 17, 22, 23, 14, 111111)],
|
||||
0.0),
|
||||
([dt(2015, 4, 17, 22, 23, 14, 111111),
|
||||
dt(2015, 4, 17, 22, 23, 15, 111111)],
|
||||
1.0),
|
||||
([dt(2015, 4, 17, 22, 23, 14, 111111),
|
||||
None],
|
||||
0.0))
|
||||
@unpack
|
||||
def test_run_time(self, timestamps, expected_result):
|
||||
patched_res = {
|
||||
0: [
|
||||
{'timestamps': timestamps}
|
||||
]
|
||||
}
|
||||
with patch.dict(subunit_trace.RESULTS, patched_res, clear=True):
|
||||
self.assertEqual(subunit_trace.run_time(), expected_result)
|
||||
|
||||
def test_return_code_all_skips(self):
|
||||
skips_stream = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
'sample_streams/all_skips.subunit')
|
||||
p = subprocess.Popen(['subunit-trace'], stdin=subprocess.PIPE)
|
||||
with open(skips_stream, 'rb') as stream:
|
||||
p.communicate(stream.read())
|
||||
self.assertEqual(1, p.returncode)
|
||||
|
||||
def test_return_code_normal_run(self):
|
||||
regular_stream = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
'sample_streams/successful.subunit')
|
||||
p = subprocess.Popen(['subunit-trace'], stdin=subprocess.PIPE)
|
||||
with open(regular_stream, 'rb') as stream:
|
||||
p.communicate(stream.read())
|
||||
self.assertEqual(0, p.returncode)
|
||||
|
||||
def test_trace(self):
|
||||
regular_stream = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
'sample_streams/successful.subunit')
|
||||
bytes_ = io.BytesIO()
|
||||
with open(regular_stream, 'rb') as stream:
|
||||
bytes_.write(six.binary_type(stream.read()))
|
||||
bytes_.seek(0)
|
||||
stdin = io.TextIOWrapper(io.BufferedReader(bytes_))
|
||||
returncode = subunit_trace.trace(stdin, sys.stdout)
|
||||
self.assertEqual(0, returncode)
|
@ -1,76 +0,0 @@
|
||||
# Copyright 2016 Hewlett Packard Enterprise Development LP
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import six
|
||||
import sys
|
||||
|
||||
from ddt import data
|
||||
from ddt import ddt
|
||||
from ddt import unpack
|
||||
|
||||
from os_testr.tests import base
|
||||
from os_testr.utils import colorizer
|
||||
|
||||
|
||||
@ddt
|
||||
class TestNullColorizer(base.TestCase):
|
||||
|
||||
@data(None, "foo", sys.stdout, )
|
||||
def test_supported_always_true(self, stream):
|
||||
self.assertTrue(colorizer.NullColorizer.supported(stream))
|
||||
|
||||
@data(("foo", "red"), ("foo", "bar"))
|
||||
@unpack
|
||||
def test_write_string_ignore_color(self, text, color):
|
||||
output = six.StringIO()
|
||||
c = colorizer.NullColorizer(output)
|
||||
c.write(text, color)
|
||||
self.assertEqual(text, output.getvalue())
|
||||
|
||||
@data((None, "red"), (None, None))
|
||||
@unpack
|
||||
def test_write_none_exception(self, text, color):
|
||||
c = colorizer.NullColorizer(sys.stdout)
|
||||
self.assertRaises(TypeError, c.write, text, color)
|
||||
|
||||
|
||||
@ddt
|
||||
class TestAnsiColorizer(base.TestCase):
|
||||
|
||||
def test_supported_false(self):
|
||||
# NOTE(masayukig): This returns False because our unittest env isn't
|
||||
# interactive
|
||||
self.assertFalse(colorizer.AnsiColorizer.supported(sys.stdout))
|
||||
|
||||
@data(None, "foo")
|
||||
def test_supported_error(self, stream):
|
||||
self.assertRaises(AttributeError,
|
||||
colorizer.AnsiColorizer.supported, stream)
|
||||
|
||||
@data(("foo", "red", "31"), ("foo", "blue", "34"))
|
||||
@unpack
|
||||
def test_write_string_valid_color(self, text, color, color_code):
|
||||
output = six.StringIO()
|
||||
c = colorizer.AnsiColorizer(output)
|
||||
c.write(text, color)
|
||||
self.assertIn(text, output.getvalue())
|
||||
self.assertIn(color_code, output.getvalue())
|
||||
|
||||
@data(("foo", None), ("foo", "invalid_color"))
|
||||
@unpack
|
||||
def test_write_string_invalid_color(self, text, color):
|
||||
output = six.StringIO()
|
||||
c = colorizer.AnsiColorizer(output)
|
||||
self.assertRaises(KeyError, c.write, text, color)
|
@ -1,98 +0,0 @@
|
||||
# Copyright 2015 NEC Corporation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# Colorizer Code is borrowed from Twisted:
|
||||
# Copyright (c) 2001-2010 Twisted Matrix Laboratories.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
import sys
|
||||
|
||||
|
||||
class AnsiColorizer(object):
|
||||
"""A colorizer is an object that loosely wraps around a stream
|
||||
|
||||
allowing callers to write text to the stream in a particular color.
|
||||
|
||||
Colorizer classes must implement C{supported()} and C{write(text, color)}.
|
||||
"""
|
||||
_colors = dict(black=30, red=31, green=32, yellow=33,
|
||||
blue=34, magenta=35, cyan=36, white=37)
|
||||
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
|
||||
@classmethod
|
||||
def supported(cls, stream=sys.stdout):
|
||||
"""Check the current platform supports coloring terminal output
|
||||
|
||||
A class method that returns True if the current platform supports
|
||||
coloring terminal output using this method. Returns False otherwise.
|
||||
"""
|
||||
if not stream.isatty():
|
||||
return False # auto color only on TTYs
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
try:
|
||||
return curses.tigetnum("colors") > 2
|
||||
except curses.error:
|
||||
curses.setupterm()
|
||||
return curses.tigetnum("colors") > 2
|
||||
except Exception:
|
||||
# guess false in case of error
|
||||
return False
|
||||
|
||||
def write(self, text, color):
|
||||
"""Write the given text to the stream in the given color.
|
||||
|
||||
@param text: Text to be written to the stream.
|
||||
|
||||
@param color: A string label for a color. e.g. 'red', 'white'.
|
||||
"""
|
||||
color = self._colors[color]
|
||||
self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text))
|
||||
|
||||
|
||||
class NullColorizer(object):
|
||||
"""See _AnsiColorizer docstring."""
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
|
||||
@classmethod
|
||||
def supported(cls, stream=sys.stdout):
|
||||
return True
|
||||
|
||||
def write(self, text, color):
|
||||
self.stream.write(text)
|
@ -1,9 +0,0 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
pbr>=1.6 # Apache-2.0
|
||||
Babel>=2.3.4 # BSD
|
||||
testrepository>=0.0.18 # Apache-2.0/BSD
|
||||
python-subunit>=0.0.18 # Apache-2.0/BSD
|
||||
testtools>=1.4.0 # MIT
|
52
setup.cfg
52
setup.cfg
@ -1,52 +0,0 @@
|
||||
[metadata]
|
||||
name = os-testr
|
||||
summary = A testr wrapper to provide functionality for OpenStack projects
|
||||
description-file =
|
||||
README.rst
|
||||
author = OpenStack
|
||||
author-email = openstack-dev@lists.openstack.org
|
||||
home-page = http://www.openstack.org/
|
||||
classifier =
|
||||
Environment :: OpenStack
|
||||
Intended Audience :: Information Technology
|
||||
Intended Audience :: System Administrators
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: POSIX :: Linux
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 2
|
||||
Programming Language :: Python :: 2.7
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.4
|
||||
|
||||
[files]
|
||||
packages =
|
||||
os_testr
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
subunit-trace = os_testr.subunit_trace:main
|
||||
ostestr = os_testr.ostestr:main
|
||||
subunit2html = os_testr.subunit2html:main
|
||||
generate-subunit = os_testr.generate_subunit:main
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
build-dir = doc/build
|
||||
all_files = 1
|
||||
|
||||
[upload_sphinx]
|
||||
upload-dir = doc/build/html
|
||||
|
||||
[compile_catalog]
|
||||
directory = os_testr/locale
|
||||
domain = os-testr
|
||||
|
||||
[update_catalog]
|
||||
domain = os-testr
|
||||
output_dir = os_testr/locale
|
||||
input_file = os_testr/locale/os-testr.pot
|
||||
|
||||
[extract_messages]
|
||||
keywords = _ gettext ngettext l_ lazy_gettext
|
||||
mapping_file = babel.cfg
|
||||
output_file = os_testr/locale/os-testr.pot
|
29
setup.py
29
setup.py
@ -1,29 +0,0 @@
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
|
||||
import setuptools
|
||||
|
||||
# In python < 2.7.4, a lazy loading of package `pbr` will break
|
||||
# setuptools if some other modules registered functions in `atexit`.
|
||||
# solution from: http://bugs.python.org/issue15881#msg170215
|
||||
try:
|
||||
import multiprocessing # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['pbr>=1.8'],
|
||||
pbr=True)
|
@ -1,14 +0,0 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
hacking<0.11,>=0.10.2 # Apache-2.0
|
||||
|
||||
coverage>=3.6 # Apache-2.0
|
||||
discover # BSD
|
||||
sphinx!=1.3b1,<1.3,>=1.2.1 # BSD
|
||||
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
|
||||
oslotest>=1.10.0 # Apache-2.0
|
||||
testscenarios>=0.4 # Apache-2.0/BSD
|
||||
ddt>=1.0.1 # MIT
|
||||
six>=1.9.0 # MIT
|
39
tox.ini
39
tox.ini
@ -1,39 +0,0 @@
|
||||
[tox]
|
||||
minversion = 1.6
|
||||
envlist = py34,py27,pypy,pep8
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
install_command = pip install -U {opts} {packages}
|
||||
setenv =
|
||||
VIRTUAL_ENV={envdir}
|
||||
whitelist_externals = find
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
commands =
|
||||
find . -type f -name "*.pyc" -delete
|
||||
ostestr {posargs}
|
||||
|
||||
[testenv:pep8]
|
||||
commands = flake8
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:cover]
|
||||
commands = python setup.py test --coverage --coverage-package-name='os_testr' --testr-args='{posargs}'
|
||||
|
||||
[testenv:docs]
|
||||
commands = python setup.py build_sphinx
|
||||
|
||||
[testenv:debug]
|
||||
commands = oslo_debug_helper {posargs}
|
||||
|
||||
[flake8]
|
||||
# E123, E125 skipped as they are invalid PEP-8.
|
||||
|
||||
show-source = True
|
||||
ignore = E123,E125
|
||||
builtins = _
|
||||
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build
|
Loading…
Reference in New Issue
Block a user