Skip to content

Commit b93bd54

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 b93bd54

6 files changed

Lines changed: 297 additions & 23 deletions

File tree

ci/detect_test_components.sh

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

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

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

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

ci/test_python.sh

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,32 @@ 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"* ]]; then
46+
./datasets/linear_programming/download_pdlp_test_dataset.sh
47+
else
48+
rapids-logger "Skipping LP dataset download (not needed for components: ${CUOPT_TEST_COMPONENTS})"
49+
fi
50+
51+
if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"mip"* ]]; then
52+
./datasets/mip/download_miplib_test_dataset.sh
53+
else
54+
rapids-logger "Skipping MIP dataset download (not needed for components: ${CUOPT_TEST_COMPONENTS})"
55+
fi
56+
57+
if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"routing"* ]]; then
58+
pushd "${RAPIDS_DATASET_ROOT_DIR}"
59+
./get_test_data.sh
60+
popd
61+
else
62+
rapids-logger "Skipping routing dataset downloads (not needed for components: ${CUOPT_TEST_COMPONENTS})"
63+
fi
4664

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

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

ci/test_wheel_cuopt.sh

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,33 @@ 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"* ]]; then
53+
./datasets/linear_programming/download_pdlp_test_dataset.sh
54+
else
55+
echo "Skipping LP dataset download (not needed for components: ${CUOPT_TEST_COMPONENTS})"
56+
fi
57+
58+
if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"mip"* ]]; then
59+
./datasets/mip/download_miplib_test_dataset.sh
60+
else
61+
echo "Skipping MIP dataset download (not needed for components: ${CUOPT_TEST_COMPONENTS})"
62+
fi
63+
64+
if [[ "${CUOPT_TEST_COMPONENTS}" == "all" || "${CUOPT_TEST_COMPONENTS}" == *"routing"* ]]; then
65+
cd ./datasets
66+
./get_test_data.sh --solomon
67+
./get_test_data.sh --tsp
68+
cd -
69+
else
70+
echo "Skipping routing dataset downloads (not needed for components: ${CUOPT_TEST_COMPONENTS})"
71+
fi
72+
5673
# Run CLI tests
5774
timeout 10m bash ./python/libcuopt/libcuopt/tests/test_cli.sh
5875

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

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

6684
# run thirdparty integration tests for only nightly builds

0 commit comments

Comments
 (0)