• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wboayue / rust-ibapi / 26435063853

26 May 2026 05:56AM UTC coverage: 92.316% (-0.008%) from 92.324%
26435063853

push

github

web-flow
accounts: decode_managed_accounts proto-only at floor 213 (PR-C3) (#636)

* accounts: decode_managed_accounts proto-only at floor 213 (PR-C3)

Gate is PROTOBUF_REST_MESSAGES_3 (213), the connection floor since #632.
Flip decode_managed_accounts wrapper to require_proto() over
&ResponseMessage; sync/async fixtures migrate from
managed_accounts().accounts(...).encode_pipe() text frames to
proto_response(IncomingMessages::ManagedAccounts, ...encode_proto()).
One-shot callsites wrap the decoder in a closure since
one_shot_with_retry processor signature is still Fn(&mut ResponseMessage).

Delete test_managed_accounts_scenarios (async) +
test_managed_accounts_additional_scenarios (sync) + ManagedAccountsTestCase
+ managed_accounts_test_cases(): the happy-path edge cases
(single-account, trailing-comma, multi-account, empty) are already covered
by the existing decode_managed_accounts_proto* unit tests in
decoders/tests.rs over the same wire format. New
test_decode_managed_accounts_rejects_text_framing locks in the proto-only
contract.

* test_utils: extract create_test_client_with_ordered_proto_responses helper

PR-C1/C2/C3 each repeated the same 4-line block to build a proto-fixture
test client: Arc::new(MessageBusStub::with_ordered_responses(vec![...]))
+ Client::stubbed(message_bus.clone(), server_versions::SIZE_RULES). PR-C3
adds five such blocks (3 sync + 2 async on managed_accounts alone). This
is the rule-of-three trigger called out in plans/floor-213-ratchet.md
§"/simplify follow-ups".

Extract the pair (async + sync siblings) alongside the existing text-only
create_test_client_with_responses helpers; fold the five new PR-C3
callsites onto it. Pre-existing manual setups in other tests stay
untouched (separate consistency-sweep PR per the plan).

* plans: mark create_test_client_with_ordered_proto_responses shipped in C3

16 of 16 new or added lines in 4 files covered. (100.0%)

5 existing lines in 3 files now uncovered.

19534 of 21160 relevant lines covered (92.32%)

26.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.96
/src/common/test_utils.rs
1
//! Test utilities shared across all modules for testing
2

3
#[cfg(test)]
4
#[allow(dead_code)] // These utilities will be used by other modules
5
pub mod helpers {
6
    use crate::stubs::MessageBusStub;
7
    use crate::{server_versions, Client};
8
    use std::sync::{Arc, RwLock};
9

10
    /// Creates a test client with an empty message bus
11
    pub fn create_test_client() -> (Client, Arc<MessageBusStub>) {
29✔
12
        create_test_client_with_version(server_versions::SIZE_RULES)
29✔
13
    }
29✔
14

15
    /// Creates a test client with a specific server version
16
    pub fn create_test_client_with_version(server_version: i32) -> (Client, Arc<MessageBusStub>) {
38✔
17
        let message_bus = Arc::new(MessageBusStub {
38✔
18
            request_messages: RwLock::new(vec![]),
38✔
19
            response_messages: vec![],
38✔
20
            ordered_responses: vec![],
38✔
21
        });
38✔
22
        let client = Client::stubbed(message_bus.clone(), server_version);
38✔
23
        (client, message_bus)
38✔
24
    }
38✔
25

26
    /// Creates a test client with specified response messages
27
    pub fn create_test_client_with_responses(responses: Vec<String>) -> (Client, Arc<MessageBusStub>) {
2✔
28
        let message_bus = Arc::new(MessageBusStub {
2✔
29
            request_messages: RwLock::new(vec![]),
2✔
30
            response_messages: responses,
2✔
31
            ordered_responses: vec![],
2✔
32
        });
2✔
33
        let client = Client::stubbed(message_bus.clone(), server_versions::SIZE_RULES);
2✔
34
        (client, message_bus)
2✔
35
    }
2✔
36

37
    /// Creates a test client with specified response messages and server version
38
    pub fn create_test_client_with_responses_and_version(responses: Vec<String>, server_version: i32) -> (Client, Arc<MessageBusStub>) {
8✔
39
        let message_bus = Arc::new(MessageBusStub {
8✔
40
            request_messages: RwLock::new(vec![]),
8✔
41
            response_messages: responses,
8✔
42
            ordered_responses: vec![],
8✔
43
        });
8✔
44
        let client = Client::stubbed(message_bus.clone(), server_version);
8✔
45
        (client, message_bus)
8✔
46
    }
8✔
47

48
    /// Creates a test client backed by [`MessageBusStub::with_ordered_responses`].
49
    /// Pairs with [`proto_response`] for proto-framed fixtures.
50
    pub fn create_test_client_with_ordered_proto_responses(responses: Vec<crate::messages::ResponseMessage>) -> (Client, Arc<MessageBusStub>) {
2✔
51
        let message_bus = Arc::new(MessageBusStub::with_ordered_responses(responses));
2✔
52
        let client = Client::stubbed(message_bus.clone(), server_versions::SIZE_RULES);
2✔
53
        (client, message_bus)
2✔
54
    }
2✔
55

56
    #[cfg(feature = "sync")]
57
    pub fn create_blocking_test_client() -> (crate::client::blocking::Client, Arc<MessageBusStub>) {
22✔
58
        create_blocking_test_client_with_version(server_versions::SIZE_RULES)
22✔
59
    }
22✔
60

61
    #[cfg(feature = "sync")]
62
    pub fn create_blocking_test_client_with_version(server_version: i32) -> (crate::client::blocking::Client, Arc<MessageBusStub>) {
32✔
63
        create_blocking_test_client_with_responses_and_version(vec![], server_version)
32✔
64
    }
32✔
65

66
    #[cfg(feature = "sync")]
UNCOV
67
    pub fn create_blocking_test_client_with_responses(responses: Vec<String>) -> (crate::client::blocking::Client, Arc<MessageBusStub>) {
×
UNCOV
68
        create_blocking_test_client_with_responses_and_version(responses, server_versions::SIZE_RULES)
×
UNCOV
69
    }
×
70

71
    #[cfg(feature = "sync")]
72
    pub fn create_blocking_test_client_with_responses_and_version(
41✔
73
        responses: Vec<String>,
41✔
74
        server_version: i32,
41✔
75
    ) -> (crate::client::blocking::Client, Arc<MessageBusStub>) {
41✔
76
        let message_bus = Arc::new(MessageBusStub {
41✔
77
            request_messages: RwLock::new(vec![]),
41✔
78
            response_messages: responses,
41✔
79
            ordered_responses: vec![],
41✔
80
        });
41✔
81
        let client = crate::client::blocking::Client::stubbed(message_bus.clone(), server_version);
41✔
82
        (client, message_bus)
41✔
83
    }
41✔
84

85
    /// Sync sibling of [`create_test_client_with_ordered_proto_responses`].
86
    #[cfg(feature = "sync")]
87
    pub fn create_blocking_test_client_with_ordered_proto_responses(
3✔
88
        responses: Vec<crate::messages::ResponseMessage>,
3✔
89
    ) -> (crate::client::blocking::Client, Arc<MessageBusStub>) {
3✔
90
        let message_bus = Arc::new(MessageBusStub::with_ordered_responses(responses));
3✔
91
        let client = crate::client::blocking::Client::stubbed(message_bus.clone(), server_versions::SIZE_RULES);
3✔
92
        (client, message_bus)
3✔
93
    }
3✔
94

95
    /// Asserts that the nth request message has the expected protobuf message ID
96
    pub fn assert_request_msg_id(message_bus: &MessageBusStub, index: usize, expected: crate::messages::OutgoingMessages) {
244✔
97
        let request_messages = message_bus.request_messages.read().unwrap();
244✔
98
        assert!(
244✔
99
            request_messages.len() > index,
244✔
100
            "Expected at least {} request messages, got {}",
101
            index + 1,
2✔
102
            request_messages.len()
2✔
103
        );
104
        assert_proto_msg_id(&request_messages[index], expected);
242✔
105
    }
242✔
106

107
    /// Gets request message count from the message bus
108
    pub fn request_message_count(message_bus: &MessageBusStub) -> usize {
156✔
109
        message_bus.request_messages.read().unwrap().len()
156✔
110
    }
156✔
111

112
    /// Decodes a protobuf request message (skips 4-byte msg_id header)
113
    pub fn decode_request_proto<T: prost::Message + Default>(message_bus: &MessageBusStub, index: usize) -> T {
237✔
114
        let request_messages = message_bus.request_messages.read().unwrap();
237✔
115
        T::decode(&request_messages[index][4..]).unwrap()
237✔
116
    }
237✔
117

118
    /// Asserts that the nth request matches the expected message id AND decodes to `expected`.
119
    /// Strict counterpart to `assert_request_msg_id`, which only checks the 4-byte header.
120
    pub fn assert_request_proto<T>(message_bus: &MessageBusStub, index: usize, expected_msg_id: crate::messages::OutgoingMessages, expected: &T)
237✔
121
    where
237✔
122
        T: prost::Message + Default + PartialEq + std::fmt::Debug,
237✔
123
    {
124
        assert_request_msg_id(message_bus, index, expected_msg_id);
237✔
125
        let actual: T = decode_request_proto(message_bus, index);
237✔
126
        assert_eq!(&actual, expected, "request {index} body mismatch");
237✔
127
    }
233✔
128

129
    /// Builder-aware variant of [`assert_request_proto`]: pulls the expected message id and
130
    /// proto body from the builder's `RequestEncoder` impl, so tests don't repeat the msg id.
131
    pub fn assert_request<B: crate::testdata::builders::RequestEncoder>(message_bus: &MessageBusStub, index: usize, expected: &B) {
233✔
132
        assert_request_proto(message_bus, index, B::MSG_ID, &expected.to_proto());
233✔
133
    }
233✔
134

135
    /// Build a text-format `ResponseMessage` for use with
136
    /// [`MessageBusStub::with_ordered_responses`]. Accepts pipe-delimited
137
    /// builder output (`encode_pipe()`) or raw NUL-delimited literals.
138
    pub fn text_response(s: impl Into<String>) -> crate::messages::ResponseMessage {
33✔
139
        crate::messages::ResponseMessage::from(&s.into().replace('|', "\0"))
33✔
140
    }
33✔
141

142
    /// Build a proto-framed `ResponseMessage` for use with
143
    /// [`MessageBusStub::with_ordered_responses`]. Pairs with
144
    /// `Builder::encode_proto()`.
145
    pub fn proto_response(msg_type: crate::messages::IncomingMessages, bytes: Vec<u8>) -> crate::messages::ResponseMessage {
288✔
146
        crate::messages::ResponseMessage::from_protobuf(msg_type as i32, bytes, server_versions::PROTOBUF_REST_MESSAGES_3)
288✔
147
    }
288✔
148

149
    /// Build a proto-framed wire payload (4-byte BE `msg_id + PROTOBUF_MSG_ID`
150
    /// followed by `proto.encode_to_vec()`). For `MemoryStream::push_inbound`
151
    /// and `spawn_handshake_listener` fixtures that need raw bytes, not a
152
    /// parsed `ResponseMessage`.
153
    pub fn binary_proto<M: prost::Message>(msg_id: i32, proto: &M) -> Vec<u8> {
50✔
154
        crate::messages::encode_protobuf_message(msg_id, &proto.encode_to_vec())
50✔
155
    }
50✔
156

157
    /// `NextValidId` proto-framed handshake frame.
158
    pub fn next_valid_id_frame(order_id: i32) -> Vec<u8> {
25✔
159
        binary_proto(
25✔
160
            crate::messages::IncomingMessages::NextValidId as i32,
25✔
161
            &crate::proto::NextValidId { order_id: Some(order_id) },
25✔
162
        )
163
    }
25✔
164

165
    /// `ManagedAccounts` proto-framed handshake frame.
166
    pub fn managed_accounts_frame(accounts: &str) -> Vec<u8> {
25✔
167
        binary_proto(
25✔
168
            crate::messages::IncomingMessages::ManagedAccounts as i32,
25✔
169
            &crate::proto::ManagedAccounts {
25✔
170
                accounts_list: Some(accounts.to_string()),
25✔
171
            },
25✔
172
        )
173
    }
25✔
174

175
    /// Common test constants that can be used across modules
176
    pub mod constants {
177
        /// Test account identifiers
178
        pub const TEST_ACCOUNT: &str = "DU1234567";
179
        pub const TEST_ACCOUNT_2: &str = "DU7654321";
180
        pub const TEST_ACCOUNT_3: &str = "DU9876543";
181

182
        /// Test model codes
183
        pub const TEST_MODEL_CODE: &str = "TARGET2024";
184
        pub const TEST_MODEL_CODE_2: &str = "GROWTH2024";
185

186
        /// Test contract IDs
187
        pub const TEST_CONTRACT_ID: i32 = 1001;
188
        pub const TEST_CONTRACT_ID_2: i32 = 2002;
189

190
        /// Test order IDs
191
        pub const TEST_ORDER_ID: i32 = 5001;
192
        pub const TEST_ORDER_ID_2: i32 = 5002;
193

194
        /// Test ticker IDs
195
        pub const TEST_TICKER_ID: i32 = 100;
196
        pub const TEST_TICKER_ID_2: i32 = 200;
197

198
        /// First request_id assigned by `Client::next_request_id()`. Mirrors
199
        /// `client::id_generator::INITIAL_REQUEST_ID` for assertions in tests
200
        /// that don't have direct access to that private constant.
201
        pub const TEST_REQ_ID_FIRST: i32 = 9000;
202
    }
203

204
    /// Re-export constants at module level for easier access
205
    pub use constants::*;
206

207
    /// Asserts the first 4 bytes of a protobuf-encoded message match the expected OutgoingMessages variant + 200 offset.
208
    pub fn assert_proto_msg_id(bytes: &[u8], expected: crate::messages::OutgoingMessages) {
319✔
209
        let msg_id = i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
319✔
210
        assert_eq!(msg_id, expected as i32 + 200);
319✔
211
    }
319✔
212

213
    /// Counts how many messages in `messages` carry the given protobuf message id (variant + 200 offset).
214
    pub fn count_proto_msgs(messages: &[Vec<u8>], expected: crate::messages::OutgoingMessages) -> usize {
6✔
215
        let target = expected as i32 + 200;
6✔
216
        messages
6✔
217
            .iter()
6✔
218
            .filter(|m| m.len() >= 4 && i32::from_be_bytes([m[0], m[1], m[2], m[3]]) == target)
11✔
219
            .count()
6✔
220
    }
6✔
221

222
    /// Builds an `Error::Notice` carrying a synthesized [`Notice`](crate::messages::Notice)
223
    /// — no wire timestamp, no advanced-order-reject JSON. Test-only sugar for the
224
    /// `Error::Notice(Notice::synthesized(code, msg))` shape used by Result-path tests
225
    /// (production code never builds these; the wire path goes through
226
    /// `From<ResponseMessage> for Error`).
227
    pub fn tws_error_notice(code: i32, message: impl Into<String>) -> crate::Error {
14✔
228
        crate::Error::Notice(crate::messages::Notice::synthesized(code, message.into()))
14✔
229
    }
14✔
230

231
    /// Asserts that `err` is `Error::Notice(notice)` where `notice.code == expected_code`
232
    /// and `notice.message` contains `expected_substring`.
233
    pub fn assert_tws_error_message(err: crate::Error, expected_code: i32, expected_substring: &str) {
39✔
234
        match err {
39✔
235
            crate::Error::Notice(notice) => {
37✔
236
                assert_eq!(notice.code, expected_code, "wrong error code");
37✔
237
                assert!(
35✔
238
                    notice.message.contains(expected_substring),
35✔
239
                    "error message {:?} does not contain {expected_substring:?}",
240
                    notice.message
241
                );
242
            }
243
            other => panic!("expected Error::Notice(code={expected_code}), got {other:?}"),
2✔
244
        }
245
    }
33✔
246
}
247

248
/// Generic round-trip / reject-unknown helpers for typed wire enums built with `impl_wire_enum!`.
249
#[cfg(test)]
250
#[allow(dead_code)] // Consumers grow as the typed-status sweep lands.
251
pub mod wire_enum {
252
    /// Assert `Display`, `FromStr`, and `ToField` agree on a hand-written
253
    /// `(variant, wire)` table. One helper covers every trait impl generated
254
    /// by `impl_wire_enum!` — independent verification (the table is not
255
    /// derived from `as_str()`, so a typo in either direction surfaces).
256
    pub fn check_wire_enum_round_trip<T>(table: &[(T, &'static str)])
12✔
257
    where
12✔
258
        T: Copy + std::fmt::Display + std::fmt::Debug + PartialEq + std::str::FromStr<Err = crate::Error> + crate::ToField,
12✔
259
    {
260
        for &(variant, wire) in table {
46✔
261
            assert_eq!(variant.to_string(), wire, "Display for {variant:?}");
46✔
262
            assert_eq!(T::from_str(wire).unwrap(), variant, "FromStr({wire})");
46✔
263
            assert_eq!(variant.to_field(), wire, "ToField for {variant:?}");
46✔
264
        }
265
    }
12✔
266

267
    /// Assert every input string in `unknowns` produces `Err(Error::Parse(..))`.
268
    pub fn check_wire_enum_rejects_unknown<T>(unknowns: &[&str])
12✔
269
    where
12✔
270
        T: std::str::FromStr<Err = crate::Error> + std::fmt::Debug,
12✔
271
    {
272
        for &s in unknowns {
70✔
273
            let err = T::from_str(s);
70✔
274
            assert!(
70✔
275
                matches!(err, Err(crate::Error::Parse(_, _, _))),
70✔
276
                "expected Parse error for {s:?}, got {err:?}",
277
            );
278
        }
279
    }
12✔
280
}
281

282
#[cfg(test)]
283
#[path = "test_utils_tests.rs"]
284
mod tests;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc