diff --git a/python/helpers/pycharm/_bdd_utils.py b/python/helpers/pycharm/_bdd_utils.py index 00f71f624409..5b745f898cff 100644 --- a/python/helpers/pycharm/_bdd_utils.py +++ b/python/helpers/pycharm/_bdd_utils.py @@ -57,6 +57,17 @@ def get_what_to_run_by_env(environment): return base_dir, scenarios, what_to_run +def get_location(base_dir, location_file, location_line): + """ + Generates location that PyCharm resolves to file + :param base_dir: base directory to resolve relative path against + :param location_file: path to file + :param location_line: line number + """ + my_file = str(location_file).lstrip("/\\") + return "file:///{0}:{1}".format(os.path.normpath(os.path.join(base_dir, my_file)), location_line) + + class BddRunner(object): """ Extends this class, implement abstract methods and use its API to implement new BDD frameworks. @@ -101,8 +112,7 @@ class BddRunner(object): :param location object with "file" (relative to base_dir) and "line" fields. :return: location in format file:line (as supported in tcmessages) """ - my_file = str(location.file).lstrip("/\\") - return "file:///{0}:{1}".format(os.path.normpath(os.path.join(self.__base_dir, my_file)), location.line) + return get_location(self.__base_dir, location.file, location.line) def _test_undefined(self, test_name, location): """ diff --git a/python/helpers/pycharm/_jb_django_behave.py b/python/helpers/pycharm/_jb_django_behave.py new file mode 100644 index 000000000000..bf54234e0503 --- /dev/null +++ b/python/helpers/pycharm/_jb_django_behave.py @@ -0,0 +1,36 @@ +# coding=utf-8 +""" +To support django-behave +""" +import os +import sys + + +def run_as_django_behave(formatter_name, feature_names, scenario_n_options): + """ + :param formatter_name: for "-f" argument + :param feature_names: feature names or folders behave arguments + :param scenario_n_options: list of ["-n", "scenario_name"] + + + :return: True if launched as django-behave. Otherwise false and need to be launched as plain behave + """ + if "DJANGO_SETTINGS_MODULE" not in os.environ: + return False + try: + import django + from django.core.management import ManagementUtility + + from behave_django import __version__ # To make sure version exists + django.setup() + from django.apps import apps + + if apps.is_installed("behave_django"): + base = sys.argv[0] + rest = sys.argv[1:] + sys.argv = [base] + rest + ["behave", "-f{0}".format(formatter_name)] + feature_names + scenario_n_options + print("manage.py " + " ".join(sys.argv[1:])) + ManagementUtility().execute() + return True + except ImportError: + pass diff --git a/python/helpers/pycharm/behave_runner.py b/python/helpers/pycharm/behave_runner.py index fd6e7c14ba36..bba297d52e2a 100644 --- a/python/helpers/pycharm/behave_runner.py +++ b/python/helpers/pycharm/behave_runner.py @@ -8,27 +8,21 @@ Other args are tag expressionsin format (--tags=.. --tags=..). See https://pythonhosted.org/behave/behave.html#tag-expression """ - -# -# TODO: Rewrite using Formatters API (http://behave.readthedocs.io/en/latest/formatters.html) before any change -# - - import functools import glob +import re import sys -import os import traceback - +from behave import __version__ as behave_version from behave.formatter.base import Formatter from behave.model import Step, ScenarioOutline, Feature, Scenario from behave.tag_expression import TagExpression -import re - -import _bdd_utils from distutils import version -from behave import __version__ as behave_version +from _jb_django_behave import run_as_django_behave +import _bdd_utils +import tcmessages from _jb_utils import VersionAgnosticUtils + _MAX_STEPS_SEARCH_FEATURES = 5000 # Do not look for features in folder that has more that this number of children _FEATURES_FOLDER = 'features' # "features" folder name. @@ -36,6 +30,8 @@ __author__ = 'Ilya.Kazakevich' from behave import configuration, runner +import os + def _get_dirs_to_run(base_dir_to_search): """ @@ -81,7 +77,7 @@ class _RunnerWrapper(runner.Runner): """ :type config configuration.Configuration :param config behave configuration - :type hooks dict + :type hooks dict or empty if new runner mode :param hooks hooks in format "before_scenario" => f(context, scenario) to load after/before hooks, provided by user """ super(_RunnerWrapper, self).__init__(config) @@ -132,7 +128,6 @@ class _BehaveRunner(_bdd_utils.BddRunner): BddRunner for behave """ - def __process_hook(self, is_started, context, element): """ Hook to be installed. Reports steps, features etc. @@ -201,7 +196,7 @@ class _BehaveRunner(_bdd_utils.BddRunner): def _collect_trace(self, element, utils): return u"".join([utils.to_unicode(l) for l in traceback.format_tb(element.exc_traceback)]) - def __init__(self, config, base_dir): + def __init__(self, config, base_dir, use_old_runner): """ :type config configuration.Configuration """ @@ -215,12 +210,11 @@ class _BehaveRunner(_bdd_utils.BddRunner): "after_scenario": functools.partial(self.__process_hook, False), "before_step": functools.partial(self.__process_hook, True), "after_step": functools.partial(self.__process_hook, False) - }) + } if use_old_runner else dict()) def _run_tests(self): self.__real_runner.run() - def __filter_scenarios_by_args(self, scenario): """ Filters out scenarios that should be skipped by tags or scenario names @@ -237,7 +231,6 @@ class _BehaveRunner(_bdd_utils.BddRunner): return True # No tags nor names are required return isinstance(expected_tags, TagExpression) and expected_tags.check(scenario.tags) - def _get_features_to_run(self): self.__real_runner.dry_run = True self.__real_runner.run() @@ -264,18 +257,11 @@ class _BehaveRunner(_bdd_utils.BddRunner): if __name__ == "__main__": # TODO: support all other params instead - - class _Null(Formatter): - """ - Null formater to prevent stdout output - """ - pass - command_args = list(filter(None, sys.argv[1:])) if command_args: if "--junit" in command_args: raise Exception("--junit report type for Behave is unsupported in PyCharm. \n " - "See: https://youtrack.jetbrains.com/issue/PY-14219") + "See: https://youtrack.jetbrains.com/issue/PY-14219") _bdd_utils.fix_win_drive(command_args[0]) (base_dir, scenario_names, what_to_run) = _bdd_utils.get_what_to_run_by_env(os.environ) @@ -284,16 +270,45 @@ if __name__ == "__main__": my_config = configuration.Configuration(command_args=command_args) - # Temporary workaround to support API changes in 1.2.5 - if version.LooseVersion(behave_version) >= version.LooseVersion("1.2.5"): - from behave.formatter import _registry - _registry.register_as("com.intellij.python.null",_Null) + loose_version = version.LooseVersion(behave_version) + assert loose_version >= version.LooseVersion("1.2.5"), "Version not supported, please upgrade Behave" + + # New version supports 1.2.6 only + use_old_runner = "PYCHARM_BEHAVE_OLD_RUNNER" in os.environ or loose_version < version.LooseVersion("1.2.6") + from behave.formatter import _registry + + FORMAT_NAME = "com.jetbrains.pycharm.formatter" + if use_old_runner: + class _Null(Formatter): + """ + Null formater to prevent stdout output + """ + pass + + + _registry.register_as(FORMAT_NAME, _Null) else: - from behave.formatter import formatters - formatters.register_as(_Null, "com.intellij.python.null") + custom_messages = tcmessages.TeamcityServiceMessages() + # Not safe to import it in old mode + from teamcity.jb_behave_formatter import TeamcityFormatter - my_config.format = ["com.intellij.python.null"] # To prevent output to stdout + class TeamcityFormatterWithLocation(TeamcityFormatter): + + def _report_suite_started(self, suite, suite_name): + location = suite.location + custom_messages.testSuiteStarted(suite_name, + _bdd_utils.get_location(base_dir, location.filename, location.line)) + + def _report_test_started(self, test, test_name): + location = test.location + custom_messages.testStarted(test_name, + _bdd_utils.get_location(base_dir, location.filename, location.line)) + + + _registry.register_as(FORMAT_NAME, TeamcityFormatterWithLocation) + + my_config.format = [FORMAT_NAME] # To prevent output to stdout my_config.reporters = [] # To prevent summary to stdout my_config.stdout_capture = False # For test output my_config.stderr_capture = False # For test output @@ -307,4 +322,7 @@ if __name__ == "__main__": my_config.paths = list(features) if what_to_run and not my_config.paths: raise Exception("Nothing to run in {0}".format(what_to_run)) - _BehaveRunner(my_config, base_dir).run() + + # Run as Django if supported, run plain otherwise + if not run_as_django_behave(FORMAT_NAME, what_to_run, command_args): + _BehaveRunner(my_config, base_dir, use_old_runner).run()