diff --git a/Don-t-crash-on-invalid-XML-chars-in-embed.patch b/Don-t-crash-on-invalid-XML-chars-in-embed.patch deleted file mode 100644 index c28708b..0000000 --- a/Don-t-crash-on-invalid-XML-chars-in-embed.patch +++ /dev/null @@ -1,57 +0,0 @@ -From 13b026c96d0e4cb6e298258a808ffa80c1b5acc1 Mon Sep 17 00:00:00 2001 -From: Vadim Rutkovsky -Date: Wed, 19 Mar 2014 12:37:38 +0100 -Subject: [PATCH] Don't crash on invalid XML chars in embed - ---- - behave/formatter/html.py | 22 ++++++++++++---------- - 1 file changed, 12 insertions(+), 10 deletions(-) - ---- a/behave/formatter/html.py -+++ b/behave/formatter/html.py -@@ -4,7 +4,8 @@ import base64 - import os.path - from behave.compat.collections import Counter - --def _valid_XML_char_ordinal(i): -+def _valid_XML_char_ordinal(char): -+ i = ord(char) - return ( # conditions ordered by presumed frequency - 0x20 <= i <= 0xD7FF - or i in (0x9, 0xA, 0xD) -@@ -208,10 +209,11 @@ class HTMLFormatter(Formatter): - embed = ET.SubElement(step, 'pre', - {'id': "embed_%s" % self.embed_id, - 'style': 'display: none; white-space: pre-wrap;'}) -- cleaned_error_message = ''.join( -- c for c in result.error_message if _valid_XML_char_ordinal(ord(c)) -- ) -- embed.text = cleaned_error_message -+ cleaned_error_message = filter(_valid_XML_char_ordinal, result.error_message) -+ try: -+ embed.text = cleaned_error_message -+ except ValueError: -+ print("Cannot embed error message as it contains nonvalid XML chars") - embed.tail = u' ' - - if result.status == 'failed': -@@ -276,14 +278,14 @@ class HTMLFormatter(Formatter): - caption = u'Data' - link.text = unicode(caption) - -- cleaned_data = ''.join( -- c for c in data if _valid_XML_char_ordinal(ord(c)) -- ) -- -+ cleaned_data = filter(_valid_XML_char_ordinal, data) - embed = ET.SubElement(span, 'pre', - {'id': "embed_%s" % self.embed_id, - 'style': 'display: none'}) -- embed.text = cleaned_data -+ try: -+ embed.text = cleaned_data -+ except ValueError: -+ print("Cannot embed text as it contains nonvalid XML chars") - embed.tail = u' ' - - def embedding(self, mime_type, data, caption=None): diff --git a/Embedding-support-link-caption-and-video-tags.patch b/Embedding-support-link-caption-and-video-tags.patch deleted file mode 100644 index e902a9c..0000000 --- a/Embedding-support-link-caption-and-video-tags.patch +++ /dev/null @@ -1,113 +0,0 @@ -From 785a8311e3c4af8b172034f7223f50c6db840f4c Mon Sep 17 00:00:00 2001 -From: Vadim Rutkovsky -Date: Mon, 27 Jan 2014 17:27:04 +0100 -Subject: [PATCH] Embedding: support link caption and video tags - ---- - behave/formatter/html.py | 36 ++++++++++++++++++++++++++++++------ - behave/formatter/json.py | 2 +- - behave/runner.py | 4 ++-- - 3 files changed, 33 insertions(+), 9 deletions(-) - ---- a/behave/formatter/html.py -+++ b/behave/formatter/html.py -@@ -227,10 +227,11 @@ class HTMLFormatter(Formatter): - if hasattr(self, 'embed_in_this_step') and self.embed_in_this_step: - self._doEmbed(self.last_step_embed_span, - self.embed_mime_type, -- self.embed_data) -+ self.embed_data, -+ self.embed_caption) - self.embed_in_this_step = None - -- def _doEmbed(self, span, mime_type, data): -+ def _doEmbed(self, span, mime_type, data, caption): - self.embed_id += 1 - - link = ET.SubElement(span, 'a') -@@ -240,8 +241,28 @@ class HTMLFormatter(Formatter): - "(embd.style.display == 'none' ? 'block' : 'none');" + - "return false") - -+ if 'video/' in mime_type: -+ if not caption: -+ caption = u'Video' -+ link.text = unicode(caption) -+ -+ embed = ET.SubElement(span, 'video', -+ {'id': 'embed_%s' % self.embed_id, -+ 'style': 'display: none', -+ 'width': '320', -+ 'controls': '', -+ }) -+ embed.tail = u' ' -+ embed_source = ET.SubElement(embed, 'source', -+ { -+ 'src': u'data:%s;base64,%s' % (mime_type, base64.b64encode(data)), -+ 'type': '%s; codecs="vp8 vorbis"' % mime_type -+ }) -+ - if 'image/' in mime_type: -- link.text = u'Screenshot' -+ if not caption: -+ caption = u'Screenshot' -+ link.text = unicode(caption) - - embed = ET.SubElement(span, 'img', - {'id': 'embed_%s' % self.embed_id, -@@ -251,7 +272,9 @@ class HTMLFormatter(Formatter): - embed.tail = u' ' - - if 'text/' in mime_type: -- link.text = u'Data' -+ if not caption: -+ caption = u'Data' -+ link.text = unicode(caption) - - cleaned_data = ''.join( - c for c in data if _valid_XML_char_ordinal(ord(c)) -@@ -263,15 +286,16 @@ class HTMLFormatter(Formatter): - embed.text = cleaned_data - embed.tail = u' ' - -- def embedding(self, mime_type, data): -+ def embedding(self, mime_type, data, caption=None): - if self.last_step.status == 'untested': - # Embed called during step execution - self.embed_in_this_step = True - self.embed_mime_type = mime_type - self.embed_data = data -+ self.embed_caption = caption - else: - # Embed called in after_* -- self._doEmbed(self.last_step_embed_span, mime_type, data) -+ self._doEmbed(self.last_step_embed_span, mime_type, data, caption) - - def close(self): - if not hasattr(self, "all_features"): ---- a/behave/formatter/json.py -+++ b/behave/formatter/json.py -@@ -180,7 +180,7 @@ class JSONFormatter(Formatter): - result_element['error_message'] = error_message - self._step_index += 1 - -- def embedding(self, mime_type, data): -+ def embedding(self, mime_type, data, caption=None): - step = self.current_feature_element['steps'][-1] - step['embeddings'].append({ - 'mime_type': mime_type, ---- a/behave/runner.py -+++ b/behave/runner.py -@@ -254,10 +254,10 @@ class Context(object): - return True - return False - -- def embed(self, mime_type, data): -+ def embed(self, mime_type, data, caption=None): - for formatter in self._runner.formatters: - if hasattr(formatter, 'embedding'): -- formatter.embedding(mime_type, data) -+ formatter.embedding(mime_type, data, caption) - - def execute_steps(self, steps_text): - '''The steps identified in the "steps" text string will be parsed and diff --git a/Fix-relpath-imports.patch b/Fix-relpath-imports.patch deleted file mode 100644 index b14c0f3..0000000 --- a/Fix-relpath-imports.patch +++ /dev/null @@ -1,458 +0,0 @@ -The only package from the ones covered by behave/compat/ libraries which -are not required in setup.py for particular version is behave/compat/os_path.py -which is for compatibility with python <= 2.5 - - ---- a/behave/formatter/progress.py -+++ b/behave/formatter/progress.py -@@ -9,9 +9,12 @@ A "dot" character that represents the re - executing a scope item. - """ - --from behave.formatter.base import Formatter --from behave.compat.os_path import relpath - import os -+from behave.formatter.base import Formatter -+try: -+ from os.path import relpath -+except ImportError: -+ from behave.compat.os_path import relpath - - # ----------------------------------------------------------------------------- - # CLASS: ProgressFormatterBase ---- a/behave/formatter/rerun.py -+++ b/behave/formatter/rerun.py -@@ -23,9 +23,12 @@ Normally, you put the RerunFormatter int - """ - - from behave.formatter.base import Formatter --from behave.compat.os_path import relpath - from datetime import datetime - import os -+try: -+ from os.path import relpath -+except ImportError: -+ from behave.compat.os_path import relpath - - - # ----------------------------------------------------------------------------- -@@ -90,7 +93,7 @@ class RerunFormatter(Formatter): - self.stream.write(message % len(self.failed_scenarios)) - if self.show_timestamp: - now = datetime.now().replace(microsecond=0) -- self.stream.write("# NOW: %s\n"% now.isoformat(" ")) -+ self.stream.write("# NOW: %s\n" % now.isoformat(" ")) - - # -- SECTION: Textual summary in comments. - if self.show_failed_scenarios_descriptions: -@@ -102,7 +105,7 @@ class RerunFormatter(Formatter): - current_feature = scenario.filename - short_filename = relpath(scenario.filename, os.getcwd()) - self.stream.write(u"# %s\n" % short_filename) -- self.stream.write(u"# %4d: %s\n" % \ -+ self.stream.write(u"# %4d: %s\n" % - (scenario.line, scenario.name)) - self.stream.write("\n") - ---- a/behave/formatter/sphinx_steps.py -+++ b/behave/formatter/sphinx_steps.py -@@ -14,11 +14,14 @@ TODO: - - from behave.formatter.steps import AbstractStepsFormatter - from behave.formatter import sphinx_util --from behave.compat.os_path import relpath - from behave.model import Table - import inspect - import os.path - import sys -+try: -+ from os.path import relpath -+except ImportError: -+ from behave.compat.os_path import relpath - - - # ----------------------------------------------------------------------------- ---- a/behave/model.py -+++ b/behave/model.py -@@ -1,6 +1,7 @@ - # -*- coding: utf-8 -*- - - from __future__ import with_statement -+import sys - import copy - import difflib - import itertools -@@ -9,7 +10,10 @@ import sys - import time - import traceback - from behave import step_registry --from behave.compat.os_path import relpath -+try: -+ from os.path import relpath -+except ImportError: -+ from behave.compat.os_path import relpath - - - class Argument(object): -@@ -1472,7 +1476,10 @@ class Row(object): - Converts the row and its cell data into a dictionary. - :return: Row data as dictionary (without comments, line info). - """ -- from behave.compat.collections import OrderedDict -+ try: -+ from collections import OrderedDict -+ except ImportError: -+ from behave.compat.collections import OrderedDict - return OrderedDict(self.items()) - - ---- a/behave/formatter/html.py -+++ b/behave/formatter/html.py -@@ -2,12 +2,16 @@ from behave.formatter.base import Format - import lxml.etree as ET - import base64 - import os.path --from behave.compat.collections import Counter -+try: -+ from collections import Counter -+except ImportError: -+ from behave.compat.collections import Counter -+ - - def _valid_XML_char_ordinal(char): - i = ord(char) - return ( # conditions ordered by presumed frequency -- 0x20 <= i <= 0xD7FF -+ 0x20 <= i <= 0xD7FF - or i in (0x9, 0xA, 0xD) - or 0xE000 <= i <= 0xFFFD - or 0x10000 <= i <= 0x10FFFF ---- a/behave/importer.py -+++ b/behave/importer.py -@@ -5,7 +5,11 @@ Importer module for lazy-loading/importi - REQUIRES: importlib (provided in Python2.7, Python3.2...) - """ - --from behave.compat import importlib -+import sys -+try: -+ import importlib -+except ImportError: -+ from behave.compat import importlib - - - class Unknown(object): ---- a/test/test_model.py -+++ b/test/test_model.py -@@ -1,11 +1,13 @@ - from __future__ import with_statement - --import re - import sys - from mock import Mock, patch - from nose.tools import * - from behave import model --from behave.compat.collections import OrderedDict -+try: -+ from collections import OrderedDict -+except ImportError: -+ from behave.compat.collections import OrderedDict - from behave import step_registry - from behave.configuration import Configuration - ---- a/behave/compat/__init__.py -+++ /dev/null -@@ -1,5 +0,0 @@ --# -*- coding: utf-8 -*- --""" --Used for behave as compatibility layer between different Python versions --and implementations. --""" -\ No newline at end of file ---- a/behave/compat/collections.py -+++ /dev/null -@@ -1,205 +0,0 @@ --# -*- coding: utf-8 -*- --""" --Compatibility of :module:`collections` between different Python versions. --""" -- --from __future__ import absolute_import --import warnings -- --try: -- # -- SINCE: Python2.7 -- from collections import OrderedDict --except ImportError: # pragma: no cover -- try: -- # -- BACK-PORTED FOR: Python 2.4 .. 2.6 -- from ordereddict import OrderedDict -- except ImportError: -- message = "collections.OrderedDict is missing: Install 'ordereddict'." -- warnings.warn(message) -- # -- BACKWARD-COMPATIBLE: Better than nothing (for behave use case). -- OrderedDict = dict -- --try: -- # -- SINCE: Python2.7 -- from collections import Counter --except ImportError: # pragma: no cover -- class Counter(dict): -- '''Dict subclass for counting hashable objects. Sometimes called a bag -- or multiset. Elements are stored as dictionary keys and their counts -- are stored as dictionary values. -- -- >>> Counter('zyzygy') -- Counter({'y': 3, 'z': 2, 'g': 1}) -- -- ''' -- -- def __init__(self, iterable=None, **kwds): -- '''Create a new, empty Counter object. And if given, count elements -- from an input iterable. Or, initialize the count from another mapping -- of elements to their counts. -- -- >>> c = Counter() # a new, empty counter -- >>> c = Counter('gallahad') # a new counter from an iterable -- >>> c = Counter({'a': 4, 'b': 2}) # a new counter from a mapping -- >>> c = Counter(a=4, b=2) # a new counter from keyword args -- -- ''' -- self.update(iterable, **kwds) -- -- def __missing__(self, key): -- return 0 -- -- def most_common(self, n=None): -- '''List the n most common elements and their counts from the most -- common to the least. If n is None, then list all element counts. -- -- >>> Counter('abracadabra').most_common(3) -- [('a', 5), ('r', 2), ('b', 2)] -- -- ''' -- if n is None: -- return sorted(self.iteritems(), key=itemgetter(1), reverse=True) -- return nlargest(n, self.iteritems(), key=itemgetter(1)) -- -- def elements(self): -- '''Iterator over elements repeating each as many times as its count. -- -- >>> c = Counter('ABCABC') -- >>> sorted(c.elements()) -- ['A', 'A', 'B', 'B', 'C', 'C'] -- -- If an element's count has been set to zero or is a negative number, -- elements() will ignore it. -- -- ''' -- for elem, count in self.iteritems(): -- for _ in repeat(None, count): -- yield elem -- -- # Override dict methods where the meaning changes for Counter objects. -- -- @classmethod -- def fromkeys(cls, iterable, v=None): -- raise NotImplementedError( -- 'Counter.fromkeys() is undefined. Use Counter(iterable) instead.') -- -- def update(self, iterable=None, **kwds): -- '''Like dict.update() but add counts instead of replacing them. -- -- Source can be an iterable, a dictionary, or another Counter instance. -- -- >>> c = Counter('which') -- >>> c.update('witch') # add elements from another iterable -- >>> d = Counter('watch') -- >>> c.update(d) # add elements from another counter -- >>> c['h'] # four 'h' in which, witch, and watch -- 4 -- -- ''' -- if iterable is not None: -- if hasattr(iterable, 'iteritems'): -- if self: -- self_get = self.get -- for elem, count in iterable.iteritems(): -- self[elem] = self_get(elem, 0) + count -- else: -- dict.update(self, iterable) # fast path when counter is empty -- else: -- self_get = self.get -- for elem in iterable: -- self[elem] = self_get(elem, 0) + 1 -- if kwds: -- self.update(kwds) -- -- def copy(self): -- 'Like dict.copy() but returns a Counter instance instead of a dict.' -- return Counter(self) -- -- def __delitem__(self, elem): -- 'Like dict.__delitem__() but does not raise KeyError for missing values.' -- if elem in self: -- dict.__delitem__(self, elem) -- -- def __repr__(self): -- if not self: -- return '%s()' % self.__class__.__name__ -- items = ', '.join(map('%r: %r'.__mod__, self.most_common())) -- return '%s({%s})' % (self.__class__.__name__, items) -- -- # Multiset-style mathematical operations discussed in: -- # Knuth TAOCP Volume II section 4.6.3 exercise 19 -- # and at http://en.wikipedia.org/wiki/Multiset -- # -- # Outputs guaranteed to only include positive counts. -- # -- # To strip negative and zero counts, add-in an empty counter: -- # c += Counter() -- -- def __add__(self, other): -- '''Add counts from two counters. -- -- >>> Counter('abbb') + Counter('bcc') -- Counter({'b': 4, 'c': 2, 'a': 1}) -- -- -- ''' -- if not isinstance(other, Counter): -- return NotImplemented -- result = Counter() -- for elem in set(self) | set(other): -- newcount = self[elem] + other[elem] -- if newcount > 0: -- result[elem] = newcount -- return result -- -- def __sub__(self, other): -- ''' Subtract count, but keep only results with positive counts. -- -- >>> Counter('abbbc') - Counter('bccd') -- Counter({'b': 2, 'a': 1}) -- -- ''' -- if not isinstance(other, Counter): -- return NotImplemented -- result = Counter() -- for elem in set(self) | set(other): -- newcount = self[elem] - other[elem] -- if newcount > 0: -- result[elem] = newcount -- return result -- -- def __or__(self, other): -- '''Union is the maximum of value in either of the input counters. -- -- >>> Counter('abbb') | Counter('bcc') -- Counter({'b': 3, 'c': 2, 'a': 1}) -- -- ''' -- if not isinstance(other, Counter): -- return NotImplemented -- _max = max -- result = Counter() -- for elem in set(self) | set(other): -- newcount = _max(self[elem], other[elem]) -- if newcount > 0: -- result[elem] = newcount -- return result -- -- def __and__(self, other): -- ''' Intersection is the minimum of corresponding counts. -- -- >>> Counter('abbb') & Counter('bcc') -- Counter({'b': 1}) -- -- ''' -- if not isinstance(other, Counter): -- return NotImplemented -- _min = min -- result = Counter() -- if len(self) < len(other): -- self, other = other, self -- for elem in ifilter(self.__contains__, other): -- newcount = _min(self[elem], other[elem]) -- if newcount > 0: -- result[elem] = newcount -- return result ---- a/behave/compat/importlib.py -+++ /dev/null -@@ -1,46 +0,0 @@ --# -*- coding: utf-8 -*- --""" --importlib was introduced in python2.7, python3.2... --""" -- --try: -- from importlib import import_module --except ImportError: -- """Backport of importlib.import_module from 3.x.""" -- # While not critical (and in no way guaranteed!), it would be nice to keep this -- # code compatible with Python 2.3. -- import sys -- -- def _resolve_name(name, package, level): -- """Return the absolute name of the module to be imported.""" -- if not hasattr(package, 'rindex'): -- raise ValueError("'package' not set to a string") -- dot = len(package) -- for x in xrange(level, 1, -1): -- try: -- dot = package.rindex('.', 0, dot) -- except ValueError: -- raise ValueError("attempted relative import beyond top-level " -- "package") -- return "%s.%s" % (package[:dot], name) -- -- -- def import_module(name, package=None): -- """Import a module. -- -- The 'package' argument is required when performing a relative import. It -- specifies the package to use as the anchor point from which to resolve the -- relative import to an absolute import. -- -- """ -- if name.startswith('.'): -- if not package: -- raise TypeError("relative imports require the 'package' argument") -- level = 0 -- for character in name: -- if character != '.': -- break -- level += 1 -- name = _resolve_name(name[level:], package, level) -- __import__(name) -- return sys.modules[name] ---- a/behave/compat/os_path.py -+++ /dev/null -@@ -1,27 +0,0 @@ --# -*- coding: utf-8 -*- --""" --Compatibility of :module:`os.path` between different Python versions. --""" -- --import os.path -- --relpath = getattr(os.path, "relpath", None) --if relpath is None: # pragma: no cover -- # -- Python2.5 doesn't know about relpath -- def relpath(path, start=os.path.curdir): -- """ -- Return a relative version of a path -- BASED-ON: Python2.7 -- """ -- if not path: -- raise ValueError("no path specified") -- -- start_list = [x for x in os.path.abspath(start).split(os.path.sep) if x] -- path_list = [x for x in os.path.abspath(path).split(os.path.sep) if x] -- # Work out how much of the filepath is shared by start and path. -- i = len(os.path.commonprefix([start_list, path_list])) -- -- rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:] -- if not rel_list: -- return os.path.curdir -- return os.path.join(*rel_list) diff --git a/HTML-Formatter.patch b/HTML-Formatter.patch index a505f01..9c5a521 100644 --- a/HTML-Formatter.patch +++ b/HTML-Formatter.patch @@ -1,6 +1,28 @@ +From eab8136d73e9c17ed61a099170ef7ed71788e376 Mon Sep 17 00:00:00 2001 +From: Vadim Rutkovsky +Date: Thu, 17 Apr 2014 10:53:39 +0200 +Subject: [PATCH] HTML Formatter + +--- + behave/compat/collections.py | 185 ++++++++++ + behave/configuration.py | 8 +- + behave/formatter/_builtins.py | 1 + + behave/formatter/behave.css | 241 +++++++++++++ + behave/formatter/html.py | 496 ++++++++++++++++++++++++++ + behave/runner.py | 5 + + features/formatter.help.feature | 1 + + features/formatter.html.feature | 749 +++++++++++++++++++++++++++++++++++++++ + issue.features/issue0031.feature | 11 + + 9 files changed, 1696 insertions(+), 1 deletion(-) + create mode 100644 behave/formatter/behave.css + create mode 100644 behave/formatter/html.py + create mode 100644 features/formatter.html.feature + +diff --git a/behave/compat/collections.py b/behave/compat/collections.py +index 530578c..cc27448 100644 --- a/behave/compat/collections.py +++ b/behave/compat/collections.py -@@ -18,3 +18,188 @@ except ImportError: # pragma: no cov +@@ -18,3 +18,188 @@ except ImportError: # pragma: no cover warnings.warn(message) # -- BACKWARD-COMPATIBLE: Better than nothing (for behave use case). OrderedDict = dict @@ -189,343 +211,86 @@ + if newcount > 0: + result[elem] = newcount + return result +diff --git a/behave/configuration.py b/behave/configuration.py +index a2563df..b91f2e8 100644 --- a/behave/configuration.py +++ b/behave/configuration.py -@@ -4,6 +4,7 @@ import os - import re - import sys - import argparse -+import codecs - import ConfigParser - import logging - import shlex ---- a/behave/formatter/formatters.py -+++ b/behave/formatter/formatters.py -@@ -76,6 +76,7 @@ def setup_formatters(): - register_as(_L("behave.formatter.steps:StepsUsageFormatter"), "steps.usage") - register_as(_L("behave.formatter.sphinx_steps:SphinxStepsFormatter"), - "sphinx.steps") -+ register_as(_L("behave.formatter.html:HTMLFormatter"), "html") +@@ -136,6 +136,10 @@ options = [ + help="""Specify name annotation schema for scenario outline + (default="{name} -- @{row.id} {examples.name}").""")), ++ ((), # -- CONFIGFILE only ++ dict(dest='css', ++ help="""Specify a different css for HTML formatter""")), ++ + # (('-g', '--guess'), + # dict(action='store_true', + # help="Guess best match for ambiguous steps.")), +@@ -503,7 +507,8 @@ class Configuration(object): + stage=None, + userdata={}, + # -- SPECIAL: +- default_format="pretty", # -- Used when no formatters are configured. ++ default_format="pretty", # -- Used when no formatters are configured. ++ css=None, + scenario_outline_annotation_schema=u"{name} -- @{row.id} {examples.name}" + ) + cmdline_only_options = set("userdata_defines") +@@ -545,6 +550,7 @@ class Configuration(object): + self.outputs = [] + self.include_re = None + self.exclude_re = None ++ self.css = None + self.scenario_outline_annotation_schema = None + self.steps_dir = "steps" + self.environment_file = "environment.py" +diff --git a/behave/formatter/_builtins.py b/behave/formatter/_builtins.py +index 8c2e52e..8e9d37c 100644 +--- a/behave/formatter/_builtins.py ++++ b/behave/formatter/_builtins.py +@@ -28,6 +28,7 @@ _BUILTIN_FORMATS = [ + ("steps.catalog", "behave.formatter.steps:StepsCatalogFormatter"), + ("steps.usage", "behave.formatter.steps:StepsUsageFormatter"), + ("sphinx.steps", "behave.formatter.sphinx_steps:SphinxStepsFormatter"), ++ ("html", "behave.formatter.html:HTMLFormatter"), + ] # ----------------------------------------------------------------------------- +diff --git a/behave/formatter/behave.css b/behave/formatter/behave.css +new file mode 100644 +index 0000000..44f7685 --- /dev/null -+++ b/behave/formatter/html.py -@@ -0,0 +1,311 @@ -+from behave.formatter.base import Formatter -+import lxml.etree as ET -+import base64 -+import os.path -+from behave.compat.collections import Counter -+ -+ -+class HTMLFormatter(Formatter): -+ name = 'html' -+ description = 'Very basic HTML formatter' -+ -+ def __init__(self, stream, config): -+ super(HTMLFormatter, self).__init__(stream, config) -+ -+ self.html = ET.Element('html') -+ -+ head = ET.SubElement(self.html, 'head') -+ ET.SubElement(head, 'title').text = u'Behave steps' -+ ET.SubElement(head, 'meta', {'content': 'text/html;charset=utf-8'}) -+ ET.SubElement(head, 'style').text =\ -+ open(os.path.join(os.path.dirname(__file__), ("report.css")), -+ 'r').read().encode('utf-8') -+ -+ self.stream = self.open() -+ body = ET.SubElement(self.html, 'body') -+ self.suite = ET.SubElement(body, 'div', {'class': 'behave'}) -+ -+ #Summary -+ self.header = ET.SubElement(self.suite, 'div', id='behave-header') -+ label = ET.SubElement(self.header, 'div', id='label') -+ ET.SubElement(label, 'h1').text = u'Behave features' -+ -+ summary = ET.SubElement(self.header, 'div', id='summary') -+ -+ totals = ET.SubElement(summary, 'p', id='totals') ++++ b/behave/formatter/behave.css +@@ -0,0 +1,241 @@ ++// SOURCE: https://raw.githubusercontent.com/vrutkovs/behave/html_with_coveralls/behave/formatter/report.css + -+ self.current_feature_totals = ET.SubElement(totals, 'p', id='feature_totals') -+ self.scenario_totals = ET.SubElement(totals, 'p', id='scenario_totals') -+ self.step_totals = ET.SubElement(totals, 'p', id='step_totals') -+ self.duration = ET.SubElement(summary, 'p', id='duration') -+ -+ expand_collapse = ET.SubElement(summary, 'div', id='expand-collapse') -+ -+ expander = ET.SubElement(expand_collapse, 'span', id='expander') -+ expander.set('onclick', \ -+ "var ols=document.getElementsByClassName('scenario_steps');" + -+ "for (var i=0; i< ols.length; i++) {" + -+ "ols[i].style.display = 'block';" + -+ "}; " + -+ "return false") -+ expander.text = u'Expand All' -+ -+ spacer = ET.SubElement(expand_collapse, 'span') -+ spacer.text = u" " -+ -+ collapser = ET.SubElement(expand_collapse, 'span', id='collapser') -+ collapser.set('onclick', \ -+ "var ols=document.getElementsByClassName('scenario_steps');" + -+ "for (var i=0; i< ols.length; i++) {" + -+ "ols[i].style.display = 'none';" + -+ "}; " + -+ "return false") -+ collapser.text = u'Collapse All' -+ -+ self.embed_id = 0 -+ self.embed_in_this_step = None -+ self.embed_data = None -+ self.embed_mime_type = None -+ self.scenario_id = 0 -+ -+ def feature(self, feature): -+ if not hasattr(self, "all_features"): -+ self.all_features = [] -+ self.all_features.append(feature) -+ -+ self.current_feature = ET.SubElement(self.suite, 'div', {'class': 'feature'}) -+ if feature.tags: -+ tags_element = ET.SubElement(self.current_feature, 'span', {'class': 'tag'}) -+ tags_element.text = u'@' + reduce(lambda d, x: "%s, @%s" % (d, x), feature.tags) -+ h2 = ET.SubElement(self.current_feature, 'h2') -+ feature_element = ET.SubElement(h2, 'span', {'class': 'val'}) -+ feature_element.text = u'%s: %s' % (feature.keyword, feature.name) -+ if feature.description: -+ description_element = ET.SubElement(self.current_feature, 'pre', {'class': 'message'}) -+ description_element.text = reduce(lambda d, x: "%s\n%s" % (d, x), feature.description) -+ -+ def background(self, background): -+ -+ self.current_background = ET.SubElement(self.suite, 'div', {'class': 'background'}) -+ -+ h3 = ET.SubElement(self.current_background, 'h3') -+ ET.SubElement(h3, 'span', {'class': 'val'}).text = \ -+ u'%s: %s' % (background.keyword, background.name) -+ -+ -+ self.steps = ET.SubElement(self.current_background, 'ol') -+ -+ def scenario(self, scenario): -+ if scenario.feature not in self.all_features: -+ self.all_features.append(scenario.feature) -+ self.scenario_el = ET.SubElement(self.suite, 'div', {'class': 'scenario'}) -+ -+ scenario_file = ET.SubElement(self.scenario_el, 'span', {'class': 'scenario_file'}) -+ scenario_file.text = "%s:%s" % (scenario.location.filename, scenario.location.line) -+ -+ if scenario.tags: -+ tags = ET.SubElement(self.scenario_el, 'span', {'class': 'tag'}) -+ tags.text = u'@' + reduce(lambda d, x: "%s, @%s" % (d, x), scenario.tags) -+ -+ self.scenario_name = ET.SubElement(self.scenario_el, 'h3') -+ span = ET.SubElement(self.scenario_name, 'span', {'class': 'val'}) -+ span.text = u'%s: %s' % (scenario.keyword, scenario.name) -+ -+ if scenario.description: -+ description_element = ET.SubElement(self.scenario_el, 'pre', {'class': 'message'}) -+ description_element.text = reduce(lambda d, x: "%s\n%s" % (d, x), scenario.description) -+ -+ self.steps = ET.SubElement(self.scenario_el, 'ol', -+ {'class': 'scenario_steps', -+ 'id': 'scenario_%s' % self.scenario_id}) -+ -+ self.scenario_name.set('onclick', \ -+ "ol=document.getElementById('scenario_%s');" % self.scenario_id + -+ "ol.style.display =" + -+ "(ol.style.display == 'none' ? 'block' : 'none');" + -+ "return false") -+ self.scenario_id += 1 -+ -+ def scenario_outline(self, outline): -+ self.scenario(self, outline) -+ self.scenario_el.set('class', 'scenario outline') -+ -+ def match(self, match): -+ self.arguments = match.arguments -+ if match.location: -+ self.location = "%s:%s" % (match.location.filename, match.location.line) -+ else: -+ self.location = "" -+ -+ def step(self, step): -+ self.arguments = None -+ self.embed_in_this_step = None -+ self.last_step = step -+ -+ def result(self, result): -+ self.last_step = result -+ step = ET.SubElement(self.steps, 'li', {'class': 'step %s' % result.status}) -+ step_name = ET.SubElement(step, 'div', {'class': 'step_name'}) -+ -+ keyword = ET.SubElement(step_name, 'span', {'class': 'keyword'}) -+ keyword.text = result.keyword + u' ' -+ -+ step_text = ET.SubElement(step_name, 'span', {'class': 'step val'}) -+ step_text.text = result.name -+ if self.arguments: -+ text_start = 0 -+ for argument in self.arguments: -+ if text_start == 0: -+ step_text.text = result.name[:argument.start] -+ else: -+ bold.tail = result.name[text_start:argument.start] -+ bold = ET.SubElement(step_text, 'b') -+ bold.text = str(argument.value) -+ text_start = argument.end -+ # Add remaining tail -+ bold.tail = result.name[self.arguments[-1].end:] -+ -+ step_file = ET.SubElement(step, 'div', {'class': 'step_file'}) -+ ET.SubElement(step_file, 'span').text = self.location -+ -+ self.last_step_embed_span = ET.SubElement(step, 'span') -+ self.last_step_embed_span.set('class', 'embed') -+ -+ if result.text: -+ message = ET.SubElement(step, 'div', {'class': 'message'}) -+ pre = ET.SubElement(message, 'pre', {'style': 'white-space: pre-wrap;'}) -+ pre.text = result.text -+ -+ if result.table: -+ table = ET.SubElement(step, 'table') -+ tr = ET.SubElement(table, 'tr') -+ for heading in result.table.headings: -+ ET.SubElement(tr, 'th').text = heading -+ -+ for row in result.table.rows: -+ tr = ET.SubElement(table, 'tr') -+ for cell in row.cells: -+ ET.SubElement(tr, 'td').text = cell -+ -+ if result.error_message: -+ self.embed_id += 1 -+ link = ET.SubElement(step, 'a', {'class': 'message'}) -+ link.set("onclick", \ -+ "rslt=document.getElementById('embed_%s');" % self.embed_id + -+ "rslt.style.display =" + -+ "(rslt.style.display == 'none' ? 'block' : 'none');" + -+ "return false") -+ link.text = u'Error message' -+ -+ embed = ET.SubElement(step, 'pre', -+ {'id': "embed_%s" % self.embed_id, -+ 'style': 'display: none; white-space: pre-wrap;'}) -+ embed.text = result.error_message -+ embed.tail = u' ' -+ -+ if result.status == 'failed': -+ style = 'background: #C40D0D; color: #FFFFFF' -+ self.scenario_name.set('style', style) -+ self.header.set('style', style) -+ -+ if result.status == 'undefined': -+ style = 'background: #FAF834; color: #000000' -+ self.scenario_name.set('style', style) -+ self.header.set('style', style) -+ -+ if hasattr(self, 'embed_in_this_step') and self.embed_in_this_step: -+ self._doEmbed(self.last_step_embed_span, -+ self.embed_mime_type, -+ self.embed_data) -+ self.embed_in_this_step = None -+ -+ def _doEmbed(self, span, mime_type, data): -+ self.embed_id += 1 -+ -+ link = ET.SubElement(span, 'a') -+ link.set("onclick", \ -+ "embd=document.getElementById('embed_%s');" % self.embed_id + -+ "embd.style.display =" + -+ "(embd.style.display == 'none' ? 'block' : 'none');" + -+ "return false") -+ -+ if 'image/' in mime_type: -+ link.text = u'Screenshot' -+ -+ embed = ET.SubElement(span, 'img', -+ {'id': 'embed_%s' % self.embed_id, -+ 'style': 'display: none', -+ 'src': u'data:%s;base64,%s' % (mime_type, base64.b64encode(data)) -+ }) -+ embed.tail = u' ' ++// -- RESULT-STATUS RELATED STYLES: ++.passed { ++} + -+ if 'text/' in mime_type: -+ link.text = u'Data' -+ -+ def valid_XML_char_ordinal(i): -+ return ( # conditions ordered by presumed frequency -+ 0x20 <= i <= 0xD7FF -+ or i in (0x9, 0xA, 0xD) -+ or 0xE000 <= i <= 0xFFFD -+ or 0x10000 <= i <= 0x10FFFF -+ ) -+ cleaned_data = ''.join( -+ c for c in data if valid_XML_char_ordinal(ord(c)) -+ ) ++.failed { ++} + -+ embed = ET.SubElement(span, 'pre', -+ {'id': "embed_%s" % self.embed_id, -+ 'style': 'display: none'}) -+ embed.text = cleaned_data -+ embed.tail = u' ' ++.error { ++} + -+ def embedding(self, mime_type, data): -+ if self.last_step.status == 'untested': -+ # Embed called during step execution -+ self.embed_in_this_step = True -+ self.embed_mime_type = mime_type -+ self.embed_data = data -+ else: -+ # Embed called in after_* -+ self._doEmbed(self.last_step_embed_span, mime_type, data) ++.skipped { ++} + -+ def close(self): -+ if not hasattr(self, "all_features"): -+ self.all_features = [] -+ self.duration.text =\ -+ u"Finished in %0.1f seconds" %\ -+ sum(map(lambda x: x.duration, self.all_features)) ++.undefined { ++} + -+ # Filling in summary details -+ result = [] -+ statuses = map(lambda x: x.status, self.all_features) -+ status_counter = Counter(statuses) -+ for k in status_counter: -+ result.append('%s: %s' % (k, status_counter[k])) -+ self.current_feature_totals.text = u'Features: %s' % ', '.join(result) ++// -- CONTENT-RELATED STYLES: ++.summary { ++} + -+ result = [] -+ scenarios_list = map(lambda x: x.scenarios, self.all_features) -+ scenarios = [] -+ if len(scenarios_list) > 0: -+ scenarios = reduce(lambda a, b: a + b, scenarios_list) -+ statuses = map(lambda x: x.status, scenarios) -+ status_counter = Counter(statuses) -+ for k in status_counter: -+ result.append('%s: %s' % (k, status_counter[k])) -+ self.scenario_totals.text = u'Scenarios: %s' % ', '.join(result) ++.failed_scenarios { ++} + -+ result = [] -+ step_list = map(lambda x: x.steps, scenarios) -+ steps = [] -+ if step_list: -+ steps = reduce(lambda a, b: a + b, step_list) -+ statuses = map(lambda x: x.status, steps) -+ status_counter = Counter(statuses) -+ for k in status_counter: -+ result.append('%s: %s' % (k, status_counter[k])) -+ self.step_totals.text = u'Steps: %s' % ', '.join(result) ++.footer { ++} + -+ # Sending the report to stream -+ if len(self.all_features) > 0: -+ self.stream.write(ET.tostring(self.html, pretty_print = True)) ---- /dev/null -+++ b/behave/formatter/report.css -@@ -0,0 +1,212 @@ ++// -- ORIGINAL-STARTS-HERE +body { + font-size: 0px; + color: white; @@ -738,20 +503,526 @@ +.behave #summary #totals, td #summary #totals, th #summary #totals { + font-size: 1.2em; +} +diff --git a/behave/formatter/html.py b/behave/formatter/html.py +new file mode 100644 +index 0000000..874696e +--- /dev/null ++++ b/behave/formatter/html.py +@@ -0,0 +1,496 @@ ++# -*- coding: utf-8 -*- ++""" ++HTML formatter for behave. ++Writes a single-page HTML file for test run with all features/scenarios. ++ ++ ++IMPROVEMENTS: ++ + Avoid to use lxml.etree, use xml.etree.ElementTree instead (bundled w/ Python) ++ + Add pretty_print functionality to provide lxml goodie. ++ + Stylesheet should be (easily) replacable ++ + Simplify collapsable-section usage: ++ => Only JavaScript-call: onclick = Collapsible_toggle('xxx') ++ => Avoid code duplications, make HTML more readable ++ + Expand All / Collapse All: Use instead of element ++ => Make active logic (actions) more visible ++ * Support external stylesheet ?!? ++ * Introduce (Html)Page class to simplify extension and replacements ++ * Separate business layer (HtmlFormatter) from technology layer (Page). ++ * Correct Python2 constructs: map()/reduce() ++ * end() or stream.close() handling is missing ++ * steps: text, table parts are no so easily detectable ++ * CSS: stylesheet should contain logical "style" classes. ++ => AVOID using combination of style attributes where style is better. ++ ++TODO: ++ * Embedding only works with one part ?!? ++ * Even empty embed elements are contained ?!? ++""" ++ ++from behave.formatter.base import Formatter ++from behave.compat.collections import Counter ++# XXX-JE-OLD: import lxml.etree as ET ++import xml.etree.ElementTree as ET ++import base64 ++# XXX-JE-NOT-USED: import os.path ++ ++ ++def _valid_XML_char_ordinal(i): ++ return ( # conditions ordered by presumed frequency ++ 0x20 <= i <= 0xD7FF ++ or i in (0x9, 0xA, 0xD) ++ or 0xE000 <= i <= 0xFFFD ++ or 0x10000 <= i <= 0x10FFFF ++ ) ++ ++# XXX-JE-FIRST-IDEA: ++# def html_prettify(elem): ++# """Return a pretty-printed XML string for the Element.""" ++# rough_string = ET.tostring(elem, "utf-8") # XXX, method="html") ++# reparsed = minidom.parseString(rough_string) ++# return reparsed.toprettyxml(indent=" ") ++ ++def ET_tostring(elem, pretty_print=False): ++ """Render an HTML element(tree) and optionally pretty-print it.""" ++ ++ text = ET.tostring(elem, "utf-8") # XXX, method="html") ++ if pretty_print: ++ # -- RECIPE: For pretty-printing w/ xml.etree.ElementTree. ++ # SEE: http://pymotw.com/2/xml/etree/ElementTree/create.html ++ from xml.dom import minidom ++ import re ++ declaration_len = len(minidom.Document().toxml()) ++ reparsed = minidom.parseString(text) ++ text = reparsed.toprettyxml(indent=" ")[declaration_len:] ++ text_re = re.compile(r'>\n\s+([^<>\s].*?)\n\s+\g<1>" ++ ++ def step(self, step): ++ self.arguments = None ++ self.embed_in_this_step = None ++ self.last_step = step ++ ++ def result(self, result): ++ self.last_step = result ++ step = ET.SubElement(self.steps, 'li', {'class': 'step %s' % result.status}) ++ step_name = ET.SubElement(step, 'div', {'class': 'step_name'}) ++ ++ keyword = ET.SubElement(step_name, 'span', {'class': 'keyword'}) ++ keyword.text = result.keyword + u' ' ++ ++ step_text = ET.SubElement(step_name, 'span', {'class': 'step val'}) ++ if self.arguments: ++ text_start = 0 ++ for argument in self.arguments: ++ step_part = ET.SubElement(step_text, 'span') ++ step_part.text = result.name[text_start:argument.start] ++ ET.SubElement(step_text, 'b').text = str(argument.value) ++ text_start = argument.end ++ step_part = ET.SubElement(step_text, 'span') ++ step_part.text = result.name[self.arguments[-1].end:] ++ else: ++ step_text.text = result.name ++ ++ step_file = ET.SubElement(step, 'div', {'class': 'step_file'}) ++ ET.SubElement(step_file, 'span').text = self.location ++ ++ self.last_step_embed_span = ET.SubElement(step, 'span') ++ self.last_step_embed_span.set('class', 'embed') ++ ++ if result.text: ++ message = ET.SubElement(step, 'div', {'class': 'message'}) ++ pre = ET.SubElement(message, 'pre') ++ pre.text = result.text ++ ++ if result.table: ++ table = ET.SubElement(step, 'table') ++ tr = ET.SubElement(table, 'tr') ++ for heading in result.table.headings: ++ ET.SubElement(tr, 'th').text = heading ++ ++ for row in result.table.rows: ++ tr = ET.SubElement(table, 'tr') ++ for cell in row.cells: ++ ET.SubElement(tr, 'td').text = cell ++ ++ if result.error_message: ++ self.embed_id += 1 ++ link = ET.SubElement(step, 'a', {'class': 'message'}) ++ link.set("onclick", ++ "Collapsible_toggle('embed_%s')" % self.embed_id) ++ link.text = u'Error message' ++ ++ embed = ET.SubElement(step, 'pre', ++ {'id': "embed_%s" % self.embed_id, ++ 'style': 'display: none'}) ++ cleaned_error_message = ''.join( ++ c for c in result.error_message if _valid_XML_char_ordinal(ord(c)) ++ ) ++ embed.text = cleaned_error_message ++ embed.tail = u' ' ++ ++ if result.status == 'failed': ++ self.scenario_name.set('class', 'failed') ++ self.header.set('class', 'failed') ++ ++ if result.status == 'undefined': ++ self.scenario_name.set('class', 'undefined') ++ self.header.set('class', 'undefined') ++ ++ if hasattr(self, 'embed_in_this_step') and self.embed_in_this_step: ++ self._doEmbed(self.last_step_embed_span, ++ self.embed_mime_type, ++ self.embed_data, ++ self.embed_caption) ++ self.embed_in_this_step = None ++ ++ def _doEmbed(self, span, mime_type, data, caption): ++ self.embed_id += 1 ++ ++ link = ET.SubElement(span, 'a') ++ link.set("onclick", "Collapsible_toggle('embed_%s')" % self.embed_id) ++ ++ if 'video/' in mime_type: ++ if not caption: ++ caption = u'Video' ++ link.text = unicode(caption) ++ ++ embed = ET.SubElement(span, 'video', ++ {'id': 'embed_%s' % self.embed_id, ++ 'style': 'display: none', ++ 'width': '320', ++ 'controls': ''}) ++ embed.tail = u' ' ++ ET.SubElement(embed, 'source',{ ++ 'src': u'data:%s;base64,%s' % (mime_type, base64.b64encode(data)), ++ 'type': '%s; codecs="vp8 vorbis"' % mime_type}) ++ ++ if 'image/' in mime_type: ++ if not caption: ++ caption = u'Screenshot' ++ link.text = unicode(caption) ++ ++ embed = ET.SubElement(span, 'img', { ++ 'id': 'embed_%s' % self.embed_id, ++ 'style': 'display: none', ++ 'src': u'data:%s;base64,%s' % ( ++ mime_type, base64.b64encode(data))}) ++ embed.tail = u' ' ++ ++ if 'text/' in mime_type: ++ if not caption: ++ caption = u'Data' ++ link.text = unicode(caption) ++ ++ cleaned_data = ''.join( ++ c for c in data if _valid_XML_char_ordinal(ord(c)) ++ ) ++ ++ embed = ET.SubElement(span, 'pre', ++ {'id': "embed_%s" % self.embed_id, ++ 'style': 'display: none'}) ++ embed.text = cleaned_data ++ embed.tail = u' ' ++ ++ def embedding(self, mime_type, data, caption=None): ++ if self.last_step.status == 'untested': ++ # Embed called during step execution ++ self.embed_in_this_step = True ++ self.embed_mime_type = mime_type ++ self.embed_data = data ++ self.embed_caption = caption ++ else: ++ # Embed called in after_* ++ self._doEmbed(self.last_step_embed_span, mime_type, data, caption) ++ ++ def close(self): ++ if not hasattr(self, "all_features"): ++ self.all_features = [] ++ self.duration.text =\ ++ u"Finished in %0.1f seconds" %\ ++ sum([x.duration for x in self.all_features]) ++ ++ # Filling in summary details ++ result = [] ++ statuses = [x.status for x in self.all_features] ++ status_counter = Counter(statuses) ++ for k in status_counter: ++ result.append('%s: %s' % (k, status_counter[k])) ++ self.current_feature_totals.text = u'Features: %s' % ', '.join(result) ++ ++ result = [] ++ scenarios_list = [x.scenarios for x in self.all_features] ++ scenarios = [] ++ if len(scenarios_list) > 0: ++ scenarios = [x for subl in scenarios_list for x in subl] ++ statuses = [x.status for x in scenarios] ++ status_counter = Counter(statuses) ++ for k in status_counter: ++ result.append('%s: %s' % (k, status_counter[k])) ++ self.scenario_totals.text = u'Scenarios: %s' % ', '.join(result) ++ ++ result = [] ++ step_list = [x.steps for x in scenarios] ++ steps = [] ++ if step_list: ++ steps = [x for subl in step_list for x in subl] ++ statuses = [x.status for x in steps] ++ status_counter = Counter(statuses) ++ for k in status_counter: ++ result.append('%s: %s' % (k, status_counter[k])) ++ self.step_totals.text = u'Steps: %s' % ', '.join(result) ++ ++ # Sending the report to stream ++ if len(self.all_features) > 0: ++ self.stream.write(u"\n") ++ self.stream.write(ET_tostring(self.html, pretty_print=True)) +diff --git a/behave/runner.py b/behave/runner.py +index 8e457c6..56ea77f 100644 --- a/behave/runner.py +++ b/behave/runner.py @@ -254,6 +254,11 @@ class Context(object): return True return False -+ def embed(self, mime_type, data): ++ def embed(self, mime_type, data, caption=None): + for formatter in self._runner.formatters: + if hasattr(formatter, 'embedding'): -+ formatter.embedding(mime_type, data) ++ formatter.embedding(mime_type, data, caption) + def execute_steps(self, steps_text): '''The steps identified in the "steps" text string will be parsed and executed in turn just as though they were defined in a feature file. +diff --git a/features/formatter.help.feature b/features/formatter.help.feature +index 48f1a02..77de2fe 100644 --- a/features/formatter.help.feature +++ b/features/formatter.help.feature @@ -11,6 +11,7 @@ Feature: Help Formatter @@ -762,9 +1033,12 @@ json JSON dump of test run json.pretty JSON dump of test run (human readable) null Provides formatter that does not output anything. +diff --git a/features/formatter.html.feature b/features/formatter.html.feature +new file mode 100644 +index 0000000..9b1bd95 --- /dev/null +++ b/features/formatter.html.feature -@@ -0,0 +1,822 @@ +@@ -0,0 +1,749 @@ +@sequential +Feature: HTML Formatter + @@ -867,7 +1141,7 @@ + + """ + -+ Scenario: Use HTML formatter on feature on one empty scenario ++ Scenario: Use HTML formatter on feature with one empty scenario + Given a file named "features/feature_one_empty_scenario.feature" with: + """ + Feature: @@ -888,14 +1162,14 @@ + +
+ features/feature_one_empty_scenario.feature:2 -+

