diff --git a/CHANGELOG.md b/CHANGELOG.md
index 943db1e..5812b1c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
# main (unreleased)
+- **[FEATURE]**: feat(capture): add support for capture tracepoints [#34](https://github.com/intergral/deep/pull/34) [@Umaaz](https://github.com/Umaaz)
+
# 1.1.0 (06/02/2024)
- **[CHANGE]**: change(build): add doc string check to flake8 [#14](https://github.com/intergral/deep/pull/14) [@Umaaz](https://github.com/Umaaz)
diff --git a/src/deep/api/tracepoint/eventsnapshot.py b/src/deep/api/tracepoint/eventsnapshot.py
index 58ed8aa..6ead750 100644
--- a/src/deep/api/tracepoint/eventsnapshot.py
+++ b/src/deep/api/tracepoint/eventsnapshot.py
@@ -391,17 +391,29 @@ def __eq__(self, o) -> bool:
return o._vid == self._vid and o._name == self._name and o._modifiers == self._modifiers
+WATCH_SOURCE_WATCH = "WATCH"
+"""Watch source for user watch statements."""
+WATCH_SOURCE_LOG = "LOG"
+"""Watch source for log expressions."""
+WATCH_SOURCE_METRIC = "METRIC"
+"""Watch source for metric expressions."""
+WATCH_SOURCE_CAPTURE = "CAPTURE"
+"""Watch source for captured data."""
+
+
class WatchResult:
"""This is the result of a watch expression."""
def __init__(self,
+ source: str,
expression: str,
result: Optional['VariableId'],
- error: Optional[str] = None
+ error: Optional[str] = None,
):
"""
Create new watch result.
+ :param source: the watch source
:param expression: the expression used
:param result: the result of the expression
:param error: the error captured during execution
@@ -409,6 +421,7 @@ def __init__(self,
self._expression = expression
self._result = result
self._error = error
+ self.__source = source
@property
def expression(self) -> str:
@@ -424,3 +437,8 @@ def result(self) -> Optional['VariableId']:
def error(self) -> Optional[str]:
"""The error."""
return self._error
+
+ @property
+ def source(self):
+ """The watch source."""
+ return self.__source
diff --git a/src/deep/grpc/__init__.py b/src/deep/grpc/__init__.py
index dffdc84..5bca57d 100644
--- a/src/deep/grpc/__init__.py
+++ b/src/deep/grpc/__init__.py
@@ -103,7 +103,7 @@ def convert_label_expressions(label_expressions) -> List[LabelExpression]:
def __convert_metric_definition(metrics):
- return [MetricDefinition(m.name, MetricType.Name(metrics[0].type), convert_label_expressions(m.labelExpressions),
+ return [MetricDefinition(m.name, MetricType.Name(m.type), convert_label_expressions(m.labelExpressions),
m.expression, m.namespace, m.help, m.unit) for m in metrics]
diff --git a/src/deep/processor/context/action_context.py b/src/deep/processor/context/action_context.py
index fcecce8..06b9de9 100644
--- a/src/deep/processor/context/action_context.py
+++ b/src/deep/processor/context/action_context.py
@@ -32,6 +32,7 @@
from typing import Tuple, TYPE_CHECKING, Dict
import deep.logging
+from deep.api.tracepoint.eventsnapshot import WATCH_SOURCE_CAPTURE
from deep.logging import logging
from deep.api.tracepoint import WatchResult, Variable
from deep.processor.variable_set_processor import VariableSetProcessor
@@ -65,10 +66,11 @@ def __exit__(self, exception_type, exception_value, exception_traceback):
if self.has_triggered():
self.location_action.record_triggered(self.trigger_context.ts)
- def eval_watch(self, watch: str) -> Tuple[WatchResult, Dict[str, Variable], str]:
+ def eval_watch(self, watch: str, source: str) -> Tuple[WatchResult, Dict[str, Variable], str]:
"""
Evaluate an expression in the current frame.
+ :param source: The watch source.
:param watch: The watch expression to evaluate.
:return: Tuple with WatchResult, collected variables, and the log string for the expression
"""
@@ -78,10 +80,23 @@ def eval_watch(self, watch: str) -> Tuple[WatchResult, Dict[str, Variable], str]
result = self.trigger_context.evaluate_expression(watch)
variable_id, log_str = var_processor.process_variable(watch, result)
- return WatchResult(watch, variable_id), var_processor.var_lookup, log_str
+ return WatchResult(source, watch, variable_id), var_processor.var_lookup, log_str
except BaseException as e:
logging.exception("Error evaluating watch %s", watch)
- return WatchResult(watch, None, str(e)), {}, str(e)
+ return WatchResult(source, watch, None, str(e)), {}, str(e)
+
+ def process_capture_variable(self, name: str, variable: any) -> Tuple[WatchResult, Dict[str, Variable], str]:
+ """
+ Process a captured variable (exception or return), into a variable set.
+
+ :param name: the name to use (raised or returned)
+ :param variable: the value to process
+ :return: Tuple with WatchResult, collected variables, and the log string for the expression
+ """
+ var_processor = VariableSetProcessor({}, self.trigger_context.var_cache)
+ variable_id, log_str = var_processor.process_variable(name, variable)
+
+ return WatchResult(WATCH_SOURCE_CAPTURE, name, variable_id), var_processor.var_lookup, log_str
def process(self):
"""Process the action."""
diff --git a/src/deep/processor/context/action_results.py b/src/deep/processor/context/action_results.py
index d4e2c0e..961932f 100644
--- a/src/deep/processor/context/action_results.py
+++ b/src/deep/processor/context/action_results.py
@@ -53,10 +53,11 @@ class ActionCallback:
"""A call back to 'close' an action."""
@abc.abstractmethod
- def process(self, event: str, frame: FrameType, arg: any) -> bool:
+ def process(self, ctx: 'TriggerContext', event: str, frame: FrameType, arg: any) -> bool:
"""
Process a callback.
+ :param ctx: the context for this trigger
:param event: the event
:param frame: the frame data
:param arg: the arg from settrace
diff --git a/src/deep/processor/context/callback_context.py b/src/deep/processor/context/callback_context.py
index a0f9e3f..09f653d 100644
--- a/src/deep/processor/context/callback_context.py
+++ b/src/deep/processor/context/callback_context.py
@@ -20,6 +20,7 @@
from deep.api.tracepoint.trigger import Location
from deep.processor.context.action_results import ActionCallback
+from deep.processor.context.trigger_context import TriggerContext
class CallbackContext(Location, ActionCallback):
@@ -59,17 +60,18 @@ def at_location(self, event: str, file: str, line: int, function_name: str, fram
else:
return self.__check_at_method_end(event)
- def process(self, event: str, frame: FrameType, arg: any):
+ def process(self, ctx: 'TriggerContext', event: str, frame: FrameType, arg: any):
"""
Process all callbacks.
+ :param ctx: the context for this trigger
:param event: the event
:param frame: the frame data
:param arg: the arg from settrace
:return: True, to keep this callback until next match.
"""
for callback in self.__callbacks:
- callback.process(event, frame, arg)
+ callback.process(ctx, event, frame, arg)
@property
def id(self) -> str:
diff --git a/src/deep/processor/context/log_action.py b/src/deep/processor/context/log_action.py
index a17d48f..3216168 100644
--- a/src/deep/processor/context/log_action.py
+++ b/src/deep/processor/context/log_action.py
@@ -33,6 +33,7 @@
from .action_context import ActionContext
from .action_results import ActionResult, ActionCallback
from ...api.tracepoint.constants import LOG_MSG
+from ...api.tracepoint.eventsnapshot import WATCH_SOURCE_LOG
from ...api.tracepoint.trigger import LocationAction
from typing import Tuple
@@ -83,7 +84,7 @@ class FormatExtractor(string.Formatter):
def get_field(self, field_name, args, kwargs):
# evaluate watch
- watch, var_lookup, log_str = ctx_self.eval_watch(field_name)
+ watch, var_lookup, log_str = ctx_self.eval_watch(field_name, WATCH_SOURCE_LOG)
# collect data
watch_results.append(watch)
_var_lookup.update(var_lookup)
diff --git a/src/deep/processor/context/snapshot_action.py b/src/deep/processor/context/snapshot_action.py
index eef47a3..8d95216 100644
--- a/src/deep/processor/context/snapshot_action.py
+++ b/src/deep/processor/context/snapshot_action.py
@@ -27,13 +27,15 @@
# along with this program. If not, see .
"""Handling for snapshot actions."""
-
+from types import FrameType
from typing import Tuple, Optional, TYPE_CHECKING
import deep.logging
from deep.api.attributes import BoundedAttributes
from deep.api.tracepoint import EventSnapshot
-from deep.api.tracepoint.constants import FRAME_TYPE, SINGLE_FRAME_TYPE, NO_FRAME_TYPE, ALL_FRAME_TYPE
+from deep.api.tracepoint.constants import FRAME_TYPE, SINGLE_FRAME_TYPE, NO_FRAME_TYPE, ALL_FRAME_TYPE, STAGE, \
+ LINE_CAPTURE, METHOD_CAPTURE
+from deep.api.tracepoint.eventsnapshot import WATCH_SOURCE_WATCH
from deep.api.tracepoint.trigger import LocationAction
from deep.processor.context.action_context import ActionContext
from deep.processor.context.action_results import ActionResult, ActionCallback
@@ -115,7 +117,7 @@ def _process_action(self):
# process the snapshot watches
for watch in self.watches:
- result, watch_lookup, _ = self.eval_watch(watch)
+ result, watch_lookup, _ = self.eval_watch(watch, WATCH_SOURCE_WATCH)
snapshot.add_watch_result(result)
snapshot.merge_var_lookup(watch_lookup)
@@ -132,11 +134,25 @@ def _process_action(self):
snapshot.merge_var_lookup(log_vars)
self.trigger_context.attach_result(LogActionResult(context.location_action, log))
- self.trigger_context.attach_result(SendSnapshotActionResult(self, snapshot))
+ if self.trigger_context.event in ['exception', 'return']:
+ watch, new_vars, _ = self.process_capture_variable(self.trigger_context.event, self.trigger_context.arg)
+ snapshot.add_watch_result(watch)
+ snapshot.merge_var_lookup(new_vars)
+ if self._is_deferred():
+ self.trigger_context.attach_result(DeferredSnapshotActionResult(self, snapshot))
+ else:
+ self.trigger_context.attach_result(SendSnapshotActionResult(self, snapshot))
-class SendSnapshotActionResult(ActionResult):
- """The result of a successful snapshot action."""
+ def _is_deferred(self):
+ stage = self.location_action.config.get(STAGE, None)
+ if stage is None:
+ return False
+ return stage == LINE_CAPTURE or stage == METHOD_CAPTURE
+
+
+class DeferredSnapshotActionResult(ActionResult):
+ """The result of a deferred snapshot action."""
def __init__(self, action_context: ActionContext, snapshot: EventSnapshot):
"""
@@ -156,6 +172,10 @@ def process(self, ctx: 'TriggerContext') -> Optional[ActionCallback]:
:return: an action callback if we need to do something at the 'end', or None
"""
+ snapshot = self._decorate_snapshot(ctx)
+ return DeferredSnapshotActionCallback(self.action_context, snapshot)
+
+ def _decorate_snapshot(self, ctx):
attributes = BoundedAttributes(attributes={'ctx_id': ctx.id}, immutable=False)
for decorator in ctx.config.snapshot_decorators:
try:
@@ -164,7 +184,63 @@ def process(self, ctx: 'TriggerContext') -> Optional[ActionCallback]:
attributes.merge_in(decorate)
except Exception:
deep.logging.exception("Failed to decorate snapshot: %s ", decorator)
-
self.snapshot.attributes.merge_in(attributes)
- ctx.push_service.push_snapshot(self.snapshot)
+ return self.snapshot
+
+
+class DeferredSnapshotActionCallback(ActionCallback):
+ """Defer the send action to the end of the line or function."""
+
+ def __init__(self, action_context: ActionContext, snapshot: EventSnapshot):
+ """
+ Create a new action callback.
+
+ :param action_context: the triggering action context
+ :param snapshot: the generated snapshot
+ """
+ self.__action_context = action_context
+ self.__snapshot = snapshot
+
+ def process(self, ctx: 'TriggerContext', event: str, frame: FrameType, arg: any) -> bool:
+ """
+ Process a callback.
+
+ :param ctx: the context for this trigger
+ :param event: the event
+ :param frame: the frame data
+ :param arg: the arg from settrace
+ :return: True, to keep this callback until next match.
+ """
+ if event in ['exception', 'return']:
+ watch, new_vars, _ = self.__action_context.process_capture_variable(event, arg)
+ self.__snapshot.add_watch_result(watch)
+ self.__snapshot.merge_var_lookup(new_vars)
+
+ ctx.push_service.push_snapshot(self.__snapshot)
+ return False
+
+
+class SendSnapshotActionResult(DeferredSnapshotActionResult):
+ """The result of a successful snapshot action."""
+
+ def __init__(self, action_context: ActionContext, snapshot: EventSnapshot):
+ """
+ Create a new snapshot action result.
+
+ :param action_context: the action context that created this result
+ :param snapshot: the snapshot result
+ """
+ self.action_context = action_context
+ self.snapshot = snapshot
+
+ def process(self, ctx: 'TriggerContext') -> Optional[ActionCallback]:
+ """
+ Process this result.
+
+ :param ctx: the triggering context
+
+ :return: an action callback if we need to do something at the 'end', or None
+ """
+ snapshot = self._decorate_snapshot(ctx)
+ ctx.push_service.push_snapshot(snapshot)
return None
diff --git a/src/deep/processor/context/span_action.py b/src/deep/processor/context/span_action.py
index 189859f..7eb0846 100644
--- a/src/deep/processor/context/span_action.py
+++ b/src/deep/processor/context/span_action.py
@@ -31,10 +31,11 @@ def __init__(self, spans):
"""Create callback."""
self.__spans = spans
- def process(self, event: str, frame: FrameType, arg: any) -> bool:
+ def process(self, ctx: 'TriggerContext', event: str, frame: FrameType, arg: any) -> bool:
"""
Process a callback.
+ :param ctx: the context for this trigger
:param event: the event
:param frame: the frame data
:param arg: the arg from settrace
diff --git a/src/deep/processor/context/trigger_context.py b/src/deep/processor/context/trigger_context.py
index 8930784..2f6db21 100644
--- a/src/deep/processor/context/trigger_context.py
+++ b/src/deep/processor/context/trigger_context.py
@@ -44,7 +44,7 @@ class TriggerContext:
collect the data and ship of the results.
"""
- def __init__(self, config: ConfigService, push_service: PushService, frame: FrameType, event: str):
+ def __init__(self, config: ConfigService, push_service: PushService, frame: FrameType, event: str, arg: any):
"""
Create a new trigger context.
@@ -52,10 +52,12 @@ def __init__(self, config: ConfigService, push_service: PushService, frame: Fram
:param push_service: the push service
:param frame: the frame data
:param event: the trigger event
+ :param arg: the trigger arg
"""
self.__push_service = push_service
self.__event = event
self.__frame = frame
+ self.__arg = arg
self.__config = config
self.__results: List[ActionResult] = []
self.__ts: int = time_ns()
@@ -114,6 +116,16 @@ def config(self) -> ConfigService:
"""The config service."""
return self.__config
+ @property
+ def arg(self):
+ """The trigger arg value."""
+ return self.__arg
+
+ @property
+ def event(self):
+ """The trigger event value."""
+ return self.__event
+
def action_context(self, action: 'LocationAction') -> 'ActionContext':
"""
Create an action context from this context, for the provided action.
diff --git a/src/deep/processor/trigger_handler.py b/src/deep/processor/trigger_handler.py
index 0198850..e049359 100644
--- a/src/deep/processor/trigger_handler.py
+++ b/src/deep/processor/trigger_handler.py
@@ -136,8 +136,10 @@ def trace_call(self, frame: FrameType, event: str, arg):
:return: None to ignore other calls, or our self to continue
"""
event, file, line, function = self.location_from_event(event, frame)
+ trigger_context = TriggerContext(self._config, self._push_service, frame, event, arg)
+
if event in ["line", "return", "exception"] and self._callbacks.is_set:
- self.__process_call_backs(arg, frame, event, file, line, function)
+ self.__process_call_backs(trigger_context, arg, frame, event, file, line, function)
# return if we do not have any tracepoints
if len(self._tp_config) == 0:
@@ -147,7 +149,6 @@ def trace_call(self, frame: FrameType, event: str, arg):
if len(actions) == 0:
return self.trace_call
- trigger_context = TriggerContext(self._config, self._push_service, frame, event)
try:
with trigger_context:
for action in actions:
@@ -176,13 +177,14 @@ def __actions_for_location(self, event, file, line, function, frame):
actions += trigger.actions
return actions
- def __process_call_backs(self, arg: any, frame: FrameType, event: str, file: str, line: int, function_name: str):
+ def __process_call_backs(self, ctx: 'TriggerContext', arg: any, frame: FrameType, event: str, file: str, line: int,
+ function_name: str):
# remove top context
context: CallbackContext = self._callbacks.value.pop()
# if it is for our location process it
if context.at_location(event, file, line, function_name, frame):
logging.debug("At callback location %s", context.name)
- context.process(event, frame, arg)
+ context.process(ctx, event, frame, arg)
else:
logging.debug("Not at callback location %s", context.name)
# else put the context back on the queue
diff --git a/src/deep/processor/variable_processor.py b/src/deep/processor/variable_processor.py
index 42cad07..aae8bf4 100644
--- a/src/deep/processor/variable_processor.py
+++ b/src/deep/processor/variable_processor.py
@@ -37,6 +37,8 @@
'module',
'unicode',
'long',
+ 'NoneType',
+ 'traceback'
]
"""A list of types that do not have child nodes, or only have child nodes we do not want to process."""
diff --git a/src/deep/push/__init__.py b/src/deep/push/__init__.py
index 15b4e5e..7f1e372 100644
--- a/src/deep/push/__init__.py
+++ b/src/deep/push/__init__.py
@@ -27,7 +27,7 @@
from deepproto.proto.common.v1.common_pb2 import KeyValue
# noinspection PyUnresolvedReferences
from deepproto.proto.tracepoint.v1.tracepoint_pb2 import Snapshot, TracePointConfig, WatchResult, Variable, \
- VariableID, StackFrame
+ VariableID, StackFrame, WatchSource
from .push_service import PushService
@@ -55,9 +55,13 @@ def __convert_frame(frame: StFr):
)
+def __convert_watch_source(source):
+ return WatchSource.Value(source)
+
+
def __convert_watch(watch: WaRe):
return WatchResult(expression=watch.expression, good_result=__convert_variable_id(watch.result),
- error_result=watch.error)
+ error_result=watch.error, source=__convert_watch_source(watch.source))
def __convert_variable(variable: Var):
diff --git a/tests/unit_tests/processor/context/test_log_action.py b/tests/unit_tests/processor/context/test_log_action.py
index 2f7f9d5..604f4ec 100644
--- a/tests/unit_tests/processor/context/test_log_action.py
+++ b/tests/unit_tests/processor/context/test_log_action.py
@@ -60,7 +60,7 @@ class TestLogMessages(unittest.TestCase):
["some log message: {person['name']}", "[deep] some log message: bob", {'person': {'name': 'bob'}}, ["bob"]],
])
def test_simple_log_interpolation(self, log_msg, expected_msg, _locals, expected_watches):
- context = LogActionContext(TriggerContext(None, None, MockFrame(_locals), "test"), None)
+ context = LogActionContext(TriggerContext(None, None, MockFrame(_locals), "test", None), None)
log, watches, _vars = context.process_log(log_msg)
self.assertEqual(expected_msg, log)
self.assertEqual(len(expected_watches), len(watches))
diff --git a/tests/unit_tests/processor/test_log_messages.py b/tests/unit_tests/processor/test_log_messages.py
index 761f3da..cc98502 100644
--- a/tests/unit_tests/processor/test_log_messages.py
+++ b/tests/unit_tests/processor/test_log_messages.py
@@ -47,7 +47,7 @@ class TestLogMessages(unittest.TestCase):
["some log message: {person['name']}", "[deep] some log message: bob", {'person': {'name': 'bob'}}, ["bob"]],
])
def test_simple_log_interpolation(self, log_msg, expected_msg, _locals, expected_watches):
- context = LogActionContext(TriggerContext(None, None, MockFrame(_locals), "test"), None)
+ context = LogActionContext(TriggerContext(None, None, MockFrame(_locals), "test", None), None)
log, watches, _vars = context.process_log(log_msg)
self.assertEqual(expected_msg, log)
diff --git a/tests/unit_tests/processor/test_trigger_handler.py b/tests/unit_tests/processor/test_trigger_handler.py
index 7d539c0..51be700 100644
--- a/tests/unit_tests/processor/test_trigger_handler.py
+++ b/tests/unit_tests/processor/test_trigger_handler.py
@@ -38,7 +38,7 @@
from deep.api.plugin.metric import MetricProcessor
from deep.api.plugin.span import SpanProcessor
from deep.api.resource import Resource
-from deep.api.tracepoint.constants import LOG_MSG, WATCHES
+from deep.api.tracepoint.constants import LOG_MSG, WATCHES, METHOD_CAPTURE, STAGE
from deep.api.tracepoint.eventsnapshot import EventSnapshot
from deep.api.tracepoint.tracepoint_config import MetricDefinition
@@ -46,7 +46,7 @@
from deep.config import ConfigService
from deep.processor.trigger_handler import TriggerHandler
from deep.push.push_service import PushService
-from unit_tests.test_target import some_test_function
+from unit_tests.test_target import some_test_function, some_test_error
class MockPushService(PushService):
@@ -91,8 +91,12 @@ def __init__(self):
def capture_trace_call(self, location: Location):
def trace_call(frame, event, args):
+ # print(frame, event, args)
event, file, line, function = TriggerHandler.location_from_event(event, frame)
- if location.at_location(event, file, line, function, frame):
+ # on raise exception we get a return immediately after,
+ # so if we have captured an exception don't capture again
+ if location.at_location(event, file, line, function, frame) and not self.captured_exception():
+ print("Capture frame:", frame, event, args)
self.captured_frame = frame
self.captured_event = event
self.captured_args = args
@@ -100,6 +104,16 @@ def trace_call(frame, event, args):
return trace_call
+ def captured_exception(self):
+ if self.captured_event == "exception":
+ return True
+ return False
+
+ def clear(self):
+ self.captured_frame = None
+ self.captured_event = None
+ self.captured_args = None
+
logging.init(MockConfigService({}))
@@ -110,11 +124,17 @@ def call_and_capture(self, location, func, args, capture):
# here we execute the real code using a mock trace call that will capture the args to trace call
# we cannot debug this section of the code
+ def func_wrap(*args):
+ try:
+ func(*args)
+ except Exception:
+ pass
+
# we use the _trace_hook and nopt gettrace() as gettrace() is not available in all tested versions of pythong
# noinspection PyUnresolvedReferences
current = threading._trace_hook
threading.settrace(capture.capture_trace_call(location))
- thread = Thread(target=func, args=args)
+ thread = Thread(target=func_wrap, args=args)
thread.start()
thread.join(10)
@@ -250,6 +270,8 @@ def test_span_action(self):
handler.trace_call(capture.captured_frame, capture.captured_event, capture.captured_args)
+ capture.clear()
+
# now extract the callback value
pop = handler._callbacks.value
# capture the real data that would be sent when we match this location
@@ -266,3 +288,101 @@ def test_span_action(self):
mockito.verify(mock_plugin, mockito.times(1)).create_span("some_test_function")
mockito.verify(mock_span, mockito.times(1)).close()
+
+ def test_method_result_capture(self):
+ capture = TraceCallCapture()
+ config = MockConfigService({})
+ push = MockPushService(None, None)
+ handler = TriggerHandler(config, push)
+
+ location = FunctionLocation('test_target.py', "some_test_function", Location.Position.START)
+ handler.new_config([Trigger(location, [
+ LocationAction("tp_id", "", {STAGE: METHOD_CAPTURE}, LocationAction.ActionType.Snapshot)])])
+
+ self.call_and_capture(location, some_test_function, ['input'], capture)
+
+ handler.trace_call(capture.captured_frame, capture.captured_event, capture.captured_args)
+
+ capture.clear()
+
+ # now extract the callback value
+ pop = handler._callbacks.value
+ # capture the real data that would be sent when we match this location
+ self.call_and_capture(pop[0], some_test_function, ['input'], capture)
+
+ # now call our trace call to check our callbacks
+ handler.trace_call(capture.captured_frame, capture.captured_event, capture.captured_args)
+
+ logged = config.logger.logged
+ self.assertEqual(0, len(logged))
+ pushed = push.pushed
+ self.assertEqual(1, len(pushed))
+
+ self.assertEqual("return", pushed[0].watches[0].expression)
+ self.assertEqual("inputsomething", pushed[0].var_lookup[pushed[0].watches[0].result.vid].value)
+
+ def test_method_exception_capture(self):
+ capture = TraceCallCapture()
+ config = MockConfigService({})
+ push = MockPushService(None, None)
+ handler = TriggerHandler(config, push)
+
+ location = FunctionLocation('test_target.py', "some_test_error", Location.Position.START)
+ handler.new_config([Trigger(location, [
+ LocationAction("tp_id", "", {STAGE: METHOD_CAPTURE}, LocationAction.ActionType.Snapshot)])])
+
+ self.call_and_capture(location, some_test_error, ['input'], capture)
+
+ handler.trace_call(capture.captured_frame, capture.captured_event, capture.captured_args)
+
+ capture.clear()
+
+ # now extract the callback value
+ pop = handler._callbacks.value
+ # capture the real data that would be sent when we match this location
+ self.call_and_capture(pop[0], some_test_error, ['input'], capture)
+
+ # now call our trace call to check our callbacks
+ handler.trace_call(capture.captured_frame, capture.captured_event, capture.captured_args)
+
+ logged = config.logger.logged
+ self.assertEqual(0, len(logged))
+ pushed = push.pushed
+ self.assertEqual(1, len(pushed))
+ self.assertEqual(1, len(pushed[0].watches))
+
+ self.assertEqual("exception", pushed[0].watches[0].expression)
+ self.assertEqual("Size: 3", pushed[0].var_lookup[pushed[0].watches[0].result.vid].value)
+
+ def test_line_exception_capture(self):
+ capture = TraceCallCapture()
+ config = MockConfigService({})
+ push = MockPushService(None, None)
+ handler = TriggerHandler(config, push)
+
+ location = LineLocation('test_target.py', 31, Location.Position.START)
+ handler.new_config([Trigger(location, [
+ LocationAction("tp_id", "", {STAGE: METHOD_CAPTURE}, LocationAction.ActionType.Snapshot)])])
+
+ self.call_and_capture(location, some_test_error, ['input'], capture)
+
+ handler.trace_call(capture.captured_frame, capture.captured_event, capture.captured_args)
+
+ capture.clear()
+
+ # now extract the callback value
+ pop = handler._callbacks.value
+ # capture the real data that would be sent when we match this location
+ self.call_and_capture(pop[0], some_test_error, ['input'], capture)
+
+ # now call our trace call to check our callbacks
+ handler.trace_call(capture.captured_frame, capture.captured_event, capture.captured_args)
+
+ logged = config.logger.logged
+ self.assertEqual(0, len(logged))
+ pushed = push.pushed
+ self.assertEqual(1, len(pushed))
+ self.assertEqual(1, len(pushed[0].watches))
+
+ self.assertEqual("exception", pushed[0].watches[0].expression)
+ self.assertEqual("Size: 3", pushed[0].var_lookup[pushed[0].watches[0].result.vid].value)
diff --git a/tests/unit_tests/push/test_push.py b/tests/unit_tests/push/test_push.py
index 70b98f3..6c6e6d3 100644
--- a/tests/unit_tests/push/test_push.py
+++ b/tests/unit_tests/push/test_push.py
@@ -13,6 +13,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from deep.api.tracepoint import WatchResult
+from deep.api.tracepoint.eventsnapshot import WATCH_SOURCE_WATCH
from deep.push import convert_snapshot
from utils import mock_snapshot, mock_frame, mock_variable, mock_variable_id
@@ -52,7 +53,7 @@ def test_convert_snapshot_with_frame_with_vars():
def test_convert_snapshot_with_watch():
event_snapshot = mock_snapshot()
- event_snapshot.add_watch_result(WatchResult("test", mock_variable_id()))
+ event_snapshot.add_watch_result(WatchResult(WATCH_SOURCE_WATCH, "test", mock_variable_id()))
snapshot = convert_snapshot(event_snapshot)
@@ -64,7 +65,7 @@ def test_convert_snapshot_with_watch():
def test_convert_snapshot_with_error_watch():
event_snapshot = mock_snapshot()
- event_snapshot.add_watch_result(WatchResult("test", None, 'test error'))
+ event_snapshot.add_watch_result(WatchResult(WATCH_SOURCE_WATCH, "test", None, 'test error'))
snapshot = convert_snapshot(event_snapshot)
diff --git a/tests/unit_tests/test_target.py b/tests/unit_tests/test_target.py
index 6638c35..5298190 100644
--- a/tests/unit_tests/test_target.py
+++ b/tests/unit_tests/test_target.py
@@ -25,3 +25,7 @@ def some_test_function(arg):
val = arg + "something"
return val
+
+
+def some_test_error(arg):
+ raise Exception(some_test_function(arg))