From 2053638989525e18157cb5cad0f4414a6662187a Mon Sep 17 00:00:00 2001 From: "Mads R. B. Kristensen" Date: Mon, 30 Sep 2024 13:27:02 +0200 Subject: [PATCH] C++ Test (#440) Implement very basic C++ tests. Authors: - Mads R. B. Kristensen (https://github.com/madsbk) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/kvikio/pull/440 --- ci/run_ctests.sh | 8 +- cpp/CMakeLists.txt | 9 +- cpp/cmake/thirdparty/get_gtest.cmake | 21 +--- cpp/examples/CMakeLists.txt | 30 +++-- cpp/tests/CMakeLists.txt | 43 +++++++ cpp/tests/main.cpp | 23 ++++ cpp/tests/test_basic_io.cpp | 43 +++++++ cpp/tests/utils.hpp | 172 +++++++++++++++++++++++++++ dependencies.yaml | 15 +-- 9 files changed, 322 insertions(+), 42 deletions(-) create mode 100644 cpp/tests/CMakeLists.txt create mode 100644 cpp/tests/main.cpp create mode 100644 cpp/tests/test_basic_io.cpp create mode 100644 cpp/tests/utils.hpp diff --git a/ci/run_ctests.sh b/ci/run_ctests.sh index 069490facf..dcb938cbdd 100755 --- a/ci/run_ctests.sh +++ b/ci/run_ctests.sh @@ -6,5 +6,9 @@ set -euo pipefail # Support customizing the ctests' install location cd "${INSTALL_PREFIX:-${CONDA_PREFIX:-/usr}}/bin/tests/libkvikio/" -# Run BASIC_IO_TEST -./BASIC_IO_TEST +# Run basic tests +./BASIC_IO_EXAMPLE +./BASIC_NO_CUDA_EXAMPLE + +# Run gtests +ctest --no-tests=error --output-on-failure "$@" diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 6a4f89a0d7..b82ceb2a02 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -37,6 +37,7 @@ rapids_cmake_build_type(Release) # build options option(KvikIO_BUILD_EXAMPLES "Configure CMake to build examples" ON) +option(KvikIO_BUILD_TESTS "Configure CMake to build tests" ON) rapids_cmake_support_conda_env(conda_env MODIFY_PREFIX_PATH) @@ -144,10 +145,14 @@ if(KvikIO_BUILD_EXAMPLES) add_subdirectory(examples) endif() -# optionally build tests if(KvikIO_BUILD_TESTS AND CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) include(cmake/thirdparty/get_gtest.cmake) - include(CTest) # calls enable_testing() + + # include CTest module -- automatically calls enable_testing() + include(CTest) + + # Always print verbose output when tests fail if run using `make test`. + list(APPEND CMAKE_CTEST_ARGUMENTS "--output-on-failure") add_subdirectory(tests) endif() diff --git a/cpp/cmake/thirdparty/get_gtest.cmake b/cpp/cmake/thirdparty/get_gtest.cmake index 1133c846c3..10e6b026d9 100644 --- a/cpp/cmake/thirdparty/get_gtest.cmake +++ b/cpp/cmake/thirdparty/get_gtest.cmake @@ -1,5 +1,5 @@ # ============================================================================= -# Copyright (c) 2021-2023, NVIDIA CORPORATION. +# Copyright (c) 2021-2024, NVIDIA CORPORATION. # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except # in compliance with the License. You may obtain a copy of the License at @@ -17,24 +17,7 @@ function(find_and_configure_gtest) include(${rapids-cmake-dir}/cpm/gtest.cmake) # Find or install GoogleTest - rapids_cpm_gtest( - BUILD_EXPORT_SET kvikio-testing-exports INSTALL_EXPORT_SET kvikio-testing-exports - ) - - if(GTest_ADDED) - rapids_export( - BUILD GTest - VERSION ${GTest_VERSION} - EXPORT_SET GTestTargets - GLOBAL_TARGETS gtest gmock gtest_main gmock_main - NAMESPACE GTest:: - ) - - include("${rapids-cmake-dir}/export/find_package_root.cmake") - rapids_export_find_package_root( - BUILD GTest [=[${CMAKE_CURRENT_LIST_DIR}]=] EXPORT_SET kvikio-testing-exports - ) - endif() + rapids_cpm_gtest(BUILD_STATIC) endfunction() diff --git a/cpp/examples/CMakeLists.txt b/cpp/examples/CMakeLists.txt index c12ddb2e52..5124c9ef71 100644 --- a/cpp/examples/CMakeLists.txt +++ b/cpp/examples/CMakeLists.txt @@ -14,19 +14,23 @@ set(TEST_INSTALL_PATH bin/tests/libkvikio) +# Example: basic_io + if(CUDAToolkit_FOUND) - add_executable(BASIC_IO_TEST basic_io.cpp) - set_target_properties(BASIC_IO_TEST PROPERTIES INSTALL_RPATH "\$ORIGIN/../../lib") - target_include_directories(BASIC_IO_TEST PRIVATE ../include ${cuFile_INCLUDE_DIRS}) - target_link_libraries(BASIC_IO_TEST PRIVATE kvikio CUDA::cudart) + add_executable(BASIC_IO_EXAMPLE basic_io.cpp) + set_target_properties(BASIC_IO_EXAMPLE PROPERTIES INSTALL_RPATH "\$ORIGIN/../../lib") + target_include_directories(BASIC_IO_EXAMPLE PRIVATE ../include ${cuFile_INCLUDE_DIRS}) + target_link_libraries(BASIC_IO_EXAMPLE PRIVATE kvikio CUDA::cudart) if(CMAKE_COMPILER_IS_GNUCXX) set(KVIKIO_CXX_FLAGS "-Wall;-Werror;-Wno-unknown-pragmas") - target_compile_options(BASIC_IO_TEST PRIVATE "$<$:${KVIKIO_CXX_FLAGS}>") + target_compile_options( + BASIC_IO_EXAMPLE PRIVATE "$<$:${KVIKIO_CXX_FLAGS}>" + ) endif() install( - TARGETS BASIC_IO_TEST + TARGETS BASIC_IO_EXAMPLE COMPONENT testing DESTINATION ${TEST_INSTALL_PATH} EXCLUDE_FROM_ALL @@ -35,20 +39,22 @@ else() message(STATUS "Cannot build the basic_io example when CUDA is not found") endif() -add_executable(BASIC_NO_CUDA_TEST basic_no_cuda.cpp) -set_target_properties(BASIC_NO_CUDA_TEST PROPERTIES INSTALL_RPATH "\$ORIGIN/../../lib") -target_include_directories(BASIC_NO_CUDA_TEST PRIVATE ../include) -target_link_libraries(BASIC_NO_CUDA_TEST PRIVATE kvikio) +# Example: basic_no_cuda + +add_executable(BASIC_NO_CUDA_EXAMPLE basic_no_cuda.cpp) +set_target_properties(BASIC_NO_CUDA_EXAMPLE PROPERTIES INSTALL_RPATH "\$ORIGIN/../../lib") +target_include_directories(BASIC_NO_CUDA_EXAMPLE PRIVATE ../include) +target_link_libraries(BASIC_NO_CUDA_EXAMPLE PRIVATE kvikio) if(CMAKE_COMPILER_IS_GNUCXX) set(KVIKIO_CXX_FLAGS "-Wall;-Werror;-Wno-unknown-pragmas") target_compile_options( - BASIC_NO_CUDA_TEST PRIVATE "$<$:${KVIKIO_CXX_FLAGS}>" + BASIC_NO_CUDA_EXAMPLE PRIVATE "$<$:${KVIKIO_CXX_FLAGS}>" ) endif() install( - TARGETS BASIC_NO_CUDA_TEST + TARGETS BASIC_NO_CUDA_EXAMPLE COMPONENT testing DESTINATION ${TEST_INSTALL_PATH} EXCLUDE_FROM_ALL diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt new file mode 100644 index 0000000000..429bd8b722 --- /dev/null +++ b/cpp/tests/CMakeLists.txt @@ -0,0 +1,43 @@ +# ============================================================================= +# Copyright (c) 2024, NVIDIA CORPORATION. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +# in compliance with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License +# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing permissions and limitations under +# the License. +# ============================================================================= + +# ################################################################################################## +# enable testing ----------------------------------------------------------------------------------- +# ################################################################################################## +enable_testing() + +include(rapids-test) +rapids_test_init() + +file(GLOB SOURCES "*.cpp") +add_executable(cpp_tests ${SOURCES}) +set_target_properties( + cpp_tests + PROPERTIES RUNTIME_OUTPUT_DIRECTORY "$" + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + # For std:: support of __int128_t. Can be removed once using cuda::std + CXX_EXTENSIONS ON + CUDA_STANDARD 17 + CUDA_STANDARD_REQUIRED ON +) +target_link_libraries(cpp_tests PRIVATE kvikio::kvikio GTest::gmock GTest::gtest) +rapids_test_add( + NAME cpp_tests + COMMAND cpp_tests + GPUS 1 + INSTALL_COMPONENT_SET testing +) + +rapids_test_install_relocatable(INSTALL_COMPONENT_SET testing DESTINATION bin/tests/libkvikio) diff --git a/cpp/tests/main.cpp b/cpp/tests/main.cpp new file mode 100644 index 0000000000..73acda862a --- /dev/null +++ b/cpp/tests/main.cpp @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/cpp/tests/test_basic_io.cpp b/cpp/tests/test_basic_io.cpp new file mode 100644 index 0000000000..12ccb6d428 --- /dev/null +++ b/cpp/tests/test_basic_io.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "utils.hpp" + +using namespace kvikio::test; + +TEST(BasicIO, write_read) +{ + TempDir tmp_dir{false}; + auto filepath = tmp_dir.path() / "test"; + + auto dev_a = DevBuffer::arange(100); + auto dev_b = DevBuffer::zero_like(dev_a); + + { + kvikio::FileHandle f(filepath, "w"); + auto nbytes = f.write(dev_a.ptr, dev_a.nbytes, 0, 0); + EXPECT_EQ(nbytes, dev_a.nbytes); + } + + { + kvikio::FileHandle f(filepath, "r"); + auto nbytes = f.read(dev_b.ptr, dev_b.nbytes, 0, 0); + EXPECT_EQ(nbytes, dev_b.nbytes); + expect_equal(dev_a, dev_b); + } +} diff --git a/cpp/tests/utils.hpp b/cpp/tests/utils.hpp new file mode 100644 index 0000000000..56a2cd5c45 --- /dev/null +++ b/cpp/tests/utils.hpp @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2024, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +namespace kvikio::test { + +/** \ + * @brief Error checking macro for CUDA runtime API functions. \ + * \ + * Invokes a CUDA runtime API function call. If the call does not return \ + * `cudaSuccess`, invokes cudaGetLastError() to clear the error and throws an \ + * exception detailing the CUDA error that occurred \ + * \ + * Defaults to throwing std::runtime_error, but a custom exception may also be \ + * specified. \ + * \ + * Example: \ + * ```c++ \ + * \ + * // Throws std::runtime_error if `cudaMalloc` fails \ + * KVIKIO_CHECK_CUDA(cudaMalloc(&p, 100)); \ + * \ + * // Throws std::runtime_error if `cudaMalloc` fails \ + * KVIKIO_CHECK_CUDA(cudaMalloc(&p, 100), std::runtime_error); \ + * ``` \ + * \ + */ \ +#define KVIKIO_CHECK_CUDA(...) \ + GET_KVIKIO_CHECK_CUDA_MACRO(__VA_ARGS__, KVIKIO_CHECK_CUDA_2, KVIKIO_CHECK_CUDA_1) \ + (__VA_ARGS__) +#define GET_KVIKIO_CHECK_CUDA_MACRO(_1, _2, NAME, ...) NAME +#define KVIKIO_CHECK_CUDA_2(_call, _exception_type) \ + do { \ + cudaError_t const error = (_call); \ + if (cudaSuccess != error) { \ + cudaGetLastError(); \ + /*NOLINTNEXTLINE(bugprone-macro-parentheses)*/ \ + throw _exception_type{std::string{"CUDA error at: "} + __FILE__ + ":" + \ + KVIKIO_STRINGIFY(__LINE__) + ": " + cudaGetErrorName(error) + " " + \ + cudaGetErrorString(error)}; \ + } \ + } while (0) +#define KVIKIO_CHECK_CUDA_1(_call) KVIKIO_CHECK_CUDA_2(_call, std::runtime_error) + +/** + * @brief Help class to create a temporary directory. + */ +class TempDir { + public: + TempDir(const bool cleanup = true) : _cleanup{cleanup} + { + std::string tpl{std::filesystem::temp_directory_path() / "kvikio.XXXXXX"}; + if (mkdtemp(tpl.data()) == nullptr) { + throw std::runtime_error("TempDir: cannot make tmpdir: " + tpl); + } + _dir_path = tpl; + } + ~TempDir() noexcept + { + if (_cleanup) { + try { + std::filesystem::remove_all(_dir_path); + } catch (...) { + std::cout << "error while trying to remove " << _dir_path.string() << std::endl; + } + } + } + + TempDir(const TempDir&) = delete; + TempDir& operator=(TempDir const&) = delete; + TempDir(const TempDir&&) = delete; + TempDir&& operator=(TempDir const&&) = delete; + + const std::filesystem::path& path() { return _dir_path; } + + operator std::string() { return path(); } + + private: + const bool _cleanup; + std::filesystem::path _dir_path{}; +}; + +/** + * @brief Help class for creating and comparing buffers. + */ +class DevBuffer { + public: + const std::size_t nelem; + const std::size_t nbytes; + void* ptr{nullptr}; + + DevBuffer(std::size_t nelem) : nelem{nelem}, nbytes{nelem * sizeof(std::int64_t)} + { + KVIKIO_CHECK_CUDA(cudaMalloc(&ptr, nbytes)); + } + DevBuffer(const std::vector& host_buffer) : DevBuffer{host_buffer.size()} + { + KVIKIO_CHECK_CUDA(cudaMemcpy(ptr, host_buffer.data(), nbytes, cudaMemcpyHostToDevice)); + } + + ~DevBuffer() noexcept { cudaFree(ptr); } + + [[nodiscard]] static DevBuffer arange(std::size_t nelem, std::int64_t start = 0) + { + std::vector host_buffer(nelem); + std::iota(host_buffer.begin(), host_buffer.end(), start); + return DevBuffer{host_buffer}; + } + + [[nodiscard]] static DevBuffer zero_like(const DevBuffer& prototype) + { + DevBuffer ret{prototype.nelem}; + KVIKIO_CHECK_CUDA(cudaMemset(ret.ptr, 0, ret.nbytes)); + return ret; + } + + [[nodiscard]] std::vector to_vector() const + { + std::vector ret(nelem); + KVIKIO_CHECK_CUDA(cudaMemcpy(ret.data(), this->ptr, nbytes, cudaMemcpyDeviceToHost)); + return ret; + } + + void pprint() const + { + std::cout << "DevBuffer("; + for (auto item : to_vector()) { + std::cout << static_cast(item) << ", "; + } + std::cout << ")" << std::endl; + } +}; + +/** + * @brief Check that two buffers are equal + */ +inline void expect_equal(const DevBuffer& a, const DevBuffer& b) +{ + EXPECT_EQ(a.nbytes, b.nbytes); + auto a_vec = a.to_vector(); + auto b_vec = b.to_vector(); + for (std::size_t i = 0; i < a.nelem; ++i) { + EXPECT_EQ(a_vec[i], b_vec[i]) << "Mismatch at index " << i; + } +} + +} // namespace kvikio::test diff --git a/dependencies.yaml b/dependencies.yaml index 123112ac1a..6c6919a250 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -17,11 +17,13 @@ files: - py_version - rapids_build_skbuild - run + - test_cpp - test_python test_cpp: output: none includes: - cuda_version + - test_cpp test_python: output: none includes: @@ -98,7 +100,7 @@ dependencies: common: - output_types: [conda, requirements, pyproject] packages: - - cmake>=3.26.4,!=3.30.0 + - &cmake_ver cmake>=3.26.4,!=3.30.0 - ninja build-cpp: common: @@ -312,6 +314,11 @@ dependencies: # See https://github.com/zarr-developers/numcodecs/pull/475 - numcodecs !=0.12.0 - packaging + test_cpp: + common: + - output_types: conda + packages: + - *cmake_ver test_python: common: - output_types: [conda, requirements, pyproject] @@ -329,9 +336,3 @@ dependencies: - matrix: # All CUDA 11 versions packages: - cuda-python>=11.7.1,<12.0a0 - test_python_legate: - common: - - output_types: [conda, requirements, pyproject] - packages: - - *dask - - distributed>=2022.05.2