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}