"""Defines the Scenario Class"""
# needed to support "type[]" class type annotations in Python 3.8:
from __future__ import annotations
import re
import igraph as ig # type: ignore
from typing import Optional, Union, List
from .step import Step, Prerequisite, Action, Assertion
[docs]class Scenario:
"""A BDD Scenario"""
SCENARIO_PATTERN: re.Pattern = re.compile(
r"""
^ # start of line
Scenario: # "Scenario:" keyword
[ ] # space
(?P<scenario_name>[^#]*) # scenario name
(?:\# (?P<comment>.+))? # optional comment
$ # end of line
""",
re.VERBOSE,
)
"""
re.Pattern
Pattern describing a BDD scenario line ("Scenario: …")
followed by an optional comment
"""
name: str
graph: ig.Graph
vertex: ig.Vertex
steps: List[Step]
comment: Optional[str]
_validated: bool
def __init__(
self,
name: str,
graph: ig.Graph,
parent_scenario: Optional["Scenario"] = None,
comment: Optional[str] = None,
) -> None:
"""Constructor method
Parameters
----------
name : str
The name of the scenario
graph : igraph.Graph
The graph
parent_scenario: Scenario (optional)
The parent scenario to connect the new scenario to
comment : str, optional
"""
self.name = name.strip()
self.graph = graph
self.vertex = graph.add_vertex()
self.vertex["scenario"] = self
self.steps = []
self._validated = False
self.comment = comment.strip() if comment is not None else None
if parent_scenario is not None:
self.graph.add_edge(parent_scenario.vertex, self.vertex)
@property
def validated(self) -> bool:
"""The "validated" property
Used to keep track of which scenarios had their assertions written
to an output scenario already so that assertions are not run multiple
times.
Returns
-------
bool
Whether or not this scenario has been validated
"""
return self._validated
@validated.setter
def validated(self, value: bool) -> None:
"""The validated property setter
Parameters
----------
value : bool
Whether or not this scenario has been validated
"""
self._validated = value
[docs] def prerequisites(self) -> List[Step]:
"""Returns all steps of type Prerequisite
Returns
-------
List[Prerequisite]
List of steps of type Prerequisite
"""
return self.steps_of_type(Prerequisite)
[docs] def actions(self) -> List[Step]:
"""Returns all steps of type Action
Returns
-------
List[Action]
List of steps of type Action
"""
return self.steps_of_type(Action)
[docs] def assertions(self) -> List[Step]:
"""Returns all steps of type Assertion
Returns
----------
list
List[Assertion]
List of steps of type Assertion
"""
return self.steps_of_type(Assertion)
[docs] def steps_of_type(
self, step_type: Union[type[Prerequisite], type[Action], type[Assertion]]
) -> List[Step]:
"""Returns all steps of the passed in type
Parameters
----------
step_class : {Prerequisite, Action, Assertion}
A step subclass
Returns
-------
List[Step]
All steps of the passed in type
"""
return [st for st in self.steps if type(st) is step_type]
def __str__(self) -> str:
"""Returns a string representation of the Scenario instance for terminal output.
Returns
-------
str
String representation of the Scenario instance
"""
return "<Scenario: {} ({} prerequisites, {} actions, {} assertions)>".format(
self.name,
len(self.prerequisites()),
len(self.actions()),
len(self.assertions()),
)
def __repr__(self) -> str:
"""Returns a string representation of the Scenario instance for terminal output.
Returns
-------
str
String representation of the Scenario instance
"""
return self.__str__()
[docs] def ancestors(self) -> List["Scenario"]:
"""Returns the scenario's ancestors, starting with a root scenario
Returns
-------
List[Scenario]
List of scenarios
"""
ancestors: List[ig.Vertex] = self.graph.neighborhood(
self.vertex,
mode="IN",
order=1000,
mindist=1,
)
ancestors.reverse()
return [vx["scenario"] for vx in self.graph.vs(ancestors)]
[docs] def parent(self) -> Optional[Scenario]:
"""Returns the scenario's parent scenario, if one exists
Returns
-------
Scenario, optional
The parent scenario
"""
parents: List[ig.Vertex] = self.graph.neighborhood(
self.vertex, mode="IN", order=1, mindist=1
)
if len(parents) == 1:
return self.graph.vs[parents[0]]["scenario"]
else:
return None
[docs] def children(self) -> List[Scenario]:
"""Returns the scenario's child scenarios
Returns
-------
List[Scenario]
The child scenarios
"""
children: List[ig.Vertex] = self.graph.neighborhood(
self.vertex, mode="OUT", order=1, mindist=1
)
return [vx["scenario"] for vx in self.graph.vs(children)]
[docs] def siblings(self) -> List[Scenario]:
"""Returns the scenario's sibling scenarios
The scenario's parent's children (including self)
Returns
-------
List[Scenario]
The sibling scenarios
"""
parent: Optional[Scenario] = self.parent()
if parent is not None:
return parent.children()
else:
return [vx["scenario"] for vx in self.graph.vs if vx.indegree() == 0]
# TODO: This duplicates the implementation of
# ScenarioForest#root_scenarios() but Scenario does not currently
# have access to its feature. Might want to change that
[docs] def path_scenarios(self) -> List["Scenario"]:
"""Returns the complete scenario path from the root scenario to
(and including) self.
Returns
-------
List[Scenario]
List of scenarios. The last scenario is self
"""
return self.ancestors() + [self]
[docs] def level(self) -> int:
"""Returns the scenario"s level in the scenario tree.
Root scenario = Level 1
Returns
-------
int
The scenario"s level
"""
return self.graph.neighborhood_size(self.vertex, mode="IN", order=1000)
[docs] def is_organizational(self) -> bool:
"""Returns whether the scenario is an "organizational" scenario.
"Organizational" scenarios are used for grouping only.
They do not have any assertions.
Returns
----------
bool
Whether the scenario is an "organizational" scenario
"""
return len(self.assertions()) == 0
[docs] def index(self) -> Optional[int]:
"""Returns the "index" of the scenario.
The scenario"s vertical position in the feature file.
Returns
----------
int
Index of self
"""
return self.vertex.index # TODO: start at index 1 (instead of 0)
[docs] def is_closed(self) -> bool:
"""Returns whether or not the scenario is "closed".
A scenario is "closed" if additional child scenarios cannot be added
which is the case when there is a "later" (higher index) scenario
with a lower or equal indentation level in the feature file.
Returns
----------
bool
Whether or not the scenario is "closed"
"""
# Later scenario with lower or equal indentation level:
closing_scenario: Optional[Scenario] = next(
(
vx
for vx in self.graph.vs()
if vx.index > self.index()
and self.graph.neighborhood_size(vx, mode="IN", order=1000)
<= self.level()
),
None,
)
return closing_scenario is not None