facebookincubator / Bowler

Safe code refactoring for modern Python.
https://pybowler.io/
MIT License
1.53k stars 89 forks source link

Support pdb in test cases #96

Open chdsbd opened 5 years ago

chdsbd commented 5 years ago

Currently it doesn't appear possible to set a breakpoint with import pdb; pdb.set_trace() in a BowlerTestCase. It looks like in_process is set to true when calling the modifier, but I still get a traceback when I attempt to use a debugger in the modifier.

I think it could be helpful to support using pdb in test cases.

from bowler.tests.lib import BowlerTestCase
from bowler.types import Leaf, TOKEN

class ExampleTestCase(BowlerTestCase):
    def test_modifier_return_value(self):
        input = "a+b"

        def modifier(node, capture, filename):
            new_op = Leaf(TOKEN.MINUS, "-")
            import pdb; pdb.set_trace() # works without this statement
            return new_op

        output = self.run_bowler_modifier(input, "'+'", modifier)
        self.assertEqual("a-b", output)
self = <refactoring.test.ExampleTestCase testMethod=test_modifier_return_value>

    def test_modifier_return_value(self):
        input = "a+b"

        def modifier(node, capture, filename):
            new_op = Leaf(TOKEN.MINUS, "-")
            import pdb; pdb.set_trace()
            return new_op

>       output = self.run_bowler_modifier(input, "'+'", modifier)

test.py:17: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <refactoring.test.ExampleTestCase testMethod=test_modifier_return_value>, input_text = 'a+b', selector = "'+'"
modifier = <function ExampleTestCase.test_modifier_return_value.<locals>.modifier at 0x11fc7c598>, selector_func = None, modifier_func = None, in_process = True
query_func = <function BowlerTestCase.run_bowler_modifier.<locals>.default_query_func at 0x11fcaaf28>

    def run_bowler_modifier(
        self,
        input_text,
        selector=None,
        modifier=None,
        selector_func=None,
        modifier_func=None,
        in_process=True,
        query_func=None,
    ):
        """Returns the modified text."""

        if not (selector or selector_func or query_func):
            raise ValueError("Pass selector")
        if not (modifier or modifier_func or query_func):
            raise ValueError("Pass modifier")

        exception_queue = multiprocessing.Queue()

        def store_exceptions_on(func):
            @functools.wraps(func)
            def inner(node, capture, filename):
                # When in_process=False, this runs in another process.  See notes below.
                try:
                    return func(node, capture, filename)
                except Exception as e:
                    exception_queue.put(e)

            return inner

        def default_query_func(files):
            if selector_func:
                q = selector_func(files)
            else:
                q = Query(files).select(selector)

            if modifier_func:
                q = modifier_func(q)
            else:
                q = q.modify(modifier)

            return q

        if query_func is None:
            query_func = default_query_func

        with tempfile.NamedTemporaryFile(suffix=".py") as f:
            # TODO: I'm almost certain this will not work on Windows, since
            # NamedTemporaryFile has it already open for writing.  Consider
            # using mktemp directly?
            with open(f.name, "w") as fw:
                fw.write(input_text + "\n")

            query = query_func([f.name])
            assert query is not None, "Remember to return the Query"
            assert query.retcode is None, "Return before calling .execute"
            assert len(query.transforms) == 1, "TODO: Support multiple"

            for i in range(len(query.current.callbacks)):
                query.current.callbacks[i] = store_exceptions_on(
                    query.current.callbacks[i]
                )

            # We require the in_process parameter in order to record coverage properly,
            # but it also helps in bubbling exceptions and letting tests read state set
            # by modifiers.
            query.execute(
                interactive=False, write=True, silent=False, in_process=in_process
            )

            # In the case of in_process=False (mirroring normal use of the tool) we use
            # the queue to ship back exceptions from local_process, which can actually
            # fail the test.  Normally exceptions in modifiers are not printed
            # at all unless you pass --debug, and even then you don't get the
            # traceback.
            # See https://github.com/facebookincubator/Bowler/issues/63
            if not exception_queue.empty():
>               raise AssertionError from exception_queue.get()
E               AssertionError
thatch commented 5 years ago

I suspect this is related to the stdout replacement at https://github.com/facebookincubator/Bowler/blob/master/bowler/tests/lib.py#L31 -- can you try restoring the write method? (Additionally, there may be further buffering in unittest itself, try something like sys.stdout=os.open(1, "w"))

If you give me a couple of days I can try to reproduce this directly.