Skip to content

Commit 5ca5492

Browse files
committed
delegate refinement stack to TraceState
1 parent bc96a30 commit 5ca5492

5 files changed

Lines changed: 180 additions & 66 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
*** Settings ***
2+
Documentation This suite has more low-level scenarios than high-level scenarios,
3+
... meaning that the high-level scenario must be repeated in order for
4+
... the second low-level scenario to be reached.
5+
Suite Setup Treat this test suite Model-based
6+
Resource ../../resources/birthday_cards_composed.resource
7+
Library robotmbt
8+
9+
*** Test Cases ***
10+
Buying a card
11+
When someone buys a birthday card
12+
then there is a blank birthday card available
13+
14+
high-level scenario
15+
Given there is a birthday card
16+
when Someone writes their name on the birthday card
17+
then the birthday card has 'Someone' written on it
18+
19+
low-level scenario A
20+
Given there is a birthday card
21+
when Someone writes their name in pen on the birthday card
22+
then the birthday card has 'Someone' written on it
23+
and there is text added in ink on the birthday card
24+
25+
low-level scenario B
26+
Given there is a birthday card
27+
when Someone writes their name in pen on the birthday card
28+
then the birthday card has 'Someone' written on it
29+
and there is text added in ink on the birthday card

atest/robotMBT tests/05__repeating_scenarios/08__impossible_trace.robot renamed to atest/robotMBT tests/05__repeating_scenarios/09__impossible_trace.robot

File renamed without changes.

robotmbt/suiteprocessors.py

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@ def process_test_suite(self, in_suite, *, seed='new'):
107107

108108
def _try_to_reach_full_coverage(self, allow_duplicate_scenarios):
109109
tracestate = TraceState(self.shuffled)
110-
refinement_stack = []
111110
while not tracestate.coverage_reached():
112111
candidate_id = tracestate.next_candidate(retry=allow_duplicate_scenarios)
113112
if candidate_id is None: # No more candidates remaining for this level
@@ -122,7 +121,7 @@ def _try_to_reach_full_coverage(self, allow_duplicate_scenarios):
122121
tracestate.reject_scenario(candidate_id)
123122
continue
124123
previous_len = len(tracestate)
125-
self._try_to_fit_in_scenario(candidate, tracestate, refinement_stack)
124+
self._try_to_fit_in_scenario(candidate, tracestate)
126125
if len(tracestate) > previous_len:
127126
self.DROUGHT_LIMIT = 50
128127
if self.__last_candidate_changed_nothing(tracestate):
@@ -146,11 +145,10 @@ def __last_candidate_changed_nothing(tracestate):
146145

147146
def _select_scenario_variant(self, candidate_id, tracestate):
148147
candidate = self._scenario_with_repeat_counter(candidate_id, tracestate)
149-
scenarios_in_refinement = tracestate.find_scenarios_with_active_refinement()
150-
if candidate_id in [s.src_id for s in scenarios_in_refinement]:
148+
if candidate_id in tracestate.active_refinements:
151149
# reuse previous solution for all parts in split-up scenario
152150
candidate = candidate.copy()
153-
candidate.data_choices = scenarios_in_refinement[candidate_id].data_choices.copy()
151+
candidate.data_choices = tracestate.get_remainder(candidate_id).data_choices.copy()
154152
else:
155153
candidate = self._generate_scenario_variant(candidate, tracestate.model or ModelSpace())
156154
return candidate
@@ -176,38 +174,36 @@ def _fail_on_step_errors(suite):
176174
for s in error_list])
177175
raise Exception(err_msg)
178176

