1
#![allow(warnings)]
2

            
3
use std::ffi::CString;
4
use std::sync::atomic::{AtomicI32, Ordering};
5
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
6

            
7
#[cfg(test)]
8
mod tests {
9
    use std::ffi::{CString, c_char, c_void};
10
    use std::process::{Command, ExitStatus};
11

            
12
    use super::*;
13

            
14
    // solutions
15
    static X_VAL: AtomicI32 = AtomicI32::new(0);
16
    static Y_VAL: AtomicI32 = AtomicI32::new(0);
17
    static Z_VAL: AtomicI32 = AtomicI32::new(0);
18
    const MIDSEARCH_SCENARIO_ENV: &str = "MINION_MIDSEARCH_SCENARIO";
19
    const MIDSEARCH_CHILD_TEST: &str = "ffi::tests::midsearch_child_runner";
20

            
21
    #[derive(Clone, Copy, Debug)]
22
    enum MidsearchScenario {
23
        AddVarOnly,
24
        AddVarThenAddEqConstraint,
25
    }
26

            
27
    impl MidsearchScenario {
28
        fn as_env_value(self) -> &'static str {
29
            match self {
30
                MidsearchScenario::AddVarOnly => "add_var_only",
31
                MidsearchScenario::AddVarThenAddEqConstraint => "add_var_then_add_eq_constraint",
32
            }
33
        }
34

            
35
        fn from_env_value(value: &str) -> Option<Self> {
36
            match value {
37
                "add_var_only" => Some(MidsearchScenario::AddVarOnly),
38
                "add_var_then_add_eq_constraint" => {
39
                    Some(MidsearchScenario::AddVarThenAddEqConstraint)
40
                }
41
                _ => None,
42
            }
43
        }
44
    }
45

            
46
    #[derive(Debug)]
47
    struct MidsearchState {
48
        scenario: MidsearchScenario,
49
        instance: *mut ProbSpec_CSPInstance,
50
        callback_count: u32,
51
        added_var_ok: bool,
52
        looked_up_new_var_ok: bool,
53
        add_constraint_result: Option<MinionResult>,
54
    }
55

            
56
    #[derive(Debug)]
57
    struct MidsearchOutcome {
58
        run_code: MinionResult,
59
        callback_count: u32,
60
        added_var_ok: bool,
61
        looked_up_new_var_ok: bool,
62
        add_constraint_result: Option<MinionResult>,
63
    }
64

            
65
    #[derive(Default, Debug)]
66
    struct SpawnStats {
67
        ok: usize,
68
        nonzero: usize,
69
        signal_11: usize,
70
        signal_6: usize,
71
        other_signal: usize,
72
    }
73

            
74
    #[cfg(unix)]
75
    fn status_signal(status: &ExitStatus) -> Option<i32> {
76
        use std::os::unix::process::ExitStatusExt;
77
        status.signal()
78
    }
79

            
80
    #[cfg(not(unix))]
81
    fn status_signal(_: &ExitStatus) -> Option<i32> {
82
        None
83
    }
84

            
85
    fn classify_status(status: &ExitStatus, stats: &mut SpawnStats) {
86
        if status.success() {
87
            stats.ok += 1;
88
            return;
89
        }
90

            
91
        stats.nonzero += 1;
92

            
93
        match status_signal(status) {
94
            Some(11) => stats.signal_11 += 1,
95
            Some(6) => stats.signal_6 += 1,
96
            Some(_) => stats.other_signal += 1,
97
            None => {}
98
        }
99
    }
