#3 Backport fixes for python 3.8 compatibility
Merged a month ago by churchyard. Opened a month ago by pschindl.
rpms/ pschindl/python-behave devel/bug1706085  into  master

@@ -0,0 +1,573 @@ 

+ From 72257e2db209f60626e43a1335e656f868b72935 Mon Sep 17 00:00:00 2001

+ From: Petr Schindler <pschindl@redhat.com>

+ Date: Fri, 20 Sep 2019 08:38:18 +0200

+ Subject: [PATCH] Backport for py38 fixes

+ 

+ Cherry picked from commits:

+ 83906ba779956af9437defcb8975debb18440e0d

+ ce8f2eddd832b34fb4f628d898383db16f5c92ed

+ e69d28ea8ce3bf361b278bfe5e10cafa6bdf6760

+ c000c88eb5239b87f299c85e83b349b0ef387ae7

+ 

+ ---

+  behave.ini                            |  3 +-

+  features/environment.py               | 14 ++++++

+  features/step.duplicated_step.feature | 20 ++++----

+  issue.features/environment.py         | 38 ++++++++++++---

+  issue.features/issue0330.feature      | 64 ++++++++++++++++++++++++

+  issue.features/issue0446.feature      | 70 +++++++++++++++++++++++++++

+  issue.features/issue0457.feature      | 49 +++++++++++++++++++

+  test/test_runner.py                   |  6 +++

+  tests/api/_test_async_step34.py       |  9 ++--

+  tests/unit/test_capture.py            |  2 +

+  tox.ini                               |  2 +-

+  11 files changed, 255 insertions(+), 22 deletions(-)

+ 

+ diff --git a/behave.ini b/behave.ini

+ index 431956d..1a18843 100644

+ --- a/behave.ini

+ +++ b/behave.ini

+ @@ -15,8 +15,9 @@ show_skipped = false

+  format   = rerun

+      progress3

+  outfiles = rerun.txt

+ -    reports/report_progress3.txt

+ +    build/behave.reports/report_progress3.txt

+  junit = true

+ +junit_directory = build/behave.reports

+  logging_level = INFO

+  # logging_format = LOG.%(levelname)-8s  %(name)-10s: %(message)s

+  # logging_format = LOG.%(levelname)-8s  %(asctime)s  %(name)-10s: %(message)s

+ diff --git a/features/environment.py b/features/environment.py

+ index 4744e89..3769ee4 100644

+ --- a/features/environment.py

+ +++ b/features/environment.py

+ @@ -1,5 +1,7 @@

+  # -*- coding: UTF-8 -*-

+ +# FILE: features/environemnt.py

+  

+ +from __future__ import absolute_import, print_function

+  from behave.tag_matcher import ActiveTagMatcher, setup_active_tag_values

+  from behave4cmd0.setup_command_shell import setup_command_shell_processors4behave

+  import platform

+ @@ -20,6 +22,15 @@ active_tag_value_provider = {

+  }

+  active_tag_matcher = ActiveTagMatcher(active_tag_value_provider)

+  

+ +

+ +def print_active_tags_summary():

+ +    active_tag_data = active_tag_value_provider

+ +    print("ACTIVE-TAG SUMMARY:")

+ +    print("use.with_python.version=%s" % active_tag_data.get("python.version"))

+ +    # print("use.with_os=%s" % active_tag_data.get("os"))

+ +    print()

+ +

+ +

+  # -----------------------------------------------------------------------------

+  # HOOKS:

+  # -----------------------------------------------------------------------------

+ @@ -30,11 +41,14 @@ def before_all(context):

+      setup_python_path()

+      setup_context_with_global_params_test(context)

+      setup_command_shell_processors4behave()

+ +    print_active_tags_summary()

+ +

+  

+  def before_feature(context, feature):

+      if active_tag_matcher.should_exclude_with(feature.tags):

+          feature.skip(reason=active_tag_matcher.exclude_reason)

+  

+ +

+  def before_scenario(context, scenario):

+      if active_tag_matcher.should_exclude_with(scenario.effective_tags):

+          scenario.skip(reason=active_tag_matcher.exclude_reason)

+ diff --git a/features/step.duplicated_step.feature b/features/step.duplicated_step.feature

+ index 59888b0..396cca2 100644

+ --- a/features/step.duplicated_step.feature

+ +++ b/features/step.duplicated_step.feature

+ @@ -32,11 +32,11 @@ Feature: Duplicated Step Definitions

+          AmbiguousStep: @given('I call Alice') has already been defined in

+          existing step @given('I call Alice') at features/steps/alice_steps.py:3

+          """

+ -    And the command output should contain:

+ -        """

+ -        File "features/steps/alice_steps.py", line 7, in <module>

+ -        @given(u'I call Alice')

+ -        """

+ +    # -- DISABLED: Python 3.8 traceback line numbers differ w/ decorators (+1).

+ +    # And the command output should contain:

+ +    #    """

+ +    #    File "features/steps/alice_steps.py", line 7, in <module>

+ +    #    """

+  

+  

+    Scenario: Duplicated Step Definition in another File

+ @@ -70,11 +70,11 @@ Feature: Duplicated Step Definitions

+          AmbiguousStep: @given('I call Bob') has already been defined in

+          existing step @given('I call Bob') at features/steps/bob1_steps.py:3

