Commit ae074e49 by Paul Bonser

add multiline string support

Includes tests, implementation, and documentation
parent 1e9cb104
...@@ -13,6 +13,7 @@ Lettuce documentation contents ...@@ -13,6 +13,7 @@ Lettuce documentation contents
intro/wtf intro/wtf
tutorial/simple tutorial/simple
tutorial/tables tutorial/tables
tutorial/multiline
tutorial/scenario-outlines tutorial/scenario-outlines
tutorial/steps-from-step-definitions tutorial/steps-from-step-definitions
reference/features reference/features
......
...@@ -104,6 +104,7 @@ walkthrough ...@@ -104,6 +104,7 @@ walkthrough
* :ref:`write your first feature <tutorial-simple>` * :ref:`write your first feature <tutorial-simple>`
* :ref:`handling data with tables <tutorial-tables>` * :ref:`handling data with tables <tutorial-tables>`
* :ref:`multi-line strings <tutorial-multiline>`
* :ref:`don't repeat yourself, meet scenario outlines <tutorial-scenario-outlines>` * :ref:`don't repeat yourself, meet scenario outlines <tutorial-scenario-outlines>`
* :ref:`clean up your spec definitions, calling one step from another <tutorial-steps-from-step-definitions>` * :ref:`clean up your spec definitions, calling one step from another <tutorial-steps-from-step-definitions>`
......
.. _tutorial-multiline:
multi-line strings
===========================
Now imagine you are writing an application which manipulates
strings. When writing the tests, you may find yourself wanting to put
multi-line strings in your steps.
Multi-line strings will do the trick
.. highlight:: ruby
::
Feature: Split a string into multiple lines on spaces
In order to make strings more readable
As a user
I want to have words split into their own lines
Scenario: Split small-ish string
Given I have the string "one two three four five"
When I ask to have the string split into lines
Then I should see the following:
"""
one
two
three
four
five
"""
A line with nothing but three quotes (""") is used to indicate the
beginning and the end of a multi-line string.
Now, let's define a step that knows how to use this.
.. highlight:: python
::
from lettuce import step
@step('I should see the following:')
def i_should_see_the_following(step):
assert step.multiline == """one
two
three
four
five"""
Nice and straightforward.
Notice that leading spaces are stripped, and there's not a newline at
the beginning or end. This is due to the way that the parser strips
blank lines and leading and trailing whitespace.
If you need blank lines leading or trailing whitespace, you can
include lines which start and/or end with double quote, and they will
be concatenated with the other multiline lines, with the quotes
stripped off and their whitespace preserved.
For example
.. highlight:: ruby
::
Feature: Split a string into multiple lines on spaces
In order to make strings more readable
As a user
I want to have words split into their own lines
Scenario: Split small-ish string
Given I have the string "one two three four five"
When I ask to have the string split into lines
Then I should see the following:
"""
" one
" two "
" three "
" four "
" five "
"
"""
Which we can verify like so:
.. highlight:: python
::
from lettuce import step
@step('I should see the following:')
def i_should_see_the_following(step):
assert step.multiline == '\n'.append([
' one',
' two ',
' three ',
' four ',
' five ',
''])
Admittedly, this is a hack, but there's no clean way to preserve
whitespace in only one section of a feature definition in the current
parser implementation.
Note that the first line doesn't have any whitespace at the end, and
thus doesn't need to have a quote at the end of it.
Also note that if you want a double quote at the beginning of a line
in your string, you'll have to start your line with two double quotes,
since the first one will be stripped off.
\ No newline at end of file
...@@ -149,7 +149,7 @@ class Step(object): ...@@ -149,7 +149,7 @@ class Step(object):
self.sentence = sentence self.sentence = sentence
self.original_sentence = sentence self.original_sentence = sentence
self._remaining_lines = remaining_lines self._remaining_lines = remaining_lines
keys, hashes = self._parse_remaining_lines(remaining_lines) keys, hashes, self.multiline = self._parse_remaining_lines(remaining_lines)
self.keys = tuple(keys) self.keys = tuple(keys)
self.hashes = list(hashes) self.hashes = list(hashes)
...@@ -250,7 +250,9 @@ class Step(object): ...@@ -250,7 +250,9 @@ class Step(object):
return u'<Step: "%s">' % self.sentence return u'<Step: "%s">' % self.sentence
def _parse_remaining_lines(self, lines): def _parse_remaining_lines(self, lines):
return strings.parse_hashes(lines) multiline = strings.parse_multiline(lines)
keys, hashes = strings.parse_hashes(lines)
return keys, hashes, multiline
def _get_match(self, ignore_case): def _get_match(self, ignore_case):
matched, func = None, lambda: None matched, func = None, lambda: None
...@@ -386,19 +388,28 @@ class Step(object): ...@@ -386,19 +388,28 @@ class Step(object):
parsed, but must be well-formed under a regular step sentence. parsed, but must be well-formed under a regular step sentence.
""" """
invalid_first_line_error = '\nFirst line of step "%(line)s" is in table form.' invalid_first_line_error = '\nFirst line of step "%s" is in %s form.'
if lines and strings.wise_startswith(lines[0], u'|'): if lines and strings.wise_startswith(lines[0], u'|'):
raise LettuceSyntaxError( raise LettuceSyntaxError(
None, None,
invalid_first_line_error % lines[0]) invalid_first_line_error % (lines[0], 'table'))
if lines and strings.wise_startswith(lines[0], u'"""'):
raise LettuceSyntaxError(
None,
invalid_first_line_error % (lines[0], 'multiline'))
# Select only lines that aren't end-to-end whitespace # Select only lines that aren't end-to-end whitespace
only_whitspace = re.compile('^\s*$') only_whitspace = re.compile('^\s*$')
lines = filter(lambda x: not only_whitspace.match(x), lines) lines = filter(lambda x: not only_whitspace.match(x), lines)
step_strings = [] step_strings = []
in_multiline = False
for line in lines: for line in lines:
if strings.wise_startswith(line, u"|"): if strings.wise_startswith(line, u'"""'):
in_multiline = not in_multiline
step_strings[-1] += "\n%s" % line
elif strings.wise_startswith(line, u"|") or in_multiline:
step_strings[-1] += "\n%s" % line step_strings[-1] += "\n%s" % line
else: else:
step_strings.append(line) step_strings.append(line)
......
...@@ -130,3 +130,17 @@ def parse_hashes(lines): ...@@ -130,3 +130,17 @@ def parse_hashes(lines):
hashes.append(dict(zip(keys, values))) hashes.append(dict(zip(keys, values)))
return keys, hashes return keys, hashes
def parse_multiline(lines):
multilines = []
in_multiline = False
for line in lines:
if line == '"""':
in_multiline = not in_multiline
elif in_multiline:
if line.startswith('"'):
line = line[1:]
if line.endswith('"'):
line = line[:-1]
multilines.append(line)
return u'\n'.join(multilines)
...@@ -23,7 +23,44 @@ I_HAVE_TASTY_BEVERAGES = """I have the following tasty beverages in my freezer: ...@@ -23,7 +23,44 @@ I_HAVE_TASTY_BEVERAGES = """I have the following tasty beverages in my freezer:
""" """
I_DIE_HAPPY = "I shall die with love in my heart" I_DIE_HAPPY = "I shall die with love in my heart"
MULTI_LINE = '''
I have a string like so:
"""
This is line one
and this is line two
and this is line three
and this is line four,
with spaces at the beginning
"""
'''
MULTI_LINE_WHITESPACE = '''
I have a string like so:
"""
This is line one
and this is line two
and this is line three
" and this is line four,
"
" with spaces at the beginning
and spaces at the end "
"""
'''
INVALID_MULTI_LINE = '''
"""
invalid one...
"""
'''
from lettuce.core import Step from lettuce.core import Step
from lettuce.exceptions import LettuceSyntaxError
from lettuce import strings
from nose.tools import assert_equals from nose.tools import assert_equals
from tests.asserts import * from tests.asserts import *
import string import string
...@@ -118,3 +155,42 @@ def test_can_parse_two_ordinary_steps(): ...@@ -118,3 +155,42 @@ def test_can_parse_two_ordinary_steps():
assert isinstance(steps[1], Step) assert isinstance(steps[1], Step)
assert_equals(steps[0].sentence, I_DIE_HAPPY) assert_equals(steps[0].sentence, I_DIE_HAPPY)
assert_equals(steps[1].sentence, I_LIKE_VEGETABLES) assert_equals(steps[1].sentence, I_LIKE_VEGETABLES)
def test_cannot_start_with_multiline():
"It should raise an error when a step starts with a multiline string"
lines = strings.get_stripped_lines(INVALID_MULTI_LINE)
try:
step = Step.many_from_lines(lines)
except LettuceSyntaxError:
return
assert False, "LettuceSyntaxError not raised"
def test_multiline_is_part_of_previous_step():
"It should correctly parse a multi-line string as part of the preceding step"
lines = strings.get_stripped_lines(MULTI_LINE)
steps = Step.many_from_lines(lines)
print steps
assert_equals(len(steps), 1)
assert isinstance(steps[0], Step)
assert_equals(steps[0].sentence, 'I have a string like so:')
def test_multiline_is_parsed():
step = Step.from_string(MULTI_LINE)
assert_equals(step.sentence, 'I have a string like so:')
assert_equals(step.multiline, u"""This is line one
and this is line two
and this is line three
and this is line four,
with spaces at the beginning""")
def test_multiline_with_whitespace():
step = Step.from_string(MULTI_LINE_WHITESPACE)
assert_equals(step.sentence, 'I have a string like so:')
assert_equals(step.multiline, u"""This is line one
and this is line two
and this is line three
and this is line four,
with spaces at the beginning
and spaces at the end """)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment