Files
cgal/STL_Extension/include/CGAL/bisect_failures.h
T

290 lines
12 KiB
C++

// Copyright (c) 2025 GeometryFactory (France).
// All rights reserved.
//
// This file is part of CGAL (www.cgal.org).
//
// $URL$
// $Id$
// SPDX-License-Identifier: LGPL-3.0-or-later OR LicenseRef-Commercial
//
//
// Author(s) : Laurent Rineau
//
// The documentation has been partially generated using Github Copilot.
#ifndef CGAL_BISECT_FAILURES_H
#define CGAL_BISECT_FAILURES_H
#include <CGAL/config.h>
#include <CGAL/exceptions.h>
#include <CGAL/utility.h>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdlib>
#include <cstddef>
#include <cstdlib>
#include <exception>
#include <iostream>
#include <optional>
#include <string>
#include <tuple>
namespace CGAL {
/**
* \ingroup PkgSTLExtensionUtilities
* \brief Enumeration of events during the bisection process
* \sa bisect_failures()
*/
enum Bisection_event {
CURRENT_DATA = 0, ///< Current data being tested
BAD_DATA, ///< Data that reproduces the original failure
ERROR_DATA, ///< Data that causes a different failure
FINAL_BAD_DATA ///< Minimal failing data that cannot be further simplified
};
/**
* \ingroup PkgSTLExtensionUtilities
*
* \brief bisects input data by iteratively simplifying it to identify a failing case, from which no
* further elements can be removed while still failing.
*
* This debugging utility helps to reduce the size of test cases when complex input data causes failures.
* It works by iteratively simplifying the data and testing whether the failure persists,
* using a bisection-like approach to narrow down to a failing case that can not be further reduced.
*
* The algorithm divides the input data into "buckets" and systematically removes each bucket
* to test if the failure persists. It starts with a coarse granularity (ratio=0.5, removing
* half the elements) and automatically becomes more fine-grained (dividing ratio by 2) when
* no fault is found. When a failure is found, it restarts the bisection with the smaller
* failing data.
*
* \tparam InputData The type of input data to bisect (must be copyable and assignable)
* \tparam GetSize Function object type, model of `Callable` with a signature `std::size_t GetSize(const InputData& data)`
* \tparam Simplify Function object type, model of `Callable` with a signature `bool Simplify(InputData& data, std::size_t start, std::size_t end)`
* \tparam Run Function object type, model of `Callable` with a signature `int Run(const InputData& data)`
* \tparam Notify Function object type, model of `Callable` with a signature `void Notify(const InputData& data, Bisection_event event)`
*
* \param data The input data to bisect
* \param get_size Function that returns the "size" of the data (e.g., number of elements).
* \param simplify Function that simplifies the data by removing elements with indices in `[start, end)`.
* Should return `true` if simplification succeeded, `false` otherwise.
* \param run Function that tests the data. Should return 0 (`EXIT_SUCCESS`) on success, non-zero on failure.
* May also throw exceptions to indicate failure.
* \param notify Function that is called with the data at different stages. It can be used to save
* the data to a file or output. Its second parameter `event`, of type `Bisection_event`,
* can be used to name the output accordingly.
*
* \return Exit code: 0 (`EXIT_SUCCESS`) if no failures found, non-zero otherwise
*
* The algorithm:
* 1. Tests the full data first (by a call `run(data)`) to verify if it fails, and captures the
* failure pattern. If the run succeeds, returns `EXIT_SUCCESS` immediately.
* 2. Starts with a ratio of 0.5 (removing 50% of elements) and divides data into "buckets".
* 3. For each bucket,
* - creates a simplified version of the data by removing that bucket, using `simplify`,
* - call `notify(data, CURRENT_DATA)` where `data` is the simplified data,
* - and tests the simplified version by a call `run(data)`.
*
* Then:
* - If it fails with the same pattern as the original, calls `notify(data, BAD_DATA)` and restarts bisection
* with this smaller dataset and the same ratio.
* - If it fails differently, calls `notify(data, ERROR_DATA)` and continues with the next bucket.
* - If it succeeds, continue with the next bucket.
* 4. After a complete pass with no matching failures found, reduces the ratio by half (0.5 → 0.25 → 0.125...).
* 5. Repeats until no further simplification is possible (minimal failing case found).
* 6. Calls `notify(min_data, FINAL_BAD_DATA)` with the minimal failing case `min_data` and returns the result of `run(min_data)`.
*
* \warning `CGAL::bisect_failures()` requires the tested code to be compiled with
* assertions enabled. That means `NDEBUG` and `CGAL_NDEBUG` should not be defined. If `run` fails
* with a segmentation fault, This function cannot catch it and will also crash.
*
* Here is an example of how to use `CGAL::bisect_failures()`:
* \snippet STL_Extension/bisect_failures.cpp bisect_failures_snippet
*/
template<typename InputData, typename GetSize, typename Simplify, typename Run, typename Notify>
int bisect_failures(const InputData& data,
GetSize get_size,
Simplify simplify,
Run run,
Notify notify)
{
// Wrapper to call `notify` with `Bisection_event` or `std::string` as second argument, for backward compatibility
auto notify_wrapper = [&](const InputData& data, Bisection_event event) {
if constexpr (std::is_invocable_v<Notify, const InputData&, Bisection_event>) {
return notify(data, event);
} else {
static constexpr std::array<const char* const, 4> event_name = {
"current",
"bad",
"error",
"final_bad"
};
std::string filename_prefix = event_name[static_cast<std::size_t>(event)];
return notify(data, filename_prefix);
}
};
// Redirect temporarily cout to cerr, and clog to stdout, for debug output
auto* old_clog_buf = std::clog.rdbuf();
auto* old_cout_buf = std::cout.rdbuf();
auto* old_cerr_tie = std::cerr.tie();
auto _ = make_scope_exit([&]() {
std::clog.rdbuf(old_clog_buf);
std::cout.rdbuf(old_cout_buf);
std::cerr.tie(old_cerr_tie);
});
std::clog.rdbuf(old_cout_buf);
std::cout.rdbuf(std::cerr.rdbuf());
std::cerr.tie(&std::clog);
// The following code uses std::clog to display its messages, and the user's
// code (run) usages of std::cout will go to std::cerr.
auto do_run = [&](InputData current_data) {
std::optional<Failure_exception> cgal_exc;
std::optional<std::string> std_exc_msg;
int exit_code = EXIT_SUCCESS;
try {
exit_code = run(current_data);
} catch(Failure_exception& e) {
cgal_exc = e;
std::clog << " CAUGHT CGAL EXCEPTION: " << e.what() << '\n';
} catch(std::exception& e) {
std::clog << " CAUGHT EXCEPTION: " << e.what() << '\n';
std_exc_msg = e.what();
}
if(exit_code != EXIT_SUCCESS)
std::clog << " RUN RETURNED EXIT CODE: " << exit_code << '\n';
return std::make_tuple(cgal_exc, std_exc_msg, exit_code);
};
std::clog << "First run of the algorithm on full data, to check how it fails\n";
auto [initial_cgal_exception, initial_std_exception, initial_exit_code] = do_run(data);
if(!initial_cgal_exception && !initial_std_exception && initial_exit_code == EXIT_SUCCESS) {
std::clog << "Initial run succeeded, no failure to bisect\n";
return EXIT_SUCCESS;
}
double ratio = 0.5; // Start with removing half the elements
InputData bad_data{data};
InputData working_data{data};
int exit_code = EXIT_SUCCESS;
while(true) {
std::size_t nb_buckets = static_cast<std::size_t>(std::floor(1.0 / ratio)) + 1;
std::clog << "RATIO: " << ratio << '\n';
bool found_fault_this_pass = false;
std::size_t nb_to_skip = 0;
for(std::size_t bucket = 0; bucket < nb_buckets;) {
const auto data_size = get_size(working_data);
nb_to_skip = static_cast<std::size_t>(std::round(data_size * ratio));
if(nb_to_skip < 1) {
nb_to_skip = 1;
nb_buckets = data_size;
}
const auto start = (std::min)(bucket * nb_to_skip, data_size);
const auto end = (std::min)(start + nb_to_skip, data_size);
std::clog << " SKIP " << (end - start) << " from " << start << " to " << end << '\n';
// Try to simplify the data
if(simplify(working_data, start, end)) {
const auto new_size = get_size(working_data);
std::clog << " size after simplification: " << new_size << '\n';
if(new_size >= data_size) {
std::clog << " ERROR: could not simplify data\n";
working_data = bad_data;
++bucket;
continue;
}
// Save current state
notify_wrapper(working_data, CURRENT_DATA);
auto [cgal_exception, std_exception, this_run_exit_code] = do_run(working_data);
bool same_exception = false;
bool same_exit_code = false;
if(cgal_exception) {
if(initial_cgal_exception &&
cgal_exception->expression() == initial_cgal_exception->expression() &&
cgal_exception->library() == initial_cgal_exception->library() &&
cgal_exception->filename() == initial_cgal_exception->filename() &&
cgal_exception->line_number() == initial_cgal_exception->line_number())
{
same_exception = true;
}
} else if(std_exception) {
// Check if this is the same type of failure we're looking for
if(initial_std_exception &&
*initial_std_exception == *std_exception)
{
same_exception = true;
}
} else if(this_run_exit_code != EXIT_SUCCESS) {
if(this_run_exit_code == initial_exit_code) {
same_exit_code = true;
}
}
if(this_run_exit_code != EXIT_SUCCESS) {
exit_code = this_run_exit_code;
}
if(same_exception || same_exit_code) {
std::clog << " -> BAD DATA! (size: " << get_size(working_data) << ")\n";
notify_wrapper(working_data, BAD_DATA);
bad_data = working_data;
found_fault_this_pass = true;
bucket = 0; // Reset to bisect further
continue;
}
if (cgal_exception || std_exception || this_run_exit_code != EXIT_SUCCESS) {
// Different type of error - log it but continue
if(exit_code == EXIT_SUCCESS) exit_code = EXIT_FAILURE;
std::clog << " -> ERROR DATA (different error type)\n";
notify_wrapper(working_data, ERROR_DATA);
std::clog << " go on...\n";
} else {
std::clog << " -> GOOD DATA :-( (size: " << get_size(working_data) << ")\n";
}
}
// Reset to bad_data for next iteration
working_data = bad_data;
++bucket;
}
// After completing a full pass through all buckets
if(!found_fault_this_pass) {
if(nb_to_skip <= 1) {
// Cannot subdivide further - we've found the minimal failing case
break;
}
// No fault found at this ratio - make ratio smaller (more granular)
ratio = ratio / 2.0;
nb_buckets = static_cast<std::size_t>(std::floor(1.0 / ratio)) + 1;
std::clog << " No fault found at this ratio. Reducing ratio to: " << ratio << '\n';
}
}
if(get_size(bad_data) < get_size(data)) {
std::clog << "FINAL BAD DATA: " << get_size(bad_data) << " elements\n";
notify_wrapper(bad_data, FINAL_BAD_DATA);
return run(bad_data);
}
return exit_code;
}
} // namespace CGAL
#endif // CGAL_BISECT_FAILURES_H