Skip to content

Commit

Permalink
Make it possible to stop on logged failures/errors. Fixes #284
Browse files Browse the repository at this point in the history
  • Loading branch information
fabioz committed Jan 26, 2022
1 parent fb2d9bc commit 5871be1
Show file tree
Hide file tree
Showing 11 changed files with 359 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class ReasonEnum(enum.Enum):
REASON_BREAKPOINT = "breakpoint"
REASON_STEP = "step"
REASON_PAUSE = "pause"
REASON_EXCEPTION = "exception"


class StepEnum(enum.Enum):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ def on_initialize_request(self, request: InitializeRequest):
capabilities.supportsConditionalBreakpoints = True
capabilities.supportsHitConditionalBreakpoints = True
capabilities.supportsLogPoints = True
capabilities.exceptionBreakpointFilters = [
{"filter": "logFailure", "label": "On log failures", "default": True},
{"filter": "logError", "label": "On log errors", "default": True},
]
# capabilities.supportsSetVariable = True
self.write_to_client_message(initialize_response)

Expand Down Expand Up @@ -254,7 +258,7 @@ def on_setExceptionBreakpoints_request(
self, request: SetExceptionBreakpointsRequest
):
if self._run_in_debug_mode and self._launch_process is not None:
self._launch_process.resend_request_to_pydevd(request)
self._launch_process.resend_request_to_robot(request)
else:
response = base_schema.build_response(request)
self.write_to_client_message(response)
Expand Down
135 changes: 114 additions & 21 deletions robotframework-ls/src/robotframework_debug_adapter/debugger_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
IBusyWait,
IEvaluationInfo,
)
from typing import Optional, List, Iterable, Union, Any, Dict, FrozenSet
from typing import Optional, List, Iterable, Union, Any, Dict, FrozenSet, Tuple
from robocorp_ls_core.basic import implements
from robocorp_ls_core.debug_adapter_core.dap.dap_schema import (
StackFrame,
Expand Down Expand Up @@ -387,6 +387,7 @@ def get_variables(self, variables_reference):
_StepEntry = namedtuple("_StepEntry", "name, lineno, source, args, variables")
_SuiteEntry = namedtuple("_SuiteEntry", "name, source")
_TestEntry = namedtuple("_TestEntry", "name, source, lineno")
_LogEntry = namedtuple("_LogEntry", "name, source, lineno")


class InvalidFrameIdError(Exception):
Expand Down Expand Up @@ -542,6 +543,12 @@ def reset(self):
self._evaluations = []
self._skip_breakpoints = 0

self._exc_name = None
self._exc_description = None

self.break_on_log_failure = False
self.break_on_log_error = False

def write_message(self, msg):
log.critical(
"Message: %s not sent!\nExpected _RobotDebuggerImpl.write_message to be replaced to the actual implementation!",
Expand All @@ -553,6 +560,14 @@ def write_message(self, msg):
def stop_reason(self) -> ReasonEnum:
return self._reason

@property
def exc_name(self) -> Optional[str]:
return self._exc_name

@property
def exc_description(self) -> Optional[str]:
return self._exc_description

def _get_stack_info_from_frame_id(self, frame_id) -> Optional[_StackInfo]:
thread_id = self._frame_id_to_tid.get(frame_id)
if thread_id is not None:
Expand Down Expand Up @@ -630,6 +645,14 @@ def _create_stack_info(self, thread_id: int):
name = "TestCase: %s" % (entry.name,)
filename = self._get_filename(entry, "TestCase")

frame_id = stack_info.add_test_entry_stack(
name, filename, entry.lineno
)

elif entry.__class__ == _LogEntry:
name = "Log (%s)" % (entry.name,)
filename = self._get_filename(entry, "Log")

frame_id = stack_info.add_test_entry_stack(
name, filename, entry.lineno
)
Expand Down Expand Up @@ -657,7 +680,15 @@ def get_current_thread_id(self, thread=None):
def wait_suspended(self, reason: ReasonEnum) -> None:
thread_id = self.get_current_thread_id()

log.info("wait_suspended", reason)
if self._exc_name or self._exc_description:
log.info(
"wait_suspended. Reason: %s. Exc name: %s. Exc description: %s",
reason,
self._exc_name,
self._exc_description,
)
else:
log.info("wait_suspended. Reason: %s", reason)
self._create_stack_info(thread_id)
try:
self._run_state = STATE_PAUSED
Expand Down Expand Up @@ -984,29 +1015,90 @@ def end_test(self, data, result):
self._stack_ctx_entries_deque.pop()

def log_message(self, message):
from robotframework_debug_adapter.message_utils import (
extract_source_and_line_from_message,
)

# When debugging show any message in the console (if possible with the
# current keyword as the source).
source = None
lineno = None
if self._stack_ctx_entries_deque:
lineno = 0
step_entry: _StepEntry = self._stack_ctx_entries_deque[-1]
source = step_entry.source
source = Source(path=source)
try:
lineno = step_entry.lineno
except AttributeError:
pass
self.write_message(
OutputEvent(
body=OutputEventBody(
source=source,
line=lineno,
output=f"{message.message}\n",
category="console",
try:
source = None
lineno = None

source_and_line = extract_source_and_line_from_message(message.message)

if source_and_line is not None:
source, lineno = source_and_line
source = Source(path=source)
else:
if self._stack_ctx_entries_deque:
lineno = 0
step_entry: _StepEntry = self._stack_ctx_entries_deque[-1]
source = step_entry.source
source = Source(path=source)
try:
lineno = step_entry.lineno
except AttributeError:
pass

self.write_message(
OutputEvent(
body=OutputEventBody(
source=source,
line=lineno,
output=f"{message.message}\n",
category="console",
)
)
)
)
self._break_on_log_or_system_message(message)
except:
log.exception("Error handling log_message.")

def message(self, message):
if message.level in ("FAIL", "ERROR"):
# We also want to show in the console, not just check breaks.
return self.log_message(message)
else:
try:
self._break_on_log_or_system_message(message)
except:
log.exception("Error handling (system) message.")

def _break_on_log_or_system_message(self, message):
stop_reason = None
if message.level == "FAIL":
if self.break_on_log_failure:
stop_reason = ReasonEnum.REASON_EXCEPTION
exc_name = "Suspended due to logged failure: "

elif message.level == "ERROR":
if self.break_on_log_error:
stop_reason = ReasonEnum.REASON_EXCEPTION
exc_name = "Suspended due to logged error: "

if stop_reason is not None:
from robotframework_debug_adapter.message_utils import (
extract_source_and_line_from_message,
)

source_and_line = extract_source_and_line_from_message(message.message)
entry = None
if source_and_line is not None:
source, lineno = source_and_line
entry = _LogEntry(message.level, source, lineno)
self._stack_ctx_entries_deque.append(entry)

self._exc_name = exc_name + message.message
self._exc_description = message.message
try:
self.wait_suspended(stop_reason)
finally:
self._exc_name = None
self._exc_description = None

if entry is not None:
self._stack_ctx_entries_deque.pop()


def _patch(
Expand Down Expand Up @@ -1111,6 +1203,7 @@ def install_robot_debugger() -> IRobotDebugger:
DebugListener.on_end_test.register(impl.end_test)

DebugListener.on_log_message.register(impl.log_message)
DebugListener.on_message.register(impl.message)

# On RobotFramework 3.x and earlier 4.x dev versions, we do some monkey-patching because
# the listener was not able to give linenumbers.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,23 +127,36 @@ def log_message(self, message: Dict[str, Any]) -> None:
if not message_string:
return

from robotframework_debug_adapter.message_utils import (
extract_source_and_line_from_message,
)

level = message.get("level")

source = None
lineno = None
test_name = None

source_and_line = extract_source_and_line_from_message(message_string)
if source_and_line is not None:
source, lineno = source_and_line

if self._source_info_stack:
from robotframework_debug_adapter import file_utils

lineno = 0
source_info: _SourceInfo = self._source_info_stack[-1]
source = source_info.source
test_name = source_info.test_name
source = file_utils.get_abs_path_real_path_and_base_from_file(source)[0]
try:
lineno = source_info.lineno
except AttributeError:
pass
test_name = source_info.test_name # May be None.

if source is None:
source = source_info.source
source = file_utils.get_abs_path_real_path_and_base_from_file(source)[0]

if lineno is None:
lineno = 0
try:
lineno = source_info.lineno
except AttributeError:
pass

from robocorp_ls_core.debug_adapter_core.dap.dap_schema import (
LogMessageEvent,
Expand All @@ -165,7 +178,10 @@ def log_message(self, message: Dict[str, Any]) -> None:
if message["level"] in ("FAIL", "ERROR"): # FAIL/WARN/INFO/DEBUG/TRACE
self._failure_messages.append(message["message"])

# message = log_message
def message(self, message):
if message["level"] in ("FAIL", "ERROR"):
# We also want to show these for system messages.
return self.log_message(message)

def start_keyword(self, name: str, attributes: Dict[str, Any]) -> None:
source = attributes.get("source")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class DebugListener(object):
on_start_test = _Callback()
on_end_test = _Callback()
on_log_message = _Callback()
on_message = _Callback()

def start_suite(self, data, result):
self.on_start_suite(data, result)
Expand All @@ -34,6 +35,9 @@ def end_test(self, data, result):
def log_message(self, message):
self.on_log_message(message)

def message(self, message):
self.on_message(message)

# def start_keyword(self, data, result):
# # This would be nice, but it's not currently supported.
#
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import Optional, Tuple


def extract_source_and_line_from_message(msg) -> Optional[Tuple[str, int]]:
"""
:param msg:
Something as:
Error in file 'C:/Users/.../case_import_failure.robot' on line 2
"""
if msg:
first_line = msg.split("\n")[0]
import re

m = re.search(r"file\s'(.*)'\son\sline\s(\d+)", first_line)
if m:
return m.group(1), int(m.group(2))
return None
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,16 @@ def _notify_stopped(self):

thread_id = self.get_current_thread_id()

exc_name = self._debugger_impl.exc_name
exc_desc = self._debugger_impl.exc_description

reason = self._debugger_impl.stop_reason
body = StoppedEventBody(
reason.value, allThreadsStopped=True, threadId=thread_id
reason.value,
allThreadsStopped=True,
threadId=thread_id,
text=exc_name,
description=exc_desc,
)
msg = StoppedEvent(body)
self.write_message(msg)
Expand Down Expand Up @@ -235,6 +242,10 @@ def on_initialize_request(self, request):
capabilities.supportsConditionalBreakpoints = True
capabilities.supportsHitConditionalBreakpoints = True
capabilities.supportsLogPoints = True
capabilities.exceptionBreakpointFilters = [
{"filter": "logFailure", "label": "On log failures", "default": True},
{"filter": "logError", "label": "On log errors", "default": True},
]
# capabilities.supportsSetVariable = True
self.write_message(initialize_response)
self.write_message(
Expand All @@ -250,6 +261,27 @@ def on_attach_request(self, request):
attach_response = build_response(request)
self.write_message(attach_response)

def on_setExceptionBreakpoints_request(self, request):
from robocorp_ls_core.debug_adapter_core.dap.dap_schema import (
SetExceptionBreakpointsArguments,
)
from robocorp_ls_core.debug_adapter_core.dap import dap_base_schema

arguments: SetExceptionBreakpointsArguments = request.arguments
filters = arguments.filters
break_on_log_failure = "logFailure" in filters
break_on_log_error = "logError" in filters

if self._debugger_impl:
self._debugger_impl.break_on_log_failure = break_on_log_failure
self._debugger_impl.break_on_log_error = break_on_log_error
else:
if break_on_log_failure or break_on_log_error:
get_log().info("Unable to break on failures/errors (no debug mode).")

# Note: no body needed.
self.write_message(dap_base_schema.build_response(request))

def on_setBreakpoints_request(self, request):
from robocorp_ls_core.debug_adapter_core.dap.dap_schema import SourceBreakpoint
from robocorp_ls_core.debug_adapter_core.dap.dap_schema import Breakpoint
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
*** Settings ***
Library Does not exist

*** Test Cases ***

Check
Log to console Something
Loading

0 comments on commit 5871be1

Please sign in to comment.