Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,17 @@ if (PARLAY_TEST)
${googletest_BINARY_DIR}
EXCLUDE_FROM_ALL)
endif()
# Download and configure rapidcheck
# Must enable Google Test integration since everything else relies on Google Test
set(RC_ENABLE_GTEST ON CACHE BOOL "" FORCE)
FetchContent_Declare(
rapid_check
GIT_REPOSITORY https://github.com/emil-e/rapidcheck.git
GIT_TAG ff6af6fc683159deb51c543b065eba14dfcf329b
GIT_SHALLOW TRUE
GIT_PROGRESS ON
)
FetchContent_MakeAvailable(rapid_check)

# Include test targets
message(STATUS "testing: Enabled")
Expand Down
2 changes: 1 addition & 1 deletion include/parlay/primitives.h
Original file line number Diff line number Diff line change
Expand Up @@ -1022,7 +1022,7 @@ auto flatten(sequence<sequence<T>>&& r) {

// Return true if the given character is considered whitespace.
// Whitespace characters are ' ', '\f', '\n', '\r'. '\t'. '\v'.
bool inline is_whitespace(unsigned char c) {
bool inline is_whitespace(char c) {
return std::isspace(c);
}

Expand Down
2 changes: 1 addition & 1 deletion include/parlay/utilities.h
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ struct hash : public std::hash<T> { };
template <typename T>
struct hash<T, typename std::enable_if_t<std::is_integral_v<T>>> {
size_t operator()(const T& p) const {
return p * UINT64_C(0xbf58476d1ce4e5b9);
return (size_t)p * UINT64_C(0xbf58476d1ce4e5b9);
}
};

Expand Down
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ add_dtests(NAME test_sample_sort FILES test_sample_sort.cpp LIBS parlay)
# -------------------------------- Primitives ---------------------------------

add_dtests(NAME test_primitives FILES test_primitives.cpp LIBS parlay)
add_dtests(NAME test_primitives_property_based FILES test_primitives_property_based.cpp LIBS parlay rapidcheck_gtest)
add_dtests(NAME test_sorting_primitives FILES test_sorting_primitives.cpp LIBS parlay)
add_dtests(NAME test_random FILES test_random.cpp LIBS parlay)
add_dtests(NAME test_group_by FILES test_group_by.cpp LIBS parlay)
Expand Down
20 changes: 12 additions & 8 deletions test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

*This documentation is intended for developers/contributors of ParlayLib*

Parlay is accompanied by a set of unit tests intended to assert the correctness of its data structures and primitives, and to mitigate the risk of introducing bugs during updates. If you are contributing code to Parlay, you should add appropriate unit tests to ensure that your code is tested, and update any existing unit tests that are affected by your changes. Always remember to run the tests locally before comitting changes.
Parlay is accompanied by a set of unit tests intended to assert the correctness of its data structures and primitives, and to mitigate the risk of introducing bugs during updates. If you are contributing code to Parlay, you should add appropriate unit tests to ensure that your code is tested, and update any existing unit tests that are affected by your changes. Always remember to run the tests locally before committing changes.

## Configuring the build for testing

To configure the CMake project to run the tests, you'll need to add the flag `-DPARLAY_TEST=On`. You should also ensure that the build is in *Debug* mode, by adding the flag `-DCMAKE_BUILD_TYPE=Debug`. In order to maintain a separation between your test environment and benchmark environment, it is a good habbit to initialize them as separate CMake builds in different directories. Creating a Debug build with tests enabled can be achieved with these minimal commands from the repository root.
To configure the CMake project to run the tests, you'll need to add the flag `-DPARLAY_TEST=On`. You should also ensure that the build is in *Debug* mode, by adding the flag `-DCMAKE_BUILD_TYPE=Debug`. In order to maintain a separation between your test environment and benchmark environment, it is a good habit to initialize them as separate CMake builds in different directories. Creating a Debug build with tests enabled can be achieved with these minimal commands from the repository root.

```
mkdir -p build/Debug && cd build/Debug
Expand All @@ -21,30 +21,34 @@ Once the build is configured, you can build the tests either by running `make` i

## Running the tests

The compiled tests are located in the *test* subdirectory of your configured build directory. Tests can be ran individually from here. Alternatively, running the target `test` (i.e. running `make test` or `cmake --build . --target test`), or executing the `ctest` command will run all of the tests.
The compiled tests are located in the *test* subdirectory of your configured build directory. Tests can be run individually from here. Alternatively, running the target `test` (i.e. running `make test` or `cmake --build . --target test`), or executing the `ctest` command will run all tests.

Property-based tests can be adjusted to run for more iterations and with larger data sets at the expense of longer time spent on testing. See [the documentation](https://github.com/emil-e/rapidcheck/blob/ff6af6fc683159deb51c543b065eba14dfcf329b/doc/configuration.md) for ways to adjust them.

## Building and running sanitizer-instrumented tests

All of the unit tests can also be compiled with [AddressSanitizer](https://clang.llvm.org/docs/AddressSanitizer.html) (ASAN), [UndefinedBehaviourSanitizer](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html) (UBSAN), and [MemorySanitizer](https://clang.llvm.org/docs/MemorySanitizer.html) (MSAN). Enabling ASAN and UBSAN is easy as long as you have a compiler that supports their full functionality. [Clang](https://clang.llvm.org/) is recommended, since GCC does not support the full functionality of these tools.
All unit tests can also be compiled with [AddressSanitizer](https://clang.llvm.org/docs/AddressSanitizer.html) (ASAN), [UndefinedBehaviourSanitizer](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html) (UBSAN), and [MemorySanitizer](https://clang.llvm.org/docs/MemorySanitizer.html) (MSAN). Enabling ASAN and UBSAN is easy as long as you have a compiler that supports their full functionality. [Clang](https://clang.llvm.org/) is recommended, since GCC does not support the full functionality of these tools.

To enable ASAN and UBSAN, add, to your CMake build configurations, the flags `-DBUILD_ASAN_TESTS=On`, and `-DBUILD_UBSAN_TESTS=On` respectively. With these flags enabled, each unit test will also be compiled to an additional target with the respective sanitizers enabled. Running the `test` target as above will run both the instrumented and non-instrumented tests. Alternatively, to run a specific subset of the tests, we also provide the targets
* `check`: Run only the non-instrumented tests
* `check-asan`: Run only the tests instrumented with ASAN
* `check-ubsan`: Run only the tests instrumented with UBSAN
* `check-msan`: Run only the tests instrumented with MSAN

Enabling MSAN is a little bit more complicated, since, unlike ASAN and UBSAN, MSAN only works when all linked code, including the standard library, is instrumented. This means that you need a separately compiled copy of the standard library that has already been instrumented with MSAN. Some instructions on how to do this are provided [here](https://github.com/google/sanitizers/wiki/MemorySanitizerLibcxxHowTo). Once you have built an MSAN-instrumented libc++, you should provide its location to the CMake build configuration via `-DLIBCXX_MSAN_PATH=<path/to/instrumented/libcxx>`. Finally, the unit tests with MSAN-instrumentation can enabled by adding to the CMake build configuration, the flag `-DBUILD_MSAN_TESTS=On`.
Enabling MSAN is a little bit more complicated, since, unlike ASAN and UBSAN, MSAN only works when all linked code, including the standard library, is instrumented. This means that you need a separately compiled copy of the standard library that has already been instrumented with MSAN. Some instructions on how to do this are provided [here](https://github.com/google/sanitizers/wiki/MemorySanitizerLibcxxHowTo). Once you have built an MSAN-instrumented libc++, you should provide its location to the CMake build configuration via `-DLIBCXX_MSAN_PATH=<path/to/instrumented/libcxx>`. Finally, the unit tests with MSAN-instrumentation can be enabled by adding to the CMake build configuration, the flag `-DBUILD_MSAN_TESTS=On`.

## Running memcheck tests

Tests can also be ran with memcheck ([Valgrind](https://valgrind.org/)) by adding the flag `-DENABLE_MEMCHECK_TESTS=On` to the CMake build configuration. This will add additional test targets which can be ran with the entire suite of tests, or separately by invoking the `check-memcheck` target.
Tests can also be run with memcheck ([Valgrind](https://valgrind.org/)) by adding the flag `-DENABLE_MEMCHECK_TESTS=On` to the CMake build configuration. This will add additional test targets which can be run with the entire suite of tests, or separately by invoking the `check-memcheck` target.

## Adding new tests

All of the tests in the suite are written using the [Google Test](https://github.com/google/googletest) framework. To add a new test set, write a new test cpp file in the *<project_root>/test* directory in the Google Test format. Then, register it in *<project_root>/test/CMakeLists.txt* with the line
Most tests in the suite are written using the [Google Test](https://github.com/google/googletest) framework. To add a new test set, write a new test cpp file in the *<project_root>/test* directory in the Google Test format. Then, register it in *<project_root>/test/CMakeLists.txt* with the line

```
add_dtests(NAME <your_test_name> FILES <your_test_cpp_files> LIBS parlay)
```

If necessary, multiple cpp files can be listed after the FILES option. You can also specify additional libraries that need to be linked with the test, if necessary, by adding them after the LIBS option.
The remaining unit tests are property-based tests written using the [rapidcheck](https://github.com/emil-e/rapidcheck) framework. See [rapidcheck's instructions](https://github.com/emil-e/rapidcheck/blob/ff6af6fc683159deb51c543b065eba14dfcf329b/doc/gtest.md) for more information. To register rapidcheck tests, follow the format of `add_dtests` above and include `rapidcheck_gtest` after the LIBS option.

If necessary, multiple cpp files can be listed after the FILES option. You can also specify additional libraries that need to be linked with the test (such as `rapidcheck_gtest`) by adding them after the LIBS option.
94 changes: 94 additions & 0 deletions test/rapid_check_utils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// For automatically generating a parlay::sequence in rapidcheck

#ifndef PARLAY_RAPID_CHECK_UTILS
#define PARLAY_RAPID_CHECK_UTILS

#include <signal.h>

#include "rapidcheck.h"

#include "parlay/sequence.h"

namespace rc {

template<typename T>
struct Arbitrary<parlay::sequence<T>> {
static Gen<parlay::sequence<T>> arbitrary() {
auto r = gen::map<std::vector<T>>([](std::vector<T> x){
return parlay::sequence<T>(x.begin(), x.end());
});
return r;
}
};

template<typename T>
std::string container_to_string(T list) {
std::ostringstream os;
os << "[";
for (size_t i = 0; i < list.size(); i++) {
os << list[i];
if (i != list.size() - 1) {
os << ", ";
}
}
os << "]";
return os.str();
}

template<typename T>
std::ostream& operator<<(std::ostream& os, const parlay::sequence<T>& seq) {
using namespace parlay;
os << container_to_string(seq);
return os;
}

template<typename T>
std::ostream& operator<<(std::ostream& os, const std::vector<T>& seq) {
using namespace std;
os << container_to_string(seq);
return os;
}

std::string test_data;

void signal_handler(int signum) {
std::cerr << "Signal " << signum << ": " << sigabbrev_np(signum) << '\n';
std::cerr << test_data << '\n';
exit(1);
}

/**
* Stores the current test data as a string and print it if the program crashes with an irrecoverable signal. This
* helps with debugging by showing the dataset that caused the crash.
*
* @tparam T
*/
template<typename T>
struct signal_interceptor {
static constexpr int signals[] = {SIGFPE, SIGSEGV, SIGABRT, SIGILL, SIGBUS};

explicit signal_interceptor(const T &data) {
std::ostringstream os;
os << data;
test_data = os.str();
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = signal_handler;
for (auto signal : signals) {
sigaction(signal, &act, nullptr);
}
}

~signal_interceptor() {
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = SIG_DFL;
for (auto signal : signals) {
sigaction(signal, &act, nullptr);
}
}
};

} // namespace rc

#endif // PARLAY_RAPID_CHECK_UTILS
Loading