+          """

+ -    And the command output should contain:

+ -        """

+ -        File "features/steps/bob2_steps.py", line 3, in <module>

+ -        @given('I call Bob')

+ -        """

+ +    # -- DISABLED: Python 3.8 traceback line numbers differ w/ decorators (+1).

+ +    # And the command output should contain:

+ +    #    """

+ +    #    File "features/steps/bob2_steps.py", line 3, in <module>

+ +    #    """

+  

+    @xfail

+    Scenario: Duplicated Same Step Definition via import from another File

+ diff --git a/issue.features/environment.py b/issue.features/environment.py

+ index 3737155..dc8a7fb 100644

+ --- a/issue.features/environment.py

+ +++ b/issue.features/environment.py

+ @@ -1,5 +1,5 @@

+  # -*- coding: UTF-8 -*-

+ -# FILE: features/environment.py

+ +# FILE: issue.features/environemnt.py

+  # pylint: disable=unused-argument

+  """

+  Functionality:

+ @@ -7,17 +7,20 @@ Functionality:

+    * active tags

+  """

+  

+ -from __future__ import print_function

+ +

+ +from __future__ import absolute_import, print_function

+  import sys

+  import platform

+  import os.path

+  import six

+  from behave.tag_matcher import ActiveTagMatcher

+  from behave4cmd0.setup_command_shell import setup_command_shell_processors4behave

+ -# PREPARED:

+ -# from behave.tag_matcher import setup_active_tag_values

+ +# PREPARED: from behave.tag_matcher import setup_active_tag_values

+  

+  

+ +# ---------------------------------------------------------------------------

+ +# TEST SUPPORT: For Active Tags

+ +# ---------------------------------------------------------------------------

+  def require_tool(tool_name):

+      """Check if a tool (an executable program) is provided on this platform.

+  

+ @@ -45,12 +48,14 @@ def require_tool(tool_name):

+      # print("TOOL-NOT-FOUND: %s" % tool_name)

+      return False

+  

+ +

+  def as_bool_string(value):

+      if bool(value):

+          return "yes"

+      else:

+          return "no"

+  

+ +

+  def discover_ci_server():

+      # pylint: disable=invalid-name

+      ci_server = "none"

+ @@ -67,11 +72,16 @@ def discover_ci_server():

+      return ci_server

+  

+  

+ +# ---------------------------------------------------------------------------

+ +# BEHAVE SUPPORT: Active Tags

+ +# ---------------------------------------------------------------------------

+  # -- MATCHES ANY TAGS: @use.with_{category}={value}

+  # NOTE: active_tag_value_provider provides category values for active tags.

+ +python_version = "%s.%s" % sys.version_info[:2]

+  active_tag_value_provider = {

+      "python2": str(six.PY2).lower(),

+      "python3": str(six.PY3).lower(),

+ +    "python.version": python_version,

+      # -- python.implementation: cpython, pypy, jython, ironpython

+      "python.implementation": platform.python_implementation().lower(),

+      "pypy":    str("__pypy__" in sys.modules).lower(),

+ @@ -81,17 +91,33 @@ active_tag_value_provider = {

+  }

+  active_tag_matcher = ActiveTagMatcher(active_tag_value_provider)

+  

+ +

+ +def print_active_tags_summary():

+ +    active_tag_data = active_tag_value_provider

+ +    print("ACTIVE-TAG SUMMARY:")

+ +    print("use.with_python.version=%s" % active_tag_data.get("python.version"))

+ +    # print("use.with_platform=%s" % active_tag_data.get("platform"))

+ +    # print("use.with_os=%s" % active_tag_data.get("os"))

+ +    print()

+ +

+ +

+ +# ---------------------------------------------------------------------------

+ +# BEHAVE HOOKS:

+ +# ---------------------------------------------------------------------------

+  def before_all(context):

+      # -- SETUP ACTIVE-TAG MATCHER (with userdata):

+      # USE: behave -D browser=safari ...

+ -    # NOT-NEEDED: setup_active_tag_values(active_tag_value_provider,

+ -    #                                     context.config.userdata)

+ +    # NOT-NEEDED:

+ +    # setup_active_tag_values(active_tag_value_provider, context.config.userdata)

+      setup_command_shell_processors4behave()

+ +    print_active_tags_summary()

+ +

+  

+  def before_feature(context, feature):

+      if active_tag_matcher.should_exclude_with(feature.tags):

+          feature.skip(reason=active_tag_matcher.exclude_reason)

+  

+ +

+  def before_scenario(context, scenario):

+      if active_tag_matcher.should_exclude_with(scenario.effective_tags):

+          scenario.skip(reason=active_tag_matcher.exclude_reason)

+ diff --git a/issue.features/issue0330.feature b/issue.features/issue0330.feature

+ index dc1ebe7..81cb6e2 100644

+ --- a/issue.features/issue0330.feature

+ +++ b/issue.features/issue0330.feature

+ @@ -70,6 +70,7 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s

+      And note that "bob.feature is skipped"

+  

+  

+ +  @not.with_python.version=3.8

+    Scenario: Junit report for skipped feature is created with --show-skipped

+      When I run "behave --junit -t @tag1 --show-skipped @alice_and_bob.featureset"

+      Then it should pass with:

+ @@ -83,6 +84,23 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s

+        <testsuite errors="0" failures="0" name="bob.Bob" skipped="1" tests="1" time="0.0">

+        """