++

+ Scenario: Simple scenario without steps +

+
    +
+ """ + -+ Scenario: Use HTML formatter on feature on one empty scenario with description ++ Scenario: Use HTML formatter on feature with one empty scenario and description + Given a file named "features/feature_one_empty_scenario_with_description.feature" with: + """ + Feature: @@ -920,7 +1194,7 @@ + +
+ features/feature_one_empty_scenario_with_description.feature:2 -+

++

+ Scenario: Simple scenario with description but without steps +

+
First scenario description line.
@@ -930,7 +1204,7 @@
 +            
+ """ + -+ Scenario: Use HTML formatter on feature on one empty scenario with tags ++ Scenario: Use HTML formatter on feature with one empty scenario and tags + Given a file named "features/feature_one_empty_scenario_with_tags.feature" with: + """ + Feature: @@ -953,14 +1227,14 @@ +
+ features/feature_one_empty_scenario_with_tags.feature:3 + @foo, @bar -+

++

+ Scenario: Simple scenario with tags but without steps +

+
    +
+ """ + -+ Scenario: Use HTML formatter on feature on one passing scenario ++ Scenario: Use HTML formatter on feature with one passing scenario + Given a file named "features/feature_one_passing_scenario.feature" with: + """ + Feature: @@ -986,7 +1260,7 @@ + +
+ features/feature_one_passing_scenario.feature:2 -+

