Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -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.
28 changes: 24 additions & 4 deletions include/hegel/core.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <typeinfo>
#include <utility>

#include "internal.h"
#include "nlohmann_reader.h"
Expand All @@ -19,7 +22,7 @@ namespace hegel::generators {
template <typename T> class CompositeGenerator;
template <typename T, typename U> 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.
Expand All @@ -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<T>::as_basic().
template <typename T> struct BasicGenerator {
Expand Down Expand Up @@ -82,7 +85,7 @@ namespace hegel::generators {
}};
}
};
/// @endcond
/** @endcond */

/**
* @brief Base interface for generators.
Expand Down Expand Up @@ -369,6 +372,23 @@ namespace hegel {

template <typename T>
T TestCase::draw(const generators::Generator<T>& 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<T>(&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);
}

Expand Down
8 changes: 8 additions & 0 deletions include/hegel/settings.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <any>
#include <cstdint>
#include <optional>
#include <string>
Expand Down Expand Up @@ -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<std::any>;

/// @cond INTERNAL
inline const char* health_check_to_string(HealthCheck c) {
switch (c) {
Expand Down Expand Up @@ -133,5 +137,9 @@ namespace hegel {

/// Health checks to suppress for this test.
std::vector<HealthCheck> suppress_health_check;

/// Explicit examples to run before random generation.
/// Each Example is a vector of values, one per draw() call.
std::vector<Example> examples;
};
} // namespace hegel
13 changes: 13 additions & 0 deletions include/hegel/test_case.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <any>
#include <string_view>

namespace hegel::impl::test_case {
Expand Down Expand Up @@ -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:
Expand Down
55 changes: 55 additions & 0 deletions src/hegel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
#include "installer.h"
#include "json_impl.h"

#include <algorithm>
#include <connection.h>
#include <functional>
#include <protocol.h>
#include <stdexcept>
#include <test_case.h>

#include <any>
#include <cerrno>
#include <cstdint>
#include <cstdio>
Expand Down Expand Up @@ -226,11 +228,64 @@ namespace hegel {
}
}

// =============================================================================
// Explicit Examples
// =============================================================================
static void
run_explicit_examples(const std::function<void(TestCase&)>& 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<std::any> 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<void(TestCase&)>& 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<std::string> command = impl::hegel_command();

// Create pipes for parent<->child stdio communication
Expand Down
16 changes: 16 additions & 0 deletions src/test_case.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include <any>
#include <hegel/internal.h>
#include <hegel/test_case.h>
#include <test_case.h>
#include <utility>

#include <iostream>
#include <string_view>
Expand All @@ -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
1 change: 1 addition & 0 deletions src/test_case.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ namespace hegel::impl::test_case {
uint32_t data_stream;
bool is_last_run;
Verbosity verbosity;
std::vector<std::any>* explicit_values = nullptr;
};

} // namespace hegel::impl::test_case
Expand Down
8 changes: 8 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
141 changes: 141 additions & 0 deletions tests/test_explicit_examples.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#include <gtest/gtest.h>

#include <hegel/hegel.h>

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<int>());
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<int>());
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<int>());
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<Point>());
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<int> 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<int>());
// 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<int>());
tc.draw(integers<int>()); // 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<int>());
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);
}
Loading