100

            
101
    unsafe fn build_two_var_instance(instance: *mut ProbSpec_CSPInstance) {
102
        let x_name = b"x\0";
103
        let y_name = b"y\0";
104

            
105
        assert_eq!(
106
            minion_newVar(
107
                instance,
108
                x_name.as_ptr() as *mut c_char,
109
                VariableType_VAR_BOUND,
110
                0,
111
                1
112
            ),
113
            MinionResult_MINION_OK
114
        );
115
        assert_eq!(
116
            minion_newVar(
117
                instance,
118
                y_name.as_ptr() as *mut c_char,
119
                VariableType_VAR_BOUND,
120
                0,
121
                1
122
            ),
123
            MinionResult_MINION_OK
124
        );
125

            
126
        let x = minion_getVarByName(instance, x_name.as_ptr() as *mut c_char);
127
        let y = minion_getVarByName(instance, y_name.as_ptr() as *mut c_char);
128
        assert_eq!(x.result, MinionResult_MINION_OK);
129
        assert_eq!(y.result, MinionResult_MINION_OK);
130

            
131
        printMatrix_addVar(instance, x.var);
132
        printMatrix_addVar(instance, y.var);
133

            
134
        let search_vars = vec_var_new();
135
        vec_var_push_back(search_vars, x.var);
136
        vec_var_push_back(search_vars, y.var);
137
        let search_order = searchOrder_new(search_vars, VarOrderEnum_ORDER_STATIC, false);
138
        instance_addSearchOrder(instance, search_order);
139
    }
140

            
141
    unsafe extern "C" fn midsearch_mutation_callback(
142
        ctx: *mut MinionContext,
143
        userdata: *mut c_void,
144
    ) -> bool {
145
        let state = &mut *(userdata as *mut MidsearchState);
146
        state.callback_count += 1;
147

            
148
        if state.callback_count > 1 {
149
            return false;
150
        }
151

            
152
        let dyn_name = b"dyn_mid\0";
153
        state.added_var_ok = minion_newVarMidsearch(
154
            ctx,
155
            state.instance,
156
            dyn_name.as_ptr() as *mut c_char,
157
            VariableType_VAR_BOUND,
158
            0,
159
            1,
160
        ) == MinionResult_MINION_OK;
161

            
162
        let dyn_var = minion_getVarByName(state.instance, dyn_name.as_ptr() as *mut c_char);
163
        state.looked_up_new_var_ok = dyn_var.result == MinionResult_MINION_OK;
164

            
165
        if matches!(state.scenario, MidsearchScenario::AddVarThenAddEqConstraint)
166
            && state.added_var_ok
167
            && state.looked_up_new_var_ok
168
        {
169
            let x_name = b"x\0";
170
            let x_var = minion_getVarByName(state.instance, x_name.as_ptr() as *mut c_char);
171
            if x_var.result == MinionResult_MINION_OK {
172
                let eq = constraint_new(ConstraintType_CT_EQ);
173
                let mut dyn_v = dyn_var.var;
174
                let mut x_v = x_var.var;
175
                constraint_addVar(eq, &mut dyn_v);
176
                constraint_addVar(eq, &mut x_v);
177
                state.add_constraint_result =
178
                    Some(minion_addConstraintMidsearch(ctx, state.instance, eq));
179
            } else {
180
                state.add_constraint_result = Some(x_var.result);
181
            }
182
        }
183

            
184
        false
185
    }
186

            
187
    unsafe fn run_midsearch_scenario_once(scenario: MidsearchScenario) -> MidsearchOutcome {
188
        let ctx = minion_newContext();
189
        let options = searchOptions_new();
190
        let args = searchMethod_new();
191
        let instance = instance_new();
192

            
193
        (*options).silent = true;
194
        (*options).print_solution = false;
195

            
196
        build_two_var_instance(instance);
197

            
198
        let mut state = MidsearchState {
199
            scenario,
200
            instance,
201
            callback_count: 0,
202
            added_var_ok: false,
203
            looked_up_new_var_ok: false,
204
            add_constraint_result: None,
205
        };
206

            
207
        let run_code = runMinion(
208
            ctx,
209
            options,
210
            args,
211
            instance,
212
            Some(midsearch_mutation_callback),
213
            (&mut state as *mut MidsearchState).cast::<c_void>(),
214
        );
215

            
216
        minion_freeContext(ctx);
217

            
218
        MidsearchOutcome {
219
            run_code,
220
            callback_count: state.callback_count,
221
            added_var_ok: state.added_var_ok,
222
            looked_up_new_var_ok: state.looked_up_new_var_ok,
223
            add_constraint_result: state.add_constraint_result,
224
        }
225
    }
