hwlocality/object/
mod.rs

1//! Objects within a hardware topology
2//!
3//! A [`Topology`] is first and foremost a tree of [`TopologyObject`] which
4//! represents resource sharing relationships in hardware: an object is
5//! considered the parent of all other objects that share the
6//! most direct/fastest/lowest-latency route to it. For example, on x86, an L3
7//! cache is the parent of a number of L2 caches, each the parent of one L1
8//! cache, which is in turn the parent of a CPU core that may or may not be
9//! shared by multiple hyperthreads (PUs in hwloc's vocabulary).
10//!
11//! This module defines the (very extensive) API through which one can query
12//! various properties of topology objects and jump from them to other elements
13//! of the surrounding topology.
14
15pub mod attributes;
16pub mod depth;
17pub mod distance;
18pub(crate) mod hierarchy;
19pub(crate) mod lists;
20pub mod search;
21pub mod types;
22
23use self::{
24    attributes::{DownstreamAttributes, ObjectAttributes, PCIDomain},
25    depth::{Depth, NormalDepth},
26    types::ObjectType,
27};
28#[cfg(doc)]
29use crate::topology::{builder::BuildFlags, support::DiscoverySupport, Topology};
30use crate::{
31    bitmap::BitmapRef,
32    cpu::cpuset::CpuSet,
33    ffi::{
34        self, int,
35        transparent::{AsNewtype, TransparentNewtype},
36    },
37    info::TextualInfo,
38    memory::nodeset::NodeSet,
39};
40#[cfg(feature = "hwloc-2_3_0")]
41use crate::{
42    errors::{self, HybridError, NulError},
43    ffi::string::LibcString,
44};
45use hwlocality_sys::{hwloc_obj, HWLOC_UNKNOWN_INDEX};
46#[allow(unused)]
47#[cfg(test)]
48use similar_asserts::assert_eq;
49use std::{
50    ffi::{c_char, CStr},
51    fmt::{self, Debug, Display},
52    iter::FusedIterator,
53    ops::Deref,
54    ptr,
55};
56
57/// Hardware topology object
58///
59/// Like `Topology`, this is a pretty big struct, so the documentation is
60/// sliced into smaller parts:
61///
62/// - [Basic identity](#basic-identity)
63/// - [Depth and ancestors](#depth-and-ancestors)
64/// - [Cousins and siblings](#cousins-and-siblings)
65/// - [Children](#children)
66/// - [CPU set](#cpu-set)
67/// - [NUMA node set](#numa-node-set)
68/// - [Key-value information](#key-value-information)
69///
70/// You cannot create an owned object of this type, it belongs to the topology.
71//
72// --- Implementation details ---
73//
74// Upstream docs:
75// - https://hwloc.readthedocs.io/en/stable/structhwloc__obj.html
76// - https://hwloc.readthedocs.io/en/stable/attributes.html
77//
78// See the matching accessor methods and hwloc documentation for more details on
79// field semantics, the struct member documentation will only be focused on
80// allowed interactions from methods.
81//
82// # Safety
83//
84// As a type invariant, all inner pointers are assumed to be safe to dereference
85// and devoid of mutable aliases if the TopologyObject is reachable at all.
86//
87// This is enforced through the following precautions:
88//
89// - No API exposes an owned TopologyObjects, only references to it bound by
90//   the source topology's lifetime are exposed.
91// - APIs for interacting with topologies and topology objects honor Rust's
92//   shared XOR mutable aliasing rules, with no internal mutability.
93//
94// Provided that objects do not link to other objects outside of the topology
95// they originate from, which is minimally sane expectation from hwloc, this
96// should be enough.
97//
98// The hwloc_obj has very complex consistency invariants that are not fully
99// documented by upstream. We assume the following:
100//
101// - If any pointer is non-null, its target can be assumed to be valid
102// - Anything that is not explicitly listed as okay to modify below should be
103//   considered unsafe to modify unless proven otherwise
104// - object_type is assumed to be in sync with attr
105// - It is okay to change attr inner data as long as no union is switched
106//   from one variant to another
107// - On hwloc <2.11, subtype may be replaced with another C string allocated by
108//   malloc(), which hwloc will automatically free() on topology destruction
109//   (source: documentation of hwloc_topology_insert_group_object() encourages
110//   it). However, this is only safe if hwloc is linked against the same libc as
111//   the application, so it should be made unsafe on Windows where linking
112//   against two different CRTs is both easy to do and hard to avoid. On hwloc
113//   2.11, the new hwloc_obj_set_subtype() avoids this problem, at the expense
114//   of requiring topology access.
115// - depth is in sync with parent
116// - logical_index is in sync with (next|prev)_cousin
117// - sibling_rank is in sync with (next|prev)_sibling
118// - arity is in sync with (children|(first|last)_child)
119// - symmetric_subtree is in sync with child pointers
120// - memory_arity is in sync with memory_first_child
121// - io_arity is in sync with io_first_child
122// - misc_arity is in sync with misc_first_child
123// - infos_count is in sync with infos
124// - userdata should not be touched as topology duplication aliases it
125// - gp_index is stable by API contract
126#[allow(clippy::non_send_fields_in_send_ty, missing_copy_implementations)]
127#[doc(alias = "hwloc_obj")]
128#[doc(alias = "hwloc_obj_t")]
129#[repr(transparent)]
130pub struct TopologyObject(hwloc_obj);
131
132/// # Basic identity
133impl TopologyObject {
134    /// Type of object
135    #[doc(alias = "hwloc_obj::type")]
136    pub fn object_type(&self) -> ObjectType {
137        // SAFETY: Object type is not user-editable so we are sure this value
138        //         comes from hwloc
139        unsafe { ObjectType::from_hwloc(self.0.ty) }
140    }
141
142    /// Subtype string to better describe the type field
143    ///
144    /// See <https://hwloc.readthedocs.io/en/stable/attributes.html#attributes_normal>
145    /// for a list of subtype strings that hwloc can emit.
146    #[doc(alias = "hwloc_obj::subtype")]
147    pub fn subtype(&self) -> Option<&CStr> {
148        // SAFETY: - Pointer validity is assumed as a type invariant
149        //         - Rust aliasing rules are enforced by deriving the reference
150        //           from &self, which itself is derived from &Topology
151        unsafe { ffi::deref_str(&self.0.subtype) }
152    }
153
154    /// Set the subtype string
155    ///
156    /// <div class="warning">
157    ///
158    /// To accomodate API changes in hwloc v2.11, this method had to be
159    /// deprecated and replaced with a new `subtype` optional parameter to the
160    /// `TopologyEditor::insert_group_object()` method.
161    ///
162    /// </div>
163    ///
164    /// This exposes [`TopologyObject::set_subtype_unchecked()`] as a safe
165    /// method on operating systems which aren't known to facilitate mixing and
166    /// matching libc versions between an application and its dependencies.
167    #[allow(clippy::missing_errors_doc, deprecated)]
168    #[cfg(all(feature = "hwloc-2_3_0", not(windows), not(tarpaulin_include)))]
169    #[deprecated = "Use the subtype parameter to TopologyEditor::insert_group_object()"]
170    pub fn set_subtype(&mut self, subtype: &str) -> Result<(), NulError> {
171        // SAFETY: Underlying OS is assumed not to ergonomically encourage
172        //         unsafe multi-libc linkage
173        unsafe { self.set_subtype_unchecked(subtype) }
174    }
175
176    /// Set the subtype string
177    ///
178    /// <div class="warning">
179    ///
180    /// To accomodate API changes in hwloc v2.11, this method had to be
181    /// deprecated and replaced with a new `subtype` optional parameter to the
182    /// `TopologyEditor::insert_group_object()` method.
183    ///
184    /// </div>
185    ///
186    /// This is something you'll often want to do when creating Group or Misc
187    /// objects in order to make them more descriptive.
188    ///
189    /// # Safety
190    ///
191    /// This method is only safe to call if you can guarantee that your
192    /// application links against the same libc/CRT as hwloc.
193    ///
194    /// While linking against a different libc is something that is either
195    /// outright UB or strongly advised against on every OS, actually following
196    /// this sanity rule is unfortunately very hard on Windows, where usage of
197    /// multiple CRTs and pre-built DLLs is the norm, and there is no easy way
198    /// to tell which CRT version your pre-built hwloc DLL links against to
199    /// adjust your application build's CRT choice accordingly.
200    ///
201    /// Which is why hwlocality tries hard to accomodate violations of the rule.
202    /// And thus the safe version of this method, which assumes that you do
203    /// follow the rule, is not available on Windows.
204    ///
205    /// # Errors
206    ///
207    /// - [`NulError`] if `subtype` contains NUL chars.
208    #[cfg(all(feature = "hwloc-2_3_0", not(tarpaulin_include)))]
209    #[deprecated = "Use the subtype parameter to TopologyEditor::insert_group_object()"]
210    pub unsafe fn set_subtype_unchecked(&mut self, subtype: &str) -> Result<(), NulError> {
211        // SAFETY: Per input precondition
212        self.0.subtype = unsafe { LibcString::new(subtype)?.into_raw() };
213        Ok(())
214    }
215
216    /// Object-specific name, if any
217    ///
218    /// Mostly used for identifying OS devices and Misc objects where a name
219    /// string is more useful than numerical indices.
220    #[doc(alias = "hwloc_obj::name")]
221    pub fn name(&self) -> Option<&CStr> {
222        // SAFETY: - Pointer validity is assumed as a type invariant
223        //         - Rust aliasing rules are enforced by deriving the reference
224        //           from &self, which itself is derived from &Topology
225        unsafe { ffi::deref_str(&self.0.name) }
226    }
227
228    /// Object type-specific attributes, if any
229    #[doc(alias = "hwloc_obj::attr")]
230    pub fn attributes(&self) -> Option<ObjectAttributes<'_>> {
231        // SAFETY: Per type invariant
232        unsafe { ObjectAttributes::new(self.object_type(), &self.0.attr) }
233    }
234
235    /// The OS-provided physical index number
236    ///
237    /// It is not guaranteed unique across the entire machine,
238    /// except for PUs and NUMA nodes.
239    ///
240    /// Not specified if unknown or irrelevant for this object.
241    #[doc(alias = "hwloc_obj::os_index")]
242    pub fn os_index(&self) -> Option<usize> {
243        (self.0.os_index != HWLOC_UNKNOWN_INDEX).then(|| int::expect_usize(self.0.os_index))
244    }
245
246    /// Global persistent index
247    ///
248    /// Generated by hwloc, unique across the topology (contrary to
249    /// [`os_index()`]) and persistent across topology changes (contrary to
250    /// [`logical_index()`]).
251    ///
252    /// All this means you can safely use this index as a cheap key representing
253    /// the object in a Set or a Map, as long as that Set or Map only refers to
254    /// [`TopologyObject`]s originating from a single [`Topology`].
255    ///
256    /// [`logical_index()`]: Self::logical_index()
257    /// [`os_index()`]: Self::os_index()
258    #[doc(alias = "hwloc_obj::gp_index")]
259    pub fn global_persistent_index(&self) -> TopologyObjectID {
260        self.0.gp_index
261    }
262}
263
264/// Global persistent [`TopologyObject`] ID
265///
266/// Generated by hwloc, unique across a given topology and persistent across
267/// topology changes. Basically, the only collisions you can expect are between
268/// objects from different topologies, which you normally shouldn't mix.
269pub type TopologyObjectID = u64;
270
271/// # Depth and ancestors
272//
273// --- Implementation details ---
274//
275// Includes functionality inspired by https://hwloc.readthedocs.io/en/stable/group__hwlocality__helper__ancestors.html
276impl TopologyObject {
277    /// Vertical index in the hierarchy
278    ///
279    /// For normal objects, this is the depth of the horizontal level that
280    /// contains this object and its cousins of the same type. If the topology
281    /// is symmetric, this is equal to the parent depth plus one, and also equal
282    /// to the number of parent/child links from the root object to here.
283    ///
284    /// For special objects (NUMA nodes, I/O and Misc) that are not in the main
285    /// tree, this is a special value that is unique to their type.
286    #[doc(alias = "hwloc_obj::depth")]
287    pub fn depth(&self) -> Depth {
288        // SAFETY: Object depth is not user-editable so we are sure this value
289        //         comes from hwloc
290        unsafe { Depth::from_hwloc(self.0.depth) }.expect("Got unexpected depth value")
291    }
292
293    /// Parent object
294    ///
295    /// Only `None` for the root `Machine` object.
296    #[doc(alias = "hwloc_obj::parent")]
297    pub fn parent(&self) -> Option<&Self> {
298        // SAFETY: - Pointer & target validity are assumed as a type invariant
299        //         - Rust aliasing rules are enforced by deriving the reference
300        //           from &self, which itself is derived from &Topology
301        unsafe { ffi::deref_ptr_mut(&self.0.parent).map(|raw| raw.as_newtype()) }
302    }
303
304    /// Chain of parent objects up to the topology root
305    pub fn ancestors(&self) -> impl FusedIterator<Item = &Self> + Clone {
306        Ancestors(self)
307    }
308
309    /// Search for an ancestor at a certain depth
310    ///
311    /// `depth` can be a [`Depth`], a [`NormalDepth`] or an [`usize`].
312    ///
313    /// Will return `None` if the requested depth is deeper than the depth of
314    /// the current object.
315    #[doc(alias = "hwloc_get_ancestor_obj_by_depth")]
316    pub fn ancestor_at_depth<DepthLike>(&self, depth: DepthLike) -> Option<&Self>
317    where
318        DepthLike: TryInto<Depth>,
319        <DepthLike as TryInto<Depth>>::Error: Debug,
320    {
321        /// Polymorphized version of this function (avoids generics code bloat)
322        fn polymorphized(self_: &TopologyObject, depth: Depth) -> Option<&TopologyObject> {
323            // Fast failure path when depth is comparable
324            let self_depth = self_.depth();
325            if let (Ok(self_depth), Ok(depth)) = (
326                NormalDepth::try_from(self_depth),
327                NormalDepth::try_from(depth),
328            ) {
329                if self_depth <= depth {
330                    return None;
331                }
332            }
333
334            // Otherwise, walk parents looking for the right depth
335            self_.ancestors().find(|ancestor| ancestor.depth() == depth)
336        }
337
338        // There cannot be any ancestor at a depth below the hwloc-supported max
339        let depth = depth.try_into().ok()?;
340        polymorphized(self, depth)
341    }
342
343    /// Search for the first ancestor with a certain type in ascending order
344    ///
345    /// If multiple matching ancestors exist (which can happen with [`Group`]
346    /// ancestors), the lowest ancestor is returned.
347    ///
348    /// Will return `None` if the requested type appears deeper than the
349    /// current object or doesn't appear in the topology.
350    ///
351    /// [`Group`]: ObjectType::Group
352    #[doc(alias = "hwloc_get_ancestor_obj_by_type")]
353    pub fn first_ancestor_with_type(&self, ty: ObjectType) -> Option<&Self> {
354        self.ancestors()
355            .find(|ancestor| ancestor.object_type() == ty)
356    }
357
358    /// Search for the first ancestor that is shared with another object
359    ///
360    /// The search will always succeed unless...
361    /// - One of `self` and `other` is the root [`Machine`](ObjectType::Machine)
362    ///   object, which has no ancestors.
363    /// - `self` and `other` do not belong to the same topology, and thus have
364    ///   no shared ancestor.
365    #[doc(alias = "hwloc_get_common_ancestor_obj")]
366    pub fn first_common_ancestor(&self, other: &Self) -> Option<&Self> {
367        // Handle degenerate case
368        if ptr::eq(self, other) {
369            return self.parent();
370        }
371
372        /// Collect ancestors with virtual depths on both sides
373        /// Returns the list of ancestors with virtual depths together with the
374        /// first ancestor with a normal depth, if any
375        fn collect_virtual_ancestors(
376            obj: &TopologyObject,
377        ) -> (Vec<&TopologyObject>, Option<&TopologyObject>) {
378            let mut ancestors = Vec::new();
379            let mut current = obj;
380            loop {
381                if let Some(parent) = current.parent() {
382                    if let Depth::Normal(_) = parent.depth() {
383                        return (ancestors, Some(parent));
384                    } else {
385                        ancestors.push(parent);
386                        current = parent;
387                    }
388                } else {
389                    return (ancestors, None);
390                }
391            }
392        }
393        let (virtual_ancestors_1, parent1) = collect_virtual_ancestors(self);
394        let (virtual_ancestors_2, parent2) = collect_virtual_ancestors(other);
395
396        // Make sure there is no common ancestor at some virtual depth (can't
397        // avoid O(N²) alg here as virtual depths cannot be compared and global
398        // persistent indices may be redundant across different topologies)
399        for ancestor1 in virtual_ancestors_1 {
400            for ancestor2 in &virtual_ancestors_2 {
401                if ptr::eq(ancestor1, *ancestor2) {
402                    return Some(ancestor1);
403                }
404            }
405        }
406
407        // Now that we have virtual depths taken care of, we can enter a fast
408        // path for parents with normal depths (if any)
409        let mut parent1 = parent1?;
410        let mut parent2 = parent2?;
411        loop {
412            // Walk up ancestors, try to reach the same depth.
413            // Only normal depths should be observed all the way through the
414            // ancestor chain, since the parent of a normal object is normal.
415            let normal_depth = |obj: &Self| {
416                NormalDepth::try_from(obj.depth()).expect("Should only observe normal depth here")
417            };
418            let depth2 = normal_depth(parent2);
419            while normal_depth(parent1) > depth2 {
420                parent1 = parent1.parent()?;
421            }
422            let depth1 = normal_depth(parent1);
423            while normal_depth(parent2) > depth1 {
424                parent2 = parent2.parent()?;
425            }
426
427            // If we reached the same parent, we're done
428            if ptr::eq(parent1, parent2) {
429                return Some(parent1);
430            }
431
432            // Otherwise, either parent2 jumped above parent1 (which can happen
433            // as hwloc topology may "skip" depths on hybrid plaforms like
434            // Adler Lake or in the presence of complicated allowed cpusets), or
435            // we reached cousin objects and must go up one level.
436            if parent1.depth() == parent2.depth() {
437                parent1 = parent1.parent()?;
438                parent2 = parent2.parent()?;
439            }
440        }
441    }
442
443    /// Truth that this object is in the subtree beginning with ancestor
444    /// object `subtree_root`
445    ///
446    /// This will return `false` if `self` and `subtree_root` do not belong to
447    /// the same topology.
448    #[doc(alias = "hwloc_obj_is_in_subtree")]
449    pub fn is_in_subtree(&self, subtree_root: &Self) -> bool {
450        // NOTE: Not reusing the cpuset-based optimization of hwloc as it is
451        //       invalid in the presence of objects that do not belong to the
452        //       same topology and there is no way to detect whether this is the
453        //       case or not without... walking the ancestors ;)
454        self.ancestors()
455            .any(|ancestor| ptr::eq(ancestor, subtree_root))
456    }
457
458    /// Get the first data (or unified) CPU cache shared between this object and
459    /// another object, if any.
460    ///
461    /// Will always return `None` if called on an I/O or Misc object that does
462    /// not contain CPUs.
463    #[doc(alias = "hwloc_get_shared_cache_covering_obj")]
464    pub fn first_shared_cache(&self) -> Option<&Self> {
465        let cpuset = self.cpuset()?;
466        self.ancestors()
467            .skip_while(|ancestor| ancestor.cpuset() == Some(cpuset))
468            .find(|ancestor| ancestor.object_type().is_cpu_data_cache())
469    }
470
471    /// Get the first non-I/O ancestor object
472    ///
473    /// Find the smallest non-I/O ancestor object. This object (normal or
474    /// memory) may then be used for binding because it has CPU and node sets
475    /// and because its locality is the same as this object.
476    ///
477    /// This operation will fail if and only if the object is the root of the
478    /// topology.
479    #[doc(alias = "hwloc_get_non_io_ancestor_obj")]
480    pub fn first_non_io_ancestor(&self) -> Option<&Self> {
481        self.ancestors().find(|obj| obj.cpuset().is_some())
482    }
483}
484
485/// Iterator over ancestors of a topology object
486#[derive(Copy, Clone, Debug)]
487struct Ancestors<'object>(&'object TopologyObject);
488//
489impl<'object> Iterator for Ancestors<'object> {
490    type Item = &'object TopologyObject;
491
492    fn next(&mut self) -> Option<Self::Item> {
493        self.0 = self.0.parent()?;
494        Some(self.0)
495    }
496
497    fn size_hint(&self) -> (usize, Option<usize>) {
498        let depth_res = usize::try_from(self.0.depth());
499        (depth_res.unwrap_or(0), depth_res.ok())
500    }
501}
502//
503impl FusedIterator for Ancestors<'_> {}
504
505/// # Cousins and siblings
506impl TopologyObject {
507    /// Horizontal index in the whole list of similar objects, hence guaranteed
508    /// unique across the entire machine
509    ///
510    /// Could be a "cousin rank" since it's the rank within the "cousin" list.
511    ///
512    /// Note that this index may change when restricting the topology
513    /// or when inserting a group.
514    #[doc(alias = "hwloc_obj::logical_index")]
515    pub fn logical_index(&self) -> usize {
516        int::expect_usize(self.0.logical_index)
517    }
518
519    /// Next object of same type and depth
520    #[doc(alias = "hwloc_obj::next_cousin")]
521    pub fn next_cousin(&self) -> Option<&Self> {
522        // SAFETY: - Pointer and target validity are assumed as a type invariant
523        //         - Rust aliasing rules are enforced by deriving the reference
524        //           from &self, which itself is derived from &Topology
525        unsafe { ffi::deref_ptr_mut(&self.0.next_cousin).map(|raw| raw.as_newtype()) }
526    }
527
528    /// Previous object of same type and depth
529    #[doc(alias = "hwloc_obj::prev_cousin")]
530    pub fn prev_cousin(&self) -> Option<&Self> {
531        // SAFETY: - Pointer and target validity are assumed as a type invariant
532        //         - Rust aliasing rules are enforced by deriving the reference
533        //           from &self, which itself is derived from &Topology
534        unsafe { ffi::deref_ptr_mut(&self.0.prev_cousin).map(|raw| raw.as_newtype()) }
535    }
536
537    /// Index in the parent's relevant child list for this object type
538    #[doc(alias = "hwloc_obj::sibling_rank")]
539    pub fn sibling_rank(&self) -> usize {
540        int::expect_usize(self.0.sibling_rank)
541    }
542
543    /// Next object below the same parent, in the same child list
544    #[doc(alias = "hwloc_obj::next_sibling")]
545    pub fn next_sibling(&self) -> Option<&Self> {
546        // SAFETY: - Pointer and target validity are assumed as a type invariant
547        //         - Rust aliasing rules are enforced by deriving the reference
548        //           from &self, which itself is derived from &Topology
549        unsafe { ffi::deref_ptr_mut(&self.0.next_sibling).map(|raw| raw.as_newtype()) }
550    }
551
552    /// Previous object below the same parent, in the same child list
553    #[doc(alias = "hwloc_obj::prev_sibling")]
554    pub fn prev_sibling(&self) -> Option<&Self> {
555        // SAFETY: - Pointer and target validity are assumed as a type invariant
556        //         - Rust aliasing rules are enforced by deriving the reference
557        //           from &self, which itself is derived from &Topology
558        unsafe { ffi::deref_ptr_mut(&self.0.prev_sibling).map(|raw| raw.as_newtype()) }
559    }
560}
561
562/// # Children
563impl TopologyObject {
564    /// Number of normal children (excluding Memory, Misc and I/O)
565    #[doc(alias = "hwloc_obj::arity")]
566    pub fn normal_arity(&self) -> usize {
567        int::expect_usize(self.0.arity)
568    }
569
570    /// Normal children of this object
571    #[doc(alias = "hwloc_obj::children")]
572    #[doc(alias = "hwloc_obj::first_child")]
573    #[doc(alias = "hwloc_obj::last_child")]
574    pub fn normal_children(
575        &self,
576    ) -> impl DoubleEndedIterator<Item = &Self> + Clone + ExactSizeIterator + FusedIterator {
577        if self.0.children.is_null() {
578            #[cfg(not(tarpaulin_include))]
579            assert_eq!(
580                self.normal_arity(),
581                0,
582                "Got null children pointer with nonzero arity"
583            );
584        }
585        (0..self.normal_arity()).map(move |offset| {
586            // SAFETY: Pointer is in bounds by construction
587            let child = unsafe { *self.0.children.add(offset) };
588            #[cfg(not(tarpaulin_include))]
589            assert!(!child.is_null(), "Got null child pointer");
590            // SAFETY: - We checked that the pointer isn't null
591            //         - Pointer & target validity assumed as a type invariant
592            //         - Rust aliasing rules are enforced by deriving the reference
593            //           from &self, which itself is derived from &Topology
594            unsafe { (&*child).as_newtype() }
595        })
596    }
597
598    // NOTE: Not exposing first_/last_child accessors for now as in the presence
599    //       of the normal_children iterator, they feel very redundant, and I
600    //       can't think of a usage situation where avoiding one pointer
601    //       indirection by exposing them would be worth the API inconsistency.
602    //       If you do, please submit an issue to the repository!
603
604    /// Truth that this object is symmetric, which means all normal children and
605    /// their children have identically shaped subtrees
606    ///
607    /// Memory, I/O and Misc children are ignored when evaluating this property,
608    /// and it is false for all of these object types.
609    ///
610    /// If this is true of the root object, then the topology may be [exported
611    /// as a synthetic string](Topology::export_synthetic()).
612    #[doc(alias = "hwloc_obj::symmetric_subtree")]
613    pub fn is_symmetric_subtree(&self) -> bool {
614        self.0.symmetric_subtree != 0
615    }
616
617    /// Get the child covering at least the given cpuset `set`
618    ///
619    /// `set` can be a `&'_ CpuSet` or a `BitmapRef<'_, CpuSet>`.
620    ///
621    /// This function will always return `None` if the given set is empty or
622    /// this topology object doesn't have a cpuset (I/O or Misc objects), as
623    /// no object is considered to cover the empty cpuset.
624    #[doc(alias = "hwloc_get_child_covering_cpuset")]
625    pub fn normal_child_covering_cpuset(&self, set: impl Deref<Target = CpuSet>) -> Option<&Self> {
626        let set: &CpuSet = &set;
627        self.normal_children()
628            .find(|child| child.covers_cpuset(set))
629    }
630
631    /// Number of memory children
632    #[doc(alias = "hwloc_obj::memory_arity")]
633    pub fn memory_arity(&self) -> usize {
634        int::expect_usize(self.0.memory_arity)
635    }
636
637    /// Memory children of this object
638    ///
639    /// NUMA nodes and Memory-side caches are listed here instead of in the
640    /// [`TopologyObject::normal_children()`] list. See also
641    /// [`ObjectType::is_memory()`].
642    ///
643    /// A memory hierarchy starts from a normal CPU-side object (e.g.
644    /// [`Package`]) and ends with NUMA nodes as leaves. There might exist some
645    /// memory-side caches between them in the middle of the memory subtree.
646    ///
647    /// [`Package`]: ObjectType::Package
648    #[doc(alias = "hwloc_obj::memory_first_child")]
649    pub fn memory_children(&self) -> impl ExactSizeIterator<Item = &Self> + Clone + FusedIterator {
650        // SAFETY: - memory_first_child is a valid first-child of this object
651        //         - memory_arity is assumed in sync as a type invariant
652        unsafe { self.singly_linked_children(self.0.memory_first_child, self.memory_arity()) }
653    }
654
655    /// Total memory (in bytes) in NUMA nodes below this object
656    ///
657    /// Requires [`DiscoverySupport::numa_memory()`].
658    #[doc(alias = "hwloc_obj::total_memory")]
659    pub fn total_memory(&self) -> u64 {
660        self.0.total_memory
661    }
662
663    /// Number of I/O children
664    #[doc(alias = "hwloc_obj::io_arity")]
665    pub fn io_arity(&self) -> usize {
666        int::expect_usize(self.0.io_arity)
667    }
668
669    /// I/O children of this object
670    ///
671    /// Bridges, PCI and OS devices are listed here instead of in the
672    /// [`TopologyObject::normal_children()`] list. See also
673    /// [`ObjectType::is_io()`].
674    #[doc(alias = "hwloc_obj::io_first_child")]
675    pub fn io_children(&self) -> impl ExactSizeIterator<Item = &Self> + Clone + FusedIterator {
676        // SAFETY: - io_first_child is a valid first-child of this object
677        //         - io_arity is assumed in sync as a type invariant
678        unsafe { self.singly_linked_children(self.0.io_first_child, self.io_arity()) }
679    }
680
681    /// Truth that this is a bridge covering the specified PCI bus
682    #[doc(alias = "hwloc_bridge_covers_pcibus")]
683    pub fn is_bridge_covering_pci_bus(&self, domain: PCIDomain, bus_id: u8) -> bool {
684        let Some(ObjectAttributes::Bridge(bridge)) = self.attributes() else {
685            return false;
686        };
687        let Some(DownstreamAttributes::PCI(pci)) = bridge.downstream_attributes() else {
688            // Cannot happen on current hwloc, but may happen someday
689            return false;
690        };
691        pci.domain() == domain && pci.secondary_bus() <= bus_id && pci.subordinate_bus() >= bus_id
692    }
693
694    /// Number of Misc children
695    #[doc(alias = "hwloc_obj::misc_arity")]
696    pub fn misc_arity(&self) -> usize {
697        int::expect_usize(self.0.misc_arity)
698    }
699
700    /// Misc children of this object
701    ///
702    /// Misc objects are listed here instead of in the
703    /// [`TopologyObject::normal_children()`] list.
704    #[doc(alias = "hwloc_obj::misc_first_child")]
705    pub fn misc_children(&self) -> impl ExactSizeIterator<Item = &Self> + Clone + FusedIterator {
706        // SAFETY: - misc_first_child is a valid first-child of this object
707        //         - misc_arity is assumed in sync as a type invariant
708        unsafe { self.singly_linked_children(self.0.misc_first_child, self.misc_arity()) }
709    }
710
711    /// Full list of children (normal, then memory, then I/O, then Misc)
712    #[doc(alias = "hwloc_get_next_child")]
713    pub fn all_children(&self) -> impl FusedIterator<Item = &Self> + Clone {
714        self.normal_children()
715            .chain(self.memory_children())
716            .chain(self.io_children())
717            .chain(self.misc_children())
718    }
719
720    /// Iterator over singly linked lists of child objects with known arity
721    ///
722    /// # Safety
723    ///
724    /// - `first` must be one of the `xyz_first_child` pointers of this object
725    /// - `arity` must be the matching `xyz_arity` child count variable
726    unsafe fn singly_linked_children(
727        &self,
728        first: *mut hwloc_obj,
729        arity: usize,
730    ) -> impl ExactSizeIterator<Item = &Self> + Clone + FusedIterator {
731        let mut current = first;
732        (0..arity).map(move |_| {
733            #[cfg(not(tarpaulin_include))]
734            assert!(!current.is_null(), "Got null child before expected arity");
735            // SAFETY: - We checked that the pointer isn't null
736            //         - Pointer & target validity assumed as a type invariant
737            //         - Rust aliasing rules are enforced by deriving the reference
738            //           from &self, which itself is derived from &Topology
739            let result: &Self = unsafe { (&*current).as_newtype() };
740            current = result.0.next_sibling;
741            result
742        })
743    }
744}
745
746/// # CPU set
747impl TopologyObject {
748    /// CPUs covered by this object
749    ///
750    /// This is the set of CPUs for which there are PU objects in the
751    /// topology under this object, i.e. which are known to be physically
752    /// contained in this object and known how (the children path between this
753    /// object and the PU objects).
754    ///
755    /// If the [`BuildFlags::INCLUDE_DISALLOWED`] topology building
756    /// configuration flag is set, some of these CPUs may be online but not
757    /// allowed for binding, see [`Topology::allowed_cpuset()`].
758    ///
759    /// All objects have CPU and node sets except Misc and I/O objects, so if
760    /// you know this object to be a normal or Memory object, you can safely
761    /// unwrap this Option.
762    ///
763    /// # Example
764    ///
765    /// ```rust
766    /// # use hwlocality::Topology;
767    /// # let topology = Topology::test_instance();
768    /// println!(
769    ///     "Visible CPUs attached to the root object: {:?}",
770    ///     topology.root_object().cpuset()
771    /// );
772    /// # Ok::<_, eyre::Report>(())
773    /// ```
774    #[doc(alias = "hwloc_obj::cpuset")]
775    pub fn cpuset(&self) -> Option<BitmapRef<'_, CpuSet>> {
776        // SAFETY: Per type invariant
777        unsafe { CpuSet::borrow_from_raw_mut(self.0.cpuset) }
778    }
779
780    /// Truth that this object is inside of the given cpuset `set`
781    ///
782    /// `set` can be a `&'_ CpuSet` or a `BitmapRef<'_, CpuSet>`.
783    ///
784    /// Objects are considered to be inside `set` if they have a non-empty
785    /// cpuset which verifies `set.includes(object_cpuset)`.
786    pub fn is_inside_cpuset(&self, set: impl Deref<Target = CpuSet>) -> bool {
787        let Some(object_cpuset) = self.cpuset() else {
788            return false;
789        };
790        set.includes(object_cpuset) && !object_cpuset.is_empty()
791    }
792
793    /// Truth that this object covers the given cpuset `set`
794    ///
795    /// `set` can be a `&'_ CpuSet` or a `BitmapRef<'_, CpuSet>`.
796    ///
797    /// Objects are considered to cover `set` if it is non-empty and the object
798    /// has a cpuset which verifies `object_cpuset.includes(set)`.
799    pub fn covers_cpuset(&self, set: impl Deref<Target = CpuSet>) -> bool {
800        let Some(object_cpuset) = self.cpuset() else {
801            return false;
802        };
803        let set: &CpuSet = &set;
804        object_cpuset.includes(set) && !set.is_empty()
805    }
806
807    /// The complete CPU set of this object
808    ///
809    /// To the CPUs listed by [`cpuset()`], this adds CPUs for which topology
810    /// information is unknown or incomplete, some offline CPUs, and CPUs that
811    /// are ignored when the [`BuildFlags::INCLUDE_DISALLOWED`] topology
812    /// building configuration flag is not set.
813    ///
814    /// Thus no corresponding PU object may be found in the topology, because
815    /// the precise position is undefined. It is however known that it would be
816    /// somewhere under this object.
817    ///
818    /// # Example
819    ///
820    /// ```rust
821    /// # use hwlocality::Topology;
822    /// # let topology = Topology::test_instance();
823    /// println!(
824    ///     "Overall CPUs attached to the root object: {:?}",
825    ///     topology.root_object().complete_cpuset()
826    /// );
827    /// # Ok::<_, eyre::Report>(())
828    /// ```
829    ///
830    /// [`cpuset()`]: Self::cpuset()
831    #[doc(alias = "hwloc_obj::complete_cpuset")]
832    pub fn complete_cpuset(&self) -> Option<BitmapRef<'_, CpuSet>> {
833        // SAFETY: Per type invariant
834        unsafe { CpuSet::borrow_from_raw_mut(self.0.complete_cpuset) }
835    }
836}
837
838/// # NUMA node set
839impl TopologyObject {
840    /// NUMA nodes covered by this object or containing this object.
841    ///
842    /// This is the set of NUMA nodes for which there are NUMA node objects in
843    /// the topology under or above this object, i.e. which are known to be
844    /// physically contained in this object or containing it and known how
845    /// (the children path between this object and the NUMA node objects). In
846    /// the end, these nodes are those that are close to the current object.
847    ///
848    #[cfg_attr(
849        feature = "hwloc-2_3_0",
850        doc = "With hwloc 2.3+, [`Topology::local_numa_nodes()`] may be used to"
851    )]
852    #[cfg_attr(feature = "hwloc-2_3_0", doc = "list those NUMA nodes more precisely.")]
853    ///
854    /// If the [`BuildFlags::INCLUDE_DISALLOWED`] topology building
855    /// configuration flag is set, some of these nodes may not be allowed for
856    /// allocation, see [`Topology::allowed_nodeset()`].
857    ///
858    /// If there are no NUMA nodes in the machine, all the memory is close to
859    /// this object, so the nodeset is full.
860    ///
861    /// All objects have CPU and node sets except Misc and I/O objects, so if
862    /// you know this object to be a normal or Memory object, you can safely
863    /// unwrap this Option.
864    ///
865    /// # Example
866    ///
867    /// ```rust
868    /// # use hwlocality::Topology;
869    /// # let topology = Topology::test_instance();
870    /// println!(
871    ///     "Visible NUMA nodes attached to the root object: {:?}",
872    ///     topology.root_object().nodeset()
873    /// );
874    /// # Ok::<_, eyre::Report>(())
875    /// ```
876    #[doc(alias = "hwloc_obj::nodeset")]
877    pub fn nodeset(&self) -> Option<BitmapRef<'_, NodeSet>> {
878        // SAFETY: Per type invariant
879        unsafe { NodeSet::borrow_from_raw_mut(self.0.nodeset) }
880    }
881
882    /// The complete NUMA node set of this object
883    ///
884    /// To the nodes listed by [`nodeset()`], this adds nodes for which topology
885    /// information is unknown or incomplete, some offline nodes, and nodes
886    /// that are ignored when the [`BuildFlags::INCLUDE_DISALLOWED`] topology
887    /// building configuration flag is not set.
888    ///
889    /// Thus no corresponding [`NUMANode`] object may be found in the topology,
890    /// because the precise position is undefined. It is however known that it
891    /// would be somewhere under this object.
892    ///
893    /// If there are no NUMA nodes in the machine, all the memory is close to
894    /// this object, so the complete nodeset is full.
895    ///
896    /// # Example
897    ///
898    /// ```rust
899    /// # use hwlocality::Topology;
900    /// # let topology = Topology::test_instance();
901    /// println!(
902    ///     "Overall NUMA nodes attached to the root object: {:?}",
903    ///     topology.root_object().complete_nodeset()
904    /// );
905    /// # Ok::<_, eyre::Report>(())
906    /// ```
907    ///
908    /// [`nodeset()`]: Self::nodeset()
909    /// [`NUMANode`]: ObjectType::NUMANode
910    #[doc(alias = "hwloc_obj::complete_nodeset")]
911    pub fn complete_nodeset(&self) -> Option<BitmapRef<'_, NodeSet>> {
912        // SAFETY: Per type invariant
913        unsafe { NodeSet::borrow_from_raw_mut(self.0.complete_nodeset) }
914    }
915}
916
917/// # Key-value information
918impl TopologyObject {
919    /// Complete list of (key, value) textual info pairs
920    ///
921    /// hwloc defines [a number of standard object info attribute names with
922    /// associated semantics](https://hwloc.readthedocs.io/en/stable/attributes.html#attributes_info).
923    ///
924    /// Beware that hwloc allows multiple informations with the same key to
925    /// exist, although sane users should not leverage this possibility.
926    #[doc(alias = "hwloc_obj::infos")]
927    pub fn infos(&self) -> &[TextualInfo] {
928        // Handle null infos pointer
929        if self.0.infos.is_null() {
930            #[cfg(not(tarpaulin_include))]
931            assert_eq!(
932                self.0.infos_count, 0,
933                "Got null infos pointer with nonzero info count"
934            );
935            return &[];
936        }
937
938        // Handle unsupported size slice edge case
939        let infos_len = int::expect_usize(self.0.infos_count);
940        #[allow(clippy::missing_docs_in_private_items)]
941        type Element = TextualInfo;
942        int::assert_slice_len::<Element>(infos_len);
943
944        // Build the output slice
945        // SAFETY: - infos and count are assumed in sync per type invariant
946        //         - infos are assumed to be valid per type invariant
947        //         - AsNewtype is trusted to be implemented correctly
948        //         - infos_len was checked to be slice-compatible above
949        unsafe { std::slice::from_raw_parts::<Element>(self.0.infos.as_newtype(), infos_len) }
950    }
951
952    /// Search the given key name in object infos and return the corresponding value
953    ///
954    /// Beware that hwloc allows multiple informations with the same key to
955    /// exist, although no sane programs should leverage this possibility.
956    /// If multiple keys match the given name, only the first one is returned.
957    ///
958    /// Calling this operation multiple times will result in duplicate work. If
959    /// you need to do this sort of search many times, consider collecting
960    /// `infos()` into a `HashMap` or `BTreeMap` for increased lookup efficiency.
961    #[doc(alias = "hwloc_obj_get_info_by_name")]
962    pub fn info(&self, key: &str) -> Option<&CStr> {
963        self.infos().iter().find_map(|info| {
964            let Ok(info_name) = info.name().to_str() else {
965                // hwloc does not currently emit invalid Unicode, but it might
966                // someday if a malicious C program tampered with the topology
967                return None;
968            };
969            (info_name == key).then_some(info.value())
970        })
971    }
972
973    /// Add the given info name and value pair to the given object
974    ///
975    /// The info is appended to the existing info array even if another key with
976    /// the same name already exists.
977    ///
978    /// This function may be used to enforce object colors in the lstopo
979    /// graphical output by using "lstopoStyle" as a name and "Background=#rrggbb"
980    /// as a value. See `CUSTOM COLORS` in the `lstopo(1)` manpage for details.
981    ///
982    /// If value contains some non-printable characters, they will be dropped
983    /// when exporting to XML.
984    ///
985    /// # Errors
986    ///
987    /// - [`NulError`] if `name` or `value` contains NUL chars.
988    #[cfg(feature = "hwloc-2_3_0")]
989    #[doc(alias = "hwloc_obj_add_info")]
990    pub fn add_info(&mut self, name: &str, value: &str) -> Result<(), HybridError<NulError>> {
991        let name = LibcString::new(name)?;
992        let value = LibcString::new(value)?;
993        // SAFETY: - An &mut TopologyObject may only be obtained from &mut Topology
994        //         - Object validity trusted by type invariant
995        //         - hwloc is trusted not to make object invalid
996        //         - LibcStrings are valid C strings by construction, and not
997        //           used after the end of their lifetimes
998        errors::call_hwloc_zero_or_minus1("hwloc_obj_add_info", || unsafe {
999            hwlocality_sys::hwloc_obj_add_info(&mut self.0, name.borrow(), value.borrow())
1000        })
1001        .map_err(HybridError::Hwloc)
1002    }
1003}
1004
1005// # Internal utilities
1006impl TopologyObject {
1007    /// Display this object's type and attributes
1008    fn display(&self, f: &mut fmt::Formatter<'_>, verbose: bool) -> fmt::Result {
1009        // SAFETY: - These are indeed snprintf-like APIs
1010        //         - Object validity trusted by type invariant
1011        //         - verbose translates nicely into a C-style boolean
1012        //         - separators are valid C strings
1013        let (type_chars, attr_chars) = unsafe {
1014            let type_chars = ffi::call_snprintf(|buf, len| {
1015                hwlocality_sys::hwloc_obj_type_snprintf(buf, len, &self.0, verbose.into())
1016            });
1017
1018            let separator = if f.alternate() {
1019                b",\n  \0".as_ptr()
1020            } else {
1021                b", \0".as_ptr()
1022            }
1023            .cast::<c_char>();
1024            let attr_chars = ffi::call_snprintf(|buf, len| {
1025                hwlocality_sys::hwloc_obj_attr_snprintf(
1026                    buf,
1027                    len,
1028                    &self.0,
1029                    separator,
1030                    verbose.into(),
1031                )
1032            });
1033            (type_chars, attr_chars)
1034        };
1035
1036        let cpuset_str = self
1037            .cpuset()
1038            .map_or_else(String::new, |cpuset| format!(" with {cpuset}"));
1039
1040        // SAFETY: - Output of call_snprintf should be valid C strings
1041        //         - We're not touching type_chars and attr_chars while type_str
1042        //           and attr_str are live.
1043        unsafe {
1044            let type_str = CStr::from_ptr(type_chars.as_ptr()).to_string_lossy();
1045            let attr_str = CStr::from_ptr(attr_chars.as_ptr()).to_string_lossy();
1046            let type_and_cpuset = format!("{type_str}{cpuset_str}");
1047            if attr_str.is_empty() {
1048                f.pad(&type_and_cpuset)
1049            } else if f.alternate() {
1050                let s = format!("{type_and_cpuset} (\n  {attr_str}\n)");
1051                f.pad(&s)
1052            } else {
1053                let s = format!("{type_and_cpuset} ({attr_str})");
1054                f.pad(&s)
1055            }
1056        }
1057    }
1058
1059    /// Delete all cpusets and nodesets from a non-inserted `Group` object
1060    ///
1061    /// This is needed as part of a dirty topology editing workaround that will
1062    /// hopefully not be needed anymore after hwloc v2.10.
1063    ///
1064    /// # (Absence of) Panics
1065    ///
1066    /// This method is called inside of destructors, it should never panic.
1067    ///
1068    /// # Safety
1069    ///
1070    /// `self_` must designate a valid `Group` object that has been allocated
1071    /// with `hwloc_topology_alloc_group_object()` but not yet inserted into a
1072    /// topology with `hwloc_topology_insert_group_object()`.
1073    #[cfg(all(feature = "hwloc-2_3_0", not(feature = "hwloc-2_10_0")))]
1074    pub(crate) unsafe fn delete_all_sets(self_: ptr::NonNull<Self>) {
1075        let self_ = self_.as_ptr();
1076        debug_assert_eq!(
1077            // SAFETY: self_ is valid per input precondition
1078            unsafe { (*self_).0.ty },
1079            hwlocality_sys::HWLOC_OBJ_GROUP,
1080            "this method should only be called on Group objects"
1081        );
1082        // SAFETY: This is safe per the input precondition that `self_` is a
1083        //         valid `TopologyObject` (which includes valid bitmap
1084        //         pointers), and it's not part of a `Topology` yet so we
1085        //         assume complete ownership of it delete its cpu/node-sets
1086        //         without worrying about unintended consequences.
1087        unsafe {
1088            for set_ptr in [
1089                ptr::addr_of_mut!((*self_).0.cpuset),
1090                ptr::addr_of_mut!((*self_).0.nodeset),
1091                ptr::addr_of_mut!((*self_).0.complete_cpuset),
1092                ptr::addr_of_mut!((*self_).0.complete_nodeset),
1093            ] {
1094                let set = set_ptr.read();
1095                if !set.is_null() {
1096                    hwlocality_sys::hwloc_bitmap_free(set);
1097                    set_ptr.write(ptr::null_mut())
1098                }
1099            }
1100        }
1101    }
1102}
1103
1104impl Debug for TopologyObject {
1105    /// Verbose display of the object's type and attributes
1106    ///
1107    /// See the [`Display`] implementation if you want a more concise display.
1108    ///
1109    /// # Example
1110    ///
1111    /// ```rust
1112    /// # use hwlocality::Topology;
1113    /// # let topology = Topology::test_instance();
1114    /// println!("Root object: {:#?}", topology.root_object());
1115    /// # Ok::<_, eyre::Report>(())
1116    /// ```
1117    #[doc(alias = "hwloc_obj_attr_snprintf")]
1118    #[doc(alias = "hwloc_obj_type_snprintf")]
1119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1120        self.display(f, true)
1121    }
1122}
1123
1124impl Display for TopologyObject {
1125    #[allow(clippy::doc_markdown)]
1126    /// Display of the type and attributes that is more concise than [`Debug`]
1127    ///
1128    /// - Shorter type names are used, e.g. "L1Cache" becomes "L1"
1129    /// - Only the major object attributes are printed
1130    ///
1131    /// # Example
1132    ///
1133    /// ```rust
1134    /// # use hwlocality::Topology;
1135    /// # let topology = Topology::test_instance();
1136    /// println!("Root object: {}", topology.root_object());
1137    /// # Ok::<_, eyre::Report>(())
1138    /// ```
1139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1140        self.display(f, false)
1141    }
1142}
1143
1144// SAFETY: No internal mutability
1145unsafe impl Send for TopologyObject {}
1146
1147// SAFETY: No internal mutability
1148unsafe impl Sync for TopologyObject {}
1149
1150// SAFETY: TopologyObject is a repr(transparent) newtype of hwloc_obj
1151unsafe impl TransparentNewtype for TopologyObject {
1152    type Inner = hwloc_obj;
1153}
1154
1155#[allow(clippy::cognitive_complexity)]
1156#[cfg(test)]
1157pub(crate) mod tests {
1158    use super::hierarchy::tests::{any_hwloc_depth, any_normal_depth, any_usize_depth};
1159    use super::*;
1160    use crate::{
1161        strategies::{any_object, any_string, set_with_reference, test_object},
1162        topology::Topology,
1163    };
1164    use proptest::prelude::*;
1165    use similar_asserts::assert_eq;
1166    use std::{collections::HashMap, ffi::CString, ops::RangeInclusive};
1167
1168    /// Run [`check_any_object()`] on every topology object
1169    #[test]
1170    fn check_all_objects() -> Result<(), TestCaseError> {
1171        let topology = Topology::test_instance();
1172        for obj in topology.objects() {
1173            check_any_object(obj)?;
1174        }
1175        Ok(())
1176    }
1177
1178    /// Stuff that should be true of any object we examine
1179    fn check_any_object(obj: &TopologyObject) -> Result<(), TestCaseError> {
1180        check_sets(obj)?;
1181        check_parent(obj)?;
1182        check_first_shared_cache(obj)?;
1183        check_cousins_and_siblings(obj)?;
1184        check_children(obj)?;
1185        check_infos(obj)?;
1186        check_displays(obj)?;
1187        Ok(())
1188    }
1189
1190    /// Check that an object's cpusets and nodesets have the expected properties
1191    fn check_sets(obj: &TopologyObject) -> Result<(), TestCaseError> {
1192        let has_sets = obj.object_type().has_sets();
1193
1194        prop_assert_eq!(obj.cpuset().is_some(), has_sets);
1195        prop_assert_eq!(obj.complete_cpuset().is_some(), has_sets);
1196        if let (Some(complete), Some(normal)) = (obj.complete_cpuset(), obj.cpuset()) {
1197            prop_assert!(complete.includes(normal));
1198            prop_assert!(obj.is_inside_cpuset(complete));
1199            prop_assert!(obj.covers_cpuset(normal));
1200        }
1201
1202        prop_assert_eq!(obj.nodeset().is_some(), has_sets);
1203        prop_assert_eq!(obj.complete_nodeset().is_some(), has_sets);
1204        if let (Some(complete), Some(normal)) = (obj.complete_nodeset(), obj.nodeset()) {
1205            prop_assert!(complete.includes(normal));
1206        }
1207
1208        Ok(())
1209    }
1210
1211    /// Check that an object's parent has the expected properties
1212    fn check_parent(obj: &TopologyObject) -> Result<(), TestCaseError> {
1213        let Some(parent) = obj.parent() else {
1214            prop_assert_eq!(obj.object_type(), ObjectType::Machine);
1215            return Ok(());
1216        };
1217
1218        let first_ancestor = obj.ancestors().next().unwrap();
1219        prop_assert!(ptr::eq(parent, first_ancestor));
1220
1221        if let (Depth::Normal(parent_depth), Depth::Normal(obj_depth)) =
1222            (parent.depth(), obj.depth())
1223        {
1224            prop_assert!(parent_depth < obj_depth);
1225        }
1226
1227        if obj.object_type().has_sets() {
1228            prop_assert!(parent.object_type().has_sets());
1229            prop_assert!(parent.cpuset().unwrap().includes(obj.cpuset().unwrap()));
1230            prop_assert!(parent.nodeset().unwrap().includes(obj.nodeset().unwrap()));
1231            prop_assert!(parent
1232                .complete_cpuset()
1233                .unwrap()
1234                .includes(obj.complete_cpuset().unwrap()));
1235            prop_assert!(parent
1236                .complete_nodeset()
1237                .unwrap()
1238                .includes(obj.complete_nodeset().unwrap()));
1239        }
1240
1241        Ok(())
1242    }
1243
1244    /// Check that [`TopologyObject::first_shared_cache()`] works as expected
1245    fn check_first_shared_cache(obj: &TopologyObject) -> Result<(), TestCaseError> {
1246        // Call the function to begin with
1247        let result = obj.first_shared_cache();
1248
1249        // Should not yield result on objects without cpusets
1250        if obj.cpuset().is_none() {
1251            prop_assert!(result.is_none());
1252            return Ok(());
1253        }
1254
1255        // Otherwise, should yield first cache parent that has multiple normal
1256        // children below it
1257        let expected = obj
1258            .ancestors()
1259            .skip_while(|ancestor| ancestor.normal_arity() == 1)
1260            .find(|ancestor| ancestor.object_type().is_cpu_data_cache());
1261        if let (Some(result), Some(expected)) = (result, expected) {
1262            prop_assert!(ptr::eq(result, expected));
1263        } else {
1264            prop_assert!(result.is_none() && expected.is_none());
1265        }
1266        Ok(())
1267    }
1268
1269    /// Check that an object's cousin and siblings have the expected properties
1270    fn check_cousins_and_siblings(obj: &TopologyObject) -> Result<(), TestCaseError> {
1271        let siblings_len = if let Some(parent) = obj.parent() {
1272            let ty = obj.object_type();
1273            if ty.is_normal() {
1274                parent.normal_arity()
1275            } else if ty.is_memory() {
1276                parent.memory_arity()
1277            } else if ty.is_io() {
1278                parent.io_arity()
1279            } else {
1280                prop_assert_eq!(ty, ObjectType::Misc);
1281                parent.misc_arity()
1282            }
1283        } else {
1284            1
1285        };
1286        let topology = Topology::test_instance();
1287        let cousins_len = topology.num_objects_at_depth(obj.depth());
1288
1289        if let Some(prev_cousin) = obj.prev_cousin() {
1290            check_cousin(obj, prev_cousin)?;
1291            prop_assert_eq!(prev_cousin.logical_index(), obj.logical_index() - 1);
1292            prop_assert!(ptr::eq(prev_cousin.next_cousin().unwrap(), obj));
1293        } else {
1294            prop_assert_eq!(obj.logical_index(), 0);
1295        }
1296        if let Some(next_cousin) = obj.next_cousin() {
1297            check_cousin(obj, next_cousin)?;
1298            prop_assert_eq!(next_cousin.logical_index(), obj.logical_index() + 1);
1299            prop_assert!(ptr::eq(next_cousin.prev_cousin().unwrap(), obj));
1300        } else {
1301            prop_assert_eq!(obj.logical_index(), cousins_len - 1);
1302        }
1303
1304        if let Some(prev_sibling) = obj.prev_sibling() {
1305            check_sibling(obj, prev_sibling)?;
1306            prop_assert_eq!(prev_sibling.sibling_rank(), obj.sibling_rank() - 1);
1307            prop_assert!(ptr::eq(prev_sibling.next_sibling().unwrap(), obj));
1308        } else {
1309            prop_assert_eq!(obj.sibling_rank(), 0);
1310        }
1311        if let Some(next_sibling) = obj.next_sibling() {
1312            check_sibling(obj, next_sibling)?;
1313            prop_assert_eq!(next_sibling.sibling_rank(), obj.sibling_rank() + 1);
1314            prop_assert!(ptr::eq(next_sibling.prev_sibling().unwrap(), obj));
1315        } else {
1316            prop_assert_eq!(obj.sibling_rank(), siblings_len - 1);
1317        }
1318
1319        Ok(())
1320    }
1321
1322    /// Check that an object's cousin has the expected properties
1323    fn check_cousin(obj: &TopologyObject, cousin: &TopologyObject) -> Result<(), TestCaseError> {
1324        prop_assert_eq!(cousin.object_type(), obj.object_type());
1325        prop_assert_eq!(cousin.depth(), obj.depth());
1326        if obj.object_type().has_sets() {
1327            prop_assert!(!obj.cpuset().unwrap().intersects(cousin.cpuset().unwrap()));
1328            prop_assert!(!obj
1329                .complete_cpuset()
1330                .unwrap()
1331                .intersects(cousin.complete_cpuset().unwrap()));
1332        }
1333        Ok(())
1334    }
1335
1336    /// Check that an object's sibling has the expected properties
1337    ///
1338    /// This does not re-check the properties checked by `check_cousin()`, since
1339    /// the sibling of some object must be the cousin of another object.
1340    fn check_sibling(obj: &TopologyObject, sibling: &TopologyObject) -> Result<(), TestCaseError> {
1341        prop_assert_eq!(sibling.0.parent, obj.0.parent);
1342        Ok(())
1343    }
1344
1345    /// Check that an object's children has the expected properties
1346    fn check_children(obj: &TopologyObject) -> Result<(), TestCaseError> {
1347        prop_assert_eq!(obj.normal_arity(), obj.normal_children().count());
1348        prop_assert_eq!(obj.memory_arity(), obj.memory_children().count());
1349        prop_assert_eq!(obj.io_arity(), obj.io_children().count());
1350        prop_assert_eq!(obj.misc_arity(), obj.misc_children().count());
1351        prop_assert_eq!(
1352            obj.all_children().count(),
1353            obj.normal_arity() + obj.memory_arity() + obj.io_arity() + obj.misc_arity()
1354        );
1355
1356        // NOTE: Most parent-child relations are checked when checking the
1357        //       parent, since that's agnostic to the kind of child we deal with
1358        for (idx, normal_child) in obj.normal_children().enumerate() {
1359            prop_assert_eq!(normal_child.sibling_rank(), idx);
1360            prop_assert!(normal_child.object_type().is_normal());
1361        }
1362        for (idx, memory_child) in obj.memory_children().enumerate() {
1363            prop_assert_eq!(memory_child.sibling_rank(), idx);
1364            prop_assert!(memory_child.object_type().is_memory());
1365        }
1366        for (idx, io_child) in obj.io_children().enumerate() {
1367            prop_assert_eq!(io_child.sibling_rank(), idx);
1368            prop_assert!(io_child.object_type().is_io());
1369        }
1370        for (idx, misc_child) in obj.misc_children().enumerate() {
1371            prop_assert_eq!(misc_child.sibling_rank(), idx);
1372            prop_assert_eq!(misc_child.object_type(), ObjectType::Misc);
1373        }
1374
1375        Ok(())
1376    }
1377
1378    /// Check that an object's info metadata matches expectations
1379    fn check_infos(obj: &TopologyObject) -> Result<(), TestCaseError> {
1380        for info in obj.infos() {
1381            if let Ok(name) = info.name().to_str() {
1382                prop_assert_eq!(
1383                    obj.info(name),
1384                    obj.infos()
1385                        .iter()
1386                        .find(|other_info| other_info.name() == info.name())
1387                        .map(TextualInfo::value)
1388                );
1389            }
1390        }
1391        // NOTE: Looking up invalid info names is tested elsewhere
1392        Ok(())
1393    }
1394
1395    /// Check that an object's displays match expectations
1396    fn check_displays(obj: &TopologyObject) -> Result<(), TestCaseError> {
1397        // Render all supported display flavors
1398        let display = format!("{obj}");
1399        let display_alternate = format!("{obj:#}");
1400        let debug = format!("{obj:?}");
1401        let debug_alternate = format!("{obj:#?}");
1402
1403        // Non-alternate displays should fit in a single line
1404        for non_alternate in [&display, &debug] {
1405            prop_assert!(!non_alternate.contains('\n'));
1406        }
1407
1408        // Debug output should be longer than or identical to Display output
1409        prop_assert!(debug.len() >= display.len());
1410        prop_assert!(debug_alternate.len() >= display_alternate.len());
1411
1412        // Alternate displays should be longer than or identical to the norm
1413        prop_assert!(debug_alternate.len() >= debug.len());
1414        prop_assert!(display_alternate.len() >= display.len());
1415        Ok(())
1416    }
1417
1418    /// Check that [`TopologyObject::is_symmetric_subtree()`] is correct
1419    #[test]
1420    fn is_symmetric_subtree() {
1421        // Iterate over topology objects from children to parent: by the time we
1422        // reach a parent, we know that we can trust the is_symmetric_subtree of
1423        // its direct (and transitive) normal children.
1424        let topology = Topology::test_instance();
1425        for depth in NormalDepth::iter_range(NormalDepth::MIN, topology.depth()).rev() {
1426            'objs: for obj in topology.objects_at_depth(depth) {
1427                // An object is a symmetric subtree if it has no children...
1428                let Some(first_child) = obj.normal_children().next() else {
1429                    assert!(obj.is_symmetric_subtree());
1430                    continue 'objs;
1431                };
1432
1433                // ...or if its children all have the following properties:
1434                let should_be_symmetric = obj.normal_children().all(|child| {
1435                    // - They are symmetric subtree themselves
1436                    child.is_symmetric_subtree()
1437
1438                    // - All of their topologically meaningful properties are
1439                    //   identical (and thus equal to that of first child)
1440                    && child.object_type() == first_child.object_type()
1441                    && child.subtype() == first_child.subtype()
1442                    && child.attributes() == first_child.attributes()
1443                    && child.depth() == first_child.depth()
1444                    && child.normal_arity() == first_child.normal_arity()
1445                });
1446                assert_eq!(obj.is_symmetric_subtree(), should_be_symmetric);
1447            }
1448        }
1449
1450        // Only normal objects can be symmetric
1451        assert!(topology
1452            .virtual_objects()
1453            .all(|obj| !obj.is_symmetric_subtree()));
1454    }
1455
1456    /// Check that [`TopologyObject::total_memory()`] is correct
1457    #[test]
1458    fn total_memory() {
1459        // We'll compute the expected total amount of memory below each NUMA
1460        // node through a bottom-up tree reduction from NUMA nodes up
1461        let topology = Topology::test_instance();
1462        let mut expected_total_memory = HashMap::new();
1463        let mut curr_objects = HashMap::new();
1464        let mut next_objects = HashMap::new();
1465
1466        // Seed tree reduction by counting local memory inside of each NUMA node
1467        for numa in topology.objects_with_type(ObjectType::NUMANode) {
1468            let Some(ObjectAttributes::NUMANode(attrs)) = numa.attributes() else {
1469                unreachable!()
1470            };
1471            let gp_index = numa.global_persistent_index();
1472            let local_memory = attrs.local_memory().map_or(0, u64::from);
1473            assert!(expected_total_memory
1474                .insert(gp_index, local_memory)
1475                .is_none());
1476            assert!(curr_objects.insert(gp_index, numa).is_none());
1477        }
1478
1479        // Compute expected total_memory value through tree reduction
1480        while !curr_objects.is_empty() {
1481            for (gp_index, obj) in curr_objects.drain() {
1482                let obj_memory = expected_total_memory[&gp_index];
1483                if let Some(parent) = obj.parent() {
1484                    let parent_gp_index = parent.global_persistent_index();
1485                    *expected_total_memory.entry(parent_gp_index).or_default() += obj_memory;
1486                    next_objects.insert(parent_gp_index, parent);
1487                }
1488            }
1489            std::mem::swap(&mut curr_objects, &mut next_objects);
1490        }
1491
1492        // At this point we should have built an accurate map of how much memory
1493        // lies in NUMA nodes below each object, use it to check every object.
1494        for obj in topology.objects() {
1495            assert_eq!(
1496                obj.total_memory(),
1497                expected_total_memory
1498                    .remove(&obj.global_persistent_index())
1499                    .unwrap_or(0)
1500            );
1501        }
1502    }
1503
1504    // --- Test operations with a depth parameter ---
1505
1506    /// Test [`TopologyObject::ancestor_at_depth()`] for a certain
1507    /// [`TopologyObject`] and at a certain desired parent depth
1508    fn check_ancestor_at_depth<DepthLike>(
1509        obj: &TopologyObject,
1510        depth: DepthLike,
1511    ) -> Result<(), TestCaseError>
1512    where
1513        DepthLike: TryInto<Depth> + Copy + Debug + Eq,
1514        Depth: PartialEq<DepthLike>,
1515        <DepthLike as TryInto<Depth>>::Error: Debug,
1516    {
1517        let actual = obj.ancestor_at_depth(depth);
1518        let expected = obj.ancestors().find(|obj| obj.depth() == depth);
1519        if let (Some(actual), Some(expected)) = (actual, expected) {
1520            prop_assert!(ptr::eq(actual, expected));
1521        } else {
1522            prop_assert!(actual.is_none() && expected.is_none());
1523        }
1524        Ok(())
1525    }
1526
1527    proptest! {
1528        // Probe ancestors by depth at valid and invalid depths
1529        #[test]
1530        fn ancestor_at_hwloc_depth(obj in test_object(),
1531                                   depth in any_hwloc_depth()) {
1532            check_ancestor_at_depth(obj, depth)?;
1533        }
1534        //
1535        #[test]
1536        fn ancestor_at_normal_depth(obj in test_object(),
1537                                    depth in any_normal_depth()) {
1538            check_ancestor_at_depth(obj, depth)?;
1539        }
1540        //
1541        #[test]
1542        fn ancestor_at_usize_depth(obj in test_object(),
1543                                   depth in any_usize_depth()) {
1544            check_ancestor_at_depth(obj, depth)?;
1545        }
1546    }
1547
1548    // --- Querying stuff by cpuset/nodeset ---
1549
1550    /// Pick an object and a related cpuset
1551    pub(crate) fn object_and_related_cpuset(
1552    ) -> impl Strategy<Value = (&'static TopologyObject, CpuSet)> {
1553        // Separate objects with and without cpusets
1554        let topology = Topology::test_instance();
1555        let mut with_cpuset = Vec::new();
1556        let mut without_cpuset = Vec::new();
1557        for obj in topology.objects() {
1558            if obj.object_type().has_sets() {
1559                with_cpuset.push(obj);
1560            } else {
1561                without_cpuset.push(obj);
1562            }
1563        }
1564
1565        // For objects with cpusets, the reference cpuset is their cpuset
1566        fn with_reference(
1567            objects: Vec<&'static TopologyObject>,
1568            ref_cpuset: impl Fn(&TopologyObject) -> BitmapRef<'_, CpuSet>,
1569        ) -> Option<impl Strategy<Value = (&'static TopologyObject, CpuSet)>> {
1570            (!objects.is_empty()).then(move || {
1571                prop::sample::select(objects)
1572                    .prop_flat_map(move |obj| (Just(obj), set_with_reference(ref_cpuset(obj))))
1573            })
1574        }
1575        let with_cpuset = with_reference(with_cpuset, |obj| obj.cpuset().unwrap());
1576
1577        // For objects without cpusets, the reference is the topology cpuset
1578        let without_cpuset = with_reference(without_cpuset, |_obj| topology.cpuset());
1579
1580        // We pick from either list with equal probability
1581        match (with_cpuset, without_cpuset) {
1582            (Some(with), Some(without)) => prop_oneof![with, without].boxed(),
1583            (Some(with), None) => with.boxed(),
1584            (None, Some(without)) => without.boxed(),
1585            (None, None) => unreachable!(),
1586        }
1587    }
1588
1589    proptest! {
1590        /// Test [`TopologyObject::normal_child_covering_cpuset()`]
1591        #[test]
1592        fn normal_child_covering_cpuset((obj, set) in object_and_related_cpuset()) {
1593            if let Some(result) = obj.normal_child_covering_cpuset(&set) {
1594                prop_assert!(result.covers_cpuset(&set));
1595            } else {
1596                prop_assert!(obj.normal_children().all(|obj| !obj.covers_cpuset(&set)));
1597            }
1598        }
1599
1600        /// Test [`TopologyObject::is_inside_cpuset()`]
1601        #[test]
1602        fn is_inside_cpuset((obj, set) in object_and_related_cpuset()) {
1603            let result = obj.is_inside_cpuset(&set);
1604            let Some(obj_set) = obj.cpuset() else {
1605                prop_assert!(!result);
1606                return Ok(());
1607            };
1608            if obj_set.is_empty() {
1609                prop_assert!(!result);
1610                return Ok(());
1611            }
1612            prop_assert_eq!(result, set.includes(obj_set));
1613        }
1614
1615        /// Test [`TopologyObject::covers_cpuset()`]
1616        #[test]
1617        fn covers_cpuset((obj, set) in object_and_related_cpuset()) {
1618            let result = obj.covers_cpuset(&set);
1619            let Some(obj_set) = obj.cpuset() else {
1620                prop_assert!(!result);
1621                return Ok(());
1622            };
1623            if set.is_empty() {
1624                prop_assert!(!result);
1625                return Ok(());
1626            }
1627            prop_assert_eq!(result, obj_set.includes(&set));
1628        }
1629    }
1630
1631    // --- Truth that an object is a bridge covering a certain PCI bus ---
1632
1633    /// Generate queries that have a reasonable chance of returning `true`
1634    fn bridge_coverage() -> impl Strategy<Value = (&'static TopologyObject, PCIDomain, u8)> {
1635        #[derive(Clone, Debug)]
1636        struct BridgeCoverage {
1637            bridge: &'static TopologyObject,
1638            domain: PCIDomain,
1639            bus_id_range: RangeInclusive<u8>,
1640        }
1641        let topology = Topology::test_instance();
1642        let bridge_coverages = topology
1643            .objects_with_type(ObjectType::Bridge)
1644            .filter_map(|bridge| {
1645                let Some(ObjectAttributes::Bridge(attrs)) = bridge.attributes() else {
1646                    unreachable!()
1647                };
1648                let Some(DownstreamAttributes::PCI(pci)) = attrs.downstream_attributes() else {
1649                    return None;
1650                };
1651                Some(BridgeCoverage {
1652                    bridge,
1653                    domain: pci.domain(),
1654                    bus_id_range: pci.secondary_bus()..=pci.subordinate_bus(),
1655                })
1656            })
1657            .collect::<Vec<_>>();
1658        if bridge_coverages.is_empty() {
1659            (any_object(), any::<PCIDomain>(), any::<u8>()).boxed()
1660        } else {
1661            prop::sample::select(bridge_coverages)
1662                .prop_flat_map(|bridge_coverage| {
1663                    let obj = prop_oneof![
1664                        3 => Just(bridge_coverage.bridge),
1665                        2 => any_object()
1666                    ];
1667                    let domain = prop_oneof![
1668                        3 => Just(bridge_coverage.domain),
1669                        2 => any::<PCIDomain>()
1670                    ];
1671                    let bus_id = prop_oneof![
1672                        3 => bridge_coverage.bus_id_range,
1673                        2 => any::<u8>()
1674                    ];
1675                    (obj, domain, bus_id)
1676                })
1677                .boxed()
1678        }
1679    }
1680
1681    proptest! {
1682        /// Test [`TopologyObject::is_bridge_covering_pci_bus()`]
1683        #[test]
1684        fn is_bridge_covering_pci_bus((obj, domain, bus_id) in bridge_coverage()) {
1685            let result = obj.is_bridge_covering_pci_bus(domain, bus_id);
1686            let Some(ObjectAttributes::Bridge(attrs)) = obj.attributes() else {
1687                prop_assert!(!result);
1688                return Ok(());
1689            };
1690            let Some(DownstreamAttributes::PCI(pci)) = attrs.downstream_attributes() else {
1691                prop_assert!(!result);
1692                return Ok(());
1693            };
1694            prop_assert_eq!(
1695                result,
1696                domain == pci.domain() && bus_id >= pci.secondary_bus() && bus_id <= pci.subordinate_bus()
1697            );
1698        }
1699    }
1700
1701    // --- Looking up textual object info by random string ---
1702
1703    proptest! {
1704        /// Test [`TopologyObject::info()`] against a random key
1705        ///
1706        /// Correct keys are checked in another test.
1707        #[test]
1708        fn info(obj in any_object(), key in any_string()) {
1709            let result = obj.info(&key);
1710            let Ok(ckey) = CString::new(key.clone()) else {
1711                assert_eq!(result, None);
1712                return Ok(());
1713            };
1714            assert_eq!(
1715                obj.info(&key),
1716                obj.infos().iter().find_map(|info|{
1717                    (info.name() == ckey.as_c_str()).then_some(info.value())
1718                })
1719            );
1720        }
1721    }
1722
1723    // --- Properties of pairs of objects ---
1724
1725    proptest! {
1726        /// Test for [`TopologyObject::first_common_ancestor()`]
1727        #[test]
1728        fn first_common_ancestor(obj in test_object(), other in any_object()) {
1729            // Check result
1730            let result = obj.first_common_ancestor(other);
1731
1732            // The topology root has no ancestor
1733            if obj.object_type() == ObjectType::Machine || other.object_type() == ObjectType::Machine
1734            {
1735                prop_assert!(result.is_none());
1736                return Ok(());
1737            }
1738
1739            // Objects from two different topologies have no common ancestor
1740            let topology = Topology::test_instance();
1741            if !topology.contains(other) {
1742                prop_assert!(result.is_none());
1743                return Ok(());
1744            }
1745
1746            // Otherwise, there should be a common ancestor
1747            let common_ancestor = result.unwrap();
1748
1749            // Check if it is indeed the first common ancestor
1750            fn prev_ancestor_candidate<'obj>(
1751                obj: &'obj TopologyObject,
1752                common_ancestor: &TopologyObject,
1753            ) -> Option<&'obj TopologyObject> {
1754                obj.ancestors().take_while(|&ancestor| !ptr::eq(ancestor, common_ancestor)).last()
1755            }
1756            let obj_ancestor = prev_ancestor_candidate(obj, common_ancestor);
1757            let other_ancestor = prev_ancestor_candidate(other, common_ancestor);
1758            if let (Some(obj_ancestor), Some(other_ancestor)) = (obj_ancestor, other_ancestor) {
1759                prop_assert!(!ptr::eq(obj_ancestor, other_ancestor));
1760            }
1761        }
1762    }
1763
1764    // --- Object editing ---
1765
1766    #[cfg(feature = "hwloc-2_3_0")]
1767    mod editing {
1768        use super::*;
1769        use std::panic::UnwindSafe;
1770
1771        // Check that a certain object editing method works
1772        fn test_object_editing<R>(check: impl FnOnce(&mut TopologyObject) -> R + UnwindSafe) -> R {
1773            let mut topology = Topology::test_instance().clone();
1774            topology.edit(|editor| {
1775                let misc = editor
1776                    .insert_misc_object("This is a modifiable test object trololol", |topology| {
1777                        topology.root_object()
1778                    })
1779                    .unwrap();
1780                check(misc)
1781            })
1782        }
1783
1784        proptest! {
1785            // Try to add an info (key, value) pair to an object
1786            #[test]
1787            fn add_info(name in any_string(), value in any_string()) {
1788                test_object_editing(|obj| {
1789                    // Try to add a (key, value) pair
1790                    let res = obj.add_info(&name, &value);
1791
1792                    // Handle inner NULs
1793                    if name.contains('\0') || value.contains('\0') {
1794                        prop_assert_eq!(res, Err(NulError.into()));
1795                        return Ok(());
1796                    }
1797
1798                    // Assume success otherwise
1799                    res.unwrap();
1800                    prop_assert_eq!(obj.info(&name).unwrap().to_str().unwrap(), value);
1801                    Ok(())
1802                })?;
1803            }
1804        }
1805    }
1806}