From cd5ebd1274556fa701def8520754cee1e01f8a1b Mon Sep 17 00:00:00 2001 From: Pavel Minaev Date: Fri, 27 Sep 2019 12:26:14 -0700 Subject: [PATCH] Add safety checks to debug test framework to prevent accidental misuse. --- tests/debug/config.py | 14 ++++++++-- tests/debug/session.py | 59 ++++++++++++++++++++++++++---------------- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/tests/debug/config.py b/tests/debug/config.py index cd694541..467b6e83 100644 --- a/tests/debug/config.py +++ b/tests/debug/config.py @@ -20,6 +20,8 @@ class DebugConfig(collections.MutableMapping): In addition, it exposes high-level wrappers over "env" and "debugOptions". """ + __slots__ = ["_dict", "_env", "_debug_options"] + # Valid configuration properties. Keys are names, and values are defaults that # are assumed by the adapter and/or the server if the property is not specified. # If the property is required, or if the default is computed in such a way that @@ -69,8 +71,8 @@ class DebugConfig(collections.MutableMapping): def __init__(self, *args, **kwargs): self._dict = dict(*args, **kwargs) - self.env = self.Env(self) - self.debug_options = self.DebugOptions(self) + self._env = self.Env(self) + self._debug_options = self.DebugOptions(self) def __iter__(self): return iter(self._dict) @@ -157,6 +159,14 @@ class DebugConfig(collections.MutableMapping): if self["waitOnAbnormalExit"]: self.debug_options.add("WaitOnAbnormalExit") + @property + def env(self): + return self._env + + @property + def debug_options(self): + return self._debug_options + class Env(collections.MutableMapping): """Wraps config["env"], automatically creating and destroying it as needed. """ diff --git a/tests/debug/session.py b/tests/debug/session.py index 8137f60f..fb36e98a 100644 --- a/tests/debug/session.py +++ b/tests/debug/session.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function, unicode_literals import collections -import contextlib import itertools import os import psutil @@ -510,17 +509,6 @@ class Session(object): self.config.normalize() start_request = self.send_request(method, self.config) - def wait_for_process_event(): - process = self.wait_for_next_event("process", freeze=False) - assert process == some.dict.containing( - { - "startMethod": self.start_request.command, - "name": some.str, - "isLocalProcess": True, - "systemProcessId": some.int, - } - ) - # Depending on whether it's "noDebug" or not, we either get the "initialized" # event, or an immediate response to our request. self.timeline.wait_until_realized( @@ -531,21 +519,35 @@ class Session(object): if start_request.response is not None: # It was an immediate response - configuration is not possible. Just get # the "process" event, and return to caller. - return wait_for_process_event() + return self.wait_for_process() # We got "initialized" - now we need to yield to the caller, so that it can - # configure the session before it starts running, and then give control back - # to us to finalize the configuration sequence. A nested context manager is - # used to ensure that all code up to this point executes eagerly. + # configure the session before it starts running. + return self._ConfigurationContextManager(self) - @contextlib.contextmanager - def configure(): - yield - self.request("configurationDone") - start_request.wait_for_response() - wait_for_process_event() + class _ConfigurationContextManager(object): + """Handles the start configuration sequence from "initialized" event until + start_request receives a response. + """ - return configure() + def __init__(self, session): + self.session = session + self._entered = False + + def __enter__(self): + self._entered = True + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.session.request("configurationDone") + self.session.start_request.wait_for_response() + self.session.wait_for_process() + + def __del__(self): + assert self._entered, ( + "The return value of request_launch() or request_attach() must be " + "used in a with-statement." + ) def request_launch(self): if "PYTHONPATH" in self.config.env: @@ -658,6 +660,17 @@ class Session(object): timeline.Event(event, body), freeze=freeze ).body + def wait_for_process(self): + process = self.wait_for_next_event("process", freeze=False) + assert process == some.dict.containing( + { + "startMethod": self.start_request.command, + "name": some.str, + "isLocalProcess": True, + "systemProcessId": some.int, + } + ) + def wait_for_stop( self, reason=some.str,