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 "</span>
++ <b>foo</b>
++ <span>" 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 "</span>
++ <b>foo</b>
++ <span>" and parameter "</span>
++ <b>bar</b>
++ <span>" 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