+  

+ +  @use.with_python.version=3.8

+ +  Scenario: Junit report for skipped feature is created with --show-skipped

+ +    When I run "behave --junit -t @tag1 --show-skipped @alice_and_bob.featureset"

+ +    Then it should pass with:

+ +      """

+ +      1 feature passed, 0 failed, 1 skipped

+ +      """

+ +    And a file named "test_results/TESTS-alice.xml" exists

+ +    And a file named "test_results/TESTS-bob.xml" exists

+ +    And the file "test_results/TESTS-bob.xml" should contain:

+ +      """

+ +      <testsuite name="bob.Bob" tests="1" errors="0" failures="0" skipped="1" time="0.0">

+ +      """

+ +      # -- HINT FOR: Python < 3.8

+ +      # <testsuite errors="0" failures="0" name="bob.Bob" skipped="1" tests="1" time="0.0">

+ +

+ +  @not.with_python.version=3.8

+    Scenario: Junit report for skipped scenario is neither shown nor counted with --no-skipped

+      When I run "behave --junit -t @tag1 --no-skipped"

+      Then it should pass with:

+ @@ -102,7 +120,30 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s

+        """

+      And note that "Charly2 is the skipped scenarion in charly.feature"

+  

+ +  @use.with_python.version=3.8

+ +  Scenario: Junit report for skipped scenario is neither shown nor counted with --no-skipped

+ +    When I run "behave --junit -t @tag1 --no-skipped"

+ +    Then it should pass with:

+ +      """

+ +      2 features passed, 0 failed, 1 skipped

+ +      2 scenarios passed, 0 failed, 2 skipped

+ +      """

+ +    And a file named "test_results/TESTS-alice.xml" exists

+ +    And a file named "test_results/TESTS-charly.xml" exists

+ +    And the file "test_results/TESTS-charly.xml" should contain:

+ +      """

+ +      <testsuite name="charly.Charly" tests="1" errors="0" failures="0" skipped="0"

+ +      """

+ +      # -- HINT FOR: Python < 3.8

+ +      # <testsuite errors="0" failures="0" name="charly.Charly" skipped="0" tests="1"

+ +    And the file "test_results/TESTS-charly.xml" should not contain:

+ +      """

+ +      <testcase classname="charly.Charly" name="Charly2"

+ +      """

+ +    And note that "Charly2 is the skipped scenarion in charly.feature"

+ +

+  

+ +  @not.with_python.version=3.8

+    Scenario: Junit report for skipped scenario is shown and counted with --show-skipped

+      When I run "behave --junit -t @tag1 --show-skipped"

+      Then it should pass with:

+ @@ -122,3 +163,26 @@ Feature: Issue #330: Skipped scenarios are included in junit reports when --no-s

+        """

+      And note that "Charly2 is the skipped scenarion in charly.feature"

+  

+ +

+ +  @use.with_python.version=3.8

+ +  Scenario: Junit report for skipped scenario is shown and counted with --show-skipped

+ +    When I run "behave --junit -t @tag1 --show-skipped"

+ +    Then it should pass with:

+ +      """

+ +      2 features passed, 0 failed, 1 skipped

+ +      2 scenarios passed, 0 failed, 2 skipped

+ +      """

+ +    And a file named "test_results/TESTS-alice.xml" exists

+ +    And a file named "test_results/TESTS-charly.xml" exists

+ +    And the file "test_results/TESTS-charly.xml" should contain:

+ +      """

+ +      <testsuite name="charly.Charly" tests="2" errors="0" failures="0" skipped="1"

+ +      """

+ +      # HINT: Python < 3.8

+ +      # <testsuite errors="0" failures="0" name="charly.Charly" skipped="1" tests="2"

+ +    And the file "test_results/TESTS-charly.xml" should contain:

+ +      """

+ +      <testcase classname="charly.Charly" name="Charly2" status="skipped"

+ +      """

+ +    And note that "Charly2 is the skipped scenarion in charly.feature"

+ +

+ diff --git a/issue.features/issue0446.feature b/issue.features/issue0446.feature

+ index a2ed892..901bdec 100644

+ --- a/issue.features/issue0446.feature

+ +++ b/issue.features/issue0446.feature

+ @@ -58,6 +58,7 @@ Feature: Issue #446 -- Support scenario hook-errors with JUnitReporter

+          behave.reporter.junit.show_hostname = False

+          """

+  

+ +    @not.with_python.version=3.8

+      Scenario: Hook error in before_scenario()

+        When I run "behave -f plain --junit features/before_scenario_failure.feature"

+        Then it should fail with:

+ @@ -86,6 +87,40 @@ Feature: Issue #446 -- Support scenario hook-errors with JUnitReporter

+        And note that "the traceback is contained in the XML element <error/>"

+  

+  

+ +    @use.with_python.version=3.8

+ +    Scenario: Hook error in before_scenario()

+ +      When I run "behave -f plain --junit features/before_scenario_failure.feature"

+ +      Then it should fail with:

+ +        """

+ +        0 scenarios passed, 1 failed, 0 skipped

+ +        """

+ +      And the command output should contain:

+ +        """

+ +        HOOK-ERROR in before_scenario: RuntimeError: OOPS

+ +        """

+ +      And the file "reports/TESTS-before_scenario_failure.xml" should contain:

+ +        """

