def test_init_launch_hostapp(setup_teardown, hostapp_args): # noqa: F811 with TSession(hostapp_args) as ts: print("Session is run") ts.send_request(REQUEST_INIT) ts.send_request(REQUEST_LAUNCH) # if we have this event, GDB successfully loaded and started the application: expectation = timeline.Event("initialized") result = ts.wait_for(expectation, timeout_s=5) assert result print("Session is stopped")
def _start_channel(self, stream): handlers = messaging.MessageHandlers( request=self._process_request, event=self._process_event, disconnect=self._process_disconnect, ) self.channel = messaging.JsonMessageChannel(stream, handlers) self.channel.start() self.wait_for_next( timeline.Event( "output", { "category": "telemetry", "output": "ptvsd", "data": { "packageVersion": some.str }, }, ) & timeline.Event( "output", { "category": "telemetry", "output": "debugpy", "data": { "packageVersion": some.str }, }, )) self.request( "initialize", { "pathFormat": "path", "clientID": self.client_id, "adapterID": "test", "linesStartAt1": True, "columnsStartAt1": True, "supportsVariableType": True, "supportsRunInTerminalRequest": True, }, )
def attach_listen(session, target, method, cwd=None, log_dir=None): log.info("Attaching {0} to {1} by socket using {2}.", session, target, method.upper()) assert method in ("api", "cli") config = _attach_common_config(session, target, cwd) config["listen"] = {} config["listen"]["host"] = host = attach_listen.host config["listen"]["port"] = port = attach_listen.port if method == "cli": args = [ os.path.dirname(debugpy.__file__), "--connect", compat.filename_str(host) + ":" + str(port), ] if log_dir is not None: args += ["--log-to", log_dir] if "subProcess" in config: args += ["--configure-subProcess", str(config["subProcess"])] debuggee_setup = None elif method == "api": args = [] api_config = {k: v for k, v in config.items() if k in {"subProcess"}} debuggee_setup = """ import debugpy if {log_dir!r}: debugpy.log_to({log_dir!r}) debugpy.configure({api_config!r}) debugpy.connect({address!r}) """ debuggee_setup = fmt(debuggee_setup, address=(host, port), log_dir=log_dir, api_config=api_config) else: raise ValueError args += target.cli(session.spawn_debuggee.env) try: del config["subProcess"] except KeyError: pass def spawn_debuggee(occ): assert occ.body == some.dict.containing({"host": host, "port": port}) session.spawn_debuggee(args, cwd=cwd, setup=debuggee_setup) session.timeline.when(timeline.Event("debugpyWaitingForServer"), spawn_debuggee) session.spawn_adapter( args=[] if log_dir is None else ["--log-dir", log_dir]) return session.request_attach()
def disconnect(self, force=False): if self.channel is None: return try: if not force: self.request("disconnect") self.timeline.wait_until_realized(timeline.Event("terminated")) except messaging.JsonIOError: pass finally: try: self.channel.close() except Exception: pass self.channel.wait() self.channel = None
def wait_for_exit(self): if self.debuggee is not None: try: self.debuggee.wait() except Exception: pass finally: watchdog.unregister_spawn(self.debuggee.pid, self.debuggee_id) self.timeline.wait_until_realized(timeline.Event("terminated")) # FIXME: "exited" event is not properly reported in attach scenarios at the # moment, so the exit code is only checked if it's present. if self.start_request.command == "launch": assert self.exit_code is not None if self.debuggee is not None and self.exit_code is not None: assert self.debuggee.returncode == self.exit_code return self.exit_code
def _request_start(self, method): self.config.normalize() start_request = self.send_request(method, self.config) # 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( timeline.Event("initialized") | timeline.Response(start_request), freeze=True, ) 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 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. return self._ConfigurationContextManager(self)
def wait_for_terminated(self): self.timeline.wait_until_realized(timeline.Event("terminated"))
def __init__(self, debug_config=None): assert Session.tmpdir is not None watchdog.start() self.id = next(Session._counter) log.info("Starting {0}", self) self.client_id = "vscode" self.debuggee = None """psutil.Popen instance for the debuggee process.""" self.adapter = None """psutil.Popen instance for the adapter process.""" self.adapter_endpoints = None """Name of the file that contains the adapter endpoints information. This file is generated by the adapter when it opens the listener sockets, and deleted by it when it exits. """ self.channel = None """JsonMessageChannel to the adapter.""" self.captured_output = {"stdout", "stderr"} """Before the debuggee is spawned, this is the set of stdio streams that should be captured once it is spawned. After it is spawned, this is a CapturedOutput object capturing those streams. """ self.backchannel = None """The BackChannel object to talk to the debuggee. Must be explicitly created with open_backchannel(). """ self.scratchpad = comms.ScratchPad(self) """The ScratchPad object to talk to the debuggee.""" self.start_request = None """The "launch" or "attach" request that started executing code in this session. """ self.expected_exit_code = 0 """The expected exit code for the debuggee process. If None, the debuggee is not expected to exit when the Session is closed. If not None, this is validated against both exit_code and debuggee.returncode. """ self.exit_code = None """The actual exit code for the debuggee process, as received from DAP. """ self.config = config.DebugConfig( debug_config if debug_config is not None else { "justMyCode": True, "name": "Test", "type": "python" }) """The debug configuration for this session.""" self.before_request = lambda command, arguments: None """Invoked for every outgoing request in this session, allowing any final tweaks to the request before it is sent. """ self.log_dir = (None if log.log_dir is None else py.path.local(log.log_dir) / str(self)) """The log directory for this session. Passed via DEBUGPY_LOG_DIR to all spawned child processes. If set to None, DEBUGPY_LOG_DIR is not automatically added, but tests can still provide it manually. """ self.tmpdir = Session.tmpdir / str(self) self.tmpdir.ensure(dir=True) self.timeline = timeline.Timeline(str(self)) self.ignore_unobserved.extend([ timeline.Event("module"), timeline.Event("continued"), timeline.Event("debugpyWaitingForServer"), timeline.Event("thread", some.dict.containing({"reason": "started"})), timeline.Event("thread", some.dict.containing({"reason": "exited"})), timeline.Event("output", some.dict.containing({"category": "stdout"})), timeline.Event("output", some.dict.containing({"category": "stderr"})), timeline.Event("output", some.dict.containing({"category": "console"})), ]) # 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. # Conversely, most tests should restrict themselves to this subset of the API, # and avoid calling members of timeline directly unless there is a good reason. self.new = self.timeline.new self.observe = self.timeline.observe self.wait_for_next = self.timeline.wait_for_next self.proceed = self.timeline.proceed self.expect_new = self.timeline.expect_new self.expect_realized = self.timeline.expect_realized self.all_occurrences_of = self.timeline.all_occurrences_of self.observe_all = self.timeline.observe_all spawn_adapter = self.spawn_adapter self.spawn_adapter = lambda *args, **kwargs: spawn_adapter( *args, **kwargs) self.spawn_adapter.env = util.Env() spawn_debuggee = self.spawn_debuggee self.spawn_debuggee = lambda *args, **kwargs: spawn_debuggee( *args, **kwargs) self.spawn_debuggee.env = util.Env()
def wait_for_next_event(self, event, body=some.object, freeze=True): return self.timeline.wait_for_next(timeline.Event(event, body), freeze=freeze).body
def all_events(self, event, body=some.object): return [ occ.body for occ in self.timeline.all_occurrences_of( timeline.Event(event, body)) ]
def test_evaluate_thread_locks(pyfile, target, run): @pyfile def code_to_debug(): """ The idea here is that a secondary thread does the processing of instructions, so, when all threads are stopped, doing an evaluation for: processor.process('xxx') would be locked until secondary threads start running. See: https://github.com/microsoft/debugpy/issues/157 """ import debuggee import threading from debugpy.common.compat import queue debuggee.setup() class EchoThread(threading.Thread): def __init__(self, queue): threading.Thread.__init__(self) self._queue = queue def run(self): while True: obj = self._queue.get() if obj == "finish": break print("processed", obj.value) obj.event.set() class NotificationObject(object): def __init__(self, value): self.value = value self.event = threading.Event() class Processor(object): def __init__(self, queue): self._queue = queue def process(self, i): obj = NotificationObject(i) self._queue.put(obj) assert obj.event.wait() def finish(self): self._queue.put("finish") if __name__ == "__main__": q = queue.Queue() echo_thread = EchoThread(q) processor = Processor(q) echo_thread.start() processor.process(1) processor.process(2) # @bp processor.process(3) processor.finish() with debug.Session() as session: # During the evaluation we'll actually have continued/stopped events because # we're letting threads run at that time. Let's ignore these in the test. session.ignore_unobserved.extend([timeline.Event("stopped")]) session.config.env.update({"PYDEVD_UNBLOCK_THREADS_TIMEOUT": "0.5"}) with run(session, target(code_to_debug)): session.set_breakpoints(code_to_debug, all) stop = session.wait_for_stop() evaluate = session.request( "evaluate", { "expression": "processor.process('foo')", "frameId": stop.frame_id }, ) assert evaluate == some.dict.containing({"result": "None"}) session.request_continue()