Skip to main content

pounce_common/
options_list.rs

1//! User-set options list.
2//!
3//! Mirrors `Common/IpOptionsList.{hpp,cpp}`. Stores name → value
4//! string mappings; lookup is case-insensitive and prefix-aware. The
5//! prefix mechanism is what gives the restoration sub-algorithm its
6//! own option scope: looking up `tol` with prefix `"resto."` first
7//! tries `resto.tol`, then falls back to `tol`.
8//!
9//! Internal value representation is always a `String`, exactly as in
10//! upstream — typed accessors parse on each call, matching Ipopt
11//! behavior.
12
13use crate::exception::{ExceptionKind, SolverException};
14use crate::reg_options::{DefaultValue, OptionType, RegisteredOptions};
15use crate::throw;
16use crate::types::{Index, Number};
17use std::collections::BTreeMap;
18use std::io::Read;
19use std::rc::Rc;
20
21#[derive(Debug, Clone)]
22struct OptionValue {
23    value: String,
24    counter: std::cell::Cell<Index>,
25    allow_clobber: bool,
26    dont_print: bool,
27}
28
29impl OptionValue {
30    fn new(value: String, allow_clobber: bool, dont_print: bool) -> Self {
31        Self {
32            value,
33            counter: std::cell::Cell::new(0),
34            allow_clobber,
35            dont_print,
36        }
37    }
38    fn get_value(&self) -> &str {
39        self.counter.set(self.counter.get() + 1);
40        &self.value
41    }
42}
43
44/// One entry of an [`OptionsList`] as it stood, absence included; see
45/// [`OptionsList::snapshot_entry`].
46#[derive(Debug, Clone)]
47pub struct OptionEntrySnapshot(Option<OptionValue>);
48
49/// Mirrors `Ipopt::OptionsList`.
50#[derive(Debug, Default, Clone)]
51pub struct OptionsList {
52    options: BTreeMap<String, OptionValue>,
53    reg_options: Option<Rc<RegisteredOptions>>,
54}
55
56impl OptionsList {
57    pub fn new() -> Self {
58        Self::default()
59    }
60
61    pub fn with_registered(reg: Rc<RegisteredOptions>) -> Self {
62        Self {
63            options: BTreeMap::new(),
64            reg_options: Some(reg),
65        }
66    }
67
68    pub fn set_registered_options(&mut self, reg: Rc<RegisteredOptions>) {
69        self.reg_options = Some(reg);
70    }
71
72    pub fn registered_options(&self) -> Option<Rc<RegisteredOptions>> {
73        self.reg_options.clone()
74    }
75
76    pub fn clear(&mut self) {
77        self.options.clear();
78    }
79
80    /// Every option name present in the list, lowercased, in sorted
81    /// order.
82    ///
83    /// "Present" means someone called a `set_*_value` for it — an unset
84    /// option is absent here even though it *reads back* as its
85    /// registered default. That distinction is the whole point of the
86    /// accessor: it answers "what did this run actually mention?",
87    /// which is not a question `get_*_value` can answer.
88    pub fn names(&self) -> impl Iterator<Item = &str> {
89        self.options.keys().map(String::as_str)
90    }
91
92    fn key(name: &str) -> String {
93        name.to_ascii_lowercase()
94    }
95
96    /// Mirrors `OptionsList::find_tag` — try `prefix+tag` first, then
97    /// bare `tag`. Returns the stored string and bumps its read counter.
98    fn find_tag(&self, tag: &str, prefix: &str) -> Option<&OptionValue> {
99        if !prefix.is_empty() {
100            let key = Self::key(&format!("{prefix}{tag}"));
101            if let Some(v) = self.options.get(&key) {
102                return Some(v);
103            }
104        }
105        self.options.get(&Self::key(tag))
106    }
107
108    fn will_allow_clobber(&self, tag: &str) -> bool {
109        match self.options.get(&Self::key(tag)) {
110            Some(v) => v.allow_clobber,
111            None => true,
112        }
113    }
114
115    /// Mirrors `SetStringValue`.
116    pub fn set_string_value(
117        &mut self,
118        tag: &str,
119        value: &str,
120        allow_clobber: bool,
121        dont_print: bool,
122    ) -> Result<bool, SolverException> {
123        if let Some(reg) = &self.reg_options {
124            let opt = reg.get_option(tag).ok_or_else(|| {
125                SolverException::new(
126                    ExceptionKind::OPTION_INVALID,
127                    format!("Unknown option \"{tag}\"."),
128                    file!(),
129                    line!() as Index,
130                )
131            })?;
132            if opt.option_type != OptionType::OT_String {
133                throw!(
134                    ExceptionKind::OPTION_INVALID,
135                    format!("Option \"{tag}\" is not a string option.")
136                );
137            }
138            if !opt.is_valid_string(value) {
139                throw!(
140                    ExceptionKind::OPTION_INVALID,
141                    format!("Invalid value \"{value}\" for string option \"{tag}\".")
142                );
143            }
144        }
145        if !self.will_allow_clobber(tag) {
146            return Ok(false);
147        }
148        let stored = value.to_ascii_lowercase();
149        self.options.insert(
150            Self::key(tag),
151            OptionValue::new(stored, allow_clobber, dont_print),
152        );
153        Ok(true)
154    }
155
156    /// Mirrors `SetNumericValue`.
157    pub fn set_numeric_value(
158        &mut self,
159        tag: &str,
160        value: Number,
161        allow_clobber: bool,
162        dont_print: bool,
163    ) -> Result<bool, SolverException> {
164        if let Some(reg) = &self.reg_options {
165            let opt = reg.get_option(tag).ok_or_else(|| {
166                SolverException::new(
167                    ExceptionKind::OPTION_INVALID,
168                    format!("Unknown option \"{tag}\"."),
169                    file!(),
170                    line!() as Index,
171                )
172            })?;
173            if opt.option_type != OptionType::OT_Number {
174                throw!(
175                    ExceptionKind::OPTION_INVALID,
176                    format!("Option \"{tag}\" is not a numeric option.")
177                );
178            }
179            if !opt.is_valid_number(value) {
180                throw!(
181                    ExceptionKind::OPTION_INVALID,
182                    format!("Numeric value {value} for option \"{tag}\" out of range.")
183                );
184            }
185        }
186        if !self.will_allow_clobber(tag) {
187            return Ok(false);
188        }
189        // Print with full precision so round-trip preserves the value.
190        let s = format!("{value:.18e}");
191        self.options.insert(
192            Self::key(tag),
193            OptionValue::new(s, allow_clobber, dont_print),
194        );
195        Ok(true)
196    }
197
198    /// Mirrors `SetIntegerValue`.
199    pub fn set_integer_value(
200        &mut self,
201        tag: &str,
202        value: Index,
203        allow_clobber: bool,
204        dont_print: bool,
205    ) -> Result<bool, SolverException> {
206        if let Some(reg) = &self.reg_options {
207            let opt = reg.get_option(tag).ok_or_else(|| {
208                SolverException::new(
209                    ExceptionKind::OPTION_INVALID,
210                    format!("Unknown option \"{tag}\"."),
211                    file!(),
212                    line!() as Index,
213                )
214            })?;
215            if opt.option_type != OptionType::OT_Integer {
216                throw!(
217                    ExceptionKind::OPTION_INVALID,
218                    format!("Option \"{tag}\" is not an integer option.")
219                );
220            }
221            if !opt.is_valid_integer(value) {
222                throw!(
223                    ExceptionKind::OPTION_INVALID,
224                    format!("Integer value {value} for option \"{tag}\" out of range.")
225                );
226            }
227        }
228        if !self.will_allow_clobber(tag) {
229            return Ok(false);
230        }
231        self.options.insert(
232            Self::key(tag),
233            OptionValue::new(value.to_string(), allow_clobber, dont_print),
234        );
235        Ok(true)
236    }
237
238    /// Mirrors `SetBoolValue`.
239    pub fn set_bool_value(
240        &mut self,
241        tag: &str,
242        value: bool,
243        allow_clobber: bool,
244        dont_print: bool,
245    ) -> Result<bool, SolverException> {
246        self.set_string_value(
247            tag,
248            if value { "yes" } else { "no" },
249            allow_clobber,
250            dont_print,
251        )
252    }
253
254    /// The exact state of one entry -- its value and flags, or its absence --
255    /// for [`Self::restore_entry`] to put back. For a caller that overrides
256    /// an option for the duration of one solve and must leave the table as
257    /// it found it on every exit, unwinding included: "absent" is restored
258    /// as absent, so a registered default never comes back reading as
259    /// caller-set.
260    pub fn snapshot_entry(&self, tag: &str) -> OptionEntrySnapshot {
261        OptionEntrySnapshot(self.options.get(&Self::key(tag)).cloned())
262    }
263
264    /// Put back an entry exactly as [`Self::snapshot_entry`] found it,
265    /// bypassing `allow_clobber` (the snapshot is the entry's own earlier
266    /// state, not a new value).
267    pub fn restore_entry(&mut self, tag: &str, snap: OptionEntrySnapshot) {
268        let key = Self::key(tag);
269        match snap.0 {
270            Some(v) => {
271                self.options.insert(key, v);
272            }
273            None => {
274                self.options.remove(&key);
275            }
276        }
277    }
278
279    /// Mirrors `UnsetValue`. Returns true if the value was removed.
280    pub fn unset_value(&mut self, tag: &str) -> bool {
281        let key = Self::key(tag);
282        if let Some(v) = self.options.get(&key) {
283            if !v.allow_clobber {
284                return false;
285            }
286            self.options.remove(&key);
287            true
288        } else {
289            false
290        }
291    }
292
293    /// Mirrors `GetStringValue`. Returns true if found in the list.
294    /// Falls back to the registered default when not found.
295    pub fn get_string_value(
296        &self,
297        tag: &str,
298        prefix: &str,
299    ) -> Result<(String, bool), SolverException> {
300        if let Some(v) = self.find_tag(tag, prefix) {
301            return Ok((v.get_value().to_string(), true));
302        }
303        if let Some(reg) = &self.reg_options {
304            if let Some(opt) = reg.get_option(tag) {
305                if let DefaultValue::String(d) = &opt.default {
306                    return Ok((d.clone(), false));
307                }
308                throw!(
309                    ExceptionKind::OPTION_INVALID,
310                    format!("Option \"{tag}\" is not a string option.")
311                );
312            }
313        }
314        Ok((String::new(), false))
315    }
316
317    /// Mirrors `GetNumericValue`.
318    pub fn get_numeric_value(
319        &self,
320        tag: &str,
321        prefix: &str,
322    ) -> Result<(Number, bool), SolverException> {
323        if let Some(v) = self.find_tag(tag, prefix) {
324            let s = v.get_value().to_string();
325            let parsed = parse_ipopt_number(&s).ok_or_else(|| {
326                SolverException::new(
327                    ExceptionKind::OPTION_INVALID,
328                    format!("Option \"{tag}\": cannot parse value \"{s}\" as Number."),
329                    file!(),
330                    line!() as Index,
331                )
332            })?;
333            return Ok((parsed, true));
334        }
335        if let Some(reg) = &self.reg_options {
336            if let Some(opt) = reg.get_option(tag) {
337                if let DefaultValue::Number(d) = &opt.default {
338                    return Ok((*d, false));
339                }
340                throw!(
341                    ExceptionKind::OPTION_INVALID,
342                    format!("Option \"{tag}\" is not a numeric option.")
343                );
344            }
345        }
346        Ok((0.0, false))
347    }
348
349    /// Mirrors `GetIntegerValue`.
350    pub fn get_integer_value(
351        &self,
352        tag: &str,
353        prefix: &str,
354    ) -> Result<(Index, bool), SolverException> {
355        if let Some(v) = self.find_tag(tag, prefix) {
356            let s = v.get_value().to_string();
357            let parsed: Index = s.trim().parse().map_err(|_| {
358                SolverException::new(
359                    ExceptionKind::OPTION_INVALID,
360                    format!("Option \"{tag}\": cannot parse value \"{s}\" as Integer."),
361                    file!(),
362                    line!() as Index,
363                )
364            })?;
365            return Ok((parsed, true));
366        }
367        if let Some(reg) = &self.reg_options {
368            if let Some(opt) = reg.get_option(tag) {
369                if let DefaultValue::Integer(d) = &opt.default {
370                    return Ok((*d, false));
371                }
372                throw!(
373                    ExceptionKind::OPTION_INVALID,
374                    format!("Option \"{tag}\" is not an integer option.")
375                );
376            }
377        }
378        Ok((0, false))
379    }
380
381    /// Mirrors `GetBoolValue`. Accepts `"yes"`/`"no"`.
382    pub fn get_bool_value(&self, tag: &str, prefix: &str) -> Result<(bool, bool), SolverException> {
383        let (s, found) = self.get_string_value(tag, prefix)?;
384        let v = match s.to_ascii_lowercase().as_str() {
385            "yes" => true,
386            "no" => false,
387            other => throw!(
388                ExceptionKind::OPTION_INVALID,
389                format!("Option \"{tag}\" has non-boolean value \"{other}\".")
390            ),
391        };
392        Ok((v, found))
393    }
394
395    /// Mirrors `GetEnumValue`. Returns the index of the value in the
396    /// registered string list.
397    pub fn get_enum_value(
398        &self,
399        tag: &str,
400        prefix: &str,
401    ) -> Result<(Index, bool), SolverException> {
402        let (s, found) = self.get_string_value(tag, prefix)?;
403        let reg = self.reg_options.as_ref().ok_or_else(|| {
404            SolverException::new(
405                ExceptionKind::OPTION_INVALID,
406                "GetEnumValue requires a RegisteredOptions registry.".to_string(),
407                file!(),
408                line!() as Index,
409            )
410        })?;
411        let opt = reg.get_option(tag).ok_or_else(|| {
412            SolverException::new(
413                ExceptionKind::OPTION_INVALID,
414                format!("Unknown option \"{tag}\"."),
415                file!(),
416                line!() as Index,
417            )
418        })?;
419        let idx = opt.map_string_to_enum(&s).ok_or_else(|| {
420            SolverException::new(
421                ExceptionKind::ERROR_CONVERTING_STRING_TO_ENUM,
422                format!("Cannot map \"{s}\" to enum for option \"{tag}\"."),
423                file!(),
424                line!() as Index,
425            )
426        })?;
427        Ok((idx, found))
428    }
429
430    /// Mirrors `ReadFromStream`. Parses an `ipopt.opt`-style file:
431    /// whitespace-separated `tag value` pairs, `#` line comments,
432    /// double-quoted tokens permitted.
433    pub fn read_from_stream<R: Read>(
434        &mut self,
435        mut r: R,
436        allow_clobber: bool,
437    ) -> Result<(), SolverException> {
438        let mut s = String::new();
439        r.read_to_string(&mut s).map_err(|e| {
440            SolverException::new(
441                ExceptionKind::OPTION_INVALID,
442                format!("I/O error reading options: {e}"),
443                file!(),
444                line!() as Index,
445            )
446        })?;
447        self.read_from_str(&s, allow_clobber)
448    }
449
450    pub fn read_from_str(&mut self, s: &str, allow_clobber: bool) -> Result<(), SolverException> {
451        let mut tokens = Tokenizer::new(s);
452        loop {
453            let Some(tag) = tokens.next_token()? else {
454                return Ok(());
455            };
456            let Some(value) = tokens.next_token()? else {
457                throw!(
458                    ExceptionKind::OPTION_INVALID,
459                    format!("Error reading value for tag {tag} from option file.")
460                );
461            };
462            self.set_from_text(&tag, &value, allow_clobber)?;
463        }
464    }
465
466    fn set_from_text(
467        &mut self,
468        tag: &str,
469        value: &str,
470        allow_clobber: bool,
471    ) -> Result<(), SolverException> {
472        if let Some(reg) = self.reg_options.clone() {
473            let opt = reg.get_option(tag).ok_or_else(|| SolverException::new(
474                ExceptionKind::OPTION_INVALID,
475                format!("Read Option: \"{tag}\". It is not a valid option. Check the list of available options."),
476                file!(), line!() as Index,
477            ))?;
478            match opt.option_type {
479                OptionType::OT_String => {
480                    let ok = self.set_string_value(tag, value, allow_clobber, false)?;
481                    if !ok {
482                        throw!(
483                            ExceptionKind::OPTION_INVALID,
484                            "Error setting string value read from option file.".to_string()
485                        );
486                    }
487                }
488                OptionType::OT_Number => {
489                    let v = parse_ipopt_number(value).ok_or_else(|| SolverException::new(
490                        ExceptionKind::OPTION_INVALID,
491                        format!("Option \"{tag}\": Double value expected, but non-numeric option value \"{value}\" found.\n"),
492                        file!(), line!() as Index,
493                    ))?;
494                    let ok = self.set_numeric_value(tag, v, allow_clobber, false)?;
495                    if !ok {
496                        throw!(
497                            ExceptionKind::OPTION_INVALID,
498                            "Error setting numeric value read from file.".to_string()
499                        );
500                    }
501                }
502                OptionType::OT_Integer => {
503                    let v: Index = value.parse().map_err(|_| SolverException::new(
504                        ExceptionKind::OPTION_INVALID,
505                        format!("Option \"{tag}\": Integer value expected, but non-integer option value \"{value}\" found.\n"),
506                        file!(), line!() as Index,
507                    ))?;
508                    let ok = self.set_integer_value(tag, v, allow_clobber, false)?;
509                    if !ok {
510                        throw!(
511                            ExceptionKind::OPTION_INVALID,
512                            "Error setting integer value read from option file.".to_string()
513                        );
514                    }
515                }
516                OptionType::OT_Unknown => {
517                    throw!(
518                        ExceptionKind::OPTION_INVALID,
519                        format!("Option \"{tag}\" has unknown type.")
520                    );
521                }
522            }
523        } else {
524            self.set_string_value(tag, value, allow_clobber, false)?;
525        }
526        Ok(())
527    }
528
529    /// Mirrors `PrintList`. One option per line: `name value # used N times`.
530    pub fn print_list(&self) -> String {
531        let mut out = String::new();
532        out.push_str("                                    Name   Value           # times used\n");
533        for (k, v) in &self.options {
534            out.push_str(&format!(
535                "{:>40} = {:<30} # {}\n",
536                k,
537                v.value,
538                v.counter.get()
539            ));
540        }
541        out
542    }
543
544    /// Mirrors `PrintUserOptions`.
545    pub fn print_user_options(&self) -> String {
546        let mut out = String::new();
547        for (k, v) in &self.options {
548            if v.dont_print {
549                continue;
550            }
551            let used = if v.counter.get() > 0 {
552                "used"
553            } else {
554                "notused"
555            };
556            out.push_str(&format!("{} {} ({})\n", k, v.value, used));
557        }
558        out
559    }
560}
561
562/// Parse a number, allowing Fortran-style `d`/`D` exponents (matching
563/// `IpOptionsList::ReadFromStream`).
564fn parse_ipopt_number(s: &str) -> Option<Number> {
565    let mut buf = String::with_capacity(s.len());
566    for c in s.chars() {
567        if c == 'd' || c == 'D' {
568            buf.push('e');
569        } else {
570            buf.push(c);
571        }
572    }
573    buf.trim().parse().ok()
574}
575
576/// Tokeniser matching `OptionsList::readnexttoken` semantics:
577/// whitespace splits tokens; `#` introduces a line comment; double
578/// quotes group whitespace into a single token.
579struct Tokenizer<'a> {
580    chars: std::str::Chars<'a>,
581    peeked: Option<char>,
582}
583
584impl<'a> Tokenizer<'a> {
585    fn new(s: &'a str) -> Self {
586        Self {
587            chars: s.chars(),
588            peeked: None,
589        }
590    }
591
592    fn next_char(&mut self) -> Option<char> {
593        self.peeked.take().or_else(|| self.chars.next())
594    }
595
596    fn next_token(&mut self) -> Result<Option<String>, SolverException> {
597        let mut c = match self.next_char() {
598            Some(c) => c,
599            None => return Ok(None),
600        };
601        loop {
602            if c.is_whitespace() { /* skip */
603            } else if c == '#' {
604                // skip until newline
605                loop {
606                    match self.next_char() {
607                        Some('\n') | None => break,
608                        _ => {}
609                    }
610                }
611            } else {
612                break;
613            }
614            c = match self.next_char() {
615                Some(c) => c,
616                None => return Ok(None),
617            };
618        }
619        let inside_quotes = c == '"';
620        let mut tok = String::new();
621        if inside_quotes {
622            c = match self.next_char() {
623                Some(c) => c,
624                None => throw!(
625                    ExceptionKind::OPTION_INVALID,
626                    "Unterminated quoted string in option file.".to_string()
627                ),
628            };
629        }
630        loop {
631            if !inside_quotes && c.is_whitespace() {
632                return Ok(Some(tok));
633            }
634            if inside_quotes && c == '"' {
635                return Ok(Some(tok));
636            }
637            tok.push(c);
638            c = match self.next_char() {
639                Some(c) => c,
640                None => {
641                    if inside_quotes {
642                        throw!(
643                            ExceptionKind::OPTION_INVALID,
644                            "Unterminated quoted string in option file.".to_string()
645                        );
646                    }
647                    return Ok(Some(tok));
648                }
649            };
650        }
651    }
652}
653
654#[cfg(test)]
655mod tests {
656    use super::*;
657
658    fn registry_with_basic() -> Rc<RegisteredOptions> {
659        let r = RegisteredOptions::new();
660        r.set_registering_category("Test");
661        r.add_lower_bounded_number_option("tol", "Convergence tolerance", 0.0, true, 1e-8, "")
662            .unwrap();
663        r.add_string_option(
664            "linear_solver",
665            "Linear solver",
666            "mumps",
667            &[("mumps", ""), ("feral", "")],
668            "",
669        )
670        .unwrap();
671        r.add_lower_bounded_integer_option("max_iter", "Maximum iterations", 0, 3000, "")
672            .unwrap();
673        r.add_bool_option("print_user_options", "", false, "")
674            .unwrap();
675        r
676    }
677
678    #[test]
679    fn prefix_lookup_overrides() {
680        let reg = registry_with_basic();
681        let mut o = OptionsList::with_registered(reg);
682        o.set_numeric_value("tol", 1e-6, true, false).unwrap();
683        o.set_numeric_value("resto.tol", 1e-3, true, false).unwrap();
684        let (v_main, _) = o.get_numeric_value("tol", "").unwrap();
685        let (v_resto, _) = o.get_numeric_value("tol", "resto.").unwrap();
686        let (v_other, _) = o.get_numeric_value("tol", "noprefix.").unwrap();
687        assert!((v_main - 1e-6).abs() < 1e-20);
688        assert!((v_resto - 1e-3).abs() < 1e-20);
689        assert!((v_other - 1e-6).abs() < 1e-20);
690    }
691
692    #[test]
693    fn defaults_returned_when_unset() {
694        let reg = registry_with_basic();
695        let o = OptionsList::with_registered(reg);
696        let (v, found) = o.get_numeric_value("tol", "").unwrap();
697        assert!((v - 1e-8).abs() < 1e-20);
698        assert!(!found);
699    }
700
701    #[test]
702    fn read_options_file_text() {
703        let reg = registry_with_basic();
704        let mut o = OptionsList::with_registered(reg);
705        let opt_file = "
706# A comment line
707tol  1.0e-7
708max_iter 500
709linear_solver mumps
710print_user_options yes
711";
712        o.read_from_str(opt_file, false).unwrap();
713        assert_eq!(o.get_numeric_value("tol", "").unwrap().0, 1e-7);
714        assert_eq!(o.get_integer_value("max_iter", "").unwrap().0, 500);
715        assert_eq!(o.get_string_value("linear_solver", "").unwrap().0, "mumps");
716        assert!(o.get_bool_value("print_user_options", "").unwrap().0);
717    }
718
719    #[test]
720    fn fortran_d_exponent_accepted() {
721        let reg = registry_with_basic();
722        let mut o = OptionsList::with_registered(reg);
723        o.read_from_str("tol 1.0d-9\n", false).unwrap();
724        assert!((o.get_numeric_value("tol", "").unwrap().0 - 1e-9).abs() < 1e-30);
725    }
726
727    #[test]
728    fn unknown_option_in_file_is_error() {
729        let reg = registry_with_basic();
730        let mut o = OptionsList::with_registered(reg);
731        let err = o.read_from_str("nonsense_option 1.0\n", false).unwrap_err();
732        assert_eq!(err.kind, ExceptionKind::OPTION_INVALID);
733    }
734
735    #[test]
736    fn invalid_string_value_rejected() {
737        let reg = registry_with_basic();
738        let mut o = OptionsList::with_registered(reg);
739        let err = o
740            .set_string_value("linear_solver", "ma27", true, false)
741            .unwrap_err();
742        assert_eq!(err.kind, ExceptionKind::OPTION_INVALID);
743    }
744
745    #[test]
746    fn out_of_range_number_rejected() {
747        let reg = registry_with_basic();
748        let mut o = OptionsList::with_registered(reg);
749        let err = o.set_numeric_value("tol", 0.0, true, false).unwrap_err();
750        assert_eq!(err.kind, ExceptionKind::OPTION_INVALID);
751    }
752
753    #[test]
754    fn enum_value_index() {
755        let reg = registry_with_basic();
756        let mut o = OptionsList::with_registered(reg);
757        o.set_string_value("linear_solver", "feral", true, false)
758            .unwrap();
759        assert_eq!(o.get_enum_value("linear_solver", "").unwrap().0, 1);
760    }
761
762    #[test]
763    fn get_value_increments_use_counter() {
764        let reg = registry_with_basic();
765        let mut o = OptionsList::with_registered(reg);
766        o.set_numeric_value("tol", 1e-6, true, false).unwrap();
767        let _ = o.get_numeric_value("tol", "").unwrap();
768        let _ = o.get_numeric_value("tol", "").unwrap();
769        let listing = o.print_list();
770        assert!(listing.contains("# 2"));
771    }
772}