diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3a05d97 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + labels: + - "ci" \ No newline at end of file diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 04da882..15ca0f0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,15 +1,18 @@ name: Build and Test Package on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: build-sdist: name: Build Source Dist runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: recursive - name: Set Up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' - name: Install Tools @@ -18,11 +21,11 @@ jobs: - name: Source Packaging run: | python -m build --sdist - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: sdist path: 'dist/spotfire-*.tar.gz' - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: test-files path: | @@ -50,12 +53,12 @@ jobs: operating-system: ['ubuntu-latest', 'windows-latest'] fail-fast: false steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: sdist path: dist - name: Set Up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install Build Requirements @@ -81,7 +84,7 @@ jobs: python -m build --wheel # Move wheel out of build dir into top-level dist dir mv dist\*.whl ..\dist - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: wheel-${{ matrix.python-version }}-${{ matrix.operating-system }} path: 'dist/spotfire-*.whl' @@ -96,16 +99,16 @@ jobs: test-environment: ${{ fromJson(needs.build-sdist.outputs.test-environments) }} fail-fast: false steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: wheel-${{ matrix.python-version }}-${{ matrix.operating-system }} path: dist - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: test-files path: test-files - name: Set Up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install Dependencies (Linux) @@ -122,7 +125,7 @@ jobs: env: TEST_FILES_DIR: ${{ github.workspace }}/test-files/spotfire/test/files TEST_ENVIRONMENT: ${{ matrix.test-environment }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 if: ${{ always() }} with: name: test-results-${{ matrix.python-version }}-${{ matrix.operating-system }}-${{ matrix.test-environment }} @@ -138,14 +141,14 @@ jobs: echo -n "python-version=" >> $GITHUB_OUTPUT echo '${{ needs.build-sdist.outputs.python-versions }}' | sed -e 's/[^"]*"//' -e 's/".*//' >> $GITHUB_OUTPUT - name: Set Up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ steps.version.outputs.python-version }} - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: sdist path: dist - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: name: wheel-${{ steps.version.outputs.python-version }}-ubuntu-latest path: dist @@ -163,3 +166,36 @@ jobs: mypy spotfire cython-lint spotfire vendor find spotfire -name '*_helpers.[ch]' | xargs cpplint --repository=. + sanitizers: + name: AddressSanitizer + UBSan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + submodules: recursive + - uses: actions/setup-python@v6 + with: + python-version: '3.13' + - name: Install dependencies + run: | + pip install setuptools Cython "numpy>=2.0.0rc1" + pip install ".[polars]" + pip install html-testRunner polars pillow + - name: Rebuild extension with AddressSanitizer + UBSan + env: + CFLAGS: "-fsanitize=address,undefined -fno-omit-frame-pointer -g -D_FORTIFY_SOURCE=2 -fstack-protector-strong" + LDFLAGS: "-fsanitize=address,undefined" + run: python setup.py build_ext --inplace + - name: Run tests under AddressSanitizer + UBSan + run: | + LIBASAN=$(gcc -print-file-name=libasan.so) + LD_PRELOAD="$LIBASAN" PYTHONMALLOC=malloc python -m spotfire.test + env: + ASAN_OPTIONS: "detect_leaks=0:allocator_may_return_null=1:intercept_cxx_exceptions=0" + UBSAN_OPTIONS: "print_stacktrace=1:halt_on_error=1" + TEST_ENVIRONMENT: asan + - uses: actions/upload-artifact@v7 + if: always() + with: + name: test-results-sanitizers + path: build/test-results/*.html diff --git a/.github/workflows/pylint.yaml b/.github/workflows/pylint.yaml index 58911b8..3f03d70 100644 --- a/.github/workflows/pylint.yaml +++ b/.github/workflows/pylint.yaml @@ -7,11 +7,11 @@ jobs: name: Check Linters runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: recursive - name: Set Up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' - name: Install Tools diff --git a/.github/workflows/sbom.yaml b/.github/workflows/sbom.yaml index 72094c3..45cd37e 100644 --- a/.github/workflows/sbom.yaml +++ b/.github/workflows/sbom.yaml @@ -35,7 +35,7 @@ jobs: outputs: python-versions: ${{ steps.dynamic.outputs.pythons }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Read python-versions id: dynamic run: | @@ -48,14 +48,14 @@ jobs: needs: setup runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: recursive # needed for vendor/sbdf-c when building/installing sdist # workflow_run: reuse artifact from build.yaml — no rebuild - name: Download sdist (from workflow_run) if: github.event_name == 'workflow_run' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: sdist path: dist @@ -64,7 +64,7 @@ jobs: # push / release / workflow_dispatch: build fresh - name: Set Up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' - name: Build sdist @@ -118,7 +118,7 @@ jobs: --tool "trivy-${{ env.TRIVY_VERSION }}" - name: Upload SBOM artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: sbom-sdist path: spotfire-sdist.sbom.spdx.json @@ -133,14 +133,14 @@ jobs: python-version: ${{ fromJson(needs.setup.outputs.python-versions) }} fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: recursive # needed for vendor/sbdf-c when building wheel fresh # workflow_run: reuse the ubuntu wheel artifact from build.yaml — no rebuild - name: Download wheel (from workflow_run) if: github.event_name == 'workflow_run' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: wheel-${{ matrix.python-version }}-ubuntu-latest path: dist @@ -150,7 +150,7 @@ jobs: # Also download the sdist so scan-env can install from it (wheel is platform-specific) - name: Download sdist (from workflow_run) if: github.event_name == 'workflow_run' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: sdist path: dist @@ -160,7 +160,7 @@ jobs: # push / release / workflow_dispatch: build fresh on Linux - name: Set Up Python if: github.event_name != 'workflow_run' - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Build wheel @@ -221,7 +221,7 @@ jobs: --tool "trivy-${{ env.TRIVY_VERSION }}" - name: Upload SBOM artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: sbom-wheel-${{ matrix.python-version }} path: spotfire-wheel-${{ matrix.python-version }}.sbom.spdx.json @@ -234,7 +234,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download all SBOM artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: sbom-* path: all-sboms diff --git a/spotfire/test/test_sbdf.py b/spotfire/test/test_sbdf.py index de89774..8803d71 100644 --- a/spotfire/test/test_sbdf.py +++ b/spotfire/test/test_sbdf.py @@ -11,9 +11,19 @@ import pandas as pd import pandas.testing as pdtest import numpy as np -import geopandas as gpd -import matplotlib.pyplot -import seaborn +try: + import geopandas as gpd # type: ignore[import-not-found] +except ImportError: + gpd = None # type: ignore[assignment] +try: + import matplotlib # type: ignore[import-not-found] + import matplotlib.pyplot +except ImportError: + matplotlib = None # type: ignore[assignment] +try: + import seaborn # type: ignore[import-not-found] +except ImportError: + seaborn = None # type: ignore[assignment] import PIL.Image from packaging import version @@ -137,6 +147,7 @@ def test_read_10001(self): self.assertEqual(dataframe.at[10000, "String"], "kiwis") self.assertEqual(dataframe.at[10000, "Binary"], b"\x7c\x7d\x7e\x7f") + @unittest.skipIf(gpd is None, "geopandas not installed") def test_read_write_geodata(self): """Test that geo-encoded data is properly converted to/from ``GeoDataFrame``.""" gdf = sbdf.import_data(utils.get_test_data_file("sbdf/NACountries.sbdf")) @@ -468,6 +479,7 @@ def test_numpy_timedelta_resolution(self): val = df2.at[1, 'x'] self.assertEqual(val, target) + @unittest.skipIf(matplotlib is None, "matplotlib not installed") def test_image_matplot(self): """Verify Matplotlib figures export properly.""" matplotlib.pyplot.clf() @@ -480,6 +492,7 @@ def test_image_matplot(self): else: self.fail(f"Expected PNG bytes, got {type(image)}: {image!r}") + @unittest.skipIf(seaborn is None, "seaborn not installed") def test_image_seaborn(self): """Verify Seaborn grids export properly.""" matplotlib.pyplot.clf()