diff --git a/README.rst b/README.rst index 2a35b437f..442efd3a0 100644 --- a/README.rst +++ b/README.rst @@ -637,6 +637,33 @@ This is allowed as long as parameter names do not clash: | carrots | | tomatoes | +To not repeat steps as in example above you could want store your data in sequent Examples sections: + + +.. code-block:: gherkin + + Feature: Outline + + Examples: + | start | eat | left | + | 12 | 5 | 7 | + | 5 | 4 | 1 | + + Scenario Outline: Eat food + Given there are + When I eat + Then I should have + + Examples: Fruits + | food | + | oranges | + | apples | + + Examples: Vegetables + | food | + | carrots | + | tomatoes | + Organizing your scenarios ------------------------- diff --git a/pytest_bdd/parser.py b/pytest_bdd/parser.py index 8cbfbb145..0faf28c9e 100644 --- a/pytest_bdd/parser.py +++ b/pytest_bdd/parser.py @@ -3,9 +3,16 @@ import textwrap import typing from collections import OrderedDict +from itertools import chain, product, zip_longest +from typing import List + +import pytest +from attr import Factory, attrib, attrs, validate from . import exceptions, types +if typing.TYPE_CHECKING: + from _pytest.mark import ParameterSet SPLIT_LINE_RE = re.compile(r"(?") COMMENT_RE = re.compile(r"(^|(?<=\s))#") @@ -83,7 +90,9 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> "Feat """ abs_filename = os.path.abspath(os.path.join(basedir, filename)) rel_filename = os.path.join(os.path.basename(basedir), filename) - feature = Feature( + build_node: typing.Union[Feature, ScenarioTemplate] + build_example_table: typing.Optional[ExampleTable] = None + feature = build_node = Feature( scenarios=OrderedDict(), filename=abs_filename, rel_filename=rel_filename, @@ -150,41 +159,51 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> "Feat keyword, parsed_line = parse_line(clean_line) if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]: tags = get_tags(prev_line) - feature.scenarios[parsed_line] = scenario = ScenarioTemplate( + feature.scenarios[parsed_line] = scenario = build_node = ScenarioTemplate( feature=feature, name=parsed_line, line_number=line_number, tags=tags ) elif mode == types.BACKGROUND: feature.background = Background(feature=feature, line_number=line_number) - elif mode == types.EXAMPLES: - mode = types.EXAMPLES_HEADERS - (scenario or feature).examples.line_number = line_number - elif mode == types.EXAMPLES_VERTICAL: - mode = types.EXAMPLE_LINE_VERTICAL - (scenario or feature).examples.line_number = line_number + elif mode in [types.EXAMPLES, types.EXAMPLES_VERTICAL]: + if mode == types.EXAMPLES: + mode, ExampleTableBuilder = types.EXAMPLES_HEADERS, ExampleTableColumns + else: + mode, ExampleTableBuilder = types.EXAMPLE_LINE_VERTICAL, ExampleTableRows + _, table_name = parse_line(clean_line) + build_example_table = ExampleTableBuilder(name=table_name or None, line_number=line_number) + build_node.examples.example_tables.append(build_example_table) elif mode == types.EXAMPLES_HEADERS: - (scenario or feature).examples.set_param_names([l for l in split_line(parsed_line) if l]) mode = types.EXAMPLE_LINE + build_example_table.example_params = [l for l in split_line(parsed_line) if l] + try: + validate(build_example_table) + except exceptions.ExamplesNotValidError as exc: + node_message_prefix = "Scenario" if scenario else "Feature" + message = f"{node_message_prefix} has not valid examples. {exc.args[0]}" + raise exceptions.FeatureError(message, line_number, clean_line, filename) from exc elif mode == types.EXAMPLE_LINE: - (scenario or feature).examples.add_example([l for l in split_line(stripped_line)]) - elif mode == types.EXAMPLE_LINE_VERTICAL: - param_line_parts = [l for l in split_line(stripped_line)] try: - (scenario or feature).examples.add_example_row(param_line_parts[0], param_line_parts[1:]) + build_example_table.examples += [[*split_line(stripped_line)]] except exceptions.ExamplesNotValidError as exc: - if scenario: - raise exceptions.FeatureError( - f"Scenario has not valid examples. {exc.args[0]}", - line_number, - clean_line, - filename, - ) - else: - raise exceptions.FeatureError( - f"Feature has not valid examples. {exc.args[0]}", - line_number, - clean_line, - filename, + node_message_prefix = "Scenario" if scenario else "Feature" + message = f"{node_message_prefix} has not valid examples. {exc.args[0]}" + raise exceptions.FeatureError(message, line_number, clean_line, filename) from exc + elif mode == types.EXAMPLE_LINE_VERTICAL: + try: + param, *examples = split_line(stripped_line) + except ValueError: + pass + else: + try: + build_example_table: ExampleTableRows + build_example_table.example_params += [param] + build_example_table.examples_transposed += [examples] + validate(build_example_table) + except exceptions.ExamplesNotValidError as exc: + message = "{node} has not valid examples. {original_message}".format( + node="Scenario" if scenario else "Feature", original_message=exc.args[0] ) + raise exceptions.FeatureError(message, line_number, clean_line, filename) from exc elif mode and mode not in (types.FEATURE, types.TAG): step = Step(name=parsed_line, type=mode, indent=line_indent, line_number=line_number, keyword=keyword) if feature.background and not scenario: @@ -258,13 +277,29 @@ def render(self, context: typing.Mapping[str, typing.Any]) -> "Scenario": ] return Scenario(feature=self.feature, name=self.name, line_number=self.line_number, steps=steps, tags=self.tags) + @property + def example_parametrizations(self) -> "typing.Optional[typing.List[ParameterSet]]": + feature_scenario_examples_combinations = product(self.feature.examples, self.examples) + + def examples(): + for feature_examples, scenario_examples in feature_scenario_examples_combinations: + common_param_names = set(feature_examples.keys()).intersection(scenario_examples.keys()) + if all( + feature_examples[param_name] == scenario_examples[param_name] for param_name in common_param_names + ): + result = {**feature_examples, **scenario_examples} + if result != {}: + yield result + + return [pytest.param(example, id="-".join(example.values())) for example in examples()] + def validate(self): """Validate the scenario. :raises ScenarioValidationError: when scenario is not valid """ params = frozenset(sum((list(step.params) for step in self.steps), [])) - example_params = set(self.examples.example_params + self.feature.examples.example_params) + example_params = set(self.examples.example_params).union(self.feature.examples.example_params) if params and example_params and params != example_params: raise exceptions.ScenarioExamplesNotValidError( """Scenario "{}" in the feature "{}" has not valid examples. """ @@ -384,67 +419,85 @@ def add_step(self, step): self.steps.append(step) +@attrs class Examples: + example_tables: List["ExampleTable"] = attrib(default=Factory(list)) - """Example table.""" + def __iter__(self) -> typing.Iterable[typing.Dict[str, typing.Any]]: + if any(self.example_tables): + yield from chain.from_iterable(self.example_tables) + else: + # We must make sure that we always have at least one element, otherwise + # examples cartesian product will be empty + yield {} - def __init__(self): - """Initialize examples instance.""" - self.example_params = [] - self.examples = [] - self.vertical_examples = [] - self.line_number = None - self.name = None + @property + def example_params(self): + return set(chain.from_iterable(set(table.example_params) for table in self.example_tables)) - def set_param_names(self, keys): - """Set parameter names. - :param names: `list` of `string` parameter names. - """ - self.example_params = [str(key) for key in keys] +@attrs +class ExampleTable: + """Example table.""" - def add_example(self, values): - """Add example. + examples: list + examples_transposed: list + example_params = attrib(default=Factory(list)) + + @example_params.validator + def unique(self, attribute, value): + unique_items = set() + for item in value: + if item in unique_items: + raise exceptions.ExamplesNotValidError( + f"""Example rows should contain unique parameters. "{item}" appeared more than once""" + ) + unique_items.add(item) + return True - :param values: `list` of `string` parameter values. - """ - self.examples.append(values) + line_number = attrib(default=None) + name = attrib(default=None) + tags = attrib(default=Factory(list)) - def add_example_row(self, param, values): - """Add example row. + def __iter__(self) -> typing.Iterable[typing.Dict[str, typing.Any]]: + examples = self.examples + for example in examples: + assert len(self.example_params) == len(example) + yield dict(zip(self.example_params, example)) - :param param: `str` parameter name - :param values: `list` of `string` parameter values - """ - if param in self.example_params: - raise exceptions.ExamplesNotValidError( - f"""Example rows should contain unique parameters. "{param}" appeared more than once""" - ) - self.example_params.append(param) - self.vertical_examples.append(values) + def __bool__(self): + """Bool comparison.""" + return bool(self.examples) - def as_contexts(self) -> typing.Iterable[typing.Dict[str, typing.Any]]: - param_count = len(self.example_params) - if self.vertical_examples and not self.examples: - for value_index in range(len(self.vertical_examples[0])): - example = [] - for param_index in range(param_count): - example.append(self.vertical_examples[param_index][value_index]) - self.examples.append(example) - if not self.examples: - return +@attrs +class ExampleTableRows(ExampleTable): + examples_transposed = attrib(default=Factory(list)) - header, rows = self.example_params, self.examples + @examples_transposed.validator + def each_row_contains_same_count_of_values(self, attribute, value): + if value: + if not all(len(value[0]) == len(item) for item in value): + raise exceptions.ExamplesNotValidError( + f"""All example columns in Examples: Vertical must have same count of values""" + ) + return True - for row in rows: - assert len(header) == len(row) + @property + def examples(self): + return list(zip_longest(*self.examples_transposed)) - yield dict(zip(header, row)) - def __bool__(self): - """Bool comparison.""" - return bool(self.vertical_examples or self.examples) +@attrs +class ExampleTableColumns(ExampleTable): + examples = attrib(default=Factory(list)) + + @examples.validator + def each_row_contains_same_count_of_values(self, attribute, value): + if value: + if not (all(len(value[0]) == len(item) for item in value) and (len(value[0]) != len(self.example_params))): + raise exceptions.ExamplesNotValidError(f"""All example rows must have same count of values""") + return True def get_tags(line): diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index c9446e38e..3174c8b02 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -24,8 +24,6 @@ from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path if typing.TYPE_CHECKING: - from _pytest.mark.structures import ParameterSet - from .parser import Feature, Scenario, ScenarioTemplate PYTHON_REPLACE_REGEX = re.compile(r"\W") @@ -175,8 +173,8 @@ def scenario_wrapper(request, _pytest_bdd_example): fixture_values = [request.getfixturevalue(arg) for arg in args] return fn(*fixture_values) - example_parametrizations = collect_example_parametrizations(templated_scenario) - if example_parametrizations is not None: + example_parametrizations = templated_scenario.example_parametrizations + if example_parametrizations: # Parametrize the scenario outlines scenario_wrapper = pytest.mark.parametrize( "_pytest_bdd_example", @@ -194,27 +192,6 @@ def scenario_wrapper(request, _pytest_bdd_example): return decorator -def collect_example_parametrizations( - templated_scenario: "ScenarioTemplate", -) -> "typing.Optional[typing.List[ParameterSet]]": - # We need to evaluate these iterators and store them as lists, otherwise - # we won't be able to do the cartesian product later (the second iterator will be consumed) - feature_contexts = list(templated_scenario.feature.examples.as_contexts()) - scenario_contexts = list(templated_scenario.examples.as_contexts()) - - contexts = [ - {**feature_context, **scenario_context} - # We must make sure that we always have at least one element in each list, otherwise - # the cartesian product will result in an empty list too, even if one of the 2 sets - # is non empty. - for feature_context in feature_contexts or [{}] - for scenario_context in scenario_contexts or [{}] - ] - if contexts == [{}]: - return None - return [pytest.param(context, id="-".join(context.values())) for context in contexts] - - def scenario(feature_name: str, scenario_name: str, encoding: str = "utf-8", features_base_dir=None): """Scenario decorator. diff --git a/requirements-testing.txt b/requirements-testing.txt index 748809f75..6b3d7cfbd 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1 +1,3 @@ +execnet +deepdiff packaging diff --git a/setup.cfg b/setup.cfg index f65f9aa6f..b109151c0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,7 @@ classifiers = [options] python_requires = >=3.6 install_requires = + attrs glob2 Mako parse diff --git a/tests/feature/test_cucumber_json.py b/tests/feature/test_cucumber_json.py index e34208183..f75cae111 100644 --- a/tests/feature/test_cucumber_json.py +++ b/tests/feature/test_cucumber_json.py @@ -3,6 +3,8 @@ import os.path import textwrap +from deepdiff import DeepDiff + def runandparse(testdir, *args): """Run tests in testdir and parse json output.""" @@ -225,4 +227,4 @@ def test_passing_outline(): } ] - assert jsonobject == expected + assert DeepDiff(jsonobject, expected, ignore_order=True, report_repetition=True) diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index 3628db8d7..f45b5b845 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -1,6 +1,8 @@ """Scenario Outline tests.""" import textwrap +import pytest + from pytest_bdd.utils import collect_dumped_objects from tests.utils import assert_outcomes @@ -32,20 +34,63 @@ def should_have_left_cucumbers(start_cucumbers, start, eat, left): assert start_cucumbers["eat"] == eat """ +STEPS_OUTLINED = """\ +from pytest_bdd import given, when, then, scenario, parsers +from pytest_bdd.utils import dump_obj + +@scenario( + "outline.feature", + "Outlined given, when, thens", +) +def test_outline(): + pass + +@given(parsers.parse("there are {start:d} {fruits}"), target_fixture="start_fruits") +def start_fruits(start, fruits): + dump_obj(start, fruits) + + assert isinstance(start, int) + return {fruits: dict(start=start)} + + +@when(parsers.parse("I eat {eat:g} {fruits}")) +def eat_fruits(start_fruits, eat, fruits): + dump_obj(eat, fruits) + + assert isinstance(eat, float) + start_fruits[fruits]["eat"] = eat + + +@then(parsers.parse("I should have {left} {fruits}")) +def should_have_left_fruits(start_fruits, start, eat, left, fruits): + dump_obj(left, fruits) + + assert isinstance(left, str) + assert start - eat == int(left) + assert start_fruits[fruits]["start"] == start + assert start_fruits[fruits]["eat"] == eat +""" -def test_outlined(testdir): +@pytest.mark.parametrize( + "examples_header", + ( + pytest.param("Examples:", id="non_named"), + pytest.param("Examples: Named", id="named"), + ), +) +def test_outlined(testdir, examples_header): testdir.makefile( ".feature", outline=textwrap.dedent( - """\ + f"""\ Feature: Outline Scenario Outline: Outlined given, when, thens Given there are cucumbers When I eat cucumbers Then I should have cucumbers - Examples: + {examples_header} | start | eat | left | | 12 | 5 | 7 | # a comment | 5 | 4 | 1 | @@ -81,7 +126,7 @@ def test_outline(request): # fmt: on -def test_wrongly_outlined(testdir): +def test_wrongly_outlined_extra_parameter(testdir): """Test parametrized scenario when the test function lacks parameters.""" testdir.makefile( @@ -122,7 +167,7 @@ def test_outline(request): result.stdout.fnmatch_lines("*should match set of example values [[]'eat', 'left', 'start', 'unknown_param'[]].*") -def test_wrong_vertical_examples_scenario(testdir): +def test_wrongly_outlined_duplicated_parameter_scenario(testdir): """Test parametrized scenario vertical example table has wrong format.""" testdir.makefile( ".feature", @@ -134,10 +179,10 @@ def test_wrong_vertical_examples_scenario(testdir): When I eat cucumbers Then I should have cucumbers - Examples: Vertical - | start | 12 | 2 | - | start | 10 | 1 | - | left | 7 | 1 | + Examples: + | start | start | left | + | 12 | 10 | 7 | + | 2 | 1 | 1 | """ ), ) @@ -162,18 +207,17 @@ def test_outline(request): ) -def test_wrong_vertical_examples_feature(testdir): - """Test parametrized feature vertical example table has wrong format.""" +def test_wrongly_outlined_duplicated_parameter_feature(testdir): + """Test parametrized scenario vertical example table has wrong format.""" testdir.makefile( ".feature", outline=textwrap.dedent( """\ - Feature: Outlines - - Examples: Vertical - | start | 12 | 2 | - | start | 10 | 1 | - | left | 7 | 1 | + Feature: Outline + Examples: + | start | start | left | + | 12 | 10 | 7 | + | 2 | 1 | 1 | Scenario Outline: Outlined with wrong vertical example table Given there are cucumbers @@ -252,20 +296,27 @@ def test_outline(other_fixture): result.assert_outcomes(passed=6) -def test_vertical_example(testdir): +@pytest.mark.parametrize( + "examples_header", + ( + pytest.param("Examples: Vertical", id="non_named"), + pytest.param("Examples: Vertical Named", id="named"), + ), +) +def test_vertical_example(testdir, examples_header): """Test outlined scenario with vertical examples table.""" testdir.makefile( ".feature", outline=textwrap.dedent( - """\ + f"""\ Feature: Outline Scenario Outline: Outlined with vertical example table Given there are cucumbers When I eat cucumbers Then I should have cucumbers - Examples: Vertical - | start | 12 | 2 | + {examples_header} + | start | 12 | 2 | # a comment | eat | 5 | 1 | | left | 7 | 1 | @@ -300,72 +351,155 @@ def test_outline(): # fmt: on -def test_outlined_feature(testdir): +def test_wrongly_outlined_extra_parameter_vertical(testdir): + """Test parametrized scenario when the test function lacks parameters.""" + testdir.makefile( ".feature", outline=textwrap.dedent( """\ Feature: Outline + Scenario Outline: Outlined with wrong examples + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers - Examples: - | start | eat | left | - | 12 | 5 | 7 | - | 5 | 4 | 1 | - - Scenario Outline: Outlined given, when, thens - Given there are - When I eat - Then I should have - - Examples: - | fruits | - | oranges | - | apples | + Examples: Vertical + | start | 12 | + | eat | 5 | + | left | 7 | + | unknown_param | value | """ ), ) + testdir.makeconftest(textwrap.dedent(STEPS)) testdir.makepyfile( textwrap.dedent( """\ - from pytest_bdd import given, when, then, scenario, parsers - from pytest_bdd.utils import dump_obj + from pytest_bdd import scenario - @scenario( - "outline.feature", - "Outlined given, when, thens", - ) - def test_outline(): + @scenario("outline.feature", "Outlined with wrong examples") + def test_outline(request): pass + """ + ) + ) + result = testdir.runpytest() + assert_outcomes(result, errors=1) + result.stdout.fnmatch_lines( + '*ScenarioExamplesNotValidError: Scenario "Outlined with wrong examples"*has not valid examples*', + ) + result.stdout.fnmatch_lines("*should match set of example values [[]'eat', 'left', 'start', 'unknown_param'[]].*") - @given(parsers.parse("there are {start:d} {fruits}"), target_fixture="start_fruits") - def start_fruits(start, fruits): - dump_obj(start, fruits) - assert isinstance(start, int) - return {fruits: dict(start=start)} +def test_wrongly_outlined_duplicated_parameter_vertical_scenario(testdir): + """Test parametrized scenario vertical example table has wrong format.""" + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + Scenario Outline: Outlined with wrong vertical example table + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + Examples: Vertical + | start | 12 | 2 | + | start | 10 | 1 | + | left | 7 | 1 | + """ + ), + ) + testdir.makeconftest(textwrap.dedent(STEPS)) - @when(parsers.parse("I eat {eat:g} {fruits}")) - def eat_fruits(start_fruits, eat, fruits): - dump_obj(eat, fruits) + testdir.makepyfile( + textwrap.dedent( + """\ + from pytest_bdd import scenario - assert isinstance(eat, float) - start_fruits[fruits]["eat"] = eat + @scenario("outline.feature", "Outlined with wrong vertical example table") + def test_outline(request): + pass + """ + ) + ) + result = testdir.runpytest() + assert_outcomes(result, errors=1) + result.stdout.fnmatch_lines( + "*Scenario has not valid examples. Example rows should contain unique parameters. " + '"start" appeared more than once.*' + ) - @then(parsers.parse("I should have {left} {fruits}")) - def should_have_left_fruits(start_fruits, start, eat, left, fruits): - dump_obj(left, fruits) +def test_wrongly_outlined_duplicated_parameter_vertical_feature(testdir): + """Test parametrized feature vertical example table has wrong format.""" + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outlines - assert isinstance(left, str) - assert start - eat == int(left) - assert start_fruits[fruits]["start"] == start - assert start_fruits[fruits]["eat"] == eat + Examples: Vertical + | start | 12 | 2 | + | start | 10 | 1 | + | left | 7 | 1 | + Scenario Outline: Outlined with wrong vertical example table + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + """ + ), + ) + testdir.makeconftest(textwrap.dedent(STEPS)) + + testdir.makepyfile( + textwrap.dedent( + """\ + from pytest_bdd import scenario + + @scenario("outline.feature", "Outlined with wrong vertical example table") + def test_outline(request): + pass """ ) ) + result = testdir.runpytest() + assert_outcomes(result, errors=1) + result.stdout.fnmatch_lines( + "*Feature has not valid examples. Example rows should contain unique parameters. " + '"start" appeared more than once.*' + ) + + +def test_outlined_feature(testdir): + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + + Examples: + | start | eat | left | + | 12 | 5 | 7 | + | 5 | 4 | 1 | + + Scenario Outline: Outlined given, when, thens + Given there are + When I eat + Then I should have + + Examples: + | fruits | + | oranges | + | apples | + """ + ), + ) + + testdir.makepyfile(STEPS_OUTLINED) result = testdir.runpytest("-s") result.assert_outcomes(passed=4) parametrizations = collect_dumped_objects(result) @@ -433,3 +567,435 @@ def i_print_the_string(string): r"bork \\", r"bork \\|", ] + + +def test_multi_outlined(testdir): + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + Scenario Outline: Outlined given, when, thens + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + + Examples: + | start | eat | left | + | 12 | 5 | 7 | # a comment + + Examples: Vertical + | start | 5 | + | eat | 4 | + | left | 1 | + """ + ), + ) + + testdir.makeconftest(textwrap.dedent(STEPS)) + + testdir.makepyfile( + textwrap.dedent( + """\ + from pytest_bdd import scenario + + @scenario( + "outline.feature", + "Outlined given, when, thens", + ) + def test_outline(request): + pass + + """ + ) + ) + result = testdir.runpytest("-s") + result.assert_outcomes(passed=2) + # fmt: off + assert collect_dumped_objects(result) == [ + 12, 5.0, "7", + 5, 4.0, "1", + ] + # fmt: on + + +def test_multi_outlined_empty_examples(testdir): + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + + Examples: + + Examples: Vertical + + Examples: + | + + Examples: Vertical + | + + Scenario Outline: Outlined given, when, thens + Given there are 12 apples + When I eat 5 apples + Then I should have 7 apples + + Examples: + + Examples: Vertical + + Examples: + | + + Examples: Vertical + | + """ + ), + ) + + testdir.makepyfile(STEPS_OUTLINED) + result = testdir.runpytest("-s") + result.assert_outcomes(passed=1) + parametrizations = collect_dumped_objects(result) + # fmt: off + assert parametrizations == [ + 12, "apples", 5.0, "apples", "7", "apples", + ] + # fmt: on + + +def test_multi_outlined_feature_with_parameter_union(testdir): + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + + Examples: + | start | eat | left | + | 12 | 5 | 7 | + + Examples: Vertical + | start | 5 | + | eat | 4 | + | left | 1 | + + Scenario Outline: Outlined given, when, thens + Given there are + When I eat + Then I should have + + Examples: + | fruits | + | oranges | + | apples | + """ + ), + ) + + testdir.makepyfile(STEPS_OUTLINED) + result = testdir.runpytest("-s") + result.assert_outcomes(passed=4) + parametrizations = collect_dumped_objects(result) + # fmt: off + assert parametrizations == [ + 12, "oranges", 5.0, "oranges", "7", "oranges", + 12, "apples", 5.0, "apples", "7", "apples", + 5, "oranges", 4.0, "oranges", "1", "oranges", + 5, "apples", 4.0, "apples", "1", "apples", + ] + # fmt: on + + +def test_multi_outlined_scenario_and_feature_with_parameter_union(testdir): + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + + Examples: + | start | eat | left | + | 12 | 5 | 7 | + + Examples: Vertical + | start | 5 | + | eat | 4 | + | left | 1 | + + Scenario Outline: Outlined given, when, thens + Given there are + When I eat + Then I should have + + Examples: Vertical + | fruits | oranges | + + Examples: + | fruits | + | apples | + """ + ), + ) + + testdir.makepyfile(STEPS_OUTLINED) + result = testdir.runpytest("-s") + result.assert_outcomes(passed=4) + parametrizations = collect_dumped_objects(result) + # fmt: off + assert parametrizations == [ + 12, "oranges", 5.0, "oranges", "7", "oranges", + 12, "apples", 5.0, "apples", "7", "apples", + 5, "oranges", 4.0, "oranges", "1", "oranges", + 5, "apples", 4.0, "apples", "1", "apples", + ] + # fmt: on + + +def test_outlined_scenario_and_feature_with_parameter_join_by_one_parameter(testdir): + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + + Examples: + | start | eat | left | + | 12 | 5 | 7 | + | 5 | 4 | 1 | + + + Scenario Outline: Outlined given, when, thens + Given there are + When I eat + Then I should have + + Examples: + | fruits | left | + | apples | 7 | + | oranges | 1 | + """ + ), + ) + + testdir.makepyfile(STEPS_OUTLINED) + result = testdir.runpytest("-s") + result.assert_outcomes(passed=2) + parametrizations = collect_dumped_objects(result) + # fmt: off + assert parametrizations == [ + 12, "apples", 5.0, "apples", "7", "apples", + 5, "oranges", 4.0, "oranges", "1", "oranges", + ] + # fmt: on + + +def test_outlined_scenario_and_feature_with_parameter_join_by_multi_parameter(testdir): + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + + Examples: + | fruits | start | eat | left | + | apples | 12 | 5 | 7 | + | apples | 12 | 9 | 3 | # not joined by + | oranges | 5 | 4 | 1 | + | cucumbers | 8 | 3 | 5 | # not joined by + + + Scenario Outline: Outlined given, when, thens + Given there are + When I eat + Then I should have + + Examples: + | fruits | eat | left | + | apples | 5 | 7 | + | oranges | 4 | 1 | + | cucumbers | 5 | 7 | # not joined by + """ + ), + ) + + testdir.makepyfile(STEPS_OUTLINED) + result = testdir.runpytest("-s") + result.assert_outcomes(passed=2) + parametrizations = collect_dumped_objects(result) + # fmt: off + assert parametrizations == [ + 12, "apples", 5.0, "apples", "7", "apples", + 5, "oranges", 4.0, "oranges", "1", "oranges", + ] + # fmt: on + + +def test_outlined_scenario_and_feature_with_parameter_join_by_multi_parameter_unbalanced(testdir): + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + Examples: + | start | eat | left | + | 14 | 6 | 8 | + | 15 | 5 | 10 | + + Examples: + | fruits | start | eat | left | + | apples | 12 | 5 | 7 | + | apples | 12 | 9 | 3 | + | oranges | 5 | 4 | 1 | + | cucumbers | 8 | 3 | 5 | + + + Scenario Outline: Outlined given, when, thens + Given there are + When I eat + Then I should have + + Examples: + | fruits | eat | left | + | apples | 5 | 7 | + | oranges | 4 | 1 | + | cucumbers | 5 | 7 | + + Examples: + | fruits | + | pineapples | + | peaches | + """ + ), + ) + + testdir.makepyfile(STEPS_OUTLINED) + result = testdir.runpytest("-s") + result.assert_outcomes(passed=6) + parametrizations = collect_dumped_objects(result) + # fmt: off + assert parametrizations == [ + 14, 'pineapples', 6.0, 'pineapples', '8', 'pineapples', + 14, 'peaches', 6.0, 'peaches', '8', 'peaches', + 15, 'pineapples', 5.0, 'pineapples', '10', 'pineapples', + 15, 'peaches', 5.0, 'peaches', '10', 'peaches', + 12, 'apples', 5.0, 'apples', '7', 'apples', + 5, 'oranges', 4.0, 'oranges', '1', 'oranges' + ] + # fmt: on + + +@pytest.mark.xfail(reason="https://github.com/pytest-dev/pytest-bdd/pull/439") +def test_outlined_scenario_and_feature_with_insufficient_parameter_join(testdir): + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + Examples: + | fruits | + | apples | + | oranges | + + Scenario Outline: Outlined given, when, thens + Given there are + When I eat + Then I should have + + Examples: + | eat | left | + | 5 | 7 | + | 4 | 1 | + + Examples: + | fruits | start | + | pineapples | 12 | + | peaches | 10 | + + """ + ), + ) + + testdir.makepyfile(STEPS_OUTLINED) + result = testdir.runpytest("-s") + result.assert_outcomes(passed=0) + + +@pytest.mark.xfail(reason="https://github.com/pytest-dev/pytest-bdd/pull/439") +def test_outlined_scenario_and_feature_with_extra_parameter_join(testdir): + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + Examples: + | fruits | extra | + | apples | not used | + | oranges | not needed | + + Scenario Outline: Outlined given, when, thens + Given there are + When I eat + Then I should have + + Examples: + | start | eat | left | + | 12 | 5 | 7 | + | 5 | 4 | 1 | + """ + ), + ) + + testdir.makepyfile(STEPS_OUTLINED) + result = testdir.runpytest("-s") + result.assert_outcomes(passed=0) + + +@pytest.mark.xfail(reason="https://github.com/pytest-dev/pytest-bdd/pull/439") +def test_outlined_scenario_and_feature_with_combine_extra_and_insufficient_parameter_join(testdir): + testdir.makefile( + ".feature", + outline=textwrap.dedent( + """\ + Feature: Outline + Examples: + | fruits | extra | + | apples | not used | + | oranges | not needed | + + Examples: + | fruits | + | cucumbers | + | peaches | + + Scenario Outline: Outlined given, when, thens + Given there are + When I eat + Then I should have + + Examples: + | start | eat | left | + | 12 | 5 | 7 | + | 5 | 4 | 1 | + + Examples: + | eat | left | + | 8 | 6 | + | 9 | 5 | + """ + ), + ) + + testdir.makepyfile(STEPS_OUTLINED) + result = testdir.runpytest("-s") + result.assert_outcomes(passed=4) + parametrizations = collect_dumped_objects(result) + # fmt: off + assert parametrizations == [ + 12, "cucumbers", 5.0, "cucumbers", "7", "cucumbers", + 12, "peaches", 5.0, "peaches", "7", "peaches", + 5, "cucumbers", 4.0, "cucumbers", "1", "cucumbers", + 5, "peaches", 4.0, "peaches", "1", "peaches", + ] + # fmt: on diff --git a/tests/feature/test_report.py b/tests/feature/test_report.py index f11f8d0f5..1fe364401 100644 --- a/tests/feature/test_report.py +++ b/tests/feature/test_report.py @@ -1,8 +1,6 @@ """Test scenario reporting.""" import textwrap -import pytest - class OfType: """Helper object comparison to which is always 'equal'.""" @@ -256,10 +254,8 @@ def should_have_left_cucumbers(start_cucumbers, start, eat, left): assert report == expected -def test_complex_types(testdir, pytestconfig): +def test_complex_types(testdir): """Test serialization of the complex types.""" - if not pytestconfig.pluginmanager.has_plugin("xdist"): - pytest.skip("Execnet not installed") import execnet.gateway_base