File indexing completed on 2026-10-29 08:25:01
0001
0002
0003
0004
0005
0006 #include <algorithms/property.h>
0007
0008 #include <catch2/catch_test_macros.hpp>
0009 #include <catch2/matchers/catch_matchers_string.hpp>
0010
0011 #include <fmt/format.h>
0012 #include <fmt/ranges.h>
0013
0014 #include <cstdint>
0015 #include <ostream>
0016 #include <sstream>
0017 #include <stdexcept>
0018 #include <string>
0019 #include <variant>
0020 #include <vector>
0021
0022 using namespace algorithms;
0023
0024 namespace {
0025
0026 enum class Mode { kFast, kSlow };
0027
0028 class Config : public PropertyMixin {
0029 public:
0030 Property<bool> m_bool{this, "bool", true, "a bool"};
0031 Property<int> m_int{this, "int", 3, "an int"};
0032 Property<unsigned> m_uint{this, "uint", 4u, "an unsigned int"};
0033 Property<float> m_float{this, "float", 1.5f, "a float"};
0034 Property<double> m_double{this, "double", "a double without default"};
0035 Property<std::string> m_string{this, "string", "text", "a string"};
0036 Property<Mode> m_mode{this, "mode", Mode::kFast, "an enum"};
0037 Property<std::vector<int>> m_ints{this, "ints", {1, 2, 3}, "a vector of ints"};
0038 Property<std::vector<std::string>> m_strings{this, "strings", {"a", "b"}, "a vector of strings"};
0039 };
0040
0041 }
0042
0043 TEST_CASE("Properties register themselves with their owner", "[property]") {
0044 const Config c;
0045 const auto& props = c.getProperties();
0046
0047 CHECK(props.size() == 9);
0048 CHECK(props.at("int").name() == "int");
0049 CHECK(props.at("int").description() == "an int");
0050 CHECK(props.at("double").description() == "a double without default");
0051 }
0052
0053 TEST_CASE("Properties without default are missing until set", "[property]") {
0054 Config c;
0055
0056 CHECK(c.hasProperty("int"));
0057 CHECK_FALSE(c.hasProperty("double"));
0058 CHECK_FALSE(c.hasProperty("unknown"));
0059 CHECK(c.missingProperties() == std::vector<std::string_view>{"double"});
0060 CHECK_THROWS_AS(c.validate(), PropertyError);
0061 CHECK_THROWS_WITH(c.validate(), R"(Missing properties: ["double"])");
0062 try {
0063 c.validate();
0064 } catch (const PropertyError& e) {
0065 CHECK(std::string{e.type()} == "algorithms::PropertyError");
0066 }
0067
0068 c.setProperty("double", 2.5);
0069 CHECK(c.hasProperty("double"));
0070 CHECK(c.missingProperties().empty());
0071 CHECK_NOTHROW(c.validate());
0072 }
0073
0074 TEST_CASE("Properties are set and retrieved through their upcast type", "[property]") {
0075 Config c;
0076
0077 c.setProperty("bool", false);
0078 c.setProperty("int", short{5});
0079 c.setProperty("uint", uint8_t{6});
0080 c.setProperty("float", 2.5f);
0081 c.setProperty("double", 3.5);
0082 c.setProperty("string", "other");
0083 c.setProperty("mode", Mode::kSlow);
0084 c.setProperty("ints", std::vector<int>{4, 5});
0085 c.setProperty("strings", std::vector<std::string>{"c"});
0086
0087 CHECK(c.getProperty<bool>("bool") == false);
0088 CHECK(c.getProperty<int64_t>("int") == 5);
0089 CHECK(c.getProperty<uint64_t>("uint") == 6);
0090 CHECK(c.getProperty<double>("float") == 2.5);
0091 CHECK(c.getProperty<double>("double") == 3.5);
0092 CHECK(c.getProperty<std::string>("string") == "other");
0093 CHECK(c.getProperty<int64_t>("mode") == static_cast<int64_t>(Mode::kSlow));
0094 CHECK(c.getProperty<std::vector<int>>("ints") == std::vector<int>{4, 5});
0095 CHECK(c.getProperty<std::vector<std::string>>("strings") == std::vector<std::string>{"c"});
0096
0097 CHECK(c.m_bool == false);
0098 CHECK(c.m_int == 5);
0099 CHECK(c.m_uint == 6u);
0100 CHECK(c.m_float == 2.5f);
0101 CHECK(c.m_double == 3.5);
0102 CHECK(c.m_string == "other");
0103 CHECK(c.m_mode == Mode::kSlow);
0104 }
0105
0106 TEST_CASE("Properties reject unknown names and wrong types", "[property]") {
0107 Config c;
0108
0109 CHECK_THROWS_AS(c.setProperty("unknown", 1), std::out_of_range);
0110 CHECK_THROWS_AS(c.getProperty<int64_t>("unknown"), std::out_of_range);
0111 CHECK_THROWS_AS(c.setProperty("int", 1.5), std::bad_variant_access);
0112 CHECK_THROWS_AS(c.getProperty<int32_t>("int"), std::bad_variant_access);
0113 }
0114
0115 TEST_CASE("Properties require a unique name and a valid owner", "[property]") {
0116 class Duplicate : public PropertyMixin {
0117 Property<int> m_a{this, "same", 1, ""};
0118 Property<int> m_b{this, "same", 2, ""};
0119 };
0120 CHECK_THROWS_WITH(Duplicate{}, "Duplicate property name: same");
0121 CHECK_THROWS_WITH((Configurable::Property<int>{nullptr, "orphan", "no owner"}),
0122 "Attempting to create Property 'orphan' without valid owner");
0123 }
0124
0125 TEST_CASE("Properties act as a const value", "[property]") {
0126 const Config c;
0127
0128 const int i = c.m_int;
0129 CHECK(i == 3);
0130 CHECK(c.m_int.value() == 3);
0131
0132 CHECK(c.m_int == 3);
0133 CHECK(3 == c.m_int);
0134 CHECK(c.m_int != 4);
0135 CHECK(c.m_int < 4);
0136 CHECK(c.m_int <= 3);
0137 CHECK(c.m_int > 2);
0138 CHECK(c.m_int >= 3);
0139 CHECK(c.m_int + 1 == 4);
0140 CHECK(c.m_int - 1 == 2);
0141 CHECK(c.m_int * 2 == 6);
0142 CHECK(c.m_int / 3 == 1);
0143 CHECK(c.m_float * 2.f == 3.f);
0144 }
0145
0146 TEST_CASE("Properties can be printed and formatted", "[property]") {
0147 const Config c;
0148
0149 std::ostringstream os;
0150 os << c.m_int << " " << c.m_string;
0151 CHECK(os.str() == "3 text");
0152
0153 CHECK(fmt::format("{} {} {}", c.m_int, c.m_string, c.m_float) == "3 text 1.5");
0154 CHECK(fmt::format("{:>4}", c.m_int) == " 3");
0155 CHECK(fmt::format("{}", c.m_ints) == "[1, 2, 3]");
0156 CHECK(fmt::format("{}", c.m_strings) == R"(["a", "b"])");
0157 }
0158
0159 namespace {
0160 template <class P>
0161 concept Collection = requires(const P& p) {
0162 p.begin();
0163 p.end();
0164 p.size();
0165 p.empty();
0166 };
0167 template <class P>
0168 concept Streamable = requires(std::ostream& os, const P& p) { os << p; };
0169 template <class T> using Prop = Configurable::Property<T>;
0170 }
0171
0172 TEST_CASE("Properties only provide collection helpers for collections", "[property]") {
0173 STATIC_REQUIRE_FALSE(Collection<Prop<int>>);
0174 STATIC_REQUIRE_FALSE(Collection<Prop<double>>);
0175 STATIC_REQUIRE(Collection<Prop<std::string>>);
0176 STATIC_REQUIRE(Collection<Prop<std::vector<int>>>);
0177
0178 STATIC_REQUIRE_FALSE(fmt::is_range<Prop<std::vector<int>>, char>::value);
0179
0180 STATIC_REQUIRE(Streamable<Prop<int>>);
0181 STATIC_REQUIRE_FALSE(Streamable<Prop<Mode>>);
0182 }
0183
0184 TEST_CASE("Properties of containers expose container access", "[property]") {
0185 Config c;
0186 const Config& cc = c;
0187
0188 CHECK(cc.m_ints.size() == 3);
0189 CHECK_FALSE(cc.m_ints.empty());
0190 CHECK(cc.m_ints[1] == 2);
0191 CHECK(c.m_ints[2] == 3);
0192 CHECK(cc.m_string.length() == 4);
0193 CHECK(cc.m_string.find("xt") == 2);
0194 CHECK(cc.m_string[0] == 't');
0195 CHECK(std::vector<int>(cc.m_ints.begin(), cc.m_ints.end()) == std::vector<int>{1, 2, 3});
0196 CHECK(std::vector<int>(c.m_ints.begin(), c.m_ints.end()) == std::vector<int>{1, 2, 3});
0197
0198 int sum = 0;
0199 for (int v : cc.m_ints) {
0200 sum += v;
0201 }
0202 CHECK(sum == 6);
0203 }
0204
0205 TEST_CASE("Properties can be copied", "[property]") {
0206 const Config c;
0207 const auto copy{c.m_strings};
0208 CHECK(copy.value() == std::vector<std::string>{"a", "b"});
0209 CHECK(copy.name() == "strings");
0210 CHECK(copy.hasValue());
0211 }
0212
0213 TEST_CASE("Properties are accessible through PropertyBase", "[property]") {
0214 Config c;
0215 auto& base = c.getProperties().at("uint");
0216
0217 base.set(uint64_t{9});
0218 CHECK(c.m_uint == 9u);
0219 CHECK(std::get<uint64_t>(base.get()) == 9);
0220 }
0221
0222 TEST_CASE("Properties can be set from lvalues", "[property]") {
0223 Config c;
0224 const int i = 7;
0225 float f = 0.5f;
0226 std::string text{"lvalue"};
0227 const char* chars = "chars";
0228
0229 c.setProperty("int", i);
0230 c.setProperty("float", f);
0231 c.setProperty("string", text);
0232 CHECK(c.m_int == 7);
0233 CHECK(c.m_float == 0.5f);
0234 CHECK(c.m_string == "lvalue");
0235
0236 c.setProperty("string", chars);
0237 CHECK(c.m_string == "chars");
0238 }