PY-21979: Use behave formatter and "manage.py django" for behave-django

* Use behave formatter from "teamcity" to integrate PyCharm with behave
* If behave is 1.2.6 and behave-django installed and Django enabled -- use "manage.py django" instead of plain behave
This commit is contained in:
Ilya.Kazakevich
2018-03-01 21:37:30 +03:00
parent 22f92b308f
commit 0e364a7554
3 changed files with 100 additions and 36 deletions
+12 -2
View File
@@ -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):
"""
@@ -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
+52 -34
View File
@@ -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()