+ +        <testsuite name="before_scenario_failure.Alice" tests="1" errors="1" failures="0" skipped="0"

+ +        """

+ +        # -- HINT FOR: Python < 3.8

+ +        # <testsuite errors="1" failures="0" name="before_scenario_failure.Alice" skipped="0" tests="1"

+ +      And the file "reports/TESTS-before_scenario_failure.xml" should contain:

+ +        """

+ +        <error type="RuntimeError" message="HOOK-ERROR in before_scenario: RuntimeError: OOPS">

+ +        """

+ +        # -- HINT FOR: Python < 3.8

+ +        # <error message="HOOK-ERROR in before_scenario: RuntimeError: OOPS" type="RuntimeError">

+ +      And the file "reports/TESTS-before_scenario_failure.xml" should contain:

+ +        """

+ +        File "features/environment.py", line 6, in before_scenario

+ +          cause_hook_failure()

+ +        File "features/environment.py", line 2, in cause_hook_failure

+ +          raise RuntimeError("OOPS")

+ +        """

+ +      And note that "the traceback is contained in the XML element <error/>"

+ +

+ +

+ +    @not.with_python.version=3.8

+      Scenario: Hook error in after_scenario()

+        When I run "behave -f plain --junit features/after_scenario_failure.feature"

+        Then it should fail with:

+ @@ -114,3 +149,38 @@ Feature: Issue #446 -- Support scenario hook-errors with JUnitReporter

+            raise RuntimeError("OOPS")

+          """

+        And note that "the traceback is contained in the XML element <error/>"

+ +

+ +

+ +    @use.with_python.version=3.8

+ +    Scenario: Hook error in after_scenario()

+ +      When I run "behave -f plain --junit features/after_scenario_failure.feature"

+ +      Then it should fail with:

+ +        """

+ +        0 scenarios passed, 1 failed, 0 skipped

+ +        """

+ +      And the command output should contain:

+ +        """

+ +          Scenario: B1

+ +            Given another step passes ... passed

+ +        HOOK-ERROR in after_scenario: RuntimeError: OOPS

+ +        """

+ +      And the file "reports/TESTS-after_scenario_failure.xml" should contain:

+ +        """

+ +        <testsuite name="after_scenario_failure.Bob" tests="1" errors="1" failures="0" skipped="0"

+ +        """

+ +        # -- HINT FOR: Python < 3.8

+ +        # <testsuite errors="1" failures="0" name="after_scenario_failure.Bob" skipped="0" tests="1"

+ +      And the file "reports/TESTS-after_scenario_failure.xml" should contain:

+ +        """

+ +        <error type="RuntimeError" message="HOOK-ERROR in after_scenario: RuntimeError: OOPS">

+ +        """

+ +        # -- HINT FOR: Python < 3.8

+ +        # <error message="HOOK-ERROR in after_scenario: RuntimeError: OOPS" type="RuntimeError">

+ +      And the file "reports/TESTS-after_scenario_failure.xml" should contain:

+ +        """

+ +        File "features/environment.py", line 10, in after_scenario

+ +          cause_hook_failure()

+ +        File "features/environment.py", line 2, in cause_hook_failure

+ +          raise RuntimeError("OOPS")

+ +        """

+ +      And note that "the traceback is contained in the XML element <error/>"

+ diff --git a/issue.features/issue0457.feature b/issue.features/issue0457.feature

+ index f80640e..46f96e9 100644

+ --- a/issue.features/issue0457.feature

+ +++ b/issue.features/issue0457.feature

+ @@ -24,6 +24,7 @@ Feature: Issue #457 -- Double-quotes in error messages of JUnit XML reports

+          """

+  

+  

+ +    @not.with_python.version=3.8

+      Scenario: Use failing assertation in a JUnit XML report

+        Given a file named "features/fails1.feature" with:

+          """

+ @@ -44,6 +45,31 @@ Feature: Issue #457 -- Double-quotes in error messages of JUnit XML reports

+          <failure message="FAILED: My name is &quot;Alice&quot;"

+          """

+  

+ +    @use.with_python.version=3.8

+ +    Scenario: Use failing assertation in a JUnit XML report

+ +      Given a file named "features/fails1.feature" with:

+ +        """

+ +        Feature:

+ +          Scenario: Alice

+ +            Given a step fails with message:

+ +              '''

+ +              My name is "Alice"

+ +              '''

+ +        """

+ +      When I run "behave --junit features/fails1.feature"

+ +      Then it should fail with:

+ +        """

+ +        0 scenarios passed, 1 failed, 0 skipped

+ +        """

+ +      And the file "reports/TESTS-fails1.xml" should contain:

+ +        """

+ +        <failure type="AssertionError" message="FAILED: My name is &quot;Alice&quot;">

+ +        """

+ +        # -- HINT FOR: Python < 3.8

+ +        # <failure message="FAILED: My name is &quot;Alice&quot;"

+ +

+ +

+ +    @not.with_python.version=3.8

+      Scenario: Use exception in a JUnit XML report

+        Given a file named "features/fails2.feature" with:

+          """

+ @@ -63,3 +89,26 @@ Feature: Issue #457 -- Double-quotes in error messages of JUnit XML reports

+          """

+          <error message="My name is &quot;Bob&quot; and &lt;here&gt; I am"

+          """

