Skip to content

Commit 38c9128

Browse files
committed
Restructure test state finalization.
Finalizing the test state causes the loggers to be reset. This also meant that a failure during the teardown phase does not affect the test outcome. Changing this so the finalization always occurs after the teardown phase and plug teardowns to gather their logs.
1 parent e7fc8c7 commit 38c9128

3 files changed

Lines changed: 74 additions & 29 deletions

File tree

openhtf/core/test_executor.py

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def __init__(self, test_descriptor, execution_uid, test_start,
7474
self._exit_stack = None
7575
self.uid = execution_uid
7676
self.failure_exceptions = failure_exceptions
77+
self._latest_outcome = None
7778

7879
def stop(self):
7980
"""Stop this test."""
@@ -150,21 +151,29 @@ def _thread_proc(self):
150151
# Full plug initialization happens _after_ the start trigger, as close to
151152
# test execution as possible, for the best chance of test equipment being
152153
# in a known-good state at the start of test execution.
153-
if not self._initialize_plugs():
154+
if self._initialize_plugs():
154155
return
155156

156157
# Everything is set, set status and begin test execution.
157158
self._execute_test_phases(phase_exec)
158159

159160
def _initialize_plugs(self, plug_types=None):
161+
"""Initialize plugs.
162+
163+
Args:
164+
plug_types: optional list of plug classes to initialize.
165+
166+
Returns:
167+
True if there was an error initializing the plugs.
168+
"""
160169
try:
161170
self.test_state.plug_manager.initialize_plugs(plug_types=plug_types)
162-
return True
171+
return False
163172
except Exception: # pylint: disable=broad-except
164-
# Exit early if plug initializaion failed.
165-
self.test_state.finalize_on_failed_plug_initialization(
173+
# Record the equivalent failure outcome and exit early.
174+
self._latest_outcome = phase_executor.PhaseExecutionOutcome(
166175
phase_executor.ExceptionInfo(*sys.exc_info()))
167-
return False
176+
return True
168177

169178
def _execute_test_start(self, phase_exec):
170179
"""Run the start trigger phase, and check that the DUT ID is set after.
@@ -181,24 +190,35 @@ def _execute_test_start(self, phase_exec):
181190
"""
182191
# Have the phase executor run the start trigger phase. Do partial plug
183192
# initialization for just the plugs needed by the start trigger phase.
184-
if not self._initialize_plugs(
185-
plug_types=[phase_plug.cls for phase_plug in self._test_start.plugs]):
193+
if self._initialize_plugs(plug_types=[
194+
phase_plug.cls for phase_plug in self._test_start.plugs]):
186195
return True
187196

188-
outcome = phase_exec.execute_phase(self._test_start)
189-
if outcome.is_terminal:
190-
self.test_state.finalize_from_phase_outcome(outcome)
197+
self._latest_outcome = phase_exec.execute_phase(self._test_start)
198+
if self._latest_outcome.is_terminal:
199+
return True
191200

192201
if self.test_state.test_record.dut_id is None:
193202
_LOG.warning('Start trigger did not set DUT ID. A later phase will need'
194203
' to do so to prevent a BlankDutIdError when the test ends.')
195-
return outcome.is_terminal
204+
return False
196205

197206
def _execute_test_teardown(self, phase_exec):
198207
phase_exec.stop(timeout_s=conf.cancel_timeout_s)
199208
if self._do_teardown_function and self._teardown_function:
200-
phase_exec.execute_phase(self._teardown_function)
209+
outcome = phase_exec.execute_phase(self._teardown_function)
210+
# Ignore teardown phase outcome if there is already a terminal error.
211+
if not self._latest_outcome or not self._latest_outcome.is_terminal:
212+
self._latest_outcome = outcome
213+
# Plug teardown does not affect the test outcome.
201214
self.test_state.plug_manager.tear_down_plugs()
215+
216+
# Now finalize the test state.
217+
if self._latest_outcome and self._latest_outcome.is_terminal:
218+
self.test_state.finalize_from_phase_outcome(self._latest_outcome)
219+
else:
220+
self.test_state.finalize_normally()
221+
202222
# Make sure if there was an error during test execution that the error
203223
# message is printed last and in a noticeable color so it doesn't get
204224
# scrolled off the screen or missed.
@@ -212,12 +232,9 @@ def _execute_test_phases(self, phase_exec):
212232

213233
try:
214234
for phase in self._test_descriptor.phases:
215-
outcome = phase_exec.execute_phase(phase)
216-
if outcome.is_terminal:
217-
self.test_state.finalize_from_phase_outcome(outcome)
235+
self._latest_outcome = phase_exec.execute_phase(phase)
236+
if self._latest_outcome.is_terminal:
218237
break
219-
else:
220-
self.test_state.finalize_normally()
221238
except KeyboardInterrupt:
222239
self.test_state.logger.info('KeyboardInterrupt caught, aborting test.')
223240
raise

openhtf/core/test_state.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -335,17 +335,6 @@ def finalize_from_phase_outcome(self, phase_execution_outcome):
335335
'A phase stopped the test run.')
336336
self._finalize(test_record.Outcome.FAIL)
337337

338-
def finalize_on_failed_plug_initialization(self, except_info):
339-
if self._is_aborted():
340-
return
341-
342-
self.logger.error('Finishing test execution early due to failed plug '
343-
'initialization.')
344-
code = except_info.exc_type.__name__
345-
description = str(except_info.exc_val)
346-
self.test_record.add_outcome_details(code, description)
347-
self._finalize(test_record.Outcome.ERROR)
348-
349338
def finalize_normally(self):
350339
"""Mark the state as finished.
351340

test/core/exe_test.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,18 @@ def fail_plug_phase(fail):
123123
del fail
124124

125125

126+
def blank_phase():
127+
pass
128+
129+
130+
class TeardownError(Exception):
131+
pass
132+
133+
134+
def teardown_fail():
135+
raise TeardownError()
136+
137+
126138
class TestExecutor(unittest.TestCase):
127139

128140
class TestDummyExceptionError(Exception):
@@ -288,7 +300,7 @@ def never_gonna_run_phase():
288300

289301
test = openhtf.Test(never_gonna_run_phase)
290302
executor = core.TestExecutor(test.descriptor, 'uid', fail_plug_phase,
291-
teardown_function=lambda: ev.set()) # pyling: disable=unnecessary-lambda
303+
teardown_function=lambda: ev.set()) # pylint: disable=unnecessary-lambda
292304
executor.start()
293305
executor.wait()
294306
record = executor.test_state.test_record
@@ -299,6 +311,33 @@ def never_gonna_run_phase():
299311
self.assertFalse(ev.is_set())
300312
self.assertFalse(ev2.is_set())
301313

314+
def test_error_during_teardown(self):
315+
test = openhtf.Test(blank_phase)
316+
executor = core.TestExecutor(test.descriptor, 'uid', start_phase,
317+
teardown_function=teardown_fail)
318+
executor.start()
319+
executor.wait()
320+
record = executor.test_state.test_record
321+
self.assertEqual(record.outcome, Outcome.ERROR)
322+
self.assertEqual(record.outcome_details[0].code, TeardownError.__name__)
323+
324+
def test_log_during_teardown(self):
325+
message = 'hello'
326+
327+
def teardown_log(test):
328+
test.logger.info(message)
329+
330+
test = openhtf.Test(blank_phase)
331+
executor = core.TestExecutor(test.descriptor, 'uid', start_phase,
332+
teardown_function=teardown_log)
333+
executor.start()
334+
executor.wait()
335+
record = executor.test_state.test_record
336+
self.assertEqual(record.outcome, Outcome.PASS)
337+
log_records = [log_record for log_record in record.log_records
338+
if log_record.message == message]
339+
self.assertTrue(log_records)
340+
302341

303342
class TestPhaseExecutor(unittest.TestCase):
304343

0 commit comments

Comments
 (0)