Repository: microsoft/ptvsd

Issue:
Show return value does not always work
## Environment data

- PTVSD version: master
- OS and version: Any
- Python version (& distribution if applicable, e.g. Anaconda): any
- Using VS Code or Visual Studio: VSC

## Steps to repro

```py
def factorial(n):
    if n == 0:
        return 1
    return n * factorial(n-1)  # does not show return value here
 
if __name__ == '__main__':
    factorial(10)  # shows return value here

```

## Some notes on the issue:
> The reason this happens is because pydevd will only show the return value on a step over when the step ends in the same function where the step was started (i.e.: https://github.com/Microsoft/ptvsd/blob/master/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_frame.py#L644), in this case the debugger is reverting to a step in (https://github.com/Microsoft/ptvsd/blob/master/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_frame.py#L475) to stop at the next available opportunity and doesn't add the return value.



Discussion:

Resolving patch:
diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_frame.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_frame.py
--- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_frame.py
+++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_frame.py
@@ -682,7 +682,11 @@ def trace_dispatch(self, frame, event, arg):
                         return self.trace_dispatch
 
                 if main_debugger.show_return_values:
-                    if is_return and info.pydev_step_cmd in (CMD_STEP_OVER, CMD_STEP_OVER_MY_CODE) and frame.f_back == stop_frame:
+                    if is_return and (
+                            (info.pydev_step_cmd in (CMD_STEP_OVER, CMD_STEP_OVER_MY_CODE) and (frame.f_back is stop_frame)) or
+                            (info.pydev_step_cmd in (CMD_STEP_RETURN, CMD_STEP_RETURN_MY_CODE) and (frame is stop_frame)) or
+                            (info.pydev_step_cmd in (CMD_STEP_INTO, CMD_STEP_INTO_MY_CODE, CMD_STEP_INTO_COROUTINE))
+                        ):
                         self.show_return_values(frame, arg)
 
                 elif main_debugger.remove_return_values_flag:
diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py
--- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py
+++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py
@@ -14,7 +14,7 @@
     CMD_STEP_RETURN, CMD_STEP_CAUGHT_EXCEPTION, CMD_ADD_EXCEPTION_BREAK, CMD_SET_BREAK, \
     CMD_SET_NEXT_STATEMENT, CMD_THREAD_SUSPEND_SINGLE_NOTIFICATION, \
     CMD_THREAD_RESUME_SINGLE_NOTIFICATION, CMD_THREAD_KILL, CMD_STOP_ON_START, CMD_INPUT_REQUESTED, \
-    CMD_EXIT, CMD_STEP_INTO_COROUTINE
+    CMD_EXIT, CMD_STEP_INTO_COROUTINE, constant_to_str, CMD_STEP_RETURN_MY_CODE
 from _pydevd_bundle.pydevd_constants import get_thread_id, dict_values, ForkSafeLock
 from _pydevd_bundle.pydevd_net_command import NetCommand, NULL_NET_COMMAND
 from _pydevd_bundle.pydevd_net_command_factory_xml import NetCommandFactory
@@ -267,6 +267,7 @@ def make_io_message(self, v, ctx):
         CMD_STEP_OVER,
         CMD_STEP_OVER_MY_CODE,
         CMD_STEP_RETURN,
+        CMD_STEP_RETURN_MY_CODE,
         CMD_STEP_INTO_MY_CODE,
         CMD_STOP_ON_START,
         CMD_STEP_INTO_COROUTINE,


Test patch:
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_return_value.py b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_return_value.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_return_value.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_return_value.py
@@ -6,7 +6,11 @@ def method2():
     return 2
 
 
-if __name__ == '__main__':
+def main():
     method1()  # break here
     method2()
     print('TEST SUCEEDED!')
+
+
+if __name__ == '__main__':
+    main()
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_return_value_asyncio.py b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_return_value_asyncio.py
new file mode 100644
--- /dev/null
+++ b/src/ptvsd/_vendored/pydevd/tests_python/resources/_debugger_case_return_value_asyncio.py
@@ -0,0 +1,26 @@
+import asyncio
+
+
+async def method1():
+    return 1
+
+
+async def method2():
+    return 2
+
+
+async def main():
+    await method1()  # break here
+    await method2()
+    print('TEST SUCEEDED!')
+
+
+if __name__ == '__main__':
+    if hasattr(asyncio, 'run'):
+        print('using asyncio.run')
+        asyncio.run(main())
+    else:
+        print('using loop.run_until_complete')
+        loop = asyncio.get_event_loop()
+        loop.run_until_complete(main())
+        loop.close()
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py
@@ -3039,13 +3039,13 @@ def get_environ(writer):
 def test_return_value(case_setup):
     with case_setup.test_file('_debugger_case_return_value.py') as writer:
         break_line = writer.get_line_index_with_content('break here')
-        writer.write_add_breakpoint(break_line, '')
+        writer.write_add_breakpoint(break_line)
         writer.write_show_return_vars()
         writer.write_make_initial_run()
-        hit = writer.wait_for_breakpoint_hit(name='<module>', line=break_line)
+        hit = writer.wait_for_breakpoint_hit(name='main', line=break_line)
 
         writer.write_step_over(hit.thread_id)
-        hit = writer.wait_for_breakpoint_hit(REASON_STEP_OVER, name='<module>', line=break_line + 1)
+        hit = writer.wait_for_breakpoint_hit(REASON_STEP_OVER, name='main', line=break_line + 1)
         writer.write_get_frame(hit.thread_id, hit.frame_id)
         writer.wait_for_vars([
             [
@@ -3055,7 +3055,7 @@ def test_return_value(case_setup):
         ])
 
         writer.write_step_over(hit.thread_id)
-        hit = writer.wait_for_breakpoint_hit(REASON_STEP_OVER, name='<module>', line=break_line + 2)
+        hit = writer.wait_for_breakpoint_hit(REASON_STEP_OVER, name='main', line=break_line + 2)
         writer.write_get_frame(hit.thread_id, hit.frame_id)
         writer.wait_for_vars([
             [
@@ -3695,6 +3695,7 @@ def get_environ(writer):
         writer.finished_ok = True
 
 
+@pytest.mark.skipif(IS_JYTHON, reason='Flaky on Jython')
 def test_generator_step_in(case_setup):
     with case_setup.test_file('_debugger_case_generator_step_in.py') as writer:
         line = writer.get_line_index_with_content('stop 1')
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/test_debugger_json.py
@@ -1093,8 +1093,17 @@ def test_stack_and_variables_dict(case_setup):
         writer.finished_ok = True
 
 
-def test_return_value(case_setup):
-    with case_setup.test_file('_debugger_case_return_value.py') as writer:
+@pytest.mark.parametrize('scenario', [
+    'step_in',
+    'step_next',
+    'step_out',
+])
+@pytest.mark.parametrize('asyncio', [True, False])
+def test_return_value_regular(case_setup, scenario, asyncio):
+    if IS_PY2 and asyncio:
+        raise pytest.skip('asyncio not available for python 2.')
+
+    with case_setup.test_file('_debugger_case_return_value.py' if not asyncio else '_debugger_case_return_value_asyncio.py') as writer:
         json_facade = JsonFacade(writer)
 
         break_line = writer.get_line_index_with_content('break here')
@@ -1103,8 +1112,26 @@ def test_return_value(case_setup):
         json_facade.write_make_initial_run()
 
         json_hit = json_facade.wait_for_thread_stopped()
-        json_facade.write_step_next(json_hit.thread_id)
-        json_hit = json_facade.wait_for_thread_stopped('step', name='<module>', line=break_line + 1)
+        if scenario == 'step_next':
+            json_facade.write_step_next(json_hit.thread_id)
+            json_hit = json_facade.wait_for_thread_stopped('step', name='main', line=break_line + 1)
+
+        elif scenario == 'step_in':
+            json_facade.write_step_in(json_hit.thread_id)
+            json_hit = json_facade.wait_for_thread_stopped('step', name='method1')
+
+            json_facade.write_step_in(json_hit.thread_id)
+            json_hit = json_facade.wait_for_thread_stopped('step', name='main')
+
+        elif scenario == 'step_out':
+            json_facade.write_step_in(json_hit.thread_id)
+            json_hit = json_facade.wait_for_thread_stopped('step', name='method1')
+
+            json_facade.write_step_out(json_hit.thread_id)
+            json_hit = json_facade.wait_for_thread_stopped('step', name='main')
+
+        else:
+            raise AssertionError('unhandled scenario: %s' % (scenario,))
 
         variables_response = json_facade.get_variables_response(json_hit.frame_id)
         return_variables = json_facade.filter_return_variables(variables_response.body.variables)