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}