From 81aaea0d054aab5a9f2e6347b47d0a130fe1816f Mon Sep 17 00:00:00 2001 From: jenisys Date: Tue, 30 Dec 2014 15:22:07 +0100 Subject: [PATCH] Various unicode related fixes (#226, #230, #251). Merged pull-requests #252, #190 (manually). --- .gitignore | 3 +- CHANGES.rst | 10 +- behave/__main__.py | 21 +- behave/configuration.py | 2 +- behave/formatter/formatters.py | 4 +- behave/formatter/pretty.py | 11 +- behave/formatter/sphinx_util.py | 4 +- behave/formatter/steps.py | 25 +- behave/formatter/tags.py | 8 +- behave/model.py | 52 +-- behave/parser.py | 12 +- behave/reporter/junit.py | 87 ++-- behave/reporter/summary.py | 12 +- behave/runner.py | 14 +- behave/runner_util.py | 6 +- behave/step_registry.py | 28 +- behave/tag_expression.py | 10 +- behave/textutil.py | 40 +- behave4cmd0/command_shell.py | 10 +- bin/behave | 1 + bin/behave_cmd.py | 44 ++ features/i18n.unicode_problems.feature | 429 ++++++++++++++++++ ...p_param.builtin_types.with_integer.feature | 8 +- features/steps/behave_context_steps.py | 5 +- features/steps/behave_select_files_steps.py | 3 +- issue.features/issue0226.feature | 47 ++ issue.features/issue0230.feature | 46 ++ issue.features/issue0251.feature | 15 + test/test_model.py | 24 +- test/test_runner.py | 9 +- 30 files changed, 838 insertions(+), 152 deletions(-) create mode 100755 bin/behave_cmd.py create mode 100644 features/i18n.unicode_problems.feature create mode 100644 issue.features/issue0226.feature create mode 100644 issue.features/issue0230.feature create mode 100644 issue.features/issue0251.feature diff --git a/.gitignore b/.gitignore index c6eaa9269..1bdeeb8a5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,11 +9,12 @@ __pycache__/ __WORKDIR__/ _build/ _WORKSPACE/ +tools/virtualenvs .cache/ .idea/ .tox/ +.venv*/ .DS_Store .coverage .ropeproject nosetests.xml -tools/virtualenvs diff --git a/CHANGES.rst b/CHANGES.rst index cd055cf11..3599cc0fc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,7 @@ NEWS and CHANGES: - General: * Improve support for Python3 (py3.3, py3.4; #268) + * Various unicode related fixes (Unicode errors with non-ASCII, etc.) * Drop support for Python 2.5 - Running: @@ -39,7 +40,7 @@ ENHANCEMENTS: * pull #272: Use option role to format command line arg docs (provided by: helenst) * pull #271: Provide steps.catalog formatter (provided by: berdroid) * pull #261: Support "setup.cfg" as configuration file, too (provided by: bittner) - * pull #260: Documenation tweaks and typo fixes (provided by: bittner) + * pull #260: Documentation tweaks and typo fixes (provided by: bittner) * pull #254: Undefined step raises NotImplementedError instead of assert False (provided by: mhfrantz) * issue #242: JUnitReporter can show scenario tags (provided by: rigomes) * issue #240: Test Stages with different step implementations (provided by: attilammagyar, jenisys) @@ -50,12 +51,19 @@ ENHANCEMENTS: FIXED: * pull #268: Fix py3 compatibility with all tests passed (provided by: sunliwen) + * pull #252: Related to #251 (provided by: mcepl) + * pull #190: UnicodeDecodeError in tracebacks (provided by: b3ni, vrutkovs, related to: #226, #230) * issue #257: Fix JUnitReporter (XML) for Python3 (provided by: actionless) * issue #249: Fix a number of docstring problems (provided by: masak) * issue #253: Various problems in PrettyFormatter.exception() + * issue #251: Unicode crash in model.py (provided by: mcepl, jenisys) * issue #236: Command line docs are confusing (solved by: #272) + * issue #230: problem with assert message that contains ascii over 128 value (provided by: jenisys) + * issue #226: UnicodeDecodeError in tracebacks (provided by: md1023, karulis, jenisys) * issue #221: Fix some PY2/PY3 incompatibilities (provided by: johbo) * issue #216: Using --wip option does not disable ANSI escape sequences (coloring). + * issue #119: Python3 support for behave (solved by: #268 and ...) + * issue #82: JUnitReporter fails with Python 3.x (fixed with: #257, #268) .. _`New and Noteworthy`: https://github.com/behave/behave/blob/master/docs/new_and_noteworthy.rst diff --git a/behave/__main__.py b/behave/__main__.py index 4ab88f1d8..954ce474c 100644 --- a/behave/__main__.py +++ b/behave/__main__.py @@ -1,13 +1,15 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, print_function -import sys from behave import __version__ from behave.configuration import Configuration, ConfigError +from behave.parser import ParserError from behave.runner import Runner from behave.runner_util import print_undefined_step_snippets, \ InvalidFileLocationError, InvalidFilenameError, FileNotFoundError -from behave.parser import ParserError +from behave.textutil import text as _text +import sys + TAG_HELP = """ Scenarios inherit tags declared on the Feature level. The simplest @@ -108,15 +110,20 @@ def main(args=None): try: failed = runner.run() except ParserError as e: - print("ParseError: %s" % e) + print(u"ParseError: %s" % e) except ConfigError as e: - print("ConfigError: %s" % e) + print(u"ConfigError: %s" % e) except FileNotFoundError as e: - print("FileNotFoundError: %s" % e) + print(u"FileNotFoundError: %s" % e) except InvalidFileLocationError as e: - print("InvalidFileLocationError: %s" % e) + print(u"InvalidFileLocationError: %s" % e) except InvalidFilenameError as e: - print("InvalidFilenameError: %s" % e) + print(u"InvalidFilenameError: %s" % e) + except Exception as e: + # -- DIAGNOSTICS: + text = _text(e) + print(u"Exception %s: %s" % (e.__class__.__name__, text)) + raise if config.show_snippets and runner.undefined_steps: print_undefined_step_snippets(runner.undefined_steps, diff --git a/behave/configuration.py b/behave/configuration.py index b5e413c2d..f413e08e0 100644 --- a/behave/configuration.py +++ b/behave/configuration.py @@ -635,7 +635,7 @@ def build_name_re(names): def exclude(self, filename): if isinstance(filename, FileLocation): - filename = str(filename) + filename = six.text_type(filename) if self.include_re and self.include_re.search(filename) is None: return True diff --git a/behave/formatter/formatters.py b/behave/formatter/formatters.py index 3d8cdd04a..af8db5242 100644 --- a/behave/formatter/formatters.py +++ b/behave/formatter/formatters.py @@ -2,7 +2,7 @@ import sys from behave.formatter.base import StreamOpener -from behave.textutil import compute_words_maxsize +from behave.textutil import compute_words_maxsize, text as _text from behave.importer import LazyDict, LazyObject @@ -33,7 +33,7 @@ def list_formatters(stream): """ formatter_names = sorted(formatters) column_size = compute_words_maxsize(formatter_names) - schema = u" %-"+ str(column_size) +"s %s\n" + schema = u" %-"+ _text(column_size) +"s %s\n" for name in formatter_names: stream.write(schema % (name, formatters[name].description)) diff --git a/behave/formatter/pretty.py b/behave/formatter/pretty.py index 7d4b8df3d..dba670180 100644 --- a/behave/formatter/pretty.py +++ b/behave/formatter/pretty.py @@ -4,7 +4,7 @@ from behave.formatter.ansi_escapes import escapes, up from behave.formatter.base import Formatter from behave.model_describe import escape_cell, escape_triple_quotes -from behave.textutil import indent +from behave.textutil import indent, text as _text import sys import six from six.moves import range @@ -211,11 +211,12 @@ def table(self, table): def doc_string(self, doc_string): #self.stream.write(' """' + doc_string.content_type + '\n') - prefix = ' ' - self.stream.write('%s"""\n' % prefix) + doc_string = _text(doc_string) + prefix = u' ' + self.stream.write(u'%s"""\n' % prefix) doc_string = escape_triple_quotes(indent(doc_string, prefix)) self.stream.write(doc_string) - self.stream.write('\n%s"""\n' % prefix) + self.stream.write(u'\n%s"""\n' % prefix) self.stream.flush() # def doc_string(self, doc_string): @@ -227,7 +228,7 @@ def doc_string(self, doc_string): # -- UNUSED: # def exception(self, exception): - # exception_text = str(exception) + # exception_text = _text(exception) # self.stream.write(self.format("failed").text(exception_text) + "\n") # self.stream.flush() diff --git a/behave/formatter/sphinx_util.py b/behave/formatter/sphinx_util.py index 930dd44d5..e1f7846ce 100644 --- a/behave/formatter/sphinx_util.py +++ b/behave/formatter/sphinx_util.py @@ -4,7 +4,7 @@ """ from __future__ import absolute_import -from behave.textutil import compute_words_maxsize +from behave.textutil import compute_words_maxsize, text as _text import codecs import six @@ -102,7 +102,7 @@ def write_table(self, table): column_size = compute_words_maxsize(column) cols_size.append(column_size) separator_parts.append("=" * column_size) - row_schema_parts.append("%-" + str(column_size) + "s") + row_schema_parts.append("%-" + _text(column_size) + "s") separator = " ".join(separator_parts) + "\n" row_schema = " ".join(row_schema_parts) + "\n" diff --git a/behave/formatter/steps.py b/behave/formatter/steps.py index 7459525fc..a1d25d6b3 100644 --- a/behave/formatter/steps.py +++ b/behave/formatter/steps.py @@ -7,7 +7,8 @@ from __future__ import absolute_import from behave.formatter.base import Formatter from behave.step_registry import StepRegistry, registry -from behave.textutil import compute_words_maxsize, indent, make_indentation +from behave.textutil import \ + compute_words_maxsize, indent, make_indentation, text as _text from behave import i18n from operator import attrgetter import inspect @@ -170,7 +171,7 @@ def report_steps_by_type(self): max_size = compute_words_maxsize(steps_text) if max_size < self.min_location_column: max_size = self.min_location_column - schema = u" %-" + str(max_size) + "s # %s\n" + schema = u" %-" + _text(max_size) + "s # %s\n" else: schema = u" %s\n" @@ -269,14 +270,14 @@ class StepsCatalogFormatter(StepsDocFormatter): """ Provides formatter class that shows the documentation of all registered step definitions. The primary purpose is to provide help for a test writer. - + In order to ease work for non-programmer testers, the technical details of the steps (i.e. function name, source location) are ommited and the steps are shown as they would apprear in a feature file (no noisy '@', or '(', etc.). - + Also, the output is sorted by step type (Given, When, Then) - + Generic step definitions are listed with all three step types. EXAMPLE: @@ -320,7 +321,7 @@ def describe_step_definition(self, step_definition, step_type=None): desc.append(text) else: desc.append(u"%s %s" % (step_type.title(), step_definition.string)) - + return '\n'.join(desc) @@ -431,9 +432,9 @@ def report_used_step_definitions(self): if max_size < self.min_location_column: max_size = self.min_location_column - schema = u"%-" + str(max_size) + "s # %s\n" + schema = u"%-" + _text(max_size) + "s # %s\n" self.stream.write(schema % (stepdef_text, step_definition.location)) - schema = u"%-" + str(max_size) + "s # %s\n" + schema = u"%-" + _text(max_size) + "s # %s\n" for step, step_text in zip(steps, steps_text): self.stream.write(schema % (step_text, step.location)) self.stream.write("\n") @@ -455,10 +456,10 @@ def report_unused_step_definitions(self): max_size = self.min_location_column-2 # -- STEP: Write report. - schema = u" %-" + str(max_size) + "s # %s\n" + schema = u" %-" + _text(max_size) + "s # %s\n" self.stream.write("UNUSED STEP DEFINITIONS[%d]:\n" % len(step_texts)) - for step_definition, text in zip(step_definitions, step_texts): - self.stream.write(schema % (text, step_definition.location)) + for step_definition, step_text in zip(step_definitions, step_texts): + self.stream.write(schema % (step_text, step_definition.location)) def report_undefined_steps(self): if not self.undefined_steps: @@ -475,7 +476,7 @@ def report_undefined_steps(self): max_size = self.min_location_column self.stream.write("\nUNDEFINED STEPS[%d]:\n" % len(steps_text)) - schema = u"%-" + str(max_size) + "s # %s\n" + schema = u"%-" + _text(max_size) + "s # %s\n" for step, step_text in zip(undefined_steps, steps_text): self.stream.write(schema % (step_text, step.location)) diff --git a/behave/formatter/tags.py b/behave/formatter/tags.py index 01fcda8be..0d107edbd 100644 --- a/behave/formatter/tags.py +++ b/behave/formatter/tags.py @@ -9,7 +9,7 @@ from __future__ import absolute_import from behave.formatter.base import Formatter -from behave.textutil import compute_words_maxsize +from behave.textutil import compute_words_maxsize, text as _text import six @@ -121,7 +121,7 @@ def report_tag_counts(self): # -- PREPARE REPORT: ordered_tags = sorted(list(self.tag_counts.keys())) tag_maxsize = compute_words_maxsize(ordered_tags) - schema = " @%-" + str(tag_maxsize) + "s %4d (used for %s)\n" + schema = " @%-" + _text(tag_maxsize) + "s %4d (used for %s)\n" # -- EMIT REPORT: self.stream.write("TAG COUNTS (alphabetically sorted):\n") @@ -138,7 +138,7 @@ def report_tag_counts_by_usage(self): ordered_tags = sorted(list(self.tag_counts.keys()), key=compare_tag_counts_size) tag_maxsize = compute_words_maxsize(ordered_tags) - schema = " @%-" + str(tag_maxsize) + "s %4d (used for %s)\n" + schema = " @%-" + _text(tag_maxsize) + "s %4d (used for %s)\n" # -- EMIT REPORT: self.stream.write("TAG COUNTS (most often used first):\n") @@ -176,7 +176,7 @@ def report_tags_by_locations(self): for tag_elements in self.tag_counts.values(): locations.update([six.text_type(x.location) for x in tag_elements]) location_column_size = compute_words_maxsize(locations) - schema = u" %-" + str(location_column_size) + "s %s\n" + schema = u" %-" + _text(location_column_size) + "s %s\n" # -- EMIT REPORT: self.stream.write("TAG LOCATIONS (alphabetically ordered):\n") diff --git a/behave/model.py b/behave/model.py index 473da3b36..1541f6f58 100644 --- a/behave/model.py +++ b/behave/model.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import, with_statement +from __future__ import absolute_import, with_statement, unicode_literals +from behave import step_registry +from behave.textutil import text as _text import copy import difflib import itertools @@ -11,7 +13,6 @@ import sys import time import traceback -from behave import step_registry class Argument(object): @@ -63,27 +64,14 @@ class FileLocation(object): * "{filename}:{line}" or * "{filename}" (if line number is not present) """ - # -- pylint: disable=R0904,R0924 + # -- pylint: disable=R0904 # R0904: 30,0:FileLocation: Too many public methods (43/30) => unicode - # R0924: 30,0:FileLocation: Badly implemented Container, ...=> unicode __pychecker__ = "missingattrs=line" # -- Ignore warnings for 'line'. def __init__(self, filename, line=None): self.filename = filename self.line = line - # def __new__(cls, filename, line=None): - # assert isinstance(filename, six.string_types) - # obj = unicode.__new__(cls, filename) - # obj.line = line - # obj.__filename = filename - # return obj - # - # @property - # def filename(self): - # # -- PREVENT: Assignments via property (and avoid self-recursion). - # return self.__filename - def get(self): return self.filename @@ -164,9 +152,16 @@ def __repr__(self): (self.filename, self.line) def __str__(self): + filename = self.filename + if isinstance(filename, six.binary_type): + filename = _text(filename, "utf-8") if self.line is None: - return self.filename - return u"%s:%d" % (self.filename, self.line) + return filename + return u"%s:%d" % (filename, self.line) + + if six.PY2: + __unicode__ = __str__ + __str__ = lambda self: self.__unicode__().encode("utf-8") class BasicStatement(object): @@ -1119,12 +1114,12 @@ def scenarios(self): for example_index, example in enumerate(self.examples): example.index = example_index+1 params["examples.name"] = example.name - params["examples.index"] = str(example.index) + params["examples.index"] = _text(example.index) for row_index, row in enumerate(example.table): row.index = row_index+1 row.id = "%d.%d" % (example.index, row.index) params["row.id"] = row.id - params["row.index"] = str(row.index) + params["row.index"] = _text(row.index) scenario_name = self.make_scenario_name(example, row, params) row_tags = self.make_row_tags(row, params) new_steps = [] @@ -1450,13 +1445,14 @@ def run(self, runner, quiet=False, capture=True): self.status = 'failed' self.store_exception_context(e) if e.args: - error = u'Assertion Failed: %s' % e + message = _text(e) + error = u'Assertion Failed: '+ message else: # no assertion text; format the exception - error = traceback.format_exc() + error = _text(traceback.format_exc()) except Exception as e: self.status = 'failed' - error = traceback.format_exc() + error = _text(traceback.format_exc()) self.store_exception_context(e) self.duration = time.time() - start @@ -1465,20 +1461,24 @@ def run(self, runner, quiet=False, capture=True): # flesh out the failure with details if self.status == 'failed': + assert isinstance(error, six.text_type) if capture: # -- CAPTURE-ONLY: Non-nested step failures. if runner.config.stdout_capture: output = runner.stdout_capture.getvalue() if output: - error += '\nCaptured stdout:\n' + output + output = _text(output) + error += u'\nCaptured stdout:\n' + output if runner.config.stderr_capture: output = runner.stderr_capture.getvalue() if output: - error += '\nCaptured stderr:\n' + output + output = _text(output) + error += u'\nCaptured stderr:\n' + output if runner.config.log_capture: output = runner.log_capture.getvalue() if output: - error += '\nCaptured logging:\n' + output + output = _text(output) + error += u'\nCaptured logging:\n' + output self.error_message = error keep_going = False diff --git a/behave/parser.py b/behave/parser.py index 76df67807..aa55a195e 100644 --- a/behave/parser.py +++ b/behave/parser.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, with_statement from behave import model, i18n +from behave.textutil import text as _text import six @@ -70,9 +71,16 @@ def __init__(self, message, line, filename=None, line_text=None): self.filename = filename def __str__(self): + arg0 = _text(self.args[0]) if self.filename: - return 'Failed to parse "%s": %s' % (self.filename, self.args[0]) - return 'Failed to parse : %s' % self.args[0] + filename = _text(self.filename) + return u'Failed to parse "%s": %s' % (filename, arg0) + else: + return u'Failed to parse : %s' % arg0 + + if six.PY2: + __unicode__ = __str__ + __str__ = lambda self: self.__unicode__().encode("utf-8") class Parser(object): diff --git a/behave/reporter/junit.py b/behave/reporter/junit.py index 8bfaad372..a85bc71d2 100644 --- a/behave/reporter/junit.py +++ b/behave/reporter/junit.py @@ -9,7 +9,7 @@ from behave.model import Scenario, ScenarioOutline, Step from behave.formatter import ansi_escapes from behave.model_describe import ModelDescriptor -from behave.textutil import indent, make_indentation +from behave.textutil import indent, make_indentation, text as _text import six @@ -36,7 +36,7 @@ def _write(self, file, node, encoding, namespaces): def _serialize_xml2(write, elem, encoding, qnames, namespaces, orig=ElementTree._serialize_xml): if elem.tag == '![CDATA[': - write("\n<%s%s]]>\n" % (elem.tag, elem.text.encode(encoding))) + write("\n<%s%s]]>\n" % (elem.tag, elem.text.encode(encoding, "xmlcharrefreplace"))) return return orig(write, elem, encoding, qnames, namespaces) @@ -93,7 +93,7 @@ class JUnitReporter(Reporter): show_multiline = True show_timings = True # -- Show step timings. show_tags = True - + def make_feature_filename(self, feature): filename = None for path in self.config.paths: @@ -105,17 +105,17 @@ def make_feature_filename(self, feature): filename = feature.location.relpath(self.config.base_dir) filename = filename.rsplit('.', 1)[0] filename = filename.replace('\\', '/').replace('/', '.') - return filename + return _text(filename) # -- REPORTER-API: def feature(self, feature): - filename = self.make_feature_filename(feature) - classname = filename - report = FeatureReportData(feature, filename) - filename = 'TESTS-%s.xml' % filename + feature_filename = self.make_feature_filename(feature) + classname = feature_filename + report = FeatureReportData(feature, feature_filename) - suite = ElementTree.Element('testsuite') - suite.set('name', '%s.%s' % (classname, feature.name or feature.filename)) + suite = ElementTree.Element(u'testsuite') + feature_name = feature.name or feature_filename + suite.set(u'name', u'%s.%s' % (classname, feature_name)) # -- BUILD-TESTCASES: From scenarios for scenario in feature: @@ -129,20 +129,21 @@ def feature(self, feature): for testcase in report.testcases: suite.append(testcase) - suite.set('tests', str(report.counts_tests)) - suite.set('errors', str(report.counts_errors)) - suite.set('failures', str(report.counts_failed)) - suite.set('skipped', str(report.counts_skipped)) # WAS: skips - # -- ORIG: suite.set('time', str(round(feature.duration, 3))) - suite.set('time', str(round(feature.duration, 6))) + suite.set(u'tests', _text(report.counts_tests)) + suite.set(u'errors', _text(report.counts_errors)) + suite.set(u'failures', _text(report.counts_failed)) + suite.set(u'skipped', _text(report.counts_skipped)) # WAS: skips + suite.set(u'time', _text(round(feature.duration, 6))) if not os.path.exists(self.config.junit_directory): # -- ENSURE: Create multiple directory levels at once. os.makedirs(self.config.junit_directory) tree = ElementTreeWithCDATA(suite) - report_filename = os.path.join(self.config.junit_directory, filename) - tree.write(codecs.open(report_filename, 'wb'), 'UTF-8') + report_dirname = self.config.junit_directory + report_basename = u'TESTS-%s.xml' % feature_filename + report_filename = os.path.join(report_dirname, report_basename) + tree.write(codecs.open(report_filename, "wb"), "UTF-8") # -- MORE: @staticmethod @@ -175,7 +176,7 @@ def select_step_with_status(status, steps): @classmethod def describe_step(cls, step): - status = str(step.status) + status = _text(step.status) if cls.show_timings: status += u" in %0.3fs" % step.duration text = u'%s %s ... ' % (step.keyword, step.name) @@ -206,8 +207,8 @@ def describe_scenario(cls, scenario): """ header_line = u'\n@scenario.begin\n' if cls.show_tags and scenario.tags: - header_line += '\n %s\n' % cls.describe_tags(scenario.tags) - header_line += ' %s: %s\n' % (scenario.keyword, scenario.name) + header_line += u'\n %s\n' % cls.describe_tags(scenario.tags) + header_line += u' %s: %s\n' % (scenario.keyword, scenario.name) footer_line = u'\n@scenario.end\n' + u'-' * 80 + '\n' text = u'' for step in scenario: @@ -237,16 +238,18 @@ def _process_scenario(self, scenario, report): """ assert isinstance(scenario, Scenario) assert not isinstance(scenario, ScenarioOutline) - feature = report.feature - classname = report.classname report.counts_tests += 1 + classname = report.classname + feature = report.feature + feature_name = feature.name + if not feature_name: + feature_name = self.make_feature_filename(feature) case = ElementTree.Element('testcase') - case.set('classname', '%s.%s' % (classname, feature.name or feature.filename)) - case.set('name', scenario.name or '') - case.set('status', scenario.status) - # -- ORIG: case.set('time', str(round(scenario.duration, 3))) - case.set('time', str(round(scenario.duration, 6))) + case.set(u'classname', u'%s.%s' % (classname, feature_name)) + case.set(u'name', scenario.name or '') + case.set(u'status', scenario.status) + case.set(u'time', _text(round(scenario.duration, 6))) step = None if scenario.status == 'failed': @@ -268,12 +271,12 @@ def _process_scenario(self, scenario, report): failure = ElementTree.Element(element_name) step_text = self.describe_step(step).rstrip() text = u"\nFailing step: %s\nLocation: %s\n" % (step_text, step.location) - message = six.text_type(step.exception) + message = _text(step.exception) if len(message) > 80: message = message[:80] + "..." - failure.set('type', step.exception.__class__.__name__) - failure.set('message', message) - text += six.text_type(step.error_message) + failure.set(u'type', step.exception.__class__.__name__) + failure.set(u'message', message) + text += _text(step.error_message) failure.append(CDATA(text)) case.append(failure) elif scenario.status in ('skipped', 'untested'): @@ -282,28 +285,30 @@ def _process_scenario(self, scenario, report): if step: # -- UNDEFINED-STEP: report.counts_failed += 1 - failure = ElementTree.Element('failure') - failure.set('type', 'undefined') - failure.set('message', ('Undefined Step: %s' % step.name)) + failure = ElementTree.Element(u'failure') + failure.set(u'type', u'undefined') + failure.set(u'message', (u'Undefined Step: %s' % step.name)) case.append(failure) else: - skip = ElementTree.Element('skipped') + skip = ElementTree.Element(u'skipped') case.append(skip) # Create stdout section for each test case - stdout = ElementTree.Element('system-out') - text = self.describe_scenario(scenario) + stdout = ElementTree.Element(u'system-out') + text = self.describe_scenario(scenario) # Append the captured standard output if scenario.stdout: - text += '\nCaptured stdout:\n%s\n' % scenario.stdout + output = _text(scenario.stdout) + text += u'\nCaptured stdout:\n%s\n' % output stdout.append(CDATA(text)) case.append(stdout) # Create stderr section for each test case if scenario.stderr: - stderr = ElementTree.Element('system-err') - text = u'\nCaptured stderr:\n%s\n' % scenario.stderr + stderr = ElementTree.Element(u'system-err') + output = _text(scenario.stderr) + text = u'\nCaptured stderr:\n%s\n' % output stderr.append(CDATA(text)) case.append(stderr) diff --git a/behave/reporter/summary.py b/behave/reporter/summary.py index 6c8864be7..a7e3ea2bc 100644 --- a/behave/reporter/summary.py +++ b/behave/reporter/summary.py @@ -4,9 +4,10 @@ """ from __future__ import absolute_import, division -import sys from behave.model import ScenarioOutline from behave.reporter.base import Reporter +from behave.formatter.base import StreamOpener +import sys # -- DISABLED: optional_steps = ('untested', 'undefined') @@ -28,9 +29,9 @@ def format_summary(statement_type, summary): label = statement_type if counts != 1: label += 's' - part = '%d %s %s' % (counts, label, status) + part = u'%d %s %s' % (counts, label, status) else: - part = '%d %s' % (counts, status) + part = u'%d %s' % (counts, status) parts.append(part) return ', '.join(parts) + '\n' @@ -41,7 +42,8 @@ class SummaryReporter(Reporter): def __init__(self, config): super(SummaryReporter, self).__init__(config) - self.stream = getattr(sys, self.output_stream_name, sys.stderr) + stream = getattr(sys, self.output_stream_name, sys.stderr) + self.stream = StreamOpener.ensure_stream_with_encoder(stream) self.feature_summary = {'passed': 0, 'failed': 0, 'skipped': 0, 'untested': 0} self.scenario_summary = {'passed': 0, 'failed': 0, 'skipped': 0, @@ -65,7 +67,7 @@ def end(self): if self.show_failed_scenarios and self.failed_scenarios: self.stream.write("\nFailing scenarios:\n") for scenario in self.failed_scenarios: - self.stream.write(" %s %s\n" % ( + self.stream.write(u" %s %s\n" % ( scenario.location, scenario.name)) self.stream.write("\n") diff --git a/behave/runner.py b/behave/runner.py index 8b3b1c3ea..5e50dddbd 100644 --- a/behave/runner.py +++ b/behave/runner.py @@ -15,8 +15,8 @@ from behave.formatter import formatters from behave.configuration import ConfigError from behave.log_capture import LoggingCapture -from behave.runner_util import \ - collect_feature_locations, parse_features +from behave.runner_util import collect_feature_locations, parse_features +from behave.textutil import text as _text class ContextMaskWarning(UserWarning): @@ -299,9 +299,14 @@ def exec_file(filename, globals={}, locals=None): locals['__file__'] = filename with open(filename) as f: # -- FIX issue #80: exec(f.read(), globals, locals) + # try: filename2 = os.path.relpath(filename, os.getcwd()) code = compile(f.read(), filename2, 'exec') exec(code, globals, locals) + # except Exception as e: + # e_text = _text(e) + # print("Exception %s: %s" % (e.__class__.__name__, e_text)) + # raise def path_getrootdir(path): @@ -649,9 +654,14 @@ def load_step_definitions(self, extra_step_paths=[]): # Reset to default matcher after each step-definition. # A step-definition may change the matcher 0..N times. # ENSURE: Each step definition has clean globals. + # try: step_module_globals = step_globals.copy() exec_file(os.path.join(path, name), step_module_globals) matchers.current_matcher = default_matcher + # except Exception as e: + # e_text = _text(e) + # print("Exception %s: %s" % (e.__class__.__name__, e_text)) + # raise def feature_locations(self): return collect_feature_locations(self.config.paths) diff --git a/behave/runner_util.py b/behave/runner_util.py index edcc8f4d1..b23fd3742 100644 --- a/behave/runner_util.py +++ b/behave/runner_util.py @@ -8,9 +8,11 @@ from behave.model import FileLocation from bisect import bisect from six import string_types +import codecs import glob import os.path import re +import six import sys @@ -257,6 +259,8 @@ def parse_file(cls, filename): if not os.path.isfile(filename): raise FileNotFoundError(filename) here = os.path.dirname(filename) or "." + # -- MAYBE BETTER: + # contents = codecs.open(filename, "utf-8").read() contents = open(filename).read() return cls.parse(contents, here) @@ -367,7 +371,7 @@ def make_undefined_step_snippet(step, language=None): schema = u"@%s(%s'%s')\ndef step_impl(context):\n" schema += u" raise NotImplementedError(%s'STEP: %s %s')\n\n" - snippet = schema % (step.step_type, prefix, step.name, + snippet = schema % (step.step_type, prefix, step.name, prefix, step.step_type.title(), step.name) return snippet diff --git a/behave/step_registry.py b/behave/step_registry.py index 18d6aac28..ff9a2c0bc 100644 --- a/behave/step_registry.py +++ b/behave/step_registry.py @@ -6,6 +6,7 @@ """ from __future__ import absolute_import +from behave.textutil import text as _text class AmbiguousStep(ValueError): @@ -27,25 +28,26 @@ def same_step_definition(step, other_string, other_location): step.location == other_location and other_location.filename != "") - def add_step_definition(self, keyword, string, func): + def add_step_definition(self, keyword, step_text, func): # TODO try to fix module dependencies to avoid this from behave import matchers, model step_location = model.Match.make_location(func) step_type = keyword.lower() + step_text = _text(step_text) step_definitions = self.steps[step_type] for existing in step_definitions: - if self.same_step_definition(existing, string, step_location): + if self.same_step_definition(existing, step_text, step_location): # -- EXACT-STEP: Same step function is already registered. # This may occur when a step module imports another one. return - elif existing.match(string): - message = '%s has already been defined in\n existing step %s' - new_step = u"@%s('%s')" % (step_type, string) + elif existing.match(step_text): + message = u'%s has already been defined in\n existing step %s' + new_step = u"@%s('%s')" % (step_type, step_text) existing.step_type = step_type existing_step = existing.describe() - existing_step += " at %s" % existing.location + existing_step += u" at %s" % existing.location raise AmbiguousStep(message % (new_step, existing_step)) - step_definitions.append(matchers.get_matcher(func, string)) + step_definitions.append(matchers.get_matcher(func, step_text)) def find_step_definition(self, step): candidates = self.steps[step.step_type] @@ -78,9 +80,9 @@ def find_match(self, step): def make_decorator(self, step_type): # pylint: disable=W0621 # W0621: 44,29:StepRegistry.make_decorator: Redefining 'step_type' .. - def decorator(string): + def decorator(step_text): def wrapper(func): - self.add_step_definition(step_type, string, func) + self.add_step_definition(step_type, step_text, func) return func return wrapper return decorator @@ -89,12 +91,12 @@ def wrapper(func): registry = StepRegistry() # -- Create the decorators -def setup_step_decorators(context=None, registry=registry): - if context is None: - context = globals() +def setup_step_decorators(run_context=None, registry=registry): + if run_context is None: + run_context = globals() for step_type in ('given', 'when', 'then', 'step'): step_decorator = registry.make_decorator(step_type) - context[step_type.title()] = context[step_type] = step_decorator + run_context[step_type.title()] = run_context[step_type] = step_decorator # ----------------------------------------------------------------------------- # MODULE INIT: diff --git a/behave/tag_expression.py b/behave/tag_expression.py index 24b3d19ca..0dd01691e 100644 --- a/behave/tag_expression.py +++ b/behave/tag_expression.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import six + class TagExpression(object): """ Tag expression, as logical boolean expression, to select @@ -101,5 +103,9 @@ def __str__(self): """Conversion back into string that represents this tag expression.""" and_parts = [] for or_terms in self.ands: - and_parts.append(",".join(or_terms)) - return " ".join(and_parts) + and_parts.append(u",".join(or_terms)) + return u" ".join(and_parts) + + if six.PY2: + __unicode__ = __str__ + __str__ = lambda self: self.__unicode__().encode("utf-8") diff --git a/behave/textutil.py b/behave/textutil.py index 95bcf9f3a..fc809699b 100644 --- a/behave/textutil.py +++ b/behave/textutil.py @@ -5,6 +5,7 @@ from __future__ import absolute_import import six +import sys def make_indentation(indent_size, part=u" "): @@ -29,7 +30,8 @@ def indent(text, prefix): elif lines and not lines[0].endswith("\n"): # -- TEXT LINES: Without trailing new-line. newline = u"\n" - return newline.join([prefix + six.text_type(line) for line in lines]) + # XXX return newline.join([prefix + six.text_type(line, errors="replace") + return newline.join([prefix + six.text_type(line) for line in lines]) def compute_words_maxsize(words): @@ -44,3 +46,39 @@ def compute_words_maxsize(words): if len(word) > max_size: max_size = len(word) return max_size + + +# -- MAYBE: def text(value, encoding=None, errors=None): +def text(value, encoding=None, errors=None): + """Convert into a unicode string. + + :param value: Value to convert into a unicode string (bytes, str, object). + :return: Unicode string + """ + if encoding is None: + encoding = "unicode-escape" + if errors is None: + errors = "replace" + + if isinstance(value, six.text_type): + # -- PASS-THROUGH UNICODE (efficiency): + return value + elif isinstance(value, six.binary_type): + return six.text_type(value, encoding, errors) + elif isinstance(value, bytes): + # -- MAYBE: filename, path, etc. + try: + return value.decode(sys.getfilesystemencoding()) + except UnicodeError: + return value.decode("utf-8", "replace") + else: + # -- CONVERT OBJECT TO TEXT: + try: + if six.PY2: + data = str(value) + text = six.text_type(data, "unicode-escape", "replace") + else: + text = six.text_type(value) + except UnicodeError as e: + text = six.text_type(e) + return text diff --git a/behave4cmd0/command_shell.py b/behave4cmd0/command_shell.py index c4944a741..bebf0ac06 100755 --- a/behave4cmd0/command_shell.py +++ b/behave4cmd0/command_shell.py @@ -86,11 +86,11 @@ def run(cls, command, cwd=".", **kwargs): command_result.command = command # -- BUILD COMMAND ARGS: - if isinstance(command, six.text_type): - if six.PY2: - command = codecs.encode(command) - else: - command = command.encode('utf-8').decode('unicode_escape') + if six.PY2 and isinstance(command, six.text_type): + # -- PREPARE-FOR: shlex.split() + # In PY2, shlex.split() requires bytes string (non-unicode). + # In PY3, shlex.split() accepts unicode string. + command = codecs.encode(command, "utf-8") cmdargs = shlex.split(command) # -- TRANSFORM COMMAND (optional) diff --git a/bin/behave b/bin/behave index 85432e787..c02e8d3d7 100755 --- a/bin/behave +++ b/bin/behave @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- from __future__ import absolute_import # -- ENSURE: Use local path during development. diff --git a/bin/behave_cmd.py b/bin/behave_cmd.py new file mode 100755 index 000000000..c02e8d3d7 --- /dev/null +++ b/bin/behave_cmd.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import absolute_import +# -- ENSURE: Use local path during development. +import sys +import os.path + +# ---------------------------------------------------------------------------- +# SETUP PATHS: +# ---------------------------------------------------------------------------- +NAME = "behave" +HERE = os.path.dirname(__file__) +TOP = os.path.join(HERE, "..") +if os.path.isdir(os.path.join(TOP, NAME)): + sys.path.insert(0, os.path.abspath(TOP)) + +# ---------------------------------------------------------------------------- +# BEHAVE-TWEAKS: +# ---------------------------------------------------------------------------- +def setup_behave(): + """ + Apply tweaks, extensions and patches to "behave". + """ + from behave.configuration import Configuration + # -- DISABLE: Timings to simplify issue.features/ tests. + # Configuration.defaults["format0"] = "pretty" + # Configuration.defaults["format0"] = "progress" + Configuration.defaults["show_timings"] = False + +def behave_main0(): + # from behave.configuration import Configuration + from behave.__main__ import main as behave_main + setup_behave() + return behave_main() + +# ---------------------------------------------------------------------------- +# MAIN: +# ---------------------------------------------------------------------------- +if __name__ == "__main__": + if "COVERAGE_PROCESS_START" in os.environ: + import coverage + coverage.process_startup() + sys.exit(behave_main0()) diff --git a/features/i18n.unicode_problems.feature b/features/i18n.unicode_problems.feature new file mode 100644 index 000000000..b61c8dbc2 --- /dev/null +++ b/features/i18n.unicode_problems.feature @@ -0,0 +1,429 @@ +@unicode +Feature: Internationalization (i18n) and Problems with Unicode Strings + + | POTENTIAL PROBLEM AREAS: + | * Feature, scenario, step names with problematic chars + | * Tags with problematic chars + | * step raises exception with problematic text (output capture) + | * step generates output with problematic and some step fails (stdout capture) + | * filenames with problematic chars: feature files, steps files + | + | CHECKED FORMATTERS and REPORTERS: + | * plain + | * pretty + | * junit (used via "behave.ini" defaults) + + + @setup + Scenario: Feature Setup + Given a new working directory + And a file named "behave.ini" with: + """ + [behave] + show_timings = false + show_skipped = false + show_source = true + junit = true + """ + And a file named "features/steps/passing_steps.py" with: + """ + from __future__ import unicode_literals + from behave import step + + @step('{word:w} step passes') + def step_passes(context, word): + pass + + @step('{word:w} step passes with "{text}"') + def step_passes_with_text(context, word, text): + pass + + @step('{word:w} step fails') + def step_fails(context, word): + assert False, "XFAIL" + + @step('{word:w} step fails with "{text}"') + def step_fails_with_text(context, word, text): + assert False, "XFAIL: "+ text + """ + And a file named "features/steps/step_write_output.py" with: + """ + from __future__ import print_function, unicode_literals + from behave import step + import six + + @step('I write text "{text}" to stdout') + def step_write_text(context, text): + if six.PY2 and isinstance(text, six.text_type): + text = text.encode("utf-8", "replace") + print(text) + + @step('I write bytes "{data}" to stdout') + def step_write_bytes(context, data): + if isinstance(data, six.text_type): + data = data.encode("unicode-escape", "replace") + print(data) + """ + + + Scenario Outline: Problematic scenario name: (case: passed, ) + Given a file named "features/scenario_name_problematic_and_pass.feature" with: + """ + Feature: + Scenario: + Given a step passes + """ + When I run "behave -f features/scenario_name_problematic_and_pass.feature" + Then it should pass with: + """ + 1 feature passed, 0 failed, 0 skipped + 1 scenario passed, 0 failed, 0 skipped + """ + And the command output should contain: + """ + Scenario: + """ + + Examples: + | format | scenario.name | + | plain | Café | + | pretty | Ärgernis ist überall | + + + Scenario Outline: Problematic scenario name: (case: failed, ) + Given a file named "features/scenario_name_problematic_and_fail.feature" with: + """ + Feature: + Scenario: + Given a step fails + """ + When I run "behave -f features/scenario_name_problematic_and_fail.feature" + Then it should fail with: + """ + 0 features passed, 1 failed, 0 skipped + 0 scenarios passed, 1 failed, 0 skipped + """ + And the command output should contain: + """ + Scenario: + """ + + Examples: + | format | scenario.name | + | plain | Café | + | pretty | Ärgernis ist überall | + + + Scenario Outline: Problematic step: (case: passed, ) + Given a file named "features/step_problematic_and_pass.feature" with: + """ + Feature: + Scenario: + Given a step passes with "" + """ + When I run "behave -f features/step_problematic_and_pass.feature" + Then it should pass with: + """ + 1 feature passed, 0 failed, 0 skipped + 1 scenario passed, 0 failed, 0 skipped + 1 step passed, 0 failed, 0 skipped, 0 undefined + """ + + Examples: + | format | step.text | + | plain | Café | + | pretty | Ärgernis ist überall | + + + Scenario Outline: Problematic step: (case: fail, ) + Given a file named "features/step_problematic_and_fail.feature" with: + """ + Feature: + Scenario: + Given a step fails with "" + """ + When I run "behave -f features/step_problematic_and_fail.feature" + Then it should fail with: + """ + 0 features passed, 1 failed, 0 skipped + 0 scenarios passed, 1 failed, 0 skipped + 0 steps passed, 1 failed, 0 skipped, 0 undefined + """ + + Examples: + | format | step.text | + | plain | Café | + | pretty | Ärgernis ist überall | + + + @problematic.feature_filename + Scenario Outline: Problematic feature filename: (case: pass, ) + Given a file named "features/_and_pass.feature" with: + """ + Feature: + Scenario: + Given a step passes + """ + When I run "behave -f features/_and_pass.feature" + Then it should pass with: + """ + 1 feature passed, 0 failed, 0 skipped + 1 scenario passed, 0 failed, 0 skipped + """ + + Examples: + | format | name | + | plain | Café | + | pretty | Ärgernis_ist_überall | + + + @problematic.feature_filename + Scenario Outline: Problematic feature filename: (case: fail, ) + Given a file named "features/_and_fail.feature" with: + """ + Feature: + Scenario: + Given a step fails + """ + When I run "behave -f features/_and_fail.feature" + Then it should fail with: + """ + 0 features passed, 1 failed, 0 skipped + 0 scenarios passed, 1 failed, 0 skipped + """ + + Examples: + | format | name | + | plain | Café | + | pretty | Ärgernis_ist_überall | + + + @problematic.step_filename + Scenario Outline: Problematic step filename: (case: pass, ) + + TEST-CONSTRAINT: Only one step file is used (= 1 name only). + Otherwise, duplicated steps occur (without cleanup in step directory). + + Given a file named "features/problematic_stepfile_and_pass.feature" with: + """ + Feature: + Scenario: + Given I use a weird step and pass + """ + And a file named "features/steps/step_pass_.py" with: + """ + from behave import step + + @step(u'I use a weird step and pass') + def step_weird_pass(context): + pass + """ + When I run "behave -f features/problematic_stepfile_and_pass.feature" + Then it should pass with: + """ + 1 feature passed, 0 failed, 0 skipped + 1 scenario passed, 0 failed, 0 skipped + """ + But note that "you should normally use only ASCII/latin-1 python filenames" + + Examples: + | format | name | + | plain | Ärgernis_ist_überall | + | pretty | Ärgernis_ist_überall | + + + @problematic.step_filename + Scenario Outline: Problematic step filename: (case: fail, ) + + TEST-CONSTRAINT: Only one step file is used (= 1 name only). + Otherwise, duplicated steps occur (without cleanup in step directory). + + Given a file named "features/problematic_stepfile_and_fail.feature" with: + """ + Feature: + Scenario: + Given I use a weird step and fail + """ + And a file named "features/steps/step_fail_.py" with: + """ + from behave import step + + @step(u'I use a weird step and fail') + def step_weird_fails(context): + assert False, "XFAIL-WEIRD" + """ + When I run "behave -f features/problematic_stepfile_and_fail.feature" + Then it should fail with: + """ + 0 features passed, 1 failed, 0 skipped + 0 scenarios passed, 1 failed, 0 skipped + """ + But note that "you should normally use only ASCII/latin-1 python filenames" + + Examples: + | format | name | + | plain | Ärgernis_ist_überall | + | pretty | Ärgernis_ist_überall | + + + @problematic.output + Scenario Outline: Problematic output: (case: pass, ) + Given a file named "features/problematic_output_and_pass.feature" with: + """ + Feature: + Scenario: + Given I write text "" to stdout + Then I write bytes "" to stdout + And a step passes + """ + When I run "behave -f --no-capture features/problematic_output_and_pass.feature" + Then it should pass with: + """ + 1 feature passed, 0 failed, 0 skipped + 1 scenario passed, 0 failed, 0 skipped + """ + + Examples: + | format | text | + | plain | Café | + | pretty | Ärgernis ist überall | + + + @problematic.output + Scenario Outline: Problematic output: (case: fail, ) + Given a file named "features/problematic_output_and_fail.feature" with: + """ + Feature: + Scenario: + Given I write text "" to stdout + Then I write bytes "" to stdout + And a step fails + """ + When I run "behave -f features/problematic_output_and_fail.feature" + Then it should fail with: + """ + 0 features passed, 1 failed, 0 skipped + 0 scenarios passed, 1 failed, 0 skipped + """ + And the command output should contain: + """ + + """ + + Examples: + | format | text | + | plain | Café | + | pretty | Ärgernis ist überall | + + + @problematic.tags + Scenario Outline: Problematic tag: (case: pass, ) + Given a file named "features/problematic_tag_and_pass.feature" with: + """ + Feature: + @ + Scenario: + Given a step passes + """ + When I run "behave -f features/problematic_tag_and_pass.feature" + Then it should pass with: + """ + 1 scenario passed, 0 failed, 0 skipped + """ + + Examples: + | format | tag | + | plain | tag.Café | + | pretty | tag.Ärgernis_ist_überall | + + + @problematic.tags + Scenario Outline: Problematic tag: (case: fail, ) + Given a file named "features/problematic_tag_and_fail.feature" with: + """ + Feature: + @ + Scenario: + Given a step fails + """ + When I run "behave -f features/problematic_tag_and_fail.feature" + Then it should fail with: + """ + 0 scenarios passed, 1 failed, 0 skipped + """ + + Examples: + | format | tag | + | plain | tag.Café | + | pretty | tag.Ärgernis_ist_überall | + + + @issue_0230 + Scenario Outline: Step assert fails with problematic chars (case: ) + + NOTE: Python2 fails silently without showing the failed step. + + Given a file named "features/steps/problematic_steps.py" with: + """ + from behave import step + + @step(u'{word:w} step fails with assert and non-ASCII text') + def step_fails_with_assert_and_problematic_text(context, word): + assert False, "XFAIL:"+ chr(190) +";" + """ + And a file named "features/assert_with_ptext.feature" with: + """ + Feature: + Scenario: + Given a step passes + When a step fails with assert and non-ASCII text + """ + When I run "behave -f features/assert_with_ptext.feature" + Then it should fail with: + """ + 0 scenarios passed, 1 failed, 0 skipped + 1 step passed, 1 failed, 0 skipped, 0 undefined + """ + And the command output should contain: + """ + Assertion Failed: XFAIL:¾; + """ + + Examples: + | format | + | plain | + | pretty | + + @issue_0226 + Scenario Outline: Step raises exception with problematic chars (case: ) + + Given a file named "features/steps/problematic_steps.py" with: + """ + from behave import step + + @step(u'{word:w} step fails with exception and non-ASCII text') + def step_fails_with_exception_and_problematic_text(context, word): + raise RuntimeError("FAIL:"+ chr(190) +";") + """ + And a file named "features/exception_with_ptext.feature" with: + """ + Feature: + Scenario: + Given a step passes + When a step fails with exception and non-ASCII text + """ + When I run "behave -f features/exception_with_ptext.feature" + Then it should fail with: + """ + 0 scenarios passed, 1 failed, 0 skipped + 1 step passed, 1 failed, 0 skipped, 0 undefined + """ + And the command output should contain: + """ + RuntimeError: FAIL:¾; + """ + + Examples: + | format | + | plain | + | pretty | diff --git a/features/step_param.builtin_types.with_integer.feature b/features/step_param.builtin_types.with_integer.feature index b34370f0d..0cae03d35 100644 --- a/features/step_param.builtin_types.with_integer.feature +++ b/features/step_param.builtin_types.with_integer.feature @@ -30,12 +30,13 @@ Feature: Parse integer data types in step parameters (type transformation) And a file named "features/steps/integer_param_steps.py" with: """ from behave import then, step + from six import integer_types class NotMatched(object): pass @step('a integer param with "{value:d}"') def step_integer_param_with(context, value): - assert type(value) is int + assert isinstance(value, integer_types), "value.type=%s" % type(value) context.value = value @step('a integer param with "{value}"') @@ -76,9 +77,8 @@ Feature: Parse integer data types in step parameters (type transformation) @then('the value should be {outcome} as integer number') def step_value_should_be_matched_as_number(context, outcome): - expected_type = int if outcome == "matched": - assert type(context.value) is expected_type, \ + assert isinstance(context.value, integer_types), \ "Unexpected type: %s" % type(context.value) else: assert context.value is NotMatched @@ -263,7 +263,7 @@ Feature: Parse integer data types in step parameters (type transformation) """ Feature: Use type "x" (hexadecimal number) for integer params - Scenario Outline: Good cases + Scenario Outline: Good cases: Given a hexadecimal number param with "" Then the value should be matched as integer number with "" diff --git a/features/steps/behave_context_steps.py b/features/steps/behave_context_steps.py index dd5f3070a..7ca73d77f 100644 --- a/features/steps/behave_context_steps.py +++ b/features/steps/behave_context_steps.py @@ -17,6 +17,7 @@ from __future__ import absolute_import from behave import given, then, step from hamcrest import assert_that, equal_to +import six # ----------------------------------------------------------------------------- # STEPS: @@ -57,10 +58,10 @@ def then_behave_context_should_contain_with_table(context): param_value = row["Value"] if param_value.startswith('"') and param_value.endswith('"'): param_value = param_value[1:-1] - actual = str(getattr(context, param_name, None)) + actual = six.text_type(getattr(context, param_name, None)) assert hasattr(context, param_name) assert_that(actual, equal_to(param_value)) @given(u'the behave context contains') def given_behave_context_contains_with_table(context): - then_behave_context_should_contain_with_table(context) \ No newline at end of file + then_behave_context_should_contain_with_table(context) diff --git a/features/steps/behave_select_files_steps.py b/features/steps/behave_select_files_steps.py index 4db73b7ce..431674efc 100644 --- a/features/steps/behave_select_files_steps.py +++ b/features/steps/behave_select_files_steps.py @@ -23,6 +23,7 @@ from hamcrest import assert_that, equal_to from copy import copy import re +import six # ----------------------------------------------------------------------------- # STEP UTILS: @@ -40,7 +41,7 @@ def select_files(self): selected = [] for filename in self.feature_files: if not self.config.exclude(filename): - selected.append(str(filename)) + selected.append(six.text_type(filename)) return selected # ----------------------------------------------------------------------------- diff --git a/issue.features/issue0226.feature b/issue.features/issue0226.feature new file mode 100644 index 000000000..d6675a59d --- /dev/null +++ b/issue.features/issue0226.feature @@ -0,0 +1,47 @@ +@issue +@unicode +Feature: UnicodeDecodeError in tracebacks (when an exception in a step implementation) + + | Exception with non-ASCII character is raised in a step implementation. + | UnicodeDecodeError occurs with: + | 'ascii' codec can't decode byte 0x82 in position 11: ordinal not in range(128) + | + | RELATED: + | * features/i18n.unicode_problems.feature + + @setup + Scenario: Feature Setup + Given a new working directory + And a file named "features/steps/steps.py" with: + """ + from behave import step + + @step(u'a step raises an exception with non-ASCII character "{char_code:d}"') + def step_raises_exception_with_non_ascii_text(context, char_code): + assert 0 <= char_code <= 255, "RANGE-ERROR: char_code=%s" % char_code + raise RuntimeError("FAIL:"+ chr(char_code) +";") + """ + + Scenario Outline: Syndrome with non-ASCII char (format=) + Given a file named "features/syndrome_0226_.feature" with: + """ + Feature: + Scenario: + Given a step raises an exception with non-ASCII character "" + """ + When I run "behave -f features/syndrome_0226_.feature" + Then it should fail with: + """ + 0 scenarios passed, 1 failed, 0 skipped + 0 steps passed, 1 failed, 0 skipped, 0 undefined + """ + And the command output should not contain "UnicodeDecodeError" + But the command output should contain: + """ + RuntimeError: FAIL: + """ + + Examples: + | format | char_code | + | plain | 130 | + | pretty | 190 | diff --git a/issue.features/issue0230.feature b/issue.features/issue0230.feature new file mode 100644 index 000000000..5309d9865 --- /dev/null +++ b/issue.features/issue0230.feature @@ -0,0 +1,46 @@ +@issue +@unicode +Feature: Assert with non-ASCII char causes UnicodeDecodeError + + | Failing assert with non-ASCII character in its message + | causes UnicodeDecodeError and silent exit in Python2. + | + | RELATED: + | * features/i18n.unicode_problems.feature + + @setup + Scenario: Feature Setup + Given a new working directory + And a file named "features/steps/steps.py" with: + """ + from behave import step + + @step(u'a step fails with non-ASCII character "{char_code:d}"') + def step_fails_with_non_ascii_text(context, char_code): + assert 0 <= char_code <= 255, "RANGE-ERROR: char_code=%s" % char_code + assert False, "FAIL:"+ chr(char_code) +";" + """ + + Scenario Outline: Syndrome with non-ASCII char (format=) + Given a file named "features/syndrome_0230_.feature" with: + """ + Feature: + Scenario: + Given a step fails with non-ASCII character "" + """ + When I run "behave -f features/syndrome_0230_.feature" + Then it should fail with: + """ + 0 scenarios passed, 1 failed, 0 skipped + 0 steps passed, 1 failed, 0 skipped, 0 undefined + """ + And the command output should not contain "UnicodeDecodeError" + But the command output should contain: + """ + Assertion Failed: FAIL: + """ + + Examples: + | format | char_code | + | plain | 130 | + | pretty | 190 | diff --git a/issue.features/issue0251.feature b/issue.features/issue0251.feature new file mode 100644 index 000000000..68bde73d5 --- /dev/null +++ b/issue.features/issue0251.feature @@ -0,0 +1,15 @@ +@issue +@unicode +Feature: UnicodeDecodeError in model.Step (when step fails) + + | Output of failing step contains non-ASCII characters. + | + | RELATED: + | * features/i18n.unicode_problems.feature + + + @reuse.colocated_test + Scenario: + Given I use the current directory as working directory + When I run "behave -f plain --tags=@setup,@problematic.output features/i18n.unicode_problems.feature" + Then it should pass diff --git a/test/test_model.py b/test/test_model.py index 288821c37..6f70e6379 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -7,13 +7,17 @@ from behave import step_registry from mock import Mock, patch from nose.tools import * -import re +import six import sys import unittest from six.moves import range from six.moves import zip +# -- CONVENIENCE-ALIAS: +_text = six.text_type + + class TestFeatureRun(unittest.TestCase): def setUp(self): self.runner = Mock() @@ -748,7 +752,7 @@ def test_compare_not_equal(self): def test_compare_less_than(self): for locations in [self.ordered_locations1, self.ordered_locations2]: for value1, value2 in zip(locations, locations[1:]): - assert value1 < value2, "FAILED: %s < %s" % (str(value1), str(value2)) + assert value1 < value2, "FAILED: %s < %s" % (_text(value1), _text(value2)) assert value1 != value2 def test_compare_less_than_with_string(self): @@ -756,33 +760,33 @@ def test_compare_less_than_with_string(self): for value1, value2 in zip(locations, locations[1:]): if value1.filename == value2.filename: continue - assert value1 < value2.filename, "FAILED: %s < %s" % (str(value1), str(value2.filename)) - assert value1.filename < value2, "FAILED: %s < %s" % (str(value1.filename), str(value2)) + assert value1 < value2.filename, "FAILED: %s < %s" % (_text(value1), _text(value2.filename)) + assert value1.filename < value2, "FAILED: %s < %s" % (_text(value1.filename), _text(value2)) def test_compare_greater_than(self): for locations in [self.ordered_locations1, self.ordered_locations2]: for value1, value2 in zip(locations, locations[1:]): - assert value2 > value1, "FAILED: %s > %s" % (str(value2), str(value1)) + assert value2 > value1, "FAILED: %s > %s" % (_text(value2), _text(value1)) assert value2 != value1 def test_compare_less_or_equal(self): for value1, value2 in self.same_locations: - assert value1 <= value2, "FAILED: %s <= %s" % (str(value1), str(value2)) + assert value1 <= value2, "FAILED: %s <= %s" % (_text(value1), _text(value2)) assert value1 == value2 for locations in [self.ordered_locations1, self.ordered_locations2]: for value1, value2 in zip(locations, locations[1:]): - assert value1 <= value2, "FAILED: %s <= %s" % (str(value1), str(value2)) + assert value1 <= value2, "FAILED: %s <= %s" % (_text(value1), _text(value2)) assert value1 != value2 def test_compare_greater_or_equal(self): for value1, value2 in self.same_locations: - assert value2 >= value1, "FAILED: %s >= %s" % (str(value2), str(value1)) + assert value2 >= value1, "FAILED: %s >= %s" % (_text(value2), _text(value1)) assert value2 == value1 for locations in [self.ordered_locations1, self.ordered_locations2]: for value1, value2 in zip(locations, locations[1:]): - assert value2 >= value1, "FAILED: %s >= %s" % (str(value2), str(value1)) + assert value2 >= value1, "FAILED: %s >= %s" % (_text(value2), _text(value1)) assert value2 != value1 def test_filename_should_be_same_as_self(self): @@ -795,7 +799,7 @@ def test_string_conversion(self): expected = u"%s:%s" % (location.filename, location.line) if location.line is None: expected = location.filename - assert str(location) == expected + assert six.text_type(location) == expected def test_repr_conversion(self): for location in self.ordered_locations2: diff --git a/test/test_runner.py b/test/test_runner.py index 66e7c6ba7..ba517805d 100644 --- a/test/test_runner.py +++ b/test/test_runner.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, print_function, with_statement from collections import defaultdict from six import StringIO +import six import os.path import sys import warnings @@ -18,6 +19,10 @@ from behave.formatter.base import StreamOpener +# -- CONVENIENCE-ALIAS: +_text = six.text_type + + class TestContext(unittest.TestCase): def setUp(self): r = Mock() @@ -294,7 +299,7 @@ def test_execute_steps_with_failing_step(self): try: result = self.context.execute_steps(doc) except AssertionError as e: - ok_("FAILED SUB-STEP: When a step fails" in str(e)) + ok_("FAILED SUB-STEP: When a step fails" in _text(e)) def test_execute_steps_with_undefined_step(self): doc = u''' @@ -306,7 +311,7 @@ def test_execute_steps_with_undefined_step(self): try: result = self.context.execute_steps(doc) except AssertionError as e: - ok_("UNDEFINED SUB-STEP: When a step is undefined" in str(e)) + ok_("UNDEFINED SUB-STEP: When a step is undefined" in _text(e)) def test_execute_steps_with_text(self): doc = u'''