Repository: microsoft/ptvsd

Issue:
Decouple debug session from launch method



Discussion:

Resolving patch:
diff --git a/pytests/conftest.py b/pytests/conftest.py
--- a/pytests/conftest.py
+++ b/pytests/conftest.py
@@ -129,7 +129,7 @@ def factory(source):
     # Setting PTVSD_SIMPLE_TESTS locally is useful to not have to run
     # all the test permutations while developing.
     _ATTACH_PARAMS = [
-        'launch',
+        ('launch',),
     ]
 
     _RUN_AS_PARAMS = [
@@ -137,9 +137,10 @@ def factory(source):
     ]
 else:
     _ATTACH_PARAMS = [
-        'launch',
-        'attach_socket',
-        # 'attach_pid',
+        ('launch',),
+        ('attach', 'socket', 'cmdline'),
+        ('attach', 'socket', 'import'),
+        ('attach', 'pid'),
     ]
 
     _RUN_AS_PARAMS = [
@@ -148,9 +149,9 @@ def factory(source):
     ]
 
 
-@pytest.fixture(params=_ATTACH_PARAMS)
+@pytest.fixture
 def debug_session(request):
-    session = DebugSession(request.param)
+    session = DebugSession()
     try:
         yield session
         try:
@@ -172,35 +173,9 @@ def _run_as(request):
     return request.param
 
 
-@pytest.fixture
-def simple_hit_paused_on_break(debug_session, pyfile, run_as):
-    '''
-    Starts debug session with a pre-defined code sample, yields with
-    a breakpoint hit and when finished, resumes the execution
-    and waits for the debug session to exit.
-
-    :note: fixture will run with all permutations of the debug_session
-    parameters as well as the run_as parameters.
-    '''
-
-    from pytests.helpers.timeline import Event
-
-    @pyfile
-    def code_to_debug():
-        a = 1
-        b = {"one": 1, "two": 2}
-        c = 3
-        print([a, b, c])
-
-    bp_line = 4
-    bp_file = code_to_debug
-    debug_session.common_setup(bp_file, run_as, breakpoints=[bp_line])
-    debug_session.start_debugging()
-    hit = debug_session.wait_for_thread_stopped()
-
-    yield hit
-
-    debug_session.send_request('continue').wait_for_response()
-    debug_session.wait_for_next(Event('continued'))
-
-    debug_session.wait_for_exit()
+@pytest.fixture(
+    name='start_method',
+    params=_ATTACH_PARAMS
+)
+def _start_method(request):
+    return request.param
diff --git a/pytests/helpers/session.py b/pytests/helpers/session.py
--- a/pytests/helpers/session.py
+++ b/pytests/helpers/session.py
@@ -27,18 +27,25 @@
 
 _Hit = namedtuple('_Hit', 'thread_stopped, stacktrace, thread_id, frame_id')
 
+START_METHOD_LAUNCH = ('launch',)
+START_METHOD_CMDLINE = ('attach', 'socket', 'cmdline')
+START_METHOD_IMPORT = ('attach', 'socket', 'import')
+START_METHOD_PID = ('attach', 'pid')
+
 
 class DebugSession(object):
     WAIT_FOR_EXIT_TIMEOUT = 5
     BACKCHANNEL_TIMEOUT = 15
 
-    def __init__(self, method='launch', ptvsd_port=None, pid=None):
-        assert method in ('launch', 'attach_pid', 'attach_socket')
-        assert ptvsd_port is None or method == 'attach_socket'
+    def __init__(self, start_method=START_METHOD_LAUNCH, ptvsd_port=None, pid=None):
+        assert start_method[0] in ('launch', 'attach')
+        assert ptvsd_port is None or (len(start_method) > 1 and start_method[1] == 'socket')
+        assert start_method != ('attach', 'socket', 'import')
 
-        print('New debug session with method %r' % method)
+        print('New debug session with method %r' % str(start_method))
 
-        self.method = method
+        self.target = ('code', 'print("OK")')
+        self.start_method = start_method
         self.ptvsd_port = ptvsd_port or 5678
         self.multiprocess = False
         self.multiprocess_port_range = None
@@ -67,6 +74,8 @@ def __init__(self, method='launch', ptvsd_port=None, pid=None):
             Event('thread', ANY.dict_with({'reason': 'exited'}))
         ])
         self.timeline.freeze()
+        self.perform_handshake = True
+        self.use_backchannel = False
 
         # Expose some common members of timeline directly - these should be the ones
         # that are the most straightforward to use, and are difficult to use incorrectly.
@@ -80,9 +89,6 @@ def __init__(self, method='launch', ptvsd_port=None, pid=None):
         self.expect_realized = self.timeline.expect_realized
         self.all_occurrences_of = self.timeline.all_occurrences_of
 
-    def add_file_to_pythonpath(self, filename):
-        self.env['PYTHONPATH'] += os.pathsep + os.path.dirname(filename)
-
     def __contains__(self, expectation):
         return expectation in self.timeline
 
@@ -122,21 +128,21 @@ def stop(self):
 
         self._wait_for_remaining_output()
 
-    def prepare_to_run(self, perform_handshake=True, filename=None, module=None, code=None, backchannel=False):
+    def prepare_to_run(self):
         """Spawns ptvsd using the configured method, telling it to execute the
         provided Python file, module, or code, and establishes a message channel
         to it.
 
-        If backchannel is True, calls self.setup_backchannel() before returning.
+        If use_backchannel is True, calls self.setup_backchannel() before returning.
 
         If perform_handshake is True, calls self.handshake() before returning.
         """
 
         argv = [sys.executable]
-        if self.method != 'attach_pid':
+        if self.start_method != ('attach', 'pid'):
             argv += [ptvsd.__main__.__file__]
 
-        if self.method == 'attach_socket':
+        if self.start_method == ('attach', 'socket', 'cmdline'):
             argv += ['--wait']
         else:
             self._listen()
