Skip to content

Commit 4260e06

Browse files
authored
build(cmake): honor BUILD_TESTING and run simg4ox via ctest (#378)
This branch aligns Simphony's testing setup with standard CMake/CTest behavior and moves the existing `simg4ox` integration check under `ctest`. The main goals are: - respect `BUILD_TESTING` in the project tree - make build-tree tests discoverable and runnable through `ctest` - ensure external drivers such as Spack can enable tests and invoke `ctest` in a conventional way - remove duplicated CI execution for `test_simg4ox.sh` ## Motivation Before this change, test subdirectories were added unconditionally, which made the project less friendly to standard CMake testing flows and to package-manager driven builds. In addition, `simg4ox` was validated via a standalone shell script instead of a registered CTest test. That meant CI and external tooling had to know about the script separately instead of relying on `ctest` as the single test entry point. ## What Changed ### Top-level CMake testing behavior - define `BUILD_TESTING` using the standard CMake pattern - default `BUILD_TESTING` to `PROJECT_IS_TOP_LEVEL` - continue to `include(CTest)` even when `BUILD_TESTING=OFF` - gate all `*/tests` subdirectories behind `if(BUILD_TESTING)` This gives the expected behavior in both cases: - top-level builds get tests by default - subproject/package-manager builds do not force this project's tests on unless explicitly requested Keeping `CTest` included even with `BUILD_TESTING=OFF` means external drivers can still run `ctest` and see an empty test set rather than missing test configuration. ### `simg4ox` CTest integration - add a dedicated top-level [tests/CMakeLists.txt](tests/CMakeLists.txt) to register `Integration.simg4ox` - run `simg4ox` from CTest using `$<TARGET_FILE:simg4ox>` - use a fixed CTest working directory created from CMake - keep the wrapper script minimal and focused on launching the executable and comparison step This turns the existing `tests/test_simg4ox.sh` flow into a normal CTest integration test instead of an external ad hoc command. ### A/B comparison script cleanup - move the old `tests/compare_ab.py` to [optiphy/ana/compare_ab.py](optiphy/ana/compare_ab.py) - remove the old `tests/compare_ab.py` - keep the script serving the same core purpose: compare A/B event outputs and validate the expected Geant4-version-dependent mismatch indices Placing it under `optiphy/ana` makes it easier to treat as a reusable analysis/helper script rather than a one-off test-local file. ### CI cleanup - remove the separate `tests/test_simg4ox.sh` invocation from `.github/workflows/build-pull-request.yaml` - rely on the existing `ctest --test-dir "$SIMPHONY_BUILD" --output-on-failure` step to run `Integration.simg4ox` This avoids running the same test twice in PR CI.
1 parent c83319d commit 4260e06

6 files changed

Lines changed: 147 additions & 59 deletions

File tree

.github/workflows/build-pull-request.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,6 @@ jobs:
186186
- name: Run tests
187187
run: |
188188
docker run --rm --gpus 'device=1' ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} bash -lc 'ctest --test-dir "$SIMPHONY_BUILD" --output-on-failure'
189-
docker run --rm --gpus 'device=1' ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} tests/test_simg4ox.sh
190189
docker run --rm --gpus 'device=1' ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} tests/test_GPURaytrace.sh
191190
docker run --rm --gpus 'device=1' ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} tests/test_triangulated.sh
192191
docker run --rm --gpus 'device=1' ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} tests/test_triangulated_multi.sh

CMakeLists.txt

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ endif()
1313

1414
project(simphony VERSION 0.6.0 LANGUAGES CXX CUDA)
1515

16+
# Use CMake's standard testing option, but avoid enabling this project's tests
17+
# by default when Simphony is pulled in as a subproject.
18+
if(NOT DEFINED BUILD_TESTING)
19+
set(BUILD_TESTING ${PROJECT_IS_TOP_LEVEL} CACHE BOOL "Build the testing tree.")
20+
endif()
21+
1622
set(CMAKE_CXX_STANDARD 20)
1723
set(CMAKE_CXX_STANDARD_REQUIRED ON)
1824
set(CMAKE_CXX_EXTENSIONS OFF)
@@ -24,6 +30,8 @@ set(CMAKE_CUDA_EXTENSIONS OFF)
2430
set(BUILD_SHARED_LIBS ON)
2531

