# To update pinned container digests and pyenv version (not handled by Dependabot):
# Containers: change tag and get new digest with
#   docker pull <image>:<tag> && docker inspect --format='{{index .RepoDigests 0}}' <image>:<tag>
# pyenv: see https://github.com/pyenv/pyenv/releases
name: Test with compiler sanitizers

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main
      - maintenance/**
    paths-ignore:
      - '**.pyi'
      - '**.md'
      - '**.rst'
      - 'tools/stubtest/**'

defaults:
  run:
    shell: bash

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

permissions:
  contents: read # to fetch code (actions/checkout)

jobs:
  clang_ASAN_UBSAN:
    # To enable this workflow on a fork, comment out:
    if: github.repository == 'numpy/numpy'
    runs-on: macos-26
    steps:
    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      with:
        submodules: recursive
        fetch-tags: true
        persist-credentials: false
    - name: Set up pyenv
      run: |
        git clone --branch v2.7.3 --depth 1 https://github.com/pyenv/pyenv.git "$HOME/.pyenv"
        PYENV_ROOT="$HOME/.pyenv"
        PYENV_BIN="$PYENV_ROOT/bin"
        PYENV_SHIMS="$PYENV_ROOT/shims"
        echo "$PYENV_BIN" >> $GITHUB_PATH
        echo "$PYENV_SHIMS" >> $GITHUB_PATH
        echo "PYENV_ROOT=$PYENV_ROOT" >> $GITHUB_ENV
    - name: Check pyenv is working
      run:
        pyenv --version
    - name: Set up LLVM
      run: |
        brew install llvm
        LLVM_PREFIX=$(brew --prefix llvm)
        echo CC="$LLVM_PREFIX/bin/clang" >> $GITHUB_ENV
        echo CXX="$LLVM_PREFIX/bin/clang++" >> $GITHUB_ENV
        echo LDFLAGS="-L$LLVM_PREFIX/lib" >> $GITHUB_ENV
        echo CPPFLAGS="-I$LLVM_PREFIX/include" >> $GITHUB_ENV
    - name: Build Python with address sanitizer
      run: |
        CONFIGURE_OPTS="--with-address-sanitizer" pyenv install -v 3.14
        pyenv global 3.14
    - name: Install dependencies
      run: |
        pip install -r requirements/build_requirements.txt
        pip install -r requirements/ci_requirements.txt
        pip install -r requirements/test_requirements.txt
        # xdist captures stdout/stderr, but we want the ASAN output
        pip uninstall -y pytest-xdist
    - name: Build
      run:
        python -m spin build -j2 -- -Db_sanitize=address,undefined -Db_lundef=false
    - name: Test
      run: |
        # pass -s to pytest to see ASAN errors and warnings, otherwise pytest captures them
        # Ignore test_casting_floatingpoint_errors on macOS for now - causes crash inside UBSAN
        ASAN_OPTIONS=detect_leaks=0:symbolize=1:strict_init_order=true:allocator_may_return_null=1 \
        UBSAN_OPTIONS=halt_on_error=1:suppressions=${GITHUB_WORKSPACE}/tools/ci/ubsan_suppressions_arm64.txt \
        python -m spin test -- -k "not test_casting_floatingpoint_errors" -v -s --timeout=600 --durations=10

  clang_TSAN:
    # To enable this workflow on a fork, comment out:
    if: github.repository == 'numpy/numpy'
    runs-on: ubuntu-latest
    container:
        image: ghcr.io/nascheme/numpy-tsan:3.14t@sha256:1ec427e2e480cc373d0fecbf21b8ac590fb94119fb81d18489945cd0afd04dd3
        options: --shm-size=2g # increase memory for large matrix ops

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - name: Trust working directory and initialize submodules
        run: |
          git config --global --add safe.directory /__w/numpy/numpy
          git submodule update --init --recursive
      - name: Uninstall pytest-xdist (conflicts with TSAN)
        run: pip uninstall -y pytest-xdist

      - name: Upgrade spin (gh-29777)
        run: pip install -U spin

      - name: Build NumPy with ThreadSanitizer
        run: python -m spin build -j2 -- -Db_sanitize=thread

      - name: Run tests under prebuilt TSAN container
        run: |
          export TSAN_OPTIONS="halt_on_error=0:allocator_may_return_null=1:suppressions=$GITHUB_WORKSPACE/tools/ci/tsan_suppressions.txt"
          echo "TSAN_OPTIONS=$TSAN_OPTIONS"
          python -m spin test \
          `find numpy -name "test*.py" | xargs grep -E -l "import threading|ThreadPoolExecutor" | tr '\n' ' '` \
           -- -v -s --timeout=600 --durations=10

  clang_ASAN:
    # To enable this workflow on a fork, comment out:
    if: github.repository == 'numpy/numpy'
    runs-on: ubuntu-latest
    container:
        image: ghcr.io/nascheme/cpython-asan:3.14@sha256:b5bfbcdca07e86d22afaf66e3b57959e1c44756452dcabda1efde7711fd0bdde
        options: --shm-size=2g # increase memory for large matrix ops

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
      - name: Trust working directory and initialize submodules
        run: |
          git config --global --add safe.directory /__w/numpy/numpy
          git submodule update --init --recursive

      - name: Install dependencies
        run: |
          pip install -r requirements/build_requirements.txt
          pip install -r requirements/ci_requirements.txt
          pip install -r requirements/test_requirements.txt
          # xdist captures stdout/stderr, but we want the ASAN output
          pip uninstall -y pytest-xdist

      - name: Build NumPy with AddressSanitizer & LeakSanitizer
        run: python -m spin build -j4 -- -Db_sanitize=address,leak

      - name: Test
        run: |
          # pass -s to pytest to see ASAN errors and warnings, otherwise pytest captures them
          export ASAN_OPTIONS="detect_leaks=1:symbolize=1:strict_init_order=true:allocator_may_return_null=1:use_sigaltstack=0"
          export LSAN_OPTIONS="suppressions=$GITHUB_WORKSPACE/tools/ci/lsan_suppressions.txt"
          python -m spin test -- -v -s --timeout=600 --durations=10
