def test_starting_a_stopping_thread_restarts(self): allow_stop = Event() def thread_func(ctx): try: while not ctx.stop_requested: ctx.sleep(0.1) except InterruptedError: pass finally: print('Waiting for allow_stop event') allow_stop.wait(2) allow_stop.clear() print('allow_stop event set') tw = ThreadWrapper(thread_func) tw.start().wait() tw.stop() tw.start() self.assertEqual(ThreadWrapper.STOPPING, tw.state) allow_stop.set() time.sleep(0.1) self.assertEqual(ThreadWrapper.RUNNING, tw.state) allow_stop.set() tw.exit()
def test_callback_is_called_when_thread_stops(self): mock = Mock(return_value=None) tw = ThreadWrapper(lambda _: None) tw.on_stopped(mock) try: for i in range(3): tw.start() tw.stop().wait() self.assertEqual(ThreadWrapper.STOPPED, tw.state) self.assertNotEqual(0, mock.call_count) finally: tw.exit()
def test_thread_wrapper_can_be_exited_if_started(self): mock = Mock() tw = ThreadWrapper(lambda x: None) tw.on_stopped(mock) tw.start() tw.exit() self.assertEqual(1, mock.call_count)
def __init__(self, owner: 'ScriptManager', script, name, global_variables: dict): self._owner = owner self._globals = global_variables.copy() self._inputs = {} self._runnable = script self.sleep = self._default_sleep self._thread = ThreadWrapper(self._run, f'ScriptThread: {name}') self.log = get_logger(f'Script: {name}') self.stop = self._thread.stop self.cleanup = self._thread.exit self.on_stopped = self._thread.on_stopped self.on_stopping = self._thread.on_stop_requested assert (callable(script)) self.log('Created')
def test_exiting_thread_can_not_be_restarted(self): counter = 0 def thread_func(ctx): nonlocal counter counter += 1 while not ctx.stop_requested: ctx.sleep(0.1) def _try_restart(): self.assertRaises(AssertionError, tw.start) tw = ThreadWrapper(thread_func) tw.on_stopped(_try_restart) tw.start() tw.exit() self.assertEqual(1, counter)
def test_stop_callbacks_called_when_thread_fn_exits(self): evt = Event() def test_fn(_): pass tw = ThreadWrapper(test_fn) try: for i in range(1, 3): with self.subTest(f'Run #{i}'): tw.on_stopped(evt.set) evt.clear() tw.start() if not evt.wait(2): self.fail('Thread function has not exited properly') finally: tw.exit()
def test_exiting_a_running_thread_stops_it(self): mock = Mock() def test_fn(ctx): evt = Event() ctx.on_stopped(evt.set) evt.wait() mock() tw = ThreadWrapper(test_fn) tw.start().wait() tw.exit() self.assertEqual(1, mock.call_count)
def test_thread_function_can_observe_stop_request(self): mock = Mock() thread_started_evt = Event() def _dummy_thread_fn(ctx: ThreadContext): thread_started_evt.set() evt = Event() ctx.on_stopped(evt.set) evt.wait() mock() tw = ThreadWrapper(_dummy_thread_fn) try: tw.start() if not thread_started_evt.wait(2): self.fail('Thread function was not executed') tw.stop().wait() self.assertEqual(1, mock.call_count) finally: tw.exit()
def test_start_does_not_raise_if_not_exited(self): def test_fn(_): print('running') tw = ThreadWrapper(test_fn) try: # this is a probabilistic failure, false negatives may happen still if the implementation is incorrect start = time.time() while time.time() - start < 5: for _ in range(10000): tw.start() except AssertionError: self.fail('start() raised event') finally: tw.exit()
def test_thread_function_receives_stop_signal(self): mock = Mock() stop_req_mock = Mock() thread_started_evt = Event() def _dummy_thread_fn(ctx: ThreadContext): mock() ctx.on_stopped(stop_req_mock) # stop signal calls callback thread_started_evt.set() while not ctx.stop_requested: # stop signal can be polled time.sleep(0.001) tw = ThreadWrapper(_dummy_thread_fn) try: tw.start() thread_started_evt.wait() self.assertEqual(1, mock.call_count) tw.stop().wait() self.assertEqual(1, stop_req_mock.call_count) finally: tw.exit()
def test_exception_stops_properly(self): evt = Event() def _dummy_thread_fn(): # wrong signature, results in TypeError pass tw = ThreadWrapper(_dummy_thread_fn) try: for i in range(1, 3): with self.subTest(f'Run #{i}'): tw.on_stopped(evt.set) # set callback first to verify it will be called after clear evt.clear() if not tw.start().wait(2): self.fail('Thread was not started properly') if not evt.wait(2): self.fail('Thread was not stopped properly') finally: tw.exit()
def test_stopping_a_starting_thread_stops_thread(self): mock = Mock() def test_fn(ctx): evt = Event() ctx.on_stopped(evt.set) if not evt.wait(2): self.fail('Thread stop was not called') mock() tw = ThreadWrapper(test_fn) try: # this is a probabilistic failure, false positives may happen still if the implementation is incorrect for i in range(1000): mock.reset_mock() tw.start() if not tw.stop().wait(2): self.fail('Failed to stop thread') self.assertEqual(ThreadWrapper.STOPPED, tw.state) self.assertEqual(1, mock.call_count) finally: tw.exit()
def test_thread_function_runs_only_once_per_start(self): mock = Mock() evt = Event() def test_fn(_): mock() evt.set() tw = ThreadWrapper(test_fn) try: for i in range(1, 3): with self.subTest(f'Run #{i}'): evt.clear() tw.start() if not evt.wait(2): self.fail('Thread function was not executed') self.assertEqual(i, mock.call_count) finally: tw.exit()
class ScriptHandle: def _default_sleep(self, _): self.log('Error: default sleep called') raise Exception('Script not running') def __init__(self, owner: 'ScriptManager', script, name, global_variables: dict): self._owner = owner self._globals = global_variables.copy() self._inputs = {} self._runnable = script self.sleep = self._default_sleep self._thread = ThreadWrapper(self._run, f'ScriptThread: {name}') self.log = get_logger(f'Script: {name}') self.stop = self._thread.stop self.cleanup = self._thread.exit self.on_stopped = self._thread.on_stopped self.on_stopping = self._thread.on_stop_requested assert (callable(script)) self.log('Created') @property def is_stop_requested(self): return self._thread.state in [ ThreadWrapper.STOPPING, ThreadWrapper.STOPPED ] @property def is_running(self): return self._thread.is_running def assign(self, name, value): self._globals[name] = value def _run(self, ctx): try: # script control interface def _terminate(): self.stop() raise InterruptedError ctx.terminate = _terminate ctx.terminate_all = self._owner.stop_all_scripts self.sleep = ctx.sleep self.log("Starting script") self._runnable(Control=ctx, ctx=ctx, time=TimeWrapper(ctx), **self._inputs) except InterruptedError: self.log('Interrupted') raise finally: # restore to release reference on context self.log("Script finished") self.sleep = self._default_sleep def start(self, **kwargs): if not kwargs: self._inputs = self._globals else: self._inputs = {**self._globals, **kwargs} return self._thread.start()
def create_remote_controller_thread(rcs: RemoteControllerScheduler): return ThreadWrapper(rcs.handle_controller, "RemoteControllerThread")
def test_thread_wrapper_can_be_exited_if_not_started(self): tw = ThreadWrapper(lambda: None) tw.exit()
def test_sleep_on_context_is_interrupted_when_thread_is_stopped(self): tw = ThreadWrapper(lambda ctx: ctx.sleep(10000)) start_time = time.time() tw.start().wait() tw.exit() self.assertLess(time.time() - start_time, 2)
def test_waiting_for_a_stopped_thread_to_stop_does_nothing(self): tw = ThreadWrapper(lambda ctx: None) tw.start().wait() tw.stop().wait() tw.stop().wait() tw.exit()
def test_exited_thread_can_not_be_restarted(self): tw = ThreadWrapper(lambda x: None) tw.exit() self.assertRaises(AssertionError, tw.start)