tests.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. #-----------------------------------------------------------------------------
  2. # Copyright (c) 2005-2023, 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 inspect
  15. import sys
  16. import textwrap
  17. import pytest
  18. from PyInstaller.utils.hooks import check_requirement
  19. # Wrap some pytest decorators to be consistent in tests.
  20. parametrize = pytest.mark.parametrize
  21. skipif = pytest.mark.skipif
  22. xfail = pytest.mark.xfail
  23. skip = pytest.mark.skip
  24. # Use these decorators to use the `pyi_builder` fixture only in onedir or only in onefile mode instead of both.
  25. onedir_only = pytest.mark.parametrize('pyi_builder', ['onedir'], indirect=True)
  26. onefile_only = pytest.mark.parametrize('pyi_builder', ['onefile'], indirect=True)
  27. def importorskip(package: str):
  28. """
  29. Skip a decorated test if **package** is not importable.
  30. Arguments:
  31. package:
  32. The name of the module. May be anything that is allowed after the ``import`` keyword. e.g. 'numpy' or
  33. 'PIL.Image'.
  34. Returns:
  35. A pytest marker which either skips the test or does nothing.
  36. This function intentionally does not import the module. Doing so can lead to `sys.path` and `PATH` being
  37. polluted, which then breaks later builds.
  38. """
  39. if not importable(package):
  40. return pytest.mark.skip(f"Can't import '{package}'.")
  41. return pytest.mark.skipif(False, reason=f"Don't skip: '{package}' is importable.")
  42. def importable(package: str):
  43. from importlib.util import find_spec
  44. # The find_spec() function is used by the importlib machinery to locate a module to import. Using it finds the
  45. # module but does not run it. Unfortunately, it does import parent modules to check submodules.
  46. if "." in package:
  47. # Using subprocesses is slow. If the top level module doesn't exist then we can skip it.
  48. if not importable(package.split(".")[0]):
  49. return False
  50. # This is a submodule, import it in isolation.
  51. from subprocess import DEVNULL, run
  52. return run([sys.executable, "-c", "import " + package], stdout=DEVNULL, stderr=DEVNULL).returncode == 0
  53. return find_spec(package) is not None
  54. def requires(requirement: str):
  55. """
  56. Mark a test to be skipped if **requirement** is not satisfied.
  57. Args:
  58. requirement:
  59. A distribution name and optional version specifier(s). See :func:`PyInstaller.utils.hooks.check_requirement`
  60. which this argument is forwarded to.
  61. Returns:
  62. Either a skip marker or a dummy marker.
  63. This function operates on distribution metadata, and does not import any modules.
  64. """
  65. if check_requirement(requirement):
  66. return pytest.mark.skipif(False, reason=f"Don't skip: '{requirement}' is satisfied.")
  67. else:
  68. return pytest.mark.skip(f"Requires {requirement}.")
  69. def gen_sourcefile(tmp_path, source, test_id=None):
  70. """
  71. Generate a source file for testing.
  72. The source will be written into a file named like the test-function. This file will then be passed to
  73. `test_script`. If you need other related file, e.g. as `.toc`-file for testing the content, put it at at the
  74. normal place. Just mind to take the basnename from the test-function's name.
  75. :param script: Source code to create executable from. This will be saved into a temporary file which is then
  76. passed on to `test_script`.
  77. :param test_id: Test-id for parametrized tests. If given, it will be appended to the script filename,
  78. separated by two underscores.
  79. """
  80. testname = inspect.stack()[1][3]
  81. if test_id:
  82. # For parametrized test append the test-id.
  83. testname = testname + '__' + test_id
  84. # Periods are not allowed in Python module names.
  85. testname = testname.replace('.', '_')
  86. scriptfile = tmp_path / (testname + '.py')
  87. source = textwrap.dedent(source)
  88. scriptfile.write_text(source, encoding='utf-8')
  89. return scriptfile