179-
def _try_to_fit_in_scenario(self, candidate, tracestate, refinement_stack):
177+
def _try_to_fit_in_scenario(self, candidate, tracestate):
180178
"""
181179
Tries to insert the candidate scenario into the trace (in full or partial) and
182-
updates tracestate and refinement_stack accordingly.
180+
updates tracestate accordingly.
183181
"""
184182
model = tracestate.model if tracestate.model else ModelSpace()
185183
model.new_scenario_scope()
186184
inserted, remainder, new_model, extra_data = self._process_scenario(candidate, model)
187185
if not inserted: # insertion failed
188-
model.end_scenario_scope() # redundant??
189186
tracestate.reject_scenario(candidate.src_id)
190187
logger.debug(extra_data['fail_msg'])
191188
self._report_tracestate_to_user(tracestate)
192189
elif not remainder: # the scenario processed in full
193190
new_model.end_scenario_scope()
194191
tracestate.confirm_full_scenario(inserted.src_id, inserted, new_model)
195192
logger.debug(f"Inserted scenario {inserted.src_id}, {inserted.name}")
196-
if refinement_stack:
197-
self._handle_refinement_exit(inserted, tracestate, refinement_stack)
193+
if tracestate.is_refinement_active():
194+
self._handle_refinement_exit(inserted, tracestate)
198195
self._report_tracestate_to_user(tracestate)
199196
logger.debug(f"last state:\n{tracestate.model.get_status_text()}")
200197
else: # the scenario is split into two parts, ready for refinement
201198
logger.debug(f"Partially inserted scenario {inserted.src_id}, {inserted.name}\n"
202199
f"Refinement needed at step: {remainder.steps[1]}")
203200
inserted.name = f"{inserted.name} (part {tracestate.highest_part(inserted.src_id)+1})"
204-
tracestate.push_partial_scenario(inserted.src_id, inserted, new_model)
205-
refinement_stack.append(remainder)
201+
tracestate.push_partial_scenario(inserted.src_id, inserted, new_model, remainder)
206202
self._report_tracestate_to_user(tracestate)
207203
logger.debug(f"last state:\n{new_model.get_status_text()}")
208204

