diff --git a/build.py b/build.py index dee8279d6..4b52cbe9d 100755 --- a/build.py +++ b/build.py @@ -38,6 +38,7 @@ use_plugin("source_distribution") use_plugin("python.unittest") +use_plugin("python.pytest") if sys.platform != 'win32': use_plugin("python.cram") diff --git a/src/main/python/pybuilder/plugins/python/coverage_plugin.py b/src/main/python/pybuilder/plugins/python/coverage_plugin.py index b50b6eb5a..37b1e6cb0 100644 --- a/src/main/python/pybuilder/plugins/python/coverage_plugin.py +++ b/src/main/python/pybuilder/plugins/python/coverage_plugin.py @@ -316,7 +316,14 @@ def _delete_non_essential_modules(): for module_name in list(sys.modules.keys()): module = sys.modules[module_name] if module: - if not _is_module_essential(module.__name__, sys_packages, sys_modules): + try: + # if we try to remove non-module object + module__name__ = module.__name__ + except: + module__name__ = None + if module__name__ is None: + module__name__ = module_name + if not _is_module_essential(module__name__, sys_packages, sys_modules): _delete_module(module_name, module) @@ -326,6 +333,9 @@ def _delete_module(module_name, module): delattr(module, module_name) except AttributeError: pass + # if we try to remove non-class object + except ImportError: + pass def _is_module_essential(module_name, sys_packages, sys_modules): @@ -338,6 +348,9 @@ def _is_module_essential(module_name, sys_packages, sys_modules): # Essential since we're in a fork for communicating exceptions back sys_packages.append("tblib") sys_packages.append("pybuilder.errors") + # External modules + sys_packages.append("pkg_resources") + sys_packages.append("_pytest") for package in sys_packages: if module_name == package or module_name.startswith(package + "."): diff --git a/src/main/python/pybuilder/plugins/python/pytest_plugin.py b/src/main/python/pybuilder/plugins/python/pytest_plugin.py new file mode 100644 index 000000000..77fe70236 --- /dev/null +++ b/src/main/python/pybuilder/plugins/python/pytest_plugin.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 Alexey Sanko +# +# 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 sys import path as sys_path + +from pybuilder.core import task, init, use_plugin, after +from pybuilder.errors import MissingPrerequisiteException, BuildFailedException +from pybuilder.utils import register_test_and_source_path_and_return_test_dir + +__author__ = 'Alexey Sanko' + +use_plugin("python.core") + + +@init +def initialize_pytest_plugin(project): + """ Init default plugin project properties. """ + project.plugin_depends_on('pytest') + project.set_property_if_unset("dir_source_pytest_python", "src/unittest/python") + project.set_property_if_unset("pytest_extra_args", []) + + +@after("prepare") +def assert_pytest_available(logger): + """ Asserts that the pytest module is available. """ + logger.debug("Checking if pytest module is available.") + + try: + import pytest + logger.debug("Found pytest version %s" % pytest.__version__) + except ImportError: + raise MissingPrerequisiteException(prerequisite="pytest module", caller="plugin python.pytest") + + +@task +def run_unit_tests(project, logger): + """ Call pytest for the sources of the given project. """ + logger.info('pytest: Run unittests.') + from pytest import main as pytest_main + test_dir = register_test_and_source_path_and_return_test_dir(project, sys_path, 'pytest') + extra_args = project.get_property("pytest_extra_args") + try: + pytest_args = [test_dir] + (extra_args if extra_args else []) + if project.get_property('verbose'): + pytest_args.append('-s') + pytest_args.append('-v') + ret = pytest_main(pytest_args) + if ret: + raise BuildFailedException('pytest: unittests failed') + else: + logger.info('pytest: All unittests passed.') + except: + raise diff --git a/src/main/python/pybuilder/plugins/python/unittest_plugin.py b/src/main/python/pybuilder/plugins/python/unittest_plugin.py index bfcf2c615..d5caec6f7 100644 --- a/src/main/python/pybuilder/plugins/python/unittest_plugin.py +++ b/src/main/python/pybuilder/plugins/python/unittest_plugin.py @@ -28,7 +28,10 @@ from pybuilder.core import init, task, description, use_plugin from pybuilder.errors import BuildFailedException -from pybuilder.utils import discover_modules_matching, render_report, fork_process +from pybuilder.utils import (discover_modules_matching, + render_report, + fork_process, + register_test_and_source_path_and_return_test_dir) from pybuilder.ci_server_interaction import test_proxy_for from pybuilder.terminal import print_text_line from types import MethodType, FunctionType @@ -77,7 +80,7 @@ def run_tests(project, logger, execution_prefix, execution_name): def do_run_tests(project, logger, execution_prefix, execution_name): - test_dir = _register_test_and_source_path_and_return_test_dir(project, sys.path, execution_prefix) + test_dir = register_test_and_source_path_and_return_test_dir(project, sys.path, execution_prefix) file_suffix = project.get_property("%s_file_suffix" % execution_prefix) if file_suffix is not None: @@ -202,14 +205,6 @@ def addFailure(self, test, err): return result -def _register_test_and_source_path_and_return_test_dir(project, system_path, execution_prefix): - test_dir = project.expand_path("$dir_source_%s_python" % execution_prefix) - system_path.insert(0, test_dir) - system_path.insert(0, project.expand_path("$dir_source_main_python")) - - return test_dir - - def write_report(name, project, logger, result, console_out): project.write_report("%s" % name, console_out) diff --git a/src/main/python/pybuilder/utils.py b/src/main/python/pybuilder/utils.py index 2e7623c69..26a3dc769 100644 --- a/src/main/python/pybuilder/utils.py +++ b/src/main/python/pybuilder/utils.py @@ -612,3 +612,10 @@ def safe_log_file_name(file_name): # per https://support.microsoft.com/en-us/kb/177506 # per https://msdn.microsoft.com/en-us/library/aa365247 return re.sub(r'\\|/|:|\*|\?|\"|<|>|\|', '_', file_name) + + +def register_test_and_source_path_and_return_test_dir(project, system_path, execution_prefix): + test_dir = project.expand_path("$dir_source_%s_python" % execution_prefix) + system_path.insert(0, test_dir) + system_path.insert(0, project.expand_path("$dir_source_main_python")) + return test_dir diff --git a/src/unittest/python/plugins/python/pytest_plugin_tests.py b/src/unittest/python/plugins/python/pytest_plugin_tests.py new file mode 100644 index 000000000..835a7ea1c --- /dev/null +++ b/src/unittest/python/plugins/python/pytest_plugin_tests.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 Alexey Sanko +# +# 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 os import mkdir +from os.path import join as path_join +from mock import Mock +from shutil import rmtree +from sys import path as sys_path +from tempfile import mkdtemp +from unittest import TestCase + +from pybuilder.core import Project +from pybuilder.errors import BuildFailedException +from pybuilder.plugins.python.pytest_plugin import initialize_pytest_plugin, run_unit_tests + + +class PytestPluginInitializationTests(TestCase): + def setUp(self): + self.project = Project("basedir") + + def test_should_set_dependency(self): + mock_project = Mock(Project) + initialize_pytest_plugin(mock_project) + mock_project.plugin_depends_on.assert_called_with('pytest') + + def test_should_set_default_properties(self): + initialize_pytest_plugin(self.project) + expected_default_properties = { + "dir_source_pytest_python": "src/unittest/python" + } + for property_name, property_value in expected_default_properties.items(): + self.assertEquals(self.project.get_property(property_name), property_value) + + def test_should_leave_user_specified_properties_when_initializing_plugin(self): + expected_properties = { + "dir_source_pytest_python": "some/path" + } + for property_name, property_value in expected_properties.items(): + self.project.set_property(property_name, property_value) + + initialize_pytest_plugin(self.project) + + for property_name, property_value in expected_properties.items(): + self.assertEquals(self.project.get_property(property_name), property_value) + + +pytest_file_success = """ +def test_pytest_base_success(): + assert True +""" + +pytest_file_failure = """ +def test_pytest_base_failure(): + assert False +""" + +pytest_conftest_result_to_file = """ +from os.path import abspath, dirname, join as path_join +curr_dir = dirname(abspath(__file__)) + +def pytest_collection_modifyitems(config, items): + verbose_flag = config.getoption('verbose') + capture = config.getoption('capture') + tests_list = [] + for item in items: + tests_list.append(item.name) + f = open(path_join(curr_dir, 'pytest_collected_config.out'), 'w') + f.write(str(verbose_flag) + '\\n') + f.write(str(capture) + '\\n') + f.write(','.join(tests_list)) + f.flush() + f.close() +""" + +pytest_file_sucess_with_extra_args = """ +def test_success_with_extra_args(test_arg): + assert test_arg == "test_value" +""" + +pytest_conftest_test_arg = """ +import pytest + +def pytest_addoption(parser): + parser.addoption("--test-arg", action="store") + +@pytest.fixture +def test_arg(request): + return request.config.getoption("--test-arg") +""" + + +class PytestPluginRunningTests(TestCase): + def setUp(self): + self.tmp_test_folder = mkdtemp() + + def create_test_project(self, name, content_dict): + project_dir = path_join(self.tmp_test_folder, name) + mkdir(project_dir) + test_project = Project(project_dir) + tests_dir = path_join(project_dir, 'tests') + mkdir(tests_dir) + test_project.set_property('dir_source_pytest_python', + 'tests') + src_dir = path_join(project_dir, 'src') + mkdir(src_dir) + test_project.set_property('dir_source_main_python', + 'src') + for file_name, content in content_dict.items(): + f = open(path_join(tests_dir, file_name), 'w') + f.write(content) + f.flush() + f.close() + return test_project + + def read_pytest_conftest_result_file(self, dir): + out_file = path_join(dir, 'pytest_collected_config.out') + f = open(out_file, 'r') + out = dict() + out.update({'verbose': f.readline().strip()}) + out.update({'capture': f.readline().strip()}) + out.update({'tests_list': f.readline().strip().split(',')}) + f.close() + return out + + def test_should_run_pytest_tests(self): + test_project = self.create_test_project('pytest_sucess', {'test_success.py': pytest_file_success}) + run_unit_tests(test_project, Mock()) + self.assertTrue(test_project.expand_path('$dir_source_main_python') in sys_path) + self.assertTrue(test_project.expand_path('$dir_source_pytest_python') in sys_path) + + def test_should_run_pytest_tests_without_verbose(self): + test_project = self.create_test_project('pytest_sucess_without_verbose', + {'test_success_without_verbose.py': pytest_file_success, + 'conftest.py': pytest_conftest_result_to_file}) + run_unit_tests(test_project, Mock()) + cfg = self.read_pytest_conftest_result_file(test_project.expand_path('$dir_source_pytest_python')) + self.assertEqual(cfg['verbose'], '0') + self.assertEqual(cfg['capture'], 'fd') + + def test_should_run_pytest_tests_with_verbose(self): + test_project = self.create_test_project('pytest_sucess_with_verbose', + {'test_success_with_verbose.py': pytest_file_success, + 'conftest.py': pytest_conftest_result_to_file}) + test_project.set_property('verbose', True) + run_unit_tests(test_project, Mock()) + cfg = self.read_pytest_conftest_result_file(test_project.expand_path('$dir_source_pytest_python')) + self.assertEqual(cfg['verbose'], '1') + self.assertEqual(cfg['capture'], 'no') + + def test_should_correct_get_pytest_failure(self): + test_project = self.create_test_project('pytest_failure', {'test_failure.py': pytest_file_failure}) + self.assertRaises(BuildFailedException, run_unit_tests, test_project, Mock()) + # has to be compatible with Python 2.6 + # with self.assertRaises(BuildFailedException) as context: + # run_unit_tests(test_project, Mock()) + + def test_should_run_pytest_tests_with_extra_args(self): + test_project = self.create_test_project('pytest_sucess_with_extra_args', + {'test_success_with_extra_args.py': pytest_file_sucess_with_extra_args, + 'conftest.py': pytest_conftest_test_arg}) + initialize_pytest_plugin(test_project) + test_project.set_property("pytest_extra_args", + test_project.get_property("pytest_extra_args") + + ["--test-arg", "test_value"]) + run_unit_tests(test_project, Mock()) + self.assertTrue(True) + + def tearDown(self): + rmtree(self.tmp_test_folder) diff --git a/src/unittest/python/plugins/python/unittest_plugin_tests.py b/src/unittest/python/plugins/python/unittest_plugin_tests.py index 08a69268c..3f9b2861c 100644 --- a/src/unittest/python/plugins/python/unittest_plugin_tests.py +++ b/src/unittest/python/plugins/python/unittest_plugin_tests.py @@ -24,7 +24,6 @@ from pybuilder.core import Project from pybuilder.plugins.python.unittest_plugin import (execute_tests, execute_tests_matching, - _register_test_and_source_path_and_return_test_dir, _instrument_result, _create_runner, _get_make_result_method_name, @@ -33,32 +32,6 @@ __author__ = 'Michael Gruber' -class PythonPathTests(TestCase): - def setUp(self): - self.project = Project('/path/to/project') - self.project.set_property('dir_source_unittest_python', 'unittest') - self.project.set_property('dir_source_main_python', 'src') - - def test_should_register_source_paths(self): - system_path = ['some/python/path'] - - _register_test_and_source_path_and_return_test_dir(self.project, system_path, "unittest") - - self.assertTrue('/path/to/project/unittest' in system_path) - self.assertTrue('/path/to/project/src' in system_path) - - def test_should_put_project_sources_before_other_sources(self): - system_path = ['irrelevant/sources'] - - _register_test_and_source_path_and_return_test_dir(self.project, system_path, "unittest") - - test_sources_index_in_path = system_path.index('/path/to/project/unittest') - main_sources_index_in_path = system_path.index('/path/to/project/src') - irrelevant_sources_index_in_path = system_path.index('irrelevant/sources') - self.assertTrue(test_sources_index_in_path < irrelevant_sources_index_in_path and - main_sources_index_in_path < irrelevant_sources_index_in_path) - - class ExecuteTestsTests(TestCase): def setUp(self): self.mock_result = Mock() diff --git a/src/unittest/python/utils_tests.py b/src/unittest/python/utils_tests.py index 789509719..b7ca39d77 100644 --- a/src/unittest/python/utils_tests.py +++ b/src/unittest/python/utils_tests.py @@ -26,6 +26,7 @@ import unittest from json import loads +from pybuilder.core import Project from pybuilder.errors import PyBuilderException from pybuilder.utils import (GlobExpression, Timer, @@ -40,7 +41,8 @@ render_report, timedelta_in_millis, fork_process, - execute_command) + execute_command, + register_test_and_source_path_and_return_test_dir) from test_utils import patch, Mock @@ -435,3 +437,29 @@ def test_execute_command(self, popen, _): self.assertEquals(execute_command(["test", "commands"], outfile_name="test.out"), 0) self.assertEquals( execute_command(["test", "commands"], outfile_name="test.out", error_file_name="test.out.err"), 0) + + +class PythonPathTests(unittest.TestCase): + def setUp(self): + self.project = Project('/path/to/project') + self.project.set_property('dir_source_unittest_python', 'unittest') + self.project.set_property('dir_source_main_python', 'src') + + def test_should_register_source_paths(self): + system_path = ['some/python/path'] + + register_test_and_source_path_and_return_test_dir(self.project, system_path, "unittest") + + self.assertTrue('/path/to/project/unittest' in system_path) + self.assertTrue('/path/to/project/src' in system_path) + + def test_should_put_project_sources_before_other_sources(self): + system_path = ['irrelevant/sources'] + + register_test_and_source_path_and_return_test_dir(self.project, system_path, "unittest") + + test_sources_index_in_path = system_path.index('/path/to/project/unittest') + main_sources_index_in_path = system_path.index('/path/to/project/src') + irrelevant_sources_index_in_path = system_path.index('irrelevant/sources') + self.assertTrue(test_sources_index_in_path < irrelevant_sources_index_in_path and + main_sources_index_in_path < irrelevant_sources_index_in_path)