summaryrefslogtreecommitdiff
path: root/src/qdoc
diff options
context:
space:
mode:
Diffstat (limited to 'src/qdoc')
-rw-r--r--src/qdoc/CMakeLists.txt1
-rw-r--r--src/qdoc/catch_generators/CMakeLists.txt14
-rw-r--r--src/qdoc/catch_generators/src/catch_generators/generators/combinators/cycle_generator.h80
-rw-r--r--src/qdoc/catch_generators/src/catch_generators/generators/combinators/oneof_generator.h185
-rw-r--r--src/qdoc/catch_generators/src/catch_generators/generators/k_partition_of_r_generator.h113
-rw-r--r--src/qdoc/catch_generators/src/catch_generators/generators/path_generator.h853
-rw-r--r--src/qdoc/catch_generators/src/catch_generators/generators/qchar_generator.h110
-rw-r--r--src/qdoc/catch_generators/src/catch_generators/generators/qstring_generator.h92
-rw-r--r--src/qdoc/catch_generators/src/catch_generators/namespaces.h14
-rw-r--r--src/qdoc/catch_generators/src/catch_generators/utilities/semantics/copy_value.h26
-rw-r--r--src/qdoc/catch_generators/src/catch_generators/utilities/semantics/generator_handler.h97
-rw-r--r--src/qdoc/catch_generators/src/catch_generators/utilities/semantics/move_into_vector.h62
-rw-r--r--src/qdoc/catch_generators/src/catch_generators/utilities/statistics/distribution.h158
-rw-r--r--src/qdoc/catch_generators/src/catch_generators/utilities/statistics/percentages.h49
-rw-r--r--src/qdoc/catch_generators/tests/CMakeLists.txt20
-rw-r--r--src/qdoc/catch_generators/tests/generators/catch_k_partition_of_r_generator.cpp41
-rw-r--r--src/qdoc/catch_generators/tests/generators/catch_path_generator.cpp755
-rw-r--r--src/qdoc/catch_generators/tests/generators/catch_qchar_generator.cpp102
-rw-r--r--src/qdoc/catch_generators/tests/generators/catch_qstring_generator.cpp89
-rw-r--r--src/qdoc/catch_generators/tests/generators/combinators/catch_cycle_generator.cpp70
-rw-r--r--src/qdoc/catch_generators/tests/generators/combinators/catch_oneof_generator.cpp362
-rw-r--r--src/qdoc/catch_generators/tests/main.cpp13
-rw-r--r--src/qdoc/catch_generators/tests/utilities/semantics/catch_generator_handler.cpp28
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());
+}