class WriteOnlyFileLike(with_metaclass(ABCMeta)): def read(self, size=None): # completeness of file-like object raise IOError('%s object is write only' % self.__class__.__name__) readline = read readlines = read def seek(self, offset, whence=None): # completeness of file-like object raise IOError('%s object cannot seek' % self.__class__.__name__) @property def mode(self): return 'w' def isatty(self): return False @abstractmethod def write(self, msg): raise NotImplementedError('write') def writelines(self, msgs): for msg in msgs: self.write(msg)
class ServerTester(with_metaclass(Py2TestMeta, TestCase)): def test_start_stop(self): """ Tests starting and stopping the server. """ server = QdbServer(client_port=0, tracer_port=0) self.assertFalse(server.is_running) server.start() self.assertTrue(server.is_running) server.stop() self.assertFalse(server.is_running) def test_runforever_exit(self): """ Tests that stopping a server from one greenlet causes serve_forever() to return. """ server = QdbServer(client_port=0, tracer_port=0) with gevent.Timeout(1, False): # Stop the server in 0.3 seconds. gevent.spawn_later(0.3, server.stop) server.serve_forever() self.assertFalse(server.is_running) def test_bad_auth_client(self): """ Tests a non-valid auth message for a client. """ with QdbServer( client_host='localhost', client_port=0, client_auth_fn=lambda _: False, # Fail all new clients. tracer_server=QdbNopServer()) as server: ws = create_connection('ws://*****:*****@parameterized.expand(['hard', 'soft']) def test_inactivity_timeout(self, mode): """ Tests that timeout sends a disable message with the proper mode.. """ with QdbServer( tracer_host='localhost', tracer_port=0, client_host='localhost', client_port=0, inactivity_timeout=0.01, # minutes sweep_time=0.01, # seconds timeout_disable_mode=mode) as server: tracer = gevent.socket.create_connection( ('localhost', server.tracer_server.server_port)) send_tracer_event(tracer, 'start', { 'uuid': 'test', 'auth': '', 'local': (0, 0), }) client = create_connection('ws://*****:*****@parameterized.expand(['hard', 'soft']) def test_client_attach_timeout(self, mode): """ Tests the case when a client attaches but no tracer does. """ with QdbServer(tracer_server=QdbNopServer(), client_host='localhost', client_port=0, attach_timeout=0.01, timeout_disable_mode=mode) as server: client = create_connection('ws://*****:*****@parameterized.expand(['hard', 'soft']) def test_tracer_attach_timeout(self, mode): """ Tests the case where a tracer attaches but no client does. """ with QdbServer(tracer_host='localhost', tracer_port=0, client_server=QdbNopServer(), attach_timeout=0.01, timeout_disable_mode=mode) as server: tracer = gevent.socket.create_connection( ('localhost', server.tracer_server.server_port)) send_tracer_event(tracer, 'start', { 'uuid': 'test', 'auth': '', 'local': (0, 0), }) disable_event = None with gevent.Timeout(0.1, False): error_event = recv_tracer_event(tracer) disable_event = recv_tracer_event(tracer) error_dict = fmt_err_msg('client', 'No client') self.assertEqual(error_dict, error_event) self.assertEqual(fmt_msg('disable', mode), disable_event) self.assertNotIn('test', server.session_store) def test_client_orphan_session(self): """ Tests that a client makes it into the session store without a tracer attaching if attach_timeout is set to ALLOW_ORPHANS or 0. """ with QdbServer(tracer_server=QdbNopServer(), client_host='localhost', client_port=0, attach_timeout=ALLOW_ORPHANS) as server: client = create_connection('ws://localhost:%d%s' % (server.client_server.server_port, DEFAULT_ROUTE_FMT.format(uuid='test'))) send_client_event(client, 'start', '') # yield to the session_store to let it get attached. gyield() self.assertIn('test', server.session_store) def test_tracer_orphan_session(self): """ Tests that a tracer makes it into the session_store without a client attaching if attach_timeout is set to ALLOW_ORPHANS or 0. """ with QdbServer(client_server=QdbNopServer(), tracer_host='localhost', tracer_port=0, attach_timeout=ALLOW_ORPHANS) as server: tracer = gevent.socket.create_connection( ('localhost', server.tracer_server.server_port)) send_tracer_event(tracer, 'start', { 'uuid': 'test', 'auth': '', 'local': (0, 0), }) # yield to the session_store to let it get attached. gyield() self.assertIn('test', server.session_store)
class RemoteCommandManagerTester(with_metaclass(Py2TestMeta, TestCase)): """ Tests the various behaviors that the RemoteCommandManager should conform to. Some tests rely on how the command manager affects the tracer that it is managing. """ @skip_py3 @classmethod def setUpClass(cls): """ Start up a tracer server for the remote command managers to connect to. """ cls.setup_server() def setUp(self): self.cmd_manager = RemoteCommandManager() @classmethod def setup_server(cls): """ Sets up the server to run on a random yet valid port. """ cls.bad_auth_msg = 'BAD-AUTH' cls.tracer_host = cls.client_host = 'localhost' cls.server = QdbServer( tracer_host=cls.tracer_host, tracer_port=0, client_host=cls.client_host, client_port=0, tracer_auth_fn=lambda a: a != cls.bad_auth_msg, attach_timeout=0, ) cls.server.start() cls.tracer_port = cls.server.tracer_server.server_port @classmethod def tearDownClass(cls): """ Stop the test server. """ cls.server.stop() def tearDown(self): if Qdb._instance: Qdb._instance.disable() def MockTracer(self): """ Construct a mock tracer. """ tracer = MagicMock() tracer.address = self.tracer_host, self.tracer_port tracer.pause_signal = signal.SIGUSR2 tracer.retry_attempts = 1 tracer.local = 0, 0 tracer.uuid = 'mock' tracer.watchlist = {} tracer.curframe = sys._getframe() tracer.stack = [(sys._getframe(), 1)] * 3 tracer.skip_fn = lambda _: False tracer.cmd_manager = self.cmd_manager return tracer def test_connect(self): """ Tests that the remote command manager can connect to the server. """ tracer = self.MockTracer() # If we fail to connect, an error is raised and we fail the test. self.cmd_manager.start(tracer) def test_fail_to_connect(self): """ Assert that the correct error is raised if we cannot connect. """ tracer = self.MockTracer() tracer.address = 'not' + self.tracer_host, self.tracer_port with self.assertRaises(QdbFailedToConnect): self.cmd_manager.start(tracer, '') def test_fail_auth(self): """ Asserts that failing auth gives us the proper error. """ tracer = self.MockTracer() with self.assertRaises(QdbAuthenticationError): cmd_manager = self.cmd_manager cmd_manager.start(tracer, self.bad_auth_msg) cmd_manager.next_command(tracer) @parameterized.expand([ ({'file': 'test.py', 'line': 2},), ({'line': 2},), ({'line': 2, 'cond': '2 + 2 == 4'},), ({'line': 2, 'func': 'f'},), ({'line': 2, 'file': 't.py', 'cond': 'f()', 'func': 'g'},) ]) def test_fmt_breakpoint_dict(self, arg_dict): tracer = self.MockTracer() tracer.default_file = 'd.py' cmd_manager = self.cmd_manager cpy = dict(arg_dict) self.assertEqual( cmd_manager.fmt_breakpoint_dict(tracer, cpy), set_break_params(tracer, **cpy) ) @parameterized.expand([ (lambda t: t.set_step, 'step'), (lambda t: t.set_next, 'next'), (lambda t: t.set_continue, 'continue'), (lambda t: t.set_return, 'return'), (lambda t: t.set_break, 'set_break', { 'file': fix_filename(__file__), 'line': 1 }), (lambda t: t.clear_break, 'clear_break', { 'file': fix_filename(__file__), 'line': 1 }), (lambda t: t.set_watch, 'set_watch', ['2 + 2']), (lambda t: t.clear_watch, 'clear_watch', ['2 + 2']), (lambda t: t.get_file, 'list'), (lambda t: t.eval_fn, 'eval' '2 + 2'), (lambda t: t.disable, 'disable', 'hard'), (lambda t: t.disable, 'disable', 'soft'), ]) def test_commands(self, attrgetter_, event, payload=None): """ Tests various commands with or without payloads. """ tracer = self.MockTracer() cmd_manager = self.cmd_manager cmd_manager.start(tracer, '') self.server.session_store.send_to_tracer( uuid=tracer.uuid, event=fmt_msg(event, payload) ) with gevent.Timeout(0.1, False): cmd_manager.next_command(tracer) tracer.start.assert_called() # Start always gets called. attrgetter_(tracer).assert_called() # Kill the session we just created self.server.session_store.slaughter(tracer.uuid) def test_locals(self): """ Tests accessing the locals. """ tracer = self.MockTracer() tracer.curframe_locals = {'a': 'a'} cmd_manager = self.cmd_manager cmd_manager.start(tracer, '') gyield() self.server.session_store.send_to_tracer( uuid=tracer.uuid, event=fmt_msg('locals') ) command_locals_called = NonLocal(False) def test_command_locals(cmd_manager, tracer, payload): command_locals_called.value = True type(self.cmd_manager).command_locals(cmd_manager, tracer, payload) cmd_locals = partial(test_command_locals, cmd_manager) with gevent.Timeout(0.1, False), \ patch.object(cmd_manager, 'command_locals', cmd_locals): cmd_manager.next_command(tracer) self.assertTrue(command_locals_called.value) tracer.start.assert_called() # Start always gets called. self.server.session_store.slaughter(tracer.uuid) @parameterized.expand([('up',), ('down',)]) def test_stack_transpose_no_skip(self, direction): """ Tests moving up the stack. """ events = [] def capture_event(self, event, payload): events.append(fmt_msg(event, payload)) class cmd_manager(type(self.cmd_manager)): """ Wrap send_stack by just capturing the output to make assertions on it. """ def send_stack(self, tracer): with patch.object(cmd_manager, 'send_event', capture_event): super(cmd_manager, self).send_stack(tracer) db = Qdb( uuid='test_' + direction, cmd_manager=cmd_manager(), host=self.tracer_host, port=self.tracer_port, redirect_output=False, green=True, ) gyield() if direction == 'down': # We are already located in the bottom frame, let's go up one # so that we may try going down. self.server.session_store.send_to_tracer( uuid=db.uuid, event=fmt_msg('up') ) self.server.session_store.send_to_tracer( uuid=db.uuid, event=fmt_msg(direction) ) self.server.session_store.send_to_tracer( uuid=db.uuid, event=fmt_msg('disable', 'soft') ) gyield() db.set_trace() start_ind = events[-2]['p']['index'] shift_ind = events[-1]['p']['index'] if direction == 'up': self.assertEqual(start_ind - shift_ind, 1) elif direction == 'down': self.assertEqual(shift_ind - start_ind, 1) else: self.fail("direction is not 'up' or 'down'") # wut did u do? def test_pause(self): """ Asserts that sending a pause to the process will raise the pause signal in the tracer process. """ pause_called = [False] def pause_handler(signal, stackframe): """ Pause handler that marks that we made it into this function. """ pause_called[0] = True db = Qdb( cmd_manager=self.cmd_manager, host=self.tracer_host, port=self.tracer_port, green=True, ) signal.signal(db.pause_signal, pause_handler) self.server.session_store.send_to_tracer( uuid=db.uuid, event=fmt_msg('pause') ) self.assertTrue(pause_called) @parameterized.expand([ ('2 + 2', None, '4'), ('print "test"', None, 'test'), ('ValueError("test")', None, "ValueError('test',)"), ('raise ValueError("test")', 'ValueError', 'ValueError: test'), ('[][10]', 'IndexError', 'IndexError: list index out of range'), ('{}["test"]', 'KeyError', "KeyError: 'test'"), ]) def test_eval_results(self, input_, exc, output): """ Tests that evaling code returns the proper results. """ prints = [] class cmd_manager(type(self.cmd_manager)): """ Captures print commands to make assertions on them. """ def send_print(self, input_, exc, output): prints.append({ 'input': input_, 'exc': exc, 'output': output }) db = Qdb( uuid='eval_test', cmd_manager=cmd_manager(), host=self.tracer_host, port=self.tracer_port, redirect_output=False, green=True, ) gyield() self.server.session_store.send_to_tracer( uuid=db.uuid, event=fmt_msg('eval', input_) ) self.server.session_store.send_to_tracer( uuid=db.uuid, event=fmt_msg('continue') ) db.set_trace(stop=True) self.server.session_store.slaughter(db.uuid) self.assertTrue(prints) print_ = prints[0] self.assertEqual(print_['input'], input_) self.assertEqual(print_['exc'], exc) self.assertEqual(print_['output'], output) @parameterized.expand([ ('2 + 2', None, '4'), ('print "test"', None, 'test'), ('ValueError("test")', None, "ValueError('test',)"), ('raise ValueError("test")', 'ValueError', 'ValueError: test'), ('[][10]', 'IndexError', 'IndexError: list index out of range'), ('{}["test"]', 'KeyError', "KeyError: 'test'"), ('(1,) * 30', None, pformat((1,) * 30)), ('set(range(30))', None, pformat(set(range(30)))), ]) def test_eval_pprint(self, input_, exc, output): """ Tests that evaling code returns the proper results. """ prints = [] class cmd_manager(type(self.cmd_manager)): """ Captures print commands to make assertions on them. """ def send_print(self, input_, exc, output): prints.append({ 'input': input_, 'exc': exc, 'output': output }) db = Qdb( uuid='eval_test', cmd_manager=cmd_manager(), host=self.tracer_host, port=self.tracer_port, redirect_output=False, ) gyield() self.server.session_store.send_to_tracer( uuid=db.uuid, event=fmt_msg('pprint', input_) ) self.server.session_store.send_to_tracer( uuid=db.uuid, event=fmt_msg('continue') ) db.set_trace(stop=True) self.server.session_store.slaughter(db.uuid) self.assertTrue(prints) print_ = prints[0] self.assertEqual(print_['input'], input_) self.assertEqual(print_['exc'], exc) self.assertEqual(print_['output'], output) def test_eval_state_update(self): """ Tests that eval may update the state of the program. """ # We will try to corrupt this variable with a stateful operation. test_var = 'pure' # NOQA db = Qdb( uuid='eval_test', cmd_manager=self.cmd_manager, host=self.tracer_host, port=self.tracer_port, redirect_output=False, green=True, ) gyield() self.server.session_store.send_to_tracer( uuid=db.uuid, event=fmt_msg('eval', "test_var = 'mutated'") ) self.server.session_store.send_to_tracer( uuid=db.uuid, event=fmt_msg('continue') ) db.set_trace(stop=True) self.server.session_store.slaughter(db.uuid) self.assertEqual(test_var, 'mutated') def test_eval_timeout(self): """ Tests that evaluating user repl commands will raise Timeouts. """ def g(): while True: pass prints = [] class cmd_manager(type(self.cmd_manager)): """ Captures print commands to make assertions on them. """ def send_print(self, input_, exc, output): prints.append({ 'input': input_, 'exc': exc, 'output': output }) to_eval = 'g()' db = Qdb( uuid='timeout_test', cmd_manager=cmd_manager(), host=self.tracer_host, port=self.tracer_port, redirect_output=False, execution_timeout=1, green=True, ) gyield() self.server.session_store.send_to_tracer( uuid=db.uuid, event=fmt_msg('eval', to_eval) ) self.server.session_store.send_to_tracer( uuid=db.uuid, event=fmt_msg('continue') ) db.set_trace(stop=True) self.server.session_store.slaughter(db.uuid) self.assertTrue(prints) print_ = prints[0] self.assertEqual(print_['input'], to_eval) self.assertTrue(print_['exc']) self.assertEqual( print_['output'], db.exception_serializer(QdbExecutionTimeout(to_eval, 1)) ) def test_send_disabled(self): """ Tests that disabling sends a 'disabled' message back to the server. """ class cmd_manager(type(self.cmd_manager)): disabled = False def send_disabled(self): self.disabled = True db = Qdb( uuid='send_disabled_test', cmd_manager=cmd_manager(), host=self.tracer_host, port=self.tracer_port, redirect_output=False, green=True, ) gyield() db.set_trace(stop=False) db.disable() self.assertTrue(db.cmd_manager.disabled) self.server.session_store.slaughter(db.uuid) @parameterized.expand([(False,), (True,)]) def test_send_stack_results(self, use_skip_fn): """ Tests that the results from sending the stack are accurate. WARNING: This test uses lines of it's own source as string literals, be sure to edit the source and the string if you make any changes. """ def skip_fn(filename): return not fix_filename(__file__) in filename events = [] def capture_event(self, event, payload): events.append(fmt_msg(event, payload)) class cmd_manager(type(self.cmd_manager)): """ Wrap send_stack by just capturing the output to make assertions on it. """ def send_stack(self, tracer): with patch.object(cmd_manager, 'send_event', capture_event): super(cmd_manager, self).send_stack(tracer) db = Qdb( uuid='send_stack_test', cmd_manager=cmd_manager(), host=self.tracer_host, port=self.tracer_port, redirect_output=False, skip_fn=skip_fn if use_skip_fn else None, green=True, ) gyield() self.server.session_store.send_to_tracer( uuid=db.uuid, event=fmt_msg('continue') ) db.set_trace(stop=True) self.assertTrue(events) # EDIT IN BOTH PLACES event = events[0] if use_skip_fn: # Assert that we actually suppressed some frames. self.assertTrue(len(event['p']['stack']) < len(db.stack)) self.assertEqual( # I love dictionaries so much! event['p']['stack'][event['p']['index']]['code'], ' self.assertTrue(events) # EDIT IN BOTH PLACES', ) self.server.session_store.slaughter(db.uuid) def test_why_are_you_executing_all_these_commands(self): db = Qdb( uuid='send_stack_test', cmd_manager=self.cmd_manager, host=self.tracer_host, port=self.tracer_port, redirect_output=False, green=True, ) gyield() for n in range(sys.getrecursionlimit()): self.server.session_store.send_to_tracer( uuid=db.uuid, event=fmt_msg('eval', 'None') ) self.server.session_store.send_to_tracer( uuid=db.uuid, event=fmt_msg('continue') ) with gevent.Timeout(1): db.set_trace(stop=True)
class CommandManager(with_metaclass(ABCMeta, object)): """ An abstract base class for the command managers that control the tracer. """ def _fmt_stackframe(self, tracer, stackframe, line): """ Formats stackframe payload data. """ filename = stackframe.f_code.co_filename func = stackframe.f_code.co_name code = tracer.get_line(filename, line) return { 'file': tracer.canonic(filename), 'line': line, 'func': func, 'code': code, } def send_disabled(self): """ Sends a message to the server to say that the tracer is done. """ try: self.send_event('disabled') except socket.error: # We may safely ignore errors that occur here because we are # already disabled. pass def send_breakpoints(self): """ Sends the breakpoint list event. """ self.send_event('breakpoints', [ fmt_breakpoint(breakpoint) for breakpoint in Breakpoint.bpbynumber if breakpoint ]) def send_watchlist(self, tracer): """ Sends the watchlist event. """ self.send_event( 'watchlist', [{ 'expr': k, 'exc': exc, 'value': val } for k, (exc, val) in items(tracer.watchlist)], ) def send_print(self, input_, exc, output): """ Sends the print event with the given input and output. """ self.send( fmt_msg('print', { 'input': input_, 'exc': exc, 'output': output }, serial=json.dumps)) def send_stack(self, tracer): """ Sends the stack event. This filters out frames based on the rules defined in the tracer's skip_fn. The index reported will account for any skipped frames, such that querying the stack at the index provided will return the current frame. """ stack = [] index = tracer.curindex skip_fn = tracer.skip_fn for n, (frame, line) in enumerate(tracer.stack): if skip_fn(frame.f_code.co_filename): if n < tracer.curindex: index -= 1 # Drop the index to account for a skip continue # Don't add frames we need to skip. stack.append(self._fmt_stackframe(tracer, frame, line)) self.send_event('stack', { 'index': index, 'stack': stack, }) def send_error(self, error_type, error_data): """ Sends a formatted error message. """ self.send(fmt_err_msg(error_type, error_data, serial=json.dumps)) def send_event(self, event, payload=None): """ Sends a formatted event. """ self.send(fmt_msg(event, payload, serial=json.dumps)) @tco def next_command(self, tracer, msg=None): """ Processes the next command from the user. If msg is given, it is sent with self.send(msg) before processing the next command. """ if msg: self.send(msg) return self.user_next_command(tracer) @abstractmethod def send(self, msg): """ Sends a raw (already jsond) message. """ raise NotImplementedError @abstractmethod def user_next_command(self, tracer): """ Processes the next command. This method must be overridden to dictate how the commands are processed. """ raise NotImplementedError @abstractmethod def start(self, tracer, auth_msg=''): """ Start acquiring new commands. """ raise NotImplementedError def stop(self): """ Stop acquiring new commands. """ self.send_disabled() self.user_stop() @abstractmethod def user_stop(self): """ Use this to release and resources needed to generate the commands. """ raise NotImplementedError