diff options
Diffstat (limited to 'src/qdoc/catch_generators/tests')
9 files changed, 1480 insertions, 0 deletions
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()); +} |