Repository: microsoft/ptvsd

Issue:
Ability to stop on the first line of user code
This is VSC specific


Discussion:
It’s also necessary for VS – if you start with the Step In or Over commands then we should break on the first line.

Will start to take a look at this.

The idea is creating a `CMD_STOP_ON_START` command for `pydevd` which has to be issued before the `CMD_RUN`.
Already done in `pydevd` (waiting for https://github.com/Microsoft/ptvsd/pull/692 to be merged before providing a pull request as changes were done on top of the contents in that pull request).

Resolving patch:
diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py
--- a/ptvsd/wrapper.py
+++ b/ptvsd/wrapper.py
@@ -719,6 +719,7 @@ def bool_parser(str):
     'FIX_FILE_PATH_CASE': bool_parser,
     'CLIENT_OS_TYPE': unquote,
     'DEBUG_STDLIB': bool_parser,
+    'STOP_ON_ENTRY': bool_parser,
 }
 
 
@@ -733,6 +734,7 @@ def bool_parser(str):
     'DebugStdLib': 'DEBUG_STDLIB=True',
     'WindowsClient': 'CLIENT_OS_TYPE=WINDOWS',
     'UnixClient': 'CLIENT_OS_TYPE=UNIX',
+    'StopOnEntry': 'STOP_ON_ENTRY=True',
 }
 
 
@@ -1373,6 +1375,10 @@ def _process_debug_options(self, opts):
         else:
             redirect_output = ''
         self.pydevd_request(pydevd_comm.CMD_REDIRECT_OUTPUT, redirect_output)
+
+        if opts.get('STOP_ON_ENTRY', False) and self.start_reason == 'launch':
+            self.pydevd_request(pydevd_comm.CMD_STOP_ON_START, '1')
+
         self._apply_code_stepping_settings()
 
     def _is_just_my_code_stepping_enabled(self):


Test patch:
diff --git a/tests/resources/system_tests/test_misc/mymod/__init__.py b/tests/resources/system_tests/test_misc/mymod/__init__.py
new file mode 100644
diff --git a/tests/resources/system_tests/test_misc/mymod/__main__.py b/tests/resources/system_tests/test_misc/mymod/__main__.py
new file mode 100644
--- /dev/null
+++ b/tests/resources/system_tests/test_misc/mymod/__main__.py
@@ -0,0 +1,3 @@
+print('one')
+print('two')
+print('three')
diff --git a/tests/resources/system_tests/test_misc/stoponentry.py b/tests/resources/system_tests/test_misc/stoponentry.py
new file mode 100644
--- /dev/null
+++ b/tests/resources/system_tests/test_misc/stoponentry.py
@@ -0,0 +1,3 @@
+print('one')
+print('two')
+print('three')
diff --git a/tests/system_tests/test_misc.py b/tests/system_tests/test_misc.py
--- a/tests/system_tests/test_misc.py
+++ b/tests/system_tests/test_misc.py
@@ -2,7 +2,10 @@
 import os.path
 import time
 from tests.helpers.resource import TestResources
-from . import (lifecycle_handshake, LifecycleTestsBase, DebugInfo)
+from . import (
+    _strip_newline_output_events, lifecycle_handshake,
+    LifecycleTestsBase, DebugInfo
+)
 
 TEST_FILES = TestResources.from_module(__name__)
 
@@ -66,3 +69,132 @@ def test_multi_thread(self):
         self.run_test_threads(
             DebugInfo(filename=filename, cwd=cwd),
             bp_filename=filename, bp_line=22, count=3)
