Skip to content

Commit e7be4f5

Browse files
committed
feat(ci): add selective test execution for PR builds (#861)
Add component-level test filtering to CI scripts so that pull requests only run tests relevant to the changed files (routing, LP, or MIP). Nightly builds continue to run all tests. - New: ci/detect_test_components.sh — detects changed components from the PR diff and exports CUOPT_TEST_COMPONENTS - Modified: run_ctests.sh — added should_run_test() to filter gtests by component - Modified: run_cuopt_pytests.sh — select pytest directories by component - Modified: test_cpp.sh, test_python.sh, test_wheel_cuopt.sh — conditional dataset downloads based on detected components Closes #861
1 parent fcfd4f0 commit e7be4f5

6 files changed

Lines changed: 281 additions & 23 deletions

File tree

ci/detect_test_components.sh

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#!/bin/bash
2+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
# Helper script to detect which test components should run based on changed files.
6+
# Source this script to set the CUOPT_TEST_COMPONENTS variable.
7+
#
8+
# Usage: source ci/detect_test_components.sh
9+
#
10+
# Sets CUOPT_TEST_COMPONENTS to a comma-separated list of components (routing,lp,mip)
11+
# or "all" if broad/shared changes are detected or if not in a pull-request build.
12+
13+
set -euo pipefail
14+
15+
detect_test_components() {
16+
# If CUOPT_TEST_COMPONENTS is already set externally, respect it
17+
if [[ -n "${CUOPT_TEST_COMPONENTS:-}" ]]; then
18+
return
19+
fi
20+
21+
# Only apply selective testing for pull-request builds
22+
if [[ "${RAPIDS_BUILD_TYPE:-}" != "pull-request" ]]; then
23+
export CUOPT_TEST_COMPONENTS="all"
24+
return
25+
fi
26+
27+
# In RAPIDS CI pull-request builds, the branch is a merge commit so
28+
# HEAD~1..HEAD gives the full PR diff. If the parent isn't reachable
29+
# (e.g. shallow clone or non-merge workflow), fall back to running all.
30+
local changed_files
31+
if ! git rev-parse --verify HEAD~1 &>/dev/null; then
32+
export CUOPT_TEST_COMPONENTS="all"
33+
return
34+
fi
35+
if ! changed_files=$(git diff --name-only HEAD~1..HEAD 2>/dev/null); then
36+
# If git diff fails, run all tests to be safe
37+
export CUOPT_TEST_COMPONENTS="all"
38+
return
39+
fi
40+
41+
if [[ -z "${changed_files}" ]]; then
42+
export CUOPT_TEST_COMPONENTS="all"
43+
return
44+
fi
45+
46+
local components=""
47+
local run_all=false
48+
49+
# Check for shared/infrastructure changes that should trigger all tests
50+
while IFS= read -r file; do
51+
case "${file}" in
52+
cpp/include/*|cpp/cmake/*|cpp/CMakeLists.txt|cpp/src/utilities/*)
53+
run_all=true
54+
break
55+
;;
56+
# Changes to test infrastructure
57+
cpp/tests/CMakeLists.txt|cpp/tests/utilities/*)
58+
run_all=true
59+
break
60+
;;
61+
# Changes to CI infrastructure
62+
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)
63+
run_all=true
64+
break
65+
;;
66+
# Changes to conda/build config
67+
conda/*|dependencies.yaml)
68+
run_all=true
69+
break
70+
;;
71+
esac
72+
done <<< "${changed_files}"
73+
74+
if ${run_all}; then
75+
export CUOPT_TEST_COMPONENTS="all"
76+
return
77+
fi
78+
79+
# Detect individual components
80+
local has_routing=false
81+
local has_lp=false
82+
local has_mip=false
83+
84+
while IFS= read -r file; do
85+
case "${file}" in
86+
# Routing component
87+
cpp/src/routing/*|cpp/src/distance/*|cpp/tests/routing/*|cpp/tests/distance_engine/*|cpp/tests/examples/routing/*)
88+
has_routing=true
89+
;;
90+
python/cuopt/cuopt/routing/*|python/cuopt/cuopt/tests/routing/*)
91+
has_routing=true
92+
;;
93+
regression/routing*)
94+
has_routing=true
95+
;;
96+
# LP component
97+
cpp/src/dual_simplex/*|cpp/src/barrier/*|cpp/src/pdlp/*|cpp/src/math_optimization/*)
98+
has_lp=true
99+
;;
100+
cpp/tests/linear_programming/*|cpp/tests/dual_simplex/*|cpp/tests/qp/*)
101+
has_lp=true
102+
;;
103+
python/cuopt/cuopt/tests/linear_programming/*|python/cuopt/cuopt/tests/quadratic_programming/*)
104+
has_lp=true
105+
;;
106+
# LP regression
107+
regression/lp*)
108+
has_lp=true
109+
;;
110+
# MIP component
111+
cpp/src/branch_and_bound/*|cpp/src/cuts/*|cpp/src/mip_heuristics/*)
112+
has_mip=true
113+
;;
114+
cpp/tests/mip/*)
115+
has_mip=true
116+
;;
117+
regression/mip*)
118+
has_mip=true
119+
;;
120+
# Python source changes that could affect any component
121+
python/cuopt/cuopt/*.py|python/cuopt/cuopt/distance_engine/*|python/cuopt/cuopt/utils/*)
122+
has_routing=true
123+
has_lp=true
124+
;;
125+
python/libcuopt/*)
126+
has_routing=true
127+
has_lp=true
128+
has_mip=true
129+
;;
130+
esac
131+
done <<< "${changed_files}"
132+
133+
# Build the components string
134+
if ${has_routing}; then
135+
components="${components:+${components},}routing"
136+
fi
137+
if ${has_lp}; then
138+
components="${components:+${components},}lp"
139+
fi
140+
if ${has_mip}; then
141+
components="${components:+${components},}mip"
142+
fi
143+
144+
# If no specific component was detected, default to all
145+
if [[ -z "${components}" ]]; then
146+
components="all"
147+
fi
148+
149+
export CUOPT_TEST_COMPONENTS="${components}"
150+
}
151+
152+
detect_test_components
153+
echo "CUOPT_TEST_COMPONENTS=${CUOPT_TEST_COMPONENTS}"

ci/run_ctests.sh

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,44 @@
44

55
set -euo pipefail
66

7+
# Determine which test binary belongs to which component
8+
should_run_test() {
9+
local test_name="$1"
10+
local components="${CUOPT_TEST_COMPONENTS:-all}"
11+
12+
# If all components should run, return true
13+
if [[ "${components}" == "all" ]]; then
14+
return 0
15+
fi
16+
17+
# Map test binary names to components
18+
case "${test_name}" in
19+
ROUTING_*|WAYPOINT_*|VEHICLE_*|OBJECTIVE_*|RETAIL_*)
20+
[[ "${components}" == *"routing"* ]] && return 0
21+
;;
22+
LP_*|PDLP_*|C_API_*|DUAL_SIMPLEX_*|QP_*)
23+
[[ "${components}" == *"lp"* ]] && return 0
24+
;;
25+
MIP_*|PROBLEM_*|ELIM_*|STANDARDIZATION_*|MULTI_PROBE_*|INCUMBENT_*|DOC_EXAMPLE_*|CUTS_*|EMPTY_*|DETERMINISM_*)
26+
[[ "${components}" == *"mip"* ]] && return 0
27+
;;
28+
# UNIT_TEST and PRESOLVE_TEST can belong to LP or MIP
29+
UNIT_TEST|PRESOLVE_TEST)
30+
[[ "${components}" == *"mip"* || "${components}" == *"lp"* ]] && return 0
31+
;;
32+
# CLI_TEST is a general utility test, run if any component is selected
33+
CLI_TEST)
34+
return 0
35+
;;
36+
# Unknown tests: run them to be safe
37+
*)
38+
return 0
39+
;;
40+
esac
41+
42+
return 1
43+
}
44+
745
# Support customizing the gtests' install location
846
# First, try the installed location (CI/conda environments)
947
installed_test_location="${INSTALL_PREFIX:-${CONDA_PREFIX:-/usr}}/bin/gtests/libcuopt/"
@@ -23,6 +61,11 @@ fi
2361

2462
for gt in "${GTEST_DIR}"/*_TEST; do
2563
test_name=$(basename "${gt}")
26-
echo "Running gtest ${test_name}"
27-
"${gt}" "$@"
64+
if should_run_test "${test_name}"; then
65+
echo "Running gtest ${test_name}"
66+
"${gt}" "$@"
67+
else
68+
echo "Skipping gtest ${test_name} (not in CUOPT_TEST_COMPONENTS=${CUOPT_TEST_COMPONENTS:-all})"
69+
fi
2870
done
71+

ci/run_cuopt_pytests.sh

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,30 @@ set -euo pipefail
77
# It is essential to cd into python/cuopt/cuopt as `pytest-xdist` + `coverage` seem to work only at this directory level.
88

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

12-
pytest -s --cache-clear "$@" tests
12+
# Build the list of test directories based on CUOPT_TEST_COMPONENTS
13+
COMPONENTS="${CUOPT_TEST_COMPONENTS:-all}"
14+
TEST_DIRS=""
15+
16+
if [[ "${COMPONENTS}" == "all" ]]; then
17+
TEST_DIRS="tests"
18+
else
19+
if [[ "${COMPONENTS}" == *"routing"* ]]; then
20+
TEST_DIRS="${TEST_DIRS} tests/routing"
21+
fi
22+
if [[ "${COMPONENTS}" == *"lp"* ]]; then
23+
TEST_DIRS="${TEST_DIRS} tests/linear_programming tests/quadratic_programming"
24+
fi
25+
# MIP does not have separate Python tests (tested through LP tests)
26+
27+
# If no Python test dirs matched, skip
28+
if [[ -z "${TEST_DIRS}" ]]; then
29+
echo "No Python test directories match CUOPT_TEST_COMPONENTS=${COMPONENTS}, skipping."
30+
exit 0
31+
fi
32+
fi
33+
34+
echo "Running pytest on: ${TEST_DIRS} (CUOPT_TEST_COMPONENTS=${COMPONENTS})"
35+
# shellcheck disable=SC2086
36+
pytest -s --cache-clear "$@" ${TEST_DIRS}

ci/test_cpp.sh

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,27 @@ rapids-print-env
3434
rapids-logger "Check GPU usage"
3535
nvidia-smi
3636

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

40+
rapids-logger "Download datasets"
4141
RAPIDS_DATASET_ROOT_DIR="$(realpath datasets)"
4242
export RAPIDS_DATASET_ROOT_DIR
43-
pushd "${RAPIDS_DATASET_ROOT_DIR}"
44-
./get_test_data.sh
45-
popd
43+
44+
if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"lp"* || "${CUOPT_TEST_COMPONENTS}" == *"mip"* ]]; then
45+
./datasets/linear_programming/download_pdlp_test_dataset.sh
46+
./datasets/mip/download_miplib_test_dataset.sh
47+
else
48+
rapids-logger "Skipping LP/MIP dataset downloads (not needed for components: ${CUOPT_TEST_COMPONENTS})"
49+
fi
50+
51+
if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"routing"* ]]; then
52+
pushd "${RAPIDS_DATASET_ROOT_DIR}"
53+
./get_test_data.sh
54+
popd
55+
else
56+
rapids-logger "Skipping routing dataset downloads (not needed for components: ${CUOPT_TEST_COMPONENTS})"
57+
fi
4658

4759
EXITCODE=0
4860
trap "EXITCODE=1" ERR
@@ -51,7 +63,7 @@ set +e
5163
# Run gtests from libcuopt-tests package
5264
export GTEST_OUTPUT=xml:${RAPIDS_TESTS_DIR}/
5365

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

5769
rapids-logger "Test script exiting with value: $EXITCODE"

ci/test_python.sh

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,27 @@ mkdir -p "${RAPIDS_TESTS_DIR}" "${RAPIDS_COVERAGE_DIR}"
3535

3636
rapids-print-env
3737

38+
# Detect which test components to run
39+
source ./ci/detect_test_components.sh
40+
3841
rapids-logger "Download datasets"
3942
RAPIDS_DATASET_ROOT_DIR="$(realpath datasets)"
4043
export RAPIDS_DATASET_ROOT_DIR
41-
./datasets/linear_programming/download_pdlp_test_dataset.sh
42-
./datasets/mip/download_miplib_test_dataset.sh
43-
pushd "${RAPIDS_DATASET_ROOT_DIR}"
44-
./get_test_data.sh
45-
popd
44+
45+
if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"lp"* || "${CUOPT_TEST_COMPONENTS}" == *"mip"* ]]; then
46+
./datasets/linear_programming/download_pdlp_test_dataset.sh
47+
./datasets/mip/download_miplib_test_dataset.sh
48+
else
49+
rapids-logger "Skipping LP/MIP dataset downloads (not needed for components: ${CUOPT_TEST_COMPONENTS})"
50+
fi
51+
52+
if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"routing"* ]]; then
53+
pushd "${RAPIDS_DATASET_ROOT_DIR}"
54+
./get_test_data.sh
55+
popd
56+
else
57+
rapids-logger "Skipping routing dataset downloads (not needed for components: ${CUOPT_TEST_COMPONENTS})"
58+
fi
4659

4760
rapids-logger "Check GPU usage"
4861
nvidia-smi
@@ -57,7 +70,7 @@ export OMP_NUM_THREADS=1
5770
rapids-logger "Test cuopt_cli"
5871
timeout 10m bash ./python/libcuopt/libcuopt/tests/test_cli.sh
5972

60-
rapids-logger "pytest cuopt"
73+
rapids-logger "pytest cuopt (components: ${CUOPT_TEST_COMPONENTS})"
6174
timeout 30m ./ci/run_cuopt_pytests.sh \
6275
--junitxml="${RAPIDS_TESTS_DIR}/junit-cuopt.xml" \
6376
--cov-config=.coveragerc \

ci/test_wheel_cuopt.sh

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,28 @@ elif command -v dnf &> /dev/null; then
4343
dnf -y install file unzip
4444
fi
4545

46-
./datasets/linear_programming/download_pdlp_test_dataset.sh
47-
./datasets/mip/download_miplib_test_dataset.sh
48-
cd ./datasets
49-
./get_test_data.sh --solomon
50-
./get_test_data.sh --tsp
51-
cd -
46+
# Detect which test components to run
47+
source ./ci/detect_test_components.sh
5248

5349
RAPIDS_DATASET_ROOT_DIR="$(realpath datasets)"
5450
export RAPIDS_DATASET_ROOT_DIR
5551

52+
if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"lp"* || "${CUOPT_TEST_COMPONENTS}" == *"mip"* ]]; then
53+
./datasets/linear_programming/download_pdlp_test_dataset.sh
54+
./datasets/mip/download_miplib_test_dataset.sh
55+
else
56+
echo "Skipping LP/MIP dataset downloads (not needed for components: ${CUOPT_TEST_COMPONENTS})"
57+
fi
58+
59+
if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"routing"* ]]; then
60+
cd ./datasets
61+
./get_test_data.sh --solomon
62+
./get_test_data.sh --tsp
63+
cd -
64+
else
65+
echo "Skipping routing dataset downloads (not needed for components: ${CUOPT_TEST_COMPONENTS})"
66+
fi
67+
5668
# Run CLI tests
5769
timeout 10m bash ./python/libcuopt/libcuopt/tests/test_cli.sh
5870

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

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

6679
# run thirdparty integration tests for only nightly builds

0 commit comments

Comments
 (0)