Coverage for manyworlds/scenario.py: 100%

54 statements  

« 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 

4 

5import re 

6import igraph as ig # type: ignore 

7from typing import Optional, Union, List 

8 

9from .step import Step, Prerequisite, Action, Assertion 

10 

11 

12class Scenario: 

13 """A BDD Scenario""" 

14 

15 SCENARIO_PATTERN: re.Pattern = re.compile("Scenario: (?P<scenario_name>.*)") 

16 """ 

17 re.Pattern 

18 

19 The string "Scenario: ", followed by arbitrary string 

20 """ 

21 

22 name: str 

23 graph: ig.Graph 

24 vertex: ig.Vertex 

25 steps: List[Step] 

26 _validated: bool 

27 

28 def __init__( 

29 self, name: str, graph: ig.Graph, parent_scenario: Optional["Scenario"] = None 

30 ) -> None: 

31 """Constructor method 

32 

33 Parameters 

34 ---------- 

35 name : str 

36 The name of the scenario 

37 

38 graph : igraph.Graph 

39 The graph 

40 

41 parent_scenario: Scenario (optional) 

42 The parent scenario to connect the new scenario to 

43 """ 

44 

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 

51 

52 if parent_scenario is not None: 

53 self.graph.add_edge(parent_scenario.vertex, self.vertex) 

54 

55 @property 

56 def validated(self) -> bool: 

57 """The "validated" property 

58 

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. 

62 

63 Returns 

64 ------- 

65 bool 

66 Whether or not this scenario has been validated 

67 """ 

68 return self._validated 

69 

70 @validated.setter 

71 def validated(self, value: bool) -> None: 

72 """The validated property setter 

73 

74 Parameters 

75 ---------- 

76 value : bool 

77 Whether or not this scenario has been validated 

78 

79 """ 

80 

81 self._validated = value 

82 

83 def prerequisites(self) -> List[Step]: 

84 """Returns all steps of type Prerequisite 

85 

86 Returns 

87 ------- 

88 List[Prerequisite] 

89 List of steps of type Prerequisite 

90 """ 

91 

92 return self.steps_of_type(Prerequisite) 

93 

94 def actions(self) -> List[Step]: 

95 """Returns all steps of type Action 

96 

97 Returns 

98 ------- 

99 List[Action] 

100 List of steps of type Action 

101 """ 

102 

103 return self.steps_of_type(Action) 

104 

105 def assertions(self) -> List[Step]: 

106 """Returns all steps of type Assertion 

107 

108 Returns 

109 ---------- 

110 list 

111 List[Assertion] 

112 List of steps of type Assertion 

113 """ 

114 

115 return self.steps_of_type(Assertion) 

116 

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 

121 

122 Parameters 

123 ---------- 

124 step_class : {Prerequisite, Action, Assertion} 

125 A step subclass 

126 

127 Returns 

128 ------- 

129 List[Step] 

130 All steps of the passed in type 

131 """ 

132 

133 return [st for st in self.steps if type(st) is step_type] 

134 

135 def __str__(self) -> str: 

136 """Returns a string representation of the Scenario instance for terminal output. 

137 

138 Returns 

139 ------- 

140 str 

141 String representation of the Scenario instance 

142 """ 

143 

144 return "<Scenario: {} ({} prerequisites, {} actions, {} assertions)>".format( 

145 self.name, 

146 len(self.prerequisites()), 

147 len(self.actions()), 

148 len(self.assertions()), 

149 ) 

150 

151 def __repr__(self) -> str: 

152 """Returns a string representation of the Scenario instance for terminal output. 

153 

154 Returns 

155 ------- 

156 str 

157 String representation of the Scenario instance 

158 """ 

159 

160 return self.__str__() 

161 

162 def ancestors(self) -> List["Scenario"]: 

163 """Returns the scenario"s ancestors, starting with a root scenario 

164 

165 Returns 

166 ------- 

167 List[Scenario] 

168 List of scenarios 

169 """ 

170 

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)] 

179 

180 def path_scenarios(self) -> List["Scenario"]: 

181 """Returns the complete scenario path from the root scenario to 

182 (and including) self. 

183 

184 Returns 

185 ------- 

186 List[Scenario] 

187 List of scenarios. The last scenario is self 

188 """ 

189 

190 return self.ancestors() + [self] 

191 

192 def level(self) -> int: 

193 """Returns the scenario"s level in the scenario tree. 

194 

195 Root scenario = Level 1 

196 

197 Returns 

198 ------- 

199 int 

200 The scenario"s level 

201 """ 

202 

203 return self.graph.neighborhood_size(self.vertex, mode="IN", order=1000) 

204 

205 def is_organizational(self) -> bool: 

206 """Returns whether the scenario is an "organizational" scenario. 

207 

208 "Organizational" scenarios are used for grouping only. 

209 They do not have any assertions. 

210 

211 Returns 

212 ---------- 

213 bool 

214 Whether the scenario is an "organizational" scenario 

215 """ 

216 

217 return len(self.assertions()) == 0 

218 

219 def index(self) -> Optional[int]: 

220 """Returns the "index" of the scenario. 

221 

222 The scenario"s vertical position in the feature file. 

223 

224 Returns 

225 ---------- 

226 int 

227 Index of self 

228 """ 

229 

230 return self.vertex.index # TODO: start at index 1 (instead of 0) 

231 

232 def is_closed(self) -> bool: 

233 """Returns whether or not the scenario is "closed". 

234 

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. 

238 

239 Returns 

240 ---------- 

241 bool 

242 Whether or not the scenario is "closed" 

243 """ 

244 

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