Skip to content

Commit 06a0bcd

Browse files
OpenHTF Ownerscopybara-github
authored andcommitted
Add repeat_on_measurement_fail to PhaseOptions so that the user can repeat a phase with failed measurements up to repeat_limit times. This setting is phase bound, allowing to repeat only the phase with failed measurements.
PiperOrigin-RevId: 511240393
1 parent c31f895 commit 06a0bcd

File tree

3 files changed

+105
-11
lines changed

3 files changed

+105
-11
lines changed

openhtf/core/phase_descriptor.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ class PhaseOptions(object):
103103
needs to wrap another phase for some reason, as PhaseDescriptors can only
104104
be invoked with a TestState instance.
105105
force_repeat: If True, force the phase to repeat up to repeat_limit times.
106+
repeat_on_measurement_fail: If true, force phase with failed
107+
measurements to repeat up to repeat_limit times.
106108
repeat_on_timeout: If consider repeat on phase timeout, default is No.
107109
repeat_limit: Maximum number of repeats. None indicates a phase will be
108110
repeated infinitely as long as PhaseResult.REPEAT is returned.
@@ -121,6 +123,7 @@ def PhaseFunc(test, port, other_info): pass
121123
run_if = attr.ib(type=Optional[Callable[[], bool]], default=None)
122124
requires_state = attr.ib(type=bool, default=False)
123125
force_repeat = attr.ib(type=bool, default=False)
126+
repeat_on_measurement_fail = attr.ib(type=bool, default=False)
124127
repeat_on_timeout = attr.ib(type=bool, default=False)
125128
repeat_limit = attr.ib(type=Optional[int], default=None)
126129
run_under_pdb = attr.ib(type=bool, default=False)
@@ -147,6 +150,10 @@ def __call__(self, phase_func: PhaseT) -> 'PhaseDescriptor':
147150
phase.options.requires_state = self.requires_state
148151
if self.repeat_on_timeout:
149152
phase.options.repeat_on_timeout = self.repeat_on_timeout
153+
if self.force_repeat:
154+
phase.options.force_repeat = self.force_repeat
155+
if self.repeat_on_measurement_fail:
156+
phase.options.repeat_on_measurement_fail = self.repeat_on_measurement_fail
150157
if self.repeat_limit is not None:
151158
phase.options.repeat_limit = self.repeat_limit
152159
if self.run_under_pdb:

openhtf/core/phase_executor.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,20 @@ def __init__(self, test_state: 'htf_test_state.TestState'):
242242
self._current_phase_thread = None # type: Optional[PhaseExecutorThread]
243243
self._stopping = threading.Event()
244244

245+
def _should_repeat(self, phase: phase_descriptor.PhaseDescriptor,
246+
phase_execution_outcome: PhaseExecutionOutcome) -> bool:
247+
"""Returns whether a phase should be repeated."""
248+
if phase_execution_outcome.is_timeout and phase.options.repeat_on_timeout:
249+
return True
250+
elif phase_execution_outcome.is_repeat:
251+
return True
252+
elif phase.options.force_repeat:
253+
return True
254+
elif phase.options.repeat_on_measurement_fail:
255+
last_phase_outcome = self.test_state.test_record.phases[-1].outcome
256+
return last_phase_outcome == test_record.PhaseOutcome.FAIL
257+
return False
258+
245259
def execute_phase(
246260
self,
247261
phase: phase_descriptor.PhaseDescriptor,
@@ -270,12 +284,8 @@ def execute_phase(
270284
is_last_repeat = repeat_count >= repeat_limit
271285
phase_execution_outcome, profile_stats = self._execute_phase_once(
272286
phase, is_last_repeat, run_with_profiling, subtest_rec)
273-
274-
# Give 3 default retries for timeout phase.
275-
# Force repeat up to the repeat limit if force_repeat is set.
276-
if ((phase_execution_outcome.is_timeout and
277-
phase.options.repeat_on_timeout) or phase_execution_outcome.is_repeat
278-
or phase.options.force_repeat) and not is_last_repeat:
287+
if (self._should_repeat(phase, phase_execution_outcome) and
288+
not is_last_repeat):
279289
repeat_count += 1
280290
continue
281291

@@ -362,7 +372,8 @@ def evaluate_checkpoint(
362372
subtest_name = None
363373
evaluated_millis = util.time_millis()
364374
try:
365-
outcome = PhaseExecutionOutcome(checkpoint.get_result(self.test_state, subtest_rec))
375+
outcome = PhaseExecutionOutcome(checkpoint.get_result(self.test_state,
376+
subtest_rec))
366377
self.logger.debug('Checkpoint %s result: %s', checkpoint.name,
367378
outcome.phase_result)
368379
if outcome.is_fail_subtest and not subtest_rec:

test/core/exe_test.py

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,18 @@ class MoreRepeatsUnittestPlug(UnittestPlug):
7171
return_continue_count = 100
7272

7373

74+
class RepeatTracker():
75+
76+
def __init__(self):
77+
self.count = 0
78+
79+
def increment(self):
80+
self.count += 1
81+
82+
def get_num_repeats(self) -> int:
83+
return self.count
84+
85+
7486
class FailedPlugError(Exception):
7587
"""Exception for the failed plug."""
7688

@@ -115,6 +127,29 @@ def phase_repeat(test, test_plug):
115127
return openhtf.PhaseResult.CONTINUE if ret else openhtf.PhaseResult.REPEAT
116128

117129

130+
@openhtf.PhaseOptions(repeat_on_measurement_fail=True, repeat_limit=5)
131+
@openhtf.measures(
132+
openhtf.Measurement('example_dimension').with_dimensions(
133+
'dim').dimension_pivot_validate(
134+
util.validators.InRange(
135+
minimum=-5,
136+
maximum=5,
137+
)))
138+
def phase_repeat_on_multidim_measurement_fail(test, meas_value: int,
139+
tracker: RepeatTracker):
140+
test.measurements['example_dimension'][0] = meas_value
141+
tracker.increment()
142+
143+
144+
@openhtf.PhaseOptions(repeat_on_measurement_fail=True, repeat_limit=5)
145+
@openhtf.measures(
146+
openhtf.Measurement('meas_val').in_range(minimum=-5, maximum=5,))
147+
def phase_repeat_on_measurement_fail(test, meas_value: int,
148+
tracker: RepeatTracker):
149+
test.measurements['meas_val'] = meas_value
150+
tracker.increment()
151+
152+
118153
@openhtf.PhaseOptions(run_if=lambda: False)
119154
def phase_skip_from_run_if(test):
120155
del test # Unused.
@@ -1129,15 +1164,16 @@ def test_branch_with_log(self):
11291164
'branch:{}'.format(diag_cond.message))
11301165

11311166

1132-
class PhaseExecutorTest(unittest.TestCase):
1167+
class PhaseExecutorTest(parameterized.TestCase):
11331168

11341169
def setUp(self):
11351170
super(PhaseExecutorTest, self).setUp()
11361171
self.test_state = mock.MagicMock(
11371172
spec=test_state.TestState,
11381173
plug_manager=plugs.PlugManager(),
11391174
execution_uid='01234567890',
1140-
state_logger=mock.MagicMock())
1175+
state_logger=mock.MagicMock(),
1176+
test_record=mock.MagicMock(spec=test_record.TestRecord))
11411177
self.test_state.plug_manager.initialize_plugs(
11421178
[UnittestPlug, MoreRepeatsUnittestPlug])
11431179
self.phase_executor = phase_executor.PhaseExecutor(self.test_state)
@@ -1148,14 +1184,54 @@ def test_execute_continue_phase(self):
11481184

11491185
def test_execute_repeat_okay_phase(self):
11501186
result, _ = self.phase_executor.execute_phase(
1151-
phase_repeat.with_plugs(test_plug=UnittestPlug))
1187+
phase_repeat.with_plugs(test_plug=UnittestPlug)
1188+
)
11521189
self.assertEqual(openhtf.PhaseResult.CONTINUE, result.phase_result)
11531190

