From a043aeb6d7d8972e96d81344429c0915d3383975 Mon Sep 17 00:00:00 2001 From: Ryan Casperson Date: Tue, 23 Jul 2019 11:12:04 -0600 Subject: [PATCH 1/7] Implement tests that reveal the scenario description issue --- tests/feature/description.feature | 4 +++ tests/feature/scenario_descriptions.feature | 36 +++++++++++++++++++++ tests/feature/test_description.py | 4 +++ tests/feature/test_scenario_descriptions.py | 4 +++ 4 files changed, 48 insertions(+) create mode 100644 tests/feature/scenario_descriptions.feature create mode 100644 tests/feature/test_scenario_descriptions.py diff --git a/tests/feature/description.feature b/tests/feature/description.feature index 08c8bd54..1747241c 100644 --- a/tests/feature/description.feature +++ b/tests/feature/description.feature @@ -9,4 +9,8 @@ Feature: Description Scenario: Description + Also, the scenario can have a description. + + It goes here between the scenario name + and the first step. Given I have a bar diff --git a/tests/feature/scenario_descriptions.feature b/tests/feature/scenario_descriptions.feature new file mode 100644 index 00000000..ef91a69a --- /dev/null +++ b/tests/feature/scenario_descriptions.feature @@ -0,0 +1,36 @@ +Feature: Scenario Descriptions + + Just as this feature has a feature descriptions, + which you are currently reading, + users should be able to add a description for a scenario + + The scenarios in this feature file are skipped intentionally. + This issue: https://github.com/pytest-dev/pytest-bdd/issues/311 + shows that scenario descriptions caused new tests to be created. + Those tests didn't have the markers attached to them, + so without the fix for that issue, + those new, rogue tests would fail with StepDefinitionNotFoundError. + + If these scenarios' steps were implemented, + then all of these new, rogue tests would execute and pass. + + @skip + Scenario: A scenario with a description + Here is a scenario description that is just one line + + Given something + When I do something + Then something happens + + @skip + Scenario: A scenario with a multiline description + Here is a scenario description. + It can also have more lines. + + There can be line breaks too. + You just can't start a line with a real step keyword, + otherwise it will be treated as a step. + + Given something + When I do something + Then something happens diff --git a/tests/feature/test_description.py b/tests/feature/test_description.py index a1577e61..7c894250 100644 --- a/tests/feature/test_description.py +++ b/tests/feature/test_description.py @@ -17,5 +17,9 @@ def test(): Some description goes here.""" + assert test.__scenario__.description == """Also, the scenario can have a description. + +It goes here between the scenario name +and the first step.""" test(request) diff --git a/tests/feature/test_scenario_descriptions.py b/tests/feature/test_scenario_descriptions.py new file mode 100644 index 00000000..a396482d --- /dev/null +++ b/tests/feature/test_scenario_descriptions.py @@ -0,0 +1,4 @@ +from pytest_bdd import scenarios + +# See feature file for reasons for executing (skipping) these tests +scenarios("scenario_descriptions.feature") From 64d2d774c532bbbd25ab6dbf33428e83e9c3729a Mon Sep 17 00:00:00 2001 From: Ryan Casperson Date: Tue, 23 Jul 2019 13:45:49 -0600 Subject: [PATCH 2/7] Allow for scenario descriptions to be present fixes #311 This treats scenario descriptions the same way it does feature descriptions. --- pytest_bdd/feature.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index e09a4dd3..0e0e9bc7 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -60,6 +60,8 @@ STEP_PARAM_RE = re.compile(r"\<(.+?)\>") COMMENT_RE = re.compile(r'(^|(?<=\s))#') +TYPES_WITH_DESCRIPTIONS = [types.FEATURE, types.SCENARIO, types.SCENARIO_OUTLINE] + def get_step_type(line): """Detect step type by the beginning of the line. @@ -291,7 +293,8 @@ def __init__(self, basedir, filename, encoding="utf-8", strict_gherkin=True): multiline_step = False stripped_line = line.strip() clean_line = strip_comments(line) - if not clean_line and (not prev_mode or prev_mode not in types.FEATURE): + if not clean_line and (not prev_mode or prev_mode not in TYPES_WITH_DESCRIPTIONS): + # Blank lines are included in feature and scenario descriptions continue mode = get_step_type(clean_line) or mode @@ -340,6 +343,13 @@ def __init__(self, basedir, filename, encoding="utf-8", strict_gherkin=True): # Remove Feature, Given, When, Then, And keyword, parsed_line = parse_line(clean_line) if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]: + # Lines between the scenario declaration + # and the scenario's first step line + # are considered part of the scenario description. + if scenario and not keyword: + scenario.add_description_line(parsed_line) + continue + tags = get_tags(prev_line) self.scenarios[parsed_line] = scenario = Scenario(self, parsed_line, line_number, tags=tags) elif mode == types.BACKGROUND: @@ -435,6 +445,7 @@ def __init__(self, feature, name, line_number, example_converters=None, tags=Non self.tags = tags or set() self.failed = False self.test_function = None + self._description_lines = [] def add_step(self, step): """Add step to the scenario. @@ -456,6 +467,22 @@ def steps(self): result.extend(self._steps) return result + def add_description_line(self, description_line): + """Add a description line to the scenario. + + :param str description_line: + """ + self._description_lines.append(description_line) + + @property + def description(self): + """Get the scenario's description. + + :return: The scenario description + """ + return u"\n".join(self._description_lines).strip() + + @property def params(self): """Get parameter names. From 8fa23ca305c9d76ff64aa6ae24cb4f994bb51731 Mon Sep 17 00:00:00 2001 From: Ryan Casperson Date: Tue, 13 Aug 2019 11:02:01 -0400 Subject: [PATCH 3/7] (fix): pycodestyle: too many blank lines --- pytest_bdd/feature.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index 0e0e9bc7..be180cbb 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -482,7 +482,6 @@ def description(self): """ return u"\n".join(self._description_lines).strip() - @property def params(self): """Get parameter names. From 80c0416767ed1009edfbeb899bf094748dff7f94 Mon Sep 17 00:00:00 2001 From: Ryan Casperson Date: Tue, 3 Dec 2019 15:24:33 -0500 Subject: [PATCH 4/7] Update tests to utilize pytester plugin --- tests/feature/scenario_descriptions.feature | 36 -------------------- tests/feature/test_description.py | 37 ++++++++++++++++++++- tests/feature/test_scenario_descriptions.py | 4 --- 3 files changed, 36 insertions(+), 41 deletions(-) delete mode 100644 tests/feature/scenario_descriptions.feature delete mode 100644 tests/feature/test_scenario_descriptions.py diff --git a/tests/feature/scenario_descriptions.feature b/tests/feature/scenario_descriptions.feature deleted file mode 100644 index ef91a69a..00000000 --- a/tests/feature/scenario_descriptions.feature +++ /dev/null @@ -1,36 +0,0 @@ -Feature: Scenario Descriptions - - Just as this feature has a feature descriptions, - which you are currently reading, - users should be able to add a description for a scenario - - The scenarios in this feature file are skipped intentionally. - This issue: https://github.com/pytest-dev/pytest-bdd/issues/311 - shows that scenario descriptions caused new tests to be created. - Those tests didn't have the markers attached to them, - so without the fix for that issue, - those new, rogue tests would fail with StepDefinitionNotFoundError. - - If these scenarios' steps were implemented, - then all of these new, rogue tests would execute and pass. - - @skip - Scenario: A scenario with a description - Here is a scenario description that is just one line - - Given something - When I do something - Then something happens - - @skip - Scenario: A scenario with a multiline description - Here is a scenario description. - It can also have more lines. - - There can be line breaks too. - You just can't start a line with a real step keyword, - otherwise it will be treated as a step. - - Given something - When I do something - Then something happens diff --git a/tests/feature/test_description.py b/tests/feature/test_description.py index 732de9be..4645ff85 100644 --- a/tests/feature/test_description.py +++ b/tests/feature/test_description.py @@ -2,6 +2,30 @@ from pytest_bdd import scenario +test_file_contents = """ +import pytest +from pytest_bdd import given, scenario + +def test_descriptions(request): + @scenario( + "descriptions.feature", + "Description", + ) + def test(): + pass + + test(request) + +@given("I have a bar") +def nothing(): + pass + +""" + +with open("./tests/feature/description.feature") as f: + feature_file_contents = f.read() + + def test_description(request): """Test description for the feature.""" @@ -18,9 +42,20 @@ def test(): Some description goes here.""" ) - assert test.__scenario__.description == """Also, the scenario can have a description. + assert ( + test.__scenario__.description + == """Also, the scenario can have a description. It goes here between the scenario name and the first step.""" + ) test(request) + + +def test_scenarios_are_created_when_they_have_scenario_descriptions(testdir): + testdir.makepyfile(test_descriptions=test_file_contents) + testdir.makefile(".feature", descriptions=feature_file_contents) + + result = testdir.runpytest() + result.assert_outcomes(passed=1) diff --git a/tests/feature/test_scenario_descriptions.py b/tests/feature/test_scenario_descriptions.py deleted file mode 100644 index a396482d..00000000 --- a/tests/feature/test_scenario_descriptions.py +++ /dev/null @@ -1,4 +0,0 @@ -from pytest_bdd import scenarios - -# See feature file for reasons for executing (skipping) these tests -scenarios("scenario_descriptions.feature") From 684c5dde75d0e8c7dacddd81fbb7cd93bd4f46d2 Mon Sep 17 00:00:00 2001 From: Ryan Casperson Date: Tue, 3 Dec 2019 15:27:05 -0500 Subject: [PATCH 5/7] (refactor): Extract some constants to clean up test code --- tests/feature/test_description.py | 38 +++++++++++++++---------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/tests/feature/test_description.py b/tests/feature/test_description.py index 4645ff85..b0b72b71 100644 --- a/tests/feature/test_description.py +++ b/tests/feature/test_description.py @@ -2,7 +2,7 @@ from pytest_bdd import scenario -test_file_contents = """ +TEST_FILE_CONTENTS = """ import pytest from pytest_bdd import given, scenario @@ -23,39 +23,37 @@ def nothing(): """ with open("./tests/feature/description.feature") as f: - feature_file_contents = f.read() + FEATURE_FILE_CONTENTS = f.read() - -def test_description(request): - """Test description for the feature.""" - - @scenario("description.feature", "Description") - def test(): - pass - - assert ( - test.__scenario__.feature.description - == """In order to achieve something +EXPECTED_FEATURE_DESCRIPTION = """In order to achieve something I want something Because it will be cool Some description goes here.""" - ) - assert ( - test.__scenario__.description - == """Also, the scenario can have a description. + +EXPECTED_SCENARIO_DESCRIPTION = """Also, the scenario can have a description. It goes here between the scenario name and the first step.""" - ) + + +def test_description(request): + """Test description for the feature.""" + + @scenario("description.feature", "Description") + def test(): + pass + + assert test.__scenario__.feature.description == EXPECTED_FEATURE_DESCRIPTION + assert test.__scenario__.description == EXPECTED_SCENARIO_DESCRIPTION test(request) def test_scenarios_are_created_when_they_have_scenario_descriptions(testdir): - testdir.makepyfile(test_descriptions=test_file_contents) - testdir.makefile(".feature", descriptions=feature_file_contents) + testdir.makepyfile(test_descriptions=TEST_FILE_CONTENTS) + testdir.makefile(".feature", descriptions=FEATURE_FILE_CONTENTS) result = testdir.runpytest() result.assert_outcomes(passed=1) From b3d464bef1af2accbef493c6be85fc6232c318ed Mon Sep 17 00:00:00 2001 From: Ryan Casperson Date: Fri, 1 May 2020 16:12:02 -0400 Subject: [PATCH 6/7] Improve description formatting This leave description lines unchanged so that raw rST can be put in them. It also prevent comments from being included in descriptions. --- pytest_bdd/feature.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index acc275a7..0ff8f5c2 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -350,7 +350,9 @@ def __init__(self, basedir, filename, encoding="utf-8", strict_gherkin=True): self.line_number = line_number self.tags = get_tags(prev_line) elif prev_mode == types.FEATURE: - description.append(clean_line) + # Do not include comments in descriptions + if not stripped_line.startswith("#"): + description.append(line) else: raise exceptions.FeatureError( "Multiple features are not allowed in a single feature file", @@ -368,7 +370,10 @@ def __init__(self, basedir, filename, encoding="utf-8", strict_gherkin=True): # and the scenario's first step line # are considered part of the scenario description. if scenario and not keyword: - scenario.add_description_line(parsed_line) + # Do not include comments in descriptions + if stripped_line.startswith("#"): + continue + scenario.add_description_line(line) continue tags = get_tags(prev_line) @@ -416,7 +421,7 @@ def __init__(self, basedir, filename, encoding="utf-8", strict_gherkin=True): target.add_step(step) prev_line = clean_line - self.description = u"\n".join(description).strip() + self.description = u"\n".join(description) @classmethod def get_feature(cls, base_path, filename, encoding="utf-8", strict_gherkin=True): @@ -499,7 +504,7 @@ def description(self): :return: The scenario description """ - return u"\n".join(self._description_lines).strip() + return u"\n".join(self._description_lines) @property def params(self): From 344b430e3b65a5c98819ee099bf6a0c4b254b71a Mon Sep 17 00:00:00 2001 From: Ryan Casperson Date: Fri, 8 May 2020 12:21:04 -0400 Subject: [PATCH 7/7] Update tests to validate non-stripped descriptions --- tests/feature/test_description.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/tests/feature/test_description.py b/tests/feature/test_description.py index b0b72b71..8a69bd0f 100644 --- a/tests/feature/test_description.py +++ b/tests/feature/test_description.py @@ -25,17 +25,24 @@ def nothing(): with open("./tests/feature/description.feature") as f: FEATURE_FILE_CONTENTS = f.read() -EXPECTED_FEATURE_DESCRIPTION = """In order to achieve something -I want something -Because it will be cool - - -Some description goes here.""" - -EXPECTED_SCENARIO_DESCRIPTION = """Also, the scenario can have a description. - -It goes here between the scenario name -and the first step.""" +EXPECTED_FEATURE_DESCRIPTION = ( + "\n" + " In order to achieve something\n" + " I want something\n" + " Because it will be cool\n" + "\n" + "\n" + " Some description goes here.\n" +) + + +EXPECTED_SCENARIO_DESCRIPTION = ( + "\n" + " Also, the scenario can have a description.\n" + "\n" + " It goes here between the scenario name\n" + " and the first step." +) def test_description(request):