226

            
227
    fn run_midsearch_child(scenario: MidsearchScenario) -> ExitStatus {
228
        let current_test_binary =
229
            std::env::current_exe().expect("could not find current test binary");
230

            
231
        Command::new(current_test_binary)
232
            .arg("--exact")
233
            .arg(MIDSEARCH_CHILD_TEST)
234
            .arg("--nocapture")
235
            .env(MIDSEARCH_SCENARIO_ENV, scenario.as_env_value())
236
            .status()
237
            .expect("could not execute child test")
238
    }
239

            
240
1
    pub extern "C" fn hello_from_rust(ctx: *mut MinionContext, _userdata: *mut c_void) -> bool {
241
        unsafe {
242
1
            X_VAL.store(printMatrix_getValue(ctx, 0) as _, Ordering::Relaxed);
243
1
            Y_VAL.store(printMatrix_getValue(ctx, 1) as _, Ordering::Relaxed);
244
1
            Z_VAL.store(printMatrix_getValue(ctx, 2) as _, Ordering::Relaxed);
245
1
            return true;
246
        }
247
1
    }
248

            
249
    #[test]
250
1
    fn xyz_raw() {
251
        // A simple constraints model, manually written using FFI functions.
252
        // Testing to see if it does not segfault.
253
        // Results can be manually inspected in the outputted minion logs.
254
        unsafe {
255
            // See https://rust-lang.github.io/rust-bindgen/cpp.html
256
1
            let ctx = minion_newContext();
257
1
            let options = searchOptions_new();
258
1
            let args = searchMethod_new();
259
1
            let instance = instance_new();
260

            
261
1
            let x_str = CString::new("x").expect("bad x");
262
1
            let y_str = CString::new("y").expect("bad y");
263
1
            let z_str = CString::new("z").expect("bad z");
264

            
265
1
            assert_eq!(
266
1
                minion_newVar(instance, x_str.as_ptr() as _, VariableType_VAR_BOUND, 1, 3),
267
                MinionResult_MINION_OK
268
            );
269
1
            assert_eq!(
270
1
                minion_newVar(instance, y_str.as_ptr() as _, VariableType_VAR_BOUND, 2, 4),
271
                MinionResult_MINION_OK
272
            );
273
1
            assert_eq!(
274
1
                minion_newVar(instance, z_str.as_ptr() as _, VariableType_VAR_BOUND, 1, 5),
275
                MinionResult_MINION_OK
276
            );
277

            
278
1
            let x_res = minion_getVarByName(instance, x_str.as_ptr() as _);
279
1
            assert_eq!(x_res.result, MinionResult_MINION_OK);
280
1
            let x = x_res.var;
281
1
            let y_res = minion_getVarByName(instance, y_str.as_ptr() as _);
282
1
            assert_eq!(y_res.result, MinionResult_MINION_OK);
283
1
            let y = y_res.var;
284
1
            let z_res = minion_getVarByName(instance, z_str.as_ptr() as _);
285
1
            assert_eq!(z_res.result, MinionResult_MINION_OK);
286
1
            let z = z_res.var;
287

            
288
            // PRINT
289
1
            printMatrix_addVar(instance, x);
290
1
            printMatrix_addVar(instance, y);
291
1
            printMatrix_addVar(instance, z);
292

            
293
            // VARORDER
294
1
            let search_vars = vec_var_new();
295
1
            vec_var_push_back(search_vars as _, x);
296
1
            vec_var_push_back(search_vars as _, y);
297
1
            vec_var_push_back(search_vars as _, z);
298
1
            let search_order = searchOrder_new(search_vars as _, VarOrderEnum_ORDER_STATIC, false);
299
1
            instance_addSearchOrder(instance, search_order);
300

            
301
            // CONSTRAINTS
302
1
            let leq = constraint_new(ConstraintType_CT_LEQSUM);
303
1
            let geq = constraint_new(ConstraintType_CT_GEQSUM);
304
1
            let ineq = constraint_new(ConstraintType_CT_INEQ);
305

            
306
1
            let rhs_vars = vec_var_new();
307
1
            vec_var_push_back(rhs_vars, constantAsVar(4));
308

            
309
            // leq / geq : [var] [var]
310
1
            constraint_addList(leq, search_vars as _);
311
1
            constraint_addList(leq, rhs_vars as _);
312

            
313
1
            constraint_addList(geq, search_vars as _);
314
1
            constraint_addList(geq, rhs_vars as _);
315

            
316
            // ineq: [var] [var] [const]
317
1
            let x_vec = vec_var_new();
318
1
            vec_var_push_back(x_vec, x);
319

            
320
1
            let y_vec = vec_var_new();
321
1
            vec_var_push_back(y_vec, y);
322

            
323
1
            let const_vec = vec_int_new();
324
1
            vec_int_push_back(const_vec, -1);
325

            
326
1
            constraint_addList(ineq, x_vec as _);
327
1
            constraint_addList(ineq, y_vec as _);
328
1
            constraint_addConstantList(ineq, const_vec as _);
329

            
330
1
            instance_addConstraint(instance, leq);
331
1
            instance_addConstraint(instance, geq);
332
1
            instance_addConstraint(instance, ineq);
333

            
334
1
            let res = runMinion(
335
1
                ctx,
336
1
                options,
337
1
                args,
338
1
                instance,
339
1
                Some(hello_from_rust),
340
1
                std::ptr::null_mut(),
341
            );
342

            
343
            // does it get this far?
344
1
            assert_eq!(res, 0);
345

            
346
            // test if solutions are correct
347
1
            assert_eq!(X_VAL.load(Ordering::Relaxed), 1);
348
1
            assert_eq!(Y_VAL.load(Ordering::Relaxed), 2);
349
1
            assert_eq!(Z_VAL.load(Ordering::Relaxed), 1);
350

            
351
1
            minion_freeContext(ctx);
352
        }
353
1
    }