@@ -149,20 +155,34 @@ def prepare_to_run(self, perform_handshake=True, filename=None, module=None, cod
         if self.multiprocess_port_range:
             argv += ['--multiprocess-port-range', '%d-%d' % self.multiprocess_port_range]
 
-        if filename:
-            assert not module and not code
-            argv += [filename]
-        elif module:
-            assert not filename and not code
-            argv += ['-m', module]
-        elif code:
-            assert not filename and not module
-            argv += ['-c', code]
+        run_as, path_or_code = self.target
+        if run_as == 'file':
+            assert os.path.isfile(path_or_code)
+            argv += [path_or_code]
+        elif run_as == 'module':
+            if os.path.isfile(path_or_code) or os.path.isdir(path_or_code):
+                self.env['PYTHONPATH'] += os.pathsep + os.path.dirname(path_or_code)
+                try:
+                    module = path_or_code[len(os.path.dirname(path_or_code)) + 1:-3]
+                except Exception:
+                    module = 'code_to_debug'
+                argv += ['-m', module]
+            else:
+                argv += ['-m', path_or_code]
+        elif run_as == 'code':
+            if os.path.isfile(path_or_code):
+                with open(path_or_code, 'r') as f:
+                    code = f.read()
+                argv += ['-c', code]
+            else:
+                argv += ['-c', path_or_code]
+        else:
+            pytest.fail()
 
         if self.program_args:
             argv += list(self.program_args)
 
-        if backchannel:
+        if self.use_backchannel:
             self.setup_backchannel()
         if self.backchannel_port:
             self.env['PTVSD_BACKCHANNEL_PORT'] = str(self.backchannel_port)
@@ -179,7 +199,7 @@ def prepare_to_run(self, perform_handshake=True, filename=None, module=None, cod
         self._capture_output(self.process.stdout, 'OUT')
         self._capture_output(self.process.stderr, 'ERR')
 
-        if self.method == 'attach_socket':
+        if self.start_method == ('attach', 'socket', 'cmdline'):
             self.connect()
         self.connected.wait()
         assert self.ptvsd_port
@@ -193,7 +213,7 @@ def prepare_to_run(self, perform_handshake=True, filename=None, module=None, cod
             'data': {'version': ptvsd.__version__}
         })
 
-        if perform_handshake:
+        if self.perform_handshake:
             return self.handshake()
 
     def __enter__(self):
@@ -351,7 +371,7 @@ def handshake(self):
         self.send_request('initialize', {'adapterID': 'test'}).wait_for_response()
         self.wait_for_next(Event('initialized', {}))
 
-        request = 'launch' if self.method == 'launch' else 'attach'
+        request = 'launch' if self.start_method == ('launch',) else 'attach'
         self.send_request(request, {'debugOptions': self.debug_options}).wait_for_response()
 
         # Issue 'threads' so that we get the 'thread' event for the main thread now,
@@ -378,7 +398,7 @@ def start_debugging(self, freeze=True):
         self.expect_new(Event('process', {
             'name': ANY.str,
             'isLocalProcess': True,
-            'startMethod': 'launch' if self.method == 'launch' else 'attach',
+            'startMethod': 'launch' if self.start_method == ('launch',) else 'attach',
             'systemProcessId': self.pid if self.pid is not None else ANY.int,
         }))
 
@@ -461,27 +481,32 @@ def _wait_for_remaining_output(self):
         for thread in self._output_capture_threads:
             thread.join()
 
-    def common_setup(self, path, run_as, breakpoints=()):
+    def initialize(
+        self,
+        **kwargs
+    ):
         self.ignore_unobserved += [
             Event('thread', ANY.dict_with({'reason': 'started'})),
             Event('module')
-        ]
-        if run_as == 'file':
-            self.prepare_to_run(filename=path)
-        elif run_as == 'module':
-            self.add_file_to_pythonpath(path)
-            self.prepare_to_run(module='code_to_debug')
-        elif run_as == 'code':
-            with open(path, 'r') as f:
-                code = f.read()
-            self.prepare_to_run(code=code)
-        else:
-            pytest.fail()
+        ] + kwargs.pop('ignore_events', [])
+
+        self.env.update(kwargs.pop('env', {}))
+        self.debug_options += kwargs.pop('debug_options', [])
+        self.program_args += kwargs.pop('program_args', [])
+
+        for k, v in kwargs.items():
+            setattr(self, k, v)
+
+        assert self.start_method[0] in ('launch', 'attach')
+        assert len(self.target) == 2
+        assert self.target[0] in ('file', 'module', 'code')
+
+        self.prepare_to_run()
 
-        if breakpoints:
-            self.send_request('setBreakpoints', arguments={
+    def set_breakpoints(self, path, lines=()):
+        self.send_request('setBreakpoints', arguments={
                 'source': {'path': path},
-                'breakpoints': [{'line': bp_line} for bp_line in breakpoints],
+                'breakpoints': [{'line': bp_line} for bp_line in lines],
             }).wait_for_response()
 
     def wait_for_thread_stopped(self, reason='breakpoint'):


Test patch:
diff --git a/pytests/func/test_breakpoints.py b/pytests/func/test_breakpoints.py
--- a/pytests/func/test_breakpoints.py
+++ b/pytests/func/test_breakpoints.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See LICENSE in the project root
 # for license information.
@@ -5,20 +6,24 @@
 from __future__ import print_function, with_statement, absolute_import
 
 import os.path
+import pytest
 from pytests.helpers.timeline import Event
-from ..helpers.pathutils import get_test_root, compare_path
+from pytests.helpers.pathutils import get_test_root, compare_path
+from pytests.helpers.session import START_METHOD_LAUNCH, START_METHOD_CMDLINE
 
 BP_TEST_ROOT = get_test_root('bp')
 
 
-def test_path_with_ampersand(debug_session):
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH, START_METHOD_CMDLINE])
+def test_path_with_ampersand(debug_session, start_method):
     bp_line = 2
     testfile = os.path.join(BP_TEST_ROOT, 'a&b', 'test.py')
