diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 581c9268..8f719202 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,19 +11,9 @@ jobs: strategy: # Duplicate changes to this matrix to 'poc_tests' matrix: - os: [ubuntu-latest, macos-11, windows-latest] - python-version: ['3.9', '3.10'] + os: [macos-11, macos-12, macos-13, macos-14] + python-version: ['3.10'] compiler: [""] - include: - - os: ubuntu-latest - python-version: '3.8' - - os: ubuntu-latest - python-version: '3.10' - compiler: 'g++' - - os: ubuntu-latest - python-version: '3.11' - - os: ubuntu-latest - python-version: '3.12' steps: - uses: actions/checkout@v4 @@ -57,370 +47,3 @@ jobs: run: | python -m pip install pytest pytest-xdist filelock python -m pytest --basetemp=.tmpdir --durations=16 -n auto test/ - - main_tests_debug: - name: Main tests on CPython debug builds - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest] - python-version: ['3.12', '3.11', '3.10', '3.9', '3.8'] - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python from deadsnakes - uses: deadsnakes/action@v2.1.1 # Upgrading will cause test failures. - with: - python-version: ${{ matrix.python-version }} - debug: true - - - name: Check Python debug build - run: python -c "import sys; print(hasattr(sys, 'gettotalrefcount'))" - - - name: Install/Upgrade Python dependencies - run: python -m pip install --upgrade pip wheel - - - name: Build - run: | - make - python -m pip install . - - - name: Run tests - run: | - python -m pip install pytest pytest-xdist filelock - python -m pytest --durations=16 -n auto test/ - - poc_tests: - name: Proof of concept tests - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-11, windows-latest] - python-version: ['3.10'] - include: - - os: ubuntu-latest - python-version: '3.8' - - os: ubuntu-latest - python-version: '3.9' - - os: ubuntu-latest - python-version: '3.11' - - os: ubuntu-latest - python-version: '3.12' - - steps: - - uses: actions/checkout@v4 - - # - template: azure-templates/ccache.yml - # parameters: - # pythonVersion: $(python.version) - # - template: azure-templates/python.yml - # parameters: - # pythonVersion: $(python.version) - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - - - name: Install/Upgrade Python dependencies - run: python -m pip install --upgrade pip wheel - shell: bash - - - name: 'Test setup.py --hpy-abi=cpython bdist_wheel' - run: proof-of-concept/test_pof.sh wheel cpython - shell: bash - - - name: 'Test setup.py --hpy-abi=universal bdist_wheel' - run: proof-of-concept/test_pof.sh wheel universal - shell: bash - - - name: 'Test setup.py --hpy-abi=cpython install' - run: proof-of-concept/test_pof.sh setup_py_install cpython - shell: bash - - - name: 'Test setup.py --hpy-abi=universal install' - run: proof-of-concept/test_pof.sh setup_py_install universal - shell: bash - - - name: 'Test setup.py --hpy-abi=cpython build_ext --inplace' - run: proof-of-concept/test_pof.sh setup_py_build_ext_inplace cpython - shell: bash - - - name: 'Test setup.py --hpy-abi=universal build_ext --inplace' - run: proof-of-concept/test_pof.sh setup_py_build_ext_inplace universal - shell: bash - - - porting_example_tests: - name: Porting example tests - runs-on: ${{ matrix.os }} - continue-on-error: true - strategy: - matrix: - os: [ubuntu-latest, macos-11, windows-latest] - python-version: ['3.9'] - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Install/Upgrade Python dependencies - run: python -m pip install --upgrade pip wheel - shell: bash - - - name: Install HPy - run: python -m pip install . - - - name: Install pytest - run: | - python -m pip install pytest - - - name: Run tests - run: make porting-example-tests - shell: bash - - - name: Run tests of completed port in debug mode - env: - HPY_DEBUG: "1" - TEST_ARGS: "-s -k hpy_final" - run: make porting-example-tests - shell: bash - - - valgrind_tests_1: - name: 'Valgrind tests (1/3)' - uses: ./.github/workflows/valgrind-tests.yml - with: - portion: '1/3' - - - valgrind_tests_2: - name: 'Valgrind tests (2/3)' - uses: ./.github/workflows/valgrind-tests.yml - with: - portion: '2/3' - - - valgrind_tests_3: - name: 'Valgrind tests (3/3)' - uses: ./.github/workflows/valgrind-tests.yml - with: - portion: '3/3' - - - docs_examples_tests: - name: Documentation examples tests - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-11, windows-latest] - python-version: ['3.10'] - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Install/Upgrade Python dependencies - run: python -m pip install --upgrade pip wheel - shell: bash - - - name: Install HPy - run: python -m pip install . - - - name: Install pytest - run: | - python -m pip install pytest pytest-xdist filelock - - name: Run tests - run: make docs-examples-tests - shell: bash - - build_docs: - name: Build documentation - runs-on: 'ubuntu-latest' - steps: - - uses: actions/checkout@v4 - - # - template: azure-templates/python.yml - - - name: Install / Upgrade system requirements - run: sudo apt update && sudo apt install -y libclang-11-dev - - - name: Install / Upgrade Python requirements - run: | - python -m pip install --upgrade pip - python -m pip install -r docs/requirements.txt - - - name: Build docs - run: | - cd docs; - python -m sphinx -T -W -E -b html -d _build/doctrees -D language=en . _build/html - - - name: Upload built HTML files - uses: actions/upload-artifact@v4 - with: - name: hpy_html_docs - path: docs/_build/html/* - if-no-files-found: error - retention-days: 5 - - c_tests: - name: C tests - runs-on: 'ubuntu-latest' - steps: - - uses: actions/checkout@v4 - - run: make -C c_test - - - check_autogen: - name: Check autogen - runs-on: 'ubuntu-latest' - steps: - - uses: actions/checkout@v4 - - # - template: azure-templates/python.yml - - - name: Set up Python - uses: actions/setup-python@v5 - with: - # autogen needs distutils - python-version: '3.11' - - - name: Install/Upgrade Python dependencies - run: python -m pip install --upgrade pip wheel - - - name: Install autogen dependencies - run: pip install -r requirements-autogen.txt - - - name: make autogen - run: | - make autogen - if [ -z "$(git status --porcelain)" ]; then - # clean working copy - echo "Working copy is clean, everything ok" - else - # Uncommitted changes - echo "ERROR: uncommitted changes after running make autogen" - echo "git status" - git status - echo - echo "git diff" - git diff - exit 1 - fi - - - check_py27_compat: - name: Check Python 2.7 compatibility - runs-on: 'ubuntu-20.04' - steps: - - uses: actions/checkout@v4 - - # - template: azure-templates/python.yml - # parameters: - # pythonVersion: "2.7" - - - name: Set up Python2 - # Copied from cython's ci.yml - run: | - sudo ln -fs python2 /usr/bin/python - sudo apt-get update - sudo apt-get install python-setuptools python2.7 python2.7-dev - curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py - sudo python2 get-pip.py - ls -l /usr/bin/pip* /usr/local/bin/pip* - which pip - - - name: Install/Upgrade Python dependencies - run: python -m pip install --upgrade pip wheel - - - name: check_py27_compat.py - run: | - python -m pip install pytest pytest-xdist filelock pathlib - python test/check_py27_compat.py - - - cpp_check: - name: Cppcheck static analysis - runs-on: 'ubuntu-22.04' - continue-on-error: true - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - - name: Install Cppcheck - run: sudo apt-get -qq -y install cppcheck=2.7-1 - - - name: Run Cppcheck - run: make cppcheck - - - infer: - name: Infer static analysis - runs-on: 'ubuntu-latest' - steps: - - uses: actions/checkout@v4 - - # - template: azure-templates/python.yml - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - - name: Install/Upgrade Python dependencies - run: python -m pip install --upgrade pip wheel - - - name: Install Infer - run: | - python -m pip install compiledb wheel; - VERSION=1.1.0; \ - curl -sSL "https://github.com/facebook/infer/releases/download/v$VERSION/infer-linux64-v$VERSION.tar.xz" \ - | sudo tar -C /opt -xJ && \ - echo "/opt/infer-linux64-v$VERSION/bin" >> $GITHUB_PATH - - - name: Run Infer - run: make infer - - check_microbench: - name: Check micro benchmarks - runs-on: 'ubuntu-latest' - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - - name: Install / Upgrade system dependencies - run: sudo apt update && sudo apt install -y valgrind - - - name: Install / Upgrade Python requirements - run: | - python -m pip install --upgrade pip wheel 'setuptools>=60.2' - python -m pip install pytest cffi - - - name: Build and install HPy - run: | - make - python -m pip install . - - - name: Run microbenchmarks - run: | - cd microbench - python setup.py build_ext -i - python -m pytest -v diff --git a/.github/workflows/valgrind-tests.yml b/.github/workflows/valgrind-tests.yml.DISABLED similarity index 100% rename from .github/workflows/valgrind-tests.yml rename to .github/workflows/valgrind-tests.yml.DISABLED diff --git a/test/hpy_devel/test_build.py b/test/hpy_devel/test_build.py new file mode 100644 index 00000000..7c9b47f9 --- /dev/null +++ b/test/hpy_devel/test_build.py @@ -0,0 +1,386 @@ +""" +Test the hpy+build integration. Most of the relevant code is in +hpy/devel/__init__.py. + +Note that this is a different kind of test than the majority of the other +files in this directory, which all inherit from HPyTest and test the API +itself. +""" + +import sys +import os +import textwrap +import subprocess +import shutil +import venv +import py +import pytest + +from ..support import atomic_run, HPY_ROOT + +# ====== IMPORTANT DEVELOPMENT TIP ===== +# You can use pytest --reuse-venv to speed up local testing. +# +# The env is created once in /tmp/venv-for-hpytest and reused among tests and +# sessions. If you want to recreate it, simply rm -r /tmp/venv-for-hpytest + +def print_CalledProcessError(p): + """ + Print all information about a CalledProcessError + """ + print('========== subprocess failed ==========') + print('command:', ' '.join(p.cmd)) + print('argv: ', p.cmd) + print('return code:', p.returncode) + print() + print('---------- ----------') + print(p.stdout.decode('latin-1')) + print('---------- ---------') + print() + print('---------- ----------') + print(p.stderr.decode('latin-1')) + print('---------- ---------') + +@pytest.fixture(scope='session') +def venv_template(request, tmpdir_factory): + if request.config.option.reuse_venv: + d = py.path.local('/tmp/venv-for-hpytest') + if d.check(dir=True): + # if it exists, we assume it's correct. If you want to recreate, + # just manually delete /tmp/venv-for-hpytest + return d + else: + d = tmpdir_factory.mktemp('venv') + + venv.create(d, with_pip=True) + + # remove the scripts: they contains a shebang and it will fail subtly + # after we clone the template. Yes, we could try to fix the shebangs, but + # it's just easier to use e.g. python -m pip + attach_python_to_venv(d) + for script in d.bin.listdir(): + if script.basename.startswith('python'): + continue + script.remove() + # + try: + atomic_run( + [str(d.python), '-m', 'pip', 'install', '--upgrade', 'build', 'pip', 'setuptools', 'wheel'], + check=True, + capture_output=True, + ) + atomic_run( + [str(d.python), '-m', 'pip', 'install', str(HPY_ROOT)], + check=True, + capture_output=True, + ) + except subprocess.CalledProcessError as cpe: + print_CalledProcessError(cpe) + raise + return d + +def attach_python_to_venv(d): + if os.name == 'nt': + d.bin = d.join('Scripts') + else: + d.bin = d.join('bin') + d.python = d.bin.join('python') + + +@pytest.mark.usefixtures('initargs') +class TestDistutils: + + @pytest.fixture() + def initargs(self, pytestconfig, tmpdir, venv_template): + self.tmpdir = tmpdir + # create a fresh venv by copying the template + self.venv = tmpdir.join('venv') + shutil.copytree(venv_template, self.venv) + attach_python_to_venv(self.venv) + # create the files for our test project + self.hpy_test_project = tmpdir.join('hpy_test_project').ensure(dir=True) + self.gen_project() + self.hpy_test_project.chdir() + + @pytest.fixture(params=['cpython', 'hybrid', 'universal']) + def hpy_abi(self, request): + return request.param + + def python(self, *args, capture=False, hpy_abi=None): + """ + Run python inside the venv; if capture==True, return stdout + """ + cmd = [str(self.venv.python)] + list(args) + if hpy_abi: # prepend an environment variable definition: HPY_ABI=... + cmd = [f'HPY_ABI={hpy_abi}'] + cmd + print('[RUN]', ' '.join(cmd)) + if capture: + proc = atomic_run(cmd, capture_output=True) + out = proc.stdout.decode('latin-1').strip() + else: + proc = atomic_run(cmd) + out = None + proc.check_returncode() + return out + + + def writefile(self, fname, content): + """ + Write a file inside hpy_test_project + """ + f = self.hpy_test_project.join(fname) + content = textwrap.dedent(content) + f.write(content) + + def gen_project(self): + """ + Generate the files needed to build the project, except setup.py + """ + self.writefile('cpymod.c', """ + // the simplest possible Python/C module + #include + static PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "cpymod", + "cpymod docstring" + }; + + PyMODINIT_FUNC + PyInit_cpymod(void) + { + return PyModule_Create(&moduledef); + } + """) + + self.writefile('hpymod.c', """ + // the simplest possible HPy module + #include + static HPyModuleDef moduledef = { + .doc = "hpymod with HPy ABI: " HPY_ABI, + }; + + HPy_MODINIT(hpymod, moduledef) + """) + + self.writefile('hpymod_legacy.c', """ + // the simplest possible HPy+legacy module + #include + #include + + static PyObject *f(PyObject *self, PyObject *args) + { + return PyLong_FromLong(1234); + } + static PyMethodDef my_legacy_methods[] = { + {"f", (PyCFunction)f, METH_NOARGS}, + {NULL} + }; + + static HPyModuleDef moduledef = { + .doc = "hpymod_legacy with HPy ABI: " HPY_ABI, + .legacy_methods = my_legacy_methods, + }; + + HPy_MODINIT(hpymod_legacy, moduledef) + """) + + def gen_setup_py(self, src): + preamble = textwrap.dedent(""" + from setuptools import setup, Extension + cpymod = Extension("cpymod", ["cpymod.c"]) + hpymod = Extension("hpymod", ["hpymod.c"]) + hpymod_legacy = Extension("hpymod_legacy", ["hpymod_legacy.c"]) + """) + src = preamble + textwrap.dedent(src) + f = self.hpy_test_project.join('setup.py') + f.write(src) + + def get_docstring(self, modname): + cmd = f'import {modname}; print({modname}.__doc__)' + return self.python('-c', cmd, capture=True) + + def test_cpymod_setup_install(self): + # CPython-only project, no hpy at all. This is a baseline to check + # that everything works even without hpy. + self.gen_setup_py(""" + setup(name = "hpy_test_project", + ext_modules = [cpymod], + ) + """) + # self.python('setup.py', 'install') + self.python('-m', 'build') + self.python('-m', 'pip', 'install', '.') + doc = self.get_docstring('cpymod') + assert doc == 'cpymod docstring' + + def test_cpymod_with_empty_hpy_ext_modules_setup_install(self): + # if we have hpy_ext_modules=[] we trigger the hpy.devel monkey + # patch. This checks that we don't ext_modules still works after that. + self.gen_setup_py(""" + setup(name = "hpy_test_project", + ext_modules = [cpymod], + hpy_ext_modules = [] + ) + """) + # self.python('setup.py', 'install') + self.python('-m', 'build') + self.python('-m', 'pip', 'install', '.') + doc = self.get_docstring('cpymod') + assert doc == 'cpymod docstring' + + def test_hpymod_py_stub(self): + # check that that we generated the .py stub for universal + self.gen_setup_py(""" + setup(name = "hpy_test_project", + hpy_ext_modules = [hpymod], + ) + """) + # self.python('setup.py', '--hpy-abi=universal', 'build') + self.python('-m', 'build', hpy_abi='universal') + self.python('-m', 'pip', 'install', '.') + build = self.hpy_test_project.join('build') + lib = build.listdir('lib*')[0] + hpymod_py = lib.join('hpymod.py') + assert hpymod_py.check(exists=True) + assert 'This file is automatically generated by hpy' in hpymod_py.read() + + def test_hpymod_build_platlib(self): + # check that if we have only hpy_ext_modules, the distribution is + # detected as "platform-specific" and not "platform-neutral". In + # particular, we want the end result to be in + # e.g. build/lib.linux-x86_64-3.8 and NOT in build/lib. + self.gen_setup_py(""" + setup(name = "hpy_test_project", + hpy_ext_modules = [hpymod], + ) + """) + # self.python('setup.py', 'build') + self.python('-m', 'build') + self.python('-m', 'pip', 'install', '.') + build = self.hpy_test_project.join('build') + libs = build.listdir('lib*') + assert len(libs) == 1 + libdir = libs[0] + # this is something like lib.linux-x86_64-cpython-38 + assert libdir.basename != 'lib' + + def test_hpymod_build_ext_inplace(self, hpy_abi): + # check that we can install hpy modules with setup.py build_ext -i + self.gen_setup_py(""" + setup(name = "hpy_test_project", + hpy_ext_modules = [hpymod], + ) + """) + # self.python('setup.py', f'--hpy-abi={hpy_abi}', 'build_ext', '--inplace') + self.python('-m', 'build', hpy_abi=hpy_abi) + self.python('-m', 'pip', 'install', '.') + doc = self.get_docstring('hpymod') + assert doc == f'hpymod with HPy ABI: {hpy_abi}' + + def test_hpymod_setup_install(self, hpy_abi): + # check that we can install hpy modules with setup.py install + self.gen_setup_py(""" + setup(name = "hpy_test_project", + hpy_ext_modules = [hpymod], + ) + """) + # self.python('setup.py', f'--hpy-abi={hpy_abi}', 'install') + self.python('-m', 'build', hpy_abi=hpy_abi) + self.python('-m', 'pip', 'install', '.') + doc = self.get_docstring('hpymod') + assert doc == f'hpymod with HPy ABI: {hpy_abi}' + + def test_hpymod_wheel(self, hpy_abi): + # check that we can build and install wheels + self.gen_setup_py(""" + setup(name = "hpy_test_project", + hpy_ext_modules = [hpymod], + ) + """) + # self.python('-m', f'--hpy-abi={hpy_abi}', 'bdist_wheel') + # self.python('setup.py', f'--hpy-abi={hpy_abi}', 'bdist_wheel') + self.python('-m', 'build', hpy_abi=hpy_abi) + self.python('-m', 'pip', 'install', 'bdist_wheel') + dist = self.hpy_test_project.join('dist') + whl = dist.listdir('*.whl')[0] + self.python('-m', 'pip', 'install', str(whl)) + doc = self.get_docstring('hpymod') + assert doc == f'hpymod with HPy ABI: {hpy_abi}' + + def test_dont_mix_cpython_and_universal_abis(self): + """ + See issue #322 + """ + # make sure that the build dirs for cpython and universal ABIs are + # distinct + self.gen_setup_py(""" + setup(name = "hpy_test_project", + hpy_ext_modules = [hpymod], + install_requires = [], + ) + """) + # self.python('setup.py', 'install') + self.python('-m', 'build') + self.python('-m', 'pip', 'install', '.') + # in the build/ dir, we should have 2 directories: temp and lib + build = self.hpy_test_project.join('build') + temps = build.listdir('temp*') + libs = build.listdir('lib*') + assert len(temps) == 1 + assert len(libs) == 1 + # + doc = self.get_docstring('hpymod') + assert doc == 'hpymod with HPy ABI: cpython' + + # now recompile with universal *without* cleaning the build + # self.python('setup.py', '--hpy-abi=universal', 'install') + self.python('-m', 'build', hpy_abi='universal') + self.python('-m', 'pip', 'install', '.') + # in the build/ dir, we should have 4 directories: 2 temp*, and 2 lib* + build = self.hpy_test_project.join('build') + temps = build.listdir('temp*') + libs = build.listdir('lib*') + assert len(temps) == 2 + assert len(libs) == 2 + # + doc = self.get_docstring('hpymod') + assert doc == 'hpymod with HPy ABI: universal' + + def test_hpymod_legacy(self, hpy_abi): + if hpy_abi == 'universal': + pytest.skip('only for cpython and hybrid ABIs') + self.gen_setup_py(""" + setup(name = "hpy_test_project", + hpy_ext_modules = [hpymod_legacy], + install_requires = [], + ) + """) + # self.python('setup.py', 'install') + self.python('-m', 'build', hpy_abi=hpy_abi) + self.python('-m', 'pip', 'install', '.') + src = 'import hpymod_legacy; print(hpymod_legacy.f())' + out = self.python('-c', src, capture=True) + assert out == '1234' + + def test_hpymod_legacy_fails_with_universal(self): + self.gen_setup_py(""" + setup(name = "hpy_test_project", + hpy_ext_modules = [hpymod_legacy], + install_requires = [], + ) + """) + self.python('-m', 'build', hpy_abi='universal') + with pytest.raises(subprocess.CalledProcessError) as exc: + # self.python('setup.py', '--hpy-abi=universal', 'install', capture=True) + self.python('-m', 'pip', 'install', '.') + expected_msg = ("It is forbidden to #include when " + "targeting the HPy Universal ABI") + + # gcc/clang prints the #error on stderr, MSVC prints it on + # stdout. Here we check that the error is printed "somewhere", we + # don't care exactly where. + out = exc.value.stdout + b'\n' + exc.value.stderr + out = out.decode('latin-1') + if expected_msg not in out: + print_CalledProcessError(exc.value) + assert expected_msg in out diff --git a/test/hpy_devel/test_distutils.py b/test/hpy_devel/test_distutils.py.DISABLED similarity index 100% rename from test/hpy_devel/test_distutils.py rename to test/hpy_devel/test_distutils.py.DISABLED