From 0b65501c73a72fb6dc5738fc0fdb8f33b66b13b4 Mon Sep 17 00:00:00 2001 From: Codex instance Date: Thu, 30 Jul 2026 16:40:29 +0100 Subject: [PATCH] test: add independent placement validation Establish a CTest harness before changing solver behavior. Validate board dimensions, multiplicities, bounds, overlap and coverage against independent placement data, including known order-8 and constructed order-9 fixtures. Document Debug and sanitizer workflows while keeping the defects tracked by #7 and #14 separate. Prepare a result adapter so the feasible solver regression can be enabled with the completion fix. Tests: Release, ASan and UBSan CTest suites (3 passed each) Refs: #1 --- CMakeLists.txt | 10 ++ README.md | 7 +- TESTING.md | 42 ++++++++ main.cc | 5 + tests/tests.cc | 270 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 TESTING.md create mode 100644 tests/tests.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 7560c22..fe680ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,3 +5,13 @@ set(CMAKE_CXX_STANDARD 20) add_executable(partridge_cpp main.cc) + +include(CTest) + +if(BUILD_TESTING) + add_executable(partridge_tests + tests/tests.cc) + add_test(NAME validator COMMAND partridge_tests validator) + add_test(NAME construction COMMAND partridge_tests construction) + add_test(NAME solver-small COMMAND partridge_tests solver-small) +endif() diff --git a/README.md b/README.md index 6bb30cf..3d49145 100644 --- a/README.md +++ b/README.md @@ -57,4 +57,9 @@ cmake --build build ```sh N=9 # Set N to largest size of square. ./build/partridge_cpp $N -``` \ No newline at end of file +``` + +### Testing + +See [TESTING.md](./TESTING.md) for CTest, Debug, and sanitizer +instructions. diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..7a1ccef --- /dev/null +++ b/TESTING.md @@ -0,0 +1,42 @@ +# Testing + +The default test suite is deterministic and has no elapsed-time assertions. +Configure a build, compile it, and run the tests with CTest: + +```sh +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build +ctest --test-dir build --output-on-failure +``` + +The tests independently check board dimensions, square multiplicities, bounds, +overlap, and complete coverage. They cover small unsatisfiable solver inputs, a +known order-8 solution, invalid placement diagnostics, and construction of an +order-9 solution from the order-8 fixture. + +## Debug and sanitizers + +Use a separate build directory for each configuration: + +```sh +cmake -S . -B build-debug -DCMAKE_BUILD_TYPE=Debug +cmake --build build-debug +ctest --test-dir build-debug --output-on-failure + +cmake -S . -B build-asan -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \ + -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" +cmake --build build-asan +ctest --test-dir build-asan --output-on-failure + +cmake -S . -B build-ubsan -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_CXX_FLAGS="-fsanitize=undefined -fno-omit-frame-pointer" \ + -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=undefined" +cmake --build build-ubsan +ctest --test-dir build-ubsan --output-on-failure +``` + +The sanitizer flags shown are supported by Clang and GCC. Other compilers may +require different flags. Debug compilation and a feasible solver run currently +expose the pre-existing defects tracked by issues #7 and #14 respectively; the +test additions deliberately do not include fixes for those separate issues. diff --git a/main.cc b/main.cc index 050cee6..ee2c9bd 100644 --- a/main.cc +++ b/main.cc @@ -67,6 +67,9 @@ namespace { [[nodiscard]] auto length() const noexcept -> size_t { return length_; } + /** Get the square placements in this result. */ + [[nodiscard]] auto squares() const noexcept -> std::vector const & { return squares_; } + /** Output the grid. */ auto output() const -> void { std::string out(length_ * length_, '.'); @@ -303,6 +306,7 @@ namespace { } } // anon namespace +#ifndef PARTRIDGE_TESTING int main(int argc, char **argv) { auto n = (argc == 1) ? 8 : std::atol(argv[1]); auto const grid = find_solution(n); @@ -310,3 +314,4 @@ int main(int argc, char **argv) { grid.output(); return 0; } +#endif diff --git a/tests/tests.cc b/tests/tests.cc new file mode 100644 index 0000000..e683d3b --- /dev/null +++ b/tests/tests.cc @@ -0,0 +1,270 @@ +/* + * Copyright 2025, Matthew Gretton-Dann + * SPDX-License-Identifier: Apache-2.0 + */ + +#define PARTRIDGE_TESTING +#include "../main.cc" + +#include +#include +#include +#include + +namespace { + struct Placement { + std::uint64_t x; + std::uint64_t y; + std::uint64_t side; + }; + + struct Validation { + std::vector 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 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 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 multiplicities(order + 1); + std::vector 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(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 { + 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 construct_next_odd(std::uint64_t even_order, + std::vector placements) + -> std::vector { + 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_small_solver() -> int { + int failures = 0; + for (auto const order: std::array{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; + } +} + +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 == "solver-small") { + return test_small_solver(); + } + std::cerr << "unknown test: " << test << '\n'; + return 2; +} -- 2.54.0