uci_parser/
parser.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7use std::{str::FromStr, time::Duration};
8
9use nom::{
10    branch::alt,
11    bytes::complete::{tag, take_until},
12    character::complete::{anychar, digit1, multispace0, multispace1},
13    combinator::{cut, eof, map, map_res, opt, recognize, value, verify},
14    multi::{many0_count, many1_count, many_till},
15    sequence::{delimited, pair, preceded},
16    Err, IResult,
17};
18
19use crate::{UciCommand, UciParseError, UciSearchOptions};
20
21#[cfg(feature = "types")]
22use crate::{uci_move, UciMove};
23
24#[cfg(feature = "validate-promotion-moves")]
25use nom::character::complete::char;
26#[cfg(feature = "validate-promotion-moves")]
27use nom::combinator::not;
28
29#[cfg(not(feature = "types"))]
30use nom::{character::complete::one_of, sequence::tuple};
31
32/// Top-level parser to convert a string into a [`UciCommand`].
33///
34/// See also [`UciCommand::new`]
35pub(crate) fn parse_uci_command(input: &str) -> Result<UciCommand, UciParseError> {
36    #[cfg(feature = "err-on-unused-input")]
37    let mut parser =
38        nom::combinator::all_consuming(map(many_till(anychar, parse_command), |(_, cmd)| cmd));
39
40    // The `many_till(anychar)` parser consumes any unknown characters until a command is parsed.
41    #[cfg(not(feature = "err-on-unused-input"))]
42    let mut parser = map(many_till(anychar, parse_command), |(_, cmd)| cmd);
43
44    parser(input).map(|(_rest, cmd)| cmd).map_err(|e| match e {
45        // A recoverable error means that the command was not recognized
46        Err::Error(_) | Err::Incomplete(_) => UciParseError::UnrecognizedCommand {
47            cmd: input.to_string(),
48        },
49
50        // A failure means that the command was recognized, but the argument(s) provided to it were invalid.
51        Err::Failure(e) => {
52            // Find the location of where this error originated
53            let place = input.rfind(e.input).unwrap_or_default();
54
55            // If the location of this error is NOT at the end of the input string, subtract 1 from it (to get rid of trailing whitespace)
56            let place = if place < input.len() {
57                place.checked_sub(1).unwrap_or_default()
58            } else {
59                place
60            };
61
62            // Get everything that was successfully parsed.
63            let cmd = input.get(..place).unwrap().to_string();
64
65            // Get everything that *wasn't* parsed.
66            let arg = e.input.to_string();
67
68            if arg.is_empty() {
69                UciParseError::InsufficientArguments { cmd }
70            } else {
71                UciParseError::InvalidArgument { cmd, arg }
72            }
73        }
74    })
75}
76
77/// Parses a single-word [`UciCommand`] like `uci` or `isready`.
78fn single_command<'a>(
79    ident: &'static str,
80    cmd: UciCommand,
81) -> impl FnMut(&'a str) -> IResult<&'a str, UciCommand> {
82    value(cmd, term(ident))
83}
84
85/// Parses a multi-word [`UciCommand`] like `go` or `setoption`.
86fn multi_command<'a, F>(
87    ident: &'static str,
88    parser: F,
89) -> impl FnMut(&'a str) -> IResult<&'a str, UciCommand>
90where
91    F: FnMut(&'a str) -> IResult<&'a str, UciCommand>,
92{
93    preceded(term(ident), cut(parser))
94}
95
96/// Parses a single UCI command
97fn parse_command(input: &str) -> IResult<&str, UciCommand> {
98    alt((
99        single_command("uci", UciCommand::Uci),
100        multi_command("debug", parse_debug_args),
101        single_command("isready", UciCommand::IsReady),
102        multi_command("setoption", parse_setoption_args),
103        multi_command("register", parse_register_args),
104        single_command("ucinewgame", UciCommand::UciNewGame),
105        multi_command("position", parse_position_args),
106        multi_command("go", parse_go_args),
107        single_command("stop", UciCommand::Stop),
108        single_command("ponderhit", UciCommand::PonderHit),
109        single_command("quit", UciCommand::Quit),
110        #[cfg(feature = "parse-bench")]
111        multi_command("bench", parse_bench_args),
112    ))(input)
113}
114
115/// Parses arguments to the `debug` command.
116fn parse_debug_args(input: &str) -> IResult<&str, UciCommand> {
117    map(
118        alt((
119            value(true, term("on")),   // "on" to `true`
120            value(false, term("off")), // "off" to `false`
121        )),
122        UciCommand::Debug,
123    )(input)
124}
125
126/// Parses arguments to the `setoption` command.
127fn parse_setoption_args(input: &str) -> IResult<&str, UciCommand> {
128    let name = between("name", "value");
129    let value = rest_after("value");
130
131    map(pair(name, opt(value)), |(name, value)| {
132        UciCommand::SetOption {
133            name: name.to_string(),
134            value: value.map(str::to_string),
135        }
136    })(input)
137}
138
139/// Parses arguments to the `register` command.
140fn parse_register_args(input: &str) -> IResult<&str, UciCommand> {
141    // Parses `later` as `(None, None)`
142    let later = value((None, None), term("later"));
143
144    let name = between("name", "code");
145    let code = rest_after("code");
146
147    map(
148        alt((
149            later, // Either `later` which maps to no name or code
150            // Or an optional name and optional code, but at least one of the two!
151            verify(pair(opt(name), opt(code)), |(n, c)| {
152                c.is_some() || n.is_some() // Ensure a name OR code was provided
153            }),
154        )),
155        |(name, code)| UciCommand::Register {
156            name: name.map(str::to_string),
157            code: code.map(str::to_string),
158        },
159    )(input)
160}
161
162/// Parses arguments to the `position` command.
163fn parse_position_args(input: &str) -> IResult<&str, UciCommand> {
164    // Either `startpos` or `fen <FEN>`
165    let fen = alt((
166        value(None, term("startpos")),
167        map(parse_fen, |s| Some(s.to_string())),
168        #[cfg(feature = "parse-position-kiwipete")]
169        value(
170            Some(String::from(
171                "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1",
172            )),
173            term("kiwipete"),
174        ),
175    ));
176
177    let moves = map(opt(moves_after("moves")), Option::unwrap_or_default);
178
179    map(pair(fen, moves), |(fen, moves)| UciCommand::Position {
180        fen,
181        moves: moves.into_iter().map(Into::into).collect(),
182    })(input)
183}
184
185/// Parses all arguments to the `go` command.
186fn parse_go_args(input: &str) -> IResult<&str, UciCommand> {
187    map(parse_search_options, UciCommand::Go)(input)
188}
189
190/// Parses individual arguments to the `go` command into a [`UciSearchOptions`] struct.
191///
192/// This is in it's own function because [`UciSearchOptions`] is used by both `go` and `bench`.
193fn parse_search_options(input: &str) -> IResult<&str, UciSearchOptions> {
194    let mut opt = UciSearchOptions::default();
195
196    // Arguments to `go` can be in any order
197    let (input, count) = many0_count(alt((
198        map(moves_after("searchmoves"), |x| {
199            opt.searchmoves = x.into_iter().map(Into::into).collect()
200        }),
201        map(term("ponder"), |_| opt.ponder = true),
202        map(time_after("wtime"), |x| opt.wtime = Some(x)),
203        map(time_after("btime"), |x| opt.btime = Some(x)),
204        map(time_after("winc"), |x| opt.winc = Some(x)),
205        map(time_after("binc"), |x| opt.binc = Some(x)),
206        map(num_after("movestogo"), |x| opt.movestogo = Some(x)),
207        map(num_after("depth"), |x| opt.depth = Some(x)),
208        map(num_after("nodes"), |x| opt.nodes = Some(x)),
209        map(num_after("mate"), |x| opt.mate = Some(x)),
210        map(time_after("movetime"), |x| opt.movetime = Some(x)),
211        map(term("infinite"), |_| opt.infinite = true),
212        #[cfg(feature = "parse-go-perft")]
213        map(num_after("perft"), |x| opt.perft = Some(x)),
214    )))(input)?;
215
216    // If no arguments were supplied, this should be treated as `go infinite`
217    if count == 0 {
218        opt.infinite = true;
219    }
220
221    Ok((input, opt))
222}
223
224/// Parses arguments to the `bench` command.
225#[cfg(feature = "parse-bench")]
226fn parse_bench_args(input: &str) -> IResult<&str, UciCommand> {
227    map(parse_search_options, UciCommand::Bench)(input)
228}
229
230/// A combinator that takes an identifier `ident` and produces a parser that
231/// parses ONLY `ident`, consuming any leading/trailing whitespace and failing if
232/// anything other than EOF or whitespace follows `ident`.
233fn term<'a>(ident: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str> {
234    delimited(multispace0, tag(ident), alt((eof, multispace1)))
235}
236
237/// A parser to consume the remainder of `input`, erroring with a [`nom::Err::Failure`] if there is no remaining input.
238fn rest_nonempty(input: &str) -> IResult<&str, &str> {
239    recognize(cut(many1_count(anychar)))(input)
240}
241
242/// A combinator that parses everything after `ident` until `end` is found, returning everything in between.
243///
244/// This fails if there is no input between `ident` and `end` to consume.
245fn between<'a>(ident: &'a str, end: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str> {
246    preceded(
247        term(ident),
248        verify(
249            map(alt((take_until(end), rest_nonempty)), str::trim),
250            |s: &str| !s.is_empty(),
251        ),
252    )
253}
254
255/// A combinator that takes an identifier `ident` and produces a parser that discards `ident`
256/// and consumes the rest of the input, failing if there is no more input to consume.
257fn rest_after<'a>(ident: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str> {
258    preceded(term(ident), map(rest_nonempty, str::trim))
259}
260
261/// Parses a non-empty list of moves after `ident`
262#[cfg(not(feature = "types"))]
263fn moves_after<'a>(ident: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, Vec<&'a str>> {
264    preceded(
265        term(ident),
266        cut(nom::multi::many0(delimited(
267            multispace0,
268            recognize(uci_move),
269            multispace0,
270        ))),
271    )
272}
273
274#[cfg(feature = "types")]
275fn moves_after<'a>(ident: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, Vec<UciMove>> {
276    preceded(
277        term(ident),
278        cut(nom::multi::many0(delimited(
279            multispace0,
280            uci_move,
281            multispace0,
282        ))),
283    )
284}
285
286/// Parses everything after `fen` as a FEN until either the keyword `moves` or the end of the line.
287///
288/// Fails if there is no non-whitespace input following `fen`.
289fn parse_fen(input: &str) -> IResult<&str, &str> {
290    between("fen", "moves")(input)
291}
292
293/// Parses and maps a base-10 number
294///
295/// Negative numbers are handled by the `clamp-negatives` crate feature.
296fn parse_num<T>(input: &str) -> IResult<&str, T>
297where
298    T: FromStr + Default + Clone,
299{
300    #[cfg(feature = "clamp-negatives")]
301    {
302        cut(alt((
303            map_res(digit1, str::parse), // First try a normal, positive number
304            preceded(tag("-"), value(T::default(), digit1)), // Then try negative, and clamp it
305        )))(input)
306    }
307
308    #[cfg(not(feature = "clamp-negatives"))]
309    cut(map_res(digit1, str::parse))(input)
310}
311
312/// Parses and maps a positive base-10 number following `ident`
313///
314/// Negative numbers are handled by the `clamp-negatives` crate feature.
315fn num_after<'a, T>(ident: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, T>
316where
317    T: FromStr + Default + Clone,
318{
319    preceded(term(ident), parse_num)
320}
321
322/// Parses and maps a [`Duration`]
323fn parse_time(input: &str) -> IResult<&str, Duration> {
324    map(parse_num, Duration::from_millis)(input)
325}
326
327/// Parses and maps a [`Duration`] following `ident`
328fn time_after<'a>(ident: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, Duration> {
329    preceded(term(ident), parse_time)
330}
331
332#[cfg(not(feature = "types"))]
333/// Parses a UCI move, which is in the format `<start square><end square>[promotion]`.
334///
335/// This can also parse a nullmove, which is the string `0000`.
336///
337/// If the feature `validate-promotion-moves` is enabled, this function will fail to
338/// parse "invalid" moves such as `e2e4q` or `b7b8p`.
339fn uci_move(input: &str) -> IResult<&str, &str> {
340    // [a-h]
341    #[inline(always)]
342    fn file(input: &str) -> IResult<&str, char> {
343        one_of("abcdefgh")(input)
344    }
345
346    // [1-8]
347    #[inline(always)]
348    fn rank(input: &str) -> IResult<&str, char> {
349        one_of("12345678")(input)
350    }
351
352    // [a-h][1-8]
353    #[inline(always)]
354    fn square(input: &str) -> IResult<&str, (char, char)> {
355        pair(file, rank)(input)
356    }
357
358    #[cfg(not(feature = "validate-promotion-moves"))]
359    {
360        // any piece
361        let piece = one_of("PpNnBbRrQqKk");
362
363        recognize(alt((
364            recognize(tuple((square, square, opt(piece)))),
365            term("0000"),
366        )))(input)
367    }
368
369    #[cfg(feature = "validate-promotion-moves")]
370    {
371        // [qnrb]
372        #[inline(always)]
373        fn piece(input: &str) -> IResult<&str, char> {
374            one_of("QqNnRrBb")(input)
375        }
376
377        // [a-h][1-8][a-h][1-8]
378        let non_promotion = recognize(tuple((square, square, not(piece))));
379
380        // [a-h]2[a-h]1[qnrb]
381        let promotion_rank1 =
382            recognize(tuple((pair(file, char('2')), pair(file, char('1')), piece)));
383
384        // [a-h]7[a-h]8[qnrb]
385        let promotion_rank8 =
386            recognize(tuple((pair(file, char('7')), pair(file, char('8')), piece)));
387
388        alt((
389            non_promotion,
390            promotion_rank1,
391            promotion_rank8,
392            term("0000"),
393        ))(input)
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    /// Creates a new command and asserts that it is `Ok`.
402    fn new_cmd(input: &str) -> UciCommand {
403        let cmd = UciCommand::new(input);
404        assert!(
405            cmd.is_ok(),
406            "Failed to parse {input:?}\nGot {:?}",
407            cmd.unwrap_err()
408        );
409        cmd.unwrap()
410    }
411
412    /// Creates a new command and asserts that it is `Err`.
413    fn new_err(input: &str) {
414        let cmd = UciCommand::new(input);
415        assert!(cmd.is_err(), "Should error from {input:?}\nGot {cmd:?}");
416    }
417
418    /// Converts a list of UCI-notation move strings to a vector of [`UciMove`] structs.
419    ///
420    /// # Panics
421    ///
422    /// Will panic if any strings in `moves` are not valid UCI notation.
423    #[cfg(feature = "types")]
424    fn moves(moves: &[&str]) -> Vec<UciMove> {
425        moves.iter().map(|s| s.parse().unwrap()).collect()
426    }
427
428    #[cfg(not(feature = "types"))]
429    fn moves(moves: &[&str]) -> Vec<String> {
430        moves.iter().map(|s| s.to_string()).collect()
431    }
432
433    #[test]
434    fn test_parse_unknown_word() {
435        new_cmd("joho debug on");
436
437        new_err("joho");
438
439        new_err("debug joho on");
440
441        #[cfg(feature = "err-on-unused-input")]
442        new_err("debug on joho");
443
444        #[cfg(not(feature = "err-on-unused-input"))]
445        {
446            new_cmd("debug on joho");
447        }
448    }
449    #[test]
450    fn test_parse_single_commands() {
451        let cmd = new_cmd("    uci ");
452        assert_eq!(cmd, UciCommand::Uci);
453
454        let cmd = new_cmd("    \t\nuci \t   \n");
455        assert_eq!(cmd, UciCommand::Uci);
456
457        let cmd = new_cmd("    isready ");
458        assert_eq!(cmd, UciCommand::IsReady);
459    }
460
461    #[test]
462    fn test_parse_debug() {
463        let cmd = new_cmd("    debug    on");
464        assert_eq!(cmd, UciCommand::Debug(true));
465
466        let cmd = new_cmd("    debug    off    ");
467        assert_eq!(cmd, UciCommand::Debug(false));
468
469        new_err("    debug    ");
470
471        new_err("    debug    no");
472    }
473
474    #[test]
475    fn test_parse_setoption() {
476        let cmd = new_cmd("setoption name Nullmove value true\t");
477        assert_eq!(
478            cmd,
479            UciCommand::SetOption {
480                name: "Nullmove".into(),
481                value: Some("true".into())
482            }
483        );
484
485        let cmd = new_cmd("setoption     name   Selectivity    value   3  ");
486        assert_eq!(
487            cmd,
488            UciCommand::SetOption {
489                name: "Selectivity".into(),
490                value: Some("3".into())
491            }
492        );
493
494        let cmd = new_cmd("setoption     name   Style    value   Risky  ");
495        assert_eq!(
496            cmd,
497            UciCommand::SetOption {
498                name: "Style".into(),
499                value: Some("Risky".into())
500            }
501        );
502
503        let cmd = new_cmd("setoption     name   Clear Hash  ");
504        assert_eq!(
505            cmd,
506            UciCommand::SetOption {
507                name: "Clear Hash".into(),
508                value: None,
509            }
510        );
511
512        let cmd = new_cmd(
513            "setoption     name   NalimovPath    value   c:\\chess\\tb\\4;c:\\chess\\tb\\5".into(),
514        );
515        assert_eq!(
516            cmd,
517            UciCommand::SetOption {
518                name: "NalimovPath".into(),
519                value: Some("c:\\chess\\tb\\4;c:\\chess\\tb\\5".into())
520            }
521        );
522
523        new_err("setoption");
524
525        new_err("setoption  name");
526
527        new_err("setoption   name  value");
528
529        new_err("setoption   name  value    Risky");
530
531        new_err("setoption   name  Clear Hash  value");
532    }
533
534    #[test]
535    fn test_parse_register() {
536        let cmd = new_cmd("register  later");
537        assert_eq!(
538            cmd,
539            UciCommand::Register {
540                name: None,
541                code: None
542            }
543        );
544
545        let cmd = new_cmd("register    name Stefan MK   code\t\t4359874324   \t");
546        assert_eq!(
547            cmd,
548            UciCommand::Register {
549                name: Some("Stefan MK".into()),
550                code: Some("4359874324".into())
551            }
552        );
553
554        let cmd = new_cmd("register    name Stefan MK \t");
555        assert_eq!(
556            cmd,
557            UciCommand::Register {
558                name: Some("Stefan MK".into()),
559                code: None
560            }
561        );
562
563        let cmd = new_cmd("register    code\t\t4359874324   \t");
564        assert_eq!(
565            cmd,
566            UciCommand::Register {
567                name: None,
568                code: Some("4359874324".into())
569            }
570        );
571
572        new_err("register");
573
574        new_err("  register   name  ");
575
576        new_err("register   name  code  ");
577
578        new_err("register   name   Stefan MK  code");
579    }
580
581    #[test]
582    fn test_parse_ucinewgame() {
583        let cmd = new_cmd("  ucinewgame  ");
584        assert_eq!(cmd, UciCommand::UciNewGame);
585    }
586
587    #[test]
588    fn test_parse_position() {
589        let res =
590            parse_fen("fen r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1");
591        assert_eq!(
592            res.unwrap().1,
593            "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"
594        );
595
596        let res = parse_fen(
597            "fen r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1 moves e2a6",
598        );
599        assert_eq!(
600            res.unwrap().1,
601            "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"
602        );
603
604        let res = moves_after("moves")("moves   e2a6 \th3g2   h1g1 e6d5");
605        assert_eq!(res.unwrap().1, moves(&["e2a6", "h3g2", "h1g1", "e6d5"]));
606
607        let cmd = new_cmd("position startpos");
608        assert_eq!(
609            cmd,
610            UciCommand::Position {
611                fen: None,
612                moves: vec![]
613            }
614        );
615
616        let cmd = new_cmd("position   fen  r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1   moves  e2a6 \t h3g2  h1g1 e6d5");
617        assert_eq!(
618            cmd,
619            UciCommand::Position {
620                fen: Some(
621                    "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1".into()
622                ),
623                moves: moves(&["e2a6", "h3g2", "h1g1", "e6d5"])
624            }
625        );
626
627        new_err("  position ");
628        new_err("position   fen   ");
629        new_err("  position \tfen\t moves\t");
630        new_err("\t position  fen   moves   e2e4\t");
631        new_err("position   moves\t");
632        new_err("\tposition\t moves    e2e4 ");
633    }
634
635    #[test]
636    fn test_parse_num() {
637        let res = parse_num::<u64>("3141");
638        assert_eq!(res.unwrap().1, 3141);
639
640        let res = parse_num::<u8>("0");
641        assert_eq!(res.unwrap().1, 0);
642
643        let res = parse_num::<u32>("-");
644        assert!(res.is_err());
645
646        #[cfg(feature = "clamp-negatives")]
647        {
648            let res = parse_num::<u32>("-42");
649            assert_eq!(res.unwrap().1, 0);
650        }
651
652        #[cfg(not(feature = "clamp-negatives"))]
653        {
654            let res = parse_num::<u32>("-42");
655            assert!(res.is_err());
656        }
657    }
658
659    #[test]
660    fn test_parse_time() {
661        let res = parse_time("0");
662        assert_eq!(res.unwrap().1, Duration::from_millis(0));
663
664        #[cfg(feature = "clamp-negatives")]
665        {
666            let res = parse_time("-42");
667            assert_eq!(res.unwrap().1, Duration::from_millis(0));
668        }
669
670        #[cfg(not(feature = "clamp-negatives"))]
671        {
672            let res = parse_time("-42");
673            assert!(res.is_err());
674        }
675    }
676
677    #[test]
678    fn test_parse_go() {
679        let cmd = new_cmd("go");
680        assert_eq!(
681            cmd,
682            UciCommand::Go(UciSearchOptions {
683                infinite: true,
684                ..Default::default()
685            })
686        );
687
688        let cmd = new_cmd("go   infinite");
689        assert_eq!(
690            cmd,
691            UciCommand::Go(UciSearchOptions {
692                infinite: true,
693                ..Default::default()
694            })
695        );
696
697        let cmd = new_cmd("go  wtime   4000 btime   500 winc   60 binc 7");
698        assert_eq!(
699            cmd,
700            UciCommand::Go(UciSearchOptions {
701                wtime: Some(Duration::from_millis(4000)),
702                btime: Some(Duration::from_millis(500)),
703                winc: Some(Duration::from_millis(60)),
704                binc: Some(Duration::from_millis(7)),
705                ..Default::default()
706            })
707        );
708
709        let cmd = new_cmd("go  ponder   searchmoves   e2e4 \t  b6b7  nodes\t 42");
710        assert_eq!(
711            cmd,
712            UciCommand::Go(UciSearchOptions {
713                ponder: true,
714                searchmoves: moves(&["e2e4", "b6b7"]),
715                nodes: Some(42),
716                ..Default::default()
717            })
718        );
719
720        let cmd = new_cmd("go  searchmoves   e2e4  b6b7 \tponder\t\t wtime  10   btime 20\t winc 30     binc 40   movestogo   5 depth 6 nodes 7 mate  8 movetime 90  infinite  ");
721        assert_eq!(
722            cmd,
723            UciCommand::Go(UciSearchOptions {
724                searchmoves: moves(&["e2e4", "b6b7"]),
725                ponder: true,
726                wtime: Some(Duration::from_millis(10)),
727                btime: Some(Duration::from_millis(20)),
728                winc: Some(Duration::from_millis(30)),
729                binc: Some(Duration::from_millis(40)),
730                movestogo: Some(5),
731                depth: Some(6),
732                nodes: Some(7),
733                mate: Some(8),
734                movetime: Some(Duration::from_millis(90)),
735                infinite: true,
736                #[cfg(feature = "parse-go-perft")]
737                perft: None,
738            })
739        );
740
741        #[cfg(feature = "err-on-unused-input")]
742        new_err("  go   joho  ");
743
744        new_err("  go   movetime    ");
745
746        new_err("  go   movestogo    ");
747
748        new_err("  go   movestogo   mate ");
749    }
750
751    #[test]
752    fn test_parse_errors() {
753        let unknown = "shutdown".parse::<UciCommand>();
754        assert!(matches!(
755            unknown.unwrap_err(),
756            UciParseError::UnrecognizedCommand { cmd: _ }
757        ));
758
759        let invalid = "position default".parse::<UciCommand>();
760        assert!(matches!(
761            invalid.unwrap_err(),
762            UciParseError::InvalidArgument { cmd: _, arg: _ }
763        ));
764
765        let insufficient = "setoption".parse::<UciCommand>();
766        assert!(matches!(
767            insufficient.unwrap_err(),
768            UciParseError::InsufficientArguments { cmd: _ }
769        ));
770    }
771
772    #[test]
773    #[cfg(feature = "types")]
774    fn test_parse_uci_move() {
775        use crate::*;
776        // Nullmove
777        let input = "0000";
778        let res = uci_move(input);
779        assert!(res.is_ok(), "Failed to parse {input:?}\nGot {res:?}");
780        assert_eq!(
781            res.unwrap().1,
782            UciMove {
783                src: Square(File::A, Rank::One),
784                dst: Square(File::A, Rank::One),
785                promote: None
786            }
787        );
788
789        // Standard, easy-to-parse move
790        let input = "e2e4";
791        let res = uci_move(input);
792        assert!(res.is_ok(), "Failed to parse {input:?}\nGot {res:?}");
793        assert_eq!(
794            res.unwrap().1,
795            UciMove {
796                src: Square(File::E, Rank::Two),
797                dst: Square(File::E, Rank::Four),
798                promote: None
799            }
800        );
801
802        // Promotion
803        let input = "b7b8q";
804        let res = uci_move(input);
805        assert!(res.is_ok(), "Failed to parse {input:?}\nGot {res:?}");
806        assert_eq!(
807            res.unwrap().1,
808            UciMove {
809                src: Square(File::B, Rank::Seven),
810                dst: Square(File::B, Rank::Eight),
811                promote: Some(Piece::Queen)
812            }
813        );
814
815        // Invalid promotion, but still proper UCI notation/grammar
816        #[cfg(not(feature = "validate-promotion-moves"))]
817        {
818            let input = "e2e4k";
819            let res = uci_move(input);
820            assert!(res.is_ok(), "Failed to parse {input:?}\nGot {res:?}");
821            assert_eq!(
822                res.unwrap().1,
823                UciMove {
824                    src: Square(File::E, Rank::Two),
825                    dst: Square(File::E, Rank::Four),
826                    promote: Some(Piece::King)
827                }
828            );
829        }
830
831        // Invalid promotion
832        #[cfg(feature = "validate-promotion-moves")]
833        {
834            let input = "e2e4k";
835            let res = uci_move(input);
836            assert!(res.is_err());
837        }
838    }
839}