From fe00c2f7b38091ff484d9ed45a2e75b275e1c95d Mon Sep 17 00:00:00 2001 From: Kostiantyn Goloveshko Date: Thu, 6 Oct 2022 18:33:06 +0300 Subject: [PATCH] Add feature autoload for StructBDD --- pytest_bdd/collector.py | 2 + pytest_bdd/plugin.py | 27 ++++++++-- pytest_bdd/struct_bdd/parser.py | 17 +++--- tests/struct_bdd/test_steps.py | 96 +++++++++++++++++++++++++++++---- 4 files changed, 122 insertions(+), 20 deletions(-) diff --git a/pytest_bdd/collector.py b/pytest_bdd/collector.py index 546516ee..adb5ed3f 100644 --- a/pytest_bdd/collector.py +++ b/pytest_bdd/collector.py @@ -3,6 +3,7 @@ from pathlib import Path from uuid import uuid4 +from attr import attrib, attrs from pytest import Module as PytestModule from pytest_bdd.scenario import _scenarios @@ -30,6 +31,7 @@ def _build_test_module(self): feature_paths=[self.fspath], scenario_filter_or_scenario_name=None, return_test_decorator=False, + parser=getattr(self, "parser", None), _caller_module_locals=module.__dict__, _caller_module_path=self.fspath, ) diff --git a/pytest_bdd/plugin.py b/pytest_bdd/plugin.py index e62e938c..d06c3826 100644 --- a/pytest_bdd/plugin.py +++ b/pytest_bdd/plugin.py @@ -3,6 +3,8 @@ from collections import deque from contextlib import suppress +from functools import partial +from operator import attrgetter, contains, methodcaller from pathlib import Path from types import ModuleType from typing import Collection @@ -21,6 +23,7 @@ from pytest_bdd.scenario import add_options as scenario_add_options from pytest_bdd.steps import StepHandler from pytest_bdd.typing.pytest import Config, Mark, MarkDecorator, Metafunc, Parser, PytestPluginManager +from pytest_bdd.typing.struct_bdd import STRUCT_BDD_INSTALLED def pytest_addhooks(pluginmanager: PytestPluginManager) -> None: @@ -119,11 +122,29 @@ def pytest_collect_file(parent, path, file_path=None): is_enabled_feature_autoload = config.getoption("feature_autoload") if is_enabled_feature_autoload is None: is_enabled_feature_autoload = config.getini("feature_autoload") - if file_path.suffix in {".gherkin", ".feature"} and is_enabled_feature_autoload: + if not is_enabled_feature_autoload: + return + if any(map(partial(contains, {".gherkin", ".feature"}), file_path.suffixes)): if hasattr(FeatureFileCollector, "from_parent"): - return FeatureFileCollector.from_parent(parent, fspath=py.path.local(file_path)) + collector = FeatureFileCollector.from_parent(parent, fspath=py.path.local(file_path)) else: - return FeatureFileCollector(parent=parent, fspath=py.path.local(file_path)) + collector = FeatureFileCollector(parent=parent, fspath=py.path.local(file_path)) + + if STRUCT_BDD_INSTALLED: + from pytest_bdd.struct_bdd.parser import StructBDDParser + + struct_bdd_parser_kind = next( + filter( + partial(contains, list(map(methodcaller("strip", "."), file_path.suffixes))), + list(map(attrgetter("value"), StructBDDParser.KIND)), + ), + None, + ) + + if struct_bdd_parser_kind is not None: + collector.parser = StructBDDParser(kind=struct_bdd_parser_kind) + + return collector @pytest.mark.trylast diff --git a/pytest_bdd/struct_bdd/parser.py b/pytest_bdd/struct_bdd/parser.py index 07b7ef01..e5dbb597 100644 --- a/pytest_bdd/struct_bdd/parser.py +++ b/pytest_bdd/struct_bdd/parser.py @@ -1,3 +1,4 @@ +from enum import Enum from functools import partial from operator import methodcaller from pathlib import Path @@ -11,7 +12,7 @@ @attrs class StructBDDParser(ParserProtocol): - class KIND: + class KIND(Enum): HOCON = "hocon" HJSON = "hjson" JSON = "json" @@ -25,7 +26,7 @@ class KIND: @kind.default def kind_default(self): - return self.KIND.YAML if getattr(self, "loader", None) is None else None + return self.KIND.YAML.value if getattr(self, "loader", None) is None else None @glob.default def glob_default(self): @@ -46,28 +47,28 @@ def parse(self, path: Path, uri: str, *args, **kwargs): ) def build_loader(self): - if self.kind == self.KIND.YAML: + if self.kind == self.KIND.YAML.value: from yaml import FullLoader from yaml import load as load_yaml return partial(load_yaml, Loader=FullLoader) - elif self.kind == self.KIND.TOML: + elif self.kind == self.KIND.TOML.value: from tomli import loads as load_toml return load_toml - elif self.kind == self.KIND.JSON: + elif self.kind == self.KIND.JSON.value: from json import loads as load_json return load_json - elif self.kind == self.KIND.JSON5: + elif self.kind == self.KIND.JSON5.value: from json5 import loads as load_json5 return load_json5 - elif self.kind == self.KIND.HJSON: + elif self.kind == self.KIND.HJSON.value: from hjson import loads as load_hjson return load_hjson - elif self.kind == self.KIND.HOCON: + elif self.kind == self.KIND.HOCON.value: from json import loads from pyhocon import ConfigFactory, HOCONConverter diff --git a/tests/struct_bdd/test_steps.py b/tests/struct_bdd/test_steps.py index 92cab642..eff800a0 100644 --- a/tests/struct_bdd/test_steps.py +++ b/tests/struct_bdd/test_steps.py @@ -9,7 +9,7 @@ "kind,file_content", [ partial(param, id="plain-yaml")( - StructBDDParser.KIND.YAML, + StructBDDParser.KIND.YAML.value, """\ Name: Steps are executed one by one Description: | @@ -30,7 +30,7 @@ """, ), partial(param, id="plain-hocon")( - StructBDDParser.KIND.HOCON, + StructBDDParser.KIND.HOCON.value, r""" Name = Steps are executed one by one Description: "Steps are executed one by one. Given and When sections\nare not mandatory in some cases.\n", @@ -54,7 +54,7 @@ """, ), partial(param, id="plain-json")( - StructBDDParser.KIND.JSON, + StructBDDParser.KIND.JSON.value, r"""{ "Name": "Steps are executed one by one", "Description": "Steps are executed one by one. Given and When sections\nare not mandatory in some cases.\n", @@ -79,7 +79,7 @@ """, ), partial(param, id="plain-hjson")( - StructBDDParser.KIND.HJSON, + StructBDDParser.KIND.HJSON.value, r"""{ Name: Steps are executed one by one Description: @@ -111,7 +111,7 @@ """, ), partial(param, id="plain-json5")( - StructBDDParser.KIND.JSON5, + StructBDDParser.KIND.JSON5.value, r"""{ Name: 'Steps are executed one by one', Description: 'Steps are executed one by one. Given and When sections\nare not mandatory in some cases.\n', @@ -136,7 +136,7 @@ """, ), partial(param, id="plain-toml")( - StructBDDParser.KIND.TOML, + StructBDDParser.KIND.TOML.value, """\ Name = "Steps are executed one by one" Description=''' @@ -159,7 +159,7 @@ """, ), partial(param, id="complex-yaml")( - StructBDDParser.KIND.YAML, + StructBDDParser.KIND.YAML.value, """\ Name: Steps are executed one by one Description: | @@ -185,7 +185,7 @@ """, ), partial(param, id="complex-toml")( - StructBDDParser.KIND.TOML, + StructBDDParser.KIND.TOML.value, """\ Name = "Steps are executed one by one" Description=''' @@ -283,7 +283,7 @@ def check_results(results): "kind,file_content", [ partial(param, id="plain-yaml")( - StructBDDParser.KIND.YAML, + StructBDDParser.KIND.YAML.value, """\ Name: Steps are executed one by one Description: | @@ -366,6 +366,84 @@ def check_results(results): result.assert_outcomes(passed=1, failed=0) +@mark.parametrize( + "kind,file_content", + [ + partial(param, id="plain-yaml")( + StructBDDParser.KIND.YAML.value, + """\ + Name: Steps are executed one by one + Description: | + Steps are executed one by one. Given and When sections + are not mandatory in some cases. + Steps: + - Step: + Name: Executed step by step + Description: Scenario description + Steps: + - Given: I have a foo fixture with value "foo" + - And: there is a list + - When: I append 1 to the list + - And: I append 2 to the list + - And: I append 3 to the list + - Then: foo should have value "foo" + - But: the list should be [1, 2, 3] + """, + ), + ], +) +def test_autoload_feature_yaml(testdir, kind, file_content): + testdir.makefile( + f".bdd.feature.{kind}", + steps=file_content, + ) + + testdir.makeconftest( + """\ + from textwrap import dedent + from pytest_bdd import given, when, then, scenario + from pytest_bdd.parser import StructBDDParser + + + @given('I have a foo fixture with value "foo"', target_fixture="foo") + def foo(): + return "foo" + + + @given("there is a list", target_fixture="results") + def results(): + return [] + + + @when("I append 1 to the list") + def append_1(results): + results.append(1) + + + @when("I append 2 to the list") + def append_2(results): + results.append(2) + + + @when("I append 3 to the list") + def append_3(results): + results.append(3) + + + @then('foo should have value "foo"') + def foo_is_foo(foo): + assert foo == "foo" + + + @then("the list should be [1, 2, 3]") + def check_results(results): + assert results == [1, 2, 3] + """ + ) + result = testdir.runpytest("--feature-autoload") + result.assert_outcomes(passed=1, failed=0) + + @mark.parametrize( "file_content", [