Skip to content

Commit 8cf5c97

Browse files
authored
tests(debugger): add more expression test cases (#14686)
## Description We add some more expression test cases to increase coverage.
1 parent 1815d40 commit 8cf5c97

File tree

2 files changed

+99
-4
lines changed

2 files changed

+99
-4
lines changed

ddtrace/debugging/_expressions.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ def get_local(_locals: Mapping[str, Any], name: str) -> Any:
9999

100100

101101
class DDCompiler:
102+
def __init__(self):
103+
self._lambda_level = 0
104+
102105
@classmethod
103106
def __getmember__(cls, o, a):
104107
return object.__getattribute__(o, a)
@@ -128,7 +131,12 @@ def _make_function(self, ast: DDASTType, args: Tuple[str, ...], name: str) -> Fu
128131
return FunctionType(abstract_code.to_code(), {}, name, (), None)
129132

130133
def _make_lambda(self, ast: DDASTType) -> Callable[[Any, Any], Any]:
131-
return self._make_function(ast, ("_dd_it", "_dd_key", "_dd_value", "_locals"), "<lambda>")
134+
self._lambda_level += 1
135+
try:
136+
return self._make_function(ast, ("_dd_it", "_dd_key", "_dd_value", "_locals"), "<lambda>")
137+
finally:
138+
assert self._lambda_level > 0 # nosec
139+
self._lambda_level -= 1
132140

133141
def _compile_direct_predicate(self, ast: DDASTType) -> Optional[List[Instr]]:
134142
# direct_predicate => {"<direct_predicate_type>": <predicate>}
@@ -248,6 +256,9 @@ def _compile_direct_operation(self, ast: DDASTType) -> Optional[List[Instr]]:
248256
return None
249257

250258
if arg in {"@it", "@key", "@value"}:
259+
if self._lambda_level <= 0:
260+
msg = f"Invalid use of {arg} outside of lambda"
261+
raise ValueError(msg)
251262
return [Instr("LOAD_FAST", f"_dd_{arg[1:]}")]
252263

253264
return self._call_function(

tests/debugging/test_expressions.py

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from dis import dis
2+
import re
23

34
import pytest
45

@@ -129,15 +130,98 @@ def __getitem__(self, name):
129130
{"bar": CustomObject("foo")},
130131
True,
131132
),
133+
# Direct predicates
134+
({"not": True}, {}, False),
135+
({"not": False}, {}, True),
136+
({"isEmpty": {"ref": "empty_str"}}, {"empty_str": ""}, True),
137+
({"isEmpty": {"ref": "s"}}, {"s": "foo"}, False),
138+
({"isEmpty": {"ref": "empty_list"}}, {"empty_list": []}, True),
139+
({"isEmpty": {"ref": "l"}}, {"l": [1]}, False),
140+
({"isEmpty": {"ref": "empty_dict"}}, {"empty_dict": {}}, True),
141+
({"isEmpty": {"ref": "d"}}, {"d": {"a": 1}}, False),
142+
# Arg predicates
143+
({"ne": [1, 2]}, {}, True),
144+
({"ne": [1, 1]}, {}, False),
145+
({"gt": [2, 1]}, {}, True),
146+
({"gt": [1, 2]}, {}, False),
147+
({"ge": [2, 1]}, {}, True),
148+
({"ge": [1, 1]}, {}, True),
149+
({"ge": [1, 2]}, {}, False),
150+
({"lt": [1, 2]}, {}, True),
151+
({"lt": [2, 1]}, {}, False),
152+
({"le": [1, 2]}, {}, True),
153+
({"le": [1, 1]}, {}, True),
154+
({"le": [2, 1]}, {}, False),
155+
({"all": [{"ref": "collection"}, {"not": {"isEmpty": {"ref": "@it"}}}]}, {"collection": ["foo", "bar"]}, True),
156+
(
157+
{"all": [{"ref": "collection"}, {"not": {"isEmpty": {"ref": "@it"}}}]},
158+
{"collection": ["foo", "bar", ""]},
159+
False,
160+
),
161+
({"endsWith": [{"ref": "local_string"}, "world!"]}, {"local_string": "hello world!"}, True),
162+
({"endsWith": [{"ref": "local_string"}, "hello"]}, {"local_string": "hello world!"}, False),
163+
# Nested expressions
164+
(
165+
{"len": {"filter": [{"ref": "collection"}, {"gt": [{"ref": "@it"}, 1]}]}},
166+
{"collection": [1, 2, 3]},
167+
2,
168+
),
169+
(
170+
{"getmember": [{"getmember": [{"getmember": [{"ref": "self"}, "field1"]}, "field2"]}, "name"]},
171+
{"self": CustomObject("test-me")},
172+
"field2",
173+
),
174+
(
175+
{
176+
"any": [
177+
{"getmember": [{"ref": "self"}, "collectionField"]},
178+
{"startsWith": [{"getmember": [{"ref": "@it"}, "name"]}, "foo"]},
179+
]
180+
},
181+
{"self": CustomObject("test-me")},
182+
True,
183+
),
184+
(
185+
{"and": [{"eq": [{"ref": "hits"}, 42]}, {"gt": [{"len": {"ref": "payload"}}, 5]}]},
186+
{"hits": 42, "payload": "hello world"},
187+
True,
188+
),
189+
(
190+
{"and": [{"eq": [{"ref": "hits"}, 42]}, {"gt": [{"len": {"ref": "payload"}}, 20]}]},
191+
{"hits": 42, "payload": "hello world"},
192+
False,
193+
),
194+
(
195+
{"index": [{"filter": [{"ref": "collection"}, {"gt": [{"ref": "@it"}, 2]}]}, 0]},
196+
{"collection": [1, 2, 3, 4]},
197+
3,
198+
),
199+
# Edge cases
200+
({"any": [{"ref": "empty_list"}, {"ref": "@it"}]}, {"empty_list": []}, False),
201+
({"all": [{"ref": "empty_list"}, {"ref": "@it"}]}, {"empty_list": []}, True),
202+
({"count": {"ref": "payload"}}, {"payload": "hello"}, 5),
203+
({"substring": [{"ref": "s"}, -5, -1]}, {"s": "hello world"}, "worl"), # codespell:ignore worl
204+
({"substring": [{"ref": "s"}, 0, 100]}, {"s": "hello"}, "hello"),
205+
({"matches": [{"ref": "s"}, "["]}, {"s": "a"}, re.error),
206+
(
207+
{"or": [True, {"ref": "side_effect"}]},
208+
{"side_effect": SideEffect("or should short-circuit")},
209+
True,
210+
),
211+
({"ref": "@it"}, {}, ValueError),
212+
(
213+
{"len": {"filter": [{"ref": "collection"}, {"any": [{"ref": "@it"}, {"eq": [{"ref": "@it"}, 1]}]}]}},
214+
{"collection": [[1, 2], [3, 4], [5]]},
215+
1,
216+
),
132217
],
133218
)
134219
def test_parse_expressions(ast, _locals, value):
135-
compiled = dd_compile(ast)
136-
137220
if isinstance(value, type) and issubclass(value, Exception):
138221
with pytest.raises(value):
139-
compiled(_locals)
222+
dd_compile(ast)(_locals)
140223
else:
224+
compiled = dd_compile(ast)
141225
assert compiled(_locals) == value, dis(compiled)
142226

143227

0 commit comments

Comments
 (0)