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
This commit was merged in pull request #17.
This commit is contained in:
@@ -5,3 +5,13 @@ set(CMAKE_CXX_STANDARD 20)
|
|||||||
|
|
||||||
add_executable(partridge_cpp
|
add_executable(partridge_cpp
|
||||||
main.cc)
|
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()
|
||||||
|
|||||||
@@ -57,4 +57,9 @@ cmake --build build
|
|||||||
```sh
|
```sh
|
||||||
N=9 # Set N to largest size of square.
|
N=9 # Set N to largest size of square.
|
||||||
./build/partridge_cpp $N
|
./build/partridge_cpp $N
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
See [TESTING.md](./TESTING.md) for CTest, Debug, and sanitizer
|
||||||
|
instructions.
|
||||||
|
|||||||
+42
@@ -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.
|
||||||
@@ -67,6 +67,9 @@ namespace {
|
|||||||
|
|
||||||
[[nodiscard]] auto length() const noexcept -> size_t { return length_; }
|
[[nodiscard]] auto length() const noexcept -> size_t { return length_; }
|
||||||
|
|
||||||
|
/** Get the square placements in this result. */
|
||||||
|
[[nodiscard]] auto squares() const noexcept -> std::vector<Square> const & { return squares_; }
|
||||||
|
|
||||||
/** Output the grid. */
|
/** Output the grid. */
|
||||||
auto output() const -> void {
|
auto output() const -> void {
|
||||||
std::string out(length_ * length_, '.');
|
std::string out(length_ * length_, '.');
|
||||||
@@ -303,6 +306,7 @@ namespace {
|
|||||||
}
|
}
|
||||||
} // anon namespace
|
} // anon namespace
|
||||||
|
|
||||||
|
#ifndef PARTRIDGE_TESTING
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
auto n = (argc == 1) ? 8 : std::atol(argv[1]);
|
auto n = (argc == 1) ? 8 : std::atol(argv[1]);
|
||||||
auto const grid = find_solution(n);
|
auto const grid = find_solution(n);
|
||||||
@@ -310,3 +314,4 @@ int main(int argc, char **argv) {
|
|||||||
grid.output();
|
grid.output();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
+270
@@ -0,0 +1,270 @@
|
|||||||
|
/*
|
||||||
|
* 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 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_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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user