Skip to content

Commit cf0db91

Browse files
authored
Replace unwraps with anyhow errors in session code (#265)
* Remove unwraps during session creation * Replace unwraps in resend function with error propagation * Swallow errors when failing to send messages to the writer * Replace unwraps with anyhow errors in session code * Add test cases to assert the writer ref doesn't panic when the receiver is dropped * Add test cases for schedule handing in session layer * Add test cases for initiator to cover basic flows * Add test cases for load_from_file * Require 80% patch coverage
1 parent a6f7560 commit cf0db91

16 files changed

Lines changed: 874 additions & 175 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codecov.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
11
ignore:
22
- "**/examples/**/*"
33
- "**/tests/**/*"
4+
5+
coverage:
6+
status:
7+
patch:
8+
default:
9+
target: 80%
10+
project:
11+
default:
12+
target: auto

crates/hotfix-dictionary/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ roxmltree.workspace = true
3333
smartstring = { workspace = true, optional = true }
3434
strum.workspace = true
3535
strum_macros.workspace = true
36+
thiserror.workspace = true

crates/hotfix-dictionary/src/dictionary.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use crate::{Component, ComponentData, Datatype, DatatypeData, Field, FieldData};
22

3+
use crate::error::ParseError;
34
use crate::message_definition::{MessageData, MessageDefinition};
4-
use crate::quickfix::{ParseDictionaryError, QuickFixReader};
5+
use crate::quickfix::QuickFixReader;
56
use crate::string::SmartString;
67
use fnv::FnvHashMap;
78

@@ -52,9 +53,9 @@ impl Dictionary {
5253

5354
/// Attempts to read a QuickFIX-style specification file and convert it into
5455
/// a [`Dictionary`].
55-
pub fn from_quickfix_spec(input: &str) -> Result<Self, ParseDictionaryError> {
56+
pub fn from_quickfix_spec(input: &str) -> Result<Self, ParseError> {
5657
let xml_document =
57-
roxmltree::Document::parse(input).map_err(|_| ParseDictionaryError::InvalidFormat)?;
58+
roxmltree::Document::parse(input).map_err(|_| ParseError::InvalidFormat)?;
5859
QuickFixReader::new(&xml_document)
5960
}
6061

@@ -71,7 +72,7 @@ impl Dictionary {
7172
self.version.as_str()
7273
}
7374

74-
pub fn load_from_file(path: &str) -> Result<Self, ParseDictionaryError> {
75+
pub fn load_from_file(path: &str) -> Result<Self, ParseError> {
7576
let spec = std::fs::read_to_string(path)
7677
.unwrap_or_else(|_| panic!("unable to read FIX dictionary file at {path}"));
7778
Dictionary::from_quickfix_spec(&spec)
@@ -304,3 +305,29 @@ impl Dictionary {
304305
.collect()
305306
}
306307
}
308+
309+
#[cfg(test)]
310+
mod tests {
311+
use super::*;
312+
313+
#[test]
314+
fn test_load_from_file_success() {
315+
let path = concat!(
316+
env!("CARGO_MANIFEST_DIR"),
317+
"/src/resources/quickfix/FIX-4.4.xml"
318+
);
319+
let dict = Dictionary::load_from_file(path).unwrap();
320+
assert_eq!(dict.version(), "FIX.4.4");
321+
assert!(dict.message_by_name("Heartbeat").is_some());
322+
}
323+
324+
#[test]
325+
fn test_load_from_file_invalid_content() {
326+
let path = concat!(
327+
env!("CARGO_MANIFEST_DIR"),
328+
"/src/test_data/quickfix_specs/empty_file.xml"
329+
);
330+
let result = Dictionary::load_from_file(path);
331+
assert!(result.is_err());
332+
}
333+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
pub(crate) type ParseResult<T> = Result<T, ParseError>;
2+
3+
/// The error type that can arise when decoding a QuickFIX Dictionary.
4+
#[derive(Clone, Debug, thiserror::Error)]
5+
pub enum ParseError {
6+
#[error("invalid format")]
7+
InvalidFormat,
8+
#[error("invalid data: {0}")]
9+
InvalidData(String),
10+
}

crates/hotfix-dictionary/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod builder;
44
mod component;
55
mod datatype;
66
mod dictionary;
7+
mod error;
78
mod field;
89
mod layout;
910
mod message_definition;
@@ -14,6 +15,7 @@ use component::{Component, ComponentData};
1415
use datatype::DatatypeData;
1516
pub use datatype::{Datatype, FixDatatype};
1617
pub use dictionary::Dictionary;
18+
pub use error::ParseError;
1719
pub use field::{Field, FieldEnum, FieldLocation, IsFieldDefinition};
1820
use field::{FieldData, FieldEnumData};
1921
use fnv::FnvHashMap;

crates/hotfix-dictionary/src/quickfix.rs

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::builder::DictionaryBuilder;
22
use crate::component::{ComponentData, FixmlComponentAttributes};
3+
use crate::error::{ParseError, ParseResult};
34
use crate::message_definition::MessageData;
45
use crate::string::SmartString;
56
use crate::{
@@ -29,7 +30,7 @@ impl<'a> QuickFixReader<'a> {
2930
if child.is_element() {
3031
let name = child
3132
.attribute("name")
32-
.ok_or(ParseDictionaryError::InvalidFormat)?
33+
.ok_or(ParseError::InvalidFormat)?
3334
.to_string();
3435
import_component(&mut reader.builder, child, &name)?;
3536
}
@@ -61,23 +62,17 @@ impl<'a> QuickFixReader<'a> {
6162
let find_tagged_child = |tag: &str| {
6263
root.children()
6364
.find(|n| n.has_tag_name(tag))
64-
.ok_or_else(|| ParseDictionaryError::InvalidData(format!("<{tag}> tag not found")))
65+
.ok_or_else(|| ParseError::InvalidData(format!("<{tag}> tag not found")))
6566
};
6667
let version_type = root
6768
.attribute("type")
68-
.ok_or(ParseDictionaryError::InvalidData(
69-
"No version attribute.".to_string(),
70-
))?;
71-
let version_major = root
72-
.attribute("major")
73-
.ok_or(ParseDictionaryError::InvalidData(
74-
"No major version attribute.".to_string(),
75-
))?;
76-
let version_minor = root
77-
.attribute("minor")
78-
.ok_or(ParseDictionaryError::InvalidData(
79-
"No minor version attribute.".to_string(),
80-
))?;
69+
.ok_or(ParseError::InvalidData("No version attribute.".to_string()))?;
70+
let version_major = root.attribute("major").ok_or(ParseError::InvalidData(
71+
"No major version attribute.".to_string(),
72+
))?;
73+
let version_minor = root.attribute("minor").ok_or(ParseError::InvalidData(
74+
"No minor version attribute.".to_string(),
75+
))?;
8176
let version_sp = root.attribute("servicepack").unwrap_or("0");
8277
let version = format!(
8378
"{}.{}.{}{}",
@@ -104,19 +99,19 @@ impl<'a> QuickFixReader<'a> {
10499

105100
fn import_field(builder: &mut DictionaryBuilder, node: roxmltree::Node) -> ParseResult<()> {
106101
if node.tag_name().name() != "field" {
107-
return Err(ParseDictionaryError::InvalidFormat);
102+
return Err(ParseError::InvalidFormat);
108103
}
109104
let data_type_name = import_datatype(builder, node);
110105
let value_restrictions = value_restrictions_from_node(node, data_type_name.clone());
111106
let name = node
112107
.attribute("name")
113-
.ok_or(ParseDictionaryError::InvalidFormat)?
108+
.ok_or(ParseError::InvalidFormat)?
114109
.into();
115110
let tag = node
116111
.attribute("number")
117-
.ok_or(ParseDictionaryError::InvalidFormat)?
112+
.ok_or(ParseError::InvalidFormat)?
118113
.parse()
119-
.map_err(|_| ParseDictionaryError::InvalidFormat)?;
114+
.map_err(|_| ParseError::InvalidFormat)?;
120115
let field = FieldData {
121116
name,
122117
tag,
@@ -142,11 +137,11 @@ fn import_message(builder: &mut DictionaryBuilder, node: roxmltree::Node) -> Par
142137
let message = MessageData {
143138
name: node
144139
.attribute("name")
145-
.ok_or(ParseDictionaryError::InvalidFormat)?
140+
.ok_or(ParseError::InvalidFormat)?
146141
.into(),
147142
msg_type: node
148143
.attribute("msgtype")
149-
.ok_or(ParseDictionaryError::InvalidFormat)?
144+
.ok_or(ParseError::InvalidFormat)?
150145
.into(),
151146
component_id: 0,
152147
layout_items,
@@ -289,7 +284,7 @@ fn import_layout_item(
289284
}
290285
}
291286
_ => {
292-
return Err(ParseDictionaryError::InvalidFormat);
287+
return Err(ParseError::InvalidFormat);
293288
}
294289
};
295290
let item = LayoutItemData { required, kind };
@@ -306,13 +301,3 @@ fn panic_missing_tag_in_element(elem: roxmltree::Node, tag: &str) -> ! {
306301
.unwrap_or("Error retrieving element text")
307302
);
308303
}
309-
310-
type ParseError = ParseDictionaryError;
311-
type ParseResult<T> = Result<T, ParseError>;
312-
313-
/// The error type that can arise when decoding a QuickFIX Dictionary.
314-
#[derive(Clone, Debug)]
315-
pub enum ParseDictionaryError {
316-
InvalidFormat,
317-
InvalidData(String),
318-
}

0 commit comments

Comments
 (0)