import contextlib
import errno
import importlib
import itertools
import inspect
import io
import logging
import os
import shutil
import signal
import socket
import stat
import subprocess
import sys
import sysconfig
import tempfile
import textwrap
import unittest
import warnings

from test import support
from test.support import hashlib_helper
from test.support import import_helper
from test.support import os_helper
from test.support import script_helper
from test.support import socket_helper
from test.support import warnings_helper

TESTFN = os_helper.TESTFN


class LogCaptureHandler(logging.StreamHandler):
    # Inspired by pytest's caplog
    def __init__(self):
        super().__init__(io.StringIO())
        self.records = []

    def emit(self, record) -> None:
        self.records.append(record)
        super().emit(record)

    def handleError(self, record):
        raise


@contextlib.contextmanager
def _caplog():
    handler = LogCaptureHandler()
    root_logger = logging.getLogger()
    root_logger.addHandler(handler)
    try:
        yield handler
    finally:
        root_logger.removeHandler(handler)


class TestSupport(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        orig_filter_len = len(warnings._get_filters())
        cls._warnings_helper_token = support.ignore_deprecations_from(
            "test.support.warnings_helper", like=".*used in test_support.*"
        )
        cls._test_support_token = support.ignore_deprecations_from(
            __name__, like=".*You should NOT be seeing this.*"
        )
        assert len(warnings._get_filters()) == orig_filter_len + 2

    @classmethod
    def tearDownClass(cls):
        orig_filter_len = len(warnings._get_filters())
        support.clear_ignored_deprecations(
            cls._warnings_helper_token,
            cls._test_support_token,
        )
        assert len(warnings._get_filters()) == orig_filter_len - 2

    def test_ignored_deprecations_are_silent(self):
        """Test support.ignore_deprecations_from() silences warnings"""
        with warnings.catch_warnings(record=True) as warning_objs:
            warnings_helper._warn_about_deprecation()
            warnings.warn("You should NOT be seeing this.", DeprecationWarning)
            messages = [str(w.message) for w in warning_objs]
        self.assertEqual(len(messages), 0, messages)

    def test_import_module(self):
        import_helper.import_module("ftplib")
        self.assertRaises(unittest.SkipTest,
                          import_helper.import_module, "foo")

    def test_import_fresh_module(self):
        import_helper.import_fresh_module("ftplib")

    def test_get_attribute(self):
        self.assertEqual(support.get_attribute(self, "test_get_attribute"),
                        self.test_get_attribute)
        self.assertRaises(unittest.SkipTest, support.get_attribute, self, "foo")

    @unittest.skipIf(support.is_android or support.is_apple_mobile,
                     'Mobile platforms redirect stdout to system log')
    def test_get_original_stdout(self):
        if isinstance(sys.stdout, io.StringIO):
            # gh-55258: When --junit-xml is used, stdout is a StringIO:
            # use sys.__stdout__ in this case.
            self.assertEqual(support.get_original_stdout(), sys.__stdout__)
        else:
            self.assertEqual(support.get_original_stdout(), sys.stdout)

    def test_unload(self):
        import sched  # noqa: F401
        self.assertIn("sched", sys.modules)
        import_helper.unload("sched")
        self.assertNotIn("sched", sys.modules)

    def test_unlink(self):
        with open(TESTFN, "w", encoding="utf-8") as f:
            pass
        os_helper.unlink(TESTFN)
        self.assertFalse(os.path.exists(TESTFN))
        os_helper.unlink(TESTFN)

    def test_rmtree(self):
        dirpath = os_helper.TESTFN + 'd'
        subdirpath = os.path.join(dirpath, 'subdir')
        os.mkdir(dirpath)
        os.mkdir(subdirpath)
        os_helper.rmtree(dirpath)
        self.assertFalse(os.path.exists(dirpath))
        with support.swap_attr(support, 'verbose', 0):
            os_helper.rmtree(dirpath)

        os.mkdir(dirpath)
        os.mkdir(subdirpath)
        os.chmod(dirpath, stat.S_IRUSR|stat.S_IXUSR)
        with support.swap_attr(support, 'verbose', 0):
            os_helper.rmtree(dirpath)
        self.assertFalse(os.path.exists(dirpath))

        os.mkdir(dirpath)
        os.mkdir(subdirpath)
        os.chmod(dirpath, 0)
        with support.swap_attr(support, 'verbose', 0):
            os_helper.rmtree(dirpath)
        self.assertFalse(os.path.exists(dirpath))

    def test_forget(self):
        mod_filename = TESTFN + '.py'
        with open(mod_filename, 'w', encoding="utf-8") as f:
            print('foo = 1', file=f)
        sys.path.insert(0, os.curdir)
        importlib.invalidate_caches()
        try:
            mod = __import__(TESTFN)
            self.assertIn(TESTFN, sys.modules)

            import_helper.forget(TESTFN)
            self.assertNotIn(TESTFN, sys.modules)
        finally:
            del sys.path[0]
            os_helper.unlink(mod_filename)
            os_helper.rmtree('__pycache__')

    @support.requires_working_socket()
    def test_HOST(self):
        s = socket.create_server((socket_helper.HOST, 0))
        s.close()

    @support.requires_working_socket()
    def test_find_unused_port(self):
        port = socket_helper.find_unused_port()
        s = socket.create_server((socket_helper.HOST, port))
        s.close()

    @support.requires_working_socket()
    def test_bind_port(self):
        s = socket.socket()
        socket_helper.bind_port(s)
        s.listen()
        s.close()

    # Tests for temp_dir()

    def test_temp_dir(self):
        """Test that temp_dir() creates and destroys its directory."""
        parent_dir = tempfile.mkdtemp()
        parent_dir = os.path.realpath(parent_dir)

        try:
            path = os.path.join(parent_dir, 'temp')
            self.assertFalse(os.path.isdir(path))
            with os_helper.temp_dir(path) as temp_path:
                self.assertEqual(temp_path, path)
                self.assertTrue(os.path.isdir(path))
            self.assertFalse(os.path.isdir(path))
        finally:
            os_helper.rmtree(parent_dir)

    def test_temp_dir__path_none(self):
        """Test passing no path."""
        with os_helper.temp_dir() as temp_path:
            self.assertTrue(os.path.isdir(temp_path))
        self.assertFalse(os.path.isdir(temp_path))

    def test_temp_dir__existing_dir__quiet_default(self):
        """Test passing a directory that already exists."""
        def call_temp_dir(path):
            with os_helper.temp_dir(path) as temp_path:
                raise Exception("should not get here")

        path = tempfile.mkdtemp()
        path = os.path.realpath(path)
        try:
            self.assertTrue(os.path.isdir(path))
            self.assertRaises(FileExistsError, call_temp_dir, path)
            # Make sure temp_dir did not delete the original directory.
            self.assertTrue(os.path.isdir(path))
        finally:
            shutil.rmtree(path)

    def test_temp_dir__existing_dir__quiet_true(self):
        """Test passing a directory that already exists with quiet=True."""
        path = tempfile.mkdtemp()
        path = os.path.realpath(path)

        try:
            with warnings_helper.check_warnings() as recorder, _caplog() as caplog:
                with os_helper.temp_dir(path, quiet=True) as temp_path:
                    self.assertEqual(path, temp_path)
                warnings = [str(w.message) for w in recorder.warnings]
            # Make sure temp_dir did not delete the original directory.
            self.assertTrue(os.path.isdir(path))
        finally:
            shutil.rmtree(path)

        self.assertListEqual(warnings, [])
        self.assertEqual(len(caplog.records), 1)
        record = caplog.records[0]
        self.assertStartsWith(
            record.getMessage(),
            f'tests may fail, unable to create '
            f'temporary directory {path!r}: '
        )

    @support.requires_fork()
    def test_temp_dir__forked_child(self):
        """Test that a forked child process does not remove the directory."""
        # See bpo-30028 for details.
        # Run the test as an external script, because it uses fork.
        script_helper.assert_python_ok("-c", textwrap.dedent("""
            import os
            from test import support
            from test.support import os_helper
            with os_helper.temp_cwd() as temp_path:
                pid = os.fork()
                if pid != 0:
                    # parent process

                    # wait for the child to terminate
                    support.wait_process(pid, exitcode=0)

                    # Make sure that temp_path is still present. When the child
                    # process leaves the 'temp_cwd'-context, the __exit__()-
                    # method of the context must not remove the temporary
                    # directory.
                    if not os.path.isdir(temp_path):
                        raise AssertionError("Child removed temp_path.")
        """))

    # Tests for change_cwd()

    def test_change_cwd(self):
        original_cwd = os.getcwd()

        with os_helper.temp_dir() as temp_path:
            with os_helper.change_cwd(temp_path) as new_cwd:
                self.assertEqual(new_cwd, temp_path)
                self.assertEqual(os.getcwd(), new_cwd)

        self.assertEqual(os.getcwd(), original_cwd)

    def test_change_cwd__non_existent_dir(self):
        """Test passing a non-existent directory."""
        original_cwd = os.getcwd()

        def call_change_cwd(path):
            with os_helper.change_cwd(path) as new_cwd:
                raise Exception("should not get here")

        with os_helper.temp_dir() as parent_dir:
            non_existent_dir = os.path.join(parent_dir, 'does_not_exist')
            self.assertRaises(FileNotFoundError, call_change_cwd,
                              non_existent_dir)

        self.assertEqual(os.getcwd(), original_cwd)

    def test_change_cwd__non_existent_dir__quiet_true(self):
        """Test passing a non-existent directory with quiet=True."""
        original_cwd = os.getcwd()

        with os_helper.temp_dir() as parent_dir:
            bad_dir = os.path.join(parent_dir, 'does_not_exist')
            with warnings_helper.check_warnings() as recorder, _caplog() as caplog:
                with os_helper.change_cwd(bad_dir, quiet=True) as new_cwd:
                    self.assertEqual(new_cwd, original_cwd)
                    self.assertEqual(os.getcwd(), new_cwd)
                warnings = [str(w.message) for w in recorder.warnings]

        self.assertListEqual(warnings, [])
        self.assertEqual(len(caplog.records), 1)
        record = caplog.records[0]
        self.assertStartsWith(
            record.getMessage(),
            f'tests may fail, unable to change '
            f'the current working directory '
            f'to {bad_dir!r}: '
        )

    # Tests for change_cwd()

    def test_change_cwd__chdir_warning(self):
        """Check the warning message when os.chdir() fails."""
        path = TESTFN + '_does_not_exist'
        with warnings_helper.check_warnings() as recorder, _caplog() as caplog:
            with os_helper.change_cwd(path=path, quiet=True):
                pass
            messages = [str(w.message) for w in recorder.warnings]

        self.assertListEqual(messages, [])
        self.assertEqual(len(caplog.records), 1)
        record = caplog.records[0]
        self.assertStartsWith(
            record.getMessage(),
            f'tests may fail, unable to change '
            f'the current working directory '
            f'to {path!r}: ',
        )

    # Tests for temp_cwd()

    def test_temp_cwd(self):
        here = os.getcwd()
        with os_helper.temp_cwd(name=TESTFN):
            self.assertEqual(os.path.basename(os.getcwd()), TESTFN)
        self.assertFalse(os.path.exists(TESTFN))
        self.assertEqual(os.getcwd(), here)


    def test_temp_cwd__name_none(self):
        """Test passing None to temp_cwd()."""
        original_cwd = os.getcwd()
        with os_helper.temp_cwd(name=None) as new_cwd:
            self.assertNotEqual(new_cwd, original_cwd)
            self.assertTrue(os.path.isdir(new_cwd))
            self.assertEqual(os.getcwd(), new_cwd)
        self.assertEqual(os.getcwd(), original_cwd)

    def test_sortdict(self):
        self.assertEqual(support.sortdict({3:3, 2:2, 1:1}), "{1: 1, 2: 2, 3: 3}")

    def test_make_bad_fd(self):
        fd = os_helper.make_bad_fd()
        with self.assertRaises(OSError) as cm:
            os.write(fd, b"foo")
        self.assertEqual(cm.exception.errno, errno.EBADF)

    def test_check_syntax_error(self):
        support.check_syntax_error(self, "def class", lineno=1, offset=5)
        with self.assertRaises(AssertionError):
            support.check_syntax_error(self, "x=1")

    def test_CleanImport(self):
        import importlib
        with import_helper.CleanImport("pprint"):
            importlib.import_module("pprint")

    def test_DirsOnSysPath(self):
        with import_helper.DirsOnSysPath('foo', 'bar'):
            self.assertIn("foo", sys.path)
            self.assertIn("bar", sys.path)
        self.assertNotIn("foo", sys.path)
        self.assertNotIn("bar", sys.path)

    def test_captured_stdout(self):
        with support.captured_stdout() as stdout:
            print("hello")
        self.assertEqual(stdout.getvalue(), "hello\n")

    def test_captured_stderr(self):
        with support.captured_stderr() as stderr:
            print("hello", file=sys.stderr)
        self.assertEqual(stderr.getvalue(), "hello\n")

    def test_captured_stdin(self):
        with support.captured_stdin() as stdin:
            stdin.write('hello\n')
            stdin.seek(0)
            # call test code that consumes from sys.stdin
            captured = input()
        self.assertEqual(captured, "hello")

    def test_gc_collect(self):
        support.gc_collect()

    def test_python_is_optimized(self):
        self.assertIsInstance(support.python_is_optimized(), bool)

    def test_swap_attr(self):
        class Obj:
            pass
        obj = Obj()
        obj.x = 1
        with support.swap_attr(obj, "x", 5) as x:
            self.assertEqual(obj.x, 5)
            self.assertEqual(x, 1)
        self.assertEqual(obj.x, 1)
        with support.swap_attr(obj, "y", 5) as y:
            self.assertEqual(obj.y, 5)
            self.assertIsNone(y)
        self.assertNotHasAttr(obj, 'y')
        with support.swap_attr(obj, "y", 5):
            del obj.y
        self.assertNotHasAttr(obj, 'y')

    def test_swap_item(self):
        D = {"x":1}
        with support.swap_item(D, "x", 5) as x:
            self.assertEqual(D["x"], 5)
            self.assertEqual(x, 1)
        self.assertEqual(D["x"], 1)
        with support.swap_item(D, "y", 5) as y:
            self.assertEqual(D["y"], 5)
            self.assertIsNone(y)
        self.assertNotIn("y", D)
        with support.swap_item(D, "y", 5):
            del D["y"]
        self.assertNotIn("y", D)

    class RefClass:
        attribute1 = None
        attribute2 = None
        _hidden_attribute1 = None
        __magic_1__ = None

    class OtherClass:
        attribute2 = None
        attribute3 = None
        __magic_1__ = None
        __magic_2__ = None

    def test_detect_api_mismatch(self):
        missing_items = support.detect_api_mismatch(self.RefClass,
                                                    self.OtherClass)
        self.assertEqual({'attribute1'}, missing_items)

        missing_items = support.detect_api_mismatch(self.OtherClass,
                                                    self.RefClass)
        self.assertEqual({'attribute3', '__magic_2__'}, missing_items)

    def test_detect_api_mismatch__ignore(self):
        ignore = ['attribute1', 'attribute3', '__magic_2__', 'not_in_either']

        missing_items = support.detect_api_mismatch(
                self.RefClass, self.OtherClass, ignore=ignore)
        self.assertEqual(set(), missing_items)

        missing_items = support.detect_api_mismatch(
                self.OtherClass, self.RefClass, ignore=ignore)
        self.assertEqual(set(), missing_items)

    def test_check__all__(self):
        extra = {'tempdir'}
        not_exported = {'template'}
        support.check__all__(self,
                             tempfile,
                             extra=extra,
                             not_exported=not_exported)

        extra = {
            'TextTestResult',
            'installHandler',
        }
        not_exported = {'load_tests', "TestProgram", "BaseTestSuite"}
        support.check__all__(self,
                             unittest,
                             ("unittest.result", "unittest.case",
                              "unittest.suite", "unittest.loader",
                              "unittest.main", "unittest.runner",
                              "unittest.signals", "unittest.async_case"),
                             extra=extra,
                             not_exported=not_exported)

        self.assertRaises(AssertionError, support.check__all__, self, unittest)

    @warnings_helper.ignore_fork_in_thread_deprecation_warnings()
    @unittest.skipUnless(hasattr(os, 'waitpid') and hasattr(os, 'WNOHANG'),
                         'need os.waitpid() and os.WNOHANG')
    @support.requires_fork()
    def test_reap_children(self):
        # Make sure that there is no other pending child process
        support.reap_children()

        # Create a child process
        pid = os.fork()
        if pid == 0:
            # child process: do nothing, just exit
            os._exit(0)

        was_altered = support.environment_altered
        try:
            support.environment_altered = False
            stderr = io.StringIO()

            for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
                with support.swap_attr(support.print_warning, 'orig_stderr', stderr):
                    support.reap_children()

                # Use environment_altered to check if reap_children() found
                # the child process
                if support.environment_altered:
                    break

            msg = "Warning -- reap_children() reaped child process %s" % pid
            self.assertIn(msg, stderr.getvalue())
            self.assertTrue(support.environment_altered)
        finally:
            support.environment_altered = was_altered

        # Just in case, check again that there is no other
        # pending child process
        support.reap_children()

    @support.requires_subprocess()
    def check_options(self, args, func, expected=None):
        code = f'from test.support import {func}; print(repr({func}()))'
        cmd = [sys.executable, *args, '-c', code]
        env = {key: value for key, value in os.environ.items()
               if not key.startswith('PYTHON')}
        proc = subprocess.run(cmd,
                              stdout=subprocess.PIPE,
                              stderr=subprocess.DEVNULL,
                              universal_newlines=True,
                              env=env)
        if expected is None:
            expected = args
        self.assertEqual(proc.stdout.rstrip(), repr(expected))
        self.assertEqual(proc.returncode, 0)

    @support.requires_resource('cpu')
    def test_args_from_interpreter_flags(self):
        # Test test.support.args_from_interpreter_flags()
        for opts in (
            # no option
            [],
            # single option
            ['-B'],
            ['-s'],
            ['-S'],
            ['-E'],
            ['-v'],
            ['-b'],
            ['-P'],
            ['-q'],
            ['-I'],
            # same option multiple times
            ['-bb'],
            ['-vvv'],
            # -W options
            ['-Wignore'],
            # -X options
            ['-X', 'dev'],
            ['-Wignore', '-X', 'dev'],
            ['-X', 'cpu_count=4'],
            ['-X', 'disable-remote-debug'],
            ['-X', 'faulthandler'],
            ['-X', 'importtime'],
            ['-X', 'importtime=2'],
            ['-X', 'int_max_str_digits=1000'],
            ['-X', 'lazy_imports=all'],
            ['-X', 'no_debug_ranges'],
            ['-X', 'showrefcount'],
            ['-X', 'tracemalloc'],
            ['-X', 'tracemalloc=3'],
            ['-X', 'warn_default_encoding'],
        ):
            with self.subTest(opts=opts):
                self.check_options(opts, 'args_from_interpreter_flags')

        with os_helper.temp_dir() as temp_path:
            prefix = os.path.join(temp_path, 'pycache')
            opts = ['-X', f'pycache_prefix={prefix}']
            with self.subTest(opts=opts):
                self.check_options(opts, 'args_from_interpreter_flags')

        self.check_options(['-I', '-E', '-s', '-P'],
                           'args_from_interpreter_flags',
                           ['-I'])

    def test_optim_args_from_interpreter_flags(self):
        # Test test.support.optim_args_from_interpreter_flags()
        for opts in (
            # no option
            [],
            ['-O'],
            ['-OO'],
            ['-OOOO'],
        ):
            with self.subTest(opts=opts):
                self.check_options(opts, 'optim_args_from_interpreter_flags')

    @unittest.skipIf(support.is_apple_mobile, "Unstable on Apple Mobile")
    @unittest.skipIf(support.is_wasi, "Unavailable on WASI")
    def test_fd_count(self):
        # We cannot test the absolute value of fd_count(): on old Linux kernel
        # or glibc versions, os.urandom() keeps a FD open on /dev/urandom
        # device and Python has 4 FD opens instead of 3. Test is unstable on
        # Emscripten and Apple Mobile platforms; these platforms start and stop
        # background threads that use pipes and epoll fds.
        start = os_helper.fd_count()
        fd = os.open(__file__, os.O_RDONLY)
        try:
            more = os_helper.fd_count()
        finally:
            os.close(fd)
        self.assertEqual(more - start, 1)

    def check_print_warning(self, msg, expected):
        stderr = io.StringIO()
        with support.swap_attr(support.print_warning, 'orig_stderr', stderr):
            support.print_warning(msg)
        self.assertEqual(stderr.getvalue(), expected)

    def test_print_warning(self):
        self.check_print_warning("msg",
                                 "Warning -- msg\n")
        self.check_print_warning("a\nb",
                                 'Warning -- a\nWarning -- b\n')

    def test_has_strftime_extensions(self):
        if sys.platform == "win32":
            self.assertFalse(support.has_strftime_extensions)
        else:
            self.assertTrue(support.has_strftime_extensions)

    def test_get_recursion_depth(self):
        # test support.get_recursion_depth()
        code = textwrap.dedent("""
            from test import support
            import sys

            def check(cond):
                if not cond:
                    raise AssertionError("test failed")

            # depth 1
            check(support.get_recursion_depth() == 1)

            # depth 2
            def test_func():
                check(support.get_recursion_depth() == 2)
            test_func()

            def test_recursive(depth, limit):
                if depth >= limit:
                    # cannot call get_recursion_depth() at this depth,
                    # it can raise RecursionError
                    return
                get_depth = support.get_recursion_depth()
                print(f"test_recursive: {depth}/{limit}: "
                      f"get_recursion_depth() says {get_depth}")
                check(get_depth == depth)
                test_recursive(depth + 1, limit)

            # depth up to 25
            with support.infinite_recursion(max_depth=25):
                limit = sys.getrecursionlimit()
                print(f"test with sys.getrecursionlimit()={limit}")
                test_recursive(2, limit)

            # depth up to 500
            with support.infinite_recursion(max_depth=500):
                limit = sys.getrecursionlimit()
                print(f"test with sys.getrecursionlimit()={limit}")
                test_recursive(2, limit)
        """)
        script_helper.assert_python_ok("-c", code)

    @support.skip_if_unlimited_stack_size
    def test_recursion(self):
        # Test infinite_recursion() and get_recursion_available() functions.
        def recursive_function(depth):
            if depth:
                recursive_function(depth - 1)

        for max_depth in (5, 25, 250, 2500):
            with support.infinite_recursion(max_depth):
                available = support.get_recursion_available()

                # Recursion up to 'available' additional frames should be OK.
                recursive_function(available)

                # Recursion up to 'available+1' additional frames must raise
                # RecursionError. Avoid self.assertRaises(RecursionError) which
                # can consume more than 3 frames and so raises RecursionError.
                try:
                    recursive_function(available + 1)
                except RecursionError:
                    pass
                else:
                    self.fail("RecursionError was not raised")

        # Test the bare minimumum: max_depth=3
        with support.infinite_recursion(3):
            try:
                recursive_function(3)
            except RecursionError:
                pass
            else:
                self.fail("RecursionError was not raised")

    def test_parse_memlimit(self):
        parse = support._parse_memlimit
        KiB = 1024
        MiB = KiB * 1024
        GiB = MiB * 1024
        TiB = GiB * 1024
        self.assertEqual(parse('0k'), 0)
        self.assertEqual(parse('3k'), 3 * KiB)
        self.assertEqual(parse('2.4m'), int(2.4 * MiB))
        self.assertEqual(parse('4g'), int(4 * GiB))
        self.assertEqual(parse('1t'), TiB)

        for limit in ('', '3', '3.5.10k', '10x'):
            with self.subTest(limit=limit):
                with self.assertRaises(ValueError):
                    parse(limit)

    def test_set_memlimit(self):
        _4GiB = 4 * 1024 ** 3
        TiB = 1024 ** 4
        old_max_memuse = support.max_memuse
        old_real_max_memuse = support.real_max_memuse
        try:
            if sys.maxsize > 2**32:
                support.set_memlimit('4g')
                self.assertEqual(support.max_memuse, _4GiB)
                self.assertEqual(support.real_max_memuse, _4GiB)

                big = 2**100 // TiB
                support.set_memlimit(f'{big}t')
                self.assertEqual(support.max_memuse, sys.maxsize)
                self.assertEqual(support.real_max_memuse, big * TiB)
            else:
                support.set_memlimit('4g')
                self.assertEqual(support.max_memuse, sys.maxsize)
                self.assertEqual(support.real_max_memuse, _4GiB)
        finally:
            support.max_memuse = old_max_memuse
            support.real_max_memuse = old_real_max_memuse

    def test_copy_python_src_ignore(self):
        # Get source directory
        src_dir = sysconfig.get_config_var('abs_srcdir')
        if not src_dir:
            src_dir = sysconfig.get_config_var('srcdir')
        src_dir = os.path.abspath(src_dir)

        # Check that the source code is available
        if not os.path.exists(src_dir):
            self.skipTest(f"cannot access Python source code directory:"
                          f" {src_dir!r}")
        # Check that the landmark copy_python_src_ignore() expects is available
        # (Previously we looked for 'Lib\os.py', which is always present on Windows.)
        landmark = os.path.join(src_dir, 'Modules')
        if not os.path.exists(landmark):
            self.skipTest(f"cannot access Python source code directory:"
                          f" {landmark!r} landmark is missing")

        # Test support.copy_python_src_ignore()

        # Source code directory
        ignored = {'.git', '__pycache__'}
        names = os.listdir(src_dir)
        self.assertEqual(support.copy_python_src_ignore(src_dir, names),
                         ignored | {'build'})

        # Doc/ directory
        path = os.path.join(src_dir, 'Doc')
        self.assertEqual(support.copy_python_src_ignore(path, os.listdir(path)),
                         ignored | {'build', 'venv'})

        # Another directory
        path = os.path.join(src_dir, 'Objects')
        self.assertEqual(support.copy_python_src_ignore(path, os.listdir(path)),
                         ignored)

    def test_get_signal_name(self):
        for exitcode, expected in (
            (-int(signal.SIGINT), 'SIGINT'),
            (-int(signal.SIGSEGV), 'SIGSEGV'),
            (128 + int(signal.SIGABRT), 'SIGABRT'),
            (3221225477, "STATUS_ACCESS_VIOLATION"),
            (0xC00000FD, "STATUS_STACK_OVERFLOW"),
            (0xC0000906, "0xC0000906"),
        ):
            self.assertEqual(support.get_signal_name(exitcode), expected,
                             exitcode)

    def test_linked_to_musl(self):
        linked = support.linked_to_musl()
        self.assertIsNotNone(linked)
        if support.is_wasm32:
            self.assertTrue(linked)
        # The value is cached, so make sure it returns the same value again.
        self.assertIs(linked, support.linked_to_musl())
        # The musl version is either triple or just a major version number.
        if linked:
            self.assertIsInstance(linked, tuple)
            self.assertIn(len(linked), (1, 3))
            for v in linked:
                self.assertIsInstance(v, int)


    # XXX -follows a list of untested API
    # make_legacy_pyc
    # is_resource_enabled
    # requires
    # fcmp
    # umaks
    # findfile
    # check_warnings
    # EnvironmentVarGuard
    # transient_internet
    # run_with_locale
    # bigmemtest
    # precisionbigmemtest
    # bigaddrspacetest
    # requires_resource
    # threading_cleanup
    # reap_threads
    # can_symlink
    # skip_unless_symlink
    # SuppressCrashReport


@hashlib_helper.requires_builtin_hashes()
class TestHashlibSupport(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.hashlib = import_helper.import_module("hashlib")
        cls.hmac = import_helper.import_module("hmac")

        # All C extension modules must be present since blocking
        # the built-in implementation while allowing OpenSSL or vice-versa
        # may result in failures depending on the exposed built-in hashes.
        cls._hashlib = import_helper.import_module("_hashlib")
        cls._hmac = import_helper.import_module("_hmac")
        cls._md5 = import_helper.import_module("_md5")

    def skip_if_fips_mode(self):
        if self._hashlib.get_fips_mode():
            self.skipTest("disabled in FIPS mode")

    def skip_if_not_fips_mode(self):
        if not self._hashlib.get_fips_mode():
            self.skipTest("requires FIPS mode")

    def check_context(self, disabled=True):
        if disabled:
            return self.assertRaises(ValueError)
        return contextlib.nullcontext()

    def try_import_attribute(self, fullname, default=None):
        if fullname is None:
            return default
        assert fullname.count('.') == 1, fullname
        module_name, attribute = fullname.split('.', maxsplit=1)
        try:
            module = importlib.import_module(module_name)
        except ImportError:
            return default
        try:
            return getattr(module, attribute, default)
        except TypeError:
            return default

    def fetch_hash_function(self, name, implementation):
        info = hashlib_helper.get_hash_func_info(name)
        match hashlib_helper.Implementation(implementation):
            case hashlib_helper.Implementation.hashlib:
                method_name = info.hashlib.member_name
                assert isinstance(method_name, str), method_name
                return getattr(self.hashlib, method_name)
            case hashlib_helper.Implementation.openssl:
                method_name = info.openssl.member_name
                assert isinstance(method_name, str | None), method_name
                return getattr(self._hashlib, method_name or "", None)
        fullname = info[implementation].fullname
        return self.try_import_attribute(fullname)

    def fetch_hmac_function(self, name):
        target = hashlib_helper.get_hmac_item_info(name)
        return target.import_member()

    def check_openssl_hash(self, name, *, disabled=True):
        """Check that OpenSSL HASH interface is enabled/disabled."""
        with self.check_context(disabled):
            _ = self._hashlib.new(name)
        if do_hash := self.fetch_hash_function(name, "openssl"):
            self.assertStartsWith(do_hash.__name__, 'openssl_')
            with self.check_context(disabled):
                _ = do_hash(b"")

    def check_openssl_hmac(self, name, *, disabled=True):
        """Check that OpenSSL HMAC interface is enabled/disabled."""
        if name in hashlib_helper.NON_HMAC_DIGEST_NAMES:
            # HMAC-BLAKE and HMAC-SHAKE raise a ValueError as they are not
            # supported at all (they do not make any sense in practice).
            with self.assertRaises(ValueError):
                self._hashlib.hmac_digest(b"", b"", name)
        else:
            with self.check_context(disabled):
                _ = self._hashlib.hmac_digest(b"", b"", name)
        # OpenSSL does not provide one-shot explicit HMAC functions

    def check_builtin_hash(self, name, *, disabled=True):
        """Check that HACL* HASH interface is enabled/disabled."""
        if do_hash := self.fetch_hash_function(name, "builtin"):
            self.assertEqual(do_hash.__name__, name)
            with self.check_context(disabled):
                _ = do_hash(b"")

    def check_builtin_hmac(self, name, *, disabled=True):
        """Check that HACL* HMAC interface is enabled/disabled."""
        if name in hashlib_helper.NON_HMAC_DIGEST_NAMES:
            # HMAC-BLAKE and HMAC-SHAKE raise a ValueError as they are not
            # supported at all (they do not make any sense in practice).
            with self.assertRaises(ValueError):
                self._hmac.compute_digest(b"", b"", name)
        else:
            with self.check_context(disabled):
                _ = self._hmac.compute_digest(b"", b"", name)

        with self.check_context(disabled):
            _ = self._hmac.new(b"", b"", name)

        if do_hmac := self.fetch_hmac_function(name):
            self.assertStartsWith(do_hmac.__name__, 'compute_')
            with self.check_context(disabled):
                _ = do_hmac(b"", b"")
        else:
            self.assertIn(name, hashlib_helper.NON_HMAC_DIGEST_NAMES)

    @support.subTests(
        ('name', 'allow_openssl', 'allow_builtin'),
        itertools.product(
            hashlib_helper.CANONICAL_DIGEST_NAMES,
            [True, False],
            [True, False],
        )
    )
    def test_disable_hash(self, name, allow_openssl, allow_builtin):
        # In FIPS mode, the function may be available but would still need
        # to raise a ValueError, so we will test the helper separately.
        self.skip_if_fips_mode()
        flags = dict(allow_openssl=allow_openssl, allow_builtin=allow_builtin)
        is_fully_disabled = not allow_builtin and not allow_openssl

        with hashlib_helper.block_algorithm(name, **flags):
            # OpenSSL's blake2s and blake2b are unknown names
            # when only the OpenSSL interface is available.
            if allow_openssl and not allow_builtin:
                aliases = {'blake2s': 'blake2s256', 'blake2b': 'blake2b512'}
                name_for_hashlib_new = aliases.get(name, name)
            else:
                name_for_hashlib_new = name

            with self.check_context(is_fully_disabled):
                _ = self.hashlib.new(name_for_hashlib_new)

            # Since _hashlib is present, explicit blake2b/blake2s constructors
            # use the built-in implementation, while others (since we are not
            # in FIPS mode and since _hashlib exists) use the OpenSSL function.
            with self.check_context(is_fully_disabled):
                _ = getattr(self.hashlib, name)()

            self.check_openssl_hash(name, disabled=not allow_openssl)
            self.check_builtin_hash(name, disabled=not allow_builtin)

            if name not in hashlib_helper.NON_HMAC_DIGEST_NAMES:
                with self.check_context(is_fully_disabled):
                    _ = self.hmac.new(b"", b"", name)
                with self.check_context(is_fully_disabled):
                    _ = self.hmac.HMAC(b"", b"", name)
                with self.check_context(is_fully_disabled):
                    _ = self.hmac.digest(b"", b"", name)

                self.check_openssl_hmac(name, disabled=not allow_openssl)
                self.check_builtin_hmac(name, disabled=not allow_builtin)

    @hashlib_helper.block_algorithm("md5")
    def test_disable_hash_md5_in_fips_mode(self):
        self.skip_if_not_fips_mode()

        self.assertRaises(ValueError, self.hashlib.new, "md5")
        self.assertRaises(ValueError, self._hashlib.new, "md5")
        self.assertRaises(ValueError, self.hashlib.md5)
        self.assertRaises(ValueError, self._hashlib.openssl_md5)

        kwargs = dict(usedforsecurity=True)
        self.assertRaises(ValueError, self.hashlib.new, "md5", **kwargs)
        self.assertRaises(ValueError, self._hashlib.new, "md5", **kwargs)
        self.assertRaises(ValueError, self.hashlib.md5, **kwargs)
        self.assertRaises(ValueError, self._hashlib.openssl_md5, **kwargs)

    @hashlib_helper.block_algorithm("md5", allow_openssl=True)
    def test_disable_hash_md5_in_fips_mode_allow_openssl(self):
        self.skip_if_not_fips_mode()
        # Allow the OpenSSL interface to be used but not the HACL* one.
        # hashlib.new("md5") is dispatched to hashlib.openssl_md5()
        self.assertRaises(ValueError, self.hashlib.new, "md5")
        # dispatched to hashlib.openssl_md5() in FIPS mode
        h2 = self.hashlib.new("md5", usedforsecurity=False)
        self.assertIsInstance(h2, self._hashlib.HASH)

        # block_algorithm() does not mock hashlib.md5 and _hashlib.openssl_md5
        self.assertNotHasAttr(self.hashlib.md5, "__wrapped__")
        self.assertNotHasAttr(self._hashlib.openssl_md5, "__wrapped__")

        hashlib_md5 = inspect.unwrap(self.hashlib.md5)
        self.assertIs(hashlib_md5, self._hashlib.openssl_md5)
        self.assertRaises(ValueError, self.hashlib.md5)
        # allow MD5 to be used in FIPS mode if usedforsecurity=False
        h3 = self.hashlib.md5(usedforsecurity=False)
        self.assertIsInstance(h3, self._hashlib.HASH)

    @hashlib_helper.block_algorithm("md5", allow_builtin=True)
    def test_disable_hash_md5_in_fips_mode_allow_builtin(self):
        self.skip_if_not_fips_mode()
        # Allow the HACL* interface to be used but not the OpenSSL one.
        h1 = self.hashlib.new("md5")  # dispatched to _md5.md5()
        self.assertNotIsInstance(h1, self._hashlib.HASH)
        h2 = self.hashlib.new("md5", usedforsecurity=False)
        self.assertIsInstance(h2, type(h1))

        # block_algorithm() mocks hashlib.md5 and _hashlib.openssl_md5
        self.assertHasAttr(self.hashlib.md5, "__wrapped__")
        self.assertHasAttr(self._hashlib.openssl_md5, "__wrapped__")

        hashlib_md5 = inspect.unwrap(self.hashlib.md5)
        openssl_md5 = inspect.unwrap(self._hashlib.openssl_md5)
        self.assertIs(hashlib_md5, openssl_md5)
        self.assertRaises(ValueError, self.hashlib.md5)
        self.assertRaises(ValueError, self.hashlib.md5,
                          usedforsecurity=False)

    @hashlib_helper.block_algorithm("md5",
                                    allow_openssl=True,
                                    allow_builtin=True)
    def test_disable_hash_md5_in_fips_mode_allow_all(self):
        self.skip_if_not_fips_mode()
        # hashlib.new() isn't blocked as it falls back to _md5.md5
        self.assertIsInstance(self.hashlib.new("md5"), self._md5.MD5Type)
        self.assertRaises(ValueError, self._hashlib.new, "md5")
        h = self._hashlib.new("md5", usedforsecurity=False)
        self.assertIsInstance(h, self._hashlib.HASH)

        self.assertNotHasAttr(self.hashlib.md5, "__wrapped__")
        self.assertNotHasAttr(self._hashlib.openssl_md5, "__wrapped__")

        self.assertIs(self.hashlib.md5, self._hashlib.openssl_md5)
        self.assertRaises(ValueError, self.hashlib.md5)
        h = self.hashlib.md5(usedforsecurity=False)
        self.assertIsInstance(h, self._hashlib.HASH)


if __name__ == '__main__':
    unittest.main()
