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 diff --git a/include/hegel/core.h b/include/hegel/core.h index 69ce7f8..bc02b20 100644 --- a/include/hegel/core.h +++ b/include/hegel/core.h @@ -4,7 +4,10 @@ #include #include #include +#include #include +#include +#include #include "internal.h" #include "nlohmann_reader.h" @@ -19,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. @@ -44,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 { @@ -82,7 +85,7 @@ namespace hegel::generators { }}; } }; - /// @endcond + /** @endcond */ /** * @brief Base interface for generators. @@ -369,6 +372,23 @@ namespace hegel { template T TestCase::draw(const generators::Generator& gen) const { + if (is_explicit_example()) { + if (!has_explicit_value()) { + throw std::runtime_error( + "Explicit example has too few values for the " + "number of draw() calls"); + } + auto val = pop_explicit_value(); + 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 std::move(*ptr); + } return gen.do_draw(*this); } diff --git a/include/hegel/settings.h b/include/hegel/settings.h index bc33eb2..e385679 100644 --- a/include/hegel/settings.h +++ b/include/hegel/settings.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -45,6 +46,9 @@ namespace hegel { LargeInitialTestCase, ///< First generated test case is unusually large. }; + /// A single explicit example: one value per draw() call, in order. + using Example = std::vector; + /// @cond INTERNAL inline const char* health_check_to_string(HealthCheck c) { switch (c) { @@ -133,5 +137,9 @@ namespace hegel { /// Health checks to suppress for this test. std::vector suppress_health_check; + + /// Explicit examples to run before random generation. + /// Each Example is a vector of values, one per draw() call. + std::vector examples; }; } // namespace hegel diff --git a/include/hegel/test_case.h b/include/hegel/test_case.h index 4d5f323..50eabcf 100644 --- a/include/hegel/test_case.h +++ b/include/hegel/test_case.h @@ -1,5 +1,6 @@ #pragma once +#include #include namespace hegel::impl::test_case { @@ -75,6 +76,18 @@ namespace hegel { // Generators reach through this accessor to talk to the backend. // Not part of the user-facing API. impl::test_case::TestCaseData* data() const { return data_; } + + /// @brief Check if the current test case has an explicit value to + /// consume. + bool has_explicit_value() const; + + /// @brief Pop the next explicit value from the current test case. + std::any pop_explicit_value() const; + + /// @brief Check if we're running an explicit example (no server + /// connection). + bool is_explicit_example() const; + /// @endcond private: diff --git a/src/hegel.cpp b/src/hegel.cpp index c91ccb5..64bfbd7 100644 --- a/src/hegel.cpp +++ b/src/hegel.cpp @@ -10,12 +10,14 @@ #include "installer.h" #include "json_impl.h" +#include #include #include #include #include #include +#include #include #include #include @@ -226,11 +228,64 @@ namespace hegel { } } + // ============================================================================= + // Explicit Examples + // ============================================================================= + static void + run_explicit_examples(const std::function& test_fn, + const Settings& settings) { + for (size_t i = 0; i < settings.examples.size(); ++i) { + // Copy the example and reverse so pop_back() yields in order + std::vector values(settings.examples[i].begin(), + settings.examples[i].end()); + std::reverse(values.begin(), values.end()); + + try { + hegel::impl::test_case::TestCaseData data{ + .connection = nullptr, + .data_stream = 0, + .is_last_run = true, + .verbosity = settings.verbosity, + .explicit_values = &values, + }; + TestCase tc(&data); + + test_fn(tc); + } catch (const internal::HegelReject&) { + throw std::runtime_error( + "assume() failed on explicit example " + std::to_string(i) + + ": explicit examples must not be filtered"); + } catch (...) { + throw; + } + + 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"); + } + } + } + void test(const std::function& test_fn, const Settings& settings) { // Resolve the command (including uv bootstrap, if needed) before // fork so any install cost is paid once in the parent, where // failures surface cleanly. + + // Run explicit examples before fork (no server needed) + if (!settings.examples.empty()) { + run_explicit_examples(test_fn, settings); + } + + // If test_cases is explicitly 0, skip server-driven testing + if (settings.test_cases.has_value() && + settings.test_cases.value() == 0) { + return; + } + std::vector command = impl::hegel_command(); // Create pipes for parent<->child stdio communication diff --git a/src/test_case.cpp b/src/test_case.cpp index d31786b..64d5d66 100644 --- a/src/test_case.cpp +++ b/src/test_case.cpp @@ -1,6 +1,8 @@ +#include #include #include #include +#include #include #include @@ -19,4 +21,18 @@ namespace hegel { } } + bool TestCase::has_explicit_value() const { + return data_->explicit_values != nullptr && + !data_->explicit_values->empty(); + } + + std::any TestCase::pop_explicit_value() const { + std::any val = std::move(data_->explicit_values->back()); + data_->explicit_values->pop_back(); + return val; + } + + bool TestCase::is_explicit_example() const { + return data_->explicit_values != nullptr; + } } // namespace hegel diff --git a/src/test_case.h b/src/test_case.h index b05b2b1..97e4ae4 100644 --- a/src/test_case.h +++ b/src/test_case.h @@ -18,6 +18,7 @@ namespace hegel::impl::test_case { uint32_t data_stream; bool is_last_run; Verbosity verbosity; + std::vector* explicit_values = nullptr; }; } // namespace hegel::impl::test_case diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9946467..a19140b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -121,6 +121,14 @@ target_compile_definitions(test_output # parallelism with other test binaries. gtest_discover_tests(test_output PROPERTIES RESOURCE_LOCK temp_project_subject) +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(test_gtest test_gtest.cpp) target_link_libraries(test_gtest PRIVATE diff --git a/tests/test_explicit_examples.cpp b/tests/test_explicit_examples.cpp new file mode 100644 index 0000000..8bf8f4e --- /dev/null +++ b/tests/test_explicit_examples.cpp @@ -0,0 +1,141 @@ +#include + +#include + +using namespace hegel::generators; + +// ============================================================================= +// Basic explicit examples (examples-only, no server) +// ============================================================================= + +TEST(ExplicitExamples, SingleDrawSingleExample) { + bool ran = false; + hegel::test( + [&ran](hegel::TestCase& tc) { + auto x = tc.draw(integers()); + EXPECT_EQ(x, 42); + ran = true; + }, + {.test_cases = 0, .examples = {{42}}}); + EXPECT_TRUE(ran); +} + +TEST(ExplicitExamples, MultipleDraws) { + bool ran = false; + hegel::test( + [&ran](hegel::TestCase& tc) { + auto x = tc.draw(integers()); + auto s = tc.draw(text()); + EXPECT_EQ(x, 7); + EXPECT_EQ(s, "hello"); + ran = true; + }, + {.test_cases = 0, .examples = {{7, std::string("hello")}}}); + EXPECT_TRUE(ran); +} + +TEST(ExplicitExamples, MultipleExamples) { + int call_count = 0; + hegel::test( + [&call_count](hegel::TestCase& tc) { + auto x = tc.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::test( + [&ran](hegel::TestCase& tc) { + auto b = tc.draw(booleans()); + EXPECT_TRUE(b); + ran = true; + }, + {.test_cases = 0, .examples = {{true}}}); + 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::test( + [&ran](hegel::TestCase& tc) { + auto p = tc.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::test( + [&ran](hegel::TestCase& tc) { + auto rng = tc.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 +// ============================================================================= + +TEST(ExplicitExamples, TooManyValuesThrows) { + EXPECT_THROW(hegel::test( + [](hegel::TestCase& tc) { + tc.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::test( + [](hegel::TestCase& tc) { + tc.draw(integers()); + tc.draw(integers()); // No explicit value left + }, + {.test_cases = 0, .examples = {{42}}}), + std::exception); +} + +TEST(ExplicitExamples, AssumeFailureIsError) { + EXPECT_THROW(hegel::test( + [](hegel::TestCase& tc) { + auto x = tc.draw(integers()); + tc.assume(x > 100); // 42 is not > 100 + }, + {.test_cases = 0, .examples = {{42}}}), + std::runtime_error); +} + +TEST(ExplicitExamples, TypeMismatchThrows) { + EXPECT_THROW(hegel::test( + [](hegel::TestCase& tc) { + // Example stores int, but we draw a bool + tc.draw(booleans()); + }, + {.test_cases = 0, .examples = {{42}}}), + std::runtime_error); +}