+ +

+ +    @use.with_python.version=3.8

+ +    Scenario: Use exception in a JUnit XML report

+ +      Given a file named "features/fails2.feature" with:

+ +        """

+ +        Feature:

+ +          Scenario: Bob

+ +            Given a step fails with error and message:

+ +              '''

+ +              My name is "Bob" and <here> I am

+ +              '''

+ +        """

+ +      When I run "behave --junit features/fails2.feature"

+ +      Then it should fail with:

+ +        """

+ +        0 scenarios passed, 1 failed, 0 skipped

+ +        """

+ +      And the file "reports/TESTS-fails2.xml" should contain:

+ +        """

+ +        <error type="RuntimeError" message="My name is &quot;Bob&quot; and &lt;here&gt; I am">

+ +        """

+ +        # -- HINT FOR: Python < 3.8

+ +        # <error message="My name is &quot;Bob&quot; and &lt;here&gt; I am"

+ diff --git a/test/test_runner.py b/test/test_runner.py

+ index 70a7002..1b5afa2 100644

+ --- a/test/test_runner.py

+ +++ b/test/test_runner.py

+ @@ -286,6 +286,7 @@ class TestContext(unittest.TestCase):

+          eq_("thing" in self.context, True)

+          del self.context.thing

+  

+ +

+  class ExampleSteps(object):

+      text = None

+      table = None

+ @@ -320,6 +321,7 @@ class ExampleSteps(object):

+          for keyword, pattern, func in step_definitions:

+              step_registry.add_step_definition(keyword, pattern, func)

+  

+ +

+  class TestContext_ExecuteSteps(unittest.TestCase):

+      """

+      Test the behave.runner.Context.execute_steps() functionality.

+ @@ -341,6 +343,8 @@ class TestContext_ExecuteSteps(unittest.TestCase):

+          runner_.config.stdout_capture = False

+          runner_.config.stderr_capture = False

+          runner_.config.log_capture = False

+ +        runner_.config.logging_format = None

+ +        runner_.config.logging_datefmt = None

+          runner_.step_registry = self.step_registry

+  

+          self.context = runner.Context(runner_)

+ @@ -658,6 +662,8 @@ class TestRunWithPaths(unittest.TestCase):

+          self.config.logging_filter = None

+          self.config.outputs = [Mock(), StreamOpener(stream=sys.stdout)]

+          self.config.format = ["plain", "progress"]

+ +        self.config.logging_format = None

+ +        self.config.logging_datefmt = None

+          self.runner = runner.Runner(self.config)

+          self.load_hooks = self.runner.load_hooks = Mock()

+          self.load_step_definitions = self.runner.load_step_definitions = Mock()

+ diff --git a/tests/api/_test_async_step34.py b/tests/api/_test_async_step34.py

+ index c93fb74..4e4edf4 100644

+ --- a/tests/api/_test_async_step34.py

+ +++ b/tests/api/_test_async_step34.py

+ @@ -37,15 +37,16 @@ from .testing_support_async import AsyncStepTheory

+  # -----------------------------------------------------------------------------

+  # TEST MARKERS:

+  # -----------------------------------------------------------------------------

+ -python_version = float("%s.%s" % sys.version_info[:2])

+ +# DEPRECATED: @asyncio.coroutine decorator (since: Python >= 3.8)

+ +_python_version = float("%s.%s" % sys.version_info[:2])

+ +requires_py34_to_py37 = pytest.mark.skipif(not (3.4 <= _python_version < 3.8),

+ +    reason="Supported only for python.versions: 3.4 .. 3.7 (inclusive)")

+  

+ -# xfail = pytest.mark.xfail

+ -py34_or_newer = pytest.mark.skipif(python_version < 3.4, reason="Needs Python >= 3.4")

+  

+  # -----------------------------------------------------------------------------

+  # TESTSUITE:

+  # -----------------------------------------------------------------------------

+ -@py34_or_newer

+ +@requires_py34_to_py37

+  class TestAsyncStepDecorator34(object):

+  

+      def test_step_decorator_async_run_until_complete2(self):

+ diff --git a/tests/unit/test_capture.py b/tests/unit/test_capture.py

+ index ac2655e..d9a3f3a 100644

+ --- a/tests/unit/test_capture.py

+ +++ b/tests/unit/test_capture.py

+ @@ -20,6 +20,8 @@ def create_capture_controller(config=None):

+          config.log_capture = True

+          config.logging_filter = None

+          config.logging_level = "INFO"

+ +        config.logging_format = "%(levelname)s:%(name)s:%(message)s"

+ +        config.logging_datefmt = None

+      return CaptureController(config)

+  

+  def setup_capture_controller(capture_controller, context=None):

+ diff --git a/tox.ini b/tox.ini

+ index 92f6679..16a392c 100644

+ --- a/tox.ini

+ +++ b/tox.ini

+ @@ -28,7 +28,7 @@

+  

+  [tox]

+  minversion   = 2.3

+ -envlist      = py26, py27, py33, py34, py35, py36, pypy, docs

+ +envlist      = py26, py27, py33, py34, py35, py36, py37, py38, pypy, docs

+  skip_missing_interpreters = True

+  sitepackages = False

+  indexserver =

+ -- 

+ 2.23.0

+ 

@@ -1,31 +0,0 @@ 

- From 1b42506d8527c523f4f98e0d817cf94cef2a7be1 Mon Sep 17 00:00:00 2001

- From: Ziv Thaller <ziv.thaller@arm.com>

- Date: Mon, 21 Mar 2016 14:21:14 +0200

- Subject: [PATCH 1/3] support ref link for each step

- 

- (cherry picked from commit 3b90000922638f56e9de521bc51f1ed46a5203c7)

- ---

-  behave/formatter/sphinx_steps.py | 7 +++++++

-  1 file changed, 7 insertions(+)

- 

- diff --git a/behave/formatter/sphinx_steps.py b/behave/formatter/sphinx_steps.py

- index 79768bb..3833b65 100644

- --- a/behave/formatter/sphinx_steps.py

- +++ b/behave/formatter/sphinx_steps.py

- @@ -294,6 +294,13 @@ The following step definitions are provided here.

-          self.document.write("%s\n" % step_definition_doc)

-          self.document.write("\n")

-  

- +        # Add step label

- +        from docutils.nodes import fully_normalize_name

- +        step_text = fully_normalize_name(step_text)

- +        step_lable = ".. _" + step_text + ":" + "\n"

- +        self.document.write(step_lable)

- +        self.document.write("\n")

- +

-  

-  # -----------------------------------------------------------------------------

-  # CLASS: SphinxStepsFormatter

- -- 

- 2.11.0

- 

@@ -1,124 +0,0 @@ 

- From 4a31e5a9ab7b5ccb1e9c153377076d522d1633f7 Mon Sep 17 00:00:00 2001

- From: jenisys <jenisys@users.noreply.github.com>

- Date: Tue, 22 Mar 2016 21:56:48 +0100

- Subject: [PATCH 2/3] Add step-label to be used in step-refs, like:   # --

-  FILE: something.rst   See also :ref:`When I create a database

-  "{db_server}:{db_name}"`

- 

- NOTES:

- * Step-labels are just before heading.

- * docutils must be installed to use them.

- 

- (cherry picked from commit d3f7a2ac99c6f7faae331b05f56096eb851d11c9)

- ---

-  CHANGES.rst                      |  3 +++

-  behave/formatter/sphinx_steps.py | 29 ++++++++++++++++++++++-------

-  behave/formatter/sphinx_util.py  |  4 +++-

-  3 files changed, 28 insertions(+), 8 deletions(-)

- 

- diff --git a/CHANGES.rst b/CHANGES.rst

- index 1bdb644..e32d2b6 100644

- --- a/CHANGES.rst

- +++ b/CHANGES.rst

- @@ -8,6 +8,9 @@ GOALS:

-  

-    - Improve support for Windows

-  

- +FORMATTERS:

- +

- +  * pull #423: sphinx.steps: Support ref link for each step (provided by: ZivThaller)

-  

-  Version: 1.2.5 (2015-01-31)

-  -------------------------------------------------------------------------------

- diff --git a/behave/formatter/sphinx_steps.py b/behave/formatter/sphinx_steps.py

- index 3833b65..4cf555d 100644

- --- a/behave/formatter/sphinx_steps.py

- +++ b/behave/formatter/sphinx_steps.py

- @@ -10,6 +10,9 @@ TODO:

-  

-  .. seealso::

-      http://sphinx-doc.org/

- +

- +.. note:: REQUIRES docutils

- +    :mod:`docutils` are needed to generate step-label for step references.

-  """

