import pprint

import sys

import unittest



from test import test_support



class TestGetProfile(unittest.TestCase):

    def setUp(self):

        sys.setprofile(None)



    def tearDown(self):

        sys.setprofile(None)



    def test_empty(self):

        assert sys.getprofile() == None



    def test_setget(self):

        def fn(*args):

            pass



        sys.setprofile(fn)

        assert sys.getprofile() == fn



class HookWatcher:

    def __init__(self):

        self.frames = []

        self.events = []



    def callback(self, frame, event, arg):

        if (event == "call"

            or event == "return"

            or event == "exception"):

            self.add_event(event, frame)



    def add_event(self, event, frame=None):

        """Add an event to the log."""

        if frame is None:

            frame = sys._getframe(1)



        try:

            frameno = self.frames.index(frame)

        except ValueError:

            frameno = len(self.frames)

            self.frames.append(frame)



        self.events.append((frameno, event, ident(frame)))



    def get_events(self):

        """Remove calls to add_event()."""

        disallowed = [ident(self.add_event.im_func), ident(ident)]

        self.frames = None



        return [item for item in self.events if item[2] not in disallowed]





class ProfileSimulator(HookWatcher):

    def __init__(self, testcase):

        self.testcase = testcase

        self.stack = []

        HookWatcher.__init__(self)



    def callback(self, frame, event, arg):

        # Callback registered with sys.setprofile()/sys.settrace()

        self.dispatch[event](self, frame)



    def trace_call(self, frame):

        self.add_event('call', frame)

        self.stack.append(frame)



    def trace_return(self, frame):

        self.add_event('return', frame)

        self.stack.pop()



    def trace_exception(self, frame):

        self.testcase.fail(

            "the profiler should never receive exception events")



    def trace_pass(self, frame):

        pass



    dispatch = {

        'call': trace_call,

        'exception': trace_exception,

        'return': trace_return,

        'c_call': trace_pass,

        'c_return': trace_pass,

        'c_exception': trace_pass,

        }





class TestCaseBase(unittest.TestCase):

    def check_events(self, callable, expected):

        events = capture_events(callable, self.new_watcher())

        if events != expected:

            self.fail("Expected events:\n%s\nReceived events:\n%s"

                      % (pprint.pformat(expected), pprint.pformat(events)))





class ProfileHookTestCase(TestCaseBase):

    def new_watcher(self):

        return HookWatcher()



    def test_simple(self):

        def f(p):

            pass

        f_ident = ident(f)

        self.check_events(f, [(1, 'call', f_ident),

                              (1, 'return', f_ident),

                              ])



    def test_exception(self):

        def f(p):

            1/0

        f_ident = ident(f)

        self.check_events(f, [(1, 'call', f_ident),

                              (1, 'return', f_ident),

                              ])



    def test_caught_exception(self):

        def f(p):

            try: 1/0

            except: pass

        f_ident = ident(f)

        self.check_events(f, [(1, 'call', f_ident),

                              (1, 'return', f_ident),

                              ])



    def test_caught_nested_exception(self):

        def f(p):

            try: 1/0

            except: pass

        f_ident = ident(f)

        self.check_events(f, [(1, 'call', f_ident),

                              (1, 'return', f_ident),

                              ])



    def test_nested_exception(self):

        def f(p):

            1/0

        f_ident = ident(f)

        self.check_events(f, [(1, 'call', f_ident),

                              # This isn't what I expected:

                              # (0, 'exception', protect_ident),

                              # I expected this again:

                              (1, 'return', f_ident),

                              ])



    def test_exception_in_except_clause(self):

        def f(p):

            1/0

        def g(p):

            try:

                f(p)

            except:

                try: f(p)

                except: pass

        f_ident = ident(f)

        g_ident = ident(g)

        self.check_events(g, [(1, 'call', g_ident),

                              (2, 'call', f_ident),

                              (2, 'return', f_ident),

                              (3, 'call', f_ident),

                              (3, 'return', f_ident),

                              (1, 'return', g_ident),

                              ])



    def test_exception_propogation(self):

        def f(p):

            1/0

        def g(p):

            try: f(p)

            finally: p.add_event("falling through")

        f_ident = ident(f)

        g_ident = ident(g)

        self.check_events(g, [(1, 'call', g_ident),

                              (2, 'call', f_ident),

                              (2, 'return', f_ident),

                              (1, 'falling through', g_ident),

                              (1, 'return', g_ident),

                              ])



    def test_raise_twice(self):

        def f(p):

            try: 1/0

            except: 1/0

        f_ident = ident(f)

        self.check_events(f, [(1, 'call', f_ident),

                              (1, 'return', f_ident),

                              ])



    def test_raise_reraise(self):

        def f(p):

            try: 1/0

            except: raise

        f_ident = ident(f)

        self.check_events(f, [(1, 'call', f_ident),

                              (1, 'return', f_ident),

                              ])



    def test_raise(self):

        def f(p):

            raise Exception()

        f_ident = ident(f)

        self.check_events(f, [(1, 'call', f_ident),

                              (1, 'return', f_ident),

                              ])



    def test_distant_exception(self):

        def f():

            1/0

        def g():

            f()

        def h():

            g()

        def i():

            h()

        def j(p):

            i()

        f_ident = ident(f)

        g_ident = ident(g)

        h_ident = ident(h)

        i_ident = ident(i)

        j_ident = ident(j)

        self.check_events(j, [(1, 'call', j_ident),

                              (2, 'call', i_ident),

                              (3, 'call', h_ident),

                              (4, 'call', g_ident),

                              (5, 'call', f_ident),

                              (5, 'return', f_ident),

                              (4, 'return', g_ident),

                              (3, 'return', h_ident),

                              (2, 'return', i_ident),

                              (1, 'return', j_ident),

                              ])



    def test_generator(self):

        def f():

            for i in range(2):

                yield i

        def g(p):

            for i in f():

                pass

        f_ident = ident(f)

        g_ident = ident(g)

        self.check_events(g, [(1, 'call', g_ident),

                              # call the iterator twice to generate values

                              (2, 'call', f_ident),

                              (2, 'return', f_ident),

                              (2, 'call', f_ident),

                              (2, 'return', f_ident),

                              # once more; returns end-of-iteration with

                              # actually raising an exception

                              (2, 'call', f_ident),

                              (2, 'return', f_ident),

                              (1, 'return', g_ident),

                              ])



    def test_stop_iteration(self):

        def f():

            for i in range(2):

                yield i

            raise StopIteration

        def g(p):

            for i in f():

                pass

        f_ident = ident(f)

        g_ident = ident(g)

        self.check_events(g, [(1, 'call', g_ident),

                              # call the iterator twice to generate values

                              (2, 'call', f_ident),

                              (2, 'return', f_ident),

                              (2, 'call', f_ident),

                              (2, 'return', f_ident),

                              # once more to hit the raise:

                              (2, 'call', f_ident),

                              (2, 'return', f_ident),

                              (1, 'return', g_ident),

                              ])





