1use std::{any::Any, fmt::Debug, rc::Rc};
21
22use ahash::AHashMap;
23use nautilus_core::{UUID4, python::to_pyruntime_err};
24use nautilus_model::identifiers::TraderId;
25use pyo3::{Py, Python, prelude::*, types::PyBytes};
26use ustr::Ustr;
27
28use crate::{
29 enums::SerializationEncoding,
30 msgbus::{
31 self as msgbus_api, BusMessage, MessageBus, MessageBusConfig,
32 core::Subscription,
33 get_message_bus,
34 matching::is_matching,
35 mstr::{Endpoint, MStr, Pattern, Topic},
36 typed_handler::{Handler, ShareableMessageHandler, TypedHandler},
37 },
38 python::config_error_to_pyvalue_err,
39};
40
41#[pymethods]
42#[pyo3_stub_gen::derive::gen_stub_pymethods]
43impl BusMessage {
44 #[getter]
45 #[pyo3(name = "topic")]
46 fn py_topic(&self) -> String {
47 self.topic.to_string()
48 }
49
50 #[getter]
51 #[pyo3(name = "payload_type")]
52 fn py_payload_type(&self) -> String {
53 self.payload_type.to_string()
54 }
55
56 #[getter]
57 #[pyo3(name = "payload")]
58 fn py_payload(&self, py: Python<'_>) -> Py<PyBytes> {
59 PyBytes::new(py, self.payload.as_ref()).into()
60 }
61
62 #[getter]
63 #[pyo3(name = "encoding")]
64 fn py_encoding(&self) -> SerializationEncoding {
65 self.encoding
66 }
67
68 fn __repr__(&self) -> String {
69 format!("{}('{}')", stringify!(BusMessage), self)
70 }
71
72 fn __str__(&self) -> String {
73 self.to_string()
74 }
75}
76
77#[pymethods]
78#[pyo3_stub_gen::derive::gen_stub_pymethods]
79impl MessageBusConfig {
80 #[new]
82 #[expect(clippy::too_many_arguments)]
83 #[pyo3(signature = (encoding=None, encoding_market_data=None, encoding_builtin=None, timestamps_as_iso8601=None, buffer_interval_ms=None, autotrim_mins=None, use_trader_prefix=None, use_trader_id=None, use_instance_id=None, streams_prefix=None, stream_per_topic=None, external_streams=None, types_filter=None, heartbeat_interval_secs=None))]
84 fn py_new(
85 encoding: Option<SerializationEncoding>,
86 encoding_market_data: Option<SerializationEncoding>,
87 encoding_builtin: Option<SerializationEncoding>,
88 timestamps_as_iso8601: Option<bool>,
89 buffer_interval_ms: Option<u32>,
90 autotrim_mins: Option<u32>,
91 use_trader_prefix: Option<bool>,
92 use_trader_id: Option<bool>,
93 use_instance_id: Option<bool>,
94 streams_prefix: Option<String>,
95 stream_per_topic: Option<bool>,
96 external_streams: Option<Vec<String>>,
97 types_filter: Option<Vec<String>>,
98 heartbeat_interval_secs: Option<u16>,
99 ) -> PyResult<Self> {
100 let default = Self::default();
101 let config = Self {
102 encoding: encoding.unwrap_or(default.encoding),
103 encoding_market_data,
104 encoding_builtin,
105 timestamps_as_iso8601: timestamps_as_iso8601.unwrap_or(default.timestamps_as_iso8601),
106 buffer_interval_ms,
107 autotrim_mins,
108 use_trader_prefix: use_trader_prefix.unwrap_or(default.use_trader_prefix),
109 use_trader_id: use_trader_id.unwrap_or(default.use_trader_id),
110 use_instance_id: use_instance_id.unwrap_or(default.use_instance_id),
111 streams_prefix: streams_prefix.unwrap_or(default.streams_prefix),
112 stream_per_topic: stream_per_topic.unwrap_or(default.stream_per_topic),
113 external_streams,
114 types_filter,
115 heartbeat_interval_secs,
116 };
117
118 config.validate().map_err(config_error_to_pyvalue_err)?;
119 Ok(config)
120 }
121
122 fn __repr__(&self) -> String {
123 format!("{self:?}")
124 }
125
126 fn __str__(&self) -> String {
127 format!("{self:?}")
128 }
129
130 #[getter]
131 fn encoding(&self) -> SerializationEncoding {
132 self.encoding
133 }
134
135 #[getter]
136 fn encoding_market_data(&self) -> Option<SerializationEncoding> {
137 self.encoding_market_data
138 }
139
140 #[getter]
141 fn encoding_builtin(&self) -> Option<SerializationEncoding> {
142 self.encoding_builtin
143 }
144
145 #[getter]
146 fn timestamps_as_iso8601(&self) -> bool {
147 self.timestamps_as_iso8601
148 }
149
150 #[getter]
151 fn buffer_interval_ms(&self) -> Option<u32> {
152 self.buffer_interval_ms
153 }
154
155 #[getter]
156 fn autotrim_mins(&self) -> Option<u32> {
157 self.autotrim_mins
158 }
159
160 #[getter]
161 fn use_trader_prefix(&self) -> bool {
162 self.use_trader_prefix
163 }
164
165 #[getter]
166 fn use_trader_id(&self) -> bool {
167 self.use_trader_id
168 }
169
170 #[getter]
171 fn use_instance_id(&self) -> bool {
172 self.use_instance_id
173 }
174
175 #[getter]
176 fn streams_prefix(&self) -> &str {
177 &self.streams_prefix
178 }
179
180 #[getter]
181 fn stream_per_topic(&self) -> bool {
182 self.stream_per_topic
183 }
184
185 #[getter]
186 fn external_streams(&self) -> Option<Vec<String>> {
187 self.external_streams.clone()
188 }
189
190 #[getter]
191 fn types_filter(&self) -> Option<Vec<String>> {
192 self.types_filter.clone()
193 }
194
195 #[getter]
196 fn heartbeat_interval_secs(&self) -> Option<u16> {
197 self.heartbeat_interval_secs
198 }
199}
200
201pub struct PyMessage(pub Py<PyAny>);
203
204impl Debug for PyMessage {
205 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206 f.debug_tuple(stringify!(PyMessage))
207 .field(&"<PyObject>")
208 .finish()
209 }
210}
211
212pub struct PyCallableHandler {
217 id: Ustr,
218 callable: Py<PyAny>,
219}
220
221impl Debug for PyCallableHandler {
222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223 f.debug_struct(stringify!(PyCallableHandler))
224 .field("id", &self.id)
225 .finish()
226 }
227}
228
229impl PyCallableHandler {
230 pub fn new(py: Python<'_>, callable: Py<PyAny>) -> PyResult<Self> {
235 let repr_str = callable.bind(py).repr()?.to_string();
236 let id = Ustr::from(&repr_str);
237 Ok(Self { id, callable })
238 }
239}
240
241impl Handler<dyn Any> for PyCallableHandler {
242 fn id(&self) -> Ustr {
243 self.id
244 }
245
246 fn handle(&self, message: &dyn Any) {
247 if let Some(py_msg) = message.downcast_ref::<PyMessage>() {
248 Python::attach(|py| {
249 if let Err(e) = self.callable.call1(py, (&py_msg.0,)) {
250 log::error!("Python handler {id} failed: {e}", id = self.id);
251 }
252 });
253 } else {
254 log::error!(
255 "Python handler {id} received non-PyMessage type",
256 id = self.id
257 );
258 }
259 }
260}
261
262fn make_handler(py: Python<'_>, callable: Py<PyAny>) -> PyResult<ShareableMessageHandler> {
263 let handler = PyCallableHandler::new(py, callable)?;
264 Ok(TypedHandler(Rc::new(handler) as Rc<dyn Handler<dyn Any>>))
265}
266
267#[pyclass(
273 module = "nautilus_trader.core.nautilus_pyo3.common",
274 name = "MessageBus",
275 unsendable
276)]
277#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")]
278pub struct PyMessageBus {
279 trader_id: TraderId,
280 instance_id: UUID4,
281 name: String,
282 has_backing: bool,
283 serializer: Option<Py<PyAny>>,
284 backing: Option<Py<PyAny>>,
285 listeners: Vec<Py<PyAny>>,
286 types_filter: Option<Py<PyAny>>,
287 streaming_types: Vec<Py<PyAny>>,
288 correlation_index: AHashMap<UUID4, Py<PyAny>>,
289 sent_count: u64,
290 req_count: u64,
291 res_count: u64,
292 pub_count: u64,
293}
294
295impl Debug for PyMessageBus {
296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297 f.debug_struct(stringify!(PyMessageBus))
298 .field("trader_id", &self.trader_id)
299 .field("name", &self.name)
300 .finish()
301 }
302}
303
304#[pymethods]
305#[pyo3_stub_gen::derive::gen_stub_pymethods]
306impl PyMessageBus {
307 #[new]
312 #[pyo3(signature = (trader_id, clock=None, instance_id=None, name=None, serializer=None, backing=None, config=None))]
313 #[expect(clippy::too_many_arguments, clippy::needless_pass_by_value)]
314 fn py_new(
315 py: Python<'_>,
316 trader_id: TraderId,
317 clock: Option<Py<PyAny>>,
318 instance_id: Option<UUID4>,
319 name: Option<String>,
320 serializer: Option<Py<PyAny>>,
321 backing: Option<Py<PyAny>>,
322 config: Option<Py<PyAny>>,
323 ) -> PyResult<Self> {
324 let _ = clock;
325 let instance_id = instance_id.unwrap_or_default();
326 let bus_name = name.clone();
327 let has_backing = backing.is_some();
328
329 let msgbus = MessageBus::new(trader_id, instance_id, bus_name, None);
330 msgbus.register_message_bus();
331
332 let types_filter = if let Some(ref cfg) = config {
333 let tf = cfg.getattr(py, "types_filter")?;
334 if tf.is_none(py) {
335 None
336 } else {
337 let tuple = py
339 .import("builtins")?
340 .call_method1("tuple", (tf,))?
341 .unbind();
342 Some(tuple)
343 }
344 } else {
345 None
346 };
347
348 Ok(Self {
349 trader_id,
350 instance_id,
351 name: name.unwrap_or_else(|| "MessageBus".to_owned()),
352 has_backing,
353 serializer,
354 backing,
355 listeners: Vec::new(),
356 types_filter,
357 streaming_types: Vec::new(),
358 correlation_index: AHashMap::new(),
359 sent_count: 0,
360 req_count: 0,
361 res_count: 0,
362 pub_count: 0,
363 })
364 }
365
366 #[getter]
368 #[pyo3(name = "trader_id")]
369 fn py_trader_id(&self) -> TraderId {
370 self.trader_id
371 }
372
373 #[getter]
375 #[pyo3(name = "instance_id")]
376 fn py_instance_id(&self) -> UUID4 {
377 self.instance_id
378 }
379
380 #[getter]
382 #[pyo3(name = "name")]
383 fn py_name(&self) -> &str {
384 &self.name
385 }
386
387 #[getter]
389 #[pyo3(name = "has_backing")]
390 fn py_has_backing(&self) -> bool {
391 self.has_backing
392 }
393
394 #[getter]
396 #[pyo3(name = "sent_count")]
397 fn py_sent_count(&self) -> u64 {
398 self.sent_count
399 }
400
401 #[getter]
403 #[pyo3(name = "req_count")]
404 fn py_req_count(&self) -> u64 {
405 self.req_count
406 }
407
408 #[getter]
410 #[pyo3(name = "res_count")]
411 fn py_res_count(&self) -> u64 {
412 self.res_count
413 }
414
415 #[getter]
417 #[pyo3(name = "pub_count")]
418 fn py_pub_count(&self) -> u64 {
419 self.pub_count
420 }
421
422 #[pyo3(name = "endpoints")]
424 fn py_endpoints(&self) -> Vec<String> {
425 let bus = get_message_bus();
426 let bus_ref = bus.borrow();
427 bus_ref.endpoints().into_iter().map(String::from).collect()
428 }
429
430 #[pyo3(name = "topics")]
432 fn py_topics(&self) -> Vec<String> {
433 let bus = get_message_bus();
434 let bus_ref = bus.borrow();
435 let mut topics: Vec<String> = bus_ref.patterns().into_iter().map(String::from).collect();
436 topics.sort();
437 topics.dedup();
438 topics
439 }
440
441 #[pyo3(name = "subscriptions")]
443 #[pyo3(signature = (pattern=None))]
444 fn py_subscriptions(&self, pattern: Option<&str>) -> Vec<String> {
445 let bus = get_message_bus();
446 let bus_ref = bus.borrow();
447 let subs: Vec<&Subscription> = bus_ref.subscriptions();
448
449 match pattern {
450 Some(p) => {
451 let filter = MStr::<Pattern>::pattern(p);
452 subs.into_iter()
453 .filter(|s| is_matching(s.pattern.as_bytes(), filter.as_bytes()))
454 .map(|s| {
455 format!(
456 "Subscription(topic={}, handler={})",
457 s.pattern, s.handler_id
458 )
459 })
460 .collect()
461 }
462 None => subs
463 .into_iter()
464 .map(|s| {
465 format!(
466 "Subscription(topic={}, handler={})",
467 s.pattern, s.handler_id
468 )
469 })
470 .collect(),
471 }
472 }
473
474 #[pyo3(name = "has_subscribers")]
476 #[pyo3(signature = (pattern=None))]
477 fn py_has_subscribers(&self, pattern: Option<&str>) -> bool {
478 let bus = get_message_bus();
479 let bus_ref = bus.borrow();
480
481 match pattern {
482 Some(p) => {
483 let filter = MStr::<Pattern>::pattern(p);
484 bus_ref
485 .subscriptions()
486 .iter()
487 .any(|s| is_matching(s.pattern.as_bytes(), filter.as_bytes()))
488 }
489 None => !bus_ref.subscriptions().is_empty(),
490 }
491 }
492
493 #[pyo3(name = "is_subscribed")]
495 fn py_is_subscribed(&self, py: Python<'_>, topic: &str, handler: Py<PyAny>) -> PyResult<bool> {
496 let handler = make_handler(py, handler)?;
497 let pattern = MStr::<Pattern>::pattern(topic);
498 let sub = Subscription::new(pattern, handler, None);
499 Ok(get_message_bus().borrow().subscriptions.contains(&sub))
500 }
501
502 #[pyo3(name = "is_pending_request")]
504 fn py_is_pending_request(&self, request_id: UUID4) -> bool {
505 self.correlation_index.contains_key(&request_id)
506 }
507
508 #[pyo3(name = "is_streaming_type")]
510 #[expect(clippy::needless_pass_by_value)]
511 fn py_is_streaming_type(&self, py: Python<'_>, cls: Py<PyAny>) -> bool {
512 let cls_ref = cls.bind(py);
513 self.streaming_types.iter().any(|t| t.bind(py).is(cls_ref))
514 }
515
516 #[pyo3(name = "streaming_types")]
518 fn py_streaming_types(&self, py: Python<'_>) -> Vec<Py<PyAny>> {
519 self.streaming_types
520 .iter()
521 .map(|t| t.clone_ref(py))
522 .collect()
523 }
524
525 #[pyo3(name = "register")]
527 fn py_register(&self, py: Python<'_>, endpoint: &str, handler: Py<PyAny>) -> PyResult<()> {
528 let handler = make_handler(py, handler)?;
529 let endpoint = MStr::<Endpoint>::from(endpoint);
530 msgbus_api::register_any(endpoint, handler);
531 Ok(())
532 }
533
534 #[pyo3(name = "deregister")]
536 #[pyo3(signature = (endpoint, handler=None))]
537 #[expect(clippy::needless_pass_by_value)]
538 fn py_deregister(&self, endpoint: &str, handler: Option<Py<PyAny>>) {
539 let _ = handler;
540 let endpoint = MStr::<Endpoint>::from(endpoint);
541 msgbus_api::deregister_any(endpoint);
542 }
543
544 #[pyo3(name = "send")]
546 fn py_send(&mut self, endpoint: &str, msg: Py<PyAny>) {
547 let endpoint = MStr::<Endpoint>::from(endpoint);
548 let py_msg = PyMessage(msg);
549 msgbus_api::send_any(endpoint, &py_msg);
550 self.sent_count += 1;
551 }
552
553 #[pyo3(name = "request")]
555 fn py_request(&mut self, py: Python<'_>, endpoint: &str, request: Py<PyAny>) -> PyResult<()> {
556 let request_ref = request.bind(py);
557
558 let request_id: UUID4 = request_ref.getattr("id")?.extract()?;
559 let callback = request_ref.getattr("callback")?;
560
561 if self.correlation_index.contains_key(&request_id) {
562 log::error!(
563 "Cannot handle request: duplicate ID {request_id} found in correlation index"
564 );
565 return Ok(());
566 }
567
568 if !callback.is_none() {
569 self.correlation_index.insert(request_id, callback.unbind());
570 }
571
572 let endpoint = MStr::<Endpoint>::from(endpoint);
573 let py_msg = PyMessage(request);
574 msgbus_api::send_any(endpoint, &py_msg);
575 self.req_count += 1;
576
577 Ok(())
578 }
579
580 #[pyo3(name = "response")]
582 #[expect(clippy::needless_pass_by_value)]
583 fn py_response(&mut self, py: Python<'_>, response: Py<PyAny>) -> PyResult<()> {
584 let correlation_id: UUID4 = response.getattr(py, "correlation_id")?.extract(py)?;
585
586 if let Some(callback) = self.correlation_index.remove(&correlation_id) {
587 callback.call1(py, (&response,))?;
588 } else {
589 log::debug!("No callback for correlation_id {correlation_id}");
590 }
591
592 self.res_count += 1;
593 Ok(())
594 }
595
596 #[pyo3(name = "subscribe")]
598 #[pyo3(signature = (topic, handler, priority=0))]
599 fn py_subscribe(
600 &self,
601 py: Python<'_>,
602 topic: &str,
603 handler: Py<PyAny>,
604 priority: u32,
605 ) -> PyResult<()> {
606 let handler = make_handler(py, handler)?;
607 let pattern = MStr::<Pattern>::pattern(topic);
608 msgbus_api::subscribe_any(pattern, handler, Some(priority));
609 Ok(())
610 }
611
612 #[pyo3(name = "unsubscribe")]
614 fn py_unsubscribe(&self, py: Python<'_>, topic: &str, handler: Py<PyAny>) -> PyResult<()> {
615 let handler = make_handler(py, handler)?;
616 let pattern = MStr::<Pattern>::pattern(topic);
617 msgbus_api::unsubscribe_any(pattern, &handler);
618 Ok(())
619 }
620
621 #[pyo3(name = "publish")]
623 #[pyo3(signature = (topic, msg, external_pub=true))]
624 #[expect(clippy::needless_pass_by_value)]
625 fn py_publish(
626 &mut self,
627 py: Python<'_>,
628 topic: &str,
629 msg: Py<PyAny>,
630 external_pub: bool,
631 ) -> PyResult<()> {
632 let topic_mstr = MStr::<Topic>::topic(topic).map_err(to_pyruntime_err)?;
633
634 let py_msg = PyMessage(msg.clone_ref(py));
635 msgbus_api::publish_any(topic_mstr, &py_msg);
636
637 if external_pub {
638 self.publish_external(py, topic, &msg)?;
639 }
640
641 self.pub_count += 1;
642 Ok(())
643 }
644
645 #[pyo3(name = "dispose")]
647 fn py_dispose(&mut self, py: Python<'_>) -> PyResult<()> {
648 log::debug!("Closing message bus");
649
650 get_message_bus().borrow_mut().dispose();
651
652 self.correlation_index.clear();
653 self.listeners.clear();
654 self.streaming_types.clear();
655
656 if let Some(ref backing) = self.backing {
657 let db = backing.bind(py);
658 if !db.call_method0("is_closed")?.extract::<bool>()? {
659 db.call_method0("close")?;
660 }
661 }
662
663 log::info!("Closed message bus");
664 Ok(())
665 }
666
667 #[pyo3(name = "add_streaming_type")]
669 fn py_add_streaming_type(&mut self, cls: Py<PyAny>) {
670 self.streaming_types.push(cls);
671 }
672
673 #[pyo3(name = "add_listener")]
675 fn py_add_listener(&mut self, listener: Py<PyAny>) {
676 self.listeners.push(listener);
677 }
678}
679
680impl PyMessageBus {
681 fn publish_external(&self, py: Python<'_>, topic: &str, msg: &Py<PyAny>) -> PyResult<()> {
682 if let Some(ref filter) = self.types_filter {
683 let is_excluded = py
684 .import("builtins")?
685 .call_method1("isinstance", (msg, filter))?
686 .extract::<bool>()?;
687
688 if is_excluded {
689 return Ok(());
690 }
691 }
692
693 let msg_ref = msg.bind(py);
695 let payload: Py<PyAny> = if msg_ref.is_instance_of::<pyo3::types::PyBytes>() {
696 msg.clone_ref(py)
697 } else if let Some(ref serializer) = self.serializer {
698 serializer.call_method1(py, "serialize", (msg,))?
699 } else {
700 return Ok(());
701 };
702
703 if let Some(ref backing) = self.backing {
704 let db = backing.bind(py);
705 if !db.call_method0("is_closed")?.extract::<bool>()? {
706 db.call_method1("publish", (topic, &payload))?;
707 }
708 }
709
710 for listener in &self.listeners {
711 let l = listener.bind(py);
712 if l.call_method0("is_closed")?.extract::<bool>()? {
713 continue;
714 }
715 l.call_method1("publish", (topic, &payload))?;
716 }
717
718 Ok(())
719 }
720}
721
722#[cfg(test)]
723mod tests {
724 use std::any::Any;
725
726 use pyo3::{exceptions::PyValueError, ffi::c_str};
727 use rstest::rstest;
728
729 use super::*;
730
731 #[rstest]
732 fn message_bus_config_py_new_maps_validate_error_to_value_error() {
733 pyo3::Python::initialize();
734 Python::attach(|py| {
735 let err = MessageBusConfig::py_new(
736 Some(SerializationEncoding::Json),
737 None,
738 Some(SerializationEncoding::Capnp),
739 None,
740 None,
741 None,
742 None,
743 None,
744 None,
745 None,
746 None,
747 None,
748 None,
749 None,
750 )
751 .unwrap_err();
752
753 assert!(err.is_instance_of::<PyValueError>(py));
754 assert_eq!(
755 err.value(py).to_string(),
756 format!(
757 "MessageBusConfig.encoding_builtin has unsupported value: {} is not supported by AccountState, OrderEventAny, PositionEvent, PortfolioSnapshot",
758 SerializationEncoding::Capnp
759 )
760 );
761 });
762 }
763
764 #[rstest]
765 fn test_py_message_downcast() {
766 pyo3::Python::initialize();
767 Python::attach(|py| {
768 let py_obj = py.eval(c_str!("42"), None, None).unwrap();
769 let msg = PyMessage(py_obj.unbind());
770
771 let any_ref: &dyn Any = &msg;
772 let downcasted = any_ref.downcast_ref::<PyMessage>();
773 assert!(downcasted.is_some());
774
775 let inner = &downcasted.unwrap().0;
776 let value: i64 = inner.extract(py).unwrap();
777 assert_eq!(value, 42);
778 });
779 }
780
781 #[rstest]
782 fn test_py_callable_handler_id_stability() {
783 pyo3::Python::initialize();
784 Python::attach(|py| {
785 let callable = py.eval(c_str!("lambda x: x"), None, None).unwrap().unbind();
786
787 let handler1 = PyCallableHandler::new(py, callable.clone_ref(py)).unwrap();
788 let handler2 = PyCallableHandler::new(py, callable).unwrap();
789
790 assert_eq!(handler1.id(), handler2.id());
791 });
792 }
793
794 #[rstest]
795 fn test_py_callable_handler_dispatch() {
796 pyo3::Python::initialize();
797 Python::attach(|py| {
798 let main = py.import("__main__").unwrap();
799 let globals = main.dict();
800 py.run(
801 c_str!("results = []\ndef handler(x): results.append(x)"),
802 Some(&globals),
803 None,
804 )
805 .unwrap();
806
807 let handler_fn = globals.get_item("handler").unwrap().unwrap().unbind();
808 let handler = PyCallableHandler::new(py, handler_fn).unwrap();
809
810 let py_obj = py.eval(c_str!("'hello'"), None, None).unwrap();
811 let msg = PyMessage(py_obj.unbind());
812
813 let any_ref: &dyn Any = &msg;
814 handler.handle(any_ref);
815
816 let results = globals.get_item("results").unwrap().unwrap();
817 let len: usize = results.len().unwrap();
818 assert_eq!(len, 1);
819 });
820 }
821}