1use crate::reader::snapshot::{MakePrimitiveProperty, ScannedBlock, Snapshot};
35use diagnostics_hierarchy::*;
36use inspect_format::{
37 Array, BlockIndex, BlockType, Bool, Buffer, Double, Int, Link, Node, Uint, Unknown,
38 ValueBlockKind,
39};
40use maplit::btreemap;
41use std::borrow::Cow;
42use std::collections::BTreeMap;
43
44pub use crate::reader::error::ReaderError;
45pub use crate::reader::readable_tree::ReadableTree;
46pub use crate::reader::tree_reader::{read, read_with_timeout};
47pub use diagnostics_hierarchy::{ArrayContent, ArrayFormat, DiagnosticsHierarchy, Property};
48pub use inspect_format::LinkNodeDisposition;
49
50mod error;
51mod readable_tree;
52pub mod snapshot;
53mod tree_reader;
54
55#[derive(Clone, Debug, PartialEq)]
60pub struct PartialNodeHierarchy {
61 pub(crate) name: String,
63
64 pub(crate) properties: Vec<Property>,
66
67 pub(crate) children: Vec<PartialNodeHierarchy>,
69
70 pub(crate) links: Vec<LinkValue>,
72}
73
74#[derive(Debug, PartialEq, Clone)]
76pub(crate) struct LinkValue {
77 pub name: String,
79
80 pub content: String,
82
83 pub disposition: LinkNodeDisposition,
85}
86
87impl PartialNodeHierarchy {
88 pub fn new(
90 name: impl Into<String>,
91 properties: Vec<Property>,
92 children: Vec<PartialNodeHierarchy>,
93 ) -> Self {
94 Self { name: name.into(), properties, children, links: vec![] }
95 }
96
97 pub fn empty() -> Self {
99 PartialNodeHierarchy::new("", vec![], vec![])
100 }
101
102 pub fn is_complete(&self) -> bool {
105 self.links.is_empty()
106 }
107}
108
109impl From<PartialNodeHierarchy> for DiagnosticsHierarchy {
112 fn from(partial: PartialNodeHierarchy) -> DiagnosticsHierarchy {
113 DiagnosticsHierarchy {
114 name: partial.name,
115 children: partial.children.into_iter().map(|child| child.into()).collect(),
116 properties: partial.properties,
117 missing: partial
118 .links
119 .into_iter()
120 .map(|link_value| MissingValue {
121 reason: MissingValueReason::LinkNeverExpanded,
122 name: link_value.name,
123 })
124 .collect(),
125 }
126 }
127}
128
129impl DiagnosticsHierarchyGetter<String> for PartialNodeHierarchy {
130 async fn get_diagnostics_hierarchy<'a>(&'a self) -> Cow<'_, DiagnosticsHierarchy>
131 where
132 String: 'a,
133 {
134 let hierarchy: DiagnosticsHierarchy = self.clone().into();
135 if !hierarchy.missing.is_empty() {
136 panic!(
137 "Missing links: {:?}",
138 hierarchy
139 .missing
140 .iter()
141 .map(|missing| {
142 format!("(name:{:?}, reason:{:?})", missing.name, missing.reason)
143 })
144 .collect::<Vec<_>>()
145 .join(", ")
146 );
147 }
148 Cow::Owned(hierarchy)
149 }
150}
151
152impl TryFrom<Snapshot> for PartialNodeHierarchy {
153 type Error = ReaderError;
154
155 fn try_from(snapshot: Snapshot) -> Result<Self, Self::Error> {
156 read_snapshot(&snapshot)
157 }
158}
159
160#[cfg(target_os = "fuchsia")]
161impl TryFrom<&zx::Vmo> for PartialNodeHierarchy {
162 type Error = ReaderError;
163
164 fn try_from(vmo: &zx::Vmo) -> Result<Self, Self::Error> {
165 let snapshot = Snapshot::try_from(vmo)?;
166 read_snapshot(&snapshot)
167 }
168}
169
170impl TryFrom<Vec<u8>> for PartialNodeHierarchy {
171 type Error = ReaderError;
172
173 fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
174 let snapshot = Snapshot::try_from(bytes)?;
175 read_snapshot(&snapshot)
176 }
177}
178
179fn read_snapshot(snapshot: &Snapshot) -> Result<PartialNodeHierarchy, ReaderError> {
181 let result = scan_blocks(snapshot)?;
182 result.reduce()
183}
184
185fn scan_blocks(snapshot: &Snapshot) -> Result<ScanResult<'_>, ReaderError> {
186 let mut result = ScanResult::new(snapshot);
187 for block in snapshot.scan() {
188 if block.index() == BlockIndex::ROOT && block.block_type() != Some(BlockType::Header) {
189 return Err(ReaderError::MissingHeader);
190 }
191 match block.block_type().ok_or(ReaderError::InvalidVmo)? {
192 BlockType::NodeValue => {
193 result.parse_node(block.cast_unchecked::<Node>())?;
194 }
195 BlockType::IntValue => {
196 result.parse_primitive_property(block.cast_unchecked::<Int>())?;
197 }
198 BlockType::UintValue => {
199 result.parse_primitive_property(block.cast_unchecked::<Uint>())?;
200 }
201 BlockType::DoubleValue => {
202 result.parse_primitive_property(block.cast_unchecked::<Double>())?;
203 }
204 BlockType::BoolValue => {
205 result.parse_primitive_property(block.cast_unchecked::<Bool>())?;
206 }
207 BlockType::ArrayValue => {
208 result.parse_array_property(block.cast_unchecked::<Array<Unknown>>())?;
209 }
210 BlockType::BufferValue => {
211 result.parse_property(block.cast_unchecked::<Buffer>())?;
212 }
213 BlockType::LinkValue => {
214 result.parse_link(block.cast_unchecked::<Link>())?;
215 }
216 BlockType::Free
217 | BlockType::Reserved
218 | BlockType::Header
219 | BlockType::Extent
220 | BlockType::Name
221 | BlockType::Tombstone
222 | BlockType::StringReference => {}
223 }
224 }
225 Ok(result)
226}
227
228struct ScanResult<'a> {
230 parsed_nodes: BTreeMap<BlockIndex, ScannedNode>,
233
234 snapshot: &'a Snapshot,
236}
237
238#[derive(Debug)]
240struct ScannedNode {
241 partial_hierarchy: PartialNodeHierarchy,
243
244 child_nodes_count: usize,
246
247 parent_index: BlockIndex,
249
250 initialized: bool,
252}
253
254impl ScannedNode {
255 fn new() -> Self {
256 ScannedNode {
257 partial_hierarchy: PartialNodeHierarchy::empty(),
258 child_nodes_count: 0,
259 parent_index: BlockIndex::EMPTY,
260 initialized: false,
261 }
262 }
263
264 fn initialize(&mut self, name: String, parent_index: BlockIndex) {
266 self.partial_hierarchy.name = name;
267 self.parent_index = parent_index;
268 self.initialized = true;
269 }
270
271 fn is_complete(&self) -> bool {
274 self.partial_hierarchy.children.len() == self.child_nodes_count
275 }
276
277 fn is_initialized(&self) -> bool {
279 self.initialized
280 }
281}
282
283macro_rules! get_or_create_scanned_node {
284 ($map:expr, $key:expr) => {
285 $map.entry($key).or_insert(ScannedNode::new())
286 };
287}
288
289impl<'a> ScanResult<'a> {
290 fn new(snapshot: &'a Snapshot) -> Self {
291 let mut root_node = ScannedNode::new();
292 root_node.initialize("root".to_string(), BlockIndex::ROOT);
293 let parsed_nodes = btreemap!(
294 BlockIndex::ROOT => root_node,
295 );
296 ScanResult { snapshot, parsed_nodes }
297 }
298
299 fn reduce(self) -> Result<PartialNodeHierarchy, ReaderError> {
300 let mut complete_nodes = Vec::<ScannedNode>::new();
302
303 let mut pending_nodes = BTreeMap::<BlockIndex, ScannedNode>::new();
306
307 let mut uninitialized_nodes = std::collections::BTreeSet::new();
308
309 for (index, scanned_node) in self.parsed_nodes.into_iter() {
311 if !scanned_node.is_initialized() {
312 uninitialized_nodes.insert(index);
314 continue;
315 }
316 if scanned_node.is_complete() {
317 if index == BlockIndex::ROOT {
318 return Ok(scanned_node.partial_hierarchy);
319 }
320 complete_nodes.push(scanned_node);
321 } else {
322 pending_nodes.insert(index, scanned_node);
323 }
324 }
325
326 while let Some(scanned_node) = complete_nodes.pop() {
330 if uninitialized_nodes.contains(&scanned_node.parent_index) {
331 continue;
334 }
335 {
336 let parent_node = pending_nodes
338 .get_mut(&scanned_node.parent_index)
339 .ok_or(ReaderError::ParentIndexNotFound(scanned_node.parent_index))?;
340 parent_node.partial_hierarchy.children.push(scanned_node.partial_hierarchy);
341 }
342 if pending_nodes
343 .get(&scanned_node.parent_index)
344 .ok_or(ReaderError::ParentIndexNotFound(scanned_node.parent_index))?
345 .is_complete()
346 {
347 let parent_node = pending_nodes.remove(&scanned_node.parent_index).unwrap();
350 if scanned_node.parent_index == BlockIndex::ROOT {
351 return Ok(parent_node.partial_hierarchy);
352 }
353 complete_nodes.push(parent_node);
354 }
355 }
356
357 Err(ReaderError::MalformedTree)
358 }
359
360 pub fn get_name(&self, index: BlockIndex) -> Option<String> {
361 self.snapshot.get_name(index)
362 }
363
364 fn parse_node(&mut self, block: ScannedBlock<'_, Node>) -> Result<(), ReaderError> {
365 let name_index = block.name_index();
366 let name = self.get_name(name_index).ok_or(ReaderError::ParseName(name_index))?;
367 let parent_index = block.parent_index();
368 get_or_create_scanned_node!(self.parsed_nodes, block.index())
369 .initialize(name, parent_index);
370 if parent_index != block.index() {
371 get_or_create_scanned_node!(self.parsed_nodes, parent_index).child_nodes_count += 1;
372 }
373 Ok(())
374 }
375
376 fn push_property(
377 &mut self,
378 parent_index: BlockIndex,
379 property: Property,
380 ) -> Result<(), ReaderError> {
381 let parent = get_or_create_scanned_node!(self.parsed_nodes, parent_index);
382 parent.partial_hierarchy.properties.push(property);
383 Ok(())
384 }
385
386 fn parse_primitive_property<'b, K>(
387 &mut self,
388 block: ScannedBlock<'b, K>,
389 ) -> Result<(), ReaderError>
390 where
391 K: ValueBlockKind,
392 ScannedBlock<'b, K>: MakePrimitiveProperty,
393 {
394 let parent_index = block.parent_index();
395 let property = self.snapshot.parse_primitive_property(block)?;
396 self.push_property(parent_index, property)?;
397 Ok(())
398 }
399
400 fn parse_array_property(
401 &mut self,
402 block: ScannedBlock<'_, Array<Unknown>>,
403 ) -> Result<(), ReaderError> {
404 let parent_index = block.parent_index();
405 let property = self.snapshot.parse_array_property(block)?;
406 self.push_property(parent_index, property)?;
407 Ok(())
408 }
409
410 fn parse_property(&mut self, block: ScannedBlock<'_, Buffer>) -> Result<(), ReaderError> {
411 let parent_index = block.parent_index();
412 let property = self.snapshot.parse_property(block)?;
413 self.push_property(parent_index, property)?;
414 Ok(())
415 }
416
417 fn parse_link(&mut self, block: ScannedBlock<'_, Link>) -> Result<(), ReaderError> {
418 let parent_index = block.parent_index();
419 let parent = get_or_create_scanned_node!(self.parsed_nodes, parent_index);
420 parent.partial_hierarchy.links.push(self.snapshot.parse_link(block)?);
421 Ok(())
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428 use crate::types::private::InspectTypeInternal;
429 use crate::writer::testing_utils::GetBlockExt;
430 use crate::{ArrayProperty, HistogramProperty, Inspector};
431 use anyhow::Error;
432 use diagnostics_assertions::{assert_data_tree, assert_json_diff};
433 use futures::prelude::*;
434 use inspect_format::{constants, BlockContainer, CopyBytes, PayloadFields};
435
436 #[fuchsia::test]
437 async fn test_load_string_reference() {
438 let inspector = Inspector::default();
439 let root = inspector.root();
440
441 let name_value = "abc";
442 let longer_name_value = "abcdefg";
443
444 let child = root.create_child(name_value);
445 child.record_int(name_value, 5);
446
447 root.record_bool(longer_name_value, false);
448
449 let result = read(&inspector).await.unwrap();
450 assert_json_diff!(result, root: {
451 abc: {
452 abc: 5i64,
453 },
454 abcdefg: false,
455 });
456 }
457
458 #[fuchsia::test]
459 async fn read_string_array() {
460 let inspector = Inspector::default();
461 let root = inspector.root();
462
463 let zero = (0..3000).map(|_| '0').collect::<String>();
464 let one = "1";
465 let two = "two";
466 let three = "three three three";
467 let four = "fourth";
468
469 let array = root.create_string_array("array", 5);
470 array.set(0, zero.as_str());
471 array.set(1, one);
472 array.set(2, two);
473 array.set(3, three);
474 array.set(4, four);
475
476 let result = read(&inspector).await.unwrap();
477 assert_json_diff!(result, root: {
478 "array": vec![
479 zero,
480 one.to_string(),
481 two.to_string(),
482 three.to_string(),
483 four.to_string(),
484 ],
485 });
486 }
487
488 #[fuchsia::test]
489 async fn read_unset_string_array() {
490 let inspector = Inspector::default();
491 let root = inspector.root();
492
493 let zero = (0..3000).map(|_| '0').collect::<String>();
494 let one = "1";
495 let four = "fourth";
496
497 let array = root.create_string_array("array", 5);
498 array.set(0, zero.as_str());
499 array.set(1, one);
500 array.set(4, four);
501
502 let result = read(&inspector).await.unwrap();
503 assert_json_diff!(result, root: {
504 "array": vec![zero, one.to_string(), "".into(), "".into(), four.to_string()],
505 });
506 }
507
508 #[fuchsia::test]
509 async fn read_vmo() {
510 let inspector = Inspector::default();
511 let root = inspector.root();
512 let _root_int = root.create_int("int-root", 3);
513 let root_double_array = root.create_double_array("property-double-array", 5);
514 let double_array_data = vec![-1.2, 2.3, 3.4, 4.5, -5.6];
515 for (i, x) in double_array_data.iter().enumerate() {
516 root_double_array.set(i, *x);
517 }
518
519 let child1 = root.create_child("child-1");
520 let _child1_uint = child1.create_uint("property-uint", 10);
521 let _child1_double = child1.create_double("property-double", -3.4);
522 let _child1_bool = child1.create_bool("property-bool", true);
523
524 let chars = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
525 let string_data = chars.iter().cycle().take(6000).collect::<String>();
526 let _string_prop = child1.create_string("property-string", &string_data);
527
528 let child1_int_array = child1.create_int_linear_histogram(
529 "property-int-array",
530 LinearHistogramParams { floor: 1, step_size: 2, buckets: 3 },
531 );
532 for x in [-1, 2, 3, 5, 8].iter() {
533 child1_int_array.insert(*x);
534 }
535
536 let child2 = root.create_child("child-2");
537 let _child2_double = child2.create_double("property-double", 5.8);
538 let _child2_bool = child2.create_bool("property-bool", false);
539
540 let child3 = child1.create_child("child-1-1");
541 let _child3_int = child3.create_int("property-int", -9);
542 let bytes_data = (0u8..=9u8).cycle().take(5000).collect::<Vec<u8>>();
543 let _bytes_prop = child3.create_bytes("property-bytes", &bytes_data);
544
545 let child3_uint_array = child3.create_uint_exponential_histogram(
546 "property-uint-array",
547 ExponentialHistogramParams {
548 floor: 1,
549 initial_step: 1,
550 step_multiplier: 2,
551 buckets: 5,
552 },
553 );
554 for x in [1, 2, 3, 4].iter() {
555 child3_uint_array.insert(*x);
556 }
557
558 let result = read(&inspector).await.unwrap();
559
560 assert_data_tree!(result, root: {
561 "int-root": 3i64,
562 "property-double-array": double_array_data,
563 "child-1": {
564 "property-uint": 10u64,
565 "property-double": -3.4,
566 "property-bool": true,
567 "property-string": string_data,
568 "property-int-array": LinearHistogram {
569 floor: 1i64,
570 step: 2,
571 counts: vec![1, 1, 1, 1, 1],
572 indexes: None,
573 size: 5
574 },
575 "child-1-1": {
576 "property-int": -9i64,
577 "property-bytes": bytes_data,
578 "property-uint-array": ExponentialHistogram {
579 floor: 1u64,
580 initial_step: 1,
581 step_multiplier: 2,
582 counts: vec![1, 1, 2],
583 indexes: Some(vec![1, 2, 3]),
584 size: 7
585 },
586 }
587 },
588 "child-2": {
589 "property-double": 5.8,
590 "property-bool": false,
591 }
592 })
593 }
594
595 #[fuchsia::test]
596 async fn siblings_with_same_name() {
597 let inspector = Inspector::default();
598
599 let foo = "foo";
600
601 inspector.root().record_int("foo", 0);
602 inspector.root().record_int("foo", 1);
603 inspector.root().record_int(foo, 2);
604 inspector.root().record_int(foo, 3);
605
606 let dh = read(&inspector).await.unwrap();
607 assert_eq!(dh.properties.len(), 4);
608 for i in 0..dh.properties.len() {
609 match &dh.properties[i] {
610 Property::Int(n, v) => {
611 assert_eq!(n, "foo");
612 assert_eq!(*v, i as i64);
613 }
614 _ => panic!("We only record int properties"),
615 }
616 }
617 }
618
619 #[fuchsia::test]
620 async fn tombstone_reads() {
621 let inspector = Inspector::default();
622 let node1 = inspector.root().create_child("child1");
623 let node2 = node1.create_child("child2");
624 let node3 = node2.create_child("child3");
625 let prop1 = node1.create_string("val", "test");
626 let prop2 = node2.create_string("val", "test");
627 let prop3 = node3.create_string("val", "test");
628
629 assert_json_diff!(inspector,
630 root: {
631 child1: {
632 val: "test",
633 child2: {
634 val: "test",
635 child3: {
636 val: "test",
637 }
638 }
639 }
640 }
641 );
642
643 std::mem::drop(node3);
644 assert_json_diff!(inspector,
645 root: {
646 child1: {
647 val: "test",
648 child2: {
649 val: "test",
650 }
651 }
652 }
653 );
654
655 std::mem::drop(node2);
656 assert_json_diff!(inspector,
657 root: {
658 child1: {
659 val: "test",
660 }
661 }
662 );
663
664 let node2 = node1.create_child("child2");
666 let _node3 = node2.create_child("child3");
667 assert_json_diff!(inspector,
668 root: {
669 child1: {
670 val: "test",
671 child2: {
672 child3: {}
673 }
674 }
675 }
676 );
677
678 std::mem::drop(node2);
680 assert_json_diff!(inspector,
681 root: {
682 child1: {
683 val: "test",
684 }
685 }
686 );
687
688 std::mem::drop(node1);
689 assert_json_diff!(inspector,
690 root: {
691 }
692 );
693
694 std::mem::drop(prop3);
695 assert_json_diff!(inspector,
696 root: {
697 }
698 );
699
700 std::mem::drop(prop2);
701 assert_json_diff!(inspector,
702 root: {
703 }
704 );
705
706 std::mem::drop(prop1);
707 assert_json_diff!(inspector,
708 root: {
709 }
710 );
711 }
712
713 #[fuchsia::test]
714 async fn from_invalid_utf8_string() {
715 let inspector = Inspector::default();
718 let root = inspector.root();
719 let prop = root.create_string("property", "hello world");
720
721 let vmo = inspector.vmo().await.unwrap();
724 let snapshot = Snapshot::try_from(&vmo).expect("getting snapshot");
725 let block = snapshot
726 .get_block(prop.block_index().unwrap())
727 .expect("getting block")
728 .cast::<Buffer>()
729 .unwrap();
730
731 let byte_offset = constants::MIN_ORDER_SIZE * (*block.extent_index() as usize)
733 + constants::HEADER_SIZE_BYTES
734 + constants::STRING_REFERENCE_TOTAL_LENGTH_BYTES;
735
736 let vmo_size = BlockContainer::len(&vmo);
738 let mut buf = vec![0u8; vmo_size];
739 vmo.copy_bytes(&mut buf[..]);
740
741 buf[byte_offset] = 0xFE;
745 let hierarchy: DiagnosticsHierarchy = PartialNodeHierarchy::try_from(Snapshot::build(&buf))
746 .expect("creating node hierarchy")
747 .into();
748
749 assert_json_diff!(hierarchy, root: {
750 property: "\u{FFFD}ello world",
751 });
752 }
753
754 #[fuchsia::test]
755 async fn test_invalid_array_slots() -> Result<(), Error> {
756 let inspector = Inspector::default();
757 let root = inspector.root();
758 let array = root.create_int_array("int-array", 3);
759
760 array.get_block_mut::<_, Array<Int>>(|array_block| {
762 PayloadFields::set_array_slots_count(array_block, 255);
763 });
764
765 let vmo = inspector.vmo().await.unwrap();
766 let vmo_size = BlockContainer::len(&vmo);
767
768 let mut buf = vec![0u8; vmo_size];
769 vmo.copy_bytes(&mut buf[..]);
770
771 assert!(PartialNodeHierarchy::try_from(Snapshot::build(&buf)).is_err());
772
773 Ok(())
774 }
775
776 #[fuchsia::test]
777 async fn lazy_nodes() -> Result<(), Error> {
778 let inspector = Inspector::default();
779 inspector.root().record_int("int", 3);
780 let child = inspector.root().create_child("child");
781 child.record_double("double", 1.5);
782 inspector.root().record_lazy_child("lazy", || {
783 async move {
784 let inspector = Inspector::default();
785 inspector.root().record_uint("uint", 5);
786 inspector.root().record_lazy_values("nested-lazy-values", || {
787 async move {
788 let inspector = Inspector::default();
789 inspector.root().record_string("string", "test");
790 let child = inspector.root().create_child("nested-lazy-child");
791 let array = child.create_int_array("array", 3);
792 array.set(0, 1);
793 child.record(array);
794 inspector.root().record(child);
795 Ok(inspector)
796 }
797 .boxed()
798 });
799 Ok(inspector)
800 }
801 .boxed()
802 });
803
804 inspector.root().record_lazy_values("lazy-values", || {
805 async move {
806 let inspector = Inspector::default();
807 let child = inspector.root().create_child("lazy-child-1");
808 child.record_string("test", "testing");
809 inspector.root().record(child);
810 inspector.root().record_uint("some-uint", 3);
811 inspector.root().record_lazy_values("nested-lazy-values", || {
812 async move {
813 let inspector = Inspector::default();
814 inspector.root().record_int("lazy-int", -3);
815 let child = inspector.root().create_child("one-more-child");
816 child.record_double("lazy-double", 4.3);
817 inspector.root().record(child);
818 Ok(inspector)
819 }
820 .boxed()
821 });
822 inspector.root().record_lazy_child("nested-lazy-child", || {
823 async move {
824 let inspector = Inspector::default();
825 let _double = inspector.root().create_double("double", -1.2);
827 Ok(inspector)
828 }
829 .boxed()
830 });
831 Ok(inspector)
832 }
833 .boxed()
834 });
835
836 let hierarchy = read(&inspector).await?;
837 assert_json_diff!(hierarchy, root: {
838 int: 3i64,
839 child: {
840 double: 1.5,
841 },
842 lazy: {
843 uint: 5u64,
844 string: "test",
845 "nested-lazy-child": {
846 array: vec![1i64, 0, 0],
847 }
848 },
849 "some-uint": 3u64,
850 "lazy-child-1": {
851 test: "testing",
852 },
853 "lazy-int": -3i64,
854 "one-more-child": {
855 "lazy-double": 4.3,
856 },
857 "nested-lazy-child": {
858 }
859 });
860
861 Ok(())
862 }
863
864 #[fuchsia::test]
865 async fn test_matching_with_inspector() {
866 let inspector = Inspector::default();
867 assert_json_diff!(inspector, root: {});
868 }
869
870 #[fuchsia::test]
871 async fn test_matching_with_partial() {
872 let propreties = vec![Property::String("sub".to_string(), "sub_value".to_string())];
873 let partial = PartialNodeHierarchy::new("root", propreties, vec![]);
874 assert_json_diff!(partial, root: {
875 sub: "sub_value",
876 });
877 }
878
879 #[fuchsia::test]
880 #[should_panic]
881 async fn test_missing_values_with_partial() {
882 let mut partial = PartialNodeHierarchy::new("root", vec![], vec![]);
883 partial.links = vec![LinkValue {
884 name: "missing-link".to_string(),
885 content: "missing-link-404".to_string(),
886 disposition: LinkNodeDisposition::Child,
887 }];
888 assert_json_diff!(partial, root: {});
889 }
890
891 #[fuchsia::test]
892 async fn test_matching_with_expression_as_key() {
893 let properties = vec![Property::String("sub".to_string(), "sub_value".to_string())];
894 let partial = PartialNodeHierarchy::new("root", properties, vec![]);
895 let value = || "sub_value";
896 let key = || "sub".to_string();
897 assert_json_diff!(partial, root: {
898 key() => value(),
899 });
900 }
901}