From ac01635f461bd9f211d89f6eea833d9d928fd113 Mon Sep 17 00:00:00 2001 From: Luca Di Sera Date: Fri, 12 May 2023 15:12:11 +0200 Subject: QDoc: Move catch_generators under src MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QDoc employs a support library, "catch_generators", to provide custom support for the data-generation that certain tests use or might want to use when using the Catch2 testing framework, which QDoc uses for some of its tests. "catch_generators" was currently kept under "tests/auto/qdoc" and its headers were included and used directly by related tests by relative paths. Due to a certain restructuring that is happening in QDoc, with one of the goals, among others, being to increase the locality of QDoc-related code under "src/qdoc", the "catch_generators" support library is now moved under "src/qdoc". To allow code that depended on it to keep their usages without requiring the addition of some relative paths from the "tests" directory to the "src" directory, a library target, "Qt::QDocCatchGeneratorsPrivate" was created for it. The target can be linked-to to gain access to the previously-directly-used headers, with include path "catch_generators/.*". To allow for this specific include path to work, the internal directory structure for "catch_generators" was slightly modified with the addition of some intermediate directories in between its root and the library headers. The root "CMakeLists.txt" file for the QDoc project was modified to add the moved "catch_generators" as a subdirectory, to include it in the build process. "catch_generators" contains its own test code under the subdirectory "tests". Previously the test target in "tests" included the relevant headers from the library by use of relative paths to its sources. Now that the library is exported as a target, the "CMakeLists.txt" file under "tests" was modified to rely on linking to the library itself to obtain access to the required headers. Similarly, targets under "tests/auto/qdoc" that used the dependency now link to it instead. Hence, their "CMakeLists.txt" files were modified to avoid using the dependency as an include directory and instead use "Qt::QDocCatcGeneratorsPrivate" as a library. The inclusions of the "catch_generators" headers in relevant sources was modified to respect the new "catch_generators/.*" path. Additionally, "catch_generators" exposed a top level header, "qdoc_catch_generators.h", including certain headers in the library that were expected to be commonly used by tests, so that the consumers did not have to include multiple headers when using various common generators. The header was removed in favor of including the library headers themselves now that they have a well-defined root provided by the exposed target. Sources that included the header were modified to include only their relevant headers from "catch_generators", respecting the additional granularity. Due to the Qt Project CI performing discovery of tests by configuring only the "tests" directory of the module; the moved "catch_generators", which contained its own testing code, will now escape the automated testing in CI for its own tests, as the relevant target does not reside under the "tests" directory of the repository. To avoid this issue, the "CMakeLists.txt" file under "tests/auto/qdoc", was modified to `include` the "CMakeLists.txt" file that describes "catch_generators"' test target, so as to "mirror" the test under the "tests" directory. To support working when included, the "CMakeLists.txt" file that describes "catch_generators"' test target was modified to refer to any relevant relative path by prefixing the value of `CMAKE_CURRENT_LIST_DIR`, so that the relative paths would not be incorrectly resolved when using `include`. Due to this "mirroring", the relevant test target would be configured twice when the whole repository was configured, resulting in an error. To ensure that this is not the case, the `include` directive was hidden behind `QT_BUILD_STANADALONE_TESTS`, which is not set during a general configuration of the repository but is set when CI performs test-discovery on the "tests" directory. Similarly, the "tests" directory of a repository is configured only when `QT_BUILD_TESTS` is `ON`, which further ensures that the required dependencies, such as `Qt::Test`, will be found before the directory is configured. By having moved the test declaration outside of the `tests` directory, the test will always be configured, failing to find its dependencies when this happens. To avoid the issue, the `add_subdirectory(tests)` directive in `src/qdoc/catch_generators/CMakeLists.txt` was conditioned over `QT_BUILD_TESTS`. Certain files that were missing an license header were enhanced with it, as the lack of an header will now be an issue outside the "tests" directory. Change-Id: I0bca477b7da75c121c24b8528fc9defa26a6123e Reviewed-by: Topi Reiniƶ --- src/qdoc/CMakeLists.txt | 1 + src/qdoc/catch_generators/CMakeLists.txt | 14 + .../generators/combinators/cycle_generator.h | 80 ++ .../generators/combinators/oneof_generator.h | 185 +++++ .../generators/k_partition_of_r_generator.h | 113 +++ .../catch_generators/generators/path_generator.h | 853 +++++++++++++++++++++ .../catch_generators/generators/qchar_generator.h | 110 +++ .../generators/qstring_generator.h | 92 +++ .../src/catch_generators/namespaces.h | 14 + .../utilities/semantics/copy_value.h | 26 + .../utilities/semantics/generator_handler.h | 97 +++ .../utilities/semantics/move_into_vector.h | 62 ++ .../utilities/statistics/distribution.h | 158 ++++ .../utilities/statistics/percentages.h | 49 ++ src/qdoc/catch_generators/tests/CMakeLists.txt | 20 + .../catch_k_partition_of_r_generator.cpp | 41 + .../tests/generators/catch_path_generator.cpp | 755 ++++++++++++++++++ .../tests/generators/catch_qchar_generator.cpp | 102 +++ .../tests/generators/catch_qstring_generator.cpp | 89 +++ .../combinators/catch_cycle_generator.cpp | 70 ++ .../combinators/catch_oneof_generator.cpp | 362 +++++++++ src/qdoc/catch_generators/tests/main.cpp | 13 + .../semantics/catch_generator_handler.cpp | 28 + tests/auto/qdoc/CMakeLists.txt | 6 +- tests/auto/qdoc/catch_generators/CMakeLists.txt | 1 - .../qdoc/catch_generators/qdoc_catch_generators.h | 5 - .../src/generators/combinators/cycle_generator.h | 80 -- .../src/generators/combinators/oneof_generator.h | 185 ----- .../src/generators/k_partition_of_r_generator.h | 113 --- .../src/generators/path_generator.h | 853 --------------------- .../src/generators/qchar_generator.h | 110 --- .../src/generators/qstring_generator.h | 92 --- tests/auto/qdoc/catch_generators/src/namespaces.h | 11 - .../src/utilities/semantics/copy_value.h | 26 - .../src/utilities/semantics/generator_handler.h | 97 --- .../src/utilities/semantics/move_into_vector.h | 62 -- .../src/utilities/statistics/distribution.h | 158 ---- .../src/utilities/statistics/percentages.h | 49 -- .../qdoc/catch_generators/tests/CMakeLists.txt | 21 - .../catch_k_partition_of_r_generator.cpp | 41 - .../tests/generators/catch_path_generator.cpp | 755 ------------------ .../tests/generators/catch_qchar_generator.cpp | 102 --- .../tests/generators/catch_qstring_generator.cpp | 89 --- .../combinators/catch_cycle_generator.cpp | 70 -- .../combinators/catch_oneof_generator.cpp | 362 --------- tests/auto/qdoc/catch_generators/tests/main.cpp | 9 - .../semantics/catch_generator_handler.cpp | 28 - tests/auto/qdoc/qdoc/CMakeLists.txt | 2 +- .../boundaries/filesystem/catch_directorypath.cpp | 2 +- .../qdoc/boundaries/filesystem/catch_filepath.cpp | 2 +- .../qdoc/qdoc/filesystem/catch_fileresolver.cpp | 2 +- 51 files changed, 3343 insertions(+), 3324 deletions(-) create mode 100644 src/qdoc/catch_generators/CMakeLists.txt create mode 100644 src/qdoc/catch_generators/src/catch_generators/generators/combinators/cycle_generator.h create mode 100644 src/qdoc/catch_generators/src/catch_generators/generators/combinators/oneof_generator.h create mode 100644 src/qdoc/catch_generators/src/catch_generators/generators/k_partition_of_r_generator.h create mode 100644 src/qdoc/catch_generators/src/catch_generators/generators/path_generator.h create mode 100644 src/qdoc/catch_generators/src/catch_generators/generators/qchar_generator.h create mode 100644 src/qdoc/catch_generators/src/catch_generators/generators/qstring_generator.h create mode 100644 src/qdoc/catch_generators/src/catch_generators/namespaces.h create mode 100644 src/qdoc/catch_generators/src/catch_generators/utilities/semantics/copy_value.h create mode 100644 src/qdoc/catch_generators/src/catch_generators/utilities/semantics/generator_handler.h create mode 100644 src/qdoc/catch_generators/src/catch_generators/utilities/semantics/move_into_vector.h create mode 100644 src/qdoc/catch_generators/src/catch_generators/utilities/statistics/distribution.h create mode 100644 src/qdoc/catch_generators/src/catch_generators/utilities/statistics/percentages.h create mode 100644 src/qdoc/catch_generators/tests/CMakeLists.txt create mode 100644 src/qdoc/catch_generators/tests/generators/catch_k_partition_of_r_generator.cpp create mode 100644 src/qdoc/catch_generators/tests/generators/catch_path_generator.cpp create mode 100644 src/qdoc/catch_generators/tests/generators/catch_qchar_generator.cpp create mode 100644 src/qdoc/catch_generators/tests/generators/catch_qstring_generator.cpp create mode 100644 src/qdoc/catch_generators/tests/generators/combinators/catch_cycle_generator.cpp create mode 100644 src/qdoc/catch_generators/tests/generators/combinators/catch_oneof_generator.cpp create mode 100644 src/qdoc/catch_generators/tests/main.cpp create mode 100644 src/qdoc/catch_generators/tests/utilities/semantics/catch_generator_handler.cpp delete mode 100644 tests/auto/qdoc/catch_generators/CMakeLists.txt delete mode 100644 tests/auto/qdoc/catch_generators/qdoc_catch_generators.h delete mode 100644 tests/auto/qdoc/catch_generators/src/generators/combinators/cycle_generator.h delete mode 100644 tests/auto/qdoc/catch_generators/src/generators/combinators/oneof_generator.h delete mode 100644 tests/auto/qdoc/catch_generators/src/generators/k_partition_of_r_generator.h delete mode 100644 tests/auto/qdoc/catch_generators/src/generators/path_generator.h delete mode 100644 tests/auto/qdoc/catch_generators/src/generators/qchar_generator.h delete mode 100644 tests/auto/qdoc/catch_generators/src/generators/qstring_generator.h delete mode 100644 tests/auto/qdoc/catch_generators/src/namespaces.h delete mode 100644 tests/auto/qdoc/catch_generators/src/utilities/semantics/copy_value.h delete mode 100644 tests/auto/qdoc/catch_generators/src/utilities/semantics/generator_handler.h delete mode 100644 tests/auto/qdoc/catch_generators/src/utilities/semantics/move_into_vector.h delete mode 100644 tests/auto/qdoc/catch_generators/src/utilities/statistics/distribution.h delete mode 100644 tests/auto/qdoc/catch_generators/src/utilities/statistics/percentages.h delete mode 100644 tests/auto/qdoc/catch_generators/tests/CMakeLists.txt delete mode 100644 tests/auto/qdoc/catch_generators/tests/generators/catch_k_partition_of_r_generator.cpp delete mode 100644 tests/auto/qdoc/catch_generators/tests/generators/catch_path_generator.cpp delete mode 100644 tests/auto/qdoc/catch_generators/tests/generators/catch_qchar_generator.cpp delete mode 100644 tests/auto/qdoc/catch_generators/tests/generators/catch_qstring_generator.cpp delete mode 100644 tests/auto/qdoc/catch_generators/tests/generators/combinators/catch_cycle_generator.cpp delete mode 100644 tests/auto/qdoc/catch_generators/tests/generators/combinators/catch_oneof_generator.cpp delete mode 100644 tests/auto/qdoc/catch_generators/tests/main.cpp delete mode 100644 tests/auto/qdoc/catch_generators/tests/utilities/semantics/catch_generator_handler.cpp diff --git a/src/qdoc/CMakeLists.txt b/src/qdoc/CMakeLists.txt index 11f947e01..bf3355298 100644 --- a/src/qdoc/CMakeLists.txt +++ b/src/qdoc/CMakeLists.txt @@ -3,6 +3,7 @@ add_subdirectory(catch) add_subdirectory(catch_conversions) +add_subdirectory(catch_generators) if(QT_FEATURE_qdoc AND QT_FEATURE_clangcpp) add_subdirectory(qdoc) diff --git a/src/qdoc/catch_generators/CMakeLists.txt b/src/qdoc/catch_generators/CMakeLists.txt new file mode 100644 index 000000000..aca48734e --- /dev/null +++ b/src/qdoc/catch_generators/CMakeLists.txt @@ -0,0 +1,14 @@ +qt_internal_add_module(QDocCatchGeneratorsPrivate + HEADER_MODULE + EXTERNAL_HEADERS_DIR src +) + +qt_internal_extend_target(QDocCatchGeneratorsPrivate + PUBLIC_INCLUDE_DIRECTORIES + $ + $ +) + +if(QT_BUILD_TESTS) + add_subdirectory(tests) +endif() diff --git a/src/qdoc/catch_generators/src/catch_generators/generators/combinators/cycle_generator.h b/src/qdoc/catch_generators/src/catch_generators/generators/combinators/cycle_generator.h new file mode 100644 index 000000000..b60600747 --- /dev/null +++ b/src/qdoc/catch_generators/src/catch_generators/generators/combinators/cycle_generator.h @@ -0,0 +1,80 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "../../namespaces.h" +#include "../../utilities/semantics/generator_handler.h" + +#include + +#include + +namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE { + namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { + + template + class CycleGenerator : public Catch::Generators::IGenerator { + public: + CycleGenerator(Catch::Generators::GeneratorWrapper&& generator) + : generator{std::move(generator)}, + cache{}, + cache_index{0} + { + // REMARK: We generally handle extracting the first + // value by using an handler, to avoid code + // duplication and the possibility of an error. + // In this specific case, we turn to a more "manual" + // approach as it better models the cache-based + // implementation, removing the need to not increment + // cache_index the first time that next is called. + cache.emplace_back(this->generator.get()); + } + + T const& get() const override { return cache[cache_index]; } + + bool next() override { + if (generator.next()) { + cache.emplace_back(generator.get()); + ++cache_index; + } else { + cache_index = (cache_index + 1) % cache.size(); + } + + return true; + } + + private: + Catch::Generators::GeneratorWrapper generator; + + std::vector cache; + std::size_t cache_index; + }; + + } // end QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE + + /*! + * Returns a generator that behaves like \a generator until \a + * generator is exhausted, repeating the same generation that \a + * generator produced, infinitely, afterwards. + * + * This is generally intended to produce infinite generators from + * finite ones. + * + * For example, consider a generator that produces values based on + * another generator that it owns. + * If the owning generator needs to produce more values that the + * owned generator can support, it might fail at some point. + * By cycling over the owned generator, we can extend the sequence + * of produced values so that enough are generated, in a controlled + * way. + * + * The type T should generally be copyable for this generator to + * work. + */ + template + inline Catch::Generators::GeneratorWrapper cycle(Catch::Generators::GeneratorWrapper&& generator) { + return Catch::Generators::GeneratorWrapper(std::unique_ptr>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::CycleGenerator(std::move(generator)))); + } + +} // end QDOC_CATCH_GENERATORS_ROOT_NAMESPACE diff --git a/src/qdoc/catch_generators/src/catch_generators/generators/combinators/oneof_generator.h b/src/qdoc/catch_generators/src/catch_generators/generators/combinators/oneof_generator.h new file mode 100644 index 000000000..5de9dcb6c --- /dev/null +++ b/src/qdoc/catch_generators/src/catch_generators/generators/combinators/oneof_generator.h @@ -0,0 +1,185 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "../../namespaces.h" +#include "../../utilities/statistics/percentages.h" +#include "../../utilities/semantics/generator_handler.h" + +#include + +#include +#include +#include +#include + +namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE { + namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { + + template + class OneOfGenerator : public Catch::Generators::IGenerator { + public: + OneOfGenerator( + std::vector>&& generators, + const std::vector& weights + ) : generators{std::move(generators)}, + random_engine{std::random_device{}()}, + choice_distribution{weights.cbegin(), weights.cend()} + { + assert(weights.size() == this->generators.size()); + assert(std::reduce(weights.cbegin(), weights.cend()) == Approx(100.0)); + + std::transform( + this->generators.begin(), this->generators.end(), this->generators.begin(), + [](auto& generator){ return QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::handler(std::move(generator)); } + ); + + static_cast(next()); + } + + T const& get() const override { return current_value; } + + bool next() override { + std::size_t generator_index{choice_distribution(random_engine)}; + + if (!generators[generator_index].next()) return false; + current_value = generators[generator_index].get(); + + return true; + } + + private: + std::vector> generators; + + std::mt19937 random_engine; + std::discrete_distribution choice_distribution; + + T current_value; + }; + + } // end QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE + + /*! + * Returns a generator whose set of elements is the union of the + * set of elements of the generators in \a generators. + * + * Each time the generator produces a value, a generator from \a + * generators is randomly chosen to produce the value. + * + * The distribution for the choice is given by \a weights. + * The \e {ith} element in \a weights represent the percentage + * probability of the \e {ith} element of \a generators to be + * chosen. + * + * It follows that the size of \a weights must be the same as the + * size of \a generators. + * + * Furthermore, the sum of elements in \a weights should be a + * hundred. + * + * The generator produces values until a generator that is chosen + * to produce a value is unable to do so. + * The first such generator to do so will stop the generation + * independently of the availability of the other generators. + * + * Similarly, values will be produced as long as the chosen + * generator can produce a value, independently of the other + * generators being exhausted already. + */ + template + inline Catch::Generators::GeneratorWrapper oneof( + std::vector>&& generators, + const std::vector& weights + ) { + return Catch::Generators::GeneratorWrapper(std::unique_ptr>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::OneOfGenerator(std::move(generators), weights))); + } + + + /*! + * Returns a generator whose set of elements is the union of the + * set of elements of the generators in \a generators and in which + * the distribution of the generated elements is uniform over \a + * generators. + * + * Each time the generator produces a value, a generator from \a + * generators is randomly chosen to produce the value. + * + * Each generator from \a generators has the same chance of being + * chosen. + * + * Do note that the distribution over the set of values is not + * necessarily uniform. + * + * The generator produces values until a generator that is chosen + * to produce a value is unable to do so. + * The first such generator to do so will stop the generation + * independently of the availability of the other generators. + * + * Similarly, values will be produced as long as the chosen + * generator can produce a value, independently of the other + * generators being exhausted already. + */ + template + inline Catch::Generators::GeneratorWrapper uniform_oneof( + std::vector>&& generators + ) { + std::vector weights( + generators.size(), + QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::uniform_probability(generators.size()) + ); + return oneof(std::move(generators), std::move(weights)); + } + + /*! + * Returns a generator whose set of elements is the union of the + * set of elements of the generators in \a generators and in which + * the distribution of the generated elements is uniform over the + * elements of \a generators. + * + * The generators in \a generator should have a uniform + * distribution and be finite. + * If the set of elements that the generators in \a generator is + * not disjoint, the distribution will be skewed towards repeated + * elements. + * + * Each time the generator produces a value, a generator from \a + * generators is randomly chosen to produce the value. + * + * Each generator from \a generators has a probability of being + * chosen based on the proportion of the cardinality of the subset + * it produces. + * + * The \e {ith} element of \a amounts should contain the + * cardinality of the set produced by the \e {ith} generator in \a + * generators. + * + * The generator produces values until a generator that is chosen + * to produce a value is unable to do so. + * The first such generator to do so will stop the generation + * independently of the availability of the other generators. + * + * Similarly, values will be produced as long as the chosen + * generator can produce a value, independently of the other + * generators being exhausted already. + */ + template + inline Catch::Generators::GeneratorWrapper uniformly_valued_oneof( + std::vector>&& generators, + const std::vector& amounts + ) { + std::size_t total_amount{std::accumulate(amounts.cbegin(), amounts.cend(), std::size_t{0})}; + + std::vector weights; + weights.reserve(amounts.size()); + + std::transform( + amounts.cbegin(), amounts.cend(), + std::back_inserter(weights), + [total_amount](auto element){ return QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::percent_of(static_cast(element), static_cast(total_amount)); } + ); + + return oneof(std::move(generators), std::move(weights)); + } + +} // end QDOC_CATCH_GENERATORS_ROOT_NAMESPACE diff --git a/src/qdoc/catch_generators/src/catch_generators/generators/k_partition_of_r_generator.h b/src/qdoc/catch_generators/src/catch_generators/generators/k_partition_of_r_generator.h new file mode 100644 index 000000000..832ee2838 --- /dev/null +++ b/src/qdoc/catch_generators/src/catch_generators/generators/k_partition_of_r_generator.h @@ -0,0 +1,113 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "../namespaces.h" + +#include + +#include +#include +#include + +namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE { + namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { + + class KPartitionOfRGenerator : public Catch::Generators::IGenerator> { + public: + KPartitionOfRGenerator(double r, std::size_t k) + : random_engine{std::random_device{}()}, + interval_distribution{0.0, r}, + k{k}, + r{r}, + current_partition(k) + { + assert(r >= 0.0); + assert(k >= 1); + + static_cast(next()); + } + + std::vector const& get() const override { return current_partition; } + + bool next() override { + if (k == 1) current_partition[0] = r; + else { + // REMARK: The following wasn't formally proved + // but is based on intuition. + // It is probably erroneous but is expected to be + // good enough for our case. + + // REMARK: We aim to provide a non skewed + // distribution for the elements of the partition. + // + // The reasoning for this is to ensure that our + // testing surface has a good chance of hitting + // many of the available elements between the many + // runs. + // + // To approximate this, a specific algorithm was chosen. + // The following code can be intuitively seen as doing the following: + // + // Consider an interval [0.0, r] on the real line, where r > 0.0. + // + // k - 1 > 0 elements of the interval are chosen, + // partitioning the interval into disjoint + // sub-intervals. + // + // --------------------------------------------------------------------------------------------------------------------- + // | | | | | + // 0 k_1 k_2 k_3 r + // | | | | | + // _______--------------------_______________________________________________________----------------------------------- + // k_1 - 0 k_2 - k_1 k_3 - k_2 r - k_3 + // p1 p2 p3 p4 + // + // The length of each sub interval is chosen as one of the elements of the partition. + // + // Trivially, the sum of the chosen elements is r. + // + // Furthermore, as long as the distribution used + // to choose the elements of the original interval + // is uniform, the probability of each partition + // being produced should tend to being uniform + // itself. + std::generate(current_partition.begin(), current_partition.end() - 1, [this](){ return interval_distribution(random_engine); }); + + current_partition.back() = r; + + std::sort(current_partition.begin(), current_partition.end()); + std::adjacent_difference(current_partition.begin(), current_partition.end(), current_partition.begin()); + } + + return true; + } + + private: + std::mt19937 random_engine; + std::uniform_real_distribution interval_distribution; + + std::size_t k; + double r; + + std::vector current_partition; + }; + + } // end QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE + + /*! + * Returns a generator that generates collections of \a k elements + * whose sum is \a r. + * + * \a r must be a real number greater or euqal to zero and \a k + * must be a natural number greater than zero. + * + * The generated partitions tends to be uniformely distributed + * over the set of partitions of r. + */ + inline Catch::Generators::GeneratorWrapper> k_partition_of_r(double r, std::size_t k) { + return Catch::Generators::GeneratorWrapper>(std::unique_ptr>>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::KPartitionOfRGenerator(r, k))); + } + +} // end QDOC_CATCH_GENERATORS_ROOT_NAMESPACE diff --git a/src/qdoc/catch_generators/src/catch_generators/generators/path_generator.h b/src/qdoc/catch_generators/src/catch_generators/generators/path_generator.h new file mode 100644 index 000000000..875502e49 --- /dev/null +++ b/src/qdoc/catch_generators/src/catch_generators/generators/path_generator.h @@ -0,0 +1,853 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +// TODO: Change the include paths to implicitly consider +// `catch_generators` a root directory and change the CMakeLists.txt +// file to make this possible. + +#include "../namespaces.h" +#include "qchar_generator.h" +#include "qstring_generator.h" +#include "../utilities/semantics/move_into_vector.h" +#include "../utilities/semantics/generator_handler.h" + +#if defined(Q_OS_WINDOWS) + + #include "combinators/cycle_generator.h" + +#endif + +#include + +#include + +#include +#include +#include +#include + +#if defined(Q_OS_WINDOWS) + + #include + +#endif + +namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE { + + + struct PathGeneratorConfiguration { + double multi_device_path_probability{0.5}; + double absolute_path_probability{0.5}; + double directory_path_probability{0.5}; + double has_trailing_separator_probability{0.5}; + std::size_t minimum_components_amount{1}; + std::size_t maximum_components_amount{10}; + + PathGeneratorConfiguration& set_multi_device_path_probability(double amount) { + multi_device_path_probability = amount; + return *this; + } + + PathGeneratorConfiguration& set_absolute_path_probability(double amount) { + absolute_path_probability = amount; + return *this; + } + + PathGeneratorConfiguration& set_directory_path_probability(double amount) { + directory_path_probability = amount; + return *this; + } + + PathGeneratorConfiguration& set_has_trailing_separator_probability(double amount) { + has_trailing_separator_probability = amount; + return *this; + } + + PathGeneratorConfiguration& set_minimum_components_amount(std::size_t amount) { + minimum_components_amount = amount; + return *this; + } + + PathGeneratorConfiguration& set_maximum_components_amount(std::size_t amount) { + maximum_components_amount = amount; + return *this; + } + }; + + /*! + * \class PathGeneratorConfiguration + * \brief Defines some parameters to customize the generation of + * paths by a PathGenerator. + */ + + /*! + * \variable PathGeneratorConfiguration::multi_device_path_probability + * + * Every path produced by a PathGenerator configured with a + * mutli_device_path_probability of n has a probability of n to be + * \e {Multi-Device} and a probability of 1.0 - n to not be \a + * {Multi-Device}. + * + * multi_device_path_probability should be a value in the range [0.0, + * 1.0]. + */ + + /*! + * \variable PathGeneratorConfiguration::absolute_path_probability + * + * Every path produced by a PathGenerator configured with an + * absolute_path_probability of n has a probability of n to be \e + * {Absolute} and a probability of 1.0 - n to be \e {Relative}. + * + * absolute_path_probability should be a value in the range [0.0, + * 1.0]. + */ + + /*! + * \variable PathGeneratorConfiguration::directory_path_probability + * + * Every path produced by a PathGenerator configured with a + * directory_path_probability of n has a probability of n to be \e + * {To a Directory} and a probability of 1.0 - n to be \e {To a + * File}. + * + * directory_path_probability should be a value in the range [0.0, + * 1.0]. + */ + + /*! + * \variable PathGeneratorConfiguration::has_trailing_separator_probability + * + * Every path produced by a PathGenerator configured with an + * has_trailing_separator_probability of n has a probability of n + * to \e {Have a Trailing Separator} and a probability of 1.0 - n + * to not \e {Have a Trailing Separator}, when this is applicable. + * + * has_trailing_separator_probability should be a value in the + * range [0.0, 1.0]. + */ + + /*! + * \variable PathGeneratorConfiguration::minimum_components_amount + * + * Every path produced by a PathGenerator configured with a + * minimum_components_amount of n will be the concatenation of at + * least n non \e {device}, non \e {root}, non \e {separator} + * components. + * + * minimum_components_amount should be greater than zero and less + * than maximum_components_amount. + */ + + /*! + * \variable PathGeneratorConfiguration::maximum_components_amount + * + * Every path produced by a PathGenerator configured with a + * maximum_components_amount of n will be the concatenation of at + * most n non \e {device}, non \e {root}, non \e {separator} components. + * + * maximum_components_amount should be greater than or equal to + * minimum_components_amount. + */ + + + namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { + + class PathGenerator : public Catch::Generators::IGenerator { + public: + PathGenerator( + Catch::Generators::GeneratorWrapper&& device_component_generator, + Catch::Generators::GeneratorWrapper&& root_component_generator, + Catch::Generators::GeneratorWrapper&& directory_component_generator, + Catch::Generators::GeneratorWrapper&& filename_component_generator, + Catch::Generators::GeneratorWrapper&& separator_component_generator, + PathGeneratorConfiguration configuration = PathGeneratorConfiguration{} + ) : device_component_generator{QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::handler(std::move(device_component_generator))}, + root_component_generator{QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::handler(std::move(root_component_generator))}, + directory_component_generator{QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::handler(std::move(directory_component_generator))}, + filename_component_generator{QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::handler(std::move(filename_component_generator))}, + separator_component_generator{QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::handler(std::move(separator_component_generator))}, + random_engine{std::random_device{}()}, + components_amount_distribution{configuration.minimum_components_amount, configuration.maximum_components_amount}, + is_multi_device_distribution{configuration.multi_device_path_probability}, + is_absolute_path_distribution{configuration.absolute_path_probability}, + is_directory_path_distribution{configuration.directory_path_probability}, + has_trailing_separator{configuration.has_trailing_separator_probability}, + current_path{} + { + assert(configuration.minimum_components_amount > 0); + assert(configuration.minimum_components_amount <= configuration.maximum_components_amount); + + if (!next()) + Catch::throw_exception("Not enough values to initialize the first string"); + } + + QString const& get() const override { return current_path; } + + bool next() override { + std::size_t components_amount{components_amount_distribution(random_engine)}; + + current_path = ""; + + // REMARK: As per our specification of a path, we + // do not count device components, and separators, + // when considering the amount of components in a + // path. + // This is a tradeoff that is not necessarily + // precise. + // Counting those kinds of components, on one + // hand, would allow a device component to stands + // on its own as a path, for example "C:", which + // might actually be correct in some path format. + // On the other hand, counting those kinds of + // components makes the construction of paths for + // our model much more complex with regards, for + // example, to the amount of component. + // + // Counting device components, since they can + // appear both in relative and absolute paths, + // makes the minimum amount of components + // different for different kinds of paths. + // + // Since absolute paths always require a root + // component, the minimum amount of components for + // a multi-device absolute path is 2. + // + // But an absolute path that is not multi-device + // would only require one minimum component. + // + // Similarly, problems arise with the existence of + // Windows' relative multi-device path, which + // require a leading separator component after a + // device component. + // + // This problem mostly comes from our model + // simplifying the definition of paths quite a bit + // into binary-forms. + // This simplifies the code and its structure, + // sacrificing some precision. + // The lost precision is almost none for POSIX + // based paths, but is graver for DOS paths, since + // they have a more complex specification. + // + // Currently, we expect that the paths that QDoc + // will encounter will mostly be in POSIX-like + // forms, even on Windows, and aim to support + // that, such that the simplification of code is + // considered a better tradeoff compared to the + // loss of precision. + // + // If this changes, the model should be changed to + // pursue a Windows-first modeling, moving the + // categorization of paths from the current binary + // model to the absolute, drive-relative and + // relative triptych that Windows uses. + // This more complex model should be able to + // completely describe posix paths too, making it + // a superior choice as long as the complexity is + // warranted. + // + // Do note that the model similarly can become + // inconsistent when used to generate format of + // paths such as the one used in some resource + // systems. + // Those are considered out-of-scope for our needs + // and were not taken into account when developing + // this generator. + if (is_multi_device_distribution(random_engine)) { + if (!device_component_generator.next()) return false; + current_path += device_component_generator.get(); + } + + // REMARK: Similarly to not counting other form of + // components, we do not count root components + // towards the amounts of components that the path + // has to simplify the code. + // To support the "special" root path on, for + // example, posix systems, we require a more + // complex branching logic that changes based on + // the path being absolute or not. + // + // We don't expect root to be a particularly + // useful path for QDoc purposes and expect to not + // have to consider it for our tests. + // If consideration for it become required, it is + // possible to test it directly in the affected + // systemss as a special case. + // + // If most systems are affected by the handling of + // a root path, then the model should be slightly + // changed to accommodate its generation. + if (is_absolute_path_distribution(random_engine)) { + if (!root_component_generator.next()) return false; + + current_path += root_component_generator.get(); + } + + std::size_t prefix_components_amount{std::max(std::size_t{1}, components_amount) - 1}; + while (prefix_components_amount > 0) { + if (!directory_component_generator.next()) return false; + if (!separator_component_generator.next()) return false; + + current_path += directory_component_generator.get() + separator_component_generator.get(); + --prefix_components_amount; + } + + if (is_directory_path_distribution(random_engine)) { + if (!directory_component_generator.next()) return false; + current_path += directory_component_generator.get(); + + if (has_trailing_separator(random_engine)) { + if (!separator_component_generator.next()) return false; + current_path += separator_component_generator.get(); + } + } else { + if (!filename_component_generator.next()) return false; + current_path += filename_component_generator.get(); + } + + return true; + } + + private: + Catch::Generators::GeneratorWrapper device_component_generator; + Catch::Generators::GeneratorWrapper root_component_generator; + Catch::Generators::GeneratorWrapper directory_component_generator; + Catch::Generators::GeneratorWrapper filename_component_generator; + Catch::Generators::GeneratorWrapper separator_component_generator; + + std::mt19937 random_engine; + std::uniform_int_distribution components_amount_distribution; + std::bernoulli_distribution is_multi_device_distribution; + std::bernoulli_distribution is_absolute_path_distribution; + std::bernoulli_distribution is_directory_path_distribution; + std::bernoulli_distribution has_trailing_separator; + + QString current_path; + }; + + } // end QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE + +/*! + * Returns a generator that produces QStrings that represent a + * path in a filesystem. + * + * A path is formed by the following components, loosely based + * on the abstraction that is used by std::filesystem::path: + * + * \list + * \li \b {device}: + * Represents the device on the filesystem that + * the path should be considered in terms of. + * This is an optional components that is sometimes + * present on multi-device systems, such as Windows, to + * distinguish which device the path refers to. + * When present, it always appears before any other + * component. + * \li \b {root}: + * A special sequence that marks the path as absolute. + * This is an optional component that is present, always, + * in absolute paths. + * \li \b {directory}: + * A component that represents a directory on the + * filesystem that the path "passes-trough". + * Zero or more of this components can be present in the + * path. + * A path pointing to a directory on the filesystem that + * is not \e {root} always ends with a component of this + * type. + * \li \b {filename}: + * A component that represents a file on the + * filesystem. + * When this component is present, it is present only once + * and always as the last component of the path. + * A path that has such a component is a path that points + * to a file on the filesystem. + * For some path formats, there is no difference in the + * format of a \e {filename} and a \e {directory}. + * \li \b {separator}: + * A component that is interleaved between other types of + * components to separate them so that they are + * recognizable. + * A path that points to a directory on the filesystem may + * sometimes have a \e {separator} at the end, after the + * ending \e {directory} component. + * \endlist + * + * Each component is representable as a string and a path is a + * concatenation of the string representation of some + * components, with the following rules: + * + * \list + * \li There is at most one \e {device} component. + * \li If a \e {device} component is present it always + * precedes all other components. + * \li There is at most one \e {root} component. + * \li If a \e {root} component is present it: + * \list + * \li Succeeds the \e {device} component if it is present. + * \li Precedes every other components if the \e {device} + * component is not present. + * \endlist + * \li There are zero or more \e {directory} component. + * \li There is at most one \e {filename} component. + * \li If a \e {filename} component is present it always + * succeeds all other components. + * \li Between any two successive \e {directory} components + * there is a \e {separator} component. + * \li Between each successive \e {directory} and \e + * {filename} component there is a \e {separator} component. + * \li If the last component is a \e {directory} component it + * can be optionally followed by a \e {separator} component. + * \li At least one component that is not a \e {device}, a \e + * {root} or \e {separator} component is present. + * \endlist + * + * For example, if "C:" is a \e {device} component, "\\" is a + * \e {root} component, "\\" is a \e {separator} component, + * "directory" is a \e {directory} component and "filename" is + * a \e {filename} component, the following are all paths: + * + * "C:\\directory", "C:\\directory\\directory", "C:filename", + * "directory\\directory\\", "\\directory\\filename", "filename". + * + * While the following aren't: + * + * "C:", "C:\\", "directory\\C:", "foo", "C:filename\\", + * "filename\\directory\\filename", "filename\\filename", + * "directorydirectory"." + * + * The format of different components type can be the same. + * For example, the \e {root} and \e {separator} component in + * the above example. + * For the purpose of generation, we do not care about the + * format itself and consider a component of a certain type + * depending only on how it is generated/where it is generated + * from. + * + * For example, if every component is formatted as the string + * "a", the string "aaa" could be a generated path. + * By the string alone, it is not possible to simply discern + * which components form it, but it would be possible to + * generate it if the first "a" is a \a {device} component, + * the second "a" is a \e {root} component and the third "a" + * is a \e {directory} or \e {filename} component. + * + * A path, is further said to have some properties, pairs of + * which are exclusive to each other. + * + * A path is said to be: + * + * \list + * \li \b {Multi-Device}: + * When it contains a \e {device} component. + * \li \b {Absolute}: + * When it contains a \e {root} component. + * If the path is \e {Absolute} it is not \e {Relative}. + * \li \b {Relative}: + * When it does not contain a \e {root} component. + * If the path is \e {Relative} it is not \e {Absolute}. + * \li \b {To a Directory}: + * When its last component is a \e {directory} component + * or a \e {directory} component followed by a \e + * {separator} component. + * If the path is \e {To a Directory} it is not \e {To a + * File}. + * \li \b {To a File}: + * When its last component is a \e {filename}. + * If the path is \e {To a File} it is not \e {To a + * Directory}. + * \endlist + * + * All path are \e {Relative/Absolute}, \e {To a + * Directory/To a File} and \e {Multi-Device} or not. + * + * Furthermore, a path that is \e {To a Directory} and whose + * last component is a \e {separator} component is said to \e + * {Have a Trailing Separator}. + */ + inline Catch::Generators::GeneratorWrapper path( + Catch::Generators::GeneratorWrapper&& device_generator, + Catch::Generators::GeneratorWrapper&& root_component_generator, + Catch::Generators::GeneratorWrapper&& directory_generator, + Catch::Generators::GeneratorWrapper&& filename_generator, + Catch::Generators::GeneratorWrapper&& separator_generator, + PathGeneratorConfiguration configuration = PathGeneratorConfiguration{} + ) { + return Catch::Generators::GeneratorWrapper( + std::unique_ptr>( + new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::PathGenerator(std::move(device_generator), std::move(root_component_generator), std::move(directory_generator), std::move(filename_generator), std::move(separator_generator), configuration) + ) + ); + } + + namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { + + // REMARK: We need a bounded length for the generation of path + // components as strings. + // We trivially do not want components to be the empty string, + // such that we have a minimum length of 1, but the maximum + // length is more malleable. + // We don't want components that are too long to avoid + // incurring in a big performance overhead, as we may generate + // many of them. + // At the same time, we want some freedom in having diffent + // length components. + // The value that was chosen is based on the general value for + // POSIX's NAME_MAX, which seems to tend to be 14 on many systems. + // We see this value as a small enough but not too much value + // that further brings with itself a relation to paths, + // increasing our portability even if it is out of scope, as + // almost no modern respects NAME_MAX. + // We don't use POSIX's NAME_MAX directly as it may not be available + // on all systems. + inline static constexpr std::size_t minimum_component_length{1}; + inline static constexpr std::size_t maximum_component_length{14}; + + /*! + * Returns a generator that generates strings that are + * suitable to be used as a root component in POSIX paths. + * + * As per + * \l {https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_02}, + * this is any sequence of slash characters that is not of + * length 2. + */ + inline Catch::Generators::GeneratorWrapper posix_root() { + return uniformly_valued_oneof( + QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::move_into_vector( + string(character('/', '/'), 1, 1), + string(character('/', '/'), 3, maximum_component_length) + ), + std::vector{1, maximum_component_length - 3} + ); + } + + /*! + * Returns a generator that generates strings that are + * suitable to be used as directory components in POSIX paths + * and that use an alphabet that should generally be supported + * by other systems. + * + * Components of this kind use the \l + * {https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282}{Portable Filename Character Set}. + */ + inline Catch::Generators::GeneratorWrapper portable_posix_directory_name() { + return string( + QDOC_CATCH_GENERATORS_QCHAR_ALPHABETS_NAMESPACE::portable_posix_filename(), + minimum_component_length, maximum_component_length + ); + } + + /*! + * Returns a generator that generates strings that are + * suitable to be used as filenames in POSIX paths and that + * use an alphabet that should generally be supported by + * other systems. + * + * Filenames of this kind use the \l + * {https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282}{Portable Filename Character Set}. + */ + inline Catch::Generators::GeneratorWrapper portable_posix_filename() { + // REMARK: "." and ".." always represent directories so we + // avoid generating them. Other than this, there is no + // difference between a file name and a directory name. + return filter([](auto& filename) { return filename != "." && filename != ".."; }, portable_posix_directory_name()); + } + + /*! + * Returns a generator that generates strings that can be used + * as POSIX compliant separators. + * + * As per \l + * {https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_271}, + * a separator is a sequence of one or more slashes. + */ + inline Catch::Generators::GeneratorWrapper posix_separator() { + return string(character('/', '/'), minimum_component_length, maximum_component_length); + } + + /*! + * Returns a generator that generates strings that can be + * suitably used as logical drive names in Windows' paths. + * + * As per \l + * {https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats#traditional-dos-paths} + * and \l + * {https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getlogicaldrives}, + * they are composed of a single letter. + * Each generated string always follows the lettet with a + * colon, as it is specifically intended for path usages, + * where this is required. + * + * We use only uppercase letters for the drives names albeit, + * depending on case sensitivity, lowercase letter could be + * used. + */ + inline Catch::Generators::GeneratorWrapper windows_logical_drives() { + // REMARK: If a Windows path is generated on Windows + // itself, we expect that it may be used to interact with + // the filesystem, similar to how we expect a POSIX path + // to be used on Linux. + // For this reason, we only generate a specific drive, the one + // that contains the current working directory, so that we + // know it is an actually available drive and to contain the + // possible modifications to the filesystem to an easily + // foundable place. + +#if defined(Q_OS_WINDOWS) + + auto root_device{QStorageInfo{QDir()}.rootPath().first(1) + ":"}; + + return cycle(Catch::Generators::value(std::move(root_device))); + +#else + + return Catch::Generators::map( + [](QString letter){ return letter + ':';}, + string(QDOC_CATCH_GENERATORS_QCHAR_ALPHABETS_NAMESPACE::ascii_uppercase(), 1, 1) + ); + +#endif + } + + /*! + * Returns a generator that generate strings that can be used + * as separators in Windows based paths. + * + * As per \l + * {https://docs.microsoft.com/en-us/dotnet/api/system.io.path.directoryseparatorchar?view=net-6.0} + * and \l + * {https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats#canonicalize-separators}, + * this is a sequence of one or more backward or forward slashes. + */ + inline Catch::Generators::GeneratorWrapper windows_separator() { + return uniform_oneof( + QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::move_into_vector( + string(character('\\', '\\'), minimum_component_length, maximum_component_length), + string(character('/', '/'), minimum_component_length, maximum_component_length) + ) + ); + } + + } // end QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE + + /*! + * Returns a generator that generates strings representing + * POSIX compatible paths. + * + * The generated paths follows the format specified in \l + * {https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_271}. + * + * The optional length-requirements, such as PATH_MAX and + * NAME_MAX, are relaxed away as they are generally not + * respected by modern systems. + * + * It is possible to set the probability of obtaining a + * relative or absolute path through \a + * absolute_path_probability and the one of obtaining a path + * potentially pointing ot a directory or on a file through \a + * directory_path_probability. + */ + inline Catch::Generators::GeneratorWrapper relaxed_portable_posix_path(double absolute_path_probability = 0.5, double directory_path_probability = 0.5) { + return path( + // POSIX path are never multi-device, so that we have + // provide an empty device component generator and set + // the probability for Multi-Device paths to zero. + string(character(), 0, 0), + QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::posix_root(), + QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::portable_posix_directory_name(), + QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::portable_posix_filename(), + QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::posix_separator(), + PathGeneratorConfiguration{} + .set_multi_device_path_probability(0.0) + .set_absolute_path_probability(absolute_path_probability) + .set_directory_path_probability(directory_path_probability) + ); + } + + /*! + * Returns a generator that produces strings that represents + * traditional DOS paths as defined in \l + * {https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats#traditional-dos-paths}. + * + * The directory and filename components of a path generated + * in this way are, currently, restricted to use a portable + * character set as defined by POSIX. + * + * Do note that most paths themselves, will not be portable, on + * the whole, albeit they may be valid paths on other systems, as + * Windows uses a path system that is generally incompatible with + * other systems. + * + * Some possibly valid special path, such as a "C:" or "\" + * will never be generated. + */ + inline Catch::Generators::GeneratorWrapper traditional_dos_path( + double absolute_path_probability = 0.5, + double directory_path_probability = 0.5, + double multi_device_path_probability = 0.5 + ) { + return path( + QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::windows_logical_drives(), + QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::windows_separator(), + // REMAKR: Windows treats trailing dots as if they were a + // component of their own, that is, as the special + // relative paths. + // This seems to not be correctly handled by Qt's + // filesystem methods, resulting in inconsistencies when + // one such path is encountered. + // To avoid the issue, considering that an equivalent path + // can be formed by actually having the dots on their own + // as a component, we filter out all those paths that have + // trailing dots but are not only composed of dots. + Catch::Generators::filter( + [](auto& path){ return !(path.endsWith(".") && path.contains(QRegularExpression("[^.]"))) ; }, + QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::portable_posix_directory_name() + ), + QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::portable_posix_filename(), + QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::windows_separator(), + PathGeneratorConfiguration{} + .set_multi_device_path_probability(multi_device_path_probability) + .set_absolute_path_probability(absolute_path_probability) + .set_directory_path_probability(directory_path_probability) + ); + } + + // TODO: Find a good way to test the following functions. + // native_path can probably be tied to the tests for the + // OS-specific functions, with TEMPLATE_TEST_CASE. + // The other ones may follow a similar pattern but require a bit + // more work so that they tie to a specific case instead of the + // general one. + // Nonetheless, this approach is both error prone and difficult to + // parse, because of the required if preprocessor directives, + // and should be avoided if possible. + + /*! + * Returns a generator that generates QStrings that represents + * paths native to the underlying OS. + * + * On Windows, paths that refer to a drive always refer to the + * root drive. + * + * native* functions should always be chosen when using paths for + * testing interfacing with the filesystem itself. + * + * System outside Linux, macOS or Windows are not supported. + */ + inline Catch::Generators::GeneratorWrapper native_path(double absolute_path_probability = 0.5, double directory_path_probability = 0.5) { +#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) + + return relaxed_portable_posix_path(absolute_path_probability, directory_path_probability); + +#elif defined(Q_OS_WINDOWS) + + // REMARK: When generating native paths for testing we + // generally want to avoid relative paths that are + // drive-specific, as we want them to be tied to a specific + // working directory that may not be the current directory on + // the drive. + // Hence, we avoid generating paths that may have a drive component. + // For tests where those kind of paths are interesting, a + // specific Windows-only test should be made, using + // traditional_dos_path to generate drive-relative paths only. + return traditional_dos_path(absolute_path_probability, directory_path_probability, 0.0); + +#endif + } + + /*! + * Returns a generator that generates QStrings that represents + * paths native to the underlying OS and that are always \e + * {Relative}. + * + * Avoids generating paths that refer to a directory that is not + * included in the path itself. + * + * System outside Linux, macOS or Windows are not supported. + */ + inline Catch::Generators::GeneratorWrapper native_relative_path(double directory_path_probability = 0.5) { + // REMARK: When testing, we generally use some specific + // directory as a root for relative paths. + // We want the generated path to be relative to that + // directory because we need a clean state for the test to + // be reliable. + // When generating paths, it is possible, correctly, to + // have a path that refers to that directory or some + // parent of it, removing us from the clean state that we + // need. + // To avoid that, we filter out paths that end up referring to a directory that is not under our "root" directory. + // + // We can think of each generated component moving us + // further down or up, in case of "..", a directory + // hierarchy, or keeping us at the same place in case of + // ".". + // Any path that ends up under our original "root" + // directory will safely keep our clean state for testing. + // + // Each "." keeps us at the same level in the hierarchy. + // Each ".." moves us up one level in the hierarchy. + // Each component that is not "." or ".." moves us down + // one level into the hierarchy. + // + // Then, to avoid referring to the "root" directory or one + // of its parents, we need to balance out each "." and + // ".." with the components that precedes or follow their + // appearance. + // + // Since "." keeps us at the same level, it can appear how + // many times it wants as long as the path referes to the + // "root" directory or a directory or file under it and at + // least one other component referes to a directory or + // file that is under the "root" directory. + // + // Since ".." moves us one level up in the hierarchy, a + // sequence of n ".." components is safe when at least n + + // 1 non "." or ".." components appear before it. + // + // To avoid the above problem, we filter away paths that + // do not respect those rules. + return Catch::Generators::filter( + [](auto& path){ + QStringList components{path.split(QRegularExpression{R"((\\|\/)+)"}, Qt::SkipEmptyParts)}; + int depth{0}; + + for (auto& component : components) { + if (component == "..") + --depth; + else if (component != ".") + ++depth; + + if (depth < 0) return false; + } + + return (depth > 0); + }, + native_path(0.0, directory_path_probability) + ); + } + + /*! + * Returns a generator that generates QStrings that represents + * paths native to the underlying OS and that are always \e + * {Relative} and \e {To a File}. + * + * System outside Linux, macOS or Windows are not supported. + */ + inline Catch::Generators::GeneratorWrapper native_relative_file_path() { + return native_relative_path(0.0); + } + + /*! + * Returns a generator that generates QStrings that represents + * paths native to the underlying OS and that are always \e + * {Relative} and \e {To a Directory}. + * + * System outside Linux, macOS or Windows are not supported. + */ + inline Catch::Generators::GeneratorWrapper native_relative_directory_path() { + return native_relative_path(1.0); + } + +} // end QDOC_CATCH_GENERATORS_ROOT_NAMESPACE diff --git a/src/qdoc/catch_generators/src/catch_generators/generators/qchar_generator.h b/src/qdoc/catch_generators/src/catch_generators/generators/qchar_generator.h new file mode 100644 index 000000000..33efc5ea4 --- /dev/null +++ b/src/qdoc/catch_generators/src/catch_generators/generators/qchar_generator.h @@ -0,0 +1,110 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "../namespaces.h" +#include "../utilities/semantics/move_into_vector.h" +#include "combinators/oneof_generator.h" + +#include + +#include + +#include + +namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE { + namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { + + class QCharGenerator : public Catch::Generators::IGenerator { + public: + QCharGenerator( + char16_t lower_bound = std::numeric_limits::min(), + char16_t upper_bound = std::numeric_limits::max() + ) : random_engine{std::random_device{}()}, + distribution{static_cast(lower_bound), static_cast(upper_bound)} + { + assert(lower_bound <= upper_bound); + static_cast(next()); + } + + QChar const& get() const override { return current_character; } + + bool next() override { + current_character = QChar(static_cast(distribution(random_engine))); + + return true; + } + + private: + QChar current_character; + + std::mt19937 random_engine; + std::uniform_int_distribution distribution; + }; + + } // end QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE + + + /*! + * Returns a generator of that generates elements of QChar whose + * ucs value is in the range [\a lower_bound, \a upper_bound]. + * + * When \a lower_bound = \a upper_bound, the generator infinitely + * generates the same character. + */ + inline Catch::Generators::GeneratorWrapper character(char16_t lower_bound = std::numeric_limits::min(), char16_t upper_bound = std::numeric_limits::max()) { + return Catch::Generators::GeneratorWrapper(std::unique_ptr>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::QCharGenerator(lower_bound, upper_bound))); + } + + + namespace QDOC_CATCH_GENERATORS_QCHAR_ALPHABETS_NAMESPACE { + + namespace QDOC_CATCH_GENERATORS_TRAITS_NAMESPACE { + + enum class Alphabets : std::size_t {digit, ascii_lowercase, ascii_uppercase, ascii_alpha, ascii_alphanumeric, portable_posix_filename}; + + template + struct sizeof_alphabet; + + template + inline constexpr std::size_t sizeof_alphabet_v = sizeof_alphabet::value; + + template <> struct sizeof_alphabet { static constexpr std::size_t value{'9' - '0'}; }; + template <> struct sizeof_alphabet { static constexpr std::size_t value{'z' - 'a'}; }; + template<> struct sizeof_alphabet { static constexpr std::size_t value{'Z' - 'A'}; }; + template<> struct sizeof_alphabet { static constexpr std::size_t value{sizeof_alphabet_v + sizeof_alphabet_v}; }; + template<> struct sizeof_alphabet{ static constexpr std::size_t value{sizeof_alphabet_v + sizeof_alphabet_v}; }; + + } // end QDOC_CATCH_GENERATORS_TRAITS_NAMESPACE + + + inline Catch::Generators::GeneratorWrapper digit() { + return Catch::Generators::GeneratorWrapper(std::unique_ptr>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::QCharGenerator('0', '9'))); + } + + inline Catch::Generators::GeneratorWrapper ascii_lowercase() { + return Catch::Generators::GeneratorWrapper(std::unique_ptr>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::QCharGenerator('a', 'z'))); + } + + inline Catch::Generators::GeneratorWrapper ascii_uppercase() { + return Catch::Generators::GeneratorWrapper(std::unique_ptr>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::QCharGenerator('A', 'Z'))); + } + + inline Catch::Generators::GeneratorWrapper ascii_alpha() { + return uniform_oneof(QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::move_into_vector(ascii_lowercase(), ascii_uppercase())); + } + + inline Catch::Generators::GeneratorWrapper ascii_alphanumeric() { + return uniformly_valued_oneof(QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::move_into_vector(ascii_alpha(), digit()), std::vector{traits::sizeof_alphabet_v , traits::sizeof_alphabet_v}); + } + + inline Catch::Generators::GeneratorWrapper portable_posix_filename() { + return uniformly_valued_oneof(QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::move_into_vector(ascii_alphanumeric(), character('.', '.'), character('-', '-'), character('_', '_')), + std::vector{traits::sizeof_alphabet_v, std::size_t{1}, std::size_t{1}, std::size_t{1}}); + } + + } // end QDOC_CATCH_GENERATORS_QCHAR_ALPHABETS_NAMESPACE + + +} // end QDOC_CATCH_GENERATORS_ROOT_NAMESPACE diff --git a/src/qdoc/catch_generators/src/catch_generators/generators/qstring_generator.h b/src/qdoc/catch_generators/src/catch_generators/generators/qstring_generator.h new file mode 100644 index 000000000..fe854d22f --- /dev/null +++ b/src/qdoc/catch_generators/src/catch_generators/generators/qstring_generator.h @@ -0,0 +1,92 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "../namespaces.h" +#include "qchar_generator.h" +#include "../utilities/semantics/generator_handler.h" + +#include + +#include + +#include + +namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE { + namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { + + class QStringGenerator : public Catch::Generators::IGenerator { + public: + QStringGenerator(Catch::Generators::GeneratorWrapper&& character_generator, qsizetype minimum_length, qsizetype maximum_length) + : character_generator{QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::handler(std::move(character_generator))}, + random_engine{std::random_device{}()}, + length_distribution{minimum_length, maximum_length}, + current_string{} + { + assert(minimum_length >= 0); + assert(maximum_length >= 0); + assert(minimum_length <= maximum_length); + + if (!next()) + Catch::throw_exception("Not enough values to initialize the first string"); + } + + QString const& get() const override { return current_string; } + + bool next() override { + qsizetype length{length_distribution(random_engine)}; + + current_string = QString(); + for (qsizetype length_index{0}; length_index < length; ++length_index) { + if (!character_generator.next()) return false; + + current_string += character_generator.get(); + } + + return true; + } + + private: + Catch::Generators::GeneratorWrapper character_generator; + + std::mt19937 random_engine; + std::uniform_int_distribution length_distribution; + + QString current_string; + }; + + } // end QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE + + /*! + * Returns a generator that generates elements of QString from + * some amount of elements taken from \a character_generator. + * + * The generated strings will have a length in the range + * [\a minimum_length, \a maximum_length]. + * + * For compatibility with the Qt API, it is possible to provide + * negative bounds for the length. This is, nonetheless, + * considered an error such that the bounds should always be + * greater or equal to zero. + * + * It is similarly considered an error to have minimum_length <= + * maximum_length. + * + * The provided generator will generate elements until \a + * character_generator is exhausted. + */ + inline Catch::Generators::GeneratorWrapper string(Catch::Generators::GeneratorWrapper&& character_generator, qsizetype minimum_length, qsizetype maximum_length) { + return Catch::Generators::GeneratorWrapper(std::unique_ptr>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::QStringGenerator(std::move(character_generator), minimum_length, maximum_length))); + } + + /*! + * Returns an infinite generator whose elements are the empty + * QString. + */ + inline Catch::Generators::GeneratorWrapper empty_string() { + return Catch::Generators::GeneratorWrapper(std::unique_ptr>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::QStringGenerator(character(), 0, 0))); + } + + +} // end QDOC_CATCH_GENERATORS_ROOT_NAMESPACE diff --git a/src/qdoc/catch_generators/src/catch_generators/namespaces.h b/src/qdoc/catch_generators/src/catch_generators/namespaces.h new file mode 100644 index 000000000..3c956d44f --- /dev/null +++ b/src/qdoc/catch_generators/src/catch_generators/namespaces.h @@ -0,0 +1,14 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#define QDOC_CATCH_GENERATORS_ROOT_NAMESPACE qdoc::catch_generators + +#define QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE details + +#define QDOC_CATCH_GENERATORS_TRAITS_NAMESPACE traits + +#define QDOC_CATCH_GENERATORS_QCHAR_ALPHABETS_NAMESPACE alphabets + +#define QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE QDOC_CATCH_GENERATORS_ROOT_NAMESPACE::QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::utils diff --git a/src/qdoc/catch_generators/src/catch_generators/utilities/semantics/copy_value.h b/src/qdoc/catch_generators/src/catch_generators/utilities/semantics/copy_value.h new file mode 100644 index 000000000..57798be1a --- /dev/null +++ b/src/qdoc/catch_generators/src/catch_generators/utilities/semantics/copy_value.h @@ -0,0 +1,26 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "../../namespaces.h" + +#include + +namespace QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE { + + /*! + * Forces \value to be copied in an expression context. + * + * This is used in contexts where inferences of a type that + * requires generality might identify a reference when ownership + * is required. + * + * Note that the compiler might optmize the copy away. This is a + * non-issue as we are only interested in breaking lifetime + * dependencies. + */ + template + std::remove_reference_t copy_value(T value) { return value; } + +} // end QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE diff --git a/src/qdoc/catch_generators/src/catch_generators/utilities/semantics/generator_handler.h b/src/qdoc/catch_generators/src/catch_generators/utilities/semantics/generator_handler.h new file mode 100644 index 000000000..328627512 --- /dev/null +++ b/src/qdoc/catch_generators/src/catch_generators/utilities/semantics/generator_handler.h @@ -0,0 +1,97 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "../../namespaces.h" + +#include + +#include +#include + +namespace QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE { + + template + class GeneratorHandler : public Catch::Generators::IGenerator { + public: + + GeneratorHandler(Catch::Generators::GeneratorWrapper&& generator) + : generator{std::move(generator)}, + first_call{true} + {} + + T const& get() const override { + assert(!first_call); + return generator.get(); + } + + bool next() override { + if (first_call) { + first_call = false; + return true; + } + + return generator.next(); + } + + private: + Catch::Generators::GeneratorWrapper generator; + bool first_call; + }; + + + /*! + * Returns a generator wrapping \a generator that ensures that + * changes its semantics so that the first call to get should be + * preceded by a call to next. + * + * Catch generators require that is valid to call get and obtain a + * valid value on a generator that was just created. + * That is, generators should be non-empty and their first value + * should be initialized on construction. + * + * Normally, this is not a problem, and the next implementation of + * the generator can be simply called in the constructor. + * But when a generator depends on other generators, doing so will + * generally skip the first value that the generator + * produces, as the wrapping generator will need to advance the + * underlying generator, losing the value in the process. + * This is in particular, a problem, on generators that are finite + * or infinite and ordered. + * + * To solve the issue, the original value can be saved before + * advancing the generator or some code can be duplicated or + * abstracted so that what a new element can be generated without + * advancing the underlying generator. + * + * While this is acceptable, it can be error prone on more complex + * generators, generators that randomly access a collection of + * generators and so on. + * + * To simplify this process, this generator changes the semantics + * of the wrapped generator such that the first value of the + * generator is produced after the first call to next and the + * generator is considered in an invalid state before the first + * advancement. + * + * In this way, by wrapping all generators that a generator + * depends on, the implementation required for the first value is + * the same as the one required for all following values, with + * regards to the sequencing of next and get operations, + * simplifying the implementation of dependent generators. + * + * Do note that, while the generator returned by this function + * implments the generator interface that Catch2 requires, it + * cannot be normally used as a generator as it fails to comply + * with the first value semantics that a generator requires. + * Indeed, it should only be used as an intermediate wrapper for + * the implementation of generators that depends on other + * generators. + */ + template + inline Catch::Generators::GeneratorWrapper handler(Catch::Generators::GeneratorWrapper&& generator) { + return Catch::Generators::GeneratorWrapper(std::unique_ptr>(new GeneratorHandler(std::move(generator)))); + } + +} // end QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE diff --git a/src/qdoc/catch_generators/src/catch_generators/utilities/semantics/move_into_vector.h b/src/qdoc/catch_generators/src/catch_generators/utilities/semantics/move_into_vector.h new file mode 100644 index 000000000..5e780085b --- /dev/null +++ b/src/qdoc/catch_generators/src/catch_generators/utilities/semantics/move_into_vector.h @@ -0,0 +1,62 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "../../namespaces.h" + +#include +#include + +namespace QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE { + + namespace QDOC_CATCH_GENERATORS_TRAITS_NAMESPACE { + + /*! + * Returns the type of the first element of Args. + * + * Args is expected to have at least one + */ + template + using first_from_pack_t = std::tuple_element_t<0, std::tuple>; + + } // end QDOC_CATCH_GENERATORS_TRAITS_NAMESPACE + + + /*! + * Builds an std::vector by moving \a movables into it. + * + * \a movables must be made of homogenous types. + * + * This function is intended to allow the construction of an + * std::vector, where T is a move only type, as an expression, + * to lighten the idiom. + * + * For example, Catch's GeneratorWrapper adapts a + * std::unique_ptr, which is move only, making it impossible to + * build a std::vector from them in place. + * + * Then, everywhere this is needed, a more complex approach of + * generating the collection of objects, generating a vector of a + * suitable size and iterating the objects to move-emplace them in + * the vector is required. + * + * This not only complicates the code but is incompatible with a + * GENERATE expression, making it extremely hard, noisy and error + * prone to use them together. + * + * In those cases, then, a call to move_into_vector can be used as + * an expression to circumvent the problem. + */ + template + inline auto move_into_vector(MoveOnlyTypes... movables) { + std::vector> + moved_into_vector; + moved_into_vector.reserve(sizeof...(movables)); + + (moved_into_vector.emplace_back(std::move(movables)), ...); + + return moved_into_vector; + } + +} // end QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE diff --git a/src/qdoc/catch_generators/src/catch_generators/utilities/statistics/distribution.h b/src/qdoc/catch_generators/src/catch_generators/utilities/statistics/distribution.h new file mode 100644 index 000000000..4374993bf --- /dev/null +++ b/src/qdoc/catch_generators/src/catch_generators/utilities/statistics/distribution.h @@ -0,0 +1,158 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "../../namespaces.h" + +#include +#include +#include +#include + +namespace QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE { + + template + using Histogram = std::unordered_map; + + template + auto make_histogram(InputIt begin, InputIt end, GroupBy&& group_by) { + Histogram> histogram{}; + + while (begin != end) { + auto key{std::invoke(std::forward(group_by), *begin)}; + + histogram.try_emplace(key, 0); + histogram[key] += 1; + ++begin; + } + + return histogram; + } + + template + struct DistributionError { + T value; + double probability; + double expected_probability; + }; + + template + inline std::ostream& operator<<(std::ostream& os, const DistributionError& error) { + return os << "DistributionError{" << + "The value { " << error.value << + " } appear with a probability of { " << error.probability << + " } while a probability of { " << error.expected_probability << " } was expected." << + "}"; + } + + // REMARK: The following should really return an Either of unit/error + // but std::variant in C++ is both extremely unusable and comes with a + // strong overhead unless certain conditions are met. + // For this reason, we keep to the less intutitive optional error. + + /*! + * Returns true when the given \a sequence approximately respects a + * given distribution. + * + * The \a sequence respects a given distribution when the count of + * each collection of values is a percentage of the total values that + * is near the percentage probability described by distribution. + * + * The values in \a sequence are collected according to \a group_by. + * \a group_by, given an element of \a sequence, should return a value + * of some type that represent the category of the inspected value. + * Values that have the same category share their count. + * + * The distribution that should be respected is given by \a + * probability_of. \a probability_of is a function that takes a + * category that was produced from a call to \a group_by and returns + * the expect probability, in percentage, of apperance for that + * category. + * + * The given probability is then compared to the one found by counting + * the element of \a sequence under \a group_by, to ensure that it + * matches. + * + * The margin of error for the comparison is given, in percentage + * points, by \a margin. + * The approximation uses an absolute comparison and scales the + * margin inversely based on the size of \a sequence, to account for the + * precision of the data set itself. + * + * When the distribution is not respected, a DistributionError is + * returned enclosed in an optional value. + * The error allows reports which the first category for which the + * comparison failed, along with its expected probability and the one + * that was actually inferred from \a sequence. + */ + template + std::optional> respects_distribution(std::vector&& sequence, GroupBy&& group_by, ProbabilityOf&& probability_of, double margin = 33) { + std::size_t data_point_amount{sequence.size()}; + + // REMARK: We scale the margin based on the data set to allow for + // an easier change in downstream tests. + // The precision required for the approximation will vary + // depending on how many values we generate. + // The amount of values we generate depends on how much time we + // want the tests to take. + // This amount may change in the future. For example, as code is + // added and tests are added, we might need some expensive + // computations here and there. + // Sometimes, this will increase the test suite runtime without an + // obvious way of improving the performance of the underlying code + // to reduce it. + // In those cases, the total run time can be decreased by running + // less generations for battle-tested tests. + // If some code has not been changed for a long time, it will have + // had thousands of generations by that point, giving us a good + // degree of certainty of it not being bugged (for whatever bugs + // the tests account for). + // Then, running a certain amount of generation is not required + // anymore such that some of them can be optimized out. + // For tests like the one using this function, where our ability + // to test is always dependent on the amount of generations, + // changing the generated amount will mean that we will need to + // change our conditions too, potentially changing the meaning of + // the test. + // To take this into account, we perform a scaling on the + // condition itself, so that if the amount of data points that are + // generated changes, we do not generally have to change anything + // in the condition. + // + // For this case, we scale logarithmically_10 for the simple + // reason that we tend to generate values in power of tens, + // starting with the 100 values default that Quickcheck used. + // + // The default value for the margin on which the scaling is based, + // was chosen heuristically. + // As we expect generation under 10^3 to be generally meaningless + // for this kind of testing, the value was chosen so that it would + // start to normalize around that amount. + // Deviation of about 5-10% were identified trough various + // generations for an amount of data points near 1000, while a + // deviation of about 1-3% was identified with about 10000 values. + // With the chosen default value, the scaling approaches those + // percentage points with some margin of error. + // + // We expect up to a 10%, or a bit more, deviation to be suitable + // for our purposes, as it would still allow for a varied + // distribution in downstream consumers. + double scaled_margin{margin * (1.0/std::log10(data_point_amount))}; + + auto histogram{make_histogram(sequence.begin(), sequence.end(), std::forward(group_by))}; + + for (auto& bin : histogram) { + auto [key, count] = bin; + + double actual_percentage{percent_of(static_cast(count), static_cast(data_point_amount))}; + double expected_percentage{std::invoke(std::forward(probability_of), key)}; + + if (!(actual_percentage == Approx(expected_percentage).margin(scaled_margin))) + return std::make_optional(DistributionError{key, actual_percentage, expected_percentage}); + } + + return std::nullopt; + } + +} // end QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE diff --git a/src/qdoc/catch_generators/src/catch_generators/utilities/statistics/percentages.h b/src/qdoc/catch_generators/src/catch_generators/utilities/statistics/percentages.h new file mode 100644 index 000000000..2d80a459f --- /dev/null +++ b/src/qdoc/catch_generators/src/catch_generators/utilities/statistics/percentages.h @@ -0,0 +1,49 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "../../namespaces.h" + +#include + +namespace QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE { + + /*! + * Returns the percentage of \amount over \a total. + * + * \a amount needs to be greater or equal to zero and \a total + * needs to be greater than zero. + */ + inline double percent_of(double amount, double total) { + assert(amount >= 0.0); + assert(total > 0.0); + + return (amount / total) * 100.0; + } + + /*! + * Given the cardinality of a set, returns the percentage + * probability that applied to every element of the set generates + * a uniform distribution. + */ + inline double uniform_probability(std::size_t cardinality) { + assert(cardinality > 0); + + return (100.0 / static_cast(cardinality)); + } + + /*! + * Returns a percentage probability that is equal to \a + * probability. + * + * \a probability must be in the range [0.0, 1.0] + */ + inline double probability_to_percentage(double probability) { + assert(probability >= 0.0); + assert(probability <= 1.0); + + return probability * 100.0; + } + +} // end QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE diff --git a/src/qdoc/catch_generators/tests/CMakeLists.txt b/src/qdoc/catch_generators/tests/CMakeLists.txt new file mode 100644 index 000000000..5a4b8667d --- /dev/null +++ b/src/qdoc/catch_generators/tests/CMakeLists.txt @@ -0,0 +1,20 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_internal_add_test(tst_QDoc_Catch_Generators + SOURCES + ${CMAKE_CURRENT_LIST_DIR}/main.cpp + ${CMAKE_CURRENT_LIST_DIR}/generators/catch_qchar_generator.cpp + ${CMAKE_CURRENT_LIST_DIR}/generators/catch_qstring_generator.cpp + ${CMAKE_CURRENT_LIST_DIR}/generators/catch_k_partition_of_r_generator.cpp + ${CMAKE_CURRENT_LIST_DIR}/generators/catch_path_generator.cpp + + ${CMAKE_CURRENT_LIST_DIR}/generators/combinators/catch_oneof_generator.cpp + ${CMAKE_CURRENT_LIST_DIR}/generators/combinators/catch_cycle_generator.cpp + + ${CMAKE_CURRENT_LIST_DIR}/utilities/semantics/catch_generator_handler.cpp + LIBRARIES + Qt::QDocCatchPrivate + Qt::QDocCatchConversionsPrivate + Qt::QDocCatchGeneratorsPrivate +) diff --git a/src/qdoc/catch_generators/tests/generators/catch_k_partition_of_r_generator.cpp b/src/qdoc/catch_generators/tests/generators/catch_k_partition_of_r_generator.cpp new file mode 100644 index 000000000..27b79c511 --- /dev/null +++ b/src/qdoc/catch_generators/tests/generators/catch_k_partition_of_r_generator.cpp @@ -0,0 +1,41 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include +#include + +#include + +#include + +using namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE; + +SCENARIO("Generating a k-partition of a real number", "[Partition][Reals]") { + GIVEN("A real number r greater or equal to zero") { + double r = GENERATE(take(10, random(0.0, 1000000.0))); + + AND_GIVEN("An amount of desired elements k greater than zero") { + std::size_t k = GENERATE(take(10, random(1, 100))); + + WHEN("A k-partition of r is generated") { + auto k_partition = GENERATE_COPY(take(10, k_partition_of_r(r, k))); + + THEN("The partition contains k elements") { + REQUIRE(k_partition.size() == k); + + AND_THEN("The sum of those elements is r") { + REQUIRE(std::accumulate(k_partition.begin(), k_partition.end(), 0.0) == Approx(r)); + } + } + } + } + } +} + +TEST_CASE("All 1-partition of r are singleton collection with r as their element", "[Partition][Reals][SpecialCase]") { + double r = GENERATE(take(10, random(0.0, 1000000.0))); + auto k_partition = GENERATE_COPY(take(10, k_partition_of_r(r, 1))); + + REQUIRE(k_partition.size() == 1); + REQUIRE(k_partition.front() == r); +} diff --git a/src/qdoc/catch_generators/tests/generators/catch_path_generator.cpp b/src/qdoc/catch_generators/tests/generators/catch_path_generator.cpp new file mode 100644 index 000000000..deb33421b --- /dev/null +++ b/src/qdoc/catch_generators/tests/generators/catch_path_generator.cpp @@ -0,0 +1,755 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include + +using namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE; +using namespace QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE; + +using namespace Qt::StringLiterals; + +TEST_CASE("A path generated with a multi_device_path_probability of 1.0 always contains a device component.", "[Path][Content][SpecialCase]") { + QString device_component_value{"C:"}; + auto path_generator = path( + Catch::Generators::value(copy_value(device_component_value)), + empty_string(), + empty_string(), + empty_string(), + empty_string(), + PathGeneratorConfiguration{}.set_multi_device_path_probability(1.0) + ); + + auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); + + REQUIRE(generated_path.contains(device_component_value)); +} + +TEST_CASE("A path generated with a multi_device_path_probability of 0.0 never contains a device component.", "[Path][Content][SpecialCase]") { + QString device_component_value{"C:"}; + auto path_generator = path( + Catch::Generators::value(copy_value(device_component_value)), + empty_string(), + empty_string(), + empty_string(), + empty_string(), + PathGeneratorConfiguration{}.set_multi_device_path_probability(0.0) + ); + + auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); + + REQUIRE(!generated_path.contains(device_component_value)); +} + +TEST_CASE("A path generated with an absolute_path_probability of 1.0 always contains a root component.", "[Path][Content][SpecialCase]") { + QString root_component_value{"\\"}; + auto path_generator = path( + empty_string(), + Catch::Generators::value(copy_value(root_component_value)), + empty_string(), + empty_string(), + empty_string(), + PathGeneratorConfiguration{}.set_absolute_path_probability(1.0) + ); + + auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); + + REQUIRE(generated_path.contains(root_component_value)); +} + +TEST_CASE("A path generated with an absolute_path_probability of 0.0 never contains a root component.", "[Path][Content][SpecialCase]") { + QString root_component_value{"\\"}; + auto path_generator = path( + empty_string(), + Catch::Generators::value(copy_value(root_component_value)), + empty_string(), + empty_string(), + empty_string(), + PathGeneratorConfiguration{}.set_absolute_path_probability(0.0) + ); + + auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); + + REQUIRE(!generated_path.contains(root_component_value)); +} + +TEST_CASE("A path generated with a directory_path_probability of 1.0 always ends with a root, directory or directory followed by separator component.", "[Path][Content][SpecialCase]") { + QString root_component_value{"root"}; + QString directory_component_value{"dir"}; + QString separator_component_value{"sep"}; + + auto path_generator = path( + cycle(Catch::Generators::value(QString("device"))), + cycle(Catch::Generators::value(copy_value(root_component_value))), + cycle(Catch::Generators::value(copy_value(directory_component_value))), + cycle(Catch::Generators::value(QString("filename"))), + cycle(Catch::Generators::value(copy_value(separator_component_value))), + PathGeneratorConfiguration{}.set_directory_path_probability(1.0) + ); + + auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); + + REQUIRE(( + generated_path.endsWith(root_component_value) || + generated_path.endsWith(directory_component_value) || + generated_path.endsWith(directory_component_value + separator_component_value) + )); +} + +TEST_CASE("A path generated with a directory_path_probability of 0.0 always ends with a filename component.", "[Path][Content][SpecialCase]") { + QString filename_component_value{"file"}; + + auto path_generator = path( + cycle(Catch::Generators::value(QString("device"))), + cycle(Catch::Generators::value(QString("root"))), + cycle(Catch::Generators::value(QString("dir"))), + cycle(Catch::Generators::value(copy_value(filename_component_value))), + cycle(Catch::Generators::value(QString("sep"))), + PathGeneratorConfiguration{}.set_directory_path_probability(0.0) + ); + + auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); + + REQUIRE(generated_path.endsWith(filename_component_value)); +} + +TEST_CASE("A directory path generated with a has_trailing_separator_probability of 1.0 always ends with a separator component.", "[Path][Content][SpecialCase]") { + QString separator_component_value{"sep"}; + + auto path_generator = path( + cycle(Catch::Generators::value(QString("device"))), + cycle(Catch::Generators::value(QString("root"))), + cycle(Catch::Generators::value(QString("directory"))), + cycle(Catch::Generators::value(QString("filename"))), + cycle(Catch::Generators::value(copy_value(separator_component_value))), + PathGeneratorConfiguration{}.set_directory_path_probability(1.0).set_has_trailing_separator_probability(1.0) + ); + + auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); + + REQUIRE(generated_path.endsWith(separator_component_value)); +} + +TEST_CASE("A directory path generated with a has_trailing_separator_probability of 0.0 never ends with a separator component.", "[Path][Content][SpecialCase]") { + QString separator_component_value{"sep"}; + + auto path_generator = path( + cycle(Catch::Generators::value(QString("device"))), + cycle(Catch::Generators::value(QString("root"))), + cycle(Catch::Generators::value(QString("directory"))), + cycle(Catch::Generators::value(QString("filename"))), + cycle(Catch::Generators::value(copy_value(separator_component_value))), + PathGeneratorConfiguration{}.set_directory_path_probability(1.0).set_has_trailing_separator_probability(0.0) + ); + + auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); + + REQUIRE(!generated_path.endsWith(separator_component_value)); +} + +SCENARIO("Binding a path to a component range", "[Path][Bounds]") { + GIVEN("A minimum amount of components") { + auto minimum_components_amount = GENERATE(take(100, random(std::size_t{1}, std::size_t{100}))); + + AND_GIVEN("A maximum amount of components that is greater or equal than the minimum amount of components") { + auto maximum_components_amount = GENERATE_COPY(take(100, random(minimum_components_amount, std::size_t{100}))); + + WHEN("A path is generated from those bounds") { + QString countable_component_value{"a"}; + + QString generated_path = GENERATE_COPY( + take(1, + path( + empty_string(), + empty_string(), + cycle(Catch::Generators::value(copy_value(countable_component_value))), + cycle(Catch::Generators::value(copy_value(countable_component_value))), + empty_string(), + PathGeneratorConfiguration{}.set_minimum_components_amount(minimum_components_amount).set_maximum_components_amount(maximum_components_amount) + ) + ) + ); + + THEN("The amount of non device, non root, non separator components in the generated path is in the range [minimum_components_amount, maximum_components_amount]") { + std::size_t components_amount{static_cast(generated_path.count(countable_component_value))}; + + REQUIRE(components_amount >= minimum_components_amount); + REQUIRE(components_amount <= maximum_components_amount); + } + } + } + } +} + +TEST_CASE( + "When the maximum amount of components and the minimum amount of components are equal, all generated paths have the same amount of non device, non root, non separator components", + "[Path][Bounds][SpecialCase]") +{ + auto components_amount = GENERATE(take(10, random(std::size_t{1}, std::size_t{100}))); + + QString countable_component_value{"a"}; + QString generated_path = GENERATE_COPY( + take(10, + path( + empty_string(), + empty_string(), + cycle(Catch::Generators::value(copy_value(countable_component_value))), + cycle(Catch::Generators::value(copy_value(countable_component_value))), + empty_string(), + PathGeneratorConfiguration{}.set_minimum_components_amount(components_amount).set_maximum_components_amount(components_amount) + ) + ) + ); + + REQUIRE(static_cast(generated_path.count(countable_component_value)) == components_amount); +} + +SCENARIO("The format of a path", "[Path][Contents]") { + GIVEN("A series of components generators") { + // TODO: Could probably move this to the global scope to + // lighen the tests. + QString device_component_value{"device"}; + QString root_component_value{"root"}; + QString directory_component_value{"dir"}; + QString filename_component_value{"file"}; + QString separator_component_value{"sep"}; + + auto device_component_generator = cycle(Catch::Generators::value(copy_value(device_component_value))); + auto root_component_generator = cycle(Catch::Generators::value(copy_value(root_component_value))); + auto directory_component_generator = cycle(Catch::Generators::value(copy_value(directory_component_value))); + auto filename_component_generator = cycle(Catch::Generators::value(copy_value(filename_component_value))); + auto separator_component_generator = cycle(Catch::Generators::value(copy_value(separator_component_value))); + + AND_GIVEN("A generator of paths using those components generator") { + // TODO: We should actually randomize the configuration by + // making a simple generator for it. + auto path_generator = path( + std::move(device_component_generator), + std::move(root_component_generator), + std::move(directory_component_generator), + std::move(filename_component_generator), + std::move(separator_component_generator) + ); + + WHEN("A path is generated from that generator") { + auto generated_path = GENERATE_REF(take(10, std::move(path_generator))); + + THEN("At most one device component is in the generated path") { + REQUIRE(generated_path.count(device_component_value) <= 1); + } + + THEN("At most one root component is in the generated path") { + REQUIRE(generated_path.count(root_component_value) <= 1); + } + + THEN("At most one filename component is in the generated path") { + REQUIRE(generated_path.count(filename_component_value) <= 1); + } + + THEN("At least one non device, non root, non separator component is in the generated path") { + REQUIRE((generated_path.contains(directory_component_value) || generated_path.contains(filename_component_value))); + } + + THEN("There is a separator component between any two successive directory components") { + // REMARK: To test this condition, which is not + // easy to test directly, as, if the generator is + // working as it should, the concept of successive + // directories stops existing. + // To test it, then, we split the condition into + // two parts, that are easier to test, that + // achieve the same effect. + // First, if all directories have a separator + // component between them, it is impossible to + // have a directory component that is directly + // followed by another directory component. + // Second, when this holds, any two directory + // components must have one or more non-directory + // components between them. + // For those directories that have exactly one + // component between them, it must be a separator. + // This is equivalent to the original condition as + // long as it is not allowed for anything else to + // be between two directory components that have + // exactly one component between them. + // This is true at the time of writing of this + // test, such that this will work correctly, but + // if this changes the test is invalidated. + // If a test for the original condition is found + // that is not contrived (as it is possible to + // test the original condition but it is a bit + // more complex than we would like the test to + // be), it should replace this current + // implementation to improve the resiliency of the + // test. + REQUIRE_FALSE(generated_path.contains(directory_component_value + directory_component_value)); + + auto successive_directories_re{ + QRegularExpression(u"%1(%2)%3"_s.arg(directory_component_value) + .arg(QStringList{device_component_value, root_component_value, filename_component_value, separator_component_value}.join("|")) + .arg(directory_component_value) + )}; + + auto successive_directories_match(successive_directories_re.match(generated_path)); + while (successive_directories_match.hasMatch()) { + auto in_between_component{successive_directories_match.captured(1)}; + + // TODO: Having this in a loop makes it so + // the amount of assertions will vary slightly + // per-run. + // It would be better to avoid this, even if + // it should not really be a problem + // generally. + // Try to find a better way to express this + // condition that does not require a loop. + // This could be as easy as just collection + // the results and then using a std::all_of. + REQUIRE(in_between_component == separator_component_value); + + successive_directories_match = successive_directories_re.match(generated_path, successive_directories_match.capturedEnd(1)); + } + } + + + THEN("There is a separator component between each successive directory and filename components") { + REQUIRE_FALSE(generated_path.contains(directory_component_value + filename_component_value)); + + auto successive_directory_filename_re{ + QRegularExpression(u"%1(%2)%3"_s.arg(directory_component_value) + .arg(QStringList{device_component_value, root_component_value, filename_component_value, separator_component_value}.join("|")) + .arg(filename_component_value) + )}; + + auto successive_directory_filename_match(successive_directory_filename_re.match(generated_path)); + while (successive_directory_filename_match.hasMatch()) { + auto in_between_component{successive_directory_filename_match.captured(1)}; + + REQUIRE(in_between_component == separator_component_value); + + successive_directory_filename_match = successive_directory_filename_re.match(generated_path, successive_directory_filename_match.capturedEnd(1)); + } + } + } + } + + AND_GIVEN("A generator of paths using those components generator that generates Multi-Device paths") { + auto path_generator = path( + std::move(device_component_generator), + std::move(root_component_generator), + std::move(directory_component_generator), + std::move(filename_component_generator), + std::move(separator_component_generator), + PathGeneratorConfiguration{}.set_multi_device_path_probability(1.0) + ); + + WHEN("A path is generated from that generator") { + auto generated_path = GENERATE_REF(take(10, std::move(path_generator))); + + THEN("Exactly one device component is in the generated path") { + REQUIRE(generated_path.count(device_component_value) == 1); + + AND_THEN("The device component is the first component in the generated path") { + REQUIRE(generated_path.startsWith(device_component_value)); + } + } + } + } + + AND_GIVEN("A generator of paths using those components generator that generates Absolute paths") { + auto path_generator = path( + std::move(device_component_generator), + std::move(root_component_generator), + std::move(directory_component_generator), + std::move(filename_component_generator), + std::move(separator_component_generator), + PathGeneratorConfiguration{}.set_absolute_path_probability(1.0) + ); + + WHEN("A path is generated from that generator") { + auto generated_path = GENERATE_REF(take(10, std::move(path_generator))); + + THEN("Exactly one root component is in the generated path") { + REQUIRE(generated_path.count(root_component_value) == 1); + } + } + } + + AND_GIVEN("A generator of paths using those components generator that generates Absolute paths that are not Multi-Device") { + auto path_generator = path( + std::move(device_component_generator), + std::move(root_component_generator), + std::move(directory_component_generator), + std::move(filename_component_generator), + std::move(separator_component_generator), + PathGeneratorConfiguration{}.set_multi_device_path_probability(0.0).set_absolute_path_probability(1.0) + ); + + WHEN("A path is generated from that generator") { + auto generated_path = GENERATE_REF(take(10, std::move(path_generator))); + + THEN("The root component is the first component in the generated path") { + REQUIRE(generated_path.startsWith(root_component_value)); + } + } + } + + AND_GIVEN("A generator of paths using those components generator that generates Multi-Device, Absolute paths") { + auto path_generator = path( + std::move(device_component_generator), + std::move(root_component_generator), + std::move(directory_component_generator), + std::move(filename_component_generator), + std::move(separator_component_generator), + PathGeneratorConfiguration{}.set_multi_device_path_probability(1.0).set_absolute_path_probability(1.0) + ); + + WHEN("A path is generated from that generator") { + auto generated_path = GENERATE_REF(take(10, std::move(path_generator))); + + THEN("The root component succeeds the device component in the generated path") { + REQUIRE(generated_path.contains(device_component_value + root_component_value)); + } + } + } + + AND_GIVEN("A generator of paths using those components generator that generates paths that are To a Directory and do not Have a Trailing Separator") { + auto path_generator = path( + std::move(device_component_generator), + std::move(root_component_generator), + std::move(directory_component_generator), + std::move(filename_component_generator), + std::move(separator_component_generator), + PathGeneratorConfiguration{}.set_directory_path_probability(1.0).set_has_trailing_separator_probability(0.0) + ); + + WHEN("A path is generated from that generator") { + auto generated_path = GENERATE_REF(take(10, std::move(path_generator))); + + THEN("The last component of in the path is a directory component") { + REQUIRE(generated_path.endsWith(directory_component_value)); + } + } + } + + AND_GIVEN("A generator of paths using those components generator that generates paths that are To a Directory and Have a Trailing Separator") { + auto path_generator = path( + std::move(device_component_generator), + std::move(root_component_generator), + std::move(directory_component_generator), + std::move(filename_component_generator), + std::move(separator_component_generator), + PathGeneratorConfiguration{}.set_directory_path_probability(1.0).set_has_trailing_separator_probability(1.0) + ); + + WHEN("A path is generated from that generator") { + auto generated_path = GENERATE_REF(take(10, std::move(path_generator))); + + THEN("The last component in the path is a separator component that is preceded by a directory component") { + REQUIRE(generated_path.endsWith(directory_component_value + separator_component_value)); + } + } + } + + + AND_GIVEN("A generator of paths using those components generator that generates paths that are To a File") { + auto path_generator = path( + std::move(device_component_generator), + std::move(root_component_generator), + std::move(directory_component_generator), + std::move(filename_component_generator), + std::move(separator_component_generator), + PathGeneratorConfiguration{}.set_directory_path_probability(0.0) + ); + + WHEN("A path is generated from that generator") { + auto generated_path = GENERATE_REF(take(10, std::move(path_generator))); + + THEN("Exactly one filename component is in the path") { + REQUIRE(generated_path.contains(filename_component_value)); + + AND_THEN("The filename component is the last component in the path") { + REQUIRE(generated_path.endsWith(filename_component_value)); + } + } + } + } + } +} + +// REMARK: [mayfail][distribution] +SCENARIO("Observing the distribution of paths based on their configuration", "[Path][Statistics][!mayfail]") { + GIVEN("A series of components generators") { + QString device_component_value{"device"}; + QString root_component_value{"root"}; + QString directory_component_value{"dir"}; + QString filename_component_value{"file"}; + QString separator_component_value{"sep"}; + + auto device_component_generator = cycle(Catch::Generators::value(copy_value(device_component_value))); + auto root_component_generator = cycle(Catch::Generators::value(copy_value(root_component_value))); + auto directory_component_generator = cycle(Catch::Generators::value(copy_value(directory_component_value))); + auto filename_component_generator = cycle(Catch::Generators::value(copy_value(filename_component_value))); + auto separator_component_generator = cycle(Catch::Generators::value(copy_value(separator_component_value))); + + AND_GIVEN("A generator of paths using those components generator that produces paths that are Multi-Device with a probability of n") { + double multi_device_path_probability = GENERATE(take(10, random(0.0, 1.0))); + + auto path_generator = path( + std::move(device_component_generator), + std::move(root_component_generator), + std::move(directory_component_generator), + std::move(filename_component_generator), + std::move(separator_component_generator), + PathGeneratorConfiguration{}.set_multi_device_path_probability(multi_device_path_probability) + ); + + WHEN("A certain amount of paths are generated from that generator") { + auto paths = GENERATE_REF(take(1, chunk(10000, std::move(path_generator)))); + + THEN("The amount of paths that are Multi-Device approximately respects the given probability and the amount of paths that are not approximately respects a probability of 1 - n") { + auto maybe_distribution_error{respects_distribution( + std::move(paths), + [&device_component_value](const QString& path){ return (path.startsWith(device_component_value)) ? "Multi-Device" : "Non Multi-Device"; }, + [multi_device_path_probability](const QString& key){ return probability_to_percentage((key == "Multi-Device") ? multi_device_path_probability : 1 - multi_device_path_probability); } + )}; + + REQUIRE_FALSE(maybe_distribution_error); + } + } + } + + AND_GIVEN("A generator of paths using those components generator that produces paths that are Absolute with a probability of n") { + double absolute_path_probability = GENERATE(take(10, random(0.0, 1.0))); + + auto path_generator = path( + std::move(device_component_generator), + std::move(root_component_generator), + std::move(directory_component_generator), + std::move(filename_component_generator), + std::move(separator_component_generator), + PathGeneratorConfiguration{}.set_absolute_path_probability(absolute_path_probability) + ); + + WHEN("A certain amount of paths are generated from that generator") { + auto paths = GENERATE_REF(take(1, chunk(10000, std::move(path_generator)))); + + THEN("The amount of paths that are Absolute approximately respects the given probability and the amount of paths that are Relative approximately respects a probability of 1 - n") { + auto maybe_distribution_error{respects_distribution( + std::move(paths), + [&root_component_value](const QString& path){ return (path.contains(root_component_value)) ? "Absolute" : "Relative"; }, + [absolute_path_probability](const QString& key){ return probability_to_percentage((key == "Absolute") ? absolute_path_probability : 1 - absolute_path_probability); } + )}; + + REQUIRE_FALSE(maybe_distribution_error); + } + } + } + + AND_GIVEN("A generator of paths using those components generator that produces paths that are To a Directory with a probability of n") { + double directory_path_probability = GENERATE(take(10, random(0.0, 1.0))); + + auto path_generator = path( + std::move(device_component_generator), + std::move(root_component_generator), + std::move(directory_component_generator), + std::move(filename_component_generator), + std::move(separator_component_generator), + PathGeneratorConfiguration{}.set_directory_path_probability(directory_path_probability) + ); + + WHEN("A certain amount of paths are generated from that generator") { + auto paths = GENERATE_REF(take(1, chunk(10000, std::move(path_generator)))); + + THEN("The amount of paths that are To a Directory approximately respects the given probability and the amount of paths that are To a File approximately respects a probability of 1 - n") { + auto maybe_distribution_error{respects_distribution( + std::move(paths), + [&filename_component_value](const QString& path){ return (path.contains(filename_component_value)) ? "To a File" : "To a Directory"; }, + [directory_path_probability](const QString& key){ return probability_to_percentage((key == "To a Directory") ? directory_path_probability : 1 - directory_path_probability); } + )}; + + REQUIRE_FALSE(maybe_distribution_error); + } + } + } + + AND_GIVEN("A generator of paths using those components generator that produces paths that are To a Directory with a probability of n to Have a Trailing Separator") { + double has_trailing_separator_probability = GENERATE(take(10, random(0.0, 1.0))); + + auto path_generator = path( + std::move(device_component_generator), + std::move(root_component_generator), + std::move(directory_component_generator), + std::move(filename_component_generator), + std::move(separator_component_generator), + PathGeneratorConfiguration{}.set_directory_path_probability(1.0).set_has_trailing_separator_probability(has_trailing_separator_probability) + ); + + WHEN("A certain amount of paths are generated from that generator") { + auto paths = GENERATE_REF(take(1, chunk(10000, std::move(path_generator)))); + + THEN("The amount of paths that are Have a Trailing Separator approximately respects the given probability and the amount of paths that do not Have a Trailing Separator approximately respects a probability of 1 - n") { + auto maybe_distribution_error{respects_distribution( + std::move(paths), + [&separator_component_value](const QString& path){ return (path.endsWith(separator_component_value)) ? "Have a Trailing Separator" : "Doesn't Have a Trailing Separator"; }, + [has_trailing_separator_probability](const QString& key){ return probability_to_percentage((key == "Have a Trailing Separator") ? has_trailing_separator_probability : 1 - has_trailing_separator_probability); } + )}; + + REQUIRE_FALSE(maybe_distribution_error); + } + } + } + } +} + +TEST_CASE("The first component of the passed in device components generator is not lost", "[Path][GeneratorFirstElement][SpecialCase]") { + QString device_component_generator_first_value{"device"}; + + auto generated_path = GENERATE_COPY(take(1, + path( + values({device_component_generator_first_value, QString{""}}), + empty_string(), + empty_string(), + empty_string(), + empty_string(), + PathGeneratorConfiguration{} + .set_multi_device_path_probability(1.0) + .set_minimum_components_amount(1) + .set_maximum_components_amount(1) + ) + )); + + REQUIRE(generated_path.contains(device_component_generator_first_value)); +} + +TEST_CASE("The first component of the passed in root components generator is not lost", "[Path][GeneratorFirstElement][SpecialCase]") { + QString root_component_generator_first_value{"root"}; + + auto generated_path = GENERATE_COPY(take(1, + path( + empty_string(), + values({root_component_generator_first_value, QString{""}}), + empty_string(), + empty_string(), + empty_string(), + PathGeneratorConfiguration{} + .set_absolute_path_probability(1.0) + .set_minimum_components_amount(1) + .set_maximum_components_amount(1) + ) + )); + + REQUIRE(generated_path.contains(root_component_generator_first_value)); +} + +TEST_CASE("The first component of the passed in directory components generator is not lost", "[Path][GeneratorFirstElement][SpecialCase]") { + QString directory_component_generator_first_value{"dir"}; + + auto generated_path = GENERATE_COPY(take(1, + path( + empty_string(), + empty_string(), + values({directory_component_generator_first_value, QString{""}}), + empty_string(), + empty_string(), + PathGeneratorConfiguration{} + .set_directory_path_probability(1.0) + .set_minimum_components_amount(1) + .set_maximum_components_amount(1) + ) + )); + + REQUIRE(generated_path.contains(directory_component_generator_first_value)); +} + +TEST_CASE("The first component of the passed in filename components generator is not lost", "[Path][GeneratorFirstElement][SpecialCase]") { + QString filename_component_generator_first_value{"dir"}; + + auto generated_path = GENERATE_COPY(take(1, + path( + empty_string(), + empty_string(), + empty_string(), + values({filename_component_generator_first_value, QString{""}}), + empty_string(), + PathGeneratorConfiguration{} + .set_directory_path_probability(0.0) + .set_minimum_components_amount(1) + .set_maximum_components_amount(1) + ) + )); + + REQUIRE(generated_path.contains(filename_component_generator_first_value)); +} + +TEST_CASE("The first component of the passed in separator components generator is not lost", "[Path][GeneratorFirstElement][SpecialCase]") { + QString separator_component_generator_first_value{"sep"}; + + auto generated_path = GENERATE_COPY(take(1, + path( + empty_string(), + empty_string(), + empty_string(), + empty_string(), + values({separator_component_generator_first_value, QString{""}}), + PathGeneratorConfiguration{} + .set_directory_path_probability(0.0) + .set_minimum_components_amount(2) + .set_maximum_components_amount(2) + ) + )); + + REQUIRE(generated_path.contains(separator_component_generator_first_value)); +} + +SCENARIO("Generating paths that are suitable to be used on POSIX systems", "[Path][POSIX][Content]") { + GIVEN("A generator that generates Strings representing paths on a POSIX system that are portable") { + auto path_generator = relaxed_portable_posix_path(); + + WHEN("A path is generated from it") { + auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); + + THEN("The path is composed only by one or more characters in the class [-_./a-zA-Z0-9]") { + REQUIRE(QRegularExpression{R"(\A[-_.\/a-zA-Z0-9]+\z)"}.match(generated_path).hasMatch()); + } + } + } +} + +SCENARIO("Generating paths that are suitable to be used on Windows", "[Path][Windows][Content]") { + GIVEN("A generator that generates Strings representing paths on a Windows system") { + auto path_generator = traditional_dos_path(); + + WHEN("A path is generated from it") { + auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); + + CAPTURE(generated_path); + + THEN("The path starts with an uppercase letter followed by a colon, a backward or forward slash or a character in the class [-_.a-zA-Z0-9]") { + QRegularExpression beginning_re{"([A-Z]:|\\|\\/|[-_.a-zA-Z0-9])"}; + + auto beginning_match{beginning_re.match(generated_path)}; + + REQUIRE(beginning_match.hasMatch()); + + generated_path.remove(0, beginning_match.capturedEnd()); + + AND_THEN("The rest of the path is composed by zero or more characters in the class [-_./\\a-zA-Z0-9]") { + REQUIRE(QRegularExpression{R"(\A[-_.\/\\a-zA-Z0-9]*\z)"}.match(generated_path).hasMatch()); + } + } + } + } +} diff --git a/src/qdoc/catch_generators/tests/generators/catch_qchar_generator.cpp b/src/qdoc/catch_generators/tests/generators/catch_qchar_generator.cpp new file mode 100644 index 000000000..718da7307 --- /dev/null +++ b/src/qdoc/catch_generators/tests/generators/catch_qchar_generator.cpp @@ -0,0 +1,102 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include +#include + +#include + +#include + +#include + +using namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE; +using namespace QDOC_CATCH_GENERATORS_QCHAR_ALPHABETS_NAMESPACE; + +SCENARIO("Binding a generated QChar to a range", "[QChar][Bounds]") { + GIVEN("A lower bound") { + auto lower_bound = GENERATE(take(100, random( + static_cast(std::numeric_limits::min()), + static_cast(std::numeric_limits::max()) + ))); + + AND_GIVEN("An upper bound that is greater or equal than the lower bound") { + auto upper_bound = GENERATE_COPY(take(100, random(lower_bound, static_cast(std::numeric_limits::max())))); + + WHEN("A QChar is generated from those bounds") { + QChar generated_character = GENERATE_COPY(take(1, character(lower_bound, upper_bound))); + + THEN("The generated character has a unicode value in the range [lower_bound, upper_bound]") { + REQUIRE(generated_character.unicode() >= lower_bound); + REQUIRE(generated_character.unicode() <= upper_bound); + } + } + } + } +} + +TEST_CASE( + "When lower_bound and upper_bound are equal, let their value be n, the only generated character is the one with unicode value n", + "[QChar][Bounds]" +) { + auto bound = GENERATE(take(100, random( + static_cast(std::numeric_limits::min()), + static_cast(std::numeric_limits::max()) + ))); + auto generated_character = GENERATE_COPY(take(100, character(bound, bound))); + + REQUIRE(generated_character.unicode() == bound); +} + +TEST_CASE("When generating digits, each generated character is in the class [0-9]", "[QChar][SpecialCase]") { + auto generated_character = GENERATE(take(100, digit())); + + REQUIRE(generated_character >= '0'); + REQUIRE(generated_character <= '9'); +} + +TEST_CASE("When generating lowercase ascii characters, each generated character is in the class [a-z]", "[QChar][SpecialCase]") { + auto generated_character = GENERATE(take(100, ascii_lowercase())); + + REQUIRE(generated_character >= 'a'); + REQUIRE(generated_character <= 'z'); +} + +TEST_CASE("When generating uppercase ascii characters, each generated character is in the class [A-Z]", "[QChar][SpecialCase]") { + auto generated_character = GENERATE(take(100, ascii_uppercase())); + + REQUIRE(generated_character >= 'A'); + REQUIRE(generated_character <= 'Z'); +} + +TEST_CASE("When generating ascii alphabetic characters, each generated character is in the class [a-zA-Z]", "[QChar][SpecialCase]") { + auto generated_character = GENERATE(take(100, ascii_alpha())); + + REQUIRE(( + (generated_character >= 'a' && generated_character <= 'z') || + (generated_character >= 'A' && generated_character <= 'Z') + )); +} + +TEST_CASE("When generating ascii alphabetic characters, each generated character is in the class [a-zA-Z0-9]", "[QChar][SpecialCase]") { + auto generated_character = GENERATE(take(100, ascii_alpha())); + + REQUIRE(( + (generated_character >= 'a' && generated_character <= 'z') || + (generated_character >= 'A' && generated_character <= 'Z') || + (generated_character >= '0' && generated_character <= '9') + )); +} + +TEST_CASE("When generating portable posix filename, each generated character is in the class [-_.a-zA-Z0-9]", "[QChar][SpecialCase]") { + auto generated_character = GENERATE(take(100, ascii_alpha())); + + REQUIRE(( + (generated_character == '-') || + (generated_character == '_') || + (generated_character == '.') || + (generated_character >= 'a' && generated_character <= 'z') || + (generated_character >= 'A' && generated_character <= 'Z') || + (generated_character >= '0' && generated_character <= '9') + )); +} diff --git a/src/qdoc/catch_generators/tests/generators/catch_qstring_generator.cpp b/src/qdoc/catch_generators/tests/generators/catch_qstring_generator.cpp new file mode 100644 index 000000000..0e92f6900 --- /dev/null +++ b/src/qdoc/catch_generators/tests/generators/catch_qstring_generator.cpp @@ -0,0 +1,89 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include +#include +#include + +#include + +#include + +using namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE; + +#include + +SCENARIO("Binding a QString to a length range", "[QString][Bounds]") { + GIVEN("A minimum length") { + auto minimum_length = GENERATE(take(100, random(0, 100))); + + AND_GIVEN("A maximum length that is greater or equal than the minimum length") { + auto maximum_length = GENERATE_COPY(take(100, random(minimum_length, 100))); + + WHEN("A QString is generated from those bounds") { + QString generated_string = GENERATE_COPY(take(1, string(character(), minimum_length, maximum_length))); + + THEN("The generated string's length is in the range [minimum_length, maximum_length]") { + REQUIRE(generated_string.size() >= minimum_length); + REQUIRE(generated_string.size() <= maximum_length); + } + } + } + } +} + +TEST_CASE("When the maximum length and the minimum length are zero all generated strings are the empty string", "[QString][Bounds][SpecialCase][BoundingValue]") { + QString generated_string = GENERATE(take(100, string(character(), 0, 0))); + + REQUIRE(generated_string.isEmpty()); +} + +TEST_CASE("When the maximum length and the minimum length are equal, all generated strings have the same length equal to the given length", "[QString][Bounds][SpecialCase]") { + auto length = GENERATE(take(100, random(0, 100))); + auto generated_string = GENERATE_COPY(take(100, string(character(), length, length))); + + REQUIRE(generated_string.size() == length); +} + +SCENARIO("Limiting the characters that can compose a QString", "[QString][Contents]") { + GIVEN("A list of characters candidates") { + auto lower_character_bound = GENERATE(take(10, random( + static_cast(std::numeric_limits::min()), + static_cast(std::numeric_limits::max()) + ))); + auto upper_character_bound = GENERATE_COPY(take(10, random(lower_character_bound, static_cast(std::numeric_limits::max())))); + + auto character_candidates = character(lower_character_bound, upper_character_bound); + + WHEN("A QString is generated from that list") { + QString generated_string = GENERATE_REF(take(100, string(std::move(character_candidates), 1, 50))); + + THEN("The string is composed only of characters that are in the list of characters") { + REQUIRE( + std::all_of( + generated_string.cbegin(), generated_string.cend(), + [lower_character_bound, upper_character_bound](QChar element){ return element.unicode() >= lower_character_bound && element.unicode() <= upper_character_bound; } + ) + ); + } + } + } +} + +TEST_CASE("The strings generated by a generator of empty string are all empty", "[QString][Contents]") { + QString generated_string = GENERATE(take(100, empty_string())); + + REQUIRE(generated_string.isEmpty()); +} + + +TEST_CASE("The first element of the passsed in generator is not lost", "[QString][GeneratorFirstElement][SpecialCase]") { + QChar first_value{'a'}; + + // REMARK: We use two values to avoid having the generator throw + // an exception if the first element is actually lost. + auto character_generator{Catch::Generators::values({first_value, QChar{'b'}})}; + auto generated_string = GENERATE_REF(take(1, string(std::move(character_generator), 1, 1))); + + REQUIRE(generated_string == QString{first_value}); +} diff --git a/src/qdoc/catch_generators/tests/generators/combinators/catch_cycle_generator.cpp b/src/qdoc/catch_generators/tests/generators/combinators/catch_cycle_generator.cpp new file mode 100644 index 000000000..5bf98d73a --- /dev/null +++ b/src/qdoc/catch_generators/tests/generators/combinators/catch_cycle_generator.cpp @@ -0,0 +1,70 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include +#include + +#include + +using namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE; + +// REMARK: We use fixed-values-generators for those tests so that it +// is trivial to identify when their generation will end, which +// values we should expect and how many values we should expect. +// This is unfortunately not general, but we don't have, by default, +// enough tools to generalize this without having to provide our own +// (being able to generate fixed values from a vector) and adding more +// to the complexity, which is already high. + +TEST_CASE( + "The xn + m element, where 0 < m < n, from a repeating generator whose underlying generator produces n elements, will produce an element equivalent to the mth element of the generation produced by the underlying generator", + "[Cycle][Combinators]" +) { + std::size_t n{10}; + + auto owned_generator{Catch::Generators::values({'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'})}; + auto owned_generator_copy{Catch::Generators::values({'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'})}; + + auto original_generation = GENERATE_REF(take(1, chunk(n, std::move(owned_generator_copy)))); + + std::size_t x = GENERATE(take(10, random(std::size_t{0}, std::size_t{20}))); + std::size_t m = GENERATE_COPY(take(10, random(std::size_t{1}, std::size_t{n}))); + + auto repeating_generator = cycle(std::move(owned_generator)); + auto repeating_generation = GENERATE_REF(take(1, chunk((x * n) + m, std::move(repeating_generator)))); + + REQUIRE(repeating_generation.back() == original_generation[m - 1]); +} + +SCENARIO("Repeating a generation ad infinitum", "[Cycle][Combinators]") { + GIVEN("Some finite generator") { + std::size_t values_amount{3}; + + auto owned_generator{Catch::Generators::values({'a', 'b', 'c'})}; + auto owned_generator_copy{Catch::Generators::values({'a', 'b', 'c'})}; + + AND_GIVEN("A way to repeat the generation of that generator infinitely") { + auto repeating_generator = cycle(std::move(owned_generator)); + + WHEN("Generating exactly enough values to exhaust the original generator") { + auto repeating_generation = GENERATE_REF(take(1, chunk(values_amount, std::move(repeating_generator)))); + auto original_generation = GENERATE_REF(take(1, chunk(values_amount, std::move(owned_generator_copy)))); + + THEN("The repeating generator behaves equally to the original finite generator") { + REQUIRE(repeating_generation == original_generation); + } + } + + WHEN("Generating exactly n times the amount of values required to exhaust the original generator") { + std::size_t n = GENERATE(take(10, random(2, 10))); + + auto original_generation = GENERATE_REF(take(1, chunk(values_amount, std::move(owned_generator_copy)))); + auto repeating_generation = GENERATE_REF(take(n, chunk(values_amount, std::move(repeating_generator)))); + + THEN("The n generation of the repeating generator are always the same as the generation of the original generation") { + REQUIRE(repeating_generation == original_generation); + } + } + } + } +} diff --git a/src/qdoc/catch_generators/tests/generators/combinators/catch_oneof_generator.cpp b/src/qdoc/catch_generators/tests/generators/combinators/catch_oneof_generator.cpp new file mode 100644 index 000000000..4d5666213 --- /dev/null +++ b/src/qdoc/catch_generators/tests/generators/combinators/catch_oneof_generator.cpp @@ -0,0 +1,362 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +using namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE; +using namespace QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE; + +SCENARIO("Choosing between one of many generators", "[OneOf][Combinators]") { + GIVEN("Some generators producing values of the same type") { + auto generators_amount = GENERATE(take(10, random(1, 10))); + auto generators_values = GENERATE_COPY(take(10, chunk(generators_amount, random(0, 100000)))); + + std::vector> generators; + generators.reserve(generators_amount); + std::transform( + generators_values.begin(), generators_values.end(), std::back_inserter(generators), + [](auto& value){ return Catch::Generators::value(copy_value(value)); } + ); + + AND_GIVEN("A generator choosing between them based on some distribution") { + std::vector weights = GENERATE_COPY(take(10, k_partition_of_r(100.0, generators_amount))); + auto choosing_generator = oneof(std::move(generators), std::move(weights)); + + WHEN("A value is extracted from the choosing generator") { + auto generated_value = GENERATE_REF(take(100, std::move(choosing_generator))); + + THEN("The generated value is a member of one of the original generators") { + REQUIRE(std::find(generators_values.cbegin(), generators_values.cend(), generated_value) != generators_values.cend()); + } + } + } + + AND_GIVEN("A generator choosing between them with the same probability") { + auto choosing_generator = uniform_oneof(std::move(generators)); + + WHEN("A value is extracted from the choosing generator") { + auto generated_value = GENERATE_REF(take(100, std::move(choosing_generator))); + + THEN("The generated value is a member of one of the original generators") { + REQUIRE(std::find(generators_values.cbegin(), generators_values.cend(), generated_value) != generators_values.cend()); + } + } + } + + AND_GIVEN("A generator choosing between them such that each possible value has the same probability of being chosen") { + auto choosing_generator = uniformly_valued_oneof(std::move(generators), std::vector(generators_amount, std::size_t{1})); + + WHEN("A value is extracted from the choosing generator") { + auto generated_value = GENERATE_REF(take(100, std::move(choosing_generator))); + + THEN("The generated value is a member of one of the original generators") { + REQUIRE(std::find(generators_values.cbegin(), generators_values.cend(), generated_value) != generators_values.cend()); + } + } + } + } +} + +// TODO: The following is a generally complex test. Nonetheless, we +// can probably ease some of the complexity by moving it out into some +// generators or by abstracting it a little to remove the need to know +// some of the implementation details. +// Check if this is possible. + +// REMARK: [mayfail][distribution] +// This tests cannot be precise as it depends on randomized output. +// For this reason, we mark it as !mayfail. +// This allows us to see cases where it fails without having the +// test-run itself fail. +// We generally expect this test to not fail, but it may fail randomly +// every now and then simply because of how a correctly randomized +// distribution may behave. +// As long as this test doesn't fail consistently, with values that +// shows an unsustainable deviation, it should be considered to be +// working. +SCENARIO("Observing the distribution of generators that are chosen from", "[OneOf][Combinators][Statistics][!mayfail]") { + GIVEN("Some generators producing values of the same type") { + std::size_t generators_amount = GENERATE(take(10, random(1, 10))); + + // REMARK: To test the distribution, we want to have some + // amount of generators to choose from whose generated values + // can be uniquely reconducted to the generating generator so + // that we may count how many times a specific generator was + // chosen. + // The easiest way would be to have generators that produce a + // single value. + // Nonetheless, to test the version that provides an + // approximate uniform distribution over the values themselves + // correctly, we need to have generators that can produce a + // different amount of elements. + // When that is not the case, indeed, a generator that + // approximately distributes uniformly over values is + // equivalent to one that approximately distributes uniformely + // over the generators themselves. + // As such, we use ranges of positive integers, as they are + // the simplest multi-valued finite generator that can be dinamically + // construted, while still providing an easy way to infer the + // amount of values it contains so that we can derive the + // cardinality of our domain. + // We produce those ranges as disjoint subsequent ranges + // starting from 0 upward. + // We require the ranges to be disjoint so that we do not lose + // the ability of uniquely identifying a generator that + // produced the value. + // + // To do so, we generate a series of disjoint least upper + // bounds for the ranges. + // Then, we produce the ith range by using the successor of + // the (i - 1)th upper bound as its lower bound and the ith + // upper bound as its upper bound. + // + // We take further care to ensure that the collection of upper + // bounds is sorted, as this simplifies to a linear search our + // need to index the collection of generators to find the + // identifying generator and its associated probability. + std::vector generators_bounds(generators_amount, 0); + std::vector> generators; + generators.reserve(generators_amount); + + std::size_t lowest_bound{0}; + std::size_t generators_step{1000}; + std::size_t lower_bound_offset{1}; + + generators_bounds[0] = Catch::Generators::random(lowest_bound, generators_step).get(); + generators.push_back(Catch::Generators::random(lowest_bound, generators_bounds[0])); + + // We use this one to group together values that are generated + // from the same generator and to provide an index for that + // generator to use for finding its associated probability. + // Since our generators are defined by their upper bounds and + // the collection of upper bounds is sorted, the first + // encountered upper bound that is not less than the value + // itself must be the least upper bound of the generator that + // produced the value. + // Then, the index of that upper bound must be the same as the + // index of the producing generator and its associated + // probability. + auto find_index_of_producing_generator = [&generators_bounds](auto value) { + return static_cast(std::distance( + generators_bounds.begin(), + std::find_if(generators_bounds.begin(), generators_bounds.end(), [&value](auto element){ return value <= element; }) + )); + }; + + for (std::size_t index{1}; index < generators_amount; ++index) { + generators_bounds[index] = Catch::Generators::random(generators_bounds[index - 1] + lower_bound_offset + 1, generators_bounds[index - 1] + lower_bound_offset + 1 + generators_step).get(); + generators.push_back(Catch::Generators::random(generators_bounds[index - 1] + lower_bound_offset, generators_bounds[index])); + } + + AND_GIVEN("A probability of being chosen, in percentage, for each of the generators, such that the sum of the percentages is one hundred") { + std::vector probabilities = GENERATE_COPY(take(10, k_partition_of_r(100.0, generators_amount))); + + AND_GIVEN("A choosing generator for those generators based on the given probabilities") { + auto choosing_generator = oneof(std::move(generators), probabilities); + + WHEN("A certain amount of values are generated from the choosing generator") { + auto values = GENERATE_REF(take(1, chunk(10000, std::move(choosing_generator)))); + + THEN("The distribution of elements for each generator approximately respects the weight that was given to it") { + auto maybe_distribution_error{respects_distribution( + std::move(values), + find_index_of_producing_generator, + [&probabilities](auto key){ return probabilities[key]; } + )}; + + REQUIRE_FALSE(maybe_distribution_error); + } + } + } + } + + AND_GIVEN("A choosing generator for those generators that will choose each generator with the same probability") { + auto choosing_generator = uniform_oneof(std::move(generators)); + + WHEN("A certain amount of values are generated from the choosing generator") { + auto values = GENERATE_REF(take(1, chunk(10000, std::move(choosing_generator)))); + + THEN("The distribution of elements approximates uniformity over the generators") { + double probability{uniform_probability(generators_amount)}; + + auto maybe_distribution_error{respects_distribution( + std::move(values), + find_index_of_producing_generator, + [&probability](auto _){ (void)(_); return probability; } + )}; + + REQUIRE_FALSE(maybe_distribution_error); + } + } + } + + AND_GIVEN("A choosing generator for those generators that will choose each generator such that each possible value has the same probability of being chosen") { + // REMARK: We need to know the total amount of + // unique values that can be generated by our + // generators, so that we can construct an + // appropriate distribution. + // Since our generators are ranges defined by the + // collection of upper bounds we can find their + // length by finding the difference between + // adjacent elements of the collection. + // + // Some more care must be taken to ensure tha the + // correct amount is produced. + // Since we need our ranges to be disjoint, we + // apply a small offset from the element of the + // upper bounds that is used as a lower bound, + // since that upper bound is inclusive for the + // range that precedes the one we are making the + // calculation for. + // + // Furthermore, the first range is treated + // specially. + // As no range precedes it, it doesn't need any + // offset to be applied. + // Additionally, we implicitly use 0 as the first + // lower bound, such that the length of the first + // range is indeed equal to its upper bound. + // + // To account for this, we remove that offset from + // the total amount for each range after the first + // one and use the first upper bound as a seeding + // value to account for the length of the first + // range. + std::vector generators_cardinality(generators_amount, generators_bounds[0]); + + std::adjacent_difference(generators_bounds.begin(), generators_bounds.end(), generators_bounds.begin()); + std::transform(std::next(generators_cardinality.begin()), generators_cardinality.end(), std::next(generators_cardinality.begin()), [](auto element){ return element - 1; }); + + std::size_t output_cardinality{std::accumulate(generators_cardinality.begin(), generators_cardinality.end(), std::size_t{0})}; + + auto choosing_generator = uniformly_valued_oneof(std::move(generators), std::move(generators_cardinality)); + + WHEN("A certain amount of values are generated from the choosing generator") { + auto values = GENERATE_REF(take(1, chunk(10000, std::move(choosing_generator)))); + + THEN("The distribution of elements approximates uniformity for each value") { + double probability{uniform_probability(output_cardinality)}; + + auto maybe_distribution_error{respects_distribution( + std::move(values), + [](auto value){ return value; }, + [&probability](auto _){ (void)(_); return probability; } + )}; + + REQUIRE_FALSE(maybe_distribution_error); + } + } + } + } +} + +TEST_CASE("A generator with a weight of zero is never chosen when choosing between many generators", "[OneOf][Combinators][SpecialCase]") { + auto excluded_value = GENERATE(take(100, random(0, 10000))); + + std::vector> generators; + generators.reserve(2); + generators.emplace_back(Catch::Generators::random(excluded_value + 1, std::numeric_limits::max())); + generators.emplace_back(Catch::Generators::value(copy_value(excluded_value))); + + auto generated_value = GENERATE_REF(take(100, oneof(std::move(generators), std::vector{100.0, 0.0}))); + + REQUIRE(generated_value != excluded_value); +} + +TEST_CASE("The first element of the passed in generators are not lost", "[OneOf][Combinators][GeneratorFirstElement][SpecialCase]") { + // REMARK: We want to test that, for each generator, the first + // time it is chosen the first value is produced. + // This is complicated because of the fact that OneOf chooses + // random generators in a random order. + // This means that some generators may never be chosen, never be + // chosen more than once and so on. + // Furthermore, this specific test is particularly important only + // for finite generators or non-completely random, ordered, + // infinite generators. + // Additionally, we need to ensure that we test with multiple + // generators, as this test is a consequence of a first bugged + // implementation where only the first chosen generator respected + // the first value, which would pass a test where a single + // generator is used. + // + // This is non-trivial due to the randomized nature of OneOf. + // It can be simplified if we express it in a non-deterministic + // way and mark it as mayfail, where we can recognize with a good + // certainty that the test is actually passing. + // + // To avoid having this flaky test, we approach it as follows: + // + // We provide some amount of infinite generators. Those generators + // are ensured to produce one specific value as their first value + // and then infinitely produce a different value. + // We ensure that each generator that is provided produces unique + // values, that is, no two generators produce a first value or 1 < + // nth value that is equal to the one produced by another + // generator. + // + // Then we pass those generators to oneof and generate enough + // values such that at least one of the generators must have been + // chosen twice or more, at random. + // + // We count the appearances of each value in the produced set. + // Then, if a value that is generated by the 1 < nth choice of a + // specific generator is encountered, we check that the first + // value that the specific generator would produce is in the set + // of values that were generated. + // That is, if a generator has produced his non-first value, it + // must have been chosen twice or more. + // This in turn implies that the first time that the generator was + // chosen, its first value was actually produced. + + struct IncreaseAfterFirst { + std::size_t increase; + bool first_application = true; + + std::size_t operator()(std::size_t value) { + if (first_application) { + first_application = false; + return value; + } + + return value + increase; + } + }; + + std::size_t maximum_generator_amount{100}; + auto generators_amount = GENERATE_COPY(take(10, random(std::size_t{1}, maximum_generator_amount))); + + std::vector> generators; + generators.reserve(generators_amount); + + for (std::size_t index{0}; index < generators_amount; ++index) { + generators.push_back(Catch::Generators::map(IncreaseAfterFirst{maximum_generator_amount}, cycle(Catch::Generators::value(copy_value(index))))); + } + + auto values = GENERATE_REF(take(1, chunk(generators_amount + 1, uniform_oneof(std::move(generators))))); + auto histogram{make_histogram(values.begin(), values.end(), [](auto e){ return e; })}; + + for (std::size_t index{0}; index < generators_amount; ++index) { + std::size_t second_value{index + maximum_generator_amount}; + histogram.try_emplace(second_value, 0); + + if (histogram[second_value] > 0) { + REQUIRE(histogram.find(index) != histogram.end()); + } + } +} diff --git a/src/qdoc/catch_generators/tests/main.cpp b/src/qdoc/catch_generators/tests/main.cpp new file mode 100644 index 000000000..48ce73f12 --- /dev/null +++ b/src/qdoc/catch_generators/tests/main.cpp @@ -0,0 +1,13 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#define CATCH_CONFIG_RUNNER +#include + +// A custom main was provided to avoid linking errors when using minGW +// that were appearing in CI. +// See https://github.com/catchorg/Catch2/issues/1287 +int main(int argc, char *argv[]) +{ + return Catch::Session().run(argc, argv); +} diff --git a/src/qdoc/catch_generators/tests/utilities/semantics/catch_generator_handler.cpp b/src/qdoc/catch_generators/tests/utilities/semantics/catch_generator_handler.cpp new file mode 100644 index 000000000..b99a6515d --- /dev/null +++ b/src/qdoc/catch_generators/tests/utilities/semantics/catch_generator_handler.cpp @@ -0,0 +1,28 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include + +#include +#include + +using namespace QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE; + +TEST_CASE( + "Calling next 0 < n times and then calling get on a GeneratorHandler wrapping a generator behaves the same as only calling next (n-1) times and then get on the generator that is wrapped", + "[GeneratorHandler][Utilities][Semantics][Generators]" +) { + auto n = GENERATE(take(100, random(1, 100))); + auto generator_values = GENERATE_COPY(take(1, chunk(n, random(0, 100000)))); + + auto generator_handler = handler(Catch::Generators::from_range(generator_values.begin(), generator_values.end())); + auto generator{Catch::Generators::from_range(generator_values.begin(), generator_values.end())}; + + generator_handler.next(); + for (int times{1}; times < n; ++times) { + generator_handler.next(); + generator.next(); + } + + REQUIRE(generator_handler.get() == generator.get()); +} diff --git a/tests/auto/qdoc/CMakeLists.txt b/tests/auto/qdoc/CMakeLists.txt index a2c3ae274..08e79d8bf 100644 --- a/tests/auto/qdoc/CMakeLists.txt +++ b/tests/auto/qdoc/CMakeLists.txt @@ -6,10 +6,14 @@ if(CMAKE_VERSION VERSION_LESS "3.19" AND MSVC AND QT_FEATURE_debug_and_release) return() endif() +set(QDOC_PROJECT_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/../../../src/qdoc/) set(QDOC_SOURCE_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/../../../src/qdoc/qdoc/) set(QDOC_INCLUDE_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/../../../src/qdoc/qdoc/) -add_subdirectory(catch_generators) +if(QT_BUILD_STANDALONE_TESTS) + include(${QDOC_PROJECT_DIRECTORY}/catch_generators/tests/CMakeLists.txt) +endif() + add_subdirectory(qdoc) add_subdirectory(config) add_subdirectory(generatedoutput) diff --git a/tests/auto/qdoc/catch_generators/CMakeLists.txt b/tests/auto/qdoc/catch_generators/CMakeLists.txt deleted file mode 100644 index 88c051636..000000000 --- a/tests/auto/qdoc/catch_generators/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -add_subdirectory(tests) diff --git a/tests/auto/qdoc/catch_generators/qdoc_catch_generators.h b/tests/auto/qdoc/catch_generators/qdoc_catch_generators.h deleted file mode 100644 index e99a57ec5..000000000 --- a/tests/auto/qdoc/catch_generators/qdoc_catch_generators.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include "src/generators/qchar_generator.h" -#include "src/generators/qstring_generator.h" -#include "src/generators/path_generator.h" diff --git a/tests/auto/qdoc/catch_generators/src/generators/combinators/cycle_generator.h b/tests/auto/qdoc/catch_generators/src/generators/combinators/cycle_generator.h deleted file mode 100644 index b60600747..000000000 --- a/tests/auto/qdoc/catch_generators/src/generators/combinators/cycle_generator.h +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#pragma once - -#include "../../namespaces.h" -#include "../../utilities/semantics/generator_handler.h" - -#include - -#include - -namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE { - namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { - - template - class CycleGenerator : public Catch::Generators::IGenerator { - public: - CycleGenerator(Catch::Generators::GeneratorWrapper&& generator) - : generator{std::move(generator)}, - cache{}, - cache_index{0} - { - // REMARK: We generally handle extracting the first - // value by using an handler, to avoid code - // duplication and the possibility of an error. - // In this specific case, we turn to a more "manual" - // approach as it better models the cache-based - // implementation, removing the need to not increment - // cache_index the first time that next is called. - cache.emplace_back(this->generator.get()); - } - - T const& get() const override { return cache[cache_index]; } - - bool next() override { - if (generator.next()) { - cache.emplace_back(generator.get()); - ++cache_index; - } else { - cache_index = (cache_index + 1) % cache.size(); - } - - return true; - } - - private: - Catch::Generators::GeneratorWrapper generator; - - std::vector cache; - std::size_t cache_index; - }; - - } // end QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE - - /*! - * Returns a generator that behaves like \a generator until \a - * generator is exhausted, repeating the same generation that \a - * generator produced, infinitely, afterwards. - * - * This is generally intended to produce infinite generators from - * finite ones. - * - * For example, consider a generator that produces values based on - * another generator that it owns. - * If the owning generator needs to produce more values that the - * owned generator can support, it might fail at some point. - * By cycling over the owned generator, we can extend the sequence - * of produced values so that enough are generated, in a controlled - * way. - * - * The type T should generally be copyable for this generator to - * work. - */ - template - inline Catch::Generators::GeneratorWrapper cycle(Catch::Generators::GeneratorWrapper&& generator) { - return Catch::Generators::GeneratorWrapper(std::unique_ptr>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::CycleGenerator(std::move(generator)))); - } - -} // end QDOC_CATCH_GENERATORS_ROOT_NAMESPACE diff --git a/tests/auto/qdoc/catch_generators/src/generators/combinators/oneof_generator.h b/tests/auto/qdoc/catch_generators/src/generators/combinators/oneof_generator.h deleted file mode 100644 index 5de9dcb6c..000000000 --- a/tests/auto/qdoc/catch_generators/src/generators/combinators/oneof_generator.h +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#pragma once - -#include "../../namespaces.h" -#include "../../utilities/statistics/percentages.h" -#include "../../utilities/semantics/generator_handler.h" - -#include - -#include -#include -#include -#include - -namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE { - namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { - - template - class OneOfGenerator : public Catch::Generators::IGenerator { - public: - OneOfGenerator( - std::vector>&& generators, - const std::vector& weights - ) : generators{std::move(generators)}, - random_engine{std::random_device{}()}, - choice_distribution{weights.cbegin(), weights.cend()} - { - assert(weights.size() == this->generators.size()); - assert(std::reduce(weights.cbegin(), weights.cend()) == Approx(100.0)); - - std::transform( - this->generators.begin(), this->generators.end(), this->generators.begin(), - [](auto& generator){ return QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::handler(std::move(generator)); } - ); - - static_cast(next()); - } - - T const& get() const override { return current_value; } - - bool next() override { - std::size_t generator_index{choice_distribution(random_engine)}; - - if (!generators[generator_index].next()) return false; - current_value = generators[generator_index].get(); - - return true; - } - - private: - std::vector> generators; - - std::mt19937 random_engine; - std::discrete_distribution choice_distribution; - - T current_value; - }; - - } // end QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE - - /*! - * Returns a generator whose set of elements is the union of the - * set of elements of the generators in \a generators. - * - * Each time the generator produces a value, a generator from \a - * generators is randomly chosen to produce the value. - * - * The distribution for the choice is given by \a weights. - * The \e {ith} element in \a weights represent the percentage - * probability of the \e {ith} element of \a generators to be - * chosen. - * - * It follows that the size of \a weights must be the same as the - * size of \a generators. - * - * Furthermore, the sum of elements in \a weights should be a - * hundred. - * - * The generator produces values until a generator that is chosen - * to produce a value is unable to do so. - * The first such generator to do so will stop the generation - * independently of the availability of the other generators. - * - * Similarly, values will be produced as long as the chosen - * generator can produce a value, independently of the other - * generators being exhausted already. - */ - template - inline Catch::Generators::GeneratorWrapper oneof( - std::vector>&& generators, - const std::vector& weights - ) { - return Catch::Generators::GeneratorWrapper(std::unique_ptr>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::OneOfGenerator(std::move(generators), weights))); - } - - - /*! - * Returns a generator whose set of elements is the union of the - * set of elements of the generators in \a generators and in which - * the distribution of the generated elements is uniform over \a - * generators. - * - * Each time the generator produces a value, a generator from \a - * generators is randomly chosen to produce the value. - * - * Each generator from \a generators has the same chance of being - * chosen. - * - * Do note that the distribution over the set of values is not - * necessarily uniform. - * - * The generator produces values until a generator that is chosen - * to produce a value is unable to do so. - * The first such generator to do so will stop the generation - * independently of the availability of the other generators. - * - * Similarly, values will be produced as long as the chosen - * generator can produce a value, independently of the other - * generators being exhausted already. - */ - template - inline Catch::Generators::GeneratorWrapper uniform_oneof( - std::vector>&& generators - ) { - std::vector weights( - generators.size(), - QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::uniform_probability(generators.size()) - ); - return oneof(std::move(generators), std::move(weights)); - } - - /*! - * Returns a generator whose set of elements is the union of the - * set of elements of the generators in \a generators and in which - * the distribution of the generated elements is uniform over the - * elements of \a generators. - * - * The generators in \a generator should have a uniform - * distribution and be finite. - * If the set of elements that the generators in \a generator is - * not disjoint, the distribution will be skewed towards repeated - * elements. - * - * Each time the generator produces a value, a generator from \a - * generators is randomly chosen to produce the value. - * - * Each generator from \a generators has a probability of being - * chosen based on the proportion of the cardinality of the subset - * it produces. - * - * The \e {ith} element of \a amounts should contain the - * cardinality of the set produced by the \e {ith} generator in \a - * generators. - * - * The generator produces values until a generator that is chosen - * to produce a value is unable to do so. - * The first such generator to do so will stop the generation - * independently of the availability of the other generators. - * - * Similarly, values will be produced as long as the chosen - * generator can produce a value, independently of the other - * generators being exhausted already. - */ - template - inline Catch::Generators::GeneratorWrapper uniformly_valued_oneof( - std::vector>&& generators, - const std::vector& amounts - ) { - std::size_t total_amount{std::accumulate(amounts.cbegin(), amounts.cend(), std::size_t{0})}; - - std::vector weights; - weights.reserve(amounts.size()); - - std::transform( - amounts.cbegin(), amounts.cend(), - std::back_inserter(weights), - [total_amount](auto element){ return QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::percent_of(static_cast(element), static_cast(total_amount)); } - ); - - return oneof(std::move(generators), std::move(weights)); - } - -} // end QDOC_CATCH_GENERATORS_ROOT_NAMESPACE diff --git a/tests/auto/qdoc/catch_generators/src/generators/k_partition_of_r_generator.h b/tests/auto/qdoc/catch_generators/src/generators/k_partition_of_r_generator.h deleted file mode 100644 index 832ee2838..000000000 --- a/tests/auto/qdoc/catch_generators/src/generators/k_partition_of_r_generator.h +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#pragma once - -#include "../namespaces.h" - -#include - -#include -#include -#include - -namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE { - namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { - - class KPartitionOfRGenerator : public Catch::Generators::IGenerator> { - public: - KPartitionOfRGenerator(double r, std::size_t k) - : random_engine{std::random_device{}()}, - interval_distribution{0.0, r}, - k{k}, - r{r}, - current_partition(k) - { - assert(r >= 0.0); - assert(k >= 1); - - static_cast(next()); - } - - std::vector const& get() const override { return current_partition; } - - bool next() override { - if (k == 1) current_partition[0] = r; - else { - // REMARK: The following wasn't formally proved - // but is based on intuition. - // It is probably erroneous but is expected to be - // good enough for our case. - - // REMARK: We aim to provide a non skewed - // distribution for the elements of the partition. - // - // The reasoning for this is to ensure that our - // testing surface has a good chance of hitting - // many of the available elements between the many - // runs. - // - // To approximate this, a specific algorithm was chosen. - // The following code can be intuitively seen as doing the following: - // - // Consider an interval [0.0, r] on the real line, where r > 0.0. - // - // k - 1 > 0 elements of the interval are chosen, - // partitioning the interval into disjoint - // sub-intervals. - // - // --------------------------------------------------------------------------------------------------------------------- - // | | | | | - // 0 k_1 k_2 k_3 r - // | | | | | - // _______--------------------_______________________________________________________----------------------------------- - // k_1 - 0 k_2 - k_1 k_3 - k_2 r - k_3 - // p1 p2 p3 p4 - // - // The length of each sub interval is chosen as one of the elements of the partition. - // - // Trivially, the sum of the chosen elements is r. - // - // Furthermore, as long as the distribution used - // to choose the elements of the original interval - // is uniform, the probability of each partition - // being produced should tend to being uniform - // itself. - std::generate(current_partition.begin(), current_partition.end() - 1, [this](){ return interval_distribution(random_engine); }); - - current_partition.back() = r; - - std::sort(current_partition.begin(), current_partition.end()); - std::adjacent_difference(current_partition.begin(), current_partition.end(), current_partition.begin()); - } - - return true; - } - - private: - std::mt19937 random_engine; - std::uniform_real_distribution interval_distribution; - - std::size_t k; - double r; - - std::vector current_partition; - }; - - } // end QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE - - /*! - * Returns a generator that generates collections of \a k elements - * whose sum is \a r. - * - * \a r must be a real number greater or euqal to zero and \a k - * must be a natural number greater than zero. - * - * The generated partitions tends to be uniformely distributed - * over the set of partitions of r. - */ - inline Catch::Generators::GeneratorWrapper> k_partition_of_r(double r, std::size_t k) { - return Catch::Generators::GeneratorWrapper>(std::unique_ptr>>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::KPartitionOfRGenerator(r, k))); - } - -} // end QDOC_CATCH_GENERATORS_ROOT_NAMESPACE diff --git a/tests/auto/qdoc/catch_generators/src/generators/path_generator.h b/tests/auto/qdoc/catch_generators/src/generators/path_generator.h deleted file mode 100644 index 875502e49..000000000 --- a/tests/auto/qdoc/catch_generators/src/generators/path_generator.h +++ /dev/null @@ -1,853 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -// TODO: Change the include paths to implicitly consider -// `catch_generators` a root directory and change the CMakeLists.txt -// file to make this possible. - -#include "../namespaces.h" -#include "qchar_generator.h" -#include "qstring_generator.h" -#include "../utilities/semantics/move_into_vector.h" -#include "../utilities/semantics/generator_handler.h" - -#if defined(Q_OS_WINDOWS) - - #include "combinators/cycle_generator.h" - -#endif - -#include - -#include - -#include -#include -#include -#include - -#if defined(Q_OS_WINDOWS) - - #include - -#endif - -namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE { - - - struct PathGeneratorConfiguration { - double multi_device_path_probability{0.5}; - double absolute_path_probability{0.5}; - double directory_path_probability{0.5}; - double has_trailing_separator_probability{0.5}; - std::size_t minimum_components_amount{1}; - std::size_t maximum_components_amount{10}; - - PathGeneratorConfiguration& set_multi_device_path_probability(double amount) { - multi_device_path_probability = amount; - return *this; - } - - PathGeneratorConfiguration& set_absolute_path_probability(double amount) { - absolute_path_probability = amount; - return *this; - } - - PathGeneratorConfiguration& set_directory_path_probability(double amount) { - directory_path_probability = amount; - return *this; - } - - PathGeneratorConfiguration& set_has_trailing_separator_probability(double amount) { - has_trailing_separator_probability = amount; - return *this; - } - - PathGeneratorConfiguration& set_minimum_components_amount(std::size_t amount) { - minimum_components_amount = amount; - return *this; - } - - PathGeneratorConfiguration& set_maximum_components_amount(std::size_t amount) { - maximum_components_amount = amount; - return *this; - } - }; - - /*! - * \class PathGeneratorConfiguration - * \brief Defines some parameters to customize the generation of - * paths by a PathGenerator. - */ - - /*! - * \variable PathGeneratorConfiguration::multi_device_path_probability - * - * Every path produced by a PathGenerator configured with a - * mutli_device_path_probability of n has a probability of n to be - * \e {Multi-Device} and a probability of 1.0 - n to not be \a - * {Multi-Device}. - * - * multi_device_path_probability should be a value in the range [0.0, - * 1.0]. - */ - - /*! - * \variable PathGeneratorConfiguration::absolute_path_probability - * - * Every path produced by a PathGenerator configured with an - * absolute_path_probability of n has a probability of n to be \e - * {Absolute} and a probability of 1.0 - n to be \e {Relative}. - * - * absolute_path_probability should be a value in the range [0.0, - * 1.0]. - */ - - /*! - * \variable PathGeneratorConfiguration::directory_path_probability - * - * Every path produced by a PathGenerator configured with a - * directory_path_probability of n has a probability of n to be \e - * {To a Directory} and a probability of 1.0 - n to be \e {To a - * File}. - * - * directory_path_probability should be a value in the range [0.0, - * 1.0]. - */ - - /*! - * \variable PathGeneratorConfiguration::has_trailing_separator_probability - * - * Every path produced by a PathGenerator configured with an - * has_trailing_separator_probability of n has a probability of n - * to \e {Have a Trailing Separator} and a probability of 1.0 - n - * to not \e {Have a Trailing Separator}, when this is applicable. - * - * has_trailing_separator_probability should be a value in the - * range [0.0, 1.0]. - */ - - /*! - * \variable PathGeneratorConfiguration::minimum_components_amount - * - * Every path produced by a PathGenerator configured with a - * minimum_components_amount of n will be the concatenation of at - * least n non \e {device}, non \e {root}, non \e {separator} - * components. - * - * minimum_components_amount should be greater than zero and less - * than maximum_components_amount. - */ - - /*! - * \variable PathGeneratorConfiguration::maximum_components_amount - * - * Every path produced by a PathGenerator configured with a - * maximum_components_amount of n will be the concatenation of at - * most n non \e {device}, non \e {root}, non \e {separator} components. - * - * maximum_components_amount should be greater than or equal to - * minimum_components_amount. - */ - - - namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { - - class PathGenerator : public Catch::Generators::IGenerator { - public: - PathGenerator( - Catch::Generators::GeneratorWrapper&& device_component_generator, - Catch::Generators::GeneratorWrapper&& root_component_generator, - Catch::Generators::GeneratorWrapper&& directory_component_generator, - Catch::Generators::GeneratorWrapper&& filename_component_generator, - Catch::Generators::GeneratorWrapper&& separator_component_generator, - PathGeneratorConfiguration configuration = PathGeneratorConfiguration{} - ) : device_component_generator{QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::handler(std::move(device_component_generator))}, - root_component_generator{QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::handler(std::move(root_component_generator))}, - directory_component_generator{QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::handler(std::move(directory_component_generator))}, - filename_component_generator{QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::handler(std::move(filename_component_generator))}, - separator_component_generator{QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::handler(std::move(separator_component_generator))}, - random_engine{std::random_device{}()}, - components_amount_distribution{configuration.minimum_components_amount, configuration.maximum_components_amount}, - is_multi_device_distribution{configuration.multi_device_path_probability}, - is_absolute_path_distribution{configuration.absolute_path_probability}, - is_directory_path_distribution{configuration.directory_path_probability}, - has_trailing_separator{configuration.has_trailing_separator_probability}, - current_path{} - { - assert(configuration.minimum_components_amount > 0); - assert(configuration.minimum_components_amount <= configuration.maximum_components_amount); - - if (!next()) - Catch::throw_exception("Not enough values to initialize the first string"); - } - - QString const& get() const override { return current_path; } - - bool next() override { - std::size_t components_amount{components_amount_distribution(random_engine)}; - - current_path = ""; - - // REMARK: As per our specification of a path, we - // do not count device components, and separators, - // when considering the amount of components in a - // path. - // This is a tradeoff that is not necessarily - // precise. - // Counting those kinds of components, on one - // hand, would allow a device component to stands - // on its own as a path, for example "C:", which - // might actually be correct in some path format. - // On the other hand, counting those kinds of - // components makes the construction of paths for - // our model much more complex with regards, for - // example, to the amount of component. - // - // Counting device components, since they can - // appear both in relative and absolute paths, - // makes the minimum amount of components - // different for different kinds of paths. - // - // Since absolute paths always require a root - // component, the minimum amount of components for - // a multi-device absolute path is 2. - // - // But an absolute path that is not multi-device - // would only require one minimum component. - // - // Similarly, problems arise with the existence of - // Windows' relative multi-device path, which - // require a leading separator component after a - // device component. - // - // This problem mostly comes from our model - // simplifying the definition of paths quite a bit - // into binary-forms. - // This simplifies the code and its structure, - // sacrificing some precision. - // The lost precision is almost none for POSIX - // based paths, but is graver for DOS paths, since - // they have a more complex specification. - // - // Currently, we expect that the paths that QDoc - // will encounter will mostly be in POSIX-like - // forms, even on Windows, and aim to support - // that, such that the simplification of code is - // considered a better tradeoff compared to the - // loss of precision. - // - // If this changes, the model should be changed to - // pursue a Windows-first modeling, moving the - // categorization of paths from the current binary - // model to the absolute, drive-relative and - // relative triptych that Windows uses. - // This more complex model should be able to - // completely describe posix paths too, making it - // a superior choice as long as the complexity is - // warranted. - // - // Do note that the model similarly can become - // inconsistent when used to generate format of - // paths such as the one used in some resource - // systems. - // Those are considered out-of-scope for our needs - // and were not taken into account when developing - // this generator. - if (is_multi_device_distribution(random_engine)) { - if (!device_component_generator.next()) return false; - current_path += device_component_generator.get(); - } - - // REMARK: Similarly to not counting other form of - // components, we do not count root components - // towards the amounts of components that the path - // has to simplify the code. - // To support the "special" root path on, for - // example, posix systems, we require a more - // complex branching logic that changes based on - // the path being absolute or not. - // - // We don't expect root to be a particularly - // useful path for QDoc purposes and expect to not - // have to consider it for our tests. - // If consideration for it become required, it is - // possible to test it directly in the affected - // systemss as a special case. - // - // If most systems are affected by the handling of - // a root path, then the model should be slightly - // changed to accommodate its generation. - if (is_absolute_path_distribution(random_engine)) { - if (!root_component_generator.next()) return false; - - current_path += root_component_generator.get(); - } - - std::size_t prefix_components_amount{std::max(std::size_t{1}, components_amount) - 1}; - while (prefix_components_amount > 0) { - if (!directory_component_generator.next()) return false; - if (!separator_component_generator.next()) return false; - - current_path += directory_component_generator.get() + separator_component_generator.get(); - --prefix_components_amount; - } - - if (is_directory_path_distribution(random_engine)) { - if (!directory_component_generator.next()) return false; - current_path += directory_component_generator.get(); - - if (has_trailing_separator(random_engine)) { - if (!separator_component_generator.next()) return false; - current_path += separator_component_generator.get(); - } - } else { - if (!filename_component_generator.next()) return false; - current_path += filename_component_generator.get(); - } - - return true; - } - - private: - Catch::Generators::GeneratorWrapper device_component_generator; - Catch::Generators::GeneratorWrapper root_component_generator; - Catch::Generators::GeneratorWrapper directory_component_generator; - Catch::Generators::GeneratorWrapper filename_component_generator; - Catch::Generators::GeneratorWrapper separator_component_generator; - - std::mt19937 random_engine; - std::uniform_int_distribution components_amount_distribution; - std::bernoulli_distribution is_multi_device_distribution; - std::bernoulli_distribution is_absolute_path_distribution; - std::bernoulli_distribution is_directory_path_distribution; - std::bernoulli_distribution has_trailing_separator; - - QString current_path; - }; - - } // end QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE - -/*! - * Returns a generator that produces QStrings that represent a - * path in a filesystem. - * - * A path is formed by the following components, loosely based - * on the abstraction that is used by std::filesystem::path: - * - * \list - * \li \b {device}: - * Represents the device on the filesystem that - * the path should be considered in terms of. - * This is an optional components that is sometimes - * present on multi-device systems, such as Windows, to - * distinguish which device the path refers to. - * When present, it always appears before any other - * component. - * \li \b {root}: - * A special sequence that marks the path as absolute. - * This is an optional component that is present, always, - * in absolute paths. - * \li \b {directory}: - * A component that represents a directory on the - * filesystem that the path "passes-trough". - * Zero or more of this components can be present in the - * path. - * A path pointing to a directory on the filesystem that - * is not \e {root} always ends with a component of this - * type. - * \li \b {filename}: - * A component that represents a file on the - * filesystem. - * When this component is present, it is present only once - * and always as the last component of the path. - * A path that has such a component is a path that points - * to a file on the filesystem. - * For some path formats, there is no difference in the - * format of a \e {filename} and a \e {directory}. - * \li \b {separator}: - * A component that is interleaved between other types of - * components to separate them so that they are - * recognizable. - * A path that points to a directory on the filesystem may - * sometimes have a \e {separator} at the end, after the - * ending \e {directory} component. - * \endlist - * - * Each component is representable as a string and a path is a - * concatenation of the string representation of some - * components, with the following rules: - * - * \list - * \li There is at most one \e {device} component. - * \li If a \e {device} component is present it always - * precedes all other components. - * \li There is at most one \e {root} component. - * \li If a \e {root} component is present it: - * \list - * \li Succeeds the \e {device} component if it is present. - * \li Precedes every other components if the \e {device} - * component is not present. - * \endlist - * \li There are zero or more \e {directory} component. - * \li There is at most one \e {filename} component. - * \li If a \e {filename} component is present it always - * succeeds all other components. - * \li Between any two successive \e {directory} components - * there is a \e {separator} component. - * \li Between each successive \e {directory} and \e - * {filename} component there is a \e {separator} component. - * \li If the last component is a \e {directory} component it - * can be optionally followed by a \e {separator} component. - * \li At least one component that is not a \e {device}, a \e - * {root} or \e {separator} component is present. - * \endlist - * - * For example, if "C:" is a \e {device} component, "\\" is a - * \e {root} component, "\\" is a \e {separator} component, - * "directory" is a \e {directory} component and "filename" is - * a \e {filename} component, the following are all paths: - * - * "C:\\directory", "C:\\directory\\directory", "C:filename", - * "directory\\directory\\", "\\directory\\filename", "filename". - * - * While the following aren't: - * - * "C:", "C:\\", "directory\\C:", "foo", "C:filename\\", - * "filename\\directory\\filename", "filename\\filename", - * "directorydirectory"." - * - * The format of different components type can be the same. - * For example, the \e {root} and \e {separator} component in - * the above example. - * For the purpose of generation, we do not care about the - * format itself and consider a component of a certain type - * depending only on how it is generated/where it is generated - * from. - * - * For example, if every component is formatted as the string - * "a", the string "aaa" could be a generated path. - * By the string alone, it is not possible to simply discern - * which components form it, but it would be possible to - * generate it if the first "a" is a \a {device} component, - * the second "a" is a \e {root} component and the third "a" - * is a \e {directory} or \e {filename} component. - * - * A path, is further said to have some properties, pairs of - * which are exclusive to each other. - * - * A path is said to be: - * - * \list - * \li \b {Multi-Device}: - * When it contains a \e {device} component. - * \li \b {Absolute}: - * When it contains a \e {root} component. - * If the path is \e {Absolute} it is not \e {Relative}. - * \li \b {Relative}: - * When it does not contain a \e {root} component. - * If the path is \e {Relative} it is not \e {Absolute}. - * \li \b {To a Directory}: - * When its last component is a \e {directory} component - * or a \e {directory} component followed by a \e - * {separator} component. - * If the path is \e {To a Directory} it is not \e {To a - * File}. - * \li \b {To a File}: - * When its last component is a \e {filename}. - * If the path is \e {To a File} it is not \e {To a - * Directory}. - * \endlist - * - * All path are \e {Relative/Absolute}, \e {To a - * Directory/To a File} and \e {Multi-Device} or not. - * - * Furthermore, a path that is \e {To a Directory} and whose - * last component is a \e {separator} component is said to \e - * {Have a Trailing Separator}. - */ - inline Catch::Generators::GeneratorWrapper path( - Catch::Generators::GeneratorWrapper&& device_generator, - Catch::Generators::GeneratorWrapper&& root_component_generator, - Catch::Generators::GeneratorWrapper&& directory_generator, - Catch::Generators::GeneratorWrapper&& filename_generator, - Catch::Generators::GeneratorWrapper&& separator_generator, - PathGeneratorConfiguration configuration = PathGeneratorConfiguration{} - ) { - return Catch::Generators::GeneratorWrapper( - std::unique_ptr>( - new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::PathGenerator(std::move(device_generator), std::move(root_component_generator), std::move(directory_generator), std::move(filename_generator), std::move(separator_generator), configuration) - ) - ); - } - - namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { - - // REMARK: We need a bounded length for the generation of path - // components as strings. - // We trivially do not want components to be the empty string, - // such that we have a minimum length of 1, but the maximum - // length is more malleable. - // We don't want components that are too long to avoid - // incurring in a big performance overhead, as we may generate - // many of them. - // At the same time, we want some freedom in having diffent - // length components. - // The value that was chosen is based on the general value for - // POSIX's NAME_MAX, which seems to tend to be 14 on many systems. - // We see this value as a small enough but not too much value - // that further brings with itself a relation to paths, - // increasing our portability even if it is out of scope, as - // almost no modern respects NAME_MAX. - // We don't use POSIX's NAME_MAX directly as it may not be available - // on all systems. - inline static constexpr std::size_t minimum_component_length{1}; - inline static constexpr std::size_t maximum_component_length{14}; - - /*! - * Returns a generator that generates strings that are - * suitable to be used as a root component in POSIX paths. - * - * As per - * \l {https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_02}, - * this is any sequence of slash characters that is not of - * length 2. - */ - inline Catch::Generators::GeneratorWrapper posix_root() { - return uniformly_valued_oneof( - QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::move_into_vector( - string(character('/', '/'), 1, 1), - string(character('/', '/'), 3, maximum_component_length) - ), - std::vector{1, maximum_component_length - 3} - ); - } - - /*! - * Returns a generator that generates strings that are - * suitable to be used as directory components in POSIX paths - * and that use an alphabet that should generally be supported - * by other systems. - * - * Components of this kind use the \l - * {https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282}{Portable Filename Character Set}. - */ - inline Catch::Generators::GeneratorWrapper portable_posix_directory_name() { - return string( - QDOC_CATCH_GENERATORS_QCHAR_ALPHABETS_NAMESPACE::portable_posix_filename(), - minimum_component_length, maximum_component_length - ); - } - - /*! - * Returns a generator that generates strings that are - * suitable to be used as filenames in POSIX paths and that - * use an alphabet that should generally be supported by - * other systems. - * - * Filenames of this kind use the \l - * {https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282}{Portable Filename Character Set}. - */ - inline Catch::Generators::GeneratorWrapper portable_posix_filename() { - // REMARK: "." and ".." always represent directories so we - // avoid generating them. Other than this, there is no - // difference between a file name and a directory name. - return filter([](auto& filename) { return filename != "." && filename != ".."; }, portable_posix_directory_name()); - } - - /*! - * Returns a generator that generates strings that can be used - * as POSIX compliant separators. - * - * As per \l - * {https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_271}, - * a separator is a sequence of one or more slashes. - */ - inline Catch::Generators::GeneratorWrapper posix_separator() { - return string(character('/', '/'), minimum_component_length, maximum_component_length); - } - - /*! - * Returns a generator that generates strings that can be - * suitably used as logical drive names in Windows' paths. - * - * As per \l - * {https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats#traditional-dos-paths} - * and \l - * {https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getlogicaldrives}, - * they are composed of a single letter. - * Each generated string always follows the lettet with a - * colon, as it is specifically intended for path usages, - * where this is required. - * - * We use only uppercase letters for the drives names albeit, - * depending on case sensitivity, lowercase letter could be - * used. - */ - inline Catch::Generators::GeneratorWrapper windows_logical_drives() { - // REMARK: If a Windows path is generated on Windows - // itself, we expect that it may be used to interact with - // the filesystem, similar to how we expect a POSIX path - // to be used on Linux. - // For this reason, we only generate a specific drive, the one - // that contains the current working directory, so that we - // know it is an actually available drive and to contain the - // possible modifications to the filesystem to an easily - // foundable place. - -#if defined(Q_OS_WINDOWS) - - auto root_device{QStorageInfo{QDir()}.rootPath().first(1) + ":"}; - - return cycle(Catch::Generators::value(std::move(root_device))); - -#else - - return Catch::Generators::map( - [](QString letter){ return letter + ':';}, - string(QDOC_CATCH_GENERATORS_QCHAR_ALPHABETS_NAMESPACE::ascii_uppercase(), 1, 1) - ); - -#endif - } - - /*! - * Returns a generator that generate strings that can be used - * as separators in Windows based paths. - * - * As per \l - * {https://docs.microsoft.com/en-us/dotnet/api/system.io.path.directoryseparatorchar?view=net-6.0} - * and \l - * {https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats#canonicalize-separators}, - * this is a sequence of one or more backward or forward slashes. - */ - inline Catch::Generators::GeneratorWrapper windows_separator() { - return uniform_oneof( - QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::move_into_vector( - string(character('\\', '\\'), minimum_component_length, maximum_component_length), - string(character('/', '/'), minimum_component_length, maximum_component_length) - ) - ); - } - - } // end QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE - - /*! - * Returns a generator that generates strings representing - * POSIX compatible paths. - * - * The generated paths follows the format specified in \l - * {https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_271}. - * - * The optional length-requirements, such as PATH_MAX and - * NAME_MAX, are relaxed away as they are generally not - * respected by modern systems. - * - * It is possible to set the probability of obtaining a - * relative or absolute path through \a - * absolute_path_probability and the one of obtaining a path - * potentially pointing ot a directory or on a file through \a - * directory_path_probability. - */ - inline Catch::Generators::GeneratorWrapper relaxed_portable_posix_path(double absolute_path_probability = 0.5, double directory_path_probability = 0.5) { - return path( - // POSIX path are never multi-device, so that we have - // provide an empty device component generator and set - // the probability for Multi-Device paths to zero. - string(character(), 0, 0), - QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::posix_root(), - QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::portable_posix_directory_name(), - QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::portable_posix_filename(), - QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::posix_separator(), - PathGeneratorConfiguration{} - .set_multi_device_path_probability(0.0) - .set_absolute_path_probability(absolute_path_probability) - .set_directory_path_probability(directory_path_probability) - ); - } - - /*! - * Returns a generator that produces strings that represents - * traditional DOS paths as defined in \l - * {https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats#traditional-dos-paths}. - * - * The directory and filename components of a path generated - * in this way are, currently, restricted to use a portable - * character set as defined by POSIX. - * - * Do note that most paths themselves, will not be portable, on - * the whole, albeit they may be valid paths on other systems, as - * Windows uses a path system that is generally incompatible with - * other systems. - * - * Some possibly valid special path, such as a "C:" or "\" - * will never be generated. - */ - inline Catch::Generators::GeneratorWrapper traditional_dos_path( - double absolute_path_probability = 0.5, - double directory_path_probability = 0.5, - double multi_device_path_probability = 0.5 - ) { - return path( - QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::windows_logical_drives(), - QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::windows_separator(), - // REMAKR: Windows treats trailing dots as if they were a - // component of their own, that is, as the special - // relative paths. - // This seems to not be correctly handled by Qt's - // filesystem methods, resulting in inconsistencies when - // one such path is encountered. - // To avoid the issue, considering that an equivalent path - // can be formed by actually having the dots on their own - // as a component, we filter out all those paths that have - // trailing dots but are not only composed of dots. - Catch::Generators::filter( - [](auto& path){ return !(path.endsWith(".") && path.contains(QRegularExpression("[^.]"))) ; }, - QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::portable_posix_directory_name() - ), - QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::portable_posix_filename(), - QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::windows_separator(), - PathGeneratorConfiguration{} - .set_multi_device_path_probability(multi_device_path_probability) - .set_absolute_path_probability(absolute_path_probability) - .set_directory_path_probability(directory_path_probability) - ); - } - - // TODO: Find a good way to test the following functions. - // native_path can probably be tied to the tests for the - // OS-specific functions, with TEMPLATE_TEST_CASE. - // The other ones may follow a similar pattern but require a bit - // more work so that they tie to a specific case instead of the - // general one. - // Nonetheless, this approach is both error prone and difficult to - // parse, because of the required if preprocessor directives, - // and should be avoided if possible. - - /*! - * Returns a generator that generates QStrings that represents - * paths native to the underlying OS. - * - * On Windows, paths that refer to a drive always refer to the - * root drive. - * - * native* functions should always be chosen when using paths for - * testing interfacing with the filesystem itself. - * - * System outside Linux, macOS or Windows are not supported. - */ - inline Catch::Generators::GeneratorWrapper native_path(double absolute_path_probability = 0.5, double directory_path_probability = 0.5) { -#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) - - return relaxed_portable_posix_path(absolute_path_probability, directory_path_probability); - -#elif defined(Q_OS_WINDOWS) - - // REMARK: When generating native paths for testing we - // generally want to avoid relative paths that are - // drive-specific, as we want them to be tied to a specific - // working directory that may not be the current directory on - // the drive. - // Hence, we avoid generating paths that may have a drive component. - // For tests where those kind of paths are interesting, a - // specific Windows-only test should be made, using - // traditional_dos_path to generate drive-relative paths only. - return traditional_dos_path(absolute_path_probability, directory_path_probability, 0.0); - -#endif - } - - /*! - * Returns a generator that generates QStrings that represents - * paths native to the underlying OS and that are always \e - * {Relative}. - * - * Avoids generating paths that refer to a directory that is not - * included in the path itself. - * - * System outside Linux, macOS or Windows are not supported. - */ - inline Catch::Generators::GeneratorWrapper native_relative_path(double directory_path_probability = 0.5) { - // REMARK: When testing, we generally use some specific - // directory as a root for relative paths. - // We want the generated path to be relative to that - // directory because we need a clean state for the test to - // be reliable. - // When generating paths, it is possible, correctly, to - // have a path that refers to that directory or some - // parent of it, removing us from the clean state that we - // need. - // To avoid that, we filter out paths that end up referring to a directory that is not under our "root" directory. - // - // We can think of each generated component moving us - // further down or up, in case of "..", a directory - // hierarchy, or keeping us at the same place in case of - // ".". - // Any path that ends up under our original "root" - // directory will safely keep our clean state for testing. - // - // Each "." keeps us at the same level in the hierarchy. - // Each ".." moves us up one level in the hierarchy. - // Each component that is not "." or ".." moves us down - // one level into the hierarchy. - // - // Then, to avoid referring to the "root" directory or one - // of its parents, we need to balance out each "." and - // ".." with the components that precedes or follow their - // appearance. - // - // Since "." keeps us at the same level, it can appear how - // many times it wants as long as the path referes to the - // "root" directory or a directory or file under it and at - // least one other component referes to a directory or - // file that is under the "root" directory. - // - // Since ".." moves us one level up in the hierarchy, a - // sequence of n ".." components is safe when at least n + - // 1 non "." or ".." components appear before it. - // - // To avoid the above problem, we filter away paths that - // do not respect those rules. - return Catch::Generators::filter( - [](auto& path){ - QStringList components{path.split(QRegularExpression{R"((\\|\/)+)"}, Qt::SkipEmptyParts)}; - int depth{0}; - - for (auto& component : components) { - if (component == "..") - --depth; - else if (component != ".") - ++depth; - - if (depth < 0) return false; - } - - return (depth > 0); - }, - native_path(0.0, directory_path_probability) - ); - } - - /*! - * Returns a generator that generates QStrings that represents - * paths native to the underlying OS and that are always \e - * {Relative} and \e {To a File}. - * - * System outside Linux, macOS or Windows are not supported. - */ - inline Catch::Generators::GeneratorWrapper native_relative_file_path() { - return native_relative_path(0.0); - } - - /*! - * Returns a generator that generates QStrings that represents - * paths native to the underlying OS and that are always \e - * {Relative} and \e {To a Directory}. - * - * System outside Linux, macOS or Windows are not supported. - */ - inline Catch::Generators::GeneratorWrapper native_relative_directory_path() { - return native_relative_path(1.0); - } - -} // end QDOC_CATCH_GENERATORS_ROOT_NAMESPACE diff --git a/tests/auto/qdoc/catch_generators/src/generators/qchar_generator.h b/tests/auto/qdoc/catch_generators/src/generators/qchar_generator.h deleted file mode 100644 index 33efc5ea4..000000000 --- a/tests/auto/qdoc/catch_generators/src/generators/qchar_generator.h +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#pragma once - -#include "../namespaces.h" -#include "../utilities/semantics/move_into_vector.h" -#include "combinators/oneof_generator.h" - -#include - -#include - -#include - -namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE { - namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { - - class QCharGenerator : public Catch::Generators::IGenerator { - public: - QCharGenerator( - char16_t lower_bound = std::numeric_limits::min(), - char16_t upper_bound = std::numeric_limits::max() - ) : random_engine{std::random_device{}()}, - distribution{static_cast(lower_bound), static_cast(upper_bound)} - { - assert(lower_bound <= upper_bound); - static_cast(next()); - } - - QChar const& get() const override { return current_character; } - - bool next() override { - current_character = QChar(static_cast(distribution(random_engine))); - - return true; - } - - private: - QChar current_character; - - std::mt19937 random_engine; - std::uniform_int_distribution distribution; - }; - - } // end QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE - - - /*! - * Returns a generator of that generates elements of QChar whose - * ucs value is in the range [\a lower_bound, \a upper_bound]. - * - * When \a lower_bound = \a upper_bound, the generator infinitely - * generates the same character. - */ - inline Catch::Generators::GeneratorWrapper character(char16_t lower_bound = std::numeric_limits::min(), char16_t upper_bound = std::numeric_limits::max()) { - return Catch::Generators::GeneratorWrapper(std::unique_ptr>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::QCharGenerator(lower_bound, upper_bound))); - } - - - namespace QDOC_CATCH_GENERATORS_QCHAR_ALPHABETS_NAMESPACE { - - namespace QDOC_CATCH_GENERATORS_TRAITS_NAMESPACE { - - enum class Alphabets : std::size_t {digit, ascii_lowercase, ascii_uppercase, ascii_alpha, ascii_alphanumeric, portable_posix_filename}; - - template - struct sizeof_alphabet; - - template - inline constexpr std::size_t sizeof_alphabet_v = sizeof_alphabet::value; - - template <> struct sizeof_alphabet { static constexpr std::size_t value{'9' - '0'}; }; - template <> struct sizeof_alphabet { static constexpr std::size_t value{'z' - 'a'}; }; - template<> struct sizeof_alphabet { static constexpr std::size_t value{'Z' - 'A'}; }; - template<> struct sizeof_alphabet { static constexpr std::size_t value{sizeof_alphabet_v + sizeof_alphabet_v}; }; - template<> struct sizeof_alphabet{ static constexpr std::size_t value{sizeof_alphabet_v + sizeof_alphabet_v}; }; - - } // end QDOC_CATCH_GENERATORS_TRAITS_NAMESPACE - - - inline Catch::Generators::GeneratorWrapper digit() { - return Catch::Generators::GeneratorWrapper(std::unique_ptr>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::QCharGenerator('0', '9'))); - } - - inline Catch::Generators::GeneratorWrapper ascii_lowercase() { - return Catch::Generators::GeneratorWrapper(std::unique_ptr>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::QCharGenerator('a', 'z'))); - } - - inline Catch::Generators::GeneratorWrapper ascii_uppercase() { - return Catch::Generators::GeneratorWrapper(std::unique_ptr>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::QCharGenerator('A', 'Z'))); - } - - inline Catch::Generators::GeneratorWrapper ascii_alpha() { - return uniform_oneof(QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::move_into_vector(ascii_lowercase(), ascii_uppercase())); - } - - inline Catch::Generators::GeneratorWrapper ascii_alphanumeric() { - return uniformly_valued_oneof(QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::move_into_vector(ascii_alpha(), digit()), std::vector{traits::sizeof_alphabet_v , traits::sizeof_alphabet_v}); - } - - inline Catch::Generators::GeneratorWrapper portable_posix_filename() { - return uniformly_valued_oneof(QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::move_into_vector(ascii_alphanumeric(), character('.', '.'), character('-', '-'), character('_', '_')), - std::vector{traits::sizeof_alphabet_v, std::size_t{1}, std::size_t{1}, std::size_t{1}}); - } - - } // end QDOC_CATCH_GENERATORS_QCHAR_ALPHABETS_NAMESPACE - - -} // end QDOC_CATCH_GENERATORS_ROOT_NAMESPACE diff --git a/tests/auto/qdoc/catch_generators/src/generators/qstring_generator.h b/tests/auto/qdoc/catch_generators/src/generators/qstring_generator.h deleted file mode 100644 index fe854d22f..000000000 --- a/tests/auto/qdoc/catch_generators/src/generators/qstring_generator.h +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#pragma once - -#include "../namespaces.h" -#include "qchar_generator.h" -#include "../utilities/semantics/generator_handler.h" - -#include - -#include - -#include - -namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE { - namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { - - class QStringGenerator : public Catch::Generators::IGenerator { - public: - QStringGenerator(Catch::Generators::GeneratorWrapper&& character_generator, qsizetype minimum_length, qsizetype maximum_length) - : character_generator{QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::handler(std::move(character_generator))}, - random_engine{std::random_device{}()}, - length_distribution{minimum_length, maximum_length}, - current_string{} - { - assert(minimum_length >= 0); - assert(maximum_length >= 0); - assert(minimum_length <= maximum_length); - - if (!next()) - Catch::throw_exception("Not enough values to initialize the first string"); - } - - QString const& get() const override { return current_string; } - - bool next() override { - qsizetype length{length_distribution(random_engine)}; - - current_string = QString(); - for (qsizetype length_index{0}; length_index < length; ++length_index) { - if (!character_generator.next()) return false; - - current_string += character_generator.get(); - } - - return true; - } - - private: - Catch::Generators::GeneratorWrapper character_generator; - - std::mt19937 random_engine; - std::uniform_int_distribution length_distribution; - - QString current_string; - }; - - } // end QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE - - /*! - * Returns a generator that generates elements of QString from - * some amount of elements taken from \a character_generator. - * - * The generated strings will have a length in the range - * [\a minimum_length, \a maximum_length]. - * - * For compatibility with the Qt API, it is possible to provide - * negative bounds for the length. This is, nonetheless, - * considered an error such that the bounds should always be - * greater or equal to zero. - * - * It is similarly considered an error to have minimum_length <= - * maximum_length. - * - * The provided generator will generate elements until \a - * character_generator is exhausted. - */ - inline Catch::Generators::GeneratorWrapper string(Catch::Generators::GeneratorWrapper&& character_generator, qsizetype minimum_length, qsizetype maximum_length) { - return Catch::Generators::GeneratorWrapper(std::unique_ptr>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::QStringGenerator(std::move(character_generator), minimum_length, maximum_length))); - } - - /*! - * Returns an infinite generator whose elements are the empty - * QString. - */ - inline Catch::Generators::GeneratorWrapper empty_string() { - return Catch::Generators::GeneratorWrapper(std::unique_ptr>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::QStringGenerator(character(), 0, 0))); - } - - -} // end QDOC_CATCH_GENERATORS_ROOT_NAMESPACE diff --git a/tests/auto/qdoc/catch_generators/src/namespaces.h b/tests/auto/qdoc/catch_generators/src/namespaces.h deleted file mode 100644 index 7b954feb0..000000000 --- a/tests/auto/qdoc/catch_generators/src/namespaces.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -#define QDOC_CATCH_GENERATORS_ROOT_NAMESPACE qdoc::catch_generators - -#define QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE details - -#define QDOC_CATCH_GENERATORS_TRAITS_NAMESPACE traits - -#define QDOC_CATCH_GENERATORS_QCHAR_ALPHABETS_NAMESPACE alphabets - -#define QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE QDOC_CATCH_GENERATORS_ROOT_NAMESPACE::QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::utils diff --git a/tests/auto/qdoc/catch_generators/src/utilities/semantics/copy_value.h b/tests/auto/qdoc/catch_generators/src/utilities/semantics/copy_value.h deleted file mode 100644 index 57798be1a..000000000 --- a/tests/auto/qdoc/catch_generators/src/utilities/semantics/copy_value.h +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#pragma once - -#include "../../namespaces.h" - -#include - -namespace QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE { - - /*! - * Forces \value to be copied in an expression context. - * - * This is used in contexts where inferences of a type that - * requires generality might identify a reference when ownership - * is required. - * - * Note that the compiler might optmize the copy away. This is a - * non-issue as we are only interested in breaking lifetime - * dependencies. - */ - template - std::remove_reference_t copy_value(T value) { return value; } - -} // end QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE diff --git a/tests/auto/qdoc/catch_generators/src/utilities/semantics/generator_handler.h b/tests/auto/qdoc/catch_generators/src/utilities/semantics/generator_handler.h deleted file mode 100644 index 328627512..000000000 --- a/tests/auto/qdoc/catch_generators/src/utilities/semantics/generator_handler.h +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#pragma once - -#include "../../namespaces.h" - -#include - -#include -#include - -namespace QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE { - - template - class GeneratorHandler : public Catch::Generators::IGenerator { - public: - - GeneratorHandler(Catch::Generators::GeneratorWrapper&& generator) - : generator{std::move(generator)}, - first_call{true} - {} - - T const& get() const override { - assert(!first_call); - return generator.get(); - } - - bool next() override { - if (first_call) { - first_call = false; - return true; - } - - return generator.next(); - } - - private: - Catch::Generators::GeneratorWrapper generator; - bool first_call; - }; - - - /*! - * Returns a generator wrapping \a generator that ensures that - * changes its semantics so that the first call to get should be - * preceded by a call to next. - * - * Catch generators require that is valid to call get and obtain a - * valid value on a generator that was just created. - * That is, generators should be non-empty and their first value - * should be initialized on construction. - * - * Normally, this is not a problem, and the next implementation of - * the generator can be simply called in the constructor. - * But when a generator depends on other generators, doing so will - * generally skip the first value that the generator - * produces, as the wrapping generator will need to advance the - * underlying generator, losing the value in the process. - * This is in particular, a problem, on generators that are finite - * or infinite and ordered. - * - * To solve the issue, the original value can be saved before - * advancing the generator or some code can be duplicated or - * abstracted so that what a new element can be generated without - * advancing the underlying generator. - * - * While this is acceptable, it can be error prone on more complex - * generators, generators that randomly access a collection of - * generators and so on. - * - * To simplify this process, this generator changes the semantics - * of the wrapped generator such that the first value of the - * generator is produced after the first call to next and the - * generator is considered in an invalid state before the first - * advancement. - * - * In this way, by wrapping all generators that a generator - * depends on, the implementation required for the first value is - * the same as the one required for all following values, with - * regards to the sequencing of next and get operations, - * simplifying the implementation of dependent generators. - * - * Do note that, while the generator returned by this function - * implments the generator interface that Catch2 requires, it - * cannot be normally used as a generator as it fails to comply - * with the first value semantics that a generator requires. - * Indeed, it should only be used as an intermediate wrapper for - * the implementation of generators that depends on other - * generators. - */ - template - inline Catch::Generators::GeneratorWrapper handler(Catch::Generators::GeneratorWrapper&& generator) { - return Catch::Generators::GeneratorWrapper(std::unique_ptr>(new GeneratorHandler(std::move(generator)))); - } - -} // end QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE diff --git a/tests/auto/qdoc/catch_generators/src/utilities/semantics/move_into_vector.h b/tests/auto/qdoc/catch_generators/src/utilities/semantics/move_into_vector.h deleted file mode 100644 index 5e780085b..000000000 --- a/tests/auto/qdoc/catch_generators/src/utilities/semantics/move_into_vector.h +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (C) 2021 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#pragma once - -#include "../../namespaces.h" - -#include -#include - -namespace QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE { - - namespace QDOC_CATCH_GENERATORS_TRAITS_NAMESPACE { - - /*! - * Returns the type of the first element of Args. - * - * Args is expected to have at least one - */ - template - using first_from_pack_t = std::tuple_element_t<0, std::tuple>; - - } // end QDOC_CATCH_GENERATORS_TRAITS_NAMESPACE - - - /*! - * Builds an std::vector by moving \a movables into it. - * - * \a movables must be made of homogenous types. - * - * This function is intended to allow the construction of an - * std::vector, where T is a move only type, as an expression, - * to lighten the idiom. - * - * For example, Catch's GeneratorWrapper adapts a - * std::unique_ptr, which is move only, making it impossible to - * build a std::vector from them in place. - * - * Then, everywhere this is needed, a more complex approach of - * generating the collection of objects, generating a vector of a - * suitable size and iterating the objects to move-emplace them in - * the vector is required. - * - * This not only complicates the code but is incompatible with a - * GENERATE expression, making it extremely hard, noisy and error - * prone to use them together. - * - * In those cases, then, a call to move_into_vector can be used as - * an expression to circumvent the problem. - */ - template - inline auto move_into_vector(MoveOnlyTypes... movables) { - std::vector> - moved_into_vector; - moved_into_vector.reserve(sizeof...(movables)); - - (moved_into_vector.emplace_back(std::move(movables)), ...); - - return moved_into_vector; - } - -} // end QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE diff --git a/tests/auto/qdoc/catch_generators/src/utilities/statistics/distribution.h b/tests/auto/qdoc/catch_generators/src/utilities/statistics/distribution.h deleted file mode 100644 index 4374993bf..000000000 --- a/tests/auto/qdoc/catch_generators/src/utilities/statistics/distribution.h +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#pragma once - -#include "../../namespaces.h" - -#include -#include -#include -#include - -namespace QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE { - - template - using Histogram = std::unordered_map; - - template - auto make_histogram(InputIt begin, InputIt end, GroupBy&& group_by) { - Histogram> histogram{}; - - while (begin != end) { - auto key{std::invoke(std::forward(group_by), *begin)}; - - histogram.try_emplace(key, 0); - histogram[key] += 1; - ++begin; - } - - return histogram; - } - - template - struct DistributionError { - T value; - double probability; - double expected_probability; - }; - - template - inline std::ostream& operator<<(std::ostream& os, const DistributionError& error) { - return os << "DistributionError{" << - "The value { " << error.value << - " } appear with a probability of { " << error.probability << - " } while a probability of { " << error.expected_probability << " } was expected." << - "}"; - } - - // REMARK: The following should really return an Either of unit/error - // but std::variant in C++ is both extremely unusable and comes with a - // strong overhead unless certain conditions are met. - // For this reason, we keep to the less intutitive optional error. - - /*! - * Returns true when the given \a sequence approximately respects a - * given distribution. - * - * The \a sequence respects a given distribution when the count of - * each collection of values is a percentage of the total values that - * is near the percentage probability described by distribution. - * - * The values in \a sequence are collected according to \a group_by. - * \a group_by, given an element of \a sequence, should return a value - * of some type that represent the category of the inspected value. - * Values that have the same category share their count. - * - * The distribution that should be respected is given by \a - * probability_of. \a probability_of is a function that takes a - * category that was produced from a call to \a group_by and returns - * the expect probability, in percentage, of apperance for that - * category. - * - * The given probability is then compared to the one found by counting - * the element of \a sequence under \a group_by, to ensure that it - * matches. - * - * The margin of error for the comparison is given, in percentage - * points, by \a margin. - * The approximation uses an absolute comparison and scales the - * margin inversely based on the size of \a sequence, to account for the - * precision of the data set itself. - * - * When the distribution is not respected, a DistributionError is - * returned enclosed in an optional value. - * The error allows reports which the first category for which the - * comparison failed, along with its expected probability and the one - * that was actually inferred from \a sequence. - */ - template - std::optional> respects_distribution(std::vector&& sequence, GroupBy&& group_by, ProbabilityOf&& probability_of, double margin = 33) { - std::size_t data_point_amount{sequence.size()}; - - // REMARK: We scale the margin based on the data set to allow for - // an easier change in downstream tests. - // The precision required for the approximation will vary - // depending on how many values we generate. - // The amount of values we generate depends on how much time we - // want the tests to take. - // This amount may change in the future. For example, as code is - // added and tests are added, we might need some expensive - // computations here and there. - // Sometimes, this will increase the test suite runtime without an - // obvious way of improving the performance of the underlying code - // to reduce it. - // In those cases, the total run time can be decreased by running - // less generations for battle-tested tests. - // If some code has not been changed for a long time, it will have - // had thousands of generations by that point, giving us a good - // degree of certainty of it not being bugged (for whatever bugs - // the tests account for). - // Then, running a certain amount of generation is not required - // anymore such that some of them can be optimized out. - // For tests like the one using this function, where our ability - // to test is always dependent on the amount of generations, - // changing the generated amount will mean that we will need to - // change our conditions too, potentially changing the meaning of - // the test. - // To take this into account, we perform a scaling on the - // condition itself, so that if the amount of data points that are - // generated changes, we do not generally have to change anything - // in the condition. - // - // For this case, we scale logarithmically_10 for the simple - // reason that we tend to generate values in power of tens, - // starting with the 100 values default that Quickcheck used. - // - // The default value for the margin on which the scaling is based, - // was chosen heuristically. - // As we expect generation under 10^3 to be generally meaningless - // for this kind of testing, the value was chosen so that it would - // start to normalize around that amount. - // Deviation of about 5-10% were identified trough various - // generations for an amount of data points near 1000, while a - // deviation of about 1-3% was identified with about 10000 values. - // With the chosen default value, the scaling approaches those - // percentage points with some margin of error. - // - // We expect up to a 10%, or a bit more, deviation to be suitable - // for our purposes, as it would still allow for a varied - // distribution in downstream consumers. - double scaled_margin{margin * (1.0/std::log10(data_point_amount))}; - - auto histogram{make_histogram(sequence.begin(), sequence.end(), std::forward(group_by))}; - - for (auto& bin : histogram) { - auto [key, count] = bin; - - double actual_percentage{percent_of(static_cast(count), static_cast(data_point_amount))}; - double expected_percentage{std::invoke(std::forward(probability_of), key)}; - - if (!(actual_percentage == Approx(expected_percentage).margin(scaled_margin))) - return std::make_optional(DistributionError{key, actual_percentage, expected_percentage}); - } - - return std::nullopt; - } - -} // end QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE diff --git a/tests/auto/qdoc/catch_generators/src/utilities/statistics/percentages.h b/tests/auto/qdoc/catch_generators/src/utilities/statistics/percentages.h deleted file mode 100644 index 2d80a459f..000000000 --- a/tests/auto/qdoc/catch_generators/src/utilities/statistics/percentages.h +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#pragma once - -#include "../../namespaces.h" - -#include - -namespace QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE { - - /*! - * Returns the percentage of \amount over \a total. - * - * \a amount needs to be greater or equal to zero and \a total - * needs to be greater than zero. - */ - inline double percent_of(double amount, double total) { - assert(amount >= 0.0); - assert(total > 0.0); - - return (amount / total) * 100.0; - } - - /*! - * Given the cardinality of a set, returns the percentage - * probability that applied to every element of the set generates - * a uniform distribution. - */ - inline double uniform_probability(std::size_t cardinality) { - assert(cardinality > 0); - - return (100.0 / static_cast(cardinality)); - } - - /*! - * Returns a percentage probability that is equal to \a - * probability. - * - * \a probability must be in the range [0.0, 1.0] - */ - inline double probability_to_percentage(double probability) { - assert(probability >= 0.0); - assert(probability <= 1.0); - - return probability * 100.0; - } - -} // end QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE diff --git a/tests/auto/qdoc/catch_generators/tests/CMakeLists.txt b/tests/auto/qdoc/catch_generators/tests/CMakeLists.txt deleted file mode 100644 index 76c366331..000000000 --- a/tests/auto/qdoc/catch_generators/tests/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (C) 2022 The Qt Company Ltd. -# SPDX-License-Identifier: BSD-3-Clause - -qt_internal_add_test(tst_QDoc_Catch_Generators - SOURCES - main.cpp - generators/catch_qchar_generator.cpp - generators/catch_qstring_generator.cpp - generators/catch_k_partition_of_r_generator.cpp - generators/catch_path_generator.cpp - - generators/combinators/catch_oneof_generator.cpp - generators/combinators/catch_cycle_generator.cpp - - utilities/semantics/catch_generator_handler.cpp - INCLUDE_DIRECTORIES - ../src - LIBRARIES - Qt::QDocCatchPrivate - Qt::QDocCatchConversionsPrivate -) diff --git a/tests/auto/qdoc/catch_generators/tests/generators/catch_k_partition_of_r_generator.cpp b/tests/auto/qdoc/catch_generators/tests/generators/catch_k_partition_of_r_generator.cpp deleted file mode 100644 index 9055c5411..000000000 --- a/tests/auto/qdoc/catch_generators/tests/generators/catch_k_partition_of_r_generator.cpp +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#include "namespaces.h" -#include "generators/k_partition_of_r_generator.h" - -#include - -#include - -using namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE; - -SCENARIO("Generating a k-partition of a real number", "[Partition][Reals]") { - GIVEN("A real number r greater or equal to zero") { - double r = GENERATE(take(10, random(0.0, 1000000.0))); - - AND_GIVEN("An amount of desired elements k greater than zero") { - std::size_t k = GENERATE(take(10, random(1, 100))); - - WHEN("A k-partition of r is generated") { - auto k_partition = GENERATE_COPY(take(10, k_partition_of_r(r, k))); - - THEN("The partition contains k elements") { - REQUIRE(k_partition.size() == k); - - AND_THEN("The sum of those elements is r") { - REQUIRE(std::accumulate(k_partition.begin(), k_partition.end(), 0.0) == Approx(r)); - } - } - } - } - } -} - -TEST_CASE("All 1-partition of r are singleton collection with r as their element", "[Partition][Reals][SpecialCase]") { - double r = GENERATE(take(10, random(0.0, 1000000.0))); - auto k_partition = GENERATE_COPY(take(10, k_partition_of_r(r, 1))); - - REQUIRE(k_partition.size() == 1); - REQUIRE(k_partition.front() == r); -} diff --git a/tests/auto/qdoc/catch_generators/tests/generators/catch_path_generator.cpp b/tests/auto/qdoc/catch_generators/tests/generators/catch_path_generator.cpp deleted file mode 100644 index 968008a56..000000000 --- a/tests/auto/qdoc/catch_generators/tests/generators/catch_path_generator.cpp +++ /dev/null @@ -1,755 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#include "namespaces.h" -#include "generators/qchar_generator.h" -#include "generators/qstring_generator.h" -#include "generators/path_generator.h" -#include "generators/combinators/cycle_generator.h" -#include "utilities/statistics/percentages.h" -#include "utilities/statistics/distribution.h" -#include "utilities/semantics/copy_value.h" - -#include - -#include - -#include -#include -#include - -using namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE; -using namespace QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE; - -using namespace Qt::StringLiterals; - -TEST_CASE("A path generated with a multi_device_path_probability of 1.0 always contains a device component.", "[Path][Content][SpecialCase]") { - QString device_component_value{"C:"}; - auto path_generator = path( - Catch::Generators::value(copy_value(device_component_value)), - empty_string(), - empty_string(), - empty_string(), - empty_string(), - PathGeneratorConfiguration{}.set_multi_device_path_probability(1.0) - ); - - auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); - - REQUIRE(generated_path.contains(device_component_value)); -} - -TEST_CASE("A path generated with a multi_device_path_probability of 0.0 never contains a device component.", "[Path][Content][SpecialCase]") { - QString device_component_value{"C:"}; - auto path_generator = path( - Catch::Generators::value(copy_value(device_component_value)), - empty_string(), - empty_string(), - empty_string(), - empty_string(), - PathGeneratorConfiguration{}.set_multi_device_path_probability(0.0) - ); - - auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); - - REQUIRE(!generated_path.contains(device_component_value)); -} - -TEST_CASE("A path generated with an absolute_path_probability of 1.0 always contains a root component.", "[Path][Content][SpecialCase]") { - QString root_component_value{"\\"}; - auto path_generator = path( - empty_string(), - Catch::Generators::value(copy_value(root_component_value)), - empty_string(), - empty_string(), - empty_string(), - PathGeneratorConfiguration{}.set_absolute_path_probability(1.0) - ); - - auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); - - REQUIRE(generated_path.contains(root_component_value)); -} - -TEST_CASE("A path generated with an absolute_path_probability of 0.0 never contains a root component.", "[Path][Content][SpecialCase]") { - QString root_component_value{"\\"}; - auto path_generator = path( - empty_string(), - Catch::Generators::value(copy_value(root_component_value)), - empty_string(), - empty_string(), - empty_string(), - PathGeneratorConfiguration{}.set_absolute_path_probability(0.0) - ); - - auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); - - REQUIRE(!generated_path.contains(root_component_value)); -} - -TEST_CASE("A path generated with a directory_path_probability of 1.0 always ends with a root, directory or directory followed by separator component.", "[Path][Content][SpecialCase]") { - QString root_component_value{"root"}; - QString directory_component_value{"dir"}; - QString separator_component_value{"sep"}; - - auto path_generator = path( - cycle(Catch::Generators::value(QString("device"))), - cycle(Catch::Generators::value(copy_value(root_component_value))), - cycle(Catch::Generators::value(copy_value(directory_component_value))), - cycle(Catch::Generators::value(QString("filename"))), - cycle(Catch::Generators::value(copy_value(separator_component_value))), - PathGeneratorConfiguration{}.set_directory_path_probability(1.0) - ); - - auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); - - REQUIRE(( - generated_path.endsWith(root_component_value) || - generated_path.endsWith(directory_component_value) || - generated_path.endsWith(directory_component_value + separator_component_value) - )); -} - -TEST_CASE("A path generated with a directory_path_probability of 0.0 always ends with a filename component.", "[Path][Content][SpecialCase]") { - QString filename_component_value{"file"}; - - auto path_generator = path( - cycle(Catch::Generators::value(QString("device"))), - cycle(Catch::Generators::value(QString("root"))), - cycle(Catch::Generators::value(QString("dir"))), - cycle(Catch::Generators::value(copy_value(filename_component_value))), - cycle(Catch::Generators::value(QString("sep"))), - PathGeneratorConfiguration{}.set_directory_path_probability(0.0) - ); - - auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); - - REQUIRE(generated_path.endsWith(filename_component_value)); -} - -TEST_CASE("A directory path generated with a has_trailing_separator_probability of 1.0 always ends with a separator component.", "[Path][Content][SpecialCase]") { - QString separator_component_value{"sep"}; - - auto path_generator = path( - cycle(Catch::Generators::value(QString("device"))), - cycle(Catch::Generators::value(QString("root"))), - cycle(Catch::Generators::value(QString("directory"))), - cycle(Catch::Generators::value(QString("filename"))), - cycle(Catch::Generators::value(copy_value(separator_component_value))), - PathGeneratorConfiguration{}.set_directory_path_probability(1.0).set_has_trailing_separator_probability(1.0) - ); - - auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); - - REQUIRE(generated_path.endsWith(separator_component_value)); -} - -TEST_CASE("A directory path generated with a has_trailing_separator_probability of 0.0 never ends with a separator component.", "[Path][Content][SpecialCase]") { - QString separator_component_value{"sep"}; - - auto path_generator = path( - cycle(Catch::Generators::value(QString("device"))), - cycle(Catch::Generators::value(QString("root"))), - cycle(Catch::Generators::value(QString("directory"))), - cycle(Catch::Generators::value(QString("filename"))), - cycle(Catch::Generators::value(copy_value(separator_component_value))), - PathGeneratorConfiguration{}.set_directory_path_probability(1.0).set_has_trailing_separator_probability(0.0) - ); - - auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); - - REQUIRE(!generated_path.endsWith(separator_component_value)); -} - -SCENARIO("Binding a path to a component range", "[Path][Bounds]") { - GIVEN("A minimum amount of components") { - auto minimum_components_amount = GENERATE(take(100, random(std::size_t{1}, std::size_t{100}))); - - AND_GIVEN("A maximum amount of components that is greater or equal than the minimum amount of components") { - auto maximum_components_amount = GENERATE_COPY(take(100, random(minimum_components_amount, std::size_t{100}))); - - WHEN("A path is generated from those bounds") { - QString countable_component_value{"a"}; - - QString generated_path = GENERATE_COPY( - take(1, - path( - empty_string(), - empty_string(), - cycle(Catch::Generators::value(copy_value(countable_component_value))), - cycle(Catch::Generators::value(copy_value(countable_component_value))), - empty_string(), - PathGeneratorConfiguration{}.set_minimum_components_amount(minimum_components_amount).set_maximum_components_amount(maximum_components_amount) - ) - ) - ); - - THEN("The amount of non device, non root, non separator components in the generated path is in the range [minimum_components_amount, maximum_components_amount]") { - std::size_t components_amount{static_cast(generated_path.count(countable_component_value))}; - - REQUIRE(components_amount >= minimum_components_amount); - REQUIRE(components_amount <= maximum_components_amount); - } - } - } - } -} - -TEST_CASE( - "When the maximum amount of components and the minimum amount of components are equal, all generated paths have the same amount of non device, non root, non separator components", - "[Path][Bounds][SpecialCase]") -{ - auto components_amount = GENERATE(take(10, random(std::size_t{1}, std::size_t{100}))); - - QString countable_component_value{"a"}; - QString generated_path = GENERATE_COPY( - take(10, - path( - empty_string(), - empty_string(), - cycle(Catch::Generators::value(copy_value(countable_component_value))), - cycle(Catch::Generators::value(copy_value(countable_component_value))), - empty_string(), - PathGeneratorConfiguration{}.set_minimum_components_amount(components_amount).set_maximum_components_amount(components_amount) - ) - ) - ); - - REQUIRE(static_cast(generated_path.count(countable_component_value)) == components_amount); -} - -SCENARIO("The format of a path", "[Path][Contents]") { - GIVEN("A series of components generators") { - // TODO: Could probably move this to the global scope to - // lighen the tests. - QString device_component_value{"device"}; - QString root_component_value{"root"}; - QString directory_component_value{"dir"}; - QString filename_component_value{"file"}; - QString separator_component_value{"sep"}; - - auto device_component_generator = cycle(Catch::Generators::value(copy_value(device_component_value))); - auto root_component_generator = cycle(Catch::Generators::value(copy_value(root_component_value))); - auto directory_component_generator = cycle(Catch::Generators::value(copy_value(directory_component_value))); - auto filename_component_generator = cycle(Catch::Generators::value(copy_value(filename_component_value))); - auto separator_component_generator = cycle(Catch::Generators::value(copy_value(separator_component_value))); - - AND_GIVEN("A generator of paths using those components generator") { - // TODO: We should actually randomize the configuration by - // making a simple generator for it. - auto path_generator = path( - std::move(device_component_generator), - std::move(root_component_generator), - std::move(directory_component_generator), - std::move(filename_component_generator), - std::move(separator_component_generator) - ); - - WHEN("A path is generated from that generator") { - auto generated_path = GENERATE_REF(take(10, std::move(path_generator))); - - THEN("At most one device component is in the generated path") { - REQUIRE(generated_path.count(device_component_value) <= 1); - } - - THEN("At most one root component is in the generated path") { - REQUIRE(generated_path.count(root_component_value) <= 1); - } - - THEN("At most one filename component is in the generated path") { - REQUIRE(generated_path.count(filename_component_value) <= 1); - } - - THEN("At least one non device, non root, non separator component is in the generated path") { - REQUIRE((generated_path.contains(directory_component_value) || generated_path.contains(filename_component_value))); - } - - THEN("There is a separator component between any two successive directory components") { - // REMARK: To test this condition, which is not - // easy to test directly, as, if the generator is - // working as it should, the concept of successive - // directories stops existing. - // To test it, then, we split the condition into - // two parts, that are easier to test, that - // achieve the same effect. - // First, if all directories have a separator - // component between them, it is impossible to - // have a directory component that is directly - // followed by another directory component. - // Second, when this holds, any two directory - // components must have one or more non-directory - // components between them. - // For those directories that have exactly one - // component between them, it must be a separator. - // This is equivalent to the original condition as - // long as it is not allowed for anything else to - // be between two directory components that have - // exactly one component between them. - // This is true at the time of writing of this - // test, such that this will work correctly, but - // if this changes the test is invalidated. - // If a test for the original condition is found - // that is not contrived (as it is possible to - // test the original condition but it is a bit - // more complex than we would like the test to - // be), it should replace this current - // implementation to improve the resiliency of the - // test. - REQUIRE_FALSE(generated_path.contains(directory_component_value + directory_component_value)); - - auto successive_directories_re{ - QRegularExpression(u"%1(%2)%3"_s.arg(directory_component_value) - .arg(QStringList{device_component_value, root_component_value, filename_component_value, separator_component_value}.join("|")) - .arg(directory_component_value) - )}; - - auto successive_directories_match(successive_directories_re.match(generated_path)); - while (successive_directories_match.hasMatch()) { - auto in_between_component{successive_directories_match.captured(1)}; - - // TODO: Having this in a loop makes it so - // the amount of assertions will vary slightly - // per-run. - // It would be better to avoid this, even if - // it should not really be a problem - // generally. - // Try to find a better way to express this - // condition that does not require a loop. - // This could be as easy as just collection - // the results and then using a std::all_of. - REQUIRE(in_between_component == separator_component_value); - - successive_directories_match = successive_directories_re.match(generated_path, successive_directories_match.capturedEnd(1)); - } - } - - - THEN("There is a separator component between each successive directory and filename components") { - REQUIRE_FALSE(generated_path.contains(directory_component_value + filename_component_value)); - - auto successive_directory_filename_re{ - QRegularExpression(u"%1(%2)%3"_s.arg(directory_component_value) - .arg(QStringList{device_component_value, root_component_value, filename_component_value, separator_component_value}.join("|")) - .arg(filename_component_value) - )}; - - auto successive_directory_filename_match(successive_directory_filename_re.match(generated_path)); - while (successive_directory_filename_match.hasMatch()) { - auto in_between_component{successive_directory_filename_match.captured(1)}; - - REQUIRE(in_between_component == separator_component_value); - - successive_directory_filename_match = successive_directory_filename_re.match(generated_path, successive_directory_filename_match.capturedEnd(1)); - } - } - } - } - - AND_GIVEN("A generator of paths using those components generator that generates Multi-Device paths") { - auto path_generator = path( - std::move(device_component_generator), - std::move(root_component_generator), - std::move(directory_component_generator), - std::move(filename_component_generator), - std::move(separator_component_generator), - PathGeneratorConfiguration{}.set_multi_device_path_probability(1.0) - ); - - WHEN("A path is generated from that generator") { - auto generated_path = GENERATE_REF(take(10, std::move(path_generator))); - - THEN("Exactly one device component is in the generated path") { - REQUIRE(generated_path.count(device_component_value) == 1); - - AND_THEN("The device component is the first component in the generated path") { - REQUIRE(generated_path.startsWith(device_component_value)); - } - } - } - } - - AND_GIVEN("A generator of paths using those components generator that generates Absolute paths") { - auto path_generator = path( - std::move(device_component_generator), - std::move(root_component_generator), - std::move(directory_component_generator), - std::move(filename_component_generator), - std::move(separator_component_generator), - PathGeneratorConfiguration{}.set_absolute_path_probability(1.0) - ); - - WHEN("A path is generated from that generator") { - auto generated_path = GENERATE_REF(take(10, std::move(path_generator))); - - THEN("Exactly one root component is in the generated path") { - REQUIRE(generated_path.count(root_component_value) == 1); - } - } - } - - AND_GIVEN("A generator of paths using those components generator that generates Absolute paths that are not Multi-Device") { - auto path_generator = path( - std::move(device_component_generator), - std::move(root_component_generator), - std::move(directory_component_generator), - std::move(filename_component_generator), - std::move(separator_component_generator), - PathGeneratorConfiguration{}.set_multi_device_path_probability(0.0).set_absolute_path_probability(1.0) - ); - - WHEN("A path is generated from that generator") { - auto generated_path = GENERATE_REF(take(10, std::move(path_generator))); - - THEN("The root component is the first component in the generated path") { - REQUIRE(generated_path.startsWith(root_component_value)); - } - } - } - - AND_GIVEN("A generator of paths using those components generator that generates Multi-Device, Absolute paths") { - auto path_generator = path( - std::move(device_component_generator), - std::move(root_component_generator), - std::move(directory_component_generator), - std::move(filename_component_generator), - std::move(separator_component_generator), - PathGeneratorConfiguration{}.set_multi_device_path_probability(1.0).set_absolute_path_probability(1.0) - ); - - WHEN("A path is generated from that generator") { - auto generated_path = GENERATE_REF(take(10, std::move(path_generator))); - - THEN("The root component succeeds the device component in the generated path") { - REQUIRE(generated_path.contains(device_component_value + root_component_value)); - } - } - } - - AND_GIVEN("A generator of paths using those components generator that generates paths that are To a Directory and do not Have a Trailing Separator") { - auto path_generator = path( - std::move(device_component_generator), - std::move(root_component_generator), - std::move(directory_component_generator), - std::move(filename_component_generator), - std::move(separator_component_generator), - PathGeneratorConfiguration{}.set_directory_path_probability(1.0).set_has_trailing_separator_probability(0.0) - ); - - WHEN("A path is generated from that generator") { - auto generated_path = GENERATE_REF(take(10, std::move(path_generator))); - - THEN("The last component of in the path is a directory component") { - REQUIRE(generated_path.endsWith(directory_component_value)); - } - } - } - - AND_GIVEN("A generator of paths using those components generator that generates paths that are To a Directory and Have a Trailing Separator") { - auto path_generator = path( - std::move(device_component_generator), - std::move(root_component_generator), - std::move(directory_component_generator), - std::move(filename_component_generator), - std::move(separator_component_generator), - PathGeneratorConfiguration{}.set_directory_path_probability(1.0).set_has_trailing_separator_probability(1.0) - ); - - WHEN("A path is generated from that generator") { - auto generated_path = GENERATE_REF(take(10, std::move(path_generator))); - - THEN("The last component in the path is a separator component that is preceded by a directory component") { - REQUIRE(generated_path.endsWith(directory_component_value + separator_component_value)); - } - } - } - - - AND_GIVEN("A generator of paths using those components generator that generates paths that are To a File") { - auto path_generator = path( - std::move(device_component_generator), - std::move(root_component_generator), - std::move(directory_component_generator), - std::move(filename_component_generator), - std::move(separator_component_generator), - PathGeneratorConfiguration{}.set_directory_path_probability(0.0) - ); - - WHEN("A path is generated from that generator") { - auto generated_path = GENERATE_REF(take(10, std::move(path_generator))); - - THEN("Exactly one filename component is in the path") { - REQUIRE(generated_path.contains(filename_component_value)); - - AND_THEN("The filename component is the last component in the path") { - REQUIRE(generated_path.endsWith(filename_component_value)); - } - } - } - } - } -} - -// REMARK: [mayfail][distribution] -SCENARIO("Observing the distribution of paths based on their configuration", "[Path][Statistics][!mayfail]") { - GIVEN("A series of components generators") { - QString device_component_value{"device"}; - QString root_component_value{"root"}; - QString directory_component_value{"dir"}; - QString filename_component_value{"file"}; - QString separator_component_value{"sep"}; - - auto device_component_generator = cycle(Catch::Generators::value(copy_value(device_component_value))); - auto root_component_generator = cycle(Catch::Generators::value(copy_value(root_component_value))); - auto directory_component_generator = cycle(Catch::Generators::value(copy_value(directory_component_value))); - auto filename_component_generator = cycle(Catch::Generators::value(copy_value(filename_component_value))); - auto separator_component_generator = cycle(Catch::Generators::value(copy_value(separator_component_value))); - - AND_GIVEN("A generator of paths using those components generator that produces paths that are Multi-Device with a probability of n") { - double multi_device_path_probability = GENERATE(take(10, random(0.0, 1.0))); - - auto path_generator = path( - std::move(device_component_generator), - std::move(root_component_generator), - std::move(directory_component_generator), - std::move(filename_component_generator), - std::move(separator_component_generator), - PathGeneratorConfiguration{}.set_multi_device_path_probability(multi_device_path_probability) - ); - - WHEN("A certain amount of paths are generated from that generator") { - auto paths = GENERATE_REF(take(1, chunk(10000, std::move(path_generator)))); - - THEN("The amount of paths that are Multi-Device approximately respects the given probability and the amount of paths that are not approximately respects a probability of 1 - n") { - auto maybe_distribution_error{respects_distribution( - std::move(paths), - [&device_component_value](const QString& path){ return (path.startsWith(device_component_value)) ? "Multi-Device" : "Non Multi-Device"; }, - [multi_device_path_probability](const QString& key){ return probability_to_percentage((key == "Multi-Device") ? multi_device_path_probability : 1 - multi_device_path_probability); } - )}; - - REQUIRE_FALSE(maybe_distribution_error); - } - } - } - - AND_GIVEN("A generator of paths using those components generator that produces paths that are Absolute with a probability of n") { - double absolute_path_probability = GENERATE(take(10, random(0.0, 1.0))); - - auto path_generator = path( - std::move(device_component_generator), - std::move(root_component_generator), - std::move(directory_component_generator), - std::move(filename_component_generator), - std::move(separator_component_generator), - PathGeneratorConfiguration{}.set_absolute_path_probability(absolute_path_probability) - ); - - WHEN("A certain amount of paths are generated from that generator") { - auto paths = GENERATE_REF(take(1, chunk(10000, std::move(path_generator)))); - - THEN("The amount of paths that are Absolute approximately respects the given probability and the amount of paths that are Relative approximately respects a probability of 1 - n") { - auto maybe_distribution_error{respects_distribution( - std::move(paths), - [&root_component_value](const QString& path){ return (path.contains(root_component_value)) ? "Absolute" : "Relative"; }, - [absolute_path_probability](const QString& key){ return probability_to_percentage((key == "Absolute") ? absolute_path_probability : 1 - absolute_path_probability); } - )}; - - REQUIRE_FALSE(maybe_distribution_error); - } - } - } - - AND_GIVEN("A generator of paths using those components generator that produces paths that are To a Directory with a probability of n") { - double directory_path_probability = GENERATE(take(10, random(0.0, 1.0))); - - auto path_generator = path( - std::move(device_component_generator), - std::move(root_component_generator), - std::move(directory_component_generator), - std::move(filename_component_generator), - std::move(separator_component_generator), - PathGeneratorConfiguration{}.set_directory_path_probability(directory_path_probability) - ); - - WHEN("A certain amount of paths are generated from that generator") { - auto paths = GENERATE_REF(take(1, chunk(10000, std::move(path_generator)))); - - THEN("The amount of paths that are To a Directory approximately respects the given probability and the amount of paths that are To a File approximately respects a probability of 1 - n") { - auto maybe_distribution_error{respects_distribution( - std::move(paths), - [&filename_component_value](const QString& path){ return (path.contains(filename_component_value)) ? "To a File" : "To a Directory"; }, - [directory_path_probability](const QString& key){ return probability_to_percentage((key == "To a Directory") ? directory_path_probability : 1 - directory_path_probability); } - )}; - - REQUIRE_FALSE(maybe_distribution_error); - } - } - } - - AND_GIVEN("A generator of paths using those components generator that produces paths that are To a Directory with a probability of n to Have a Trailing Separator") { - double has_trailing_separator_probability = GENERATE(take(10, random(0.0, 1.0))); - - auto path_generator = path( - std::move(device_component_generator), - std::move(root_component_generator), - std::move(directory_component_generator), - std::move(filename_component_generator), - std::move(separator_component_generator), - PathGeneratorConfiguration{}.set_directory_path_probability(1.0).set_has_trailing_separator_probability(has_trailing_separator_probability) - ); - - WHEN("A certain amount of paths are generated from that generator") { - auto paths = GENERATE_REF(take(1, chunk(10000, std::move(path_generator)))); - - THEN("The amount of paths that are Have a Trailing Separator approximately respects the given probability and the amount of paths that do not Have a Trailing Separator approximately respects a probability of 1 - n") { - auto maybe_distribution_error{respects_distribution( - std::move(paths), - [&separator_component_value](const QString& path){ return (path.endsWith(separator_component_value)) ? "Have a Trailing Separator" : "Doesn't Have a Trailing Separator"; }, - [has_trailing_separator_probability](const QString& key){ return probability_to_percentage((key == "Have a Trailing Separator") ? has_trailing_separator_probability : 1 - has_trailing_separator_probability); } - )}; - - REQUIRE_FALSE(maybe_distribution_error); - } - } - } - } -} - -TEST_CASE("The first component of the passed in device components generator is not lost", "[Path][GeneratorFirstElement][SpecialCase]") { - QString device_component_generator_first_value{"device"}; - - auto generated_path = GENERATE_COPY(take(1, - path( - values({device_component_generator_first_value, QString{""}}), - empty_string(), - empty_string(), - empty_string(), - empty_string(), - PathGeneratorConfiguration{} - .set_multi_device_path_probability(1.0) - .set_minimum_components_amount(1) - .set_maximum_components_amount(1) - ) - )); - - REQUIRE(generated_path.contains(device_component_generator_first_value)); -} - -TEST_CASE("The first component of the passed in root components generator is not lost", "[Path][GeneratorFirstElement][SpecialCase]") { - QString root_component_generator_first_value{"root"}; - - auto generated_path = GENERATE_COPY(take(1, - path( - empty_string(), - values({root_component_generator_first_value, QString{""}}), - empty_string(), - empty_string(), - empty_string(), - PathGeneratorConfiguration{} - .set_absolute_path_probability(1.0) - .set_minimum_components_amount(1) - .set_maximum_components_amount(1) - ) - )); - - REQUIRE(generated_path.contains(root_component_generator_first_value)); -} - -TEST_CASE("The first component of the passed in directory components generator is not lost", "[Path][GeneratorFirstElement][SpecialCase]") { - QString directory_component_generator_first_value{"dir"}; - - auto generated_path = GENERATE_COPY(take(1, - path( - empty_string(), - empty_string(), - values({directory_component_generator_first_value, QString{""}}), - empty_string(), - empty_string(), - PathGeneratorConfiguration{} - .set_directory_path_probability(1.0) - .set_minimum_components_amount(1) - .set_maximum_components_amount(1) - ) - )); - - REQUIRE(generated_path.contains(directory_component_generator_first_value)); -} - -TEST_CASE("The first component of the passed in filename components generator is not lost", "[Path][GeneratorFirstElement][SpecialCase]") { - QString filename_component_generator_first_value{"dir"}; - - auto generated_path = GENERATE_COPY(take(1, - path( - empty_string(), - empty_string(), - empty_string(), - values({filename_component_generator_first_value, QString{""}}), - empty_string(), - PathGeneratorConfiguration{} - .set_directory_path_probability(0.0) - .set_minimum_components_amount(1) - .set_maximum_components_amount(1) - ) - )); - - REQUIRE(generated_path.contains(filename_component_generator_first_value)); -} - -TEST_CASE("The first component of the passed in separator components generator is not lost", "[Path][GeneratorFirstElement][SpecialCase]") { - QString separator_component_generator_first_value{"sep"}; - - auto generated_path = GENERATE_COPY(take(1, - path( - empty_string(), - empty_string(), - empty_string(), - empty_string(), - values({separator_component_generator_first_value, QString{""}}), - PathGeneratorConfiguration{} - .set_directory_path_probability(0.0) - .set_minimum_components_amount(2) - .set_maximum_components_amount(2) - ) - )); - - REQUIRE(generated_path.contains(separator_component_generator_first_value)); -} - -SCENARIO("Generating paths that are suitable to be used on POSIX systems", "[Path][POSIX][Content]") { - GIVEN("A generator that generates Strings representing paths on a POSIX system that are portable") { - auto path_generator = relaxed_portable_posix_path(); - - WHEN("A path is generated from it") { - auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); - - THEN("The path is composed only by one or more characters in the class [-_./a-zA-Z0-9]") { - REQUIRE(QRegularExpression{R"(\A[-_.\/a-zA-Z0-9]+\z)"}.match(generated_path).hasMatch()); - } - } - } -} - -SCENARIO("Generating paths that are suitable to be used on Windows", "[Path][Windows][Content]") { - GIVEN("A generator that generates Strings representing paths on a Windows system") { - auto path_generator = traditional_dos_path(); - - WHEN("A path is generated from it") { - auto generated_path = GENERATE_REF(take(100, std::move(path_generator))); - - CAPTURE(generated_path); - - THEN("The path starts with an uppercase letter followed by a colon, a backward or forward slash or a character in the class [-_.a-zA-Z0-9]") { - QRegularExpression beginning_re{"([A-Z]:|\\|\\/|[-_.a-zA-Z0-9])"}; - - auto beginning_match{beginning_re.match(generated_path)}; - - REQUIRE(beginning_match.hasMatch()); - - generated_path.remove(0, beginning_match.capturedEnd()); - - AND_THEN("The rest of the path is composed by zero or more characters in the class [-_./\\a-zA-Z0-9]") { - REQUIRE(QRegularExpression{R"(\A[-_.\/\\a-zA-Z0-9]*\z)"}.match(generated_path).hasMatch()); - } - } - } - } -} diff --git a/tests/auto/qdoc/catch_generators/tests/generators/catch_qchar_generator.cpp b/tests/auto/qdoc/catch_generators/tests/generators/catch_qchar_generator.cpp deleted file mode 100644 index 47ef23364..000000000 --- a/tests/auto/qdoc/catch_generators/tests/generators/catch_qchar_generator.cpp +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#include "namespaces.h" -#include "generators/qchar_generator.h" - -#include - -#include - -#include - -using namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE; -using namespace QDOC_CATCH_GENERATORS_QCHAR_ALPHABETS_NAMESPACE; - -SCENARIO("Binding a generated QChar to a range", "[QChar][Bounds]") { - GIVEN("A lower bound") { - auto lower_bound = GENERATE(take(100, random( - static_cast(std::numeric_limits::min()), - static_cast(std::numeric_limits::max()) - ))); - - AND_GIVEN("An upper bound that is greater or equal than the lower bound") { - auto upper_bound = GENERATE_COPY(take(100, random(lower_bound, static_cast(std::numeric_limits::max())))); - - WHEN("A QChar is generated from those bounds") { - QChar generated_character = GENERATE_COPY(take(1, character(lower_bound, upper_bound))); - - THEN("The generated character has a unicode value in the range [lower_bound, upper_bound]") { - REQUIRE(generated_character.unicode() >= lower_bound); - REQUIRE(generated_character.unicode() <= upper_bound); - } - } - } - } -} - -TEST_CASE( - "When lower_bound and upper_bound are equal, let their value be n, the only generated character is the one with unicode value n", - "[QChar][Bounds]" -) { - auto bound = GENERATE(take(100, random( - static_cast(std::numeric_limits::min()), - static_cast(std::numeric_limits::max()) - ))); - auto generated_character = GENERATE_COPY(take(100, character(bound, bound))); - - REQUIRE(generated_character.unicode() == bound); -} - -TEST_CASE("When generating digits, each generated character is in the class [0-9]", "[QChar][SpecialCase]") { - auto generated_character = GENERATE(take(100, digit())); - - REQUIRE(generated_character >= '0'); - REQUIRE(generated_character <= '9'); -} - -TEST_CASE("When generating lowercase ascii characters, each generated character is in the class [a-z]", "[QChar][SpecialCase]") { - auto generated_character = GENERATE(take(100, ascii_lowercase())); - - REQUIRE(generated_character >= 'a'); - REQUIRE(generated_character <= 'z'); -} - -TEST_CASE("When generating uppercase ascii characters, each generated character is in the class [A-Z]", "[QChar][SpecialCase]") { - auto generated_character = GENERATE(take(100, ascii_uppercase())); - - REQUIRE(generated_character >= 'A'); - REQUIRE(generated_character <= 'Z'); -} - -TEST_CASE("When generating ascii alphabetic characters, each generated character is in the class [a-zA-Z]", "[QChar][SpecialCase]") { - auto generated_character = GENERATE(take(100, ascii_alpha())); - - REQUIRE(( - (generated_character >= 'a' && generated_character <= 'z') || - (generated_character >= 'A' && generated_character <= 'Z') - )); -} - -TEST_CASE("When generating ascii alphabetic characters, each generated character is in the class [a-zA-Z0-9]", "[QChar][SpecialCase]") { - auto generated_character = GENERATE(take(100, ascii_alpha())); - - REQUIRE(( - (generated_character >= 'a' && generated_character <= 'z') || - (generated_character >= 'A' && generated_character <= 'Z') || - (generated_character >= '0' && generated_character <= '9') - )); -} - -TEST_CASE("When generating portable posix filename, each generated character is in the class [-_.a-zA-Z0-9]", "[QChar][SpecialCase]") { - auto generated_character = GENERATE(take(100, ascii_alpha())); - - REQUIRE(( - (generated_character == '-') || - (generated_character == '_') || - (generated_character == '.') || - (generated_character >= 'a' && generated_character <= 'z') || - (generated_character >= 'A' && generated_character <= 'Z') || - (generated_character >= '0' && generated_character <= '9') - )); -} diff --git a/tests/auto/qdoc/catch_generators/tests/generators/catch_qstring_generator.cpp b/tests/auto/qdoc/catch_generators/tests/generators/catch_qstring_generator.cpp deleted file mode 100644 index 75d7efcf1..000000000 --- a/tests/auto/qdoc/catch_generators/tests/generators/catch_qstring_generator.cpp +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#include "namespaces.h" -#include "generators/qchar_generator.h" -#include "generators/qstring_generator.h" - -#include - -#include - -using namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE; - -#include - -SCENARIO("Binding a QString to a length range", "[QString][Bounds]") { - GIVEN("A minimum length") { - auto minimum_length = GENERATE(take(100, random(0, 100))); - - AND_GIVEN("A maximum length that is greater or equal than the minimum length") { - auto maximum_length = GENERATE_COPY(take(100, random(minimum_length, 100))); - - WHEN("A QString is generated from those bounds") { - QString generated_string = GENERATE_COPY(take(1, string(character(), minimum_length, maximum_length))); - - THEN("The generated string's length is in the range [minimum_length, maximum_length]") { - REQUIRE(generated_string.size() >= minimum_length); - REQUIRE(generated_string.size() <= maximum_length); - } - } - } - } -} - -TEST_CASE("When the maximum length and the minimum length are zero all generated strings are the empty string", "[QString][Bounds][SpecialCase][BoundingValue]") { - QString generated_string = GENERATE(take(100, string(character(), 0, 0))); - - REQUIRE(generated_string.isEmpty()); -} - -TEST_CASE("When the maximum length and the minimum length are equal, all generated strings have the same length equal to the given length", "[QString][Bounds][SpecialCase]") { - auto length = GENERATE(take(100, random(0, 100))); - auto generated_string = GENERATE_COPY(take(100, string(character(), length, length))); - - REQUIRE(generated_string.size() == length); -} - -SCENARIO("Limiting the characters that can compose a QString", "[QString][Contents]") { - GIVEN("A list of characters candidates") { - auto lower_character_bound = GENERATE(take(10, random( - static_cast(std::numeric_limits::min()), - static_cast(std::numeric_limits::max()) - ))); - auto upper_character_bound = GENERATE_COPY(take(10, random(lower_character_bound, static_cast(std::numeric_limits::max())))); - - auto character_candidates = character(lower_character_bound, upper_character_bound); - - WHEN("A QString is generated from that list") { - QString generated_string = GENERATE_REF(take(100, string(std::move(character_candidates), 1, 50))); - - THEN("The string is composed only of characters that are in the list of characters") { - REQUIRE( - std::all_of( - generated_string.cbegin(), generated_string.cend(), - [lower_character_bound, upper_character_bound](QChar element){ return element.unicode() >= lower_character_bound && element.unicode() <= upper_character_bound; } - ) - ); - } - } - } -} - -TEST_CASE("The strings generated by a generator of empty string are all empty", "[QString][Contents]") { - QString generated_string = GENERATE(take(100, empty_string())); - - REQUIRE(generated_string.isEmpty()); -} - - -TEST_CASE("The first element of the passsed in generator is not lost", "[QString][GeneratorFirstElement][SpecialCase]") { - QChar first_value{'a'}; - - // REMARK: We use two values to avoid having the generator throw - // an exception if the first element is actually lost. - auto character_generator{Catch::Generators::values({first_value, QChar{'b'}})}; - auto generated_string = GENERATE_REF(take(1, string(std::move(character_generator), 1, 1))); - - REQUIRE(generated_string == QString{first_value}); -} diff --git a/tests/auto/qdoc/catch_generators/tests/generators/combinators/catch_cycle_generator.cpp b/tests/auto/qdoc/catch_generators/tests/generators/combinators/catch_cycle_generator.cpp deleted file mode 100644 index 43bae006b..000000000 --- a/tests/auto/qdoc/catch_generators/tests/generators/combinators/catch_cycle_generator.cpp +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#include "namespaces.h" -#include "generators/combinators/cycle_generator.h" - -#include - -using namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE; - -// REMARK: We use fixed-values-generators for those tests so that it -// is trivial to identify when their generation will end, which -// values we should expect and how many values we should expect. -// This is unfortunately not general, but we don't have, by default, -// enough tools to generalize this without having to provide our own -// (being able to generate fixed values from a vector) and adding more -// to the complexity, which is already high. - -TEST_CASE( - "The xn + m element, where 0 < m < n, from a repeating generator whose underlying generator produces n elements, will produce an element equivalent to the mth element of the generation produced by the underlying generator", - "[Cycle][Combinators]" -) { - std::size_t n{10}; - - auto owned_generator{Catch::Generators::values({'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'})}; - auto owned_generator_copy{Catch::Generators::values({'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'})}; - - auto original_generation = GENERATE_REF(take(1, chunk(n, std::move(owned_generator_copy)))); - - std::size_t x = GENERATE(take(10, random(std::size_t{0}, std::size_t{20}))); - std::size_t m = GENERATE_COPY(take(10, random(std::size_t{1}, std::size_t{n}))); - - auto repeating_generator = cycle(std::move(owned_generator)); - auto repeating_generation = GENERATE_REF(take(1, chunk((x * n) + m, std::move(repeating_generator)))); - - REQUIRE(repeating_generation.back() == original_generation[m - 1]); -} - -SCENARIO("Repeating a generation ad infinitum", "[Cycle][Combinators]") { - GIVEN("Some finite generator") { - std::size_t values_amount{3}; - - auto owned_generator{Catch::Generators::values({'a', 'b', 'c'})}; - auto owned_generator_copy{Catch::Generators::values({'a', 'b', 'c'})}; - - AND_GIVEN("A way to repeat the generation of that generator infinitely") { - auto repeating_generator = cycle(std::move(owned_generator)); - - WHEN("Generating exactly enough values to exhaust the original generator") { - auto repeating_generation = GENERATE_REF(take(1, chunk(values_amount, std::move(repeating_generator)))); - auto original_generation = GENERATE_REF(take(1, chunk(values_amount, std::move(owned_generator_copy)))); - - THEN("The repeating generator behaves equally to the original finite generator") { - REQUIRE(repeating_generation == original_generation); - } - } - - WHEN("Generating exactly n times the amount of values required to exhaust the original generator") { - std::size_t n = GENERATE(take(10, random(2, 10))); - - auto original_generation = GENERATE_REF(take(1, chunk(values_amount, std::move(owned_generator_copy)))); - auto repeating_generation = GENERATE_REF(take(n, chunk(values_amount, std::move(repeating_generator)))); - - THEN("The n generation of the repeating generator are always the same as the generation of the original generation") { - REQUIRE(repeating_generation == original_generation); - } - } - } - } -} diff --git a/tests/auto/qdoc/catch_generators/tests/generators/combinators/catch_oneof_generator.cpp b/tests/auto/qdoc/catch_generators/tests/generators/combinators/catch_oneof_generator.cpp deleted file mode 100644 index 68e990813..000000000 --- a/tests/auto/qdoc/catch_generators/tests/generators/combinators/catch_oneof_generator.cpp +++ /dev/null @@ -1,362 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#include - -#include "namespaces.h" -#include "generators/k_partition_of_r_generator.h" -#include "generators/combinators/oneof_generator.h" -#include "generators/combinators/cycle_generator.h" -#include "utilities/statistics/percentages.h" -#include "utilities/statistics/distribution.h" -#include "utilities/semantics/copy_value.h" - -#include - -#include -#include -#include -#include -#include - -using namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE; -using namespace QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE; - -SCENARIO("Choosing between one of many generators", "[OneOf][Combinators]") { - GIVEN("Some generators producing values of the same type") { - auto generators_amount = GENERATE(take(10, random(1, 10))); - auto generators_values = GENERATE_COPY(take(10, chunk(generators_amount, random(0, 100000)))); - - std::vector> generators; - generators.reserve(generators_amount); - std::transform( - generators_values.begin(), generators_values.end(), std::back_inserter(generators), - [](auto& value){ return Catch::Generators::value(copy_value(value)); } - ); - - AND_GIVEN("A generator choosing between them based on some distribution") { - std::vector weights = GENERATE_COPY(take(10, k_partition_of_r(100.0, generators_amount))); - auto choosing_generator = oneof(std::move(generators), std::move(weights)); - - WHEN("A value is extracted from the choosing generator") { - auto generated_value = GENERATE_REF(take(100, std::move(choosing_generator))); - - THEN("The generated value is a member of one of the original generators") { - REQUIRE(std::find(generators_values.cbegin(), generators_values.cend(), generated_value) != generators_values.cend()); - } - } - } - - AND_GIVEN("A generator choosing between them with the same probability") { - auto choosing_generator = uniform_oneof(std::move(generators)); - - WHEN("A value is extracted from the choosing generator") { - auto generated_value = GENERATE_REF(take(100, std::move(choosing_generator))); - - THEN("The generated value is a member of one of the original generators") { - REQUIRE(std::find(generators_values.cbegin(), generators_values.cend(), generated_value) != generators_values.cend()); - } - } - } - - AND_GIVEN("A generator choosing between them such that each possible value has the same probability of being chosen") { - auto choosing_generator = uniformly_valued_oneof(std::move(generators), std::vector(generators_amount, std::size_t{1})); - - WHEN("A value is extracted from the choosing generator") { - auto generated_value = GENERATE_REF(take(100, std::move(choosing_generator))); - - THEN("The generated value is a member of one of the original generators") { - REQUIRE(std::find(generators_values.cbegin(), generators_values.cend(), generated_value) != generators_values.cend()); - } - } - } - } -} - -// TODO: The following is a generally complex test. Nonetheless, we -// can probably ease some of the complexity by moving it out into some -// generators or by abstracting it a little to remove the need to know -// some of the implementation details. -// Check if this is possible. - -// REMARK: [mayfail][distribution] -// This tests cannot be precise as it depends on randomized output. -// For this reason, we mark it as !mayfail. -// This allows us to see cases where it fails without having the -// test-run itself fail. -// We generally expect this test to not fail, but it may fail randomly -// every now and then simply because of how a correctly randomized -// distribution may behave. -// As long as this test doesn't fail consistently, with values that -// shows an unsustainable deviation, it should be considered to be -// working. -SCENARIO("Observing the distribution of generators that are chosen from", "[OneOf][Combinators][Statistics][!mayfail]") { - GIVEN("Some generators producing values of the same type") { - std::size_t generators_amount = GENERATE(take(10, random(1, 10))); - - // REMARK: To test the distribution, we want to have some - // amount of generators to choose from whose generated values - // can be uniquely reconducted to the generating generator so - // that we may count how many times a specific generator was - // chosen. - // The easiest way would be to have generators that produce a - // single value. - // Nonetheless, to test the version that provides an - // approximate uniform distribution over the values themselves - // correctly, we need to have generators that can produce a - // different amount of elements. - // When that is not the case, indeed, a generator that - // approximately distributes uniformly over values is - // equivalent to one that approximately distributes uniformely - // over the generators themselves. - // As such, we use ranges of positive integers, as they are - // the simplest multi-valued finite generator that can be dinamically - // construted, while still providing an easy way to infer the - // amount of values it contains so that we can derive the - // cardinality of our domain. - // We produce those ranges as disjoint subsequent ranges - // starting from 0 upward. - // We require the ranges to be disjoint so that we do not lose - // the ability of uniquely identifying a generator that - // produced the value. - // - // To do so, we generate a series of disjoint least upper - // bounds for the ranges. - // Then, we produce the ith range by using the successor of - // the (i - 1)th upper bound as its lower bound and the ith - // upper bound as its upper bound. - // - // We take further care to ensure that the collection of upper - // bounds is sorted, as this simplifies to a linear search our - // need to index the collection of generators to find the - // identifying generator and its associated probability. - std::vector generators_bounds(generators_amount, 0); - std::vector> generators; - generators.reserve(generators_amount); - - std::size_t lowest_bound{0}; - std::size_t generators_step{1000}; - std::size_t lower_bound_offset{1}; - - generators_bounds[0] = Catch::Generators::random(lowest_bound, generators_step).get(); - generators.push_back(Catch::Generators::random(lowest_bound, generators_bounds[0])); - - // We use this one to group together values that are generated - // from the same generator and to provide an index for that - // generator to use for finding its associated probability. - // Since our generators are defined by their upper bounds and - // the collection of upper bounds is sorted, the first - // encountered upper bound that is not less than the value - // itself must be the least upper bound of the generator that - // produced the value. - // Then, the index of that upper bound must be the same as the - // index of the producing generator and its associated - // probability. - auto find_index_of_producing_generator = [&generators_bounds](auto value) { - return static_cast(std::distance( - generators_bounds.begin(), - std::find_if(generators_bounds.begin(), generators_bounds.end(), [&value](auto element){ return value <= element; }) - )); - }; - - for (std::size_t index{1}; index < generators_amount; ++index) { - generators_bounds[index] = Catch::Generators::random(generators_bounds[index - 1] + lower_bound_offset + 1, generators_bounds[index - 1] + lower_bound_offset + 1 + generators_step).get(); - generators.push_back(Catch::Generators::random(generators_bounds[index - 1] + lower_bound_offset, generators_bounds[index])); - } - - AND_GIVEN("A probability of being chosen, in percentage, for each of the generators, such that the sum of the percentages is one hundred") { - std::vector probabilities = GENERATE_COPY(take(10, k_partition_of_r(100.0, generators_amount))); - - AND_GIVEN("A choosing generator for those generators based on the given probabilities") { - auto choosing_generator = oneof(std::move(generators), probabilities); - - WHEN("A certain amount of values are generated from the choosing generator") { - auto values = GENERATE_REF(take(1, chunk(10000, std::move(choosing_generator)))); - - THEN("The distribution of elements for each generator approximately respects the weight that was given to it") { - auto maybe_distribution_error{respects_distribution( - std::move(values), - find_index_of_producing_generator, - [&probabilities](auto key){ return probabilities[key]; } - )}; - - REQUIRE_FALSE(maybe_distribution_error); - } - } - } - } - - AND_GIVEN("A choosing generator for those generators that will choose each generator with the same probability") { - auto choosing_generator = uniform_oneof(std::move(generators)); - - WHEN("A certain amount of values are generated from the choosing generator") { - auto values = GENERATE_REF(take(1, chunk(10000, std::move(choosing_generator)))); - - THEN("The distribution of elements approximates uniformity over the generators") { - double probability{uniform_probability(generators_amount)}; - - auto maybe_distribution_error{respects_distribution( - std::move(values), - find_index_of_producing_generator, - [&probability](auto _){ (void)(_); return probability; } - )}; - - REQUIRE_FALSE(maybe_distribution_error); - } - } - } - - AND_GIVEN("A choosing generator for those generators that will choose each generator such that each possible value has the same probability of being chosen") { - // REMARK: We need to know the total amount of - // unique values that can be generated by our - // generators, so that we can construct an - // appropriate distribution. - // Since our generators are ranges defined by the - // collection of upper bounds we can find their - // length by finding the difference between - // adjacent elements of the collection. - // - // Some more care must be taken to ensure tha the - // correct amount is produced. - // Since we need our ranges to be disjoint, we - // apply a small offset from the element of the - // upper bounds that is used as a lower bound, - // since that upper bound is inclusive for the - // range that precedes the one we are making the - // calculation for. - // - // Furthermore, the first range is treated - // specially. - // As no range precedes it, it doesn't need any - // offset to be applied. - // Additionally, we implicitly use 0 as the first - // lower bound, such that the length of the first - // range is indeed equal to its upper bound. - // - // To account for this, we remove that offset from - // the total amount for each range after the first - // one and use the first upper bound as a seeding - // value to account for the length of the first - // range. - std::vector generators_cardinality(generators_amount, generators_bounds[0]); - - std::adjacent_difference(generators_bounds.begin(), generators_bounds.end(), generators_bounds.begin()); - std::transform(std::next(generators_cardinality.begin()), generators_cardinality.end(), std::next(generators_cardinality.begin()), [](auto element){ return element - 1; }); - - std::size_t output_cardinality{std::accumulate(generators_cardinality.begin(), generators_cardinality.end(), std::size_t{0})}; - - auto choosing_generator = uniformly_valued_oneof(std::move(generators), std::move(generators_cardinality)); - - WHEN("A certain amount of values are generated from the choosing generator") { - auto values = GENERATE_REF(take(1, chunk(10000, std::move(choosing_generator)))); - - THEN("The distribution of elements approximates uniformity for each value") { - double probability{uniform_probability(output_cardinality)}; - - auto maybe_distribution_error{respects_distribution( - std::move(values), - [](auto value){ return value; }, - [&probability](auto _){ (void)(_); return probability; } - )}; - - REQUIRE_FALSE(maybe_distribution_error); - } - } - } - } -} - -TEST_CASE("A generator with a weight of zero is never chosen when choosing between many generators", "[OneOf][Combinators][SpecialCase]") { - auto excluded_value = GENERATE(take(100, random(0, 10000))); - - std::vector> generators; - generators.reserve(2); - generators.emplace_back(Catch::Generators::random(excluded_value + 1, std::numeric_limits::max())); - generators.emplace_back(Catch::Generators::value(copy_value(excluded_value))); - - auto generated_value = GENERATE_REF(take(100, oneof(std::move(generators), std::vector{100.0, 0.0}))); - - REQUIRE(generated_value != excluded_value); -} - -TEST_CASE("The first element of the passed in generators are not lost", "[OneOf][Combinators][GeneratorFirstElement][SpecialCase]") { - // REMARK: We want to test that, for each generator, the first - // time it is chosen the first value is produced. - // This is complicated because of the fact that OneOf chooses - // random generators in a random order. - // This means that some generators may never be chosen, never be - // chosen more than once and so on. - // Furthermore, this specific test is particularly important only - // for finite generators or non-completely random, ordered, - // infinite generators. - // Additionally, we need to ensure that we test with multiple - // generators, as this test is a consequence of a first bugged - // implementation where only the first chosen generator respected - // the first value, which would pass a test where a single - // generator is used. - // - // This is non-trivial due to the randomized nature of OneOf. - // It can be simplified if we express it in a non-deterministic - // way and mark it as mayfail, where we can recognize with a good - // certainty that the test is actually passing. - // - // To avoid having this flaky test, we approach it as follows: - // - // We provide some amount of infinite generators. Those generators - // are ensured to produce one specific value as their first value - // and then infinitely produce a different value. - // We ensure that each generator that is provided produces unique - // values, that is, no two generators produce a first value or 1 < - // nth value that is equal to the one produced by another - // generator. - // - // Then we pass those generators to oneof and generate enough - // values such that at least one of the generators must have been - // chosen twice or more, at random. - // - // We count the appearances of each value in the produced set. - // Then, if a value that is generated by the 1 < nth choice of a - // specific generator is encountered, we check that the first - // value that the specific generator would produce is in the set - // of values that were generated. - // That is, if a generator has produced his non-first value, it - // must have been chosen twice or more. - // This in turn implies that the first time that the generator was - // chosen, its first value was actually produced. - - struct IncreaseAfterFirst { - std::size_t increase; - bool first_application = true; - - std::size_t operator()(std::size_t value) { - if (first_application) { - first_application = false; - return value; - } - - return value + increase; - } - }; - - std::size_t maximum_generator_amount{100}; - auto generators_amount = GENERATE_COPY(take(10, random(std::size_t{1}, maximum_generator_amount))); - - std::vector> generators; - generators.reserve(generators_amount); - - for (std::size_t index{0}; index < generators_amount; ++index) { - generators.push_back(Catch::Generators::map(IncreaseAfterFirst{maximum_generator_amount}, cycle(Catch::Generators::value(copy_value(index))))); - } - - auto values = GENERATE_REF(take(1, chunk(generators_amount + 1, uniform_oneof(std::move(generators))))); - auto histogram{make_histogram(values.begin(), values.end(), [](auto e){ return e; })}; - - for (std::size_t index{0}; index < generators_amount; ++index) { - std::size_t second_value{index + maximum_generator_amount}; - histogram.try_emplace(second_value, 0); - - if (histogram[second_value] > 0) { - REQUIRE(histogram.find(index) != histogram.end()); - } - } -} diff --git a/tests/auto/qdoc/catch_generators/tests/main.cpp b/tests/auto/qdoc/catch_generators/tests/main.cpp deleted file mode 100644 index cec18afba..000000000 --- a/tests/auto/qdoc/catch_generators/tests/main.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#define CATCH_CONFIG_RUNNER -#include - -// A custom main was provided to avoid linking errors when using minGW -// that were appearing in CI. -// See https://github.com/catchorg/Catch2/issues/1287 -int main(int argc, char* argv[]) { - return Catch::Session().run(argc, argv); -} diff --git a/tests/auto/qdoc/catch_generators/tests/utilities/semantics/catch_generator_handler.cpp b/tests/auto/qdoc/catch_generators/tests/utilities/semantics/catch_generator_handler.cpp deleted file mode 100644 index 913646be5..000000000 --- a/tests/auto/qdoc/catch_generators/tests/utilities/semantics/catch_generator_handler.cpp +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (C) 2022 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#include - -#include "namespaces.h" -#include "utilities/semantics/generator_handler.h" - -using namespace QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE; - -TEST_CASE( - "Calling next 0 < n times and then calling get on a GeneratorHandler wrapping a generator behaves the same as only calling next (n-1) times and then get on the generator that is wrapped", - "[GeneratorHandler][Utilities][Semantics][Generators]" -) { - auto n = GENERATE(take(100, random(1, 100))); - auto generator_values = GENERATE_COPY(take(1, chunk(n, random(0, 100000)))); - - auto generator_handler = handler(Catch::Generators::from_range(generator_values.begin(), generator_values.end())); - auto generator{Catch::Generators::from_range(generator_values.begin(), generator_values.end())}; - - generator_handler.next(); - for (int times{1}; times < n; ++times) { - generator_handler.next(); - generator.next(); - } - - REQUIRE(generator_handler.get() == generator.get()); -} diff --git a/tests/auto/qdoc/qdoc/CMakeLists.txt b/tests/auto/qdoc/qdoc/CMakeLists.txt index 925290ecd..7e145ab30 100644 --- a/tests/auto/qdoc/qdoc/CMakeLists.txt +++ b/tests/auto/qdoc/qdoc/CMakeLists.txt @@ -15,8 +15,8 @@ qt_internal_add_test(tst_QDoc ${QDOC_SOURCE_DIRECTORY}/filesystem/fileresolver.cpp INCLUDE_DIRECTORIES ${QDOC_INCLUDE_DIRECTORY} - ../catch_generators/ LIBRARIES Qt::QDocCatchPrivate Qt::QDocCatchConversionsPrivate + Qt::QDocCatchGeneratorsPrivate ) diff --git a/tests/auto/qdoc/qdoc/boundaries/filesystem/catch_directorypath.cpp b/tests/auto/qdoc/qdoc/boundaries/filesystem/catch_directorypath.cpp index 5321e2619..31a45bfa4 100644 --- a/tests/auto/qdoc/qdoc/boundaries/filesystem/catch_directorypath.cpp +++ b/tests/auto/qdoc/qdoc/boundaries/filesystem/catch_directorypath.cpp @@ -7,7 +7,7 @@ #include -#include +#include #include #include diff --git a/tests/auto/qdoc/qdoc/boundaries/filesystem/catch_filepath.cpp b/tests/auto/qdoc/qdoc/boundaries/filesystem/catch_filepath.cpp index 46481e4d6..957a13127 100644 --- a/tests/auto/qdoc/qdoc/boundaries/filesystem/catch_filepath.cpp +++ b/tests/auto/qdoc/qdoc/boundaries/filesystem/catch_filepath.cpp @@ -7,7 +7,7 @@ #include -#include +#include #include #include diff --git a/tests/auto/qdoc/qdoc/filesystem/catch_fileresolver.cpp b/tests/auto/qdoc/qdoc/filesystem/catch_fileresolver.cpp index fca33debb..75774c9cc 100644 --- a/tests/auto/qdoc/qdoc/filesystem/catch_fileresolver.cpp +++ b/tests/auto/qdoc/qdoc/filesystem/catch_fileresolver.cpp @@ -7,7 +7,7 @@ #include -#include +#include #include #include -- cgit v1.2.1