209-
def _handle_refinement_exit(self, inserted_refinement, tracestate, refinement_stack):
210-
refinement_tail = refinement_stack.pop()
205+
def _handle_refinement_exit(self, inserted_refinement, tracestate):
206+
refinement_tail = tracestate.get_remainder(tracestate.active_refinements[-1])
211207
exit_conditions = refinement_tail.steps[1].model_info['OUT']
212208
exit_conditions_processed = False
213209
for expr in exit_conditions:
@@ -221,7 +217,6 @@ def _handle_refinement_exit(self, inserted_refinement, tracestate, refinement_st
221217

222218
if not exit_conditions_processed:
223219
self._rewind(tracestate) # Reject insterted scenario. Even though it fits, it is not a refinement.
224-
refinement_stack.append(refinement_tail)
225220
logger.debug(f"Reconsidering scenario {inserted_refinement.src_id}, {inserted_refinement.name}, "
226221
f"did not meet refinement conditions: {exit_conditions}")
227222
return
@@ -238,14 +233,13 @@ def _handle_refinement_exit(self, inserted_refinement, tracestate, refinement_st
238233
new_model.end_scenario_scope()
239234
tracestate.confirm_full_scenario(tail_inserted.src_id, tail_inserted, new_model)
240235
logger.debug(f"Scenario '{tail_inserted.name}' completed after refinement")
241-
if refinement_stack:
242-
self._handle_refinement_exit(tail_inserted, tracestate, refinement_stack)
236+
if tracestate.is_refinement_active():
237+
self._handle_refinement_exit(tail_inserted, tracestate)
243238
else:
244239
logger.debug(f"Partially inserted remainder of scenario {tail_inserted.src_id}, {tail_inserted.name}\n"
245240
f"refinement needed at step: {remainder.steps[1]}")
246241
tail_inserted.name = f"{tail_inserted.name} (part {tracestate.highest_part(tail_inserted.src_id)+1})"
247-
tracestate.push_partial_scenario(tail_inserted.src_id, tail_inserted, new_model)
248-
refinement_stack.append(remainder)
242+
tracestate.push_partial_scenario(tail_inserted.src_id, tail_inserted, new_model, remainder)
249243

250244
@staticmethod
251245
def _split_for_refinement(scenario, step):
@@ -264,7 +258,9 @@ def _split_for_refinement(scenario, step):
264258
return (front, back)
265259

266260
def _rewind(self, tracestate, drought_recovery=False):
267-
# Todo: Rewind needs to consider refinement stack.
261+
if tracestate[-1].remainder and tracestate.highest_part(tracestate[-1].remainder.src_id) > 1:
262+
# When rewinding an 'in between' part, rewind both the part and the refinement
263+
tracestate.rewind()
268264
tail = tracestate.rewind()
269265
while drought_recovery and tracestate.coverage_drought:
270266
tail = tracestate.rewind()
@@ -278,24 +274,23 @@ def _escape_robot_vars(text):
278274

279275
@staticmethod
280276
def _process_scenario(scenario, model):
281-
m = model.copy()
282277
for step in scenario.steps:
283278
if 'error' in step.model_info:
284-
return None, None, m, dict(fail_masg=f"Error in scenario {scenario.name} "
285-
f"at step {step}: {step.model_info['error']}")
279+
return None, None, model, dict(fail_masg=f"Error in scenario {scenario.name} "
280+
f"at step {step}: {step.model_info['error']}")
286281
for expr in SuiteProcessors._relevant_expressions(step):
287282
try:
288-
if m.process_expression(expr, step.args) is False:
283+
if model.process_expression(expr, step.args) is False:
289284
if step.gherkin_kw in ['when', None] and expr in step.model_info['OUT']:
290285
part1, part2 = SuiteProcessors._split_for_refinement(scenario, step)
291-
return part1, part2, m, dict()
286+
return part1, part2, model, dict()
292287
else:
293-
return None, None, m, dict(fail_msg=f"Unable to insert scenario {scenario.src_id}, "
294-
f"{scenario.name}, due to step '{step}': [{expr}] is False")
288+
return None, None, model, dict(fail_msg=f"Unable to insert scenario {scenario.src_id}, "
289+
f"{scenario.name}, due to step '{step}': [{expr}] is False")
295290
except Exception as err:
296-
return None, None, m, dict(fail_msg=f"Unable to insert scenario {scenario.src_id}, {scenario.name},"
297-
f" due to step '{step}': [{expr}] {err}")
298-
return scenario.copy(), None, m, dict()
291+
return None, None, model, dict(fail_msg=f"Unable to insert scenario {scenario.src_id}, "
292+
f"{scenario.name}, due to step '{step}': [{expr}] {err}")
293+
return scenario.copy(), None, model, dict()
299294

300295
@staticmethod
301296
def _relevant_expressions(step):

robotmbt/tracestate.py

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,6 @@ def tried(self):
4949
"""returns the indices that were rejected or previously inserted at the current position"""
5050
return tuple(self._tried[-1])
5151

52-
def coverage_reached(self):
53-
return all(self.c_pool.values())
54-
5552
@property
5653
def coverage_drought(self):
5754
"""Number of scenarios since last new coverage"""
@@ -61,44 +58,64 @@ def coverage_drought(self):
6158
def id_trace(self):
6259
return [snap.id for snap in self._snapshots]
6360

61+
@property
62+
def active_refinements(self):
63+
return self._open_refinements[:]
64+
65+
def coverage_reached(self):
66+
return all(self.c_pool.values())
67+
6468
def get_trace(self):
6569
return [snap.scenario for snap in self._snapshots]
6670

6771
def next_candidate(self, retry=False):
6872
for i in self.c_pool:
69-
if i not in self._tried[-1] and not self._is_refinement_active(i) and self.count(i) == 0:
73+
if i not in self._tried[-1] and not self.is_refinement_active(i) and self.count(i) == 0:
7074
return i
7175
if not retry:
7276
return None
7377
for i in self.c_pool:
74-
if i not in self._tried[-1] and not self._is_refinement_active(i):
78+
if i not in self._tried[-1] and not self.is_refinement_active(i):
7579
return i
7680
return None
7781

7882
def count(self, index):
79-
"""Count the number of times the index is present in the trace.
80-
unfinished partial scenarios are excluded."""
83+
"""
84+
Count the number of times the index is present in the trace.
85+
unfinished partial scenarios are excluded.
86+
"""
8187
return self.c_pool[index]
8288

8389
def highest_part(self, index):
84-
"""Given the current trace and an index, returns the highest part number of an ongoing
85-
refinement for the related scenario. Returns 0 when there is no refinement active."""
90+
"""
91+
Given the current trace and an index, returns the highest part number of an ongoing
92+
refinement for the related scenario. Returns 0 when there is no refinement active.
93+
"""
8694
for i in range(1, len(self.id_trace)+1):
8795
if self.id_trace[-i] == f'{index}':
8896
return 0
8997
if self.id_trace[-i].startswith(f'{index}.'):
9098
return int(self.id_trace[-i].split('.')[1])
9199
return 0
92100

93-
def _is_refinement_active(self, index):
94-
return self.highest_part(index) != 0
101+
def is_refinement_active(self, index=None):
102+
"""
103+
When called with an index, returns True if that scenario is currently being refined
104+
When index is ommitted, return True if any refinement is active
105+
"""
106+
if index is None:
107+
return self._open_refinements != []
108+
else:
109+
return self.highest_part(index) != 0
95110

96-
def find_scenarios_with_active_refinement(self):
97-
scenarios = []
98-
for i in self._open_refinements:
99-
index = -self.id_trace[::-1].index(f'{i}.1')-1
100-
scenarios.append(self._snapshots[index].scenario)
101-
return scenarios
111+
def get_remainder(self, index):
112+
"""
113+
When pushing a partial scenario, the remainder can be passed along for safe keeping.
114+
This method retrieves the remainder for the last part that was pushed.
115+
"""
116+
last_part = self.highest_part(index)
117+
index = -self.id_trace[::-1].index(f'{index}.{last_part}')-1
118+
return self._snapshots[index].remainder
102119

103120
def reject_scenario(self, i_scenario):
104121
"""Trying a scenario excludes it from further cadidacy on this level"""
@@ -107,24 +124,24 @@ def reject_scenario(self, i_scenario):
107124
def confirm_full_scenario(self, index, scenario, model):
108125
c_drought = 0 if self.c_pool[index] == 0 else self.coverage_drought+1
109126
self.c_pool[index] += 1
110-
if self._is_refinement_active(index):
127+
if self.is_refinement_active(index):
111128
id = f"{index}.0"
112129
self._open_refinements.pop()
113130
else:
114131
id = str(index)
115132
self._tried[-1].append(index)
116133
self._tried.append([])
117-
self._snapshots.append(TraceSnapShot(id, scenario, model, c_drought))
134+
self._snapshots.append(TraceSnapShot(id, scenario, model, drought=c_drought))
118135

119-
def push_partial_scenario(self, index, scenario, model):
120-
if self._is_refinement_active(index):
136+
def push_partial_scenario(self, index, scenario, model, remainder=None):
137+
if self.is_refinement_active(index):
121138
id = f"{index}.{self.highest_part(index)+1}"
122139
else:
123140
id = f"{index}.1"
124141
self._tried[-1].append(index)
125142
self._tried.append([])
126143
self._open_refinements.append(index)
127-
self._snapshots.append(TraceSnapShot(id, scenario, model, self.coverage_drought))
144+
self._snapshots.append(TraceSnapShot(id, scenario, model, remainder, self.coverage_drought))
128145

129146
def can_rewind(self):
130147
return len(self._snapshots) > 0
@@ -159,9 +176,10 @@ def __len__(self):
159176

160177

161178
class TraceSnapShot:
162-
def __init__(self, id, inserted_scenario, model_state, drought=0):
179+
def __init__(self, id, inserted_scenario, model_state, remainder=None, drought=0):
163180
self.id = id
164181
self.scenario = inserted_scenario
182+
self.remainder = remainder
165183
self._model = model_state.copy()
166184
self.coverage_drought = drought
167185

0 commit comments

Comments
 (0)