Add simple wrapper for pytest

It helps to:
 - publish report at upstream CI(mask report as testr-report)
 - launch single tests. Returns ability to launch single test or group of
   tests like:
      tox -e py27 -- tests.unit.test_osclients.CachedTestCase.test_cached

Change-Id: I2463360bc2f0559bcdd830ec6f4ba26cc51c80c3
This commit is contained in:
Andrey Kurilin 2016-09-05 16:05:16 +03:00
parent 9312a0fdab
commit 7443555b4d
5 changed files with 196 additions and 10 deletions

View File

@ -142,7 +142,7 @@ To run a single unit test e.g. test_deployment
$ tox -e <name> -- <test_name> $ tox -e <name> -- <test_name>
#NOTE: <name> is one of py34, py27 or pep8 #NOTE: <name> is one of py34, py27 or pep8
# <test_name> is the unit test case name # <test_name> is the unit test case name, e.g tests.unit.test_osclients
To debug issues on the unit test: To debug issues on the unit test:

View File

@ -25,28 +25,32 @@ To run unit tests locally::
$ pip install tox $ pip install tox
$ tox $ tox
To run py26, py27 or pep8 only:: To run py27, py34, py35 or pep8 only::
$ tox -e <name> $ tox -e <name>
#NOTE: <name> is one of py26, py27 or pep8 # NOTE: <name> is one of py27, py34, py35 or pep8
To run py26, py27 against mysql or psql To run py27/py34/py35 against mysql or psql
$ export RALLY_UNITTEST_DB_URL="mysql://user:secret@localhost/rally" $ export RALLY_UNITTEST_DB_URL="mysql://user:secret@localhost/rally"
$ tox -epy27 $ tox -epy27
To run specific test of py27/py34/py35::
$ tox -e py27 -- tests.unit.test_osclients
To get test coverage:: To get test coverage::
$ tox -e cover $ tox -e cover
#NOTE: Results will be in /cover/index.html # NOTE: Results will be in ./cover/index.html
To generate docs:: To generate docs::
$ tox -e docs $ tox -e docs
#NOTE: Documentation will be in doc/source/_build/html/index.html # NOTE: Documentation will be in doc/source/_build/html/index.html
Functional tests Functional tests
---------------- ----------------
@ -62,7 +66,7 @@ To run functional tests locally::
$ rally deployment create --fromenv --name testing $ rally deployment create --fromenv --name testing
$ tox -e cli $ tox -e cli
#NOTE: openrc file with OpenStack admin credentials # NOTE: openrc file with OpenStack admin credentials
Output of every Rally execution will be collected under some reports root in Output of every Rally execution will be collected under some reports root in
directory structure like: reports_root/ClassName/MethodName_suffix.extension directory structure like: reports_root/ClassName/MethodName_suffix.extension

107
tests/ci/pytest_launcher.py Executable file
View File

