Repository: microsoft/ptvsd

Issue:
Migrate to pytests: part 1
* Output tests
* Break point tests
* Thread tests
* Return value tests


Discussion:

Resolving patch:
diff --git a/pytests/conftest.py b/pytests/conftest.py
--- a/pytests/conftest.py
+++ b/pytests/conftest.py
@@ -6,6 +6,7 @@
 
 import inspect
 import os
+import platform
 import pytest
 import threading
 import types
@@ -144,6 +145,7 @@ def factory(source):
         #'attach_socket_import',
         #'attach_pid',
     ]
+    _ATTACH_PARAMS += ['attach_socket_import'] if platform.system() == 'Windows' else []
 
     _RUN_AS_PARAMS = [
         'file',
@@ -163,5 +165,5 @@ def _run_as(request):
     name='start_method',
     params=_ATTACH_PARAMS
 )
-def _start_method(request):
+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
@@ -192,7 +192,6 @@ def _get_target(self):
             pytest.fail()
         return argv
 
-
     def initialize(self, **kwargs):
         """Spawns ptvsd using the configured method, telling it to execute the
         provided Python file, module, or code, and establishes a message channel
@@ -582,3 +581,9 @@ def connect_to_child_session(self, ptvsd_subprocess):
     def connect_to_next_child_session(self):
         ptvsd_subprocess = self.wait_for_next(Event('ptvsd_subprocess'))
         return self.connect_to_child_session(ptvsd_subprocess)
+
+    def get_stdout_as_string(self):
+        return b''.join(self.output_data['OUT'])
+
+    def get_stderr_as_string(self):
+        return b''.join(self.output_data['ERR'])


Test patch:
diff --git a/pytests/func/test_args.py b/pytests/func/test_args.py
new file mode 100644
--- /dev/null
+++ b/pytests/func/test_args.py
@@ -0,0 +1,29 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See LICENSE in the project root
+# for license information.
+
+from __future__ import print_function, with_statement, absolute_import
+from pytests.helpers.session import DebugSession
+import pytest
+
+@pytest.mark.parametrize('run_as', ['file', 'module', 'code'])
+def test_args(pyfile, run_as, start_method):
+    @pyfile
+    def code_to_debug():
+        import sys
+        from dbgimporter import import_and_enable_debugger
+        import_and_enable_debugger()
+        assert sys.argv[1] == '--arg1'
+        assert sys.argv[2] == 'arg2'
+        assert sys.argv[3] == '-arg3'
+
+    args = ['--arg1', 'arg2', '-arg3']
+    with DebugSession() as session:
+        session.initialize(
+            target=(run_as, code_to_debug),
+            start_method=start_method,
+            program_args=args
+        )
+        session.start_debugging()
+
+        session.wait_for_exit()
diff --git a/pytests/func/test_break_into_dbg.py b/pytests/func/test_break_into_dbg.py
new file mode 100644
--- /dev/null
+++ b/pytests/func/test_break_into_dbg.py
@@ -0,0 +1,35 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See LICENSE in the project root
+# for license information.
+
+from __future__ import print_function, with_statement, absolute_import
+from pytests.helpers.session import DebugSession
+from pytests.helpers.timeline import Event
+import pytest
+
+@pytest.mark.parametrize('run_as', ['file', 'module', 'code'])
+def test_with_wait_for_attach(pyfile, run_as, start_method):
+    @pyfile
+    def code_to_debug():
+        # NOTE: These tests verify break_into_debugger for launch
+        # and attach cases. For attach this is always after wait_for_attach
+        from dbgimporter import import_and_enable_debugger
+        import_and_enable_debugger()
+        import ptvsd
+        ptvsd.break_into_debugger()
+        print('break here')
+
+    with DebugSession() as session:
+        session.initialize(
+            target=(run_as, code_to_debug),
+            start_method=start_method,
+            ignore_unobserved=[Event('continued')]
+        )
+        session.start_debugging()
+        hit = session.wait_for_thread_stopped()
+        frames = hit.stacktrace.body['stackFrames']
+        assert frames[0]['line'] == 7
+
+        session.send_request('continue').wait_for_response(freeze=False)
+        session.wait_for_exit()
+
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
@@ -12,6 +12,7 @@
 from pytests.helpers.pathutils import get_test_root, compare_path
 from pytests.helpers.session import DebugSession
 from pytests.helpers.timeline import Event
+from pytests.helpers.pattern import ANY, Pattern
 
 
 BP_TEST_ROOT = get_test_root('bp')
@@ -57,3 +58,71 @@ def test_path_with_unicode(run_as, start_method):
 
         session.send_request('continue').wait_for_response(freeze=False)
         session.wait_for_exit()
+
+
+@pytest.mark.parametrize('condition_key', [
+    'condition_var',
+    'hitCondition_#',
+    'hitCondition_eq',
+    'hitCondition_gt',
+    'hitCondition_ge',
+    'hitCondition_lt',
+    'hitCondition_le',
+    'hitCondition_mod',
+])
+def test_conditional_breakpoint(pyfile, run_as, start_method, condition_key):
+    @pyfile
+    def code_to_debug():
+        from dbgimporter import import_and_enable_debugger
+        import_and_enable_debugger()
+        for i in range(0, 10):
+            print(i)
+
+    expected = {
+        'condition_var': ('condition', 'i==5', '5', 1),
+        'hitCondition_#': ('hitCondition', '5', '4', 1),
+        'hitCondition_eq': ('hitCondition', '==5', '4', 1),
+        'hitCondition_gt': ('hitCondition', '>5', '5', 5),
+        'hitCondition_ge': ('hitCondition', '>=5', '4', 6),
+        'hitCondition_lt': ('hitCondition', '<5', '0', 4),
+        'hitCondition_le': ('hitCondition', '<=5', '0', 5),
+        'hitCondition_mod': ('hitCondition', '%3', '2', 3),
+    }
+    condition_type, condition, value, hits = expected[condition_key]
+
+    bp_line = 4
+    with DebugSession() as session:
+        session.initialize(
+            target=(run_as, code_to_debug),
+            start_method=start_method,
+            ignore_unobserved=[Event('continued')],
+        )
+        session.send_request('setBreakpoints', arguments={
+            'source': {'path': code_to_debug},
+            'breakpoints': [{'line': bp_line, condition_type: condition}],
+        }).wait_for_response()
+        session.start_debugging()
+        hit = session.wait_for_thread_stopped()
+        frames = hit.stacktrace.body['stackFrames']
+        assert bp_line == frames[0]['line']
+
+        resp_scopes = session.send_request('scopes', arguments={
+            'frameId': hit.frame_id
+        }).wait_for_response()
+        scopes = resp_scopes.body['scopes']
+        assert len(scopes) > 0
+
+        resp_variables = session.send_request('variables', arguments={
+            'variablesReference': scopes[0]['variablesReference']
+        }).wait_for_response()
+        variables = list(v for v in resp_variables.body['variables']
+                         if v['name'] == 'i')
+        assert variables == Pattern([
+            ANY.dict_with({'name': 'i', 'type': 'int', 'value': value, 'evaluateName': 'i'})
+        ])
+
+        session.send_request('continue').wait_for_response(freeze=False)
+        for i in range(1, hits):
+            session.wait_for_thread_stopped()
+            session.send_request('continue').wait_for_response(freeze=False)
+        session.wait_for_exit()
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,7 +4,7 @@
 
 from __future__ import print_function, with_statement, absolute_import
 
-from pytests.helpers.pattern import ANY
+from pytests.helpers.pattern import ANY, Pattern
 from pytests.helpers.session import DebugSession
 from pytests.helpers.timeline import Event
 
@@ -235,3 +235,78 @@ def code_to_debug():
 
         session.send_request('continue').wait_for_response(freeze=False)
         session.wait_for_exit()
+
+
+def test_return_values(pyfile, run_as, start_method):
+    @pyfile
+    def code_to_debug():
+        from dbgimporter import import_and_enable_debugger
+        import_and_enable_debugger()
+        class MyClass(object):
+            def do_something(self):
+                return 'did something'
+        def my_func():
+            return 'did more things'
+        MyClass().do_something()
+        my_func()
+        print('done')
+
+    expected1 = ANY.dict_with({
+        'name': '(return) MyClass.do_something',
+        'value': "'did something'",
+        'type': 'str',
+        'presentationHint': ANY.dict_with({
+            'attributes': ANY.such_that(lambda x: 'readOnly' in x)
+        }),
+    })
+
+    expected2 = ANY.dict_with({
+        'name': '(return) my_func',
+        'value': "'did more things'",
+        'type': 'str',
+        'presentationHint': ANY.dict_with({
+            'attributes': ANY.such_that(lambda x: 'readOnly' in x)
+        }),
+    })
+
+    with DebugSession() as session:
+        session.initialize(
+            target=(run_as, code_to_debug),
+            start_method=start_method,
+            debug_options=['ShowReturnValue'],
+            ignore_unobserved=[Event('continued')])
+        session.set_breakpoints(code_to_debug, [8])
+        session.start_debugging()
+        hit = session.wait_for_thread_stopped()
+
+        session.send_request('next', {'threadId': hit.thread_id}).wait_for_response()
+        hit = session.wait_for_thread_stopped(reason='step')
+
+        resp_scopes = session.send_request('scopes', arguments={
+            'frameId': hit.frame_id
+        }).wait_for_response()
+        scopes = resp_scopes.body['scopes']
+        assert len(scopes) > 0
+
+        resp_variables = session.send_request('variables', arguments={
+            'variablesReference': scopes[0]['variablesReference']
+        }).wait_for_response()
+        variables = list(v for v in resp_variables.body['variables']
+                         if v['name'].startswith('(return)'))
+
+        assert variables == Pattern([expected1])
+
+        session.send_request('next', {'threadId': hit.thread_id}).wait_for_response()
+        hit = session.wait_for_thread_stopped(reason='step')
+
+        # Scope should not have changed so use the same scope
+        resp_variables = session.send_request('variables', arguments={
+            'variablesReference': scopes[0]['variablesReference']
+        }).wait_for_response()
+        variables = list(v for v in resp_variables.body['variables']
+                         if v['name'].startswith('(return)'))
+
+        assert variables == Pattern([expected1, expected2])
+
+        session.send_request('continue').wait_for_response()
+        session.wait_for_exit()
diff --git a/pytests/func/test_output.py b/pytests/func/test_output.py
new file mode 100644
--- /dev/null
+++ b/pytests/func/test_output.py
@@ -0,0 +1,39 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See LICENSE in the project root
+# for license information.
+
+from __future__ import print_function, with_statement, absolute_import
+from pytests.helpers.session import DebugSession
+from pytests.helpers.timeline import Event
+from pytests.helpers.pattern import ANY
+
+
+def test_with_no_output(pyfile, run_as, start_method):
+    @pyfile
+    def code_to_debug():
+        from dbgimporter import import_and_enable_debugger
+        import_and_enable_debugger()
+        # Do nothing, and check if there is any output
+
+    with DebugSession() as session:
+        session.initialize(target=(run_as, code_to_debug), start_method=start_method)
+        session.start_debugging()
+        session.wait_for_exit()
+        assert b'' == session.get_stdout_as_string()
+        assert b'' == session.get_stderr_as_string()
+
+
+def test_with_tab_in_output(pyfile, run_as, start_method):
+    @pyfile
+    def code_to_debug():
+        from dbgimporter import import_and_enable_debugger
+        import_and_enable_debugger()
+        print('Hello\tWorld')
+
+    with DebugSession() as session:
+        session.initialize(target=(run_as, code_to_debug), start_method=start_method)
+        session.start_debugging()
+        session.wait_for_exit()
+        output = session.all_occurrences_of(Event('output', body=ANY.dict_with({'category': 'stdout'})))
+        output_str = ''.join(o.body['output'] for o in output)
+        assert output_str.startswith('Hello\tWorld')
diff --git a/pytests/func/test_threads.py b/pytests/func/test_threads.py
new file mode 100644
--- /dev/null
+++ b/pytests/func/test_threads.py
@@ -0,0 +1,48 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See LICENSE in the project root
+# for license information.
+
+from __future__ import print_function, with_statement, absolute_import
+import pytest
+from pytests.helpers.timeline import Event
+from pytests.helpers.session import DebugSession
+
+
+@pytest.mark.parametrize('count', [1, 3])
+def test_thread_count(pyfile, run_as, start_method, count):
+    @pyfile
+    def code_to_debug():
+        import threading
+        import time
+        import sys
+        from dbgimporter import import_and_enable_debugger
+        import_and_enable_debugger()
+        stop = False
+        def worker(tid, offset):
+            i = 0
+            global stop
+            while not stop:
+                time.sleep(0.01)
+                i += 1
+        threads = []
+        if sys.argv[1] != '1':
+            for i in [111, 222]:
+                thread = threading.Thread(target=worker, args=(i, len(threads)))
+                threads.append(thread)
+                thread.start()
+        print('check here')
+        stop = True
+
+    with DebugSession() as session:
+        session.initialize(target=(run_as, code_to_debug), start_method=start_method, program_args=[str(count)])
+        session.set_breakpoints(code_to_debug, [19])
+        session.start_debugging()
+        session.wait_for_thread_stopped()
+        resp_threads = session.send_request('threads').wait_for_response()
+
+        assert len(resp_threads.body['threads']) == count
+
+        session.send_request('continue').wait_for_response()
+        session.wait_for_next(Event('continued'))
+
+        session.wait_for_exit()
diff --git a/pytests/test_debugger.py b/pytests/test_debugger.py
--- a/pytests/test_debugger.py
+++ b/pytests/test_debugger.py
@@ -1,3 +1,7 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See LICENSE in the project root
+# for license information.
+
 import sys
 import pytest
 
diff --git a/pytests/test_internals_filter.py b/pytests/test_internals_filter.py
--- a/pytests/test_internals_filter.py
+++ b/pytests/test_internals_filter.py
@@ -1,3 +1,7 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See LICENSE in the project root
+# for license information.
+
 import os
 import pytest
 import ptvsd.untangle
diff --git a/pytests/test_modules_manager.py b/pytests/test_modules_manager.py
--- a/pytests/test_modules_manager.py
+++ b/pytests/test_modules_manager.py
@@ -1,3 +1,7 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See LICENSE in the project root
+# for license information.
+
 import sys
 import threading
 import time
diff --git a/pytests/test_parse_args.py b/pytests/test_parse_args.py
--- a/pytests/test_parse_args.py
+++ b/pytests/test_parse_args.py
@@ -1,3 +1,7 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See LICENSE in the project root
+# for license information.
+
 import pytest
 from ptvsd.socket import Address
 from ptvsd.__main__ import parse_args
diff --git a/pytests/test_pathutils.py b/pytests/test_pathutils.py
--- a/pytests/test_pathutils.py
+++ b/pytests/test_pathutils.py
@@ -1,3 +1,7 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See LICENSE in the project root
+# for license information.
+
 import os
 import os.path
 import platform