Skip to content

Commit 45cf63f

Browse files
author
Adam Hrbac
committed
[GR-39614] bci: implement sys.settrace
PullRequest: graalpython/2368
2 parents fe26d12 + 032f315 commit 45cf63f

File tree

14 files changed

+701
-40
lines changed

14 files changed

+701
-40
lines changed

graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/PythonTests.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,12 @@ public static Context enterContext(Map<String, String> options, String[] args) {
112112
PythonTests.outArray.reset();
113113
PythonTests.errArray.reset();
114114
Context prevContext = context;
115-
context = Context.newBuilder().engine(engine).allowExperimentalOptions(true).allowAllAccess(true).options(options).arguments("python", args).option("python.Executable", executable).build();
115+
Context.Builder builder = Context.newBuilder().engine(engine).allowExperimentalOptions(true).allowAllAccess(true).options(options).arguments("python", args).option("python.Executable",
116+
executable);
117+
if (usingBytecodeCompiler()) {
118+
builder.option("python.EnableBytecodeInterpreter", "true").option("python.DisableFrozenModules", "true");
119+
}
120+
context = builder.build();
116121
context.initialize("python");
117122
assert prevContext == null;
118123
context.enter();
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
package com.oracle.graal.python.test.runtime;
42+
43+
import com.oracle.graal.python.test.PythonTests;
44+
import org.junit.Assume;
45+
import org.junit.Before;
46+
import org.junit.Test;
47+
48+
public class TracingTests {
49+
50+
@Before
51+
public void ensureBytecode() {
52+
Assume.assumeTrue(PythonTests.usingBytecodeCompiler());
53+
}
54+
55+
@Test
56+
public void traceInExistingScopeWithOnlyReturn() {
57+
String source = "import sys\n" +
58+
"def trace(fr, ev, arg): print(fr.f_lineno, ev); return trace\n" +
59+
"def fun():\n" +
60+
" sys._getframe().f_trace = trace\n" +
61+
" sys.settrace(lambda:None)\n" +
62+
"fun()";
63+
PythonTests.assertPrints("5 return\n", source);
64+
}
65+
66+
@Test
67+
public void traceCall() {
68+
String source = "import sys\n" +
69+
"sys.settrace(lambda frame,ev,arg: ev == 'call' and print(ev))\n" +
70+
"def foo(): str(1);print('7')\n" +
71+
"foo()";
72+
PythonTests.assertPrints("call\n7\n", source);
73+
}
74+
75+
@Test
76+
public void gettraceReturnsSettrace() {
77+
String source = "import sys\n" +
78+
"print(sys.gettrace())\n" +
79+
"def foo(a,b,c): pass\n" +
80+
"sys.settrace(foo)\n" +
81+
"print(sys.gettrace() is foo)\n" +
82+
"sys.settrace(None)\n" +
83+
"print(sys.gettrace())";
84+
PythonTests.assertPrints("None\nTrue\nNone\n", source);
85+
}
86+
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
2+
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
3+
#
4+
# The Universal Permissive License (UPL), Version 1.0
5+
#
6+
# Subject to the condition set forth below, permission is hereby granted to any
7+
# person obtaining a copy of this software, associated documentation and/or
8+
# data (collectively the "Software"), free of charge and under any and all
9+
# copyright rights in the Software, and any and all patent rights owned or
10+
# freely licensable by each licensor hereunder covering either (i) the
11+
# unmodified Software as contributed to or provided by such licensor, or (ii)
12+
# the Larger Works (as defined below), to deal in both
13+
#
14+
# (a) the Software, and
15+
#
16+
# (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
17+
# one is included with the Software each a "Larger Work" to which the Software
18+
# is contributed by such licensors),
19+
#
20+
# without restriction, including without limitation the rights to copy, create
21+
# derivative works of, display, perform, and distribute the Software and make,
22+
# use, sell, offer for sale, import, export, have made, and have sold the
23+
# Software and the Larger Work(s), and to sublicense the foregoing rights on
24+
# either these or other terms.
25+
#
26+
# This license is subject to the following condition:
27+
#
28+
# The above copyright notice and either this complete permission notice or at a
29+
# minimum a reference to the UPL must be included in all copies or substantial
30+
# portions of the Software.
31+
#
32+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38+
# SOFTWARE.
39+
40+
import unittest
41+
import difflib
42+
import sys
43+
44+
import builtins
45+
46+
# only while the bytecode interpreter is not the default
47+
skip = not getattr(getattr(builtins, '__graalpython__', None), 'uses_bytecode_interpreter', True)
48+
49+
def basic():
50+
return 'return value'
51+
52+
53+
basic.events = [((), [(0, 'basic', 'call', None),
54+
(1, 'basic', 'line', None),
55+
(1, 'basic', 'return', 'return value')])]
56+
57+
def more_complex(a):
58+
if a:
59+
b = 3
60+
else:
61+
b = 5
62+
63+
64+
more_complex.events = [((True,), [(0, 'more_complex', 'call', None),
65+
(1, 'more_complex', 'line', None),
66+
(2, 'more_complex', 'line', None),
67+
(2, 'more_complex', 'return', None)]),
68+
((False,), [(0, 'more_complex', 'call', None),
69+
(1, 'more_complex', 'line', None),
70+
(4, 'more_complex', 'line', None),
71+
(4, 'more_complex', 'return', None)])]
72+
73+
74+
# there are 5 line events on line 3, since there is the first check of the loop flag, then 4 backward jumps, last one ending the loop
75+
76+
def oneline_loop():
77+
items = range(0, 3)
78+
i = 4
79+
while i: i -= 1
80+
81+
82+
oneline_loop.events = [((), [(0, 'oneline_loop', 'call', None),
83+
(1, 'oneline_loop', 'line', None),
84+
(2, 'oneline_loop', 'line', None),
85+
(3, 'oneline_loop', 'line', None),
86+
(3, 'oneline_loop', 'line', None),
87+
(3, 'oneline_loop', 'line', None),
88+
(3, 'oneline_loop', 'line', None),
89+
(3, 'oneline_loop', 'line', None),
90+
(3, 'oneline_loop', 'return', None)])]
91+
92+
def helper(): # line -5
93+
str(1)
94+
return 2
95+
96+
97+
def two_functions():
98+
str(1)
99+
var = helper()
100+
return var + 1
101+
102+
two_functions.events = [((), [(0, 'two_functions', 'call', None),
103+
(1, 'two_functions', 'line', None),
104+
(2, 'two_functions', 'line', None),
105+
(-5, 'helper', 'call', None),
106+
(-4, 'helper', 'line', None),
107+
(-3, 'helper', 'line', None),
108+
(-3, 'helper', 'return', 2),
109+
(3, 'two_functions', 'line', None),
110+
(3, 'two_functions', 'return', 3)])]
111+
112+
def gen(): # line -5
113+
yield 1
114+
yield 2
115+
116+
117+
def generator_example():
118+
x, m = gen(), {}
119+
while m.setdefault('i', next(x, False)): # this prevents CPython from tracing a StopIteration once gen() ends
120+
del m['i']
121+
return m['i']
122+
123+
generator_example.events = [((), [(0, 'generator_example', 'call', None),
124+
(1, 'generator_example', 'line', None),
125+
(2, 'generator_example', 'line', None),
126+
(-5, 'gen', 'call', None),
127+
(-4, 'gen', 'line', None),
128+
(-4, 'gen', 'return', 1),
129+
(3, 'generator_example', 'line', None),
130+
(2, 'generator_example', 'line', None),
131+
(-4, 'gen', 'call', None),
132+
(-3, 'gen', 'line', None),
133+
(-3, 'gen', 'return', 2),
134+
(3, 'generator_example', 'line', None),
135+
(2, 'generator_example', 'line', None),
136+
(-3, 'gen', 'call', None),
137+
(-3, 'gen', 'return', None),
138+
(4, 'generator_example', 'line', None),
139+
(4, 'generator_example', 'return', False)])]
140+
141+
def make_test_method(fun, name):
142+
def test_case(self):
143+
for args, events in fun.events:
144+
try:
145+
self.events = []
146+
self.first_line = fun.__code__.co_firstlineno
147+
sys.settrace(self.trace)
148+
fun(*args)
149+
finally:
150+
sys.settrace(None)
151+
if self.events != events:
152+
self.fail(str(args) + '\n' + '\n'.join(difflib.ndiff([str(x) for x in events], [str(x) for x in self.events])))
153+
154+
test_case.__name__ = name
155+
return test_case
156+
157+
@unittest.skipIf(skip, 'sys.settrace only works in the bytecode interpreter')
158+
class TraceTests(unittest.TestCase):
159+
def trace(self, frame, event, arg):
160+
code = frame.f_code
161+
name = code.co_name
162+
if event == 'exception':
163+
self.events.append((frame.f_lineno - self.first_line, name, event, arg[0]))
164+
else:
165+
self.events.append((frame.f_lineno - self.first_line, name, event, arg))
166+
return self.trace
167+
test_01_basic = make_test_method(basic, 'test_01_basic')
168+
test_02_more_complex = make_test_method(more_complex, 'test_02_more_complex')
169+
test_03_oneline_loop = make_test_method(oneline_loop, 'test_03_oneline_loop')
170+
test_04_two_functions = make_test_method(two_functions, 'test_04_two_functions')
171+
test_05_generator_example = make_test_method(generator_example, 'test_05_generator_example')
172+
def test_06_f_trace_preserved(self):
173+
def erroring_trace(*_):
174+
raise ValueError
175+
def fun2():
176+
str(0)
177+
def fun1():
178+
try:
179+
sys.settrace(erroring_trace)
180+
sys._getframe().f_trace = self.trace
181+
fun2()
182+
except ValueError:
183+
pass
184+
else:
185+
self.fail("didn't raise ValueError")
186+
str(1)
187+
sys.settrace(self.trace)
188+
str(2)
189+
sys.settrace(None)
190+
191+
self.first_line = fun1.__code__.co_firstlineno
192+
self.events = []
193+
sys.settrace(None)
194+
fun1()
195+
events = [(4, 'fun1', 'line', None),
196+
(11, 'fun1', 'line', None),
197+
(12, 'fun1', 'line', None)]
198+
if self.events != events:
199+
self.fail('\n'+'\n'.join(difflib.ndiff([str(x) for x in events], [str(x) for x in self.events])))
Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
1-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_01_basic
2-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_02_arigo0
3-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_02_arigo1
4-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_02_arigo2
5-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_03_one_instr
6-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_04_no_pop_blocks
7-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_05_no_pop_tops
8-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_06_call
9-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_07_raise
10-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_08_settrace_and_return
11-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_09_settrace_and_raise
12-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_10_ireturn
13-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_11_tightloop
14-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_12_tighterloop
15-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_13_genexp
16-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_14_onliner_if
17-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_15_loops
18-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_16_blank_lines
19-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_17_none_f_trace
20-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_set_and_retrieve_func
21-
*graalpython.lib-python.3.test.test_sys_settrace.TraceOpcodesTestCase.test_set_and_retrieve_none
1+
*graalpython.lib-python.3.test.test_sys_settrace.RaisingTraceFuncTestCase.test_call
2+
*graalpython.lib-python.3.test.test_sys_settrace.RaisingTraceFuncTestCase.test_exception
3+
*graalpython.lib-python.3.test.test_sys_settrace.RaisingTraceFuncTestCase.test_exception_arguments
4+
*graalpython.lib-python.3.test.test_sys_settrace.RaisingTraceFuncTestCase.test_line
5+
*graalpython.lib-python.3.test.test_sys_settrace.RaisingTraceFuncTestCase.test_return
6+
*graalpython.lib-python.3.test.test_sys_settrace.RaisingTraceFuncTestCase.test_trash_stack
7+
*graalpython.lib-python.3.test.test_sys_settrace.SkipLineEventsTraceTestCase.test_01_basic
8+
*graalpython.lib-python.3.test.test_sys_settrace.SkipLineEventsTraceTestCase.test_02_arigo0
9+
*graalpython.lib-python.3.test.test_sys_settrace.SkipLineEventsTraceTestCase.test_02_arigo1
10+
*graalpython.lib-python.3.test.test_sys_settrace.SkipLineEventsTraceTestCase.test_02_arigo2
11+
*graalpython.lib-python.3.test.test_sys_settrace.SkipLineEventsTraceTestCase.test_03_one_instr
12+
*graalpython.lib-python.3.test.test_sys_settrace.SkipLineEventsTraceTestCase.test_04_no_pop_blocks
13+
*graalpython.lib-python.3.test.test_sys_settrace.SkipLineEventsTraceTestCase.test_05_no_pop_tops
14+
*graalpython.lib-python.3.test.test_sys_settrace.SkipLineEventsTraceTestCase.test_06_call
15+
*graalpython.lib-python.3.test.test_sys_settrace.SkipLineEventsTraceTestCase.test_07_raise
16+
*graalpython.lib-python.3.test.test_sys_settrace.SkipLineEventsTraceTestCase.test_08_settrace_and_return
17+
*graalpython.lib-python.3.test.test_sys_settrace.SkipLineEventsTraceTestCase.test_14_onliner_if
18+
*graalpython.lib-python.3.test.test_sys_settrace.SkipLineEventsTraceTestCase.test_15_loops
19+
*graalpython.lib-python.3.test.test_sys_settrace.SkipLineEventsTraceTestCase.test_17_none_f_trace
20+
*graalpython.lib-python.3.test.test_sys_settrace.SkipLineEventsTraceTestCase.test_set_and_retrieve_func
21+
*graalpython.lib-python.3.test.test_sys_settrace.SkipLineEventsTraceTestCase.test_set_and_retrieve_none
22+
*graalpython.lib-python.3.test.test_sys_settrace.TraceTestCase.test_01_basic
23+
*graalpython.lib-python.3.test.test_sys_settrace.TraceTestCase.test_03_one_instr
24+
*graalpython.lib-python.3.test.test_sys_settrace.TraceTestCase.test_04_no_pop_blocks
25+
*graalpython.lib-python.3.test.test_sys_settrace.TraceTestCase.test_05_no_pop_tops
26+
*graalpython.lib-python.3.test.test_sys_settrace.TraceTestCase.test_06_call
27+
*graalpython.lib-python.3.test.test_sys_settrace.TraceTestCase.test_08_settrace_and_return
28+
*graalpython.lib-python.3.test.test_sys_settrace.TraceTestCase.test_14_onliner_if
29+
*graalpython.lib-python.3.test.test_sys_settrace.TraceTestCase.test_17_none_f_trace
30+
*graalpython.lib-python.3.test.test_sys_settrace.TraceTestCase.test_set_and_retrieve_func
31+
*graalpython.lib-python.3.test.test_sys_settrace.TraceTestCase.test_set_and_retrieve_none

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/PythonLanguage.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ public final class PythonLanguage extends TruffleLanguage<PythonContext> {
216216

217217
private static final LanguageReference<PythonLanguage> REFERENCE = LanguageReference.create(PythonLanguage.class);
218218

219+
/**
220+
* This assumption will be valid if no context set a trace function at any point. Calling
221+
* sys.settrace(None) will not invalidate it
222+
*/
223+
public final Assumption noTracingAssumption = Assumption.create("No tracing function was set");
224+
219225
@CompilationFinal private boolean singleContext = true;
220226

221227
public boolean isSingleContext() {

0 commit comments

Comments
 (0)