use crate::writer::private::InspectTypeInternal;
use crate::writer::{
BoolProperty, BytesProperty, DoubleArrayProperty, DoubleExponentialHistogramProperty,
DoubleLinearHistogramProperty, DoubleProperty, Error, Inner, InnerData, InnerType, InspectType,
InspectTypeReparentable, Inspector, IntArrayProperty, IntExponentialHistogramProperty,
IntLinearHistogramProperty, IntProperty, LazyNode, State, StringArrayProperty, StringProperty,
StringReference, UintArrayProperty, UintExponentialHistogramProperty,
UintLinearHistogramProperty, UintProperty, ValueList,
};
use diagnostics_hierarchy::{ArrayFormat, ExponentialHistogramParams, LinearHistogramParams};
use futures::future::BoxFuture;
use inspect_format::{BlockIndex, LinkNodeDisposition, PropertyFormat};
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(Debug, PartialEq, Eq, Default)]
pub struct Node {
pub(crate) inner: Inner<InnerNodeType>,
}
impl InspectType for Node {}
crate::impl_inspect_type_internal!(Node);
impl Node {
pub fn clone_weak(&self) -> Node {
Self { inner: self.inner.clone_weak() }
}
#[must_use]
pub fn create_child(&self, name: impl Into<StringReference>) -> Node {
self.inner
.inner_ref()
.and_then(|inner_ref| {
inner_ref
.state
.try_lock()
.and_then(|mut state| state.create_node(name.into(), inner_ref.block_index))
.map(|block_index| Node::new(inner_ref.state.clone(), block_index))
.ok()
})
.unwrap_or_else(Node::new_no_op)
}
pub fn record_child<F>(&self, name: impl Into<StringReference>, initialize: F)
where
F: FnOnce(&Node),
{
self.atomic_update(move |n| {
let child = n.create_child(name.into());
initialize(&child);
n.record(child);
});
}
pub fn atomic_update<F, R>(&self, update_fn: F) -> R
where
F: FnOnce(&Node) -> R,
{
self.atomic_access(update_fn)
}
pub fn record(&self, property: impl InspectType + 'static) {
if let Some(inner_ref) = self.inner.inner_ref() {
inner_ref.data.values.record(property);
}
}
pub fn clear_recorded(&self) {
if let Some(inner_ref) = self.inner.inner_ref() {
inner_ref.data.values.clear();
}
}
#[must_use]
pub fn create_int(&self, name: impl Into<StringReference>, value: i64) -> IntProperty {
self.inner
.inner_ref()
.and_then(|inner_ref| {
inner_ref
.state
.try_lock()
.and_then(|mut state| {
state.create_int_metric(name.into(), value, inner_ref.block_index)
})
.map(|block_index| IntProperty::new(inner_ref.state.clone(), block_index))
.ok()
})
.unwrap_or_else(IntProperty::new_no_op)
}
pub fn record_int(&self, name: impl Into<StringReference>, value: i64) {
let property = self.create_int(name.into(), value);
self.record(property);
}
#[must_use]
pub fn create_uint(&self, name: impl Into<StringReference>, value: u64) -> UintProperty {
self.inner
.inner_ref()
.and_then(|inner_ref| {
inner_ref
.state
.try_lock()
.and_then(|mut state| {
state.create_uint_metric(name.into(), value, inner_ref.block_index)
})
.map(|block_index| UintProperty::new(inner_ref.state.clone(), block_index))
.ok()
})
.unwrap_or_else(UintProperty::new_no_op)
}
pub fn record_uint(&self, name: impl Into<StringReference>, value: u64) {
let property = self.create_uint(name.into(), value);
self.record(property);
}
#[must_use]
pub fn create_double(&self, name: impl Into<StringReference>, value: f64) -> DoubleProperty {
self.inner
.inner_ref()
.and_then(|inner_ref| {
inner_ref
.state
.try_lock()
.and_then(|mut state| {
state.create_double_metric(name.into(), value, inner_ref.block_index)
})
.map(|block_index| DoubleProperty::new(inner_ref.state.clone(), block_index))
.ok()
})
.unwrap_or_else(DoubleProperty::new_no_op)
}
pub fn record_double(&self, name: impl Into<StringReference>, value: f64) {
let property = self.create_double(name.into(), value);
self.record(property);
}
#[must_use]
pub fn create_string_array(
&self,
name: impl Into<StringReference>,
slots: usize,
) -> StringArrayProperty {
self.inner
.inner_ref()
.and_then(|inner_ref| {
inner_ref
.state
.try_lock()
.and_then(|mut state| {
state.create_string_array(name.into(), slots, inner_ref.block_index)
})
.map(|block_index| {
StringArrayProperty::new(inner_ref.state.clone(), block_index)
})
.ok()
})
.unwrap_or_else(StringArrayProperty::new_no_op)
}
#[must_use]
pub fn create_int_array(
&self,
name: impl Into<StringReference>,
slots: usize,
) -> IntArrayProperty {
self.create_int_array_internal(name.into(), slots, ArrayFormat::Default)
}
#[must_use]
pub(crate) fn create_int_array_internal(
&self,
name: impl Into<StringReference>,
slots: usize,
format: ArrayFormat,
) -> IntArrayProperty {
self.inner
.inner_ref()
.and_then(|inner_ref| {
inner_ref
.state
.try_lock()
.and_then(|mut state| {
state.create_int_array(name.into(), slots, format, inner_ref.block_index)
})
.map(|block_index| IntArrayProperty::new(inner_ref.state.clone(), block_index))
.ok()
})
.unwrap_or_else(IntArrayProperty::new_no_op)
}
#[must_use]
pub fn create_uint_array(
&self,
name: impl Into<StringReference>,
slots: usize,
) -> UintArrayProperty {
self.create_uint_array_internal(name.into(), slots, ArrayFormat::Default)
}
#[must_use]
pub(crate) fn create_uint_array_internal(
&self,
name: impl Into<StringReference>,
slots: usize,
format: ArrayFormat,
) -> UintArrayProperty {
self.inner
.inner_ref()
.and_then(|inner_ref| {
inner_ref
.state
.try_lock()
.and_then(|mut state| {
state.create_uint_array(name.into(), slots, format, inner_ref.block_index)
})
.map(|block_index| UintArrayProperty::new(inner_ref.state.clone(), block_index))
.ok()
})
.unwrap_or_else(UintArrayProperty::new_no_op)
}
#[must_use]
pub fn create_double_array(
&self,
name: impl Into<StringReference>,
slots: usize,
) -> DoubleArrayProperty {
self.create_double_array_internal(name.into(), slots, ArrayFormat::Default)
}
#[must_use]
pub(crate) fn create_double_array_internal(
&self,
name: impl Into<StringReference>,
slots: usize,
format: ArrayFormat,
) -> DoubleArrayProperty {
self.inner
.inner_ref()
.and_then(|inner_ref| {
inner_ref
.state
.try_lock()
.and_then(|mut state| {
state.create_double_array(name.into(), slots, format, inner_ref.block_index)
})
.map(|block_index| {
DoubleArrayProperty::new(inner_ref.state.clone(), block_index)
})
.ok()
})
.unwrap_or_else(DoubleArrayProperty::new_no_op)
}
#[must_use]
pub fn create_int_linear_histogram(
&self,
name: impl Into<StringReference>,
params: LinearHistogramParams<i64>,
) -> IntLinearHistogramProperty {
IntLinearHistogramProperty::new(name.into(), params, self)
}
#[must_use]
pub fn create_uint_linear_histogram(
&self,
name: impl Into<StringReference>,
params: LinearHistogramParams<u64>,
) -> UintLinearHistogramProperty {
UintLinearHistogramProperty::new(name.into(), params, self)
}
#[must_use]
pub fn create_double_linear_histogram(
&self,
name: impl Into<StringReference>,
params: LinearHistogramParams<f64>,
) -> DoubleLinearHistogramProperty {
DoubleLinearHistogramProperty::new(name.into(), params, self)
}
#[must_use]
pub fn create_int_exponential_histogram(
&self,
name: impl Into<StringReference>,
params: ExponentialHistogramParams<i64>,
) -> IntExponentialHistogramProperty {
IntExponentialHistogramProperty::new(name.into(), params, self)
}
#[must_use]
pub fn create_uint_exponential_histogram(
&self,
name: impl Into<StringReference>,
params: ExponentialHistogramParams<u64>,
) -> UintExponentialHistogramProperty {
UintExponentialHistogramProperty::new(name.into(), params, self)
}
#[must_use]
pub fn create_double_exponential_histogram(
&self,
name: impl Into<StringReference>,
params: ExponentialHistogramParams<f64>,
) -> DoubleExponentialHistogramProperty {
DoubleExponentialHistogramProperty::new(name.into(), params, self)
}
#[must_use]
pub fn create_lazy_child<F>(&self, name: impl Into<StringReference>, callback: F) -> LazyNode
where
F: Fn() -> BoxFuture<'static, Result<Inspector, anyhow::Error>> + Sync + Send + 'static,
{
self.inner
.inner_ref()
.and_then(|inner_ref| {
inner_ref
.state
.try_lock()
.and_then(|mut state| {
state.create_lazy_node(
name.into(),
inner_ref.block_index,
LinkNodeDisposition::Child,
callback,
)
})
.map(|block_index| LazyNode::new(inner_ref.state.clone(), block_index))
.ok()
})
.unwrap_or_else(LazyNode::new_no_op)
}
pub fn record_lazy_child<F>(&self, name: impl Into<StringReference>, callback: F)
where
F: Fn() -> BoxFuture<'static, Result<Inspector, anyhow::Error>> + Sync + Send + 'static,
{
let property = self.create_lazy_child(name.into(), callback);
self.record(property);
}
#[must_use]
pub fn create_lazy_values<F>(&self, name: impl Into<StringReference>, callback: F) -> LazyNode
where
F: Fn() -> BoxFuture<'static, Result<Inspector, anyhow::Error>> + Sync + Send + 'static,
{
self.inner
.inner_ref()
.and_then(|inner_ref| {
inner_ref
.state
.try_lock()
.and_then(|mut state| {
state.create_lazy_node(
name.into(),
inner_ref.block_index,
LinkNodeDisposition::Inline,
callback,
)
})
.map(|block_index| LazyNode::new(inner_ref.state.clone(), block_index))
.ok()
})
.unwrap_or_else(LazyNode::new_no_op)
}
pub fn record_lazy_values<F>(&self, name: impl Into<StringReference>, callback: F)
where
F: Fn() -> BoxFuture<'static, Result<Inspector, anyhow::Error>> + Sync + Send + 'static,
{
let property = self.create_lazy_values(name.into(), callback);
self.record(property);
}
#[must_use]
pub fn create_string(
&self,
name: impl Into<StringReference>,
value: impl AsRef<str>,
) -> StringProperty {
self.inner
.inner_ref()
.and_then(|inner_ref| {
inner_ref
.state
.try_lock()
.and_then(|mut state| {
state.create_property(
name.into(),
value.as_ref().as_bytes(),
PropertyFormat::String,
inner_ref.block_index,
)
})
.map(|block_index| StringProperty::new(inner_ref.state.clone(), block_index))
.ok()
})
.unwrap_or_else(StringProperty::new_no_op)
}
pub fn record_string(&self, name: impl Into<StringReference>, value: impl AsRef<str>) {
let property = self.create_string(name.into(), value);
self.record(property);
}
#[must_use]
pub fn create_bytes(
&self,
name: impl Into<StringReference>,
value: impl AsRef<[u8]>,
) -> BytesProperty {
self.inner
.inner_ref()
.and_then(|inner_ref| {
inner_ref
.state
.try_lock()
.and_then(|mut state| {
state.create_property(
name.into(),
value.as_ref(),
PropertyFormat::Bytes,
inner_ref.block_index,
)
})
.map(|block_index| BytesProperty::new(inner_ref.state.clone(), block_index))
.ok()
})
.unwrap_or_else(BytesProperty::new_no_op)
}
pub fn record_bytes(&self, name: impl Into<StringReference>, value: impl AsRef<[u8]>) {
let property = self.create_bytes(name.into(), value);
self.record(property);
}
#[must_use]
pub fn create_bool(&self, name: impl Into<StringReference>, value: bool) -> BoolProperty {
self.inner
.inner_ref()
.and_then(|inner_ref| {
inner_ref
.state
.try_lock()
.and_then(|mut state| {
state.create_bool(name.into(), value, inner_ref.block_index)
})
.map(|block_index| BoolProperty::new(inner_ref.state.clone(), block_index))
.ok()
})
.unwrap_or_else(BoolProperty::new_no_op)
}
pub fn record_bool(&self, name: impl Into<StringReference>, value: bool) {
let property = self.create_bool(name.into(), value);
self.record(property);
}
pub fn adopt<T: InspectTypeReparentable>(&self, child: &T) -> Result<(), Error> {
child.reparent(self)
}
pub fn forget(&self) {
if let Some(inner_ref) = self.inner.inner_ref() {
let _ = InnerNodeType::free(&inner_ref.state, &inner_ref.data, inner_ref.block_index);
}
}
pub(crate) fn new_root(state: State) -> Node {
Node::new(state, BlockIndex::ROOT)
}
}
#[derive(Default, Debug)]
pub(crate) struct InnerNodeType;
#[derive(Default, Debug)]
pub(crate) struct NodeData {
values: ValueList,
destroyed: AtomicBool,
}
impl InnerData for NodeData {
fn is_valid(&self) -> bool {
!self.destroyed.load(Ordering::SeqCst)
}
}
impl InnerType for InnerNodeType {
type Data = NodeData;
fn free(state: &State, data: &Self::Data, block_index: BlockIndex) -> Result<(), Error> {
if block_index == BlockIndex::ROOT {
return Ok(());
}
let mut state_lock = state.try_lock()?;
if data.destroyed.swap(true, Ordering::SeqCst) {
return Ok(());
}
state_lock.free_value(block_index).map_err(|err| Error::free("node", block_index, err))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reader;
use crate::writer::testing_utils::{get_state, GetBlockExt};
use crate::writer::ArrayProperty;
use diagnostics_assertions::{assert_data_tree, assert_json_diff};
use futures::FutureExt;
use inspect_format::BlockType;
#[fuchsia::test]
fn node() {
let default = Node::default();
default.record_int("a", 0);
let state = get_state(4096);
let root = Node::new_root(state);
let node = root.create_child("node");
node.get_block(|node_block| {
assert_eq!(node_block.block_type(), BlockType::NodeValue);
assert_eq!(node_block.child_count().unwrap(), 0);
});
{
let child = node.create_child("child");
child.get_block(|child_block| {
assert_eq!(child_block.block_type(), BlockType::NodeValue);
assert_eq!(child_block.child_count().unwrap(), 0);
});
node.get_block(|node_block| {
assert_eq!(node_block.child_count().unwrap(), 1);
});
}
node.get_block(|node_block| {
assert_eq!(node_block.child_count().unwrap(), 0);
});
}
#[fuchsia::test]
async fn lazy_child() {
let inspector = Inspector::default();
let _lazy = inspector.root().create_lazy_child("lazy-1", || {
async move {
let insp = Inspector::default();
insp.root().record_lazy_child("parent", || {
async move {
let insp2 = Inspector::default();
insp2.root().record_int("create-lazy-child", 0);
insp2.root().record_int("create-lazy-child-2", 2);
Ok(insp2)
}
.boxed()
});
Ok(insp)
}
.boxed()
});
inspector.root().record_lazy_child("lazy-2", || {
async move {
let insp = Inspector::default();
insp.root().record_bool("recorded-lazy-child", true);
Ok(insp)
}
.boxed()
});
inspector.root().record_lazy_values("lazy", || {
async move {
let insp = Inspector::default();
insp.root().record_bool("recorded-lazy-values", true);
Ok(insp)
}
.boxed()
});
let result = reader::read(&inspector).await.unwrap();
assert_data_tree!(result, root: {
"lazy-1": {
"parent": {
"create-lazy-child": 0i64,
"create-lazy-child-2": 2i64,
},
},
"lazy-2": {
"recorded-lazy-child": true,
},
"recorded-lazy-values": true,
});
}
#[fuchsia::test]
fn test_adoption() {
let insp = Inspector::default();
let root = insp.root();
let a = root.create_child("a");
let b = root.create_child("b");
let c = b.create_child("c");
assert_data_tree!(insp, root: {
a: {},
b: {
c: {},
},
});
a.adopt(&b).unwrap();
assert_data_tree!(insp, root: {
a: {
b: {
c: {},
},
},
});
assert!(c.adopt(&a).is_err());
assert!(c.adopt(&b).is_err());
assert!(b.adopt(&a).is_err());
assert!(a.adopt(root).is_err());
assert!(a.adopt(&a).is_err());
{
let d = root.create_int("d", 4);
assert_data_tree!(insp, root: {
a: {
b: {
c: {},
},
},
d: 4i64,
});
c.adopt(&d).unwrap();
assert_data_tree!(insp, root: {
a: {
b: {
c: {
d: 4i64,
},
},
},
});
}
assert_data_tree!(insp, root: {
a: {
b: {
c: {},
},
},
});
}
#[fuchsia::test]
fn node_no_op_clone_weak() {
let default = Node::default();
assert!(!default.is_valid());
let weak = default.clone_weak();
assert!(!weak.is_valid());
let _ = weak.create_child("child");
std::mem::drop(default);
let _ = weak.create_uint("age", 1337);
assert!(!weak.is_valid());
}
#[fuchsia::test]
fn node_clone_weak() {
let state = get_state(4096);
let root = Node::new_root(state);
let node = root.create_child("node");
let node_weak = node.clone_weak();
let node_weak_2 = node_weak.clone_weak(); node.get_block(|node_block| {
assert_eq!(node_block.block_type(), BlockType::NodeValue);
assert_eq!(node_block.child_count().unwrap(), 0);
});
let child_from_strong = node.create_child("child");
let child = node_weak.create_child("child_1");
let child_2 = node_weak_2.create_child("child_2");
std::mem::drop(node_weak_2);
node.get_block(|block| {
assert_eq!(block.child_count().unwrap(), 3);
});
std::mem::drop(child_from_strong);
node.get_block(|node_block| {
assert_eq!(node_block.child_count().unwrap(), 2);
});
std::mem::drop(child);
node.get_block(|node_block| {
assert_eq!(node_block.child_count().unwrap(), 1);
});
assert!(node_weak.is_valid());
assert!(child_2.is_valid());
std::mem::drop(node);
assert!(!node_weak.is_valid());
let _ = node_weak.create_child("orphan");
let _ = child_2.create_child("orphan");
}
#[fuchsia::test]
fn dummy_partialeq() {
let inspector = Inspector::default();
let root = inspector.root();
assert_eq!(root, &root.create_child("child1"));
assert_eq!(root.create_int("property1", 1), root.create_int("property2", 2));
assert_eq!(root.create_double("property1", 1.0), root.create_double("property2", 2.0));
assert_eq!(root.create_uint("property1", 1), root.create_uint("property2", 2));
assert_eq!(
root.create_string("property1", "value1"),
root.create_string("property2", "value2")
);
assert_eq!(
root.create_bytes("property1", b"value1"),
root.create_bytes("property2", b"value2")
);
}
#[fuchsia::test]
fn record() {
let inspector = Inspector::default();
let property = inspector.root().create_uint("a", 1);
inspector.root().record_uint("b", 2);
{
let child = inspector.root().create_child("child");
child.record(property);
child.record_double("c", 3.25);
assert_data_tree!(inspector, root: {
a: 1u64,
b: 2u64,
child: {
c: 3.25,
}
});
}
assert_data_tree!(inspector, root: {
b: 2u64,
});
inspector.root().clear_recorded();
assert_data_tree!(inspector, root: {});
}
#[fuchsia::test]
fn clear_recorded() {
let inspector = Inspector::default();
let one = inspector.root().create_child("one");
let two = inspector.root().create_child("two");
let one_recorded = one.create_child("one_recorded");
let two_recorded = two.create_child("two_recorded");
one.record(one_recorded);
two.record(two_recorded);
assert_json_diff!(inspector, root: {
one: {
one_recorded: {},
},
two: {
two_recorded: {},
},
});
two.clear_recorded();
assert_json_diff!(inspector, root: {
one: {
one_recorded: {},
},
two: {},
});
one.clear_recorded();
assert_json_diff!(inspector, root: {
one: {},
two: {},
});
}
#[fuchsia::test]
fn record_child() {
let inspector = Inspector::default();
inspector.root().record_child("test", |node| {
node.record_int("a", 1);
});
assert_data_tree!(inspector, root: {
test: {
a: 1i64,
}
})
}
#[fuchsia::test]
fn record_weak() {
let inspector = Inspector::default();
let main = inspector.root().create_child("main");
let main_weak = main.clone_weak();
let property = main_weak.create_uint("a", 1);
main_weak.record_uint("b", 2);
main.record_uint("c", 3);
{
let child = main_weak.create_child("child");
child.record(property);
child.record_double("c", 3.25);
assert_data_tree!(inspector, root: { main: {
a: 1u64,
b: 2u64,
c: 3u64,
child: {
c: 3.25,
}
}});
}
assert_data_tree!(inspector, root: { main: {
b: 2u64,
c: 3u64
}});
std::mem::drop(main);
main_weak.record_double("d", 1.0);
assert_data_tree!(inspector, root: { });
}
#[fuchsia::test]
fn string_arrays_on_record() {
let inspector = Inspector::default();
inspector.root().record_child("child", |node| {
node.record_int("my_int", 1i64);
let arr: crate::StringArrayProperty = node.create_string_array("my_string_array", 1);
arr.set(0, "test");
node.record(arr);
});
assert_data_tree!(inspector, root: {
child: {
my_int: 1i64,
my_string_array: vec!["test"]
}
});
}
#[fuchsia::test]
fn we_can_delete_a_node_explicitly_with_the_weak_clone() {
let insp = Inspector::default();
let a = insp.root().create_child("a");
let _property = a.create_int("foo", 1);
assert_data_tree!(insp, root: {
a: {
foo: 1i64,
}
});
let a_weak = a.clone_weak();
a_weak.forget();
assert!(a.inner.inner_ref().is_none());
assert_data_tree!(insp, root: {});
}
}
#[cfg(all(test, target_os = "fuchsia"))]
mod fuchsia_tests {
use super::*;
use crate::hierarchy::DiagnosticsHierarchy;
use crate::{reader, NumericProperty};
use diagnostics_assertions::assert_json_diff;
use std::sync::Arc;
use zx::{self as zx, AsHandleRef, Peered};
#[fuchsia::test]
async fn atomic_update_reader() {
let inspector = Inspector::default();
let vmo = Arc::new(inspector.duplicate_vmo().expect("duplicate vmo handle"));
let (p1, p2) = zx::EventPair::create();
macro_rules! notify_and_wait_reader {
() => {
p1.signal_peer(zx::Signals::NONE, zx::Signals::USER_0).unwrap();
p1.wait_handle(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE).unwrap();
p1.signal_handle(zx::Signals::USER_0, zx::Signals::NONE).unwrap();
};
}
macro_rules! wait_and_notify_writer {
($code:block) => {
p2.wait_handle(zx::Signals::USER_0, zx::MonotonicInstant::INFINITE).unwrap();
p2.signal_handle(zx::Signals::USER_0, zx::Signals::NONE).unwrap();
$code
p2.signal_peer(zx::Signals::NONE, zx::Signals::USER_0).unwrap();
}
}
let thread = std::thread::spawn(move || {
wait_and_notify_writer! {{
let hierarchy: DiagnosticsHierarchy<String> =
reader::PartialNodeHierarchy::try_from(vmo.as_ref()).unwrap().into();
assert_eq!(hierarchy, DiagnosticsHierarchy::new_root());
}};
wait_and_notify_writer! {{
assert!(reader::PartialNodeHierarchy::try_from(vmo.as_ref()).is_err());
}};
wait_and_notify_writer! {{
assert!(reader::PartialNodeHierarchy::try_from(vmo.as_ref()).is_err());
}};
wait_and_notify_writer! {{
assert!(reader::PartialNodeHierarchy::try_from(vmo.as_ref()).is_err());
}};
wait_and_notify_writer! {{
let hierarchy: DiagnosticsHierarchy<String> =
reader::PartialNodeHierarchy::try_from(vmo.as_ref()).unwrap().into();
assert_json_diff!(hierarchy, root: {
value: 2i64,
child: {
a: 1i64,
b: 2i64,
}
});
}};
});
let mut child = Node::default();
notify_and_wait_reader!();
let int_val = inspector.root().create_int("value", 1);
inspector
.root()
.atomic_update(|node| {
child = node.create_child("child");
notify_and_wait_reader!();
child.record_int("a", 1);
notify_and_wait_reader!();
child.record_int("b", 2);
notify_and_wait_reader!();
int_val.add(1);
Ok::<(), Error>(())
})
.expect("successful atomic update");
notify_and_wait_reader!();
let _ = thread.join();
child.record_int("c", 3);
assert_json_diff!(inspector, root: {
value: 2i64,
child: {
a: 1i64,
b: 2i64,
c: 3i64,
}
});
}
}