Skip to content

Commit

Permalink
Add UserDataNamespace and use it in JUnitReporter.
Browse files Browse the repository at this point in the history
  • Loading branch information
jenisys committed Jan 17, 2018
1 parent cc05dd2 commit dc9ac62
Show file tree
Hide file tree
Showing 7 changed files with 581 additions and 209 deletions.
80 changes: 80 additions & 0 deletions behave/contrib/formatter_missing_steps.py
@@ -0,0 +1,80 @@
# -*- coding: UTF-8 -*-
"""
Provides a formatter that writes prototypes for missing step functions
into a step module file by using step snippets.
NOTE: This is only simplistic, proof-of-concept code.
"""

from __future__ import absolute_import, print_function
from behave.runner_util import make_undefined_step_snippets
from .steps import StepsUsageFormatter


STEP_MODULE_TEMPLATE = '''\
# -*- coding: {encoding} -*-
"""
Missing step implementations (proof-of-concept).
"""
from behave import given, when, then, step
{step_snippets}
'''


class MissingStepsFormatter(StepsUsageFormatter):
"""Formatter that writes missing steps snippets into a step module file.
Reuses StepsUsageFormatter class because it already contains the logic
for discovering missing/undefined steps.
.. code-block:: ini
# -- FILE: behave.ini
# NOTE: Long text value needs indentation on following lines.
[behave.userdata]
behave.formatter.missing_steps.template = # -*- coding: {encoding} -*-
# Missing step implementations.
from behave import given, when, then, step
{step_snippets}
"""
name = "missing-steps"
description = "Writes implementation for missing step definitions."
template = STEP_MODULE_TEMPLATE
scope = "behave.formatter.missing_steps"

def __init__(self, stream_opener, config):
super(MissingStepsFormatter, self).__init__(stream_opener, config)
self.template = self.__class__.template
self.init_from_userdata(config.userdata)

def init_from_userdata(self, userdata):
scoped_name = "%s.%s" %(self.scope, "template")
template_text = userdata.get(scoped_name, self.template)
self.template = template_text

def close(self):
"""Called at end of test run.
NOTE: Overwritten to avoid to truncate/overwrite output-file.
"""
if self.step_registry and self.undefined_steps:
# -- ENSURE: Output stream is open.
self.stream = self.open()
self.report()

# -- FINALLY:
self.close_stream()

# -- REPORT SPECIFIC-API:
def report(self):
"""Writes missing step implementations by using step snippets."""
step_snippets = make_undefined_step_snippets(undefined_steps)
encoding = self.stream.encoding or "UTF-8"
function_separator = u"\n\n\n"
step_snippets_text = function_separator.join(step_snippets)
module_text = self.template.format(encoding=encoding,
step_snippets=step_snippets_text)
self.stream.write(module_text)
self.stream.write("\n")
21 changes: 10 additions & 11 deletions behave/reporter/junit.py
Expand Up @@ -80,6 +80,7 @@
from behave.formatter import ansi_escapes
from behave.model_describe import ModelDescriptor
from behave.textutil import indent, make_indentation, text as _text
from behave.userdata import UserDataNamespace
import six
if six.PY2:
# -- USE: Python3 backport for better unicode compatibility.
Expand Down Expand Up @@ -193,17 +194,15 @@ def setup_with_userdata(self, userdata):
behave.reporter.junit.show_hostname = false
"""
# -- EXPERIMENTAL:
option_names = [
"show_timings", "show_skipped_always",
"show_timestamp", "show_hostname",
"show_scenarios", "show_tags", "show_multiline",
]
for option_name in option_names:
name = "%s.%s" % (self.userdata_scope, option_name)
default_value = getattr(self, option_name)
value = userdata.getbool(name, default_value)
if value != default_value:
setattr(self, option_name, value)
config = UserDataNamespace(self.userdata_scope, userdata)
self.show_hostname = config.getbool("show_hostname", self.show_hostname)
self.show_multiline = config.getbool("show_multiline", self.show_multiline)
self.show_scenarios = config.getbool("show_scenarios", self.show_scenarios)
self.show_tags = config.getbool("show_tags", self.show_tags)
self.show_timings = config.getbool("show_timings", self.show_timings)
self.show_timestamp = config.getbool("show_timestamp", self.show_timestamp)
self.show_skipped_always = config.getbool("show_skipped_always",
self.show_skipped_always)

def make_feature_filename(self, feature):
filename = None
Expand Down
39 changes: 27 additions & 12 deletions behave/runner_util.py
Expand Up @@ -414,10 +414,9 @@ def load_step_modules(step_paths):


def make_undefined_step_snippet(step, language=None):
"""
Helper function to create an undefined-step snippet for a step.
"""Helper function to create an undefined-step snippet for a step.
:param step: Step to use (as Step object or step text).
:param step: Step to use (as Step object or string).
:param language: i18n language, optionally needed for step text parsing.
:return: Undefined-step snippet (as string).
"""
Expand All @@ -426,9 +425,7 @@ def make_undefined_step_snippet(step, language=None):
steps = parser.parse_steps(step_text, language=language)
step = steps[0]
assert step, "ParseError: %s" % step_text
# prefix = u""
# if sys.version_info[0] == 2:
# prefix = u"u"

prefix = u"u"
single_quote = "'"
if single_quote in step.name:
Expand All @@ -441,6 +438,29 @@ def make_undefined_step_snippet(step, language=None):
return snippet


