示例#1
0
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)
示例#2
0
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)
示例#3
0
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)
示例#4
0
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