Skip to content

Commit b1e6fe7

Browse files
authored
fix UnicodeDecodeError when exception contains non-ascii characters (#158)
1 parent 32877f2 commit b1e6fe7

File tree

9 files changed

+249
-24
lines changed

9 files changed

+249
-24
lines changed

allure-behave/features/step.feature

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
11
Feature: Step
22

3+
Scenario: Failed step
4+
Given feature definition
5+
"""
6+
Feature: Step status
7+
8+
Scenario: Scenario with failed step
9+
Given simple failed step
10+
"""
11+
When I run behave with allure formatter
12+
Then allure report has a scenario with name "Scenario with failed step"
13+
And scenario contains step "Given simple failed step"
14+
And this step has "failed" status
15+
And this step has status details with message "AssertionError: Assert message"
16+
17+
18+
Scenario: Broken step
19+
Given feature definition
20+
"""
21+
Feature: Step status
22+
23+
Scenario: Scenario with broken step
24+
Given simple broken step
25+
"""
26+
When I run behave with allure formatter
27+
Then allure report has a scenario with name "Scenario with broken step"
28+
And scenario contains step "Given simple broken step"
29+
And this step has "broken" status
30+
And this step has status details with message "ZeroDivisionError"
31+
32+
333
Scenario: Step text parameter
434
Given feature definition
535
"""

allure-behave/features/steps/behave_steps.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import os
2-
import sys
32
from tempfile import mkdtemp
43
from allure_commons_test.report import AllureReport
54
from behave.parser import Parser

allure-behave/features/steps/dummy_steps.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@ def step_impl(*args, **kwargs):
2222
assert False, 'Assert message'
2323

2424

25+
@given(u'провальный шаг')
26+
def step_impl(*args, **kwargs):
27+
assert False, u'Фиаско!'
28+
29+
30+
@given(u'провальный шаг с ascii')
31+
def step_impl(*args, **kwargs):
32+
assert False, 'Фиаско!'
33+
34+
35+
@given(u'проходящий шаг')
36+
def step_impl(*args, **kwargs):
37+
pass
38+
39+
2540
@given(u'broken step')
2641
@given(u'{what} broken step')
2742
@given(u'broken step {where}')

allure-behave/features/steps/report_steps.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from allure_commons_test.result import has_step
77
from allure_commons_test.result import has_attachment
88
from allure_commons_test.result import has_parameter
9+
from allure_commons_test.result import has_status_details
10+
from allure_commons_test.result import with_status_message
911
from allure_commons_test.container import has_container
1012
from allure_commons_test.container import has_before, has_after
1113
from allure_commons_test.label import has_severity
@@ -80,6 +82,14 @@ def step_status(context, item, status):
8082
assert_that(context.allure_report, matcher())
8183

8284

85+
@then(u'{item} has status details with message "{message}"')
86+
@then(u'this {item} has status details with message "{message}"')
87+
def step_status(context, item, message):
88+
context_matcher = getattr(context, item)
89+
matcher = partial(context_matcher, has_status_details, with_status_message, message)
90+
assert_that(context.allure_report, matcher())
91+
92+
8393
@then(u'scenario has "{severity}" severity')
8494
@then(u'this scenario has "{severity}" severity')
8595
def step_severity(context, severity):

allure-behave/features/unicode.feature

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Feature: Language
22
Scenario: Use russian
3-
Given feature definition ru
3+
Given feature definition ru
44
"""
55
Свойство: Юникод
66
@@ -18,4 +18,32 @@ Feature: Language
1818
And scenario contains step "Пусть всегда будет солнце"
1919
And scenario contains step "Пусть всегда будет небо"
2020
And scenario contains step "Пусть всегда будет мама"
21-
And scenario contains step "Пусть всегда буду я"
21+
And scenario contains step "Пусть всегда буду я"
22+
23+
24+
Scenario: Assert message in step
25+
Given feature definition ru
26+
"""
27+
Свойство: Юникод
28+
29+
Сценарий: Ошибка с utf-8 сообщением
30+
Допустим провальный шаг
31+
"""
32+
When I run behave with allure formatter with options "--lang ru"
33+
Then allure report has a scenario with name "Ошибка с utf-8 сообщением"
34+
And scenario contains step "Допустим провальный шаг"
35+
And step has status details with message "AssertionError: Фиаско!"
36+
37+
38+
Scenario: ASCII assert message in step
39+
Given feature definition ru
40+
"""
41+
Свойство: Юникод
42+
43+
Сценарий: Ошибка с utf-8 сообщением
44+
Допустим провальный шаг с ascii
45+
"""
46+
When I run behave with allure formatter with options "--lang ru"
47+
Then allure report has a scenario with name "Ошибка с utf-8 сообщением"
48+
And scenario contains step "Допустим провальный шаг с ascii"
49+
And step has status details with message "AssertionError: Фиаско!"

allure-behave/src/utils.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
14
from behave.model import ScenarioOutline
25
from behave.runner_util import make_undefined_step_snippet
36
from allure_commons.types import Severity
47
from allure_commons.model2 import Status, Parameter, Label
58
from allure_commons.model2 import StatusDetails
69
from allure_commons.utils import md5
7-
from allure_commons.utils import represent
8-
import traceback
10+
from allure_commons.utils import format_exception, format_traceback
911

1012
STATUS = {
1113
'passed': Status.PASSED,
@@ -28,7 +30,7 @@ def scenario_history_id(scenario):
2830
parts = [scenario.feature.name, scenario.name]
2931
if scenario._row:
3032
row = scenario._row
31-
parts.extend([u'{name}={value}'.format(name=name, value=value) for name, value in zip(row.headings, row.cells)])
33+
parts.extend(['{name}={value}'.format(name=name, value=value) for name, value in zip(row.headings, row.cells)])
3234
return md5(*parts)
3335

3436

@@ -71,10 +73,8 @@ def fixture_status(exception, exc_traceback):
7173

7274
def fixture_status_details(exception, exc_traceback):
7375
if exception:
74-
message = u','.join(map(str, exception.args))
75-
message = u'{name}: {message}'.format(name=exception.__class__.__name__, message=message)
76-
trace = u'\n'.join(traceback.format_tb(exc_traceback)) if exc_traceback else None
77-
return StatusDetails(message=message, trace=trace)
76+
return StatusDetails(message=format_exception(type(exception), exception),
77+
trace=format_traceback(exc_traceback))
7878
return None
7979

8080

@@ -87,12 +87,10 @@ def step_status(result):
8787

8888
def step_status_details(result):
8989
if result.exception:
90-
message = u','.join(map(lambda s: u'%s' % s, result.exception.args))
91-
message = u'{name}: {message}'.format(name=result.exception.__class__.__name__, message=message)
92-
trace = u'\n'.join(traceback.format_tb(result.exc_traceback)) if result.exc_traceback else None
93-
return StatusDetails(message=message, trace=trace)
90+
return StatusDetails(message=format_exception(type(result.exception), result.exception),
91+
trace=format_traceback(result.exc_traceback))
9492
elif result.status == 'undefined':
95-
message = u'\nYou can implement step definitions for undefined steps with these snippets:\n\n'
93+
message = '\nYou can implement step definitions for undefined steps with these snippets:\n\n'
9694
message += make_undefined_step_snippet(result)
9795
return StatusDetails(message=message)
9896

allure-python-commons-test/src/result.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
from hamcrest import all_of, anything
6666
from hamcrest import equal_to, not_none
6767
from hamcrest import has_entry, has_item
68+
from hamcrest import contains_string
6869

6970

7071
def has_step(name, *matchers):
@@ -129,7 +130,7 @@ def has_status_details(*matchers):
129130

130131

131132
def with_status_message(message):
132-
return has_entry('message', message)
133+
return has_entry('message', contains_string(message))
133134

134135

135136
def has_history_id():
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from __future__ import absolute_import, division, print_function
2+
import types
3+
4+
5+
def format_exception_only(etype, value):
6+
"""Format the exception part of a traceback.
7+
8+
The arguments are the exception type and value such as given by
9+
sys.last_type and sys.last_value. The return value is a list of
10+
strings, each ending in a newline.
11+
12+
Normally, the list contains a single string; however, for
13+
SyntaxError exceptions, it contains several lines that (when
14+
printed) display detailed information about where the syntax
15+
error occurred.
16+
17+
The message indicating which exception occurred is always the last
18+
string in the list.
19+
20+
"""
21+
22+
# An instance should not have a meaningful value parameter, but
23+
# sometimes does, particularly for string exceptions, such as
24+
# >>> raise string1, string2 # deprecated
25+
#
26+
# Clear these out first because issubtype(string1, SyntaxError)
27+
# would throw another exception and mask the original problem.
28+
if (isinstance(etype, BaseException) or
29+
isinstance(etype, types.InstanceType) or
30+
etype is None or type(etype) is str): # noqa: E129
31+
return [_format_final_exc_line(etype, value)]
32+
33+
stype = etype.__name__
34+
35+
if not issubclass(etype, SyntaxError):
36+
return [_format_final_exc_line(stype, value)]
37+
38+
# It was a syntax error; show exactly where the problem was found.
39+
lines = []
40+
try:
41+
msg, (filename, lineno, offset, badline) = value.args
42+
except Exception:
43+
pass
44+
else:
45+
filename = filename or "<string>"
46+
lines.append(' File "%s", line %d\n' % (filename, lineno))
47+
if badline is not None:
48+
lines.append(' %s\n' % badline.strip())
49+
if offset is not None:
50+
caretspace = badline.rstrip('\n')[:offset].lstrip()
51+
# non-space whitespace (likes tabs) must be kept for alignment
52+
caretspace = ((c.isspace() and c or ' ') for c in caretspace)
53+
# only three spaces to account for offset1 == pos 0
54+
lines.append(' %s^\n' % ''.join(caretspace))
55+
value = msg
56+
57+
lines.append(_format_final_exc_line(stype, value))
58+
return lines
59+
60+
61+
def _format_final_exc_line(etype, value):
62+
"""Return a list of a single line -- normal case for format_exception_only"""
63+
valuestr = _some_str(value)
64+
if value is None or not valuestr:
65+
line = "%s\n" % etype
66+
else:
67+
line = "%s: %s\n" % (etype, valuestr)
68+
return line
69+
70+
71+
def _some_str(value):
72+
try:
73+
return str(value)
74+
except UnicodeError:
75+
try:
76+
value = unicode(value)
77+
return value.encode('utf-8', 'replace')
78+
except Exception:
79+
pass
80+
return '<unprintable %s object>' % type(value).__name__

0 commit comments

Comments
 (0)