Skip to content

Commit 34b7f96

Browse files
Mark90youtux
authored andcommitted
Escape quote in generation (#327)
* Escape single quotes in generated test names * Escape single, double, triple-single and triple-double quoted (doc)strings in generated code
1 parent 9dc09bc commit 34b7f96

File tree

5 files changed

+170
-7
lines changed

5 files changed

+170
-7
lines changed

pytest_bdd/generation.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from mako.lookup import TemplateLookup
77
import py
88

9-
from .scenario import find_argumented_step_fixture_name, make_python_name
9+
from .scenario import find_argumented_step_fixture_name, make_python_docstring, make_python_name, make_string_literal
1010
from .steps import get_step_fixture_name
1111
from .feature import get_features
1212
from .types import STEP_TYPES
@@ -47,7 +47,12 @@ def generate_code(features, scenarios, steps):
4747
grouped_steps = group_steps(steps)
4848
template = template_lookup.get_template("test.py.mak")
4949
return template.render(
50-
features=features, scenarios=scenarios, steps=grouped_steps, make_python_name=make_python_name
50+
features=features,
51+
scenarios=scenarios,
52+
steps=grouped_steps,
53+
make_python_name=make_python_name,
54+
make_python_docstring=make_python_docstring,
55+
make_string_literal=make_string_literal,
5156
)
5257

5358

pytest_bdd/scenario.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,16 @@ def make_python_name(string):
292292
return re.sub(ALPHA_REGEX, "", string).lower()
293293

294294

295+
def make_python_docstring(string):
296+
"""Make a python docstring literal out of a given string."""
297+
return u'"""{}."""'.format(string.replace(u'"""', u'\\"\\"\\"'))
298+
299+
300+
def make_string_literal(string):
301+
"""Make python string literal out of a given string."""
302+
return u"'{}'".format(string.replace(u"'", u"\\'"))
303+
304+
295305
def get_python_name_generator(name):
296306
"""Generate a sequence of suitable python names out of given arbitrary string name."""
297307
python_name = make_python_name(name)

pytest_bdd/scripts.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import re
66

77
import glob2
8+
import six
89

910
from .generation import generate_code, parse_feature_files
1011

@@ -48,7 +49,10 @@ def print_generated_code(args):
4849
"""Print generated test code for the given filenames."""
4950
features, scenarios, steps = parse_feature_files(args.files)
5051
code = generate_code(features, scenarios, steps)
51-
print (code)
52+
if six.PY2:
53+
print (code.encode("utf-8"))
54+
else:
55+
print (code)
5256

5357

5458
def main():

pytest_bdd/templates/test.py.mak

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,16 @@ from pytest_bdd import (
1212

1313
% endif
1414
% for scenario in sorted(scenarios, key=lambda scenario: scenario.name):
15-
@scenario('${scenario.feature.rel_filename}', '${scenario.name}')
15+
@scenario('${scenario.feature.rel_filename}', ${ make_string_literal(scenario.name)})
1616
def test_${ make_python_name(scenario.name)}():
17-
"""${scenario.name}."""
17+
${make_python_docstring(scenario.name)}
1818

1919

2020
% endfor
2121
% for step in steps:
22-
@${step.type}('${step.name}')
22+
@${step.type}(${ make_string_literal(step.name)})
2323
def ${ make_python_name(step.name)}():
24-
"""${step.name}."""
24+
${make_python_docstring(step.name)}
2525
raise NotImplementedError
2626
% if not loop.last:
2727

tests/scripts/test_generate.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
# coding=utf-8
12
"""Test code generation command."""
23
import os
34
import sys
45
import textwrap
56

7+
import six
8+
69
from pytest_bdd.scripts import main
710

811
PATH = os.path.dirname(__file__)
@@ -60,3 +63,144 @@ def my_list_should_be_1():
6063
u"'", u"'"
6164
)
6265
)
66+
67+
68+
def test_generate_with_quotes(testdir):
69+
"""Test that code generation escapes quote characters properly."""
70+
testdir.makefile(
71+
".feature",
72+
generate_with_quotes=textwrap.dedent(
73+
'''\
74+
Feature: Handling quotes in code generation
75+
76+
Scenario: A step definition with quotes should be escaped as needed
77+
Given I have a fixture with 'single' quotes
78+
And I have a fixture with "double" quotes
79+
And I have a fixture with single-quote \'\'\'triple\'\'\' quotes
80+
And I have a fixture with double-quote """triple""" quotes
81+
82+
When I generate the code
83+
84+
Then The generated string should be written
85+
'''
86+
),
87+
)
88+
89+
result = testdir.run("pytest-bdd", "generate", "generate_with_quotes.feature")
90+
assert result.stdout.str() == textwrap.dedent(
91+
'''\
92+
# coding=utf-8
93+
"""Handling quotes in code generation feature tests."""
94+
95+
from pytest_bdd import (
96+
given,
97+
scenario,
98+
then,
99+
when,
100+
)
101+
102+
103+
@scenario('generate_with_quotes.feature', 'A step definition with quotes should be escaped as needed')
104+
def test_a_step_definition_with_quotes_should_be_escaped_as_needed():
105+
"""A step definition with quotes should be escaped as needed."""
106+
107+
108+
@given('I have a fixture with "double" quotes')
109+
def i_have_a_fixture_with_double_quotes():
110+
"""I have a fixture with "double" quotes."""
111+
raise NotImplementedError
112+
113+
114+
@given('I have a fixture with \\'single\\' quotes')
115+
def i_have_a_fixture_with_single_quotes():
116+
"""I have a fixture with 'single' quotes."""
117+
raise NotImplementedError
118+
119+
120+
@given('I have a fixture with double-quote """triple""" quotes')
121+
def i_have_a_fixture_with_doublequote_triple_quotes():
122+
"""I have a fixture with double-quote \\"\\"\\"triple\\"\\"\\" quotes."""
123+
raise NotImplementedError
124+
125+
126+
@given('I have a fixture with single-quote \\'\\'\\'triple\\'\\'\\' quotes')
127+
def i_have_a_fixture_with_singlequote_triple_quotes():
128+
"""I have a fixture with single-quote \'\'\'triple\'\'\' quotes."""
129+
raise NotImplementedError
130+
131+
132+
@when('I generate the code')
133+
def i_generate_the_code():
134+
"""I generate the code."""
135+
raise NotImplementedError
136+
137+
138+
@then('The generated string should be written')
139+
def the_generated_string_should_be_written():
140+
"""The generated string should be written."""
141+
raise NotImplementedError
142+
'''
143+
)
144+
145+
146+
def test_unicode_characters(testdir):
147+
"""Test generating code with unicode characters.
148+
149+
Primary purpose is to ensure compatibility with Python2.
150+
"""
151+
152+
testdir.makefile(
153+
".feature",
154+
unicode_characters=textwrap.dedent(
155+
"""\
156+
# coding=utf-8
157+
Feature: Generating unicode characters
158+
159+
Scenario: Calculating the circumference of a circle
160+
Given We have a circle
161+
When We want to know its circumference
162+
Then We calculate 2 * ℼ * 𝑟
163+
"""
164+
),
165+
)
166+
167+
result = testdir.run("pytest-bdd", "generate", "unicode_characters.feature")
168+
expected_output = textwrap.dedent(
169+
u'''\
170+
# coding=utf-8
171+
"""Generating unicode characters feature tests."""
172+
173+
from pytest_bdd import (
174+
given,
175+
scenario,
176+
then,
177+
when,
178+
)
179+
180+
181+
@scenario('unicode_characters.feature', 'Calculating the circumference of a circle')
182+
def test_calculating_the_circumference_of_a_circle():
183+
"""Calculating the circumference of a circle."""
184+
185+
186+
@given('We have a circle')
187+
def we_have_a_circle():
188+
"""We have a circle."""
189+
raise NotImplementedError
190+
191+
192+
@when('We want to know its circumference')
193+
def we_want_to_know_its_circumference():
194+
"""We want to know its circumference."""
195+
raise NotImplementedError
196+
197+
198+
@then('We calculate 2 * ℼ * 𝑟')
199+
def {function_with_unicode_chars}():
200+
"""We calculate 2 * ℼ * 𝑟."""
201+
raise NotImplementedError
202+
'''.format(
203+
function_with_unicode_chars=u"we_calculate_2__ℼ__𝑟" if not six.PY2 else u"we_calculate_2____"
204+
)
205+
)
206+
assert result.stdout.str() == expected_output

0 commit comments

Comments
 (0)