From b88ff39b03694c4d2b7d449ddee8ce933f5dfbe0 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 09:31:47 +0200 Subject: [PATCH 01/19] Added .github/workflows/gpu_ci_trigger.yml --- .github/workflows/gpu_ci_trigger.yml | 23 ++++++ .gitlab-ci.yml | 104 +++++++++++++-------------- 2 files changed, 75 insertions(+), 52 deletions(-) create mode 100644 .github/workflows/gpu_ci_trigger.yml diff --git a/.github/workflows/gpu_ci_trigger.yml b/.github/workflows/gpu_ci_trigger.yml new file mode 100644 index 0000000..c8da567 --- /dev/null +++ b/.github/workflows/gpu_ci_trigger.yml @@ -0,0 +1,23 @@ +name: Trigger GitLab GPU CI + +on: + push: + branches: [main, devel] + pull_request: + branches: [main, devel] + workflow_dispatch: # Allows manual triggering + +jobs: + trigger-gitlab: + runs-on: ubuntu-latest + steps: + - name: Trigger GitLab Pipeline + run: | + # Use curl to call the GitLab Trigger API + # We pass the GitHub SHA and Repository to GitLab so it knows exactly what to clone. + curl --request POST \ + --form token=${{ secrets.GITLAB_TRIGGER_TOKEN }} \ + --form ref=main \ + --form "variables[GH_SHA]=${{ github.event.pull_request.head.sha || github.sha }}" \ + --form "variables[GH_REPO]=${{ github.repository }}" \ + "https://gitlab.mpcdf.mpg.de/api/v4/projects/${{ secrets.GITLAB_PROJECT_ID }}/trigger/pipeline" diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7de373c..d607cc8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,57 +1,57 @@ -# This file is a template, and might need editing before it works on your project. -# To contribute improvements to CI/CD templates, please follow the Development guide at: -# https://docs.gitlab.com/ee/development/cicd/templates.html -# This specific template is located at: -# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Python.gitlab-ci.yml - -# Official language image. Look for the different tagged releases at: -# https://hub.docker.com/r/library/python/tags/ -image: python:latest - -# Change pip's cache directory to be inside the project directory since we can -# only cache local items. variables: - PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" - -# https://pip.pypa.io/en/stable/topics/caching/ -cache: - paths: - - .cache/pip - -before_script: - - python --version ; pip --version # For debugging - - pip install virtualenv - - virtualenv venv - - source venv/bin/activate - -test: - script: - - pip install ruff tox # you can also use tox - - pip install --editable ".[test]" - - tox -e py,ruff - -run: + # Base image with CUDA and C++ tools + CUDA_IMAGE: "nvidia/cuda:12.4.1-devel-ubuntu22.04" + +stages: + - checkout + - test + +# 1. Checkout Step: Clone the canonical GitHub repo at the exact SHA +checkout_github: + stage: checkout + image: alpine:latest + tags: + - mpcdf-shared script: - - pip install . - # run the command here + - apk add --no-cache git + - echo "Cloning GitHub repo ${GH_REPO} at SHA ${GH_SHA}" + - git clone https://x-access-token:${GH_PAT}@github.com/${GH_REPO}.git workspace + - cd workspace + - git checkout ${GH_SHA} artifacts: paths: - - build/* - -pages: + - workspace/ + expire_in: 1 hour + +# 2. GPU Test Step: Run CUDA sanity checks, build, and test +gpu_tests: + stage: test + image: ${CUDA_IMAGE} + tags: + - gpu-nvidia + - gpu-nvidia-cc80 + dependencies: + - checkout_github + before_script: + - apt-get update && apt-get install -y cmake python3-pip git + - cd workspace script: - - pip install sphinx sphinx-rtd-theme - - cd doc - - make html - - mv build/html/ ../public/ - artifacts: - paths: - - public - rules: - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - -deploy: - stage: deploy - script: echo "Define your deployment script!" - environment: production - + - echo "--- CUDA Sanity Check ---" + - nvidia-smi + + - echo "--- Detect Compute Capability ---" + # Simple check for CC 8.0 (A100) + - nvidia-smi --query-gpu=compute_cap --format=csv,noheader + + - echo "--- CMake Build ---" + # Example for C++ components if they exist + - if [ -f "CMakeLists.txt" ]; then + mkdir build && cd build; + cmake ..; + make -j$(nproc); + cd ..; + fi + + - echo "--- Pytest Execution ---" + - pip3 install .[test] + - pytest tests/unit/ From 9f0c910e0fae50c7ad41e48fb817b7b409123f5a Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 09:47:33 +0200 Subject: [PATCH 02/19] run the gitlab workflow on the same branch --- .github/workflows/gpu_ci_trigger.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gpu_ci_trigger.yml b/.github/workflows/gpu_ci_trigger.yml index c8da567..94c4bbc 100644 --- a/.github/workflows/gpu_ci_trigger.yml +++ b/.github/workflows/gpu_ci_trigger.yml @@ -13,11 +13,13 @@ jobs: steps: - name: Trigger GitLab Pipeline run: | - # Use curl to call the GitLab Trigger API - # We pass the GitHub SHA and Repository to GitLab so it knows exactly what to clone. + # Use the branch name that triggered this workflow + # github.head_ref is used for PRs, github.ref_name for pushes + BRANCH_NAME="${{ github.head_ref || github.ref_name }}" + curl --request POST \ --form token=${{ secrets.GITLAB_TRIGGER_TOKEN }} \ - --form ref=main \ + --form "ref=$BRANCH_NAME" \ --form "variables[GH_SHA]=${{ github.event.pull_request.head.sha || github.sha }}" \ --form "variables[GH_REPO]=${{ github.repository }}" \ "https://gitlab.mpcdf.mpg.de/api/v4/projects/${{ secrets.GITLAB_PROJECT_ID }}/trigger/pipeline" From ec18a9455a1808ca522f779e3aacdfb0ba0559a6 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 09:50:23 +0200 Subject: [PATCH 03/19] push from github->gitlab --- .github/workflows/gpu_ci_trigger.yml | 41 +++++++++++++++++++--------- .gitlab-ci.yml | 26 ++---------------- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/.github/workflows/gpu_ci_trigger.yml b/.github/workflows/gpu_ci_trigger.yml index 94c4bbc..e8e841d 100644 --- a/.github/workflows/gpu_ci_trigger.yml +++ b/.github/workflows/gpu_ci_trigger.yml @@ -1,25 +1,40 @@ -name: Trigger GitLab GPU CI +name: Sync to GitLab and Run GPU CI on: push: branches: [main, devel] pull_request: branches: [main, devel] - workflow_dispatch: # Allows manual triggering + workflow_dispatch: jobs: - trigger-gitlab: + sync-and-test: runs-on: ubuntu-latest steps: - - name: Trigger GitLab Pipeline + - name: Checkout Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for all branches and tags + + - name: Push to GitLab + env: + # You will need to add this secret to GitHub + GITLAB_PUSH_TOKEN: ${{ secrets.GITLAB_PUSH_TOKEN }} run: | - # Use the branch name that triggered this workflow - # github.head_ref is used for PRs, github.ref_name for pushes - BRANCH_NAME="${{ github.head_ref || github.ref_name }}" + # Determine the branch name + # For PRs, we push to a branch named 'pr-' on GitLab + # For regular pushes, we use the actual branch name + if [ "${{ github.event_name }}" == "pull_request" ]; then + TARGET_BRANCH="pr-${{ github.event.number }}" + else + TARGET_BRANCH="${{ github.ref_name }}" + fi + + echo "Pushing to GitLab branch: $TARGET_BRANCH" + + # Add GitLab as a remote using the Project Access Token + # Format: https://oauth2:TOKEN@gitlab.mpcdf.mpg.de/path/to/repo.git + git remote add gitlab "https://oauth2:${GITLAB_PUSH_TOKEN}@gitlab.mpcdf.mpg.de/maxlin/cunumpy.git" - curl --request POST \ - --form token=${{ secrets.GITLAB_TRIGGER_TOKEN }} \ - --form "ref=$BRANCH_NAME" \ - --form "variables[GH_SHA]=${{ github.event.pull_request.head.sha || github.sha }}" \ - --form "variables[GH_REPO]=${{ github.repository }}" \ - "https://gitlab.mpcdf.mpg.de/api/v4/projects/${{ secrets.GITLAB_PROJECT_ID }}/trigger/pipeline" + # Force push the current HEAD to the target branch on GitLab + git push -f gitlab HEAD:refs/heads/$TARGET_BRANCH diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d607cc8..9706eaf 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,48 +3,26 @@ variables: CUDA_IMAGE: "nvidia/cuda:12.4.1-devel-ubuntu22.04" stages: - - checkout - test -# 1. Checkout Step: Clone the canonical GitHub repo at the exact SHA -checkout_github: - stage: checkout - image: alpine:latest - tags: - - mpcdf-shared - script: - - apk add --no-cache git - - echo "Cloning GitHub repo ${GH_REPO} at SHA ${GH_SHA}" - - git clone https://x-access-token:${GH_PAT}@github.com/${GH_REPO}.git workspace - - cd workspace - - git checkout ${GH_SHA} - artifacts: - paths: - - workspace/ - expire_in: 1 hour - -# 2. GPU Test Step: Run CUDA sanity checks, build, and test +# GPU Test Step: Run CUDA sanity checks, build, and test +# Since GitHub now pushes the code directly to GitLab, we can just use the local repo. gpu_tests: stage: test image: ${CUDA_IMAGE} tags: - gpu-nvidia - gpu-nvidia-cc80 - dependencies: - - checkout_github before_script: - apt-get update && apt-get install -y cmake python3-pip git - - cd workspace script: - echo "--- CUDA Sanity Check ---" - nvidia-smi - echo "--- Detect Compute Capability ---" - # Simple check for CC 8.0 (A100) - nvidia-smi --query-gpu=compute_cap --format=csv,noheader - echo "--- CMake Build ---" - # Example for C++ components if they exist - if [ -f "CMakeLists.txt" ]; then mkdir build && cd build; cmake ..; From e23cef93ba2d0a6b71a8d2cde33834a2388a18b0 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 09:57:54 +0200 Subject: [PATCH 04/19] trigger CI From 1c9322fb1db177c680c40578d69858fa50833b2d Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 10:05:05 +0200 Subject: [PATCH 05/19] push with ssh --- .github/workflows/gpu_ci_trigger.yml | 33 +++++++++++++++------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/.github/workflows/gpu_ci_trigger.yml b/.github/workflows/gpu_ci_trigger.yml index e8e841d..b421ebb 100644 --- a/.github/workflows/gpu_ci_trigger.yml +++ b/.github/workflows/gpu_ci_trigger.yml @@ -14,27 +14,30 @@ jobs: - name: Checkout Code uses: actions/checkout@v4 with: - fetch-depth: 0 # Fetch all history for all branches and tags + fetch-depth: 0 - - name: Push to GitLab - env: - # You will need to add this secret to GitHub - GITLAB_PUSH_TOKEN: ${{ secrets.GITLAB_PUSH_TOKEN }} + - name: Install SSH Key + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} + + - name: Push to GitLab via SSH run: | - # Determine the branch name - # For PRs, we push to a branch named 'pr-' on GitLab - # For regular pushes, we use the actual branch name + # 1. Setup SSH known hosts to prevent interactive prompts + mkdir -p ~/.ssh + ssh-keyscan gitlab.mpcdf.mpg.de >> ~/.ssh/known_hosts + + # 2. Determine target branch if [ "${{ github.event_name }}" == "pull_request" ]; then TARGET_BRANCH="pr-${{ github.event.number }}" else TARGET_BRANCH="${{ github.ref_name }}" fi - + echo "Pushing to GitLab branch: $TARGET_BRANCH" - - # Add GitLab as a remote using the Project Access Token - # Format: https://oauth2:TOKEN@gitlab.mpcdf.mpg.de/path/to/repo.git - git remote add gitlab "https://oauth2:${GITLAB_PUSH_TOKEN}@gitlab.mpcdf.mpg.de/maxlin/cunumpy.git" - - # Force push the current HEAD to the target branch on GitLab + + # 3. Add GitLab SSH remote + git remote add gitlab git@gitlab.mpcdf.mpg.de:maxlin/cunumpy.git + + # 4. Force push git push -f gitlab HEAD:refs/heads/$TARGET_BRANCH From 7d2bd4ff098f9fc90a62c43349b5a5734f05d3df Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 10:11:07 +0200 Subject: [PATCH 06/19] Updated .gitlab-ci.yml --- .gitlab-ci.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9706eaf..d569820 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,12 +1,12 @@ variables: # Base image with CUDA and C++ tools CUDA_IMAGE: "nvidia/cuda:12.4.1-devel-ubuntu22.04" + # Instruct pip to install to a directory in the path, or avoid the 'UNKNOWN' warning + PIP_DISABLE_PIP_VERSION_CHECK: "1" stages: - test -# GPU Test Step: Run CUDA sanity checks, build, and test -# Since GitHub now pushes the code directly to GitLab, we can just use the local repo. gpu_tests: stage: test image: ${CUDA_IMAGE} @@ -14,6 +14,8 @@ gpu_tests: - gpu-nvidia - gpu-nvidia-cc80 before_script: + # Set non-interactive timezone to prevent tzdata prompting + - export DEBIAN_FRONTEND=noninteractive - apt-get update && apt-get install -y cmake python3-pip git script: - echo "--- CUDA Sanity Check ---" @@ -31,5 +33,11 @@ gpu_tests: fi - echo "--- Pytest Execution ---" - - pip3 install .[test] - - pytest tests/unit/ + # Ensure pip is up to date and install test requirements directly + - python3 -m pip install --upgrade pip + - pip3 install pytest cupy-cuda12x + # Install the package itself + - pip3 install -e . + # Set the backend to cupy for tests and run them + - export ARRAY_BACKEND=cupy + - python3 -m pytest tests/unit/ From 856ef94951e88a8ea9704b982827cf78ab434d98 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 10:17:03 +0200 Subject: [PATCH 07/19] use a venv on gitalb ci --- .gitlab-ci.yml | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d569820..9a622ed 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -23,21 +23,18 @@ gpu_tests: - echo "--- Detect Compute Capability ---" - nvidia-smi --query-gpu=compute_cap --format=csv,noheader - - - echo "--- CMake Build ---" - - if [ -f "CMakeLists.txt" ]; then - mkdir build && cd build; - cmake ..; - make -j$(nproc); - cd ..; - fi - + + - ls + - pwd + - echo "--- Pytest Execution ---" # Ensure pip is up to date and install test requirements directly - - python3 -m pip install --upgrade pip - - pip3 install pytest cupy-cuda12x + - python3 -m venv .venv + - source .venv/bin/activate + - pip install --upgrade pip + - pip install pytest cupy-cuda12x # Install the package itself - - pip3 install -e . + - pip install -e . # Set the backend to cupy for tests and run them - export ARRAY_BACKEND=cupy - python3 -m pytest tests/unit/ From 677d04cf5fdbccaf5b47ab956f703d5e36806ca9 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 10:21:46 +0200 Subject: [PATCH 08/19] Use gitlab-registry.mpcdf.mpg.de/mpcdf/ci-module-image/nvhpcsdk_26:2026 --- .gitlab-ci.yml | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9a622ed..3e11419 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,6 @@ variables: - # Base image with CUDA and C++ tools - CUDA_IMAGE: "nvidia/cuda:12.4.1-devel-ubuntu22.04" - # Instruct pip to install to a directory in the path, or avoid the 'UNKNOWN' warning + # Use the specialized MPCDF HPC image + CUDA_IMAGE: "gitlab-registry.mpcdf.mpg.de/mpcdf/ci-module-image/nvhpcsdk_26:2026" PIP_DISABLE_PIP_VERSION_CHECK: "1" stages: @@ -13,28 +12,34 @@ gpu_tests: tags: - gpu-nvidia - gpu-nvidia-cc80 - before_script: - # Set non-interactive timezone to prevent tzdata prompting - - export DEBIAN_FRONTEND=noninteractive - - apt-get update && apt-get install -y cmake python3-pip git script: - echo "--- CUDA Sanity Check ---" - nvidia-smi - echo "--- Detect Compute Capability ---" - nvidia-smi --query-gpu=compute_cap --format=csv,noheader - - - ls - - pwd - + + - echo "--- Tool Versions ---" + - cmake --version + - python3 --version + - git --version + + - echo "--- CMake Build ---" + - if [ -f "CMakeLists.txt" ]; then + mkdir -p build && cd build; + cmake ..; + make -j$(nproc); + cd ..; + fi + - echo "--- Pytest Execution ---" - # Ensure pip is up to date and install test requirements directly - - python3 -m venv .venv - - source .venv/bin/activate - - pip install --upgrade pip - - pip install pytest cupy-cuda12x - # Install the package itself - - pip install -e . - # Set the backend to cupy for tests and run them + # The MPCDF image likely has a specific python environment. + # We install our dependencies into the user directory or a virtualenv. + - python3 -m pip install --user pytest cupy-cuda12x + - python3 -m pip install --user -e . + + # Add the user bin to PATH for pytest + - export PATH="$HOME/.local/bin:$PATH" - export ARRAY_BACKEND=cupy + - python3 -m pytest tests/unit/ From 15ecddb321091b50d146397d5e60f93c28d37e47 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 10:24:30 +0200 Subject: [PATCH 09/19] load modules --- .gitlab-ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3e11419..80ce53f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,6 +12,11 @@ gpu_tests: tags: - gpu-nvidia - gpu-nvidia-cc80 + before_script: + - echo "Running GPU tests on image: ${CUDA_IMAGE}" + # Load modules + - module load python-waterboa/2025.06 + - module load nvhpcsdk/26 script: - echo "--- CUDA Sanity Check ---" - nvidia-smi From 8a55a188edc9ef24bae1ef3c2d4966ee491cf9d8 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 10:28:16 +0200 Subject: [PATCH 10/19] cleanup --- .gitlab-ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 80ce53f..48cd289 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,8 +13,6 @@ gpu_tests: - gpu-nvidia - gpu-nvidia-cc80 before_script: - - echo "Running GPU tests on image: ${CUDA_IMAGE}" - # Load modules - module load python-waterboa/2025.06 - module load nvhpcsdk/26 script: From ba45341d36a2d0267e2b5c912ed50bbf7d5bc28e Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 10:29:58 +0200 Subject: [PATCH 11/19] cleanup --- .gitlab-ci.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 48cd289..15971fc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -27,14 +27,6 @@ gpu_tests: - python3 --version - git --version - - echo "--- CMake Build ---" - - if [ -f "CMakeLists.txt" ]; then - mkdir -p build && cd build; - cmake ..; - make -j$(nproc); - cd ..; - fi - - echo "--- Pytest Execution ---" # The MPCDF image likely has a specific python environment. # We install our dependencies into the user directory or a virtualenv. From 32a2362ef4126721fdc60010d33de61e9d271f75 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 10:31:27 +0200 Subject: [PATCH 12/19] Skip parity test --- tests/unit/test_app.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 53e095e..1eb6e61 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -18,6 +18,9 @@ def test_numpy_symbols_accessible(): This validates the runtime behaviour that the stub file (__init__.pyi) declares to Pylance so that `xp.` shows numpy completions in VS Code. """ + if xp.cupy_backend: + pytest.skip("CuPy does not have 100% symbol parity with NumPy.") + # Exclude our custom methods from the numpy check custom_methods = [ "to_numpy", From a08068da98ca616ef7ce1046abf4e20dda1752cc Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 10:32:48 +0200 Subject: [PATCH 13/19] Wait for gitlab job completion --- .github/workflows/gpu_ci_trigger.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/gpu_ci_trigger.yml b/.github/workflows/gpu_ci_trigger.yml index b421ebb..9cbe903 100644 --- a/.github/workflows/gpu_ci_trigger.yml +++ b/.github/workflows/gpu_ci_trigger.yml @@ -41,3 +41,26 @@ jobs: # 4. Force push git push -f gitlab HEAD:refs/heads/$TARGET_BRANCH + + # Store branch for next step + echo "TARGET_BRANCH=$TARGET_BRANCH" >> $GITHUB_ENV + + - name: Trigger GitLab Pipeline & Provide Link + run: | + # Trigger the pipeline and capture the JSON response + RESPONSE=$(curl --silent --request POST \ + --form token=${{ secrets.GITLAB_TRIGGER_TOKEN }} \ + --form "ref=$TARGET_BRANCH" \ + "https://gitlab.mpcdf.mpg.de/api/v4/projects/${{ secrets.GITLAB_PROJECT_ID }}/trigger/pipeline") + + # Extract the pipeline web_url using python (built-in to runner) + PIPELINE_URL=$(echo $RESPONSE | python3 -c "import sys, json; print(json.load(sys.stdin).get('web_url', ''))") + + if [ -z "$PIPELINE_URL" ]; then + echo "::error::Failed to trigger GitLab pipeline. Response: $RESPONSE" + exit 1 + fi + + echo "::notice::GitLab GPU CI Pipeline triggered successfully!" + echo "::notice::View Pipeline: $PIPELINE_URL" + echo "PIPELINE_URL=$PIPELINE_URL" >> $GITHUB_ENV From e2557dbcd9b5a33c56a632f4555cbde14d966ae3 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 10:35:05 +0200 Subject: [PATCH 14/19] Split tests --- .gitlab-ci.yml | 2 +- CHANGELOG.md | 1 + tests/unit/test_app.py | 134 ------------------------------------- tests/unit/test_cunumpy.py | 51 ++++++++++++++ tests/unit/test_cupy.py | 34 ++++++++++ tests/unit/test_numpy.py | 40 +++++++++++ 6 files changed, 127 insertions(+), 135 deletions(-) delete mode 100644 tests/unit/test_app.py create mode 100644 tests/unit/test_cunumpy.py create mode 100644 tests/unit/test_cupy.py create mode 100644 tests/unit/test_numpy.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 15971fc..b76c09b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -30,7 +30,7 @@ gpu_tests: - echo "--- Pytest Execution ---" # The MPCDF image likely has a specific python environment. # We install our dependencies into the user directory or a virtualenv. - - python3 -m pip install --user pytest cupy-cuda12x + - python3 -m pip install --user cupy-cuda12x - python3 -m pip install --user -e . # Add the user bin to PATH for pytest diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fae537..ec7876c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `xp.synchronize()`: Blocks until GPU operations are complete (no-op on CPU). Essential for accurate benchmarking. - **Developer Experience**: - Added `isort` configuration to `pyproject.toml` with `black` profile compatibility. + - Reorganized test suite into specialized files: `test_numpy.py`, `test_cupy.py`, and `test_cunumpy.py`. ### Changed - **Dynamic Dispatch Architecture**: Refactored `src/cunumpy/xp.py` to use module-level `__getattr__`. This ensures that `cunumpy.` calls always resolve to the currently active backend module, enabling seamless runtime switching via `set_backend`. diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py deleted file mode 100644 index 1eb6e61..0000000 --- a/tests/unit/test_app.py +++ /dev/null @@ -1,134 +0,0 @@ -import numpy as np -import pytest - -import cunumpy as xp - - -def test_xp_array(): - - arr = xp.array([1, 2]) - arr *= 2 - - print(f"{arr = } {type(arr) = }") - - -def test_numpy_symbols_accessible(): - """All public numpy symbols must be reachable via cunumpy. - - This validates the runtime behaviour that the stub file (__init__.pyi) - declares to Pylance so that `xp.` shows numpy completions in VS Code. - """ - if xp.cupy_backend: - pytest.skip("CuPy does not have 100% symbol parity with NumPy.") - - # Exclude our custom methods from the numpy check - custom_methods = [ - "to_numpy", - "to_cupy", - "to_cunumpy", - "get_backend", - "is_gpu", - "is_cpu", - "use_backend", - "set_backend", - "synchronize", - "numpy_backend", - "cupy_backend", - "xp", - ] - missing = [ - name - for name in np.__all__ - if not hasattr(xp, name) and name not in custom_methods - ] - assert missing == [], f"Symbols not accessible via cunumpy: {missing}" - - -def test_to_numpy(): - arr = xp.array([1, 2, 3]) - # Even if it's already numpy, to_numpy should work - arr_np = xp.to_numpy(arr) - assert isinstance(arr_np, np.ndarray) - assert np.array_equal(arr_np, [1, 2, 3]) - - -def test_to_cupy_not_available(): - try: - import cupy - - pytest.skip("CuPy is installed, cannot test missing cupy error") - except ImportError: - pass - - arr = np.array([1, 2, 3]) - - with pytest.raises(ImportError): - xp.to_cupy(arr) - - -def test_to_cunumpy(): - arr = np.array([1, 2, 3]) - arr_xp = xp.to_cunumpy(arr) - # Backend is numpy in tests usually - assert isinstance(arr_xp, (np.ndarray, xp.ndarray)) - - -def test_get_backend_and_is_gpu_cpu(): - arr = np.array([1, 2, 3]) - assert xp.get_backend(arr) == "numpy" - assert xp.is_gpu(arr) is False - assert xp.is_cpu(arr) is True - - -def test_use_backend(): - # Initial backend should be numpy (default) in this test environment - # Accessing xp.xp triggers the dynamic __getattr__ in xp.py - assert "numpy" in xp.xp.__name__ - - with xp.use_backend("numpy"): - assert "numpy" in xp.xp.__name__ - arr = xp.zeros(10) - assert isinstance(arr, np.ndarray) - - assert "numpy" in xp.xp.__name__ - - -def test_set_backend(): - # Set to numpy - xp.set_backend("numpy") - assert "numpy" in xp.xp.__name__ - arr = xp.array([1]) - assert isinstance(arr, np.ndarray) - - # Set to cupy (falls back to numpy if not available) - xp.set_backend("cupy") - # If cupy is not installed, xp.xp will be numpy module - # We just verify it doesn't crash and we can still call things - arr2 = xp.array([2]) - assert arr2 is not None - - -def test_synchronize(): - # Should not crash on any backend - xp.synchronize() - - with xp.use_backend("numpy"): - xp.synchronize() - - with xp.use_backend("cupy"): - xp.synchronize() - - -def test_backend_bools(): - with xp.use_backend("numpy"): - assert xp.numpy_backend is True - assert xp.cupy_backend is False - - # Note: in test env without cupy, cupy_backend might be false - # even inside use_backend('cupy') if fallback occurs. - # Our implementation of use_backend calls _load_backend which returns np if cp missing. - - -if __name__ == "__main__": - test_xp_array() - test_numpy_symbols_accessible() diff --git a/tests/unit/test_cunumpy.py b/tests/unit/test_cunumpy.py new file mode 100644 index 0000000..e2b0302 --- /dev/null +++ b/tests/unit/test_cunumpy.py @@ -0,0 +1,51 @@ +import numpy as np +import pytest +import cunumpy as xp + +def test_to_numpy(): + arr = xp.array([1, 2, 3]) + # Even if it's already numpy, to_numpy should work + arr_np = xp.to_numpy(arr) + assert isinstance(arr_np, np.ndarray) + assert np.array_equal(arr_np, [1, 2, 3]) + +def test_to_cunumpy(): + arr = np.array([1, 2, 3]) + arr_xp = xp.to_cunumpy(arr) + # Backend is numpy in tests usually + assert isinstance(arr_xp, (np.ndarray, xp.ndarray)) + +def test_get_backend_and_is_gpu_cpu(): + arr = np.array([1, 2, 3]) + assert xp.get_backend(arr) == "numpy" + assert xp.is_gpu(arr) is False + assert xp.is_cpu(arr) is True + +def test_use_backend(): + # Initial backend should be numpy (default) in this test environment + assert "numpy" in xp.xp.__name__ + + with xp.use_backend("numpy"): + assert "numpy" in xp.xp.__name__ + arr = xp.zeros(10) + assert isinstance(arr, np.ndarray) + + assert "numpy" in xp.xp.__name__ + +def test_set_backend(): + # Set to numpy + xp.set_backend("numpy") + assert "numpy" in xp.xp.__name__ + arr = xp.array([1]) + assert isinstance(arr, np.ndarray) + + # Set to cupy (falls back to numpy if not available) + xp.set_backend("cupy") + # If cupy is not installed, xp.xp will be numpy module + arr2 = xp.array([2]) + assert arr2 is not None + +def test_backend_bools(): + with xp.use_backend("numpy"): + assert xp.numpy_backend is True + assert xp.cupy_backend is False diff --git a/tests/unit/test_cupy.py b/tests/unit/test_cupy.py new file mode 100644 index 0000000..f1d2afb --- /dev/null +++ b/tests/unit/test_cupy.py @@ -0,0 +1,34 @@ +import pytest +import cunumpy as xp +import numpy as np + +def test_to_cupy_available(): + try: + import cupy as cp + except ImportError: + pytest.skip("CuPy not installed") + + arr = np.array([1, 2, 3]) + arr_cp = xp.to_cupy(arr) + assert isinstance(arr_cp, cp.ndarray) + +def test_to_cupy_not_available(): + try: + import cupy + pytest.skip("CuPy is installed, cannot test missing cupy error") + except ImportError: + pass + + arr = np.array([1, 2, 3]) + with pytest.raises(ImportError): + xp.to_cupy(arr) + +def test_synchronize(): + # Should not crash on any backend + xp.synchronize() + + with xp.use_backend("numpy"): + xp.synchronize() + + with xp.use_backend("cupy"): + xp.synchronize() diff --git a/tests/unit/test_numpy.py b/tests/unit/test_numpy.py new file mode 100644 index 0000000..bd481ee --- /dev/null +++ b/tests/unit/test_numpy.py @@ -0,0 +1,40 @@ +import numpy as np +import pytest +import cunumpy as xp + +def test_xp_array(): + arr = xp.array([1, 2]) + arr *= 2 + assert isinstance(arr, np.ndarray) + assert np.array_equal(arr, [2, 4]) + +def test_numpy_symbols_accessible(): + """All public numpy symbols must be reachable via cunumpy. + + This validates the runtime behaviour that the stub file (__init__.pyi) + declares to Pylance so that `xp.` shows numpy completions in VS Code. + """ + if xp.cupy_backend: + pytest.skip("CuPy does not have 100% symbol parity with NumPy.") + + # Exclude our custom methods from the numpy check + custom_methods = [ + "to_numpy", + "to_cupy", + "to_cunumpy", + "get_backend", + "is_gpu", + "is_cpu", + "use_backend", + "set_backend", + "synchronize", + "numpy_backend", + "cupy_backend", + "xp", + ] + missing = [ + name + for name in np.__all__ + if not hasattr(xp, name) and name not in custom_methods + ] + assert missing == [], f"Symbols not accessible via cunumpy: {missing}" From f190154f486c6897f0451181df5e5ee77168d9fa Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 10:35:30 +0200 Subject: [PATCH 15/19] Fix gitlab ci trigger --- .github/workflows/gpu_ci_trigger.yml | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/gpu_ci_trigger.yml b/.github/workflows/gpu_ci_trigger.yml index 9cbe903..05457ec 100644 --- a/.github/workflows/gpu_ci_trigger.yml +++ b/.github/workflows/gpu_ci_trigger.yml @@ -47,20 +47,28 @@ jobs: - name: Trigger GitLab Pipeline & Provide Link run: | + # Give GitLab a moment to index the newly pushed branch + sleep 2 + # Trigger the pipeline and capture the JSON response - RESPONSE=$(curl --silent --request POST \ + # We use -w to get the HTTP status code as well + RESPONSE_DATA=$(curl --silent --show-error --write-out "\n%{http_code}" --request POST \ --form token=${{ secrets.GITLAB_TRIGGER_TOKEN }} \ --form "ref=$TARGET_BRANCH" \ "https://gitlab.mpcdf.mpg.de/api/v4/projects/${{ secrets.GITLAB_PROJECT_ID }}/trigger/pipeline") - # Extract the pipeline web_url using python (built-in to runner) - PIPELINE_URL=$(echo $RESPONSE | python3 -c "import sys, json; print(json.load(sys.stdin).get('web_url', ''))") + HTTP_STATUS=$(echo "$RESPONSE_DATA" | tail -n1) + RESPONSE_BODY=$(echo "$RESPONSE_DATA" | sed '$d') - if [ -z "$PIPELINE_URL" ]; then - echo "::error::Failed to trigger GitLab pipeline. Response: $RESPONSE" + if [ "$HTTP_STATUS" != "201" ]; then + echo "::error::GitLab API returned $HTTP_STATUS" + echo "::error::Response: $RESPONSE_BODY" + echo "::error::Check if GITLAB_PROJECT_ID (${{ secrets.GITLAB_PROJECT_ID }}) and GITLAB_TRIGGER_TOKEN are correct." exit 1 fi + # Extract the pipeline web_url + PIPELINE_URL=$(echo $RESPONSE_BODY | python3 -c "import sys, json; print(json.load(sys.stdin).get('web_url', ''))") + echo "::notice::GitLab GPU CI Pipeline triggered successfully!" echo "::notice::View Pipeline: $PIPELINE_URL" - echo "PIPELINE_URL=$PIPELINE_URL" >> $GITHUB_ENV From 6be17e80a948ee95831ab764137108a6b8fb1818 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 10:37:03 +0200 Subject: [PATCH 16/19] Sleep for 10s --- .github/workflows/gpu_ci_trigger.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gpu_ci_trigger.yml b/.github/workflows/gpu_ci_trigger.yml index 05457ec..6cb16fc 100644 --- a/.github/workflows/gpu_ci_trigger.yml +++ b/.github/workflows/gpu_ci_trigger.yml @@ -48,7 +48,7 @@ jobs: - name: Trigger GitLab Pipeline & Provide Link run: | # Give GitLab a moment to index the newly pushed branch - sleep 2 + sleep 10 # Trigger the pipeline and capture the JSON response # We use -w to get the HTTP status code as well From 0210104c1e4bea8aaa16244a76dcd982b1a98678 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 10:39:11 +0200 Subject: [PATCH 17/19] Simplify workflow --- .github/workflows/gpu_ci_trigger.yml | 43 ++++++---------------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/.github/workflows/gpu_ci_trigger.yml b/.github/workflows/gpu_ci_trigger.yml index 6cb16fc..9ed33d7 100644 --- a/.github/workflows/gpu_ci_trigger.yml +++ b/.github/workflows/gpu_ci_trigger.yml @@ -21,9 +21,9 @@ jobs: with: ssh-private-key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} - - name: Push to GitLab via SSH + - name: Push to GitLab via SSH & Provide Link run: | - # 1. Setup SSH known hosts to prevent interactive prompts + # 1. Setup SSH known hosts mkdir -p ~/.ssh ssh-keyscan gitlab.mpcdf.mpg.de >> ~/.ssh/known_hosts @@ -34,41 +34,16 @@ jobs: TARGET_BRANCH="${{ github.ref_name }}" fi - echo "Pushing to GitLab branch: $TARGET_BRANCH" - # 3. Add GitLab SSH remote git remote add gitlab git@gitlab.mpcdf.mpg.de:maxlin/cunumpy.git - # 4. Force push + # 4. Force push (This automatically starts the GitLab Pipeline) git push -f gitlab HEAD:refs/heads/$TARGET_BRANCH - - # Store branch for next step - echo "TARGET_BRANCH=$TARGET_BRANCH" >> $GITHUB_ENV - - name: Trigger GitLab Pipeline & Provide Link - run: | - # Give GitLab a moment to index the newly pushed branch - sleep 10 - - # Trigger the pipeline and capture the JSON response - # We use -w to get the HTTP status code as well - RESPONSE_DATA=$(curl --silent --show-error --write-out "\n%{http_code}" --request POST \ - --form token=${{ secrets.GITLAB_TRIGGER_TOKEN }} \ - --form "ref=$TARGET_BRANCH" \ - "https://gitlab.mpcdf.mpg.de/api/v4/projects/${{ secrets.GITLAB_PROJECT_ID }}/trigger/pipeline") - - HTTP_STATUS=$(echo "$RESPONSE_DATA" | tail -n1) - RESPONSE_BODY=$(echo "$RESPONSE_DATA" | sed '$d') - - if [ "$HTTP_STATUS" != "201" ]; then - echo "::error::GitLab API returned $HTTP_STATUS" - echo "::error::Response: $RESPONSE_BODY" - echo "::error::Check if GITLAB_PROJECT_ID (${{ secrets.GITLAB_PROJECT_ID }}) and GITLAB_TRIGGER_TOKEN are correct." - exit 1 - fi - - # Extract the pipeline web_url - PIPELINE_URL=$(echo $RESPONSE_BODY | python3 -c "import sys, json; print(json.load(sys.stdin).get('web_url', ''))") - - echo "::notice::GitLab GPU CI Pipeline triggered successfully!" + # 5. Provide the direct link + # We construct the URL manually since the push triggers the pipeline automatically + PIPELINE_URL="https://gitlab.mpcdf.mpg.de/maxlin/cunumpy/-/pipelines?ref=$TARGET_BRANCH" + + echo "::notice::GitLab GPU CI Pipeline started automatically via Push!" echo "::notice::View Pipeline: $PIPELINE_URL" + From dbfa33a5a24700a500cf0096e99f3ae5d210740d Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 10:43:11 +0200 Subject: [PATCH 18/19] specify backend in tests --- tests/unit/test_cupy.py | 26 ++++++++++++++----- tests/unit/test_numpy.py | 55 ++++++++++++++++++++-------------------- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/tests/unit/test_cupy.py b/tests/unit/test_cupy.py index f1d2afb..b0cc42f 100644 --- a/tests/unit/test_cupy.py +++ b/tests/unit/test_cupy.py @@ -8,9 +8,10 @@ def test_to_cupy_available(): except ImportError: pytest.skip("CuPy not installed") - arr = np.array([1, 2, 3]) - arr_cp = xp.to_cupy(arr) - assert isinstance(arr_cp, cp.ndarray) + with xp.use_backend("cupy"): + arr = np.array([1, 2, 3]) + arr_cp = xp.to_cupy(arr) + assert isinstance(arr_cp, cp.ndarray) def test_to_cupy_not_available(): try: @@ -19,9 +20,10 @@ def test_to_cupy_not_available(): except ImportError: pass - arr = np.array([1, 2, 3]) - with pytest.raises(ImportError): - xp.to_cupy(arr) + with xp.use_backend("cupy"): + arr = np.array([1, 2, 3]) + with pytest.raises(ImportError): + xp.to_cupy(arr) def test_synchronize(): # Should not crash on any backend @@ -32,3 +34,15 @@ def test_synchronize(): with xp.use_backend("cupy"): xp.synchronize() + +def test_xp_array_cupy(): + try: + import cupy as cp + except ImportError: + pytest.skip("CuPy not installed") + + with xp.use_backend("cupy"): + arr = xp.array([1, 2]) + arr *= 2 + assert isinstance(arr, cp.ndarray) + assert cp.asnumpy(arr).tolist() == [2, 4] diff --git a/tests/unit/test_numpy.py b/tests/unit/test_numpy.py index bd481ee..5572eda 100644 --- a/tests/unit/test_numpy.py +++ b/tests/unit/test_numpy.py @@ -3,10 +3,11 @@ import cunumpy as xp def test_xp_array(): - arr = xp.array([1, 2]) - arr *= 2 - assert isinstance(arr, np.ndarray) - assert np.array_equal(arr, [2, 4]) + with xp.use_backend("numpy"): + arr = xp.array([1, 2]) + arr *= 2 + assert isinstance(arr, np.ndarray) + assert np.array_equal(arr, [2, 4]) def test_numpy_symbols_accessible(): """All public numpy symbols must be reachable via cunumpy. @@ -14,27 +15,25 @@ def test_numpy_symbols_accessible(): This validates the runtime behaviour that the stub file (__init__.pyi) declares to Pylance so that `xp.` shows numpy completions in VS Code. """ - if xp.cupy_backend: - pytest.skip("CuPy does not have 100% symbol parity with NumPy.") - - # Exclude our custom methods from the numpy check - custom_methods = [ - "to_numpy", - "to_cupy", - "to_cunumpy", - "get_backend", - "is_gpu", - "is_cpu", - "use_backend", - "set_backend", - "synchronize", - "numpy_backend", - "cupy_backend", - "xp", - ] - missing = [ - name - for name in np.__all__ - if not hasattr(xp, name) and name not in custom_methods - ] - assert missing == [], f"Symbols not accessible via cunumpy: {missing}" + with xp.use_backend("numpy"): + # Exclude our custom methods from the numpy check + custom_methods = [ + "to_numpy", + "to_cupy", + "to_cunumpy", + "get_backend", + "is_gpu", + "is_cpu", + "use_backend", + "set_backend", + "synchronize", + "numpy_backend", + "cupy_backend", + "xp", + ] + missing = [ + name + for name in np.__all__ + if not hasattr(xp, name) and name not in custom_methods + ] + assert missing == [], f"Symbols not accessible via cunumpy: {missing}" From 89769ee8812dca594a48f7fc990387d563c72c60 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 10:43:23 +0200 Subject: [PATCH 19/19] formatting --- tests/unit/test_cunumpy.py | 7 +++++++ tests/unit/test_cupy.py | 16 +++++++++++----- tests/unit/test_numpy.py | 3 +++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_cunumpy.py b/tests/unit/test_cunumpy.py index e2b0302..8f07ed5 100644 --- a/tests/unit/test_cunumpy.py +++ b/tests/unit/test_cunumpy.py @@ -1,7 +1,9 @@ import numpy as np import pytest + import cunumpy as xp + def test_to_numpy(): arr = xp.array([1, 2, 3]) # Even if it's already numpy, to_numpy should work @@ -9,18 +11,21 @@ def test_to_numpy(): assert isinstance(arr_np, np.ndarray) assert np.array_equal(arr_np, [1, 2, 3]) + def test_to_cunumpy(): arr = np.array([1, 2, 3]) arr_xp = xp.to_cunumpy(arr) # Backend is numpy in tests usually assert isinstance(arr_xp, (np.ndarray, xp.ndarray)) + def test_get_backend_and_is_gpu_cpu(): arr = np.array([1, 2, 3]) assert xp.get_backend(arr) == "numpy" assert xp.is_gpu(arr) is False assert xp.is_cpu(arr) is True + def test_use_backend(): # Initial backend should be numpy (default) in this test environment assert "numpy" in xp.xp.__name__ @@ -32,6 +37,7 @@ def test_use_backend(): assert "numpy" in xp.xp.__name__ + def test_set_backend(): # Set to numpy xp.set_backend("numpy") @@ -45,6 +51,7 @@ def test_set_backend(): arr2 = xp.array([2]) assert arr2 is not None + def test_backend_bools(): with xp.use_backend("numpy"): assert xp.numpy_backend is True diff --git a/tests/unit/test_cupy.py b/tests/unit/test_cupy.py index b0cc42f..935a27a 100644 --- a/tests/unit/test_cupy.py +++ b/tests/unit/test_cupy.py @@ -1,21 +1,25 @@ +import numpy as np import pytest + import cunumpy as xp -import numpy as np + def test_to_cupy_available(): try: import cupy as cp except ImportError: pytest.skip("CuPy not installed") - + with xp.use_backend("cupy"): arr = np.array([1, 2, 3]) arr_cp = xp.to_cupy(arr) assert isinstance(arr_cp, cp.ndarray) + def test_to_cupy_not_available(): try: import cupy + pytest.skip("CuPy is installed, cannot test missing cupy error") except ImportError: pass @@ -25,22 +29,24 @@ def test_to_cupy_not_available(): with pytest.raises(ImportError): xp.to_cupy(arr) + def test_synchronize(): # Should not crash on any backend xp.synchronize() - + with xp.use_backend("numpy"): xp.synchronize() - + with xp.use_backend("cupy"): xp.synchronize() + def test_xp_array_cupy(): try: import cupy as cp except ImportError: pytest.skip("CuPy not installed") - + with xp.use_backend("cupy"): arr = xp.array([1, 2]) arr *= 2 diff --git a/tests/unit/test_numpy.py b/tests/unit/test_numpy.py index 5572eda..30f00d7 100644 --- a/tests/unit/test_numpy.py +++ b/tests/unit/test_numpy.py @@ -1,7 +1,9 @@ import numpy as np import pytest + import cunumpy as xp + def test_xp_array(): with xp.use_backend("numpy"): arr = xp.array([1, 2]) @@ -9,6 +11,7 @@ def test_xp_array(): assert isinstance(arr, np.ndarray) assert np.array_equal(arr, [2, 4]) + def test_numpy_symbols_accessible(): """All public numpy symbols must be reachable via cunumpy.