Skip to content

Commit

Permalink
FIX: Test regression on Windows
Browse files Browse the repository at this point in the history
Gherkin parser:

* Strip trailing whitespace in multi-line text
  REASON: Whitespace normalization (may contain carriage-return on Windows)
  • Loading branch information
jenisys committed Apr 3, 2023
1 parent 2f45157 commit 0085ff8
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 18 deletions.
83 changes: 66 additions & 17 deletions behave/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ def parse(self, text, filename=None):

for line in text.split("\n"):
self.line += 1
if not line.strip() and self.state != "multiline":
if not line.strip() and self.state != "multiline_text":
# -- SKIP EMPTY LINES, except in multiline string args.
continue
self.action(line)
Expand Down Expand Up @@ -381,7 +381,7 @@ def ask_parse_failure_oracle(self, line):
return None

def action(self, line):
if line.strip().startswith("#") and self.state != "multiline":
if line.strip().startswith("#") and self.state != "multiline_text":
if self.state != "init" or self.tags or self.variant != "feature":
return

Expand Down Expand Up @@ -584,8 +584,24 @@ def action_steps(self, line):
# pylint: disable=R0911
# R0911 Too many return statements (8/6)
stripped = line.lstrip()
# if self.statement.steps:
# # -- ENSURE: Multi-line text follows a step.
# if stripped.startswith('"""') or stripped.startswith("'''"):
# # -- CASE: Multi-line text (docstring) after a step detected.
# self.state = "multiline_text"
# self.multiline_start = self.line
# self.multiline_terminator = stripped[:3]
# self.multiline_leading = line.index(stripped[0])
# return True

if stripped.startswith('"""') or stripped.startswith("'''"):
self.state = "multiline"
# -- CASE: Multi-line text (docstring) after a step detected.
# REQUIRE: Multi-line text follows a step.
if not self.statement.steps:
raise ParserError("Multi-line text before any step",
self.line, self.filename)

self.state = "multiline_text"
self.multiline_start = self.line
self.multiline_terminator = stripped[:3]
self.multiline_leading = line.index(stripped[0])
Expand All @@ -602,25 +618,47 @@ def action_steps(self, line):
return True

if line.startswith("|"):
assert self.statement.steps, "TABLE-START without step detected."
# -- CASE: TABLE-START detected for data-table of a step
# OLD: assert self.statement.steps, "TABLE-START without step detected"
if not self.statement.steps:
raise ParserError("TABLE-START without step detected",
self.line, self.filename)
self.state = "table"
return self.action_table(line)

return False

def action_multiline(self, line):
def action_multiline_text(self, line):
"""Parse remaining multi-line/docstring text below a step
after the triple-quotes were detected:
* triple-double-quotes or
* triple-single-quotes
Leading and trailing triple-quotes must be the same.
:param line: Parsed line, as part of a multi-line text (as string).
"""
if line.strip().startswith(self.multiline_terminator):
step = self.statement.steps[-1]
step.text = model.Text(u"\n".join(self.lines), u"text/plain",
self.multiline_start)
if step.name.endswith(":"):
step.name = step.name[:-1]
# -- CASE: Handle the end of a multi-line text part.
# Store the multi-line text in the step object (and continue).
this_step = self.statement.steps[-1]
text = u"\n".join(self.lines)
this_step.text = model.Text(text, u"text/plain", self.multiline_start)
if this_step.name.endswith(":"):
this_step.name = this_step.name[:-1]

# -- RESET INTERNALS: For next step
self.lines = []
self.multiline_terminator = None
self.state = "steps"
self.state = "steps" # NEXT-STATE: Accept additional step(s).
return True

self.lines.append(line[self.multiline_leading:])
# -- SPECIAL CASE: Strip trailing whitespace (whitespace normalization).
# HINT: Required for Windows line-endings, like "\r\n", etc.
text_line = line[self.multiline_leading:].rstrip()
self.lines.append(text_line)

# -- BETTER DIAGNOSTICS: May remove non-whitespace in execute_steps()
removed_line_prefix = line[:self.multiline_leading]
if removed_line_prefix.strip():
Expand All @@ -631,35 +669,46 @@ def action_multiline(self, line):
return True

def action_table(self, line):
line = line.strip()
"""Parse a table, with pipe-separated columns:
* Data table of a step (after the step line)
* Examples table of a ScenarioOutline
"""
line = line.strip()
if not line.startswith("|"):
# -- CASE: End-of-table detected
if self.examples:
# -- CASE: Examples table of a ScenarioOutline
self.examples.table = self.table
self.examples = None
else:
# -- CASE: Data table of a step
step = self.statement.steps[-1]
step.table = self.table
if step.name.endswith(":"):
step.name = step.name[:-1]

# -- RESET: Parameters for parsing the next step(s).
self.table = None
self.state = "steps"
return self.action_steps(line)

if not re.match(r"^(|.+)\|$", line):
logger = logging.getLogger("behave")
logger.warning(u"Malformed table row at %s: line %i", self.feature.filename, self.line)
logger.warning(u"Malformed table row at %s: line %i",
self.feature.filename, self.line)

# -- SUPPORT: Escaped-pipe(s) in Gherkin cell values.
# Search for pipe(s) that are not preceeded with an escape char.
cells = [cell.replace("\\|", "|").strip()
for cell in re.split(r"(?<!\\)\|", line[1:-1])]
if self.table is None:
# -- CASE: First row of the table
self.table = model.Table(cells, self.line)
else:
# -- CASE: Following rows of the table
if len(cells) != len(self.table.headings):
raise ParserError(u"Malformed table", self.line, self.filename)
# MAYBE: self.filename)
self.table.add_row(cells, self.line)
return True

Expand Down Expand Up @@ -689,7 +738,7 @@ def parse_rule(self, text, filename=None):

for line in text.split("\n"):
self.line += 1
if not line.strip() and self.state != "multiline":
if not line.strip() and self.state != "multiline_text":
# -- SKIP EMPTY LINES, except in multiline string args.
continue
self.action(line)
Expand Down Expand Up @@ -775,7 +824,7 @@ def parse_steps(self, text, filename=None):

for line in text.split("\n"):
self.line += 1
if not line.strip() and self.state != "multiline":
if not line.strip() and self.state != "multiline_text":
# -- SKIP EMPTY LINES, except in multiline string args.
continue
self.action(line)
Expand Down
26 changes: 25 additions & 1 deletion tests/unit/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1711,7 +1711,7 @@ def test_parse_steps_when_last_step_has_a_table(self):
("then", "Then", "the last step has a final table", None, table2),
])

def test_parse_steps_with_malformed_table(self):
def test_parse_steps_with_malformed_table_fails(self):
text = u'''
Given a step with a malformed table:
| Name | City |
Expand All @@ -1720,3 +1720,27 @@ def test_parse_steps_with_malformed_table(self):
'''.lstrip()
with pytest.raises(parser.ParserError):
parser.parse_steps(text)

def test_parse_steps_with_multiline_text_before_any_step_fails(self):
text = u'''
"""
BAD MULTI-LINE TEXT (before any step)
"""
Given another step
'''.lstrip()
with pytest.raises(parser.ParserError) as exc:
parser.parse_steps(text)

assert exc.match("Multi-line text before any step")

def test_parse_steps_with_datatable_before_any_step_fails(self):
text = u'''
| name | birthyear |
| Alice | 1980 |
| Bob | 2005 |
Given another step
'''.lstrip()
with pytest.raises(parser.ParserError) as exc:
parser.parse_steps(text)

assert exc.match("TABLE-START without step detected")

0 comments on commit 0085ff8

Please sign in to comment.