hyperactor_mesh/
lib.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! This crate provides hyperactor's mesh abstractions.
10
11#![feature(assert_matches)]
12#![feature(exit_status_error)]
13#![feature(impl_trait_in_bindings)]
14#![feature(get_disjoint_mut_helpers)]
15#![feature(exact_size_is_empty)]
16#![feature(async_fn_track_caller)]
17// EnumAsInner generates code that triggers a false positive
18// unused_assignments lint on struct variant fields. #[allow] on the
19// enum itself doesn't propagate into derive-macro-generated code, so
20// the suppression must be at module scope.
21#![allow(unused_assignments)]
22
23pub mod actor_mesh;
24pub mod alloc;
25mod assign;
26pub mod bootstrap;
27pub mod casting;
28pub mod comm;
29pub mod config;
30pub mod connect;
31pub mod global_context;
32pub mod host_mesh;
33pub mod introspect;
34pub mod logging;
35pub mod mesh;
36pub mod mesh_admin;
37pub mod mesh_controller;
38pub mod mesh_selection;
39mod metrics;
40pub mod namespace;
41pub mod proc_agent;
42pub mod proc_launcher;
43pub mod proc_mesh;
44pub mod reference;
45pub mod resource;
46pub mod shared_cell;
47pub mod shortuuid;
48pub mod supervision;
49#[cfg(target_os = "linux")]
50mod systemd;
51pub mod test_utils;
52pub mod testactor;
53pub mod testing;
54mod testresource;
55pub mod transport;
56pub mod value_mesh;
57
58use std::io;
59use std::str::FromStr;
60
61pub use actor_mesh::ActorMesh;
62pub use actor_mesh::ActorMeshRef;
63pub use bootstrap::Bootstrap;
64pub use bootstrap::bootstrap;
65pub use bootstrap::bootstrap_or_die;
66pub use casting::CastError;
67pub use comm::CommActor;
68pub use dashmap;
69use enum_as_inner::EnumAsInner;
70pub use global_context::GlobalClientActor;
71pub use global_context::GlobalContext;
72pub use global_context::context;
73pub use global_context::this_host;
74pub use global_context::this_proc;
75pub use host_mesh::HostMeshRef;
76use hyperactor::host::HostError;
77use hyperactor::mailbox::MailboxSenderError;
78use hyperactor::reference as hyperactor_reference;
79pub use hyperactor_mesh_macros::sel;
80pub use mesh::Mesh;
81// Re-exported for internal test binaries that don't have ndslice as a direct dependency
82pub use ndslice::extent;
83use ndslice::view;
84pub use proc_mesh::ProcMesh;
85pub use proc_mesh::ProcMeshRef;
86use serde::Deserialize;
87use serde::Serialize;
88use typeuri::Named;
89pub use value_mesh::ValueMesh;
90
91use crate::host_mesh::HostAgent;
92use crate::host_mesh::HostMeshRefParseError;
93use crate::host_mesh::host_agent::ProcState;
94use crate::resource::RankedValues;
95use crate::resource::Status;
96use crate::shortuuid::ShortUuid;
97use crate::supervision::MeshFailure;
98
99/// A mesh of per-rank lifecycle statuses.
100///
101/// `StatusMesh` is `ValueMesh<Status>` and supports dense or
102/// compressed encodings. Updates are applied via sparse overlays with
103/// **last-writer-wins** semantics (see
104/// [`ValueMesh::merge_from_overlay`]). The mesh's `Region` defines
105/// the rank space; all updates must match that region.
106pub type StatusMesh = ValueMesh<Status>;
107
108/// A sparse set of `(Range<usize>, Status)` updates for a
109/// [`StatusMesh`].
110///
111/// `StatusOverlay` carries **normalized** runs (sorted,
112/// non-overlapping, and coalesced). Applying an overlay to a
113/// `StatusMesh` uses **right-wins** semantics on overlap and
114/// preserves first-appearance order in the compressed table.
115/// Construct via `ValueOverlay::try_from_runs` after normalizing.
116pub type StatusOverlay = value_mesh::ValueOverlay<Status>;
117
118/// Errors that occur during mesh operations.
119#[derive(Debug, EnumAsInner, thiserror::Error)]
120pub enum Error {
121    #[error("invalid mesh ref: expected {expected} ranks, but contains {actual} ranks")]
122    InvalidRankCardinality { expected: usize, actual: usize },
123
124    #[error(transparent)]
125    NameParseError(#[from] NameParseError),
126
127    #[error(transparent)]
128    HostMeshRefParseError(#[from] HostMeshRefParseError),
129
130    #[error(transparent)]
131    AllocatorError(#[from] Box<crate::alloc::AllocatorError>),
132
133    #[error(transparent)]
134    ChannelError(#[from] Box<hyperactor::channel::ChannelError>),
135
136    #[error(transparent)]
137    MailboxError(#[from] Box<hyperactor::mailbox::MailboxError>),
138
139    #[error(transparent)]
140    CodecError(#[from] CodecError),
141
142    #[error("error during mesh configuration: {0}")]
143    ConfigurationError(anyhow::Error),
144
145    // This is a temporary error to ensure we don't create unroutable
146    // meshes.
147    #[error("configuration error: mesh is unroutable")]
148    UnroutableMesh(),
149
150    #[error("error while calling actor {0}: {1}")]
151    CallError(hyperactor_reference::ActorId, anyhow::Error),
152
153    #[error("actor not registered for type {0}")]
154    ActorTypeNotRegistered(String),
155
156    // TODO: this should be a valuemesh of statuses
157    #[error("error while spawning actor {0}: {1}")]
158    GspawnError(Name, String),
159
160    #[error("error while sending message to actor {0}: {1}")]
161    SendingError(hyperactor_reference::ActorId, Box<MailboxSenderError>),
162
163    #[error("error while casting message to {0}: {1}")]
164    CastingError(Name, anyhow::Error),
165
166    #[error("error configuring host mesh agent {0}: {1}")]
167    HostMeshAgentConfigurationError(hyperactor_reference::ActorId, String),
168
169    #[error(
170        "error creating proc (host rank {host_rank}) on host mesh agent {mesh_agent}, state: {state}"
171    )]
172    ProcCreationError {
173        state: Box<resource::State<ProcState>>,
174        host_rank: usize,
175        mesh_agent: hyperactor_reference::ActorRef<HostAgent>,
176    },
177
178    #[error(
179        "error spawning proc mesh: statuses: {}",
180        RankedValues::invert(statuses)
181    )]
182    ProcSpawnError { statuses: RankedValues<Status> },
183
184    #[error(
185        "error spawning actor mesh: statuses: {}",
186        RankedValues::invert(statuses)
187    )]
188    ActorSpawnError { statuses: RankedValues<Status> },
189
190    #[error(
191        "error stopping actor mesh: statuses: {}",
192        RankedValues::invert(statuses)
193    )]
194    ActorStopError { statuses: RankedValues<Status> },
195
196    #[error("error spawning actor: {0}")]
197    SingletonActorSpawnError(anyhow::Error),
198
199    #[error("error spawning controller actor for mesh {0}: {1}")]
200    ControllerActorSpawnError(Name, anyhow::Error),
201
202    #[error("proc {0} must be direct-addressable")]
203    RankedProc(hyperactor_reference::ProcId),
204
205    #[error("{0}")]
206    Supervision(Box<MeshFailure>),
207
208    #[error("error: {0} does not exist")]
209    NotExist(Name),
210
211    #[error(transparent)]
212    Io(#[from] io::Error),
213
214    #[error(transparent)]
215    Host(#[from] HostError),
216
217    #[error(transparent)]
218    Other(#[from] anyhow::Error),
219}
220
221/// Errors that occur during serialization and deserialization.
222#[derive(Debug, thiserror::Error)]
223pub enum CodecError {
224    #[error(transparent)]
225    BincodeError(#[from] Box<bincode::Error>),
226    #[error(transparent)]
227    JsonError(#[from] Box<serde_json::Error>),
228    #[error(transparent)]
229    Base64Error(#[from] Box<base64::DecodeError>),
230    #[error(transparent)]
231    Utf8Error(#[from] Box<std::str::Utf8Error>),
232}
233
234impl From<bincode::Error> for Error {
235    fn from(e: bincode::Error) -> Self {
236        Error::CodecError(Box::new(e).into())
237    }
238}
239
240impl From<serde_json::Error> for Error {
241    fn from(e: serde_json::Error) -> Self {
242        Error::CodecError(Box::new(e).into())
243    }
244}
245
246impl From<base64::DecodeError> for Error {
247    fn from(e: base64::DecodeError) -> Self {
248        Error::CodecError(Box::new(e).into())
249    }
250}
251
252impl From<std::str::Utf8Error> for Error {
253    fn from(e: std::str::Utf8Error) -> Self {
254        Error::CodecError(Box::new(e).into())
255    }
256}
257
258impl From<crate::alloc::AllocatorError> for Error {
259    fn from(e: crate::alloc::AllocatorError) -> Self {
260        Error::AllocatorError(Box::new(e))
261    }
262}
263
264impl From<hyperactor::channel::ChannelError> for Error {
265    fn from(e: hyperactor::channel::ChannelError) -> Self {
266        Error::ChannelError(Box::new(e))
267    }
268}
269
270impl From<hyperactor::mailbox::MailboxError> for Error {
271    fn from(e: hyperactor::mailbox::MailboxError) -> Self {
272        Error::MailboxError(Box::new(e))
273    }
274}
275
276impl From<view::InvalidCardinality> for Error {
277    fn from(e: view::InvalidCardinality) -> Self {
278        Error::InvalidRankCardinality {
279            expected: e.expected,
280            actual: e.actual,
281        }
282    }
283}
284
285/// The type of result used in `hyperactor_mesh`.
286pub type Result<T> = std::result::Result<T, Error>;
287
288/// Construct a per-actor display name from a mesh-level base name and a
289/// rank's coordinates. Inserts `point.format_as_dict()` before the last
290/// `>` in `base`, or appends it if no `>` is found. Returns `base`
291/// unchanged for scalar (empty) points.
292pub(crate) fn actor_display_name(base: &str, point: &view::Point) -> String {
293    if point.is_empty() {
294        return base.to_string();
295    }
296    let coords = point.format_as_dict();
297    if let Some(pos) = base.rfind('>') {
298        format!("{}{}{}", &base[..pos], coords, &base[pos..])
299    } else {
300        format!("{}{}", base, coords)
301    }
302}
303
304/// Names are used to identify objects in the system. They have a user-provided name,
305/// and a unique UUID.
306///
307/// Names have a concrete syntax--`{name}-{uuid}`--printed by `Display` and parsed by `FromStr`.
308#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Named, EnumAsInner)]
309pub enum Name {
310    /// Normal names for most actors.
311    Suffixed(String, ShortUuid),
312    /// Reserved names for system actors without UUIDs.
313    Reserved(String),
314}
315wirevalue::register_type!(Name);
316
317// The delimiter between the name and the uuid when a Name::Suffixed is stringified.
318// Actor names must be parseable as an actor identifier. We do not allow this delimiter
319// in reserved names so that these names parse unambiguously.
320static NAME_SUFFIX_DELIMITER: &str = "-";
321
322impl Name {
323    /// Create a new `Name` from a user-provided base name.
324    pub fn new(name: impl Into<String>) -> Result<Self> {
325        Ok(Self::new_with_uuid(name, Some(ShortUuid::generate()))?)
326    }
327
328    /// Create a Reserved `Name` with no uuid. Only for use by system actors.
329    pub fn new_reserved(name: impl Into<String>) -> Result<Self> {
330        Ok(Self::new_with_uuid(name, None)?)
331    }
332
333    fn new_with_uuid(
334        name: impl Into<String>,
335        uuid: Option<ShortUuid>,
336    ) -> std::result::Result<Self, NameParseError> {
337        let mut name = name.into();
338        if name.is_empty() {
339            name = "unnamed".to_string();
340        }
341        if !hyperactor_reference::is_valid_ident(&name) {
342            return Err(NameParseError::InvalidName(name));
343        }
344        if let Some(uuid) = uuid {
345            Ok(Self::Suffixed(name, uuid))
346        } else {
347            Ok(Self::Reserved(name))
348        }
349    }
350
351    /// The name portion of this `Name`.
352    pub fn name(&self) -> &str {
353        match self {
354            Self::Suffixed(n, _) => n,
355            Self::Reserved(n) => n,
356        }
357    }
358
359    /// The UUID portion of this `Name`.
360    /// Only Some for Name::Suffixed, if called on Name::Reserved it'll be None.
361    pub fn uuid(&self) -> Option<&ShortUuid> {
362        match self {
363            Self::Suffixed(_, uuid) => Some(uuid),
364            Self::Reserved(_) => None,
365        }
366    }
367}
368
369impl Serialize for Name {
370    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
371    where
372        S: serde::Serializer,
373    {
374        // Consider doing this only when `serializer.is_human_readable()`:
375        serializer.serialize_str(&self.to_string())
376    }
377}
378
379impl<'de> Deserialize<'de> for Name {
380    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
381    where
382        D: serde::Deserializer<'de>,
383    {
384        let s = String::deserialize(deserializer)?;
385        Name::from_str(&s).map_err(serde::de::Error::custom)
386    }
387}
388
389/// Errors that occur when parsing names.
390#[derive(thiserror::Error, Debug)]
391pub enum NameParseError {
392    #[error("invalid name: missing name")]
393    MissingName,
394
395    #[error("invalid name: missing uuid")]
396    MissingUuid,
397
398    /// Strictly speaking Monarch identifier also supports other characters. But
399    /// to avoid confusion for the user, we only state intuitive characters here
400    /// so the error message is more actionable.
401    #[error(
402        "invalid name '{0}': names must contain only alphanumeric characters \
403        and underscores, and must start with a letter or underscore"
404    )]
405    InvalidName(String),
406
407    #[error(transparent)]
408    InvalidUuid(#[from] <ShortUuid as FromStr>::Err),
409
410    #[error("invalid name: missing separator")]
411    MissingSeparator,
412}
413
414impl FromStr for Name {
415    type Err = NameParseError;
416
417    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
418        // The delimiter ('-') is allowable in elements, but not identifiers;
419        // thus splitting on this unambiguously parses suffixed and reserved names.
420        if let Some((name, uuid)) = s.split_once(NAME_SUFFIX_DELIMITER) {
421            if name.is_empty() {
422                return Err(NameParseError::MissingName);
423            }
424            if uuid.is_empty() {
425                return Err(NameParseError::MissingName);
426            }
427
428            Name::new_with_uuid(name.to_string(), Some(uuid.parse()?))
429        } else {
430            if s.is_empty() {
431                return Err(NameParseError::MissingName);
432            }
433            Name::new_with_uuid(s, None)
434        }
435    }
436}
437
438impl std::fmt::Display for Name {
439    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
440        match self {
441            Self::Suffixed(n, uuid) => {
442                write!(f, "{}{}", n, NAME_SUFFIX_DELIMITER)?;
443                uuid.format(f, true /*raw*/)
444            }
445            Self::Reserved(n) => write!(f, "{}", n),
446        }
447    }
448}
449
450#[cfg(test)]
451mod tests {
452
453    #[test]
454    fn basic() {
455        use ndslice::selection::dsl;
456        use ndslice::selection::structurally_equal;
457
458        let actual = sel!(*, 0:4, *);
459        let expected = dsl::all(dsl::range(
460            ndslice::shape::Range(0, Some(4), 1),
461            dsl::all(dsl::true_()),
462        ));
463        assert!(structurally_equal(&actual, &expected));
464    }
465
466    #[cfg(FALSE)]
467    #[test]
468    fn shouldnt_compile() {
469        let _ = sel!(foobar);
470    }
471    // error: sel! parse failed: unexpected token: Ident { sym: foobar, span: #0 bytes(605..611) }
472    //   --> fbcode/monarch/hyperactor_mesh_macros/tests/basic.rs:19:13
473    //    |
474    // 19 |     let _ = sel!(foobar);
475    //    |             ^^^^^^^^^^^^ in this macro invocation
476    //   --> fbcode/monarch/hyperactor_mesh_macros/src/lib.rs:12:1
477    //    |
478    //    = note: in this expansion of `sel!`
479
480    use std::str::FromStr;
481
482    use hyperactor_mesh_macros::sel;
483    use ndslice::assert_round_trip;
484    use ndslice::assert_structurally_eq;
485    use ndslice::selection::Selection;
486
487    macro_rules! assert_round_trip_match {
488        ($left:expr, $right:expr) => {{
489            assert_structurally_eq!($left, $right);
490            assert_round_trip!($left);
491            assert_round_trip!($right);
492        }};
493    }
494
495    #[test]
496    fn token_parser() {
497        use ndslice::selection::dsl::*;
498        use ndslice::shape;
499
500        assert_round_trip_match!(all(true_()), sel!(*));
501        assert_round_trip_match!(range(3, true_()), sel!(3));
502        assert_round_trip_match!(range(1..4, true_()), sel!(1:4));
503        assert_round_trip_match!(all(range(1..4, true_())), sel!(*, 1:4));
504        assert_round_trip_match!(range(shape::Range(0, None, 1), true_()), sel!(:));
505        assert_round_trip_match!(any(true_()), sel!(?));
506        assert_round_trip_match!(any(range(1..4, all(true_()))), sel!(?, 1:4, *));
507        assert_round_trip_match!(union(range(0, true_()), range(1, true_())), sel!(0 | 1));
508        assert_round_trip_match!(
509            intersection(range(0..4, true_()), range(2..6, true_())),
510            sel!(0:4 & 2:6)
511        );
512        assert_round_trip_match!(range(shape::Range(0, None, 1), true_()), sel!(:));
513        assert_round_trip_match!(all(true_()), sel!(*));
514        assert_round_trip_match!(any(true_()), sel!(?));
515        assert_round_trip_match!(all(all(all(true_()))), sel!(*, *, *));
516        assert_round_trip_match!(intersection(all(true_()), all(true_())), sel!(* & *));
517        assert_round_trip_match!(
518            all(all(union(
519                range(0..2, true_()),
520                range(shape::Range(6, None, 1), true_())
521            ))),
522            sel!(*, *, (:2|6:))
523        );
524        assert_round_trip_match!(
525            all(all(range(shape::Range(1, None, 2), true_()))),
526            sel!(*, *, 1::2)
527        );
528        assert_round_trip_match!(
529            range(
530                shape::Range(0, Some(1), 1),
531                any(range(shape::Range(0, Some(4), 1), true_()))
532            ),
533            sel!(0, ?, :4)
534        );
535        assert_round_trip_match!(range(shape::Range(1, Some(4), 2), true_()), sel!(1:4:2));
536        assert_round_trip_match!(range(shape::Range(0, None, 2), true_()), sel!(::2));
537        assert_round_trip_match!(
538            union(range(0..4, true_()), range(4..8, true_())),
539            sel!(0:4 | 4:8)
540        );
541        assert_round_trip_match!(
542            intersection(range(0..4, true_()), range(2..6, true_())),
543            sel!(0:4 & 2:6)
544        );
545        assert_round_trip_match!(
546            all(union(range(1..4, all(true_())), range(5..6, all(true_())))),
547            sel!(*, (1:4 | 5:6), *)
548        );
549        assert_round_trip_match!(
550            range(
551                0,
552                intersection(
553                    range(1..4, range(7, true_())),
554                    range(2..5, range(7, true_()))
555                )
556            ),
557            sel!(0, (1:4 & 2:5), 7)
558        );
559        assert_round_trip_match!(
560            all(all(union(
561                union(range(0..2, true_()), range(4..6, true_())),
562                range(shape::Range(6, None, 1), true_())
563            ))),
564            sel!(*, *, (:2 | 4:6 | 6:))
565        );
566        assert_round_trip_match!(intersection(all(true_()), all(true_())), sel!(* & *));
567        assert_round_trip_match!(union(all(true_()), all(true_())), sel!(* | *));
568        assert_round_trip_match!(
569            intersection(
570                range(0..2, true_()),
571                union(range(1, true_()), range(2, true_()))
572            ),
573            sel!(0:2 & (1 | 2))
574        );
575        assert_round_trip_match!(
576            all(all(intersection(
577                range(1..2, true_()),
578                range(2..3, true_())
579            ))),
580            sel!(*,*,(1:2&2:3))
581        );
582        assert_round_trip_match!(
583            intersection(all(all(all(true_()))), all(all(all(true_())))),
584            sel!((*,*,*) & (*,*,*))
585        );
586        assert_round_trip_match!(
587            intersection(
588                range(0, all(all(true_()))),
589                range(0, union(range(1, all(true_())), range(3, all(true_()))))
590            ),
591            sel!((0, *, *) & (0, (1 | 3), *))
592        );
593        assert_round_trip_match!(
594            intersection(
595                range(0, all(all(true_()))),
596                range(
597                    0,
598                    union(
599                        range(1, range(2..5, true_())),
600                        range(3, range(2..5, true_()))
601                    )
602                )
603            ),
604            sel!((0, *, *) & (0, (1 | 3), 2:5))
605        );
606        assert_round_trip_match!(all(true_()), sel!((*)));
607        assert_round_trip_match!(range(1..4, range(2, true_())), sel!(((1:4), 2)));
608        assert_round_trip_match!(sel!(1:4 & 5:6 | 7:8), sel!((1:4 & 5:6) | 7:8));
609        assert_round_trip_match!(
610            union(
611                intersection(all(all(true_())), all(all(true_()))),
612                all(all(true_()))
613            ),
614            sel!((*,*) & (*,*) | (*,*))
615        );
616        assert_round_trip_match!(all(true_()), sel!(*));
617        assert_round_trip_match!(sel!(((1:4))), sel!(1:4));
618        assert_round_trip_match!(sel!(*, (*)), sel!(*, *));
619        assert_round_trip_match!(
620            intersection(
621                range(0, range(1..4, true_())),
622                range(0, union(range(2, all(true_())), range(3, all(true_()))))
623            ),
624            sel!((0,1:4)&(0,(2|3),*))
625        );
626
627        //assert_round_trip_match!(true_(), sel!(foo)); // sel! macro: parse error: Parsing Error: Error { input: "foo", code: Tag }
628
629        assert_round_trip_match!(
630            sel!(0 & (0, (1|3), *)),
631            intersection(
632                range(0, true_()),
633                range(0, union(range(1, all(true_())), range(3, all(true_()))))
634            )
635        );
636        assert_round_trip_match!(
637            sel!(0 & (0, (3|1), *)),
638            intersection(
639                range(0, true_()),
640                range(0, union(range(3, all(true_())), range(1, all(true_()))))
641            )
642        );
643        assert_round_trip_match!(
644            sel!((*, *, *) & (*, *, (2 | 4))),
645            intersection(
646                all(all(all(true_()))),
647                all(all(union(range(2, true_()), range(4, true_()))))
648            )
649        );
650        assert_round_trip_match!(
651            sel!((*, *, *) & (*, *, (4 | 2))),
652            intersection(
653                all(all(all(true_()))),
654                all(all(union(range(4, true_()), range(2, true_()))))
655            )
656        );
657        assert_round_trip_match!(
658            sel!((*, (1|2)) & (*, (2|1))),
659            intersection(
660                all(union(range(1, true_()), range(2, true_()))),
661                all(union(range(2, true_()), range(1, true_())))
662            )
663        );
664        assert_round_trip_match!(
665            sel!((*, *, *) & *),
666            intersection(all(all(all(true_()))), all(true_()))
667        );
668        assert_round_trip_match!(
669            sel!(* & (*, *, *)),
670            intersection(all(true_()), all(all(all(true_()))))
671        );
672
673        assert_round_trip_match!(
674            sel!( (*, *, *) & ((*, *, *) & (*, *, *)) ),
675            intersection(
676                all(all(all(true_()))),
677                intersection(all(all(all(true_()))), all(all(all(true_()))))
678            )
679        );
680        assert_round_trip_match!(
681            sel!((1, *, *) | (0 & (0, 3, *))),
682            union(
683                range(1, all(all(true_()))),
684                intersection(range(0, true_()), range(0, range(3, all(true_()))))
685            )
686        );
687        assert_round_trip_match!(
688            sel!(((0, *)| (1, *)) & ((1, *) | (0, *))),
689            intersection(
690                union(range(0, all(true_())), range(1, all(true_()))),
691                union(range(1, all(true_())), range(0, all(true_())))
692            )
693        );
694        assert_round_trip_match!(sel!(*, 8:8), all(range(8..8, true_())));
695        assert_round_trip_match!(
696            sel!((*, 1) & (*, 8 : 8)),
697            intersection(all(range(1..2, true_())), all(range(8..8, true_())))
698        );
699        assert_round_trip_match!(
700            sel!((*, 8 : 8) | (*, 1)),
701            union(all(range(8..8, true_())), all(range(1..2, true_())))
702        );
703        assert_round_trip_match!(
704            sel!((*, 1) | (*, 2:8)),
705            union(all(range(1..2, true_())), all(range(2..8, true_())))
706        );
707        assert_round_trip_match!(
708            sel!((*, *, *) & (*, *, 2:8)),
709            intersection(all(all(all(true_()))), all(all(range(2..8, true_()))))
710        );
711    }
712
713    #[test]
714    fn test_name_unique() {
715        use super::Name;
716        assert_ne!(Name::new("foo").unwrap(), Name::new("foo").unwrap());
717        let name = Name::new("foo").unwrap();
718        assert_eq!(name, name);
719    }
720
721    #[test]
722    fn test_name_roundtrip() {
723        use super::Name;
724        use super::ShortUuid;
725        let uuid = "111111111111".parse::<ShortUuid>().unwrap();
726        let name = Name::new_with_uuid("foo", Some(uuid)).unwrap();
727        let str = name.to_string();
728        assert_eq!(str, "foo-111111111111");
729        assert_eq!(name, Name::from_str(&str).unwrap());
730    }
731
732    #[test]
733    fn test_name_roundtrip_with_underscore() {
734        use super::Name;
735        use super::ShortUuid;
736        // A ShortUuid may have an underscore prefix if the first character is a digit.
737        // Make sure this doesn't impact parsing.
738        let uuid = "_1a2b3c4d5e6f".parse::<ShortUuid>().unwrap();
739        let name = Name::new_with_uuid("foo", Some(uuid)).unwrap();
740        let str = name.to_string();
741        // Leading underscore is stripped as not needed.
742        assert_eq!(str, "foo-1a2b3c4d5e6f");
743        assert_eq!(name, Name::from_str(&str).unwrap());
744    }
745
746    #[test]
747    fn test_name_roundtrip_random() {
748        use super::Name;
749        let name = Name::new("foo").unwrap();
750        assert_eq!(name, Name::from_str(&name.to_string()).unwrap());
751    }
752
753    #[test]
754    fn test_name_roundtrip_reserved() {
755        use super::Name;
756        let name = Name::new_reserved("foo").unwrap();
757        let str = name.to_string();
758        assert_eq!(str, "foo");
759        assert_eq!(name, Name::from_str(&str).unwrap());
760    }
761
762    #[test]
763    fn test_name_parse() {
764        use super::Name;
765        // Multiple underscores are allowed in the name, as ShortUuid will choose
766        // the part after the last underscore.
767        let name = Name::from_str("foo_bar_1a2b3c4d5e6f").unwrap();
768        assert_eq!(format!("{}", name), "foo_bar_1a2b3c4d5e6f");
769    }
770
771    #[test]
772    fn test_invalid() {
773        use super::Name;
774        // We assign "unnamed" to empty names.
775        assert!(Name::new("").is_ok());
776        // These are not valid identifiers:
777        assert!(Name::new("foo-").is_err());
778        assert!(Name::new("foo-bar").is_err());
779    }
780}