Skip to main content

laminar_connectors/fix/
testing.rs

1//! Local FIX counterparty for tests: a real QuickFIX acceptor, not a protocol simulator.
2//!
3//! SYNTHETIC test tooling. Passing tests against it is local conformance evidence for the
4//! engine integration, not venue certification.
5
6use std::path::Path;
7use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10
11use parking_lot::Mutex;
12
13use super::engine::{
14    self, Acceptor, Application, CallbackOutcome, GroupEntry, MessageRef, OwnedMessage, Section,
15    SessionCallbacks, SessionKey, Settings,
16};
17use crate::error::ConnectorError;
18
19/// One repeating-group entry of a test message: ordered fields, then nested groups.
20#[derive(Debug, Clone, Default)]
21pub struct TestGroupEntry {
22    /// Fields in dictionary order; the first is the group delimiter.
23    pub fields: Vec<(i32, String)>,
24    /// Nested groups as `(count tag, entries)`.
25    pub groups: Vec<(i32, Vec<TestGroupEntry>)>,
26}
27
28/// An application message sent by the test venue.
29#[derive(Debug, Clone, Default)]
30pub struct TestMessage {
31    /// MsgType (35).
32    pub msg_type: String,
33    /// Header fields beyond those the engine fills, such as ApplVerID (1128).
34    pub header_fields: Vec<(i32, String)>,
35    /// Body fields.
36    pub fields: Vec<(i32, String)>,
37    /// Body groups as `(count tag, entries)`.
38    pub groups: Vec<(i32, Vec<TestGroupEntry>)>,
39}
40
41#[derive(Default)]
42struct VenueCallbacks {
43    logged_on: AtomicBool,
44    logons: AtomicUsize,
45    rejects: Mutex<Vec<Vec<u8>>>,
46    logon_requests: Mutex<Vec<Vec<u8>>>,
47    application: Mutex<Vec<Vec<u8>>>,
48}
49
50impl SessionCallbacks for VenueCallbacks {
51    fn on_logon(&self) {
52        self.logons.fetch_add(1, Ordering::SeqCst);
53        self.logged_on.store(true, Ordering::SeqCst);
54    }
55
56    fn on_logout(&self) {
57        self.logged_on.store(false, Ordering::SeqCst);
58    }
59
60    fn inbound_admin(&self, message: MessageRef<'_>) -> CallbackOutcome {
61        let msg_type = message.header().and_then(|header| header.field(35));
62        let received = match msg_type {
63            Ok(Some(b"3")) => &self.rejects,
64            Ok(Some(b"A")) => &self.logon_requests,
65            _ => return CallbackOutcome::Accept,
66        };
67        let mut bytes = Vec::new();
68        if message.serialize_into(&mut bytes).is_ok() {
69            received.lock().push(bytes);
70        }
71        CallbackOutcome::Accept
72    }
73
74    fn inbound_app(&self, message: MessageRef<'_>) -> CallbackOutcome {
75        let mut bytes = Vec::new();
76        if message.serialize_into(&mut bytes).is_ok() {
77            self.application.lock().push(bytes);
78        }
79        CallbackOutcome::Accept
80    }
81
82    fn on_panic(&self, _callback: &'static str) {}
83}
84
85/// A local acceptor that plays the counterparty. It persists outbound messages, so it answers
86/// resend requests with real retransmissions.
87pub struct FixTestVenue {
88    port: u16,
89    key: SessionKey,
90    acceptor: Acceptor,
91    callbacks: Arc<VenueCallbacks>,
92}
93
94fn engine_error(error: engine::EngineError) -> ConnectorError {
95    ConnectorError::from(error)
96}
97
98impl FixTestVenue {
99    /// Start listening on an ephemeral loopback port as `venue_comp_id` for a client that logs
100    /// on as `client_comp_id`.
101    ///
102    /// # Errors
103    /// Fails when the engine rejects the settings or cannot listen.
104    pub fn start(
105        store: &Path,
106        dictionary: &Path,
107        begin_string: &str,
108        venue_comp_id: &str,
109        client_comp_id: &str,
110    ) -> Result<Self, ConnectorError> {
111        Self::start_with_settings(
112            store,
113            dictionary,
114            begin_string,
115            venue_comp_id,
116            client_comp_id,
117            &[],
118        )
119    }
120
121    /// As [`Self::start`], with additional engine settings for the venue.
122    ///
123    /// # Errors
124    /// Fails when the engine rejects the settings or cannot listen.
125    pub fn start_with_settings(
126        store: &Path,
127        dictionary: &Path,
128        begin_string: &str,
129        venue_comp_id: &str,
130        client_comp_id: &str,
131        settings: &[(&'static str, String)],
132    ) -> Result<Self, ConnectorError> {
133        let port = std::net::TcpListener::bind("127.0.0.1:0")
134            .and_then(|listener| listener.local_addr())
135            .map(|address| address.port())?;
136        Self::listen(
137            port,
138            store,
139            dictionary,
140            begin_string,
141            venue_comp_id,
142            client_comp_id,
143            settings,
144        )
145    }
146
147    /// As [`Self::start_with_settings`], listening on `port`.
148    ///
149    /// # Errors
150    /// Fails when the engine rejects the settings or cannot listen.
151    pub fn listen(
152        port: u16,
153        store: &Path,
154        dictionary: &Path,
155        begin_string: &str,
156        venue_comp_id: &str,
157        client_comp_id: &str,
158        settings: &[(&'static str, String)],
159    ) -> Result<Self, ConnectorError> {
160        let mut defaults = vec![
161            ("ConnectionType", "acceptor".to_string()),
162            ("SocketAcceptPort", port.to_string()),
163            ("FileStorePath", store.display().to_string()),
164            ("UseDataDictionary", "Y".to_string()),
165            ("DataDictionary", dictionary.display().to_string()),
166            ("StartTime", "00:00:00".to_string()),
167            ("EndTime", "00:00:00".to_string()),
168            ("PersistMessages", "Y".to_string()),
169        ];
170        defaults.extend(settings.iter().cloned());
171        let key = SessionKey {
172            begin_string: begin_string.to_string(),
173            sender_comp_id: venue_comp_id.to_string(),
174            target_comp_id: client_comp_id.to_string(),
175            qualifier: None,
176        };
177        let settings = Settings::single_session(&defaults, &key, &[]).map_err(engine_error)?;
178        let callbacks = Arc::new(VenueCallbacks::default());
179        let application = Application::new(callbacks.clone()).map_err(engine_error)?;
180        let acceptor = Acceptor::new(settings, application).map_err(engine_error)?;
181        acceptor.start().map_err(engine_error)?;
182        Ok(Self {
183            port,
184            key,
185            acceptor,
186            callbacks,
187        })
188    }
189
190    /// Listening port.
191    #[must_use]
192    pub fn port(&self) -> u16 {
193        self.port
194    }
195
196    /// Whether the client is logged on.
197    #[must_use]
198    pub fn logged_on(&self) -> bool {
199        self.callbacks.logged_on.load(Ordering::SeqCst)
200    }
201
202    /// Completed logons so far.
203    #[must_use]
204    pub fn logons(&self) -> usize {
205        self.callbacks.logons.load(Ordering::SeqCst)
206    }
207
208    /// Wait until the client is logged on, up to `timeout`.
209    #[must_use]
210    pub fn wait_logged_on(&self, timeout: Duration) -> bool {
211        let deadline = Instant::now() + timeout;
212        while !self.logged_on() {
213            if Instant::now() >= deadline {
214                return false;
215            }
216            std::thread::sleep(Duration::from_millis(10));
217        }
218        true
219    }
220
221    /// Send an application message. The engine assigns MsgSeqNum and SendingTime.
222    ///
223    /// # Errors
224    /// Fails when the message cannot be built or the session is not registered.
225    pub fn send(&self, message: &TestMessage) -> Result<(), ConnectorError> {
226        let mut outbound = OwnedMessage::new().map_err(engine_error)?;
227        outbound
228            .set_field(Section::Header, 35, message.msg_type.as_bytes())
229            .map_err(engine_error)?;
230        for (tag, value) in &message.header_fields {
231            outbound
232                .set_field(Section::Header, *tag, value.as_bytes())
233                .map_err(engine_error)?;
234        }
235        for (tag, value) in &message.fields {
236            outbound
237                .set_field(Section::Body, *tag, value.as_bytes())
238                .map_err(engine_error)?;
239        }
240        for (count_tag, entries) in &message.groups {
241            for entry in entries {
242                outbound
243                    .add_group(*count_tag, &borrow(entry))
244                    .map_err(engine_error)?;
245            }
246        }
247        outbound.send_to(&self.key).map_err(engine_error)
248    }
249
250    /// Skip `count` outbound sequence numbers, so the client sees a gap and requests a resend
251    /// that the venue answers with a gap fill.
252    ///
253    /// # Errors
254    /// Fails when the session is not registered.
255    pub fn skip_sequence_numbers(&self, count: u64) -> Result<(), ConnectorError> {
256        let state = engine::session_state(&self.key).map_err(engine_error)?;
257        engine::set_next_sender(&self.key, state.next_sender + count).map_err(engine_error)
258    }
259
260    /// Send SequenceReset-Reset to `new_seq` and continue numbering from it, skipping messages
261    /// the client can never recover.
262    ///
263    /// # Errors
264    /// Fails when the message cannot be sent.
265    pub fn sequence_reset(&self, new_seq: u64) -> Result<(), ConnectorError> {
266        let mut reset = OwnedMessage::new().map_err(engine_error)?;
267        reset
268            .set_field(Section::Header, 35, b"4")
269            .map_err(engine_error)?;
270        reset
271            .set_field(Section::Body, 36, new_seq.to_string().as_bytes())
272            .map_err(engine_error)?;
273        reset.send_to(&self.key).map_err(engine_error)?;
274        engine::set_next_sender(&self.key, new_seq).map_err(engine_error)
275    }
276
277    /// Reset the venue session: Logout, disconnect, and restart both sequence numbers at 1.
278    ///
279    /// # Errors
280    /// Fails when the session is not registered.
281    pub fn reset_session(&self) -> Result<(), ConnectorError> {
282        engine::reset_session(&self.key).map_err(engine_error)
283    }
284
285    /// Next outbound sequence number of the venue.
286    ///
287    /// # Errors
288    /// Fails when the session is not registered.
289    pub fn next_sender(&self) -> Result<u64, ConnectorError> {
290        Ok(engine::session_state(&self.key)
291            .map_err(engine_error)?
292            .next_sender)
293    }
294
295    /// Session-level rejects received from the client, as engine serializations.
296    #[must_use]
297    pub fn rejects(&self) -> Vec<Vec<u8>> {
298        self.callbacks.rejects.lock().clone()
299    }
300
301    /// Logon requests received from the client, as engine serializations.
302    #[must_use]
303    pub fn logon_requests(&self) -> Vec<Vec<u8>> {
304        self.callbacks.logon_requests.lock().clone()
305    }
306
307    /// Application messages received from the client, as engine serializations.
308    #[must_use]
309    pub fn received_application(&self) -> Vec<Vec<u8>> {
310        self.callbacks.application.lock().clone()
311    }
312
313    /// Stop the acceptor and join its threads.
314    ///
315    /// # Errors
316    /// Fails when the engine reports a stop error.
317    pub fn stop(&self) -> Result<(), ConnectorError> {
318        self.acceptor.stop(true).map_err(engine_error)
319    }
320}
321
322fn borrow(entry: &TestGroupEntry) -> GroupEntry<'_> {
323    GroupEntry {
324        fields: entry
325            .fields
326            .iter()
327            .map(|(tag, value)| (*tag, value.as_str()))
328            .collect(),
329        groups: entry
330            .groups
331            .iter()
332            .map(|(tag, entries)| (*tag, entries.iter().map(borrow).collect()))
333            .collect(),
334    }
335}