#     Copyright 2026, Kay Hayen, mailto:kay.hayen@gmail.com find license text at end of file


"""Common test infrastructure functions. To be used by test runners."""

import atexit
import gc
import os
import shutil
import signal
import sys
import tempfile
import threading
import time
from contextlib import contextmanager

from nuitka.__past__ import md5, subprocess
from nuitka.containers.OrderedDicts import OrderedDict
from nuitka.importing.StandardLibrary import isStandardLibraryPath
from nuitka.options.CommandLineOptionsTools import makeOptionsParser
from nuitka.PythonVersions import (
    getSitePackageCandidateNames,
    getTestExecutionPythonVersions,
    isDebugPython,
)
from nuitka.Tracing import OurLogger, flushStandardOutputs, my_print
from nuitka.tree.SourceHandling import readSourceCodeFromFilename
from nuitka.utils.AppDirs import getCacheDir
from nuitka.utils.Execution import (
    check_output,
    createProcess,
    executeProcess,
    getNullInput,
    getNullOutput,
    withEnvironmentVarOverridden,
)
from nuitka.utils.FileOperations import (
    areSamePaths,
    deleteFile,
    getExternalUsePath,
    getFileContentByLine,
    getFileContents,
    getFileList,
    getNormalizedPathJoin,
    getParentDirectories,
    isFilenameSameAsOrBelowPath,
    makePath,
    openTextFile,
    removeDirectory,
)
from nuitka.utils.InstalledPythons import findInstalledPython
from nuitka.utils.Jinja2 import getTemplate
from nuitka.utils.Utils import isFreeBSD, isLinux, isMacOS, isWin32Windows
from nuitka.Version import getCommercialVersion

from .SearchModes import SearchMode

# spell-checker: ignore popenargs,pathsep,killpg

test_logger = OurLogger("", base_style="blue")


def check_result(*popenargs, **kwargs):
    if "stdout" in kwargs:
        raise ValueError("stdout argument not allowed, it will be overridden.")

    process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs)
    _unused_output, _unused_err = process.communicate()
    ret_value = process.poll()

    if ret_value:
        return False
    else:
        return True


_start_dir = None


def goMainDir():
    global _start_dir  # singleton, pylint: disable=global-statement
    _start_dir = os.getcwd()

    # Go its own directory, to have it easy with path knowledge.
    os.chdir(os.path.dirname(os.path.abspath(sys.modules["__main__"].__file__)))


def getStartDir():
    return _start_dir


_python_version_str = None
_python_version = None
_python_arch = None
_python_executable = None
_python_vendor = None
_python_debug = None


def _getMinVersionSuffixRequirements():
    result = []

    for python_version_str in getTestExecutionPythonVersions():
        major, minor = tuple(int(value) for value in python_version_str.split("."))

        # No need for a "26" suffix, every interpreter supported for running
        # tests is already new enough for it.
        if (major, minor) == (2, 6):
            continue

        result.append(("%d%d.py" % (major, minor), (major, minor)))

    return tuple(result)


def _getMaxVersionSuffixRequirements():
    result = []

    for python_version_str in getTestExecutionPythonVersions():
        major, minor = tuple(int(value) for value in python_version_str.split("."))

        result.append(("_%d%d.py" % (major, minor), (major, minor + 1)))

    return tuple(result)


_min_version_suffix_requirements = _getMinVersionSuffixRequirements()
_max_version_suffix_requirements = _getMaxVersionSuffixRequirements()


def _parsePythonVersionOutput(python_binary):
    version_output = check_output(
        (
            python_binary,
            "-c",
            """\
import sys, os;\
print(".".join(str(s) for s in list(sys.version_info)[:3]));\
print(\
("x86_64" if "AMD64" in sys.version else (\
"arm64" if "ARM64" in sys.version else "x86"))\
if os.name == "nt" else os.uname()[4]\
);\
print(sys.executable);\
print("Anaconda" if os.path.exists(os.path.join(sys.prefix, 'conda-meta')) else "Unknown");\
print(hasattr(sys, "gettotalrefcount"))\
""",
        ),
        stderr=subprocess.STDOUT,
    )

    if str is not bytes:
        version_output = version_output.decode("utf8")

    python_version_str = version_output.split("\n")[0].strip()
    python_arch = version_output.split("\n")[1].strip()
    python_executable = version_output.split("\n")[2].strip()
    python_vendor = version_output.split("\n")[3].strip()
    python_debug = version_output.split("\n")[4].strip()

    assert type(python_version_str) is str, repr(python_version_str)
    assert type(python_arch) is str, repr(python_arch)
    assert type(python_executable) is str, repr(_python_executable)

    python_version = tuple(int(d) for d in python_version_str.split("."))
    python_debug = python_debug == "True"

    return (
        python_version,
        python_version_str,
        python_arch,
        python_executable,
        python_vendor,
        python_debug,
    )


def setup(suite="", needs_io_encoding=False, silent=False, go_main=True):
    if go_main:
        goMainDir()

    if "PYTHON" not in os.environ:
        os.environ["PYTHON"] = sys.executable

    # Allow test code to use this to make caching specific.
    os.environ["NUITKA_TEST_SUITE"] = suite

    # Allow providing 33, 27, and expand that to python2.7
    if (
        len(os.environ["PYTHON"]) == 2
        and os.environ["PYTHON"].isdigit()
        and not isWin32Windows()
    ):
        os.environ["PYTHON"] = "python%s.%s" % (
            os.environ["PYTHON"][0],
            os.environ["PYTHON"][1],
        )

    if needs_io_encoding and "PYTHONIOENCODING" not in os.environ:
        os.environ["PYTHONIOENCODING"] = "utf-8"

    global _python_version_str, _python_version, _python_arch, _python_executable, _python_vendor, _python_debug  # singleton, pylint: disable=global-statement

    (
        _python_version,
        _python_version_str,
        _python_arch,
        _python_executable,
        _python_vendor,
        _python_debug,
    ) = _parsePythonVersionOutput(python_binary=os.environ["PYTHON"])

    if not silent:
        my_print("Using concrete python", _python_version_str, "on", _python_arch)

    if "COVERAGE_FILE" not in os.environ:
        os.environ["COVERAGE_FILE"] = os.path.join(
            os.path.dirname(__file__), "..", "..", "..", ".coverage"
        )

    return _python_version


def getPythonBinary():
    return _python_executable


def getPythonArch():
    return _python_arch


def getPythonVendor():
    return _python_vendor


def getPythonDebug():
    return _python_debug


def getPythonVersionString():
    return _python_version_str


tmp_dir = None


def getTempDir():
    # Create a temporary directory to work in, automatically remove it in case
    # it is empty in the end.
    global tmp_dir  # singleton, pylint: disable=global-statement

    if tmp_dir is None:
        tmp_dir = tempfile.mkdtemp(
            prefix=os.path.basename(
                os.path.dirname(os.path.abspath(sys.modules["__main__"].__file__))
            )
            + "-",
            dir=None if not (isLinux() and os.path.exists("/var/tmp")) else "/var/tmp",
        )

        def removeTempDir():
            removeDirectory(
                path=tmp_dir,
                logger=test_logger,
                ignore_errors=True,
                extra_recommendation=None,
            )

        atexit.register(removeTempDir)

    return tmp_dir


def convertUsing2to3(path, force=False):
    command = [os.environ["PYTHON"], "-m", "py_compile", path]

    if not force:
        if "xrange" not in getFileContents(path):
            with getNullOutput() as null_output:
                if check_result(command, stderr=null_output):
                    return path, False

    filename = os.path.basename(path)

    new_path = os.path.join(getTempDir(), filename)

    # This may already be a temp file, e.g. because of construct creation.
    try:
        shutil.copy(path, new_path)
    except shutil.Error:
        pass

    # For Python2.6 and 3.2 the -m lib2to3 was not yet supported.
    use_binary = sys.version_info[:2] in ((2, 6), (3, 2))

    if use_binary:
        # On Windows, we cannot rely on 2to3 to be in the path.
        if isWin32Windows():
            command = [
                sys.executable,
                os.path.join(os.path.dirname(sys.executable), "Tools/Scripts/2to3.py"),
            ]
        else:
            command = ["2to3"]
    else:
        command = [sys.executable, "-m", "lib2to3"]

    command += ("-w", "-n", "--no-diffs", new_path)

    with getNullOutput() as null_output:
        try:
            check_output(command, stderr=null_output)

        except subprocess.CalledProcessError:
            if isWin32Windows():
                raise

            command[0:3] = ["2to3"]

            check_output(command, stderr=null_output)

    data = getFileContents(new_path)

    with openTextFile(new_path, "w") as result_file:
        result_file.write("__file__ = %r\n" % os.path.abspath(path))
        result_file.write(data)

    return new_path, True


def decideFilenameVersionSkip(filename):
    """Make decision whether to skip based on filename and Python version.

    This codifies certain rules that files can have as suffixes or prefixes
    to make them be part of the set of tests executed for a version or not.

    Generally, an ending of "<major><minor>.py" indicates that it must be that
    Python version or higher. There is no need for ending in "26.py" as this
    is the minimum version anyway.

    The "_2.py" indicates a maximum version of 2.7, i.e. not Python 3.x, for
    language syntax no more supported.
    """

    # This will make many decisions with immediate returns.
    # pylint: disable=too-many-return-statements

    assert type(filename) is str, repr(filename)

    # There are commercial only test cases
    if "commercial" in filename.lower() and getCommercialVersion() is None:
        return False

    # Skip runner scripts by default.
    if filename.startswith("run_"):
        return False

    if filename.endswith(".j2"):
        filename = filename[:-3]

    # Skip tests that require Python 2 at maximum.
    if filename.endswith("_2.py") and _python_version >= (3,):
        return False

    for version_suffix, max_excluded_version in _max_version_suffix_requirements:
        if (
            filename.endswith(version_suffix)
            and _python_version >= max_excluded_version
        ):
            return False

    # Skip tests that require Python 3.2 at least.
    if filename.endswith("32.py") and _python_version < (3, 2):
        return False

    # Skip tests that require Python 3.3 at least.
    if filename.endswith("33.py") and _python_version < (3, 3):
        return False

    for version_suffix, min_version in _min_version_suffix_requirements:
        if filename.endswith(version_suffix) and _python_version < min_version:
            return False

    return True


def _removeCPythonTestSuiteDir():
    # Cleanup, some tests apparently forget that.
    try:
        if os.path.isdir("@test"):
            removeDirectory(
                "@test",
                logger=test_logger,
                ignore_errors=False,
                extra_recommendation=None,
            )
        else:
            deleteFile("@test", must_exist=False)
    except OSError:
        # TODO: Move this into removeDirectory maybe. Doing an external
        # call as last resort could be a good idea.

        # This seems to work for broken "lnk" files.
        if isWin32Windows():
            os.system("rmdir /S /Q @test")

        if os.path.exists("@test"):
            raise


def _compareWithCPythonVariations(
    dirname, filename, extra_flags, search_mode, on_error=None
):
    if "" not in extra_flags:
        return test_logger.sysexit("Error, default flags not present")

    # Drop empty values or identical values.
    extra_flags = OrderedDict(
        (key, value) for (key, value) in extra_flags.items() if (value or key == "")
    )

    def _getVariation(source, variant_name):
        if type(source) in (dict, OrderedDict):
            return source.get(variant_name, source.get(""))
        else:
            return source

    if len(extra_flags) == 1:
        return compareWithCPython(
            dirname=dirname,
            filename=filename,
            extra_flags=_getVariation(extra_flags, ""),
            search_mode=search_mode,
            on_error=on_error,
        )

    for test_variant_name in extra_flags.keys():
        if test_variant_name == "":
            test_logger.info("Default variation of the test.")
        else:
            test_logger.info("Extra variation '%s'." % test_variant_name)

        with withEnvironmentVarOverridden("NUITKA_TEST_VARIANT", test_variant_name):
            compareWithCPython(
                dirname=_getVariation(dirname, test_variant_name),
                filename=_getVariation(filename, test_variant_name),
                extra_flags=_getVariation(extra_flags, test_variant_name),
                search_mode=search_mode,
                on_error=on_error,
            )


def compareWithCPython(dirname, filename, extra_flags, search_mode, on_error=None):
    """Call the comparison tool. For a given directory filename.

    The search mode decides if the test case aborts on error or gets extra
    flags that are exceptions.

    """

    # Allow being called with variations of extra flags.
    if type(extra_flags) in (dict, OrderedDict):
        return _compareWithCPythonVariations(
            dirname=dirname,
            filename=filename,
            extra_flags=extra_flags,
            search_mode=search_mode,
            on_error=on_error,
        )

    if dirname is None:
        path = filename
    else:
        path = os.path.join(dirname, filename)

    if os.getenv("NUITKA_TEST_INSTALLED", "") == "1":
        command = [
            sys.executable,
            "-m",
            "nuitka.tools.testing.compare_with_cpython",
            path,
            "silent",
        ]
    else:
        compare_with_cpython = os.path.join("..", "..", "bin", "compare_with_cpython")
        if os.path.exists(compare_with_cpython):
            command = [sys.executable, compare_with_cpython, path, "silent"]
        else:
            return test_logger.sysexit("Error, cannot locate Nuitka comparison runner.")

    if extra_flags is not None:
        command += extra_flags

    command += search_mode.getExtraFlags(dirname, filename)

    # Cleanup before and after test stage directory.
    _removeCPythonTestSuiteDir()

    try:
        result = subprocess.call(command)
    except KeyboardInterrupt:
        result = 2

    # Cleanup before and after test stage directory.
    _removeCPythonTestSuiteDir()

    if result != 0 and result != 2 and search_mode.abortOnFinding(dirname, filename):
        if on_error is not None:
            on_error(dirname, filename)

        search_mode.onErrorDetected("Error exit! %s" % result)

    if result == 2:
        return test_logger.sysexit("Interrupted, with CTRL-C\n", exit_code=2)


