From 153222a0ebe768dfced78b29be3c5d5f505c5c00 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 16 Jun 2020 12:09:19 -0600 Subject: [PATCH 1/6] Clean up test.test_interpreters.TestSendRecv. --- Lib/test/support/interpreters.py | 2 +- Lib/test/test_interpreters.py | 48 +++++++++++++++----------------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/Lib/test/support/interpreters.py b/Lib/test/support/interpreters.py index 09508e1bbeca03..aed7892ccdf7c4 100644 --- a/Lib/test/support/interpreters.py +++ b/Lib/test/support/interpreters.py @@ -91,7 +91,7 @@ def run(self, src_str, /, *, channels=None): Run the given source code in the interpreter. This blocks the current Python thread until done. """ - _interpreters.run_string(self._id, src_str) + _interpreters.run_string(self._id, src_str, channels) def create_channel(): diff --git a/Lib/test/test_interpreters.py b/Lib/test/test_interpreters.py index 3451a4c8759d8b..6eb0a2da16ea44 100644 --- a/Lib/test/test_interpreters.py +++ b/Lib/test/test_interpreters.py @@ -31,10 +31,10 @@ def clean_up_interpreters(): pass # already destroyed -def _run_output(interp, request, shared=None): +def _run_output(interp, request, channels=None): script, rpipe = _captured_script(request) with rpipe: - interp.run(script) + interp.run(script, channels=channels) return rpipe.read() @@ -464,7 +464,7 @@ class TestSendRecv(TestBase): def test_send_recv_main(self): r, s = interpreters.create_channel() orig = b'spam' - s.send(orig) + s.send_nowait(orig) obj = r.recv() self.assertEqual(obj, orig) @@ -472,14 +472,14 @@ def test_send_recv_main(self): def test_send_recv_same_interpreter(self): interp = interpreters.create() - out = _run_output(interp, dedent(""" + interp.run(dedent(""" from test.support import interpreters r, s = interpreters.create_channel() orig = b'spam' - s.send(orig) + s.send_nowait(orig) obj = r.recv() - assert obj is not orig - assert obj == orig + assert obj == orig, 'expected: obj == orig' + assert obj is not orig, 'expected: obj is not orig' """)) def test_send_recv_different_threads(self): @@ -496,40 +496,38 @@ def f(): t = threading.Thread(target=f) t.start() - s.send(b'spam') + orig = b'spam' + s.send(orig) t.join() obj = r.recv() - self.assertEqual(obj, b'spam') + self.assertEqual(obj, orig) + self.assertIsNot(obj, orig) def test_send_recv_nowait_main(self): r, s = interpreters.create_channel() orig = b'spam' - s.send(orig) + s.send_nowait(orig) obj = r.recv_nowait() self.assertEqual(obj, orig) self.assertIsNot(obj, orig) + def test_send_recv_nowait_main_with_default(self): + r, _ = interpreters.create_channel() + obj = r.recv_nowait(None) + + self.assertIsNone(obj) + def test_send_recv_nowait_same_interpreter(self): interp = interpreters.create() - out = _run_output(interp, dedent(""" + interp.run(dedent(""" from test.support import interpreters r, s = interpreters.create_channel() orig = b'spam' - s.send(orig) + s.send_nowait(orig) obj = r.recv_nowait() - assert obj is not orig - assert obj == orig + assert obj == orig, 'expected: obj == orig' + # When going back to the same interpreter we get the same object. + assert obj is not orig, 'expected: obj is not orig' """)) - - r, s = interpreters.create_channel() - - def f(): - while True: - try: - obj = r.recv_nowait() - break - except _interpreters.ChannelEmptyError: - time.sleep(0.1) - s.send(obj) From 09d572a123cb7136601a02e8643c5a835d14ca09 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 16 Jun 2020 12:55:07 -0600 Subject: [PATCH 2/6] Add TestSendRecv tests for different interpreters. --- Lib/test/test_interpreters.py | 48 +++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Lib/test/test_interpreters.py b/Lib/test/test_interpreters.py index 6eb0a2da16ea44..0d561445f275f8 100644 --- a/Lib/test/test_interpreters.py +++ b/Lib/test/test_interpreters.py @@ -482,6 +482,30 @@ def test_send_recv_same_interpreter(self): assert obj is not orig, 'expected: obj is not orig' """)) + @unittest.skip('broken (see BPO-...)') + def test_send_recv_different_interpreters(self): + r1, s1 = interpreters.create_channel() + r2, s2 = interpreters.create_channel() + orig1 = b'spam' + s1.send_nowait(orig1) + out = _run_output( + interpreters.create(), + dedent(f""" + obj1 = r.recv() + assert obj1 == b'spam', 'expected: obj1 == orig1' + # When going to another interpreter we get a copy. + assert id(obj1) != {id(orig1)}, 'expected: obj1 is not orig1' + orig2 = b'eggs' + print(id(orig2)) + s.send_nowait(orig2) + """), + channels=dict(r=r1, s=s2), + ) + obj2 = r2.recv() + + self.assertEqual(obj2, b'eggs') + self.assertNotEqual(id(obj2), int(out)) + def test_send_recv_different_threads(self): r, s = interpreters.create_channel() @@ -531,3 +555,27 @@ def test_send_recv_nowait_same_interpreter(self): # When going back to the same interpreter we get the same object. assert obj is not orig, 'expected: obj is not orig' """)) + + @unittest.skip('broken (see BPO-...)') + def test_send_recv_nowait_different_interpreters(self): + r1, s1 = interpreters.create_channel() + r2, s2 = interpreters.create_channel() + orig1 = b'spam' + s1.send_nowait(orig1) + out = _run_output( + interpreters.create(), + dedent(f""" + obj1 = r.recv_nowait() + assert obj1 == b'spam', 'expected: obj1 == orig1' + # When going to another interpreter we get a copy. + assert id(obj1) != {id(orig1)}, 'expected: obj1 is not orig1' + orig2 = b'eggs' + print(id(orig2)) + s.send_nowait(orig2) + """), + channels=dict(r=r1, s=s2), + ) + obj2 = r2.recv_nowait() + + self.assertEqual(obj2, b'eggs') + self.assertNotEqual(id(obj2), int(out)) From 5f04304d4f1cba82c9b09ef0c2b06ed0d5be3370 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 16 Jun 2020 13:45:02 -0600 Subject: [PATCH 3/6] Clean up interpreters.py a bit. --- Lib/test/support/interpreters.py | 167 ++++++++++++++++--------------- 1 file changed, 89 insertions(+), 78 deletions(-) diff --git a/Lib/test/support/interpreters.py b/Lib/test/support/interpreters.py index aed7892ccdf7c4..72f59f0d18db12 100644 --- a/Lib/test/support/interpreters.py +++ b/Lib/test/support/interpreters.py @@ -1,5 +1,6 @@ """Subinterpreters High Level Module.""" +import time import _xxsubinterpreters as _interpreters # aliases: @@ -19,47 +20,51 @@ def create(*, isolated=True): - """ - Initialize a new (idle) Python interpreter. - """ + """Return a new (idle) Python interpreter.""" id = _interpreters.create(isolated=isolated) return Interpreter(id, isolated=isolated) def list_all(): - """ - Get all existing interpreters. - """ - return [Interpreter(id) for id in - _interpreters.list_all()] + """Return all existing interpreters.""" + return [Interpreter(id) for id in _interpreters.list_all()] def get_current(): - """ - Get the currently running interpreter. - """ + """Return the currently running interpreter.""" id = _interpreters.get_current() return Interpreter(id) def get_main(): - """ - Get the main interpreter. - """ + """Return the main interpreter.""" id = _interpreters.get_main() return Interpreter(id) class Interpreter: - """ - The Interpreter object represents - a single interpreter. - """ + """A single Python interpreter.""" def __init__(self, id, *, isolated=None): + if not isinstance(id, (int, _interpreters.InterpreterID)): + raise TypeError(f'id must be an int, got {id!r}') self._id = id self._isolated = isolated + def __repr__(self): + data = dict(id=int(self._id), isolated=self._isolated) + kwargs = (f'{k}={v!r}' for k, v in data.items()) + return f'{type(self).__name__}({", ".join(kwargs)})' + + def __hash__(self): + return hash(self._id) + + def __eq__(self, other): + if not isinstance(other, Interpreter): + return NotImplemented + else: + return other._id == self._id + @property def id(self): return self._id @@ -67,84 +72,97 @@ def id(self): @property def isolated(self): if self._isolated is None: + # XXX The low-level function has not been added yet. self._isolated = _interpreters.is_isolated(self._id) return self._isolated def is_running(self): - """ - Return whether or not the identified - interpreter is running. - """ + """Return whether or not the identified interpreter is running.""" return _interpreters.is_running(self._id) def close(self): - """ - Finalize and destroy the interpreter. + """Finalize and destroy the interpreter. - Attempting to destroy the current - interpreter results in a RuntimeError. + Attempting to destroy the current interpreter results + in a RuntimeError. """ return _interpreters.destroy(self._id) def run(self, src_str, /, *, channels=None): - """ - Run the given source code in the interpreter. + """Run the given source code in the interpreter. + This blocks the current Python thread until done. """ _interpreters.run_string(self._id, src_str, channels) def create_channel(): + """Return (recv, send) for a new cross-interpreter channel. + + The channel may be used to pass data safely between interpreters. """ - Create a new channel for passing data between - interpreters. - """ - cid = _interpreters.channel_create() - return (RecvChannel(cid), SendChannel(cid)) + recv, send = RecvChannel(cid), SendChannel(cid) + return recv, send def list_all_channels(): - """ - Get all open channels. - """ + """Return a list of (recv, send) for all open channels.""" return [(RecvChannel(cid), SendChannel(cid)) for cid in _interpreters.channel_list_all()] +class _ChannelEnd: + """The base class for RecvChannel and SendChannel.""" + + def __init__(self, id): + if not isinstance(id, (int, _interpreters.ChannelID)): + raise TypeError(f'id must be an int, got {id!r}') + self._id = id + + def __repr__(self): + return f'{type(self).__name__}(id={int(self._id)})' + + def __hash__(self): + return hash(self._id) + + def __eq__(self, other): + if isinstance(self, RecvChannel): + if not isinstance(other, RecvChannel): + return NotImplemented + elif not isinstance(other, SendChannel): + return NotImplemented + return other._id == self._id + + @property + def id(self): + return self._id + + _NOT_SET = object() -class RecvChannel: - """ - The RecvChannel object represents - a receiving channel. - """ +class RecvChannel(_ChannelEnd): + """The receiving end of a cross-interpreter channel.""" - def __init__(self, id): - self._id = id + def recv(self, *, _sentinel=object(), _delay=10 / 1000): # 10 milliseconds + """Return the next object from the channel. - def recv(self, *, _delay=10 / 1000): # 10 milliseconds + This blocks until an object has been sent, if none have been + sent already. """ - Get the next object from the channel, - and wait if none have been sent. - Associate the interpreter with the channel. - """ - import time - sentinel = object() - obj = _interpreters.channel_recv(self._id, sentinel) - while obj is sentinel: + obj = _interpreters.channel_recv(self._id, _sentinel) + while obj is _sentinel: time.sleep(_delay) - obj = _interpreters.channel_recv(self._id, sentinel) + obj = _interpreters.channel_recv(self._id, _sentinel) return obj def recv_nowait(self, default=_NOT_SET): - """ - Like recv(), but return the default - instead of waiting. + """Return the next object from the channel. - This function is blocked by a missing low-level - implementation of channel_recv_wait(). + If none have been sent then return the default if one + is provided or fail with ChannelEmptyError. Otherwise this + is the same as recv(). """ if default is _NOT_SET: return _interpreters.channel_recv(self._id) @@ -152,32 +170,25 @@ def recv_nowait(self, default=_NOT_SET): return _interpreters.channel_recv(self._id, default) -class SendChannel: - """ - The SendChannel object represents - a sending channel. - """ - - def __init__(self, id): - self._id = id +class SendChannel(_ChannelEnd): + """The sending end of a cross-interpreter channel.""" def send(self, obj): + """Send the object (i.e. its data) to the channel's receiving end. + + This blocks until the object is received. """ - Send the object (i.e. its data) to the receiving - end of the channel and wait. Associate the interpreter - with the channel. - """ - import time _interpreters.channel_send(self._id, obj) + # XXX We are missing a low-level channel_send_wait(). + # Until that shows up we fake it: time.sleep(2) def send_nowait(self, obj): - """ - Like send(), but return False if not received. + """Send the object to the channel's receiving end. - This function is blocked by a missing low-level - implementation of channel_send_wait(). + If the object is immediately received then return True + (else False). Otherwise this is the same as send(). """ - - _interpreters.channel_send(self._id, obj) - return False + # XXX Note that at the moment channel_send() only ever returns + # None. This should be fixed when channel_send_wait() is added. + return _interpreters.channel_send(self._id, obj) From 4168c224d3e18164251960bf2a300137101ddf0c Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 16 Jun 2020 17:14:52 -0600 Subject: [PATCH 4/6] Clean up test.test_interpreters a bit. --- Lib/test/test__xxsubinterpreters.py | 12 - Lib/test/test_interpreters.py | 339 ++++++++++++++++++++-------- 2 files changed, 250 insertions(+), 101 deletions(-) diff --git a/Lib/test/test__xxsubinterpreters.py b/Lib/test/test__xxsubinterpreters.py index 7aec021fb19a5e..550a847616cdc5 100644 --- a/Lib/test/test__xxsubinterpreters.py +++ b/Lib/test/test__xxsubinterpreters.py @@ -759,21 +759,9 @@ def test_still_running(self): class RunStringTests(TestBase): - SCRIPT = dedent(""" - with open('{}', 'w') as out: - out.write('{}') - """) - FILENAME = 'spam' - def setUp(self): super().setUp() self.id = interpreters.create() - self._fs = None - - def tearDown(self): - if self._fs is not None: - self._fs.close() - super().tearDown() def test_success(self): script, file = _captured_script('print("it worked!", end="")') diff --git a/Lib/test/test_interpreters.py b/Lib/test/test_interpreters.py index 0d561445f275f8..d29fc363c2742c 100644 --- a/Lib/test/test_interpreters.py +++ b/Lib/test/test_interpreters.py @@ -68,25 +68,22 @@ class CreateTests(TestBase): def test_in_main(self): interp = interpreters.create() - lst = interpreters.list_all() - self.assertEqual(interp.id, lst[1].id) + self.assertIsInstance(interp, interpreters.Interpreter) + self.assertIn(interp, interpreters.list_all()) def test_in_thread(self): lock = threading.Lock() - id = None - interp = interpreters.create() - lst = interpreters.list_all() + interp = None def f(): - nonlocal id - id = interp.id + nonlocal interp + interp = interpreters.create() lock.acquire() lock.release() - t = threading.Thread(target=f) with lock: t.start() t.join() - self.assertEqual(interp.id, lst[1].id) + self.assertIn(interp, interpreters.list_all()) def test_in_subinterpreter(self): main, = interpreters.list_all() @@ -94,11 +91,10 @@ def test_in_subinterpreter(self): out = _run_output(interp, dedent(""" from test.support import interpreters interp = interpreters.create() - print(interp) + print(interp.id) """)) - interp2 = out.strip() - - self.assertEqual(len(set(interpreters.list_all())), len({main, interp, interp2})) + interp2 = interpreters.Interpreter(int(out)) + self.assertEqual(interpreters.list_all(), [main, interp, interp2]) def test_after_destroy_all(self): before = set(interpreters.list_all()) @@ -112,7 +108,7 @@ def test_after_destroy_all(self): interp.close() # Finally, create another. interp = interpreters.create() - self.assertEqual(len(set(interpreters.list_all())), len(before | {interp})) + self.assertEqual(set(interpreters.list_all()), before | {interp}) def test_after_destroy_some(self): before = set(interpreters.list_all()) @@ -125,15 +121,15 @@ def test_after_destroy_some(self): interp2.close() # Finally, create another. interp = interpreters.create() - self.assertEqual(len(set(interpreters.list_all())), len(before | {interp3, interp})) + self.assertEqual(set(interpreters.list_all()), before | {interp3, interp}) class GetCurrentTests(TestBase): def test_main(self): - main_interp_id = _interpreters.get_main() - cur_interp_id = interpreters.get_current().id - self.assertEqual(cur_interp_id, main_interp_id) + main = interpreters.get_main() + current = interpreters.get_current() + self.assertEqual(current, main) def test_subinterpreter(self): main = _interpreters.get_main() @@ -141,10 +137,10 @@ def test_subinterpreter(self): out = _run_output(interp, dedent(""" from test.support import interpreters cur = interpreters.get_current() - print(cur) + print(cur.id) """)) - cur = out.strip() - self.assertNotEqual(cur, main) + current = interpreters.Interpreter(int(out)) + self.assertNotEqual(current, main) class ListAllTests(TestBase): @@ -177,26 +173,75 @@ def test_after_destroying(self): self.assertEqual(ids, [main.id, second.id]) -class TestInterpreterId(TestBase): +class TestInterpreterAttrs(TestBase): - def test_in_main(self): - main = interpreters.get_current() - self.assertEqual(0, main.id) + def test_id_type(self): + main = interpreters.get_main() + current = interpreters.get_current() + interp = interpreters.create() + self.assertIsInstance(main.id, _interpreters.InterpreterID) + self.assertIsInstance(current.id, _interpreters.InterpreterID) + self.assertIsInstance(interp.id, _interpreters.InterpreterID) + + def test_main_id(self): + main = interpreters.get_main() + self.assertEqual(main.id, 0) - def test_with_custom_num(self): + def test_custom_id(self): interp = interpreters.Interpreter(1) - self.assertEqual(1, interp.id) + self.assertEqual(interp.id, 1) + + with self.assertRaises(TypeError): + interpreters.Interpreter('1') - def test_for_readonly_property(self): + def test_id_readonly(self): interp = interpreters.Interpreter(1) with self.assertRaises(AttributeError): interp.id = 2 + @unittest.skip('not ready yet (see BPO-...)') + def test_main_isolated(self): + main = interpreters.get_main() + self.assertFalse(main.isolated) + + @unittest.skip('not ready yet (see BPO-...)') + def test_subinterpreter_isolated_default(self): + interp = interpreters.create() + self.assertFalse(interp.isolated) + + def test_subinterpreter_isolated_explicit(self): + interp1 = interpreters.create(isolated=True) + interp2 = interpreters.create(isolated=False) + self.assertTrue(interp1.isolated) + self.assertFalse(interp2.isolated) + + @unittest.skip('not ready yet (see BPO-...)') + def test_custom_isolated_default(self): + interp = interpreters.Interpreter(1) + self.assertFalse(interp.isolated) + + def test_custom_isolated_explicit(self): + interp1 = interpreters.Interpreter(1, isolated=True) + interp2 = interpreters.Interpreter(1, isolated=False) + self.assertTrue(interp1.isolated) + self.assertFalse(interp2.isolated) + + def test_isolated_readonly(self): + interp = interpreters.Interpreter(1) + with self.assertRaises(AttributeError): + interp.isolated = True + + def test_equality(self): + interp1 = interpreters.create() + interp2 = interpreters.create() + self.assertEqual(interp1, interp1) + self.assertNotEqual(interp1, interp2) + class TestInterpreterIsRunning(TestBase): def test_main(self): - main = interpreters.get_current() + main = interpreters.get_main() self.assertTrue(main.is_running()) def test_subinterpreter(self): @@ -224,16 +269,29 @@ def test_already_destroyed(self): with self.assertRaises(RuntimeError): interp.is_running() + def test_does_not_exist(self): + interp = interpreters.Interpreter(1_000_000) + with self.assertRaises(RuntimeError): + interp.is_running() + + def test_bad_id(self): + interp = interpreters.Interpreter(-1) + with self.assertRaises(ValueError): + interp.is_running() -class TestInterpreterDestroy(TestBase): + +class TestInterpreterClose(TestBase): def test_basic(self): + main = interpreters.get_main() interp1 = interpreters.create() interp2 = interpreters.create() interp3 = interpreters.create() - self.assertEqual(4, len(interpreters.list_all())) + self.assertEqual(set(interpreters.list_all()), + {main, interp1, interp2, interp3}) interp2.close() - self.assertEqual(3, len(interpreters.list_all())) + self.assertEqual(set(interpreters.list_all()), + {main, interp1, interp3}) def test_all(self): before = set(interpreters.list_all()) @@ -241,10 +299,10 @@ def test_all(self): for _ in range(3): interp = interpreters.create() interps.add(interp) - self.assertEqual(len(set(interpreters.list_all())), len(before | interps)) + self.assertEqual(set(interpreters.list_all()), before | interps) for interp in interps: interp.close() - self.assertEqual(len(set(interpreters.list_all())), len(before)) + self.assertEqual(set(interpreters.list_all()), before) def test_main(self): main, = interpreters.list_all() @@ -265,32 +323,44 @@ def test_already_destroyed(self): with self.assertRaises(RuntimeError): interp.close() + def test_does_not_exist(self): + interp = interpreters.Interpreter(1_000_000) + with self.assertRaises(RuntimeError): + interp.close() + + def test_bad_id(self): + interp = interpreters.Interpreter(-1) + with self.assertRaises(ValueError): + interp.close() + def test_from_current(self): main, = interpreters.list_all() interp = interpreters.create() - script = dedent(f""" + out = _run_output(interp, dedent(f""" from test.support import interpreters + interp = interpreters.Interpreter({int(interp.id)}) try: - main = interpreters.get_current() - main.close() + interp.close() except RuntimeError: - pass - """) - - interp.run(script) - self.assertEqual(len(set(interpreters.list_all())), len({main, interp})) + print('failed') + """)) + self.assertEqual(out.strip(), 'failed') + self.assertEqual(set(interpreters.list_all()), {main, interp}) def test_from_sibling(self): main, = interpreters.list_all() interp1 = interpreters.create() - script = dedent(f""" + interp2 = interpreters.create() + self.assertEqual(set(interpreters.list_all()), + {main, interp1, interp2}) + interp1.run(dedent(f""" from test.support import interpreters - interp2 = interpreters.create() + interp2 = interpreters.Interpreter(int({interp2.id})) interp2.close() - """) - interp1.run(script) - - self.assertEqual(len(set(interpreters.list_all())), len({main, interp1})) + interp3 = interpreters.create() + interp3.close() + """)) + self.assertEqual(set(interpreters.list_all()), {main, interp1}) def test_from_other_thread(self): interp = interpreters.create() @@ -312,41 +382,21 @@ def test_still_running(self): class TestInterpreterRun(TestBase): - SCRIPT = dedent(""" - with open('{}', 'w') as out: - out.write('{}') - """) - FILENAME = 'spam' - - def setUp(self): - super().setUp() - self.interp = interpreters.create() - self._fs = None - - def tearDown(self): - if self._fs is not None: - self._fs.close() - super().tearDown() - - @property - def fs(self): - if self._fs is None: - self._fs = FSFixture(self) - return self._fs - def test_success(self): + interp = interpreters.create() script, file = _captured_script('print("it worked!", end="")') with file: - self.interp.run(script) + interp.run(script) out = file.read() self.assertEqual(out, 'it worked!') def test_in_thread(self): + interp = interpreters.create() script, file = _captured_script('print("it worked!", end="")') with file: def f(): - self.interp.run(script) + interp.run(script) t = threading.Thread(target=f) t.start() @@ -357,6 +407,7 @@ def f(): @unittest.skipUnless(hasattr(os, 'fork'), "test needs os.fork()") def test_fork(self): + interp = interpreters.create() import tempfile with tempfile.NamedTemporaryFile('w+') as file: file.write('') @@ -371,24 +422,39 @@ def test_fork(self): with open('{file.name}', 'w') as out: out.write('{expected}') """) - self.interp.run(script) + interp.run(script) file.seek(0) content = file.read() self.assertEqual(content, expected) def test_already_running(self): - with _running(self.interp): + interp = interpreters.create() + with _running(interp): with self.assertRaises(RuntimeError): - self.interp.run('print("spam")') + interp.run('print("spam")') + + def test_does_not_exist(self): + interp = interpreters.Interpreter(1_000_000) + with self.assertRaises(RuntimeError): + interp.run('print("spam")') + + def test_bad_id(self): + interp = interpreters.Interpreter(-1) + with self.assertRaises(ValueError): + interp.run('print("spam")') def test_bad_script(self): + interp = interpreters.create() with self.assertRaises(TypeError): - self.interp.run(10) + interp.run(10) def test_bytes_for_script(self): + interp = interpreters.create() with self.assertRaises(TypeError): - self.interp.run(b'print("spam")') + interp.run(b'print("spam")') + + # test_xxsubinterpreters covers the remaining Interpreter.run() behavior. class TestIsShareable(TestBase): @@ -405,8 +471,8 @@ def test_default_shareables(self): ] for obj in shareables: with self.subTest(obj): - self.assertTrue( - interpreters.is_shareable(obj)) + shareable = interpreters.is_shareable(obj) + self.assertTrue(shareable) def test_not_shareable(self): class Cheese: @@ -441,22 +507,71 @@ class SubBytes(bytes): interpreters.is_shareable(obj)) -class TestChannel(TestBase): +class TestChannels(TestBase): - def test_create_cid(self): + def test_create(self): r, s = interpreters.create_channel() self.assertIsInstance(r, interpreters.RecvChannel) self.assertIsInstance(s, interpreters.SendChannel) - def test_sequential_ids(self): - before = interpreters.list_all_channels() - channels1 = interpreters.create_channel() - channels2 = interpreters.create_channel() - channels3 = interpreters.create_channel() - after = interpreters.list_all_channels() + def test_list_all(self): + self.assertEqual(interpreters.list_all_channels(), []) + created = set() + for _ in range(3): + ch = interpreters.create_channel() + created.add(ch) + after = set(interpreters.list_all_channels()) + self.assertEqual(after, created) + + +class TestRecvChannelAttrs(TestBase): + + def test_id_type(self): + rch, _ = interpreters.create_channel() + self.assertIsInstance(rch.id, _interpreters.ChannelID) + + def test_custom_id(self): + rch = interpreters.RecvChannel(1) + self.assertEqual(rch.id, 1) + + with self.assertRaises(TypeError): + interpreters.RecvChannel('1') + + def test_id_readonly(self): + rch = interpreters.RecvChannel(1) + with self.assertRaises(AttributeError): + rch.id = 2 + + def test_equality(self): + ch1, _ = interpreters.create_channel() + ch2, _ = interpreters.create_channel() + self.assertEqual(ch1, ch1) + self.assertNotEqual(ch1, ch2) - self.assertEqual(len(set(after) - set(before)), - len({channels1, channels2, channels3})) + +class TestSendChannelAttrs(TestBase): + + def test_id_type(self): + _, sch = interpreters.create_channel() + self.assertIsInstance(sch.id, _interpreters.ChannelID) + + def test_custom_id(self): + sch = interpreters.SendChannel(1) + self.assertEqual(sch.id, 1) + + with self.assertRaises(TypeError): + interpreters.SendChannel('1') + + def test_id_readonly(self): + sch = interpreters.SendChannel(1) + with self.assertRaises(AttributeError): + sch.id = 2 + + def test_equality(self): + _, ch1 = interpreters.create_channel() + _, ch2 = interpreters.create_channel() + self.assertEqual(ch1, ch1) + self.assertNotEqual(ch1, ch2) class TestSendRecv(TestBase): @@ -579,3 +694,49 @@ def test_send_recv_nowait_different_interpreters(self): self.assertEqual(obj2, b'eggs') self.assertNotEqual(id(obj2), int(out)) + + def test_recv_channel_does_not_exist(self): + ch = interpreters.RecvChannel(1_000_000) + with self.assertRaises(interpreters.ChannelNotFoundError): + ch.recv() + + def test_send_channel_does_not_exist(self): + ch = interpreters.SendChannel(1_000_000) + with self.assertRaises(interpreters.ChannelNotFoundError): + ch.send(b'spam') + + def test_recv_nowait_channel_does_not_exist(self): + ch = interpreters.RecvChannel(1_000_000) + with self.assertRaises(interpreters.ChannelNotFoundError): + ch.recv_nowait() + + def test_send_nowait_channel_does_not_exist(self): + ch = interpreters.SendChannel(1_000_000) + with self.assertRaises(interpreters.ChannelNotFoundError): + ch.send_nowait(b'spam') + + def test_recv_nowait_empty(self): + ch, _ = interpreters.create_channel() + with self.assertRaises(interpreters.ChannelEmptyError): + ch.recv_nowait() + + def test_recv_nowait_default(self): + default = object() + rch, sch = interpreters.create_channel() + obj1 = rch.recv_nowait(default) + sch.send_nowait(None) + sch.send_nowait(1) + sch.send_nowait(b'spam') + sch.send_nowait(b'eggs') + obj2 = rch.recv_nowait(default) + obj3 = rch.recv_nowait(default) + obj4 = rch.recv_nowait() + obj5 = rch.recv_nowait(default) + obj6 = rch.recv_nowait(default) + + self.assertIs(obj1, default) + self.assertIs(obj2, None) + self.assertEqual(obj3, 1) + self.assertEqual(obj4, b'spam') + self.assertEqual(obj5, b'eggs') + self.assertIs(obj6, default) From 5f41b028d3fc63a4b607e7c11a21f48d06799767 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 16 Jun 2020 17:25:17 -0600 Subject: [PATCH 5/6] Fill in some BPO numbers. --- Lib/test/support/interpreters.py | 3 +++ Lib/test/test_interpreters.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Lib/test/support/interpreters.py b/Lib/test/support/interpreters.py index 72f59f0d18db12..46e460eaf6cff0 100644 --- a/Lib/test/support/interpreters.py +++ b/Lib/test/support/interpreters.py @@ -73,6 +73,7 @@ def id(self): def isolated(self): if self._isolated is None: # XXX The low-level function has not been added yet. + # See bpo-.... self._isolated = _interpreters.is_isolated(self._id) return self._isolated @@ -180,6 +181,7 @@ def send(self, obj): """ _interpreters.channel_send(self._id, obj) # XXX We are missing a low-level channel_send_wait(). + # See bpo-32604 and gh-19829. # Until that shows up we fake it: time.sleep(2) @@ -191,4 +193,5 @@ def send_nowait(self, obj): """ # XXX Note that at the moment channel_send() only ever returns # None. This should be fixed when channel_send_wait() is added. + # See bpo-32604 and gh-19829. return _interpreters.channel_send(self._id, obj) diff --git a/Lib/test/test_interpreters.py b/Lib/test/test_interpreters.py index d29fc363c2742c..58258bb66af8a8 100644 --- a/Lib/test/test_interpreters.py +++ b/Lib/test/test_interpreters.py @@ -199,12 +199,12 @@ def test_id_readonly(self): with self.assertRaises(AttributeError): interp.id = 2 - @unittest.skip('not ready yet (see BPO-...)') + @unittest.skip('not ready yet (see bpo-32604)') def test_main_isolated(self): main = interpreters.get_main() self.assertFalse(main.isolated) - @unittest.skip('not ready yet (see BPO-...)') + @unittest.skip('not ready yet (see bpo-32604)') def test_subinterpreter_isolated_default(self): interp = interpreters.create() self.assertFalse(interp.isolated) @@ -215,7 +215,7 @@ def test_subinterpreter_isolated_explicit(self): self.assertTrue(interp1.isolated) self.assertFalse(interp2.isolated) - @unittest.skip('not ready yet (see BPO-...)') + @unittest.skip('not ready yet (see bpo-32604)') def test_custom_isolated_default(self): interp = interpreters.Interpreter(1) self.assertFalse(interp.isolated) From 72d4838d3b763c0bbb8893d352e5428275c7e6e3 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 16 Jun 2020 17:42:23 -0600 Subject: [PATCH 6/6] Fix whitespace. --- Lib/test/support/interpreters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/support/interpreters.py b/Lib/test/support/interpreters.py index 46e460eaf6cff0..2935708f9df1a5 100644 --- a/Lib/test/support/interpreters.py +++ b/Lib/test/support/interpreters.py @@ -99,7 +99,7 @@ def run(self, src_str, /, *, channels=None): def create_channel(): """Return (recv, send) for a new cross-interpreter channel. - + The channel may be used to pass data safely between interpreters. """ cid = _interpreters.channel_create()