1use std::{
2 collections::HashMap,
3 fmt,
4 time::{Duration, Instant},
5};
6
7use candid::Principal;
8use pocket_ic::{CanisterLogRecord, CanisterStatusResult, PocketIc};
9
10use super::{
11 PocketIcCallFailure,
12 transport::{self, capture_call},
13};
14
15pub const DEFAULT_CANISTER_LOG_RECORD_LIMIT: usize = 32;
17
18pub const DEFAULT_CANISTER_LOG_BYTE_LIMIT: usize = 16 * 1024;
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub struct CanisterLogRenderLimits {
24 pub record_limit: usize,
26 pub byte_limit: usize,
28}
29
30impl Default for CanisterLogRenderLimits {
31 fn default() -> Self {
32 Self {
33 record_limit: DEFAULT_CANISTER_LOG_RECORD_LIMIT,
34 byte_limit: DEFAULT_CANISTER_LOG_BYTE_LIMIT,
35 }
36 }
37}
38
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub struct CanisterDiagnosticsRequest {
42 pub canister_id: Principal,
44 pub status_sender: Principal,
46 pub log_sender: Principal,
48 pub log_limits: CanisterLogRenderLimits,
50}
51
52#[derive(Clone, Debug, Eq, PartialEq)]
54pub struct LabeledCanisterDiagnosticsRequest {
55 pub label: String,
57 pub request: CanisterDiagnosticsRequest,
59}
60
61#[non_exhaustive]
63#[derive(Clone, Debug, Eq, PartialEq)]
64pub enum CanisterDiagnosticsBatchContractError {
65 EmptyLabel {
67 index: usize,
69 },
70 DuplicateLabel {
72 label: String,
74 first_index: usize,
76 duplicate_index: usize,
78 },
79}
80
81#[derive(Clone, Debug, Eq, PartialEq)]
83pub struct CanisterDiagnosticLogRecord {
84 index: u64,
85 timestamp_nanos: u64,
86 content: String,
87 original_content_bytes: usize,
88 omitted_content_bytes: usize,
89}
90
91impl CanisterDiagnosticLogRecord {
92 #[must_use]
94 pub const fn index(&self) -> u64 {
95 self.index
96 }
97
98 #[must_use]
100 pub const fn timestamp_nanos(&self) -> u64 {
101 self.timestamp_nanos
102 }
103
104 #[must_use]
106 pub fn content(&self) -> &str {
107 &self.content
108 }
109
110 #[must_use]
112 pub const fn original_content_bytes(&self) -> usize {
113 self.original_content_bytes
114 }
115
116 #[must_use]
118 pub const fn omitted_content_bytes(&self) -> usize {
119 self.omitted_content_bytes
120 }
121
122 #[must_use]
124 pub const fn was_truncated(&self) -> bool {
125 self.omitted_content_bytes != 0
126 }
127}
128
129#[derive(Clone, Debug, Eq, PartialEq)]
131pub struct CanisterDiagnosticLogs {
132 records: Vec<CanisterDiagnosticLogRecord>,
133 total_records: usize,
134 total_content_bytes: usize,
135 omitted_content_bytes: usize,
136}
137
138impl CanisterDiagnosticLogs {
139 #[must_use]
141 pub fn records(&self) -> &[CanisterDiagnosticLogRecord] {
142 &self.records
143 }
144
145 #[must_use]
147 pub const fn total_records(&self) -> usize {
148 self.total_records
149 }
150
151 #[must_use]
153 pub const fn total_content_bytes(&self) -> usize {
154 self.total_content_bytes
155 }
156
157 #[must_use]
159 pub const fn omitted_records(&self) -> usize {
160 self.total_records - self.records.len()
161 }
162
163 #[must_use]
165 pub const fn omitted_content_bytes(&self) -> usize {
166 self.omitted_content_bytes
167 }
168
169 #[must_use]
171 pub const fn was_truncated(&self) -> bool {
172 self.omitted_records() != 0 || self.omitted_content_bytes != 0
173 }
174}
175
176impl fmt::Display for CanisterDiagnosticLogs {
177 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
178 if self.records.is_empty() {
179 if self.total_records == 0 {
180 formatter.write_str("<empty>")?;
181 } else {
182 formatter.write_str("<no retained records>")?;
183 }
184 } else {
185 for (position, record) in self.records.iter().enumerate() {
186 if position != 0 {
187 formatter.write_str(", ")?;
188 }
189 write!(
190 formatter,
191 "[{}@{}]={:?}",
192 record.index, record.timestamp_nanos, record.content
193 )?;
194 if record.was_truncated() {
195 write!(
196 formatter,
197 " (truncated {} bytes)",
198 record.omitted_content_bytes
199 )?;
200 }
201 }
202 }
203 if self.was_truncated() {
204 write!(
205 formatter,
206 "; truncated omitted_records={} omitted_content_bytes={}",
207 self.omitted_records(),
208 self.omitted_content_bytes
209 )?;
210 }
211 Ok(())
212 }
213}
214
215#[derive(Debug)]
217pub struct CanisterDiagnosticsReport {
218 request: CanisterDiagnosticsRequest,
219 status: Result<CanisterStatusResult, PocketIcCallFailure>,
220 logs: Result<CanisterDiagnosticLogs, PocketIcCallFailure>,
221}
222
223impl CanisterDiagnosticsReport {
224 #[must_use]
226 pub const fn request(&self) -> CanisterDiagnosticsRequest {
227 self.request
228 }
229
230 pub const fn status(&self) -> Result<&CanisterStatusResult, &PocketIcCallFailure> {
232 self.status.as_ref()
233 }
234
235 pub const fn logs(&self) -> Result<&CanisterDiagnosticLogs, &PocketIcCallFailure> {
237 self.logs.as_ref()
238 }
239
240 #[must_use]
242 pub const fn is_success(&self) -> bool {
243 self.status.is_ok() && self.logs.is_ok()
244 }
245
246 pub fn into_parts(
248 self,
249 ) -> (
250 CanisterDiagnosticsRequest,
251 Result<CanisterStatusResult, PocketIcCallFailure>,
252 Result<CanisterDiagnosticLogs, PocketIcCallFailure>,
253 ) {
254 (self.request, self.status, self.logs)
255 }
256}
257
258#[derive(Debug)]
260pub struct CanisterDiagnosticsBatchEntry {
261 label: String,
262 report: CanisterDiagnosticsReport,
263 entry_elapsed: Duration,
264}
265
266impl CanisterDiagnosticsBatchEntry {
267 #[must_use]
269 pub fn label(&self) -> &str {
270 &self.label
271 }
272
273 #[must_use]
275 pub const fn report(&self) -> &CanisterDiagnosticsReport {
276 &self.report
277 }
278
279 #[must_use]
281 pub const fn is_success(&self) -> bool {
282 self.report.is_success()
283 }
284
285 #[must_use]
287 pub const fn entry_elapsed(&self) -> Duration {
288 self.entry_elapsed
289 }
290
291 #[must_use]
293 pub fn into_parts(self) -> (String, CanisterDiagnosticsReport, Duration) {
294 (self.label, self.report, self.entry_elapsed)
295 }
296}
297
298#[derive(Debug, Default)]
300pub struct CanisterDiagnosticsBatchReport {
301 entries: Vec<CanisterDiagnosticsBatchEntry>,
302 total: Duration,
303}
304
305impl CanisterDiagnosticsBatchReport {
306 #[must_use]
308 pub fn entries(&self) -> &[CanisterDiagnosticsBatchEntry] {
309 &self.entries
310 }
311
312 pub fn failures(&self) -> impl Iterator<Item = &CanisterDiagnosticsBatchEntry> {
314 self.entries.iter().filter(|entry| !entry.is_success())
315 }
316
317 #[must_use]
319 pub fn is_success(&self) -> bool {
320 self.entries
321 .iter()
322 .all(CanisterDiagnosticsBatchEntry::is_success)
323 }
324
325 #[must_use]
327 pub const fn total(&self) -> Duration {
328 self.total
329 }
330
331 #[must_use]
333 pub fn into_entries(self) -> Vec<CanisterDiagnosticsBatchEntry> {
334 self.entries
335 }
336}
337
338impl fmt::Display for CanisterDiagnosticsBatchReport {
339 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
340 write!(
341 formatter,
342 "diagnostics={} total={:?}",
343 self.entries.len(),
344 self.total
345 )?;
346 for entry in &self.entries {
347 write!(
348 formatter,
349 "; label={:?} elapsed={:?} {}",
350 entry.label, entry.entry_elapsed, entry.report
351 )?;
352 }
353 Ok(())
354 }
355}
356
357impl fmt::Display for CanisterDiagnosticsReport {
358 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
359 write!(
360 formatter,
361 "canister={} status_sender={} status=",
362 self.request.canister_id, self.request.status_sender
363 )?;
364 match &self.status {
365 Ok(status) => write!(
366 formatter,
367 "ok(state={:?} version={} controllers={} module_hash_bytes={} memory_bytes={} cycles={})",
368 status.status,
369 status.version,
370 status.settings.controllers.len(),
371 status.module_hash.as_ref().map_or(0, Vec::len),
372 status.memory_size,
373 status.cycles,
374 ),
375 Err(failure) => write!(formatter, "<{failure}>"),
376 }?;
377 write!(formatter, " log_sender={} logs=", self.request.log_sender)?;
378 match &self.logs {
379 Err(failure) => write!(formatter, "<{failure}>")?,
380 Ok(logs) => write!(formatter, "{logs}")?,
381 }
382 Ok(())
383 }
384}
385
386pub trait PocketIcDiagnosticsExt {
388 fn collect_canister_diagnostics(
393 &self,
394 request: CanisterDiagnosticsRequest,
395 ) -> CanisterDiagnosticsReport;
396
397 fn collect_canister_diagnostics_batch(
403 &self,
404 requests: &[LabeledCanisterDiagnosticsRequest],
405 ) -> Result<CanisterDiagnosticsBatchReport, CanisterDiagnosticsBatchContractError> {
406 validate_diagnostics_batch_labels(requests)?;
407 let started = Instant::now();
408 let entries = requests
409 .iter()
410 .map(|labeled| {
411 let entry_started = Instant::now();
412 let request = labeled.request;
413 let report =
414 transport::capture_operation(|| self.collect_canister_diagnostics(request))
415 .unwrap_or_else(|source| CanisterDiagnosticsReport {
416 request,
417 status: Err(PocketIcCallFailure::Operation(source.clone())),
418 logs: Err(PocketIcCallFailure::Operation(source)),
419 });
420 CanisterDiagnosticsBatchEntry {
421 label: labeled.label.clone(),
422 report,
423 entry_elapsed: entry_started.elapsed(),
424 }
425 })
426 .collect();
427 Ok(CanisterDiagnosticsBatchReport {
428 entries,
429 total: started.elapsed(),
430 })
431 }
432}
433
434fn validate_diagnostics_batch_labels(
435 requests: &[LabeledCanisterDiagnosticsRequest],
436) -> Result<(), CanisterDiagnosticsBatchContractError> {
437 let mut seen = HashMap::new();
438 for (index, request) in requests.iter().enumerate() {
439 let label = request.label.as_str();
440 if label.is_empty() {
441 return Err(CanisterDiagnosticsBatchContractError::EmptyLabel { index });
442 }
443 if let Some(first_index) = seen.insert(label, index) {
444 return Err(CanisterDiagnosticsBatchContractError::DuplicateLabel {
445 label: label.to_owned(),
446 first_index,
447 duplicate_index: index,
448 });
449 }
450 }
451 Ok(())
452}
453
454impl fmt::Display for CanisterDiagnosticsBatchContractError {
455 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
456 match self {
457 Self::EmptyLabel { index } => {
458 write!(
459 formatter,
460 "diagnostics batch label at index {index} is empty"
461 )
462 }
463 Self::DuplicateLabel {
464 label,
465 first_index,
466 duplicate_index,
467 } => write!(
468 formatter,
469 "diagnostics batch label {label:?} at index {duplicate_index} duplicates index {first_index}",
470 ),
471 }
472 }
473}
474
475impl std::error::Error for CanisterDiagnosticsBatchContractError {}
476
477impl PocketIcDiagnosticsExt for PocketIc {
478 fn collect_canister_diagnostics(
479 &self,
480 request: CanisterDiagnosticsRequest,
481 ) -> CanisterDiagnosticsReport {
482 let status =
483 capture_call(|| self.canister_status(request.canister_id, Some(request.status_sender)));
484 let logs =
485 capture_call(|| self.fetch_canister_logs(request.canister_id, request.log_sender))
486 .map(|records| render_log_records(records, request.log_limits));
487
488 CanisterDiagnosticsReport {
489 request,
490 status,
491 logs,
492 }
493 }
494}
495
496fn render_log_records(
497 records: Vec<CanisterLogRecord>,
498 limits: CanisterLogRenderLimits,
499) -> CanisterDiagnosticLogs {
500 let total_records = records.len();
501 let mut total_content_bytes = 0usize;
502 let mut rendered = Vec::with_capacity(total_records.min(limits.record_limit));
503 let mut retained_bytes = 0usize;
504 let mut omitted_content_bytes = 0usize;
505
506 for record in records {
507 total_content_bytes = total_content_bytes.saturating_add(record.content.len());
508 if rendered.len() == limits.record_limit || retained_bytes == limits.byte_limit {
509 omitted_content_bytes = omitted_content_bytes.saturating_add(record.content.len());
510 continue;
511 }
512
513 let available = limits.byte_limit.saturating_sub(retained_bytes);
514 let retained = record.content.len().min(available);
515 let omitted = record.content.len().saturating_sub(retained);
516 let content = String::from_utf8_lossy(&record.content[..retained]).into_owned();
517 retained_bytes = retained_bytes.saturating_add(retained);
518 omitted_content_bytes = omitted_content_bytes.saturating_add(omitted);
519 rendered.push(CanisterDiagnosticLogRecord {
520 index: record.idx,
521 timestamp_nanos: record.timestamp_nanos,
522 content,
523 original_content_bytes: record.content.len(),
524 omitted_content_bytes: omitted,
525 });
526 }
527
528 CanisterDiagnosticLogs {
529 records: rendered,
530 total_records,
531 total_content_bytes,
532 omitted_content_bytes,
533 }
534}
535
536#[cfg(test)]
537mod tests {
538 use std::cell::Cell;
539
540 use candid::Principal;
541 use pocket_ic::CanisterLogRecord;
542
543 use super::{
544 CanisterDiagnosticsBatchContractError, CanisterDiagnosticsReport,
545 CanisterDiagnosticsRequest, CanisterLogRenderLimits, LabeledCanisterDiagnosticsRequest,
546 PocketIcCallFailure, PocketIcDiagnosticsExt, capture_call, render_log_records,
547 };
548
549 #[test]
550 fn diagnostic_transport_classification_survives_context_and_io_wrapping() {
551 const REFUSED: &str = "HTTP failure: reqwest::Error { kind: Request, url: \"http://127.0.0.1:1234/instances/0/read/get_time\", source: ConnectError(\"tcp connect error\", Os { code: 111, kind: ConnectionRefused, message: \"Connection refused\" }) }";
552 const RESET: &str = "HTTP failure: reqwest::Error { kind: Request, url: \"http://127.0.0.1:49238/instances/0/read/get_time\", source: hyper_util::client::legacy::Error(SendRequest, hyper::Error(Io, Os { code: 54, kind: ConnectionReset, message: \"Connection reset by peer\" })) }";
553 for message in [REFUSED, RESET, "application worker channel closed"] {
554 let error = capture_call::<()>(|| panic!("{message}"))
555 .expect_err("diagnostic operation should capture the panic");
556 let expected = message != "application worker channel closed";
557 let PocketIcCallFailure::Operation(source) = &error else {
558 panic!("expected the original operation error");
559 };
560 assert_eq!(source.message(), message);
561 assert_eq!(source.is_transport(), expected);
562 let chained = std::error::Error::source(&error)
563 .and_then(|cause| cause.downcast_ref::<super::super::PocketIcOperationError>())
564 .expect("diagnostic error exposes its original typed cause");
565 assert_eq!(chained, source);
566 assert_eq!(
567 super::super::is_dead_pocket_ic_transport_error(&error),
568 expected,
569 );
570 let wrapped = std::io::Error::other(error);
571 assert_eq!(
572 super::super::is_dead_pocket_ic_transport_error(&wrapped),
573 expected,
574 );
575 }
576 }
577
578 struct PanickingThenReporting {
579 calls: Cell<usize>,
580 }
581
582 impl PocketIcDiagnosticsExt for PanickingThenReporting {
583 fn collect_canister_diagnostics(
584 &self,
585 request: CanisterDiagnosticsRequest,
586 ) -> CanisterDiagnosticsReport {
587 let call = self.calls.get();
588 self.calls.set(call + 1);
589 assert_ne!(call, 0, "synthetic first-entry diagnostic panic");
590 CanisterDiagnosticsReport {
591 request,
592 status: Err(PocketIcCallFailure::Operation(
593 super::super::PocketIcOperationError::new("synthetic status failure"),
594 )),
595 logs: Err(PocketIcCallFailure::Operation(
596 super::super::PocketIcOperationError::new("synthetic log failure"),
597 )),
598 }
599 }
600 }
601
602 #[test]
603 fn labeled_batch_retains_order_and_continues_after_entry_panic() {
604 let collector = PanickingThenReporting {
605 calls: Cell::new(0),
606 };
607 let first = CanisterDiagnosticsRequest {
608 canister_id: Principal::from_slice(&[1]),
609 status_sender: Principal::from_slice(&[2]),
610 log_sender: Principal::from_slice(&[3]),
611 log_limits: CanisterLogRenderLimits::default(),
612 };
613 let second = CanisterDiagnosticsRequest {
614 canister_id: Principal::from_slice(&[4]),
615 status_sender: Principal::from_slice(&[5]),
616 log_sender: Principal::from_slice(&[6]),
617 log_limits: CanisterLogRenderLimits::default(),
618 };
619 let report = collector
620 .collect_canister_diagnostics_batch(&[
621 LabeledCanisterDiagnosticsRequest {
622 label: "root".into(),
623 request: first,
624 },
625 LabeledCanisterDiagnosticsRequest {
626 label: "worker".into(),
627 request: second,
628 },
629 ])
630 .expect("valid labeled diagnostics batch");
631
632 assert_eq!(collector.calls.get(), 2);
633 assert_eq!(report.entries().len(), 2);
634 assert_eq!(report.entries()[0].label(), "root");
635 assert_eq!(report.entries()[0].report().request(), first);
636 assert_eq!(report.entries()[1].label(), "worker");
637 assert_eq!(report.entries()[1].report().request(), second);
638 assert!(
639 report
640 .entries()
641 .iter()
642 .all(|entry| entry.entry_elapsed() <= report.total())
643 );
644 assert_eq!(report.failures().count(), 2);
645 assert!(!report.is_success());
646 let compact = report.to_string();
647 assert!(compact.contains("label=\"root\""));
648 assert!(compact.contains("label=\"worker\""));
649 assert!(compact.contains("synthetic first-entry diagnostic panic"));
650 }
651
652 #[test]
653 fn diagnostic_batch_rejects_invalid_labels_before_collection() {
654 let collector = PanickingThenReporting {
655 calls: Cell::new(0),
656 };
657 let request = CanisterDiagnosticsRequest {
658 canister_id: Principal::from_slice(&[1]),
659 status_sender: Principal::from_slice(&[2]),
660 log_sender: Principal::from_slice(&[3]),
661 log_limits: CanisterLogRenderLimits::default(),
662 };
663 let empty = collector
664 .collect_canister_diagnostics_batch(&[LabeledCanisterDiagnosticsRequest {
665 label: String::new(),
666 request,
667 }])
668 .expect_err("empty label must reject diagnostics batch");
669 assert_eq!(
670 empty,
671 CanisterDiagnosticsBatchContractError::EmptyLabel { index: 0 }
672 );
673 assert_eq!(collector.calls.get(), 0);
674
675 let duplicate = collector
676 .collect_canister_diagnostics_batch(&[
677 LabeledCanisterDiagnosticsRequest {
678 label: "same".into(),
679 request,
680 },
681 LabeledCanisterDiagnosticsRequest {
682 label: "same".into(),
683 request,
684 },
685 ])
686 .expect_err("duplicate labels must reject diagnostics batch");
687 assert_eq!(
688 duplicate,
689 CanisterDiagnosticsBatchContractError::DuplicateLabel {
690 label: "same".to_owned(),
691 first_index: 0,
692 duplicate_index: 1,
693 }
694 );
695 assert_eq!(collector.calls.get(), 0);
696 }
697
698 #[test]
699 fn log_rendering_is_bounded_lossy_utf8_and_reports_truncation() {
700 let logs = render_log_records(
701 vec![
702 CanisterLogRecord {
703 idx: 7,
704 timestamp_nanos: 11,
705 content: vec![b'f', 0x80, b'o'],
706 },
707 CanisterLogRecord {
708 idx: 8,
709 timestamp_nanos: 12,
710 content: b"bar".to_vec(),
711 },
712 ],
713 CanisterLogRenderLimits {
714 record_limit: 1,
715 byte_limit: 2,
716 },
717 );
718
719 assert_eq!(logs.total_records(), 2);
720 assert_eq!(logs.total_content_bytes(), 6);
721 assert_eq!(logs.omitted_records(), 1);
722 assert_eq!(logs.omitted_content_bytes(), 4);
723 assert!(logs.was_truncated());
724 assert_eq!(logs.records().len(), 1);
725 assert_eq!(logs.records()[0].content(), "f�");
726 assert_eq!(logs.records()[0].original_content_bytes(), 3);
727 assert_eq!(logs.records()[0].omitted_content_bytes(), 1);
728 assert!(logs.records()[0].was_truncated());
729 let rendered = logs.to_string();
730 assert!(rendered.contains("f�"));
731 assert!(rendered.contains("truncated omitted_records=1 omitted_content_bytes=4"));
732 }
733
734 #[test]
735 fn zero_log_bounds_retain_only_aggregate_truncation() {
736 for (record_limit, byte_limit) in [(0, 0), (0, 16), (16, 0)] {
737 let logs = render_log_records(
738 vec![CanisterLogRecord {
739 idx: 1,
740 timestamp_nanos: 2,
741 content: b"hello".to_vec(),
742 }],
743 CanisterLogRenderLimits {
744 record_limit,
745 byte_limit,
746 },
747 );
748
749 assert_eq!(logs.records(), []);
750 assert_eq!(logs.total_records(), 1);
751 assert_eq!(logs.total_content_bytes(), 5);
752 assert_eq!(logs.omitted_records(), 1);
753 assert_eq!(logs.omitted_content_bytes(), 5);
754 assert!(logs.was_truncated());
755 assert_eq!(
756 logs.to_string(),
757 "<no retained records>; truncated omitted_records=1 omitted_content_bytes=5"
758 );
759 }
760 }
761
762 #[test]
763 fn logs_within_bounds_preserve_empty_records_and_raw_byte_totals() {
764 let limits = CanisterLogRenderLimits {
765 record_limit: 3,
766 byte_limit: 3,
767 };
768 let empty = render_log_records(vec![], limits);
769 assert_eq!(empty.total_records(), 0);
770 assert_eq!(empty.total_content_bytes(), 0);
771 assert_eq!(empty.omitted_records(), 0);
772 assert_eq!(empty.omitted_content_bytes(), 0);
773 assert!(!empty.was_truncated());
774 assert_eq!(empty.to_string(), "<empty>");
775
776 let logs = render_log_records(
777 vec![
778 CanisterLogRecord {
779 idx: 1,
780 timestamp_nanos: 2,
781 content: vec![],
782 },
783 CanisterLogRecord {
784 idx: 3,
785 timestamp_nanos: 4,
786 content: vec![0x80, b'a'],
787 },
788 CanisterLogRecord {
789 idx: 5,
790 timestamp_nanos: 6,
791 content: vec![],
792 },
793 ],
794 limits,
795 );
796 assert_eq!(logs.total_records(), 3);
797 assert_eq!(logs.records().len(), 3);
798 assert_eq!(logs.total_content_bytes(), 2);
799 assert_eq!(logs.records()[0].content(), "");
800 assert_eq!(logs.records()[1].content(), "�a");
801 assert_eq!(logs.records()[2].content(), "");
802 assert_eq!(logs.omitted_records(), 0);
803 assert_eq!(logs.omitted_content_bytes(), 0);
804 assert!(!logs.was_truncated());
805 }
806}