2632
include(GNUInstallDirs)
33+
# Keep CTest included even when BUILD_TESTING=OFF so external drivers can run
34+
# ctest and observe an empty test set rather than a missing configuration.
2735
include(CTest)
2836
include(CheckLinkerFlag)
2937

@@ -53,19 +61,23 @@ set(GPHOX_INSTALL_FULL_DATADIR "${CMAKE_INSTALL_FULL_DATADIR}/${PROJECT_NAME}")
5361
set(GPHOX_BUILD_PTX_DIR "${PROJECT_BINARY_DIR}/ptx")
5462

5563
add_subdirectory(sysrap)
56-
add_subdirectory(sysrap/tests)
5764
add_subdirectory(CSG)
58-
add_subdirectory(CSG/tests)
5965
add_subdirectory(qudarap)
60-
add_subdirectory(qudarap/tests)
6166
add_subdirectory(CSGOptiX)
62-
add_subdirectory(CSGOptiX/tests)
6367
add_subdirectory(u4)
64-
add_subdirectory(u4/tests)
6568
add_subdirectory(g4cx)
66-
add_subdirectory(g4cx/tests)
6769
add_subdirectory(src)
6870

71+
if(BUILD_TESTING)
72+
add_subdirectory(sysrap/tests)
73+
add_subdirectory(CSG/tests)
74+
add_subdirectory(qudarap/tests)
75+
add_subdirectory(CSGOptiX/tests)
76+
add_subdirectory(u4/tests)
77+
add_subdirectory(g4cx/tests)
78+
add_subdirectory(tests)
79+
endif()
80+
6981
# Optional DD4hep integration plugins
7082
find_package(DD4hep QUIET COMPONENTS DDG4 DDCore)
7183
if(DD4hep_FOUND)

optiphy/ana/compare_ab.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#!/usr/bin/env python3
2+
"""
3+
compare_ab.py : pass/fail comparison of A/B event records
4+
=========================================================
5+
6+
Validates persisted `record.npy` outputs from paired A/B event directories by
7+
comparing aligned Opticks/G4 records and checking the known
8+
Geant4-version-dependent mismatch indices.
9+
"""
10+
11+
import argparse
12+
import sys
13+
from pathlib import Path
14+
15+
import numpy as np
16+
17+
18+
REPO_ROOT = Path(__file__).resolve().parents[2]
19+
if str(REPO_ROOT) not in sys.path:
20+
sys.path.insert(0, str(REPO_ROOT))
21+
22+
from optiphy.geant4_version import detect_geant4_version, geant4_series
23+
24+
25+
EXPECTED_DIFF = {
26+
"11.3": [14, 22, 32, 34, 40, 81, 85],
27+
"11.4+": [0, 30, 32, 34, 42, 69, 78, 85, 86],
28+
}
29+
30+
31+
def expected_diff_for_version(version):
32+
return EXPECTED_DIFF[geant4_series(version)]
33+
34+
35+
def load_records(base, a_record, b_record):
36+
a_path = base / a_record
37+
b_path = base / b_record
38+
39+
if not a_path.is_file():
40+
raise FileNotFoundError(f"Missing Opticks record file: {a_path}")
41+
if not b_path.is_file():
42+
raise FileNotFoundError(f"Missing Geant4 record file: {b_path}")
43+
44+
return np.load(a_path), np.load(b_path)
45+
46+
47+
def compare_records(a, b):
48+
if a.shape != b.shape:
49+
raise AssertionError(f"Shape mismatch: {a.shape} != {b.shape}")
50+
51+
# Geant4 and Opticks record one-step-shifted sequences for this geometry,
52+
# so compare aligned slices directly, including time.
53+
a_cmp = a[:, 1:]
54+
b_cmp = b[:, :-1]
55+
56+
return [
57+
index
58+
for index, (a_row, b_row) in enumerate(zip(a_cmp, b_cmp))
59+
if not np.allclose(a_row, b_row, rtol=0.0, atol=1e-5)
60+
]
61+
62+
63+
def main():
64+
parser = argparse.ArgumentParser(description=__doc__)
65+
parser.add_argument("--base", default=".", help="directory containing the A/B event outputs")
66+
parser.add_argument(
67+
"--a-record",
68+
default="ALL0_no_opticks_event_name/A000/record.npy",
69+
help="path to the A-side record.npy relative to --base",
70+
)
71+
parser.add_argument(
72+
"--b-record",
73+
default="ALL0_no_opticks_event_name/B000/f000/record.npy",
74+
help="path to the B-side record.npy relative to --base",
75+
)
76+
args = parser.parse_args()
77+
78+
base = Path(args.base).resolve()
79+
geant4_version = detect_geant4_version()
80+
expected_diff = expected_diff_for_version(geant4_version)
81+
82+
a, b = load_records(base, Path(args.a_record), Path(args.b_record))
83+
diff = compare_records(a, b)
84+
85+
print(f"BASE={base}")
86+
print(f"A_SHAPE={a.shape}")
87+
print(f"B_SHAPE={b.shape}")
88+
print(f"GEANT4_VERSION={geant4_version}")
89+
print(f"EXPECTED_DIFF={expected_diff}")
90+
print(f"ACTUAL_DIFF={diff}")
91+
92+
if diff != expected_diff:
93+
raise AssertionError(f"Mismatch indices differ: expected {expected_diff}, got {diff}")
94+
95+
96+
if __name__ == "__main__":
97+
main()

