debian: type the errors of the format primitives
Give every parser of the shared format layer a proper error enum (thiserror): VersionParseError, ArchError, deps::ParseError (wrapping the version and arch failures as #[source] so the context chain survives), ControlParseError, ChecksumParseError, FilesParseError and TestsControlParseError, each carrying its structured pieces (the offending string, the stanza number, the field name) instead of a pre-formatted String. Display texts are byte-identical to the old format! strings, so user-facing output and the message-asserting tests are unchanged; the few tests asserting on error text now render the error first. Flow boundaries that still take String (build::env, quirks, test::expand) render with to_string() at the edge — typing those flows is a later step. ControlInfo's FromStr impl follows parse_content's error type.
This commit is contained in:
+31
-11
@@ -54,6 +54,30 @@ pub struct TestsControl {
|
||||
pub tests: Vec<Test>,
|
||||
}
|
||||
|
||||
/// Failure of [`TestsControl::parse_content`]: a malformed stanza, naming
|
||||
/// it by its 1-based position.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum TestsControlParseError {
|
||||
/// The stanza carries both `Tests:` and `Test-Command:`.
|
||||
#[error("stanza {stanza}: Tests and Test-Command are mutually exclusive")]
|
||||
MutuallyExclusive {
|
||||
/// The 1-based stanza position.
|
||||
stanza: usize,
|
||||
},
|
||||
/// The stanza carries neither `Tests:` nor `Test-Command:`.
|
||||
#[error("stanza {stanza}: missing a Tests or Test-Command field")]
|
||||
MissingField {
|
||||
/// The 1-based stanza position.
|
||||
stanza: usize,
|
||||
},
|
||||
/// `test-name:` appeared on a `Tests:` (script list) stanza.
|
||||
#[error("stanza {stanza}: test-name only applies to a Test-Command stanza")]
|
||||
TestNameOnScripts {
|
||||
/// The 1-based stanza position.
|
||||
stanza: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl TestsControl {
|
||||
/// Parse the test control file at `path`.
|
||||
pub fn parse(path: &Path) -> Result<TestsControl, Box<dyn std::error::Error>> {
|
||||
@@ -69,7 +93,7 @@ impl TestsControl {
|
||||
/// `Test-Command:` stanza. A file without stanzas parses to an empty
|
||||
/// [`TestsControl`] — whether the package declares tests at all is
|
||||
/// the caller's check (`Testsuite: autopkgtest`).
|
||||
pub fn parse_content(content: &str) -> Result<TestsControl, String> {
|
||||
pub fn parse_content(content: &str) -> Result<TestsControl, TestsControlParseError> {
|
||||
let mut tests_control = TestsControl::default();
|
||||
let mut command_index = 0usize;
|
||||
for (index, paragraph) in parse_paragraphs(content).iter().enumerate() {
|
||||
@@ -85,20 +109,14 @@ impl TestsControl {
|
||||
|
||||
match (tests_field, command_field) {
|
||||
(Some(_), Some(_)) => {
|
||||
return Err(format!(
|
||||
"stanza {stanza}: Tests and Test-Command are mutually exclusive"
|
||||
));
|
||||
return Err(TestsControlParseError::MutuallyExclusive { stanza });
|
||||
}
|
||||
(None, None) => {
|
||||
return Err(format!(
|
||||
"stanza {stanza}: missing a Tests or Test-Command field"
|
||||
));
|
||||
return Err(TestsControlParseError::MissingField { stanza });
|
||||
}
|
||||
(Some(scripts), None) => {
|
||||
if test_name.is_some() {
|
||||
return Err(format!(
|
||||
"stanza {stanza}: test-name only applies to a Test-Command stanza"
|
||||
));
|
||||
return Err(TestsControlParseError::TestNameOnScripts { stanza });
|
||||
}
|
||||
for script in scripts.split_whitespace() {
|
||||
tests_control.tests.push(Test {
|
||||
@@ -259,7 +277,9 @@ mod tests {
|
||||
"only applies",
|
||||
),
|
||||
] {
|
||||
let error = TestsControl::parse_content(content).unwrap_err();
|
||||
let error = TestsControl::parse_content(content)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(error.contains(expected), "{error}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user