diff --git a/.github/workflows/build-wheels-defined.yml b/.github/workflows/build-wheels-defined.yml index 0c43309..12fc0b6 100644 --- a/.github/workflows/build-wheels-defined.yml +++ b/.github/workflows/build-wheels-defined.yml @@ -40,6 +40,11 @@ on: type: boolean required: false default: true + os_linux_armv7_legacy: + description: Build on linux armv7 legacy (bullseye, glibc 2.31) + type: boolean + required: false + default: false env: GH_TOKEN : ${{ secrets.GITHUB_TOKEN }} @@ -86,7 +91,7 @@ jobs: - name: Upload artifacts of downloaded_wheels directory uses: actions/upload-artifact@v4 with: - name: wheels-download-directory-ubuntu-${{ matrix.python-version }} + name: wheels-download-directory-linux-x86_64-${{ matrix.python-version }} path: ./downloaded_wheels @@ -123,7 +128,7 @@ jobs: - name: Upload artifacts of downloaded_wheels directory uses: actions/upload-artifact@v4 with: - name: wheels-download-directory-windows-${{ matrix.python-version }} + name: wheels-download-directory-windows-x86_64-${{ matrix.python-version }} path: ./downloaded_wheels @@ -165,7 +170,7 @@ jobs: - name: Upload artifacts of downloaded_wheels directory uses: actions/upload-artifact@v4 with: - name: wheels-download-directory-macos-x86-${{ matrix.python-version }} + name: wheels-download-directory-macos-x86_64-${{ matrix.python-version }} path: ./downloaded_wheels @@ -246,6 +251,7 @@ jobs: -w /work \ -e GH_TOKEN="${GH_TOKEN}" \ -e PIP_NO_CACHE_DIR=1 \ + -e LDFLAGS="-Wl,-z,max-page-size=0x1000" \ python:${{ matrix.python-version }}-bookworm \ bash -c " set -e @@ -262,7 +268,7 @@ jobs: - name: Upload artifacts of downloaded_wheels directory uses: actions/upload-artifact@v4 with: - name: wheels-download-directory-linux-arm7-${{ matrix.python-version }} + name: wheels-download-directory-linux-armv7-${{ matrix.python-version }} path: ./downloaded_wheels @@ -301,17 +307,76 @@ jobs: name: wheels-download-directory-linux-arm64-${{ matrix.python-version }} path: ./downloaded_wheels + + linux-armv7-legacy: + needs: get-supported-versions + name: linux aarch32 (armv7 legacy) + if: ${{ inputs.os_linux_armv7_legacy }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ${{ fromJson(needs.get-supported-versions.outputs.supported_python) }} + exclude: + # Python 3.14 doesn't have bullseye images for ARM + - python-version: '3.14' + steps: + - name: Set up QEMU for ARMv7 + uses: docker/setup-qemu-action@v3 + with: + platforms: linux/arm/v7 + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build wheels - ARMv7 Legacy (in Docker) + # Build on Bullseye (glibc 2.31) for compatibility with older systems + run: | + docker run --rm --platform linux/arm/v7 \ + -v $(pwd):/work \ + -w /work \ + -e GH_TOKEN="${GH_TOKEN}" \ + -e PIP_NO_CACHE_DIR=1 \ + -e LDFLAGS="-Wl,-z,max-page-size=0x1000" \ + python:${{ matrix.python-version }}-bullseye \ + bash -c " + set -e + python --version + # Install pip packages without cache to reduce memory usage + python -m pip install --no-cache-dir --upgrade pip + python -m pip install --no-cache-dir -r build_requirements.txt + bash os_dependencies/linux_arm.sh + # Source Rust environment after installation + . \$HOME/.cargo/env + python build_wheels_from_file.py --requirements '${{ inputs.packages }}' + " + + - name: Upload artifacts of downloaded_wheels directory + uses: actions/upload-artifact@v4 + with: + name: wheels-download-directory-linux-armv7legacy-${{ matrix.python-version }} + path: ./downloaded_wheels + # Repair wheels for dynamically linked libraries on all platforms # https://github.com/espressif/idf-python-wheels/blob/main/README.md#universal-wheel-tag---linking-of-dynamic-libraries repair-wheels: if: ${{ always() }} - needs: [get-supported-versions, ubuntu-latest, windows-latest, macos-latest, macos-m1, linux-armv7, linux-arm64] + needs: [get-supported-versions, ubuntu-latest, windows-latest, macos-latest, macos-m1, linux-armv7, linux-arm64, linux-armv7-legacy] name: Repair wheels uses: ./.github/workflows/wheels-repair.yml + # Test that all wheels can be installed on all supported platforms + test-wheels: + if: ${{ always() }} + needs: [get-supported-versions, repair-wheels] + name: Test wheels installation + uses: ./.github/workflows/test-wheels-install.yml + with: + supported_python_versions: ${{ needs.get-supported-versions.outputs.supported_python }} + upload-python-wheels: if: ${{ always() }} - needs: [repair-wheels] + needs: [test-wheels] name: Upload Python wheels - uses: espressif/idf-python-wheels/.github/workflows/upload-python-wheels.yml@main + uses: ./.github/workflows/upload-python-wheels.yml secrets: inherit diff --git a/.github/workflows/build-wheels-platforms.yml b/.github/workflows/build-wheels-platforms.yml index 313962a..cfbf869 100644 --- a/.github/workflows/build-wheels-platforms.yml +++ b/.github/workflows/build-wheels-platforms.yml @@ -213,8 +213,16 @@ jobs: name: Repair wheels uses: ./.github/workflows/wheels-repair.yml + # Test that all wheels can be installed on all supported platforms + test-wheels: + needs: [get-supported-versions, repair-wheels] + name: Test wheels installation + uses: ./.github/workflows/test-wheels-install.yml + with: + supported_python_versions: ${{ needs.get-supported-versions.outputs.supported_python }} + upload-python-wheels: - needs: [repair-wheels] + needs: [test-wheels] name: Upload Python wheels uses: ./.github/workflows/upload-python-wheels.yml secrets: inherit diff --git a/.github/workflows/test-wheels-install.yml b/.github/workflows/test-wheels-install.yml new file mode 100644 index 0000000..6de4c1c --- /dev/null +++ b/.github/workflows/test-wheels-install.yml @@ -0,0 +1,111 @@ +name: Test wheels installation + +# Test that all built wheels are valid and platform-compatible +# This workflow runs after repair-wheels and before upload to catch any issues +# Uses test_wheels_install.py for consistent testing across all platforms + +on: + workflow_call: + inputs: + supported_python_versions: + description: 'JSON array of supported Python versions' + required: true + type: string + +jobs: + test-install: + name: Test ${{ matrix.os }} - Python ${{ matrix.python-version }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + os: + - Windows + - Linux x86_64 + - macOS Intel + - macOS ARM + - Linux ARM64 + - Linux ARMv7 + - Linux ARMv7 Legacy + include: + - os: Windows + runner: windows-latest + arch: windows-x86_64 + - os: Linux x86_64 + runner: ubuntu-latest + arch: linux-x86_64 + - os: macOS Intel + runner: macos-15-intel + arch: macos-x86_64 + - os: macOS ARM + runner: macos-latest + arch: macos-arm64 + - os: Linux ARM64 + runner: ubuntu-24.04-arm + arch: linux-arm64 + - os: Linux ARMv7 + runner: ubuntu-latest + arch: linux-armv7 + - os: Linux ARMv7 Legacy + runner: ubuntu-latest + arch: linux-armv7legacy + python-version: ${{ fromJson(inputs.supported_python_versions) }} + exclude: + # Python 3.14 doesn't have bullseye images for ARM + - python-version: '3.14' + os: Linux ARMv7 Legacy + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU for ARMv7 + if: matrix.os == 'Linux ARMv7' || matrix.os == 'Linux ARMv7 Legacy' + uses: docker/setup-qemu-action@v3 + with: + platforms: linux/arm/v7 + + - name: Download repaired wheels + uses: actions/download-artifact@v4 + with: + name: wheels-repaired-${{ matrix.arch }} + path: ./downloaded_wheels + + - name: Setup Python + if: matrix.os != 'Linux ARMv7' && matrix.os != 'Linux ARMv7 Legacy' + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Test wheel installation + if: matrix.os != 'Linux ARMv7' && matrix.os != 'Linux ARMv7 Legacy' + run: | + python --version + python -m pip install --upgrade pip + python test/test_wheels_install.py + + - name: Test wheel installation - ARMv7 (in Docker) + if: matrix.os == 'Linux ARMv7' + run: | + docker run --rm --platform linux/arm/v7 \ + -v $(pwd):/work \ + -w /work \ + python:${{ matrix.python-version }}-bookworm \ + bash -c " + python --version + python -m pip install --upgrade pip + python test/test_wheels_install.py + " + + - name: Test wheel installation - ARMv7 Legacy (in Docker) + if: matrix.os == 'Linux ARMv7 Legacy' + run: | + docker run --rm --platform linux/arm/v7 \ + -v $(pwd):/work \ + -w /work \ + python:${{ matrix.python-version }}-bullseye \ + bash -c " + python --version + python -m pip install --upgrade pip + python test/test_wheels_install.py + " diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..9e1f5d4 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,38 @@ +name: Unit Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + get-supported-versions: + name: Get Supported Versions + uses: ./.github/workflows/get-supported-versions.yml + secrets: inherit + + unit-tests: + name: Unit Tests (Python ${{ matrix.python-version }}) + needs: get-supported-versions + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ${{ fromJson(needs.get-supported-versions.outputs.supported_python) }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install packaging pyyaml colorama requests + + - name: Run unit tests + run: python -m unittest discover -s test -v diff --git a/.github/workflows/wheels-repair.yml b/.github/workflows/wheels-repair.yml index ea0376a..c58f407 100644 --- a/.github/workflows/wheels-repair.yml +++ b/.github/workflows/wheels-repair.yml @@ -82,37 +82,50 @@ jobs: path: ./downloaded_wheels merge-multiple: true + - name: Check for wheels + id: check-wheels + run: | + if [ -d "./downloaded_wheels" ] && [ -n "$(find ./downloaded_wheels -name '*.whl' 2>/dev/null)" ]; then + echo "has_wheels=true" >> $GITHUB_OUTPUT + echo "Found wheels to repair" + else + echo "has_wheels=false" >> $GITHUB_OUTPUT + echo "No wheels found for ${{ matrix.platform }} - skipping repair" + fi + shell: bash + - name: Setup Python + if: steps.check-wheels.outputs.has_wheels == 'true' uses: actions/setup-python@v5 with: python-version: '3.12' - name: Set up QEMU - if: matrix.setup_qemu + if: matrix.setup_qemu && steps.check-wheels.outputs.has_wheels == 'true' uses: docker/setup-qemu-action@v3 with: platforms: ${{ matrix.qemu_platform }} - name: Install repair tool - Windows - if: matrix.tool == 'delvewheel' + if: matrix.tool == 'delvewheel' && steps.check-wheels.outputs.has_wheels == 'true' run: python -m pip install delvewheel - name: Install repair tool - macOS - if: matrix.tool == 'delocate' + if: matrix.tool == 'delocate' && steps.check-wheels.outputs.has_wheels == 'true' run: python -m pip install delocate - name: Install OS dependencies - macOS - if: matrix.tool == 'delocate' + if: matrix.tool == 'delocate' && steps.check-wheels.outputs.has_wheels == 'true' run: bash os_dependencies/macos.sh - name: Install dependencies and repair - Windows/macOS - if: matrix.tool != 'auditwheel' + if: matrix.tool != 'auditwheel' && steps.check-wheels.outputs.has_wheels == 'true' run: | python -m pip install -r build_requirements.txt python repair_wheels.py - name: Repair Linux x86_64 wheels in manylinux container - if: matrix.platform == 'Linux x86_64' + if: matrix.platform == 'Linux x86_64' && steps.check-wheels.outputs.has_wheels == 'true' run: | docker run --rm \ -v $(pwd):/work \ @@ -127,7 +140,7 @@ jobs: " - name: Repair Linux ARM64 wheels in manylinux container - if: matrix.platform == 'Linux ARM64' + if: matrix.platform == 'Linux ARM64' && steps.check-wheels.outputs.has_wheels == 'true' run: | docker run --rm \ --platform ${{ matrix.docker_platform }} \ @@ -144,7 +157,7 @@ jobs: - name: Repair Linux ARMv7 wheels in manylinux container # Using --break-system-packages and --ignore-installed to avoid conflicts with system packages - if: matrix.platform == 'Linux ARMv7' + if: matrix.platform == 'Linux ARMv7' && steps.check-wheels.outputs.has_wheels == 'true' run: | docker run --rm \ --platform ${{ matrix.docker_platform }} \ @@ -160,7 +173,7 @@ jobs: - name: Repair Linux ARMv7 Legacy wheels in manylinux container # Using --break-system-packages and --ignore-installed to avoid conflicts with system packages - if: matrix.platform == 'Linux ARMv7 Legacy' + if: matrix.platform == 'Linux ARMv7 Legacy' && steps.check-wheels.outputs.has_wheels == 'true' run: | docker run --rm \ --platform ${{ matrix.docker_platform }} \ @@ -175,6 +188,7 @@ jobs: " - name: Re-upload artifacts with repaired wheels + if: steps.check-wheels.outputs.has_wheels == 'true' uses: actions/upload-artifact@v4 with: name: wheels-repaired-${{ matrix.arch }} diff --git a/test/test_build_wheels.py b/test/test_build_wheels.py new file mode 100644 index 0000000..27021d0 --- /dev/null +++ b/test/test_build_wheels.py @@ -0,0 +1,325 @@ +# ruff: noqa: E501 +# line too long skip in ruff for whole file (formatting would be worst than long lines) +# +# SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD +# +# SPDX-License-Identifier: Apache-2.0 +# +import sys +import unittest + +from pathlib import Path +from unittest.mock import patch + +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from packaging.requirements import Requirement + +from _helper_functions import get_no_binary_args +from _helper_functions import merge_requirements +from build_wheels import _add_into_requirements +from build_wheels import get_used_idf_branches +from yaml_list_adapter import YAMLListAdapter + + +class TestChangeSpecifierLogic(unittest.TestCase): + """Test the _change_specifier_logic method.""" + + def setUp(self): + """Create a YAMLListAdapter instance for testing.""" + # Create instance with a minimal valid YAML file + self.adapter = YAMLListAdapter.__new__(YAMLListAdapter) + self.adapter._yaml_list = [] + self.adapter.exclude = False + self.adapter.requirements = set() + + def test_change_specifier_logic(self): + """Test that specifier logic is correctly inverted (logical negation).""" + # The function performs logical negation: + # > becomes <= (not greater means less or equal) + # < becomes >= (not less means greater or equal) + # >= becomes < (not greater-or-equal means less) + # <= becomes > (not less-or-equal means greater) + test_cases = ( + (">0.9.0.2", "<=0.9.0.2"), + ("<0.9.0.2", ">=0.9.0.2"), + ("==0.9.0.2", "!=0.9.0.2"), + (">=0.9.0.2", "<0.9.0.2"), + ("<=0.9.0.2", ">0.9.0.2"), + ("!=0.9.0.2", "==0.9.0.2"), + ("===0.9.0.2", "===0.9.0.2"), + ) + + for original, expected in test_cases: + with self.subTest(original=original): + new_spec, ver, _ = self.adapter._change_specifier_logic(original) + result = f"{new_spec}{ver}" + self.assertEqual(result, expected) + + +class TestYAMLtoRequirement(unittest.TestCase): + """Test the _yaml_to_requirement method.""" + + def setUp(self): + """Create a YAMLListAdapter instance for testing.""" + self.adapter = YAMLListAdapter.__new__(YAMLListAdapter) + self.adapter._yaml_list = [] + self.adapter.exclude = False + self.adapter.requirements = set() + + def test_simple_package(self): + """Test conversion of a simple package without markers.""" + yaml_list = [{"package_name": "numpy"}] + result = self.adapter._yaml_to_requirement(yaml_list) + self.assertEqual(result, {Requirement("numpy")}) + + def test_package_with_version(self): + """Test conversion of a package with version specifier.""" + yaml_list = [{"package_name": "numpy", "version": "<1.20"}] + result = self.adapter._yaml_to_requirement(yaml_list) + self.assertEqual(result, {Requirement("numpy<1.20")}) + + def test_package_with_multiple_versions(self): + """Test conversion of a package with multiple version specifiers.""" + yaml_list = [{"package_name": "numpy", "version": ["<1.20", ">=1.10"]}] + result = self.adapter._yaml_to_requirement(yaml_list) + self.assertEqual(result, {Requirement("numpy<1.20,>=1.10")}) + + def test_package_with_platform(self): + """Test conversion of a package with platform marker.""" + yaml_list = [{"package_name": "pywin32", "platform": "win32"}] + result = self.adapter._yaml_to_requirement(yaml_list) + self.assertEqual(result, {Requirement("pywin32; sys_platform == 'win32'")}) + + def test_package_with_multiple_platforms(self): + """Test conversion of a package with multiple platform markers.""" + yaml_list = [{"package_name": "pkg", "platform": ["win32", "linux"]}] + result = self.adapter._yaml_to_requirement(yaml_list) + self.assertEqual(result, {Requirement("pkg; sys_platform == 'win32' or sys_platform == 'linux'")}) + + def test_package_with_python_version(self): + """Test conversion of a package with python version marker.""" + yaml_list = [{"package_name": "pkg", "python": ">=3.8"}] + result = self.adapter._yaml_to_requirement(yaml_list) + self.assertEqual(result, {Requirement("pkg; python_version >= '3.8'")}) + + def test_package_with_version_and_platform(self): + """Test conversion of a package with version and platform.""" + yaml_list = [{"package_name": "numpy", "version": "<=1.20", "platform": "win32"}] + result = self.adapter._yaml_to_requirement(yaml_list) + self.assertEqual(result, {Requirement("numpy<=1.20; sys_platform == 'win32'")}) + + def test_exclude_simple_platform(self): + """Test exclude mode with platform marker.""" + yaml_list = [{"package_name": "pkg", "platform": "win32"}] + result = self.adapter._yaml_to_requirement(yaml_list, exclude=True) + self.assertEqual(result, {Requirement("pkg; sys_platform != 'win32'")}) + + def test_exclude_version(self): + """Test exclude mode with version specifier.""" + yaml_list = [{"package_name": "numpy", "version": "<1.20"}] + result = self.adapter._yaml_to_requirement(yaml_list, exclude=True) + self.assertEqual(result, {Requirement("numpy>=1.20")}) + + +class TestYAMLListAdapterIntegration(unittest.TestCase): + """Integration tests using actual YAML files.""" + + def test_load_include_list(self): + """Test loading the include_list.yaml file.""" + try: + adapter = YAMLListAdapter("include_list.yaml") + self.assertIsInstance(adapter.requirements, set) + except FileNotFoundError: + self.skipTest("include_list.yaml not found") + + def test_load_exclude_list(self): + """Test loading the exclude_list.yaml file.""" + try: + adapter = YAMLListAdapter("exclude_list.yaml", exclude=True) + self.assertIsInstance(adapter.requirements, set) + except FileNotFoundError: + self.skipTest("exclude_list.yaml not found") + + +class TestWheelCompatibility(unittest.TestCase): + """Test the is_wheel_compatible function from test_wheels_install.py.""" + + def setUp(self): + """Import the function to test.""" + sys.path.insert(0, str(Path(__file__).parent)) + from test_wheels_install import is_wheel_compatible + + self.is_wheel_compatible = is_wheel_compatible + + def test_exact_python_version_match(self): + """Test that cpXY wheels match the exact Python version.""" + self.assertTrue(self.is_wheel_compatible("numpy-1.0.0-cp311-cp311-linux_x86_64.whl", "311")) + self.assertFalse(self.is_wheel_compatible("numpy-1.0.0-cp310-cp310-linux_x86_64.whl", "311")) + + def test_universal_py3_wheel(self): + """Test that py3 wheels are compatible with any Python 3.""" + self.assertTrue(self.is_wheel_compatible("six-1.0.0-py3-none-any.whl", "311")) + self.assertTrue(self.is_wheel_compatible("six-1.0.0-py3-none-any.whl", "39")) + + def test_universal_py2_py3_wheel(self): + """Test that py2.py3 wheels are compatible with any Python.""" + self.assertTrue(self.is_wheel_compatible("six-1.0.0-py2.py3-none-any.whl", "311")) + self.assertTrue(self.is_wheel_compatible("six-1.0.0-py2.py3-none-any.whl", "39")) + + def test_abi3_wheel(self): + """Test that abi3 wheels are compatible.""" + self.assertTrue(self.is_wheel_compatible("cryptography-41.0.0-cp39-abi3-linux_x86_64.whl", "311")) + self.assertTrue(self.is_wheel_compatible("cryptography-41.0.0-cp39-abi3-linux_x86_64.whl", "39")) + + +class TestGetUsedIdfBranches(unittest.TestCase): + """Test the get_used_idf_branches function.""" + + @patch("build_wheels.MIN_IDF_MAJOR_VERSION", 5) + @patch("build_wheels.MIN_IDF_MINOR_VERSION", 0) + def test_filters_old_branches(self): + """Test that branches older than minimum version are filtered out.""" + branches = [ + "release/v4.4", + "release/v5.0", + "release/v5.1", + "release/v5.2", + "master", + ] + result = get_used_idf_branches(branches) + self.assertIn("release/v5.0", result) + self.assertIn("release/v5.1", result) + self.assertIn("release/v5.2", result) + self.assertIn("master", result) + self.assertNotIn("release/v4.4", result) + + @patch("build_wheels.MIN_IDF_MAJOR_VERSION", 5) + @patch("build_wheels.MIN_IDF_MINOR_VERSION", 1) + def test_filters_by_minor_version(self): + """Test that filtering works correctly with minor version.""" + branches = [ + "release/v5.0", + "release/v5.1", + "release/v5.2", + ] + result = get_used_idf_branches(branches) + self.assertNotIn("release/v5.0", result) + self.assertIn("release/v5.1", result) + self.assertIn("release/v5.2", result) + + def test_ignores_non_release_branches(self): + """Test that non-release branches (except master) are ignored.""" + branches = [ + "feature/test", + "bugfix/something", + "release/v5.0", + ] + result = get_used_idf_branches(branches) + self.assertNotIn("feature/test", result) + self.assertNotIn("bugfix/something", result) + self.assertIn("master", result) + + +class TestAddIntoRequirements(unittest.TestCase): + """Test the _add_into_requirements function.""" + + def test_parses_simple_requirements(self): + """Test parsing simple requirement lines.""" + lines = ["numpy", "pandas>=1.0", "requests==2.28.0"] + result = _add_into_requirements(lines) + self.assertEqual(len(result), 3) + names = {r.name for r in result} + self.assertIn("numpy", names) + self.assertIn("pandas", names) + self.assertIn("requests", names) + + def test_ignores_comments(self): + """Test that comment lines are ignored.""" + lines = [ + "# This is a comment", + "numpy", + "pandas # inline comment", + ] + result = _add_into_requirements(lines) + self.assertEqual(len(result), 2) + + def test_ignores_empty_lines(self): + """Test that empty lines are ignored.""" + lines = ["numpy", "", " ", "pandas"] + result = _add_into_requirements(lines) + self.assertEqual(len(result), 2) + + def test_handles_whitespace(self): + """Test that leading/trailing whitespace is handled.""" + lines = [" numpy ", "\tpandas\t"] + result = _add_into_requirements(lines) + self.assertEqual(len(result), 2) + + +class TestMergeRequirements(unittest.TestCase): + """Test the merge_requirements function.""" + + def test_merge_specifiers(self): + """Test merging two requirements with version specifiers.""" + req1 = Requirement("numpy>=1.0") + req2 = Requirement("numpy<2.0") + result = merge_requirements(req1, req2) + self.assertEqual(result.name, "numpy") + self.assertIn(">=1.0", str(result.specifier)) + self.assertIn("<2.0", str(result.specifier)) + + def test_merge_markers(self): + """Test merging two requirements with markers.""" + req1 = Requirement("numpy; sys_platform == 'win32'") + req2 = Requirement("numpy; python_version >= '3.8'") + result = merge_requirements(req1, req2) + self.assertEqual(result.name, "numpy") + self.assertIn("sys_platform", str(result.marker)) + self.assertIn("python_version", str(result.marker)) + + def test_merge_preserves_name(self): + """Test that package name is preserved after merge.""" + req1 = Requirement("requests>=2.0") + req2 = Requirement("requests; sys_platform == 'linux'") + result = merge_requirements(req1, req2) + self.assertEqual(result.name, "requests") + + +class TestGetNoBinaryArgs(unittest.TestCase): + """Test the get_no_binary_args function.""" + + @patch("_helper_functions.platform.system", return_value="Linux") + def test_returns_args_for_source_build_packages_on_linux(self, mock_system): + """Test that --no-binary args are returned for specified packages on Linux.""" + result = get_no_binary_args("cffi") + self.assertEqual(result, ["--no-binary", "cffi"]) + + @patch("_helper_functions.platform.system", return_value="Linux") + def test_handles_requirement_with_version(self, mock_system): + """Test that package name is extracted from requirement string.""" + result = get_no_binary_args("cffi>=1.0") + self.assertEqual(result, ["--no-binary", "cffi"]) + + @patch("_helper_functions.platform.system", return_value="Windows") + def test_returns_empty_on_windows(self, mock_system): + """Test that empty list is returned on Windows.""" + result = get_no_binary_args("cffi") + self.assertEqual(result, []) + + @patch("_helper_functions.platform.system", return_value="Darwin") + def test_returns_empty_on_macos(self, mock_system): + """Test that empty list is returned on macOS.""" + result = get_no_binary_args("cffi") + self.assertEqual(result, []) + + @patch("_helper_functions.platform.system", return_value="Linux") + def test_returns_empty_for_non_source_build_package(self, mock_system): + """Test that empty list is returned for packages not in source build list.""" + result = get_no_binary_args("requests") + self.assertEqual(result, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_wheels_install.py b/test/test_wheels_install.py new file mode 100644 index 0000000..0d3a68b --- /dev/null +++ b/test/test_wheels_install.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 + +""" +Test wheel installation script for CI workflows. + +This script finds and installs wheels compatible with the current Python version, +verifying that wheel files are valid and platform-compatible. +""" + +from __future__ import annotations + +import re +import subprocess +import sys + +from pathlib import Path + +WHEELS_DIR = Path("./downloaded_wheels") + + +def get_python_version_tag() -> str: + """Get the Python version tag (e.g., '311' for Python 3.11).""" + return f"{sys.version_info.major}{sys.version_info.minor}" + + +def is_wheel_compatible(wheel_name: str, python_version: str) -> bool: + """ + Check if a wheel is compatible with the given Python version. + + Compatible wheels are: + - cpXY: exact Python version match (e.g., cp311 for Python 3.11) + - py3: universal Python 3 wheels + - py2.py3: universal Python 2/3 wheels + - abi3: stable ABI wheels (compatible with Python >= base version) + """ + patterns = [ + rf"-cp{python_version}-", # Exact version match + r"-py3-", # Universal Python 3 + r"-py2\.py3-", # Universal Python 2/3 + r"-abi3-", # Stable ABI + ] + return any(re.search(pattern, wheel_name) for pattern in patterns) + + +def find_compatible_wheels(python_version: str) -> list[Path]: + """Find all wheel files compatible with the given Python version.""" + if not WHEELS_DIR.exists(): + return [] + + wheels = [] + for wheel_path in WHEELS_DIR.glob("*.whl"): + if is_wheel_compatible(wheel_path.name, python_version): + wheels.append(wheel_path) + + return sorted(wheels) + + +def install_wheel(wheel_path: Path) -> tuple[bool, str]: + """ + Install a wheel with --no-deps to verify wheel validity. + + Returns: + tuple: (success: bool, error_message: str) + """ + cmd = [ + sys.executable, + "-m", + "pip", + "install", + "--no-deps", + "--no-index", + "--find-links", + str(WHEELS_DIR), + str(wheel_path), + ] + + result = subprocess.run(cmd, capture_output=True, text=True) + + if result.returncode == 0: + return True, "" + + return False, (result.stderr or result.stdout).strip() + + +def main() -> int: + python_version = get_python_version_tag() + + # Find compatible wheels + wheels = find_compatible_wheels(python_version) + print(f"Found {len(wheels)} compatible wheels to test\n") + + if not wheels: + print("No compatible wheels found!") + return 1 + + # Install each wheel + installed = 0 + failed = 0 + failed_wheels = [] + + print("Installing wheels (--no-deps to test wheel validity only)...") + print("-" * 60) + + for wheel_path in wheels: + success, error_message = install_wheel(wheel_path) + + if success: + installed += 1 + else: + failed += 1 + failed_wheels.append((wheel_path.name, error_message)) + print() + print(f"ERROR: Failed to install {wheel_path.name}") + if error_message: + for line in error_message.split("\n"): + print(f" {line}") + print() + + print("-" * 60) + print(f"Results: {installed} installed successfully, {failed} failed\n") + + # Print summary of failures + if failed_wheels: + print("Failed wheels:") + for wheel_name, _ in failed_wheels: + print(f" - {wheel_name}") + print() + return 1 + + print("All wheels installed successfully!") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test_build_wheels.py b/test_build_wheels.py deleted file mode 100644 index 11ef7c9..0000000 --- a/test_build_wheels.py +++ /dev/null @@ -1,165 +0,0 @@ -# ruff: noqa: E501 -# line too long skip in ruff for whole file (formatting would be worst than long lines) -# -# SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD -# -# SPDX-License-Identifier: Apache-2.0 -# -import unittest - -from packaging.requirements import Requirement - -from yaml_list_adapter import YAMLListAdapter - - -class TestYAMLtoRequirement(unittest.TestCase): - def test_change_specifier_logic(self): - version_with_specifier = ( - (">0.9.0.2", "<0.9.0.2"), - ("<0.9.0.2", ">0.9.0.2"), - ("==0.9.0.2", "!=0.9.0.2"), - (">=0.9.0.2", "<=0.9.0.2"), - ("<=0.9.0.2", ">=0.9.0.2"), - ("!=0.9.0.2", "==0.9.0.2"), - ("===0.9.0.2", "===0.9.0.2"), - ) - - for case in version_with_specifier: - self.assertEqual( - f"{YAMLListAdapter._change_specifier_logic(case[0])[0]}{YAMLListAdapter._change_specifier_logic(case[0])[1]}", - case[1], - ) - - def test_yaml_to_requirement(self): - test_requirements = { - Requirement("platform;sys_platform == 'win32'"), - Requirement("platform;sys_platform == 'win32' or sys_platform == 'linux'"), - Requirement("version<42"), - Requirement("version<42,>50"), - Requirement("python;python_version > '3.10'"), - Requirement("python;python_version > '3.10' and python_version != '3.8'"), - Requirement("version-platform<=0.9.0.2;sys_platform == 'win32'"), - Requirement("version-platform<=0.9.0.2,>0.9.1;sys_platform == 'win32'"), - Requirement("version-platform<=0.9.0.2;sys_platform == 'win32' or sys_platform == 'linux'"), - Requirement("version-platform<=0.9.0.2,>0.9.1;sys_platform == 'win32' or sys_platform == 'linux'"), - Requirement("version-python<=0.9.0.2;python_version < '3.8'"), - Requirement("version-python<=0.9.0.2,>0.9.1;python_version < '3.8'"), - Requirement("version-python<=0.9.0.2;python_version < '3.8' and python_version > '3.11'"), - Requirement("version-python<=0.9.0.2,>0.9.1;python_version < '3.8' and python_version > '3.11'"), - Requirement("platform-python;sys_platform == 'win32' and python_version < '3.8'"), - Requirement( - "platform-python;sys_platform == 'win32' or sys_platform == 'linux' and python_version < '3.8'" - ), - Requirement( - "platform-python;sys_platform == 'win32' and python_version < '3.8' and python_version > '3.11'" - ), - Requirement( - "platform-python;sys_platform == 'win32' or sys_platform == 'linux' and python_version < '3.8' and python_version > '3.11'" - ), - Requirement("version-platform-python<=0.9.0.2;sys_platform == 'win32' and python_version < '3.8'"), - Requirement("version-platform-python<=0.9.0.2,>0.9.1;sys_platform == 'win32' and python_version < '3.8'"), - Requirement( - "version-platform-python<=0.9.0.2,>0.9.1;sys_platform == 'win32' or sys_platform == 'linux' and python_version < '3.8'" - ), - Requirement( - "version-platform-python<=0.9.0.2;sys_platform == 'win32' or sys_platform == 'linux' and python_version < '3.8'" - ), - Requirement( - "version-platform-python<=0.9.0.2;sys_platform == 'win32' and python_version < '3.8' and python_version > '3.11'" - ), - Requirement( - "version-platform-python<=0.9.0.2,>0.9.1;sys_platform == 'win32' and python_version < '3.8' and python_version > '3.11'" - ), - Requirement( - "version-platform-python<=0.9.0.2,>0.9.1;sys_platform == 'win32' or sys_platform == 'linux' and python_version < '3.8' and python_version > '3.11'" - ), - Requirement( - "version-platform-python<=0.9.0.2;sys_platform == 'win32' or sys_platform == 'linux' and python_version < '3.8' and python_version > '3.11'" - ), - } - - self.assertEqual(YAMLListAdapter._yaml_to_requirement("test/test_list.yaml"), test_requirements) - - def test_yaml_to_requirement_exclude(self): - test_requirements_exclude = { - Requirement("platform;sys_platform != 'win32'"), - Requirement("platform;sys_platform != 'win32' or sys_platform != 'linux'"), - Requirement("version>42"), - Requirement("version>42,<50"), - Requirement("python;python_version < '3.10'"), - Requirement("python;python_version < '3.10' and python_version == '3.8'"), - Requirement("version-platform>=0.9.0.2;sys_platform == 'win32'"), - Requirement("version-platform;sys_platform != 'win32'"), - Requirement("version-platform>=0.9.0.2,<0.9.1;sys_platform == 'win32'"), - Requirement("version-platform;sys_platform != 'win32'"), - Requirement("version-platform>=0.9.0.2;sys_platform == 'win32' or sys_platform == 'linux'"), - Requirement("version-platform;sys_platform != 'win32' or sys_platform != 'linux'"), - Requirement("version-platform>=0.9.0.2,<0.9.1;sys_platform == 'win32' or sys_platform == 'linux'"), - Requirement("version-platform;sys_platform != 'win32' or sys_platform != 'linux'"), - Requirement("version-python>=0.9.0.2;python_version < '3.8'"), - Requirement("version-python;python_version > '3.8'"), - Requirement("version-python>=0.9.0.2,<0.9.1;python_version < '3.8'"), - Requirement("version-python;python_version > '3.8'"), - Requirement("version-python>=0.9.0.2;python_version < '3.8' and python_version > '3.11'"), - Requirement("version-python;python_version > '3.8' and python_version < '3.11'"), - Requirement("version-python>=0.9.0.2,<0.9.1;python_version < '3.8' and python_version > '3.11'"), - Requirement("version-python;python_version > '3.8' and python_version < '3.11'"), - Requirement("platform-python;sys_platform != 'win32' and python_version > '3.8'"), - Requirement( - "platform-python;sys_platform != 'win32' or sys_platform != 'linux' and python_version > '3.8'" - ), - Requirement( - "platform-python;sys_platform != 'win32' and python_version > '3.8' and python_version < '3.11'" - ), - Requirement( - "platform-python;sys_platform != 'win32' or sys_platform != 'linux' and python_version > '3.8' and python_version < '3.11'" - ), - Requirement("version-platform-python>=0.9.0.2;sys_platform == 'win32' and python_version < '3.8'"), - Requirement("version-platform-python;sys_platform != 'win32' and python_version > '3.8'"), - Requirement("version-platform-python>=0.9.0.2,<0.9.1;sys_platform == 'win32' and python_version < '3.8'"), - Requirement("version-platform-python;sys_platform != 'win32' and python_version > '3.8'"), - Requirement( - "version-platform-python>=0.9.0.2,<0.9.1;sys_platform == 'win32' or sys_platform == 'linux' and python_version < '3.8'" - ), - Requirement( - "version-platform-python;sys_platform != 'win32' or sys_platform != 'linux' and python_version > '3.8'" - ), - Requirement( - "version-platform-python>=0.9.0.2;sys_platform == 'win32' or sys_platform == 'linux' and python_version < '3.8'" - ), - Requirement( - "version-platform-python;sys_platform != 'win32' or sys_platform != 'linux' and python_version > '3.8'" - ), - Requirement( - "version-platform-python>=0.9.0.2;sys_platform == 'win32' and python_version < '3.8' and python_version > '3.11'" - ), - Requirement( - "version-platform-python;sys_platform != 'win32' and python_version > '3.8' and python_version < '3.11'" - ), - Requirement( - "version-platform-python>=0.9.0.2,<0.9.1;sys_platform == 'win32' and python_version < '3.8' and python_version > '3.11'" - ), - Requirement( - "version-platform-python;sys_platform != 'win32' and python_version > '3.8' and python_version < '3.11'" - ), - Requirement( - "version-platform-python>=0.9.0.2,<0.9.1;sys_platform == 'win32' or sys_platform == 'linux' and python_version < '3.8' and python_version > '3.11'" - ), - Requirement( - "version-platform-python;sys_platform != 'win32' or sys_platform != 'linux' and python_version > '3.8' and python_version < '3.11'" - ), - Requirement( - "version-platform-python>=0.9.0.2;sys_platform == 'win32' or sys_platform == 'linux' and python_version < '3.8' and python_version > '3.11'" - ), - Requirement( - "version-platform-python;sys_platform != 'win32' or sys_platform != 'linux' and python_version > '3.8' and python_version < '3.11'" - ), - } - - self.assertEqual( - YAMLListAdapter._yaml_to_requirement("test/test_list.yaml", exclude=True), test_requirements_exclude - ) - - -if __name__ == "__main__": - unittest.main()