diff --git a/docs/python-c.mdx b/docs/python-c.mdx index 721b5bf9..3de4d262 100644 --- a/docs/python-c.mdx +++ b/docs/python-c.mdx @@ -77,7 +77,7 @@ The generated C/C++ bindings are in the auto-generated solver library. In particular * The header files are at `the_optimizer/the_optimizer_bindings.{h,hpp}` -* The static and dynamical library files are located in `the_optimizer/target/{debug,release}` (depending on whether it was a [*debug*] or [*release*] build) +* The static and dynamic library files are located in `the_optimizer/target/{debug,release}` (depending on whether it was a [*debug*] or [*release*] build) Note that `the_optimizer` is the name given to the optimizer in the Python codegen above. @@ -112,13 +112,16 @@ typedef struct exampleCache exampleCache; typedef struct { exampleExitStatus exit_status; + int error_code; + char error_message[1024]; unsigned long num_outer_iterations; unsigned long num_inner_iterations; - double last_problem_norm_fpr; + double last_problem_norm_fpr; unsigned long long solve_time_ns; double penalty; double delta_y_norm_over_c; double f2_norm; + double cost; const double *lagrange; } exampleSolverStatus; @@ -137,10 +140,61 @@ This is designed to follow a new-use-free pattern. Function `{optimizer-name}_new` will allocate memory and setup a new solver instance and can be used to create as many solvers as necessary. Each solver instance can be used with `{optimizer-name}_solve` to solve the corresponding problem as many times as needed. -Parameter `u` is the starting guess and also the return of the decision variables and `params` is the array of static parameters. The size of `u` and `params` are `{optimizer-name}_NUM_DECISION_VARIABLES` and `{optimizer-name}_NUM_PARAMETERS` respectively. +Parameter `u` is the starting guess and also the return of the decision variables and `params` is the array of static parameters. The size of `u` and `params` are `{optimizer-name}_NUM_DECISION_VARIABLES` and `{optimizer-name}_NUM_PARAMETERS` respectively. Arguments `y0` and `c0` are optional: pass `0` (or `NULL`) to use the default initial Lagrange multipliers and penalty parameter. + +The returned `exampleSolverStatus` always contains a coarse solver outcome in +`exit_status`. On success it also contains `error_code = 0` and an empty +`error_message`. If the solver fails internally, the bindings return a +structured error report with a nonzero `error_code` and a descriptive +`error_message`. Finally, when done with the solver, use `{optimizer-name}_free` to release the memory allocated by `{optimizer-name}_new`. +## Handling errors + +The C bindings always return a value of type `exampleSolverStatus`. This means +that solver calls do not report failure by returning `NULL` or by using a +separate exception-like mechanism. Instead, callers should inspect both +`exit_status` and `error_code`. + +- `error_code = 0` means the solver call completed without an internal error +- `error_code != 0` means the solver failed and `error_message` contains a + descriptive explanation +- `exit_status` gives the coarse outcome of the solve attempt, such as + converged, reached the iteration limit, or failed because of a numerical + issue + +The recommended pattern is: + +1. Call `{optimizer-name}_solve(...)` +2. Check whether `status.error_code != 0` +3. If so, report `status.error_message` and treat the call as failed +4. Otherwise, inspect `status.exit_status` to determine whether the solver + converged or returned the best available non-converged iterate + +For example: + +```c +exampleSolverStatus status = example_solve(cache, u, p, 0, &initial_penalty); + +if (status.error_code != 0) { + fprintf(stderr, "Solver failed: [%d] %s\n", + status.error_code, status.error_message); + example_free(cache); + return EXIT_FAILURE; +} + +if (status.exit_status != exampleConverged) { + fprintf(stderr, "Warning: solver did not converge fully\n"); +} +``` + +The generated C example follows exactly this pattern. + +At the ABI level, callers are still responsible for passing valid pointers and +correctly sized arrays for `u`, `params`, and optional arguments such as `y0`. +Those are contract violations, not recoverable solver errors. + ## Using the bindings in an app @@ -155,26 +209,41 @@ The auto-generated example has the following form: /* File: the_optimizer/example_optimizer.c */ #include +#include #include "example_bindings.h" -int main() { - double p[EXAMPLE_NUM_PARAMETERS] = {1.0, 10.0}; // parameter +int main(void) { + double p[EXAMPLE_NUM_PARAMETERS] = {0}; // parameter double u[EXAMPLE_NUM_DECISION_VARIABLES] = {0}; // initial guess + double initial_penalty = 15.0; exampleCache *cache = example_new(); - exampleSolverStatus status = example_solve(cache, u, p); - example_free(cache); - - for (int i = 0; i < EXAMPLE_NUM_DECISION_VARIABLES; ++i) { - printf("u[%d] = %g\n", i, u[i]); + if (cache == NULL) { + fprintf(stderr, "Could not allocate solver cache\n"); + return EXIT_FAILURE; } + exampleSolverStatus status = example_solve(cache, u, p, 0, &initial_penalty); + printf("exit status = %d\n", status.exit_status); + printf("error code = %d\n", status.error_code); + printf("error message = %s\n", status.error_message); printf("iterations = %lu\n", status.num_inner_iterations); printf("outer iterations = %lu\n", status.num_outer_iterations); printf("solve time = %f ms\n", (double)status.solve_time_ns / 1e6); - return 0; + if (status.error_code != 0) { + example_free(cache); + return EXIT_FAILURE; + } + + for (int i = 0; i < EXAMPLE_NUM_DECISION_VARIABLES; ++i) { + printf("u[%d] = %g\n", i, u[i]); + } + + example_free(cache); + + return EXIT_SUCCESS; } ``` @@ -185,19 +254,18 @@ int main() { To compile your C program you need to link to the auto-generated C bindings (see [next section](#compile-your-own-code)). However, OpEn generates automatically a `CMakeLists.txt` file -to facilitate the compilation/linking procedure. To build the -auto-generated example run +to facilitate the compilation/linking procedure. A typical build is: ```bash -cmake . -make +cmake -S . -B build +cmake --build build ``` -once you build your optimizer you can run the executable (`optimizer`) -with +Once you build your optimizer you can run the executable (`optimizer`) +with: ```bash -make run +cmake --build build --target run ``` #### Compile your own code @@ -260,14 +328,18 @@ LD_LIBRARY_PATH=./target/release ./optimizer The output looks like this: ```text +exit status = 0 +error code = 0 +error message = +iterations = 69 +outer iterations = 5 +solve time = 0.140401 ms u[0] = 0.654738 u[1] = 0.982045 u[2] = 0.98416 u[3] = 0.984188 u[4] = 0.969986 -exit status = 0 -iterations = 69 -outer iterations = 5 -solve time = 0.140401 ms ``` +If `error_code` is nonzero, the solver failed to produce a valid result and +`error_message` contains the propagated reason from the generated Rust solver. diff --git a/open-codegen/CHANGELOG.md b/open-codegen/CHANGELOG.md index b7dfdaa3..3c38beb8 100644 --- a/open-codegen/CHANGELOG.md +++ b/open-codegen/CHANGELOG.md @@ -21,7 +21,7 @@ Note: This is the Changelog file of `opengen` - the Python interface of OpEn - Extended `RosConfiguration` so it can be used for both ROS and ROS2 package generation - Breaking change: the direct interface (Python bindings) now has an API which mirrors that of the TCP interface: the method `solve` returns either a solution or an error object. Website documentation is updated. New unit tests are implemented. Note that `solver.run()` does not return the solution object directly, but rather works in the same way as the TCP interface: it returns a response object (instance of `SolverResponse`), on which the method `.get()` returns either a `SolverStatus` or `SolverError`. - Added helpful `__repr__` methods to generated Python binding response/status/error objects, TCP solver response/error objects, and `GeneratedOptimizer` for easier inspection and debugging -- Updated generated TCP server and C interface templates to work with the richer Rust solver error model and expose better failure information to clients +- Updated generated TCP server and C interface templates to work with the richer Rust solver error model and expose better failure information to clients. Updated auto-generated `CMakeLists.txt` file. Tighter unit tests. - ROS2 generated packages now publish detailed `error_code` and `error_message` fields, plus `STATUS_INVALID_REQUEST`, so invalid requests and solver failures are reported explicitly instead of being silently ignored diff --git a/open-codegen/opengen/templates/c/example_cmakelists.txt b/open-codegen/opengen/templates/c/example_cmakelists.txt index 13a79032..63d2bca9 100644 --- a/open-codegen/opengen/templates/c/example_cmakelists.txt +++ b/open-codegen/opengen/templates/c/example_cmakelists.txt @@ -3,17 +3,40 @@ cmake_minimum_required(VERSION 3.5) # Project name project({{meta.optimizer_name}}) +# Build the generated example as C99. +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# Resolve the generated static library name for the current platform. +if(WIN32) + set(OPEN_STATIC_LIB ${CMAKE_CURRENT_SOURCE_DIR}/target/{{ build_config.build_mode }}/{{meta.optimizer_name}}.lib) +else() + set(OPEN_STATIC_LIB ${CMAKE_CURRENT_SOURCE_DIR}/target/{{ build_config.build_mode }}/lib{{meta.optimizer_name}}.a) +endif() + +find_package(Threads REQUIRED) + # Add the executable add_executable(optimizer example_optimizer.c) # Add libraries to the executable -target_link_libraries(optimizer ${CMAKE_SOURCE_DIR}/target/{{ build_config.build_mode }}/lib{{meta.optimizer_name}}.a) -target_link_libraries(optimizer m) -target_link_libraries(optimizer dl) -target_link_libraries(optimizer pthread) +target_link_libraries( + optimizer + PRIVATE + ${OPEN_STATIC_LIB} + Threads::Threads +) + +if(UNIX) + target_link_libraries(optimizer PRIVATE m) +endif() + +if(CMAKE_DL_LIBS) + target_link_libraries(optimizer PRIVATE ${CMAKE_DL_LIBS}) +endif() add_custom_target(run - COMMAND optimizer + COMMAND $ DEPENDS optimizer - WORKING_DIRECTORY ${CMAKE_PROJECT_DIR} -) \ No newline at end of file + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) diff --git a/open-codegen/opengen/templates/c/example_optimizer_c_bindings.c b/open-codegen/opengen/templates/c/example_optimizer_c_bindings.c index 282bd2c8..31cf8e66 100644 --- a/open-codegen/opengen/templates/c/example_optimizer_c_bindings.c +++ b/open-codegen/opengen/templates/c/example_optimizer_c_bindings.c @@ -20,17 +20,35 @@ */ #include +#include #include "{{meta.optimizer_name}}_bindings.h" /* * Feel free to customize the following code... */ +static const char *exit_status_to_string({{meta.optimizer_name}}ExitStatus exit_status) { + switch (exit_status) { + case {{meta.optimizer_name}}Converged: + return "Converged"; + case {{meta.optimizer_name}}NotConvergedIterations: + return "NotConvergedIterations"; + case {{meta.optimizer_name}}NotConvergedOutOfTime: + return "NotConvergedOutOfTime"; + case {{meta.optimizer_name}}NotConvergedCost: + return "NotConvergedCost"; + case {{meta.optimizer_name}}NotConvergedNotFiniteComputation: + return "NotConvergedNotFiniteComputation"; + default: + return "Unknown"; + } +} + int main(void) { int i; /* parameters */ - double p[{{meta.optimizer_name|upper}}_NUM_PARAMETERS] = {2.0, 10.0}; + double p[{{meta.optimizer_name|upper}}_NUM_PARAMETERS] = {0}; /* initial guess */ double u[{{meta.optimizer_name|upper}}_NUM_DECISION_VARIABLES] = {0}; @@ -45,28 +63,18 @@ int main(void) { /* obtain cache */ {{meta.optimizer_name}}Cache *cache = {{meta.optimizer_name}}_new(); + if (cache == NULL) { + fprintf(stderr, "Could not allocate solver cache\n"); + return EXIT_FAILURE; + } /* solve */ {{meta.optimizer_name}}SolverStatus status = {{meta.optimizer_name}}_solve(cache, u, p, {% if problem.dim_constraints_aug_lagrangian() > 0 %}y{% else %}0{% endif %}, &init_penalty); - /* print results */ - printf("\n\n-------------------------------------------------\n"); - printf(" Solution\n"); - printf("-------------------------------------------------\n"); - - for (i = 0; i < {{meta.optimizer_name|upper}}_NUM_DECISION_VARIABLES; ++i) { - printf("u[%d] = %g\n", i, u[i]); - } - - printf("\n"); - for (i = 0; i < {{meta.optimizer_name|upper}}_N1; ++i) { - printf("y[%d] = %g\n", i, status.lagrange[i]); - } - printf("\n\n-------------------------------------------------\n"); printf(" Solver Statistics\n"); printf("-------------------------------------------------\n"); - printf("exit status : %d\n", status.exit_status); + printf("exit status : %d (%s)\n", status.exit_status, exit_status_to_string(status.exit_status)); printf("error code : %d\n", status.error_code); printf("error message : %s\n", status.error_message); printf("iterations : %lu\n", status.num_inner_iterations); @@ -78,10 +86,34 @@ int main(void) { printf("Cost : %f\n", status.cost); printf("||FRP|| : %f\n\n", status.last_problem_norm_fpr); + if (status.error_code != 0) { + fprintf(stderr, "Solver returned an error; solution vector is not printed.\n"); + {{meta.optimizer_name}}_free(cache); + return EXIT_FAILURE; + } + + if (status.exit_status != {{meta.optimizer_name}}Converged) { + fprintf(stderr, "Warning: solver did not converge, printing best available iterate.\n"); + } + + /* print results */ + printf("\n\n-------------------------------------------------\n"); + printf(" Solution\n"); + printf("-------------------------------------------------\n"); + + for (i = 0; i < {{meta.optimizer_name|upper}}_NUM_DECISION_VARIABLES; ++i) { + printf("u[%d] = %g\n", i, u[i]); + } + + printf("\n"); + for (i = 0; i < {{meta.optimizer_name|upper}}_N1; ++i) { + printf("y[%d] = %g\n", i, status.lagrange[i]); + } + /* free memory */ {{meta.optimizer_name}}_free(cache); - return 0; + return EXIT_SUCCESS; } diff --git a/open-codegen/opengen/templates/c/optimizer_cinterface.rs.jinja b/open-codegen/opengen/templates/c/optimizer_cinterface.rs.jinja index 26cbe1f5..db2dec64 100644 --- a/open-codegen/opengen/templates/c/optimizer_cinterface.rs.jinja +++ b/open-codegen/opengen/templates/c/optimizer_cinterface.rs.jinja @@ -1,5 +1,6 @@ {% if activate_clib_generation -%} -// ---Export functionality from Rust to C/C++------------------------------------------------------------ +{% set error_message_capacity = 1024 -%} +// ---Export functionality from Rust to C/C++ -------------- /// Solver cache (structure `{{meta.optimizer_name}}Cache`) /// @@ -10,7 +11,7 @@ pub struct {{meta.optimizer_name}}Cache { const {{meta.optimizer_name|upper}}_NO_ERROR_CODE: c_int = 0; const {{meta.optimizer_name|upper}}_SOLVER_ERROR_CODE: c_int = 2000; -const {{meta.optimizer_name|upper}}_ERROR_MESSAGE_CAPACITY: usize = 256; +const {{meta.optimizer_name|upper}}_ERROR_MESSAGE_CAPACITY: usize = {{ error_message_capacity }}; impl {{meta.optimizer_name}}Cache { pub fn new(cache: AlmCache) -> Self { @@ -18,13 +19,13 @@ impl {{meta.optimizer_name}}Cache { } } -fn empty_error_message() -> [c_char; 256] { - [0 as c_char; 256] +fn empty_error_message() -> [c_char; {{ error_message_capacity }}] { + [0 as c_char; {{ error_message_capacity }}] } fn error_message_to_c_array( message: &str, -) -> [c_char; 256] { +) -> [c_char; {{ error_message_capacity }}] { let mut buffer = empty_error_message(); let max_len = {{meta.optimizer_name|upper}}_ERROR_MESSAGE_CAPACITY - 1; for (idx, byte) in message.as_bytes().iter().copied().take(max_len).enumerate() { @@ -63,7 +64,7 @@ pub struct {{meta.optimizer_name}}SolverStatus { /// Detailed error code (0 on success) error_code: c_int, /// Detailed error message (empty string on success) - error_message: [c_char; 256], + error_message: [c_char; {{ error_message_capacity }}], /// Number of outer iterations num_outer_iterations: c_ulong, /// Total number of inner iterations diff --git a/open-codegen/test/test.py b/open-codegen/test/test.py index f61b354b..79d4373a 100644 --- a/open-codegen/test/test.py +++ b/open-codegen/test/test.py @@ -304,7 +304,8 @@ def setUpSolverError(cls): .with_open_version(local_path=RustBuildTestCase.get_open_local_absolute_path()) \ .with_build_directory(RustBuildTestCase.TEST_DIR) \ .with_build_mode(og.config.BuildConfiguration.DEBUG_MODE) \ - .with_tcp_interface_config(tcp_interface_config=tcp_config) + .with_tcp_interface_config(tcp_interface_config=tcp_config) \ + .with_build_c_bindings() og.builder.OpEnOptimizerBuilder(problem, metadata=meta, build_configuration=build_config, @@ -791,48 +792,168 @@ def test_rust_build_parametric_halfspace(self): self.assertTrue(sum([u[i] * c[i] for i in range(5)]) - b <= eps) self.assertTrue(-sum([u[i] * c[i] for i in range(5)]) + b <= eps) + @staticmethod + def rebuild_generated_staticlib(optimizer_name): + optimizer_dir = os.path.join(RustBuildTestCase.TEST_DIR, optimizer_name) + process = subprocess.Popen( + ["cargo", "build"], + cwd=optimizer_dir, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + _stdout, stderr = process.communicate() + return process.returncode, stderr.decode() + @staticmethod def c_bindings_helper(optimizer_name): - p = subprocess.Popen(["/usr/bin/gcc", - RustBuildTestCase.TEST_DIR + "/" + optimizer_name + "/example_optimizer.c", - "-I" + RustBuildTestCase.TEST_DIR + "/" + optimizer_name, - "-pthread", - RustBuildTestCase.TEST_DIR + "/" + optimizer_name + - "/target/debug/lib" + optimizer_name + ".a", - "-lm", - "-ldl", - "-std=c99", - "-o", - RustBuildTestCase.TEST_DIR + "/" + optimizer_name + "/optimizer"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - - # Make sure it compiles - p.communicate() - rc1 = p.returncode - - # Run the optimizer - p = subprocess.Popen([RustBuildTestCase.TEST_DIR + "/" + optimizer_name + "/optimizer"], - stdout=subprocess.DEVNULL) - p.communicate() - rc2 = p.returncode - - return rc1, rc2 + compile_process = subprocess.Popen( + ["/usr/bin/gcc", + RustBuildTestCase.TEST_DIR + "/" + optimizer_name + "/example_optimizer.c", + "-I" + RustBuildTestCase.TEST_DIR + "/" + optimizer_name, + "-pthread", + RustBuildTestCase.TEST_DIR + "/" + optimizer_name + + "/target/debug/lib" + optimizer_name + ".a", + "-lm", + "-ldl", + "-std=c99", + "-o", + RustBuildTestCase.TEST_DIR + "/" + optimizer_name + "/optimizer"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + compile_stdout, compile_stderr = compile_process.communicate() + + run_stdout = b"" + run_stderr = b"" + run_returncode = None + if compile_process.returncode == 0: + run_process = subprocess.Popen( + [RustBuildTestCase.TEST_DIR + "/" + optimizer_name + "/optimizer"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + run_stdout, run_stderr = run_process.communicate() + run_returncode = run_process.returncode + + return { + "compile_returncode": compile_process.returncode, + "compile_stdout": compile_stdout.decode(), + "compile_stderr": compile_stderr.decode(), + "run_returncode": run_returncode, + "run_stdout": run_stdout.decode(), + "run_stderr": run_stderr.decode(), + } + + @staticmethod + def patch_c_bindings_example_parameter_initializer(optimizer_name, replacement_line): + example_file = os.path.join( + RustBuildTestCase.TEST_DIR, optimizer_name, "example_optimizer.c") + with open(example_file, "r", encoding="utf-8") as fh: + example_source = fh.read() + + original_line = None + for line in example_source.splitlines(): + if "double p[" in line and "= {" in line: + original_line = line + break + + if original_line is None: + raise RuntimeError("Could not locate parameter initializer in example_optimizer.c") + + with open(example_file, "w", encoding="utf-8") as fh: + fh.write(example_source.replace(original_line, replacement_line, 1)) + + return original_line + + @staticmethod + def c_bindings_cmake_helper(optimizer_name): + cmake_executable = shutil.which("cmake") + if cmake_executable is None: + raise unittest.SkipTest("cmake is not available in PATH") + + optimizer_dir = os.path.join(RustBuildTestCase.TEST_DIR, optimizer_name) + build_dir = os.path.join(optimizer_dir, "cmake-build-test") + if os.path.isdir(build_dir): + shutil.rmtree(build_dir) + os.makedirs(build_dir) + + configure_process = subprocess.Popen( + [cmake_executable, ".."], + cwd=build_dir, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + configure_stdout, configure_stderr = configure_process.communicate() + + build_stdout = b"" + build_stderr = b"" + build_returncode = None + if configure_process.returncode == 0: + build_process = subprocess.Popen( + [cmake_executable, "--build", "."], + cwd=build_dir, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + build_stdout, build_stderr = build_process.communicate() + build_returncode = build_process.returncode + + return { + "configure_returncode": configure_process.returncode, + "configure_stdout": configure_stdout.decode(), + "configure_stderr": configure_stderr.decode(), + "build_returncode": build_returncode, + "build_stdout": build_stdout.decode(), + "build_stderr": build_stderr.decode(), + } def test_c_bindings(self): - rc1, rc2 = RustBuildTestCase.c_bindings_helper( - optimizer_name="only_f1") - self.assertEqual(0, rc1) - self.assertEqual(0, rc2) - - rc1, rc2 = RustBuildTestCase.c_bindings_helper( - optimizer_name="only_f2") - self.assertEqual(0, rc1) - self.assertEqual(0, rc2) - - rc1, rc2 = RustBuildTestCase.c_bindings_helper(optimizer_name="plain") - self.assertEqual(0, rc1) - self.assertEqual(0, rc2) + result = RustBuildTestCase.c_bindings_helper(optimizer_name="only_f1") + self.assertEqual(0, result["compile_returncode"], msg=result["compile_stderr"]) + self.assertEqual(0, result["run_returncode"], msg=result["run_stderr"]) + self.assertIn("Converged", result["run_stdout"]) + self.assertIn("exit status : 0", result["run_stdout"]) + self.assertIn("error code : 0", result["run_stdout"]) + + result = RustBuildTestCase.c_bindings_helper(optimizer_name="only_f2") + self.assertEqual(0, result["compile_returncode"], msg=result["compile_stderr"]) + self.assertIn("Converged", result["run_stdout"]) + self.assertEqual(0, result["run_returncode"], msg=result["run_stderr"]) + self.assertIn("exit status : 0", result["run_stdout"]) + self.assertIn("error code : 0", result["run_stdout"]) + + result = RustBuildTestCase.c_bindings_helper(optimizer_name="plain") + self.assertIn("Converged", result["run_stdout"]) + self.assertEqual(0, result["compile_returncode"], msg=result["compile_stderr"]) + self.assertEqual(0, result["run_returncode"], msg=result["run_stderr"]) + self.assertIn("exit status : 0", result["run_stdout"]) + self.assertIn("error code : 0", result["run_stdout"]) + + def test_c_bindings_error_path(self): + rebuild_rc, rebuild_stderr = RustBuildTestCase.rebuild_generated_staticlib( + optimizer_name="solver_error") + self.assertEqual(0, rebuild_rc, msg=rebuild_stderr) + + original_line = RustBuildTestCase.patch_c_bindings_example_parameter_initializer( + optimizer_name="solver_error", + replacement_line=" double p[SOLVER_ERROR_NUM_PARAMETERS] = {-1.0};") + try: + result = RustBuildTestCase.c_bindings_helper(optimizer_name="solver_error") + finally: + RustBuildTestCase.patch_c_bindings_example_parameter_initializer( + optimizer_name="solver_error", + replacement_line=original_line) + self.assertEqual(0, result["compile_returncode"], msg=result["compile_stderr"]) + self.assertNotEqual(0, result["run_returncode"]) + self.assertIn("error code : 2000", result["run_stdout"]) + self.assertIn("forced solver error for TCP test", result["run_stdout"]) + self.assertIn( + "Solver returned an error; solution vector is not printed.", + result["run_stderr"]) + + def test_c_bindings_cmake_example_builds(self): + result = RustBuildTestCase.c_bindings_cmake_helper(optimizer_name="plain") + self.assertEqual(0, result["configure_returncode"], msg=result["configure_stderr"]) + self.assertEqual(0, result["build_returncode"], msg=result["build_stderr"]) def test_tcp_generated_server_builds(self): tcp_iface_dir = os.path.join(