-    debug_session.common_setup(testfile, 'file', [bp_line])
+    debug_session.initialize(target=('file', testfile), start_method=start_method)
+    debug_session.set_breakpoints(testfile, [bp_line])
     debug_session.start_debugging()
     hit = debug_session.wait_for_thread_stopped()
     frames = hit.stacktrace.body['stackFrames']
-    assert compare_path(frames[0]['source']['path'], testfile)
+    assert compare_path(frames[0]['source']['path'], testfile, show=False)
 
     debug_session.send_request('continue').wait_for_response()
     debug_session.wait_for_next(Event('continued'))
diff --git a/pytests/func/test_completions.py b/pytests/func/test_completions.py
--- a/pytests/func/test_completions.py
+++ b/pytests/func/test_completions.py
@@ -5,11 +5,32 @@
 from __future__ import print_function, with_statement, absolute_import
 
 import pytest
-from ..helpers.pattern import ANY
-from ..helpers.timeline import Event
+from pytests.helpers.pattern import ANY
+from pytests.helpers.timeline import Event
+from pytests.helpers.session import START_METHOD_LAUNCH, START_METHOD_CMDLINE
 
 
-def run_test_completion(debug_session, pyfile, bp_line, expected):
+expected_at_line = {
+    6: [
+        {'label': 'SomeClass', 'type': 'class'},
+        {'label': 'someFunction', 'type': 'function'},
+        {'label': 'someVariable', 'type': 'field'},
+    ],
+    9: [
+        {'label': 'SomeClass', 'type': 'class'},
+        {'label': 'someFunction', 'type': 'function'},
+        {'label': 'someVar', 'type': 'field'},
+        {'label': 'someVariable', 'type': 'field'},
+    ],
+    11: [
+        {'label': 'SomeClass', 'type': 'class'},
+        {'label': 'someFunction', 'type': 'function'},
+    ],
+}
+
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH, START_METHOD_CMDLINE])
+@pytest.mark.parametrize('bp_line', expected_at_line.keys())
+def test_completions_scope(debug_session, pyfile, bp_line, run_as, start_method):
     @pyfile
     def code_to_debug():
         class SomeClass():
@@ -24,19 +45,13 @@ def someFunction(someVar):
         someFunction('value')
         print('done')
 
-    debug_session.multiprocess = False
+    expected = expected_at_line[bp_line]
     debug_session.ignore_unobserved += [
-        Event('thread', ANY.dict_with({'reason': 'started'})),
-        Event('module'),
         Event('stopped'),
         Event('continued')
     ]
-    debug_session.prepare_to_run(filename=code_to_debug)
-    debug_session.send_request('setBreakpoints', arguments={
-        'source': {'path': code_to_debug},
-        'breakpoints': [{'line': bp_line}, ],
-    }).wait_for_response()
-
+    debug_session.initialize(target=(run_as, code_to_debug), start_method=start_method)
+    debug_session.set_breakpoints(code_to_debug, [bp_line])
     debug_session.start_debugging()
 
     thread_stopped = debug_session.wait_for_next(Event('stopped'), ANY.dict_with({'reason': 'breakpoint'}))
@@ -59,6 +74,7 @@ def someFunction(someVar):
     targets = resp_completions.body['targets']
 
     debug_session.send_request('continue').wait_for_response()
+    debug_session.wait_for_next(Event('continued'))
 
     targets.sort(key=lambda t: t['label'])
     expected.sort(key=lambda t: t['label'])
@@ -67,30 +83,21 @@ def someFunction(someVar):
     debug_session.wait_for_exit()
 
 
-expected_at_line = {
-    6: [
-        {'label': 'SomeClass', 'type': 'class'},
-        {'label': 'someFunction', 'type': 'function'},
-        {'label': 'someVariable', 'type': 'field'},
-    ],
-    9: [
-        {'label': 'SomeClass', 'type': 'class'},
-        {'label': 'someFunction', 'type': 'function'},
-        {'label': 'someVar', 'type': 'field'},
-        {'label': 'someVariable', 'type': 'field'},
-    ],
-    11: [
-        {'label': 'SomeClass', 'type': 'class'},
-        {'label': 'someFunction', 'type': 'function'},
-    ],
-}
-
-@pytest.mark.parametrize('bp_line', expected_at_line.keys())
-def test_completions_scope(debug_session, pyfile, bp_line):
-    run_test_completion(debug_session, pyfile, bp_line, expected_at_line[bp_line])
-
-def test_completions(debug_session, simple_hit_paused_on_break):
-    hit = simple_hit_paused_on_break
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH, START_METHOD_CMDLINE])
+def test_completions(debug_session, pyfile, start_method, run_as):
+    @pyfile
+    def code_to_debug():
+        a = 1
+        b = {"one": 1, "two": 2}
+        c = 3
+        print([a, b, c])
+
+    bp_line = 4
+    bp_file = code_to_debug
+    debug_session.initialize(target=(run_as, bp_file), start_method=start_method)
+    debug_session.set_breakpoints(bp_file, [bp_line])
+    debug_session.start_debugging()
+    hit = debug_session.wait_for_thread_stopped()
 
     response = debug_session.send_request('completions', arguments={
         'frameId': hit.frame_id,
@@ -106,3 +113,8 @@ def test_completions(debug_session, simple_hit_paused_on_break):
     }).wait_for_response()
 
     assert not response.body['targets']
+
+    debug_session.send_request('continue').wait_for_response()
+    debug_session.wait_for_next(Event('continued'))
+
+    debug_session.wait_for_exit()
diff --git a/pytests/func/test_django.py b/pytests/func/test_django.py
--- a/pytests/func/test_django.py
+++ b/pytests/func/test_django.py
@@ -8,11 +8,12 @@
 import pytest
 import sys
 
-from ..helpers.pattern import ANY
-from ..helpers.session import DebugSession
-from ..helpers.timeline import Event
-from ..helpers.pathutils import get_test_root, compare_path
-from ..helpers.webhelper import get_url_from_str, get_web_content, wait_for_connection
+from pytests.helpers.pattern import ANY
+from pytests.helpers.session import DebugSession
+from pytests.helpers.timeline import Event
+from pytests.helpers.pathutils import get_test_root, compare_path
+from pytests.helpers.webhelper import get_url_from_str, get_web_content, wait_for_connection
+from pytests.helpers.session import START_METHOD_LAUNCH, START_METHOD_CMDLINE
 
 DJANGO1_ROOT = get_test_root('django1')
 DJANGO1_MANAGE = os.path.join(DJANGO1_ROOT, 'app.py')
@@ -20,35 +21,26 @@
 DJANGO_LINK = 'http://127.0.0.1:8000/'
 DJANGO_PORT = 8000
 
-def _django_no_multiproc_common(debug_session):
-    debug_session.multiprocess = False
-    debug_session.program_args += ['runserver', '--noreload', '--nothreading']
-
-    debug_session.ignore_unobserved += [
-        Event('thread', ANY.dict_with({'reason': 'started'})),
-        Event('module')
-    ]
-
-    debug_session.debug_options += ['Django']
-    debug_session.cwd = DJANGO1_ROOT
-    debug_session.expected_returncode = ANY  # No clean way to kill Django server
 
 @pytest.mark.parametrize('bp_file, bp_line, bp_name', [
   (DJANGO1_MANAGE, 40, 'home'),
   (DJANGO1_TEMPLATE, 8, 'Django Template'),
 ])
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH, START_METHOD_CMDLINE])
 @pytest.mark.skipif(sys.version_info < (3, 0), reason='Bug #923')
 @pytest.mark.timeout(60)
-def test_django_breakpoint_no_multiproc(debug_session, bp_file, bp_line, bp_name):
-    _django_no_multiproc_common(debug_session)
-    debug_session.prepare_to_run(filename=DJANGO1_MANAGE)
+def test_django_breakpoint_no_multiproc(debug_session, bp_file, bp_line, bp_name, start_method):
+    debug_session.initialize(
+        start_method=start_method,
+        target=('file', DJANGO1_MANAGE),
+        program_args=['runserver', '--noreload', '--nothreading'],
+        debug_options=['Django'],
+        cwd=DJANGO1_ROOT,
+        expected_returncode=ANY.int,  # No clean way to kill Flask server
+    )
 
     bp_var_content = 'Django-Django-Test'
-    debug_session.send_request('setBreakpoints', arguments={
-        'source': {'path': bp_file},
-        'breakpoints': [{'line': bp_line}, ],
-    }).wait_for_response()
-
+    debug_session.set_breakpoints(bp_file, [bp_line])
     debug_session.start_debugging()
 
     # wait for Django server to start
@@ -109,11 +101,18 @@ def test_django_breakpoint_no_multiproc(debug_session, bp_file, bp_line, bp_name
   ('handled', 50),
   ('unhandled', 64),
 ])
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH, START_METHOD_CMDLINE])
 @pytest.mark.skipif(sys.version_info < (3, 0), reason='Bug #923')
 @pytest.mark.timeout(60)
-def test_django_exception_no_multiproc(debug_session, ex_type, ex_line):
-    _django_no_multiproc_common(debug_session)
-    debug_session.prepare_to_run(filename=DJANGO1_MANAGE)
+def test_django_exception_no_multiproc(debug_session, ex_type, ex_line, start_method):
+    debug_session.initialize(
+        start_method=start_method,
+        target=('file', DJANGO1_MANAGE),
+        program_args=['runserver', '--noreload', '--nothreading'],
+        debug_options=['Django'],
+        cwd=DJANGO1_ROOT,
+        expected_returncode=ANY.int,  # No clean way to kill Flask server
+    )
 
     debug_session.send_request('setExceptionBreakpoints', arguments={
         'filters': ['raised', 'uncaught'],
@@ -185,7 +184,7 @@ def _wait_for_child_process(debug_session):
 
     child_port = child_subprocess.body['port']
 
-    child_session = DebugSession(method='attach_socket', ptvsd_port=child_port)
+    child_session = DebugSession(start_method=START_METHOD_CMDLINE, ptvsd_port=child_port)
     child_session.ignore_unobserved = debug_session.ignore_unobserved
     child_session.debug_options = debug_session.debug_options
     child_session.connect()
@@ -194,29 +193,22 @@ def _wait_for_child_process(debug_session):
 
 @pytest.mark.skip()
 @pytest.mark.timeout(120)
-def test_django_breakpoint_multiproc(debug_session):
-    debug_session.multiprocess = True
-    debug_session.program_args += ['runserver']
-
-    debug_session.ignore_unobserved += [
-        Event('thread', ANY.dict_with({'reason': 'started'})),
-        Event('module'),
-        Event('stopped'),
-        Event('continued')
-    ]
-
-    debug_session.debug_options += ['Django']
-    debug_session.cwd = DJANGO1_ROOT
-    debug_session.expected_returncode = ANY  # No clean way to kill Django server
-    debug_session.prepare_to_run(filename=DJANGO1_MANAGE)
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH])
+def test_django_breakpoint_multiproc(debug_session, start_method):
+    debug_session.initialize(
+        start_method=start_method,
+        target=('file', DJANGO1_MANAGE),
+        multiprocess=True,
+        program_args=['runserver', ],
+        debug_options=['Django'],
+        cwd=DJANGO1_ROOT,
+        ignore_events=[Event('stopped'), Event('continued')],
+        expected_returncode=ANY.int,  # No clean way to kill Flask server
+    )
 
     bp_line = 40
     bp_var_content = 'Django-Django-Test'
-    debug_session.send_request('setBreakpoints', arguments={
-        'source': {'path': DJANGO1_MANAGE},
-        'breakpoints': [{'line': bp_line}, ],
-    }).wait_for_response()
-
+    debug_session.set_breakpoints(DJANGO1_MANAGE, [bp_line])
     debug_session.start_debugging()
 
     child_session = _wait_for_child_process(debug_session)
diff --git a/pytests/func/test_evaluate.py b/pytests/func/test_evaluate.py
--- a/pytests/func/test_evaluate.py
+++ b/pytests/func/test_evaluate.py
@@ -4,13 +4,29 @@
 
 from __future__ import print_function, with_statement, absolute_import
 
