tests.py 5.6 KB

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