Files
partridge-cpp/tests/tests.cc
T
Codex instance 598667b2f3 bench: add repeatable solver measurements
Add an opt-in benchmark probe and JSON runner that separate solve, construction, validation, and rendering time. Record stable search counters, environment metadata, warm-up and repetition policy, timeouts, errors, median spread, and instrumentation overhead.

Compile production solving without counters and interleave counted and plain trials when measuring overhead. Keep heavyweight cases outside the default correctness path while testing counter and report behavior cheaply.

Tests: Debug and Release CTest suites (7 passed each)

Refs: #8
2026-07-30 17:08:05 +01:00

346 lines
12 KiB
C++

/*
* Copyright 2025, Matthew Gretton-Dann
* SPDX-License-Identifier: Apache-2.0
*/
#define PARTRIDGE_TESTING
#include "../main.cc"
#include <algorithm>
#include <array>
#include <sstream>
#include <string>
namespace {
struct Placement {
std::uint64_t x;
std::uint64_t y;
std::uint64_t side;
};
struct Validation {
std::vector<std::string> diagnostics;
[[nodiscard]] auto valid() const noexcept -> bool { return diagnostics.empty(); }
[[nodiscard]] auto text() const -> std::string {
std::ostringstream result;
for (auto const &diagnostic: diagnostics) {
result << diagnostic << '\n';
}
return result.str();
}
};
struct IndependentResult {
std::uint64_t width;
std::uint64_t height;
std::vector<Placement> placements;
};
auto describe(std::size_t index, Placement const &placement) -> std::string {
return "placement " + std::to_string(index) + " at (" +
std::to_string(placement.x) + ", " + std::to_string(placement.y) +
") with side " + std::to_string(placement.side);
}
auto validate(std::uint64_t order, std::uint64_t width, std::uint64_t height,
std::vector<Placement> const &placements) -> Validation {
Validation result;
auto const expected_side = order * (order + 1) / 2;
if (width != expected_side || height != expected_side) {
result.diagnostics.emplace_back(
"board dimensions must both equal the triangular number for the order");
}
std::vector<std::uint64_t> multiplicities(order + 1);
std::vector<int> occupied(width * height, -1);
for (std::size_t index = 0; index < placements.size(); ++index) {
auto const &placement = placements[index];
if (placement.side == 0 || placement.side > order) {
result.diagnostics.push_back(describe(index, placement) +
" has an invalid side length");
continue;
}
++multiplicities[placement.side];
if (placement.x >= width || placement.y >= height ||
placement.side > width - placement.x ||
placement.side > height - placement.y) {
result.diagnostics.push_back(describe(index, placement) +
" is outside the board bounds");
continue;
}
int overlapping_placement = -1;
for (auto y = placement.y; y < placement.y + placement.side; ++y) {
for (auto x = placement.x; x < placement.x + placement.side; ++x) {
auto &cell = occupied[x + y * width];
if (cell != -1) {
overlapping_placement = cell;
} else {
cell = static_cast<int>(index);
}
}
}
if (overlapping_placement != -1) {
result.diagnostics.push_back(
describe(index, placement) + " overlaps placement " +
std::to_string(overlapping_placement));
}
}
for (std::uint64_t side = 1; side <= order; ++side) {
if (multiplicities[side] != side) {
result.diagnostics.push_back(
"side " + std::to_string(side) + " has multiplicity " +
std::to_string(multiplicities[side]) + "; expected " +
std::to_string(side));
}
}
if (std::ranges::find(occupied, -1) != occupied.end()) {
result.diagnostics.emplace_back("board is not completely covered");
}
return result;
}
auto to_independent(Results const &result) -> IndependentResult {
IndependentResult converted{result.length(), result.length(), {}};
converted.placements.reserve(result.squares().size());
for (auto const &square: result.squares()) {
converted.placements.push_back({
square.pos() % result.length(),
square.pos() / result.length(),
square.length(),
});
}
return converted;
}
auto validate(std::uint64_t order, Results const &result) -> Validation {
auto const converted = to_independent(result);
return validate(order, converted.width, converted.height,
converted.placements);
}
auto known_order_8() -> std::vector<Placement> {
return {
{0, 0, 8}, {8, 0, 8}, {16, 0, 8}, {24, 0, 8},
{32, 0, 4}, {32, 4, 4}, {0, 8, 8}, {8, 8, 8},
{16, 8, 8}, {24, 8, 6}, {30, 8, 6}, {24, 14, 5},
{29, 14, 7}, {0, 16, 6}, {6, 16, 3}, {9, 16, 8},
{17, 16, 7}, {6, 19, 3}, {24, 19, 5}, {29, 21, 7},
{0, 22, 7}, {7, 22, 2}, {17, 23, 1}, {18, 23, 6},
{7, 24, 6}, {13, 24, 5}, {24, 24, 5}, {29, 28, 3},
{32, 28, 4}, {0, 29, 7}, {13, 29, 7}, {20, 29, 7},
{27, 29, 2}, {7, 30, 6}, {27, 31, 5}, {32, 32, 4},
};
}
auto renderable_result(std::uint64_t side,
std::vector<Placement> const &placements) -> Results {
std::vector<Square> squares;
squares.reserve(placements.size());
for (auto const &placement: placements) {
squares.emplace_back(placement.x + placement.y * side, placement.side);
}
return Results(side, std::move(squares));
}
auto construct_next_odd(std::uint64_t even_order,
std::vector<Placement> placements)
-> std::vector<Placement> {
auto const old_side = even_order * (even_order + 1) / 2;
auto const square_side = even_order + 1;
for (std::uint64_t y = 0; y < old_side; y += square_side) {
placements.push_back({old_side, y, square_side});
}
for (std::uint64_t x = 0; x <= old_side; x += square_side) {
placements.push_back({x, old_side, square_side});
}
return placements;
}
auto expect(bool condition, std::string const &message) -> int {
if (condition) {
return 0;
}
std::cerr << "FAIL: " << message << '\n';
return 1;
}
auto has(Validation const &validation, std::string_view diagnostic) -> bool {
return std::ranges::any_of(validation.diagnostics,
[diagnostic](std::string const &candidate) {
return candidate.find(diagnostic) != std::string::npos;
});
}
auto test_validator() -> int {
int failures = 0;
auto const valid = known_order_8();
auto validation = validate(8, 36, 36, valid);
failures += expect(validation.valid(),
"known order-8 solution was rejected:\n" + validation.text());
validation = validate(8, 35, 36, valid);
failures += expect(has(validation, "dimensions"),
"invalid board dimensions were not diagnosed");
auto invalid = valid;
invalid.pop_back();
validation = validate(8, 36, 36, invalid);
failures += expect(has(validation, "side 4 has multiplicity 3; expected 4"),
"invalid square multiplicity lacked side and counts");
failures += expect(has(validation, "completely covered"),
"incomplete coverage was not diagnosed");
invalid = valid;
invalid.front().x = 36;
validation = validate(8, 36, 36, invalid);
failures += expect(
has(validation,
"placement 0 at (36, 0) with side 8 is outside the board bounds"),
"out-of-bounds diagnostic lacked placement details");
invalid = valid;
invalid[1].x = invalid[0].x;
invalid[1].y = invalid[0].y;
validation = validate(8, 36, 36, invalid);
failures += expect(
has(validation,
"placement 1 at (0, 0) with side 8 overlaps placement 0"),
"overlap diagnostic lacked placement details");
invalid = valid;
invalid.front().side = 9;
validation = validate(8, 36, 36, invalid);
failures += expect(
has(validation,
"placement 0 at (0, 0) with side 9 has an invalid side length"),
"invalid-side diagnostic lacked placement details");
return failures;
}
auto test_construction() -> int {
auto const constructed = construct_next_odd(8, known_order_8());
auto const validation = validate(9, 45, 45, constructed);
return expect(validation.valid(),
"even-to-odd construction was rejected:\n" + validation.text());
}
auto test_rendering() -> int {
auto const result = renderable_result(36, known_order_8());
std::ostringstream rendered;
auto *const original_buffer = std::cout.rdbuf(rendered.rdbuf());
result.output();
std::cout.rdbuf(original_buffer);
int failures = 0;
std::istringstream lines(rendered.str());
std::string line;
std::uint64_t line_count = 0;
while (std::getline(lines, line)) {
++line_count;
failures += expect(line.size() == result.length(),
"rendered row has incorrect width");
failures += expect(line.find('.') == std::string::npos,
"valid solution left an unrendered cell");
}
failures += expect(line_count == result.length(),
"rendered output has incorrect height");
return failures;
}
auto test_small_solver() -> int {
int failures = 0;
for (auto const order: std::array<std::uint64_t, 2>{2, 3}) {
auto const solution = find_solution(order);
auto const converted = to_independent(solution);
auto const expected_side = order * (order + 1) / 2;
failures += expect(converted.width == expected_side &&
converted.height == expected_side,
"result adapter returned incorrect board dimensions");
failures += expect(!has(validate(order, solution), "dimensions"),
"result adapter supplied invalid validator dimensions");
failures += expect(converted.placements.empty(),
"solver reported a solution for an unsatisfiable order");
}
Results const encoded(10, {Square(23, 2)});
auto const converted = to_independent(encoded);
failures += expect(
converted.width == 10 && converted.height == 10 &&
converted.placements.size() == 1 &&
converted.placements.front().x == 3 &&
converted.placements.front().y == 2 &&
converted.placements.front().side == 2,
"result adapter did not convert encoded placement coordinates");
return failures;
}
auto test_search_counters() -> int {
SearchCounters counters;
auto const solution = find_solution_instrumented(2, counters);
int failures = 0;
failures += expect(solution.squares().empty(),
"instrumented solver changed an infeasible result");
failures += expect(counters.search_nodes > 0 &&
counters.loop_iterations >= counters.search_nodes,
"instrumented solver did not count search work");
failures += expect(counters.attempted_placements > 0 &&
counters.backtracks > 0,
"instrumented solver did not count placements/backtracks");
failures += expect(counters.prune_checks == 0 &&
counters.prune_hits == 0 &&
counters.generated_tasks == 0 &&
counters.completed_tasks == 0,
"unimplemented solver counters were not zero");
return failures;
}
auto test_solver_completion() -> int {
int failures = 0;
for (auto const order: std::array<std::uint64_t, 2>{1, 8}) {
auto const solution = find_solution(order);
auto const validation = validate(order, solution);
failures += expect(
validation.valid(),
"solver returned an invalid order-" + std::to_string(order) +
" solution:\n" + validation.text());
}
return failures;
}
}
int main(int argc, char **argv) {
if (argc != 2) {
std::cerr << "usage: partridge_tests TEST-NAME\n";
return 2;
}
auto const test = std::string_view(argv[1]);
if (test == "validator") {
return test_validator();
}
if (test == "construction") {
return test_construction();
}
if (test == "rendering") {
return test_rendering();
}
if (test == "solver-small") {
return test_small_solver();
}
if (test == "solver-completion") {
return test_solver_completion();
}
if (test == "search-counters") {
return test_search_counters();
}
std::cerr << "unknown test: " << test << '\n';
return 2;
}