++

+ Scenario: Simple scenario with passing steps +

+
    @@ -1044,7 +1318,7 @@ +
+ """ + -+ Scenario: Use HTML formatter on feature on one failing scenario ++ Scenario: Use HTML formatter on feature with one failing scenario + Given a file named "features/feature_one_failing_scenario.feature" with: + """ + Feature: @@ -1070,7 +1344,7 @@ + +
+ features/feature_one_failing_scenario.feature:2 -+

++

+ Scenario: Simple scenario with failing step +

+
    @@ -1114,7 +1388,19 @@ +
+ + -+
  • But a step fails
    features/steps/steps.py:7
    Error message
  • ++
  • ++
    ++ But ++ a step fails ++
    ++
    ++ features/steps/steps.py:7 ++
    ++ ++ Error message ++ ++ ++
  • + + + """ @@ -1145,7 +1431,7 @@ + +
    + features/feature_one_failing_scenario_with_skipped_steps.feature:2 -+

    ++

    + Scenario: Simple scenario with failing and skipped steps +

    +
      @@ -1159,7 +1445,18 @@ +
    + + -+
  • When a step fails
    features/steps/steps.py:7
    Error message
  • ++
  • ++
    ++ When ++ a step fails ++
    ++
    ++ features/steps/steps.py:7 ++
    ++ ++ Error message ++ ++
  • + + + """ @@ -1167,25 +1464,19 @@ + Scenario: Use HTML formatter on feature with three scenarios + Given a file named "features/feature_three_scenarios.feature" with: + """ -+ Feature: -+ Scenario: Simple passing scenario ++ Feature: Many Scenarios ++ Scenario: Passing + Given a step passes -+ When a step passes + Then a step passes -+ And a step passes -+ But a step passes -+ Scenario: Simple failing scenario ++ ++ Scenario: Failing + Given a step passes -+ When a step passes -+ Then a step passes -+ And a step passes -+ But a step fails -+ Scenario: Simple failing scenario with skipped steps ++ Then a step fails ++ ++ Scenario: Failing with skipped steps + Given a step passes -+ When a step passes ++ When a step fails + Then a step passes -+ And a step passes -+ But a step fails + """ + When I run "behave -f html features/feature_three_scenarios.feature" + Then it should fail with: @@ -1197,8 +1488,8 @@ + """ +
    + features/feature_three_scenarios.feature:2 -+

    -+ Scenario: Simple passing scenario ++

    ++ Scenario: Passing +

    +
      +
    1. @@ -1213,16 +1504,6 @@ +
    2. +
    3. +
      -+ When -+ a step passes -+
      -+
      -+ features/steps/steps.py:3 -+
      -+ -+
    4. -+
    5. -+
      + Then + a step passes +
      @@ -1231,32 +1512,12 @@ +
    + + -+
  • -+
    -+ And -+ a step passes -+
    -+
    -+ features/steps/steps.py:3 -+
    -+ -+
  • -+
  • -+
    -+ But -+ a step passes -+
    -+
    -+ features/steps/steps.py:3 -+
    -+ -+
  • + + +
    -+ features/feature_three_scenarios.feature:8 -+

    -+ Scenario: Simple failing scenario ++ features/feature_three_scenarios.feature:6 ++

    ++ Scenario: Failing +

    +
      +
    1. @@ -1269,43 +1530,25 @@ +
    + + -+
  • -+
    -+ When -+ a step passes -+
    -+
    -+ features/steps/steps.py:3 -+
    -+ -+
  • -+
  • ++
  • +
    + Then -+ a step passes -+
    -+
    -+ features/steps/steps.py:3 -+
    -+ -+
  • -+
  • -+
    -+ And -+ a step passes ++ a step fails +
    +
    -+ features/steps/steps.py:3 ++ features/steps/steps.py:7 +
    + ++ Error message ++ ++ +
  • -+
  • But a step fails
    features/steps/steps.py:7
    Error message
  • + + +
    -+ features/feature_three_scenarios.feature:14 -+

    -+ Scenario: Simple failing scenario with skipped steps ++ features/feature_three_scenarios.feature:10 ++

    ++ Scenario: Failing with skipped steps +

    +
      +
    1. @@ -1318,37 +1561,19 @@ +
    + + -+
  • ++
  • +
    + When -+ a step passes -+
    -+
    -+ features/steps/steps.py:3 -+
    -+ -+
  • -+
  • -+
    -+ Then -+ a step passes ++ a step fails +
    +
    -+ features/steps/steps.py:3 -+
    -+ -+
  • -+
  • -+
    -+ And -+ a step passes -+
    -+
    -+ features/steps/steps.py:3 ++ features/steps/steps.py:7 +
    + ++ Error message ++ ++ +
  • -+
  • But a step fails
    features/steps/steps.py:7
    Error message
  • + + + """ @@ -1370,11 +1595,11 @@ + """ + And the command output should contain: + """ -+
    ++
    + features/feature_step_with_one_parameter.feature:2 -+

    -+ Scenario: Simple scenario with one parameter in step -+

    ++

    ++ Scenario: Simple scenario with one parameter in step ++

    +
      +
    1. +
      @@ -1389,7 +1614,11 @@ +
    2. +
      + When -+ a step with parameter "foo" passes ++ ++ a step with parameter " ++ foo ++ " passes ++ +
      +
      + features/steps/steps.py:11 @@ -1404,7 +1633,7 @@ +
      + features/steps/steps.py:3 +
      -+ ++ +
    3. +
    +
    @@ -1429,7 +1658,7 @@ + """ +
    + features/feature_step_with_parameters.feature:2 -+

    ++

    + Scenario: Simple scenario with parameters in step +

    +
      @@ -1446,7 +1675,13 @@ +
    1. +
      + When -+ a step with parameter "foo" and parameter "bar" passes ++ ++ a step with parameter " ++ foo ++ " and parameter " ++ bar ++ " passes ++ +
      +
      + features/steps/steps.py:15 @@ -1472,14 +1707,15 @@ + """ + Feature: + Scenario: Simple scenario with multiline string in step -+ Given a step passes -+ When a step passes: ++ Given a step passes ++ When a step passes: + ''' -+ Tiger, tiger, burning bright -+ In the forests of the night, -+ What immortal hand or eye -+ Could frame thy fearful symmetry? ++ Tiger, tiger, burning bright ++ In the forests of the night, ++ What immortal hand or eye ++ Could frame thy fearful symmetry? + ''' ++ Then a step passes + """ + When I run "behave -f html features/feature_multiline_step.feature" + Then it should pass with: @@ -1489,52 +1725,35 @@ + """ + And the command output should contain: + """ -+
      -+ features/feature_multiline_step.feature:2 -+

      -+ Scenario: Simple scenario with multiline string in step -+

      -+
        -+
      1. -+
        -+ Given -+ a step passes -+
        -+
        -+ features/steps/steps.py:3 -+
        -+ -+
      2. -+
      3. -+
        -+ When -+ a step passes -+
        -+
        -+ features/steps/steps.py:3 -+
        -+ -+
        -+
          Tiger, tiger, burning bright
        -+                    In the forests of the night,
        -+                    What immortal hand or eye
        -+                    Could frame thy fearful symmetry?
        -+
        -+
      4. -+
      -+
      ++
    2. ++
      ++ When ++ a step passes ++
      ++
      ++ features/steps/steps.py:3 ++
      ++ ++
      ++
      Tiger, tiger, burning bright
      ++                In the forests of the night,
      ++                What immortal hand or eye
      ++                Could frame thy fearful symmetry?
      ++
      ++
    3. + """ + + Scenario: Use HTML formatter on step with table + Given a file named "features/feature_step_with_table.feature" with: + """ -+ Feature: -+ Scenario: Simple scenario with failing and skipped steps -+ Given a step passes -+ When a step passes: -+ | Field | Value | -+ | Foo | bar | -+ | baz | qux | ++ Feature: Step with table data ++ Scenario: ++ Given a step passes ++ When a step passes: ++ | Name | Value | ++ | Foo | 42 | ++ | Bar | qux | ++ Then a step passes + """ + When I run "behave -f html features/feature_step_with_table.feature" + Then it should pass with: @@ -1544,57 +1763,56 @@ + """ + And the command output should contain: + """ -+
      -+ features/feature_step_with_table.feature:2 -+

      -+ Scenario: Simple scenario with failing and skipped steps -+

      -+
        -+
      1. -+
        -+ Given -+ a step passes -+
        -+
        -+ features/steps/steps.py:3 -+
        -+ -+
      2. -+
      3. -+
        -+ When -+ a step passes -+
        -+
        -+ features/steps/steps.py:3 -+
        -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+
        FieldValue
        Foobar
        bazqux
        -+
      4. -+
      -+
      -+ """ ---- a/setup.py -+++ b/setup.py -@@ -73,6 +73,8 @@ setup( - "behave_test = setuptools_behave:behave_test" - ] - }, -+ package_data={'': ['report.css']}, -+ include_package_data=True, - install_requires=requirements, - test_suite="nose.collector", - tests_require=["nose>=1.3", "mock>=1.0", "PyHamcrest>=1.8"], ++
    4. ++
      ++ When ++ a step passes ++
      ++
      ++ features/steps/steps.py:3 ++
      ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++
      NameValue
      Foo42
      Barqux
      ++
    5. ++ """ +diff --git a/issue.features/issue0031.feature b/issue.features/issue0031.feature +index 8f1b493..a0abb1c 100644 +--- a/issue.features/issue0031.feature ++++ b/issue.features/issue0031.feature +@@ -8,9 +8,20 @@ Feature: Issue #31 "behave --format help" raises an error + And the command output should contain: + """ + Available formatters: ++ html Very basic HTML formatter + json JSON dump of test run + json.pretty JSON dump of test run (human readable) + null Provides formatter that does not output anything. + plain Very basic formatter with maximum compatibility + pretty Standard colourised pretty formatter ++ progress Shows dotted progress for each executed scenario. ++ progress2 Shows dotted progress for each executed step. ++ progress3 Shows detailed progress for each step of a scenario. ++ rerun Emits scenario file locations of failing scenarios ++ sphinx.steps Generate sphinx-based documentation for step definitions. ++ steps Shows step definitions (step implementations). ++ steps.doc Shows documentation for step definitions. ++ steps.usage Shows how step definitions are used by steps. ++ tags Shows tags (and how often they are used). ++ tags.location Shows tags and the location where they are used. + """ +-- +1.8.3.1 + diff --git a/html-formatter-strip-incorrect-chars-from-error-mess.patch b/html-formatter-strip-incorrect-chars-from-error-mess.patch deleted file mode 100644 index e71fc1e..0000000 --- a/html-formatter-strip-incorrect-chars-from-error-mess.patch +++ /dev/null @@ -1,55 +0,0 @@ -From f5ffd5e581af27999c475cb3f74d69981cc758c0 Mon Sep 17 00:00:00 2001 -From: Vadim Rutkovsky -Date: Thu, 20 Feb 2014 12:06:25 +0100 -Subject: [PATCH] html formatter: strip incorrect chars from error message - ---- - behave/formatter/html.py | 20 ++++++++++++-------- - 1 file changed, 12 insertions(+), 8 deletions(-) - ---- a/behave/formatter/html.py -+++ b/behave/formatter/html.py -@@ -4,6 +4,14 @@ import base64 - import os.path - from behave.compat.collections import Counter - -+def _valid_XML_char_ordinal(i): -+ return ( # conditions ordered by presumed frequency -+ 0x20 <= i <= 0xD7FF -+ or i in (0x9, 0xA, 0xD) -+ or 0xE000 <= i <= 0xFFFD -+ or 0x10000 <= i <= 0x10FFFF -+ ) -+ - - class HTMLFormatter(Formatter): - name = 'html' -@@ -200,7 +208,10 @@ class HTMLFormatter(Formatter): - embed = ET.SubElement(step, 'pre', - {'id': "embed_%s" % self.embed_id, - 'style': 'display: none; white-space: pre-wrap;'}) -- embed.text = result.error_message -+ cleaned_error_message = ''.join( -+ c for c in result.error_message if _valid_XML_char_ordinal(ord(c)) -+ ) -+ embed.text = cleaned_error_message - embed.tail = u' ' - - if result.status == 'failed': -@@ -242,15 +253,8 @@ class HTMLFormatter(Formatter): - if 'text/' in mime_type: - link.text = u'Data' - -- def valid_XML_char_ordinal(i): -- return ( # conditions ordered by presumed frequency -- 0x20 <= i <= 0xD7FF -- or i in (0x9, 0xA, 0xD) -- or 0xE000 <= i <= 0xFFFD -- or 0x10000 <= i <= 0x10FFFF -- ) - cleaned_data = ''.join( -- c for c in data if valid_XML_char_ordinal(ord(c)) -+ c for c in data if _valid_XML_char_ordinal(ord(c)) - ) - - embed = ET.SubElement(span, 'pre', diff --git a/python-behave.spec b/python-behave.spec index 40ed01c..68da683 100644 --- a/python-behave.spec +++ b/python-behave.spec @@ -11,8 +11,8 @@ %global modname behave Name: python-%{modname} -Version: 1.2.4 -Release: 4%{?dist} +Version: 1.2.5 +Release: 1%{?dist} Summary: Tools for the behavior-driven development, Python style License: BSD @@ -21,16 +21,6 @@ Source0: http://pypi.python.org/packages/source/b/%{modname}/%{modnam # Pending pull request in the upstream repository # https://github.com/behave/behave/pull/86 Patch0: HTML-Formatter.patch -# Fix for RHBZ# 1067388 -Patch1: html-formatter-strip-incorrect-chars-from-error-mess.patch -# Fix for RHBZ# 1058371 -Patch2: Embedding-support-link-caption-and-video-tags.patch -# Fix for RHBZ# -Patch3: Don-t-crash-on-invalid-XML-chars-in-embed.patch -# Fix for unnecessary relpath compatibility library -Patch4: Fix-relpath-imports.patch -# Fix for https://github.com/behave/behave/issues/251 -Patch5: yet-another-unicode-error.patch BuildArch: noarch Requires: python-setuptools @@ -87,11 +77,6 @@ brief feature-examples. %prep %setup -q -n %{modname}-%{version} %patch0 -p1 -b .HTMLformatter -%patch1 -p1 -b .HTMLformatterFixUTF8 -%patch2 -p1 -b .EmbeddedVideo -%patch3 -p1 -b .NoCrash -%patch4 -p1 -b .relpath -%patch5 -p1 -b .UnicodeError # Remove bundled egg-info in case it exists rm -rf %{modname}*.egg-info diff --git a/sources b/sources index a7e9917..99de1d5 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -13c21668c1434f67941955ffe9e12d26 behave-1.2.4.tar.gz +918a1dffbca87b4baa097335d6c2d20e behave-1.2.5.tar.gz diff --git a/yet-another-unicode-error.patch b/yet-another-unicode-error.patch deleted file mode 100644 index 22caae8..0000000 --- a/yet-another-unicode-error.patch +++ /dev/null @@ -1,49 +0,0 @@ -From 456305ab21d7307a7d9742e332caffae4109dc8e Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Mat=C4=9Bj=20Cepl?= -Date: Mon, 15 Sep 2014 15:30:18 +0200 -Subject: [PATCH] Fix Unicode-related crash in model.py -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -Fixes #251 - -Signed-off-by: Matěj Cepl ---- - behave/model.py | 15 +++++++++------ - 1 file changed, 9 insertions(+), 6 deletions(-) - -diff --git a/behave/model.py b/behave/model.py -index 51f9b56..3a8c646 100644 ---- a/behave/model.py -+++ b/behave/model.py -@@ -1432,17 +1432,20 @@ class Step(BasicStatement, Replayable): - if capture: - # -- CAPTURE-ONLY: Non-nested step failures. - if runner.config.stdout_capture: -- output = runner.stdout_capture.getvalue() -+ output = unicode(runner.stdout_capture.getvalue(), -+ 'unicode-escape') - if output: -- error += '\nCaptured stdout:\n' + output -+ error += u'\nCaptured stdout:\n' + output - if runner.config.stderr_capture: -- output = runner.stderr_capture.getvalue() -+ output = unicode(runner.stderr_capture.getvalue(), -+ 'unicode-escape') - if output: -- error += '\nCaptured stderr:\n' + output -+ error += u'\nCaptured stderr:\n' + output - if runner.config.log_capture: -- output = runner.log_capture.getvalue() -+ output = unicode(runner.log_capture.getvalue(), -+ 'unicode-escape') - if output: -- error += '\nCaptured logging:\n' + output -+ error += u'\nCaptured logging:\n' + output - self.error_message = error - keep_going = False - --- -1.8.3.1 -