+import pytest
 from pytests.helpers import print
 from pytests.helpers.pattern import ANY
 from pytests.helpers.timeline import Event
+from pytests.helpers.session import START_METHOD_LAUNCH, START_METHOD_CMDLINE
 
 
-def test_variables_and_evaluate(debug_session, simple_hit_paused_on_break):
-    hit = simple_hit_paused_on_break
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH, START_METHOD_CMDLINE])
+def test_variables_and_evaluate(debug_session, pyfile, run_as, start_method):
+
+    @pyfile
+    def code_to_debug():
+        a = 1
+        b = {"one": 1, "two": 2}
+        c = 3
+        print([a, b, c])
+
+    bp_line = 4
+    bp_file = code_to_debug
+    debug_session.initialize(target=(run_as, bp_file), start_method=start_method)
+    debug_session.set_breakpoints(bp_file, [bp_line])
+    debug_session.start_debugging()
+    hit = debug_session.wait_for_thread_stopped()
 
     resp_scopes = debug_session.send_request('scopes', arguments={
         'frameId': hit.frame_id,
@@ -79,9 +95,14 @@ def test_variables_and_evaluate(debug_session, simple_hit_paused_on_break):
         'result': '2'
     })
 
+    debug_session.send_request('continue').wait_for_response()
+    debug_session.wait_for_next(Event('continued'))
+
+    debug_session.wait_for_exit()
 
-def test_set_variable(debug_session, pyfile, run_as):
 
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH, START_METHOD_CMDLINE])
+def test_set_variable(debug_session, pyfile, run_as, start_method):
     @pyfile
     def code_to_debug():
         a = 1
@@ -89,7 +110,8 @@ def code_to_debug():
 
     bp_line = 2
     bp_file = code_to_debug
-    debug_session.common_setup(bp_file, run_as, breakpoints=[bp_line])
+    debug_session.initialize(target=(run_as, bp_file), start_method=start_method)
+    debug_session.set_breakpoints(bp_file, [bp_line])
     debug_session.start_debugging()
     hit = debug_session.wait_for_thread_stopped()
 
@@ -132,7 +154,8 @@ def code_to_debug():
     debug_session.wait_for_exit()
 
 
-def test_variable_sort(debug_session, pyfile, run_as):
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH, START_METHOD_CMDLINE])
+def test_variable_sort(debug_session, pyfile, run_as, start_method):
 
     @pyfile
     def code_to_debug():
@@ -153,7 +176,8 @@ def code_to_debug():
 
     bp_line = 13
     bp_file = code_to_debug
-    debug_session.common_setup(bp_file, run_as, breakpoints=[bp_line])
+    debug_session.initialize(target=(run_as, bp_file), start_method=start_method)
+    debug_session.set_breakpoints(bp_file, [bp_line])
     debug_session.start_debugging()
     hit = debug_session.wait_for_thread_stopped()
 
diff --git a/pytests/func/test_flask.py b/pytests/func/test_flask.py
--- a/pytests/func/test_flask.py
+++ b/pytests/func/test_flask.py
@@ -9,11 +9,12 @@
 import pytest
 import sys
 
-from ..helpers.pattern import ANY
-from ..helpers.session import DebugSession
-from ..helpers.timeline import Event
-from ..helpers.webhelper import get_web_content, wait_for_connection
-from ..helpers.pathutils import get_test_root, compare_path
+from pytests.helpers.pattern import ANY
+from pytests.helpers.session import DebugSession
+from pytests.helpers.timeline import Event
+from pytests.helpers.webhelper import get_web_content, wait_for_connection
+from pytests.helpers.pathutils import get_test_root, compare_path
+from pytests.helpers.session import START_METHOD_LAUNCH, START_METHOD_CMDLINE
 
 
 FLASK1_ROOT = get_test_root('flask1')
@@ -22,9 +23,7 @@
 FLASK_LINK = 'http://127.0.0.1:5000/'
 FLASK_PORT = 5000
 
-def _flask_no_multiproc_common(debug_session):
-    debug_session.multiprocess = False
-    debug_session.program_args += ['run', '--no-debugger', '--no-reload', '--with-threads']
+def _flask_no_multiproc_common(debug_session, start_method):
     env = {
         'FLASK_APP': 'app.py',
         'FLASK_ENV': 'development',
@@ -37,35 +36,29 @@ def _flask_no_multiproc_common(debug_session):
             'LANG': locale,
         })
 
-    # we may get multiple stopped(pause), or continued events.
-    # we only care about stopped(breakpoint/exception) events
-    debug_session.ignore_unobserved += [
-        Event('thread', ANY.dict_with({'reason': 'started'})),
-        Event('module'),
-        Event('stopped'),
-        Event('continued')
-    ]
+    debug_session.initialize(
+        start_method=start_method,
+        target=('module', 'flask'),
+        program_args=['run', '--no-debugger', '--no-reload', '--with-threads'],
+        ignore_events=[Event('stopped'), Event('continued')],
+        debug_options=['Jinja'],
+        cwd=FLASK1_ROOT,
+        env=env,
+        expected_returncode=ANY.int,  # No clean way to kill Flask server
+    )
 
-    debug_session.debug_options += ['Jinja']
-    debug_session.cwd = FLASK1_ROOT
-    debug_session.env.update(env)
-    debug_session.expected_returncode = ANY.int  # No clean way to kill Flask server
 
 @pytest.mark.parametrize('bp_file, bp_line, bp_name', [
   (FLASK1_APP, 11, 'home'),
   (FLASK1_TEMPLATE, 8, 'template'),
 ])
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH, START_METHOD_CMDLINE])
 @pytest.mark.timeout(60)
-def test_flask_breakpoint_no_multiproc(debug_session, bp_file, bp_line, bp_name):
-    _flask_no_multiproc_common(debug_session)
-    debug_session.prepare_to_run(module='flask')
+def test_flask_breakpoint_no_multiproc(debug_session, bp_file, bp_line, bp_name, start_method):
+    _flask_no_multiproc_common(debug_session, start_method)
 
     bp_var_content = 'Flask-Jinja-Test'
