diff options
Diffstat (limited to 'src/qdoc')
23 files changed, 3334 insertions, 0 deletions
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 + $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src> + $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/QtQDocCatchGeneratorsPrivate> +) + +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 <catch/catch.hpp> + +#include <vector> + +namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE { + namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { + + template<typename T> + class CycleGenerator : public Catch::Generators::IGenerator<T> { + public: + CycleGenerator(Catch::Generators::GeneratorWrapper<T>&& 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<T> generator; + + std::vector<T> 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<typename T> + inline Catch::Generators::GeneratorWrapper<T> cycle(Catch::Generators::GeneratorWrapper<T>&& generator) { + return Catch::Generators::GeneratorWrapper<T>(std::unique_ptr<Catch::Generators::IGenerator<T>>(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 <catch/catch.hpp> + +#include <vector> +#include <random> +#include <algorithm> +#include <numeric> + +namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE { + namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { + + template<typename T> + class OneOfGenerator : public Catch::Generators::IGenerator<T> { + public: + OneOfGenerator( + std::vector<Catch::Generators::GeneratorWrapper<T>>&& generators, + const std::vector<double>& 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<void>(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<Catch::Generators::GeneratorWrapper<T>> generators; + + std::mt19937 random_engine; + std::discrete_distribution<std::size_t> 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<typename T> + inline Catch::Generators::GeneratorWrapper<T> oneof( + std::vector<Catch::Generators::GeneratorWrapper<T>>&& generators, + const std::vector<double>& weights + ) { + return Catch::Generators::GeneratorWrapper<T>(std::unique_ptr<Catch::Generators::IGenerator<T>>(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<typename T> + inline Catch::Generators::GeneratorWrapper<T> uniform_oneof( + std::vector<Catch::Generators::GeneratorWrapper<T>>&& generators + ) { + std::vector<double> 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<typename T> + inline Catch::Generators::GeneratorWrapper<T> uniformly_valued_oneof( + std::vector<Catch::Generators::GeneratorWrapper<T>>&& generators, + const std::vector<std::size_t>& amounts + ) { + std::size_t total_amount{std::accumulate(amounts.cbegin(), amounts.cend(), std::size_t{0})}; + + std::vector<double> 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<double>(element), static_cast<double>(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 <catch/catch.hpp> + +#include <random> +#include <numeric> +#include <algorithm> + +namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE { + namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { + + class KPartitionOfRGenerator : public Catch::Generators::IGenerator<std::vector<double>> { + 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<void>(next()); + } + + std::vector<double> 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<double> interval_distribution; + + std::size_t k; + double r; + + std::vector<double> 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<std::vector<double>> k_partition_of_r(double r, std::size_t k) { + return Catch::Generators::GeneratorWrapper<std::vector<double>>(std::unique_ptr<Catch::Generators::IGenerator<std::vector<double>>>(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 <catch/catch.hpp> + +#include <random> + +#include <QChar> +#include <QString> +#include <QStringList> +#include <QRegularExpression> + +#if defined(Q_OS_WINDOWS) + + #include <QStorageInfo> + +#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<QString> { + public: + PathGenerator( + Catch::Generators::GeneratorWrapper<QString>&& device_component_generator, + Catch::Generators::GeneratorWrapper<QString>&& root_component_generator, + Catch::Generators::GeneratorWrapper<QString>&& directory_component_generator, + Catch::Generators::GeneratorWrapper<QString>&& filename_component_generator, + Catch::Generators::GeneratorWrapper<QString>&& 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<QString> device_component_generator; + Catch::Generators::GeneratorWrapper<QString> root_component_generator; + Catch::Generators::GeneratorWrapper<QString> directory_component_generator; + Catch::Generators::GeneratorWrapper<QString> filename_component_generator; + Catch::Generators::GeneratorWrapper<QString> separator_component_generator; + + std::mt19937 random_engine; + std::uniform_int_distribution<std::size_t> 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<QString> path( + Catch::Generators::GeneratorWrapper<QString>&& device_generator, + Catch::Generators::GeneratorWrapper<QString>&& root_component_generator, + Catch::Generators::GeneratorWrapper<QString>&& directory_generator, + Catch::Generators::GeneratorWrapper<QString>&& filename_generator, + Catch::Generators::GeneratorWrapper<QString>&& separator_generator, + PathGeneratorConfiguration configuration = PathGeneratorConfiguration{} + ) { + return Catch::Generators::GeneratorWrapper<QString>( + std::unique_ptr<Catch::Generators::IGenerator<QString>>( + 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<QString> 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<QString> 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<QString> 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<QString> 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<QString> 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<QString> 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<QString> 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<QString> 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<QString> 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<QString> 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<QString> 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<QString> 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 <catch/catch.hpp> + +#include <random> + +#include <QChar> + +namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE { + namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { + + class QCharGenerator : public Catch::Generators::IGenerator<QChar> { + public: + QCharGenerator( + char16_t lower_bound = std::numeric_limits<char16_t>::min(), + char16_t upper_bound = std::numeric_limits<char16_t>::max() + ) : random_engine{std::random_device{}()}, + distribution{static_cast<unsigned int>(lower_bound), static_cast<unsigned int>(upper_bound)} + { + assert(lower_bound <= upper_bound); + static_cast<void>(next()); + } + + QChar const& get() const override { return current_character; } + + bool next() override { + current_character = QChar(static_cast<char16_t>(distribution(random_engine))); + + return true; + } + + private: + QChar current_character; + + std::mt19937 random_engine; + std::uniform_int_distribution<unsigned int> 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<QChar> character(char16_t lower_bound = std::numeric_limits<char16_t>::min(), char16_t upper_bound = std::numeric_limits<char16_t>::max()) { + return Catch::Generators::GeneratorWrapper<QChar>(std::unique_ptr<Catch::Generators::IGenerator<QChar>>(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<Alphabets alphabet> + struct sizeof_alphabet; + + template<Alphabets alphabet> + inline constexpr std::size_t sizeof_alphabet_v = sizeof_alphabet<alphabet>::value; + + template <> struct sizeof_alphabet<Alphabets::digit> { static constexpr std::size_t value{'9' - '0'}; }; + template <> struct sizeof_alphabet<Alphabets::ascii_lowercase> { static constexpr std::size_t value{'z' - 'a'}; }; + template<> struct sizeof_alphabet<Alphabets::ascii_uppercase> { static constexpr std::size_t value{'Z' - 'A'}; }; + template<> struct sizeof_alphabet<Alphabets::ascii_alpha> { static constexpr std::size_t value{sizeof_alphabet_v<Alphabets::ascii_lowercase> + sizeof_alphabet_v<Alphabets::ascii_uppercase>}; }; + template<> struct sizeof_alphabet<Alphabets::ascii_alphanumeric>{ static constexpr std::size_t value{sizeof_alphabet_v<Alphabets::ascii_alpha> + sizeof_alphabet_v<Alphabets::digit>}; }; + + } // end QDOC_CATCH_GENERATORS_TRAITS_NAMESPACE + + + inline Catch::Generators::GeneratorWrapper<QChar> digit() { + return Catch::Generators::GeneratorWrapper<QChar>(std::unique_ptr<Catch::Generators::IGenerator<QChar>>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::QCharGenerator('0', '9'))); + } + + inline Catch::Generators::GeneratorWrapper<QChar> ascii_lowercase() { + return Catch::Generators::GeneratorWrapper<QChar>(std::unique_ptr<Catch::Generators::IGenerator<QChar>>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::QCharGenerator('a', 'z'))); + } + + inline Catch::Generators::GeneratorWrapper<QChar> ascii_uppercase() { + return Catch::Generators::GeneratorWrapper<QChar>(std::unique_ptr<Catch::Generators::IGenerator<QChar>>(new QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE::QCharGenerator('A', 'Z'))); + } + + inline Catch::Generators::GeneratorWrapper<QChar> ascii_alpha() { + return uniform_oneof(QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE::move_into_vector(ascii_lowercase(), ascii_uppercase())); + } + + inline Catch::Generators::GeneratorWrapper<QChar> 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::Alphabets::ascii_alpha> , traits::sizeof_alphabet_v<traits::Alphabets::digit>}); + } + + inline Catch::Generators::GeneratorWrapper<QChar> 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<traits::Alphabets::ascii_alphanumeric>, 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 <catch/catch.hpp> + +#include <random> + +#include <QString> + +namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE { + namespace QDOC_CATCH_GENERATORS_PRIVATE_NAMESPACE { + + class QStringGenerator : public Catch::Generators::IGenerator<QString> { + public: + QStringGenerator(Catch::Generators::GeneratorWrapper<QChar>&& 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<QChar> character_generator; + + std::mt19937 random_engine; + std::uniform_int_distribution<qsizetype> 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<QString> string(Catch::Generators::GeneratorWrapper<QChar>&& character_generator, qsizetype minimum_length, qsizetype maximum_length) { + return Catch::Generators::GeneratorWrapper<QString>(std::unique_ptr<Catch::Generators::IGenerator<QString>>(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<QString> empty_string() { + return Catch::Generators::GeneratorWrapper<QString>(std::unique_ptr<Catch::Generators::IGenerator<QString>>(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 <type_traits> + +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<typename T> + std::remove_reference_t<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 <catch/catch.hpp> + +#include <optional> +#include <cassert> + +namespace QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE { + + template<typename T> + class GeneratorHandler : public Catch::Generators::IGenerator<T> { + public: + + GeneratorHandler(Catch::Generators::GeneratorWrapper<T>&& 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<T> 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<typename T> + inline Catch::Generators::GeneratorWrapper<T> handler(Catch::Generators::GeneratorWrapper<T>&& generator) { + return Catch::Generators::GeneratorWrapper<T>(std::unique_ptr<Catch::Generators::IGenerator<T>>(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 <vector> +#include <tuple> + +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<typename... Args> + using first_from_pack_t = std::tuple_element_t<0, std::tuple<Args...>>; + + } // 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<T>, where T is a move only type, as an expression, + * to lighten the idiom. + * + * For example, Catch's GeneratorWrapper<T> 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<typename... MoveOnlyTypes> + inline auto move_into_vector(MoveOnlyTypes... movables) { + std::vector<QDOC_CATCH_GENERATORS_TRAITS_NAMESPACE::first_from_pack_t<MoveOnlyTypes...>> + 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 <functional> +#include <optional> +#include <ostream> +#include <unordered_map> + +namespace QDOC_CATCH_GENERATORS_UTILITIES_ABSOLUTE_NAMESPACE { + + template<typename T> + using Histogram = std::unordered_map<T, std::size_t>; + + template<typename InputIt, typename GroupBy> + auto make_histogram(InputIt begin, InputIt end, GroupBy&& group_by) { + Histogram<std::invoke_result_t<GroupBy, decltype(*begin)>> histogram{}; + + while (begin != end) { + auto key{std::invoke(std::forward<GroupBy>(group_by), *begin)}; + + histogram.try_emplace(key, 0); + histogram[key] += 1; + ++begin; + } + + return histogram; + } + + template<typename T> + struct DistributionError { + T value; + double probability; + double expected_probability; + }; + + template<typename T> + inline std::ostream& operator<<(std::ostream& os, const DistributionError<T>& 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<typename T, typename GroupBy, typename ProbabilityOf> + std::optional<DistributionError<T>> respects_distribution(std::vector<T>&& 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<GroupBy>(group_by))}; + + for (auto& bin : histogram) { + auto [key, count] = bin; + + double actual_percentage{percent_of(static_cast<double>(count), static_cast<double>(data_point_amount))}; + double expected_percentage{std::invoke(std::forward<ProbabilityOf>(probability_of), key)}; + + if (!(actual_percentage == Approx(expected_percentage).margin(scaled_margin))) + return std::make_optional(DistributionError<T>{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 <cassert> + +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<double>(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 <catch_generators/namespaces.h> +#include <catch_generators/generators/k_partition_of_r_generator.h> + +#include <catch/catch.hpp> + +#include <numeric> + +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 <catch_generators/namespaces.h> +#include <catch_generators/generators/qchar_generator.h> +#include <catch_generators/generators/qstring_generator.h> +#include <catch_generators/generators/path_generator.h> +#include <catch_generators/generators/combinators/cycle_generator.h> +#include <catch_generators/utilities/statistics/percentages.h> +#include <catch_generators/utilities/statistics/distribution.h> +#include <catch_generators/utilities/semantics/copy_value.h> + +#include <catch_conversions/qt_catch_conversions.h> + +#include <catch/catch.hpp> + +#include <QString> +#include <QStringList> +#include <QRegularExpression> + +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<std::size_t>(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<std::size_t>(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 <catch_generators/namespaces.h> +#include <catch_generators/generators/qchar_generator.h> + +#include <catch_conversions/qt_catch_conversions.h> + +#include <catch/catch.hpp> + +#include <QChar> + +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<unsigned int>(std::numeric_limits<char16_t>::min()), + static_cast<unsigned int>(std::numeric_limits<char16_t>::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<unsigned int>(std::numeric_limits<char16_t>::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<unsigned int>(std::numeric_limits<char16_t>::min()), + static_cast<unsigned int>(std::numeric_limits<char16_t>::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 <catch_generators/namespaces.h> +#include <catch_generators/generators/qchar_generator.h> +#include <catch_generators/generators/qstring_generator.h> + +#include <catch_conversions/qt_catch_conversions.h> + +#include <catch/catch.hpp> + +using namespace QDOC_CATCH_GENERATORS_ROOT_NAMESPACE; + +#include <algorithm> + +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<unsigned int>(std::numeric_limits<char16_t>::min()), + static_cast<unsigned int>(std::numeric_limits<char16_t>::max()) + ))); + auto upper_character_bound = GENERATE_COPY(take(10, random(lower_character_bound, static_cast<unsigned int>(std::numeric_limits<char16_t>::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 <catch_generators/namespaces.h> +#include <catch_generators/generators/combinators/cycle_generator.h> + +#include <catch/catch.hpp> + +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 <catch_conversions/std_catch_conversions.h> + +#include <catch_generators/namespaces.h> +#include <catch_generators/generators/k_partition_of_r_generator.h> +#include <catch_generators/generators/combinators/oneof_generator.h> +#include <catch_generators/generators/combinators/cycle_generator.h> +#include <catch_generators/utilities/statistics/percentages.h> +#include <catch_generators/utilities/statistics/distribution.h> +#include <catch_generators/utilities/semantics/copy_value.h> + +#include <catch/catch.hpp> + +#include <cmath> +#include <iterator> +#include <vector> +#include <algorithm> +#include <unordered_map> + +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<Catch::Generators::GeneratorWrapper<int>> 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<double> 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<std::size_t> generators_bounds(generators_amount, 0); + std::vector<Catch::Generators::GeneratorWrapper<std::size_t>> 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::size_t>(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<double> 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<std::size_t> 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<Catch::Generators::GeneratorWrapper<int>> generators; + generators.reserve(2); + generators.emplace_back(Catch::Generators::random(excluded_value + 1, std::numeric_limits<int>::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<Catch::Generators::GeneratorWrapper<std::size_t>> 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 <catch/catch.hpp> + +// 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 <catch/catch.hpp> + +#include <catch_generators/namespaces.h> +#include <catch_generators/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()); +} |