354

            
355
    #[test]
356
1
    fn midsearch_child_runner() {
357
1
        let Ok(scenario_raw) = std::env::var(MIDSEARCH_SCENARIO_ENV) else {
358
1
            return;
359
        };
360
        let scenario = MidsearchScenario::from_env_value(&scenario_raw)
361
            .expect("invalid value for MINION_MIDSEARCH_SCENARIO");
362

            
363
        let outcome = unsafe { run_midsearch_scenario_once(scenario) };
364

            
365
        assert_eq!(
366
            outcome.run_code, MinionResult_MINION_OK,
367
            "runMinion failed in child with outcome={outcome:#?}"
368
        );
369
        assert!(
370
            outcome.callback_count >= 1,
371
            "callback was never called; outcome={outcome:#?}"
372
        );
373
        assert!(
374
            outcome.added_var_ok,
375
            "minion_newVarMidsearch failed in callback; outcome={outcome:#?}"
376
        );
377
        assert!(
378
            outcome.looked_up_new_var_ok,
379
            "minion_getVarByName for new variable failed; outcome={outcome:#?}"
380
        );
381

            
382
        if matches!(scenario, MidsearchScenario::AddVarThenAddEqConstraint) {
383
            assert_eq!(
384
                outcome.add_constraint_result,
385
                Some(MinionResult_MINION_OK),
386
                "mid-search eq constraint using fresh variable failed; outcome={outcome:#?}"
387
            );
388
        }
389
1
    }
390

            
391
    #[test]
392
    #[ignore = "diagnostic stress test; runs child processes to detect crashes while mutating model mid-search"]
393
    fn midsearch_variable_addition_stress() {
394
        let scenarios = [
395
            MidsearchScenario::AddVarOnly,
396
            MidsearchScenario::AddVarThenAddEqConstraint,
397
        ];
398
        let iterations = 20usize;
399
        let mut any_failures = false;
400

            
401
        for scenario in scenarios {
402
            let mut stats = SpawnStats::default();
403

            
404
            for _ in 0..iterations {
405
                let status = run_midsearch_child(scenario);
406
                classify_status(&status, &mut stats);
407
            }
408

            
409
            eprintln!(
410
                "midsearch scenario={} iterations={} stats={stats:?}",
411
                scenario.as_env_value(),
412
                iterations
413
            );
414

            
415
            if stats.nonzero > 0 {
416
                any_failures = true;
417
            }
418
        }
419

            
420
        assert!(
421
            !any_failures,
422
            "at least one subprocess crashed/failed in mid-search variable-addition stress run"
423
        );
424
    }
425
}