@ -0,0 +1,107 @@
#
# 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 os
import subprocess
import sys
PYTEST_REPORT = os.environ.get("PYTEST_REPORT",
".test_results/pytest_results.html")
TESTR_REPORT = "testr_results.html"
def error(msg):
print(msg)
exit(1)
def main(args):
parser = argparse.ArgumentParser(args[0])
parser.add_argument("discovery_path", metavar="<path>", type=str,
help="Path to location of all tests.")
parser.add_argument("--posargs", metavar="<str>", type=str, default="",
help="TOX posargs. Currently supported only string to "
"partial test or tests group to launch.")
args = parser.parse_args(args[1:])
# We allow only one parameter - path to partial test or tests group
path = args.posargs
if len(path.split(" ")) > 1:
error("Wrong value of posargs. It should include only path to single "
"test or tests group to launch.")
# NOTE(andreykurilin): Previously, next format was supported:
# tests.unit.test_osclients.SomeTestCase.some_method
# It is more simple and pythonic than native pytest-way:
# tests/unit/test_osclients.py::SomeTestCase::some_method
# Let's return this support
if path:
if "/" not in path:
path = path.split(".")
module = ""
for i in range(0, len(path)):
part = os.path.join(module, path[i])
if os.path.exists(part):
module = part
continue
if os.path.exists("%s.py" % part):
if i != (len(path) - 1):
module = "%s.py::%s" % (part, "::".join(path[i + 1:]))
else:
module = "%s.py" % part
break
error("Non-existing path to single test or tests group to "
"launch. %s %s" % (module, part))
path = module
path = os.path.abspath(os.path.expanduser(path))
if not path.startswith(os.path.abspath(args.discovery_path)):
# Prevent to launch functional tests from unit tests launcher.
error("Wrong path to single test or tests group to launch. It "
"should be in %s." % args.discovery_path)
else:
path = args.discovery_path
print("Test(s) to launch (pytest format): %s" % path)
# NOTE(andreykurilin): we cannot publish pytest reports at gates, but we
# can mask them as testr reports. It looks like a dirty hack and I
# prefer to avoid it, but I see no other solutions at this point.
# apply dirty hack only in gates.
if os.environ.get("ZUUL_PROJECT"):
pytest_report = TESTR_REPORT
else:
pytest_report = PYTEST_REPORT
try:
subprocess.check_call(["py.test", "--html=%s" % pytest_report,
"--durations=10", "-n", "auto", path],
stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
# NOTE(andreykurilin): it is ok, since tests can fail.
exit_code = 1
else:
exit_code = 0
if os.path.exists(pytest_report) and os.environ.get("ZUUL_PROJECT"):
subprocess.check_call(["gzip", "-9", "-f", pytest_report],
stderr=subprocess.STDOUT)
if exit_code == 1:
error("")
if __name__ == "__main__":
main(sys.argv)

View File

@ -0,0 +1,76 @@
# 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 os
import mock
from tests.ci import pytest_launcher
from tests.unit import test
PATH = "tests.ci.pytest_launcher"
class ExitError(Exception):
pass
class PyTestLauncherTestCase(test.TestCase):
def setUp(self):
super(PyTestLauncherTestCase, self).setUp()
sp_patcher = mock.patch("%s.subprocess" % PATH)
self.sp = sp_patcher.start()
self.addCleanup(sp_patcher.stop)
exit_patcher = mock.patch("%s.exit" % PATH, side_effect=ExitError)
self.exit = exit_patcher.start()
self.addCleanup(exit_patcher.stop)
os_patcher = mock.patch("%s.os" % PATH)
self.os = os_patcher.start()
self.addCleanup(os_patcher.stop)
# emulate local run by default
self.os.environ = {}
self.os.path.join.side_effect = os.path.join
self.os.path.abspath.side_effect = os.path.abspath
self.os.path.expanduser.side_effect = os.path.expanduser
def test_wrong_posargs(self):
self.assertRaises(ExitError, pytest_launcher.main,
["script name", "test_path",
"--posargs='posargs with spaces'"])
self.assertFalse(self.sp.called)
self.assertFalse(self.os.called)
def test_parsing_path(self):
def os_path_exists(path):
dpath = "some/path/to/some/test"
return dpath.startswith(path) or path == "%s/module.py" % dpath
self.os.path.exists.side_effect = os_path_exists
pytest_launcher.main(
["script_name", "some/path",
"--posargs=some.path.to.some.test.module.TestCase.test"])
expected_path = os.path.abspath(
"some/path/to/some/test/module.py::TestCase::test")
self.assertEqual(1, self.sp.check_call.call_count)
call_args_obj = self.sp.check_call.call_args_list[0]
call_args = call_args_obj[0]
self.assertEqual(expected_path, call_args[0][-1])

View File

@ -19,7 +19,7 @@ install_command = pip install -U {opts} {packages}
usedevelop = True usedevelop = True
commands = commands =
find . -type f -name "*.pyc" -delete find . -type f -name "*.pyc" -delete
py.test --html=.test_results/pytest_results.html --durations=10 -n auto "tests/unit" {posargs} python {toxinidir}/tests/ci/pytest_launcher.py tests/unit --posargs={posargs}
distribute = false distribute = false
basepython = python2.7 basepython = python2.7
passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY
@ -55,8 +55,7 @@ commands = oslo_debug_helper -t tests {posargs}
sitepackages = True sitepackages = True
commands = commands =
find . -type f -name "*.pyc" -delete find . -type f -name "*.pyc" -delete
py.test --html=pytest_results.html --durations=10 -n auto "tests/functional" {posargs} python {toxinidir}/tests/ci/pytest_launcher.py "tests/functional" --posargs={posargs}
[testenv:cover] [testenv:cover]
commands = {toxinidir}/tests/ci/cover.sh {posargs} commands = {toxinidir}/tests/ci/cover.sh {posargs}