mcepl pushed to python-behave (master). "Update to behave 1.2.5"

notifications at fedoraproject.org notifications at fedoraproject.org
Wed Apr 29 09:56:54 UTC 2015


>From 6b2c4c1f9e1f3f795d046d4fb4ca3d2d762a0aef Mon Sep 17 00:00:00 2001
From: Vadim Rutkovsky <vrutkovs at redhat.com>
Date: Wed, 29 Apr 2015 11:16:30 +0200
Subject: Update to behave 1.2.5


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 <vrutkovs at redhat.com>
-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 <vrutkovs at redhat.com>
-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 <vrutkovs at redhat.com>
+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 = "<unknown>"
-+
-+    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 <a> instead of <span> 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+</', re.DOTALL)
++        text = text_re.sub(r'>\g<1></', text)
++    return text
++
++class JavascriptLibrary(object):
++    collapsible = """
++function Collapsible_toggle(id)
++{
++    var elem = document.getElementById(id);
++    elem.style.display = (elem.style.display == 'none' ? 'block' : 'none');
++    return false;
++}
++
++function Collapsible_expandAll(className)
++{
++    var elems = document.getElementsByClassName(className);
++    for (var i=0; i < elems.length; i++) {
++        elems[i].style.display = 'block';
++    }
++}
++
++function Collapsible_collapseAll(className)
++{
++    var elems = document.getElementsByClassName(className);
++    for (var i=0; i < elems.length; i++) {
++        elems[i].style.display = 'none';
++    }
++}
++
++function Collapsible_expandAllFailed()
++{
++    var elems = document.getElementsByClassName('failed');
++    for (var i=0; i < elems.length; i++) {
++        var elem = elems[i];
++        if (elem.nodeName == 'H3'){
++            elem.parentElement.getElementsByTagName('ol')[0].style.display = 'block';
++        }
++    }
++}
++"""
++
++
++class BasicTheme(object):
++    stylesheet_text = """
++body{font-size:0;color:#fff;margin:0;
++padding:0}.behave,td,th{font:400 11px "Lucida Grande",Helvetica,sans-serif;
++background:#fff;color:#000}.behave #behave-header,td #behave-header,
++th #behave-header{background:#65c400;color:#fff;height:8em}.behave
++#behave-header #expand-collapse p,td #behave-header #expand-collapse
++p,th #behave-header #expand-collapse p{float:right;margin:0 0 0 10px}
++.background h3,.behave .scenario h3,td .scenario h3,th .scenario h3{
++font-size:11px;padding:3px;margin:0;background:#65c400;color:#fff;
++font-weight:700}.background h3{font-size:1.2em;background:#666}.behave
++h1,td h1,th h1{margin:0 10px;padding:10px;font-family:'Lucida Grande',
++Helvetica,sans-serif;font-size:2em;position:absolute}.behave h4,td h4,
++th h4{margin-bottom:2px}.behave div.feature,td div.feature,th div.feature
++{padding:2px;margin:0 10px 5px}.behave div.examples,td div.examples,th
++div.examples{padding:0 0 0 1em}.behave .stats,td .stats,th .stats{margin:2em}
++.behave .summary ul.features li,td .summary ul.features li,th .summary
++ul.features li{display:inline}.behave .step_name,td .step_name,th .step_name
++{float:left}.behave .step_file,td .step_file,th .step_file{text-align:right;
++color:#999}.behave .step_file a,td .step_file a,th .step_file a{color:#999}.behave
++.scenario_file,td .scenario_file,th .scenario_file{float:right;color:#999}.behave
++.tag,td .tag,th .tag{font-weight:700;color:#246ac1}.behave .backtrace,td
++.backtrace,th .backtrace{margin-top:0;margin-bottom:0;margin-left:1em;color:#000}
++.behave a,td a,th a{text-decoration:none;color:#be5c00}.behave a:hover,
++td a:hover,th a:hover{text-decoration:underline}.behave a:visited,td a:visited,
++th a:visited{font-weight:400}.behave a div.examples,td a div.examples,
++th a div.examples{margin:5px 0 5px 15px;color:#000}.behave .outline table,
++td .outline table,th .outline table{margin:0 0 5px 10px}.behave table,
++td table,th table{border-collapse:collapse}.behave table td,td table td,
++th table td{padding:3px 3px 3px 5px}.behave table td.failed,td table td.failed,
++th table td.failed{border-left:5px solid #c20000;border-bottom:1px solid
++#c20000;background:#fffbd3;color:#c20000}.behave table td.passed,td table
++td.passed,th table td.passed{border-left:5px solid #65c400;border-bottom:1px
++solid #65c400;background:#dbffb4;color:#3d7700}.behave table td.skipped,td
++table td.skipped,th table td.skipped{border-left:5px solid #0ff;border-bottom:1px
++solid #0ff;background:#e0ffff;color:#011}.behave table td.pending,.behave table
++td.undefined,td table td.pending,td table td.undefined,th table td.pending,th table
++td.undefined{border-left:5px solid #faf834;border-bottom:1px solid #faf834;
++background:#fcfb98;color:#131313}.behave table td.message,td table td.message,th
++table td.message{border-left:5px solid #0ff;border-bottom:1px solid #0ff;
++background:#e0ffff;color:#011}.behave ol,td ol,th ol{list-style:none;
++margin:0;padding:0}.behave ol li.step,td ol li.step,th ol li.step{
++padding:3px 3px 3px 18px;margin:5px 0 5px 5px}.behave ol li,td ol li,th
++ol li{margin:0 0 0 1em;padding:0 0 0 .2em}.behave ol li span.param,td
++ol li span.param,th ol li span.param{font-weight:700}.behave ol li.failed,td
++ol li.failed,th ol li.failed{border-left:5px solid #c20000;border-bottom:1px
++solid #c20000;background:#fffbd3;color:#c20000}.behave ol li.passed,td ol
++li.passed,th ol li.passed{border-left:5px solid #65c400;border-bottom:1px
++solid #65c400;background:#dbffb4;color:#3d7700}.behave ol li.skipped,td ol
++li.skipped,th ol li.skipped{border-left:5px solid #0ff;border-bottom:1px
++solid #0ff;background:#e0ffff;color:#011}.behave ol li.pending,.behave ol
++li.undefined,td ol li.pending,td ol li.undefined,th ol li.pending,th ol
++li.undefined{border-left:5px solid #faf834;border-bottom:1px solid
++#faf834;background:#fcfb98;color:#131313}.behave ol li.message,td ol
++li.message,th ol li.message{border-left:5px solid #0ff;border-bottom:1px
++solid #0ff;background:#e0ffff;color:#011;margin-left:10px}.behave #summary,td
++#summary,th #summary{margin:0;padding:5px 10px;text-align:right;top:0;
++right:0;float:right}.behave #summary p,td #summary p,th #summary
++p{margin:0 0 0 2px}.behave #summary #totals,td #summary #totals,th
++#summary #totals{font-size:1.2em} h3.failed,#behave-header.failed{background:
++#c40d0d !important} h3.undefined,#behave-header.undefined{background:#faf834
++ !important; color:#000 !important} #behave-header.failed a{color:#fff} pre {
++ white-space: pre-wrap}
++"""
++
++
++class Page(object):
++    """
++    Provides a HTML page construct (as technological layer).
++    XXX
++    """
++    theme = BasicTheme
++
++    def __init__(self, title=None):
++        pass
++
++
++class HTMLFormatter(Formatter):
++    """Provides a single-page HTML formatter
++    that writes the result of a  test run.
++    """
++    name = 'html'
++    description = 'Very basic HTML formatter'
++    title = u"Behave Test Report"
++
++    def __init__(self, stream, config):
++        super(HTMLFormatter, self).__init__(stream, config)
++
++        # -- XXX-JE-PREPARED-BUT-DISABLED:
++        # XXX Seldom changed value.
++        # XXX Should only be in configuration-file in own section "behave.formatter.html" ?!?
++        # XXX Config support must be provided.
++        # XXX REASON: Don't clutter behave config-space w/ formatter/plugin related config data.
++        # self.css = self.default_css
++        # if config.css is not None:
++        #    self.css = config.css
++        self.html = ET.Element('html')
++        head = ET.SubElement(self.html, 'head')
++        ET.SubElement(head, 'title').text = self.title
++        ET.SubElement(head, 'meta', {'content': 'text/html;charset=utf-8'})
++        style = ET.SubElement(head, 'style', type=u"text/css")
++        style.append(ET.Comment(Page.theme.stylesheet_text))
++        script = ET.SubElement(head, 'script', type=u"text/javascript")
++        script_text = ET.Comment(JavascriptLibrary.collapsible)
++        script.append(script_text)
++
++        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 = self.title
++
++        summary = ET.SubElement(self.header, 'div', id='summary')
++
++        totals = ET.SubElement(summary, 'p', id='totals')
++
++        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')
++
++        # -- PART: Expand/Collapse All
++        expand_collapse = ET.SubElement(summary, 'div', id='expand-collapse')
++        expander = ET.SubElement(expand_collapse, 'a', id='expander', href="#")
++        expander.set('onclick', "Collapsible_expandAll('scenario_steps')")
++        expander.text = u'Expand All'
++        cea_spacer = ET.SubElement(expand_collapse, 'span')
++        cea_spacer.text = u" | "
++        collapser = ET.SubElement(expand_collapse, 'a', id='collapser', href="#")
++        collapser.set('onclick', "Collapsible_collapseAll('scenario_steps')")
++        collapser.text = u'Collapse All'
++        cea_spacer = ET.SubElement(expand_collapse, 'span')
++        cea_spacer.text = u" | "
++        expander = ET.SubElement(expand_collapse, 'a', id='failed_expander', href="#")
++        expander.set('onclick', "Collapsible_expandAllFailed()")
++        expander.text = u'Expand All Failed'
++
++
++        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'@' + ', @'.join(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 = '\n'.join(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'@' + ', @'.join(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 = '\n'.join(scenario.description)
++
++        self.steps = ET.SubElement(self.scenario_el, 'ol',
++                                   {'class': 'scenario_steps',
++                                    'id': 'scenario_%s' % self.scenario_id})
++
++        self.scenario_name.set('onclick',
++                "Collapsible_toggle('scenario_%s')" % self.scenario_id)
++        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 = "<unknown>"
++
++    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"<!DOCTYPE HTML>\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 @@
 + at sequential
 +Feature: HTML Formatter
 +
@@ -867,7 +1141,7 @@
 +            </div>
 +            """
 +
-+    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 @@
 +            </div>
 +            <div class="scenario">
 +              <span class="scenario_file">features/feature_one_empty_scenario.feature:2</span>
-+              <h3 onclick="ol=document.getElementById('scenario_0');ol.style.display =(ol.style.display == 'none' ? 'block' : 'none');return false">
++              <h3 onclick="Collapsible_toggle('scenario_0')">
 +                <span class="val">Scenario: Simple scenario without steps</span>
 +              </h3>
 +              <ol class="scenario_steps" id="scenario_0"/>
 +            </div>
 +            """
 +
-+    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 @@
 +            </div>
 +            <div class="scenario">
 +              <span class="scenario_file">features/feature_one_empty_scenario_with_description.feature:2</span>
-+              <h3 onclick="ol=document.getElementById('scenario_0');ol.style.display =(ol.style.display == 'none' ? 'block' : 'none');return false">
++              <h3 onclick="Collapsible_toggle('scenario_0')">
 +                <span class="val">Scenario: Simple scenario with description but without steps</span>
 +              </h3>
 +              <pre class="message">First scenario description line.
@@ -930,7 +1204,7 @@
 +            </div>
 +            """
 +
-+    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 @@
 +            <div class="scenario">
 +              <span class="scenario_file">features/feature_one_empty_scenario_with_tags.feature:3</span>
 +              <span class="tag">@foo, @bar</span>
-+              <h3 onclick="ol=document.getElementById('scenario_0');ol.style.display =(ol.style.display == 'none' ? 'block' : 'none');return false">
++              <h3 onclick="Collapsible_toggle('scenario_0')">
 +                <span class="val">Scenario: Simple scenario with tags but without steps</span>
 +              </h3>
 +              <ol class="scenario_steps" id="scenario_0"/>
 +            </div>
 +            """
 +
-+    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 @@
 +            </div>
 +            <div class="scenario">
 +              <span class="scenario_file">features/feature_one_passing_scenario.feature:2</span>
-+              <h3 onclick="ol=document.getElementById('scenario_0');ol.style.display =(ol.style.display == 'none' ? 'block' : 'none');return false">
++              <h3 onclick="Collapsible_toggle('scenario_0')">
 +                <span class="val">Scenario: Simple scenario with passing steps</span>
 +              </h3>
 +              <ol class="scenario_steps" id="scenario_0">
@@ -1044,7 +1318,7 @@
 +            </div>
 +            """
 +
-+    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 @@
 +            </div>
 +            <div class="scenario">
 +              <span class="scenario_file">features/feature_one_failing_scenario.feature:2</span>
-+              <h3 onclick="ol=document.getElementById('scenario_0');ol.style.display =(ol.style.display == 'none' ? 'block' : 'none');return false" style="background: #C40D0D; color: #FFFFFF">
++              <h3 class="failed" onclick="Collapsible_toggle('scenario_0')">
 +                <span class="val">Scenario: Simple scenario with failing step</span>
 +              </h3>
 +              <ol class="scenario_steps" id="scenario_0">
@@ -1114,7 +1388,19 @@
 +                  </div>
 +                  <span class="embed"/>
 +                </li>
-+                <li class="step failed"><div class="step_name"><span class="keyword">But </span><span class="step val">a step fails</span></div><div class="step_file"><span>features/steps/steps.py:7</span></div><span class="embed"/><a class="message" onclick="rslt=document.getElementById('embed_1');rslt.style.display =(rslt.style.display == 'none' ? 'block' : 'none');return false">Error message</a><pre id="embed_1" style="display: none; white-space: pre-wrap;">Assertion Failed: XFAIL-STEP</pre>    </li>
++                <li class="step failed">
++                  <div class="step_name">
++                    <span class="keyword">But </span>
++                    <span class="step val">a step fails</span>
++                  </div>
++                  <div class="step_file">
++                    <span>features/steps/steps.py:7</span>
++                  </div>
++                  <span class="embed"/>
++                  <a class="message" onclick="Collapsible_toggle('embed_1')">Error message</a>
++                  <pre id="embed_1" style="display: none">Assertion Failed: XFAIL-STEP</pre>
++
++                </li>
 +              </ol>
 +            </div>
 +            """
@@ -1145,7 +1431,7 @@
 +            </div>
 +            <div class="scenario">
 +              <span class="scenario_file">features/feature_one_failing_scenario_with_skipped_steps.feature:2</span>
-+              <h3 onclick="ol=document.getElementById('scenario_0');ol.style.display =(ol.style.display == 'none' ? 'block' : 'none');return false" style="background: #C40D0D; color: #FFFFFF">
++              <h3 class="failed" onclick="Collapsible_toggle('scenario_0')">
 +                <span class="val">Scenario: Simple scenario with failing and skipped steps</span>
 +              </h3>
 +              <ol class="scenario_steps" id="scenario_0">
@@ -1159,7 +1445,18 @@
 +                  </div>
 +                  <span class="embed"/>
 +                </li>
-+                <li class="step failed"><div class="step_name"><span class="keyword">When </span><span class="step val">a step fails</span></div><div class="step_file"><span>features/steps/steps.py:7</span></div><span class="embed"/><a class="message" onclick="rslt=document.getElementById('embed_1');rslt.style.display =(rslt.style.display == 'none' ? 'block' : 'none');return false">Error message</a><pre id="embed_1" style="display: none; white-space: pre-wrap;">Assertion Failed: XFAIL-STEP</pre>    </li>
++                <li class="step failed">
++                  <div class="step_name">
++                    <span class="keyword">When </span>
++                    <span class="step val">a step fails</span>
++                  </div>
++                  <div class="step_file">
++                    <span>features/steps/steps.py:7</span>
++                  </div>
++                  <span class="embed"/>
++                  <a class="message" onclick="Collapsible_toggle('embed_1')">Error message</a>
++                  <pre id="embed_1" style="display: none">Assertion Failed: XFAIL-STEP</pre>
++                </li>
 +              </ol>
 +            </div>
 +            """
@@ -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 @@
 +            """
 +            <div class="scenario">
 +              <span class="scenario_file">features/feature_three_scenarios.feature:2</span>
-+              <h3 onclick="ol=document.getElementById('scenario_0');ol.style.display =(ol.style.display == 'none' ? 'block' : 'none');return false">
-+                <span class="val">Scenario: Simple passing scenario</span>
++              <h3 onclick="Collapsible_toggle('scenario_0')">
++                <span class="val">Scenario: Passing</span>
 +              </h3>
 +              <ol class="scenario_steps" id="scenario_0">
 +                <li class="step passed">
@@ -1213,16 +1504,6 @@
 +                </li>
 +                <li class="step passed">
 +                  <div class="step_name">
-+                    <span class="keyword">When </span>
-+                    <span class="step val">a step passes</span>
-+                  </div>
-+                  <div class="step_file">
-+                    <span>features/steps/steps.py:3</span>
-+                  </div>
-+                  <span class="embed"/>
-+                </li>
-+                <li class="step passed">
-+                  <div class="step_name">
 +                    <span class="keyword">Then </span>
 +                    <span class="step val">a step passes</span>
 +                  </div>
@@ -1231,32 +1512,12 @@
 +                  </div>
 +                  <span class="embed"/>
 +                </li>
-+                <li class="step passed">
-+                  <div class="step_name">
-+                    <span class="keyword">And </span>
-+                    <span class="step val">a step passes</span>
-+                  </div>
-+                  <div class="step_file">
-+                    <span>features/steps/steps.py:3</span>
-+                  </div>
-+                  <span class="embed"/>
-+                </li>
-+                <li class="step passed">
-+                  <div class="step_name">
-+                    <span class="keyword">But </span>
-+                    <span class="step val">a step passes</span>
-+                  </div>
-+                  <div class="step_file">
-+                    <span>features/steps/steps.py:3</span>
-+                  </div>
-+                  <span class="embed"/>
-+                </li>
 +              </ol>
 +            </div>
 +            <div class="scenario">
-+              <span class="scenario_file">features/feature_three_scenarios.feature:8</span>
-+              <h3 onclick="ol=document.getElementById('scenario_1');ol.style.display =(ol.style.display == 'none' ? 'block' : 'none');return false" style="background: #C40D0D; color: #FFFFFF">
-+                <span class="val">Scenario: Simple failing scenario</span>
++              <span class="scenario_file">features/feature_three_scenarios.feature:6</span>
++              <h3 class="failed" onclick="Collapsible_toggle('scenario_1')">
++                <span class="val">Scenario: Failing</span>
 +              </h3>
 +              <ol class="scenario_steps" id="scenario_1">
 +                <li class="step passed">
@@ -1269,43 +1530,25 @@
 +                  </div>
 +                  <span class="embed"/>
 +                </li>
-+                <li class="step passed">
-+                  <div class="step_name">
-+                    <span class="keyword">When </span>
-+                    <span class="step val">a step passes</span>
-+                  </div>
-+                  <div class="step_file">
-+                    <span>features/steps/steps.py:3</span>
-+                  </div>
-+                  <span class="embed"/>
-+                </li>
-+                <li class="step passed">
++                <li class="step failed">
 +                  <div class="step_name">
 +                    <span class="keyword">Then </span>
-+                    <span class="step val">a step passes</span>
-+                  </div>
-+                  <div class="step_file">
-+                    <span>features/steps/steps.py:3</span>
-+                  </div>
-+                  <span class="embed"/>
-+                </li>
-+                <li class="step passed">
-+                  <div class="step_name">
-+                    <span class="keyword">And </span>
-+                    <span class="step val">a step passes</span>
++                    <span class="step val">a step fails</span>
 +                  </div>
 +                  <div class="step_file">
-+                    <span>features/steps/steps.py:3</span>
++                    <span>features/steps/steps.py:7</span>
 +                  </div>
 +                  <span class="embed"/>
++                  <a class="message" onclick="Collapsible_toggle('embed_1')">Error message</a>
++                  <pre id="embed_1" style="display: none">Assertion Failed: XFAIL-STEP</pre>
++
 +                </li>
-+                <li class="step failed"><div class="step_name"><span class="keyword">But </span><span class="step val">a step fails</span></div><div class="step_file"><span>features/steps/steps.py:7</span></div><span class="embed"/><a class="message" onclick="rslt=document.getElementById('embed_1');rslt.style.display =(rslt.style.display == 'none' ? 'block' : 'none');return false">Error message</a><pre id="embed_1" style="display: none; white-space: pre-wrap;">Assertion Failed: XFAIL-STEP</pre>    </li>
 +              </ol>
 +            </div>
 +            <div class="scenario">
-+              <span class="scenario_file">features/feature_three_scenarios.feature:14</span>
-+              <h3 onclick="ol=document.getElementById('scenario_2');ol.style.display =(ol.style.display == 'none' ? 'block' : 'none');return false" style="background: #C40D0D; color: #FFFFFF">
-+                <span class="val">Scenario: Simple failing scenario with skipped steps</span>
++              <span class="scenario_file">features/feature_three_scenarios.feature:10</span>
++              <h3 class="failed" onclick="Collapsible_toggle('scenario_2')">
++                <span class="val">Scenario: Failing with skipped steps</span>
 +              </h3>
 +              <ol class="scenario_steps" id="scenario_2">
 +                <li class="step passed">
@@ -1318,37 +1561,19 @@
 +                  </div>
 +                  <span class="embed"/>
 +                </li>
-+                <li class="step passed">
++                <li class="step failed">
 +                  <div class="step_name">
 +                    <span class="keyword">When </span>
-+                    <span class="step val">a step passes</span>
-+                  </div>
-+                  <div class="step_file">
-+                    <span>features/steps/steps.py:3</span>
-+                  </div>
-+                  <span class="embed"/>
-+                </li>
-+                <li class="step passed">
-+                  <div class="step_name">
-+                    <span class="keyword">Then </span>
-+                    <span class="step val">a step passes</span>
++                    <span class="step val">a step fails</span>
 +                  </div>
 +                  <div class="step_file">
-+                    <span>features/steps/steps.py:3</span>
-+                  </div>
-+                  <span class="embed"/>
-+                </li>
-+                <li class="step passed">
-+                  <div class="step_name">
-+                    <span class="keyword">And </span>
-+                    <span class="step val">a step passes</span>
-+                  </div>
-+                  <div class="step_file">
-+                    <span>features/steps/steps.py:3</span>
++                    <span>features/steps/steps.py:7</span>
 +                  </div>
 +                  <span class="embed"/>
++                  <a class="message" onclick="Collapsible_toggle('embed_2')">Error message</a>
++                  <pre id="embed_2" style="display: none">Assertion Failed: XFAIL-STEP</pre>
++
 +                </li>
-+                <li class="step failed"><div class="step_name"><span class="keyword">But </span><span class="step val">a step fails</span></div><div class="step_file"><span>features/steps/steps.py:7</span></div><span class="embed"/><a class="message" onclick="rslt=document.getElementById('embed_2');rslt.style.display =(rslt.style.display == 'none' ? 'block' : 'none');return false">Error message</a><pre id="embed_2" style="display: none; white-space: pre-wrap;">Assertion Failed: XFAIL-STEP</pre>    </li>
 +              </ol>
 +            </div>
 +            """
@@ -1370,11 +1595,11 @@
 +            """
 +        And the command output should contain:
 +            """
-+                        <div class="scenario">
++            <div class="scenario">
 +              <span class="scenario_file">features/feature_step_with_one_parameter.feature:2</span>
-+              <h3 onclick="ol=document.getElementById('scenario_0');ol.style.display =(ol.style.display == 'none' ? 'block' : 'none');return false">
-+                <span class="val">Scenario: Simple scenario with one parameter in step</span>
-+              </h3>
++                <h3 onclick="Collapsible_toggle('scenario_0')">
++                  <span class="val">Scenario: Simple scenario with one parameter in step</span>
++                </h3>
 +              <ol class="scenario_steps" id="scenario_0">
 +                <li class="step passed">
 +                  <div class="step_name">
@@ -1389,7 +1614,11 @@
 +                <li class="step passed">
 +                  <div class="step_name">
 +                    <span class="keyword">When </span>
-+                    <span class="step val">a step with parameter "<b>foo</b>" passes</span>
++                    <span class="step val">
++                      <span>a step with parameter &quot;</span>
++                      <b>foo</b>
++                      <span>&quot; passes</span>
++                    </span>
 +                  </div>
 +                  <div class="step_file">
 +                    <span>features/steps/steps.py:11</span>
@@ -1404,7 +1633,7 @@
 +                  <div class="step_file">
 +                    <span>features/steps/steps.py:3</span>
 +                  </div>
-+                  <span class="embed"/>
++                    <span class="embed"/>
 +                </li>
 +              </ol>
 +            </div>
@@ -1429,7 +1658,7 @@
 +            """
 +            <div class="scenario">
 +              <span class="scenario_file">features/feature_step_with_parameters.feature:2</span>
-+              <h3 onclick="ol=document.getElementById('scenario_0');ol.style.display =(ol.style.display == 'none' ? 'block' : 'none');return false">
++              <h3 onclick="Collapsible_toggle('scenario_0')">
 +                <span class="val">Scenario: Simple scenario with parameters in step</span>
 +              </h3>
 +              <ol class="scenario_steps" id="scenario_0">
@@ -1446,7 +1675,13 @@
 +                <li class="step passed">
 +                  <div class="step_name">
 +                    <span class="keyword">When </span>
-+                    <span class="step val">a step with parameter "<b>foo</b>" and parameter "<b>bar</b>" passes</span>
++                    <span class="step val">
++                      <span>a step with parameter &quot;</span>
++                      <b>foo</b>
++                      <span>&quot; and parameter &quot;</span>
++                      <b>bar</b>
++                      <span>&quot; passes</span>
++                    </span>
 +                  </div>
 +                  <div class="step_file">
 +                    <span>features/steps/steps.py:15</span>
@@ -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:
 +            """
-+            <div class="scenario">
-+              <span class="scenario_file">features/feature_multiline_step.feature:2</span>
-+              <h3 onclick="ol=document.getElementById('scenario_0');ol.style.display =(ol.style.display == 'none' ? 'block' : 'none');return false">
-+                <span class="val">Scenario: Simple scenario with multiline string in step</span>
-+              </h3>
-+              <ol class="scenario_steps" id="scenario_0">
-+                <li class="step passed">
-+                  <div class="step_name">
-+                    <span class="keyword">Given </span>
-+                    <span class="step val">a step passes</span>
-+                  </div>
-+                  <div class="step_file">
-+                    <span>features/steps/steps.py:3</span>
-+                  </div>
-+                  <span class="embed"/>
-+                </li>
-+                <li class="step passed">
-+                  <div class="step_name">
-+                    <span class="keyword">When </span>
-+                    <span class="step val">a step passes</span>
-+                  </div>
-+                  <div class="step_file">
-+                    <span>features/steps/steps.py:3</span>
-+                  </div>
-+                  <span class="embed"/>
-+                  <div class="message">
-+                    <pre style="white-space: pre-wrap;">  Tiger, tiger, burning bright
-+                    In the forests of the night,
-+                    What immortal hand or eye
-+                    Could frame thy fearful symmetry?</pre>
-+                  </div>
-+                </li>
-+              </ol>
-+            </div>
++            <li class="step passed">
++              <div class="step_name">
++                <span class="keyword">When </span>
++                <span class="step val">a step passes</span>
++              </div>
++              <div class="step_file">
++                <span>features/steps/steps.py:3</span>
++              </div>
++              <span class="embed"/>
++              <div class="message">
++                <pre>Tiger, tiger, burning bright
++                In the forests of the night,
++                What immortal hand or eye
++                Could frame thy fearful symmetry?</pre>
++              </div>
++            </li>
 +            """
 +
 +    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:
 +            """
-+            <div class="scenario">
-+              <span class="scenario_file">features/feature_step_with_table.feature:2</span>
-+              <h3 onclick="ol=document.getElementById('scenario_0');ol.style.display =(ol.style.display == 'none' ? 'block' : 'none');return false">
-+                <span class="val">Scenario: Simple scenario with failing and skipped steps</span>
-+              </h3>
-+              <ol class="scenario_steps" id="scenario_0">
-+                <li class="step passed">
-+                  <div class="step_name">
-+                    <span class="keyword">Given </span>
-+                    <span class="step val">a step passes</span>
-+                  </div>
-+                  <div class="step_file">
-+                    <span>features/steps/steps.py:3</span>
-+                  </div>
-+                  <span class="embed"/>
-+                </li>
-+                <li class="step passed">
-+                  <div class="step_name">
-+                    <span class="keyword">When </span>
-+                    <span class="step val">a step passes</span>
-+                  </div>
-+                  <div class="step_file">
-+                    <span>features/steps/steps.py:3</span>
-+                  </div>
-+                  <span class="embed"/>
-+                  <table>
-+                    <tr>
-+                      <th>Field</th>
-+                      <th>Value</th>
-+                    </tr>
-+                    <tr>
-+                      <td>Foo</td>
-+                      <td>bar</td>
-+                    </tr>
-+                    <tr>
-+                      <td>baz</td>
-+                      <td>qux</td>
-+                    </tr>
-+                  </table>
-+                </li>
-+              </ol>
-+            </div>
-+            """
---- 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"],
++            <li class="step passed">
++              <div class="step_name">
++                <span class="keyword">When </span>
++                <span class="step val">a step passes</span>
++              </div>
++              <div class="step_file">
++                <span>features/steps/steps.py:3</span>
++              </div>
++              <span class="embed"/>
++              <table>
++                <tr>
++                  <th>Name</th>
++                  <th>Value</th>
++                </tr>
++                <tr>
++                  <td>Foo</td>
++                  <td>42</td>
++                </tr>
++                <tr>
++                  <td>Bar</td>
++                  <td>qux</td>
++                </tr>
++              </table>
++            </li>
++            """
+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 <vrutkovs at redhat.com>
-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?= <mcepl at redhat.com>
-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 <mcepl at redhat.com>
----
- 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
-
-- 
cgit v0.10.2


	http://pkgs.fedoraproject.org/cgit/python-behave.git/commit/?h=master&id=6b2c4c1f9e1f3f795d046d4fb4ca3d2d762a0aef


More information about the scm-commits mailing list