-    debug_session.send_request('setBreakpoints', arguments={
-        'source': {'path': bp_file},
-        'breakpoints': [{'line': bp_line}, ],
-    }).wait_for_response()
-
+    debug_session.set_breakpoints(bp_file, [bp_line])
     debug_session.start_debugging()
 
     # wait for Flask web server to start
@@ -130,10 +123,10 @@ def test_flask_breakpoint_no_multiproc(debug_session, bp_file, bp_line, bp_name)
   ('handled', 21),
   ('unhandled', 33),
 ])
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH, START_METHOD_CMDLINE])
 @pytest.mark.timeout(60)
-def test_flask_exception_no_multiproc(debug_session, ex_type, ex_line):
-    _flask_no_multiproc_common(debug_session)
-    debug_session.prepare_to_run(module='flask')
+def test_flask_exception_no_multiproc(debug_session, ex_type, ex_line, start_method):
+    _flask_no_multiproc_common(debug_session, start_method)
 
     debug_session.send_request('setExceptionBreakpoints', arguments={
         'filters': ['raised', 'uncaught'],
@@ -206,7 +199,7 @@ def _wait_for_child_process(debug_session):
 
     child_port = child_subprocess.body['port']
 
-    child_session = DebugSession(method='attach_socket', ptvsd_port=child_port)
+    child_session = DebugSession(start_method=START_METHOD_CMDLINE, ptvsd_port=child_port)
     child_session.ignore_unobserved = debug_session.ignore_unobserved
     child_session.debug_options = debug_session.debug_options
     child_session.connect()
@@ -215,10 +208,9 @@ def _wait_for_child_process(debug_session):
 
 
 @pytest.mark.timeout(120)
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH])
 @pytest.mark.skipif((sys.version_info < (3, 0)) and (platform.system() != 'Windows'), reason='Bug #935')
-def test_flask_breakpoint_multiproc(debug_session):
-    debug_session.multiprocess = True
-    debug_session.program_args += ['run', ]
+def test_flask_breakpoint_multiproc(debug_session, start_method):
     env = {
         'FLASK_APP': 'app',
         'FLASK_ENV': 'development',
@@ -231,26 +223,21 @@ def test_flask_breakpoint_multiproc(debug_session):
             'LANG': locale,
         })
 
-    debug_session.ignore_unobserved += [
-        Event('thread', ANY.dict_with({'reason': 'started'})),
-        Event('module'),
-        Event('stopped'),
-        Event('continued')
-    ]
-
-    debug_session.debug_options += ['Jinja']
-    debug_session.cwd = FLASK1_ROOT
-    debug_session.env.update(env)
-    debug_session.expected_returncode = ANY.int  # No clean way to kill Flask server
-    debug_session.prepare_to_run(module='flask')
+    debug_session.initialize(
+        start_method=start_method,
+        target=('module', 'flask'),
+        multiprocess=True,
+        program_args=['run', ],
+        ignore_events=[Event('stopped'), Event('continued')],
+        debug_options=['Jinja'],
+        cwd=FLASK1_ROOT,
+        env=env,
+        expected_returncode=ANY.int,  # No clean way to kill Flask server
+    )
 
     bp_line = 11
     bp_var_content = 'Flask-Jinja-Test'
-    debug_session.send_request('setBreakpoints', arguments={
-        'source': {'path': FLASK1_APP},
-        'breakpoints': [{'line': bp_line}, ],
-    }).wait_for_response()
-
+    debug_session.set_breakpoints(FLASK1_APP, [bp_line])
     debug_session.start_debugging()
     child_session = _wait_for_child_process(debug_session)
 
diff --git a/pytests/func/test_multiproc.py b/pytests/func/test_multiproc.py
--- a/pytests/func/test_multiproc.py
+++ b/pytests/func/test_multiproc.py
@@ -11,12 +11,14 @@
 from pytests.helpers.pattern import ANY
 from pytests.helpers.session import DebugSession
 from pytests.helpers.timeline import Event, Request, Response
+from pytests.helpers.session import START_METHOD_LAUNCH, START_METHOD_CMDLINE
 
 
 @pytest.mark.timeout(60)
 @pytest.mark.skipif(platform.system() != 'Windows',
                     reason='Debugging multiprocessing module only works on Windows')
-def test_multiprocessing(debug_session, pyfile):
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH, START_METHOD_CMDLINE])
+def test_multiprocessing(debug_session, pyfile, run_as, start_method):
     @pyfile
     def code_to_debug():
         import multiprocessing
@@ -64,14 +66,7 @@ def child(q):
             q.close()
             backchannel.write_json('done')
 
-    debug_session.ignore_unobserved += [
-        # The queue module can spawn helper background threads, depending on Python version
-        # and platform. Since this is an implementation detail, we don't care about those.
-        Event('thread', ANY.dict_with({'reason': 'started'}))
-    ]
-
-    debug_session.multiprocess = True
-    debug_session.prepare_to_run(filename=code_to_debug, backchannel=True)
+    debug_session.initialize(multiprocess=True, target=(run_as, code_to_debug), start_method=start_method, use_backchannel=True)
     debug_session.start_debugging()
 
     root_start_request, = debug_session.all_occurrences_of(Request('launch') | Request('attach'))
@@ -95,7 +90,7 @@ def child(q):
     })
     child_port = child_subprocess.body['port']
 
-    child_session = DebugSession(method='attach_socket', ptvsd_port=child_port)
+    child_session = DebugSession(start_method=START_METHOD_CMDLINE, ptvsd_port=child_port)
     child_session.ignore_unobserved = debug_session.ignore_unobserved
     child_session.connect()
     child_session.handshake()
@@ -117,7 +112,7 @@ def child(q):
     })
     child_child_port = child_child_subprocess.body['port']
 
