1use anyhow::{Context as _, Result};
6use fidl::HandleBased;
7
8use cm_rust::FidlIntoNative;
9use fidl_fuchsia_component_decl as fdecl;
10use fidl_fuchsia_component_test as ftest;
11use fidl_fuchsia_driver_development as fdd;
12use fidl_fuchsia_driver_test as fdt;
13use fidl_fuchsia_io as fio;
14use flyweights::FlyStr;
15use fuchsia_async as fasync;
16use fuchsia_component::server::ServiceFs;
17use fuchsia_component_test::{
18 Capability, ChildOptions, ChildRef, CollectionRef, LocalComponentHandles, RealmBuilder,
19 RealmInstance, Ref, Route,
20};
21use futures::{StreamExt, TryStreamExt};
22use std::sync::Arc;
23use zx::AsHandleRef;
24
25fn clone(
26 dir: &fio::DirectoryProxy,
27) -> Result<fidl::endpoints::ClientEnd<fio::DirectoryMarker>, fidl::Error> {
28 let (client_end, server_end) = fidl::endpoints::create_endpoints::<fio::DirectoryMarker>();
29 dir.clone(fidl::endpoints::ServerEnd::new(server_end.into_channel()))?;
30 Ok(client_end)
31}
32
33async fn internal_serve(
34 stream: fidl_fuchsia_driver_test::InternalRequestStream,
35 test_pkg_dir: Arc<fio::DirectoryProxy>,
36 test_resolution_context: Arc<Option<fidl_fuchsia_component_resolution::Context>>,
37 boot_dir: Arc<Option<fio::DirectoryProxy>>,
38 boot_driver_components: Arc<Option<Vec<String>>>,
39) {
40 stream
41 .try_for_each_concurrent(None, |request| {
42 let test_pkg_dir = test_pkg_dir.clone();
43 let test_resolution_context = test_resolution_context.clone();
44 let boot_dir = boot_dir.clone();
45 let boot_driver_components = boot_driver_components.clone();
46 async move {
47 match request {
48 fidl_fuchsia_driver_test::InternalRequest::GetTestPackage { responder } => {
49 let cloned = clone(test_pkg_dir.as_ref())?;
50 responder.send(Ok(Some(cloned)))?;
51 }
52 fidl_fuchsia_driver_test::InternalRequest::GetTestResolutionContext {
53 responder,
54 } => responder.send(Ok(test_resolution_context.as_ref().as_ref()))?,
55 fidl_fuchsia_driver_test::InternalRequest::GetBootDirectory { responder } => {
56 match boot_dir.as_ref() {
57 Some(boot_dir) => {
58 let cloned = clone(boot_dir)?;
59 responder.send(Ok(Some(cloned)))?;
60 }
61 None => responder.send(Ok(None))?,
62 }
63 }
64 fidl_fuchsia_driver_test::InternalRequest::GetBootDriverOverrides {
65 responder,
66 } => responder.send(Ok(boot_driver_components
67 .as_ref()
68 .clone()
69 .unwrap_or(vec![])
70 .as_slice()))?,
71 };
72 Ok(())
73 }
74 })
75 .await
76 .expect("fuchsia.driver.test.Internal failed.");
77}
78
79async fn resource_provider_serve(
80 stream: fidl_fuchsia_driver_test::ResourceProviderRequestStream,
81 devicetree: Arc<Option<zx::Vmo>>,
82) {
83 stream
84 .try_for_each_concurrent(None, |request| {
85 let devicetree = devicetree.clone();
86 async move {
87 match request {
88 fidl_fuchsia_driver_test::ResourceProviderRequest::GetDeviceTree {
89 responder,
90 } => {
91 if let Some(vmo) = devicetree.as_ref().as_ref().map(|d| {
92 d.duplicate_handle(zx::Rights::SAME_RIGHTS).expect("duplicate")
93 }) {
94 responder.send(Ok(vmo))?;
95 } else {
96 responder.send(Err(zx::Status::NOT_FOUND.into_raw()))?;
97 }
98 }
99 };
100 Ok(())
101 }
102 })
103 .await
104 .expect("fuchsia.driver.test.Internal failed.");
105}
106
107async fn run_internal_server(
108 handles: LocalComponentHandles,
109 test_pkg_dir: Arc<fio::DirectoryProxy>,
110 test_resolution_context: Arc<Option<fidl_fuchsia_component_resolution::Context>>,
111 boot_dir: Arc<Option<fio::DirectoryProxy>>,
112 boot_driver_components: Arc<Option<Vec<String>>>,
113 devicetree: Arc<Option<zx::Vmo>>,
114) -> Result<()> {
115 let mut fs = ServiceFs::new();
116
117 fs.dir("svc").add_fidl_service(
118 move |stream: fidl_fuchsia_driver_test::InternalRequestStream| {
119 fasync::Task::spawn(internal_serve(
120 stream,
121 test_pkg_dir.clone(),
122 test_resolution_context.clone(),
123 boot_dir.clone(),
124 boot_driver_components.clone(),
125 ))
126 .detach();
127 },
128 );
129 fs.dir("svc").add_fidl_service(
130 move |stream: fidl_fuchsia_driver_test::ResourceProviderRequestStream| {
131 fasync::Task::spawn(resource_provider_serve(stream, devicetree.clone())).detach();
132 },
133 );
134 fs.serve_connection(handles.outgoing_dir)?;
135 fs.collect::<()>().await;
136 Ok(())
137}
138
139fn capabilities_eq_name(a: &ftest::Capability, b: &ftest::Capability) -> bool {
141 match (a, b) {
142 (ftest::Capability::Protocol(a), ftest::Capability::Protocol(b)) => a.name == b.name,
143 (ftest::Capability::Directory(a), ftest::Capability::Directory(b)) => a.name == b.name,
144 (ftest::Capability::Storage(a), ftest::Capability::Storage(b)) => a.name == b.name,
145 (ftest::Capability::Service(a), ftest::Capability::Service(b)) => a.name == b.name,
146 (ftest::Capability::EventStream(a), ftest::Capability::EventStream(b)) => a.name == b.name,
147 (ftest::Capability::Config(a), ftest::Capability::Config(b)) => a.name == b.name,
148 (ftest::Capability::Dictionary(a), ftest::Capability::Dictionary(b)) => a.name == b.name,
149 (ftest::Capability::Resolver(a), ftest::Capability::Resolver(b)) => a.name == b.name,
150 (ftest::Capability::Runner(a), ftest::Capability::Runner(b)) => a.name == b.name,
151 _ => false,
152 }
153}
154
155#[derive(Debug, Clone, Default)]
156pub struct Options {
157 using_subpackage: Option<bool>,
158 driver_offers: Option<(Ref, Vec<ftest::Capability>)>,
159 driver_exposes: Option<Vec<ftest::Capability>>,
160 extra_realm_capabilities: Vec<(ftest::Capability, Ref)>,
161}
162
163impl Options {
164 pub fn new() -> Self {
165 Self::default()
166 }
167
168 pub fn using_subpackage(mut self, using_subpackage: bool) -> Self {
169 self.using_subpackage = Some(using_subpackage);
170 self
171 }
172
173 pub fn driver_offers(mut self, provider: Ref, offers: Vec<ftest::Capability>) -> Self {
174 self.driver_offers = Some((provider, offers));
175 self
176 }
177
178 pub fn driver_exposes(mut self, exposes: Vec<ftest::Capability>) -> Self {
179 self.driver_exposes = Some(exposes);
180 self
181 }
182
183 pub fn add_extra_realm_capability(
184 mut self,
185 capability: ftest::Capability,
186 from: impl Into<Ref>,
187 ) -> Self {
188 self.extra_realm_capabilities.push((capability, from.into()));
189 self
190 }
191}
192
193#[async_trait::async_trait]
194pub trait DriverTestRealmBuilder {
195 async fn driver_test_realm_setup(
199 &self,
200 options: Options,
201 args: fdt::RealmArgs,
202 ) -> Result<&Self>;
203}
204
205#[async_trait::async_trait]
206impl DriverTestRealmBuilder for RealmBuilder {
207 async fn driver_test_realm_setup(
208 &self,
209 options: Options,
210 args: fdt::RealmArgs,
211 ) -> Result<&Self> {
212 let manifest_provider =
213 fuchsia_component::client::connect_to_protocol::<fdt::ManifestProviderMarker>()?;
214 let stream = manifest_provider
215 .get_manifest(&fdt::GetManifestRequest {
216 using_subpackage: options.using_subpackage,
217 ..Default::default()
218 })
219 .await?
220 .expect("manifest stream");
221
222 let mut manifest: Vec<u8> = vec![];
223 loop {
224 let mut read = stream.read_to_vec(
225 zx::StreamReadOptions::empty(),
226 fidl_fuchsia_io::MAX_TRANSFER_SIZE as usize,
227 )?;
228 if read.is_empty() {
229 break;
230 }
231 manifest.append(&mut read);
232 }
233
234 let component = fidl::unpersist::<fdecl::Component>(manifest.as_slice())
235 .context("unpersisting the manifest vector")?;
236
237 let realm = self
240 .add_child_realm_from_decl(
241 "driver_test_realm",
242 component.fidl_into_native(),
243 ChildOptions::new(),
244 )
245 .await?;
246
247 self.add_route(
249 Route::new()
250 .capability(Capability::protocol_by_name("fuchsia.diagnostics.ArchiveAccessor"))
251 .from(Ref::parent())
252 .to(&realm),
253 )
254 .await?;
255
256 let dtr_support: ChildRef = "dtr_support".into();
257 let fake_resolver: ChildRef = "fake_resolver".into();
258 let driver_manager: ChildRef = "driver_manager".into();
259 let driver_index: ChildRef = "driver_index".into();
260
261 let boot_drivers: CollectionRef = "boot-drivers".into();
262 let base_drivers: CollectionRef = "base-drivers".into();
263 let full_drivers: CollectionRef = "full-drivers".into();
264
265 let devicetree = Arc::new(args.devicetree);
266
267 let test_component = if let Some(test_component) = args.test_component {
269 test_component
270 } else {
271 let realm = fuchsia_component::client::connect_to_protocol::<
272 fidl_fuchsia_component::RealmMarker,
273 >()?;
274
275 realm.get_resolved_info().await.unwrap().unwrap()
276 };
277
278 let test_resolution_context = Arc::new(test_component.resolution_context);
279 let test_pkg_dir = Arc::new(
280 test_component.package.expect("a pkg").directory.expect("a directory").into_proxy(),
281 );
282
283 let boot_dir = Arc::new(match args.boot {
284 Some(boot_dir) => {
285 if !boot_dir.as_handle_ref().is_invalid() {
286 Some(boot_dir.into_proxy())
287 } else {
288 None
289 }
290 }
291 None => None,
292 });
293 let boot_driver_components = Arc::new(args.boot_driver_components);
294
295 self.add_route(
297 Route::new()
298 .capability(
299 Capability::protocol_by_name("fuchsia.component.resolution.Resolver-hermetic")
300 .optional(),
301 )
302 .capability(
303 Capability::protocol_by_name("fuchsia.pkg.PackageResolver-hermetic").optional(),
304 )
305 .from(Ref::parent())
306 .to(&realm),
307 )
308 .await?;
309
310 let driver_test_internal = realm
312 .add_local_child(
313 "driver_test_internal",
314 move |handles| {
315 let test_pkg_dir = test_pkg_dir.clone();
316 let test_resolution_context = test_resolution_context.clone();
317 let boot_dir = boot_dir.clone();
318 let boot_driver_components = boot_driver_components.clone();
319 let devicetree = devicetree.clone();
320
321 Box::pin(run_internal_server(
322 handles,
323 test_pkg_dir,
324 test_resolution_context,
325 boot_dir,
326 boot_driver_components,
327 devicetree,
328 ))
329 },
330 ChildOptions::new(),
331 )
332 .await?;
333
334 let mut tunnel_boot_items = false;
337 let mut voided_offers: Vec<ftest::Capability> = vec![
338 Capability::protocol_by_name("fuchsia.tracing.provider.Registry").optional().into(),
339 Capability::protocol_by_name("fuchsia.boot.WriteOnlyLog").optional().into(),
340 Capability::protocol_by_name("fuchsia.scheduler.RoleManager").optional().into(),
341 Capability::protocol_by_name("fuchsia.boot.Items").optional().into(),
342 Capability::protocol_by_name("fuchsia.kernel.IommuResource").optional().into(),
343 Capability::protocol_by_name("fuchsia.diagnostics.LogFlusher").optional().into(),
344 Capability::protocol_by_name("fuchsia.kernel.MexecResource").optional().into(),
345 Capability::protocol_by_name("fuchsia.kernel.PowerResource").optional().into(),
346 ];
347 for (capability, from) in options.extra_realm_capabilities {
348 voided_offers.retain(|voided| !capabilities_eq_name(voided, &capability));
350
351 if from != Ref::void()
352 && capabilities_eq_name(
353 &Capability::protocol_by_name("fuchsia.boot.Items").into(),
354 &capability,
355 )
356 {
357 tunnel_boot_items = true;
358 }
359
360 self.add_route(Route::new().capability(capability).from(from).to(&realm)).await?;
361 }
362
363 for voided in voided_offers {
365 self.add_route(Route::new().capability(voided).from(Ref::void()).to(&realm)).await?;
366 }
367
368 if args.dtr_offers.is_some() {
371 panic!("Please use |Options::driver_offers| instead of dtr_offers.")
372 }
373 if let Some((provider, offers)) = options.driver_offers {
374 for offer in offers {
375 self.add_route(
376 Route::new().capability(offer.clone()).from(provider.clone()).to(&realm),
377 )
378 .await?;
379 realm
380 .add_route(
381 Route::new()
382 .capability(offer)
383 .from(Ref::parent())
384 .to(&boot_drivers)
385 .to(&base_drivers)
386 .to(&full_drivers),
387 )
388 .await?;
389 }
390 }
391
392 if args.dtr_exposes.is_some() {
394 panic!("Please use |Options::driver_exposes| instead of dtr_exposes.")
395 }
396 if let Some(exposes) = options.driver_exposes {
397 for expose in exposes {
398 realm
399 .add_route(
400 Route::new()
401 .capability(expose.clone())
402 .from(&boot_drivers)
403 .to(Ref::parent()),
404 )
405 .await?;
406 realm
407 .add_route(
408 Route::new()
409 .capability(expose.clone())
410 .from(&base_drivers)
411 .to(Ref::parent()),
412 )
413 .await?;
414 realm
415 .add_route(
416 Route::new()
417 .capability(expose.clone())
418 .from(&full_drivers)
419 .to(Ref::parent()),
420 )
421 .await?;
422
423 self.add_route(Route::new().capability(expose).from(&realm).to(Ref::parent()))
424 .await?;
425 }
426 }
427
428 realm
430 .add_route(
431 Route::new()
432 .capability(Capability::protocol_by_name(
433 "fuchsia.driver.test.ResourceProvider",
434 ))
435 .from(&driver_test_internal)
436 .to(&dtr_support),
437 )
438 .await?;
439
440 realm
442 .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
443 name: "fuchsia.driver.testrealm.TunnelBootItems".parse()?,
444 value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Bool(
445 tunnel_boot_items,
446 )),
447 }))
448 .await?;
449
450 realm
451 .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
452 name: "fuchsia.driver.testrealm.BoardName".parse()?,
453 value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::String(
454 args.board_name.unwrap_or_default().into(),
455 )),
456 }))
457 .await?;
458
459 realm
460 .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
461 name: "fuchsia.driver.testrealm.PlatformVid".parse()?,
462 value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::String(
463 args.platform_vid.map(|v| v.to_string()).unwrap_or_default().into(),
464 )),
465 }))
466 .await?;
467
468 realm
469 .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
470 name: "fuchsia.driver.testrealm.PlatformPid".parse()?,
471 value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::String(
472 args.platform_pid.map(|v| v.to_string()).unwrap_or_default().into(),
473 )),
474 }))
475 .await?;
476
477 let bind_eager = args.driver_bind_eager.unwrap_or_default();
478 realm
479 .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
480 name: "fuchsia.driver.BindEager".parse()?,
481 value: cm_rust::ConfigValue::Vector(cm_rust::ConfigVectorValue::StringVector(
482 bind_eager.into_iter().map(FlyStr::new).collect::<Box<[_]>>(),
483 )),
484 }))
485 .await?;
486
487 let driver_disable = args.driver_disable.unwrap_or_default();
488 realm
489 .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
490 name: "fuchsia.driver.DisabledDrivers".parse()?,
491 value: cm_rust::ConfigValue::Vector(cm_rust::ConfigVectorValue::StringVector(
492 driver_disable.into_iter().map(FlyStr::new).collect::<Box<[_]>>(),
493 )),
494 }))
495 .await?;
496
497 let driver_index_stop_timeout_millis = args.driver_index_stop_timeout_millis.unwrap_or(-1);
498 realm
499 .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
500 name: "fuchsia.driver.index.StopOnIdleTimeoutMillis".parse()?,
501 value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Int64(
502 driver_index_stop_timeout_millis,
503 )),
504 }))
505 .await?;
506 realm
507 .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
508 name: "fuchsia.power.WaitForSuspendingToken".parse()?,
509 value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::Bool(false)),
510 }))
511 .await?;
512
513 let root_driver = match args.root_driver {
514 Some(val) => val,
515 None => "fuchsia-boot:///dtr#meta/test-parent-sys.cm".to_string(),
516 };
517 realm
518 .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
519 name: "fuchsia.driver.manager.RootDriver".parse()?,
520 value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::String(
521 root_driver.into(),
522 )),
523 }))
524 .await?;
525
526 let software_devs_src = match args.software_devices {
528 Some(devs) => {
529 let names = devs.iter().map(|dev| dev.device_name.clone()).collect::<Vec<_>>();
530 let ids = devs.iter().map(|dev| dev.device_id).collect::<Vec<_>>();
531 realm
532 .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
533 name: "fuchsia.platform.bus.SoftwareDeviceNames".parse()?,
534 value: cm_rust::ConfigValue::Vector(
535 cm_rust::ConfigVectorValue::StringVector(
536 names.into_iter().map(FlyStr::new).collect::<Box<[_]>>(),
537 ),
538 ),
539 }))
540 .await?;
541 realm
542 .add_capability(cm_rust::CapabilityDecl::Config(cm_rust::ConfigurationDecl {
543 name: "fuchsia.platform.bus.SoftwareDeviceIds".parse()?,
544 value: cm_rust::ConfigValue::Vector(
545 cm_rust::ConfigVectorValue::Uint32Vector(ids.into()),
546 ),
547 }))
548 .await?;
549 Ref::self_()
550 }
551 None => Ref::void(),
552 };
553
554 realm
556 .add_route(
557 Route::new()
558 .capability(Capability::configuration("fuchsia.driver.BindEager"))
559 .capability(Capability::configuration("fuchsia.driver.DisabledDrivers"))
560 .capability(Capability::configuration(
561 "fuchsia.driver.index.StopOnIdleTimeoutMillis",
562 ))
563 .from(Ref::self_())
564 .to(&driver_index),
565 )
566 .await?;
567
568 realm
569 .add_route(
570 Route::new()
571 .capability(Capability::configuration("fuchsia.driver.manager.RootDriver"))
572 .from(Ref::self_())
573 .to(&driver_manager),
574 )
575 .await?;
576
577 realm
578 .add_route(
579 Route::new()
580 .capability(
581 Capability::configuration(
582 "fuchsia.driver.manager.SetRootDriverHostCritical",
583 )
584 .optional(),
585 )
586 .capability(
587 Capability::configuration("fuchsia.driver.manager.SuspendTimeoutFallback")
588 .optional(),
589 )
590 .from(Ref::void())
591 .to(&driver_manager),
592 )
593 .await?;
594
595 realm
596 .add_route(
597 Route::new()
598 .capability(Capability::configuration(
599 "fuchsia.driver.testrealm.TunnelBootItems",
600 ))
601 .capability(Capability::configuration("fuchsia.driver.testrealm.BoardName"))
602 .capability(Capability::configuration("fuchsia.driver.testrealm.PlatformVid"))
603 .capability(Capability::configuration("fuchsia.driver.testrealm.PlatformPid"))
604 .from(Ref::self_())
605 .to(&dtr_support),
606 )
607 .await?;
608
609 realm
610 .add_route(
611 Route::new()
612 .capability(Capability::configuration("fuchsia.power.WaitForSuspendingToken"))
613 .from(Ref::self_())
614 .to(&driver_manager),
615 )
616 .await?;
617
618 realm
619 .add_route(
620 Route::new()
621 .capability(
622 Capability::configuration("fuchsia.platform.bus.SoftwareDeviceNames")
623 .optional(),
624 )
625 .capability(
626 Capability::configuration("fuchsia.platform.bus.SoftwareDeviceIds")
627 .optional(),
628 )
629 .from(software_devs_src)
630 .to(&boot_drivers),
631 )
632 .await?;
633
634 realm
636 .add_route(
637 Route::new()
638 .capability(Capability::protocol_by_name("fuchsia.driver.test.Internal"))
639 .from(&driver_test_internal)
640 .to(&fake_resolver),
641 )
642 .await?;
643
644 self.add_route(
646 Route::new()
647 .capability(Capability::directory("dev-class"))
648 .capability(Capability::directory("dev-topological"))
649 .capability(Capability::protocol_by_name(
650 "fuchsia.driver.registrar.DriverRegistrar",
651 ))
652 .capability(Capability::protocol_by_name("fuchsia.driver.development.Manager"))
653 .capability(Capability::protocol_by_name(
654 "fuchsia.driver.framework.CompositeNodeManager",
655 ))
656 .capability(Capability::protocol_by_name("fuchsia.system.state.Administrator"))
657 .from(&realm)
658 .to(Ref::parent()),
659 )
660 .await?;
661 Ok(&self)
663 }
664}
665
666#[async_trait::async_trait]
667pub trait DriverTestRealmInstance {
668 fn driver_test_realm_connect_to_dev(&self) -> Result<fio::DirectoryProxy>;
670
671 async fn wait_for_bootup(&self) -> Result<()>;
675
676 async fn wait_for_node(&self, moniker: &str) -> Result<fdd::NodeInfo>;
678}
679
680#[async_trait::async_trait]
681impl DriverTestRealmInstance for RealmInstance {
682 fn driver_test_realm_connect_to_dev(&self) -> Result<fio::DirectoryProxy> {
683 fuchsia_fs::directory::open_directory_async(
684 self.root.get_exposed_dir(),
685 "dev-topological",
686 fio::Flags::empty(),
687 )
688 .map_err(Into::into)
689 }
690
691 async fn wait_for_bootup(&self) -> Result<()> {
692 let manager: fdd::ManagerProxy = self.root.connect_to_protocol_at_exposed_dir()?;
693 manager.wait_for_bootup().await?;
694 Ok(())
695 }
696
697 async fn wait_for_node(&self, moniker: &str) -> Result<fdd::NodeInfo> {
698 let manager: fdd::ManagerProxy = self.root.connect_to_protocol_at_exposed_dir()?;
699 loop {
700 let (iterator, iterator_server) =
701 fidl::endpoints::create_proxy::<fdd::NodeInfoIteratorMarker>();
702 manager.get_node_info(&[moniker.to_string()], iterator_server, true)?;
703 let next = iterator.get_next().await;
704 if let Ok(nodes) = next
705 && !nodes.is_empty()
706 && nodes[0].moniker == Some(moniker.to_string())
707 {
708 return Ok(nodes[0].clone());
709 }
710 }
711 }
712}