def make_undefined_step_snippets(undefined_steps, make_snippet=None):
"""Creates a list of undefined step snippets.
Note that duplicated steps are removed internally.
:param undefined_steps: List of undefined steps (as Step object or string).
:param make_snippet: Function that generates snippet (optional)
:return: List of undefined step snippets (as list of strings)
"""
if make_snippet is None:
make_snippet = make_undefined_step_snippet

# -- NOTE: Remove any duplicated undefined steps.
step_snippets = []
collected_steps = set()
for undefined_step in undefined_steps:
if undefined_step in collected_steps:
continue
collected_steps.add(undefined_step)
step_snippet = make_snippet(undefined_step)
step_snippets.append(step_snippet)
return step_snippets


def print_undefined_step_snippets(undefined_steps, stream=None, colored=True):
"""
Print snippets for the undefined steps that were discovered.
Expand All @@ -456,12 +476,7 @@ def print_undefined_step_snippets(undefined_steps, stream=None, colored=True):

msg = u"\nYou can implement step definitions for undefined steps with "
msg += u"these snippets:\n\n"
printed = set()
for step in undefined_steps:
if step in printed:
continue
printed.add(step)
msg += make_undefined_step_snippet(step)
msg += u"\n".join(make_undefined_step_snippets(undefined_steps))

if colored:
# -- OOPS: Unclear if stream supports ANSI coloring.
Expand Down
90 changes: 90 additions & 0 deletions behave/userdata.py
Expand Up @@ -128,3 +128,93 @@ def getbool(self, name, default=False):
:raises: ValueError, if type conversion fails.
"""
return self.getas(parse_bool, name, default, valuetype=bool)

@classmethod
def make(cls, data):
if data is None:
data = cls()
elif not isinstance(data, cls):
data = cls(data)
return data


class UserDataNamespace(object):
"""Provides a light-weight dictview to the user data that allows you
to access all params in a namespace, that use "{namespace}.*" names.
.. code-block:: python
my_config = UserDataNamespace("my.config", userdata)
value1 = my_config.getint("value1") # USE: my.config.value1
value2 = my_config.get("value2") # USE: my.config.value2
"""

def __init__(self, namespace, data=None):
self.namespace = namespace or ""
self.data = UserData.make(data)

@staticmethod
def make_scoped(namespace, name):
"""Creates a scoped-name from its parts."""
if not namespace: # noqa
return name
return "%s.%s" % (namespace, name)

# -- DICT-LIKE:
def get(self, name, default=None):
scoped_name = self.make_scoped(self.namespace, name)
return self.data.get(scoped_name, default)

def getas(self, convert, name, default=None, valuetype=None):
scoped_name = self.make_scoped(self.namespace, name)
return self.data.getas(convert, scoped_name, default=default,
valuetype=valuetype)

def getint(self, name, default=0):
scoped_name = self.make_scoped(self.namespace, name)
return self.data.getint(scoped_name, default=default)

def getfloat(self, name, default=0.0):
scoped_name = self.make_scoped(self.namespace, name)
return self.data.getfloat(scoped_name, default=default)

def getbool(self, name, default=False):
scoped_name = self.make_scoped(self.namespace, name)
return self.data.getbool(scoped_name, default=default)

def __contains__(self, name):
scoped_name = self.make_scoped(self.namespace, name)
return scoped_name in self.data

def __getitem__(self, name):
scoped_name = self.make_scoped(self.namespace, name)
return self.data[scoped_name]

def __setitem__(self, name, value):
scoped_name = self.make_scoped(self.namespace, name)
self.data[scoped_name] = value

def __len__(self):
return len(self.scoped_keys())

def scoped_keys(self):
if not self.namespace: # noqa
return self.data.keys()
prefix = "%s." % self.namespace
return [key for key in self.data.keys() if key.startswith(prefix)]

def keys(self):
prefix = "%s." % self.namespace
for scoped_name in self.scoped_keys():
name = scoped_name.replace(prefix, "", 1)
yield name

def values(self):
for scoped_name in self.scoped_keys():
yield self.data[scoped_name]

def items(self):
for name in self.keys():
scoped_name = self.make_scoped(self.namespace, name)
value = self.data[scoped_name]
yield (name, value)
14 changes: 12 additions & 2 deletions docs/formatters.rst
Expand Up @@ -71,7 +71,9 @@ Behave allows you to provide your own formatter (class)::
behave -f foo.bar:Json2Formatter ...

The usage of a user-defined formatter can be simplified by providing an
alias name for it in the configuration file::
alias name for it in the configuration file:

.. code-block:: ini
# -- FILE: behave.ini
# ALIAS SUPPORTS: behave -f json2 ...
Expand All @@ -80,7 +82,9 @@ alias name for it in the configuration file::
json2 = foo.bar:Json2Formatter
If your formatter can be configured, you should use the userdata concept
to provide them. The formatter should use the attribute schema::
to provide them. The formatter should use the attribute schema:

.. code-block:: ini
# -- FILE: behave.ini
# SCHEMA: behave.formatter.<FORMATTER_NAME>.<ATTRIBUTE_NAME>
Expand All @@ -105,4 +109,10 @@ teamcity :pypi:`behave-teamcity`, a formatter for Jetbrains TeamCity CI te
with behave.
============== =========================================================================

.. code-block:: ini
# -- FILE: behave.ini
# FORMATTER ALIASES: behave -f allure ...
[behave.formatters]
allure = allure_behave.formatter:AllureFormatter
teamcity = behave_teamcity:TeamcityFormatter

0 comments on commit dc9ac62

Please sign in to comment.