Skip to content

Support post_mortem mode via entering specific frames #1103

Closed

Description

Background

This would be a continuation from #722. I would need this feature to implement post-mortem debugging for unit tests (nvim-neotest/neotest-python#23).

#722 discusses how the debugger could stop at an exception that would be thrown from an user code to a library code (e.g., pytest) using the "user uncaught" exception breakpoints (#111). However, I don't think it is enough to implement post-mortem debugging, because it will pause too aggressively at exceptions that regularly crosses the boundaries between user and library code as pointed out in #1102.

One quick example would be like this:

def test_foo():
  with pytest.raises(SomeUserExpectedError):
      _an_user_code_raises_exception()   # <--  (A) The debugger will always stop here even if it's "expected"

  assert "something has failed"  # <---- (B) The debugger should stop here only

Because an exception that is thrown from a user code to the pytest framework will be considered as an "user uncaught" exception, it will hit a breakpoint on the line (A).

New feature: entering specific frames

In another issue #1102, I suggest implementing some filters or condition logics to control exception breakpoints in a more fine-grained manner, but we could also think of a feature to let the debugger enter specific stack frames in a post-mortem mode.

I was trying something like

                tb = excinfo._excinfo[2]
                
                debugpy.server.api._settrace(
                    suspend=True,
                    patch_multiprocessing=False,
                    stop_at_frame=tb.tb_frame,
                )

but it does not work as pydevd.settrace does not support stopping at or entering a frame that is not relevant with the current stacktrace (it just says Frame skipped from debugging during step-in.).

Note: PuDB has such a feature!

PuDB allows to enter some specific stack frames obtained from a thrown exception.

dbg.interaction(frame, exc_tuple)

A use case can be found at pytest-pudb: https://github.com/wronglink/pytest-pudb/blob/master/pytest_pudb.py#L100-L122.

Activity

  1. fabioz commented on Oct 27, 2022

    @fabioz
    Collaborator

    Closing this as this is a duplicate of: #723 (note that there's some code on that issue on how to do that with the current version of the debugger -- it uses some internal APIs, and we'd like to have better public APIs for this, but that can probably be used in the meanwhile... that code has been stable for quite a while and there are no plans to change it).

  2. wookayin commented on Oct 27, 2022

    @wookayin
    Author

    Thanks for the pointer to #723 (comment) --- which I didn't find while searching the issues with the keywords I was using!

    Based on the code example you provided and a single function extracted from that (#723 (comment)), I was able to make the debugger enter and stop at the exception raised out of a pytest method.

    Below is a proof-of-concept, but a single-file script to demonstrate the behavior:

    """A script to run pytest tests with debugger stopping at exceptions."""
    
    from typing import Union
    
    import pytest
    
    
    def test_foo():
      x = "some local variable"
      with pytest.raises(ValueError):
        raise ValueError("The debugger should not stop here")
      assert False, "A debugger should stop here"
    
    
    def test_bar():
      assert False, "A debugger should stop here AS WELL"
    
    
    def maybe_debugpy_postmortem(excinfo):
      """Make the debugpy debugger enter and stop at a raised exception.
    
      excinfo: A (type(e), e, e.__traceback__) tuple. See sys.exc_info()
      """
      import debugpy
      if not debugpy.is_client_connected():
        return  # Do not thing if not running with a DAP debugger
    
      import threading
      import pydevd
    
      py_db = pydevd.get_global_debugger()
      thread = threading.current_thread()
      additional_info = py_db.set_additional_thread_info(thread)
      additional_info.is_tracing += 1
      try:
        py_db.stop_on_unhandled_exception(py_db, thread, additional_info, excinfo)
      finally:
        additional_info.is_tracing -= 1
    
    
    class PytestDebuggerIntegration:
    
      @staticmethod
      def pytest_exception_interact(
          node: Union['pytest.Item', 'pytest.Collector'],
          call: 'pytest.CallInfo',
          report: Union['pytest.CollectReport', 'pytest.TestReport'],
      ):
        # https://docs.pytest.org/en/latest/reference/reference.html#pytest.hookspec.pytest_exception_interact
        maybe_debugpy_postmortem(call.excinfo._excinfo)
    
    
    if __name__ == '__main__':
      # yapf: disable
      # pytest.main([__file__], plugins=[])     # <-- Does not hit breakpoints
      pytest.main([__file__], plugins=[PytestDebuggerIntegration])  # <-- The debugger should stop twice 
      # yapf: enable

    I might write and publish this as a separate pytest plugin soon, if you don't mind. What do you think?

  3. fabioz commented on Oct 27, 2022

    @fabioz
    Collaborator

    I might write and publish this as a separate pytest plugin soon, if you don't mind. What do you think?

    I think that'd be ok, I just have 1 note:

    Don't check for debugpy.is_client_connected():... just check if pydevd.get_global_debugger() returns None or if you can't import pydevd (then it'd work with both debugpy as well as pydevd).

  4. wookayin commented on Oct 27, 2022

    @wookayin
    Author

    Don't check for debugpy.is_client_connected():... just check if pydevd.get_global_debugger() returns None or if you can't import pydevd (then it'd work with both debugpy as well as pydevd).

    That's a great point. I have updated the code snippet in #723. Thanks so much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions