zmc
2023-08-08 e792e9a60d958b93aef96050644f369feb25d61b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
"""
Decorators for skipping PyInstaller tests when specific requirements are not met.
"""
 
import distutils.ccompiler
import inspect
import os
import shutil
import textwrap
 
import pytest
import sys
 
from PyInstaller.compat import is_win
 
# Wrap some pytest decorators to be consistent in tests.
parametrize = pytest.mark.parametrize
skipif = pytest.mark.skipif
xfail = pytest.mark.xfail
 
 
def _check_for_compiler():
    import tempfile
 
    # Change to some tempdir since cc.has_function() would compile into the current directory, leaving garbage.
    old_wd = os.getcwd()
    tmp = tempfile.mkdtemp()
    os.chdir(tmp)
    cc = distutils.ccompiler.new_compiler()
    if is_win:
        try:
            cc.initialize()
            has_compiler = True
        # This error is raised on Windows if a compiler can't be found.
        except distutils.errors.DistutilsPlatformError:
            has_compiler = False
    else:
        # The C standard library contains the ``clock`` function. Use that to determine if a compiler is installed. This
        # does not work on Windows::
        #
        #   Users\bjones\AppData\Local\Temp\a.out.exe.manifest : general error
        #   c1010070: Failed to load and parse the manifest. The system cannot
        #   find the file specified.
        has_compiler = cc.has_function('clock', includes=['time.h'])
    os.chdir(old_wd)
    # TODO: Find a way to remove the generated clockXXXX.c file, too
    shutil.rmtree(tmp)
    return has_compiler
 
 
# A decorator to skip tests if a C compiler is not detected.
has_compiler = _check_for_compiler()
skipif_no_compiler = skipif(not has_compiler, reason="Requires a C compiler")
 
skip = pytest.mark.skip
 
 
def importorskip(package: str):
    """
    Skip a decorated test if **package** is not importable.
 
    Arguments:
        package:
            The name of the module. May be anything that is allowed after the ``import`` keyword. e.g. 'numpy' or
            'PIL.Image'.
    Returns:
        A pytest marker which either skips the test or does nothing.
 
    This function intentionally does not import the module. Doing so can lead to `sys.path` and `PATH` being
    polluted, which then breaks later builds.
    """
    if not importable(package):
        return pytest.mark.skip(f"Can't import '{package}'.")
    return pytest.mark.skipif(False, reason=f"Don't skip: '{package}' is importable.")
 
 
def importable(package: str):
    from importlib.util import find_spec
 
    # The find_spec() function is used by the importlib machinery to locate a module to import. Using it finds the
    # module but does not run it. Unfortunately, it does import parent modules to check submodules.
    if "." in package:
        # Using subprocesses is slow. If the top level module doesn't exist then we can skip it.
        if not importable(package.split(".")[0]):
            return False
        # This is a submodule, import it in isolation.
        from subprocess import DEVNULL, run
        return run([sys.executable, "-c", "import " + package], stdout=DEVNULL, stderr=DEVNULL).returncode == 0
 
    return find_spec(package) is not None
 
 
def requires(requirement: str):
    """
    Mark a test to be skipped if **requirement** is not satisfied.
 
    Args:
        requirement:
            A distribution name and optionally a version. See :func:`pkg_resources.require` which this argument is
            forwarded to.
    Returns:
        Either a skip marker or a dummy marker.
 
    This function intentionally does not import the module. Doing so can lead to `sys.path` and `PATH` being
    polluted, which then breaks later builds.
    """
    import pkg_resources
    try:
        pkg_resources.require(requirement)
        return pytest.mark.skipif(False, reason=f"Don't skip: '{requirement}' is satisfied.")
    except pkg_resources.ResolutionError:
        return pytest.mark.skip("Requires " + requirement)
 
 
def gen_sourcefile(tmpdir, source, test_id=None):
    """
    Generate a source file for testing.
 
    The source will be written into a file named like the test-function. This file will then be passed to
    `test_script`. If you need other related file, e.g. as `.toc`-file for testing the content, put it at at the
    normal place. Just mind to take the basnename from the test-function's name.
 
    :param script: Source code to create executable from. This will be saved into a temporary file which is then
                   passed on to `test_script`.
 
    :param test_id: Test-id for parametrized tests. If given, it will be appended to the script filename,
                    separated by two underscores.
 
    Ensure that the caller of `test_source` is in a UTF-8 encoded file with the correct '# -*- coding: utf-8 -*-'
    marker.
    """
    testname = inspect.stack()[1][3]
    if test_id:
        # For parametrized test append the test-id.
        testname = testname + '__' + test_id
 
    # Periods are not allowed in Python module names.
    testname = testname.replace('.', '_')
    scriptfile = tmpdir / (testname + '.py')
    source = textwrap.dedent(source)
    with scriptfile.open('w', encoding='utf-8') as ofh:
        print('# -*- coding: utf-8 -*-', file=ofh)
        print(source, file=ofh)
    return scriptfile