-    child_child_session = DebugSession(method='attach_socket', ptvsd_port=child_child_port)
+    child_child_session = DebugSession(start_method=START_METHOD_CMDLINE, ptvsd_port=child_child_port)
     child_child_session.ignore_unobserved = debug_session.ignore_unobserved
     child_child_session.connect()
     child_child_session.handshake()
@@ -136,12 +131,14 @@ def child(q):
         child_session.wait_for_disconnect()
 
     assert debug_session.read_json() == 'done'
+    debug_session.wait_for_exit()
 
 
 @pytest.mark.timeout(60)
 @pytest.mark.skipif(sys.version_info < (3, 0) and (platform.system() != 'Windows'),
                     reason='Bug #935')
-def test_subprocess(debug_session, pyfile):
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH, START_METHOD_CMDLINE])
+def test_subprocess(debug_session, pyfile, start_method, run_as):
     @pyfile
     def child():
         import sys
@@ -158,9 +155,8 @@ def parent():
         process = subprocess.Popen(argv, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
         process.wait()
 
-    debug_session.multiprocess = True
     debug_session.program_args += [child]
-    debug_session.prepare_to_run(filename=parent, backchannel=True)
+    debug_session.initialize(multiprocess=True, target=(run_as, parent), start_method=start_method, use_backchannel=True)
     debug_session.start_debugging()
 
     root_start_request, = debug_session.all_occurrences_of(Request('launch') | Request('attach'))
@@ -184,7 +180,7 @@ def parent():
     child_port = child_subprocess.body['port']
     debug_session.proceed()
 
-    child_session = DebugSession(method='attach_socket', ptvsd_port=child_port, pid=child_pid)
+    child_session = DebugSession(start_method=START_METHOD_CMDLINE, ptvsd_port=child_port, pid=child_pid)
     child_session.ignore_unobserved = debug_session.ignore_unobserved
     child_session.connect()
     child_session.handshake()
@@ -200,7 +196,8 @@ def parent():
 @pytest.mark.timeout(60)
 @pytest.mark.skipif(sys.version_info < (3, 0) and (platform.system() != 'Windows'),
                     reason='Bug #935')
-def test_autokill(debug_session, pyfile):
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH, START_METHOD_CMDLINE])
+def test_autokill(debug_session, pyfile, start_method, run_as):
     @pyfile
     def child():
         while True:
@@ -217,9 +214,8 @@ def parent():
         subprocess.Popen(argv, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
         backchannel.read_json()
 
-    debug_session.multiprocess = True
     debug_session.program_args += [child]
-    debug_session.prepare_to_run(filename=parent, backchannel=True)
+    debug_session.initialize(multiprocess=True, target=(run_as, parent), start_method=start_method, use_backchannel=True)
     debug_session.start_debugging()
 
     child_subprocess = debug_session.wait_for_next(Event('ptvsd_subprocess'))
@@ -228,13 +224,13 @@ def parent():
 
     debug_session.proceed()
 
-    child_session = DebugSession(method='attach_socket', ptvsd_port=child_port, pid=child_pid)
+    child_session = DebugSession(start_method=START_METHOD_CMDLINE, ptvsd_port=child_port, pid=child_pid)
     child_session.expected_returncode = ANY
     child_session.connect()
     child_session.handshake()
     child_session.start_debugging()
 
-    if debug_session.method == 'launch':
+    if debug_session.start_method == START_METHOD_LAUNCH:
         # In launch scenario, terminate the parent process by disconnecting from it.
         debug_session.expected_returncode = ANY
         disconnect = debug_session.send_request('disconnect', {})
diff --git a/pytests/func/test_run.py b/pytests/func/test_run.py
--- a/pytests/func/test_run.py
+++ b/pytests/func/test_run.py
@@ -12,10 +12,12 @@
 from pytests.helpers import print
 from pytests.helpers.pattern import ANY
 from pytests.helpers.timeline import Event
+from pytests.helpers.session import START_METHOD_LAUNCH, START_METHOD_CMDLINE
 
 
 @pytest.mark.parametrize('run_as', ['file', 'module', 'code'])
-def test_run(debug_session, pyfile, run_as):
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH, START_METHOD_CMDLINE])
+def test_run(debug_session, pyfile, run_as, start_method):
     @pyfile
     def code_to_debug():
         import os
@@ -27,18 +29,7 @@ def code_to_debug():
         backchannel.write_json(os.path.abspath(sys.modules['ptvsd'].__file__))
         print('end')
 
-    if run_as == 'file':
-        debug_session.prepare_to_run(filename=code_to_debug, backchannel=True)
-    elif run_as == 'module':
-        debug_session.add_file_to_pythonpath(code_to_debug)
-        debug_session.prepare_to_run(module='code_to_debug', backchannel=True)
-    elif run_as == 'code':
-        with open(code_to_debug, 'r') as f:
-            code = f.read()
-        debug_session.prepare_to_run(code=code, backchannel=True)
-    else:
-        pytest.fail()
-
+    debug_session.initialize(target=(run_as, code_to_debug), start_method=start_method, use_backchannel=True)
     debug_session.start_debugging()
     assert debug_session.timeline.is_frozen
 
diff --git a/pytests/func/test_start_stop.py b/pytests/func/test_start_stop.py
--- a/pytests/func/test_start_stop.py
+++ b/pytests/func/test_start_stop.py
@@ -11,28 +11,11 @@
 from pytests.helpers import print
 from pytests.helpers.pattern import ANY
 from pytests.helpers.timeline import Event
+from pytests.helpers.session import START_METHOD_LAUNCH, START_METHOD_CMDLINE
 
 
-def _common_setup(debug_session, path, run_as):
-    debug_session.ignore_unobserved += [
-        Event('thread', ANY.dict_with({'reason': 'started'})),
-        Event('module')
-    ]
-    if run_as == 'file':
-        debug_session.prepare_to_run(filename=path)
-    elif run_as == 'module':
-        debug_session.add_file_to_pythonpath(path)
-        debug_session.prepare_to_run(module='code_to_debug')
-    elif run_as == 'code':
-        with open(path, 'r') as f:
-            code = f.read()
-        debug_session.prepare_to_run(code=code)
-    else:
-        pytest.fail()
-
-
-@pytest.mark.parametrize('run_as', ['file', 'module'])
-def test_break_on_entry(debug_session, pyfile, run_as):
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH])
+def test_break_on_entry(debug_session, pyfile, run_as, start_method):
     @pyfile
     def code_to_debug():
         print('one')
