ibapi 5.4.0

An unofficial Rust client for the Interactive Brokers TWS API: market data, orders, and account data, with async (Tokio) and blocking clients for TWS and IB Gateway.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
//! Test utilities shared across all modules for testing

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

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

    /// Creates a test client with a specific server version
    pub fn create_test_client_with_version(server_version: i32) -> (Client, Arc<MessageBusStub>) {
        let message_bus = Arc::new(MessageBusStub::default());
        let client = Client::stubbed(message_bus.clone(), server_version);
        (client, message_bus)
    }

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

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

    #[cfg(feature = "sync")]
    pub fn create_blocking_test_client_with_version(server_version: i32) -> (crate::client::blocking::Client, Arc<MessageBusStub>) {
        let message_bus = Arc::new(MessageBusStub::default());
        let client = crate::client::blocking::Client::stubbed(message_bus.clone(), server_version);
        (client, message_bus)
    }

    /// Sync sibling of [`create_test_client_with_ordered_proto_responses`].
    #[cfg(feature = "sync")]
    pub fn create_blocking_test_client_with_ordered_proto_responses(
        responses: Vec<crate::messages::ResponseMessage>,
    ) -> (crate::client::blocking::Client, Arc<MessageBusStub>) {
        let message_bus = Arc::new(MessageBusStub::with_ordered_responses(responses));
        let client = crate::client::blocking::Client::stubbed(message_bus.clone(), server_versions::SIZE_RULES);
        (client, message_bus)
    }

    /// Asserts that the nth request message has the expected protobuf message ID
    pub fn assert_request_msg_id(message_bus: &MessageBusStub, index: usize, expected: crate::messages::OutgoingMessages) {
        let request_messages = message_bus.request_messages.read().unwrap();
        assert!(
            request_messages.len() > index,
            "Expected at least {} request messages, got {}",
            index + 1,
            request_messages.len()
        );
        assert_proto_msg_id(&request_messages[index], expected);
    }

    /// Gets request message count from the message bus
    pub fn request_message_count(message_bus: &MessageBusStub) -> usize {
        message_bus.request_messages.read().unwrap().len()
    }

    /// Decodes a protobuf request message (skips the msg_id header)
    pub fn decode_request_proto<T: prost::Message + Default>(message_bus: &MessageBusStub, index: usize) -> T {
        let request_messages = message_bus.request_messages.read().unwrap();
        T::decode(&request_messages[index][crate::messages::MESSAGE_ID_LEN..]).unwrap()
    }

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

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

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

    /// Build a proto-framed `ResponseMessage` for use with
    /// [`MessageBusStub::with_ordered_responses`]. Pairs with
    /// `Builder::encode_proto()`.
    pub fn proto_response(msg_type: crate::messages::IncomingMessages, bytes: Vec<u8>) -> crate::messages::ResponseMessage {
        crate::messages::ResponseMessage::from_protobuf(msg_type as i32, bytes)
    }

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

    /// A message id that maps to no [`IncomingMessages`](crate::messages::IncomingMessages)
    /// variant — what a mis-framed read produces once the length prefix has
    /// slipped. Assertions on the resulting diagnostic name this value, so it
    /// lives here rather than being spelled at each site.
    pub const UNKNOWN_MESSAGE_ID: i32 = 9799;

    /// Proto-framed wire payload whose message id maps to no known kind, for
    /// driving the unroutable-frame reporting. Payload is filler — nothing
    /// decodes it, because nothing can route it.
    pub fn unknown_message_frame() -> Vec<u8> {
        crate::messages::encode_protobuf_message(UNKNOWN_MESSAGE_ID, &[0x08, 0x64])
    }

    /// `NextValidId` proto-framed handshake frame.
    pub fn next_valid_id_frame(order_id: i32) -> Vec<u8> {
        use crate::testdata::builders::{orders::next_valid_id, ResponseProtoEncoder};
        binary_proto(
            crate::messages::IncomingMessages::NextValidId as i32,
            &next_valid_id().order_id(order_id).to_proto(),
        )
    }

    /// `ManagedAccounts` proto-framed handshake frame.
    pub fn managed_accounts_frame(accounts: &str) -> Vec<u8> {
        binary_proto(
            crate::messages::IncomingMessages::ManagedAccounts as i32,
            &crate::proto::ManagedAccounts {
                accounts_list: Some(accounts.to_string()),
            },
        )
    }

    /// Build a `proto::ErrorMessage` envelope with `error_time` and
    /// `advanced_order_reject_json` defaulted (set those fields on the returned
    /// struct when a test needs them). `None` for `request_id` / `code`
    /// expresses a request-less / code-less frame — the fields are optional on
    /// the wire and IB Gateway omits both on informational notices.
    pub fn error_envelope(request_id: Option<i32>, code: Option<i32>, msg: impl Into<String>) -> crate::proto::ErrorMessage {
        crate::proto::ErrorMessage {
            id: request_id,
            error_time: None,
            error_code: code,
            error_msg: Some(msg.into()),
            advanced_order_reject_json: None,
        }
    }

    /// Proto-framed `Error` [`ResponseMessage`](crate::messages::ResponseMessage)
    /// for `MessageBusStub::with_ordered_responses` fixtures.
    pub fn proto_error_response(request_id: i32, code: i32, msg: impl Into<String>) -> crate::messages::ResponseMessage {
        proto_response(
            crate::messages::IncomingMessages::Error,
            prost::Message::encode_to_vec(&error_envelope(Some(request_id), Some(code), msg)),
        )
    }

    /// Proto-framed `Error` wire payload (`[4-byte BE msg_id][proto bytes]`)
    /// for `MemoryStream::push_inbound` / `spawn_handshake_listener` fixtures.
    pub fn error_frame(request_id: i32, code: i32, msg: impl Into<String>) -> Vec<u8> {
        binary_proto(
            crate::messages::IncomingMessages::Error as i32,
            &error_envelope(Some(request_id), Some(code), msg),
        )
    }

    /// Binary-text wire payload: `[4-byte BE msg_id][payload]`. `payload` is
    /// passed through verbatim — NUL-delimit the fields yourself, or use
    /// [`body`] for the pipe-delimited shorthand.
    pub fn binary_text(msg_id: i32, payload: &str) -> Vec<u8> {
        let mut data = Vec::with_capacity(4 + payload.len());
        data.extend_from_slice(&msg_id.to_be_bytes());
        data.extend_from_slice(payload.as_bytes());
        data
    }

    /// Build a binary-text-payload response body from a pipe-delimited test input.
    /// `"msg_id|f1|f2|..."` → `[4-byte BE msg_id][f1\0f2\0...]`. Pipes are
    /// stand-ins for NULs so test inputs stay readable. For `Error` frames,
    /// use [`error_frame`] — they ship as protobuf post-floor-213 and the
    /// binary-text-payload path defaults to an empty Notice.
    pub fn body(text: &str) -> Vec<u8> {
        let fields: Vec<&str> = text.split_terminator('|').collect();
        let msg_id: i32 = fields[0].parse().expect("body() fixture must start with a numeric msg_id");
        debug_assert_ne!(
            msg_id,
            crate::messages::IncomingMessages::Error as i32,
            "Error frames must use error_frame() — protobuf-framed since PR-D1"
        );
        let payload: String = fields[1..].iter().map(|f| format!("{f}\0")).collect();
        binary_text(msg_id, &payload)
    }

    /// Raw-text handshake response frame: `"<sv>\0<connection-time>\0"`.
    /// For tests that interleave extra frames into the handshake; otherwise
    /// use [`handshake_frames`].
    pub fn handshake_response_frame(server_version: i32, zone: &str) -> Vec<u8> {
        format!("{server_version}\020240120 12:00:00 {zone}\0").into_bytes()
    }

    /// Connect-time frames a server sends: [`handshake_response_frame`], then
    /// `NextValidId` and `ManagedAccounts` (for [`TEST_ACCOUNT`]).
    pub fn handshake_frames(server_version: i32, zone: &str, next_order_id: i32) -> Vec<Vec<u8>> {
        vec![
            handshake_response_frame(server_version, zone),
            next_valid_id_frame(next_order_id),
            managed_accounts_frame(TEST_ACCOUNT),
        ]
    }

    /// Message text of the farm-OK informational notice (code 2104).
    pub const FARM_OK_MSG: &str = "Market data farm connection is OK:usfarm";

    /// Farm-OK notice frame scoped to `RequestId::nth(42)`.
    pub fn farm_ok_frame_42() -> Vec<u8> {
        error_frame(crate::client::ids::RequestId::nth(42).raw(), 2104, FARM_OK_MSG)
    }

    /// Farm-OK notice frame with no request id (`-1`), i.e. unrouted.
    pub fn farm_ok_frame_unrouted() -> Vec<u8> {
        error_frame(-1, 2104, FARM_OK_MSG)
    }

    /// Proto-framed ExecutionData frame. `request_id` is at proto tag 1; the
    /// dispatcher's `order_id` / `execution_id` accessors read the nested
    /// `execution.{order_id, exec_id}` sub-message via `ExecutionDetailsMinimal`.
    pub fn execution_data_frame(client_id: i32, request_id: i32, order_id: i32, execution_id: &str) -> Vec<u8> {
        use crate::testdata::builders::ResponseProtoEncoder;
        let response = crate::testdata::builders::orders::execution_data()
            .client_id(client_id)
            .request_id(request_id)
            .order_id(order_id)
            .execution_id(execution_id);
        binary_proto(crate::messages::IncomingMessages::ExecutionData as i32, &response.to_proto())
    }

    /// Proto-framed OrderStatus frame for `client_id`'s order; `order_id` is at proto tag 1.
    pub fn order_status_frame(client_id: i32, order_id: i32, status: crate::orders::OrderStatusKind) -> Vec<u8> {
        use crate::testdata::builders::ResponseProtoEncoder;
        let response = crate::testdata::builders::orders::order_status()
            .client_id(client_id)
            .order_id(order_id)
            .status(status);
        binary_proto(crate::messages::IncomingMessages::OrderStatus as i32, &response.to_proto())
    }

    /// [`proto_response`] for an OrderStatus builder.
    pub fn order_status_response(status: crate::testdata::builders::orders::OrderStatusResponse) -> crate::messages::ResponseMessage {
        use crate::testdata::builders::ResponseProtoEncoder;
        proto_response(crate::messages::IncomingMessages::OrderStatus, status.encode_proto())
    }

    /// Stub [`StreamDecoder`](crate::subscriptions::StreamDecoder) that
    /// accepts `HistogramData` and decodes every frame to a unit value. For
    /// tests that care about routing/notices, not payloads.
    #[derive(Debug)]
    pub struct NoticeTestData;

    impl crate::subscriptions::StreamDecoder<NoticeTestData> for NoticeTestData {
        const RESPONSE_MESSAGE_IDS: &'static [crate::messages::IncomingMessages] = &[crate::messages::IncomingMessages::HistogramData];

        fn decode(_context: &crate::subscriptions::DecoderContext, _msg: &crate::messages::ResponseMessage) -> Result<NoticeTestData, crate::Error> {
            Ok(NoticeTestData)
        }
    }

    /// [`NoticeSink`](crate::transport::common::NoticeSink) that captures
    /// every delivered notice.
    #[derive(Default)]
    pub struct CapturingSink {
        notices: std::sync::Mutex<Vec<crate::messages::Notice>>,
    }

    impl CapturingSink {
        /// Snapshot of every notice delivered so far.
        pub fn notices(&self) -> Vec<crate::messages::Notice> {
            self.notices.lock().unwrap().clone()
        }
        /// Most recently delivered notice, if any.
        pub fn last(&self) -> Option<crate::messages::Notice> {
            self.notices.lock().unwrap().last().cloned()
        }
        /// Number of notices delivered so far.
        pub fn count(&self) -> usize {
            self.notices.lock().unwrap().len()
        }
    }

    impl crate::transport::common::NoticeSink for CapturingSink {
        fn deliver(&self, notice: crate::messages::Notice) {
            self.notices.lock().unwrap().push(notice);
        }
    }

    /// A request-less IB notice for tests (no error_time / advanced-reject
    /// payload). Field-for-field [`Notice::synthesized`](crate::messages::Notice::synthesized);
    /// named separately so TWS-code tests don't read as client-sentinel tests.
    pub fn test_notice(code: i32, message: impl Into<String>) -> crate::messages::Notice {
        crate::messages::Notice::synthesized(code, message.into())
    }

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

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

        /// Test contract IDs
        pub const TEST_CONTRACT_ID: i32 = 1001;
        pub const TEST_CONTRACT_ID_2: i32 = 2002;

        /// Test order IDs
        pub const TEST_ORDER_ID: i32 = 5001;
        pub const TEST_ORDER_ID_2: i32 = 5002;

        /// Test ticker IDs
        pub const TEST_TICKER_ID: i32 = 100;
        pub const TEST_TICKER_ID_2: i32 = 200;

        /// First request_id assigned by `Client::mint_request_id()`: the
        /// request-range floor (`client::ids::REQUEST_ID_FLOOR`).
        pub const TEST_REQ_ID_FIRST: i32 = crate::client::ids::REQUEST_ID_FLOOR;

        /// Order-id seed of `Client::stubbed`; any value below the request-id
        /// floor works.
        pub const TEST_ORDER_ID_SEED: i32 = 9000;
    }

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

    /// The raw msg_id header of an encoded message, `None` if too short to hold one.
    fn wire_msg_id(bytes: &[u8]) -> Option<i32> {
        bytes
            .split_first_chunk::<{ crate::messages::MESSAGE_ID_LEN }>()
            .map(|(header, _)| i32::from_be_bytes(*header))
    }

    /// Asserts the msg_id header of a protobuf-encoded message matches the expected OutgoingMessages variant + `PROTOBUF_MSG_ID` offset.
    pub fn assert_proto_msg_id(bytes: &[u8], expected: crate::messages::OutgoingMessages) {
        assert_eq!(wire_msg_id(bytes), Some(expected as i32 + crate::messages::PROTOBUF_MSG_ID));
    }

    /// Counts how many messages in `messages` carry the given protobuf message id (variant + `PROTOBUF_MSG_ID` offset).
    pub fn count_proto_msgs(messages: &[Vec<u8>], expected: crate::messages::OutgoingMessages) -> usize {
        let target = Some(expected as i32 + crate::messages::PROTOBUF_MSG_ID);
        messages.iter().filter(|m| wire_msg_id(m) == target).count()
    }

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

    /// Asserts that `err` is `Error::Notice(notice)` where `notice.code == expected_code`
    /// and `notice.message` contains `expected_substring`.
    pub fn assert_tws_error_message(err: crate::Error, expected_code: i32, expected_substring: &str) {
        match err {
            crate::Error::Notice(notice) => {
                assert_eq!(notice.code, expected_code, "wrong error code");
                assert!(
                    notice.message.contains(expected_substring),
                    "error message {:?} does not contain {expected_substring:?}",
                    notice.message
                );
            }
            other => panic!("expected Error::Notice(code={expected_code}), got {other:?}"),
        }
    }

    /// Asserts that a decoder rejected a malformed decimal wire field, naming the
    /// offending value in the error.
    ///
    /// Used by the per-decoder "is this field wired to `parse_optional_decimal`"
    /// tests. The helper's own semantics are covered exhaustively in
    /// `src/proto/decoders_tests.rs`; these call sites only prove the wiring, so
    /// they all want this one assertion rather than their own `matches!`.
    pub fn assert_decimal_parse_error<T: std::fmt::Debug>(result: Result<T, crate::Error>, offending_value: &str) {
        match result {
            Err(crate::Error::Parse(_, value, msg)) => {
                assert_eq!(value, offending_value, "error should carry the offending wire value");
                assert!(msg.contains("invalid decimal wire value"), "unexpected message: {msg}");
            }
            other => panic!("expected Error::Parse for {offending_value:?}, got {other:?}"),
        }
    }

    /// Asserts that a decoder failed on an absent required field, via
    /// `proto::decoders::required`: `Error::Parse` naming the field and the
    /// message that should have carried it.
    pub fn assert_missing_field<T: std::fmt::Debug>(result: Result<T, crate::Error>, field: &str, message: &str) {
        match result {
            Err(crate::Error::Parse(_, name, reason)) => {
                assert_eq!(name, field);
                assert_eq!(reason, format!("missing in {message}"));
            }
            other => panic!("expected Error::Parse for a missing {field} in {message}, got {other:?}"),
        }
    }

    /// Asserts that a proto-only decoder rejects a text-framed frame of the type
    /// it handles, with [`Error::UnexpectedWireFormat`](crate::Error::UnexpectedWireFormat).
    ///
    /// One assertion for every `*_rejects_text_framing` test. The 22 of them
    /// spelled it four ways (`expect_err` + `matches!`, `unwrap_err` + `matches!`,
    /// a `match` with `panic!`, and three different panic messages), which is why
    /// #731's rename of a single variant produced ~600 test-side lines.
    ///
    /// `expected` is checked against the frame's own leading discriminant before
    /// the decoder runs. That is not ceremony: `require_proto` fails on framing
    /// without reading the type, so a fixture naming the wrong message id passes
    /// the assertion anyway — #738 found exactly that in four fixtures (`87` for
    /// `MarketRule`, which is 93; a literal `"newsProviders"` that parses as no
    /// discriminant at all). A fixture field no assertion depends on will be
    /// wrong eventually.
    pub fn assert_rejects_text_framing<T: std::fmt::Debug>(
        expected: crate::messages::IncomingMessages,
        text_frame: &str,
        decode: impl FnOnce(&crate::messages::ResponseMessage) -> Result<T, crate::Error>,
    ) {
        let message = crate::messages::ResponseMessage::from(text_frame);
        assert_eq!(
            message.message_type(),
            expected,
            "fixture is framed as the wrong message type; the decoder never reads it"
        );

        match decode(&message) {
            Err(crate::Error::UnexpectedWireFormat(_)) => {}
            other => panic!("expected Error::UnexpectedWireFormat for a text-framed {expected:?}, got {other:?}"),
        }
    }
}

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

    /// Assert every input string in `unknowns` produces `Err(Error::Parse(..))`.
    pub fn check_wire_enum_rejects_unknown<T>(unknowns: &[&str])
    where
        T: std::str::FromStr<Err = crate::Error> + std::fmt::Debug,
    {
        for &s in unknowns {
            let err = T::from_str(s);
            assert!(
                matches!(err, Err(crate::Error::Parse(_, _, _))),
                "expected Parse error for {s:?}, got {err:?}",
            );
        }
    }

    /// Codes probed for the completeness half of [`check_wire_code_round_trip`].
    /// Wide enough to cover any code IB would plausibly add to one of these
    /// enums — the largest in use is `TriggerMethod::Midpoint` at 8 — and the
    /// negative end guards a `From<i32>` that reaches for `.abs()` or `as u32`.
    const PROBE_CODES: std::ops::RangeInclusive<i32> = -8..=64;

    /// Assert `From<i32>` and `From<T> for i32` agree on a hand-written
    /// `(variant, code)` table for an integer-coded wire enum,
    /// **and that the table lists every code the enum models**.
    ///
    /// The completeness half is what the `_every_wire_code` in the callers'
    /// names claims, and the reason this takes `unknown`: `From<i32>` is total,
    /// so every code outside the table has to land on `unknown(code)`. A
    /// variant added without a table row is modeled, so the probe finds it —
    /// the gap `all_tifs_covers_every_variant` closes for the string enums
    /// (docs/rules/wire/enum-typing.md).
    ///
    /// Listing `Unknown(code)` rows in the table is still useful — only those
    /// exercise `Into<i32>` on the payload variant — and costs
    /// nothing: a listed code is skipped by the probe that would re-derive it.
    pub fn check_wire_code_round_trip<T>(table: &[(T, i32)], unknown: fn(i32) -> T)
    where
        T: From<i32> + Into<i32> + Copy + PartialEq + std::fmt::Debug,
    {
        for &(variant, code) in table {
            assert_eq!(T::from(code), variant, "From({code})");
            assert_eq!(variant.into(), code, "i32::from({variant:?})");
        }

        for code in PROBE_CODES {
            if table.iter().any(|&(_, listed)| listed == code) {
                continue;
            }
            assert_eq!(
                T::from(code),
                unknown(code),
                "code {code} is modeled by a variant the table does not list"
            );
        }
    }
}

/// Walking the crate's own source, for the gates that check a hand-listed roster
/// against the tree.
#[cfg(test)]
pub mod source_scan {
    use std::path::Path;

    /// Hand every production `.rs` file under `src/` to `visit` as
    /// `(path, contents)`.
    ///
    /// **What counts as production source is defined here and nowhere else.**
    /// `tests.rs` and `*_tests.rs` are skipped: they hold test-only decoders and
    /// fixtures that exist to exercise the drivers, not to decode a wire
    /// message, and a roster gate that counted them would report failures no
    /// production change could fix. `response_message_ids_tests` is the one gate
    /// that depends on this definition, since #749 replaced the other — the
    /// one-shot pairing roster — with a trait the compiler enumerates.
    ///
    /// Takes a visitor rather than returning a `Vec` so a caller looking for
    /// several things reads each file once.
    pub fn visit_production_sources(visit: &mut impl FnMut(&Path, &str)) {
        visit_dir(&Path::new(env!("CARGO_MANIFEST_DIR")).join("src"), visit);
    }

    fn visit_dir(dir: &Path, visit: &mut impl FnMut(&Path, &str)) {
        let entries = std::fs::read_dir(dir).unwrap_or_else(|e| panic!("read_dir {}: {e}", dir.display()));

        for entry in entries.flatten() {
            let path = entry.path();
            // `file_type()` reuses what `readdir` already returned; `is_dir()`
            // would re-`stat` every entry.
            if entry.file_type().is_ok_and(|t| t.is_dir()) {
                visit_dir(&path, visit);
                continue;
            }

            let name = path.file_name().and_then(|n| n.to_str()).unwrap_or_default();
            if !name.ends_with(".rs") || name == "tests.rs" || name.ends_with("_tests.rs") {
                continue;
            }

            let contents = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
            visit(&path, &contents);
        }
    }
}

#[cfg(test)]
#[path = "test_utils_tests.rs"]
mod tests;