summaryrefslogtreecommitdiffstats
path: root/Lib/test/test_interpreters
diff options
context:
space:
mode:
authorEric Snow <ericsnowcurrently@gmail.com>2024-04-02 23:16:50 (GMT)
committerGitHub <noreply@github.com>2024-04-02 23:16:50 (GMT)
commit857d3151c9efa029268e8249e91d26eb1b31c2fd (patch)
treef2a6b3d675c1141dee34711334e483fac6b96aeb /Lib/test/test_interpreters
parentf341d6017dd4e80509b69b5a9e2625b71b70f205 (diff)
downloadcpython-857d3151c9efa029268e8249e91d26eb1b31c2fd.zip
cpython-857d3151c9efa029268e8249e91d26eb1b31c2fd.tar.gz
cpython-857d3151c9efa029268e8249e91d26eb1b31c2fd.tar.bz2
gh-76785: Consolidate Some Interpreter-related Testing Helpers (gh-117485)
This eliminates the duplication of functionally identical helpers in the _testinternalcapi and _xxsubinterpreters modules.
Diffstat (limited to 'Lib/test/test_interpreters')
-rw-r--r--Lib/test/test_interpreters/test_api.py211
-rw-r--r--Lib/test/test_interpreters/test_queues.py6
-rw-r--r--Lib/test/test_interpreters/utils.py21
3 files changed, 231 insertions, 7 deletions
diff --git a/Lib/test/test_interpreters/test_api.py b/Lib/test/test_interpreters/test_api.py
index 3cde9bd..2aa7f9b 100644
--- a/Lib/test/test_interpreters/test_api.py
+++ b/Lib/test/test_interpreters/test_api.py
@@ -1,13 +1,14 @@
import os
import pickle
-import threading
from textwrap import dedent
+import threading
+import types
import unittest
from test import support
from test.support import import_helper
# Raise SkipTest if subinterpreters not supported.
-import_helper.import_module('_xxsubinterpreters')
+_interpreters = import_helper.import_module('_xxsubinterpreters')
from test.support import interpreters
from test.support.interpreters import InterpreterNotFoundError
from .utils import _captured_script, _run_output, _running, TestBase
@@ -932,6 +933,212 @@ class TestIsShareable(TestBase):
interpreters.is_shareable(obj))
+class LowLevelTests(TestBase):
+
+ # The behaviors in the low-level module are important in as much
+ # as they are exercised by the high-level module. Therefore the
+ # most important testing happens in the high-level tests.
+ # These low-level tests cover corner cases that are not
+ # encountered by the high-level module, thus they
+ # mostly shouldn't matter as much.
+
+ def test_new_config(self):
+ # This test overlaps with
+ # test.test_capi.test_misc.InterpreterConfigTests.
+
+ default = _interpreters.new_config('isolated')
+ with self.subTest('no arg'):
+ config = _interpreters.new_config()
+ self.assert_ns_equal(config, default)
+ self.assertIsNot(config, default)
+
+ with self.subTest('default'):
+ config1 = _interpreters.new_config('default')
+ self.assert_ns_equal(config1, default)
+ self.assertIsNot(config1, default)
+
+ config2 = _interpreters.new_config('default')
+ self.assert_ns_equal(config2, config1)
+ self.assertIsNot(config2, config1)
+
+ for arg in ['', 'default']:
+ with self.subTest(f'default ({arg!r})'):
+ config = _interpreters.new_config(arg)
+ self.assert_ns_equal(config, default)
+ self.assertIsNot(config, default)
+
+ supported = {
+ 'isolated': types.SimpleNamespace(
+ use_main_obmalloc=False,
+ allow_fork=False,
+ allow_exec=False,
+ allow_threads=True,
+ allow_daemon_threads=False,
+ check_multi_interp_extensions=True,
+ gil='own',
+ ),
+ 'legacy': types.SimpleNamespace(
+ use_main_obmalloc=True,
+ allow_fork=True,
+ allow_exec=True,
+ allow_threads=True,
+ allow_daemon_threads=True,
+ check_multi_interp_extensions=False,
+ gil='shared',
+ ),
+ 'empty': types.SimpleNamespace(
+ use_main_obmalloc=False,
+ allow_fork=False,
+ allow_exec=False,
+ allow_threads=False,
+ allow_daemon_threads=False,
+ check_multi_interp_extensions=False,
+ gil='default',
+ ),
+ }
+ gil_supported = ['default', 'shared', 'own']
+
+ for name, vanilla in supported.items():
+ with self.subTest(f'supported ({name})'):
+ expected = vanilla
+ config1 = _interpreters.new_config(name)
+ self.assert_ns_equal(config1, expected)
+ self.assertIsNot(config1, expected)
+
+ config2 = _interpreters.new_config(name)
+ self.assert_ns_equal(config2, config1)
+ self.assertIsNot(config2, config1)
+
+ with self.subTest(f'noop override ({name})'):
+ expected = vanilla
+ overrides = vars(vanilla)
+ config = _interpreters.new_config(name, **overrides)
+ self.assert_ns_equal(config, expected)
+
+ with self.subTest(f'override all ({name})'):
+ overrides = {k: not v for k, v in vars(vanilla).items()}
+ for gil in gil_supported:
+ if vanilla.gil == gil:
+ continue
+ overrides['gil'] = gil
+ expected = types.SimpleNamespace(**overrides)
+ config = _interpreters.new_config(name, **overrides)
+ self.assert_ns_equal(config, expected)
+
+ # Override individual fields.
+ for field, old in vars(vanilla).items():
+ if field == 'gil':
+ values = [v for v in gil_supported if v != old]
+ else:
+ values = [not old]
+ for val in values:
+ with self.subTest(f'{name}.{field} ({old!r} -> {val!r})'):
+ overrides = {field: val}
+ expected = types.SimpleNamespace(
+ **dict(vars(vanilla), **overrides),
+ )
+ config = _interpreters.new_config(name, **overrides)
+ self.assert_ns_equal(config, expected)
+
+ with self.subTest('extra override'):
+ with self.assertRaises(ValueError):
+ _interpreters.new_config(spam=True)
+
+ # Bad values for bool fields.
+ for field, value in vars(supported['empty']).items():
+ if field == 'gil':
+ continue
+ assert isinstance(value, bool)
+ for value in [1, '', 'spam', 1.0, None, object()]:
+ with self.subTest(f'bad override ({field}={value!r})'):
+ with self.assertRaises(TypeError):
+ _interpreters.new_config(**{field: value})
+
+ # Bad values for .gil.
+ for value in [True, 1, 1.0, None, object()]:
+ with self.subTest(f'bad override (gil={value!r})'):
+ with self.assertRaises(TypeError):
+ _interpreters.new_config(gil=value)
+ for value in ['', 'spam']:
+ with self.subTest(f'bad override (gil={value!r})'):
+ with self.assertRaises(ValueError):
+ _interpreters.new_config(gil=value)
+
+ def test_get_config(self):
+ # This test overlaps with
+ # test.test_capi.test_misc.InterpreterConfigTests.
+
+ with self.subTest('main'):
+ expected = _interpreters.new_config('legacy')
+ expected.gil = 'own'
+ interpid = _interpreters.get_main()
+ config = _interpreters.get_config(interpid)
+ self.assert_ns_equal(config, expected)
+
+ with self.subTest('isolated'):
+ expected = _interpreters.new_config('isolated')
+ interpid = _interpreters.create('isolated')
+ config = _interpreters.get_config(interpid)
+ self.assert_ns_equal(config, expected)
+
+ with self.subTest('legacy'):
+ expected = _interpreters.new_config('legacy')
+ interpid = _interpreters.create('legacy')
+ config = _interpreters.get_config(interpid)
+ self.assert_ns_equal(config, expected)
+
+ def test_create(self):
+ isolated = _interpreters.new_config('isolated')
+ legacy = _interpreters.new_config('legacy')
+ default = isolated
+
+ with self.subTest('no arg'):
+ interpid = _interpreters.create()
+ config = _interpreters.get_config(interpid)
+ self.assert_ns_equal(config, default)
+
+ with self.subTest('arg: None'):
+ interpid = _interpreters.create(None)
+ config = _interpreters.get_config(interpid)
+ self.assert_ns_equal(config, default)
+
+ with self.subTest('arg: \'empty\''):
+ with self.assertRaises(RuntimeError):
+ # The "empty" config isn't viable on its own.
+ _interpreters.create('empty')
+
+ for arg, expected in {
+ '': default,
+ 'default': default,
+ 'isolated': isolated,
+ 'legacy': legacy,
+ }.items():
+ with self.subTest(f'str arg: {arg!r}'):
+ interpid = _interpreters.create(arg)
+ config = _interpreters.get_config(interpid)
+ self.assert_ns_equal(config, expected)
+
+ with self.subTest('custom'):
+ orig = _interpreters.new_config('empty')
+ orig.use_main_obmalloc = True
+ orig.gil = 'shared'
+ interpid = _interpreters.create(orig)
+ config = _interpreters.get_config(interpid)
+ self.assert_ns_equal(config, orig)
+
+ with self.subTest('missing fields'):
+ orig = _interpreters.new_config()
+ del orig.gil
+ with self.assertRaises(ValueError):
+ _interpreters.create(orig)
+
+ with self.subTest('extra fields'):
+ orig = _interpreters.new_config()
+ orig.spam = True
+ with self.assertRaises(ValueError):
+ _interpreters.create(orig)
+
+
if __name__ == '__main__':
# Test needs to be a package, so we can do relative imports.
unittest.main()
diff --git a/Lib/test/test_interpreters/test_queues.py b/Lib/test/test_interpreters/test_queues.py
index d16d294..8ab9ebb 100644
--- a/Lib/test/test_interpreters/test_queues.py
+++ b/Lib/test/test_interpreters/test_queues.py
@@ -28,9 +28,9 @@ class TestBase(_TestBase):
class LowLevelTests(TestBase):
- # The behaviors in the low-level module is important in as much
- # as it is exercised by the high-level module. Therefore the
- # most # important testing happens in the high-level tests.
+ # The behaviors in the low-level module are important in as much
+ # as they are exercised by the high-level module. Therefore the
+ # most important testing happens in the high-level tests.
# These low-level tests cover corner cases that are not
# encountered by the high-level module, thus they
# mostly shouldn't matter as much.
diff --git a/Lib/test/test_interpreters/utils.py b/Lib/test/test_interpreters/utils.py
index 973d05d..5ade676 100644
--- a/Lib/test/test_interpreters/utils.py
+++ b/Lib/test/test_interpreters/utils.py
@@ -68,6 +68,9 @@ def _running(interp):
class TestBase(unittest.TestCase):
+ def tearDown(self):
+ clean_up_interpreters()
+
def pipe(self):
def ensure_closed(fd):
try:
@@ -156,5 +159,19 @@ class TestBase(unittest.TestCase):
self.assertNotEqual(exitcode, 0)
return stdout, stderr
- def tearDown(self):
- clean_up_interpreters()
+ def assert_ns_equal(self, ns1, ns2, msg=None):
+ # This is mostly copied from TestCase.assertDictEqual.
+ self.assertEqual(type(ns1), type(ns2))
+ if ns1 == ns2:
+ return
+
+ import difflib
+ import pprint
+ from unittest.util import _common_shorten_repr
+ standardMsg = '%s != %s' % _common_shorten_repr(ns1, ns2)
+ diff = ('\n' + '\n'.join(difflib.ndiff(
+ pprint.pformat(vars(ns1)).splitlines(),
+ pprint.pformat(vars(ns2)).splitlines())))
+ diff = f'namespace({diff})'
+ standardMsg = self._truncateMessage(standardMsg, diff)
+ self.fail(self._formatMessage(msg, standardMsg))