############################################################# # Functions for adding tests to ctest ############################################################# # Useful macros to run an existing executable: # RUN_QMC_APP # Run QMCPACK with a given number of threads and MPI processes # # QMC_RUN_AND_CHECK # Run QMCPACK and check scalar output values. This is the # primary function used for system tests. # # SIMPLE_RUN_AND_CHECK # Run QMCPACK on the given input file and check output # using a specified script ############################################################# include(test_labels) # Function to copy a directory function(COPY_DIRECTORY SRC_DIR DST_DIR) execute_process(COMMAND ${CMAKE_COMMAND} -E copy_directory "${SRC_DIR}" "${DST_DIR}") endfunction() # Create symlinks for a list of files. function(SYMLINK_LIST_OF_FILES FILENAMES DST_DIR) foreach(F IN LISTS FILENAMES) get_filename_component(NAME_ONLY ${F} NAME) file(CREATE_LINK ${F} "${DST_DIR}/${NAME_ONLY}" SYMBOLIC) endforeach() endfunction() # Function to copy a directory using symlinks for the files to save storage space. # Subdirectories are ignored. # SRC_DIR must be an absolute path # The -s flag copies using symlinks # The -t ${DST_DIR} ensures the destination must be a directory function(COPY_DIRECTORY_USING_SYMLINK SRC_DIR DST_DIR) file(MAKE_DIRECTORY "${DST_DIR}") # Find all the files but not subdirectories file( GLOB FILE_ONLY_NAMES LIST_DIRECTORIES TRUE "${SRC_DIR}/*") symlink_list_of_files("${FILE_ONLY_NAMES}" "${DST_DIR}") endfunction() # Copy selected files only. h5, pseudopotentials, wavefunction, structure and the used one input file are copied. function(COPY_DIRECTORY_USING_SYMLINK_LIMITED SRC_DIR DST_DIR ${ARGN}) file(MAKE_DIRECTORY "${DST_DIR}") # Find all the files but not subdirectories file( GLOB FILE_FOLDER_NAMES LIST_DIRECTORIES TRUE "${SRC_DIR}/qmc_ref" "${SRC_DIR}/qmc-ref" "${SRC_DIR}/*.h5" "${SRC_DIR}/*.opt.xml" "${SRC_DIR}/*.ncpp.xml" "${SRC_DIR}/*.BFD.xml" "${SRC_DIR}/*.ccECP.xml" "${SRC_DIR}/*.py" "${SRC_DIR}/*.sh" "${SRC_DIR}/*.restart.xml" "${SRC_DIR}/Li.xml" "${SRC_DIR}/H.xml" "${SRC_DIR}/*.L2_test.xml" "${SRC_DIR}/*.opt_L2.xml" "${SRC_DIR}/*.wfnoj.xml" "${SRC_DIR}/*.wfj*.xml" "${SRC_DIR}/*.wfs*.xml" "${SRC_DIR}/*.wfn*.xml" "${SRC_DIR}/*.cuspInfo.xml" "${SRC_DIR}/*.H*.xml" "${SRC_DIR}/*.structure.xml" "${SRC_DIR}/*ptcl.xml") symlink_list_of_files("${FILE_FOLDER_NAMES}" "${DST_DIR}") list(TRANSFORM ARGN PREPEND "${SRC_DIR}/") symlink_list_of_files("${ARGN}" "${DST_DIR}") # Special handling for .txt input files; assumed to be an ensemble run. Link referenced ensemble inputs if(${ARGN} MATCHES ".txt") file(STRINGS ${ARGN} ENSEMBLE_INPUTS) list(TRANSFORM ENSEMBLE_INPUTS PREPEND "${SRC_DIR}/") symlink_list_of_files("${ENSEMBLE_INPUTS}" "${DST_DIR}") endif() endfunction() # Control copy vs. symlink with top-level variable function(COPY_DIRECTORY_MAYBE_USING_SYMLINK SRC_DIR DST_DIR ${ARGN}) if(QMC_SYMLINK_TEST_FILES) copy_directory_using_symlink_limited("${SRC_DIR}" "${DST_DIR}" ${ARGN}) else() copy_directory("${SRC_DIR}" "${DST_DIR}") endif() endfunction() # Symlink or copy an individual file function(MAYBE_SYMLINK SRC_FILE DST_FILE) if(QMC_SYMLINK_TEST_FILES) file(CREATE_LINK ${SRC_FILE} ${DST_FILE} SYMBOLIC) else() # file(COPY ...) takes a destination directory and doesn't rename the file. # cmake_path requires CMake v3.20 and file(COPY_FILE ...) requires CMake v3.21. # Instead we use configure_file, which takes an input and output filename and # updates files that change in the source directory or qmc_dir. configure_file(${SRC_FILE} ${DST_FILE} COPYONLY) endif() endfunction() # Macro to create the test name macro(CREATE_TEST_NAME TEST ${ARGN}) set(TESTNAME "${TEST}") foreach(tmp ${ARGN}) set(TESTNAME "${TESTNAME}--${tmp}") endforeach() # STRING(REGEX REPLACE "--" "-" TESTNAME ${TESTNAME} ) endmacro() # Runs qmcpack # Note that TEST_ADDED is an output variable function( RUN_QMC_APP_NO_COPY TESTNAME WORKDIR PROCS THREADS TEST_ADDED TEST_LABELS ${ARGN}) math(EXPR TOT_PROCS "${PROCS} * ${THREADS}") set(QMC_APP $) set(TEST_ADDED_TEMP FALSE) if(NOT QMC_OMP) if(${THREADS} GREATER 1) message(VERBOSE "Disabling test ${TESTNAME} (exceeds maximum number of threads=1 if OpenMP is disabled -DQMC_OMP=0)") return() endif() endif() if(HAVE_MPI) if(${TOT_PROCS} GREATER ${TEST_MAX_PROCS}) message(VERBOSE "Disabling test ${TESTNAME} (exceeds maximum number of processors ${TEST_MAX_PROCS})") else() add_test(NAME ${TESTNAME} COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} ${PROCS} ${MPIEXEC_PREFLAGS} ${QMC_APP} ${ARGN}) set_tests_properties( ${TESTNAME} PROPERTIES FAIL_REGULAR_EXPRESSION "QMCPACK ERROR" PASS_REGULAR_EXPRESSION "QMCPACK execution completed successfully" PROCESSORS ${TOT_PROCS} PROCESSOR_AFFINITY TRUE WORKING_DIRECTORY ${WORKDIR} ENVIRONMENT OMP_NUM_THREADS=${THREADS}) set(TEST_ADDED_TEMP TRUE) if("asan" IN_LIST ENABLE_SANITIZER) set_property( TEST ${TESTNAME} APPEND PROPERTY ENVIRONMENT LSAN_OPTIONS=${LSAN_OPTIONS}) endif() endif() else() if((${PROCS} STREQUAL "1")) add_test(NAME ${TESTNAME} COMMAND ${QMC_APP} ${ARGN}) set_tests_properties( ${TESTNAME} PROPERTIES FAIL_REGULAR_EXPRESSION "QMCPACK ERROR" PASS_REGULAR_EXPRESSION "QMCPACK execution completed successfully" PROCESSORS ${TOT_PROCS} PROCESSOR_AFFINITY TRUE WORKING_DIRECTORY ${WORKDIR} ENVIRONMENT OMP_NUM_THREADS=${THREADS}) set(TEST_ADDED_TEMP TRUE) else() message(VERBOSE "Disabling test ${TESTNAME} (building without MPI)") endif() endif() # set additional test properties when the test gets added set(TEST_LABELS_TEMP "") if(TEST_ADDED_TEMP) add_test_labels(${TESTNAME} TEST_LABELS_TEMP) set_property( TEST ${TESTNAME} APPEND PROPERTY LABELS "QMCPACK") if(ENABLE_CUDA OR ENABLE_ROCM OR ENABLE_SYCL OR ENABLE_OFFLOAD) set_tests_properties(${TESTNAME} PROPERTIES RESOURCE_LOCK exclusively_owned_gpus) endif() if(ENABLE_OFFLOAD) set_property(TEST ${TESTNAME} APPEND PROPERTY ENVIRONMENT "OMP_TARGET_OFFLOAD=mandatory") endif() endif() set(${TEST_ADDED} ${TEST_ADDED_TEMP} PARENT_SCOPE) set(${TEST_LABELS} ${TEST_LABELS_TEMP} PARENT_SCOPE) endfunction() # Runs qmcpack # Note that TEST_ADDED is an output variable function( RUN_QMC_APP TESTNAME SRC_DIR PROCS THREADS TEST_ADDED TEST_LABELS ${ARGN}) # restrict ARGN to only one file or empty list(LENGTH ARGN INPUT_FILE_LENGTH) if(INPUT_FILE_LENGTH GREATER 1) message(FATAL_ERROR "Incorrect invocation of RUN_QMC_APP by ${TESTNAME}. ARGN value is \"${ARGN}\"") endif() copy_directory_maybe_using_symlink("${SRC_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/${TESTNAME}" "${ARGN}") set(TEST_ADDED_TEMP FALSE) set(TEST_LABELS_TEMP "") run_qmc_app_no_copy( ${TESTNAME} ${CMAKE_CURRENT_BINARY_DIR}/${TESTNAME} ${PROCS} ${THREADS} TEST_ADDED_TEMP TEST_LABELS_TEMP ${ARGN}) set(${TEST_ADDED} ${TEST_ADDED_TEMP} PARENT_SCOPE) set(${TEST_LABELS} ${TEST_LABELS_TEMP} PARENT_SCOPE) endfunction() if(QMC_NO_SLOW_CUSTOM_TESTING_COMMANDS) function(QMC_RUN_AND_CHECK) endfunction() function(QMC_RUN_AND_CHECK_CUSTOM_SCALAR) endfunction() function(SIMPLE_RUN_AND_CHECK) endfunction() else(QMC_NO_SLOW_CUSTOM_TESTING_COMMANDS) # Add a test run and associated scalar checks # ---required inputs--- # BASE_NAME - name of test (number of MPI processes, number of threads, and value to check (if applicable) # will be appended to get the full test name) # BASE_DIR - source location of test input files # PREFIX - prefix for output files # INPUT_FILE - XML input file to QMCPACK # PROCS - number of MPI processes # THREADS - number of OpenMP threads # SHOULD_SUCCEED - whether the test is expected to pass or fail. Expected failing tests will not have # the scalar tests added. # ---optional inputs--- # ---any number of SERIES/SCALAR_VALUES list pairs can be provided # ---input pairs beyond the first result in the series number being added to the test name # ---support for this functionality is provided through the ARGN catch-all input list # SERIES - series index to compute # SCALAR_VALUES - list of output values to check with check_scalars.py # The list entries alternate between the value name and the value (usually a string with the # both the average and error). function( QMC_RUN_AND_CHECK BASE_NAME BASE_DIR PREFIX INPUT_FILE PROCS THREADS SHOULD_SUCCEED) # Map from name of check to appropriate flag for check_scalars.py list( APPEND SCALAR_CHECK_TYPE "kinetic" "totenergy" "variance" "eeenergy" "samples" "potential" "ionion" "localecp" "nonlocalecp" "flux" "kinetic_mixed" "kinetic_pure" "eeenergy_mixed" "eeenergy_pure" "potential_pure" "totenergy_A" "totenergy_B" "dtotenergy_AB" "ionion_A" "ionion_B" "dionion_AB" "eeenergy_A" "eeenergy_B" "deeenergy_AB" "Eloc" "ElocEstim" "latdev" "EnergyEstim__nume_real" "kecorr" "mpc" "soecp") list( APPEND CHECK_SCALAR_FLAG "--ke" "--le" "--va" "--ee" "--ts" "--lp" "--ii" "--lpp" "--nlpp" "--fl" "--ke_m" "--ke_p" "--ee_m" "--ee_p" "--lp_p" "--le_A" "--le_B" "--dle_AB" "--ii_A" "--ii_B" "--dii_AB" "--ee_A" "--ee_B" "--dee_AB" "--eloc" "--elocest" "--latdev" "--el" "--kec" "--mpc" "--sopp") set(TEST_ADDED FALSE) set(TEST_LABELS "") set(FULL_NAME "${BASE_NAME}-r${PROCS}-t${THREADS}") message(VERBOSE "Adding test ${FULL_NAME}") run_qmc_app( ${FULL_NAME} ${BASE_DIR} ${PROCS} ${THREADS} TEST_ADDED TEST_LABELS ${INPUT_FILE}) if(TEST_ADDED AND NOT SHOULD_SUCCEED) set_property(TEST ${FULL_NAME} APPEND PROPERTY WILL_FAIL TRUE) #MESSAGE("Test ${FULL_NAME} should fail") endif() if(TEST_ADDED AND SHOULD_SUCCEED) set(IDX0 0) foreach(V ${ARGN}) math(EXPR MOD_IDX0 "${IDX0}%2") if(MOD_IDX0 EQUAL 0) #MESSAGE(" SERIES : ${V}") set(SERIES ${V}) endif() if(MOD_IDX0 EQUAL 1) #MESSAGE(" CHECKLIST: ${V}") set(SCALAR_VALUES ${V}) set(SCALAR_VALUE_FOUND FALSE) if(NOT ${SCALAR_VALUES}) message(FATAL_ERROR "Scalar values not found in variable ${SCALAR_VALUES}") endif() foreach(SCALAR_CHECK IN LISTS SCALAR_CHECK_TYPE) list(FIND ${SCALAR_VALUES} ${SCALAR_CHECK} IDX1) if(IDX1 GREATER -1) set(SCALAR_VALUE_FOUND TRUE) list(FIND SCALAR_CHECK_TYPE ${SCALAR_CHECK} IDX) list(GET CHECK_SCALAR_FLAG ${IDX} FLAG) math(EXPR IDX2 "${IDX1} + 1") list(GET ${SCALAR_VALUES} ${IDX2} VALUE) if(IDX0 LESS 2) set(TEST_NAME "${FULL_NAME}-${SCALAR_CHECK}") else() set(TEST_NAME "${FULL_NAME}-s${SERIES}-${SCALAR_CHECK}") endif() #MESSAGE("Adding scalar check ${TEST_NAME}") set(CHECK_CMD ${qmcpack_SOURCE_DIR}/tests/scripts/check_scalars.py --ns 3 --series ${SERIES} -p ${PREFIX} -e 2 ${FLAG} ${VALUE}) #MESSAGE("check command = ${CHECK_CMD}") add_test( NAME ${TEST_NAME} COMMAND ${Python3_EXECUTABLE} ${CHECK_CMD} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${FULL_NAME}") set_property(TEST ${TEST_NAME} APPEND PROPERTY DEPENDS ${FULL_NAME}) set_property(TEST ${TEST_NAME} APPEND PROPERTY LABELS "QMCPACK-checking-results") set_property(TEST ${TEST_NAME} APPEND PROPERTY LABELS ${TEST_LABELS}) endif() endforeach(SCALAR_CHECK) if(NOT SCALAR_VALUE_FOUND) message(FATAL_ERROR "Unknown scalar value to check in ${${SCALAR_VALUES}}") endif() endif() math(EXPR IDX0 "${IDX0}+1") endforeach(V) endif() endfunction() # Add a test run and associated scalar checks for a custom named scalar # Arguments # BASE_NAME - name of test (number of MPI processes, number of threads, and value to check (if applicable) # will be appended to get the full test name) # BASE_DIR - source location of test input files # PREFIX - prefix for output files # INPUT_FILE - XML input file to QMCPACK # PROCS - number of MPI processes (default: 1) # THREADS - number of OpenMP threads (default: 1) # SERIES - series index to compute (default: 0) # SCALAR_VALUES - name of list of output values to check with check_scalars.py # The list entries contain consecutively the name, the value, and the error. # The name of the variable is passed (instead of the value) in case future support # for multiple SERIES/SCALAR_VALUES pairs is added function(QMC_RUN_AND_CHECK_CUSTOM_SCALAR) set(OPTIONS SHOULD_FAIL) set(ONE_VALUE_ARGS BASE_NAME BASE_DIR PREFIX INPUT_FILE PROCS THREADS SERIES SCALAR_VALUES EQUILIBRATION) # Eventually many want to support multiple SERIES/SCALAR_VALUES pairs #SET(MULTI_VALUE_ARGS SERIES SCALAR_VALUES) cmake_parse_arguments(QRC "${options}" "${ONE_VALUE_ARGS}" "${MULTI_VALUE_ARGS}" ${ARGN}) set(PROCS 1) if(QRC_PROCS) set(PROCS ${QRC_PROCS}) endif() set(THREADS 1) if(QRC_THREADS) set(THREADS ${QRC_THREADS}) endif() set(BASE_NAME ${QRC_BASE_NAME}) set(BASE_DIR ${QRC_BASE_DIR}) set(PREFIX ${QRC_PREFIX}) set(INPUT_FILE ${QRC_INPUT_FILE}) set(EQUIL 2) if(DEFINED QRC_EQUILIBRATION) set(EQUIL ${QRC_EQUILIBRATION}) endif() set(TEST_ADDED FALSE) set(TEST_LABELS "") set(FULL_NAME "${BASE_NAME}-r${PROCS}-t${THREADS}") message(VERBOSE "Adding test ${FULL_NAME}") run_qmc_app( ${FULL_NAME} ${BASE_DIR} ${PROCS} ${THREADS} TEST_ADDED TEST_LABELS ${INPUT_FILE}) if(TEST_ADDED) set_property(TEST ${FULL_NAME} APPEND PROPERTY LABELS "QMCPACK") endif() if(TEST_ADDED AND SHOULD_FAIL) set_property(TEST ${FULL_NAME} APPEND PROPERTY WILL_FAIL TRUE) endif() if(TEST_ADDED AND NOT SHOULD_FAIL) # Derefence the list of scalar values by variable name set(SCALAR_VALUES "${${QRC_SCALAR_VALUES}}") list(LENGTH SCALAR_VALUES listlen) math(EXPR listlen2 "${listlen}-1") foreach(sv_idx RANGE 0 ${listlen2} 3) math(EXPR sv_idx_p1 "${sv_idx}+1") math(EXPR sv_idx_p2 "${sv_idx}+2") list(GET SCALAR_VALUES ${sv_idx} SCALAR_NAME) list(GET SCALAR_VALUES ${sv_idx_p1} SCALAR_VALUE) list(GET SCALAR_VALUES ${sv_idx_p2} SCALAR_ERROR) set(SERIES 0) if(QRC_SERIES) set(SERIES ${QRC_SERIES}) set(TEST_NAME "${FULL_NAME}-s${SERIES}-${SCALAR_NAME}") else() set(TEST_NAME "${FULL_NAME}-${SCALAR_NAME}") endif() set(CHECK_CMD ${qmcpack_SOURCE_DIR}/tests/scripts/check_scalars.py --ns 3 --series ${SERIES} -p ${PREFIX} -e ${EQUIL} --name ${SCALAR_NAME} --ref-value ${SCALAR_VALUE} --ref-error ${SCALAR_ERROR}) add_test( NAME ${TEST_NAME} COMMAND ${Python3_EXECUTABLE} ${CHECK_CMD} WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${FULL_NAME}") set_property(TEST ${TEST_NAME} APPEND PROPERTY DEPENDS ${FULL_NAME}) set_property(TEST ${TEST_NAME} APPEND PROPERTY LABELS "QMCPACK-checking-results") set_property(TEST ${TEST_NAME} APPEND PROPERTY LABELS ${TEST_LABELS}) endforeach() endif() endfunction() function( SIMPLE_RUN_AND_CHECK base_name base_dir input_file procs threads check_script) # "simple run and check" function does 2 things: # 1. run qmcpack executable on $input_file located in $base_dir # 2. run $check_script located in the same folder ($base_dir) # note: NAME, COMMAND, and WORKING_DIRECTORY must be upper case in add_test! # build test name set(full_name "${base_name}-${procs}-${threads}") message(VERBOSE "Adding test ${full_name}") # add run (task 1) set(test_added false) set(test_labels "") run_qmc_app( ${full_name} ${base_dir} ${procs} ${threads} test_added test_labels ${input_file}) if(NOT test_added) return() endif() # set up command to run check, assume check_script is in the same folder as input if(EXISTS "${CMAKE_CURRENT_BINARY_DIR}/${full_name}/${check_script}") set(check_cmd "${CMAKE_CURRENT_BINARY_DIR}/${full_name}/${check_script}") elseif(EXISTS "${qmcpack_SOURCE_DIR}/tests/scripts/${check_script}") set(check_cmd "${qmcpack_SOURCE_DIR}/tests/scripts/${check_script}") else() message(FATAL_ERROR "Check script not found: ${check_script}") endif() #message(${check_cmd}) # add test (task 2) set(test_name "${full_name}-check") # hard-code for single test set(work_dir "${CMAKE_CURRENT_BINARY_DIR}/${full_name}") #message(${work_dir}) add_test( NAME "${test_name}" COMMAND ${Python3_EXECUTABLE} ${check_cmd} ${ARGN} WORKING_DIRECTORY "${work_dir}") # make test depend on the run set_property(TEST ${test_name} APPEND PROPERTY DEPENDS ${full_name}) set_property(TEST ${test_name} APPEND PROPERTY LABELS ${test_labels}) endfunction() endif(QMC_NO_SLOW_CUSTOM_TESTING_COMMANDS) function(COVERAGE_RUN TESTNAME SRC_DIR PROCS THREADS ${ARGN}) set(FULLNAME "coverage-${TESTNAME}") set(TEST_ADDED FALSE) set(TEST_LABELS "") run_qmc_app( ${FULLNAME} ${SRC_DIR} ${PROCS} ${THREADS} TEST_ADDED TEST_LABELS ${ARGN}) if(TEST_ADDED) set_property(TEST ${FULLNAME} APPEND PROPERTY LABELS "coverage") endif() endfunction() function( CPU_LIMIT_RUN TESTNAME SRC_DIR PROCS THREADS TIME ${ARGN}) set(FULLNAME "cpu_limit-${TESTNAME}") set(TEST_ADDED FALSE) set(TEST_LABELS "") run_qmc_app( ${FULLNAME} ${SRC_DIR} ${PROCS} ${THREADS} TEST_ADDED TEST_LABELS ${ARGN}) if(TEST_ADDED) set_property(TEST ${FULLNAME} APPEND PROPERTY TIMEOUT ${TIME}) set_property(TEST ${FULLNAME} APPEND PROPERTY PASS_REGULAR_EXPRESSION "Time limit reached for") endif() endfunction() # Add a test to see if a file exists in the desired location. function(add_test_check_file_existence TEST_DEP_IN FILE_NAME SHOULD_SUCCEED) if(TEST ${TEST_DEP_IN}) get_test_property(${TEST_DEP_IN} WORKING_DIRECTORY TEST_DEP_IN_WORK_DIR) set(TESTNAME ${TEST_DEP_IN}-exists-${FILE_NAME}) add_test(NAME ${TESTNAME} COMMAND ls ${TEST_DEP_IN_WORK_DIR}/${FILE_NAME}) if(NOT SHOULD_SUCCEED) set_property(TEST ${TESTNAME} PROPERTY WILL_FAIL TRUE) endif() set_tests_properties(${TESTNAME} PROPERTIES DEPENDS ${TEST_DEP_IN}) endif() endfunction()