1use std::any::Any;
98use std::collections::HashMap;
99use std::fmt::Display;
100use std::ops::Index;
101use std::ops::IndexMut;
102use std::str::FromStr;
103use std::sync::LazyLock;
104
105use chrono::DateTime;
106use chrono::Utc;
107use erased_serde::Deserializer as ErasedDeserializer;
108use erased_serde::Serialize as ErasedSerialize;
109use serde::Deserialize;
110use serde::Deserializer;
111use serde::Serialize;
112use serde::Serializer;
113use serde::de::DeserializeOwned;
114use serde::de::MapAccess;
115use serde::de::Visitor;
116use serde::ser::SerializeMap;
117
118use crate::data::Named;
119
120#[doc(hidden)]
124pub struct AttrKeyInfo {
125 pub name: &'static str,
127 pub typehash: fn() -> u64,
129 pub deserialize_erased:
131 fn(&mut dyn ErasedDeserializer) -> Result<Box<dyn SerializableValue>, erased_serde::Error>,
132 pub meta: &'static LazyLock<Attrs>,
134 pub display: fn(&dyn SerializableValue) -> String,
136 pub parse: fn(&str) -> Result<Box<dyn SerializableValue>, anyhow::Error>,
138 pub default: Option<&'static dyn SerializableValue>,
140 pub erased: &'static dyn ErasedKey,
143}
144
145inventory::collect!(AttrKeyInfo);
146
147pub struct Key<T: 'static> {
153 name: &'static str,
154 default_value: Option<&'static T>,
155 attrs: &'static LazyLock<Attrs>,
156}
157
158impl<T> Key<T> {
159 pub fn name(&self) -> &'static str {
161 self.name
162 }
163}
164
165impl<T: Named + 'static> Key<T> {
166 pub const fn new(
168 name: &'static str,
169 default_value: Option<&'static T>,
170 attrs: &'static LazyLock<Attrs>,
171 ) -> Self {
172 Self {
173 name,
174 default_value,
175 attrs,
176 }
177 }
178
179 pub fn default(&self) -> Option<&'static T> {
181 self.default_value
182 }
183
184 pub fn has_default(&self) -> bool {
186 self.default_value.is_some()
187 }
188
189 pub fn typehash(&self) -> u64 {
191 T::typehash()
192 }
193
194 pub fn attrs(&self) -> &'static LazyLock<Attrs> {
196 self.attrs
197 }
198}
199
200impl<T: 'static> Clone for Key<T> {
201 fn clone(&self) -> Self {
202 *self
204 }
205}
206
207impl<T: 'static> Copy for Key<T> {}
208
209pub trait ErasedKey: Any + Send + Sync + 'static {
211 fn name(&self) -> &'static str;
213
214 fn typehash(&self) -> u64;
216
217 fn typename(&self) -> &'static str;
219}
220
221impl dyn ErasedKey {
222 pub fn downcast_ref<T: Named + 'static>(&'static self) -> Option<&'static Key<T>> {
224 (self as &dyn Any).downcast_ref::<Key<T>>()
225 }
226}
227
228impl<T: AttrValue> ErasedKey for Key<T> {
229 fn name(&self) -> &'static str {
230 self.name
231 }
232
233 fn typehash(&self) -> u64 {
234 T::typehash()
235 }
236
237 fn typename(&self) -> &'static str {
238 T::typename()
239 }
240}
241
242impl<T: AttrValue> Index<Key<T>> for Attrs {
244 type Output = T;
245
246 fn index(&self, key: Key<T>) -> &Self::Output {
247 self.get(key).unwrap()
248 }
249}
250
251impl<T: AttrValue> IndexMut<Key<T>> for Attrs {
254 fn index_mut(&mut self, key: Key<T>) -> &mut Self::Output {
255 self.get_mut(key).unwrap()
256 }
257}
258
259pub trait AttrValue:
270 Named + Sized + Serialize + DeserializeOwned + Send + Sync + Clone + 'static
271{
272 fn display(&self) -> String;
275
276 fn parse(value: &str) -> Result<Self, anyhow::Error>;
278}
279
280#[macro_export]
294macro_rules! impl_attrvalue {
295 ($($ty:ty),+ $(,)?) => {
296 $(
297 impl $crate::attrs::AttrValue for $ty {
298 fn display(&self) -> String {
299 self.to_string()
300 }
301
302 fn parse(value: &str) -> Result<Self, anyhow::Error> {
303 value.parse().map_err(|e| anyhow::anyhow!("failed to parse {}: {}", stringify!($ty), e))
304 }
305 }
306 )+
307 };
308}
309
310impl_attrvalue!(
314 i8,
315 i16,
316 i32,
317 i64,
318 i128,
319 isize,
320 u8,
321 u16,
322 u32,
323 u64,
324 u128,
325 usize,
326 f32,
327 f64,
328 String,
329 std::net::IpAddr,
330 std::net::Ipv4Addr,
331 std::net::Ipv6Addr,
332 crate::ActorId,
333 ndslice::Shape,
334 ndslice::Point,
335);
336
337impl AttrValue for std::time::Duration {
338 fn display(&self) -> String {
339 humantime::format_duration(*self).to_string()
340 }
341
342 fn parse(value: &str) -> Result<Self, anyhow::Error> {
343 Ok(humantime::parse_duration(value)?)
344 }
345}
346
347impl AttrValue for std::time::SystemTime {
348 fn display(&self) -> String {
349 let datetime: DateTime<Utc> = (*self).into();
350 datetime.to_rfc3339()
351 }
352
353 fn parse(value: &str) -> Result<Self, anyhow::Error> {
354 let datetime = DateTime::parse_from_rfc3339(value)?;
355 Ok(datetime.into())
356 }
357}
358
359impl<T, E> AttrValue for std::ops::Range<T>
360where
361 T: Named
362 + Display
363 + FromStr<Err = E>
364 + Send
365 + Sync
366 + Serialize
367 + DeserializeOwned
368 + Clone
369 + 'static,
370 E: Into<anyhow::Error> + Send + Sync + 'static,
371{
372 fn display(&self) -> String {
373 format!("{}..{}", self.start, self.end)
374 }
375
376 fn parse(value: &str) -> Result<Self, anyhow::Error> {
377 let (start, end) = value.split_once("..").ok_or_else(|| {
378 anyhow::anyhow!("expected range in format `start..end`, got `{}`", value)
379 })?;
380 let start = start.parse().map_err(|e: E| e.into())?;
381 let end = end.parse().map_err(|e: E| e.into())?;
382 Ok(start..end)
383 }
384}
385
386impl AttrValue for bool {
387 fn display(&self) -> String {
388 if *self { 1.to_string() } else { 0.to_string() }
389 }
390
391 fn parse(value: &str) -> Result<Self, anyhow::Error> {
392 let value = value.to_ascii_lowercase();
393 match value.as_str() {
394 "0" | "false" => Ok(false),
395 "1" | "true" => Ok(true),
396 _ => Err(anyhow::anyhow!(
397 "expected `0`, `1`, `true` or `false`, got `{}`",
398 value
399 )),
400 }
401 }
402}
403
404#[doc(hidden)]
406pub trait SerializableValue: Send + Sync {
407 fn as_any(&self) -> &dyn Any;
409 fn as_any_mut(&mut self) -> &mut dyn Any;
411 fn as_erased_serialize(&self) -> &dyn ErasedSerialize;
413 fn cloned(&self) -> Box<dyn SerializableValue>;
415 fn display(&self) -> String;
417}
418
419impl<T: AttrValue> SerializableValue for T {
420 fn as_any(&self) -> &dyn Any {
421 self
422 }
423
424 fn as_any_mut(&mut self) -> &mut dyn Any {
425 self
426 }
427
428 fn as_erased_serialize(&self) -> &dyn ErasedSerialize {
429 self
430 }
431
432 fn cloned(&self) -> Box<dyn SerializableValue> {
433 Box::new(self.clone())
434 }
435
436 fn display(&self) -> String {
437 self.display()
438 }
439}
440
441pub struct Attrs {
459 values: HashMap<&'static str, Box<dyn SerializableValue>>,
460}
461
462impl Attrs {
463 pub fn new() -> Self {
465 Self {
466 values: HashMap::new(),
467 }
468 }
469
470 pub fn set<T: AttrValue>(&mut self, key: Key<T>, value: T) {
472 self.values.insert(key.name, Box::new(value));
473 }
474
475 fn maybe_set_from_default<T: AttrValue>(&mut self, key: Key<T>) {
476 if self.contains_key(key) {
477 return;
478 }
479 let Some(default) = key.default() else { return };
480 self.set(key, default.clone());
481 }
482
483 pub fn get<T: AttrValue>(&self, key: Key<T>) -> Option<&T> {
486 self.values
487 .get(key.name)
488 .and_then(|value| value.as_any().downcast_ref::<T>())
489 .or_else(|| key.default())
490 }
491
492 pub fn get_mut<T: AttrValue>(&mut self, key: Key<T>) -> Option<&mut T> {
495 self.maybe_set_from_default(key);
496 self.values
497 .get_mut(key.name)
498 .and_then(|value| value.as_any_mut().downcast_mut::<T>())
499 }
500
501 pub fn remove<T: AttrValue>(&mut self, key: Key<T>) -> bool {
503 self.values.remove(key.name).is_some()
505 }
506
507 pub fn contains_key<T: AttrValue>(&self, key: Key<T>) -> bool {
509 self.values.contains_key(key.name)
510 }
511
512 pub fn len(&self) -> usize {
514 self.values.len()
515 }
516
517 pub fn is_empty(&self) -> bool {
519 self.values.is_empty()
520 }
521
522 pub fn clear(&mut self) {
524 self.values.clear();
525 }
526
527 pub(crate) fn remove_value<T: 'static>(
530 &mut self,
531 key: Key<T>,
532 ) -> Option<Box<dyn SerializableValue>> {
533 self.values.remove(key.name)
534 }
535
536 pub(crate) fn insert_value<T: 'static>(
538 &mut self,
539 key: Key<T>,
540 value: Box<dyn SerializableValue>,
541 ) {
542 self.values.insert(key.name, value);
543 }
544
545 pub(crate) fn insert_value_by_name_unchecked(
547 &mut self,
548 name: &'static str,
549 value: Box<dyn SerializableValue>,
550 ) {
551 self.values.insert(name, value);
552 }
553
554 pub(crate) fn get_value_by_name(&self, name: &'static str) -> Option<&dyn SerializableValue> {
557 self.values.get(name).map(|b| b.as_ref())
558 }
559
560 pub(crate) fn merge(&mut self, other: Attrs) {
566 self.values.extend(other.values);
567 }
568}
569
570impl Clone for Attrs {
571 fn clone(&self) -> Self {
572 let mut values = HashMap::new();
573 for (key, value) in &self.values {
574 values.insert(*key, value.cloned());
575 }
576 Self { values }
577 }
578}
579
580impl std::fmt::Display for Attrs {
581 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
582 let mut first = true;
583 for (key, value) in &self.values {
584 if first {
585 first = false;
586 } else {
587 write!(f, ",")?;
588 }
589 write!(f, "{}={}", key, value.display())?
590 }
591 Ok(())
592 }
593}
594
595impl std::fmt::Debug for Attrs {
596 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
597 let mut debug_map = std::collections::BTreeMap::new();
599 for (key, value) in &self.values {
600 match serde_json::to_string(value.as_erased_serialize()) {
601 Ok(json) => {
602 debug_map.insert(*key, json);
603 }
604 Err(_) => {
605 debug_map.insert(*key, "<serialization error>".to_string());
606 }
607 }
608 }
609
610 f.debug_struct("Attrs").field("values", &debug_map).finish()
611 }
612}
613
614impl Default for Attrs {
615 fn default() -> Self {
616 Self::new()
617 }
618}
619
620impl Serialize for Attrs {
621 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
622 where
623 S: Serializer,
624 {
625 let mut map = serializer.serialize_map(Some(self.values.len()))?;
626
627 for (key_name, value) in &self.values {
628 map.serialize_entry(key_name, value.as_erased_serialize())?;
629 }
630
631 map.end()
632 }
633}
634
635struct AttrsVisitor;
636
637impl<'de> Visitor<'de> for AttrsVisitor {
638 type Value = Attrs;
639
640 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
641 formatter.write_str("a map of attribute keys to their serialized values")
642 }
643
644 fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
645 where
646 M: MapAccess<'de>,
647 {
648 static KEYS_BY_NAME: std::sync::LazyLock<HashMap<&'static str, &'static AttrKeyInfo>> =
649 std::sync::LazyLock::new(|| {
650 inventory::iter::<AttrKeyInfo>()
651 .map(|info| (info.name, info))
652 .collect()
653 });
654 let keys_by_name = &*KEYS_BY_NAME;
655
656 let exe_name = std::env::current_exe()
657 .ok()
658 .map(|p| p.display().to_string())
659 .unwrap_or_else(|| "<unknown-exe>".to_string());
660
661 let mut attrs = Attrs::new();
662 while let Some(key_name) = access.next_key::<String>()? {
663 let Some(&key) = keys_by_name.get(key_name.as_str()) else {
664 access.next_value::<serde::de::IgnoredAny>().map_err(|_| {
684 serde::de::Error::custom(format!(
685 "unknown attr key '{}' on binary '{}'; \
686 this binary doesn't know this key and cannot skip its value safely under bincode",
687 key_name, exe_name,
688 ))
689 })?;
690 continue;
691 };
692
693 let seed = ValueDeserializeSeed {
695 deserialize_erased: key.deserialize_erased,
696 };
697 match access.next_value_seed(seed) {
698 Ok(value) => {
699 attrs.values.insert(key.name, value);
700 }
701 Err(err) => {
702 return Err(serde::de::Error::custom(format!(
703 "failed to deserialize value for key {}: {}",
704 key_name, err
705 )));
706 }
707 }
708 }
709
710 Ok(attrs)
711 }
712}
713
714struct ValueDeserializeSeed {
716 deserialize_erased:
717 fn(&mut dyn ErasedDeserializer) -> Result<Box<dyn SerializableValue>, erased_serde::Error>,
718}
719
720impl<'de> serde::de::DeserializeSeed<'de> for ValueDeserializeSeed {
721 type Value = Box<dyn SerializableValue>;
722
723 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
724 where
725 D: serde::de::Deserializer<'de>,
726 {
727 let mut erased = <dyn erased_serde::Deserializer>::erase(deserializer);
728 (self.deserialize_erased)(&mut erased).map_err(serde::de::Error::custom)
729 }
730}
731
732impl<'de> Deserialize<'de> for Attrs {
733 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
734 where
735 D: Deserializer<'de>,
736 {
737 deserializer.deserialize_map(AttrsVisitor)
738 }
739}
740
741#[doc(hidden)]
744pub const fn ascii_to_lowercase_const<const N: usize>(input: &str) -> [u8; N] {
745 let bytes = input.as_bytes();
746 let mut result = [0u8; N];
747 let mut i = 0;
748
749 while i < bytes.len() && i < N {
750 let byte = bytes[i];
751 if byte >= b'A' && byte <= b'Z' {
752 result[i] = byte + 32; } else {
754 result[i] = byte;
755 }
756 i += 1;
757 }
758
759 result
760}
761
762#[doc(hidden)]
764#[macro_export]
765macro_rules! const_ascii_lowercase {
766 ($s:expr) => {{
767 const INPUT: &str = $s;
768 const LEN: usize = INPUT.len();
769 const BYTES: [u8; LEN] = $crate::attrs::ascii_to_lowercase_const::<LEN>(INPUT);
770 unsafe { std::str::from_utf8_unchecked(&BYTES) }
772 }};
773}
774
775#[doc(hidden)]
778#[macro_export]
779macro_rules! assert_impl {
780 ($ty:ty, $trait:path) => {
781 const _: fn() = || {
782 fn check<T: $trait>() {}
783 check::<$ty>();
784 };
785 };
786}
787
788#[macro_export]
831macro_rules! declare_attrs {
832 ($(
834 $(#[$attr:meta])*
835 $(@meta($($meta_key:ident = $meta_value:expr),* $(,)?))*
836 $vis:vis attr $name:ident: $type:ty $(= $default:expr)?;
837 )*) => {
838 $(
839 $crate::declare_attrs! {
840 @single
841 $(@meta($($meta_key = $meta_value),*))*
842 $(#[$attr])* ;
843 $vis attr $name: $type $(= $default)?;
844 }
845 )*
846 };
847
848 (@single $(@meta($($meta_key:ident = $meta_value:expr),* $(,)?))* $(#[$attr:meta])* ; $vis:vis attr $name:ident: $type:ty = $default:expr;) => {
850 $crate::assert_impl!($type, $crate::attrs::AttrValue);
851
852 $crate::paste! {
854 static [<$name _DEFAULT>]: $type = $default;
855 static [<$name _META_ATTRS>]: std::sync::LazyLock<$crate::attrs::Attrs> =
856 std::sync::LazyLock::new(|| {
857 #[allow(unused_mut)]
858 let mut attrs = $crate::attrs::Attrs::new();
859 $($(
860 attrs.set($meta_key, $meta_value);
861 )*)*
862 attrs
863 });
864 }
865
866 $(#[$attr])*
867 $vis static $name: $crate::attrs::Key<$type> = {
868 $crate::assert_impl!($type, $crate::attrs::AttrValue);
869
870 const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
871 const LOWER_NAME: &str = $crate::const_ascii_lowercase!(FULL_NAME);
872 $crate::paste! {
873 $crate::attrs::Key::new(
874 LOWER_NAME,
875 Some(&[<$name _DEFAULT>]),
876 $crate::paste! { &[<$name _META_ATTRS>] },
877 )
878 }
879 };
880
881 $crate::submit! {
883 $crate::attrs::AttrKeyInfo {
884 name: {
885 const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
886 $crate::const_ascii_lowercase!(FULL_NAME)
887 },
888 typehash: <$type as $crate::data::Named>::typehash,
889 deserialize_erased: |deserializer| {
890 let value: $type = erased_serde::deserialize(deserializer)?;
891 Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
892 },
893 meta: $crate::paste! { &[<$name _META_ATTRS>] },
894 display: |value: &dyn $crate::attrs::SerializableValue| {
895 let value = value.as_any().downcast_ref::<$type>().unwrap();
896 $crate::attrs::AttrValue::display(value)
897 },
898 parse: |value: &str| {
899 let value: $type = $crate::attrs::AttrValue::parse(value)?;
900 Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
901 },
902 default: Some($crate::paste! { &[<$name _DEFAULT>] }),
903 erased: &$name,
904 }
905 }
906 };
907
908 (@single $(@meta($($meta_key:ident = $meta_value:expr),* $(,)?))* $(#[$attr:meta])* ; $vis:vis attr $name:ident: $type:ty;) => {
910 $crate::assert_impl!($type, $crate::attrs::AttrValue);
911
912 $crate::paste! {
913 static [<$name _META_ATTRS>]: std::sync::LazyLock<$crate::attrs::Attrs> =
914 std::sync::LazyLock::new(|| {
915 #[allow(unused_mut)]
916 let mut attrs = $crate::attrs::Attrs::new();
917 $($(
918 attrs.set($meta_key, $meta_value);
921 )*)*
922 attrs
923 });
924 }
925
926 $(#[$attr])*
927 $vis static $name: $crate::attrs::Key<$type> = {
928 const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
929 const LOWER_NAME: &str = $crate::const_ascii_lowercase!(FULL_NAME);
930 $crate::attrs::Key::new(LOWER_NAME, None, $crate::paste! { &[<$name _META_ATTRS>] })
931 };
932
933
934 $crate::submit! {
936 $crate::attrs::AttrKeyInfo {
937 name: {
938 const FULL_NAME: &str = concat!(std::module_path!(), "::", stringify!($name));
939 $crate::const_ascii_lowercase!(FULL_NAME)
940 },
941 typehash: <$type as $crate::data::Named>::typehash,
942 deserialize_erased: |deserializer| {
943 let value: $type = erased_serde::deserialize(deserializer)?;
944 Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
945 },
946 meta: $crate::paste! { &[<$name _META_ATTRS>] },
947 display: |value: &dyn $crate::attrs::SerializableValue| {
948 let value = value.as_any().downcast_ref::<$type>().unwrap();
949 $crate::attrs::AttrValue::display(value)
950 },
951 parse: |value: &str| {
952 let value: $type = $crate::attrs::AttrValue::parse(value)?;
953 Ok(Box::new(value) as Box<dyn $crate::attrs::SerializableValue>)
954 },
955 default: None,
956 erased: &$name,
957 }
958 }
959 };
960}
961
962pub use declare_attrs;
963
964#[cfg(test)]
965mod tests {
966 use std::time::Duration;
967
968 use super::*;
969
970 declare_attrs! {
971 attr TEST_TIMEOUT: Duration;
972 attr TEST_COUNT: u32;
973 @meta(TEST_COUNT = 42)
974 pub attr TEST_NAME: String;
975 }
976
977 #[test]
978 fn test_basic_operations() {
979 let mut attrs = Attrs::new();
980
981 attrs.set(TEST_TIMEOUT, Duration::from_secs(5));
983 attrs.set(TEST_COUNT, 42u32);
984 attrs.set(TEST_NAME, "test".to_string());
985
986 assert_eq!(attrs.get(TEST_TIMEOUT), Some(&Duration::from_secs(5)));
987 assert_eq!(attrs.get(TEST_COUNT), Some(&42u32));
988 assert_eq!(attrs.get(TEST_NAME), Some(&"test".to_string()));
989
990 assert!(attrs.contains_key(TEST_TIMEOUT));
992 assert!(attrs.contains_key(TEST_COUNT));
993 assert!(attrs.contains_key(TEST_NAME));
994
995 assert_eq!(attrs.len(), 3);
997 assert!(!attrs.is_empty());
998
999 assert_eq!(TEST_NAME.attrs().get(TEST_COUNT).unwrap(), &42u32);
1001 }
1002
1003 #[test]
1004 fn test_get_mut() {
1005 let mut attrs = Attrs::new();
1006 attrs.set(TEST_COUNT, 10u32);
1007
1008 if let Some(count) = attrs.get_mut(TEST_COUNT) {
1009 *count += 5;
1010 }
1011
1012 assert_eq!(attrs.get(TEST_COUNT), Some(&15u32));
1013 }
1014
1015 #[test]
1016 fn test_remove() {
1017 let mut attrs = Attrs::new();
1018 attrs.set(TEST_COUNT, 42u32);
1019
1020 let removed = attrs.remove(TEST_COUNT);
1021 assert!(removed);
1022 assert_eq!(attrs.get(TEST_COUNT), None);
1023 assert!(!attrs.contains_key(TEST_COUNT));
1024 }
1025
1026 #[test]
1027 fn test_clear() {
1028 let mut attrs = Attrs::new();
1029 attrs.set(TEST_TIMEOUT, Duration::from_secs(1));
1030 attrs.set(TEST_COUNT, 42u32);
1031
1032 attrs.clear();
1033 assert!(attrs.is_empty());
1034 assert_eq!(attrs.len(), 0);
1035 }
1036
1037 #[test]
1038 fn test_key_properties() {
1039 assert_eq!(
1040 TEST_TIMEOUT.name(),
1041 "hyperactor::attrs::tests::test_timeout"
1042 );
1043 }
1044
1045 #[test]
1046 fn test_serialization() {
1047 let mut attrs = Attrs::new();
1048 attrs.set(TEST_TIMEOUT, Duration::from_secs(5));
1049 attrs.set(TEST_COUNT, 42u32);
1050 attrs.set(TEST_NAME, "test".to_string());
1051
1052 let serialized = serde_json::to_string(&attrs).expect("Failed to serialize");
1054
1055 assert!(serialized.contains("hyperactor::attrs::tests::test_timeout"));
1057 assert!(serialized.contains("hyperactor::attrs::tests::test_count"));
1058 assert!(serialized.contains("hyperactor::attrs::tests::test_name"));
1059 }
1060
1061 #[test]
1062 fn test_deserialization() {
1063 let mut original_attrs = Attrs::new();
1065 original_attrs.set(TEST_TIMEOUT, Duration::from_secs(5));
1066 original_attrs.set(TEST_COUNT, 42u32);
1067 original_attrs.set(TEST_NAME, "test".to_string());
1068
1069 let serialized = serde_json::to_string(&original_attrs).expect("Failed to serialize");
1071
1072 let deserialized_attrs: Attrs =
1074 serde_json::from_str(&serialized).expect("Failed to deserialize");
1075
1076 assert_eq!(
1078 deserialized_attrs.get(TEST_TIMEOUT),
1079 Some(&Duration::from_secs(5))
1080 );
1081 assert_eq!(deserialized_attrs.get(TEST_COUNT), Some(&42u32));
1082 assert_eq!(deserialized_attrs.get(TEST_NAME), Some(&"test".to_string()));
1083 }
1084
1085 #[test]
1086 fn test_roundtrip_serialization() {
1087 let mut original = Attrs::new();
1089 original.set(TEST_TIMEOUT, Duration::from_secs(10));
1090 original.set(TEST_COUNT, 5u32);
1091 original.set(TEST_NAME, "test-service".to_string());
1092
1093 let serialized = serde_json::to_string(&original).unwrap();
1095
1096 let deserialized: Attrs = serde_json::from_str(&serialized).unwrap();
1098
1099 assert_eq!(
1101 deserialized.get(TEST_TIMEOUT),
1102 Some(&Duration::from_secs(10))
1103 );
1104 assert_eq!(deserialized.get(TEST_COUNT), Some(&5u32));
1105 assert_eq!(
1106 deserialized.get(TEST_NAME),
1107 Some(&"test-service".to_string())
1108 );
1109 }
1110
1111 #[test]
1112 fn test_empty_attrs_serialization() {
1113 let attrs = Attrs::new();
1114 let serialized = serde_json::to_string(&attrs).unwrap();
1115
1116 assert_eq!(serialized, "{}");
1118
1119 let deserialized: Attrs = serde_json::from_str(&serialized).unwrap();
1120
1121 assert!(deserialized.is_empty());
1122 }
1123
1124 #[test]
1125 fn test_format_independence() {
1126 let mut attrs = Attrs::new();
1128 attrs.set(TEST_COUNT, 42u32);
1129 attrs.set(TEST_NAME, "test".to_string());
1130
1131 let json_output = serde_json::to_string(&attrs).unwrap();
1133 let yaml_output = serde_yaml::to_string(&attrs).unwrap();
1134
1135 assert!(json_output.contains(":"));
1137 assert!(json_output.contains("\""));
1138
1139 assert!(json_output.contains("42"));
1141 assert!(!json_output.contains("\"42\""));
1142
1143 assert!(yaml_output.contains(":"));
1145 assert!(yaml_output.contains("42"));
1146
1147 assert!(!yaml_output.contains("\"42\""));
1149
1150 assert_ne!(json_output, yaml_output);
1152
1153 let from_json: Attrs = serde_json::from_str(&json_output).unwrap();
1155 let from_yaml: Attrs = serde_yaml::from_str(&yaml_output).unwrap();
1156
1157 assert_eq!(from_json.get(TEST_COUNT), Some(&42u32));
1158 assert_eq!(from_yaml.get(TEST_COUNT), Some(&42u32));
1159 assert_eq!(from_json.get(TEST_NAME), Some(&"test".to_string()));
1160 assert_eq!(from_yaml.get(TEST_NAME), Some(&"test".to_string()));
1161 }
1162
1163 #[test]
1164 fn test_clone() {
1165 let mut original = Attrs::new();
1167 original.set(TEST_COUNT, 42u32);
1168 original.set(TEST_NAME, "test".to_string());
1169 original.set(TEST_TIMEOUT, std::time::Duration::from_secs(10));
1170
1171 let cloned = original.clone();
1173
1174 assert_eq!(cloned.get(TEST_COUNT), Some(&42u32));
1176 assert_eq!(cloned.get(TEST_NAME), Some(&"test".to_string()));
1177 assert_eq!(
1178 cloned.get(TEST_TIMEOUT),
1179 Some(&std::time::Duration::from_secs(10))
1180 );
1181
1182 original.set(TEST_COUNT, 100u32);
1184 assert_eq!(original.get(TEST_COUNT), Some(&100u32));
1185 assert_eq!(cloned.get(TEST_COUNT), Some(&42u32)); let mut cloned_mut = cloned.clone();
1189 cloned_mut.set(TEST_NAME, "modified".to_string());
1190 assert_eq!(cloned_mut.get(TEST_NAME), Some(&"modified".to_string()));
1191 assert_eq!(original.get(TEST_NAME), Some(&"test".to_string())); }
1193
1194 #[test]
1195 fn test_debug_with_json() {
1196 let mut attrs = Attrs::new();
1197 attrs.set(TEST_COUNT, 42u32);
1198 attrs.set(TEST_NAME, "test".to_string());
1199
1200 let debug_output = format!("{:?}", attrs);
1202
1203 assert!(debug_output.contains("Attrs"));
1205
1206 assert!(debug_output.contains("42"));
1208
1209 assert!(debug_output.contains("hyperactor::attrs::tests::test_count"));
1211 assert!(debug_output.contains("hyperactor::attrs::tests::test_name"));
1212
1213 assert!(debug_output.contains("test"));
1216 }
1217
1218 declare_attrs! {
1219 attr TIMEOUT_WITH_DEFAULT: Duration = Duration::from_secs(10);
1221
1222 pub(crate) attr CRATE_LOCAL_ATTR: String;
1224 }
1225
1226 #[test]
1227 fn test_defaults() {
1228 assert!(TIMEOUT_WITH_DEFAULT.has_default());
1229 assert!(!CRATE_LOCAL_ATTR.has_default());
1230
1231 assert_eq!(
1232 Attrs::new().get(TIMEOUT_WITH_DEFAULT),
1233 Some(&Duration::from_secs(10))
1234 );
1235 }
1236
1237 #[test]
1238 fn test_indexing() {
1239 let mut attrs = Attrs::new();
1240
1241 assert_eq!(attrs[TIMEOUT_WITH_DEFAULT], Duration::from_secs(10));
1242 attrs[TIMEOUT_WITH_DEFAULT] = Duration::from_secs(100);
1243 assert_eq!(attrs[TIMEOUT_WITH_DEFAULT], Duration::from_secs(100));
1244
1245 attrs.set(CRATE_LOCAL_ATTR, "test".to_string());
1246 assert_eq!(attrs[CRATE_LOCAL_ATTR], "test".to_string());
1247 }
1248
1249 #[test]
1250 fn attrs_deserialize_unknown_key_is_error() {
1251 let bad_key: &'static str = "monarch_hyperactor::pytokio::unawaited_pytokio_traceback";
1276
1277 let mut attrs = Attrs::new();
1281 attrs.insert_value_by_name_unchecked(bad_key, Box::new(0u32));
1282
1283 let wire_bytes = bincode::serialize(&attrs).unwrap();
1286
1287 let err = bincode::deserialize::<Attrs>(&wire_bytes)
1290 .expect_err("should error on unknown attr key");
1291
1292 let exe_str = std::env::current_exe()
1293 .ok()
1294 .map(|p| p.display().to_string())
1295 .unwrap_or_else(|| "<unknown-exe>".to_string());
1296 let msg = format!("{err}");
1297
1298 assert!(msg.contains("unknown attr key"), "got: {msg}");
1299 assert!(msg.contains(bad_key), "got: {msg}");
1300 assert!(msg.contains(&exe_str), "got: {msg}");
1301 }
1302}