Skip to content

Commit cd9994e

Browse files
authored
pythongh-152132: Test all PyRun functions in test_capi.test_run (python#153352)
PyRun_InteractiveOneObjectEx() now raises TypeError if the second parameter type is not str.
1 parent 53b0498 commit cd9994e

3 files changed

Lines changed: 993 additions & 62 deletions

File tree

Lib/test/test_capi/test_run.py

Lines changed: 285 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,72 @@
11
import os
2+
import sys
3+
import tempfile
24
import unittest
35
from collections import UserDict
6+
from test import support
47
from test.support import import_helper
58
from test.support.os_helper import unlink, TESTFN, TESTFN_ASCII, TESTFN_UNDECODABLE
69

10+
711
NULL = None
812
_testcapi = import_helper.import_module('_testcapi')
913
Py_single_input = _testcapi.Py_single_input
1014
Py_file_input = _testcapi.Py_file_input
1115
Py_eval_input = _testcapi.Py_eval_input
16+
STDIN = '<stdin>'
17+
STDERR_FD = 2
18+
19+
# Code raising a SyntaxError
20+
SYNTAX_ERROR = 'True = 1'
21+
22+
# Raise a SystemExit with exit code 42
23+
CODE_EXIT_42 = 'raise SystemExit(42)'
24+
25+
26+
def create_text_file(filename, text):
27+
with open(filename, 'w', encoding='utf8') as fp:
28+
fp.write(text)
1229

1330

1431
class DictSubclass(dict):
1532
pass
1633

1734

18-
class CAPITest(unittest.TestCase):
19-
# TODO: Test the following functions:
20-
#
21-
# PyRun_SimpleStringFlags
22-
# PyRun_AnyFileExFlags
23-
# PyRun_SimpleFileExFlags
24-
# PyRun_InteractiveOneFlags
25-
# PyRun_InteractiveOneObject
26-
# PyRun_InteractiveLoopFlags
27-
# PyRun_String (may be a macro)
28-
# PyRun_AnyFile (may be a macro)
29-
# PyRun_AnyFileEx (may be a macro)
30-
# PyRun_AnyFileFlags (may be a macro)
31-
# PyRun_SimpleString (may be a macro)
32-
# PyRun_SimpleFile (may be a macro)
33-
# PyRun_SimpleFileEx (may be a macro)
34-
# PyRun_InteractiveOne (may be a macro)
35-
# PyRun_InteractiveLoop (may be a macro)
36-
# PyRun_File (may be a macro)
37-
# PyRun_FileEx (may be a macro)
38-
# PyRun_FileFlags (may be a macro)
35+
class capture_excepthook:
36+
def __init__(self):
37+
self.exc = None
38+
self._old_hook = None
3939

40-
def test_run_stringflags(self):
41-
# Test PyRun_StringFlags().
42-
# XXX: fopen() uses different path encoding than Python on Windows.
40+
def _hook(self, exc_type, exc_value, exc_tb):
41+
# Storing the exception instance creates a reference cycle.
42+
self.exc = exc_value
43+
44+
def __enter__(self):
45+
self._old_hook = sys.excepthook
46+
sys.excepthook = self._hook
47+
return self
48+
49+
def __exit__(self, *exc_info):
50+
sys.excepthook = self._old_hook
51+
self.exc = None
52+
53+
54+
class CAPITest(unittest.TestCase):
55+
def check_run_string(self, func):
4356
def run(s, *args):
44-
return _testcapi.run_stringflags(s, Py_file_input, *args)
45-
source = b'a\n'
57+
return func(s, Py_file_input, *args)
58+
59+
ns = {}
60+
self.assertIsNone(run(b'x = 1', ns, ns))
61+
self.assertEqual(ns['x'], 1)
62+
63+
with self.assertRaises(SyntaxError) as cm:
64+
run(SYNTAX_ERROR.encode(), {})
65+
self.assertEqual(cm.exception.filename, '<string>')
66+
67+
with self.assertRaises(ValueError) as cm:
68+
run(b'raise ValueError("BUG")', {})
69+
self.assertEqual(str(cm.exception), 'BUG')
4670

4771
self.assertIsNone(run(b'a\n', dict(a=1)))
4872
self.assertIsNone(run(b'a\n', dict(a=1), {}))
@@ -70,20 +94,29 @@ def run(s, *args):
7094

7195
# CRASHES run(NULL, {})
7296

73-
def test_run_fileexflags(self):
74-
# Test PyRun_FileExFlags().
97+
def test_run_string(self):
98+
# Test PyRun_String()
99+
self.check_run_string(_testcapi.run_string)
100+
101+
def test_run_stringflags(self):
102+
# Test PyRun_StringFlags().
103+
self.check_run_string(_testcapi.run_stringflags)
104+
105+
def check_run_file(self, func, support_closeit):
106+
# XXX: fopen() uses different path encoding than Python on Windows.
75107
filename = os.fsencode(TESTFN if os.name != 'nt' else TESTFN_ASCII)
76-
with open(filename, 'wb') as fp:
77-
fp.write(b'a\n')
108+
create_text_file(filename, 'a\n')
78109
self.addCleanup(unlink, filename)
79110
def run(*args):
80-
return _testcapi.run_fileexflags(filename, Py_file_input, *args)
111+
return func(filename, Py_file_input, *args)
81112

82113
self.assertIsNone(run(dict(a=1)))
83114
self.assertIsNone(run(dict(a=1), {}))
84115
self.assertIsNone(run({}, dict(a=1)))
85116
self.assertIsNone(run({}, UserDict(a=1)))
86-
self.assertIsNone(run(dict(a=1), {}, 1)) # closeit = True
117+
if support_closeit:
118+
closeit = 1
119+
self.assertIsNone(run(dict(a=1), {}, closeit))
87120

88121
self.assertRaises(NameError, run, {})
89122
self.assertRaises(NameError, run, {}, {})
@@ -101,17 +134,228 @@ def run(*args):
101134
self.assertRaises(SystemError, run, UserDict(), {})
102135
self.assertRaises(SystemError, run, UserDict(), dict(a=1))
103136

104-
@unittest.skipUnless(TESTFN_UNDECODABLE, 'only works if there are undecodable paths')
105-
@unittest.skipIf(os.name == 'nt', 'does not work on Windows')
106-
def test_run_fileexflags_with_undecodable_filename(self):
107-
run = _testcapi.run_fileexflags
137+
# syntax error
138+
create_text_file(filename, SYNTAX_ERROR)
139+
with self.assertRaises(SyntaxError) as cm:
140+
self.assertIsNone(run({}))
141+
self.assertEqual(cm.exception.filename, os.fsdecode(filename))
142+
143+
# raise exception
144+
create_text_file(filename, 'raise ValueError("BUG")')
145+
with self.assertRaises(ValueError) as cm:
146+
self.assertIsNone(run({}))
147+
self.assertEqual(str(cm.exception), 'BUG')
148+
149+
# Test undecodable filename
150+
if TESTFN_UNDECODABLE and os.name != 'nt':
151+
try:
152+
create_text_file(TESTFN_UNDECODABLE, 'a\n')
153+
self.addCleanup(unlink, TESTFN_UNDECODABLE)
154+
except OSError:
155+
# undecodable paths are not supported
156+
pass
157+
else:
158+
self.assertIsNone(func(TESTFN_UNDECODABLE, Py_file_input, dict(a=1)))
159+
160+
def test_run_file(self):
161+
# Test PyRun_File().
162+
self.check_run_file(_testcapi.run_file, False)
163+
164+
def test_run_fileex(self):
165+
# Test PyRun_FileEx().
166+
self.check_run_file(_testcapi.run_fileex, True)
167+
168+
def test_run_fileflags(self):
169+
# Test PyRun_FileFlags().
170+
self.check_run_file(_testcapi.run_fileflags, True)
171+
172+
def test_run_fileexflags(self):
173+
# Test PyRun_FileExFlags().
174+
self.check_run_file(_testcapi.run_fileexflags, True)
175+
176+
def check_run_simplefile(self, func_name, have_closeit):
177+
run = getattr(_testcapi, func_name)
178+
179+
open_filename = TESTFN
180+
with open(open_filename, 'w') as fp:
181+
print('import sys', file=fp)
182+
print('print(__file__)', file=fp)
183+
print('mod = sys.modules["__main__"]', file=fp)
184+
print('print(type(mod.__loader__).__name__)', file=fp)
185+
self.addCleanup(unlink, open_filename)
186+
187+
class MockLoader:
188+
pass
189+
loader = MockLoader()
190+
191+
for closeit in (0, 1):
192+
for set_file in (False, True):
193+
for filename in (open_filename, 'custom filename', STDIN):
194+
set_loader = (filename != STDIN)
195+
with self.subTest(closeit=closeit,
196+
set_file=set_file,
197+
filename=filename):
198+
mod = sys.modules['__main__']
199+
mod_file = mod.__file__
200+
with (support.captured_stdout() as stdout,
201+
support.swap_attr(mod, '__loader__', loader)):
202+
try:
203+
if set_file:
204+
del mod.__file__
205+
filename_arg = os.fsencode(filename)
206+
if have_closeit:
207+
res = run(open_filename, filename_arg, closeit)
208+
else:
209+
res = run(open_filename, filename_arg)
210+
if set_file:
211+
self.assertFalse(hasattr(mod, '__file__'))
212+
finally:
213+
mod.__file__ = mod_file
214+
215+
self.assertEqual(res, 0)
216+
expected = [filename if set_file else mod_file]
217+
if set_loader:
218+
expected.append('SourceFileLoader')
219+
else:
220+
expected.append('MockLoader')
221+
self.assertEqual(stdout.getvalue().splitlines(),
222+
expected)
223+
224+
def test_run_simplefile(self):
225+
# Test PyRun_SimpleFile()
226+
self.check_run_simplefile('run_simplefile', False)
227+
228+
def test_run_simplefileex(self):
229+
# Test PyRun_SimpleFileEx()
230+
self.check_run_simplefile('run_simplefileex', True)
231+
232+
def test_run_simplefileexflags(self):
233+
# Test PyRun_SimpleFileExFlags()
234+
self.check_run_simplefile('run_simplefileexflags', True)
235+
236+
def test_run_anyfile(self):
237+
# Test PyRun_AnyFile()
238+
self.check_run_simplefile('run_anyfile', False)
239+
240+
def test_run_anyfileex(self):
241+
# Test PyRun_AnyFileEx()
242+
self.check_run_simplefile('run_anyfileex', True)
243+
244+
def test_run_anyfileflags(self):
245+
# Test PyRun_AnyFileFlags()
246+
self.check_run_simplefile('run_anyfileflags', True)
247+
248+
def _check_run_interactive(self, run, encode_filename, use_loop):
249+
open_filename = TESTFN
250+
if use_loop:
251+
with open(open_filename, 'w') as fp:
252+
print('welcome = "hello REPL"', file=fp)
253+
print('print(welcome)', file=fp)
254+
else:
255+
create_text_file(open_filename, 'print("hello REPL")')
256+
self.addCleanup(unlink, open_filename)
257+
258+
for filename in ('filename', STDIN):
259+
with self.subTest(filename=filename):
260+
with support.captured_stdout() as stdout:
261+
if encode_filename:
262+
filename = filename.encode()
263+
self.assertEqual(run(open_filename, filename), 0)
264+
self.assertEqual(stdout.getvalue(), 'hello REPL\n')
265+
266+
create_text_file(open_filename, SYNTAX_ERROR)
267+
filename = 'custom filename'
268+
if encode_filename:
269+
filename_arg = filename.encode()
270+
else:
271+
filename_arg = filename
272+
with capture_excepthook() as excepthook:
273+
expected = (0 if use_loop else -1)
274+
self.assertEqual(run(open_filename, filename_arg), expected)
275+
self.assertIsInstance(excepthook.exc, SyntaxError)
276+
self.assertEqual(excepthook.exc.filename, filename)
277+
278+
create_text_file(open_filename, 'raise ValueError("BUG")')
279+
with capture_excepthook() as excepthook:
280+
expected = (0 if use_loop else -1)
281+
self.assertEqual(run(open_filename, filename_arg), expected)
282+
self.assertIsInstance(excepthook.exc, ValueError)
283+
self.assertEqual(str(excepthook.exc), 'BUG')
284+
285+
if not encode_filename:
286+
# wrong type for the second parameter (filename)
287+
with capture_excepthook() as excepthook:
288+
self.assertEqual(run(open_filename, b'bytes'), -1)
289+
self.assertIsInstance(excepthook.exc, TypeError)
290+
291+
def check_run_interactive(self, run, encode_filename, use_loop=False):
292+
# Redirect stderr to a temporary file to hide '>>> ' from the REPL
108293
try:
109-
with open(TESTFN_UNDECODABLE, 'wb') as fp:
110-
fp.write(b'a\n')
111-
self.addCleanup(unlink, TESTFN_UNDECODABLE)
294+
stderr_copy = os.dup(STDERR_FD)
112295
except OSError:
113-
self.skipTest('undecodable paths are not supported')
114-
self.assertIsNone(run(TESTFN_UNDECODABLE, Py_file_input, dict(a=1)))
296+
# On WASI, dup(STDERR_FD) fails with "OSError: [Errno 58] Not
297+
# supported". In this case, run the test without redirecting
298+
# stderr to a temporary file.
299+
self._check_run_interactive(run, encode_filename, use_loop)
300+
else:
301+
with tempfile.TemporaryFile() as tmp:
302+
try:
303+
os.dup2(tmp.fileno(), STDERR_FD)
304+
self._check_run_interactive(run, encode_filename, use_loop)
305+
finally:
306+
os.dup2(stderr_copy, STDERR_FD)
307+
308+
def test_run_interactiveone(self):
309+
# Test PyRun_InteractiveOne()
310+
run = _testcapi.run_interactiveone
311+
self.check_run_interactive(run, True)
312+
313+
def test_run_interactiveoneflags(self):
314+
# Test PyRun_InteractiveOneFlags()
315+
run = _testcapi.run_interactiveoneflags
316+
self.check_run_interactive(run, True)
317+
318+
def test_run_interactiveoneobject(self):
319+
# Test PyRun_InteractiveOneObject()
320+
run = _testcapi.run_interactiveoneobject
321+
self.check_run_interactive(run, False)
322+
323+
def test_run_interactiveloop(self):
324+
# Test PyRun_InteractiveLoop()
325+
run = _testcapi.run_interactiveloop
326+
self.check_run_interactive(run, True, use_loop=True)
327+
328+
def test_run_interactiveloopflags(self):
329+
# Test PyRun_InteractiveLoopFlags()
330+
run = _testcapi.run_interactiveloopflags
331+
self.check_run_interactive(run, True, use_loop=True)
332+
333+
def test_run_anyfileexflags(self):
334+
# Test PyRun_AnyFileExFlags()
335+
self.check_run_simplefile('run_anyfileexflags', True)
336+
337+
def check_run_simplestring(self, run):
338+
with support.captured_stdout() as stdout:
339+
run(b'print("simple hello")')
340+
self.assertEqual(stdout.getvalue(), 'simple hello\n')
341+
342+
with capture_excepthook() as excepthook:
343+
self.assertEqual(run(SYNTAX_ERROR.encode()), -1)
344+
self.assertIsInstance(excepthook.exc, SyntaxError)
345+
self.assertEqual(excepthook.exc.filename, '<string>')
346+
347+
with capture_excepthook() as excepthook:
348+
self.assertEqual(run(b'raise ValueError("BUG")'), -1)
349+
self.assertIsInstance(excepthook.exc, ValueError)
350+
self.assertEqual(str(excepthook.exc), 'BUG')
351+
352+
def test_run_simplestring(self):
353+
# Test PyRun_SimpleString()
354+
self.check_run_simplestring(_testcapi.run_simplestring)
355+
356+
def test_run_simplestringflags(self):
357+
# Test PyRun_SimpleStringFlags()
358+
self.check_run_simplestring(_testcapi.run_simplestringflags)
115359

116360

117361
if __name__ == '__main__':

0 commit comments

Comments
 (0)