@@ -40,24 +23,23 @@ def code_to_debug():
         print('three')
 
     debug_session.debug_options += ['StopOnEntry']
-    _common_setup(debug_session, code_to_debug, run_as)
+    debug_session.initialize(target=(run_as, code_to_debug), start_method=start_method)
     debug_session.start_debugging()
 
-    if debug_session.method == 'launch':
-        thread_stopped = debug_session.wait_for_next(Event('stopped'), ANY.dict_with({'reason': 'step'}))
-        assert thread_stopped.body['threadId'] is not None
+    thread_stopped = debug_session.wait_for_next(Event('stopped'), ANY.dict_with({'reason': 'step'}))
+    assert thread_stopped.body['threadId'] is not None
 
-        tid = thread_stopped.body['threadId']
+    tid = thread_stopped.body['threadId']
 
-        resp_stacktrace = debug_session.send_request('stackTrace', arguments={
-            'threadId': tid,
-        }).wait_for_response()
-        assert resp_stacktrace.body['totalFrames'] > 0
-        frames = resp_stacktrace.body['stackFrames']
-        assert frames[0]['line'] == 1
+    resp_stacktrace = debug_session.send_request('stackTrace', arguments={
+        'threadId': tid,
+    }).wait_for_response()
+    assert resp_stacktrace.body['totalFrames'] > 0
+    frames = resp_stacktrace.body['stackFrames']
+    assert frames[0]['line'] == 1
 
-        debug_session.send_request('continue').wait_for_response()
-        debug_session.wait_for_next(Event('continued'))
+    debug_session.send_request('continue').wait_for_response()
+    debug_session.wait_for_next(Event('continued'))
 
     debug_session.wait_for_next(Event('exited'))
     output = [e.body['output'] for e in debug_session.all_occurrences_of(Event('output'))
@@ -68,10 +50,10 @@ def code_to_debug():
     debug_session.wait_for_exit()
 
 
-@pytest.mark.parametrize('run_as', ['file', 'module'])
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH, START_METHOD_CMDLINE])
 @pytest.mark.skipif(sys.version_info < (3, 0) and platform.system() == 'Windows',
                     reason="On windows py2.7 unable to send key strokes to test.")
-def test_wait_on_normal_exit_enabled(debug_session, pyfile, run_as):
+def test_wait_on_normal_exit_enabled(debug_session, pyfile, run_as, start_method):
     @pyfile
     def code_to_debug():
         print('one')
@@ -82,12 +64,8 @@ def code_to_debug():
 
     bp_line = 3
     bp_file = code_to_debug
-    _common_setup(debug_session, bp_file, run_as)
-
-    debug_session.send_request('setBreakpoints', arguments={
-        'source': {'path': bp_file},
-        'breakpoints': [{'line': bp_line}, ],
-    }).wait_for_response()
+    debug_session.initialize(target=(run_as, bp_file), start_method=start_method)
+    debug_session.set_breakpoints(bp_file, [bp_line])
     debug_session.start_debugging()
 
     debug_session.wait_for_next(Event('stopped'), ANY.dict_with({'reason': 'breakpoint'}))
@@ -114,10 +92,10 @@ def _decode(text):
                if _decode(l).startswith('Press'))
 
 
-@pytest.mark.parametrize('run_as', ['file', 'module'])
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH, START_METHOD_CMDLINE])
 @pytest.mark.skipif(sys.version_info < (3, 0) and platform.system() == 'Windows',
                     reason="On windows py2.7 unable to send key strokes to test.")
-def test_wait_on_abnormal_exit_enabled(debug_session, pyfile, run_as):
+def test_wait_on_abnormal_exit_enabled(debug_session, pyfile, run_as, start_method):
     @pyfile
     def code_to_debug():
         import sys
@@ -130,12 +108,8 @@ def code_to_debug():
 
     bp_line = 5
     bp_file = code_to_debug
-    _common_setup(debug_session, bp_file, run_as)
-
-    debug_session.send_request('setBreakpoints', arguments={
-        'source': {'path': bp_file},
-        'breakpoints': [{'line': bp_line}, ],
-    }).wait_for_response()
+    debug_session.initialize(target=(run_as, bp_file), start_method=start_method)
+    debug_session.set_breakpoints(bp_file, [bp_line])
     debug_session.start_debugging()
 
     debug_session.wait_for_next(Event('stopped'), ANY.dict_with({'reason': 'breakpoint'}))
@@ -162,8 +136,8 @@ def _decode(text):
                if _decode(l).startswith('Press'))
 
 
-@pytest.mark.parametrize('run_as', ['file', 'module'])
-def test_exit_normally_with_wait_on_abnormal_exit_enabled(debug_session, pyfile, run_as):
+@pytest.mark.parametrize('start_method', [START_METHOD_LAUNCH, START_METHOD_CMDLINE])
+def test_exit_normally_with_wait_on_abnormal_exit_enabled(debug_session, pyfile, run_as, start_method):
     @pyfile
     def code_to_debug():
         print('one')
@@ -174,12 +148,8 @@ def code_to_debug():
 
     bp_line = 3
     bp_file = code_to_debug
-    _common_setup(debug_session, bp_file, run_as)
-
-    debug_session.send_request('setBreakpoints', arguments={
-        'source': {'path': bp_file},
-        'breakpoints': [{'line': bp_line}, ],
-    }).wait_for_response()
+    debug_session.initialize(target=(run_as, bp_file), start_method=start_method)
+    debug_session.set_breakpoints(bp_file, [bp_line])
     debug_session.start_debugging()
 
     debug_session.wait_for_next(Event('stopped'), ANY.dict_with({'reason': 'breakpoint'}))