Coverage for manyworlds/scenario.py: 100%
54 statements
« prev ^ index » next coverage.py v7.3.0, created at 2023-08-25 16:44 +0200
« prev ^ index » next coverage.py v7.3.0, created at 2023-08-25 16:44 +0200
1"""Defines the Scenario Class"""
2# needed to support "type[]" class type annotations in Python 3.8:
3from __future__ import annotations
5import re
6import igraph as ig # type: ignore
7from typing import Optional, Union, List
9from .step import Step, Prerequisite, Action, Assertion
12class Scenario:
13 """A BDD Scenario"""
15 SCENARIO_PATTERN: re.Pattern = re.compile("Scenario: (?P<scenario_name>.*)")
16 """
17 re.Pattern
19 The string "Scenario: ", followed by arbitrary string
20 """
22 name: str
23 graph: ig.Graph
24 vertex: ig.Vertex
25 steps: List[Step]
26 _validated: bool
28 def __init__(
29 self, name: str, graph: ig.Graph, parent_scenario: Optional["Scenario"] = None
30 ) -> None:
31 """Constructor method
33 Parameters
34 ----------
35 name : str
36 The name of the scenario
38 graph : igraph.Graph
39 The graph
41 parent_scenario: Scenario (optional)
42 The parent scenario to connect the new scenario to
43 """
45 self.name = name.strip()
46 self.graph = graph
47 self.vertex = graph.add_vertex()
48 self.vertex["scenario"] = self
49 self.steps = []
50 self._validated = False
52 if parent_scenario is not None:
53 self.graph.add_edge(parent_scenario.vertex, self.vertex)
55 @property
56 def validated(self) -> bool:
57 """The "validated" property
59 Used to keep track of which scenarios had their assertions written
60 to an output scenario already so that assertions are not run multiple
61 times.
63 Returns
64 -------
65 bool
66 Whether or not this scenario has been validated
67 """
68 return self._validated
70 @validated.setter
71 def validated(self, value: bool) -> None:
72 """The validated property setter
74 Parameters
75 ----------
76 value : bool
77 Whether or not this scenario has been validated
79 """
81 self._validated = value
83 def prerequisites(self) -> List[Step]:
84 """Returns all steps of type Prerequisite
86 Returns
87 -------
88 List[Prerequisite]
89 List of steps of type Prerequisite
90 """
92 return self.steps_of_type(Prerequisite)
94 def actions(self) -> List[Step]:
95 """Returns all steps of type Action
97 Returns
98 -------
99 List[Action]
100 List of steps of type Action
101 """
103 return self.steps_of_type(Action)
105 def assertions(self) -> List[Step]:
106 """Returns all steps of type Assertion
108 Returns
109 ----------
110 list
111 List[Assertion]
112 List of steps of type Assertion
113 """
115 return self.steps_of_type(Assertion)
117 def steps_of_type(
118 self, step_type: Union[type[Prerequisite], type[Action], type[Assertion]]
119 ) -> List[Step]:
120 """Returns all steps of the passed in type
122 Parameters
123 ----------
124 step_class : {Prerequisite, Action, Assertion}
125 A step subclass
127 Returns
128 -------
129 List[Step]
130 All steps of the passed in type
131 """
133 return [st for st in self.steps if type(st) is step_type]
135 def __str__(self) -> str:
136 """Returns a string representation of the Scenario instance for terminal output.
138 Returns
139 -------
140 str
141 String representation of the Scenario instance
142 """
144 return "<Scenario: {} ({} prerequisites, {} actions, {} assertions)>".format(
145 self.name,
146 len(self.prerequisites()),
147 len(self.actions()),
148 len(self.assertions()),
149 )
151 def __repr__(self) -> str:
152 """Returns a string representation of the Scenario instance for terminal output.
154 Returns
155 -------
156 str
157 String representation of the Scenario instance
158 """
160 return self.__str__()
162 def ancestors(self) -> List["Scenario"]:
163 """Returns the scenario"s ancestors, starting with a root scenario
165 Returns
166 -------
167 List[Scenario]
168 List of scenarios
169 """
171 ancestors: List[Scenario] = self.graph.neighborhood(
172 self.vertex,
173 mode="IN",
174 order=1000,
175 mindist=1,
176 )
177 ancestors.reverse()
178 return [vx["scenario"] for vx in self.graph.vs(ancestors)]
180 def path_scenarios(self) -> List["Scenario"]:
181 """Returns the complete scenario path from the root scenario to
182 (and including) self.
184 Returns
185 -------
186 List[Scenario]
187 List of scenarios. The last scenario is self
188 """
190 return self.ancestors() + [self]
192 def level(self) -> int:
193 """Returns the scenario"s level in the scenario tree.
195 Root scenario = Level 1
197 Returns
198 -------
199 int
200 The scenario"s level
201 """
203 return self.graph.neighborhood_size(self.vertex, mode="IN", order=1000)
205 def is_organizational(self) -> bool:
206 """Returns whether the scenario is an "organizational" scenario.
208 "Organizational" scenarios are used for grouping only.
209 They do not have any assertions.
211 Returns
212 ----------
213 bool
214 Whether the scenario is an "organizational" scenario
215 """
217 return len(self.assertions()) == 0
219 def index(self) -> Optional[int]:
220 """Returns the "index" of the scenario.
222 The scenario"s vertical position in the feature file.
224 Returns
225 ----------
226 int
227 Index of self
228 """
230 return self.vertex.index # TODO: start at index 1 (instead of 0)
232 def is_closed(self) -> bool:
233 """Returns whether or not the scenario is "closed".
235 A scenario is "closed" if additional child scenarios cannot be added
236 which is the case when there is a "later" (higher index) scenario
237 with a lower or equal indentation level in the feature file.
239 Returns
240 ----------
241 bool
242 Whether or not the scenario is "closed"
243 """
245 # Later scenario with lower or equal indentation level:
246 closing_scenario: Optional[Scenario] = next(
247 (
248 vx
249 for vx in self.graph.vs()
250 if vx.index > self.index()
251 and self.graph.neighborhood_size(vx, mode="IN", order=1000)
252 <= self.level()
253 ),
254 None,
255 )
256 return closing_scenario is not None