-  

-  from __future__ import absolute_import, print_function

- @@ -21,6 +24,16 @@ import inspect

-  import os.path

-  import sys

-  

- +try:

- +    # -- SAFETY-NET:

- +    import docutils

- +    has_docutils = True

- +

- +    # -- NEEDED FOR: step-labels (and step-refs)

- +    from docutils.nodes import fully_normalize_name

- +except ImportError:

- +    has_docutils = False

- +

-  

-  # -----------------------------------------------------------------------------

-  # HELPER CLASS:

- @@ -102,6 +115,7 @@ class SphinxStepsDocumentGenerator(object):

-      shows_step_module_info = True

-      shows_step_module_overview = True

-      make_step_index_entries = True

- +    make_step_labels = has_docutils

-  

-      document_separator = "# -- DOCUMENT-END " + "-" * 60

-      step_document_prefix = "step_module."

- @@ -287,19 +301,20 @@ The following step definitions are provided here.

-              index_id = self.make_step_definition_index_id(step_definition)

-  

-          heading = step_text

- +        step_label = None

-          if self.step_heading_prefix:

-              heading = self.step_heading_prefix + step_text

- -        self.document.write_heading(heading, level=2, index_id=index_id)

- +        if has_docutils and self.make_step_labels:

- +            # -- ADD STEP-LABEL (supports: step-refs by name)

- +            # EXAMPLE: See also :ref:`When my step does "{something}"`.

- +            step_label = fully_normalize_name(step_text)

- +            # SKIP-HERE: self.document.write(".. _%s:\n\n" % step_label)

- +        self.document.write_heading(heading, level=2, index_id=index_id, 

- +                                    label=step_label)

-          step_definition_doc = self.make_step_definition_doc(step_definition)

-          self.document.write("%s\n" % step_definition_doc)

-          self.document.write("\n")

-  

- -        # Add step label

- -        from docutils.nodes import fully_normalize_name

