|
21 | 21 | """ |
22 | 22 | import argparse |
23 | 23 | import collections |
| 24 | +import contextlib |
24 | 25 | import logging |
25 | 26 | import os |
| 27 | +import signal |
26 | 28 | import sys |
27 | 29 | import textwrap |
28 | 30 | import threading |
@@ -107,6 +109,17 @@ def create_arg_parser(add_help=False): |
107 | 109 | return parser |
108 | 110 |
|
109 | 111 |
|
| 112 | +@contextlib.contextmanager |
| 113 | +def sigint_context(): |
| 114 | + """Context within which handle_sig_int is the signal handler for SIGINT.""" |
| 115 | + # Register signal handler to stop all tests on SIGINT. |
| 116 | + replaced_sigint_handler = signal.signal(signal.SIGINT, Test.handle_sig_int) |
| 117 | + try: |
| 118 | + yield |
| 119 | + finally: |
| 120 | + signal.signal(signal.SIGINT, replaced_sigint_handler) |
| 121 | + |
| 122 | + |
110 | 123 | class Test(object): |
111 | 124 | """An object that represents an OpenHTF test. |
112 | 125 |
|
@@ -277,80 +290,81 @@ def execute(self, test_start=None): |
277 | 290 | Raises: |
278 | 291 | InvalidTestStateError: if this test is already being executed. |
279 | 292 | """ |
280 | | - # Lock this section so we don't .stop() the executor between instantiating |
281 | | - # it and .Start()'ing it, doing so does weird things to the executor state. |
282 | | - with self._lock: |
283 | | - # Sanity check to make sure someone isn't doing something weird like |
284 | | - # trying to Execute() the same test twice in two separate threads. We |
285 | | - # hold the lock between here and Start()'ing the executor to guarantee |
286 | | - # that only one thread is successfully executing the test. |
287 | | - if self._executor: |
288 | | - raise InvalidTestStateError('Test already running', self._executor) |
289 | | - |
290 | | - # Snapshot some things we care about and store them. |
291 | | - self._test_desc.metadata['test_name'] = self._test_options.name |
292 | | - self._test_desc.metadata['config'] = conf._asdict() |
293 | | - self.last_run_time_millis = util.time_millis() |
294 | | - |
295 | | - if isinstance(test_start, LambdaType): |
296 | | - @phase_descriptor.PhaseOptions() |
297 | | - def trigger_phase(test): |
298 | | - test.test_record.dut_id = test_start() |
299 | | - trigger = trigger_phase |
300 | | - else: |
301 | | - trigger = test_start |
302 | | - |
303 | | - if conf.capture_source: |
304 | | - trigger.code_info = test_record.CodeInfo.for_function(trigger.func) |
305 | | - |
306 | | - test_desc = self._get_running_test_descriptor() |
307 | | - self._executor = test_executor.TestExecutor( |
308 | | - test_desc, self.make_uid(), trigger, self._test_options) |
309 | | - |
310 | | - _LOG.info('Executing test: %s', self.descriptor.code_info.name) |
311 | | - self.TEST_INSTANCES[self.uid] = self |
312 | | - self._executor.start() |
313 | | - |
314 | | - try: |
315 | | - self._executor.wait() |
316 | | - except KeyboardInterrupt: |
317 | | - # The SIGINT handler only raises the KeyboardInterrupt once, so only retry |
318 | | - # that once. |
319 | | - self._executor.wait() |
320 | | - raise |
321 | | - finally: |
322 | | - try: |
323 | | - final_state = self._executor.finalize() |
324 | | - |
325 | | - _LOG.debug('Test completed for %s, outputting now.', |
326 | | - final_state.test_record.metadata['test_name']) |
327 | | - for output_cb in self._test_options.output_callbacks: |
328 | | - try: |
329 | | - output_cb(final_state.test_record) |
330 | | - except Exception: # pylint: disable=broad-except |
331 | | - _LOG.exception( |
332 | | - 'Output callback %s raised; continuing anyway', output_cb) |
333 | | - # Make sure the final outcome of the test is printed last and in a |
334 | | - # noticeable color so it doesn't get scrolled off the screen or missed. |
335 | | - if final_state.test_record.outcome == test_record.Outcome.ERROR: |
336 | | - for detail in final_state.test_record.outcome_details: |
337 | | - console_output.error_print(detail.description) |
| 293 | + with sigint_context(): |
| 294 | + # Lock this section so we don't .stop() the executor between instantiating |
| 295 | + # it and .Start()'ing it, doing so does weird things to the executor state. |
| 296 | + with self._lock: |
| 297 | + # Sanity check to make sure someone isn't doing something weird like |
| 298 | + # trying to Execute() the same test twice in two separate threads. We |
| 299 | + # hold the lock between here and Start()'ing the executor to guarantee |
| 300 | + # that only one thread is successfully executing the test. |
| 301 | + if self._executor: |
| 302 | + raise InvalidTestStateError('Test already running', self._executor) |
| 303 | + |
| 304 | + # Snapshot some things we care about and store them. |
| 305 | + self._test_desc.metadata['test_name'] = self._test_options.name |
| 306 | + self._test_desc.metadata['config'] = conf._asdict() |
| 307 | + self.last_run_time_millis = util.time_millis() |
| 308 | + |
| 309 | + if isinstance(test_start, LambdaType): |
| 310 | + @phase_descriptor.PhaseOptions() |
| 311 | + def trigger_phase(test): |
| 312 | + test.test_record.dut_id = test_start() |
| 313 | + trigger = trigger_phase |
338 | 314 | else: |
339 | | - colors = collections.defaultdict(lambda: colorama.Style.BRIGHT) |
340 | | - colors[test_record.Outcome.PASS] = ''.join((colorama.Style.BRIGHT, |
341 | | - colorama.Fore.GREEN)) |
342 | | - colors[test_record.Outcome.FAIL] = ''.join((colorama.Style.BRIGHT, |
343 | | - colorama.Fore.RED)) |
344 | | - msg_template = 'test: {name} outcome: {color}{outcome}{rst}' |
345 | | - console_output.banner_print(msg_template.format( |
346 | | - name=final_state.test_record.metadata['test_name'], |
347 | | - color=colors[final_state.test_record.outcome], |
348 | | - outcome=final_state.test_record.outcome.name, |
349 | | - rst=colorama.Style.RESET_ALL)) |
| 315 | + trigger = test_start |
| 316 | + |
| 317 | + if conf.capture_source: |
| 318 | + trigger.code_info = test_record.CodeInfo.for_function(trigger.func) |
| 319 | + |
| 320 | + test_desc = self._get_running_test_descriptor() |
| 321 | + self._executor = test_executor.TestExecutor( |
| 322 | + test_desc, self.make_uid(), trigger, self._test_options) |
| 323 | + |
| 324 | + _LOG.info('Executing test: %s', self.descriptor.code_info.name) |
| 325 | + self.TEST_INSTANCES[self.uid] = self |
| 326 | + self._executor.start() |
| 327 | + |
| 328 | + try: |
| 329 | + self._executor.wait() |
| 330 | + except KeyboardInterrupt: |
| 331 | + # The SIGINT handler only raises the KeyboardInterrupt once, so only retry |
| 332 | + # that once. |
| 333 | + self._executor.wait() |
| 334 | + raise |
350 | 335 | finally: |
351 | | - del self.TEST_INSTANCES[self.uid] |
352 | | - self._executor.close() |
353 | | - self._executor = None |
| 336 | + try: |
| 337 | + final_state = self._executor.finalize() |
| 338 | + |
| 339 | + _LOG.debug('Test completed for %s, outputting now.', |
| 340 | + final_state.test_record.metadata['test_name']) |
| 341 | + for output_cb in self._test_options.output_callbacks: |
| 342 | + try: |
| 343 | + output_cb(final_state.test_record) |
| 344 | + except Exception: # pylint: disable=broad-except |
| 345 | + _LOG.exception( |
| 346 | + 'Output callback %s raised; continuing anyway', output_cb) |
| 347 | + # Make sure the final outcome of the test is printed last and in a |
| 348 | + # noticeable color so it doesn't get scrolled off the screen or missed. |
| 349 | + if final_state.test_record.outcome == test_record.Outcome.ERROR: |
| 350 | + for detail in final_state.test_record.outcome_details: |
| 351 | + console_output.error_print(detail.description) |
| 352 | + else: |
| 353 | + colors = collections.defaultdict(lambda: colorama.Style.BRIGHT) |
| 354 | + colors[test_record.Outcome.PASS] = ''.join((colorama.Style.BRIGHT, |
| 355 | + colorama.Fore.GREEN)) |
| 356 | + colors[test_record.Outcome.FAIL] = ''.join((colorama.Style.BRIGHT, |
| 357 | + colorama.Fore.RED)) |
| 358 | + msg_template = 'test: {name} outcome: {color}{outcome}{rst}' |
| 359 | + console_output.banner_print(msg_template.format( |
| 360 | + name=final_state.test_record.metadata['test_name'], |
| 361 | + color=colors[final_state.test_record.outcome], |
| 362 | + outcome=final_state.test_record.outcome.name, |
| 363 | + rst=colorama.Style.RESET_ALL)) |
| 364 | + finally: |
| 365 | + del self.TEST_INSTANCES[self.uid] |
| 366 | + self._executor.close() |
| 367 | + self._executor = None |
354 | 368 |
|
355 | 369 | return final_state.test_record.outcome == test_record.Outcome.PASS |
356 | 370 |
|
|
0 commit comments