tests.rs 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. use super::*;
  2. use crate::power::toggle::off_extras;
  3. fn cfg_with_ha() -> Config {
  4. let toml = r#"
  5. ssh_user = "u"
  6. ssh_key_path = "/k"
  7. [[clusters]]
  8. name = "x"
  9. master_ib_ip = "1.1.1.1"
  10. nodes = [
  11. { name = "n1", ssh_ip = "10.0.0.1", ib_ip = "10.0.0.1", rank = 0, entity = "switch.n1" },
  12. { name = "n2", ssh_ip = "10.0.0.2", ib_ip = "10.0.0.2", rank = 1 },
  13. ]
  14. extra_entities = ["switch.network_x"]
  15. [nccl."x"]
  16. ib_hca = "h"
  17. socket_ifname = "s"
  18. ib_gid_index = 1
  19. ib_disable = 0
  20. [home_assistant]
  21. url = "http://ha.local:8123"
  22. token = "abcdef0123456789abcdef0123456789"
  23. shutdown_delay_seconds = 5
  24. "#;
  25. toml::from_str(toml).unwrap()
  26. }
  27. fn cfg_no_ha() -> Config {
  28. let toml = r#"
  29. ssh_user = "u"
  30. ssh_key_path = "/k"
  31. [[clusters]]
  32. name = "x"
  33. master_ib_ip = "1.1.1.1"
  34. nodes = [
  35. { name = "n1", ssh_ip = "10.0.0.1", ib_ip = "10.0.0.1", rank = 0, entity = "switch.n1" },
  36. ]
  37. [nccl."x"]
  38. ib_hca = "h"
  39. socket_ifname = "s"
  40. ib_gid_index = 1
  41. ib_disable = 0
  42. "#;
  43. toml::from_str(toml).unwrap()
  44. }
  45. #[test]
  46. fn select_targets_filters_by_node() {
  47. let cfg = cfg_with_ha();
  48. let c = crate::config::select_cluster_by_name(&cfg, "x").unwrap();
  49. let t = select_targets(&c, &["n1".to_string()], "test").unwrap();
  50. assert_eq!(t.len(), 1);
  51. assert_eq!(t[0].name, "n1");
  52. }
  53. #[test]
  54. fn select_targets_all() {
  55. let cfg = cfg_with_ha();
  56. let c = crate::config::select_cluster_by_name(&cfg, "x").unwrap();
  57. let t = select_targets(&c, &[], "test").unwrap();
  58. assert_eq!(t.len(), 2);
  59. }
  60. #[test]
  61. fn select_targets_multiple_nodes() {
  62. // `stop --node a --node b --off` must power off *both* nodes, not just
  63. // the first. (Regression for I-5.)
  64. let cfg = cfg_with_ha();
  65. let c = crate::config::select_cluster_by_name(&cfg, "x").unwrap();
  66. let t = select_targets(&c, &["n1".to_string(), "n2".to_string()], "test").unwrap();
  67. assert_eq!(t.len(), 2);
  68. let names: Vec<_> = t.iter().map(|n| n.name.as_str()).collect();
  69. assert!(names.contains(&"n1"));
  70. assert!(names.contains(&"n2"));
  71. }
  72. #[test]
  73. fn select_targets_partial_missing_errors() {
  74. let cfg = cfg_with_ha();
  75. let c = crate::config::select_cluster_by_name(&cfg, "x").unwrap();
  76. // One valid + one missing → error listing the missing one.
  77. let r = select_targets(&c, &["n1".to_string(), "nope".to_string()], "test");
  78. assert!(r.is_err());
  79. assert!(r.unwrap_err().to_string().contains("nope"));
  80. }
  81. #[test]
  82. fn select_targets_unknown_node_errors() {
  83. let cfg = cfg_with_ha();
  84. let c = crate::config::select_cluster_by_name(&cfg, "x").unwrap();
  85. let r = select_targets(&c, &["nope".to_string()], "test");
  86. assert!(r.is_err());
  87. }
  88. #[test]
  89. fn power_on_requires_ha_config() {
  90. let cfg = cfg_no_ha();
  91. assert!(cfg.home_assistant.is_none());
  92. }
  93. #[test]
  94. fn cluster_extra_entities_parsed() {
  95. use crate::config::ExtraEntity;
  96. let cfg = cfg_with_ha();
  97. let c = crate::config::select_cluster_by_name(&cfg, "x").unwrap();
  98. assert_eq!(
  99. c.extra_entities,
  100. vec![ExtraEntity::Simple("switch.network_x".into())]
  101. );
  102. }
  103. #[test]
  104. fn off_extras_whole_cluster_keeps_extras_subset_skips_them() {
  105. // A whole-cluster power off (no --node) turns the shared fabric off too.
  106. let cfg = cfg_with_ha();
  107. let c = crate::config::select_cluster_by_name(&cfg, "x").unwrap();
  108. assert_eq!(off_extras(&c, &[]), vec!["switch.network_x".to_string()]);
  109. // A subset power off (--node) must NOT take the shared switch down —
  110. // the nodes that stay up depend on it (NCCL over the fabric).
  111. assert_eq!(off_extras(&c, &["n1".to_string()]), Vec::<String>::new());
  112. assert_eq!(
  113. off_extras(&c, &["n1".to_string(), "n2".to_string()]),
  114. Vec::<String>::new()
  115. );
  116. }
  117. #[test]
  118. fn extra_entity_forms_blocking_and_soft() {
  119. use crate::config::ExtraEntity;
  120. let toml = r#"
  121. ssh_user = "u"
  122. ssh_key_path = "/k"
  123. [[clusters]]
  124. name = "x"
  125. nodes = [{ name = "n1", ssh_ip = "10.0.0.1", rank = 0 }]
  126. extra_entities = ["switch.plug", { entity = "switch.fan", blocks = false }, { entity = "switch.other" }]
  127. "#;
  128. let cfg: crate::config::Config = toml::from_str(toml).unwrap();
  129. let c = crate::config::select_cluster_by_name(&cfg, "x").unwrap();
  130. let e = &c.extra_entities;
  131. assert_eq!(e.len(), 3);
  132. assert!(matches!(&e[0], ExtraEntity::Simple(s) if s == "switch.plug"));
  133. assert!(e[0].blocks());
  134. assert!(
  135. matches!(&e[1], ExtraEntity::Detailed { entity, blocks } if entity == "switch.fan" && !blocks)
  136. );
  137. assert!(!e[1].blocks());
  138. // table form without `blocks` defaults to blocking
  139. assert!(
  140. matches!(&e[2], ExtraEntity::Detailed { entity, blocks } if entity == "switch.other" && *blocks)
  141. );
  142. assert!(e[2].blocks());
  143. assert_eq!(e[1].entity(), "switch.fan");
  144. }
  145. #[test]
  146. fn cluster_without_extra_entities_is_empty() {
  147. let cfg = cfg_no_ha();
  148. let c = crate::config::select_cluster_by_name(&cfg, "x").unwrap();
  149. assert!(c.extra_entities.is_empty());
  150. }
  151. #[test]
  152. fn mikrotik_dispatch_lookup() {
  153. let cfg = cfg_with_ha();
  154. // No [mikrotik] in this config — every entity stays on the HA path.
  155. assert!(super::mikro::device(cfg.mikrotik.as_ref(), "switch.n1").is_none());
  156. let toml = r#"
  157. ssh_user = "u"
  158. ssh_key_path = "/k"
  159. [[clusters]]
  160. name = "x"
  161. nodes = [{ name = "n1", ssh_ip = "10.0.0.1", rank = 0, entity = "router.lab" }]
  162. extra_entities = ["router.lab", "switch.plug"]
  163. [mikrotik]
  164. [[mikrotik.devices]]
  165. name = "router.lab"
  166. address = "192.168.88.1"
  167. username = "admin"
  168. password = "p"
  169. "#;
  170. let cfg: crate::config::Config = toml::from_str(toml).unwrap();
  171. let c = crate::config::select_cluster_by_name(&cfg, "x").unwrap();
  172. let off = super::mikro::collect_off(&cfg, &c.nodes, &super::extra_ids(&c));
  173. assert_eq!(off, vec!["router.lab".to_string()]);
  174. // same device referenced twice (node + extra) → listed once
  175. let dev = super::mikro::device(cfg.mikrotik.as_ref(), "router.lab");
  176. assert!(dev.is_some());
  177. }
  178. #[test]
  179. fn decide_on_cycles_only_when_plug_on_and_node_down() {
  180. use crate::power::OnAction;
  181. use crate::power::decide_on;
  182. assert_eq!(decide_on(Some("on"), true), OnAction::Cycle);
  183. assert_eq!(decide_on(Some("on"), false), OnAction::AlreadyOn);
  184. assert_eq!(decide_on(Some("off"), true), OnAction::TurnOn);
  185. assert_eq!(decide_on(None, true), OnAction::TurnOn);
  186. }