- -        step_text = fully_normalize_name(step_text)

- -        step_lable = ".. _" + step_text + ":" + "\n"

- -        self.document.write(step_lable)

- -        self.document.write("\n")

-  

-  

-  # -----------------------------------------------------------------------------

- diff --git a/behave/formatter/sphinx_util.py b/behave/formatter/sphinx_util.py

- index e1f7846..5e5cfbf 100644

- --- a/behave/formatter/sphinx_util.py

- +++ b/behave/formatter/sphinx_util.py

- @@ -40,7 +40,7 @@ class DocumentWriter(object):

-              self.stream.close()

-          self.stream = None

-  

- -    def write_heading(self, heading, level=0, index_id=None):

- +    def write_heading(self, heading, level=0, index_id=None, label=None):

-          assert self.stream

-          assert heading, "Heading should not be empty"

-          assert 0 <= level < len(self.heading_styles)

- @@ -55,6 +55,8 @@ class DocumentWriter(object):

-              if isinstance(index_id, (list, tuple)):

-                  index_id = ", ".join(index_id)

-              self.stream.write(".. index:: %s\n\n" % index_id)

- +        if label:

- +            self.stream.write(".. _%s:\n\n" % label)

-          if level == 0:

-              self.stream.write("%s\n" % separator)

-          self.stream.write("%s\n" % heading)

- -- 

- 2.11.0

- 

@@ -1,29 +0,0 @@ 

- From b750b601da576517340dc5cb8a3ddb921dad740c Mon Sep 17 00:00:00 2001

- From: jenisys <jenisys@users.noreply.github.com>

- Date: Sat, 1 Oct 2016 12:42:11 +0200

- Subject: [PATCH 3/3] PREPARE for Python 3.6: re.LOCALE was removed

- 

- (cherry picked from commit f52cc2f59df8e21a5caf21b268bbd9ca6fa3e1e1)

- ---

-  behave/configuration.py | 4 +++-

-  1 file changed, 3 insertions(+), 1 deletion(-)

- 

- diff --git a/behave/configuration.py b/behave/configuration.py

- index a2563df..134004c 100644

- --- a/behave/configuration.py

- +++ b/behave/configuration.py

- @@ -661,8 +661,10 @@ class Configuration(object):

-          :param names: List of name parts or regular expressions (as text).

-          :return: Compiled regular expression to use.

