From 005c88c3cc0b66037e7068e7ee351c3a25ea83cb Mon Sep 17 00:00:00 2001 From: Ethan Chou Date: Thu, 2 Apr 2026 14:51:44 -0400 Subject: [PATCH 01/13] rough impl of @example for hegel --- include/hegel/core.h | 46 +++++----- include/hegel/hegel.h | 4 + include/hegel/internal.h | 6 ++ include/hegel/options.h | 10 +++ src/data.h | 3 + src/hegel.cpp | 57 ++++++++++++ src/internal.cpp | 15 ++++ src/json_impl.h | 1 + tests/CMakeLists.txt | 8 ++ tests/test_explicit_examples.cpp | 149 +++++++++++++++++++++++++++++++ 10 files changed, 277 insertions(+), 22 deletions(-) create mode 100644 tests/test_explicit_examples.cpp diff --git a/include/hegel/core.h b/include/hegel/core.h index 84ea8ea..37475ec 100644 --- a/include/hegel/core.h +++ b/include/hegel/core.h @@ -20,6 +20,29 @@ namespace hegel::internal { template hegel::internal::json::json type_schema() { return hegel::internal::json::json::parse(rfl::json::to_schema()); } + + /// Deserialize a json_raw_ref into a value of type T. + template + T json_value_to(const hegel::internal::json::json_raw_ref& result) { + if constexpr (std::is_same_v) { + return result.get_string(); + } else if constexpr (std::is_same_v, bool>) { + return result.get_bool(); + } else if constexpr (std::is_floating_point_v) { + return static_cast(result.get_double()); + } else if constexpr (std::is_unsigned_v) { + return static_cast(result.get_uint64_t()); + } else if constexpr (std::is_integral_v) { + return static_cast(result.get_int64_t()); + } else { + auto parse_result = read_nlohmann(result); + if (!parse_result.has_value()) { + throw std::runtime_error( + "Failed to parse value into requested type"); + } + return parse_result.value(); + } + } } // namespace hegel::internal /** @@ -346,32 +369,11 @@ namespace hegel::generators { hegel::internal::json::json response = internal::communicate_with_socket(schema_, data); - // Extract the result value if (!response.contains("result")) { throw std::runtime_error( "Server response missing 'result' field"); } - hegel::internal::json::json_raw_ref result = response["result"]; - - if constexpr (std::is_same_v) { - return result.get_string(); - } else if constexpr (std::is_same_v, bool>) { - return result.get_bool(); - } else if constexpr (std::is_floating_point_v) { - return static_cast(result.get_double()); - } else if constexpr (std::is_unsigned_v) { - return static_cast(result.get_uint64_t()); - } else if constexpr (std::is_integral_v) { - return static_cast(result.get_int64_t()); - } else { - auto parse_result = internal::read_nlohmann(result); - if (!parse_result.has_value()) { - throw std::runtime_error( - "Failed to parse server response into " - "requested type"); - } - return parse_result.value(); - } + return internal::json_value_to(response["result"]); } private: diff --git a/include/hegel/hegel.h b/include/hegel/hegel.h index 162912a..541b405 100644 --- a/include/hegel/hegel.h +++ b/include/hegel/hegel.h @@ -84,6 +84,10 @@ namespace hegel { throw std::runtime_error( "draw() cannot be called outside of a Hegel test"); } + if (internal::has_explicit_value(data)) { + return internal::json_value_to( + internal::pop_explicit_value(data)); + } return gen.do_draw(data); } diff --git a/include/hegel/internal.h b/include/hegel/internal.h index 27c13c3..cdf057a 100644 --- a/include/hegel/internal.h +++ b/include/hegel/internal.h @@ -72,5 +72,11 @@ namespace hegel::internal { /// @brief Get the current test case data, or nullptr if not in a test. impl::data::TestCaseData* get_test_case_data(); + /// @brief Check if the current test case has an explicit value to consume. + bool has_explicit_value(impl::data::TestCaseData* data); + + /// @brief Pop the next explicit value from the current test case. + json::json_raw_ref pop_explicit_value(impl::data::TestCaseData* data); + } // namespace hegel::internal /// @endcond diff --git a/include/hegel/options.h b/include/hegel/options.h index ded3534..85ce73f 100644 --- a/include/hegel/options.h +++ b/include/hegel/options.h @@ -3,6 +3,9 @@ #include #include #include +#include + +#include "json.h" /** @namespace hegel::options * @brief The HegelOptions struct for configuring your Hegel run, and supporting @@ -38,6 +41,9 @@ namespace hegel::options { } } + /// A single explicit example: one json value per draw() call, in order. + using Example = std::vector; + /** * @brief Configuration options for embedded mode execution. * @see hegel::hegel() @@ -53,5 +59,9 @@ namespace hegel::options { std::optional hegel_path; std::optional seed; + + /// Explicit examples to run before random generation. + /// Each Example is a vector of json values, one per draw() call. + std::vector examples; }; } // namespace hegel::options diff --git a/src/data.h b/src/data.h index 2922b76..8de29bb 100644 --- a/src/data.h +++ b/src/data.h @@ -1,7 +1,9 @@ #pragma once #include +#include #include +#include namespace hegel::impl { class Connection; @@ -18,6 +20,7 @@ namespace hegel::impl::data { bool is_last_run; bool test_aborted; options::Verbosity verbosity; + std::vector* explicit_values = nullptr; }; void set(TestCaseData* data); diff --git a/src/hegel.cpp b/src/hegel.cpp index dd9a38c..9206947 100644 --- a/src/hegel.cpp +++ b/src/hegel.cpp @@ -9,6 +9,7 @@ #include "json_impl.h" +#include #include #include #include @@ -215,11 +216,67 @@ namespace hegel { } } + // ============================================================================= + // Explicit Examples + // ============================================================================= + static void run_explicit_examples(const std::function& test_fn, + const options::HegelOptions& options) { + for (size_t i = 0; i < options.examples.size(); ++i) { + // Copy the example and reverse so pop_back() yields in order + std::vector values( + options.examples[i].begin(), options.examples[i].end()); + std::reverse(values.begin(), values.end()); + + impl::data::TestCaseData data{ + .connection = nullptr, + .data_channel = 0, + .is_last_run = true, + .test_aborted = false, + .verbosity = options.verbosity, + .explicit_values = &values, + }; + impl::data::set(&data); + + try { + test_fn(); + } catch (const internal::HegelReject&) { + impl::data::clear(); + throw std::runtime_error( + "assume() failed on explicit example " + + std::to_string(i) + + ": explicit examples must not be filtered"); + } catch (...) { + impl::data::clear(); + throw; + } + + impl::data::clear(); + + if (!values.empty()) { + throw std::runtime_error( + "Explicit example " + std::to_string(i) + " has " + + std::to_string(values.size()) + + " unconsumed value(s): too many values for the " + "number of draw() calls"); + } + } + } + // ============================================================================= // Entry Point // ============================================================================= void hegel(const std::function& test_fn, const options::HegelOptions& options) { + // Run explicit examples before fork (no server needed) + if (!options.examples.empty()) { + run_explicit_examples(test_fn, options); + } + + // If test_cases is explicitly 0, skip server-driven testing + if (options.test_cases.has_value() && options.test_cases.value() == 0) { + return; + } + // Create temp directory with socket path std::string temp_dir = "/tmp/hegel_" + std::to_string(getpid()); std::filesystem::create_directories(temp_dir); diff --git a/src/internal.cpp b/src/internal.cpp index c2e7709..25929b1 100644 --- a/src/internal.cpp +++ b/src/internal.cpp @@ -1,6 +1,8 @@ #include #include +#include "json_impl.h" + #include #include #include @@ -32,4 +34,17 @@ namespace hegel::internal { } [[noreturn]] void stop_test() { throw HegelReject(); } + + bool has_explicit_value(impl::data::TestCaseData* data) { + return data->explicit_values != nullptr && + !data->explicit_values->empty(); + } + + json::json_raw_ref pop_explicit_value(impl::data::TestCaseData* data) { + auto& val = data->explicit_values->back(); + json::json_raw_ref ref( + new json::json_ref_holder(json::ImplUtil::raw(val))); + data->explicit_values->pop_back(); + return ref; + } } // namespace hegel::internal diff --git a/src/json_impl.h b/src/json_impl.h index 2e9337d..c9d657f 100644 --- a/src/json_impl.h +++ b/src/json_impl.h @@ -24,6 +24,7 @@ namespace hegel::internal::json { static const nlohmann::json& raw(const json& json) { return json.impl->data; } + static nlohmann::json& raw(json& j) { return j.impl->data; } static json create(const nlohmann::json& from) { json result(nullptr); result.impl.reset(new json_holder(from)); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e535a6b..9460116 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -55,6 +55,14 @@ target_link_libraries(test_no_nlohmann_include ) gtest_discover_tests(test_no_nlohmann_include) +add_executable(test_explicit_examples test_explicit_examples.cpp) +target_link_libraries(test_explicit_examples + PRIVATE + hegel + GTest::gtest_main +) +gtest_discover_tests(test_explicit_examples) + add_executable(gtest_failure_test gtest_failure_test.cpp) target_link_libraries(gtest_failure_test PRIVATE diff --git a/tests/test_explicit_examples.cpp b/tests/test_explicit_examples.cpp new file mode 100644 index 0000000..a4d54ea --- /dev/null +++ b/tests/test_explicit_examples.cpp @@ -0,0 +1,149 @@ +#include + +#include + +using namespace hegel::generators; + +// ============================================================================= +// Basic explicit examples (examples-only, no server) +// ============================================================================= + +TEST(ExplicitExamples, SingleDrawSingleExample) { + bool ran = false; + hegel::hegel( + [&] { + auto x = hegel::draw(integers()); + EXPECT_EQ(x, 42); + ran = true; + }, + {.test_cases = 0, .examples = {{42}}}); + EXPECT_TRUE(ran); +} + +TEST(ExplicitExamples, MultipleDraws) { + bool ran = false; + hegel::hegel( + [&] { + auto x = hegel::draw(integers()); + auto s = hegel::draw(text()); + EXPECT_EQ(x, 7); + EXPECT_EQ(s, "hello"); + ran = true; + }, + {.test_cases = 0, .examples = {{7, "hello"}}}); + EXPECT_TRUE(ran); +} + +TEST(ExplicitExamples, MultipleExamples) { + int call_count = 0; + hegel::hegel( + [&] { + auto x = hegel::draw(integers()); + if (call_count == 0) { + EXPECT_EQ(x, 10); + } else { + EXPECT_EQ(x, 20); + } + call_count++; + }, + {.test_cases = 0, .examples = {{10}, {20}}}); + EXPECT_EQ(call_count, 2); +} + +TEST(ExplicitExamples, BooleanValues) { + bool ran = false; + hegel::hegel( + [&] { + auto b = hegel::draw(booleans()); + EXPECT_TRUE(b); + ran = true; + }, + {.test_cases = 0, .examples = {{true}}}); + EXPECT_TRUE(ran); +} + +TEST(ExplicitExamples, FloatingPointValues) { + bool ran = false; + hegel::hegel( + [&] { + auto f = hegel::draw(floats()); + EXPECT_DOUBLE_EQ(f, 3.14); + ran = true; + }, + {.test_cases = 0, .examples = {{3.14}}}); + EXPECT_TRUE(ran); +} + +// ============================================================================= +// Error conditions +// ============================================================================= + +TEST(ExplicitExamples, TooManyValuesThrows) { + EXPECT_THROW( + hegel::hegel( + [&] { + hegel::draw(integers()); + // Only one draw, but two values provided + }, + {.test_cases = 0, .examples = {{1, 2}}}), + std::runtime_error); +} + +TEST(ExplicitExamples, TooFewValuesUsesGenerator) { + // When explicit values run out, draw() falls through to the generator. + // With connection=nullptr, this will throw (no server). + EXPECT_THROW( + hegel::hegel( + [&] { + hegel::draw(integers()); + hegel::draw(integers()); // No explicit value left + }, + {.test_cases = 0, .examples = {{42}}}), + std::exception); +} + +TEST(ExplicitExamples, AssumeFailureIsError) { + EXPECT_THROW( + hegel::hegel( + [&] { + auto x = hegel::draw(integers()); + hegel::assume(x > 100); // 42 is not > 100 + }, + {.test_cases = 0, .examples = {{42}}}), + std::runtime_error); +} + +// ============================================================================= +// Examples with test_cases=0 means examples-only +// ============================================================================= + +TEST(ExplicitExamples, TestCasesZeroSkipsServer) { + // This should not attempt to connect to a server + bool ran = false; + hegel::hegel( + [&] { + auto x = hegel::draw(integers()); + EXPECT_EQ(x, 99); + ran = true; + }, + {.test_cases = 0, .examples = {{99}}}); + EXPECT_TRUE(ran); +} + +TEST(ExplicitExamples, NoExamplesTestCasesZeroIsNoop) { + // No examples and test_cases=0 should just return + hegel::hegel([] {}, {.test_cases = 0}); +} + +TEST(ExplicitExamples, TestFailureInExamplePropagates) { + EXPECT_THROW( + hegel::hegel( + [&] { + auto x = hegel::draw(integers()); + if (x == 42) { + throw std::runtime_error("found the answer"); + } + }, + {.test_cases = 0, .examples = {{42}}}), + std::runtime_error); +} From 23e6926abf182fe7a05f6203b27223c8163bd892 Mon Sep 17 00:00:00 2001 From: Ethan Chou Date: Thu, 2 Apr 2026 18:16:32 -0400 Subject: [PATCH 02/13] fix use after free in explicit example mode --- include/hegel/hegel.h | 4 ++-- include/hegel/internal.h | 2 +- include/hegel/json.h | 7 +++++++ src/connection.cpp | 4 ++-- src/hegel.cpp | 2 +- src/internal.cpp | 8 +++----- src/json.cpp | 18 +++++++++++++++--- src/socket.cpp | 8 ++++++++ 8 files changed, 39 insertions(+), 14 deletions(-) diff --git a/include/hegel/hegel.h b/include/hegel/hegel.h index 541b405..3eb153c 100644 --- a/include/hegel/hegel.h +++ b/include/hegel/hegel.h @@ -85,8 +85,8 @@ namespace hegel { "draw() cannot be called outside of a Hegel test"); } if (internal::has_explicit_value(data)) { - return internal::json_value_to( - internal::pop_explicit_value(data)); + auto val = internal::pop_explicit_value(data); + return internal::json_value_to(val.to_ref()); } return gen.do_draw(data); } diff --git a/include/hegel/internal.h b/include/hegel/internal.h index cdf057a..55ead75 100644 --- a/include/hegel/internal.h +++ b/include/hegel/internal.h @@ -76,7 +76,7 @@ namespace hegel::internal { bool has_explicit_value(impl::data::TestCaseData* data); /// @brief Pop the next explicit value from the current test case. - json::json_raw_ref pop_explicit_value(impl::data::TestCaseData* data); + json::json pop_explicit_value(impl::data::TestCaseData* data); } // namespace hegel::internal /// @endcond diff --git a/include/hegel/json.h b/include/hegel/json.h index 4bc201a..ef7f243 100644 --- a/include/hegel/json.h +++ b/include/hegel/json.h @@ -50,6 +50,9 @@ namespace hegel::internal::json { json_raw_ref& operator=(const size_t& other); json_raw_ref& operator=(const double& other); json_raw_ref& operator=(const std::nullptr_t& other); +#ifdef __APPLE__ + json_raw_ref& operator=(const uint64_t& other); +#endif json_raw_ref operator[](size_t index) const; @@ -74,12 +77,16 @@ namespace hegel::internal::json { json(const int64_t init); json(const uint32_t init); json(const uint64_t init); +#ifdef __APPLE__ + json(const unsigned long init); +#endif json(const bool init); json(const double init); json(const std::string& init); json(std::nullptr_t init = nullptr); ~json(); + json_raw_ref to_ref(); json_raw_ref operator[](const std::string& key); json& operator=(json other) noexcept; diff --git a/src/connection.cpp b/src/connection.cpp index 958e4b3..6eec0da 100644 --- a/src/connection.cpp +++ b/src/connection.cpp @@ -46,8 +46,8 @@ namespace hegel::impl { // ============================================================================= // Handshake // ============================================================================= - static constexpr const char* MIN_PROTOCOL_VERSION = "0.1"; - static constexpr const char* MAX_PROTOCOL_VERSION = "0.7"; + static constexpr const char* MIN_PROTOCOL_VERSION = "0.6"; + static constexpr const char* MAX_PROTOCOL_VERSION = "0.8"; static constexpr const char* HANDSHAKE_STRING = "hegel_handshake_start"; void Connection::handshake() { diff --git a/src/hegel.cpp b/src/hegel.cpp index 9206947..1d69683 100644 --- a/src/hegel.cpp +++ b/src/hegel.cpp @@ -113,7 +113,7 @@ namespace hegel { {"test_cases", test_cases}, {"channel_id", test_channel}}; if (options.seed.has_value()) { - run_test_msg["seed"] = options.seed.value(); + run_test_msg["seed"] = static_cast(options.seed.value()); } else { run_test_msg["seed"] = nullptr; } diff --git a/src/internal.cpp b/src/internal.cpp index 25929b1..afcfbfd 100644 --- a/src/internal.cpp +++ b/src/internal.cpp @@ -40,11 +40,9 @@ namespace hegel::internal { !data->explicit_values->empty(); } - json::json_raw_ref pop_explicit_value(impl::data::TestCaseData* data) { - auto& val = data->explicit_values->back(); - json::json_raw_ref ref( - new json::json_ref_holder(json::ImplUtil::raw(val))); + json::json pop_explicit_value(impl::data::TestCaseData* data) { + json::json val = std::move(data->explicit_values->back()); data->explicit_values->pop_back(); - return ref; + return val; } } // namespace hegel::internal diff --git a/src/json.cpp b/src/json.cpp index 70950d8..679bd21 100644 --- a/src/json.cpp +++ b/src/json.cpp @@ -55,20 +55,26 @@ namespace hegel::internal::json { json::json(const int64_t init) : impl(new json_holder(init)) {} json::json(const uint32_t init) : impl(new json_holder(init)) {} json::json(const uint64_t init) : impl(new json_holder(init)) {} +#ifdef __APPLE__ + json::json(const unsigned long init) + : impl(new json_holder(static_cast(init))) {} +#endif json::json(const bool init) : impl(new json_holder(init)) {} json::json(const double init) : impl(new json_holder(init)) {} json::json(const std::string& init) : impl(new json_holder(init)) {} json::json(std::nullptr_t init) : impl(new json_holder(init)) {} json::~json() = default; + json_raw_ref json::to_ref() { + return json_raw_ref(new json_ref_holder(impl->data)); + } + json_raw_ref json::operator[](const std::string& key) { return json_raw_ref(new json_ref_holder(impl->data[key])); } json& json::operator=(json other) noexcept { - if (this != &other) { - impl->operator=(*other.impl); - } + impl = std::move(other.impl); return *this; } @@ -154,6 +160,12 @@ namespace hegel::internal::json { ref->data = other; return *this; } +#ifdef __APPLE__ + json_raw_ref& json_raw_ref::operator=(const uint64_t& other) { + ref->data = other; + return *this; + } +#endif bool json_raw_ref::is_string() const noexcept { return ref->data.is_string(); diff --git a/src/socket.cpp b/src/socket.cpp index 1b8a366..7538cd1 100644 --- a/src/socket.cpp +++ b/src/socket.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -43,10 +44,17 @@ namespace hegel::impl::socket { } int connect_to_socket(const std::string& path) { +#ifdef SOCK_CLOEXEC int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); +#else + int fd = ::socket(AF_UNIX, SOCK_STREAM, 0); +#endif if (fd < 0) { throw std::runtime_error("Failed to create socket"); } +#ifndef SOCK_CLOEXEC + fcntl(fd, F_SETFD, FD_CLOEXEC); +#endif struct sockaddr_un addr{}; addr.sun_family = AF_UNIX; From d1af49e9d9656e7aa9ec43f003e5d335ff7c757c Mon Sep 17 00:00:00 2001 From: Ethan Chou Date: Thu, 2 Apr 2026 18:36:21 -0400 Subject: [PATCH 03/13] better error message when types don't match --- include/hegel/hegel.h | 13 ++++++++++++- include/hegel/json.h | 2 ++ src/json.cpp | 4 ++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/include/hegel/hegel.h b/include/hegel/hegel.h index 3eb153c..a72cf34 100644 --- a/include/hegel/hegel.h +++ b/include/hegel/hegel.h @@ -86,7 +86,18 @@ namespace hegel { } if (internal::has_explicit_value(data)) { auto val = internal::pop_explicit_value(data); - return internal::json_value_to(val.to_ref()); + auto ref = val.to_ref(); + if constexpr (std::is_constructible_v) { + auto expected = internal::json::json(T{}).to_ref().type_name(); + auto got = ref.type_name(); + if (expected != got) { + throw std::runtime_error( + "Explicit example type mismatch: expected " + + expected + ", got " + got + " " + val.dump()); + } + } + + return internal::json_value_to(ref); } return gen.do_draw(data); } diff --git a/include/hegel/json.h b/include/hegel/json.h index ef7f243..97f199b 100644 --- a/include/hegel/json.h +++ b/include/hegel/json.h @@ -38,6 +38,8 @@ namespace hegel::internal::json { size_t size() const noexcept; + std::string type_name() const noexcept; + bool is_string() const noexcept; bool is_null() const noexcept; bool is_boolean() const noexcept; diff --git a/src/json.cpp b/src/json.cpp index 679bd21..0a46e25 100644 --- a/src/json.cpp +++ b/src/json.cpp @@ -167,6 +167,10 @@ namespace hegel::internal::json { } #endif + std::string json_raw_ref::type_name() const noexcept { + return ref->data.type_name(); + } + bool json_raw_ref::is_string() const noexcept { return ref->data.is_string(); } From 944f3ad8bcc17c7c7909656eae6e53e9acccd5cf Mon Sep 17 00:00:00 2001 From: Ethan Chou Date: Thu, 2 Apr 2026 18:54:10 -0400 Subject: [PATCH 04/13] simplify deserialize code and error handling --- include/hegel/core.h | 67 ++++++++++++++++++------------------------- include/hegel/hegel.h | 1 + 2 files changed, 29 insertions(+), 39 deletions(-) diff --git a/include/hegel/core.h b/include/hegel/core.h index 37475ec..dacf6df 100644 --- a/include/hegel/core.h +++ b/include/hegel/core.h @@ -18,30 +18,17 @@ namespace hegel::internal { /// Generate a schema for type T (wrapper around reflect-cpp) template hegel::internal::json::json type_schema() { - return hegel::internal::json::json::parse(rfl::json::to_schema()); + return hegel::internal::json::json::parse(rfl::json::to_schema().c_str()); } /// Deserialize a json_raw_ref into a value of type T. template T json_value_to(const hegel::internal::json::json_raw_ref& result) { - if constexpr (std::is_same_v) { - return result.get_string(); - } else if constexpr (std::is_same_v, bool>) { - return result.get_bool(); - } else if constexpr (std::is_floating_point_v) { - return static_cast(result.get_double()); - } else if constexpr (std::is_unsigned_v) { - return static_cast(result.get_uint64_t()); - } else if constexpr (std::is_integral_v) { - return static_cast(result.get_int64_t()); - } else { - auto parse_result = read_nlohmann(result); - if (!parse_result.has_value()) { - throw std::runtime_error( - "Failed to parse value into requested type"); - } - return parse_result.value(); + auto parse_result = read_nlohmann(result); + if (!parse_result.has_value()) { + throw std::runtime_error(parse_result.error().what()); } + return parse_result.value(); } } // namespace hegel::internal @@ -387,27 +374,6 @@ namespace hegel::generators { /** * @brief Create a generator for type T using automatic schema derivation. * - * Uses reflect-cpp to derive a schema from the type's structure. - * Works with structs, classes, and standard library types. - * - * @code{.cpp} - * struct Person { - * std::string name; - * int age; - * }; - * - * auto gen = hegel::generators::default_generator(); - * Person p = hegel::draw(gen); - * @endcode - * - * @tparam T The type to generate (must be reflect-cpp compatible) - * @return A SchemaBackedGenerator instance - */ - template Generator default_generator() { - return from_schema(internal::type_schema()); - } - - /** * @brief Construct a generator from a function. * @param fn Function that produces values of type T given test case data */ @@ -457,4 +423,27 @@ namespace hegel::generators { return Generator(new SchemaBackedGenerator(std::move(schema))); } + /** + * @brief Generate values of type T using reflect-cpp schema derivation. + * + * Uses reflect-cpp to derive a schema from the type's structure. + * Works with structs, classes, and standard library types. + * + * @code{.cpp} + * struct Person { + * std::string name; + * int age; + * }; + * + * auto gen = hegel::generators::default_generator(); + * Person p = hegel::draw(gen); + * @endcode + * + * @tparam T The type to generate (must be reflect-cpp compatible) + * @return A SchemaBackedGenerator instance + */ + template Generator default_generator() { + return from_schema(internal::type_schema()); + } + } // namespace hegel::generators diff --git a/include/hegel/hegel.h b/include/hegel/hegel.h index a72cf34..a28174f 100644 --- a/include/hegel/hegel.h +++ b/include/hegel/hegel.h @@ -96,6 +96,7 @@ namespace hegel { expected + ", got " + got + " " + val.dump()); } } + // need better error messages for structs probably return internal::json_value_to(ref); } From eb8b2827e949e0369df708266936905a92e77ab4 Mon Sep 17 00:00:00 2001 From: Ethan Chou Date: Fri, 3 Apr 2026 00:37:29 -0400 Subject: [PATCH 05/13] handle when too few example values --- include/hegel/core.h | 3 +- include/hegel/hegel.h | 14 +++-- include/hegel/internal.h | 3 ++ src/hegel.cpp | 3 +- src/internal.cpp | 4 ++ tests/test_explicit_examples.cpp | 92 ++++++++------------------------ 6 files changed, 41 insertions(+), 78 deletions(-) diff --git a/include/hegel/core.h b/include/hegel/core.h index dacf6df..36f47eb 100644 --- a/include/hegel/core.h +++ b/include/hegel/core.h @@ -18,7 +18,8 @@ namespace hegel::internal { /// Generate a schema for type T (wrapper around reflect-cpp) template hegel::internal::json::json type_schema() { - return hegel::internal::json::json::parse(rfl::json::to_schema().c_str()); + return hegel::internal::json::json::parse( + rfl::json::to_schema().c_str()); } /// Deserialize a json_raw_ref into a value of type T. diff --git a/include/hegel/hegel.h b/include/hegel/hegel.h index a28174f..10fe3d3 100644 --- a/include/hegel/hegel.h +++ b/include/hegel/hegel.h @@ -92,13 +92,19 @@ namespace hegel { auto got = ref.type_name(); if (expected != got) { throw std::runtime_error( - "Explicit example type mismatch: expected " + - expected + ", got " + got + " " + val.dump()); + "Explicit example type mismatch: expected " + expected + + ", got " + got + " " + val.dump()); } + + return internal::json_value_to(ref); } // need better error messages for structs probably - - return internal::json_value_to(ref); + // doesn't work for all types + } + if (internal::is_explicit_example(data)) { + throw std::runtime_error( + "Explicit example has too few values for the number " + "of draw() calls"); } return gen.do_draw(data); } diff --git a/include/hegel/internal.h b/include/hegel/internal.h index 55ead75..c5404d7 100644 --- a/include/hegel/internal.h +++ b/include/hegel/internal.h @@ -78,5 +78,8 @@ namespace hegel::internal { /// @brief Pop the next explicit value from the current test case. json::json pop_explicit_value(impl::data::TestCaseData* data); + /// @brief Check if we're running an explicit example (no server connection). + bool is_explicit_example(impl::data::TestCaseData* data); + } // namespace hegel::internal /// @endcond diff --git a/src/hegel.cpp b/src/hegel.cpp index 1d69683..b11e0b4 100644 --- a/src/hegel.cpp +++ b/src/hegel.cpp @@ -242,8 +242,7 @@ namespace hegel { } catch (const internal::HegelReject&) { impl::data::clear(); throw std::runtime_error( - "assume() failed on explicit example " + - std::to_string(i) + + "assume() failed on explicit example " + std::to_string(i) + ": explicit examples must not be filtered"); } catch (...) { impl::data::clear(); diff --git a/src/internal.cpp b/src/internal.cpp index afcfbfd..2865cab 100644 --- a/src/internal.cpp +++ b/src/internal.cpp @@ -45,4 +45,8 @@ namespace hegel::internal { data->explicit_values->pop_back(); return val; } + + bool is_explicit_example(impl::data::TestCaseData* data) { + return data->explicit_values != nullptr; + } } // namespace hegel::internal diff --git a/tests/test_explicit_examples.cpp b/tests/test_explicit_examples.cpp index a4d54ea..66f79da 100644 --- a/tests/test_explicit_examples.cpp +++ b/tests/test_explicit_examples.cpp @@ -62,88 +62,38 @@ TEST(ExplicitExamples, BooleanValues) { EXPECT_TRUE(ran); } -TEST(ExplicitExamples, FloatingPointValues) { - bool ran = false; - hegel::hegel( - [&] { - auto f = hegel::draw(floats()); - EXPECT_DOUBLE_EQ(f, 3.14); - ran = true; - }, - {.test_cases = 0, .examples = {{3.14}}}); - EXPECT_TRUE(ran); -} - // ============================================================================= // Error conditions // ============================================================================= TEST(ExplicitExamples, TooManyValuesThrows) { - EXPECT_THROW( - hegel::hegel( - [&] { - hegel::draw(integers()); - // Only one draw, but two values provided - }, - {.test_cases = 0, .examples = {{1, 2}}}), - std::runtime_error); + EXPECT_THROW(hegel::hegel( + [&] { + hegel::draw(integers()); + // Only one draw, but two values provided + }, + {.test_cases = 0, .examples = {{1, 2}}}), + std::runtime_error); } TEST(ExplicitExamples, TooFewValuesUsesGenerator) { // When explicit values run out, draw() falls through to the generator. // With connection=nullptr, this will throw (no server). - EXPECT_THROW( - hegel::hegel( - [&] { - hegel::draw(integers()); - hegel::draw(integers()); // No explicit value left - }, - {.test_cases = 0, .examples = {{42}}}), - std::exception); + EXPECT_THROW(hegel::hegel( + [&] { + hegel::draw(integers()); + hegel::draw(integers()); // No explicit value left + }, + {.test_cases = 0, .examples = {{42}}}), + std::exception); } TEST(ExplicitExamples, AssumeFailureIsError) { - EXPECT_THROW( - hegel::hegel( - [&] { - auto x = hegel::draw(integers()); - hegel::assume(x > 100); // 42 is not > 100 - }, - {.test_cases = 0, .examples = {{42}}}), - std::runtime_error); -} - -// ============================================================================= -// Examples with test_cases=0 means examples-only -// ============================================================================= - -TEST(ExplicitExamples, TestCasesZeroSkipsServer) { - // This should not attempt to connect to a server - bool ran = false; - hegel::hegel( - [&] { - auto x = hegel::draw(integers()); - EXPECT_EQ(x, 99); - ran = true; - }, - {.test_cases = 0, .examples = {{99}}}); - EXPECT_TRUE(ran); -} - -TEST(ExplicitExamples, NoExamplesTestCasesZeroIsNoop) { - // No examples and test_cases=0 should just return - hegel::hegel([] {}, {.test_cases = 0}); -} - -TEST(ExplicitExamples, TestFailureInExamplePropagates) { - EXPECT_THROW( - hegel::hegel( - [&] { - auto x = hegel::draw(integers()); - if (x == 42) { - throw std::runtime_error("found the answer"); - } - }, - {.test_cases = 0, .examples = {{42}}}), - std::runtime_error); + EXPECT_THROW(hegel::hegel( + [&] { + auto x = hegel::draw(integers()); + hegel::assume(x > 100); // 42 is not > 100 + }, + {.test_cases = 0, .examples = {{42}}}), + std::runtime_error); } From 21c19fd44cc7d18942c95220418614ad1b0a6fa4 Mon Sep 17 00:00:00 2001 From: Ethan Chou Date: Fri, 3 Apr 2026 00:41:55 -0400 Subject: [PATCH 06/13] separate out explicit example code --- include/hegel/hegel.h | 44 ++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/include/hegel/hegel.h b/include/hegel/hegel.h index 10fe3d3..cc2f584 100644 --- a/include/hegel/hegel.h +++ b/include/hegel/hegel.h @@ -78,33 +78,39 @@ namespace hegel { * @return A randomly generated value of type T * @throws std::runtime_error if called outside of a Hegel test */ - template T draw(const generators::Generator& gen) { - auto* data = internal::get_test_case_data(); - if (!data) { - throw std::runtime_error( - "draw() cannot be called outside of a Hegel test"); - } - if (internal::has_explicit_value(data)) { - auto val = internal::pop_explicit_value(data); + /// @cond INTERNAL + namespace internal { + template + T draw_explicit(impl::data::TestCaseData* data) { + if (!has_explicit_value(data)) { + throw std::runtime_error( + "Explicit example has too few values for the " + "number of draw() calls"); + } + auto val = pop_explicit_value(data); auto ref = val.to_ref(); - if constexpr (std::is_constructible_v) { - auto expected = internal::json::json(T{}).to_ref().type_name(); + if constexpr (std::is_constructible_v) { + auto expected = json::json(T{}).to_ref().type_name(); auto got = ref.type_name(); if (expected != got) { throw std::runtime_error( - "Explicit example type mismatch: expected " + expected + - ", got " + got + " " + val.dump()); + "Explicit example type mismatch: expected " + + expected + ", got " + got + " " + val.dump()); } - - return internal::json_value_to(ref); } - // need better error messages for structs probably - // doesn't work for all types + return json_value_to(ref); } - if (internal::is_explicit_example(data)) { + } // namespace internal + /// @endcond + + template T draw(const generators::Generator& gen) { + auto* data = internal::get_test_case_data(); + if (!data) { throw std::runtime_error( - "Explicit example has too few values for the number " - "of draw() calls"); + "draw() cannot be called outside of a Hegel test"); + } + if (internal::is_explicit_example(data)) { + return internal::draw_explicit(data); } return gen.do_draw(data); } From 367a0cc52c786fb9ad8f956b46636a0d9e236dbd Mon Sep 17 00:00:00 2001 From: echoumcp1 <101858583+echoumcp1@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:06:45 -0400 Subject: [PATCH 07/13] use std::any instead of json for explicit examples --- include/hegel/hegel.h | 20 +++++++------- include/hegel/internal.h | 3 ++- include/hegel/options.h | 8 +++--- src/data.h | 4 +-- src/hegel.cpp | 4 +-- src/internal.cpp | 4 +-- tests/test_explicit_examples.cpp | 46 +++++++++++++++++++++++++++++++- 7 files changed, 67 insertions(+), 22 deletions(-) diff --git a/include/hegel/hegel.h b/include/hegel/hegel.h index cc2f584..10b62f4 100644 --- a/include/hegel/hegel.h +++ b/include/hegel/hegel.h @@ -45,7 +45,9 @@ #include "generators/random.h" #include "generators/strings.h" +#include #include +#include /** @namespace hegel * @brief Main Hegel namespace @@ -88,17 +90,15 @@ namespace hegel { "number of draw() calls"); } auto val = pop_explicit_value(data); - auto ref = val.to_ref(); - if constexpr (std::is_constructible_v) { - auto expected = json::json(T{}).to_ref().type_name(); - auto got = ref.type_name(); - if (expected != got) { - throw std::runtime_error( - "Explicit example type mismatch: expected " + - expected + ", got " + got + " " + val.dump()); - } + auto* ptr = std::any_cast(&val); + if (!ptr) { + throw std::runtime_error( + "Explicit example type mismatch: expected " + + std::string(typeid(T).name()) + ", got " + + (val.has_value() ? std::string(val.type().name()) + : "empty")); } - return json_value_to(ref); + return std::move(*ptr); } } // namespace internal /// @endcond diff --git a/include/hegel/internal.h b/include/hegel/internal.h index c5404d7..a0f78f5 100644 --- a/include/hegel/internal.h +++ b/include/hegel/internal.h @@ -12,6 +12,7 @@ * and exist only in the src/ directory. */ +#include #include #include #include @@ -76,7 +77,7 @@ namespace hegel::internal { bool has_explicit_value(impl::data::TestCaseData* data); /// @brief Pop the next explicit value from the current test case. - json::json pop_explicit_value(impl::data::TestCaseData* data); + std::any pop_explicit_value(impl::data::TestCaseData* data); /// @brief Check if we're running an explicit example (no server connection). bool is_explicit_example(impl::data::TestCaseData* data); diff --git a/include/hegel/options.h b/include/hegel/options.h index 85ce73f..f606df4 100644 --- a/include/hegel/options.h +++ b/include/hegel/options.h @@ -5,7 +5,7 @@ #include #include -#include "json.h" +#include /** @namespace hegel::options * @brief The HegelOptions struct for configuring your Hegel run, and supporting @@ -41,8 +41,8 @@ namespace hegel::options { } } - /// A single explicit example: one json value per draw() call, in order. - using Example = std::vector; + /// A single explicit example: one value per draw() call, in order. + using Example = std::vector; /** * @brief Configuration options for embedded mode execution. @@ -61,7 +61,7 @@ namespace hegel::options { std::optional seed; /// Explicit examples to run before random generation. - /// Each Example is a vector of json values, one per draw() call. + /// Each Example is a vector of values, one per draw() call. std::vector examples; }; } // namespace hegel::options diff --git a/src/data.h b/src/data.h index 8de29bb..e221ebe 100644 --- a/src/data.h +++ b/src/data.h @@ -1,7 +1,7 @@ #pragma once +#include #include -#include #include #include @@ -20,7 +20,7 @@ namespace hegel::impl::data { bool is_last_run; bool test_aborted; options::Verbosity verbosity; - std::vector* explicit_values = nullptr; + std::vector* explicit_values = nullptr; }; void set(TestCaseData* data); diff --git a/src/hegel.cpp b/src/hegel.cpp index b11e0b4..3e2537e 100644 --- a/src/hegel.cpp +++ b/src/hegel.cpp @@ -223,8 +223,8 @@ namespace hegel { const options::HegelOptions& options) { for (size_t i = 0; i < options.examples.size(); ++i) { // Copy the example and reverse so pop_back() yields in order - std::vector values( - options.examples[i].begin(), options.examples[i].end()); + std::vector values(options.examples[i].begin(), + options.examples[i].end()); std::reverse(values.begin(), values.end()); impl::data::TestCaseData data{ diff --git a/src/internal.cpp b/src/internal.cpp index 2865cab..73a5770 100644 --- a/src/internal.cpp +++ b/src/internal.cpp @@ -40,8 +40,8 @@ namespace hegel::internal { !data->explicit_values->empty(); } - json::json pop_explicit_value(impl::data::TestCaseData* data) { - json::json val = std::move(data->explicit_values->back()); + std::any pop_explicit_value(impl::data::TestCaseData* data) { + std::any val = std::move(data->explicit_values->back()); data->explicit_values->pop_back(); return val; } diff --git a/tests/test_explicit_examples.cpp b/tests/test_explicit_examples.cpp index 66f79da..3ad2751 100644 --- a/tests/test_explicit_examples.cpp +++ b/tests/test_explicit_examples.cpp @@ -30,7 +30,7 @@ TEST(ExplicitExamples, MultipleDraws) { EXPECT_EQ(s, "hello"); ran = true; }, - {.test_cases = 0, .examples = {{7, "hello"}}}); + {.test_cases = 0, .examples = {{7, std::string("hello")}}}); EXPECT_TRUE(ran); } @@ -62,6 +62,39 @@ TEST(ExplicitExamples, BooleanValues) { EXPECT_TRUE(ran); } + +struct Point { + int x; + int y; + bool operator==(const Point& o) const { return x == o.x && y == o.y; } +}; + +TEST(ExplicitExamples, StructValues) { + bool ran = false; + hegel::hegel( + [&] { + auto p = hegel::draw(builds()); + EXPECT_EQ(p, (Point{3, 4})); + ran = true; + }, + {.test_cases = 0, .examples = {{Point{3, 4}}}}); + EXPECT_TRUE(ran); +} + +TEST(ExplicitExamples, RandomValue) { + bool ran = false; + hegel::hegel( + [&] { + auto rng = hegel::draw(randoms()); + std::uniform_int_distribution dist(1, 100); + int val = dist(rng); + EXPECT_GE(val, 1); + EXPECT_LE(val, 100); + ran = true; + }, + {.test_cases = 0, .examples = {{HegelRandom(uint64_t{42})}}}); + EXPECT_TRUE(ran); +} // ============================================================================= // Error conditions // ============================================================================= @@ -97,3 +130,14 @@ TEST(ExplicitExamples, AssumeFailureIsError) { {.test_cases = 0, .examples = {{42}}}), std::runtime_error); } + +TEST(ExplicitExamples, TypeMismatchThrows) { + EXPECT_THROW( + hegel::hegel( + [&] { + // Example stores int, but we draw a bool + hegel::draw(booleans()); + }, + {.test_cases = 0, .examples = {{42}}}), + std::runtime_error); +} From 38742dc6b1bf830a0e836ff818bfbf89822c0ba7 Mon Sep 17 00:00:00 2001 From: echoumcp1 <101858583+echoumcp1@users.noreply.github.com> Date: Fri, 3 Apr 2026 11:14:41 -0400 Subject: [PATCH 08/13] lint --- include/hegel/hegel.h | 3 +-- include/hegel/internal.h | 3 ++- src/hegel.cpp | 2 +- tests/test_explicit_examples.cpp | 16 +++++++--------- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/include/hegel/hegel.h b/include/hegel/hegel.h index 10b62f4..16b48fa 100644 --- a/include/hegel/hegel.h +++ b/include/hegel/hegel.h @@ -82,8 +82,7 @@ namespace hegel { */ /// @cond INTERNAL namespace internal { - template - T draw_explicit(impl::data::TestCaseData* data) { + template T draw_explicit(impl::data::TestCaseData* data) { if (!has_explicit_value(data)) { throw std::runtime_error( "Explicit example has too few values for the " diff --git a/include/hegel/internal.h b/include/hegel/internal.h index a0f78f5..69473aa 100644 --- a/include/hegel/internal.h +++ b/include/hegel/internal.h @@ -79,7 +79,8 @@ namespace hegel::internal { /// @brief Pop the next explicit value from the current test case. std::any pop_explicit_value(impl::data::TestCaseData* data); - /// @brief Check if we're running an explicit example (no server connection). + /// @brief Check if we're running an explicit example (no server + /// connection). bool is_explicit_example(impl::data::TestCaseData* data); } // namespace hegel::internal diff --git a/src/hegel.cpp b/src/hegel.cpp index 3e2537e..281c056 100644 --- a/src/hegel.cpp +++ b/src/hegel.cpp @@ -224,7 +224,7 @@ namespace hegel { for (size_t i = 0; i < options.examples.size(); ++i) { // Copy the example and reverse so pop_back() yields in order std::vector values(options.examples[i].begin(), - options.examples[i].end()); + options.examples[i].end()); std::reverse(values.begin(), values.end()); impl::data::TestCaseData data{ diff --git a/tests/test_explicit_examples.cpp b/tests/test_explicit_examples.cpp index 3ad2751..12b6a3b 100644 --- a/tests/test_explicit_examples.cpp +++ b/tests/test_explicit_examples.cpp @@ -62,7 +62,6 @@ TEST(ExplicitExamples, BooleanValues) { EXPECT_TRUE(ran); } - struct Point { int x; int y; @@ -132,12 +131,11 @@ TEST(ExplicitExamples, AssumeFailureIsError) { } TEST(ExplicitExamples, TypeMismatchThrows) { - EXPECT_THROW( - hegel::hegel( - [&] { - // Example stores int, but we draw a bool - hegel::draw(booleans()); - }, - {.test_cases = 0, .examples = {{42}}}), - std::runtime_error); + EXPECT_THROW(hegel::hegel( + [&] { + // Example stores int, but we draw a bool + hegel::draw(booleans()); + }, + {.test_cases = 0, .examples = {{42}}}), + std::runtime_error); } From ada6cb80cbdde4582675758f9543755216f2ac8a Mon Sep 17 00:00:00 2001 From: echoumcp1 <101858583+echoumcp1@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:00:50 -0400 Subject: [PATCH 09/13] fix data_stream --- src/hegel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hegel.cpp b/src/hegel.cpp index 8dfa332..7720ad4 100644 --- a/src/hegel.cpp +++ b/src/hegel.cpp @@ -227,7 +227,7 @@ namespace hegel { impl::data::TestCaseData data{ .connection = nullptr, - .data_channel = 0, + .data_stream = 0, .is_last_run = true, .test_aborted = false, .verbosity = options.verbosity, From bc02639d91e7d2f53ecddeb30c299664fb95325a Mon Sep 17 00:00:00 2001 From: echoumcp1 <101858583+echoumcp1@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:39:58 -0400 Subject: [PATCH 10/13] cleanup --- include/hegel/hegel.h | 10 ---------- include/hegel/internal.h | 1 + include/hegel/json.h | 3 --- src/json.cpp | 12 +++--------- src/json_impl.h | 1 - tests/test_explicit_examples.cpp | 20 ++++++++++---------- 6 files changed, 14 insertions(+), 33 deletions(-) diff --git a/include/hegel/hegel.h b/include/hegel/hegel.h index 96fc5a5..a1187d2 100644 --- a/include/hegel/hegel.h +++ b/include/hegel/hegel.h @@ -227,23 +227,13 @@ #include "generators/random.h" #include "generators/strings.h" -#include #include -#include /** @namespace hegel * @brief Main namespace */ namespace hegel { /** - * @brief Run property-based tests using Hegel in embedded mode. - * - * This is the recommended way to run property tests. The function: - * 1. Creates a Unix socket server for communication - * 2. Spawns the Hegel CLI as a subprocess - * 3. Runs the test function for each generated test case - * 4. Handles shrinking when failures occur - * 5. Throws std::runtime_error if any test case fails * @brief Run a Hegel test. * * @code{.cpp} diff --git a/include/hegel/internal.h b/include/hegel/internal.h index 744f6a0..6c81700 100644 --- a/include/hegel/internal.h +++ b/include/hegel/internal.h @@ -40,6 +40,7 @@ namespace hegel::internal { return "test case stopped by backend"; } }; + } // namespace hegel::internal /// @endcond diff --git a/include/hegel/json.h b/include/hegel/json.h index fe4b569..47fc1d1 100644 --- a/include/hegel/json.h +++ b/include/hegel/json.h @@ -41,8 +41,6 @@ namespace hegel::internal::json { size_t size() const noexcept; - std::string type_name() const noexcept; - bool is_string() const noexcept; bool is_null() const noexcept; bool is_boolean() const noexcept; @@ -95,7 +93,6 @@ namespace hegel::internal::json { json(const json_raw_ref& init); ~json(); - json_raw_ref to_ref(); json_raw_ref operator[](const std::string& key); json& operator=(json other) noexcept; diff --git a/src/json.cpp b/src/json.cpp index 9f2eb06..0fb0214 100644 --- a/src/json.cpp +++ b/src/json.cpp @@ -67,16 +67,14 @@ namespace hegel::internal::json { : impl(new json_holder(ImplUtil::raw(init))) {} json::~json() = default; - json_raw_ref json::to_ref() { - return json_raw_ref(new json_ref_holder(impl->data)); - } - json_raw_ref json::operator[](const std::string& key) { return json_raw_ref(new json_ref_holder(impl->data[key])); } json& json::operator=(json other) noexcept { - impl = std::move(other.impl); + if (this != &other) { + impl->operator=(*other.impl); + } return *this; } @@ -182,10 +180,6 @@ namespace hegel::internal::json { } #endif - std::string json_raw_ref::type_name() const noexcept { - return ref->data.type_name(); - } - bool json_raw_ref::is_string() const noexcept { return ref->data.is_string(); } diff --git a/src/json_impl.h b/src/json_impl.h index c9d657f..2e9337d 100644 --- a/src/json_impl.h +++ b/src/json_impl.h @@ -24,7 +24,6 @@ namespace hegel::internal::json { static const nlohmann::json& raw(const json& json) { return json.impl->data; } - static nlohmann::json& raw(json& j) { return j.impl->data; } static json create(const nlohmann::json& from) { json result(nullptr); result.impl.reset(new json_holder(from)); diff --git a/tests/test_explicit_examples.cpp b/tests/test_explicit_examples.cpp index 6ce03c6..8bf8f4e 100644 --- a/tests/test_explicit_examples.cpp +++ b/tests/test_explicit_examples.cpp @@ -11,7 +11,7 @@ using namespace hegel::generators; TEST(ExplicitExamples, SingleDrawSingleExample) { bool ran = false; hegel::test( - [&ran](hegel::TestCase& tc){ + [&ran](hegel::TestCase& tc) { auto x = tc.draw(integers()); EXPECT_EQ(x, 42); ran = true; @@ -23,7 +23,7 @@ TEST(ExplicitExamples, SingleDrawSingleExample) { TEST(ExplicitExamples, MultipleDraws) { bool ran = false; hegel::test( - [&ran](hegel::TestCase& tc){ + [&ran](hegel::TestCase& tc) { auto x = tc.draw(integers()); auto s = tc.draw(text()); EXPECT_EQ(x, 7); @@ -37,7 +37,7 @@ TEST(ExplicitExamples, MultipleDraws) { TEST(ExplicitExamples, MultipleExamples) { int call_count = 0; hegel::test( - [&call_count](hegel::TestCase& tc){ + [&call_count](hegel::TestCase& tc) { auto x = tc.draw(integers()); if (call_count == 0) { EXPECT_EQ(x, 10); @@ -53,7 +53,7 @@ TEST(ExplicitExamples, MultipleExamples) { TEST(ExplicitExamples, BooleanValues) { bool ran = false; hegel::test( - [&ran](hegel::TestCase& tc){ + [&ran](hegel::TestCase& tc) { auto b = tc.draw(booleans()); EXPECT_TRUE(b); ran = true; @@ -71,7 +71,7 @@ struct Point { TEST(ExplicitExamples, StructValues) { bool ran = false; hegel::test( - [&ran](hegel::TestCase& tc){ + [&ran](hegel::TestCase& tc) { auto p = tc.draw(builds()); EXPECT_EQ(p, (Point{3, 4})); ran = true; @@ -83,7 +83,7 @@ TEST(ExplicitExamples, StructValues) { TEST(ExplicitExamples, RandomValue) { bool ran = false; hegel::test( - [&ran](hegel::TestCase& tc){ + [&ran](hegel::TestCase& tc) { auto rng = tc.draw(randoms()); std::uniform_int_distribution dist(1, 100); int val = dist(rng); @@ -100,7 +100,7 @@ TEST(ExplicitExamples, RandomValue) { TEST(ExplicitExamples, TooManyValuesThrows) { EXPECT_THROW(hegel::test( - [](hegel::TestCase& tc){ + [](hegel::TestCase& tc) { tc.draw(integers()); // Only one draw, but two values provided }, @@ -112,7 +112,7 @@ TEST(ExplicitExamples, TooFewValuesUsesGenerator) { // When explicit values run out, draw() falls through to the generator. // With connection=nullptr, this will throw (no server). EXPECT_THROW(hegel::test( - [](hegel::TestCase& tc){ + [](hegel::TestCase& tc) { tc.draw(integers()); tc.draw(integers()); // No explicit value left }, @@ -122,7 +122,7 @@ TEST(ExplicitExamples, TooFewValuesUsesGenerator) { TEST(ExplicitExamples, AssumeFailureIsError) { EXPECT_THROW(hegel::test( - [](hegel::TestCase& tc){ + [](hegel::TestCase& tc) { auto x = tc.draw(integers()); tc.assume(x > 100); // 42 is not > 100 }, @@ -132,7 +132,7 @@ TEST(ExplicitExamples, AssumeFailureIsError) { TEST(ExplicitExamples, TypeMismatchThrows) { EXPECT_THROW(hegel::test( - [](hegel::TestCase& tc){ + [](hegel::TestCase& tc) { // Example stores int, but we draw a bool tc.draw(booleans()); }, From f40082b732fcf7f24be18a3f897172690b2dc4b1 Mon Sep 17 00:00:00 2001 From: echoumcp1 <101858583+echoumcp1@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:07:51 -0400 Subject: [PATCH 11/13] docs fix --- include/hegel/core.h | 8 ++++---- include/hegel/hegel.h | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/include/hegel/core.h b/include/hegel/core.h index 5e927d6..bc02b20 100644 --- a/include/hegel/core.h +++ b/include/hegel/core.h @@ -22,7 +22,7 @@ namespace hegel::generators { template class CompositeGenerator; template class MappedGenerator; - /// @cond INTERNAL + /** @cond INTERNAL */ // Default client-side parser used by schema-backed generators whose parse // step is determined solely by T. Primitives use typed accessors on the // raw json_raw_ref; reflectable composite types fall back to reflect-cpp. @@ -47,9 +47,9 @@ namespace hegel::generators { return parse_result.value(); } } - /// @endcond + /** @endcond */ - /// @cond INTERNAL + /** @cond INTERNAL */ // Schema + client-side parser bundle. Every schema-backed generator // exposes one of these via IGenerator::as_basic(). template struct BasicGenerator { @@ -85,7 +85,7 @@ namespace hegel::generators { }}; } }; - /// @endcond + /** @endcond */ /** * @brief Base interface for generators. diff --git a/include/hegel/hegel.h b/include/hegel/hegel.h index a1187d2..9cb20a5 100644 --- a/include/hegel/hegel.h +++ b/include/hegel/hegel.h @@ -233,6 +233,7 @@ * @brief Main namespace */ namespace hegel { + /** * @brief Run a Hegel test. * From 8000b13fd5251adcf52517f5192afe95be31e887 Mon Sep 17 00:00:00 2001 From: echoumcp1 <101858583+echoumcp1@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:14:25 -0400 Subject: [PATCH 12/13] lint --- src/hegel.cpp | 1 + src/test_case.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/hegel.cpp b/src/hegel.cpp index 5db986f..9adc1f4 100644 --- a/src/hegel.cpp +++ b/src/hegel.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include diff --git a/src/test_case.cpp b/src/test_case.cpp index 5061ed7..64d5d66 100644 --- a/src/test_case.cpp +++ b/src/test_case.cpp @@ -1,6 +1,8 @@ +#include #include #include #include +#include #include #include From 05bcc5be74d145f06aa2b159963e8e8b20ab0dd4 Mon Sep 17 00:00:00 2001 From: echoumcp1 <101858583+echoumcp1@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:23:12 -0400 Subject: [PATCH 13/13] RELEASE.md --- RELEASE.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 RELEASE.md diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..c50b156 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,3 @@ +RELEASE_TYPE: patch + +This patch adds an explicit test case setting for providing explicit example-based test cases alongside property-based tests. \ No newline at end of file