diff --git a/cpp/include/cuopt/mathematical_optimization/cuopt_c.h b/cpp/include/cuopt/mathematical_optimization/cuopt_c.h index edb2188503..bc3f38e439 100644 --- a/cpp/include/cuopt/mathematical_optimization/cuopt_c.h +++ b/cpp/include/cuopt/mathematical_optimization/cuopt_c.h @@ -828,6 +828,44 @@ cuopt_int_t cuOptGetFloatParameter(cuOptSolverSettings settings, const char* parameter_name, cuopt_float_t* parameter_value); +/** + * @brief Type of callback invoked once per standard solver log line. + * + * Receives the same lines the solver would print to the console — nothing more. + * Internal diagnostics (debug and trace messages) are never delivered, and no + * severity is reported: the callback exists to display or forward solver + * output, not to let callers classify or branch on it. + * + * @param message Null-terminated log line without trailing newline. + * @param user_data Opaque pointer passed to cuOptSetLogCallback. + * + * @note Invoked from the calling thread for a local solve, and from an internal + * log-streaming thread when the solve runs on a remote server. Do not call back + * into cuOpt from inside the callback. + * @warning Log message formatting is not part of the stable API and may change + * between releases. The callback is intended for display purposes (GUI integration, + * log forwarding, stdout capture) — do not parse message content for programmatic + * control flow. + */ +typedef void (*cuOptLogCallback)(const char* message, void* user_data); + +/** + * @brief Register a callback to receive solver log messages. + * + * The callback is invoked once per log line. It is called in addition to any + * file or console sink already enabled via ``log_to_console`` / ``log_file`` + * parameters. Pass NULL to remove a previously registered callback. + * + * @param[in] settings The solver settings object. + * @param[in] callback Callback function, or NULL to clear. + * @param[in] user_data Opaque pointer forwarded to the callback unchanged. + * + * @return A status code indicating success or failure. + */ +cuopt_int_t cuOptSetLogCallback(cuOptSolverSettings settings, + cuOptLogCallback callback, + void* user_data); + /** * @brief Type of callback for receiving incumbent MIP solutions with user context. * diff --git a/cpp/src/grpc/client/solve_remote.cpp b/cpp/src/grpc/client/solve_remote.cpp index eabea39e05..45d97f73ee 100644 --- a/cpp/src/grpc/client/solve_remote.cpp +++ b/cpp/src/grpc/client/solve_remote.cpp @@ -86,13 +86,18 @@ std::unique_ptr> solve_lp_remote( } bool want_console = settings.log_to_console; bool want_file = log_file_stream && log_file_stream->is_open(); - - if (want_console || want_file) { - config.stream_logs = true; - config.log_callback = [want_console, want_file, &log_file_stream](const std::string& line) { - if (want_console) { std::cout << line << std::endl; } - if (want_file) { *log_file_stream << line << std::endl; } - }; + // Captured here, not read inside the lambda: the streaming thread carries no + // registration of its own. + auto user_cb = cuopt::current_log_callback(); + + if (want_console || want_file || user_cb.callback) { + config.stream_logs = true; + config.log_callback = + [want_console, want_file, &log_file_stream, user_cb](const std::string& line) { + if (want_console) { std::cout << line << std::endl; } + if (want_file) { *log_file_stream << line << std::endl; } + if (user_cb.callback) { user_cb.callback(line.c_str(), user_cb.user_data); } + }; } // Create client and connect @@ -139,13 +144,16 @@ std::unique_ptr> solve_mip_remote( } bool want_console = settings.log_to_console; bool want_file = log_file_stream && log_file_stream->is_open(); - - if (want_console || want_file) { - config.stream_logs = true; - config.log_callback = [want_console, want_file, &log_file_stream](const std::string& line) { - if (want_console) { std::cout << line << std::endl; } - if (want_file) { *log_file_stream << line << std::endl; } - }; + auto user_cb = cuopt::current_log_callback(); + + if (want_console || want_file || user_cb.callback) { + config.stream_logs = true; + config.log_callback = + [want_console, want_file, &log_file_stream, user_cb](const std::string& line) { + if (want_console) { std::cout << line << std::endl; } + if (want_file) { *log_file_stream << line << std::endl; } + if (user_cb.callback) { user_cb.callback(line.c_str(), user_cb.user_data); } + }; } // Check if user has set incumbent callbacks diff --git a/cpp/src/mip_heuristics/diversity/diversity_manager.cu b/cpp/src/mip_heuristics/diversity/diversity_manager.cu index ec82c4b423..0103c8764b 100644 --- a/cpp/src/mip_heuristics/diversity/diversity_manager.cu +++ b/cpp/src/mip_heuristics/diversity/diversity_manager.cu @@ -212,7 +212,7 @@ void diversity_manager_t::add_user_given_solutions( *problem_ptr->original_problem_ptr, h_original, h_crushed); init_sol_assignment = cuopt::device_copy(h_crushed, sol.handle_ptr->get_stream()); -#if CUOPT_LOG_ACTIVE_LEVEL <= CUOPT_LOG_LEVEL_DEBUG +#if CUOPT_LOG_ACTIVE_LEVEL <= RAPIDS_LOGGER_LOG_LEVEL_DEBUG const auto& reduced_problem = *problem_ptr->original_problem_ptr; const std::vector h_red_obj = reduced_problem.get_objective_coefficients_host(); const std::vector& h_ori_obj = presolver_ptr->get_original_objective_coefficients(); diff --git a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu index 4efd73e454..3709d42109 100644 --- a/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu +++ b/cpp/src/mip_heuristics/feasibility_jump/feasibility_jump.cu @@ -946,7 +946,7 @@ i_t fj_t::host_loop(solution_t& solution, i_t climber_idx) } } } -#if CUOPT_LOG_ACTIVE_LEVEL == CUOPT_LOG_LEVEL_TRACE +#if CUOPT_LOG_ACTIVE_LEVEL == RAPIDS_LOGGER_LOG_LEVEL_TRACE auto h_sol = cuopt::host_copy(solution.assignment, climber_stream); static std::set> solutions_set; bool same_sol = solutions_set.count(h_sol) > 0; diff --git a/cpp/src/pdlp/cuopt_c.cpp b/cpp/src/pdlp/cuopt_c.cpp index 05e972ad1b..238e69551c 100644 --- a/cpp/src/pdlp/cuopt_c.cpp +++ b/cpp/src/pdlp/cuopt_c.cpp @@ -18,6 +18,8 @@ #include #include +#include + #include #include @@ -92,6 +94,9 @@ struct solver_settings_handle_t { ~solver_settings_handle_t() { delete settings; } solver_settings_t* settings; std::vector> callbacks; + // Log callback registered via cuOptSetLogCallback + cuOptLogCallback log_callback{nullptr}; + void* log_callback_user_data{nullptr}; }; solver_settings_handle_t* get_settings_handle(cuOptSolverSettings settings) @@ -1043,6 +1048,17 @@ cuopt_int_t cuOptSetMIPSetSolutionCallback(cuOptSolverSettings settings, return CUOPT_SUCCESS; } +cuopt_int_t cuOptSetLogCallback(cuOptSolverSettings settings, + cuOptLogCallback callback, + void* user_data) +{ + if (settings == nullptr) { return CUOPT_INVALID_ARGUMENT; } + solver_settings_handle_t* handle = get_settings_handle(settings); + handle->log_callback = callback; + handle->log_callback_user_data = user_data; + return CUOPT_SUCCESS; +} + cuopt_int_t cuOptSetInitialPrimalSolution(cuOptSolverSettings settings, const cuopt_float_t* primal_solution, cuopt_int_t num_variables) @@ -1119,6 +1135,14 @@ cuopt_int_t cuOptSolve(cuOptOptimizationProblem problem, if (settings == nullptr) { return CUOPT_INVALID_ARGUMENT; } if (solution_ptr == nullptr) { return CUOPT_INVALID_ARGUMENT; } + // Register the callback for this thread for the duration of the solve. + // cuOptLogCallback and log_callback_with_data_t share the same signature. + solver_settings_handle_t* handle = get_settings_handle(settings); + std::optional log_scope; + if (handle->log_callback) { + log_scope.emplace(handle->log_callback, handle->log_callback_user_data); + } + problem_and_stream_view_t* problem_and_stream_view = static_cast(problem); diff --git a/cpp/src/utilities/logger.cpp b/cpp/src/utilities/logger.cpp index 217f9c64cb..81bc87878a 100644 --- a/cpp/src/utilities/logger.cpp +++ b/cpp/src/utilities/logger.cpp @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -8,6 +8,11 @@ #include #include +#include +#include +#include +#include + namespace cuopt { struct buffered_entry { @@ -82,13 +87,44 @@ rapids_logger::sink_ptr default_sink() */ inline std::string default_pattern() { return "[%Y-%m-%d %H:%M:%S:%f] [%n] [%-6l] %v"; } +/** + * @brief Runtime log-level override from the `CUOPT_LOG_LEVEL` environment variable. + * + * Accepts a level name (case-insensitive): TRACE, DEBUG, INFO, WARN, ERROR, CRITICAL, OFF. + * Returns std::nullopt if the variable is unset or holds an unrecognised value. + * + * @note Statements below the compile-time `CUOPT_LOG_ACTIVE_LEVEL` (default INFO) are + * removed at build time, so raising verbosity above the build level has no effect; + * lowering it (e.g. WARN/ERROR/OFF to suppress output) always works. + */ +inline std::optional env_log_level() +{ + const char* env = std::getenv("CUOPT_LOG_LEVEL"); + if (env == nullptr) { return std::nullopt; } + std::string level{env}; + std::transform(level.begin(), level.end(), level.begin(), [](unsigned char c) { + return static_cast(std::toupper(c)); + }); + if (level == "TRACE") { return rapids_logger::level_enum::trace; } + if (level == "DEBUG") { return rapids_logger::level_enum::debug; } + if (level == "INFO") { return rapids_logger::level_enum::info; } + if (level == "WARN") { return rapids_logger::level_enum::warn; } + if (level == "ERROR") { return rapids_logger::level_enum::error; } + if (level == "CRITICAL") { return rapids_logger::level_enum::critical; } + if (level == "OFF") { return rapids_logger::level_enum::off; } + return std::nullopt; // unrecognised value: keep the compiled default +} + /** * @brief Returns the default log level for the global logger. * + * The `CUOPT_LOG_LEVEL` environment variable, when set, overrides the compile-time default. + * * @return rapids_logger::level_enum The default log level. */ inline rapids_logger::level_enum default_level() { + if (auto lvl = env_log_level()) { return *lvl; } #if CUOPT_LOG_ACTIVE_LEVEL == RAPIDS_LOGGER_LOG_LEVEL_TRACE return rapids_logger::level_enum::trace; #elif CUOPT_LOG_ACTIVE_LEVEL == RAPIDS_LOGGER_LOG_LEVEL_DEBUG @@ -137,14 +173,54 @@ void reset_default_logger() default_logger().flush_on(rapids_logger::level_enum::debug); } -// Guard object whose destructor resets the logger +static std::mutex g_guard_mutex; + +// Guard object whose destructor resets the logger. struct logger_config_guard { ~logger_config_guard() { cuopt::reset_default_logger(); } }; // Weak reference to detect if any init_logger_t instance is still alive static std::weak_ptr g_active_guard; -static std::mutex g_guard_mutex; + +// Registration is per-thread, not global: the sink is shared by every solve, so +// the callback has to be selected by who is logging rather than by who +// registered last (#1752). +namespace { +struct thread_log_callback_t { + log_callback_with_data_t callback = nullptr; + void* user_data = nullptr; +}; +thread_local thread_log_callback_t t_log_callback; +} // namespace + +static void user_log_bridge(int lvl, const char* msg) +{ + // Standard solver output only; debug/trace are internal diagnostics and must + // not reach user code even in a lower-level build. + if (lvl < static_cast(rapids_logger::level_enum::info)) { return; } + + const auto& cb = t_log_callback; + if (cb.callback) { cb.callback(msg, cb.user_data); } +} + +log_callback_registration_t current_log_callback() +{ + return {t_log_callback.callback, t_log_callback.user_data}; +} + +scoped_log_callback_t::scoped_log_callback_t(log_callback_with_data_t cb, void* user_data) + : prev_callback_(t_log_callback.callback), prev_user_data_(t_log_callback.user_data) +{ + t_log_callback.callback = cb; + t_log_callback.user_data = user_data; +} + +scoped_log_callback_t::~scoped_log_callback_t() +{ + t_log_callback.callback = prev_callback_; + t_log_callback.user_data = prev_user_data_; +} init_logger_t::init_logger_t(std::string log_file, bool log_to_console) { @@ -152,7 +228,7 @@ init_logger_t::init_logger_t(std::string log_file, bool log_to_console) auto existing_guard = g_active_guard.lock(); if (existing_guard) { - // Reuse existing configuration, just hold a reference to keep it alive + // Reuse existing configuration, just hold a reference to keep it alive. guard_ = existing_guard; return; } @@ -169,6 +245,12 @@ init_logger_t::init_logger_t(std::string log_file, bool log_to_console) std::make_shared(log_file, true)); cuopt::default_logger().flush_on(rapids_logger::level_enum::debug); } + auto guard = std::make_shared(); + + // Always installed: the bridge no-ops unless the logging thread has a + // registration, so delivery no longer depends on which solve built the guard. + cuopt::default_logger().sinks().push_back( + std::make_shared(user_log_bridge)); #if CUOPT_LOG_ACTIVE_LEVEL >= RAPIDS_LOGGER_LOG_LEVEL_INFO cuopt::default_logger().set_pattern("%v"); @@ -182,8 +264,6 @@ init_logger_t::init_logger_t(std::string log_file, bool log_to_console) cuopt::default_logger().log(entry.level, entry.msg.c_str()); } - // Create guard and store weak reference for future instances to find - auto guard = std::make_shared(); g_active_guard = guard; guard_ = guard; } diff --git a/cpp/src/utilities/logger.hpp b/cpp/src/utilities/logger.hpp index 2f9053b05f..dcd211e949 100644 --- a/cpp/src/utilities/logger.hpp +++ b/cpp/src/utilities/logger.hpp @@ -38,6 +38,42 @@ rapids_logger::logger& default_logger(); */ void reset_default_logger(); +// C-compatible log callback type. Matches cuOptLogCallback in cuopt_c.h. +// Carries no severity by design: a level would become a de-facto public API. +using log_callback_with_data_t = void (*)(const char* message, void* user_data); + +struct log_callback_registration_t { + log_callback_with_data_t callback = nullptr; + void* user_data = nullptr; +}; + +/** + * @brief The calling thread's current registration, if any. + * + * For forwarding log lines produced on a thread that carries no registration of + * its own, such as the remote-solve log-streaming thread. + */ +log_callback_registration_t current_log_callback(); + +/** + * @brief Registers a log callback for the calling thread, for its own lifetime. + * + * Registration is per-thread so concurrent solves cannot capture each other's + * callback. Log lines emitted on other threads are not delivered. + */ +class scoped_log_callback_t { + public: + scoped_log_callback_t(log_callback_with_data_t cb, void* user_data); + ~scoped_log_callback_t(); + + scoped_log_callback_t(const scoped_log_callback_t&) = delete; + scoped_log_callback_t& operator=(const scoped_log_callback_t&) = delete; + + private: + log_callback_with_data_t prev_callback_; + void* prev_user_data_; +}; + // Ref-counted logger initializer class init_logger_t { // Using shared_ptr for ref-counting diff --git a/cpp/tests/linear_programming/c_api_tests/c_api_test.c b/cpp/tests/linear_programming/c_api_tests/c_api_test.c index 16852b95ef..134d7deca4 100644 --- a/cpp/tests/linear_programming/c_api_tests/c_api_test.c +++ b/cpp/tests/linear_programming/c_api_tests/c_api_test.c @@ -292,6 +292,157 @@ cuopt_int_t test_mip_get_callbacks_only() { return test_mip_callbacks_internal(0 cuopt_int_t test_mip_get_set_callbacks() { return test_mip_callbacks_internal(1); } +/* ------------------------------------------------------------------------- + * Log callback tests + * Use a small LP (1 variable, 1 constraint) so no GPU / dataset is needed. + * ------------------------------------------------------------------------- */ + +/* Build a trivial 1-variable LP: min x s.t. x >= 1, 0 <= x <= inf */ +static cuopt_int_t make_trivial_lp(cuOptOptimizationProblem* problem_out, + cuOptSolverSettings* settings_out) +{ + cuopt_float_t obj[] = {1.0}; + cuopt_int_t row_off[] = {0, 1}; + cuopt_int_t col_idx[] = {0}; + cuopt_float_t coeff[] = {1.0}; + char sense[] = {CUOPT_GREATER_THAN}; + cuopt_float_t rhs[] = {1.0}; + cuopt_float_t lb[] = {0.0}; + cuopt_float_t ub[] = {1e30}; + char vtype[] = {CUOPT_CONTINUOUS}; + + cuopt_int_t status = + cuOptCreateProblem(1, 1, CUOPT_MINIMIZE, 0.0, obj, row_off, col_idx, coeff, + sense, rhs, lb, ub, vtype, problem_out); + if (status != CUOPT_SUCCESS) return status; + return cuOptCreateSolverSettings(settings_out); +} + +typedef struct { + int calls; + void* received_user_data; +} log_cb_context_t; + +static void counting_log_callback(const char* message, void* user_data) +{ + (void)message; + log_cb_context_t* ctx = (log_cb_context_t*)user_data; + ctx->calls++; + ctx->received_user_data = user_data; +} + +cuopt_int_t test_log_callback(void) +{ + cuOptOptimizationProblem problem = NULL; + cuOptSolverSettings settings = NULL; + cuOptSolution solution = NULL; + log_cb_context_t ctx = {0, NULL}; + cuopt_int_t status = make_trivial_lp(&problem, &settings); + if (status != CUOPT_SUCCESS) goto DONE; + + status = cuOptSetLogCallback(settings, counting_log_callback, &ctx); + if (status != CUOPT_SUCCESS) goto DONE; + + status = cuOptSolve(problem, settings, &solution); + if (status != CUOPT_SUCCESS) goto DONE; + + if (ctx.calls < 1) { + printf("Expected log callback to be called at least once; got %d calls\n", ctx.calls); + status = CUOPT_INVALID_ARGUMENT; + goto DONE; + } + if (ctx.received_user_data != &ctx) { + printf("user_data pointer was not forwarded correctly\n"); + status = CUOPT_INVALID_ARGUMENT; + goto DONE; + } + +DONE: + cuOptDestroyProblem(&problem); + cuOptDestroySolverSettings(&settings); + cuOptDestroySolution(&solution); + return status; +} + +cuopt_int_t test_log_callback_cleared(void) +{ + cuOptOptimizationProblem problem = NULL; + cuOptSolverSettings settings = NULL; + cuOptSolution solution = NULL; + log_cb_context_t ctx = {0, NULL}; + cuopt_int_t status = make_trivial_lp(&problem, &settings); + if (status != CUOPT_SUCCESS) goto DONE; + + /* Register then immediately clear the callback */ + status = cuOptSetLogCallback(settings, counting_log_callback, &ctx); + if (status != CUOPT_SUCCESS) goto DONE; + status = cuOptSetLogCallback(settings, NULL, NULL); + if (status != CUOPT_SUCCESS) goto DONE; + + status = cuOptSolve(problem, settings, &solution); + if (status != CUOPT_SUCCESS) goto DONE; + + if (ctx.calls != 0) { + printf("Expected 0 callback calls after clearing; got %d\n", ctx.calls); + status = CUOPT_INVALID_ARGUMENT; + goto DONE; + } + +DONE: + cuOptDestroyProblem(&problem); + cuOptDestroySolverSettings(&settings); + cuOptDestroySolution(&solution); + return status; +} + +/* A callback registered for one solve must not fire on a later solve that uses + * fresh settings with no callback — otherwise a stale callback could run against + * destroyed user_data once the first solve's RAII scope ends. */ +cuopt_int_t test_log_callback_not_leaked_across_solves(void) +{ + cuOptOptimizationProblem problem1 = NULL; + cuOptSolverSettings settings1 = NULL; + cuOptSolution solution1 = NULL; + cuOptOptimizationProblem problem2 = NULL; + cuOptSolverSettings settings2 = NULL; + cuOptSolution solution2 = NULL; + log_cb_context_t ctx = {0, NULL}; + int calls_after_first = 0; + + cuopt_int_t status = make_trivial_lp(&problem1, &settings1); + if (status != CUOPT_SUCCESS) goto DONE; + + status = cuOptSetLogCallback(settings1, counting_log_callback, &ctx); + if (status != CUOPT_SUCCESS) goto DONE; + status = cuOptSolve(problem1, settings1, &solution1); + if (status != CUOPT_SUCCESS) goto DONE; + + calls_after_first = ctx.calls; + + /* Second solve uses fresh settings with no callback registered. */ + status = make_trivial_lp(&problem2, &settings2); + if (status != CUOPT_SUCCESS) goto DONE; + status = cuOptSolve(problem2, settings2, &solution2); + if (status != CUOPT_SUCCESS) goto DONE; + + if (ctx.calls != calls_after_first) { + printf("Callback leaked across solves; expected %d calls, got %d\n", + calls_after_first, + ctx.calls); + status = CUOPT_INVALID_ARGUMENT; + goto DONE; + } + +DONE: + cuOptDestroyProblem(&problem1); + cuOptDestroySolverSettings(&settings1); + cuOptDestroySolution(&solution1); + cuOptDestroyProblem(&problem2); + cuOptDestroySolverSettings(&settings2); + cuOptDestroySolution(&solution2); + return status; +} + cuopt_int_t burglar_problem() { cuOptOptimizationProblem problem = NULL; @@ -2696,6 +2847,59 @@ cuopt_int_t test_qcqp_solution_dual_methods() * This simulates a CPU host without GPU access. * Note: Environment variables must be set before calling this function. */ +/* Remote solve must deliver the *server's* log to the user callback, not just + the client-side lines. Requires CUOPT_REMOTE_HOST/PORT set by the caller. */ +typedef struct { + int calls; + int saw_solver_line; +} remote_log_ctx_t; + +static void remote_log_callback(const char* message, void* user_data) +{ + remote_log_ctx_t* ctx = (remote_log_ctx_t*)user_data; + ctx->calls++; + /* Emitted by the solver itself, so in remote mode it can only have come from + the server. Client-side lines alone would not contain it. */ + if (message && strstr(message, "Status:") != NULL) { ctx->saw_solver_line = 1; } +} + +cuopt_int_t test_log_callback_remote(const char* filename) +{ + cuOptOptimizationProblem problem = NULL; + cuOptSolverSettings settings = NULL; + cuOptSolution solution = NULL; + remote_log_ctx_t ctx = {0, 0}; + cuopt_int_t status; + + status = cuOptReadProblem(filename, &problem); + if (status != CUOPT_SUCCESS) goto DONE; + + status = cuOptCreateSolverSettings(&settings); + if (status != CUOPT_SUCCESS) goto DONE; + + status = cuOptSetLogCallback(settings, remote_log_callback, &ctx); + if (status != CUOPT_SUCCESS) goto DONE; + + status = cuOptSolve(problem, settings, &solution); + if (status != CUOPT_SUCCESS) goto DONE; + + if (ctx.calls < 1) { + printf("Expected remote solve to deliver log lines; got %d calls\n", ctx.calls); + status = CUOPT_INVALID_ARGUMENT; + goto DONE; + } + if (!ctx.saw_solver_line) { + printf("Remote solve delivered %d lines but none from the server's solver log\n", ctx.calls); + status = CUOPT_INVALID_ARGUMENT; + } + +DONE: + if (solution) cuOptDestroySolution(&solution); + if (settings) cuOptDestroySolverSettings(&settings); + if (problem) cuOptDestroyProblem(&problem); + return status; +} + cuopt_int_t test_cpu_only_execution(const char* filename) { cuOptOptimizationProblem problem = NULL; diff --git a/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp b/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp index a54ad1aec6..dadaf243fc 100644 --- a/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp +++ b/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp @@ -116,6 +116,15 @@ TEST(c_api, mip_get_callbacks_only) { EXPECT_EQ(test_mip_get_callbacks_only(), C TEST(c_api, mip_get_set_callbacks) { EXPECT_EQ(test_mip_get_set_callbacks(), CUOPT_SUCCESS); } +TEST(c_api, log_callback) { EXPECT_EQ(test_log_callback(), CUOPT_SUCCESS); } + +TEST(c_api, log_callback_cleared) { EXPECT_EQ(test_log_callback_cleared(), CUOPT_SUCCESS); } + +TEST(c_api, log_callback_not_leaked_across_solves) +{ + EXPECT_EQ(test_log_callback_not_leaked_across_solves(), CUOPT_SUCCESS); +} + TEST(c_api, burglar) { EXPECT_EQ(burglar_problem(), CUOPT_SUCCESS); } TEST(c_api, test_missing_file) { EXPECT_EQ(test_missing_file(), CUOPT_MPS_FILE_ERROR); } @@ -781,6 +790,13 @@ TEST_F(CpuOnlyWithServerTest, lp_solve) EXPECT_EQ(test_cpu_only_execution(lp_file.c_str()), CUOPT_SUCCESS); } +TEST_F(CpuOnlyWithServerTest, log_callback_remote) +{ + const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); + std::string lp_file = rapidsDatasetRootDir + "/linear_programming/afiro_original.mps"; + EXPECT_EQ(test_log_callback_remote(lp_file.c_str()), CUOPT_SUCCESS); +} + TEST_F(CpuOnlyWithServerTest, mip_solve) { const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); diff --git a/cpp/tests/linear_programming/c_api_tests/c_api_tests.h b/cpp/tests/linear_programming/c_api_tests/c_api_tests.h index a7242d1063..f24a9e78d3 100644 --- a/cpp/tests/linear_programming/c_api_tests/c_api_tests.h +++ b/cpp/tests/linear_programming/c_api_tests/c_api_tests.h @@ -30,6 +30,9 @@ cuopt_int_t test_infeasible_problem(); cuopt_int_t test_bad_parameter_name(); cuopt_int_t test_mip_get_callbacks_only(); cuopt_int_t test_mip_get_set_callbacks(); +cuopt_int_t test_log_callback(); +cuopt_int_t test_log_callback_cleared(); +cuopt_int_t test_log_callback_not_leaked_across_solves(); cuopt_int_t test_ranged_problem(cuopt_int_t* termination_status_ptr, cuopt_float_t* objective_ptr); cuopt_int_t test_semi_continuous_problem(cuopt_int_t* termination_status_ptr, cuopt_float_t* objective_ptr, @@ -79,6 +82,7 @@ cuopt_int_t test_pdlp_precision_mixed(const char* filename, /* CPU-only execution tests (require env vars CUDA_VISIBLE_DEVICES="" and CUOPT_REMOTE_HOST) */ cuopt_int_t test_cpu_only_execution(const char* filename); +cuopt_int_t test_log_callback_remote(const char* filename); cuopt_int_t test_cpu_only_mip_execution(const char* filename); /* CPU-host read/create C API (require CUDA_VISIBLE_DEVICES="", no remote, no solve) */