Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions ci/detect_test_components.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#!/bin/bash
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Helper script to detect which test components should run based on changed files.
# Source this script to set the CUOPT_TEST_COMPONENTS variable.
#
# Usage: source ci/detect_test_components.sh
#
# Sets CUOPT_TEST_COMPONENTS to a comma-separated list of components (routing,lp,mip)
# or "all" if broad/shared changes are detected or if not in a pull-request build.

set -euo pipefail

detect_test_components() {
# If CUOPT_TEST_COMPONENTS is already set externally, respect it
if [[ -n "${CUOPT_TEST_COMPONENTS:-}" ]]; then
export CUOPT_TEST_COMPONENTS
return
fi

# Only apply selective testing for pull-request builds
if [[ "${RAPIDS_BUILD_TYPE:-}" != "pull-request" ]]; then
export CUOPT_TEST_COMPONENTS="all"
return
fi

# In RAPIDS CI pull-request builds, the branch is a merge commit so
# HEAD~1..HEAD gives the full PR diff. If the parent isn't reachable
# (e.g. shallow clone or non-merge workflow), fall back to running all.
local changed_files
if ! git rev-parse --verify HEAD~1 &>/dev/null; then
export CUOPT_TEST_COMPONENTS="all"
return
fi
if ! changed_files=$(git diff --name-only HEAD~1..HEAD 2>/dev/null); then
Comment on lines +28 to +36
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not true. copy-pr-bot copies commits from the PR branch to a branch on the repo called pull-request/{pr-number} and there HEAD is exactly HEAD of the PR branch.

It isn't like in other open-source projects on GitHub where HEAD is a merge commit created by GitHub that merges the PR branch into the target branch.

That is why in the RAPIDS changed-files action, we do some work to figure out what the target branch of the PR was:

https://github.com/rapidsai/shared-actions/blob/f722a09c99e883ccca580bbb7135945d8efb90e0/changed-files/action.yml#L40-L45

I think this check HEAD~1..HEAD is only going to compare between the difference between the latest and previous-to-that commit on the PR branch, which is not what you want.

You may want to look into something like step-security/changed-files, which the existing RAPIDS changed-files action uses:

It produces an output listing all the files that changed: https://github.com/step-security/changed-files?tab=readme-ov-file#outputs-

# If git diff fails, run all tests to be safe
Comment on lines +28 to +37
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the full script context to understand the code better
cd /tmp && git ls-files | head -20

Repository: NVIDIA/cuopt

Length of output: 124


🏁 Script executed:

# Find and examine the script
find . -name "detect_test_components.sh" -type f 2>/dev/null

Repository: NVIDIA/cuopt

Length of output: 86


🏁 Script executed:

# Check GitHub Actions workflows
find .github/workflows -name "*.yml" -o -name "*.yaml" 2>/dev/null | head -20

Repository: NVIDIA/cuopt

Length of output: 505


🏁 Script executed:

# Read the full detect_test_components.sh script
cat -n ci/detect_test_components.sh

Repository: NVIDIA/cuopt

Length of output: 6237


🏁 Script executed:

# Check the PR workflow configuration
cat -n .github/workflows/pr.yaml

Repository: NVIDIA/cuopt

Length of output: 20632


🏁 Script executed:

# Also check test.yaml
cat -n .github/workflows/test.yaml

Repository: NVIDIA/cuopt

Length of output: 4785


🏁 Script executed:

# Look for any documentation about how RAPIDS CI handles PR checkouts
find . -name "*.md" -o -name "*.txt" | xargs grep -l "RAPIDS_BUILD_TYPE\|pull-request" 2>/dev/null | head -5

Repository: NVIDIA/cuopt

Length of output: 73


🏁 Script executed:

# Check if there are any comments/docs in the script about the merge commit assumption
grep -n "merge\|parent\|HEAD~1" ci/detect_test_components.sh

Repository: NVIDIA/cuopt

Length of output: 427


🏁 Script executed:

# Search for any references to the merge commit handling in other CI scripts
grep -r "HEAD~1\|merge.*commit" ci/ --include="*.sh" 2>/dev/null | head -10

Repository: NVIDIA/cuopt

Length of output: 452


Add a merge-commit check before using HEAD~1..HEAD for PR diff.

The current check at line 32 only verifies that HEAD~1 exists, but doesn't guarantee HEAD is a merge commit. On a single-parent commit, HEAD~1..HEAD shows only that commit, not the full PR diff—potentially under-selecting tests. The code comment already acknowledges non-merge workflows as a fallback case, so add the verification:

Proposed fix
    if ! git rev-parse --verify HEAD~1 &>/dev/null; then
        export CUOPT_TEST_COMPONENTS="all"
        return
    fi
+   # Require merge-commit context for HEAD~1..HEAD PR diff assumption.
+   # `git rev-list --parents -n 1 HEAD` returns: <sha> <parent1> [parent2...]
+   if [[ $(git rev-list --parents -n 1 HEAD | awk '{print NF}') -lt 3 ]]; then
+       export CUOPT_TEST_COMPONENTS="all"
+       return
+   fi
    if ! changed_files=$(git diff --name-only HEAD~1..HEAD 2>/dev/null); then
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ci/detect_test_components.sh` around lines 28 - 37, The script currently only
verifies HEAD~1 exists before using git diff HEAD~1..HEAD, which can
under-select tests on non-merge commits; update the pre-check to also verify
that HEAD is a merge commit (has at least two parents) before using
HEAD~1..HEAD. In the block that checks git rev-parse --verify HEAD~1 and before
running git diff, add a check (e.g., test for HEAD^2 via git rev-parse --verify
HEAD^2 or inspect parents) and if that check fails set
CUOPT_TEST_COMPONENTS="all" and return; keep the existing changed_files variable
and subsequent git diff HEAD~1..HEAD logic unchanged otherwise.

export CUOPT_TEST_COMPONENTS="all"
return
fi

if [[ -z "${changed_files}" ]]; then
export CUOPT_TEST_COMPONENTS="all"
return
fi

local components=""
local run_all=false

# Check for shared/infrastructure changes that should trigger all tests
while IFS= read -r file; do
case "${file}" in
cpp/include/*|cpp/cmake/*|cpp/CMakeLists.txt|cpp/src/utilities/*)
run_all=true
break
;;
# Changes to test infrastructure
cpp/tests/CMakeLists.txt|cpp/tests/utilities/*)
run_all=true
break
;;
# Changes to CI infrastructure
ci/test_cpp.sh|ci/test_python.sh|ci/test_wheel_cuopt.sh|ci/run_ctests.sh|ci/run_cuopt_pytests.sh|ci/detect_test_components.sh)
run_all=true
break
;;
# Changes to conda/build config
conda/*|dependencies.yaml)
run_all=true
break
;;
esac
done <<< "${changed_files}"

if ${run_all}; then
export CUOPT_TEST_COMPONENTS="all"
return
fi

# Detect individual components
local has_routing=false
local has_lp=false
local has_mip=false

while IFS= read -r file; do
case "${file}" in
# Routing component
cpp/src/routing/*|cpp/src/distance/*|cpp/tests/routing/*|cpp/tests/distance_engine/*|cpp/tests/examples/routing/*)
has_routing=true
;;
python/cuopt/cuopt/routing/*|python/cuopt/cuopt/tests/routing/*)
has_routing=true
;;
regression/routing*)
has_routing=true
;;
# LP component
cpp/src/dual_simplex/*|cpp/src/barrier/*|cpp/src/pdlp/*|cpp/src/math_optimization/*)
has_lp=true
;;
cpp/tests/linear_programming/*|cpp/tests/dual_simplex/*|cpp/tests/qp/*)
has_lp=true
;;
python/cuopt/cuopt/tests/linear_programming/*|python/cuopt/cuopt/tests/quadratic_programming/*)
has_lp=true
;;
# LP regression
regression/lp*)
has_lp=true
;;
# MIP component
cpp/src/branch_and_bound/*|cpp/src/cuts/*|cpp/src/mip_heuristics/*)
has_mip=true
;;
cpp/tests/mip/*)
has_mip=true
;;
regression/mip*)
has_mip=true
;;
# Python source changes that could affect any component
python/cuopt/cuopt/*.py|python/cuopt/cuopt/distance_engine/*|python/cuopt/cuopt/utils/*)
has_routing=true
has_lp=true
;;
python/libcuopt/*)
has_routing=true
has_lp=true
has_mip=true
;;
esac
done <<< "${changed_files}"

# Build the components string
if ${has_routing}; then
components="${components:+${components},}routing"
fi
if ${has_lp}; then
components="${components:+${components},}lp"
fi
if ${has_mip}; then
components="${components:+${components},}mip"
fi

# If no specific component was detected, default to all
if [[ -z "${components}" ]]; then
components="all"
fi

export CUOPT_TEST_COMPONENTS="${components}"
}

detect_test_components
echo "CUOPT_TEST_COMPONENTS=${CUOPT_TEST_COMPONENTS}"
47 changes: 45 additions & 2 deletions ci/run_ctests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,44 @@

set -euo pipefail

# Determine which test binary belongs to which component
should_run_test() {
local test_name="$1"
local components="${CUOPT_TEST_COMPONENTS:-all}"

# If all components should run, return true
if [[ "${components}" == "all" ]]; then
return 0
fi

# Map test binary names to components
case "${test_name}" in
ROUTING_*|WAYPOINT_*|VEHICLE_*|OBJECTIVE_*|RETAIL_*)
[[ "${components}" == *"routing"* ]] && return 0
;;
LP_*|PDLP_*|C_API_*|DUAL_SIMPLEX_*|QP_*)
[[ "${components}" == *"lp"* ]] && return 0
;;
MIP_*|PROBLEM_*|ELIM_*|STANDARDIZATION_*|MULTI_PROBE_*|INCUMBENT_*|DOC_EXAMPLE_*|CUTS_*|EMPTY_*|DETERMINISM_*)
[[ "${components}" == *"mip"* ]] && return 0
;;
# UNIT_TEST and PRESOLVE_TEST can belong to LP or MIP
UNIT_TEST|PRESOLVE_TEST)
[[ "${components}" == *"mip"* || "${components}" == *"lp"* ]] && return 0
;;
# CLI_TEST is a general utility test, run if any component is selected
CLI_TEST)
return 0
;;
# Unknown tests: run them to be safe
*)
return 0
;;
esac

return 1
}

# Support customizing the gtests' install location
# First, try the installed location (CI/conda environments)
installed_test_location="${INSTALL_PREFIX:-${CONDA_PREFIX:-/usr}}/bin/gtests/libcuopt/"
Expand All @@ -23,6 +61,11 @@ fi

for gt in "${GTEST_DIR}"/*_TEST; do
test_name=$(basename "${gt}")
echo "Running gtest ${test_name}"
"${gt}" "$@"
if should_run_test "${test_name}"; then
echo "Running gtest ${test_name}"
"${gt}" "$@"
else
echo "Skipping gtest ${test_name} (not in CUOPT_TEST_COMPONENTS=${CUOPT_TEST_COMPONENTS:-all})"
fi
done

28 changes: 26 additions & 2 deletions ci/run_cuopt_pytests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ set -euo pipefail
# It is essential to cd into python/cuopt/cuopt as `pytest-xdist` + `coverage` seem to work only at this directory level.

# Support invoking run_cuopt_pytests.sh outside the script directory
cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")"/../python/cuopt/cuopt/
cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../python/cuopt/cuopt/"

pytest -s --cache-clear "$@" tests
# Build the list of test directories based on CUOPT_TEST_COMPONENTS
COMPONENTS="${CUOPT_TEST_COMPONENTS:-all}"
TEST_DIRS=""

if [[ "${COMPONENTS}" == "all" ]]; then
TEST_DIRS="tests"
else
if [[ "${COMPONENTS}" == *"routing"* ]]; then
TEST_DIRS="${TEST_DIRS} tests/routing"
fi
if [[ "${COMPONENTS}" == *"lp"* ]]; then
TEST_DIRS="${TEST_DIRS} tests/linear_programming tests/quadratic_programming"
fi
# MIP does not have separate Python tests (tested through LP tests)

# If no Python test dirs matched, skip
if [[ -z "${TEST_DIRS}" ]]; then
echo "No Python test directories match CUOPT_TEST_COMPONENTS=${COMPONENTS}, skipping."
exit 0
fi
fi

echo "Running pytest on: ${TEST_DIRS} (CUOPT_TEST_COMPONENTS=${COMPONENTS})"
# shellcheck disable=SC2086
pytest -s --cache-clear "$@" ${TEST_DIRS}
31 changes: 24 additions & 7 deletions ci/test_cpp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,32 @@ rapids-print-env
rapids-logger "Check GPU usage"
nvidia-smi

rapids-logger "Download datasets"
./datasets/linear_programming/download_pdlp_test_dataset.sh
./datasets/mip/download_miplib_test_dataset.sh
# Detect which test components to run
source ./ci/detect_test_components.sh

rapids-logger "Download datasets"
RAPIDS_DATASET_ROOT_DIR="$(realpath datasets)"
export RAPIDS_DATASET_ROOT_DIR
pushd "${RAPIDS_DATASET_ROOT_DIR}"
./get_test_data.sh
popd

if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"lp"* ]]; then
./datasets/linear_programming/download_pdlp_test_dataset.sh
else
rapids-logger "Skipping LP dataset download (not needed for components: ${CUOPT_TEST_COMPONENTS})"
fi

if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"mip"* ]]; then
./datasets/mip/download_miplib_test_dataset.sh
else
rapids-logger "Skipping MIP dataset download (not needed for components: ${CUOPT_TEST_COMPONENTS})"
fi

if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"routing"* ]]; then
pushd "${RAPIDS_DATASET_ROOT_DIR}"
./get_test_data.sh
popd
else
rapids-logger "Skipping routing dataset downloads (not needed for components: ${CUOPT_TEST_COMPONENTS})"
fi

EXITCODE=0
trap "EXITCODE=1" ERR
Expand All @@ -51,7 +68,7 @@ set +e
# Run gtests from libcuopt-tests package
export GTEST_OUTPUT=xml:${RAPIDS_TESTS_DIR}/

rapids-logger "Run gtests"
rapids-logger "Run gtests (components: ${CUOPT_TEST_COMPONENTS})"
timeout 40m ./ci/run_ctests.sh

rapids-logger "Test script exiting with value: $EXITCODE"
Expand Down
30 changes: 24 additions & 6 deletions ci/test_python.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,32 @@ mkdir -p "${RAPIDS_TESTS_DIR}" "${RAPIDS_COVERAGE_DIR}"

rapids-print-env

# Detect which test components to run
source ./ci/detect_test_components.sh

rapids-logger "Download datasets"
RAPIDS_DATASET_ROOT_DIR="$(realpath datasets)"
export RAPIDS_DATASET_ROOT_DIR
./datasets/linear_programming/download_pdlp_test_dataset.sh
./datasets/mip/download_miplib_test_dataset.sh
pushd "${RAPIDS_DATASET_ROOT_DIR}"
./get_test_data.sh
popd

if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"lp"* ]]; then
./datasets/linear_programming/download_pdlp_test_dataset.sh
else
rapids-logger "Skipping LP dataset download (not needed for components: ${CUOPT_TEST_COMPONENTS})"
fi

if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"mip"* ]]; then
./datasets/mip/download_miplib_test_dataset.sh
else
rapids-logger "Skipping MIP dataset download (not needed for components: ${CUOPT_TEST_COMPONENTS})"
fi

if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"routing"* ]]; then
pushd "${RAPIDS_DATASET_ROOT_DIR}"
./get_test_data.sh
popd
else
rapids-logger "Skipping routing dataset downloads (not needed for components: ${CUOPT_TEST_COMPONENTS})"
fi

rapids-logger "Check GPU usage"
nvidia-smi
Expand All @@ -57,7 +75,7 @@ export OMP_NUM_THREADS=1
rapids-logger "Test cuopt_cli"
timeout 10m bash ./python/libcuopt/libcuopt/tests/test_cli.sh

rapids-logger "pytest cuopt"
rapids-logger "pytest cuopt (components: ${CUOPT_TEST_COMPONENTS})"
timeout 30m ./ci/run_cuopt_pytests.sh \
--junitxml="${RAPIDS_TESTS_DIR}/junit-cuopt.xml" \
--cov-config=.coveragerc \
Expand Down
30 changes: 24 additions & 6 deletions ci/test_wheel_cuopt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,33 @@ elif command -v dnf &> /dev/null; then
dnf -y install file unzip
fi

./datasets/linear_programming/download_pdlp_test_dataset.sh
./datasets/mip/download_miplib_test_dataset.sh
cd ./datasets
./get_test_data.sh --solomon
./get_test_data.sh --tsp
cd -
# Detect which test components to run
source ./ci/detect_test_components.sh

RAPIDS_DATASET_ROOT_DIR="$(realpath datasets)"
export RAPIDS_DATASET_ROOT_DIR

if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"lp"* ]]; then
./datasets/linear_programming/download_pdlp_test_dataset.sh
else
echo "Skipping LP dataset download (not needed for components: ${CUOPT_TEST_COMPONENTS})"
fi

if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"mip"* ]]; then
./datasets/mip/download_miplib_test_dataset.sh
else
echo "Skipping MIP dataset download (not needed for components: ${CUOPT_TEST_COMPONENTS})"
fi

if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"routing"* ]]; then
cd ./datasets
./get_test_data.sh --solomon
./get_test_data.sh --tsp
cd -
else
echo "Skipping routing dataset downloads (not needed for components: ${CUOPT_TEST_COMPONENTS})"
fi

# Run CLI tests
timeout 10m bash ./python/libcuopt/libcuopt/tests/test_cli.sh

Expand All @@ -61,6 +78,7 @@ timeout 10m bash ./python/libcuopt/libcuopt/tests/test_cli.sh
# Due to race condition in certain cases UCX might not be able to cleanup properly, so we set the number of threads to 1
export OMP_NUM_THREADS=1

echo "Running cuopt pytests (components: ${CUOPT_TEST_COMPONENTS})"
timeout 30m ./ci/run_cuopt_pytests.sh --verbose --capture=no

# run thirdparty integration tests for only nightly builds
Expand Down