Skip to main content

rshogi_usi/
transcript.rs

1use std::error::Error;
2use std::fmt;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum TranscriptError<E> {
6    ParseFailed { line_no: usize, input: String, error: E },
7    FormatMismatch { line_no: usize, input: String, expected: String, actual: String },
8    UnexpectedSuccess { line_no: usize, input: String },
9}
10
11impl<E> TranscriptError<E> {
12    #[must_use]
13    pub const fn line_no(&self) -> usize {
14        match self {
15            Self::ParseFailed { line_no, .. }
16            | Self::FormatMismatch { line_no, .. }
17            | Self::UnexpectedSuccess { line_no, .. } => *line_no,
18        }
19    }
20}
21
22impl<E: fmt::Display> fmt::Display for TranscriptError<E> {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self {
25            Self::ParseFailed { line_no, input, error } => {
26                write!(f, "line {line_no} should parse: `{input}`: {error}")
27            }
28            Self::FormatMismatch { line_no, input, expected, actual } => write!(
29                f,
30                "line {line_no} formatted mismatch for `{input}`: expected `{expected}`, got `{actual}`"
31            ),
32            Self::UnexpectedSuccess { line_no, input } => {
33                write!(f, "line {line_no} should fail: `{input}`")
34            }
35        }
36    }
37}
38
39impl<E> Error for TranscriptError<E>
40where
41    E: Error + 'static,
42{
43    fn source(&self) -> Option<&(dyn Error + 'static)> {
44        match self {
45            Self::ParseFailed { error, .. } => Some(error),
46            Self::FormatMismatch { .. } | Self::UnexpectedSuccess { .. } => None,
47        }
48    }
49}
50
51pub fn assert_valid_transcript<T, E, P, F>(
52    content: &str,
53    mut parse: P,
54    mut format: F,
55) -> Result<(), TranscriptError<E>>
56where
57    P: FnMut(&str) -> Result<T, E>,
58    F: FnMut(&T) -> String,
59{
60    for (line_no, input, expected) in valid_cases(content) {
61        let parsed = parse(input).map_err(|error| TranscriptError::ParseFailed {
62            line_no,
63            input: input.to_string(),
64            error,
65        })?;
66        let actual = format(&parsed);
67        if actual != expected {
68            return Err(TranscriptError::FormatMismatch {
69                line_no,
70                input: input.to_string(),
71                expected: expected.to_string(),
72                actual,
73            });
74        }
75    }
76
77    Ok(())
78}
79
80pub fn assert_invalid_transcript<T, E, P>(
81    content: &str,
82    mut parse: P,
83) -> Result<(), TranscriptError<E>>
84where
85    P: FnMut(&str) -> Result<T, E>,
86{
87    for (line_no, input) in invalid_cases(content) {
88        if parse(input).is_ok() {
89            return Err(TranscriptError::UnexpectedSuccess { line_no, input: input.to_string() });
90        }
91    }
92
93    Ok(())
94}
95
96fn valid_cases(content: &str) -> impl Iterator<Item = (usize, &str, &str)> {
97    content.lines().enumerate().filter_map(|(index, line)| {
98        let trimmed = line.trim();
99        if trimmed.is_empty() || trimmed.starts_with('#') {
100            return None;
101        }
102
103        let (input, expected) = trimmed
104            .split_once("=>")
105            .map_or((trimmed, trimmed), |(lhs, rhs)| (lhs.trim(), rhs.trim()));
106        Some((index + 1, input, expected))
107    })
108}
109
110fn invalid_cases(content: &str) -> impl Iterator<Item = (usize, &str)> {
111    content.lines().enumerate().filter_map(|(index, line)| {
112        let trimmed = line.trim();
113        if trimmed.is_empty() || trimmed.starts_with('#') {
114            None
115        } else {
116            Some((index + 1, trimmed))
117        }
118    })
119}
120
121#[cfg(test)]
122mod tests {
123    use crate::{format_command, parse_line, parse_line_strict};
124
125    use super::{assert_invalid_transcript, assert_valid_transcript, TranscriptError};
126
127    #[test]
128    fn valid_transcript_helper_tracks_line_numbers() {
129        let err = assert_valid_transcript(
130            "usi\nposition startpos moves => position startpos",
131            parse_line_strict,
132            format_command,
133        )
134        .expect_err("second line should fail strict parsing");
135        assert_eq!(
136            err,
137            TranscriptError::ParseFailed {
138                line_no: 2,
139                input: "position startpos moves".to_string(),
140                error: crate::ParseError::new(crate::ParseErrorKind::NonCanonical {
141                    canonical: "position startpos".to_string(),
142                })
143                .with_canonical_token_mismatch(crate::CanonicalTokenMismatch {
144                    token_position: 3,
145                    expected: None,
146                    found: Some("moves".to_string()),
147                })
148                .with_site(crate::ParseErrorSite {
149                    token_position: 3,
150                    byte_start: 18,
151                    byte_end: 23,
152                    token: Some("moves".to_string()),
153                }),
154            }
155        );
156    }
157
158    #[test]
159    fn invalid_transcript_helper_reports_unexpected_success() {
160        let err = assert_invalid_transcript("usi", parse_line)
161            .expect_err("valid line should not pass invalid transcript");
162        assert_eq!(
163            err,
164            TranscriptError::UnexpectedSuccess { line_no: 1, input: "usi".to_string() }
165        );
166    }
167}