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:
parent
9312a0fdab
commit
7443555b4d
@ -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:
|
||||||
|
|
||||||
|
@ -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
107
tests/ci/pytest_launcher.py
Executable 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)
|
76
tests/unit/test_pytest_launcher.py
Normal file
76
tests/unit/test_pytest_launcher.py
Normal 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])
|
5
tox.ini
5
tox.ini
@ -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}
|
||||||
|
Loading…
Reference in New Issue
Block a user