11541191
def test_execute_repeat_limited_phase(self):
11551192
result, _ = self.phase_executor.execute_phase(
1156-
phase_repeat.with_plugs(test_plug=MoreRepeatsUnittestPlug))
1193+
phase_repeat.with_plugs(test_plug=MoreRepeatsUnittestPlug)
1194+
)
11571195
self.assertEqual(openhtf.PhaseResult.STOP, result.phase_result)
11581196

1197+
@parameterized.named_parameters(
1198+
# NAME, PHASE, MEASUREMENT_VALUE, OUTCOME, EXPECTED_NUMBER_OF_RUNS.
1199+
# Not failing phase with a simple measurement value in range [-5, +5].
1200+
('measurement_phase_not_failing', phase_repeat_on_measurement_fail, 4,
1201+
test_record.PhaseOutcome.PASS, 1),
1202+
# Failing phase with simple measurement value out of range.
1203+
('measurement_phase_failing', phase_repeat_on_measurement_fail, 10,
1204+
test_record.PhaseOutcome.FAIL, 5),
1205+
# Not failing phase with a multidim measurement value in range [-5, +5].
1206+
('multidim_measurement_phase_not_failing',
1207+
phase_repeat_on_multidim_measurement_fail, 4,
1208+
test_record.PhaseOutcome.PASS, 1),
1209+
# Failing phase with multidim measurement value out of range.
1210+
('multidim_measurement_phase_failing',
1211+
phase_repeat_on_multidim_measurement_fail, 10,
1212+
test_record.PhaseOutcome.FAIL, 5),
1213+
)
1214+
def test_execute_repeat_on_measurement_fail_phase(self, phase, meas_value,
1215+
outcome, num_runs):
1216+
mock_test_state = mock.MagicMock(
1217+
spec=test_state.TestState,
1218+
plug_manager=plugs.PlugManager(),
1219+
execution_uid='01234567890',
1220+
state_logger=mock.MagicMock(),
1221+
test_record=test_record.TestRecord('mock-dut-id', 'mock-station-id'))
1222+
mock_test_state.plug_manager.initialize_plugs(
1223+
[UnittestPlug, MoreRepeatsUnittestPlug])
1224+
my_phase_record = test_record.PhaseRecord.from_descriptor(phase)
1225+
my_phase_record.outcome = outcome
1226+
mock_test_state.test_record.add_phase_record(my_phase_record)
1227+
my_phase_executor = phase_executor.PhaseExecutor(mock_test_state)
1228+
tracker = RepeatTracker()
1229+
result, _ = my_phase_executor.execute_phase(
1230+
phase.with_args(tracker=tracker, meas_value=meas_value)
1231+
)
1232+
self.assertEqual(openhtf.PhaseResult.CONTINUE, result.phase_result)
1233+
self.assertEqual(tracker.get_num_repeats(), num_runs)
1234+
11591235
def test_execute_run_if_false(self):
11601236
result, _ = self.phase_executor.execute_phase(phase_skip_from_run_if)
11611237
self.assertEqual(openhtf.PhaseResult.SKIP, result.phase_result)

0 commit comments

Comments
 (0)