class ProfileSimulatorTestCase(TestCaseBase):

    def new_watcher(self):

        return ProfileSimulator(self)



    def test_simple(self):

        def f(p):

            pass

        f_ident = ident(f)

        self.check_events(f, [(1, 'call', f_ident),

                              (1, 'return', f_ident),

                              ])



    def test_basic_exception(self):

        def f(p):

            1/0

        f_ident = ident(f)

        self.check_events(f, [(1, 'call', f_ident),

                              (1, 'return', f_ident),

                              ])



    def test_caught_exception(self):

        def f(p):

            try: 1/0

            except: pass

        f_ident = ident(f)

        self.check_events(f, [(1, 'call', f_ident),

                              (1, 'return', f_ident),

                              ])



    def test_distant_exception(self):

        def f():

            1/0

        def g():

            f()

        def h():

            g()

        def i():

            h()

        def j(p):

            i()

        f_ident = ident(f)

        g_ident = ident(g)

        h_ident = ident(h)

        i_ident = ident(i)

        j_ident = ident(j)

        self.check_events(j, [(1, 'call', j_ident),

                              (2, 'call', i_ident),

                              (3, 'call', h_ident),

                              (4, 'call', g_ident),

                              (5, 'call', f_ident),

                              (5, 'return', f_ident),

                              (4, 'return', g_ident),

                              (3, 'return', h_ident),

                              (2, 'return', i_ident),

                              (1, 'return', j_ident),

                              ])





def ident(function):

    if hasattr(function, "f_code"):

        code = function.f_code

    else:

        code = function.func_code

    return code.co_firstlineno, code.co_name





def protect(f, p):

    try: f(p)

    except: pass



protect_ident = ident(protect)





def capture_events(callable, p=None):

    try:

        sys.setprofile()

    except TypeError:

        pass

    else:

        raise test_support.TestFailed(

            'sys.setprofile() did not raise TypeError')



    if p is None:

        p = HookWatcher()

    sys.setprofile(p.callback)

    protect(callable, p)

    sys.setprofile(None)

    return p.get_events()[1:-1]





def show_events(callable):

    import pprint

    pprint.pprint(capture_events(callable))





def test_main():

    test_support.run_unittest(

        TestGetProfile,

        ProfileHookTestCase,

        ProfileSimulatorTestCase

    )





if __name__ == "__main__":

    test_main()

