diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..5732c0f --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +__*__/ diff --git a/tests/pythontest.spec b/tests/pythontest.spec new file mode 100644 index 0000000..32bb039 --- /dev/null +++ b/tests/pythontest.spec @@ -0,0 +1,78 @@ +%global basedir /opt/test/byte_compilation + +# We have 3 different ways of bytecompiling: for 3.9+, 3.4-3.8, and 2.7 +# Test with a representative of each. +%global python36_sitelib /usr/lib/python3.6/site-packages +%global python27_sitelib /usr/lib/python2.7/site-packages + +Name: pythontest +Version: 0 +Release: 0%{?dist} +Summary: ... +License: MIT +BuildRequires: python3-devel +BuildRequires: python3.6 +BuildRequires: python2.7 + +%description +... + +%install +mkdir -p %{buildroot}%{basedir}/directory/to/test/recursion + +echo "print()" > %{buildroot}%{basedir}/file.py +echo "print()" > %{buildroot}%{basedir}/directory/to/test/recursion/file_in_dir.py + +%py_byte_compile %{python3} %{buildroot}%{basedir}/file.py +%py_byte_compile %{python3} %{buildroot}%{basedir}/directory + +# Files in sitelib are compiled automatically by brp-python-bytecompile +mkdir -p %{buildroot}%{python3_sitelib}/directory/ +echo "print()" > %{buildroot}%{python3_sitelib}/directory/file.py + +mkdir -p %{buildroot}%{python36_sitelib}/directory/ +echo "print()" > %{buildroot}%{python36_sitelib}/directory/file.py + +mkdir -p %{buildroot}%{python27_sitelib}/directory/ +echo "print()" > %{buildroot}%{python27_sitelib}/directory/file.py + +%check +LOCATIONS=" + %{buildroot}%{basedir} + %{buildroot}%{python3_sitelib}/directory/ + %{buildroot}%{python36_sitelib}/directory/ + %{buildroot}%{python27_sitelib}/directory/ +" + +# Count .py and .pyc files +PY=$(find $LOCATIONS -name "*.py" | wc -l) +PYC=$(find $LOCATIONS -name "*.py[co]" | wc -l) + +# We should have 5 .py files (3 for python3, one each for 3.6 & 2.7) +test $PY -eq 5 + +# Every .py file should be byte-compiled to two .pyc files (optimization level 0 and 1) +# so we should have two times more .pyc files than .py files +test $(expr $PY \* 2) -eq $PYC + +# In this case the .pyc files should be identical across omtimization levels +# (they don't use docstrings and assert staements) +# So they should be hardlinked; the number of distinct inodes should match the +# number of source files. (Or be smaller, if the dupe detection is done +# across all files.) + +INODES=$(stat --format %i $(find $LOCATIONS -name "*.py[co]") | sort -u | wc -l) +test $PY -ge $INODES + + +%files +%pycached %{basedir}/file.py +%pycached %{basedir}/directory/to/test/recursion/file_in_dir.py +%pycached %{python3_sitelib}/directory/file.py +%pycached %{python36_sitelib}/directory/file.py +%{python27_sitelib}/directory/file.py* + + +%changelog +* Thu Jan 01 2015 Fedora Packager - 0-0 +- This changelog entry exists and is deliberately set in the past diff --git a/tests/test_evals.py b/tests/test_evals.py new file mode 100644 index 0000000..294faaa --- /dev/null +++ b/tests/test_evals.py @@ -0,0 +1,892 @@ +import os +import subprocess +import platform +import re +import sys +import textwrap + +import pytest + +X_Y = f'{sys.version_info[0]}.{sys.version_info[1]}' +XY = f'{sys.version_info[0]}{sys.version_info[1]}' + +# Handy environment variable you can use to run the tests +# with modified macros files. Multiple files should be +# separated by colon. +# You can use * if you escape it from your Shell: +# TESTED_FILES='macros.*' pytest -v +# Remember that some tests might need more macros files than just +# the local ones. You might need to use: +# TESTED_FILES='/usr/lib/rpm/macros:/usr/lib/rpm/platform/x86_64-linux/macros:macros.*' +TESTED_FILES = os.getenv("TESTED_FILES", None) + + +def rpm_eval(expression, fails=False, **kwargs): + cmd = ['rpmbuild'] + if TESTED_FILES: + cmd += ['--macros', TESTED_FILES] + for var, value in kwargs.items(): + if value is None: + cmd += ['--undefine', var] + else: + cmd += ['--define', f'{var} {value}'] + cmd += ['--eval', expression] + cp = subprocess.run(cmd, text=True, env={**os.environ, 'LANG': 'C.utf-8'}, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + if fails: + assert cp.returncode != 0, cp.stdout + elif fails is not None: + assert cp.returncode == 0, cp.stdout + return cp.stdout.strip().splitlines() + + +@pytest.fixture(scope="session") +def lib(): + lib_eval = rpm_eval("%_lib")[0] + if lib_eval == "%_lib" and TESTED_FILES: + raise ValueError( + "%_lib is not resolved to an actual value. " + "You may want to include /usr/lib/rpm/platform/x86_64-linux/macros to TESTED_FILES." + ) + return lib_eval + + +def get_alt_x_y(): + """ + Some tests require alternate Python version to be installed. + In order to allow any Python version (or none at all), + this function/fixture exists. + You can control the behavior by setting the $ALTERNATE_PYTHON_VERSION + environment variable to X.Y (e.g. 3.6) or SKIP. + The environment variable must be set. + """ + env_name = "ALTERNATE_PYTHON_VERSION" + alternate_python_version = os.getenv(env_name, "") + if alternate_python_version.upper() == "SKIP": + pytest.skip(f"${env_name} set to SKIP") + if not alternate_python_version: + raise ValueError(f"${env_name} must be set, " + f"set it to SKIP if you want to skip tests that " + f"require alternate Python version.") + if not re.match(r"^\d+\.\d+$", alternate_python_version): + raise ValueError(f"${env_name} must be X.Y") + return alternate_python_version + + +def get_alt_xy(): + """ + Same as get_alt_x_y() but without a dot + """ + return get_alt_x_y().replace(".", "") + + +# We don't use decorators, to be able to call the functions directly +alt_x_y = pytest.fixture(scope="session")(get_alt_x_y) +alt_xy = pytest.fixture(scope="session")(get_alt_xy) + + +# https://fedoraproject.org/wiki/Changes/PythonSafePath +def safe_path_flag(x_y): + return 'P' if tuple(int(i) for i in x_y.split('.')) >= (3, 11) else '' + + +def shell_stdout(script): + return subprocess.check_output(script, + env={**os.environ, 'LANG': 'C.utf-8'}, + text=True, + shell=True).rstrip() + + +@pytest.mark.parametrize('macro', ['%__python3', '%python3']) +def test_python3(macro): + assert rpm_eval(macro) == ['/usr/bin/python3'] + + +@pytest.mark.parametrize('macro', ['%__python3', '%python3']) +@pytest.mark.parametrize('pkgversion', ['3', '3.9', '3.12']) +def test_python3_with_pkgversion(macro, pkgversion): + assert rpm_eval(macro, python3_pkgversion=pkgversion) == [f'/usr/bin/python{pkgversion}'] + + +@pytest.mark.parametrize('argument, result', [ + ('a', 'a'), + ('a-a', 'a-a'), + ('a_a', 'a-a'), + ('a.a', 'a-a'), + ('a---a', 'a-a'), + ('a-_-a', 'a-a'), + ('a-_-a', 'a-a'), + ('a[b]', 'a[b]'), + ('Aha[Boom]', 'aha[boom]'), + ('a.a[b.b]', 'a-a[b-b]'), +]) +def test_pydist_name(argument, result): + assert rpm_eval(f'%py_dist_name {argument}') == [result] + + +def test_py2_dist(): + assert rpm_eval(f'%py2_dist Aha[Boom] a') == ['python2dist(aha[boom]) python2dist(a)'] + + +def test_py3_dist(): + assert rpm_eval(f'%py3_dist Aha[Boom] a') == ['python3dist(aha[boom]) python3dist(a)'] + + +def test_py3_dist_with_python3_pkgversion_redefined(alt_x_y): + assert rpm_eval(f'%py3_dist Aha[Boom] a', python3_pkgversion=alt_x_y) == [f'python{alt_x_y}dist(aha[boom]) python{alt_x_y}dist(a)'] + + +def test_python_provide_python(): + assert rpm_eval('%python_provide python-foo') == [] + + +def test_python_provide_python3(): + lines = rpm_eval('%python_provide python3-foo', version='6', release='1.fc66') + assert 'Obsoletes: python-foo < 6-1.fc66' in lines + assert 'Provides: python-foo = 6-1.fc66' in lines + assert f'Provides: python{X_Y}-foo = 6-1.fc66' in lines + assert len(lines) == 3 + + +def test_python_provide_python3_epoched(): + lines = rpm_eval('%python_provide python3-foo', epoch='1', version='6', release='1.fc66') + assert 'Obsoletes: python-foo < 1:6-1.fc66' in lines + assert 'Provides: python-foo = 1:6-1.fc66' in lines + assert f'Provides: python{X_Y}-foo = 1:6-1.fc66' in lines + assert len(lines) == 3 + + +def test_python_provide_python3X(): + lines = rpm_eval(f'%python_provide python{X_Y}-foo', version='6', release='1.fc66') + assert 'Obsoletes: python-foo < 6-1.fc66' in lines + assert 'Provides: python-foo = 6-1.fc66' in lines + assert 'Provides: python3-foo = 6-1.fc66' in lines + assert len(lines) == 3 + + +def test_python_provide_python3X_epoched(): + lines = rpm_eval(f'%python_provide python{X_Y}-foo', epoch='1', version='6', release='1.fc66') + assert 'Obsoletes: python-foo < 1:6-1.fc66' in lines + assert 'Provides: python-foo = 1:6-1.fc66' in lines + assert 'Provides: python3-foo = 1:6-1.fc66' in lines + assert len(lines) == 3 + + +def test_python_provide_doubleuse(): + lines = rpm_eval('%{python_provide python3-foo}%{python_provide python3-foo}', + version='6', release='1.fc66') + assert 'Obsoletes: python-foo < 6-1.fc66' in lines + assert 'Provides: python-foo = 6-1.fc66' in lines + assert f'Provides: python{X_Y}-foo = 6-1.fc66' in lines + assert len(lines) == 6 + assert len(set(lines)) == 3 + + +@pytest.mark.parametrize('rhel', [None, 10]) +def test_py_provides_python(rhel): + lines = rpm_eval('%py_provides python-foo', version='6', release='1.fc66', rhel=rhel) + assert 'Provides: python-foo = 6-1.fc66' in lines + assert len(lines) == 1 + + +@pytest.mark.parametrize('rhel', [None, 12]) +def test_py_provides_whatever(rhel): + lines = rpm_eval('%py_provides whatever', version='6', release='1.fc66', rhel=rhel) + assert 'Provides: whatever = 6-1.fc66' in lines + assert len(lines) == 1 + + +@pytest.mark.parametrize('rhel', [None, 9]) +def test_py_provides_python3(rhel): + lines = rpm_eval('%py_provides python3-foo', version='6', release='1.fc66', rhel=rhel) + assert 'Provides: python3-foo = 6-1.fc66' in lines + assert 'Provides: python-foo = 6-1.fc66' in lines + assert f'Provides: python{X_Y}-foo = 6-1.fc66' in lines + if rhel: + assert f'Obsoletes: python{X_Y}-foo < 6-1.fc66' in lines + assert len(lines) == 4 + else: + assert len(lines) == 3 + + +@pytest.mark.parametrize('rhel', [None, 9]) +def test_py_provides_python3_with_isa(rhel): + lines = rpm_eval('%py_provides python3-foo(x86_64)', version='6', release='1.fc66', rhel=rhel) + assert 'Provides: python3-foo(x86_64) = 6-1.fc66' in lines + assert 'Provides: python-foo(x86_64) = 6-1.fc66' in lines + assert f'Provides: python{X_Y}-foo(x86_64) = 6-1.fc66' in lines + assert f'Obsoletes: python{X_Y}-foo(x86_64) < 6-1.fc66' not in lines + assert len(lines) == 3 + + +@pytest.mark.parametrize('rhel', [None, 13]) +def test_py_provides_python3_epoched(rhel): + lines = rpm_eval('%py_provides python3-foo', epoch='1', version='6', release='1.fc66', rhel=rhel) + assert 'Provides: python3-foo = 1:6-1.fc66' in lines + assert 'Provides: python-foo = 1:6-1.fc66' in lines + assert f'Provides: python{X_Y}-foo = 1:6-1.fc66' in lines + if rhel: + assert f'Obsoletes: python{X_Y}-foo < 1:6-1.fc66' in lines + assert len(lines) == 4 + else: + assert len(lines) == 3 + + +@pytest.mark.parametrize('rhel', [None, 13]) +def test_py_provides_python3X(rhel): + lines = rpm_eval(f'%py_provides python{X_Y}-foo', version='6', release='1.fc66', rhel=rhel) + assert f'Provides: python{X_Y}-foo = 6-1.fc66' in lines + assert 'Provides: python-foo = 6-1.fc66' in lines + assert 'Provides: python3-foo = 6-1.fc66' in lines + assert len(lines) == 3 + + +@pytest.mark.parametrize('rhel', [None, 27]) +def test_py_provides_python3X_epoched(rhel): + lines = rpm_eval(f'%py_provides python{X_Y}-foo', epoch='1', version='6', release='1.fc66', rhel=rhel) + assert f'Provides: python{X_Y}-foo = 1:6-1.fc66' in lines + assert 'Provides: python-foo = 1:6-1.fc66' in lines + assert 'Provides: python3-foo = 1:6-1.fc66' in lines + assert len(lines) == 3 + + +@pytest.mark.parametrize('rhel', [None, 2]) +def test_py_provides_doubleuse(rhel): + lines = rpm_eval('%{py_provides python3-foo}%{py_provides python3-foo}', + version='6', release='1.fc66', rhel=rhel) + assert 'Provides: python3-foo = 6-1.fc66' in lines + assert 'Provides: python-foo = 6-1.fc66' in lines + assert f'Provides: python{X_Y}-foo = 6-1.fc66' in lines + if rhel: + assert f'Obsoletes: python{X_Y}-foo < 6-1.fc66' in lines + assert len(lines) == 8 + assert len(set(lines)) == 4 + else: + assert len(lines) == 6 + assert len(set(lines)) == 3 + + +@pytest.mark.parametrize('rhel', [None, 2]) +def test_py_provides_with_evr(rhel): + lines = rpm_eval('%py_provides python3-foo 123', + version='6', release='1.fc66', rhel=rhel) + assert 'Provides: python3-foo = 123' in lines + assert 'Provides: python-foo = 123' in lines + assert f'Provides: python{X_Y}-foo = 123' in lines + if rhel: + assert f'Obsoletes: python{X_Y}-foo < 123' in lines + assert len(lines) == 4 + else: + assert len(lines) == 3 + + +def test_python_wheel_pkg_prefix(): + assert rpm_eval('%python_wheel_pkg_prefix', fedora='44', rhel=None, eln=None) == ['python'] + assert rpm_eval('%python_wheel_pkg_prefix', fedora='44', rhel=None, eln=None, python3_pkgversion='3.9') == ['python'] + assert rpm_eval('%python_wheel_pkg_prefix', fedora=None, rhel='1', eln='1') == ['python'] + assert rpm_eval('%python_wheel_pkg_prefix', fedora=None, rhel='1', eln=None) == ['python3'] + assert rpm_eval('%python_wheel_pkg_prefix', fedora=None, rhel='1', eln=None, python3_pkgversion='3.10') == ['python3.10'] + assert rpm_eval('%python_wheel_pkg_prefix', fedora=None, rhel='1', eln=None, python3_pkgversion='3.11') == ['python3.11'] + + +def test_python_wheel_dir(): + assert rpm_eval('%python_wheel_dir', fedora='44', rhel=None, eln=None) == ['/usr/share/python-wheels'] + assert rpm_eval('%python_wheel_dir', fedora='44', rhel=None, eln=None, python3_pkgversion='3.9') == ['/usr/share/python-wheels'] + assert rpm_eval('%python_wheel_dir', fedora=None, rhel='1', eln='1') == ['/usr/share/python-wheels'] + assert rpm_eval('%python_wheel_dir', fedora=None, rhel='1', eln=None) == ['/usr/share/python3-wheels'] + assert rpm_eval('%python_wheel_dir', fedora=None, rhel='1', eln=None, python3_pkgversion='3.10') == ['/usr/share/python3.10-wheels'] + assert rpm_eval('%python_wheel_dir', fedora=None, rhel='1', eln=None, python3_pkgversion='3.11') == ['/usr/share/python3.11-wheels'] + + +def test_pytest_passes_options_naturally(): + lines = rpm_eval('%pytest -k foo') + assert '/usr/bin/pytest -k foo' in lines[-1] + + +def test_pytest_different_command(): + lines = rpm_eval('%pytest', __pytest='pytest-3') + assert 'pytest-3' in lines[-1] + + +def test_pytest_command_suffix(): + lines = rpm_eval('%pytest -v') + assert '/usr/bin/pytest -v' in lines[-1] + +# this test does not require alternate Pythons to be installed +@pytest.mark.parametrize('version', ['3.6', '3.7', '3.12']) +def test_pytest_command_suffix_alternate_pkgversion(version): + lines = rpm_eval('%pytest -v', python3_pkgversion=version, python3_version=version) + assert f'/usr/bin/pytest-{version} -v' in lines[-1] + + +def test_pytest_sets_pytest_xdist_auto_num_workers(): + lines = rpm_eval('%pytest', _smp_build_ncpus=2) + assert 'PYTEST_XDIST_AUTO_NUM_WORKERS=2' in '\n'.join(lines) + + +def test_pytest_undefined_addopts_are_not_set(): + lines = rpm_eval('%pytest', __pytest_addopts=None) + assert 'PYTEST_ADDOPTS' not in '\n'.join(lines) + + +def test_pytest_defined_addopts_are_set(): + lines = rpm_eval('%pytest', __pytest_addopts="--ignore=stuff") + assert 'PYTEST_ADDOPTS="${PYTEST_ADDOPTS:-} --ignore=stuff"' in '\n'.join(lines) + + +@pytest.mark.parametrize('__pytest_addopts', ['--macronized-option', 'x y z', None]) +def test_pytest_addopts_preserves_envvar(__pytest_addopts): + # this is the line a packager might put in the spec file before running %pytest: + spec_line = 'export PYTEST_ADDOPTS="--exported-option1 --exported-option2"' + + # instead of actually running /usr/bin/pytest, + # we run a small shell script that echoes the tested value for inspection + lines = rpm_eval('%pytest', __pytest_addopts=__pytest_addopts, + __pytest="sh -c 'echo $PYTEST_ADDOPTS'") + + echoed = shell_stdout('\n'.join([spec_line] + lines)) + + # assert all values were echoed + assert '--exported-option1' in echoed + assert '--exported-option2' in echoed + if __pytest_addopts is not None: + assert __pytest_addopts in echoed + + # assert the options are separated + assert 'option--' not in echoed + assert 'z--' not in echoed + + +@pytest.mark.parametrize('__pytest_addopts', ['-X', None]) +def test_py3_test_envvars(lib, __pytest_addopts): + lines = rpm_eval('%{py3_test_envvars}\\\n%{python3} -m unittest', + buildroot='BUILDROOT', + _smp_build_ncpus='3', + __pytest_addopts=__pytest_addopts) + assert all(l.endswith('\\') for l in lines[:-1]) + stripped_lines = [l.strip(' \\') for l in lines] + sitearch = f'BUILDROOT/usr/{lib}/python{X_Y}/site-packages' + sitelib = f'BUILDROOT/usr/lib/python{X_Y}/site-packages' + assert f'PYTHONPATH="${{PYTHONPATH:-{sitearch}:{sitelib}}}"' in stripped_lines + assert 'PATH="BUILDROOT/usr/bin:$PATH"' in stripped_lines + assert 'CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"' in stripped_lines + assert 'PYTHONDONTWRITEBYTECODE=1' in stripped_lines + assert 'PYTEST_XDIST_AUTO_NUM_WORKERS=3' in stripped_lines + if __pytest_addopts: + assert f'PYTEST_ADDOPTS="${{PYTEST_ADDOPTS:-}} {__pytest_addopts}"' in stripped_lines + else: + assert 'PYTEST_ADDOPTS' not in ''.join(lines) + assert stripped_lines[-1] == '/usr/bin/python3 -m unittest' + + +def test_pypi_source_default_name(): + urls = rpm_eval('%pypi_source', + name='foo', version='6') + assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz'] + + +def test_pypi_source_default_srcname(): + urls = rpm_eval('%pypi_source', + name='python-foo', srcname='foo', version='6') + assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz'] + + +def test_pypi_source_default_pypi_name(): + urls = rpm_eval('%pypi_source', + name='python-foo', pypi_name='foo', version='6') + assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz'] + + +def test_pypi_source_default_name_uppercase(): + urls = rpm_eval('%pypi_source', + name='Foo', version='6') + assert urls == ['https://files.pythonhosted.org/packages/source/F/Foo/Foo-6.tar.gz'] + + +def test_pypi_source_provided_name(): + urls = rpm_eval('%pypi_source foo', + name='python-bar', pypi_name='bar', version='6') + assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz'] + + +def test_pypi_source_provided_name_version(): + urls = rpm_eval('%pypi_source foo 6', + name='python-bar', pypi_name='bar', version='3') + assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6.tar.gz'] + + +def test_pypi_source_provided_name_version_ext(): + url = rpm_eval('%pypi_source foo 6 zip', + name='python-bar', pypi_name='bar', version='3') + assert url == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6.zip'] + + +def test_pypi_source_prerelease(): + urls = rpm_eval('%pypi_source', + name='python-foo', pypi_name='foo', version='6~b2') + assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6b2.tar.gz'] + + +def test_pypi_source_explicit_tilde(): + urls = rpm_eval('%pypi_source foo 6~6', + name='python-foo', pypi_name='foo', version='6') + assert urls == ['https://files.pythonhosted.org/packages/source/f/foo/foo-6~6.tar.gz'] + + +def test_py3_shebang_fix(): + cmd = rpm_eval('%py3_shebang_fix arg1 arg2 arg3')[-1].strip() + assert cmd == '/usr/bin/python3 -B /usr/lib/rpm/redhat/pathfix.py -pni /usr/bin/python3 $shebang_flags arg1 arg2 arg3' + + +def test_py3_shebang_fix_default_shebang_flags(): + lines = rpm_eval('%py3_shebang_fix arg1 arg2') + lines[-1] = 'echo $shebang_flags' + assert shell_stdout('\n'.join(lines)) == f'-kas{safe_path_flag(X_Y)}' + + +def test_py3_shebang_fix_custom_shebang_flags(): + lines = rpm_eval('%py3_shebang_fix arg1 arg2', py3_shebang_flags='Es') + lines[-1] = 'echo $shebang_flags' + assert shell_stdout('\n'.join(lines)) == '-kaEs' + + +@pytest.mark.parametrize('_py3_shebang_s', [None, '%{nil}']) +def test_py3_shebang_fix_undefined_py3_shebang_s(_py3_shebang_s): + lines = rpm_eval('%py3_shebang_fix arg1 arg2', _py3_shebang_s=_py3_shebang_s) + lines[-1] = 'echo $shebang_flags' + expected = f'-ka{safe_path_flag(X_Y)}' if safe_path_flag(X_Y) else '-k' + assert shell_stdout('\n'.join(lines)) == expected + + +@pytest.mark.parametrize('_py3_shebang_P', [None, '%{nil}']) +def test_py3_shebang_fix_undefined_py3_shebang_P(_py3_shebang_P): + lines = rpm_eval('%py3_shebang_fix arg1 arg2', _py3_shebang_P=_py3_shebang_P) + lines[-1] = 'echo $shebang_flags' + assert shell_stdout('\n'.join(lines)) == '-kas' + + +@pytest.mark.parametrize('_py3_shebang_s', [None, '%{nil}']) +@pytest.mark.parametrize('_py3_shebang_P', [None, '%{nil}']) +def test_py3_shebang_fix_undefined_py3_shebang_sP(_py3_shebang_s, _py3_shebang_P): + lines = rpm_eval('%py3_shebang_fix arg1 arg2', + _py3_shebang_s=_py3_shebang_s, + _py3_shebang_P=_py3_shebang_P) + lines[-1] = 'echo $shebang_flags' + assert shell_stdout('\n'.join(lines)) == '-k' + + +@pytest.mark.parametrize('flags', [None, '%{nil}']) +def test_py3_shebang_fix_no_shebang_flags(flags): + lines = rpm_eval('%py3_shebang_fix arg1 arg2', py3_shebang_flags=flags) + lines[-1] = 'echo $shebang_flags' + assert shell_stdout('\n'.join(lines)) == '-k' + + +def test_py_shebang_fix_custom_python(): + cmd = rpm_eval('%py_shebang_fix arg1 arg2 arg3', __python='/usr/bin/pypy')[-1].strip() + assert cmd == '/usr/bin/pypy -B /usr/lib/rpm/redhat/pathfix.py -pni /usr/bin/pypy $shebang_flags arg1 arg2 arg3' + + +def test_pycached_in_sitelib(): + lines = rpm_eval('%pycached %{python3_sitelib}/foo*.py') + assert lines == [ + f'/usr/lib/python{X_Y}/site-packages/foo*.py', + f'/usr/lib/python{X_Y}/site-packages/__pycache__/foo*.cpython-{XY}{{,.opt-?}}.pyc' + ] + + +def test_pycached_in_sitearch(lib): + lines = rpm_eval('%pycached %{python3_sitearch}/foo*.py') + assert lines == [ + f'/usr/{lib}/python{X_Y}/site-packages/foo*.py', + f'/usr/{lib}/python{X_Y}/site-packages/__pycache__/foo*.cpython-{XY}{{,.opt-?}}.pyc' + ] + + +# this test does not require alternate Pythons to be installed +@pytest.mark.parametrize('version', ['3.6', '3.7', '3.12']) +def test_pycached_with_alternate_version(version): + version_nodot = version.replace('.', '') + lines = rpm_eval(f'%pycached /usr/lib/python{version}/site-packages/foo*.py') + assert lines == [ + f'/usr/lib/python{version}/site-packages/foo*.py', + f'/usr/lib/python{version}/site-packages/__pycache__/foo*.cpython-{version_nodot}{{,.opt-?}}.pyc' + ] + + +def test_pycached_in_custom_dir(): + lines = rpm_eval('%pycached /bar/foo*.py') + assert lines == [ + '/bar/foo*.py', + '/bar/__pycache__/foo*.cpython-3*{,.opt-?}.pyc' + ] + + +def test_pycached_with_exclude(): + lines = rpm_eval('%pycached %exclude %{python3_sitelib}/foo*.py') + assert lines == [ + f'%exclude /usr/lib/python{X_Y}/site-packages/foo*.py', + f'%exclude /usr/lib/python{X_Y}/site-packages/__pycache__/foo*.cpython-{XY}{{,.opt-?}}.pyc' + ] + + +def test_pycached_fails_with_extension_glob(): + lines = rpm_eval('%pycached %{python3_sitelib}/foo.py*', fails=True) + assert lines[0] == 'error: %pycached can only be used with paths explicitly ending with .py' + + +def test_python_extras_subpkg_i(): + lines = rpm_eval('%python_extras_subpkg -n python3-setuptools_scm -i %{python3_sitelib}/*.egg-info toml yaml', + version='6', release='7') + expected = textwrap.dedent(f""" + %package -n python3-setuptools_scm+toml + Summary: Metapackage for python3-setuptools_scm: toml extras + Requires: python3-setuptools_scm = 6-7 + %description -n python3-setuptools_scm+toml + This is a metapackage bringing in toml extras requires for + python3-setuptools_scm. + It makes sure the dependencies are installed. + + %files -n python3-setuptools_scm+toml + %ghost /usr/lib/python{X_Y}/site-packages/*.egg-info + + %package -n python3-setuptools_scm+yaml + Summary: Metapackage for python3-setuptools_scm: yaml extras + Requires: python3-setuptools_scm = 6-7 + %description -n python3-setuptools_scm+yaml + This is a metapackage bringing in yaml extras requires for + python3-setuptools_scm. + It makes sure the dependencies are installed. + + %files -n python3-setuptools_scm+yaml + %ghost /usr/lib/python{X_Y}/site-packages/*.egg-info + """).lstrip().splitlines() + assert lines == expected + + +def test_python_extras_subpkg_f(): + lines = rpm_eval('%python_extras_subpkg -n python3-setuptools_scm -f ghost_filelist toml yaml', + version='6', release='7') + expected = textwrap.dedent(f""" + %package -n python3-setuptools_scm+toml + Summary: Metapackage for python3-setuptools_scm: toml extras + Requires: python3-setuptools_scm = 6-7 + %description -n python3-setuptools_scm+toml + This is a metapackage bringing in toml extras requires for + python3-setuptools_scm. + It makes sure the dependencies are installed. + + %files -n python3-setuptools_scm+toml -f ghost_filelist + + %package -n python3-setuptools_scm+yaml + Summary: Metapackage for python3-setuptools_scm: yaml extras + Requires: python3-setuptools_scm = 6-7 + %description -n python3-setuptools_scm+yaml + This is a metapackage bringing in yaml extras requires for + python3-setuptools_scm. + It makes sure the dependencies are installed. + + %files -n python3-setuptools_scm+yaml -f ghost_filelist + """).lstrip().splitlines() + assert lines == expected + + +def test_python_extras_subpkg_F(): + lines = rpm_eval('%python_extras_subpkg -n python3-setuptools_scm -F toml yaml', + version='6', release='7') + expected = textwrap.dedent(f""" + %package -n python3-setuptools_scm+toml + Summary: Metapackage for python3-setuptools_scm: toml extras + Requires: python3-setuptools_scm = 6-7 + %description -n python3-setuptools_scm+toml + This is a metapackage bringing in toml extras requires for + python3-setuptools_scm. + It makes sure the dependencies are installed. + + + + %package -n python3-setuptools_scm+yaml + Summary: Metapackage for python3-setuptools_scm: yaml extras + Requires: python3-setuptools_scm = 6-7 + %description -n python3-setuptools_scm+yaml + This is a metapackage bringing in yaml extras requires for + python3-setuptools_scm. + It makes sure the dependencies are installed. + """).lstrip().splitlines() + assert lines == expected + + +def test_python_extras_subpkg_underscores(): + lines = rpm_eval('%python_extras_subpkg -n python3-webscrapbook -F adhoc_ssl', + version='0.33.3', release='1.fc33') + expected = textwrap.dedent(f""" + %package -n python3-webscrapbook+adhoc_ssl + Summary: Metapackage for python3-webscrapbook: adhoc_ssl extras + Requires: python3-webscrapbook = 0.33.3-1.fc33 + %description -n python3-webscrapbook+adhoc_ssl + This is a metapackage bringing in adhoc_ssl extras requires for + python3-webscrapbook. + It makes sure the dependencies are installed. + """).lstrip().splitlines() + assert lines == expected + + +@pytest.mark.parametrize('sep', [pytest.param(('', ' ', ' ', ''), id='spaces'), + pytest.param(('', ',', ',', ''), id='commas'), + pytest.param(('', ',', ',', ','), id='commas-trailing'), + pytest.param((',', ',', ',', ''), id='commas-leading'), + pytest.param((',', ',', ',', ','), id='commas-trailing-leading'), + pytest.param(('', ',', ' ', ''), id='mixture'), + pytest.param((' ', ' ', '\t\t, ', '\t'), id='chaotic-good'), + pytest.param(('', '\t ,, \t\r ', ',,\t , ', ',,'), id='chaotic-evil')]) +def test_python_extras_subpkg_arg_separators(sep): + lines = rpm_eval('%python_extras_subpkg -n python3-hypothesis -F {}cli{}ghostwriter{}pytz{}'.format(*sep), + version='6.6.0', release='1.fc35') + expected = textwrap.dedent(f""" + %package -n python3-hypothesis+cli + Summary: Metapackage for python3-hypothesis: cli extras + Requires: python3-hypothesis = 6.6.0-1.fc35 + %description -n python3-hypothesis+cli + This is a metapackage bringing in cli extras requires for python3-hypothesis. + It makes sure the dependencies are installed. + + + + %package -n python3-hypothesis+ghostwriter + Summary: Metapackage for python3-hypothesis: ghostwriter extras + Requires: python3-hypothesis = 6.6.0-1.fc35 + %description -n python3-hypothesis+ghostwriter + This is a metapackage bringing in ghostwriter extras requires for + python3-hypothesis. + It makes sure the dependencies are installed. + + + + %package -n python3-hypothesis+pytz + Summary: Metapackage for python3-hypothesis: pytz extras + Requires: python3-hypothesis = 6.6.0-1.fc35 + %description -n python3-hypothesis+pytz + This is a metapackage bringing in pytz extras requires for python3-hypothesis. + It makes sure the dependencies are installed. + """).lstrip().splitlines() + assert lines == expected + + +@pytest.mark.parametrize('basename_len', [1, 10, 30, 45, 78]) +@pytest.mark.parametrize('extra_len', [1, 13, 28, 52, 78]) +def test_python_extras_subpkg_description_wrapping(basename_len, extra_len): + basename = 'x' * basename_len + extra = 'y' * extra_len + lines = rpm_eval(f'%python_extras_subpkg -n {basename} -F {extra}', + version='6', release='7') + for idx, line in enumerate(lines): + if line.startswith('%description'): + start = idx + 1 + lines = lines[start:] + assert all(len(l) < 80 for l in lines) + assert len(lines) < 6 + if len(" ".join(lines[:-1])) < 80: + assert len(lines) == 2 + expected_singleline = (f"This is a metapackage bringing in {extra} extras " + f"requires for {basename}. " + f"It makes sure the dependencies are installed.") + description_singleline = " ".join(lines) + assert description_singleline == expected_singleline + + +unversioned_macros = pytest.mark.parametrize('macro', [ + '%__python', + '%python', + '%python_version', + '%python_version_nodots', + '%python_sitelib', + '%python_sitearch', + '%python_platform', + '%python_platform_triplet', + '%python_ext_suffix', + '%python_cache_tag', + '%py_shebang_fix', + '%py_build', + '%py_build_egg', + '%py_build_wheel', + '%py_install', + '%py_install_egg', + '%py_install_wheel', + '%py_check_import', + '%py_test_envvars', +]) + + +@unversioned_macros +def test_unversioned_python_errors(macro): + lines = rpm_eval(macro, fails=True) + assert lines[0] == ( + 'error: attempt to use unversioned python, ' + 'define %__python to /usr/bin/python2 or /usr/bin/python3 explicitly' + ) + # when the macros are %global, the error is longer + # we deliberately allow this extra line to be optional + if len(lines) > 1: + # the failed macro is not unnecessarily our tested macro + pattern = r'error: Macro %\S+ failed to expand' + assert re.match(pattern, lines[1]) + # but there should be no more lines + assert len(lines) < 3 + + +@unversioned_macros +def test_unversioned_python_works_when_defined(macro): + macro3 = macro.replace('python', 'python3').replace('py_', 'py3_') + assert rpm_eval(macro, __python='/usr/bin/python3') == rpm_eval(macro3) + + +# we could rework the test for multiple architectures, but the Fedora CI currently only runs on x86_64 +x86_64_only = pytest.mark.skipif(platform.machine() != "x86_64", reason="works on x86_64 only") + + +@x86_64_only +def test_platform_triplet(): + assert rpm_eval("%python3_platform_triplet") == ["x86_64-linux-gnu"] + + +@x86_64_only +def test_ext_suffix(): + assert rpm_eval("%python3_ext_suffix") == [f".cpython-{XY}-x86_64-linux-gnu.so"] + + +def test_cache_tag(): + assert rpm_eval("%python3_cache_tag") == [f"cpython-{XY}"] + + +def test_cache_tag_alternate_python(alt_x_y, alt_xy): + assert rpm_eval("%python_cache_tag", __python=f"/usr/bin/python{alt_x_y}") == [f"cpython-{alt_xy}"] + + +def test_cache_tag_alternate_python3(alt_x_y, alt_xy): + assert rpm_eval("%python3_cache_tag", __python3=f"/usr/bin/python{alt_x_y}") == [f"cpython-{alt_xy}"] + + +def test_python_sitelib_value_python3(): + macro = '%python_sitelib' + assert rpm_eval(macro, __python='%__python3') == [f'/usr/lib/python{X_Y}/site-packages'] + + +def test_python_sitelib_value_alternate_python(alt_x_y): + macro = '%python_sitelib' + assert rpm_eval(macro, __python=f'/usr/bin/python{alt_x_y}') == [f'/usr/lib/python{alt_x_y}/site-packages'] + + +def test_python3_sitelib_value_default(): + macro = '%python3_sitelib' + assert rpm_eval(macro) == [f'/usr/lib/python{X_Y}/site-packages'] + + +def test_python3_sitelib_value_alternate_python(alt_x_y): + macro = '%python3_sitelib' + assert (rpm_eval(macro, __python3=f'/usr/bin/python{alt_x_y}') == + rpm_eval(macro, python3_pkgversion=alt_x_y) == + [f'/usr/lib/python{alt_x_y}/site-packages']) + + +def test_python3_sitelib_value_alternate_prefix(): + macro = '%python3_sitelib' + assert rpm_eval(macro, _prefix='/app') == [f'/app/lib/python{X_Y}/site-packages'] + + +def test_python_sitearch_value_python3(lib): + macro = '%python_sitearch' + assert rpm_eval(macro, __python='%__python3') == [f'/usr/{lib}/python{X_Y}/site-packages'] + + +def test_python_sitearch_value_alternate_python(lib, alt_x_y): + macro = '%python_sitearch' + assert rpm_eval(macro, __python=f'/usr/bin/python{alt_x_y}') == [f'/usr/{lib}/python{alt_x_y}/site-packages'] + + +def test_python3_sitearch_value_default(lib): + macro = '%python3_sitearch' + assert rpm_eval(macro) == [f'/usr/{lib}/python{X_Y}/site-packages'] + + +def test_python3_sitearch_value_alternate_python(lib, alt_x_y): + macro = '%python3_sitearch' + assert (rpm_eval(macro, __python3=f'/usr/bin/python{alt_x_y}') == + rpm_eval(macro, python3_pkgversion=alt_x_y) == + [f'/usr/{lib}/python{alt_x_y}/site-packages']) + + +def test_python3_sitearch_value_alternate_prefix(lib): + macro = '%python3_sitearch' + assert rpm_eval(macro, _prefix='/app') == [f'/app/{lib}/python{X_Y}/site-packages'] + + +@pytest.mark.parametrize( + 'args, expected_args', + [ + ('six', 'six'), + ('-f foo.txt', '-f foo.txt'), + ('-t -f foo.txt six, seven', '-t -f foo.txt six, seven'), + ('-e "foo*" -f foo.txt six, seven', '-e "foo*" -f foo.txt six, seven'), + ('six.quarter six.half,, SIX', 'six.quarter six.half,, SIX'), + ('-f foo.txt six\nsix.half\nSIX', '-f foo.txt six six.half SIX'), + ('six \\ -e six.half', 'six -e six.half'), + ] +) +@pytest.mark.parametrize('__python3', + [None, + f'/usr/bin/python{X_Y}', + '/usr/bin/pythonX.Y']) +def test_py3_check_import(args, expected_args, __python3, lib): + x_y = X_Y + macros = { + 'buildroot': 'BUILDROOT', + '_rpmconfigdir': 'RPMCONFIGDIR', + } + if __python3 is not None: + if 'X.Y' in __python3: + __python3 = __python3.replace('X.Y', get_alt_x_y()) + macros['__python3'] = __python3 + # If the __python3 command has version at the end, parse it and expect it. + # Note that the command is used to determine %python3_sitelib and %python3_sitearch, + # so we only test known CPython schemes here and not PyPy for simplicity. + if (match := re.match(r'.+python(\d+\.\d+)$', __python3)): + x_y = match.group(1) + + invocation = '%{py3_check_import ' + args +'}' + lines = rpm_eval(invocation, **macros) + + # An equality check is a bit inflexible here, + # every time we change the macro we need to change this test. + # However actually executing it and verifying the result is much harder :/ + # At least, let's make the lines saner to check: + lines = [line.rstrip('\\').strip() for line in lines] + expected = textwrap.dedent(fr""" + PATH="BUILDROOT/usr/bin:$PATH" + PYTHONPATH="${{PYTHONPATH:-BUILDROOT/usr/{lib}/python{x_y}/site-packages:BUILDROOT/usr/lib/python{x_y}/site-packages}}" + _PYTHONSITE="BUILDROOT/usr/{lib}/python{x_y}/site-packages:BUILDROOT/usr/lib/python{x_y}/site-packages" + PYTHONDONTWRITEBYTECODE=1 + {__python3 or '/usr/bin/python3'} -s{safe_path_flag(x_y)} RPMCONFIGDIR/redhat/import_all_modules.py {expected_args} + """) + assert lines == expected.splitlines() + + +@pytest.mark.parametrize( + 'shebang_flags_value, expected_shebang_flags', + [ + ('sP', '-sP'), + ('s', '-s'), + ('%{nil}', ''), + (None, ''), + ('Es', '-Es'), + ] +) +def test_py3_check_import_respects_shebang_flags(shebang_flags_value, expected_shebang_flags, lib): + macros = { + '_rpmconfigdir': 'RPMCONFIGDIR', + '__python3': '/usr/bin/python3', + 'py3_shebang_flags': shebang_flags_value, + } + lines = rpm_eval('%py3_check_import sys', **macros) + # Compare the last line of the command, that's where lua part is evaluated + expected = f'/usr/bin/python3 {expected_shebang_flags} RPMCONFIGDIR/redhat/import_all_modules.py sys' + assert lines[-1].strip() == expected diff --git a/tests/test_import_all_modules.py b/tests/test_import_all_modules.py new file mode 100644 index 0000000..71feeff --- /dev/null +++ b/tests/test_import_all_modules.py @@ -0,0 +1,426 @@ +from import_all_modules import argparser, exclude_unwanted_module_globs +from import_all_modules import main as modules_main +from import_all_modules import read_modules_from_cli, filter_top_level_modules_only + +from pathlib import Path + +import pytest +import shlex +import sys + + +@pytest.fixture(autouse=True) +def preserve_sys_path(): + original_sys_path = list(sys.path) + yield + sys.path = original_sys_path + + +@pytest.fixture(autouse=True) +def preserve_sys_modules(): + original_sys_modules = dict(sys.modules) + yield + sys.modules = original_sys_modules + + +@pytest.mark.parametrize( + 'args, imports', + [ + ('six', ['six']), + ('five six seven', ['five', 'six', 'seven']), + ('six,seven, eight', ['six', 'seven', 'eight']), + ('six.quarter six.half,, SIX', ['six.quarter', 'six.half', 'SIX']), + ('six.quarter six.half,, SIX \\ ', ['six.quarter', 'six.half', 'SIX']), + ] +) +def test_read_modules_from_cli(args, imports): + argv = shlex.split(args) + cli_args = argparser().parse_args(argv) + assert read_modules_from_cli(cli_args.modules) == imports + + +@pytest.mark.parametrize( + 'all_mods, imports', + [ + (['six'], ['six']), + (['five', 'six', 'seven'], ['five', 'six', 'seven']), + (['six.seven', 'eight'], ['eight']), + (['SIX', 'six.quarter', 'six.half.and.sth', 'seven'], ['SIX', 'seven']), + ], +) +def test_filter_top_level_modules_only(all_mods, imports): + assert filter_top_level_modules_only(all_mods) == imports + + +@pytest.mark.parametrize( + 'globs, expected', + [ + (['*.*'], ['foo', 'boo']), + (['?oo'], ['foo.bar', 'foo.bar.baz', 'foo.baz']), + (['*.baz'], ['foo', 'foo.bar', 'boo']), + (['foo'], ['foo.bar', 'foo.bar.baz', 'foo.baz', 'boo']), + (['foo*'], ['boo']), + (['foo*', '*bar'], ['boo']), + (['foo', 'bar'], ['foo.bar', 'foo.bar.baz', 'foo.baz', 'boo']), + (['*'], []), + ] +) +def test_exclude_unwanted_module_globs(globs, expected): + my_modules = ['foo', 'foo.bar', 'foo.bar.baz', 'foo.baz', 'boo'] + tested = exclude_unwanted_module_globs(globs, my_modules) + assert tested == expected + + +def test_cli_with_all_args(): + '''A smoke test, all args must be parsed correctly.''' + mods = ['foo', 'foo.bar', 'baz'] + files = ['-f', './foo'] + top = ['-t'] + exclude = ['-e', 'foo*'] + cli_args = argparser().parse_args([*mods, *files, *top, *exclude]) + + assert cli_args.filename == [Path('foo')] + assert cli_args.top_level is True + assert cli_args.modules == ['foo', 'foo.bar', 'baz'] + assert cli_args.exclude == ['foo*'] + + +def test_cli_without_filename_toplevel(): + '''Modules provided on command line (without files) must be parsed correctly.''' + mods = ['foo', 'foo.bar', 'baz'] + cli_args = argparser().parse_args(mods) + + assert cli_args.filename is None + assert cli_args.top_level is False + assert cli_args.modules == ['foo', 'foo.bar', 'baz'] + + +def test_cli_with_filename_no_cli_mods(): + '''Files (without any modules provided on command line) must be parsed correctly.''' + + files = ['-f', './foo', '-f', './bar', '-f', './baz'] + cli_args = argparser().parse_args(files) + + assert cli_args.filename == [Path('foo'), Path('./bar'), Path('./baz')] + assert not cli_args.top_level + + +def test_main_raises_error_when_no_modules_provided(): + '''If no filename nor modules were provided, ValueError is raised.''' + + with pytest.raises(ValueError): + modules_main([]) + + +def test_import_all_modules_does_not_import(): + '''Ensure the files from /usr/lib/rpm/redhat cannot be imported and + checked for import''' + + # We already imported it in this file once, make sure it's not imported + # from the cache + sys.modules.pop('import_all_modules') + with pytest.raises(ModuleNotFoundError): + modules_main(['import_all_modules']) + + +def test_modules_from_cwd_not_found(tmp_path, monkeypatch): + test_module = tmp_path / 'this_is_a_module_in_cwd.py' + test_module.write_text('') + monkeypatch.chdir(tmp_path) + with pytest.raises(ModuleNotFoundError): + modules_main(['this_is_a_module_in_cwd']) + + +def test_modules_from_sys_path_found(tmp_path): + test_module = tmp_path / 'this_is_a_module_in_sys_path.py' + test_module.write_text('') + sys.path.append(str(tmp_path)) + modules_main(['this_is_a_module_in_sys_path']) + assert 'this_is_a_module_in_sys_path' in sys.modules + + +def test_modules_from_file_are_found(tmp_path): + test_file = tmp_path / 'this_is_a_file_in_tmp_path.txt' + test_file.write_text('math\nwave\ncsv\n') + + # Make sure the tested modules are not already in sys.modules + for m in ('math', 'wave', 'csv'): + sys.modules.pop(m, None) + + modules_main(['-f', str(test_file)]) + + assert 'csv' in sys.modules + assert 'math' in sys.modules + assert 'wave' in sys.modules + + +def test_modules_from_files_are_found(tmp_path): + test_file_1 = tmp_path / 'this_is_a_file_in_tmp_path_1.txt' + test_file_2 = tmp_path / 'this_is_a_file_in_tmp_path_2.txt' + test_file_3 = tmp_path / 'this_is_a_file_in_tmp_path_3.txt' + + test_file_1.write_text('math\nwave\n') + test_file_2.write_text('csv\npathlib\n') + test_file_3.write_text('logging\ncsv\n') + + # Make sure the tested modules are not already in sys.modules + for m in ('math', 'wave', 'csv', 'pathlib', 'logging'): + sys.modules.pop(m, None) + + modules_main(['-f', str(test_file_1), '-f', str(test_file_2), '-f', str(test_file_3), ]) + for module in ('csv', 'math', 'wave', 'pathlib', 'logging'): + assert module in sys.modules + + +def test_nonexisting_modules_raise_exception_on_import(tmp_path): + test_file = tmp_path / 'this_is_a_file_in_tmp_path.txt' + test_file.write_text('nonexisting_module\nanother\n') + with pytest.raises(ModuleNotFoundError): + modules_main(['-f', str(test_file)]) + + +def test_nested_modules_found_when_expected(tmp_path, monkeypatch, capsys): + + # This one is supposed to raise an error + cwd_path = tmp_path / 'test_cwd' + Path.mkdir(cwd_path) + test_module_1 = cwd_path / 'this_is_a_module_in_cwd.py' + + # Nested structure that is supposed to be importable + nested_path_1 = tmp_path / 'nested' + nested_path_2 = nested_path_1 / 'more_nested' + + for path in (nested_path_1, nested_path_2): + Path.mkdir(path) + + test_module_2 = tmp_path / 'this_is_a_module_in_level_0.py' + test_module_3 = nested_path_1 / 'this_is_a_module_in_level_1.py' + test_module_4 = nested_path_2 / 'this_is_a_module_in_level_2.py' + + for module in (test_module_1, test_module_2, test_module_3, test_module_4): + module.write_text('') + + sys.path.append(str(tmp_path)) + monkeypatch.chdir(cwd_path) + + with pytest.raises(ModuleNotFoundError): + modules_main([ + 'this_is_a_module_in_level_0', + 'nested.this_is_a_module_in_level_1', + 'nested.more_nested.this_is_a_module_in_level_2', + 'this_is_a_module_in_cwd']) + + _, err = capsys.readouterr() + assert 'Check import: this_is_a_module_in_level_0' in err + assert 'Check import: nested.this_is_a_module_in_level_1' in err + assert 'Check import: nested.more_nested.this_is_a_module_in_level_2' in err + assert 'Check import: this_is_a_module_in_cwd' in err + + +def test_modules_both_from_files_and_cli_are_imported(tmp_path): + test_file_1 = tmp_path / 'this_is_a_file_in_tmp_path_1.txt' + test_file_1.write_text('this_is_a_module_in_tmp_path_1') + + test_file_2 = tmp_path / 'this_is_a_file_in_tmp_path_2.txt' + test_file_2.write_text('this_is_a_module_in_tmp_path_2') + + test_module_1 = tmp_path / 'this_is_a_module_in_tmp_path_1.py' + test_module_2 = tmp_path / 'this_is_a_module_in_tmp_path_2.py' + test_module_3 = tmp_path / 'this_is_a_module_in_tmp_path_3.py' + + for module in (test_module_1, test_module_2, test_module_3): + module.write_text('') + + sys.path.append(str(tmp_path)) + modules_main([ + '-f', str(test_file_1), + 'this_is_a_module_in_tmp_path_3', + '-f', str(test_file_2), + ]) + + expected = ( + 'this_is_a_module_in_tmp_path_1', + 'this_is_a_module_in_tmp_path_2', + 'this_is_a_module_in_tmp_path_3', + ) + for module in expected: + assert module in sys.modules + + +def test_non_existing_module_raises_exception(tmp_path): + + test_module_1 = tmp_path / 'this_is_a_module_in_tmp_path_1.py' + test_module_1.write_text('') + sys.path.append(str(tmp_path)) + + with pytest.raises(ModuleNotFoundError): + modules_main([ + 'this_is_a_module_in_tmp_path_1', + 'this_is_a_module_in_tmp_path_2', + ]) + + +def test_module_with_error_propagates_exception(tmp_path): + + test_module_1 = tmp_path / 'this_is_a_module_in_tmp_path_1.py' + test_module_1.write_text('0/0') + sys.path.append(str(tmp_path)) + + # The correct exception must be raised + with pytest.raises(ZeroDivisionError): + modules_main([ + 'this_is_a_module_in_tmp_path_1', + ]) + + +def test_correct_modules_are_excluded(tmp_path): + test_module_1 = tmp_path / 'module_in_tmp_path_1.py' + test_module_2 = tmp_path / 'module_in_tmp_path_2.py' + test_module_3 = tmp_path / 'module_in_tmp_path_3.py' + + for module in (test_module_1, test_module_2, test_module_3): + module.write_text('') + + sys.path.append(str(tmp_path)) + test_file_1 = tmp_path / 'a_file_in_tmp_path_1.txt' + test_file_1.write_text('module_in_tmp_path_1\nmodule_in_tmp_path_2\nmodule_in_tmp_path_3\n') + + modules_main([ + '-e', 'module_in_tmp_path_2', + '-f', str(test_file_1), + '-e', 'module_in_tmp_path_3', + ]) + + assert 'module_in_tmp_path_1' in sys.modules + assert 'module_in_tmp_path_2' not in sys.modules + assert 'module_in_tmp_path_3' not in sys.modules + + +def test_excluding_all_modules_raises_error(tmp_path): + test_module_1 = tmp_path / 'module_in_tmp_path_1.py' + test_module_2 = tmp_path / 'module_in_tmp_path_2.py' + test_module_3 = tmp_path / 'module_in_tmp_path_3.py' + + for module in (test_module_1, test_module_2, test_module_3): + module.write_text('') + + sys.path.append(str(tmp_path)) + test_file_1 = tmp_path / 'a_file_in_tmp_path_1.txt' + test_file_1.write_text('module_in_tmp_path_1\nmodule_in_tmp_path_2\nmodule_in_tmp_path_3\n') + + with pytest.raises(ValueError): + modules_main([ + '-e', 'module_in_tmp_path*', + '-f', str(test_file_1), + ]) + + +def test_only_toplevel_modules_found(tmp_path): + + # Nested structure that is supposed to be importable + nested_path_1 = tmp_path / 'nested' + nested_path_2 = nested_path_1 / 'more_nested' + + for path in (nested_path_1, nested_path_2): + Path.mkdir(path) + + test_module_1 = tmp_path / 'this_is_a_module_in_level_0.py' + test_module_2 = nested_path_1 / 'this_is_a_module_in_level_1.py' + test_module_3 = nested_path_2 / 'this_is_a_module_in_level_2.py' + + for module in (test_module_1, test_module_2, test_module_3): + module.write_text('') + + sys.path.append(str(tmp_path)) + + modules_main([ + 'this_is_a_module_in_level_0', + 'nested.this_is_a_module_in_level_1', + 'nested.more_nested.this_is_a_module_in_level_2', + '-t']) + + assert 'nested.this_is_a_module_in_level_1' not in sys.modules + assert 'nested.more_nested.this_is_a_module_in_level_2' not in sys.modules + + +def test_only_toplevel_included_modules_found(tmp_path): + + # Nested structure that is supposed to be importable + nested_path_1 = tmp_path / 'nested' + nested_path_2 = nested_path_1 / 'more_nested' + + for path in (nested_path_1, nested_path_2): + Path.mkdir(path) + + test_module_1 = tmp_path / 'this_is_a_module_in_level_0.py' + test_module_4 = tmp_path / 'this_is_another_module_in_level_0.py' + + test_module_2 = nested_path_1 / 'this_is_a_module_in_level_1.py' + test_module_3 = nested_path_2 / 'this_is_a_module_in_level_2.py' + + for module in (test_module_1, test_module_2, test_module_3, test_module_4): + module.write_text('') + + sys.path.append(str(tmp_path)) + + modules_main([ + 'this_is_a_module_in_level_0', + 'this_is_another_module_in_level_0', + 'nested.this_is_a_module_in_level_1', + 'nested.more_nested.this_is_a_module_in_level_2', + '-t', + '-e', '*another*' + ]) + + assert 'nested.this_is_a_module_in_level_1' not in sys.modules + assert 'nested.more_nested.this_is_a_module_in_level_2' not in sys.modules + assert 'this_is_another_module_in_level_0' not in sys.modules + assert 'this_is_a_module_in_level_0' in sys.modules + + +def test_module_list_from_relative_path(tmp_path, monkeypatch): + + monkeypatch.chdir(tmp_path) + test_file_1 = Path('this_is_a_file_in_cwd.txt') + test_file_1.write_text('wave') + + sys.modules.pop('wave', None) + + modules_main([ + '-f', 'this_is_a_file_in_cwd.txt' + ]) + + assert 'wave' in sys.modules + + +@pytest.mark.parametrize('arch_in_path', [True, False]) +def test_pth_files_are_read_from__PYTHONSITE(arch_in_path, tmp_path, monkeypatch, capsys): + sitearch = tmp_path / 'lib64' + sitearch.mkdir() + sitelib = tmp_path / 'lib' + sitelib.mkdir() + + for where, word in (sitearch, "ARCH"), (sitelib, "LIB"), (sitelib, "MOD"): + module = where / f'print{word}.py' + module.write_text(f'print("{word}")') + + pth_sitearch = sitearch / 'ARCH.pth' + pth_sitearch.write_text('import printARCH\n') + + pth_sitelib = sitelib / 'LIB.pth' + pth_sitelib.write_text('import printLIB\n') + + if arch_in_path: + sys.path.append(str(sitearch)) + sys.path.append(str(sitelib)) + + # we always add sitearch to _PYTHONSITE + # but when not in sys.path, it should not be processed for .pth files + monkeypatch.setenv('_PYTHONSITE', f'{sitearch}:{sitelib}') + + modules_main(['printMOD']) + out, err = capsys.readouterr() + if arch_in_path: + assert out == 'ARCH\nLIB\nMOD\n' + else: + assert out == 'LIB\nMOD\n' diff --git a/tests/tests.yml b/tests/tests.yml new file mode 100644 index 0000000..4b8b2dd --- /dev/null +++ b/tests/tests.yml @@ -0,0 +1,40 @@ +--- +- hosts: localhost + tags: + - classic + tasks: + - dnf: + name: "*" + state: latest + +- hosts: localhost + roles: + - role: standard-test-basic + tags: + - classic + tests: + - pytest: + dir: . + run: PYTHONPATH=/usr/lib/rpm/redhat ALTERNATE_PYTHON_VERSION=3.6 pytest -v + - manual_byte_compilation_clamp_mtime_off: + dir: . + run: rpmbuild --define 'dist .clamp0' --define 'clamp_mtime_to_source_date_epoch 0' -ba pythontest.spec + - manual_byte_compilation_clamp_mtime_on: + dir: . + run: rpmbuild --define 'dist .clamp1' --define 'clamp_mtime_to_source_date_epoch 1' -ba pythontest.spec + - rpmlint_clamp_mtime_off: + dir: . + run: rpmlint ~/rpmbuild/RPMS/x86_64/pythontest-0-0.clamp0.x86_64.rpm | grep python-bytecode-inconsistent-mtime || exit 0 && exit 1 + - rpmlint_clamp_mtime_on: + dir: . + run: rpmlint ~/rpmbuild/RPMS/x86_64/pythontest-0-0.clamp1.x86_64.rpm | grep python-bytecode-inconsistent-mtime || exit 0 && exit 1 + required_packages: + - rpm-build + - rpmlint + - python-rpm-macros + - python3-rpm-macros + - python3-devel + - python3-pytest + - python3.6 + - python2.7 +