-          """

- +        # -- NOTE: re.LOCALE is removed in Python 3.6 (deprecated in Python 3.5)

- +        # flags = (re.UNICODE | re.LOCALE)

-          pattern = u"|".join(names)

- -        return re.compile(pattern, flags=(re.UNICODE | re.LOCALE))

- +        return re.compile(pattern, flags=re.UNICODE)

-  

-      def exclude(self, filename):

-          if isinstance(filename, FileLocation):

- -- 

- 2.11.0

- 

@@ -1,63 +0,0 @@ 

- commit 2cb052d072d9a14d82ac6a8806213fcb69e06c61

- Author: Vitezslav Humpa <vhumpa@redhat.com>

- Date:   Tue Sep 20 17:18:09 2016 +0200

- 

-     Fix several python2 unicode issues + python3 unicode() function compat

- 

- --- a/behave/formatter/html.py

- +++ b/behave/formatter/html.py

- @@ -33,6 +33,7 @@ from behave.compat.collections import Co

-  import xml.etree.ElementTree as ET

-  import base64

-  # XXX-JE-NOT-USED: import os.path

- +import six

-  

-  

-  def _valid_XML_char_ordinal(i):

- @@ -334,7 +335,9 @@ class HTMLFormatter(Formatter):

-              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)

- +                if isinstance(argument.value, six.integer_types):

- +                    argument.value = str(argument.value)

- +                ET.SubElement(step_text, 'b').text = argument.value

-                  text_start = argument.end

-              step_part = ET.SubElement(step_text, 'span')

-              step_part.text = result.name[self.arguments[-1].end:]

- @@ -403,7 +406,7 @@ class HTMLFormatter(Formatter):

-          if 'video/' in mime_type:

-              if not caption:

-                  caption = u'Video'

- -            link.text = unicode(caption)

- +            link.text = six.u(caption)

-  

-              embed = ET.SubElement(span, 'video',

-                                    {'id': 'embed_%s' % self.embed_id,

- @@ -418,7 +421,7 @@ class HTMLFormatter(Formatter):

-          if 'image/' in mime_type:

-              if not caption:

-                  caption = u'Screenshot'

- -            link.text = unicode(caption)

- +            link.text = six.u(caption)

-  

-              embed = ET.SubElement(span, 'img', {

-                                    'id': 'embed_%s' % self.embed_id,

- @@ -430,7 +433,7 @@ class HTMLFormatter(Formatter):

-          if 'text/' in mime_type:

-              if not caption:

-                  caption = u'Data'

- -            link.text = unicode(caption)

- +            link.text = six.u(caption)

-  

-              cleaned_data = ''.join(

-                  c for c in data if _valid_XML_char_ordinal(ord(c))

- @@ -439,7 +442,7 @@ class HTMLFormatter(Formatter):

-              embed = ET.SubElement(span, 'pre',

-                                    {'id': "embed_%s" % self.embed_id,

-                                     'style': 'display: none'})

- -            embed.text = cleaned_data

- +            embed.text = six.u(cleaned_data)

-              embed.tail = u'    '

-  

-      def embedding(self, mime_type, data, caption=None):

file removed
-1818

@@ -1,1818 +0,0 @@ 

- From eab8136d73e9c17ed61a099170ef7ed71788e376 Mon Sep 17 00:00:00 2001

- From: Vadim Rutkovsky <vrutkovs@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 cover

-          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

- diff --git a/behave/configuration.py b/behave/configuration.py

- index a2563df..b91f2e8 100644

- --- a/behave/configuration.py

- +++ b/behave/configuration.py

- @@ -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/behave.css

- @@ -0,0 +1,241 @@

- +// SOURCE: https://raw.githubusercontent.com/vrutkovs/behave/html_with_coveralls/behave/formatter/report.css

- +

- +// -- RESULT-STATUS RELATED STYLES:

- +.passed {

- +}

- +

- +.failed {

- +}

- +

- +.error {

- +}

- +

- +.skipped {

- +}

- +

- +.undefined {

- +}

- +

- +// -- CONTENT-RELATED STYLES:

- +.summary {

- +}

- +

- +.failed_scenarios {

- +}

- +

- +.footer {

- +}

- +

- +// -- ORIGINAL-STARTS-HERE

- +body {

- +  font-size: 0px;

- +  color: white;

- +  margin: 0px;

- +  padding: 0px;

- +}

- +

- +.behave, td, th {

- +  font: normal 11px "Lucida Grande", Helvetica, sans-serif;

- +  background: white;

- +  color: black;

- +}

- +.behave #behave-header, td #behave-header, th #behave-header {

- +  background: #65c400;

- +  color: white;

- +  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;

- +}

- +.behave .scenario h3, td .scenario h3, th .scenario h3, .background h3 {

- +  font-size: 11px;

- +  padding: 3px;

- +  margin: 0;

- +  background: #65c400;

- +  color: white;

- +  font-weight: bold;

- +}

- +

- +.background h3 {

- +  font-size: 1.2em;

- +  background: #666;

- +}

- +

- +.behave h1, td h1, th h1 {

- +  margin: 0px 10px 0px 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: 0px 10px 5px 10px;

- +}

- +.behave div.examples, td div.examples, th div.examples {

- +  padding: 0em 0em 0em 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: #999999;

- +}

- +.behave .step_file a, td .step_file a, th .step_file a {

- +  color: #999999;

- +}

- +.behave .scenario_file, td .scenario_file, th .scenario_file {

- +  float: right;

- +  color: #999999;

- +}

- +.behave .tag, td .tag, th .tag {

- +  font-weight: bold;

- +  color: #246ac1;

- +}

- +.behave .backtrace, td .backtrace, th .backtrace {

- +  margin-top: 0;

- +  margin-bottom: 0;

- +  margin-left: 1em;

- +  color: black;

- +}

- +.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: normal;

- +}

- +.behave a div.examples, td a div.examples, th a div.examples {

- +  margin: 5px 0px 5px 15px;

- +  color: black;

- +}

- +.behave .outline table, td .outline table, th .outline table {

- +  margin: 0px 0px 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, .behave table td.passed, .behave table td.skipped, .behave table td.pending, .behave table td.undefined, td table td.failed, td table td.passed, td table td.skipped, td table td.pending

- +  padding-left: 18px;

- +  padding-right: 10px;

- +}

- +.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 aqua;

- +  border-bottom: 1px solid aqua;

- +  background: #e0ffff;

- +  color: #001111;

- +}

- +.behave table td.pending, td table td.pending, th table td.pending {

- +  border-left: 5px solid #faf834;

- +  border-bottom: 1px solid #faf834;

- +  background: #fcfb98;

- +  color: #131313;

- +}

- +.behave table td.undefined, td table td.undefined, 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 aqua;

- +  border-bottom: 1px solid aqua;

- +  background: #e0ffff;

- +  color: #001111;

- +}

- +.behave ol, td ol, th ol {

- +  list-style: none;

- +  margin: 0px;

- +  padding: 0px;

- +}

- +.behave ol li.step, td ol li.step, th ol li.step {

- +  padding: 3px 3px 3px 18px;

- +  margin: 5px 0px 5px 5px;

- +}

- +.behave ol li, td ol li, th ol li {

- +  margin: 0em 0em 0em 1em;

- +  padding: 0em 0em 0em 0.2em;

- +}

- +.behave ol li span.param, td ol li span.param, th ol li span.param {

- +  font-weight: bold;

- +}

- +.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 aqua;

- +  border-bottom: 1px solid aqua;

- +  background: #e0ffff;

- +  color: #001111;

- +}

- +.behave ol li.pending, td ol li.pending, th ol li.pending {

- +  border-left: 5px solid #faf834;

- +  border-bottom: 1px solid #faf834;

- +  background: #fcfb98;

- +  color: #131313;

- +}

- +.behave ol li.undefined, td ol li.undefined, 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 aqua;

- +  border-bottom: 1px solid aqua;

- +  background: #e0ffff;

- +  color: #001111;

- +  margin-left: 10px;

- +}

- +.behave #summary, td #summary, th #summary {

- +  margin: 0px;

- +  padding: 5px 10px;

- +  text-align: right;

- +  top: 0px;

- +  right: 0px;

- +  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;

- +}

- 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

- +    """