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