tests.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  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 os
  15. import sys
  16. import distutils.ccompiler
  17. import inspect
  18. import textwrap
  19. import shutil
  20. import pytest
  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, sys
  28. # change to some tempdir since cc.has_function() would compile into the
  29. # current directory, leaving garbage
  30. old_wd = os.getcwd()
  31. tmp = tempfile.mkdtemp()
  32. os.chdir(tmp)
  33. cc = distutils.ccompiler.new_compiler()
  34. if is_win:
  35. try:
  36. cc.initialize()
  37. has_compiler = True
  38. # This error is raised on Windows if a compiler can't be found.
  39. except distutils.errors.DistutilsPlatformError:
  40. has_compiler = False
  41. else:
  42. # The C standard library contains the ``clock`` function. Use that to
  43. # determine if a compiler is installed. This doesn't work on Windows::
  44. #
  45. # Users\bjones\AppData\Local\Temp\a.out.exe.manifest : general error
  46. # c1010070: Failed to load and parse the manifest. The system cannot
  47. # find the file specified.
  48. has_compiler = cc.has_function('clock', includes=['time.h'])
  49. os.chdir(old_wd)
  50. # TODO: Find a way to remove the generated clockXXXX.c file, too
  51. shutil.rmtree(tmp)
  52. return has_compiler
  53. # A decorator to skip tests if a C compiler isn't detected.
  54. has_compiler = _check_for_compiler()
  55. skipif_no_compiler = skipif(not has_compiler, reason="Requires a C compiler")
  56. skip = pytest.mark.skip
  57. def importorskip(package: str):
  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
  62. ``import`` keyword. e.g. 'numpy' or '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
  66. to `sys.path` and `PATH` being 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(
  71. False, reason=f"Don't skip: '{package}' is importable.")
  72. def importable(package: str):
  73. from importlib.util import find_spec
  74. # The find_spec() function is used by the importlib machinery to locate a
  75. # module to import. Using it finds the module but doesn't run it.
  76. # Unfortunately, it does import parent modules to check submodules.
  77. if "." in package:
  78. # Using subprocesses is slow. If the top level module doesn't exist
  79. # then we can skip it.
  80. if not importable(package.split(".")[0]):
  81. return False
  82. # This is a submodule, import it in isolation.
  83. from subprocess import run, DEVNULL
  84. return run([sys.executable, "-c", "import " + package],
  85. stdout=DEVNULL, stderr=DEVNULL).returncode == 0
  86. return find_spec(package) is not None
  87. def requires(requirement: str):
  88. """Mark a test to be skipped if **requirement** is not satisfied.
  89. Args:
  90. requirement:
  91. A distribution name and optionally a version. See
  92. :func:`pkg_resources.require` which this argument is
  93. forwarded to.
  94. Returns:
  95. Either a skip marker or a dummy marker.
  96. This function intentionally does not import the module. Doing so can lead
  97. to `sys.path` and `PATH` being polluted, which then breaks later builds.
  98. """
  99. import pkg_resources
  100. try:
  101. pkg_resources.require(requirement)
  102. return pytest.mark.skipif(
  103. False, reason=f"Don't skip: '{requirement}' is satisfied.")
  104. except pkg_resources.DistributionNotFound:
  105. return pytest.mark.skip("Requires " + requirement)
  106. def gen_sourcefile(tmpdir, source, test_id=None):
  107. """
  108. Generate a source file for testing.
  109. The source will be written into a file named like the
  110. test-function. This file will then be passed to `test_script`.
  111. If you need other related file, e.g. as `.toc`-file for
  112. testing the content, put it at at the normal place. Just mind
  113. to take the basnename from the test-function's name.
  114. :param script: Source code to create executable from. This
  115. will be saved into a temporary file which is
  116. then passed on to `test_script`.
  117. :param test_id: Test-id for parametrized tests. If given, it
  118. will be appended to the script filename,
  119. separated by two underscores.
  120. Ensure that the caller of `test_source` is in a UTF-8
  121. encoded file with the correct '# -*- coding: utf-8 -*-' marker.
  122. """
  123. testname = inspect.stack()[1][3]
  124. if test_id:
  125. # For parametrized test append the test-id.
  126. testname = testname + '__' + test_id
  127. # Periods are not allowed in Python module names.
  128. testname = testname.replace('.', '_')
  129. scriptfile = tmpdir / (testname + '.py')
  130. source = textwrap.dedent(source)
  131. with scriptfile.open('w', encoding='utf-8') as ofh:
  132. print(u'# -*- coding: utf-8 -*-', file=ofh)
  133. print(source, file=ofh)
  134. return scriptfile