tests/CMakeLists.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
find_package(Python3 REQUIRED COMPONENTS Interpreter)
2+
find_program(BASH_EXECUTABLE bash REQUIRED)
3+
4+
set(SIMPHONY_SIMG4OX_TEST_WORKDIR "${CMAKE_CURRENT_BINARY_DIR}/simg4ox")
5+
file(MAKE_DIRECTORY "${SIMPHONY_SIMG4OX_TEST_WORKDIR}")
6+
7+
add_test(
8+
NAME Integration.simg4ox
9+
COMMAND ${CMAKE_COMMAND} -E env
10+
REPO_DIR=${PROJECT_SOURCE_DIR}
11+
PYTHON=${Python3_EXECUTABLE}
12+
SIMG4OX_BIN=$<TARGET_FILE:simg4ox>
13+
${BASH_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_simg4ox.sh
14+
)
15+
16+
set_tests_properties(Integration.simg4ox PROPERTIES
17+
WORKING_DIRECTORY "${SIMPHONY_SIMG4OX_TEST_WORKDIR}"
18+
LABELS "integration"
19+
TIMEOUT 120
20+
)

tests/compare_ab.py

Lines changed: 0 additions & 48 deletions
This file was deleted.

tests/test_simg4ox.sh

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
#!/usr/bin/env bash
22

3-
set -e
3+
set -euo pipefail
44

5-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
6+
REPO_DIR=${REPO_DIR:-$(cd "${SCRIPT_DIR}/.." && pwd)}
67

7-
simg4ox -g "$SCRIPT_DIR/geom/raindrop.gdml" -m "$SCRIPT_DIR/run.mac"
8-
python3 "$SCRIPT_DIR/compare_ab.py"
8+
SIMG4OX_BIN=${SIMG4OX_BIN:-simg4ox}
9+
PYTHON=${PYTHON:-python3}
10+
11+
export OPTICKS_HOME="${REPO_DIR}"
12+
export GPHOX_CONFIG_DIR="${GPHOX_CONFIG_DIR:-${REPO_DIR}/config}"
13+
export PYTHONPATH="${REPO_DIR}${PYTHONPATH:+:${PYTHONPATH}}"
14+
15+
"${SIMG4OX_BIN}" -g "${REPO_DIR}/tests/geom/raindrop.gdml" -m "${REPO_DIR}/tests/run.mac" -c dev
16+
"${PYTHON}" "${REPO_DIR}/optiphy/ana/compare_ab.py"

0 commit comments

Comments
 (0)