def checkCompilesNotWithCPython(dirname, filename, search_mode):
    if dirname is None:
        path = filename
    else:
        path = os.path.join(dirname, filename)

    command = [
        _python_executable,
        "-m",
        "compileall",
        path,
    ]

    try:
        result = subprocess.call(command)
    except KeyboardInterrupt:
        result = 2

    if result != 1 and result != 2 and search_mode.abortOnFinding(dirname, filename):
        search_mode.onErrorDetected("Error exit! %s" % result)


def checkSucceedsWithCPython(filename):
    command = [_python_executable, filename]

    process_result = executeProcess(command)

    if process_result.exit_code != 0:
        my_print("stdout", process_result.stdout)
        my_print("stderr", process_result.stderr)

    return process_result.exit_code == 0


def getDebugPython():
    # For all Python, if it's the one also executing the runner, which is
    # very probably the case, we check that. We don't check the provided
    # binary here, this could be done as well.
    if sys.executable == os.environ["PYTHON"] and isDebugPython():
        return sys.executable

    # On Debian systems, these work.
    debug_python = os.path.join("/usr/bin/", os.environ["PYTHON"] + "-dbg")
    if os.path.exists(debug_python):
        return debug_python

    # On Fedora systems, these work, but on for Python3
    debug_python = os.path.join("/usr/bin/", os.environ["PYTHON"] + "-debug")
    if os.path.exists(debug_python) and _parsePythonVersionOutput(debug_python)[0] >= (
        3,
    ):
        return debug_python

    # On Windows systems, debug binaries conventionally use the "_d.exe"
    # suffix next to the release executable.
    debug_python = os.environ["PYTHON"]
    if debug_python.lower().endswith(".exe"):
        debug_python = debug_python[:-4]
    debug_python = debug_python + "_d.exe"
    if os.path.exists(debug_python):
        return debug_python

    # Otherwise no.
    return None


def displayRuntimeTraces(logger, path):
    if not os.path.exists(path):
        # TODO: Have a logger package passed.
        logger.sysexit("Error, cannot find %r (%r)." % (path, os.path.abspath(path)))

    path = os.path.abspath(path)

    # TODO: Merge code for building command with below function, this is otherwise
    # horribly bad.

    if os.name == "posix":
        # Run with traces to help debugging, specifically in CI environment.
        if isMacOS() or isFreeBSD():
            test_logger.info("dtruss:")
            flushStandardOutputs()
            os.system("sudo dtruss %s" % path)
        else:
            test_logger.info("strace:")
            flushStandardOutputs()
            os.system("strace -s4096 -e file %s" % path)


def hasModule(module_name):
    with getNullOutput() as null_output:
        result = subprocess.call(
            (os.environ["PYTHON"], "-c", "import %s" % module_name),
            stdout=null_output,
            stderr=subprocess.STDOUT,
        )

    return result == 0


m1 = {}
m2 = {}


def cleanObjRefCntMaps():
    m1.clear()
    m2.clear()

    # Warm out repr
    for x in gc.get_objects():
        try:
            str(x)
        except Exception:  # Catch all the things, pylint: disable=broad-except
            pass


def snapObjRefCntMap(before):
    # Inherently complex, pylint: disable=too-many-branches

    if before:
        m = m1
    else:
        m = m2

    m.clear()
    gc.collect()

    for x in gc.get_objects():
        # The dictionary is cyclic, and contains itself, avoid that.
        if x is m1 or x is m2:
            continue

        if type(x) is str and (x in m1 or x in m2):
            continue

        if type(x) is not str and isinstance(x, str):
            k = "str_overload_" + x.__class__.__name__ + str(x)
        elif type(x) is dict:
            if "__builtins__" in x:
                k = "<module dict %s>" % x.get("__name__", "<maybe not a module>")
            elif "__spec__" in x and "__name__" in x:
                k = "<module dict %s>" % x["__name__"]
            else:
                k = str(x)
        elif hasattr(x, "__class__") and x.__class__.__name__ == "compiled_frame":
            k = "<compiled_frame at xxx, line %d code %s" % (x.f_lineno, x.f_code)
        else:
            k = str(x)

        c = sys.getrefcount(x)  # spell-checker: ignore getrefcount

        if k in m:
            m[k] += c
        else:
            m[k] = c


orig_print = None


def disablePrinting():
    # Singleton, pylint: disable=global-statement
    global orig_print

    if orig_print is None:
        orig_print = __builtins__["print"]
        __builtins__["print"] = lambda *args, **kwargs: None


def reenablePrinting():
    # Singleton, pylint: disable=global-statement
    global orig_print

    if orig_print is not None:
        __builtins__["print"] = orig_print
        orig_print = None


_debug_python = isDebugPython()


def getTotalReferenceCount():
    # Force clear internal caches to stabilize the reference count, and not be
    # polluted by cached types or IO values. pylint: disable=protected-access
    if hasattr(sys, "_clear_internal_caches"):
        sys._clear_internal_caches()
    elif hasattr(sys, "_clear_type_cache"):
        sys._clear_type_cache()

    if _debug_python:
        gc.collect()
        return sys.gettotalrefcount()
    else:
        gc.collect()
        all_objects = gc.get_objects()

        # Sum object reference twice, once without the sum value type, then switch
        # the type, and use the type used to avoid the integers before that.
        result = 0.0
        for obj in all_objects:
            if type(obj) is float:
                continue

            result += sys.getrefcount(obj)

        result = int(result)

        for obj in all_objects:
            if type(obj) is not float:
                continue

            result += sys.getrefcount(obj)

        return result


def checkReferenceCount(checked_function, max_rounds=20, explain=False, no_print=True):
    # This is obviously going to be complex, pylint: disable=too-many-branches

    # Clean start conditions.
    assert sys.exc_info() == (None, None, None), sys.exc_info()

    my_print(checked_function.__name__ + ": ", end="")
    sys.stdout.flush()

    if no_print:
        disablePrinting()

    # Make sure reference for these are already taken at the start.
    ref_count1 = 17
    ref_count2 = 17

    if explain:
        cleanObjRefCntMaps()

    assert max_rounds > 0

    result = False

    for count in range(max_rounds):
        if explain and count == max_rounds - 1:
            snapObjRefCntMap(before=True)

        ref_count1 = getTotalReferenceCount()

        checked_function()

        ref_count2 = getTotalReferenceCount()

        # Not allowed, but happens when bugs occur.
        assert sys.exc_info() == (None, None, None), sys.exc_info()

        if ref_count1 == ref_count2:
            result = True
            break

        if explain and count == max_rounds - 1:
            snapObjRefCntMap(before=False)

    if no_print:
        reenablePrinting()

    if result:
        my_print("PASSED")
    else:
        my_print(
            "FAILED %d %d leaked %d" % (ref_count1, ref_count2, ref_count2 - ref_count1)
        )

        if explain:
            print("REPORT of differences:")
            assert m1
            assert m2

            # Using items will unwanted usages, pylint: disable=consider-using-dict-items
            for key in m1:
                if key not in m2:
                    my_print("*" * 80)
                    my_print("extra:", m1[key], key)
                elif m1[key] != m2[key]:
                    my_print("*" * 80)
                    my_print(m1[key], "->", m2[key], repr(key))
                else:
                    pass

            for key in m2:
                if key not in m1:
                    my_print("*" * 80)
                    my_print("missing:", m2[key], key)

                    # print m1[key]

    assert sys.exc_info() == (None, None, None), sys.exc_info()

    gc.collect()
    sys.stdout.flush()

    return result


def createSearchMode():
    # Dealing with many options, pylint: disable=too-many-branches

    parser = makeOptionsParser(
        usage="%prog [options]",
        epilog="""\
The following shortcuts are available for the default "search" mode:

resume [pattern]    : Same as "search --resume [pattern]"
skip [pattern]      : Same as "search --skip [pattern]" (resumes, skips current)
only [pattern]      : Same as "search --only-one [pattern]"
all [pattern]       : Same as "search --all [pattern]"
coverage [pattern]  : Same as "search --coverage [pattern]"

Examples:
  ./run_all.py                    # Run all tests, abort on first error
  ./run_all.py --all              # Run all tests, continue on error
  ./run_all.py --all --pattern=A* # Run all tests starting with A
  ./run_all.py resume             # Resume from last failed test
  ./run_all.py skip               # Skip failure and resume from next
  ./run_all.py only MyTest        # Run only MyTest.py
""",
    )

    select_group = parser.add_option_group("Select Tests")

    select_group.add_option(
        "--pattern",
        action="store",
        dest="pattern",
        default="",
        help="""\
Start at the first test matching the pattern. With '--only-one', execute only
the first matching test. Defaults to all tests.""",
    )
    select_group.add_option(
        "--all",
        action="store_true",
        dest="all",
        default=False,
        help="""\
Execute all tests, continue execution even after failure of one.""",
    )
    select_group.add_option(
        "--max-failures",
        action="store",
        dest="max_failures",
        default=None,
        type="int",
        help="""\
Stop after this many failures. Default is to not stop. Only valid with --all.""",
    )
    select_group.add_option(
        "--jobs",
        action="store",
        dest="jobs",
        default=None,
        help="""\
Pass this as the --jobs value to Nuitka.""",
    )

    del select_group

    debug_group = parser.add_option_group("Test features")

    debug_group.add_option(
        "--debug",
        action="store_true",
        dest="debug",
        default=False,
        help="""\
Executing all self checks possible to find errors in Nuitka, good for test coverage.
Defaults to off.""",
    )

    debug_group.add_option(
        "--commands",
        action="store_true",
        dest="show_commands",
        default=False,
        help="""Output commands being done in output comparison.
Defaults to off.""",
    )

    debug_group.add_option(
        "--resume",
        action="store_true",
        dest="resume",
        default=False,
        help="""Resume from last run.""",
    )

    debug_group.add_option(
        "--only-one",
        action="store_true",
        dest="only",
        default=False,
        help="""Execute only the first matching test.""",
    )

    debug_group.add_option(
        "--coverage",
        action="store_true",
        dest="coverage",
        default=False,
        help="""\
Run tests with coverage enabled.""",
    )

    del debug_group

    options, positional_args = parser.parse_args()

    if options.debug:
        addExtendedExtraOptions("--debug")

    if options.jobs is not None:
        addExtendedExtraOptions("--jobs=%s" % options.jobs)

    if options.show_commands:
        os.environ["NUITKA_TRACE_COMMANDS"] = "1"

    # Default to searching.
    mode = positional_args[0] if positional_args else "search"

    # Avoid having to use options style.
    if mode in ("search", "only", "coverage"):
        if len(positional_args) >= 2 and not options.pattern:
            options.pattern = positional_args[1]

    if mode == "resume":
        options.resume = True
    elif mode == "skip":
        options.resume = True
        options.skip = True
    elif mode == "only":
        options.only = True
    elif mode == "all":
        options.all = True
    elif mode == "coverage":
        options.coverage = True
    elif mode == "search":
        pass
    else:
        return test_logger.sysexit("Error, using unknown search mode %r" % mode)

    if options.max_failures is not None and not options.all:
        return test_logger.sysexit("Error, '--max-failures' requires '--all'.")

    if options.pattern and options.all:
        return test_logger.sysexit(
            "Error, '--pattern' cannot be combined with '--all'. Use only '--pattern' to run just the matching tests."
        )

    start_at = options.pattern.replace("/", os.path.sep) if options.pattern else None

    return SearchMode(
        logger=test_logger,
        start_at=start_at,
        start_dir=getStartDir(),
        resume=options.resume,
        only=options.only,
        abort_on_error=not options.all,
        skip=getattr(options, "skip", False),
        coverage=options.coverage,
        max_failures=options.max_failures,
    )


def reportSkip(reason, dirname, filename):
    case = os.path.join(dirname, filename)
    case = os.path.normpath(case)

    test_logger.info("Skipped, %s (%s)." % (case, reason))


def executeReferenceChecked(
    prefix,
    names,
    tests_skipped=(),
    tests_stderr=(),
    max_rounds=20,
    explain=False,
    no_print=True,
):
    gc.disable()

    extract_number = lambda name: int(name.replace(prefix, "") or "0")

    # Find the function names.
    matching_names = tuple(
        name for name in names if name.startswith(prefix) and name[-1].isdigit()
    )

    old_stderr = sys.stderr

    # Everything passed
    result = True

    for name in sorted(matching_names, key=extract_number):
        number = extract_number(name)

        # print(tests_skipped)
        if number in tests_skipped:
            my_print(name + ": SKIPPED (%s)" % tests_skipped[number])
            continue

        # Avoid non-raisable output.
        with getNullOutput() as null_output:
            try:
                if number in tests_stderr:
                    sys.stderr = null_output
            except OSError:  # Windows
                if not checkReferenceCount(
                    names[name],
                    explain=explain,
                    no_print=no_print,
                    max_rounds=max_rounds,
                ):
                    result = False
            else:
                if not checkReferenceCount(
                    names[name],
                    explain=explain,
                    no_print=no_print,
                    max_rounds=max_rounds,
                ):
                    result = False
            finally:
                if number in tests_stderr:
                    sys.stderr = old_stderr

    gc.enable()
    return result


def addToPythonPath(python_path, in_front=False):
    if type(python_path) in (tuple, list):
        python_path = os.pathsep.join(python_path)

    if python_path:
        if "PYTHONPATH" in os.environ:
            if in_front:
                os.environ["PYTHONPATH"] = (
                    python_path + os.pathsep + os.environ["PYTHONPATH"]
                )
            else:
                os.environ["PYTHONPATH"] += os.pathsep + python_path
        else:
            os.environ["PYTHONPATH"] = python_path


@contextmanager
def withPythonPathChange(python_path):
    if python_path:
        if type(python_path) not in (tuple, list):
            python_path = python_path.split(os.pathsep)

        python_path = [
            os.path.normpath(os.path.abspath(element)) for element in python_path
        ]

        python_path = os.pathsep.join(python_path)

        if "PYTHONPATH" in os.environ:
            old_path = os.environ["PYTHONPATH"]
            os.environ["PYTHONPATH"] += os.pathsep + python_path
        else:
            old_path = None
            os.environ["PYTHONPATH"] = python_path

    yield

    if python_path:
        if old_path is None:
            del os.environ["PYTHONPATH"]
        else:
            os.environ["PYTHONPATH"] = old_path


def addExtendedExtraOptions(*args):
    old_value = os.getenv("NUITKA_EXTRA_OPTIONS")

    value = old_value

    for arg in args:
        if value is None:
            value = arg
        else:
            value += " " + arg

    os.environ["NUITKA_EXTRA_OPTIONS"] = value

    return old_value


@contextmanager
def withExtendedExtraOptions(*args):
    assert args

    old_value = addExtendedExtraOptions(*args)

    yield

    if old_value is None:
        del os.environ["NUITKA_EXTRA_OPTIONS"]
    else:
        os.environ["NUITKA_EXTRA_OPTIONS"] = old_value


def indentedCode(codes, count):
    """Indent code, used for generating test codes."""
    indent = " " * count
    return "\n".join(indent + line if line else "" for line in codes)


def compileLibraryPath(search_mode, path, stage_dir, decide, action):
    my_print("Checking standard library path:", path)

    for root, dirnames, filenames in os.walk(path):
        dirnames_to_remove = [dirname for dirname in dirnames if "-" in dirname]

        for dirname in dirnames_to_remove:
            dirnames.remove(dirname)

        dirnames.sort()

        filenames = [filename for filename in filenames if decide(root, filename)]

        for filename in sorted(filenames):
            if not search_mode.consider(root, filename):
                continue

            full_path = os.path.join(root, filename)

            my_print(full_path, ":", end=" ")
            sys.stdout.flush()

            action(stage_dir, path, full_path)


def compileLibraryTest(search_mode, stage_dir, decide, action):
    if not os.path.exists(stage_dir):
        os.makedirs(stage_dir)

    my_dirname = getNormalizedPathJoin(os.path.dirname(__file__), "../../..")

    paths = [
        path
        for path in sys.path
        if not isFilenameSameAsOrBelowPath(path=my_dirname, filename=path)
        if not isStandardLibraryPath(path)
        if os.path.isdir(path)
        if path != sys.prefix
    ]

    my_print("Using library paths:")
    for path in paths:
        my_print(path)

    for path in paths:
        print("Checking path:", path)
        compileLibraryPath(
            search_mode=search_mode,
            path=path,
            stage_dir=stage_dir,
            decide=decide,
            action=action,
        )

    search_mode.finish()


def run_async(coro):
    """Execute a coroutine until it's done."""

    values = []
    result = None
    while True:
        try:
            values.append(coro.send(None))
        except StopIteration as ex:
            result = ex.args[0] if ex.args else None
            break
    return values, result


def async_iterate(g):
    """Execute async generator until it's done."""

    # Test code for Python3, catches all kinds of exceptions.
    # pylint: disable=broad-except

    # Also Python3 only, pylint: disable=I0021,undefined-variable

    res = []
    while True:
        try:
            g.__anext__().__next__()
        except StopAsyncIteration:
            res.append("STOP")
            break
        except StopIteration as ex:
            if ex.args:
                res.append("StopIteration args %s" % ex.args)
            else:
                res.append("EMPTY StopIteration")
                break
        except Exception as ex:
            res.append(str(type(ex)))

    return res


def sync_iterate(g):
    """Execute sync generator until it's done."""
    res = []

    while True:
        try:
            if str is bytes:
                res.append(next(g))
            else:
                res.append(g.__next__())
        except StopIteration as ex:
            if ex.args:
                res.append("StopIteration arg %s" % ex.args[0])
            else:
                res.append("EMPTY StopIteration")

            break
        except Exception as ex:  # pylint: disable=broad-exception-caught

            res.append(str(type(ex)))

    return res


def getTestingCacheDir():
    cache_dir = getCacheDir("tests_state")
    makePath(cache_dir)
    return cache_dir


def getTestingCPythonOutputsCacheDir():
    cache_dir = getCacheDir("cpython_outputs")

    result = os.path.join(cache_dir, os.getenv("NUITKA_TEST_SUITE", ""))

    makePath(result)
    return result


def scanDirectoryForTestCases(
    dirname, template_context=None, cases_are_directories=False
):
    filenames = os.listdir(dirname)

    if not cases_are_directories:
        filenames = [
            filename
            for filename in filenames
            if (filename.endswith(".py") and filename + ".j2" not in filenames)
            or filename.endswith(".j2")
        ]

    for filename in sorted(filenames):
        if filename.endswith((".build", ".onefile-build", ".dist", ".app")):
            continue

        filename_full = os.path.join(dirname, filename)

        if cases_are_directories:
            if not os.path.isdir(filename_full):
                continue
        else:
            if not os.path.isfile(filename_full):
                continue

        if not decideFilenameVersionSkip(filename):
            continue

        if cases_are_directories and filename.endswith(".j2"):
            # Needs to be a dictionary with template arguments.
            assert template_context is not None

            template = getTemplate(
                package_name=None,
                template_name=filename,
                template_subdir=dirname,
                extensions=("jinja2.ext.do",),
            )

            code = template.render(name=template.name, **template_context)

            filename = filename[:-3]
            with openTextFile(filename, "w") as output:
                output.write(
                    "'''Automatically generated test, not part of releases or git.\n\n'''\n"
                )

                output.write(code)

        yield filename


def scanDirectoryForTestCaseFolders(dirname, allow_none=False):
    filenames = os.listdir(dirname)

    for filename in sorted(filenames):
        if not decideFilenameVersionSkip(filename + ".py"):
            continue

        filename = os.path.join(dirname, filename)
        filename = os.path.relpath(filename)

        if (
            not os.path.isdir(filename)
            or filename.endswith((".build", ".onefile-build", ".dist", ".app"))
            or os.path.basename(filename).startswith("venv_")
        ):
            continue

        filename_main = getMainProgramFilename(filename, allow_none=allow_none)

        yield filename, filename_main


def setupCacheHashSalt(test_code_path):
    assert os.path.exists(test_code_path)

    if os.path.exists(os.path.join(test_code_path, ".git")):
        git_cmd = ["git", "ls-tree", "-r", "HEAD", test_code_path]

        with getNullInput() as null_input:
            process = subprocess.Popen(
                args=git_cmd,
                stdin=null_input,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )

        stdout_git, stderr_git = process.communicate()
        assert process.returncode == 0, stderr_git

        salt_value = md5(stdout_git)
    else:
        salt_value = md5()

        for filename in getFileList(test_code_path):
            if filename.endswith(".py"):
                salt_value.update(getFileContents(filename, mode="rb"))

    os.environ["NUITKA_HASH_SALT"] = salt_value.hexdigest()


def displayFolderContents(name, path):
    test_logger.info("Listing of %s '%s':" % (name, path))
    flushStandardOutputs()

    if os.path.exists(path):
        if isWin32Windows():
            command = "dir /b /s /a:-D %s" % path
        else:
            command = "ls -Rla %s" % path

        os.system(command)
    else:
        test_logger.info("Does not exist.")


def displayFileContents(name, path):
    test_logger.info("Contents of %s %r:" % (name, path))

    if os.path.exists(path):
        for line in getFileContentByLine(path):
            my_print(line)
    else:
        test_logger.info("Does not exist.")

    flushStandardOutputs()


def someGenerator():
    yield 1
    yield 2
    yield 3


def someGeneratorRaising():
    yield 1
    raise TypeError(2)


# checks requirements needed to run each test module, according to the specified special comment
# special comments are in the following formats:
#     "# nuitka-skip-unless-expression: expression to be evaluated"
#       OR
#     "# nuitka-skip-unless-imports: module1,module2,..."
def checkTestRequirements(filename):
    if os.path.isdir(filename):
        candidate = os.path.join(filename, "__main__.py")

        if os.path.isfile(candidate):
            filename = candidate
        else:
            filename = os.path.join(filename, getMainProgramFilename(filename))

    for line in readSourceCodeFromFilename(None, filename).splitlines():
        if line.startswith("# nuitka-skip-unless-"):
            if line[21:33] == "expression: ":
                expression = line[33:]

                with getNullOutput() as null_output:
                    result = subprocess.call(
                        (
                            os.environ["PYTHON"],
                            "-c",
                            "import sys, os; sys.exit(not bool(%s))" % expression,
                        ),
                        stdout=null_output,
                        stderr=subprocess.STDOUT,
                    )
                if result != 0:
                    return (False, "Expression '%s' evaluated to false" % expression)

            elif line[21:30] == "imports: ":
                imports_needed = line[30:].rstrip().split(",")
                for i in imports_needed:
                    if not hasModule(i):
                        return (
                            False,
                            i
                            + " not installed for this Python version, but test needs it",
                        )
    # default return value
    return (True, "")


class DelayedExecutionThread(threading.Thread):
    def __init__(self, timeout, func):
        threading.Thread.__init__(self)
        self.timeout = timeout

        self.func = func

    def run(self):
        time.sleep(self.timeout)
        self.func()


def executeAfterTimePassed(message, timeout, func):
    test_logger.info(message % timeout)

    alarm = DelayedExecutionThread(timeout=timeout, func=func)
    alarm.start()


def killProcessGroup(process_name, pid):
    """Kill a process in a portable way.

    Right now SIGINT is used, unclear what to do on Windows
    with Python2 or non-related processes.
    """

    if isWin32Windows():
        return test_logger.sysexit("Error, cannot send kill signal on Windows")
    else:
        test_logger.info("Killing test process group '%s'." % process_name)
        os.killpg(pid, signal.SIGINT)


def checkLoadedFileAccesses(loaded_filenames, current_dir):
    # Many details to consider, pylint: disable=too-many-branches,too-many-statements

    current_dir = os.path.normpath(current_dir)
    current_dir = os.path.normcase(current_dir)
    current_dir_ext = os.path.normcase(getExternalUsePath(current_dir))
    current_dir_real = os.path.realpath(current_dir)

    illegal_accesses = []

    for loaded_filename in loaded_filenames:
        orig_loaded_filename = loaded_filename

        loaded_filename = os.path.normpath(loaded_filename)
        loaded_filename = os.path.normcase(loaded_filename)
        loaded_basename = os.path.basename(loaded_filename)

        if isWin32Windows():
            # spell-checker: ignore SYSTEMROOT
            if areSamePaths(
                os.path.dirname(loaded_filename),
                os.path.normpath(os.path.join(os.environ["SYSTEMROOT"], "System32")),
            ):
                continue
            if areSamePaths(
                os.path.dirname(loaded_filename),
                os.path.normpath(os.path.join(os.environ["SYSTEMROOT"], "SysWOW64")),
            ):
                continue

            # spell-checker: ignore winsxs
            if r"windows\winsxs" in loaded_filename:
                continue

            # GitHub actions have these in PATH overriding SYSTEMROOT,
            # spell-checker: ignore tortoisesvn
            if r"windows performance toolkit" in loaded_filename:
                continue
            if r"powershell" in loaded_filename:
                continue
            if r"azure dev spaces cli" in loaded_filename:
                continue
            if r"tortoisesvn" in loaded_filename:
                continue

        if loaded_filename.startswith(current_dir):
            continue

        if loaded_filename.startswith(os.path.abspath(current_dir)):
            continue

        if loaded_filename.startswith(current_dir_ext):
            continue

        if loaded_filename.startswith(current_dir_real):
            continue

        # Parent directories are OK too
        if (
            loaded_filename in getParentDirectories(current_dir)
            or loaded_filename in getParentDirectories(current_dir_ext)
            or loaded_filename in getParentDirectories(current_dir_real)
        ):
            continue

        ignore = True
        for ignored_dir in (
            # System configuration is OK
            "/etc",
            "/usr/etc",
            "/usr/local/etc",
            # Runtime user state and kernel information is OK.
            "/proc",
            "/dev",
            "/run",
            "/sys",
            "/tmp",
            "/var",
            "/private/var",
            # Locals may of course be loaded.
            "/usr/lib/locale",
            "/usr/share/locale",
            "/usr/share/X11/locale",
            # Themes may of course be loaded.
            "/usr/share/themes",
            # Terminal info files are OK too.
            "/usr/share/terminfo",
            "/lib/terminfo",
        ):
            if isFilenameSameAsOrBelowPath(ignored_dir, loaded_filename):
                ignore = False
                break
        if not ignore:
            continue

        # Themes may of course be loaded.
        if loaded_filename.startswith("/usr/share/themes"):
            continue
        if "gtk" in loaded_filename and "/engines/" in loaded_filename:
            continue

        if loaded_filename in (
            "/usr",
            "/usr/local",
            "/usr/local/lib",
            "/usr/share",
            "/usr/local/share",
            "/usr/lib64",
        ):
            continue

        # TCL/tk for tkinter for non-Windows is OK, spell-checker: ignore tcltk
        if loaded_filename.startswith(
            (
                "/usr/lib/tcltk/",
                "/usr/share/tcltk/",
                "/usr/lib/tcl/",
                "/usr/lib64/tcl/",
            )
        ):
            continue
        if loaded_filename in (
            "/usr/lib/tcltk",
            "/usr/share/tcltk",
            "/usr/lib/tcl",
            "/usr/lib64/tcl",
        ):
            continue

        if loaded_filename in (
            "/lib",
            "/lib64",
            "/lib/sse2",
            "/lib/tls",
            "/lib64/tls",
            "/usr/lib/sse2",
            "/usr/lib/tls",
            "/usr/lib64/tls",
        ):
            continue

        if loaded_filename in ("/usr/share/tcl8.6", "/usr/share/tcl8.5"):
            continue
        if loaded_filename in (
            "/usr/share/tcl8.6/init.tcl",
            "/usr/share/tcl8.5/init.tcl",
        ):
            continue
        if loaded_filename in (
            "/usr/share/tcl8.6/encoding",
            "/usr/share/tcl8.5/encoding",
        ):
            continue

        # System SSL config on Linux. TODO: Should this not be included and
        # read from dist folder.
        if loaded_basename == "openssl.cnf":
            continue

        # Taking these from system is harmless and desirable, spell-checker: ignore libz libgcc
        if loaded_basename.startswith(("libz.so", "libgcc_s.so")):
            continue

        # System C libraries are to be expected,
        # spell-checker: ignore libm,libdl,libpthread,libanl,libcrypt
        # spell-checker: ignore libcidn,libBrokenLocale,libSegFault,libutil
        # spell-checker: ignore libnsl,libnss_compat,libnss_db,libnss_dns
        # spell-checker: ignore libnss_files,libnss_hesiod,libnss_nis
        # spell-checker: ignore libnss_nisplus,libpcprofile,libresolv,
        # spell-checker: ignore librt,libthread_db,libmemusage,libmvec
        if loaded_basename.startswith(
            (
                "ld-linux-x86-64.so",
                "libc.so.",
                "libpthread.so.",
                "libm.so.",
                "libdl.so.",
                "libBrokenLocale.so.",
                "libSegFault.so",
                "libanl.so.",
                "libcidn.so.",
                "libcrypt.so.",
                "libmemusage.so",
                "libmvec.so.",
                "libnsl.so.",
                "libnss_compat.so.",
                "libnss_db.so.",
                "libnss_dns.so.",
                "libnss_files.so.",
                "libnss_hesiod.so.",
                "libnss_nis.so.",
                "libnss_nisplus.so.",
                "libpcprofile.so",
                "libresolv.so.",
                "librt.so.",
                "libthread_db-1.0.so",
                "libthread_db.so.",
                "libutil.so.",
            )
        ):
            continue

        # System C++ standard library is also OK.
        # spell-checker: ignore libstdc++
        if loaded_basename.startswith("libstdc++."):
            continue

        # Curses library is OK from system too.
        # spell-checker: ignore libtinfo
        if loaded_basename.startswith("libtinfo.so."):
            continue

        # Loaded by C library potentially for DNS lookups.
        # spell-checker: ignore libnss,libnsl,libattr,libbz2,libcap,
        # spell-checker: ignore libdw,libelf,liblzma,libselinux,libpcre,
        # spell-checker: ignore libblkid,libmount,libpcre2-8,libuuid
        if loaded_basename.startswith(
            (
                "libnss_",
                "libnsl",
                # Some systems load a lot more, this is CentOS 7 on OBS
                "libattr.so.",
                "libbz2.so.",
                "libcap.so.",
                "libdw.so.",
                "libelf.so.",
                "liblzma.so.",
                # Some systems load a lot more, this is Fedora 26 on OBS
                "libselinux.so.",
                "libpcre.so.",
                # And this is Fedora 29 on OBS
                "libblkid.so.",
                "libmount.so.",
                "libpcre2-8.so.",
                # CentOS 8 on OBS
                "libuuid.so.",
            )
        ):
            continue

        # Loaded by dtruss on macOS X.
        if loaded_filename.startswith("/usr/lib/dtrace/"):
            continue

        # Loaded by cowbuilder and pbuilder on Debian
        # spell-checker: ignore .ilist,cowbuilder,cowdancer,eatmydata
        if loaded_basename == ".ilist":
            continue
        if "cowdancer" in loaded_filename:
            continue
        if "eatmydata" in loaded_filename:
            continue

        # Loading from home directories is OK too.
        if any(
            isFilenameSameAsOrBelowPath(path, loaded_filename)
            for path in (
                "/home",
                "/data",
                "/root",
                "/Users",
                "/Library/Preferences",
                "/agent",  # Azure
            )
        ):
            continue

        # For Debian builders, /build is OK too.
        if loaded_filename.startswith("/build/") or loaded_filename == "/build":
            continue

        # TODO: Unclear, loading gconv from filesystem of installed system
        # may be OK or not. I think it should be.
        # spell-checker: ignore gconv,libicu
        if loaded_basename == "gconv-modules.cache":
            continue
        if "/gconv/" in loaded_filename:
            continue
        if loaded_basename.startswith("libicu"):
            continue
        if loaded_filename.startswith("/usr/share/icu/"):
            continue

        # Loading from caches is OK.
        if loaded_filename.startswith("/var/cache/"):
            continue

        # At least Python3.7 considers the default Python3 path and checks it.
        if loaded_filename == "/usr/bin/python3":
            continue

        # Accessing the versioned Python3.x binary is also happening.
        if loaded_filename in (
            "/usr/bin/python." + version for version in getTestExecutionPythonVersions()
        ):
            continue

        binary_path = _python_executable

        found = False
        while binary_path:
            if loaded_filename == binary_path:
                found = True
                break

            if binary_path == os.path.dirname(binary_path):
                break

            binary_path = os.path.dirname(binary_path)

            if loaded_filename == os.path.join(
                binary_path,
                "python" + ("%d%d" % (_python_version[0], _python_version[1])),
            ):
                found = True
                break

        if found:
            continue

        lib_prefix_dir = "/usr/lib/python%d.%s" % (
            _python_version[0],
            _python_version[1],
        )

        # TODO: These must all go away, we should not compile from Debian packages at all,
        # it is warned against, and it really don't matter what wrong files that accesses
        # or not.

        # PySide accesses its directory.
        if loaded_filename == os.path.join(lib_prefix_dir, "dist-packages/PySide"):
            continue

        # GTK accesses package directories only, spell-checker: ignore gobject
        if loaded_filename == os.path.join(lib_prefix_dir, "dist-packages/gtk-2.0/gtk"):
            continue
        if loaded_filename == os.path.join(lib_prefix_dir, "dist-packages/glib"):
            continue
        if loaded_filename == os.path.join(lib_prefix_dir, "dist-packages/gtk-2.0/gio"):
            continue
        if loaded_filename == os.path.join(lib_prefix_dir, "dist-packages/gobject"):
            continue

        # PyQt5 and PySide6 seems to do this, but won't use contents then.
        if loaded_filename in (
            "/usr/lib/qt6/plugins",
            "/usr/lib/qt6",
            "/usr/lib64/qt6/plugins",
            "/usr/lib64/qt6",
            "/usr/lib/qt5/plugins",
            "/usr/lib/qt5",
            "/usr/lib64/qt5/plugins",
            "/usr/lib64/qt5",
            "/usr/lib/x86_64-linux-gnu/qt5/plugins",
            "/usr/lib/x86_64-linux-gnu/qt5",
            "/usr/lib/x86_64-linux-gnu",
            "/usr/lib",
        ):
            continue

        # Can look at the interpreters of the system.
        if loaded_basename in "python3":
            continue
        if loaded_basename in (
            "python%s" + supported_version
            for supported_version in getTestExecutionPythonVersions()
        ):
            continue

        # Current Python executable can actually be a symlink and
        # the real executable which it points to will be on the
        # loaded_filenames list. This is all fine, let's ignore it.
        # Also, because the loaded_filename can be yet another symlink
        # (this is weird, but it's true), let's better resolve its real
        # path too.
        if os.path.realpath(loaded_filename) == os.path.realpath(sys.executable):
            continue

        # Accessing SE-Linux is OK.
        if loaded_filename in ("/sys/fs/selinux", "/selinux"):
            continue

        # Looking at device is OK.
        if loaded_filename.startswith("/sys/devices/"):
            continue

        # Allow reading time zone info of local system.
        if loaded_filename.startswith("/usr/share/zoneinfo/"):
            continue

        # The access to .pth files has no effect.
        if loaded_filename.endswith(".pth"):
            continue

        # Looking at site-package dir alone is alone.
        if loaded_filename.endswith(getSitePackageCandidateNames()):
            continue

        # QtNetwork insist on doing this it seems, spell-checker: ignore libcrypto
        if loaded_basename.startswith(("libcrypto.so", "libssl.so")):
            continue

        # macOS uses these, spell-checker: ignore libfribidi
        if loaded_basename in (
            "libc.dylib",
            "libcrypto.1.0.0.dylib",
            "libssl.1.0.0.dylib",
            "libcrypto.1.1.dylib",
            "libffi.dylib",
            "libffi-trampolines.dylib",
            "libfribidi.dylib",
        ):
            continue

        # MSVC run time DLLs, due to SxS come from system.
        # spell-checker: ignore msvcr90
        if loaded_basename.upper() in ("MSVCRT.DLL", "MSVCR90.DLL"):
            continue

        if isMacOS():
            # spell-checker: ignore libexec,preboot
            ignore = True
            for ignored_dir in (
                "/System/Library",
                "/AppleInternal",
                "/System/Volumes/Preboot",
                "/usr/lib/system/",
                "/usr/libexec/swift",
            ):
                if isFilenameSameAsOrBelowPath(ignored_dir, loaded_filename):
                    ignore = False
                    break
            if not ignore:
                continue

            if loaded_filename == "/System/Library":
                continue

            if loaded_filename == "/usr/libexec/rosetta/runtime":
                continue

            # spell-checker: ignore libfakelink,liboah,libobjc
            if loaded_filename in (
                "/usr/lib/libSystem.B.dylib",
                "/usr/lib/libc++.1.dylib",
                "/usr/lib/libc++abi.dylib",
                "/usr/lib/libfakelink.dylib",
                "/usr/lib/liboah.dylib",
                "/usr/lib/libobjc.A.dylib",
            ):
                continue

        illegal_accesses.append(orig_loaded_filename)

    return illegal_accesses


