solver: break board dihedral symmetry #25
@@ -18,6 +18,9 @@ the probe renders into an in-memory stream so grids do not perturb terminal I/O.
|
||||
Override the policy with `--orders`, `--warmup`, `--repetitions`, `--timeout`,
|
||||
and `--candidate-order`. The choices are `ascending`, `descending`, and
|
||||
`best-fit`; ascending candidate sizes are the production default.
|
||||
The production default also removes equivalent D4 board orientations by
|
||||
constraining the unique unit square. Pass `--no-symmetry` to obtain an
|
||||
otherwise identical unconstrained baseline.
|
||||
Order 9 uses the constructive odd-order path, searching order 8 and then tiling
|
||||
the enlarged border, so it is suitable for normal local benchmarking:
|
||||
|
||||
@@ -69,6 +72,50 @@ Do not use wall-clock thresholds as correctness checks. Keep the generated
|
||||
JSON outside version control unless it is being deliberately added as a named
|
||||
comparison baseline.
|
||||
|
||||
## D4 board symmetry
|
||||
|
||||
Every solution contains exactly one 1-by-1 square. Rotations and reflections
|
||||
of the whole board preserve square sizes, multiplicities, and coverage, so the
|
||||
unit square can select the orientation without assigning identities to any of
|
||||
the repeated larger squares. For a board of width `W`, the solver accepts the
|
||||
unit square only in the closed fundamental triangle
|
||||
`x <= y <= floor((W - 1) / 2)`. Reflecting a cell toward the left edge,
|
||||
swapping its coordinates if necessary, and reflecting toward the top edge
|
||||
maps every D4 orbit into this triangle. Closed diagonal and midline
|
||||
boundaries retain the smaller orbits of symmetric cells.
|
||||
|
||||
The check is made only when the skyline search is ready to place the unit
|
||||
square, so it never rejects a partial state before the square's position is
|
||||
decidable. The implementation remains compile-time counter-free in normal
|
||||
solver calls and keeps the single-threaded deterministic search policy.
|
||||
Instrumented runs count examined and rejected unit-square placements as prune
|
||||
checks and hits.
|
||||
|
||||
Measurements used the issue #3 dirty working tree based on commit `74bde26`,
|
||||
Apple Clang 21.0.0, `-O3 -DNDEBUG`, macOS arm64, one worker, one warm-up, and
|
||||
five sequential measured repetitions. All results passed the benchmark's
|
||||
independent cell-coverage validator and counts were stable:
|
||||
|
||||
| Route | D4 constraint | Median solve (range) | Nodes | Prune hits |
|
||||
| --- | --- | ---: | ---: | ---: |
|
||||
| Order 8 public | enabled | 0.245 s (0.244-0.248 s) | 2,931,203 | 1,329,567 |
|
||||
| Order 8 public | disabled | 0.679 s (0.659-0.709 s) | 7,735,369 | 0 |
|
||||
| Order 9 direct | enabled | 1.705 s (1.671-1.740 s) | 16,231,918 | 6,755,171 |
|
||||
| Order 9 direct | disabled | 4.542 s (4.482-4.662 s) | 45,840,266 | 0 |
|
||||
|
||||
Thus the constraint reduced nodes by 62% for order 8 and 65% for direct order
|
||||
9; counted median solve time fell by 64% and 62%, respectively.
|
||||
|
||||
A public order-10 probe with the D4 constraint, ascending policy, and a
|
||||
45-second per-process bound did not complete. Public order 11 first performs
|
||||
that identical order-10 core search and only then adds its inexpensive odd
|
||||
border, so adding either order to routine correctness tests would duplicate
|
||||
the same unresolved bottleneck. They remain useful opt-in heavyweight
|
||||
benchmark targets with explicit timeouts. A direct order-11 benchmark is a
|
||||
different experiment: it bypasses the public odd construction and searches
|
||||
the larger core itself, so it must not be presented as public order-11
|
||||
performance.
|
||||
|
||||
## Smallest-valley skyline
|
||||
|
||||
The solver stores one filled height per board column instead of one value per
|
||||
|
||||
@@ -25,6 +25,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME solver-completion COMMAND partridge_tests solver-completion)
|
||||
add_test(NAME search-counters COMMAND partridge_tests search-counters)
|
||||
add_test(NAME skyline-search COMMAND partridge_tests skyline-search)
|
||||
add_test(NAME d4-symmetry COMMAND partridge_tests d4-symmetry)
|
||||
find_package(Python3 COMPONENTS Interpreter)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME benchmark-format
|
||||
|
||||
+5
-1
@@ -18,7 +18,11 @@ check smallest-width valley selection and deterministic tie-breaking, validate
|
||||
an order-8 result with descending candidates, and independently validate a
|
||||
direct order-9 search with ascending candidates. An
|
||||
exhaustive infeasible order also checks that all three policies visit the same
|
||||
search space. The direct order-9 test is deliberately separate from the
|
||||
search space. Focused D4 tests cover all eight rotations and reflections,
|
||||
even and odd midlines, diagonals, corners, and the odd-board centre. They
|
||||
independently validate order-8 solutions with symmetry enabled and disabled
|
||||
and check that only the enabled search records symmetry pruning. The direct
|
||||
order-9 test is deliberately separate from the
|
||||
public order-9 route, which uses even-predecessor construction. Orders 10 and
|
||||
11 are not routine tests because both public routes require the same long
|
||||
direct order-10 search; the route-boundary test still checks that order 11
|
||||
|
||||
+22
-7
@@ -28,6 +28,7 @@ namespace {
|
||||
|
||||
auto solve(std::uint64_t order, bool instrument,
|
||||
SearchPolicy policy, bool direct_search,
|
||||
SymmetryBreaking symmetry,
|
||||
SearchCounters &counters)
|
||||
-> TimedSolution {
|
||||
auto const predecessor_order =
|
||||
@@ -36,8 +37,8 @@ namespace {
|
||||
auto predecessor =
|
||||
instrument
|
||||
? search_solution_instrumented(predecessor_order, counters,
|
||||
policy)
|
||||
: search_solution(predecessor_order, policy);
|
||||
policy, symmetry)
|
||||
: search_solution(predecessor_order, policy, symmetry);
|
||||
auto const search_end = Clock::now();
|
||||
|
||||
auto const construction_begin = Clock::now();
|
||||
@@ -93,9 +94,10 @@ namespace {
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 3 || argc > 5) {
|
||||
if (argc < 3 || argc > 6) {
|
||||
std::cerr << "usage: partridge_benchmark ORDER counters|plain "
|
||||
"[ascending|descending|best-fit] [public|direct]\n";
|
||||
"[ascending|descending|best-fit] [public|direct] "
|
||||
"[d4|none]\n";
|
||||
return 2;
|
||||
}
|
||||
auto const order = static_cast<std::uint64_t>(std::strtoull(argv[1], nullptr, 10));
|
||||
@@ -117,15 +119,25 @@ int main(int argc, char **argv) {
|
||||
return 2;
|
||||
}
|
||||
auto const direct_search =
|
||||
argc == 5 && std::string_view(argv[4]) == "direct";
|
||||
if (argc == 5 && std::string_view(argv[4]) != "public" &&
|
||||
argc >= 5 && std::string_view(argv[4]) == "direct";
|
||||
if (argc >= 5 && std::string_view(argv[4]) != "public" &&
|
||||
std::string_view(argv[4]) != "direct") {
|
||||
std::cerr << "search route must be public or direct\n";
|
||||
return 2;
|
||||
}
|
||||
auto const symmetry =
|
||||
argc < 6 || std::string_view(argv[5]) == "d4"
|
||||
? SymmetryBreaking::d4_unit_square
|
||||
: SymmetryBreaking::disabled;
|
||||
if (argc == 6 && std::string_view(argv[5]) != "d4" &&
|
||||
std::string_view(argv[5]) != "none") {
|
||||
std::cerr << "symmetry mode must be d4 or none\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
SearchCounters counters;
|
||||
auto timed = solve(order, instrument, policy, direct_search, counters);
|
||||
auto timed =
|
||||
solve(order, instrument, policy, direct_search, symmetry, counters);
|
||||
|
||||
auto const validation_begin = Clock::now();
|
||||
auto const validation_ok = valid(order, timed.result);
|
||||
@@ -150,6 +162,9 @@ int main(int argc, char **argv) {
|
||||
<< "\""
|
||||
<< ",\"search_route\":\""
|
||||
<< (direct_search ? "direct" : "public") << "\""
|
||||
<< ",\"symmetry_breaking\":\""
|
||||
<< (symmetry == SymmetryBreaking::d4_unit_square ? "d4" : "none")
|
||||
<< "\""
|
||||
<< ",\"solved\":" << boolean(!timed.result.squares().empty())
|
||||
<< ",\"valid\":" << boolean(validation_ok)
|
||||
<< ",\"timing_seconds\":{\"solve\":" << timed.search_seconds
|
||||
|
||||
+19
-2
@@ -79,11 +79,20 @@ def environment(binary):
|
||||
}
|
||||
|
||||
|
||||
def run_once(binary, order, mode, search_policy, search_route, timeout):
|
||||
def run_once(
|
||||
binary, order, mode, search_policy, search_route, symmetry, timeout
|
||||
):
|
||||
started = time.monotonic()
|
||||
try:
|
||||
process = subprocess.run(
|
||||
[str(binary), str(order), mode, search_policy, search_route],
|
||||
[
|
||||
str(binary),
|
||||
str(order),
|
||||
mode,
|
||||
search_policy,
|
||||
search_route,
|
||||
symmetry,
|
||||
],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -223,6 +232,11 @@ def main():
|
||||
default="ascending",
|
||||
)
|
||||
parser.add_argument("--direct-search", action="store_true")
|
||||
parser.add_argument(
|
||||
"--no-symmetry",
|
||||
action="store_true",
|
||||
help="disable unique-unit-square D4 symmetry breaking",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--measure-overhead",
|
||||
action="store_true",
|
||||
@@ -254,6 +268,7 @@ def main():
|
||||
mode,
|
||||
args.search_policy,
|
||||
"direct" if args.direct_search else "public",
|
||||
"none" if args.no_symmetry else "d4",
|
||||
args.timeout,
|
||||
)
|
||||
for trial in range(args.repetitions):
|
||||
@@ -265,6 +280,7 @@ def main():
|
||||
mode,
|
||||
args.search_policy,
|
||||
"direct" if args.direct_search else "public",
|
||||
"none" if args.no_symmetry else "d4",
|
||||
args.timeout,
|
||||
)
|
||||
)
|
||||
@@ -293,6 +309,7 @@ def main():
|
||||
"per_run_timeout_seconds": args.timeout,
|
||||
"candidate_order": args.search_policy,
|
||||
"search_route": "direct" if args.direct_search else "public",
|
||||
"symmetry_breaking": "none" if args.no_symmetry else "d4",
|
||||
"stdout": "captured; rendered grid suppressed by probe",
|
||||
},
|
||||
"cases": cases,
|
||||
|
||||
@@ -162,6 +162,29 @@ namespace {
|
||||
best_fit,
|
||||
};
|
||||
|
||||
/** Whether to remove equivalent board orientations from the search. */
|
||||
enum class SymmetryBreaking {
|
||||
disabled,
|
||||
d4_unit_square,
|
||||
};
|
||||
|
||||
/** Return whether a cell is the canonical representative of its D4 orbit.
|
||||
*
|
||||
* Reflect a cell into the left half of the board, rotate so its distance
|
||||
* from the left edge is no greater than its distance from the top edge, and
|
||||
* finally reflect it into the top half. The resulting fundamental region is
|
||||
* the closed triangle x <= y <= floor((length - 1) / 2). Closed boundaries
|
||||
* retain representatives whose orbit is smaller than eight.
|
||||
*/
|
||||
[[nodiscard]] auto canonical_unit_position(size_t const x,
|
||||
size_t const y,
|
||||
size_t const length) noexcept
|
||||
-> bool {
|
||||
assert(x < length);
|
||||
assert(y < length);
|
||||
return x <= y && y <= (length - 1) / 2;
|
||||
}
|
||||
|
||||
/** A maximal level skyline segment which is lower than its neighbours. */
|
||||
struct Valley {
|
||||
size_t x;
|
||||
@@ -200,7 +223,7 @@ namespace {
|
||||
return best;
|
||||
}
|
||||
|
||||
template<bool Instrument>
|
||||
template<bool Instrument, bool BreakD4Symmetry>
|
||||
auto search_skyline(size_t const n, size_t const length,
|
||||
SearchPolicy const policy,
|
||||
std::vector<size_t> &skyline, Avail &available,
|
||||
@@ -226,6 +249,20 @@ namespace {
|
||||
return false;
|
||||
}
|
||||
|
||||
if constexpr (BreakD4Symmetry) {
|
||||
if (side == 1) {
|
||||
if constexpr (Instrument) {
|
||||
++counters->prune_checks;
|
||||
}
|
||||
if (!canonical_unit_position(valley.x, valley.height, length)) {
|
||||
if constexpr (Instrument) {
|
||||
++counters->prune_hits;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (Instrument) {
|
||||
++counters->attempted_placements;
|
||||
}
|
||||
@@ -234,8 +271,8 @@ namespace {
|
||||
side, valley.height + side);
|
||||
squares.emplace_back(valley.x + valley.height * length, side);
|
||||
|
||||
if (search_skyline<Instrument>(n, length, policy, skyline,
|
||||
available, squares, counters)) {
|
||||
if (search_skyline<Instrument, BreakD4Symmetry>(
|
||||
n, length, policy, skyline, available, squares, counters)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -281,7 +318,7 @@ namespace {
|
||||
* candidates and updating up to n columns for each costs O(n^2), for
|
||||
* O(board width + n^2) local work per node and the same total state.
|
||||
*/
|
||||
template<bool Instrument>
|
||||
template<bool Instrument, bool BreakD4Symmetry>
|
||||
auto search_solution_impl(size_t const n, SearchPolicy const policy,
|
||||
SearchCounters *const counters) noexcept
|
||||
-> Results {
|
||||
@@ -293,7 +330,7 @@ namespace {
|
||||
}
|
||||
std::vector<Square> squares;
|
||||
squares.reserve(length);
|
||||
static_cast<void>(search_skyline<Instrument>(
|
||||
static_cast<void>(search_skyline<Instrument, BreakD4Symmetry>(
|
||||
n, length, policy, skyline, available, squares, counters));
|
||||
|
||||
return {length, std::move(squares)};
|
||||
@@ -301,18 +338,28 @@ namespace {
|
||||
|
||||
auto search_solution(
|
||||
size_t const n,
|
||||
SearchPolicy const policy = SearchPolicy::ascending) noexcept
|
||||
SearchPolicy const policy = SearchPolicy::ascending,
|
||||
SymmetryBreaking const symmetry =
|
||||
SymmetryBreaking::d4_unit_square) noexcept
|
||||
-> Results {
|
||||
return search_solution_impl<false>(n, policy, nullptr);
|
||||
if (symmetry == SymmetryBreaking::d4_unit_square) {
|
||||
return search_solution_impl<false, true>(n, policy, nullptr);
|
||||
}
|
||||
return search_solution_impl<false, false>(n, policy, nullptr);
|
||||
}
|
||||
|
||||
auto search_solution_instrumented(size_t const n,
|
||||
SearchCounters &counters,
|
||||
SearchPolicy const policy =
|
||||
SearchPolicy::ascending) noexcept
|
||||
SearchPolicy::ascending,
|
||||
SymmetryBreaking const symmetry =
|
||||
SymmetryBreaking::d4_unit_square) noexcept
|
||||
-> Results {
|
||||
counters = {};
|
||||
return search_solution_impl<true>(n, policy, &counters);
|
||||
if (symmetry == SymmetryBreaking::d4_unit_square) {
|
||||
return search_solution_impl<true, true>(n, policy, &counters);
|
||||
}
|
||||
return search_solution_impl<true, false>(n, policy, &counters);
|
||||
}
|
||||
|
||||
/** Construct an odd-order solution from its even-order predecessor. */
|
||||
@@ -347,23 +394,29 @@ namespace {
|
||||
|
||||
auto find_solution(
|
||||
size_t const n,
|
||||
SearchPolicy const policy = SearchPolicy::ascending) noexcept -> Results {
|
||||
SearchPolicy const policy = SearchPolicy::ascending,
|
||||
SymmetryBreaking const symmetry =
|
||||
SymmetryBreaking::d4_unit_square) noexcept -> Results {
|
||||
if (uses_odd_construction(n)) {
|
||||
return construct_odd_solution(n, search_solution(n - 1, policy));
|
||||
return construct_odd_solution(
|
||||
n, search_solution(n - 1, policy, symmetry));
|
||||
}
|
||||
return search_solution(n, policy);
|
||||
return search_solution(n, policy, symmetry);
|
||||
}
|
||||
|
||||
auto find_solution_instrumented(size_t const n,
|
||||
SearchCounters &counters,
|
||||
SearchPolicy const policy =
|
||||
SearchPolicy::ascending) noexcept
|
||||
SearchPolicy::ascending,
|
||||
SymmetryBreaking const symmetry =
|
||||
SymmetryBreaking::d4_unit_square) noexcept
|
||||
-> Results {
|
||||
if (uses_odd_construction(n)) {
|
||||
return construct_odd_solution(
|
||||
n, search_solution_instrumented(n - 1, counters, policy));
|
||||
n, search_solution_instrumented(
|
||||
n - 1, counters, policy, symmetry));
|
||||
}
|
||||
return search_solution_instrumented(n, counters, policy);
|
||||
return search_solution_instrumented(n, counters, policy, symmetry);
|
||||
}
|
||||
} // anon namespace
|
||||
|
||||
|
||||
+122
-3
@@ -166,6 +166,35 @@ namespace {
|
||||
});
|
||||
}
|
||||
|
||||
auto d4_orbit(Placement const &placement, std::uint64_t const length)
|
||||
-> std::array<Placement, 8> {
|
||||
auto const far_x = length - 1 - placement.x;
|
||||
auto const far_y = length - 1 - placement.y;
|
||||
return {{
|
||||
{placement.x, placement.y, placement.side},
|
||||
{far_y, placement.x, placement.side},
|
||||
{far_x, far_y, placement.side},
|
||||
{placement.y, far_x, placement.side},
|
||||
{far_x, placement.y, placement.side},
|
||||
{far_y, far_x, placement.side},
|
||||
{placement.x, far_y, placement.side},
|
||||
{placement.y, placement.x, placement.side},
|
||||
}};
|
||||
}
|
||||
|
||||
auto canonical_members(Placement const &placement, std::uint64_t const length)
|
||||
-> std::vector<Placement> {
|
||||
auto orbit = d4_orbit(placement, length);
|
||||
std::vector<Placement> members;
|
||||
for (auto const &member: orbit) {
|
||||
if (canonical_unit_position(member.x, member.y, length) &&
|
||||
std::ranges::find(members, member) == members.end()) {
|
||||
members.push_back(member);
|
||||
}
|
||||
}
|
||||
return members;
|
||||
}
|
||||
|
||||
auto test_validator() -> int {
|
||||
int failures = 0;
|
||||
auto const valid = known_order_8();
|
||||
@@ -337,11 +366,98 @@ namespace {
|
||||
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 &&
|
||||
failures += expect(counters.prune_checks >= counters.prune_hits &&
|
||||
counters.generated_tasks == 0 &&
|
||||
counters.completed_tasks == 0,
|
||||
"unimplemented solver counters were not zero");
|
||||
"search counters are inconsistent");
|
||||
return failures;
|
||||
}
|
||||
|
||||
auto test_d4_symmetry() -> int {
|
||||
int failures = 0;
|
||||
|
||||
// A generic cell has eight distinct images: identity, three rotations,
|
||||
// and four reflected rotations. Exactly one must survive.
|
||||
auto const generic = d4_orbit({5, 9, 1}, 36);
|
||||
auto distinct_generic = std::vector<Placement>(generic.begin(),
|
||||
generic.end());
|
||||
std::ranges::sort(
|
||||
distinct_generic, {}, [](Placement const &placement) {
|
||||
return std::array{placement.x, placement.y};
|
||||
});
|
||||
distinct_generic.erase(
|
||||
std::ranges::unique(distinct_generic).begin(),
|
||||
distinct_generic.end());
|
||||
failures += expect(distinct_generic.size() == generic.size(),
|
||||
"generic D4 test point does not have eight images");
|
||||
for (std::size_t index = 0; index < generic.size(); ++index) {
|
||||
failures += expect(
|
||||
canonical_members(generic[index], 36) ==
|
||||
std::vector<Placement>{{5, 9, 1}},
|
||||
"D4 transform " + std::to_string(index) +
|
||||
" did not select the same canonical representative");
|
||||
}
|
||||
|
||||
// Closed boundaries are important because diagonal, midline, corner, and
|
||||
// centre cells have non-trivial stabilizers and therefore smaller orbits.
|
||||
for (auto const test_case:
|
||||
std::array<std::pair<std::uint64_t, Placement>, 8>{{
|
||||
{36, {0, 0, 1}},
|
||||
{36, {0, 17, 1}},
|
||||
{36, {7, 7, 1}},
|
||||
{36, {7, 17, 1}},
|
||||
{45, {0, 22, 1}},
|
||||
{45, {11, 11, 1}},
|
||||
{45, {11, 22, 1}},
|
||||
{45, {22, 22, 1}},
|
||||
}}) {
|
||||
failures += expect(
|
||||
canonical_members(test_case.second, test_case.first).size() == 1,
|
||||
"boundary orbit did not retain exactly one distinct representative");
|
||||
}
|
||||
failures += expect(
|
||||
canonical_unit_position(7, 17, 36) &&
|
||||
!canonical_unit_position(8, 7, 36) &&
|
||||
!canonical_unit_position(7, 18, 36) &&
|
||||
canonical_unit_position(22, 22, 45) &&
|
||||
!canonical_unit_position(22, 23, 45),
|
||||
"canonical triangle mishandled a diagonal or midline boundary");
|
||||
|
||||
SearchCounters enabled_counters;
|
||||
SearchCounters disabled_counters;
|
||||
auto const enabled = search_solution_instrumented(
|
||||
8, enabled_counters, SearchPolicy::ascending,
|
||||
SymmetryBreaking::d4_unit_square);
|
||||
auto const disabled = search_solution_instrumented(
|
||||
8, disabled_counters, SearchPolicy::ascending,
|
||||
SymmetryBreaking::disabled);
|
||||
auto enabled_validation = validate(8, enabled);
|
||||
auto disabled_validation = validate(8, disabled);
|
||||
failures += expect(
|
||||
enabled_validation.valid(),
|
||||
"symmetry-enabled solution failed independent validation:\n" +
|
||||
enabled_validation.text());
|
||||
failures += expect(
|
||||
disabled_validation.valid(),
|
||||
"symmetry-disabled solution failed independent validation:\n" +
|
||||
disabled_validation.text());
|
||||
|
||||
auto const unit = std::ranges::find_if(
|
||||
enabled.squares(),
|
||||
[](Square const &square) { return square.length() == 1; });
|
||||
failures += expect(
|
||||
unit != enabled.squares().end() &&
|
||||
canonical_unit_position(unit->pos() % enabled.length(),
|
||||
unit->pos() / enabled.length(),
|
||||
enabled.length()),
|
||||
"symmetry-enabled search placed the unique unit square outside the "
|
||||
"canonical region");
|
||||
failures += expect(
|
||||
enabled_counters.prune_checks > 0 && enabled_counters.prune_hits > 0 &&
|
||||
disabled_counters.prune_checks == 0 &&
|
||||
disabled_counters.prune_hits == 0,
|
||||
"symmetry instrumentation did not distinguish enabled and disabled "
|
||||
"search");
|
||||
return failures;
|
||||
}
|
||||
|
||||
@@ -440,6 +556,9 @@ int main(int argc, char **argv) {
|
||||
if (test == "skyline-search") {
|
||||
return test_skyline_search();
|
||||
}
|
||||
if (test == "d4-symmetry") {
|
||||
return test_d4_symmetry();
|
||||
}
|
||||
std::cerr << "unknown test: " << test << '\n';
|
||||
return 2;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user