+
+
+class StopOnEntryTests(LifecycleTestsBase):
+    def run_test_stop_on_entry_enabled(self, debug_info, expected_source_path):
+        options = {'debugOptions': ['RedirectOutput', 'StopOnEntry']}
+        with self.start_debugging(debug_info) as dbg:
+            session = dbg.session
+            with session.wait_for_event('stopped') as result:
+                lifecycle_handshake(
+                                    session, debug_info.starttype,
+                                    options=options)
+            received_before = self.find_events(session.received, 'output')
+
+            event = result['msg']
+            tid = event.body['threadId']
+
+            req_stacktrace = session.send_request(
+                'stackTrace',
+                threadId=tid,
+            )
+            req_stacktrace.wait()
+            stacktrace = req_stacktrace.resp.body
+
+            session.send_request('continue', threadId=tid)
+
+        # We should be broken in on the first line:
+        self.assertGreaterEqual(stacktrace['totalFrames'], 1)
+        self.assert_is_subset(stacktrace, {
+            'stackFrames': [{
+                'id': 1,
+                'name': '<module>',
+                'source': {
+                    'path': expected_source_path,
+                    'sourceReference': 0,
+                },
+                'line': 1,
+                'column': 1,
+            }],
+        })
+
+        # Make sure there is no stdout based output event
+        for e in received_before:
+            self.assertFalse(e.body['category'] == 'stdout')
+
+        received = list(_strip_newline_output_events(session.received))
+        self.assert_contains(received, [
+            self.new_event('continued', threadId=tid),
+            self.new_event('output', category='stdout', output='one'),
+            self.new_event('output', category='stdout', output='two'),
+            self.new_event('output', category='stdout', output='three'),
+            self.new_event('exited', exitCode=0),
+            self.new_event('terminated'),
+        ])
+
+    def run_test_stop_on_entry_disabled(self, debug_info):
+        options = {'debugOptions': ['RedirectOutput']}
+        with self.start_debugging(debug_info) as dbg:
+            session = dbg.session
+            lifecycle_handshake(
+                                session, debug_info.starttype,
+                                options=options)
+
+        received = list(_strip_newline_output_events(session.received))
+        self.assert_contains(received, [
+            self.new_event('output', category='stdout', output='one'),
+            self.new_event('output', category='stdout', output='two'),
+            self.new_event('output', category='stdout', output='three'),
+            self.new_event('exited', exitCode=0),
+            self.new_event('terminated'),
+        ])
+
+
+class StopOnEntryLaunchFileTests(StopOnEntryTests):
+    def test_stop_on_entry_enabled(self):
+        filename = TEST_FILES.resolve('stoponentry.py')
+        cwd = os.path.dirname(filename)
+        self.run_test_stop_on_entry_enabled(
+            DebugInfo(filename=filename, cwd=cwd),
+            filename)
+
+    def test_stop_on_entry_disabled(self):
+        filename = TEST_FILES.resolve('stoponentry.py')
+        cwd = os.path.dirname(filename)
+        self.run_test_stop_on_entry_disabled(
+            DebugInfo(filename=filename, cwd=cwd))
+
+
+class StopOnEntryLaunchModuleTests(StopOnEntryTests):
+    def test_stop_on_entry_enabled(self):
+        filename = TEST_FILES.resolve('stoponentry.py')
+        env = TEST_FILES.env_with_py_path()
+        cwd = TEST_FILES.root
+        self.run_test_stop_on_entry_enabled(
+            DebugInfo(
+                modulename='stoponentry',
+                cwd=cwd,
+                env=env),
+            filename)
+
+    def test_stop_on_entry_disabled(self):
+        env = TEST_FILES.env_with_py_path()
+        cwd = TEST_FILES.root
+        self.run_test_stop_on_entry_disabled(
+            DebugInfo(
+                modulename='stoponentry',
+                cwd=cwd,
+                env=env))
+
+
+class StopOnEntryLaunchPackageTests(StopOnEntryTests):
+    def test_stop_on_entry_enabled(self):
+        filename = TEST_FILES.resolve('mymod', '__init__.py')
+        env = TEST_FILES.env_with_py_path()
+        cwd = TEST_FILES.root
+        self.run_test_stop_on_entry_enabled(
+            DebugInfo(
+                modulename='mymod',
+                cwd=cwd,
+                env=env),
+            filename)
+
+    def test_stop_on_entry_disabled(self):
+        env = TEST_FILES.env_with_py_path()
+        cwd = TEST_FILES.root
+        self.run_test_stop_on_entry_disabled(
+            DebugInfo(
+                modulename='mymod',
+                cwd=cwd,
+                env=env))