def getMainProgramFilename(filename, allow_none=False):
    for filename_main in os.listdir(filename):
        if filename_main.endswith(("Main.py", "Main")):
            return filename_main

        if filename_main in (
            "setup.py",
            "setup.cfg",
            "pyproject.cpython.toml",
            "pyproject.nuitka.toml",
        ):
            return filename_main

    if allow_none:
        return None

    return test_logger.sysexit(
        """\
Error, no file ends with 'Main.py' or 'Main' in '%s', incomplete test case."""
        % (filename)
    )


def getInstalledPythonVersion(python_version, must_exist):
    result = findInstalledPython(
        python_versions=(python_version,),
        module_specs=None,
    )

    if result is None and must_exist:
        return test_logger.sysexit(
            "Error, cannot find required Python version %s installation."
            % python_version
        )

    return result


_sys_path_path = None


def getPythonSysPath():
    global _sys_path_path  # singleton, pylint: disable=global-statement

    if _sys_path_path is None:
        _sys_path_path = check_output(
            [
                os.environ["PYTHON"],
                "-c",
                "import sys, os; print(os.pathsep.join(sys.path))",
            ]
        )

        if str is not bytes:
            _sys_path_path = _sys_path_path.decode("utf8")

        _sys_path_path = _sys_path_path.strip()

    return _sys_path_path


_web_server_process = None
_web_server_port = 27272
_web_server_hostname = "localhost"


def getLocalWebServerUrl():
    return "http://%s:%d" % (_web_server_hostname, _web_server_port)


def getLocalWebServerDir(base_dir):
    global _web_server_process  # singleton, pylint: disable=global-statement

    web_dir = os.path.join(getTempDir(), "local-web-server", base_dir)

    if _web_server_process is None:
        web_server_directory_supporting_pythons = tuple(
            python_version
            for python_version in getTestExecutionPythonVersions()
            if python_version not in ("3.6", "3.5", "3.4", "2.7", "2.6")
        )

        web_server_python = findInstalledPython(
            python_versions=web_server_directory_supporting_pythons,
            module_specs=None,
        )

        if web_server_python is None:
            return None

        os.makedirs(web_dir)

        command = [
            web_server_python.getPythonExe(),
            "-m",
            "http.server",
            "--bind",
            _web_server_hostname,
            "--dir",
            web_dir,
            str(_web_server_port),
        ]

        test_logger.my_print(" ".join(command))

        _web_server_process = createProcess(
            command,
            stdout=sys.stderr,
            stderr=sys.stderr,
        )

        def killWebServerProcess():
            _web_server_process.kill()

        atexit.register(killWebServerProcess)

    return web_dir


def traceExecutedCommand(description, command):
    my_print(description, ":", *command, style="pink")


def extractNuitkaVersionFromFilePath(version_filename):
    with openTextFile(version_filename, "r") as f:
        option_lines = f.readlines()

    (version_line,) = [line for line in option_lines if line.startswith("Nuitka V")]

    return version_line[8:].rstrip()


def decryptOutput(project_options, output):
    nuitka_decrypt_call = [
        os.getenv("PYTHON", sys.executable),
        "-m",
        "nuitka.tools.commercial.decrypt",  # Note: Needed for Python2.6
    ]

    for project_option in project_options:
        if project_option.startswith("--encryption-key="):
            nuitka_decrypt_call.append("--key=" + project_option.split("=", 1)[1])

    process_result = executeProcess(
        nuitka_decrypt_call,
        stdin=output,
    )

    assert process_result.exit_code == 0, process_result.stderr

    return process_result.stdout


#     Part of "Nuitka", an optimizing Python compiler that is compatible and
#     integrates with CPython, but also works on its own.
#
#     Licensed under the GNU Affero General Public License, Version 3 (the "License");
#     you may not use this file except in compliance with the License.
#     You may obtain a copy of the License at
#
#        https://www.gnu.org/licenses/agpl-3.0.txt
#
#     See also: "Nuitka Runtime Library Exception, Version 1.0" in file
#     "LICENSE-RUNTIME.txt" for additional permissions granted under Section 7.
#
#     Unless required by applicable law or agreed to in writing, software
#     distributed under the License is distributed on an "AS IS" BASIS,
#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#     See the License for the specific language governing permissions and
#     limitations under the License.
