From f3be3c2b9dbbf9cd2cfb1fa37afba4538598a100 Mon Sep 17 00:00:00 2001 From: Pavel Minaev Date: Tue, 9 Jul 2019 17:49:12 -0700 Subject: [PATCH] Django test fixes. Fix test logs not being captured by pytest. Fix "import debug_me" check improperly applied in tests where it is unnecessary. Fix some clarifying patterns not respecting the underlying pattern. Add pattern helpers for strings: starting_with, ending_with, containing. Move DAP test helpers to a separate module, and add a helper for frames. Various Unicode handling test fixes. --- src/ptvsd/common/compat.py | 8 +- src/ptvsd/common/log.py | 4 +- src/ptvsd/common/messaging.py | 6 + .../DEBUGGEE_PYTHONPATH/debug_me/__init__.py | 5 + .../debug_me/backchannel.py | 2 +- tests/__init__.py | 2 + tests/debug.py | 153 ++++++-- tests/net.py | 80 +++- tests/patterns/__init__.py | 320 +--------------- tests/patterns/_impl.py | 358 ++++++++++++++++++ tests/patterns/dap.py | 32 ++ tests/patterns/some.py | 49 +-- tests/ptvsd/common/test_messaging.py | 2 +- tests/ptvsd/server/test_attach.py | 3 +- tests/ptvsd/server/test_breakpoints.py | 67 ++-- tests/ptvsd/server/test_django.py | 317 ++++++---------- tests/ptvsd/server/test_evaluate.py | 12 +- tests/ptvsd/server/test_exception.py | 2 +- tests/ptvsd/server/test_flask.py | 22 +- tests/ptvsd/server/test_multiproc.py | 8 +- tests/ptvsd/server/test_run.py | 4 +- tests/test_data/django1/app.py | 4 +- tests/tests/test_patterns.py | 48 +++ 23 files changed, 835 insertions(+), 673 deletions(-) create mode 100644 tests/patterns/_impl.py create mode 100644 tests/patterns/dap.py diff --git a/src/ptvsd/common/compat.py b/src/ptvsd/common/compat.py index 6e1d1dc9c..8056dc25b 100644 --- a/src/ptvsd/common/compat.py +++ b/src/ptvsd/common/compat.py @@ -71,7 +71,7 @@ def force_bytes(s, encoding, errors="strict"): return s -def force_str(s, encoding, errors="strict"): +def force_str(s, encoding="ascii", errors="strict"): """Converts s to str (which is bytes on Python 2, and unicode on Python 3), using the provided encoding if necessary. If s is already str, it is returned as is. @@ -137,6 +137,12 @@ def nameof(obj, quote=False): return force_unicode(name, "utf-8", "replace") +def unicode_repr(obj): + """Like repr(), but guarantees that the result is Unicode even on Python 2. + """ + return force_unicode(repr(obj), "ascii") + + def srcnameof(obj): """Returns the most descriptive name of a Python module, class, or function, including source information (filename and linenumber), if available. diff --git a/src/ptvsd/common/log.py b/src/ptvsd/common/log.py index f32eec97a..b8d1f6157 100644 --- a/src/ptvsd/common/log.py +++ b/src/ptvsd/common/log.py @@ -21,6 +21,8 @@ """Logging levels, lowest to highest importance. """ +stderr = sys.__stderr__ + stderr_levels = {"warning", "error"} """What should be logged to stderr. """ @@ -62,7 +64,7 @@ def write(level, text): with _lock: if level in stderr_levels: try: - sys.__stderr__.write(output) + stderr.write(output) except Exception: pass diff --git a/src/ptvsd/common/messaging.py b/src/ptvsd/common/messaging.py index a454a12db..980fb8f49 100644 --- a/src/ptvsd/common/messaging.py +++ b/src/ptvsd/common/messaging.py @@ -70,7 +70,13 @@ def from_socket(cls, socket, name=None): socket.settimeout(None) # make socket blocking if name is None: name = repr(socket) + + # TODO: investigate switching to buffered sockets; readline() on unbuffered + # sockets is very slow! Although the implementation of readline() itself is + # native code, it calls read(1) in a loop - and that then ultimately calls + # SocketIO.readinto(), which is implemented in Python. socket_io = socket.makefile("rwb", 0) + return cls(socket_io, socket_io, name) def __init__(self, reader, writer, name=None): diff --git a/tests/DEBUGGEE_PYTHONPATH/debug_me/__init__.py b/tests/DEBUGGEE_PYTHONPATH/debug_me/__init__.py index f0412030c..96bd29e04 100644 --- a/tests/DEBUGGEE_PYTHONPATH/debug_me/__init__.py +++ b/tests/DEBUGGEE_PYTHONPATH/debug_me/__init__.py @@ -40,5 +40,10 @@ # to DebugSession - the debuggee simply needs to execute it as is. _code = os.getenv("PTVSD_DEBUG_ME") if _code: + # Remove it, so that subprocesses don't try to manually configure ptvsd on the + # same port. In multiprocess scenarios, subprocesses are supposed to load ptvsd + # via code that is automatically injected into the subprocess by its parent. + del os.environ["PTVSD_DEBUG_ME"] + _code = compile(_code, "", "exec") eval(_code, {}) diff --git a/tests/DEBUGGEE_PYTHONPATH/debug_me/backchannel.py b/tests/DEBUGGEE_PYTHONPATH/debug_me/backchannel.py index 3e7b98f81..da46e04d1 100644 --- a/tests/DEBUGGEE_PYTHONPATH/debug_me/backchannel.py +++ b/tests/DEBUGGEE_PYTHONPATH/debug_me/backchannel.py @@ -22,7 +22,7 @@ port = os.getenv("PTVSD_BACKCHANNEL_PORT") if port is not None: port = int(port) - # Remove it, so that child processes don't try to use the same backchannel. + # Remove it, so that subprocesses don't try to use the same backchannel. del os.environ["PTVSD_BACKCHANNEL_PORT"] diff --git a/tests/__init__.py b/tests/__init__.py index d68c8cdea..6f2985bae 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -11,6 +11,7 @@ import pkgutil import pytest import py.path +import sys # Do not import anything from ptvsd until assert rewriting is enabled below! @@ -55,6 +56,7 @@ def _register_assert_rewrite(modname): # Enable full logging to stderr, and make timestamps shorter to match maximum test # run time better. +log.stderr = sys.stderr # use pytest-captured stderr rather than __stderr__ log.stderr_levels = set(log.LEVELS) log.timestamp_format = "06.3f" diff --git a/tests/debug.py b/tests/debug.py index 5cf76f77d..97603be1e 100644 --- a/tests/debug.py +++ b/tests/debug.py @@ -4,7 +4,7 @@ from __future__ import absolute_import, print_function, unicode_literals -from collections import namedtuple +import collections import itertools import os import platform @@ -22,7 +22,7 @@ import tests from tests import net from tests.patterns import some -from tests.timeline import Timeline, Event, Response +from tests.timeline import Timeline, Event, Request, Response PTVSD_DIR = py.path.local(ptvsd.__file__) / ".." PTVSD_PORT = net.get_test_server_port(5678, 5800) @@ -41,7 +41,7 @@ """ -StopInfo = namedtuple('StopInfo', [ +StopInfo = collections.namedtuple('StopInfo', [ 'body', 'frames', 'thread_id', @@ -51,11 +51,24 @@ class Session(object): WAIT_FOR_EXIT_TIMEOUT = 10 + """Timeout used by wait_for_exit() before it kills the ptvsd process. + """ + + START_METHODS = { + 'launch', # ptvsd --client ... foo.py + 'attach_socket_cmdline', # ptvsd ... foo.py + 'attach_socket_import', # python foo.py (foo.py must import debug_me) + 'attach_pid', # python foo.py && ptvsd ... --pid + 'custom_client' # python foo.py (foo.py has to manually connect to session) + } + + DEBUG_ME_START_METHODS = {"attach_socket_import"} + """Start methods that require import debug_me.""" _counter = itertools.count(1) def __init__(self, start_method='launch', ptvsd_port=None, pid=None): - assert start_method in ('launch', 'attach_pid', 'attach_socket_cmdline', 'attach_socket_import', 'custom_client') + assert start_method in self.START_METHODS assert ptvsd_port is None or start_method.startswith('attach_socket_') self.id = next(self._counter) @@ -237,11 +250,13 @@ def _validate_pyfile(self, filename): assert os.path.isfile(filename) with open(filename, "rb") as f: code = f.read() - if self.start_method != "custom_client": - assert b"debug_me" in code, ( - "Python source code that is run via tests.debug.Session must " - "import debug_me" + if self.start_method in self.DEBUG_ME_START_METHODS: + assert b"debug_me" in code, fmt( + "{0} is started via {1}, but it doesn't import debug_me.", + filename, + self.start_method, ) + return code def _get_target(self): @@ -289,7 +304,7 @@ def _setup_session(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) - assert self.start_method in ('launch', 'attach_pid', 'attach_socket_cmdline', 'attach_socket_import', 'custom_client') + assert self.start_method in self.START_METHODS assert len(self.target) == 2 assert self.target[0] in ('file', 'module', 'code') @@ -371,7 +386,7 @@ def initialize(self, **kwargs): # Assume that values are filenames - it's usually either that, or numbers. make_filename = compat.filename_bytes if sys.version_info < (3,) else compat.filename env = { - compat.force_str(k, "ascii"): make_filename(v) + compat.force_str(k): make_filename(v) for k, v in self.env.items() } @@ -695,18 +710,26 @@ def _process_response(self, request_occ, response): def _process_request(self, request): assert False, 'ptvsd should not be sending requests.' - def request_continue(self): - self.send_request('continue').wait_for_response(freeze=False) - - def set_breakpoints(self, path, lines=()): - return self.request('setBreakpoints', arguments={ - 'source': {'path': path}, - 'breakpoints': [{'line': bp_line} for bp_line in lines], - }).get('breakpoints', {}) - def wait_for_next_event(self, event, body=some.object): return self.timeline.wait_for_next(Event(event, body)).body + def output(self, category): + """Returns all output of a given category as a single string, assembled from + all the "output" events received for that category so far. + """ + events = self.all_occurrences_of( + Event("output", some.dict.containing({"category": category})) + ) + return "".join(event.body["output"] for event in events) + + def captured_stdout(self, encoding=None): + return self.captured_output.stdout(encoding) + + def captured_stderr(self, encoding=None): + return self.captured_output.stderr(encoding) + + # Helpers for specific DAP patterns. + def wait_for_stop(self, reason=some.str, expected_frames=None, expected_text=None, expected_description=None): stopped_event = self.wait_for_next(Event('stopped', some.dict.containing({'reason': reason}))) stopped = stopped_event.body @@ -737,7 +760,66 @@ def wait_for_stop(self, reason=some.str, expected_frames=None, expected_text=Non return StopInfo(stopped, frames, tid, fid) - def connect_to_child_session(self, ptvsd_subprocess): + def request_continue(self): + self.send_request('continue').wait_for_response(freeze=False) + + def set_breakpoints(self, path, lines=()): + return self.request('setBreakpoints', arguments={ + 'source': {'path': path}, + 'breakpoints': [{'line': bp_line} for bp_line in lines], + }).get('breakpoints', {}) + + def get_variables(self, *varnames, **kwargs): + """Fetches the specified variables from the frame specified by frame_id, or + from the topmost frame in the last "stackTrace" response if frame_id is not + specified. + + If varnames is empty, then all variables in the frame are returned. The result + is an OrderedDict, in which every entry has variable name as the key, and a + DAP Variable object as the value. The original order of variables as reported + by the debugger is preserved. + + If varnames is not empty, then only the specified variables are returned. + The result is a tuple, in which every entry is a DAP Variable object; those + entries are in the same order as varnames. + """ + + assert self.timeline.is_frozen + + frame_id = kwargs.pop("frame_id", None) + if frame_id is None: + stackTrace_responses = self.all_occurrences_of( + Response(Request("stackTrace")) + ) + assert stackTrace_responses, ( + 'get_variables() without frame_id requires at least one response ' + 'to a "stackTrace" request in the timeline.' + ) + stack_trace = stackTrace_responses[-1].body + frame_id = stack_trace["stackFrames"][0]["id"] + + scopes = self.request("scopes", {"frameId": frame_id})["scopes"] + assert len(scopes) > 0 + + variables = self.request( + "variables", {"variablesReference": scopes[0]["variablesReference"]} + )["variables"] + + variables = collections.OrderedDict(((v["name"], v) for v in variables)) + if varnames: + assert set(varnames) <= set(variables.keys()) + return tuple((variables[name] for name in varnames)) + else: + return variables + + def get_variable(self, varname, frame_id=None): + """Same as get_variables(...)[0]. + """ + return self.get_variables(varname, frame_id=frame_id)[0] + + def attach_to_subprocess(self, ptvsd_subprocess): + assert ptvsd_subprocess == Event("ptvsd_subprocess") + child_port = ptvsd_subprocess.body['port'] assert child_port != 0 @@ -754,11 +836,20 @@ def connect_to_child_session(self, ptvsd_subprocess): else: return child_session - def connect_to_next_child_session(self): + def attach_to_next_subprocess(self): ptvsd_subprocess = self.wait_for_next(Event('ptvsd_subprocess')) - return self.connect_to_child_session(ptvsd_subprocess) + return self.attach_to_subprocess(ptvsd_subprocess) + + def reattach(self, **kwargs): + """Creates and initializes a new Session that tries to attach to the same + process. + + Upon return, handshake() has been performed, but the caller is responsible + for invoking start_debugging(). + """ + + assert self.start_method.startswith("attach_socket_") - def connect_with_new_session(self, **kwargs): ns = Session(start_method='attach_socket_import', ptvsd_port=self.ptvsd_port) try: ns._setup_session(**kwargs) @@ -776,24 +867,10 @@ def connect_with_new_session(self, **kwargs): ns.handshake() except Exception: ns.close() + raise else: return ns - def output(self, category): - """Returns all output of a given category as a single string, assembled from - all the "output" events received for that category so far. - """ - events = self.all_occurrences_of( - Event("output", some.dict.containing({"category": category})) - ) - return "".join(event.body["output"] for event in events) - - def captured_stdout(self, encoding=None): - return self.captured_output.stdout(encoding) - - def captured_stderr(self, encoding=None): - return self.captured_output.stderr(encoding) - class CapturedOutput(object): """Captured stdout and stderr of the debugged process. diff --git a/tests/net.py b/tests/net.py index 84e189020..324e3f8f9 100644 --- a/tests/net.py +++ b/tests/net.py @@ -31,13 +31,13 @@ def get_test_server_port(start, stop): """ try: - worker_id = compat.force_ascii(os.environ['PYTEST_XDIST_WORKER']) + worker_id = compat.force_ascii(os.environ["PYTEST_XDIST_WORKER"]) except KeyError: n = 0 else: - assert worker_id == some.str.matching(br"gw(\d+)"), ( - "Unrecognized PYTEST_XDIST_WORKER format" - ) + assert worker_id == some.bytes.matching( + br"gw(\d+)" + ), "Unrecognized PYTEST_XDIST_WORKER format" n = int(worker_id[2:]) port = start + n @@ -60,13 +60,15 @@ def wait_until_port_is_listening(port, interval=1, max_attempts=1000): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: - for i in compat.xrange(0, max_attempts): + for i in compat.xrange(1, max_attempts + 1): try: - log.info("Trying to connect to port {0} (attempt {1})", port, i) + log.info("Probing localhost:{0} (attempt {1})...", port, i) sock.connect(("localhost", port)) - return except socket.error: time.sleep(interval) + else: + log.info("localhost:{0} is listening - server is up!", port) + return finally: sock.close() @@ -86,25 +88,64 @@ def post(*args, **kwargs): def __init__(self, method, url, *args, **kwargs): """Invokes requests.method(url, *args, **kwargs) on a background thread, and immediately returns. + + If method() raises an exception, it is logged, unless log_errors=False. """ + self.method = method + self.url = url + + self.log_errors = kwargs.pop("log_errors", True) + self.request = None - """The underlying Request object. Not set until wait_for_response() returns. + """The underlying requests.Request object. + + Not set until wait_for_response() returns. """ - method = getattr(requests, method) + self.exception = None + """Exception that occurred while performing the request, if any. + + Not set until wait_for_response() returns. + """ + + log.info("{0}", self) + + func = getattr(requests, method) self._worker_thread = threading.Thread( - target=lambda: self._worker(method, url, *args, **kwargs), - name=fmt("WebRequest({0!r})", url) + target=lambda: self._worker(func, *args, **kwargs), + name=fmt("WebRequest({0})", self), ) - - def _worker(self, method, url, *args, **kwargs): - self.request = method(url, *args, **kwargs) + self._worker_thread.daemon = True + self._worker_thread.start() + + def __str__(self): + return fmt("HTTP {0} {1}", self.method.upper(), self.url) + + def _worker(self, func, *args, **kwargs): + try: + self.request = func(self.url, *args, **kwargs) + except Exception as exc: + if self.log_errors: + log.exception("{0} failed:", self) + self.exception = exc + else: + log.info( + "{0} --> {1} {2}", + self, + self.request.status_code, + self.request.reason + ) def wait_for_response(self, timeout=None): """Blocks until the request completes, and returns self.request. """ - self._worker_thread.join(timeout) + if self._worker_thread.is_alive(): + log.info("Waiting for response to {0} ...", self) + self._worker_thread.join(timeout) + + if self.exception is not None: + raise self.exception return self.request def response_text(self): @@ -124,13 +165,16 @@ def __init__(self, port): def __enter__(self): """Blocks until the server starts listening on self.port. """ - wait_until_port_is_listening(self.port) + log.info("Web server expected on {0}", self.url) + wait_until_port_is_listening(self.port, interval=3) return self def __exit__(self, exc_type, exc_value, exc_tb): - """Sends an HTTP /exit POST request to the server. + """Sends an HTTP /exit GET request to the server. + + The server is expected to terminate its process while handling that request. """ - self.post("exit").wait_for_response() + self.get("/exit", log_errors=False) def get(self, path, *args, **kwargs): return WebRequest.get(self.url + path, *args, **kwargs) diff --git a/tests/patterns/__init__.py b/tests/patterns/__init__.py index 7e1202db2..8c6923007 100644 --- a/tests/patterns/__init__.py +++ b/tests/patterns/__init__.py @@ -7,320 +7,8 @@ """Do not import this package directly - import tests.patterns.some instead. """ -# The actual patterns are defined here, so that tests.patterns.some can redefine -# builtin names like str, int etc without affecting the implementations in this -# file - some.* then provides shorthand aliases. +# Wire up some.dap to be an alias for dap, to allow writing some.dap.id etc. +from tests.patterns import some +from tests.patterns import dap -import itertools -import py.path -import re -import sys - -from ptvsd.common import compat, fmt -from ptvsd.common.compat import unicode, xrange -import pydevd_file_utils - - -class Some(object): - """A pattern that can be tested against a value with == to see if it matches. - """ - - def __repr__(self): - try: - return self.name - except AttributeError: - raise NotImplementedError - - def __eq__(self, value): - raise NotImplementedError - - def __ne__(self, other): - return not (self == other) - - def __invert__(self): - """The inverse pattern - matches everything that this one doesn't. - """ - return Not(self) - - def __or__(self, pattern): - """Union pattern - matches if either of the two patterns match. - """ - return Either(self, pattern) - - def such_that(self, condition): - """Same pattern, but it only matches if condition() is true. - """ - return SuchThat(self, condition) - - def in_range(self, start, stop): - """Same pattern, but it only matches if the start <= value < stop. - """ - return InRange(self, start, stop) - - -class Not(Some): - """Matches the inverse of the pattern. - """ - - def __init__(self, pattern): - self.pattern = pattern - - def __repr__(self): - return fmt("~{0!r}", self.pattern) - - def __eq__(self, value): - return value != self.pattern - - -class Either(Some): - """Matches either of the patterns. - """ - - def __init__(self, *patterns): - assert len(patterns) > 0 - self.patterns = tuple(patterns) - - def __repr__(self): - try: - return self.name - except AttributeError: - return fmt("({0})", " | ".join(repr(pat) for pat in self.patterns)) - - def __eq__(self, value): - return any(pattern == value for pattern in self.patterns) - - def __or__(self, pattern): - return Either(*(self.patterns + (pattern,))) - - -class SuchThat(Some): - """Matches only if condition is true. - """ - - def __init__(self, pattern, condition): - self.pattern = pattern - self.condition = condition - - def __repr__(self): - try: - return self.name - except AttributeError: - return fmt("({0!r} if {1})", self.pattern, compat.nameof(self.condition)) - - def __eq__(self, value): - return self.condition(value) and value == self.pattern - - -class InRange(Some): - """Matches only if the value is within the specified range. - """ - - def __init__(self, pattern, start, stop): - self.pattern = pattern - self.start = start - self.stop = stop - - def __repr__(self): - try: - return self.name - except AttributeError: - return fmt("({0!r} <= {1!r} < {2!r})", self.start, self.pattern, self.stop) - - def __eq__(self, value): - return self.start <= value < self.stop and value == self.pattern - - -class Object(Some): - """Matches anything. - """ - - name = "" - - def __eq__(self, value): - return True - - def equal_to(self, obj): - return EqualTo(obj) - - def same_as(self, obj): - return SameAs(obj) - - -class Thing(Some): - """Matches anything that is not None. - """ - - name = "<>" - - def __eq__(self, value): - return value is not None - - -class InstanceOf(Some): - """Matches any object that is an instance of the specified type. - """ - - def __init__(self, classinfo, name=None): - if isinstance(classinfo, type): - classinfo = (classinfo,) - assert ( - len(classinfo) > 0 and - all((isinstance(cls, type) for cls in classinfo)) - ), "classinfo must be a type or a tuple of types" - - self.name = name - self.classinfo = classinfo - - def __repr__(self): - if self.name: - name = self.name - else: - name = " | ".join(cls.__name__ for cls in self.classinfo) - return fmt("<{0}>", name) - - def __eq__(self, value): - return isinstance(value, self.classinfo) - - -class EqualTo(Some): - """Matches any object that is equal to the specified object. - """ - - def __init__(self, obj): - self.obj = obj - - def __repr__(self): - return repr(self.obj) - - def __eq__(self, value): - return self.obj == value - - -class SameAs(Some): - """Matches one specific object only (i.e. makes '==' behave like 'is'). - """ - - def __init__(self, obj): - self.obj = obj - - def __repr__(self): - return fmt("is {0!r}", self.obj) - - def __eq__(self, value): - return self.obj is value - - -class Matching(Some): - """Matches any string that matches the specified regular expression. - """ - - def __init__(self, regex): - assert isinstance(regex, bytes) or isinstance(regex, unicode) - self.regex = regex - - def __repr__(self): - s = repr(self.regex) - if s[0] in "bu": - return s[0] + "/" + s[2:-1] + "/" - else: - return "/" + s[1:-1] + "/" - - def __eq__(self, other): - regex = self.regex - if isinstance(regex, bytes) and not isinstance(other, bytes): - return NotImplemented - if isinstance(regex, unicode) and not isinstance(other, unicode): - return NotImplemented - return re.match(regex, other) is not None - - -class Path(Some): - """Matches any string that matches the specified path. - - Uses os.path.normcase() to normalize both strings before comparison. - - If one string is unicode, but the other one is not, both strings are normalized - to unicode using sys.getfilesystemencoding(). - """ - - def __init__(self, path): - if isinstance(path, py.path.local): - path = path.strpath - if isinstance(path, bytes): - path = path.encode(sys.getfilesystemencoding()) - assert isinstance(path, unicode) - self.path = path - - def __repr__(self): - return fmt("some.path({0!r})", self.path) - - def __eq__(self, other): - if isinstance(other, py.path.local): - other = other.strpath - - if isinstance(other, unicode): - pass - elif isinstance(other, bytes): - other = other.encode(sys.getfilesystemencoding()) - else: - return NotImplemented - - left = pydevd_file_utils.get_path_with_real_case(self.path) - right = pydevd_file_utils.get_path_with_real_case(other) - return left == right - - -class ListContaining(Some): - """Matches any list that contains the specified subsequence of elements. - """ - - def __init__(self, *items): - self.items = tuple(items) - - def __repr__(self): - if not self.items: - return "[...]" - s = repr(list(self.items)) - return fmt("[..., {0}, ...]", s[1:-1]) - - def __eq__(self, other): - if not isinstance(other, list): - return NotImplemented - - items = self.items - if not items: - return True # every list contains an empty sequence - if len(items) == 1: - return self.items[0] in other - - # Zip the other list with itself, shifting by one every time, to produce - # tuples of equal length with items - i.e. all potential subsequences. So, - # given other=[1, 2, 3, 4, 5] and items=(2, 3, 4), we want to get a list - # like [(1, 2, 3), (2, 3, 4), (3, 4, 5)] - and then search for items in it. - iters = [itertools.islice(other, i, None) for i in xrange(0, len(items))] - subseqs = compat.izip(*iters) - return any(subseq == items for subseq in subseqs) - - -class DictContaining(Some): - """Matches any dict that contains the specified key-value pairs:: - - d1 = {'a': 1, 'b': 2, 'c': 3} - d2 = {'a': 1, 'b': 2} - assert d1 == some.dict.containing(d2) - assert d2 != some.dict.containing(d1) - """ - - def __init__(self, items): - self.items = dict(items) - - def __repr__(self): - return repr(self.items)[:-1] + ', ...}' - - def __eq__(self, other): - if not isinstance(other, dict): - return NotImplemented - any = Object() - d = {key: any for key in other} - d.update(self.items) - return d == other +some.dap = dap diff --git a/tests/patterns/_impl.py b/tests/patterns/_impl.py new file mode 100644 index 000000000..93ba555d3 --- /dev/null +++ b/tests/patterns/_impl.py @@ -0,0 +1,358 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root +# for license information. + +from __future__ import absolute_import, print_function, unicode_literals + +# The actual patterns are defined here, so that tests.patterns.some can redefine +# builtin names like str, int etc without affecting the implementations in this +# file - some.* then provides shorthand aliases. + +import itertools +import py.path +import re +import sys + +from ptvsd.common import compat, fmt +from ptvsd.common.compat import unicode, xrange +import pydevd_file_utils + + +class Some(object): + """A pattern that can be tested against a value with == to see if it matches. + """ + + def matches(self, value): + raise NotImplementedError + + def __repr__(self): + try: + return self.name + except AttributeError: + raise NotImplementedError + + def __eq__(self, value): + return self.matches(value) + + def __ne__(self, value): + return not self.matches(value) + + def __invert__(self): + """The inverse pattern - matches everything that this one doesn't. + """ + return Not(self) + + def __or__(self, pattern): + """Union pattern - matches if either of the two patterns match. + """ + return Either(self, pattern) + + def such_that(self, condition): + """Same pattern, but it only matches if condition() is true. + """ + return SuchThat(self, condition) + + def in_range(self, start, stop): + """Same pattern, but it only matches if the start <= value < stop. + """ + return InRange(self, start, stop) + + def equal_to(self, obj): + return EqualTo(self, obj) + + def same_as(self, obj): + return SameAs(self, obj) + + def matching(self, regex): + """Same pattern, but it only matches if re.match(regex) is not None. + """ + return Matching(self, regex) + + +class Not(Some): + """Matches the inverse of the pattern. + """ + + def __init__(self, pattern): + self.pattern = pattern + + def __repr__(self): + return fmt("~{0!r}", self.pattern) + + def matches(self, value): + return value != self.pattern + + +class Either(Some): + """Matches either of the patterns. + """ + + def __init__(self, *patterns): + assert len(patterns) > 0 + self.patterns = tuple(patterns) + + def __repr__(self): + try: + return self.name + except AttributeError: + return fmt("({0})", " | ".join(repr(pat) for pat in self.patterns)) + + def matches(self, value): + return any(pattern == value for pattern in self.patterns) + + def __or__(self, pattern): + return Either(*(self.patterns + (pattern,))) + + +class Object(Some): + """Matches anything. + """ + + name = "" + + def matches(self, value): + return True + + +class Thing(Some): + """Matches anything that is not None. + """ + + name = "<>" + + def matches(self, value): + return value is not None + + +class InstanceOf(Some): + """Matches any object that is an instance of the specified type. + """ + + def __init__(self, classinfo, name=None): + if isinstance(classinfo, type): + classinfo = (classinfo,) + assert ( + len(classinfo) > 0 and + all((isinstance(cls, type) for cls in classinfo)) + ), "classinfo must be a type or a tuple of types" + + self.name = name + self.classinfo = classinfo + + def __repr__(self): + if self.name: + name = self.name + else: + name = " | ".join(cls.__name__ for cls in self.classinfo) + return fmt("<{0}>", name) + + def matches(self, value): + return isinstance(value, self.classinfo) + + +class Path(Some): + """Matches any string that matches the specified path. + + Uses os.path.normcase() to normalize both strings before comparison. + + If one string is unicode, but the other one is not, both strings are normalized + to unicode using sys.getfilesystemencoding(). + """ + + def __init__(self, path): + if isinstance(path, py.path.local): + path = path.strpath + if isinstance(path, bytes): + path = path.encode(sys.getfilesystemencoding()) + assert isinstance(path, unicode) + self.path = path + + def __repr__(self): + return fmt("some.path({0!r})", self.path) + + def matches(self, other): + if isinstance(other, py.path.local): + other = other.strpath + + if isinstance(other, unicode): + pass + elif isinstance(other, bytes): + other = other.encode(sys.getfilesystemencoding()) + else: + return NotImplemented + + left = pydevd_file_utils.get_path_with_real_case(self.path) + right = pydevd_file_utils.get_path_with_real_case(other) + return left == right + + +class ListContaining(Some): + """Matches any list that contains the specified subsequence of elements. + """ + + def __init__(self, *items): + self.items = tuple(items) + + def __repr__(self): + if not self.items: + return "[...]" + s = repr(list(self.items)) + return fmt("[..., {0}, ...]", s[1:-1]) + + def matches(self, other): + if not isinstance(other, list): + return NotImplemented + + items = self.items + if not items: + return True # every list contains an empty sequence + if len(items) == 1: + return self.items[0] in other + + # Zip the other list with itself, shifting by one every time, to produce + # tuples of equal length with items - i.e. all potential subsequences. So, + # given other=[1, 2, 3, 4, 5] and items=(2, 3, 4), we want to get a list + # like [(1, 2, 3), (2, 3, 4), (3, 4, 5)] - and then search for items in it. + iters = [itertools.islice(other, i, None) for i in xrange(0, len(items))] + subseqs = compat.izip(*iters) + return any(subseq == items for subseq in subseqs) + + +class DictContaining(Some): + """Matches any dict that contains the specified key-value pairs:: + + d1 = {'a': 1, 'b': 2, 'c': 3} + d2 = {'a': 1, 'b': 2} + assert d1 == some.dict.containing(d2) + assert d2 != some.dict.containing(d1) + """ + + def __init__(self, items): + self.items = dict(items) + + def __repr__(self): + return repr(self.items)[:-1] + ', ...}' + + def matches(self, other): + if not isinstance(other, dict): + return NotImplemented + any = Object() + d = {key: any for key in other} + d.update(self.items) + return d == other + + +class Also(Some): + """Base class for patterns that narrow down another pattern. + """ + + def __init__(self, pattern): + self.pattern = pattern + + def matches(self, value): + return self.pattern == value and self._also(value) + + def _also(self, value): + raise NotImplementedError + + +class SuchThat(Also): + """Matches only if condition is true. + """ + + def __init__(self, pattern, condition): + super(SuchThat, self).__init__(pattern) + self.condition = condition + + def __repr__(self): + try: + return self.name + except AttributeError: + return fmt("({0!r} if {1})", self.pattern, compat.nameof(self.condition)) + + def _also(self, value): + return self.condition(value) + + +class InRange(Also): + """Matches only if the value is within the specified range. + """ + + def __init__(self, pattern, start, stop): + super(InRange, self).__init__(pattern) + self.start = start + self.stop = stop + + def __repr__(self): + try: + return self.name + except AttributeError: + return fmt("({0!r} <= {1!r} < {2!r})", self.start, self.pattern, self.stop) + + def _also(self, value): + return self.start <= value < self.stop + + +class EqualTo(Also): + """Matches any object that is equal to the specified object. + """ + + def __init__(self, pattern, obj): + super(EqualTo, self).__init__(pattern) + self.obj = obj + + def __repr__(self): + return repr(self.obj) + + def _also(self, value): + return self.obj == value + + +class SameAs(Also): + """Matches one specific object only (i.e. makes '==' behave like 'is'). + """ + + def __init__(self, pattern, obj): + super(SameAs, self).__init__(pattern) + self.obj = obj + + def __repr__(self): + return fmt("is {0!r}", self.obj) + + def _also(self, value): + return self.obj is value + + +class Matching(Also): + """Matches any string that matches the specified regular expression. + """ + + def __init__(self, pattern, regex): + assert isinstance(regex, bytes) or isinstance(regex, unicode) + super(Matching, self).__init__(pattern) + self.regex = regex + + def __repr__(self): + s = repr(self.regex) + if s[0] in "bu": + return s[0] + "/" + s[2:-1] + "/" + else: + return "/" + s[1:-1] + "/" + + def _also(self, value): + regex = self.regex + + # re.match() always starts matching at the beginning, but does not require + # a complete match of the string - append "$" to ensure the latter. + if isinstance(regex, bytes): + if not isinstance(value, bytes): + return NotImplemented + regex += b"$" + elif isinstance(regex, unicode): + if not isinstance(value, unicode): + return NotImplemented + regex += "$" + else: + raise AssertionError() + + return re.match(regex, value) is not None diff --git a/tests/patterns/dap.py b/tests/patterns/dap.py new file mode 100644 index 000000000..ddab5b67d --- /dev/null +++ b/tests/patterns/dap.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root +# for license information. + +from __future__ import absolute_import, print_function, unicode_literals + +"""Patterns that are specific to the Debug Adapter Protocol. +""" + +from tests.patterns import some + + +id = some.int.in_range(0, 10000) +"""Matches a DAP "id", assuming some reasonable range for an implementation that +generates those ids sequentially. +""" + + +def source(path, **kwargs): + """Matches DAP Source objects. + """ + d = {"path": path} + d.update(kwargs) + return some.dict.containing(d) + + +def frame(source, line, **kwargs): + """Matches DAP Frame objects. + """ + d = {"id": some.dap.id, "column": 1} + d.update(kwargs) + return some.dict.containing(d) diff --git a/tests/patterns/some.py b/tests/patterns/some.py index dc5fbd9da..7d01cb92f 100644 --- a/tests/patterns/some.py +++ b/tests/patterns/some.py @@ -33,14 +33,20 @@ assert Exception() == some.error assert object() == some.object.same_as(object()) + assert b"abc" == some.bytes assert u"abc" == some.str if sys.version_info < (3,): assert b"abc" == some.str else: assert b"abc" != some.str - assert "abbbc" == some.str.matching(r".(b+).") - assert "abbbc" != some.str.matching(r"bbb") + assert "abbc" == some.str.starting_with("ab") + assert "abbc" == some.str.ending_with("bc") + assert "abbc" == some.str.containing("bb") + + assert "abbc" == some.str.matching(r".(b+).") + assert "abbc" != some.str.matching(r"ab") + assert "abbc" != some.str.matching(r"bc") if platform.system() == "Windows": assert "\\Foo\\Bar" == some.path("/foo/bar") @@ -65,7 +71,7 @@ __all__ = [ "bool", "bytes", - "dap_id", + "dap", "dict", "error", "instanceof", @@ -73,23 +79,23 @@ "list", "number", "path", - "source", "str", "thing", "tuple", ] import numbers +import re import sys from ptvsd.common.compat import builtins -from tests import patterns as some +from tests.patterns import _impl -object = some.Object() -thing = some.Thing() -instanceof = some.InstanceOf -path = some.Path +object = _impl.Object() +thing = _impl.Thing() +instanceof = _impl.InstanceOf +path = _impl.Path bool = instanceof(builtins.bool) @@ -100,7 +106,9 @@ bytes = instanceof(builtins.bytes) -bytes.matching = some.Matching +bytes.starting_with = lambda prefix: bytes.matching(re.escape(prefix) + b".*") +bytes.ending_with = lambda suffix: bytes.matching(b".*" + re.escape(suffix)) +bytes.containing = lambda sub: bytes.matching(b".*" + re.escape(sub) + b".*") """In Python 2, matches both str and unicode. In Python 3, only matches str. @@ -109,24 +117,19 @@ str = instanceof((builtins.str, builtins.unicode), "str") else: str = instanceof(builtins.str) -str.matching = some.Matching + +str.starting_with = lambda prefix: str.matching(re.escape(prefix) + ".*") +str.ending_with = lambda suffix: str.matching(".*" + re.escape(suffix)) +str.containing = lambda sub: str.matching(".*" + re.escape(sub) + ".*") list = instanceof(builtins.list) -list.containing = some.ListContaining +list.containing = _impl.ListContaining dict = instanceof(builtins.dict) -dict.containing = some.DictContaining - - -dap_id = int.in_range(0, 10000) -"""Matches a DAP "id", assuming some reasonable range for an implementation that -generates those ids sequentially. -""" +dict.containing = _impl.DictContaining -def source(path): - """Matches "source": {"path": ...} values in DAP. - """ - return dict.containing({"path": path}) +# Set in __init__.py to avoid circular dependency. +dap = None diff --git a/tests/ptvsd/common/test_messaging.py b/tests/ptvsd/common/test_messaging.py index 5af3f855e..f7ceedb4c 100644 --- a/tests/ptvsd/common/test_messaging.py +++ b/tests/ptvsd/common/test_messaging.py @@ -592,7 +592,7 @@ def pause_request(self, request): input_exhausted.wait() def missing_property(name): - return some.str.matching("Invalid message:.*" + re.escape(name)) + return some.str.matching("Invalid message:.*" + re.escape(name) + ".*") assert output == [ { diff --git a/tests/ptvsd/server/test_attach.py b/tests/ptvsd/server/test_attach.py index 329076d95..8e37b872c 100644 --- a/tests/ptvsd/server/test_attach.py +++ b/tests/ptvsd/server/test_attach.py @@ -94,8 +94,7 @@ def code_to_debug(): session.wait_for_disconnect() assert backchannel.receive() == "continued" - # re-attach - with session.connect_with_new_session(target=(run_as, code_to_debug)) as session2: + with session.reattach(target=(run_as, code_to_debug)) as session2: session2.start_debugging() hit = session2.wait_for_stop() assert code_to_debug.lines["second"] == hit.frames[0]["line"] diff --git a/tests/ptvsd/server/test_breakpoints.py b/tests/ptvsd/server/test_breakpoints.py index 069f25f98..8dad26162 100644 --- a/tests/ptvsd/server/test_breakpoints.py +++ b/tests/ptvsd/server/test_breakpoints.py @@ -29,7 +29,7 @@ def test_path_with_ampersand(start_method, run_as): session.wait_for_stop( "breakpoint", - expected_frames=[some.dict.containing({"source": some.source(test_py)})], + expected_frames=[some.dict.containing({"source": some.dap.source(test_py)})], ) session.request_continue() @@ -54,7 +54,7 @@ def test_path_with_unicode(start_method, run_as): session.wait_for_stop("breakpoint", expected_frames=[ some.dict.containing({ - "source": some.source(test_py), + "source": some.dap.source(test_py), "name": "ಏನಾದರೂ_ಮಾಡು", }), ]) @@ -66,14 +66,14 @@ def test_path_with_unicode(start_method, run_as): @pytest.mark.parametrize( "condition_kind", [ - ("condition",), - ("hitCondition",), - ("hitCondition", "eq"), - ("hitCondition", "gt"), - ("hitCondition", "ge"), - ("hitCondition", "lt"), - ("hitCondition", "le"), - ("hitCondition", "mod"), + "condition", + "hitCondition", + "hitCondition-eq", + "hitCondition-gt", + "hitCondition-ge", + "hitCondition-lt", + "hitCondition-le", + "hitCondition-mod", ], ) def test_conditional_breakpoint(pyfile, start_method, run_as, condition_kind): @@ -84,16 +84,16 @@ def code_to_debug(): for i in range(0, 10): print(i) # @bp - condition_property = condition_kind[0] + condition_property = condition_kind.partition("-")[0] condition, value, hits = { - ("condition",): ("i==5", "5", 1), - ("hitCondition",): ("5", "4", 1), - ("hitCondition", "eq"): ("==5", "4", 1), - ("hitCondition", "gt"): (">5", "5", 5), - ("hitCondition", "ge"): (">=5", "4", 6), - ("hitCondition", "lt"): ("<5", "0", 4), - ("hitCondition", "le"): ("<=5", "0", 5), - ("hitCondition", "mod"): ("%3", "2", 3), + "condition": ("i==5", "5", 1), + "hitCondition": ("5", "4", 1), + "hitCondition-eq": ("==5", "4", 1), + "hitCondition-gt": (">5", "5", 5), + "hitCondition-ge": (">=5", "4", 6), + "hitCondition-lt": ("<5", "0", 4), + "hitCondition-le": ("<=5", "0", 5), + "hitCondition-mod": ("%3", "2", 3), }[condition_kind] lines = code_to_debug.lines @@ -108,27 +108,16 @@ def code_to_debug(): ) session.start_debugging() - frame_id = session.wait_for_stop(expected_frames=[ + session.wait_for_stop(expected_frames=[ some.dict.containing({"line": lines["bp"]}) - ]).frame_id - - scopes = session.request( - "scopes", arguments={"frameId": frame_id} - )["scopes"] - - assert len(scopes) > 0 + ]) - variables = session.request( - "variables", - arguments={"variablesReference": scopes[0]["variablesReference"]}, - )["variables"] + session.get_variables() - variables = [v for v in variables if v["name"] == "i"] - assert variables == [ - some.dict.containing( - {"name": "i", "type": "int", "value": value, "evaluateName": "i"} - ) - ] + var_i = session.get_variable("i") + assert var_i == some.dict.containing( + {"name": "i", "type": "int", "value": value, "evaluateName": "i"} + ) session.request_continue() for i in range(1, hits): @@ -161,7 +150,7 @@ def script2(): session.wait_for_stop(expected_frames=[ some.dict.containing({ - "source": some.source(script2), + "source": some.dap.source(script2), "line": script2.lines["bp"], }) ]) @@ -170,7 +159,7 @@ def script2(): session.wait_for_stop(expected_frames=[ some.dict.containing({ - "source": some.source(script1), + "source": some.dap.source(script1), "line": script1.lines["bp"], }) ]) diff --git a/tests/ptvsd/server/test_django.py b/tests/ptvsd/server/test_django.py index 255adc4bb..5cd773d80 100644 --- a/tests/ptvsd/server/test_django.py +++ b/tests/ptvsd/server/test_django.py @@ -5,80 +5,75 @@ from __future__ import absolute_import, print_function, unicode_literals import pytest + +from ptvsd.common import compat from tests import code, debug, net, test_data from tests.patterns import some -from tests.timeline import Event -from tests.net import find_http_url + +pytestmark = pytest.mark.timeout(60) + +django = net.WebServer(net.get_test_server_port(8000, 8100)) -DJANGO1_ROOT = test_data / "django1" -DJANGO1_MANAGE = DJANGO1_ROOT / "app.py" -DJANGO1_TEMPLATE = DJANGO1_ROOT / "templates" / "hello.html" -DJANGO1_BAD_TEMPLATE = DJANGO1_ROOT / "templates" / "bad.html" -DJANGO_PORT = net.get_test_server_port(8000, 8100) +class paths: + django1 = test_data / "django1" + app_py = django1 / "app.py" + hello_html = django1 / "templates" / "hello.html" + bad_html = django1 / "templates" / "bad.html" -django = net.WebServer(DJANGO_PORT) -app_py_lines = code.get_marked_line_numbers(DJANGO1_MANAGE) + +class lines: + app_py = code.get_marked_line_numbers(paths.app_py) @pytest.mark.parametrize("bp_target", ["code", "template"]) @pytest.mark.parametrize("start_method", ["launch", "attach_socket_cmdline"]) -@pytest.mark.timeout(60) def test_django_breakpoint_no_multiproc(start_method, bp_target): bp_file, bp_line, bp_name = { - "code": (DJANGO1_MANAGE, app_py_lines["bphome"], "home"), - "template": (DJANGO1_TEMPLATE, 8, "Django Template"), + "code": (paths.app_py, lines.app_py["bphome"], "home"), + "template": (paths.hello_html, 8, "Django Template"), }[bp_target] + bp_var_content = compat.force_str("Django-Django-Test") with debug.Session() as session: session.initialize( start_method=start_method, - target=("file", DJANGO1_MANAGE), - program_args=["runserver", "--noreload", "--", str(DJANGO_PORT)], + target=("file", paths.app_py), + program_args=["runserver", "--noreload", "--", str(django.port)], debug_options=["Django"], - cwd=DJANGO1_ROOT, + cwd=paths.django1, expected_returncode=some.int, # No clean way to kill Django server ) - bp_var_content = "Django-Django-Test" session.set_breakpoints(bp_file, [bp_line]) session.start_debugging() + with django: - home_request = django.get("home") - stop = session.wait_for_stop( + home_request = django.get("/home") + session.wait_for_stop( "breakpoint", [ { - "id": some.dap_id, + "id": some.dap.id, "name": bp_name, - "source": { - "sourceReference": some.str, - "path": some.path(bp_file), - }, + "source": some.dap.source(bp_file), "line": bp_line, "column": 1, } ], ) - scopes = session.request("scopes", arguments={"frameId": stop.frame_id}) - assert len(scopes) > 0 - - variables = session.request( - "variables", - arguments={"variablesReference": scopes[0]["variablesReference"]}, - ) - variables = [v for v in variables["variables"] if v["name"] == "content"] - assert variables == [ + var_content = session.get_variable("content") + assert var_content == some.dict.containing( { "name": "content", "type": "str", - "value": repr(bp_var_content), + "value": compat.unicode_repr(bp_var_content), "presentationHint": {"attributes": ["rawString"]}, "evaluateName": "content", "variablesReference": 0, } - ] + ) session.request_continue() assert bp_var_content in home_request.response_text() @@ -87,64 +82,45 @@ def test_django_breakpoint_no_multiproc(start_method, bp_target): @pytest.mark.parametrize("start_method", ["launch", "attach_socket_cmdline"]) -@pytest.mark.timeout(60) def test_django_template_exception_no_multiproc(start_method): with debug.Session() as session: session.initialize( start_method=start_method, - target=("file", DJANGO1_MANAGE), - program_args=["runserver", "--noreload", "--nothreading", str(DJANGO_PORT)], + target=("file", paths.app_py), + program_args=["runserver", "--noreload", "--nothreading", str(django.port)], debug_options=["Django"], - cwd=DJANGO1_ROOT, + cwd=paths.django1, expected_returncode=some.int, # No clean way to kill Django server ) - - session.send_request( - "setExceptionBreakpoints", arguments={"filters": ["raised", "uncaught"]} - ).wait_for_response() - + session.request("setExceptionBreakpoints", {"filters": ["raised", "uncaught"]}) session.start_debugging() - with django: - web_request = django.get("badtemplate") - hit = session.wait_for_stop(reason="exception") - assert hit.frames[0] == some.dict.containing( - { - "id": some.dap_id, - "name": "Django TemplateSyntaxError", - "source": some.dict.containing( - { - "sourceReference": some.dap_id, - "path": some.path(DJANGO1_BAD_TEMPLATE), - } - ), - "line": 8, - "column": 1, - } + with django: + django.get("/badtemplate", log_errors=False) + stop = session.wait_for_stop( + "exception", + expected_frames=[ + some.dap.frame( + some.dap.source(paths.bad_html), + line=8, + name="Django TemplateSyntaxError", + ) + ], ) # Will stop once in the plugin - resp_exception_info = session.send_request( - "exceptionInfo", arguments={"threadId": hit.thread_id} - ).wait_for_response() - exception = resp_exception_info.body - assert exception == some.dict.containing( + exception_info = session.request( + "exceptionInfo", {"threadId": stop.thread_id} + ) + assert exception_info == some.dict.containing( { - "exceptionId": some.str.such_that( - lambda s: s.endswith("TemplateSyntaxError") - ), + "exceptionId": some.str.ending_with("TemplateSyntaxError"), "breakMode": "always", - "description": some.str.such_that( - lambda s: s.find("doesnotexist") > -1 - ), - "details": some.dict_with( + "description": some.str.containing("doesnotexist"), + "details": some.dict.containing( { - "message": some.str.such_that( - lambda s: s.endswith("doesnotexist") > -1 - ), - "typeName": some.str.such_that( - lambda s: s.endswith("TemplateSyntaxError") - ), + "message": some.str.containing("doesnotexist"), + "typeName": some.str.ending_with("TemplateSyntaxError"), } ), } @@ -153,194 +129,121 @@ def test_django_template_exception_no_multiproc(start_method): session.request_continue() # And a second time when the exception reaches the user code. - hit = session.wait_for_stop(reason="exception") + session.wait_for_stop("exception") session.request_continue() - # ignore response for exception tests - web_request.wait_for_response() - session.wait_for_exit() -@pytest.mark.parametrize("ex_type", ["handled", "unhandled"]) +@pytest.mark.parametrize("exc_type", ["handled", "unhandled"]) @pytest.mark.parametrize("start_method", ["launch", "attach_socket_cmdline"]) -@pytest.mark.timeout(60) -def test_django_exception_no_multiproc(ex_type, start_method): - ex_line = {"handled": 50, "unhandled": 64}[ex_type] +def test_django_exception_no_multiproc(exc_type, start_method): + exc_line = lines.app_py["exc_" + exc_type] with debug.Session() as session: session.initialize( start_method=start_method, - target=("file", DJANGO1_MANAGE), - program_args=["runserver", "--noreload", "--nothreading", str(DJANGO_PORT)], + target=("file", paths.app_py), + program_args=["runserver", "--noreload", "--nothreading", str(django.port)], debug_options=["Django"], - cwd=DJANGO1_ROOT, + cwd=paths.django1, expected_returncode=some.int, # No clean way to kill Django server ) - - session.send_request( - "setExceptionBreakpoints", arguments={"filters": ["raised", "uncaught"]} - ).wait_for_response() - + session.request("setExceptionBreakpoints", {"filters": ["raised", "uncaught"]}) session.start_debugging() + with django: - web_request = django.get(ex_type) + django.get("/" + exc_type) + stopped = session.wait_for_stop( + "exception", + expected_frames=[ + some.dap.frame( + some.dap.source(paths.app_py), + line=exc_line, + name="bad_route_" + exc_type, + ) + ], + ).body - thread_stopped = session.wait_for_next( - Event("stopped", some.dict.containing({"reason": "exception"})) + assert stopped == some.dict.containing( + { + "reason": "exception", + "text": some.str.ending_with("ArithmeticError"), + "description": "Hello", + } ) - assert thread_stopped == Event( - "stopped", - some.dict.containing( - { - "reason": "exception", - "text": some.str.such_that( - lambda s: s.endswith("ArithmeticError") - ), - "description": "Hello", - } - ), + + exception_info = session.request( + "exceptionInfo", {"threadId": stopped["threadId"]} ) - tid = thread_stopped.body["threadId"] - resp_exception_info = session.send_request( - "exceptionInfo", arguments={"threadId": tid} - ).wait_for_response() - exception = resp_exception_info.body - assert exception == { - "exceptionId": some.str.such_that( - lambda s: s.endswith("ArithmeticError") - ), + assert exception_info == { + "exceptionId": some.str.ending_with("ArithmeticError"), "breakMode": "always", "description": "Hello", "details": { "message": "Hello", - "typeName": some.str.such_that( - lambda s: s.endswith("ArithmeticError") - ), - "source": some.path(DJANGO1_MANAGE), - "stackTrace": some.str.such_that(lambda s: True), + "typeName": some.str.ending_with("ArithmeticError"), + "source": some.path(paths.app_py), + "stackTrace": some.str, }, } - resp_stacktrace = session.send_request( - "stackTrace", arguments={"threadId": tid} - ).wait_for_response() - assert resp_stacktrace.body["totalFrames"] > 1 - frames = resp_stacktrace.body["stackFrames"] - assert frames[0] == { - "id": some.dap_id, - "name": "bad_route_" + ex_type, - "source": { - "sourceReference": some.dap_id, - "path": some.path(DJANGO1_MANAGE), - }, - "line": ex_line, - "column": 1, - } - session.request_continue() - # ignore response for exception tests - web_request.wait_for_response() - session.wait_for_exit() -@pytest.mark.skip() -@pytest.mark.timeout(120) @pytest.mark.parametrize("start_method", ["launch"]) def test_django_breakpoint_multiproc(start_method): + bp_line = lines.app_py["bphome"] + bp_var_content = compat.force_str("Django-Django-Test") + with debug.Session() as parent_session: parent_session.initialize( start_method=start_method, - target=("file", DJANGO1_MANAGE), + target=("file", paths.app_py), multiprocess=True, program_args=["runserver"], debug_options=["Django"], - cwd=DJANGO1_ROOT, - ignore_unobserved=[Event("stopped")], + cwd=paths.django1, expected_returncode=some.int, # No clean way to kill Django server ) - bp_line = app_py_lines["bphome"] - bp_var_content = "Django-Django-Test" - parent_session.set_breakpoints(DJANGO1_MANAGE, [bp_line]) + parent_session.set_breakpoints(paths.app_py, [bp_line]) parent_session.start_debugging() - with parent_session.connect_to_next_child_session() as child_session: - child_session.send_request( + with parent_session.attach_to_next_subprocess() as child_session: + child_session.request( "setBreakpoints", - arguments={ - "source": {"path": DJANGO1_MANAGE}, - "breakpoints": [{"line": bp_line}], - }, - ).wait_for_response() + {"source": {"path": paths.app_py}, "breakpoints": [{"line": bp_line}]}, + ) child_session.start_debugging() - # wait for Django server to start - while True: - child_session.proceed() - o = child_session.wait_for_next(Event("output")) - if find_http_url(o.body["output"]) is not None: - break - with django: - web_request = django.get("home") - - thread_stopped = child_session.wait_for_next( - Event("stopped", some.dict.containing({"reason": "breakpoint"})) + web_request = django.get("/home") + child_session.wait_for_stop( + "breakpoint", + expected_frames=[ + some.dap.frame( + some.dap.source(paths.app_py), line=bp_line, name="home" + ) + ], ) - assert thread_stopped.body["threadId"] is not None - - tid = thread_stopped.body["threadId"] - - resp_stacktrace = child_session.send_request( - "stackTrace", arguments={"threadId": tid} - ).wait_for_response() - assert resp_stacktrace.body["totalFrames"] > 0 - frames = resp_stacktrace.body["stackFrames"] - assert frames[0] == { - "id": some.dap_id, - "name": "home", - "source": { - "sourceReference": some.dap_id, - "path": some.path(DJANGO1_MANAGE), - }, - "line": bp_line, - "column": 1, - } - fid = frames[0]["id"] - resp_scopes = child_session.send_request( - "scopes", arguments={"frameId": fid} - ).wait_for_response() - scopes = resp_scopes.body["scopes"] - assert len(scopes) > 0 - - resp_variables = child_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"] == "content" - ) - assert variables == [ + var_content = child_session.get_variable("content") + assert var_content == some.dict.containing( { "name": "content", "type": "str", - "value": repr(bp_var_content), + "value": compat.unicode_repr(bp_var_content), "presentationHint": {"attributes": ["rawString"]}, "evaluateName": "content", } - ] + ) child_session.request_continue() - - web_content = web_request.wait_for_response() - assert web_content.find(bp_var_content) != -1 + assert bp_var_content in web_request.response_text() child_session.wait_for_termination() parent_session.wait_for_exit() diff --git a/tests/ptvsd/server/test_evaluate.py b/tests/ptvsd/server/test_evaluate.py index 6e3cea064..ab9753f16 100644 --- a/tests/ptvsd/server/test_evaluate.py +++ b/tests/ptvsd/server/test_evaluate.py @@ -54,14 +54,14 @@ def code_to_debug(): assert b_variables[0] == { "type": "int", "value": "1", - "name": some.str.matching(r".*one.*"), + "name": some.str.containing("one"), "evaluateName": "b['one']", "variablesReference": 0, } assert b_variables[1] == { "type": "int", "value": "2", - "name": some.str.matching(r".*two.*"), + "name": some.str.containing("two"), "evaluateName": "b['two']", "variablesReference": 0, } @@ -427,7 +427,7 @@ def code_to_debug(): "value": "[0x1, 0xa, 0x64]", "type": "list", "evaluateName": "b", - "variablesReference": some.dap_id, + "variablesReference": some.dap.id, } ) @@ -477,7 +477,7 @@ def code_to_debug(): "value": "{0xa: 0xa, 0x64: 0x64, 0x3e8: 0x3e8}", "type": "dict", "evaluateName": "c", - "variablesReference": some.dap_id, + "variablesReference": some.dap.id, } ) @@ -527,7 +527,7 @@ def code_to_debug(): "value": "{(0x1, 0xa, 0x64): (0x2710, 0x186a0, 0x186a0)}", "type": "dict", "evaluateName": "d", - "variablesReference": some.dap_id, + "variablesReference": some.dap.id, } ) resp_variables = session.send_request( @@ -544,7 +544,7 @@ def code_to_debug(): "value": "(0x2710, 0x186a0, 0x186a0)", "type": "tuple", "evaluateName": "d[(1, 10, 100)]", - "variablesReference": some.dap_id, + "variablesReference": some.dap.id, }, { "name": "__len__", diff --git a/tests/ptvsd/server/test_exception.py b/tests/ptvsd/server/test_exception.py index be55fa8dd..6f1d6cda6 100644 --- a/tests/ptvsd/server/test_exception.py +++ b/tests/ptvsd/server/test_exception.py @@ -12,7 +12,7 @@ pytestmark = pytest.mark.skip("Exception tests are broken") -str_matching_ArithmeticError = some.str.matching(r"($|.*\.)ArithmeticError") +str_matching_ArithmeticError = some.str.matching(r".+?\.ArithmeticError") @pytest.mark.parametrize("raised", ["raisedOn", "raisedOff"]) diff --git a/tests/ptvsd/server/test_flask.py b/tests/ptvsd/server/test_flask.py index 3f19ee158..3c5dbc1fc 100644 --- a/tests/ptvsd/server/test_flask.py +++ b/tests/ptvsd/server/test_flask.py @@ -69,9 +69,9 @@ def test_flask_breakpoint_no_multiproc(bp_target, start_method): hit = session.wait_for_stop(reason="breakpoint") assert hit.frames[0] == { - "id": some.dap_id, + "id": some.dap.id, "name": bp_name, - "source": {"sourceReference": some.dap_id, "path": some.path(bp_file)}, + "source": {"sourceReference": some.dap.id, "path": some.path(bp_file)}, "line": bp_line, "column": 1, } @@ -118,18 +118,18 @@ def test_flask_template_exception_no_multiproc(start_method): # wait for Flask web server to start with flask_server: - web_request = flask_server.get("badtemplate") + web_request = flask_server.get("/badtemplate") hit = session.wait_for_stop() assert hit.frames[0] == some.dict.containing( { - "id": some.dap_id, + "id": some.dap.id, "name": "template" if sys.version_info[0] >= 3 else "Jinja2 TemplateSyntaxError", "source": some.dict.containing( { - "sourceReference": some.dap_id, + "sourceReference": some.dap.id, "path": some.path(FLASK1_BAD_TEMPLATE), } ), @@ -188,7 +188,7 @@ def test_flask_exception_no_multiproc(ex_type, start_method): session.start_debugging() with flask_server: - web_request = flask_server.get(ex_type) + web_request = flask_server.get("/" + ex_type) thread_stopped = session.wait_for_next( Event("stopped", some.dict.containing({"reason": "exception"})) @@ -227,9 +227,9 @@ def test_flask_exception_no_multiproc(ex_type, start_method): assert resp_stacktrace.body["totalFrames"] > 0 frames = resp_stacktrace.body["stackFrames"] assert frames[0] == { - "id": some.dap_id, + "id": some.dap.id, "name": "bad_route_" + ex_type, - "source": {"sourceReference": some.dap_id, "path": some.path(FLASK1_APP)}, + "source": {"sourceReference": some.dap.id, "path": some.path(FLASK1_APP)}, "line": ex_line, "column": 1, } @@ -271,7 +271,7 @@ def test_flask_breakpoint_multiproc(start_method): parent_session.set_breakpoints(FLASK1_APP, [bp_line]) parent_session.start_debugging() - with parent_session.connect_to_next_child_session() as child_session: + with parent_session.attach_to_next_subprocess() as child_session: child_session.send_request( "setBreakpoints", arguments={ @@ -286,10 +286,10 @@ def test_flask_breakpoint_multiproc(start_method): hit = child_session.wait_for_stop(reason="breakpoint") assert hit.frames[0] == { - "id": some.dap_id, + "id": some.dap.id, "name": "home", "source": { - "sourceReference": some.dap_id, + "sourceReference": some.dap.id, "path": some.path(FLASK1_APP), }, "line": bp_line, diff --git a/tests/ptvsd/server/test_multiproc.py b/tests/ptvsd/server/test_multiproc.py index b9a4d7ed0..69d3a89e0 100644 --- a/tests/ptvsd/server/test_multiproc.py +++ b/tests/ptvsd/server/test_multiproc.py @@ -105,7 +105,7 @@ def child(q): ) parent_session.proceed() - with parent_session.connect_to_child_session(child_subprocess) as child_session: + with parent_session.attach_to_subprocess(child_subprocess) as child_session: child_session.start_debugging() grandchild_subprocess = parent_session.wait_for_next( @@ -128,7 +128,7 @@ def child(q): ) parent_session.proceed() - with parent_session.connect_to_child_session( + with parent_session.attach_to_subprocess( grandchild_subprocess ) as grandchild_session: grandchild_session.start_debugging() @@ -208,7 +208,7 @@ def parent(): ) parent_session.proceed() - with parent_session.connect_to_child_session(child_subprocess) as child_session: + with parent_session.attach_to_subprocess(child_subprocess) as child_session: child_session.start_debugging() child_argv = parent_backchannel.receive() @@ -260,7 +260,7 @@ def parent(): ) parent_session.start_debugging() - with parent_session.connect_to_next_child_session() as child_session: + with parent_session.attach_to_next_subprocess() as child_session: child_session.start_debugging() if parent_session.start_method == "launch": diff --git a/tests/ptvsd/server/test_run.py b/tests/ptvsd/server/test_run.py index 2161b041d..ec6ea3c76 100644 --- a/tests/ptvsd/server/test_run.py +++ b/tests/ptvsd/server/test_run.py @@ -37,7 +37,7 @@ def code_to_debug(): expected_name = ( "-c" if run_as == "code" - else some.str.matching(re.escape(code_to_debug.strpath) + r"(c|o)?$") + else some.str.matching(re.escape(code_to_debug.strpath) + r"(c|o)?") ) assert process_event == Event( "process", some.dict.containing({"name": expected_name}) @@ -47,7 +47,7 @@ def code_to_debug(): expected_ptvsd_path = path.abspath(ptvsd.__file__) backchannel.expect(some.str.matching( - re.escape(expected_ptvsd_path) + r"(c|o)?$" + re.escape(expected_ptvsd_path) + r"(c|o)?" )) session.wait_for_exit() diff --git a/tests/test_data/django1/app.py b/tests/test_data/django1/app.py index 17603823f..dbc2d6d15 100644 --- a/tests/test_data/django1/app.py +++ b/tests/test_data/django1/app.py @@ -47,7 +47,7 @@ def home(request): def bad_route_handled(request): try: - raise ArithmeticError('Hello') + raise ArithmeticError('Hello') # @exc_handled except Exception: pass title = 'hello' @@ -61,7 +61,7 @@ def bad_route_handled(request): def bad_route_unhandled(request): - raise ArithmeticError('Hello') + raise ArithmeticError('Hello') # @exc_unhandled title = 'hello' content = 'Django-Django-Test' template = loader.get_template('hello.html') diff --git a/tests/tests/test_patterns.py b/tests/tests/test_patterns.py index a8732e364..84450c35e 100644 --- a/tests/tests/test_patterns.py +++ b/tests/tests/test_patterns.py @@ -131,6 +131,54 @@ def test_matching(): assert pattern != b"abbbc" +def test_starting_with(): + pattern = some.str.starting_with("aa") + log_repr(pattern) + assert pattern == "aabbbb" + assert pattern != "bbbbaa" + assert pattern != "bbaabb" + assert pattern != "ababab" + + pattern = some.bytes.starting_with(b"aa") + log_repr(pattern) + assert pattern == b"aabbbb" + assert pattern != b"bbbbaa" + assert pattern != b"bbaabb" + assert pattern != b"ababab" + + +def test_ending_with(): + pattern = some.str.ending_with("aa") + log_repr(pattern) + assert pattern == "bbbbaa" + assert pattern != "aabbbb" + assert pattern != "bbaabb" + assert pattern != "ababab" + + pattern = some.bytes.ending_with(b"aa") + log_repr(pattern) + assert pattern == b"bbbbaa" + assert pattern != b"aabbbb" + assert pattern != b"bbaabb" + assert pattern != b"ababab" + + +def test_containing(): + pattern = some.str.containing("aa") + log_repr(pattern) + assert pattern == "aabbbb" + assert pattern == "bbbbaa" + assert pattern == "bbaabb" + assert pattern != "ababab" + + pattern = some.bytes.containing(b"aa") + log_repr(pattern) + assert pattern == b"aabbbb" + assert pattern == b"bbbbaa" + assert pattern == b"bbaabb" + assert pattern != b"ababab" + + def test_list(): assert [1, 2, 3] == [1, some.thing, 3] assert [1, 2, 3, 4] != [1, some.thing, 4]