diff --git a/.clang-format b/.clang-format index 74a959a..42556a6 100644 --- a/.clang-format +++ b/.clang-format @@ -1,54 +1,13 @@ --- -Language: Cpp BasedOnStyle: LLVM -AccessModifierOffset: -4 -AlignConsecutiveAssignments: false -AlignConsecutiveDeclarations: false -AlignOperands: false -AlignTrailingComments: false -AlwaysBreakTemplateDeclarations: Yes -BraceWrapping: - AfterCaseLabel: true - AfterClass: true - AfterControlStatement: true - AfterEnum: true - AfterFunction: true - AfterNamespace: true - AfterStruct: true - AfterUnion: true - AfterExternBlock: false - BeforeCatch: true - BeforeElse: true - BeforeLambdaBody: true - BeforeWhile: true - SplitEmptyFunction: true - SplitEmptyRecord: true - SplitEmptyNamespace: true -BreakBeforeBraces: Custom -BreakConstructorInitializers: AfterColon -BreakConstructorInitializersBeforeComma: false -ColumnLimit: 120 -ConstructorInitializerAllOnOneLineOrOnePerLine: false -IncludeCategories: - - Regex: '^<.*' - Priority: 1 - - Regex: '^".*' - Priority: 2 - - Regex: '.*' - Priority: 3 -IncludeIsMainRegex: '([-_](test|unittest))?$' -IndentCaseBlocks: true -IndentWidth: 4 -InsertNewlineAtEOF: true -MacroBlockBegin: '' -MacroBlockEnd: '' -MaxEmptyLinesToKeep: 2 -NamespaceIndentation: All +--- +Language: Cpp +# Force pointers to the type for C++. +DerivePointerAlignment: false PointerAlignment: Left -SpaceInEmptyParentheses: false -SpacesInAngles: false -SpacesInConditionalStatement: false -SpacesInCStyleCastParentheses: false -SpacesInParentheses: false -TabWidth: 4 -... +ColumnLimit: 100 +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +BinPackArguments: false +BinPackParameters: false +AlignAfterOpenBracket: BlockIndent diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..751c65c --- /dev/null +++ b/.clangd @@ -0,0 +1,5 @@ +CompileFlags: + Add: + - "-std=c++23" + - "-stdlib=libc++" + CompilationDatabase: .build diff --git a/.vscode/settings.json b/.vscode/settings.json index 5cbe5ab..21288fe 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,12 @@ { "clangd.arguments": [ - "--compile-commands-dir=${workspaceFolder}/build", + "--compile-commands-dir=${workspaceFolder}/.build", "--background-index", "--clang-tidy", "--completion-style=detailed", - "--header-insertion=iwyu", - "--query-driver=/usr/bin/clang++" - ] + "--header-insertion=iwyu" + ], + "files.associations": { + "algorithm": "cpp" + } } \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 790012e..9cca122 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,21 @@ CPMAddPackage( VERSION 2.4.12 ) +# Add RapidCheck for property-based testing +CPMAddPackage( + NAME rapidcheck + GITHUB_REPOSITORY emil-e/rapidcheck + GIT_TAG master + OPTIONS "RC_ENABLE_TESTS OFF" "RC_ENABLE_EXAMPLES OFF" +) + +# Suppress deprecation warnings originating from RapidCheck headers only +if (TARGET rapidcheck) + target_compile_options(rapidcheck INTERFACE + $<$:-Wno-deprecated-declarations> + ) +endif() + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) add_library(positionless INTERFACE) @@ -32,12 +47,12 @@ target_compile_options(positionless INTERFACE $<$:/W4> ) -add_executable(unit_tests test/partitioning_tests.cpp) -target_link_libraries(unit_tests PRIVATE positionless doctest::doctest) -target_compile_options(unit_tests PRIVATE - $<$:-Wall -Wextra -Wpedantic> - $<$:/W4> +add_executable(unit_tests + test/tests_main.cpp + test/partitioning_tests.cpp + test/algorithms_tests.cpp ) +target_link_libraries(unit_tests PRIVATE positionless doctest::doctest rapidcheck) enable_testing() add_test(NAME unit_tests COMMAND unit_tests) diff --git a/README.md b/README.md index 2471dc0..df1cc65 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,25 @@ We want to have an implementation of positionless algorithms and a translation f ## Positionless features -TODO +- Construction from a range (forward access, bidirectional and random access) +- `paritioning` accessors: + - `parts_count() -> size_t` + - `part(size_t part_index) -> std::pair` + - `is_part_empty(size_t part_index) -> bool` + - `part_size(size_t part_index) -> size_t` -- constant time for random access, otherwise linear +- growing parts (at the end): + - `grow` + - `grow_by` +- shrinking parts (for bidirectional collections): + - `shrink` + - `shrink_by` +- transferring elements between parts: + - `transfer_to_prev` + - `transfer_to_next` +- creation and destruction of parts: + - `add_part_end` / `add_part_begin` + - `add_parts_end` / `add_parts_begin` + - `remove_part` ## Translation from iterators TODO diff --git a/include/positionless/algorithms.hpp b/include/positionless/algorithms.hpp new file mode 100644 index 0000000..3e546b8 --- /dev/null +++ b/include/positionless/algorithms.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "positionless/detail/precondition.hpp" +#include "positionless/partitioning.hpp" + +#include +#include + +namespace positionless { + +/// Swaps the first element from part `i` with the first element from part `j`. +/// +/// - Precondition: `i < p.parts_count()` +/// - Precondition: `j < p.parts_count()` +/// - Precondition: parts `i` and `j` are not empty +template +inline void swap_first(partitioning& p, size_t i, size_t j) { + PRECONDITION(i < p.parts_count()); + PRECONDITION(j < p.parts_count()); + PRECONDITION(!p.is_part_empty(i)); + PRECONDITION(!p.is_part_empty(j)); + + auto [begin_i, end_i] = p.part(i); + auto [begin_j, end_j] = p.part(j); + + std::iter_swap(begin_i, begin_j); +} + +} // namespace positionless diff --git a/include/positionless/detail/precondition.hpp b/include/positionless/detail/precondition.hpp new file mode 100644 index 0000000..220a9cd --- /dev/null +++ b/include/positionless/detail/precondition.hpp @@ -0,0 +1,18 @@ +#pragma once + +#if defined(NDEBUG) + +#include + +/// Assert-like precondition check that calls `std::terminate` on failure. +#define PRECONDITION(expr) \ + do { \ + if (!(expr)) \ + std::terminate(); \ + } while (false) +#else + +#include +#define PRECONDITION(expr) assert(expr) + +#endif \ No newline at end of file diff --git a/include/positionless/partitioning.hpp b/include/positionless/partitioning.hpp index 395e1ee..25e9a73 100644 --- a/include/positionless/partitioning.hpp +++ b/include/positionless/partitioning.hpp @@ -1,14 +1,246 @@ #pragma once -namespace positionless +#include "positionless/detail/precondition.hpp" + +#include +#include +#include +#include + +namespace positionless { + +/// A separation of some collection into multiple contiguous parts. +/// +/// A partitioning is constructed from a range defined by a pair of iterators. +/// The range must remain valid for the lifetime of the partitioning and the iterators given to +/// constructor most not be invalidated. +/// +/// - Invariant: parts_count() >= 1 +template class partitioning { +public: + using iterator = Iterator; + using value_type = std::iter_value_t; + using difference_type = std::iter_difference_t; + + /// An instance covering the range [begin, end), having just one part. + constexpr partitioning(Iterator begin, Iterator end); + + /// Returns the number of parts in the partitioning. + [[nodiscard]] + size_t parts_count() const noexcept; + + /// Returns the iterators delimiting the `i`th part. + /// + /// - Precondition: `i < parts_count()` + [[nodiscard]] + std::pair part(size_t i) const noexcept; + + /// Returns `true` if the `i`th part is empty. + [[nodiscard]] + bool is_part_empty(size_t i) const noexcept; + + /// Returns the size of the `i`th part. + /// + /// Complexity: O(1) for random access iterators, O(n) otherwise. + [[nodiscard]] + size_t part_size(size_t i) const; + + /// Increases the size of the `i`th part by moving its end + /// boundary forward by one element, and decreasing the size of the next part. + /// + /// - Precondition: `i + 1 < parts_count()` + /// - Precondition: !is_part_empty(i + 1) + void grow(size_t i); + + /// Increases the size of the `i`th part by moving its end + /// boundary forward by `n` elements, and decreasing the size of the next part. + /// + /// - Precondition: `i + 1 < parts_count()` + /// - Precondition: `part_size(i + 1) >= n` + /// - Complexity: O(n) for forward iterators, O(1) for random access iterators + void grow_by(size_t i, size_t n); + + /// Transfers all the elements of `i`th part to `i-1`th part, making the former empty. + /// + /// - Precondition: `0 < i < parts_count()` + void transfer_to_prev(size_t i); + + /// Transfers all the elements of `i`th part to `i+1`th part, making the former empty. + /// + /// - Precondition: `i < parts_count() - 1` + void transfer_to_next(size_t i); + + /// Adds a new empty part at the end of the `i`th part. + void add_part_end(size_t i); + + /// Adds a new empty part at the beginning of the `i`th part. + void add_part_begin(size_t i); + + /// Adds `count` new empty parts at the end of the `i`th part. + void add_parts_end(size_t i, size_t count); + + /// Adds `count` new empty parts at the beginning of the `i`th part. + void add_parts_begin(size_t i, size_t count); + + /// Removes the `i`th part, growing the previous part to cover its range. + /// + /// - Precondition: `0 < i < parts_count()` + void remove_part(size_t i); + + /// Decreases the size of the `i`th part by moving its end boundary back by one element, and + /// increasing the size of the next part. + /// + /// - Precondition: `i + 1 < parts_count()` + /// - Precondition: !is_part_empty(i) + void shrink(size_t i) + requires std::bidirectional_iterator; + + /// Decreases the size of the `i`th part by moving its end + /// boundary back by `n` elements, and increasing the size of the next part. + /// + /// - Precondition: `i + 1 < parts_count()` + /// - Precondition: `part_size(i) >= n` + /// - Complexity: O(n) for bidirectional iterators, O(1) for random access iterators + void shrink_by(size_t i, size_t n) + requires std::bidirectional_iterator; + +private: + /// The boundaries of each part in the partitioning. + /// + /// The first element is the begin iterator of the range, and the last + /// element is the end iterator of the underlying range. + std::vector boundaries_{}; +}; + +// Inline definitions + +template +inline constexpr partitioning::partitioning(Iterator begin, Iterator end) { + boundaries_.reserve(10); + boundaries_.emplace_back(std::move(begin)); + boundaries_.emplace_back(std::move(end)); +} + +template +inline size_t partitioning::parts_count() const noexcept { + return boundaries_.size() - 1; +} + +template +inline std::pair partitioning::part(size_t i) const noexcept { + PRECONDITION(i < parts_count()); + return {boundaries_[i], boundaries_[i + 1]}; +} + +template +inline bool partitioning::is_part_empty(size_t i) const noexcept { + PRECONDITION(i < parts_count()); + auto [begin, end] = part(i); + return begin == end; +} + +template +inline size_t partitioning::part_size(size_t i) const { + PRECONDITION(i < parts_count()); + return std::distance(boundaries_[i], boundaries_[i + 1]); +} + +template inline void partitioning::grow(size_t i) { + PRECONDITION(i + 1 < parts_count()); + PRECONDITION(!is_part_empty(i + 1)); + boundaries_[i + 1]++; +} + +template +inline void partitioning::grow_by(size_t i, size_t n) { + PRECONDITION(i + 1 < parts_count()); + + if constexpr (std::random_access_iterator) { + // For random access iterators, we can check size and advance in O(1) + auto [begin, end] = part(i + 1); + PRECONDITION(static_cast(std::distance(begin, end)) >= n); + boundaries_[i + 1] += n; + } else { + // For forward iterators, we need to check and advance step by step + for (size_t k = 0; k < n; ++k) { + PRECONDITION(!is_part_empty(i + 1)); + boundaries_[i + 1]++; + } + } +} + +template +inline void partitioning::transfer_to_prev(size_t i) { + PRECONDITION(0 < i); + PRECONDITION(i < parts_count()); + // Transfer all elements to previous part by moving the boundary between them + boundaries_[i] = boundaries_[i + 1]; +} + +template +inline void partitioning::transfer_to_next(size_t i) { + PRECONDITION(i < parts_count() - 1); + // Transfer all elements to next part by moving the boundary between them + boundaries_[i + 1] = boundaries_[i]; +} + +template +inline void partitioning::add_part_end(size_t i) { + PRECONDITION(i < parts_count()); + boundaries_.insert(boundaries_.begin() + i + 1, boundaries_[i + 1]); +} + +template +inline void partitioning::add_part_begin(size_t i) { + PRECONDITION(i < parts_count()); + boundaries_.insert(boundaries_.begin() + i, boundaries_[i]); +} + +template +inline void partitioning::add_parts_end(size_t i, size_t count) { + PRECONDITION(i < parts_count()); + boundaries_.insert(boundaries_.begin() + i + 1, count, boundaries_[i + 1]); +} + +template +inline void partitioning::add_parts_begin(size_t i, size_t count) { + PRECONDITION(i < parts_count()); + boundaries_.insert(boundaries_.begin() + i, count, boundaries_[i]); +} + +template +inline void partitioning::remove_part(size_t i) { + PRECONDITION(i < parts_count()); + boundaries_.erase(boundaries_.begin() + i); +} + +template +inline void partitioning::shrink(size_t i) + requires std::bidirectional_iterator +{ + PRECONDITION(i + 1 < parts_count()); + PRECONDITION(!is_part_empty(i)); + boundaries_[i + 1]--; +} + +template +inline void partitioning::shrink_by(size_t i, size_t n) + requires std::bidirectional_iterator { + PRECONDITION(i + 1 < parts_count()); - template - struct partitioning - { - Iterator begin; - Iterator end; - // TODO - }; + if constexpr (std::random_access_iterator) { + // For random access iterators, we can check size and advance in O(1) + auto [begin, end] = part(i); + PRECONDITION(static_cast(std::distance(begin, end)) >= n); + boundaries_[i + 1] -= n; + } else { + // For bidirectional iterators, we need to check and advance step by step + for (size_t k = 0; k < n; ++k) { + PRECONDITION(!is_part_empty(i)); + boundaries_[i + 1]--; + } + } +} } // namespace positionless diff --git a/test/algorithms_tests.cpp b/test/algorithms_tests.cpp new file mode 100644 index 0000000..edac05d --- /dev/null +++ b/test/algorithms_tests.cpp @@ -0,0 +1,220 @@ +#include "positionless/algorithms.hpp" + +#include "detail/rapidcheck_wrapper.hpp" +#include "detail/vector_partitioning.hpp" + +#include + +using positionless::partitioning; +using positionless::swap_first; + +TEST_PROPERTY( + "`swap_first` swaps the first elements of two parts", + [](vector_partitioning vp) { + RC_PRE(vp.partitioning_.parts_count() >= size_t{2}); + + // Find all non-empty parts + std::vector non_empty_parts; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + if (!vp.partitioning_.is_part_empty(idx)) { + non_empty_parts.push_back(idx); + } + } + RC_PRE(non_empty_parts.size() >= size_t{2}); + + // Generate two distinct non-empty part indices + const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto i = non_empty_parts[i_idx]; + const auto j = non_empty_parts[j_idx]; + + // Get the original first elements + const auto part_i = vp.partitioning_.part(i); + const auto part_j = vp.partitioning_.part(j); + const auto original_first_i = *part_i.first; + const auto original_first_j = *part_j.first; + + // Perform the swap + swap_first(vp.partitioning_, i, j); + + // Verify the swap occurred + const auto after_part_i = vp.partitioning_.part(i); + const auto after_part_j = vp.partitioning_.part(j); + + if (i == j) { + // Swapping with itself should leave it unchanged + RC_ASSERT(*after_part_i.first == original_first_i); + } else { + // First elements should be swapped + RC_ASSERT(*after_part_i.first == original_first_j); + RC_ASSERT(*after_part_j.first == original_first_i); + } + } +); + +TEST_PROPERTY( + "`swap_first` can be used multiple times without error", + [](vector_partitioning vp) { + RC_PRE(vp.partitioning_.parts_count() >= size_t{2}); + + // Find all non-empty parts + std::vector non_empty_parts; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + if (!vp.partitioning_.is_part_empty(idx)) { + non_empty_parts.push_back(idx); + } + } + RC_PRE(non_empty_parts.size() >= size_t{2}); + + // Generate multiple swap operations + const auto num_swaps = *rc::gen::inRange(1, 20); + + for (int k = 0; k < num_swaps; ++k) { + // Pick two random non-empty parts + const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto i = non_empty_parts[i_idx]; + const auto j = non_empty_parts[j_idx]; + + // This should not throw or violate any invariants + swap_first(vp.partitioning_, i, j); + } + + // Verify the partitioning is still valid + RC_ASSERT(vp.partitioning_.parts_count() >= size_t{1}); + + // Verify parts still cover the entire data + size_t total_size = 0; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + total_size += vp.partitioning_.part_size(idx); + } + RC_ASSERT(total_size == vp.data_.size()); + } +); + +TEST_PROPERTY("`swap_first` on the same part should be a no-op", [](vector_partitioning vp) { + RC_PRE(vp.partitioning_.parts_count() >= size_t{1}); + + // Find a non-empty part + std::vector non_empty_parts; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + if (!vp.partitioning_.is_part_empty(idx)) { + non_empty_parts.push_back(idx); + } + } + RC_PRE(non_empty_parts.size() >= size_t{1}); + + // Pick a random non-empty part + const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto i = non_empty_parts[i_idx]; + + // Save original data + const auto original_data = vp.data_; + + // Swap part with itself + swap_first(vp.partitioning_, i, i); + + // Verify data is unchanged + RC_ASSERT(vp.data_ == original_data); +}); + +TEST_PROPERTY( + "`swap_first` twice returns to original state (idempotent)", + [](vector_partitioning vp) { + RC_PRE(vp.partitioning_.parts_count() >= size_t{2}); + + // Find all non-empty parts + std::vector non_empty_parts; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + if (!vp.partitioning_.is_part_empty(idx)) { + non_empty_parts.push_back(idx); + } + } + RC_PRE(non_empty_parts.size() >= size_t{2}); + + // Generate two distinct non-empty part indices + const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto i = non_empty_parts[i_idx]; + const auto j = non_empty_parts[j_idx]; + + // Save original data + const auto original_data = vp.data_; + + // Swap twice + swap_first(vp.partitioning_, i, j); + swap_first(vp.partitioning_, i, j); + + // Verify data is back to original state + RC_ASSERT(vp.data_ == original_data); + } +); + +TEST_PROPERTY("`swap_first` preserves data as a permutation", [](vector_partitioning vp) { + RC_PRE(vp.partitioning_.parts_count() >= size_t{2}); + + // Find all non-empty parts + std::vector non_empty_parts; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + if (!vp.partitioning_.is_part_empty(idx)) { + non_empty_parts.push_back(idx); + } + } + RC_PRE(non_empty_parts.size() >= size_t{2}); + + // Generate two distinct non-empty part indices + const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto i = non_empty_parts[i_idx]; + const auto j = non_empty_parts[j_idx]; + + // Save original data + const auto original_data = vp.data_; + + // Perform the swap + swap_first(vp.partitioning_, i, j); + + // Verify it's still a permutation of the original data + RC_ASSERT(std::is_permutation(vp.data_.begin(), vp.data_.end(), original_data.begin())); +}); + +TEST_PROPERTY( + "`swap_first` only modifies first elements of the two parts", + [](vector_partitioning vp) { + RC_PRE(vp.partitioning_.parts_count() >= size_t{2}); + + // Find all non-empty parts + std::vector non_empty_parts; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + if (!vp.partitioning_.is_part_empty(idx)) { + non_empty_parts.push_back(idx); + } + } + RC_PRE(non_empty_parts.size() >= size_t{2}); + + // Generate two part indices + const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto i = non_empty_parts[i_idx]; + const auto j = non_empty_parts[j_idx]; + + // Save all elements except the first from each part (if they have more than 1 element) + const auto part_i = vp.partitioning_.part(i); + const auto part_j = vp.partitioning_.part(j); + std::vector rest_of_i(part_i.first + 1, part_i.second); + std::vector rest_of_j(part_j.first + 1, part_j.second); + + // Perform the swap + swap_first(vp.partitioning_, i, j); + + // Verify non-first elements remain unchanged + const auto after_part_i = vp.partitioning_.part(i); + const auto after_part_j = vp.partitioning_.part(j); + + std::vector new_rest_of_i(after_part_i.first + 1, after_part_i.second); + std::vector new_rest_of_j(after_part_j.first + 1, after_part_j.second); + + RC_ASSERT(new_rest_of_i == rest_of_i); + RC_ASSERT(new_rest_of_j == rest_of_j); + } +); diff --git a/test/detail/partitioning_generators.hpp b/test/detail/partitioning_generators.hpp new file mode 100644 index 0000000..2764580 --- /dev/null +++ b/test/detail/partitioning_generators.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "positionless/partitioning.hpp" +#include "rapidcheck_wrapper.hpp" + +#include +#include +#include + +namespace testgen { + +/// Generates `k` parts to fill up `n` elements, and returns the sizes of the first `k-1` parts. +inline std::vector generate_partition_sizes(size_t n, size_t k) { + if (k == 0) + return {}; + if (k == 1) + return {n}; + std::vector r = + *rc::gen::container>(k - 1, rc::gen::inRange(0, n + 1)); + r.push_back(0); + r.push_back(n); + std::sort(r.begin(), r.end()); + std::adjacent_difference(r.begin(), r.end(), r.begin()); + r.erase(r.begin()); + r.pop_back(); + for (size_t s : r) { + PRECONDITION(s <= n); + } + return r; // size k-1; the last part will be inferred +} + +/// Generate an arbitrary split into parts for partitioning `p`. +/// +/// Precondition: `p.parts_count() == 1` +template auto generate_splits(positionless::partitioning& p) { + PRECONDITION(p.parts_count() == 1); + const size_t n = static_cast(std::distance(p.part(0).first, p.part(0).second)); + const size_t maxK = std::max(1, n == 0 ? 4 : std::min(n, 8)); + const size_t k = *rc::gen::inRange(1, maxK + 1); + + if (k > 1) { + auto sizes = generate_partition_sizes(n, k); + for (size_t part_len : sizes) { + p.add_part_begin(p.parts_count() - 1); + p.grow_by(p.parts_count() - 2, part_len); + } + } + PRECONDITION(p.parts_count() == k); +} + +} // namespace testgen diff --git a/test/detail/rapidcheck_wrapper.hpp b/test/detail/rapidcheck_wrapper.hpp new file mode 100644 index 0000000..b43dade --- /dev/null +++ b/test/detail/rapidcheck_wrapper.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include +#include +#include +#include + +/// Defines a test case that uses RapidCheck to check a property. +#define TEST_PROPERTY(name, body) \ + TEST_CASE(name) { CHECK(rc::check(name, body)); } diff --git a/test/detail/vector_partitioning.hpp b/test/detail/vector_partitioning.hpp new file mode 100644 index 0000000..89d595c --- /dev/null +++ b/test/detail/vector_partitioning.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "partitioning_generators.hpp" +#include "positionless/partitioning.hpp" +#include "rapidcheck_wrapper.hpp" + +#include +#include +#include + +// A `std::vector` with a partitioning over its elements. +template struct vector_partitioning { + using container_t = std::vector; + using iterator_t = typename container_t::iterator; + + /// The underlying data. + container_t data_; + /// The partitioning over the data. + positionless::partitioning partitioning_; + + /// An instance with `k` parts over the elements of `d`. + explicit vector_partitioning(container_t d, size_t k = 1) + : data_(std::move(d)), partitioning_(data_.begin(), data_.end()) { + if (k > 1) { + partitioning_.add_parts_begin(0, k - 1); + } + } +}; + +namespace rc { + +/// RapidCheck generator for vector_partitioning +template struct Arbitrary> { + static Gen> arbitrary() { + return gen::exec([]() { + const auto n = *gen::inRange(0, 64); + auto data = *gen::container>(n, gen::arbitrary()); + vector_partitioning r(std::move(data)); + testgen::generate_splits(r.partitioning_); + return r; + }); + } +}; + +} // namespace rc diff --git a/test/partitioning_tests.cpp b/test/partitioning_tests.cpp index 092e5b4..2c234f0 100644 --- a/test/partitioning_tests.cpp +++ b/test/partitioning_tests.cpp @@ -1,6 +1,513 @@ #include "positionless/partitioning.hpp" -#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN -#include +#include "detail/rapidcheck_wrapper.hpp" +#include "detail/vector_partitioning.hpp" -TEST_CASE("Example test") { CHECK(1 + 1 == 2); } +#include +#include +#include + +using positionless::partitioning; + +TEST_PROPERTY("parts of a partitioning cover the entire data", [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + size_t sum = 0; + for (size_t i = 0; i < k; ++i) { + sum += vp.partitioning_.part_size(i); + } + RC_ASSERT(sum == vp.data_.size()); +}) + +TEST_PROPERTY("partitioning allows accessing all the elements", [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + std::vector reconstructed; + for (size_t i = 0; i < k; ++i) { + const auto part = vp.partitioning_.part(i); + reconstructed.insert(reconstructed.end(), part.first, part.second); + } + RC_ASSERT(reconstructed == vp.data_); +}) + +TEST_PROPERTY( + "partitioning part count matches vector_partitioning", + [](vector_partitioning vp) { RC_ASSERT(vp.partitioning_.parts_count() >= size_t(1)); } +) + +TEST_PROPERTY( + "`is_part_empty` returns true for empty parts, and false otherwise", + [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + for (size_t i = 0; i < k; ++i) { + const bool empty = vp.partitioning_.is_part_empty(i); + const size_t size = vp.partitioning_.part_size(i); + RC_ASSERT(empty == (size == 0)); + } + } +) + +TEST_PROPERTY("`grow` increases the size of a part by 1", [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + RC_PRE(k >= size_t(2)); + + // Find a valid index where next part is non-empty + size_t idx = 0; + for (size_t i = 0; i + 1 < k; ++i) { + if (!vp.partitioning_.is_part_empty(i + 1)) { + idx = i; + break; + } + } + RC_PRE(!vp.partitioning_.is_part_empty(idx + 1)); + + const size_t before = vp.partitioning_.part_size(idx); + const size_t next_before = vp.partitioning_.part_size(idx + 1); + vp.partitioning_.grow(idx); + const size_t after = vp.partitioning_.part_size(idx); + const size_t next_after = vp.partitioning_.part_size(idx + 1); + + RC_ASSERT(after == before + 1); + RC_ASSERT(next_after == next_before - 1); +}) + +TEST_PROPERTY("`grow_by` increases the size of a part by `n`", [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + RC_PRE(k >= size_t{2}); + + // Find a valid index where next part is non-empty + size_t idx = 0; + size_t max_grow = 0; + for (size_t i = 0; i + 1 < k; ++i) { + const size_t next_size = vp.partitioning_.part_size(i + 1); + if (next_size > max_grow) { + idx = i; + max_grow = next_size; + } + } + RC_PRE(max_grow > size_t(0)); + + const size_t n = *rc::gen::inRange(1, max_grow + 1); + const size_t before = vp.partitioning_.part_size(idx); + const size_t next_before = vp.partitioning_.part_size(idx + 1); + + vp.partitioning_.grow_by(idx, n); + + const size_t after = vp.partitioning_.part_size(idx); + const size_t next_after = vp.partitioning_.part_size(idx + 1); + + RC_ASSERT(after == before + n); + RC_ASSERT(next_after == next_before - n); +}) + +TEST_PROPERTY("`shrink` decreases the size of a part by 1", [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + RC_PRE(k >= size_t(2)); + + // Find a valid index where current part is non-empty + size_t idx = 0; + for (size_t i = 0; i + 1 < k; ++i) { + if (!vp.partitioning_.is_part_empty(i)) { + idx = i; + break; + } + } + RC_PRE(!vp.partitioning_.is_part_empty(idx)); + + const size_t before = vp.partitioning_.part_size(idx); + const size_t next_before = vp.partitioning_.part_size(idx + 1); + vp.partitioning_.shrink(idx); + const size_t after = vp.partitioning_.part_size(idx); + const size_t next_after = vp.partitioning_.part_size(idx + 1); + + RC_ASSERT(after == before - 1); + RC_ASSERT(next_after == next_before + 1); +}) + +TEST_PROPERTY("`shrink_by` decreases the size of a part by `n`", [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + RC_PRE(k >= size_t{2}); + + // Find a valid index where current part is non-empty + size_t idx = 0; + size_t max_shrink = 0; + for (size_t i = 0; i + 1 < k; ++i) { + const size_t size = vp.partitioning_.part_size(i); + if (size > max_shrink) { + idx = i; + max_shrink = size; + } + } + RC_PRE(max_shrink > size_t(0)); + + const size_t n = *rc::gen::inRange(1, max_shrink + 1); + const size_t before = vp.partitioning_.part_size(idx); + const size_t next_before = vp.partitioning_.part_size(idx + 1); + + vp.partitioning_.shrink_by(idx, n); + + const size_t after = vp.partitioning_.part_size(idx); + const size_t next_after = vp.partitioning_.part_size(idx + 1); + + RC_ASSERT(after == before - n); + RC_ASSERT(next_after == next_before + n); +}) + +TEST_PROPERTY( + "`grow` followed by `shrink` returns to original state", + [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + RC_PRE(k >= size_t{2}); + + // Find a valid index where next part is non-empty + size_t idx = 0; + for (size_t i = 0; i + 1 < k; ++i) { + if (!vp.partitioning_.is_part_empty(i + 1)) { + idx = i; + break; + } + } + RC_PRE(!vp.partitioning_.is_part_empty(idx + 1)); + + const size_t before = vp.partitioning_.part_size(idx); + const size_t next_before = vp.partitioning_.part_size(idx + 1); + + vp.partitioning_.grow(idx); + vp.partitioning_.shrink(idx); + + const size_t after = vp.partitioning_.part_size(idx); + const size_t next_after = vp.partitioning_.part_size(idx + 1); + + RC_ASSERT(after == before); + RC_ASSERT(next_after == next_before); + } +) + +TEST_PROPERTY( + "`shrink_by` is equivalent to calling `shrink` `n` times", + [](vector_partitioning vp) { + auto copy1 = vp; + auto copy2 = vp; + const size_t k = copy1.partitioning_.parts_count(); + RC_PRE(k >= size_t{2}); + + // Find a valid index where current part is non-empty + size_t idx = 0; + size_t max_shrink = 0; + for (size_t i = 0; i + 1 < k; ++i) { + const size_t size = copy1.partitioning_.part_size(i); + if (size > max_shrink) { + idx = i; + max_shrink = size; + } + } + RC_PRE(max_shrink > size_t(0)); + + const size_t n = *rc::gen::inRange(1, max_shrink + 1); + + // Use shrink_by + copy1.partitioning_.shrink_by(idx, n); + + // Use shrink n times + for (size_t i = 0; i < n; ++i) { + copy2.partitioning_.shrink(idx); + } + + RC_ASSERT(copy1.partitioning_.parts_count() == copy2.partitioning_.parts_count()); + for (size_t i = 0; i < copy1.partitioning_.parts_count(); ++i) { + RC_ASSERT(copy1.partitioning_.part_size(i) == copy2.partitioning_.part_size(i)); + } + } +) + +TEST_PROPERTY( + "`transfer_to_prev` transfers all elements of a part to the previous part", + [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + RC_PRE(k >= size_t{2}); + const size_t idx = *rc::gen::inRange(1, k); + + const size_t size_before = vp.partitioning_.part_size(idx); + const size_t prev_size_before = vp.partitioning_.part_size(idx - 1); + vp.partitioning_.transfer_to_prev(idx); + + RC_ASSERT(vp.partitioning_.parts_count() == k); + RC_ASSERT(vp.partitioning_.is_part_empty(idx)); + RC_ASSERT(vp.partitioning_.part_size(idx - 1) == prev_size_before + size_before); + } +) + +TEST_PROPERTY( + "`transfer_to_next` transfers all elements of a part to the next part", + [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + RC_PRE(k >= size_t{2}); + const size_t idx = *rc::gen::inRange(0, k - 1); + + const size_t size_before = vp.partitioning_.part_size(idx); + const size_t next_size_before = vp.partitioning_.part_size(idx + 1); + vp.partitioning_.transfer_to_next(idx); + + RC_ASSERT(vp.partitioning_.parts_count() == k); + RC_ASSERT(vp.partitioning_.is_part_empty(idx)); + RC_ASSERT(vp.partitioning_.part_size(idx + 1) == next_size_before + size_before); + } +) + +TEST_PROPERTY( + "`add_part_end` adds a new empty part at the end `part_index`", + [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + const size_t idx = *rc::gen::inRange(0, k); + + const size_t size_before = vp.partitioning_.part_size(idx); + vp.partitioning_.add_part_end(idx); + + RC_ASSERT(vp.partitioning_.parts_count() == k + 1); + RC_ASSERT(vp.partitioning_.part_size(idx) == size_before); + RC_ASSERT(vp.partitioning_.is_part_empty(idx + 1)); + } +) + +TEST_PROPERTY( + "`add_part_begin` adds a new empty part at the begin `part_index`", + [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + const size_t idx = *rc::gen::inRange(0, k); + + const size_t size_before = vp.partitioning_.part_size(idx); + vp.partitioning_.add_part_begin(idx); + + RC_ASSERT(vp.partitioning_.parts_count() == k + 1); + RC_ASSERT(vp.partitioning_.is_part_empty(idx)); + RC_ASSERT(vp.partitioning_.part_size(idx + 1) == size_before); + } +) + +TEST_PROPERTY( + "`add_parts_end` adds `n` empty parts at the end `part_index`", + [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + const size_t idx = *rc::gen::inRange(0, k); + const size_t n = *rc::gen::inRange(1, 6); + + const size_t size_before = vp.partitioning_.part_size(idx); + vp.partitioning_.add_parts_end(idx, n); + + RC_ASSERT(vp.partitioning_.parts_count() == k + n); + RC_ASSERT(vp.partitioning_.part_size(idx) == size_before); + for (size_t i = 1; i <= n; ++i) { + RC_ASSERT(vp.partitioning_.is_part_empty(idx + i)); + } + } +) + +TEST_PROPERTY( + "`add_parts_begin` adds `n` empty parts at the begin `part_index`", + [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + const size_t idx = *rc::gen::inRange(0, k); + const size_t n = *rc::gen::inRange(1, 6); + + const size_t size_before = vp.partitioning_.part_size(idx); + vp.partitioning_.add_parts_begin(idx, n); + + RC_ASSERT(vp.partitioning_.parts_count() == k + n); + for (size_t i = 0; i < n; ++i) { + RC_ASSERT(vp.partitioning_.is_part_empty(idx + i)); + } + RC_ASSERT(vp.partitioning_.part_size(idx + n) == size_before); + } +) + +TEST_PROPERTY("`remove_part` decreases the number of parts by 1", [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + RC_PRE(k >= size_t{2}); + + const size_t idx = *rc::gen::inRange(1, k); + vp.partitioning_.remove_part(idx); + + RC_ASSERT(vp.partitioning_.parts_count() == k - 1); +}) + +TEST_PROPERTY( + "`remove_part` transfer all the elements of `part_index` to part `part_index-1`", + [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + RC_PRE(k >= size_t{2}); + + const size_t idx = *rc::gen::inRange(1, k); + const size_t expected_size = + vp.partitioning_.part_size(idx - 1) + vp.partitioning_.part_size(idx); + + vp.partitioning_.remove_part(idx); + + RC_ASSERT(vp.partitioning_.part_size(idx - 1) == expected_size); + } +) + +TEST_PROPERTY( + "`add_parts_end` is equivalent to calling `add_part_end` `n` times", + [](vector_partitioning vp) { + auto copy1 = vp; + auto copy2 = vp; + const size_t k = copy1.partitioning_.parts_count(); + const size_t idx = *rc::gen::inRange(0, k); + const size_t n = *rc::gen::inRange(1, 6); + + // Use add_parts_end + copy1.partitioning_.add_parts_end(idx, n); + + // Use add_part_end n times + for (size_t i = 0; i < n; ++i) { + copy2.partitioning_.add_part_end(idx); + } + + RC_ASSERT(copy1.partitioning_.parts_count() == copy2.partitioning_.parts_count()); + for (size_t i = 0; i < copy1.partitioning_.parts_count(); ++i) { + RC_ASSERT(copy1.partitioning_.part_size(i) == copy2.partitioning_.part_size(i)); + } + } +) + +TEST_PROPERTY( + "`add_parts_begin` is equivalent to calling `add_part_begin` `n` times", + [](vector_partitioning vp) { + auto copy1 = vp; + auto copy2 = vp; + const size_t k = copy1.partitioning_.parts_count(); + const size_t idx = *rc::gen::inRange(0, k); + const size_t n = *rc::gen::inRange(1, 6); + + // Use add_parts_begin + copy1.partitioning_.add_parts_begin(idx, n); + + // Use add_part_begin n times + for (size_t i = 0; i < n; ++i) { + copy2.partitioning_.add_part_begin(idx); + } + + RC_ASSERT(copy1.partitioning_.parts_count() == copy2.partitioning_.parts_count()); + for (size_t i = 0; i < copy1.partitioning_.parts_count(); ++i) { + RC_ASSERT(copy1.partitioning_.part_size(i) == copy2.partitioning_.part_size(i)); + } + } +) + +TEST_PROPERTY("forward_list partitioning covers entire data", [](std::forward_list data) { + partitioning::iterator> p(data.begin(), data.end()); + testgen::generate_splits(p); + + const size_t k = p.parts_count(); + size_t sum = 0; + for (size_t i = 0; i < k; ++i) { + sum += p.part_size(i); + } + RC_ASSERT(static_cast(sum) == std::distance(data.begin(), data.end())); +}) + +TEST_PROPERTY( + "can use all operations on a forward list partitioning", + [](std::forward_list data) { + partitioning::iterator> p(data.begin(), data.end()); + testgen::generate_splits(p); + + const size_t k = p.parts_count(); + RC_PRE(k >= size_t{2}); + const size_t i = *rc::gen::inRange(0, k - 1); + + // We just want to check that all operations are correctly instantiated. + + (void)p.part(i); + (void)p.is_part_empty(i); + (void)p.part_size(i); // O(N) complexity + + if (!p.is_part_empty(i + 1)) { + p.grow(i); + } + if (!p.is_part_empty(i + 1)) { + p.grow_by(i, 1); + } + p.add_part_end(i); + p.add_part_begin(i); + p.add_parts_end(i, 2); + p.add_parts_begin(i, 2); + p.remove_part(i); + } +) + +TEST_PROPERTY("bidirectional list partitioning covers entire data", [](std::list data) { + partitioning::iterator> p(data.begin(), data.end()); + testgen::generate_splits(p); + + const size_t k = p.parts_count(); + size_t sum = 0; + for (size_t i = 0; i < k; ++i) { + sum += p.part_size(i); + } + RC_ASSERT(sum == data.size()); +}) + +TEST_PROPERTY( + "can use all operations on a bidirectional list partitioning", + [](std::list data) { + partitioning::iterator> p(data.begin(), data.end()); + testgen::generate_splits(p); + + const size_t k = p.parts_count(); + RC_PRE(k >= size_t{2}); + const size_t i = *rc::gen::inRange(0, k - 1); + + // We just want to check that all operations are correctly instantiated. + + (void)p.part(i); + (void)p.is_part_empty(i); + (void)p.part_size(i); // O(N) complexity + + if (!p.is_part_empty(i + 1)) { + p.grow(i); + } + if (!p.is_part_empty(i + 1)) { + p.grow_by(i, 1); + } + if (!p.is_part_empty(i)) { + p.shrink(i); + } + if (!p.is_part_empty(i)) { + p.shrink_by(i, 1); + } + p.add_part_end(i); + p.add_part_begin(i); + p.add_parts_end(i, 2); + p.add_parts_begin(i, 2); + p.remove_part(i); + } +) + +TEST_PROPERTY( + "bidirectional list: growing a part increases its size by 1", + [](std::list data) { + partitioning::iterator> p(data.begin(), data.end()); + testgen::generate_splits(p); + const size_t k = p.parts_count(); + RC_PRE(k >= size_t{2}); + const size_t i = *rc::gen::inRange(0, k - 1); + RC_PRE(!p.is_part_empty(i + 1)); + const size_t old_size = p.part_size(i); + p.grow(i); + RC_ASSERT(p.part_size(i) == old_size + 1); + } +) + +TEST_PROPERTY( + "bidirectional list: shrinking a part decreases its size by 1", + [](std::list data) { + partitioning::iterator> p(data.begin(), data.end()); + testgen::generate_splits(p); + const size_t k = p.parts_count(); + RC_PRE(k >= size_t{2}); + const size_t i = *rc::gen::inRange(0, k - 1); + RC_PRE(!p.is_part_empty(i)); + const size_t old_size = p.part_size(i); + p.shrink(i); + RC_ASSERT(p.part_size(i) == old_size - 1); + } +) diff --git a/test/tests_main.cpp b/test/tests_main.cpp new file mode 100644 index 0000000..0a3f254 --- /dev/null +++ b/test/tests_main.cpp @@ -0,0 +1,2 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include